Repository: gcanti/tcomb (js)

Issue:
Error message by using fromJSON
### Version
3.2.2

### Expected behaviour
If I apply type on the object, that has incorrect structure, tcomb returns the error description with the reference to the not valid Type and Field, for example:

```js
var t = require("tcomb");
var fromJSON = require("tcomb/lib/fromJSON");

var TestType = t.struct({
  id: t.Number,
}, "TestType");

var obj = {
  id: "string",
};

TestType(obj);
```

Result
```
TypeError: [tcomb] Invalid value "string" supplied to TestType/id: Number
```

I expect the format of the error message will be the same when I use  `fromJSON` function.

### Actual behaviour

But when I use `fromJSON` function, the error message doesn't have information about Type and Field.

```js
fromJSON(obj, TestType);
```

Result
```
TypeError: [tcomb] Invalid value "string" supplied to Number
```

Do I miss something, or is this a bug?
Thank you!







Pull request description:
fromJSON: track error path, fix #235

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 105dcd2..9a781bc 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -14,6 +14,8 @@
 
 # 3.2.9
 
+- **New Feature**
+  - fromJSON: track error path, fix #235 (@gcanti)
 - **Internal**
   - change shallow copy in order to improve perfs (@gcanti)
 
diff --git a/lib/fromJSON.js b/lib/fromJSON.js
index 6ce5526..1ed83b1 100644
--- a/lib/fromJSON.js
+++ b/lib/fromJSON.js
@@ -7,15 +7,16 @@ var isArray = require('./isArray');
 var isType = require('./isType');
 var create = require('./create');
 
-function fromJSON(value, type) {
+function fromJSON(value, type, path) {
   if (process.env.NODE_ENV !== 'production') {
     assert(isFunction(type), function () {
       return 'Invalid argument type ' + assert.stringify(type) + ' supplied to fromJSON(value, type) (expected a type)';
     });
+    path = path || [getTypeName(type)];
   }
 
   if (isFunction(type.fromJSON)) {
-    return create(type, type.fromJSON(value));
+    return create(type, type.fromJSON(value), path);
   }
 
   if (!isType(type)) {
@@ -29,10 +30,10 @@ function fromJSON(value, type) {
   switch (kind) {
 
     case 'maybe' :
-      return isNil(value) ? null : fromJSON(value, type.meta.type);
+      return isNil(value) ? null : fromJSON(value, type.meta.type, path);
 
     case 'subtype' : // the kind of a refinement is 'subtype' (for legacy reasons)
-      ret = fromJSON(value, type.meta.type);
+      ret = fromJSON(value, type.meta.type, path);
       if (process.env.NODE_ENV !== 'production') {
         assert(type.meta.predicate(ret), function () {
           return 'Invalid argument value ' + assert.stringify(value) + ' supplied to fromJSON(value, type) (expected a valid ' + getTypeName(type) + ')';
@@ -50,7 +51,7 @@ function fromJSON(value, type) {
       ret = {};
       for (k in props) {
         if (props.hasOwnProperty(k)) {
-          ret[k] = fromJSON(value[k], props[k]);
+          ret[k] = fromJSON(value[k], props[k], ( process.env.NODE_ENV !== 'production' ? path.concat(k + ': ' + getTypeName(props[k])) : null ));
         }
       }
       return new type(ret);
@@ -65,7 +66,7 @@ function fromJSON(value, type) {
       ret = {};
       for (k in interProps) {
         if (interProps.hasOwnProperty(k)) {
-          ret[k] = fromJSON(value[k], interProps[k]);
+          ret[k] = fromJSON(value[k], interProps[k], ( process.env.NODE_ENV !== 'production' ? path.concat(k + ': ' + getTypeName(interProps[k])) : null ));
         }
       }
       return ret;
@@ -77,8 +78,9 @@ function fromJSON(value, type) {
         });
       }
       var elementType = type.meta.type;
-      return value.map(function (element) {
-        return fromJSON(element, elementType);
+      var elementTypeName = getTypeName(elementType);
+      return value.map(function (element, i) {
+        return fromJSON(element, elementType, ( process.env.NODE_ENV !== 'production' ? path.concat(i + ': ' + elementTypeName) : null ));
       });
 
     case 'union' :
@@ -88,7 +90,7 @@ function fromJSON(value, type) {
           return 'Invalid argument value ' + assert.stringify(value) + ' supplied to fromJSON(value, type) (no constructor returned by dispatch of union ' + getTypeName(type) + ')';
         });
       }
-      return fromJSON(value, actualType);
+      return fromJSON(value, actualType, path);
 
     case 'tuple' :
       if (process.env.NODE_ENV !== 'production') {
@@ -103,7 +105,7 @@ function fromJSON(value, type) {
         });
       }
       return value.map(function (element, i) {
-        return fromJSON(element, types[i]);
+        return fromJSON(element, types[i], ( process.env.NODE_ENV !== 'production' ? path.concat(i + ': ' + getTypeName(types[i])) : null ));
       });
 
     case 'dict' :
@@ -114,16 +116,18 @@ function fromJSON(value, type) {
       }
       var domain = type.meta.domain;
       var codomain = type.meta.codomain;
+      var domainName = getTypeName(domain);
+      var codomainName = getTypeName(codomain);
       ret = {};
       for (k in value) {
         if (value.hasOwnProperty(k)) {
-          ret[domain(k)] = fromJSON(value[k], codomain);
+          ret[domain(k, ( process.env.NODE_ENV !== 'production' ? path.concat(domainName) : null ))] = fromJSON(value[k], codomain, ( process.env.NODE_ENV !== 'production' ? path.concat(k + ': ' + codomainName) : null ));
         }
       }
       return ret;
 
     default : // enums, irreducible, intersection
-      return type(value);
+      return type(value, path);
   }
 }
 
diff --git a/lib/getTypeName.js b/lib/getTypeName.js
index 432ad90..ec68302 100644
--- a/lib/getTypeName.js
+++ b/lib/getTypeName.js
@@ -1,9 +1,9 @@
 var isType = require('./isType');
 var getFunctionName = require('./getFunctionName');
 
-module.exports = function getTypeName(constructor) {
-  if (isType(constructor)) {
-    return constructor.displayName;
+module.exports = function getTypeName(ctor) {
+  if (isType(ctor)) {
+    return ctor.displayName;
   }
-  return getFunctionName(constructor);
+  return getFunctionName(ctor);
 };
\ No newline at end of file


Test patch:
diff --git a/test/fromJSON.js b/test/fromJSON.js
index 589e0ee..0332636 100644
--- a/test/fromJSON.js
+++ b/test/fromJSON.js
@@ -56,6 +56,9 @@ describe('fromJSON', function () {
     var MyType = t.maybe(MyDate);
     assert.strictEqual(fromJSON(null, MyType), null);
     assert.deepEqual(fromJSON(jsonify(date), MyType), date);
+    util.throwsWithMessage(function () {
+      fromJSON(1, t.maybe(t.String));
+    }, '[tcomb] Invalid value 1 supplied to ?String');
   });
 
   it('should handle refinement', function () {
@@ -66,6 +69,9 @@ describe('fromJSON', function () {
     util.throwsWithMessage(function () {
       fromJSON(jsonify(new Date(123375600000)), MyType);
     }, '[tcomb] Invalid argument value "1973-11-28T23:00:00.000Z" supplied to fromJSON(value, type) (expected a valid MyType)');
+    util.throwsWithMessage(function () {
+      fromJSON(1, t.refinement(t.String, function (s) { return s.length > 2; }));
+    }, '[tcomb] Invalid value 1 supplied to {String | <function1>}');
   });
 
   it('should handle struct', function () {
@@ -86,6 +92,10 @@ describe('fromJSON', function () {
     var actual = fromJSON(json, MyType);
     assert.ok(actual instanceof MyType);
     assert.deepEqual(actual, source);
+
+    util.throwsWithMessage(function () {
+      fromJSON({}, MyType);
+    }, '[tcomb] Invalid value undefined supplied to MyType/name: String');
   });
 
   it('should handle interface', function () {
@@ -104,6 +114,10 @@ describe('fromJSON', function () {
     };
     var json = jsonify(source);
     assert.deepEqual(fromJSON(json, MyType), source);
+
+    util.throwsWithMessage(function () {
+      fromJSON({}, MyType);
+    }, '[tcomb] Invalid value undefined supplied to MyType/name: String');
   });
 
   it('should handle list', function () {
@@ -116,6 +130,10 @@ describe('fromJSON', function () {
     var source = [new Date(2016, 10, 30), new Date(2073, 10, 30)];
     var json = jsonify(source);
     assert.deepEqual(fromJSON(json, MyType), source);
+
+    util.throwsWithMessage(function () {
+      fromJSON([1], t.list(t.String));
+    }, '[tcomb] Invalid value 1 supplied to Array<String>/0: String');
   });
 
   it('should handle union', function () {
@@ -148,6 +166,10 @@ describe('fromJSON', function () {
     var source = ['s', new Date(2016, 10, 30)];
     var json = jsonify(source);
     assert.deepEqual(fromJSON(json, MyType), source);
+
+    util.throwsWithMessage(function () {
+      fromJSON([1, 1], MyType);
+    }, '[tcomb] Invalid value 1 supplied to MyType/0: String');
   });
 
   it('should handle dict', function () {
@@ -160,6 +182,13 @@ describe('fromJSON', function () {
     var source = {a: new Date(2016, 10, 30)};
     var json = jsonify(source);
     assert.deepEqual(fromJSON(json, MyType), source);
+
+    util.throwsWithMessage(function () {
+      fromJSON({a: 'a'}, t.dict(t.refinement(t.String, function (s) { s.length > 2; }, 'Gt2'), t.String));
+    }, '[tcomb] Invalid value "a" supplied to {[key: Gt2]: String}/Gt2');
+    util.throwsWithMessage(function () {
+      fromJSON({a: 1}, t.dict(t.String, t.String));
+    }, '[tcomb] Invalid value 1 supplied to {[key: String]: String}/a: String');
   });
 
 });