Repository: gcanti/tcomb (js)

Issue:
Implementation of a general deserialize function
**Problem**: write a function which, given a JSON / JavaScript blob and a `tcomb` type, returns an instance of that type, performing conversions automatically if required (ex: from a date in ISO-8601 to a `Date` object)

**Proposed solution**. A function `deserialize` with the following signature:

```js
(value: any, type: TcombType) => type
```

and a series of `type.fromJSON(value: any) => type` helpers

**Reference** [Runtime type introspection, playing with structs](https://github.com/gcanti/tcomb/blob/master/docs/GUIDE.md#runtime-type-introspection-playing-with-structs) in the GUIDE.md file


Pull request description:
fix #169

Patch:
diff --git a/docs/API.md b/docs/API.md
index 71d996e..9b2ea59 100644
--- a/docs/API.md
+++ b/docs/API.md
@@ -848,3 +848,46 @@ Returns `true` if `x` is an instance of `type`.
 ```js
 (x: any, type: TcombType) => boolean
 ```
+
+# Other modules
+
+## The `lib/fromJSON` module
+
+Generic deserialization function.
+
+**Signature**
+
+```js
+(value: any, type: TcombType) => type
+```
+
+**Example**
+
+```js
+import fromJSON from 'tcomb/lib/fromJSON'
+
+const Person = t.struct({
+  name: t.String,
+  birthDate: t.Date
+});
+
+// configure your types
+t.Date.fromJSON = function (s) {
+  t.assert(t.String.is(s));
+  return new Date(s);
+};
+
+const source = {
+  name: 'Giulio',
+  birthDate: new Date(1973, 10, 30)
+};
+
+const json = JSON.parse(JSON.stringify(source));
+
+Person(json); // => throws '[tcomb] Invalid value "1973-11-29T23:00:00.000Z" supplied to Person/birthDate: Date'
+
+const person = fromJSON(json, Person);
+
+assert.ok(person instanceof Person); // => true
+assert.deepEqual(person, source); // => ok
+```
\ No newline at end of file
diff --git a/docs/GUIDE.md b/docs/GUIDE.md
index eae091f..9bbffc5 100644
--- a/docs/GUIDE.md
+++ b/docs/GUIDE.md
@@ -611,7 +611,7 @@ Problem. `'1973-11-29T23:00:00.000Z'` is a string but `Anagraphics` wants a `Dat
 
 Solution. Use runtime type introspection to define a general reviver.
 
-**Disclaimer**. This is just an example, it doesn't mean to be complete.
+**Disclaimer**. This is just an example, it doesn't mean to be complete (for a complete implementation see the `lib/fromJSON` module).
 
 ```js
 import _ from 'lodash';
diff --git a/lib/fromJSON.js b/lib/fromJSON.js
new file mode 100644
index 0000000..fd82465
--- /dev/null
+++ b/lib/fromJSON.js
@@ -0,0 +1,109 @@
+var assert = require('./assert');
+var isFunction = require('./isFunction');
+var isNil = require('./isNil');
+var getTypeName = require('./getTypeName');
+var isObject = require('./isObject');
+var isArray = require('./isArray');
+
+function fromJSON(value, type) {
+  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)';
+    });
+  }
+
+  if (isFunction(type.fromJSON)) {
+    return type.fromJSON(value);
+  }
+
+  var kind = type.meta.kind;
+  var k;
+  var ret;
+
+  switch (kind) {
+
+    case 'maybe' :
+      return isNil(value) ? null : fromJSON(value, type.meta.type);
+
+    case 'subtype' : // the kind of a refinement is 'subtype' (for legacy reasons)
+      ret = fromJSON(value, type.meta.type);
+      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) + ')';
+        });
+      }
+      return ret;
+
+    case 'struct' :
+      if (process.env.NODE_ENV !== 'production') {
+        assert(isObject(value), function () {
+          return 'Invalid argument value ' + assert.stringify(value) + ' supplied to fromJSON(value, type) (expected an object for type ' + getTypeName(type) + ')';
+        });
+      }
+      var props = type.meta.props;
+      ret = {};
+      for (k in props) {
+        if (props.hasOwnProperty(k)) {
+          ret[k] = fromJSON(value[k], props[k]);
+        }
+      }
+      return new type(ret);
+
+    case 'list' :
+      if (process.env.NODE_ENV !== 'production') {
+        assert(isArray(value), function () {
+          return 'Invalid argument value ' + assert.stringify(value) + ' supplied to fromJSON(value, type) (expected an array for type ' + getTypeName(type) + ')';
+        });
+      }
+      var elementType = type.meta.type;
+      return value.map(function (element) {
+        return fromJSON(element, elementType);
+      });
+
+    case 'union' :
+      var actualType = type.dispatch(value);
+      if (process.env.NODE_ENV !== 'production') {
+        assert(isFunction(actualType), function () {
+          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);
+
+    case 'tuple' :
+      if (process.env.NODE_ENV !== 'production') {
+        assert(isArray(value), function () {
+          return 'Invalid argument value ' + assert.stringify(value) + ' supplied to fromJSON(value, type) (expected an array for type ' + getTypeName(type) + ')';
+        });
+      }
+      var types = type.meta.types;
+      if (process.env.NODE_ENV !== 'production') {
+        assert(isArray(value) && value.length === types.length, function () {
+          return 'Invalid value ' + assert.stringify(value) + ' supplied to fromJSON(value, type) (expected an array of length ' + types.length + ' for type ' + getTypeName(type) + ')';
+        });
+      }
+      return value.map(function (element, i) {
+        return fromJSON(element, types[i]);
+      });
+
+    case 'dict' :
+      if (process.env.NODE_ENV !== 'production') {
+        assert(isObject(value), function () {
+          return 'Invalid argument value ' + assert.stringify(value) + ' supplied to fromJSON(value, type) (expected an object for type ' + getTypeName(type) + ')';
+        });
+      }
+      var domain = type.meta.domain;
+      var codomain = type.meta.codomain;
+      ret = {};
+      for (k in value) {
+        if (value.hasOwnProperty(k)) {
+          ret[domain(k)] = fromJSON(value[k], codomain);
+        }
+      }
+      return ret;
+
+    default : // enums, irreducible, intersection
+      return type(value);
+  }
+}
+
+module.exports = fromJSON;


Test patch:
diff --git a/test/fromJSON.js b/test/fromJSON.js
new file mode 100644
index 0000000..56f895e
--- /dev/null
+++ b/test/fromJSON.js
@@ -0,0 +1,127 @@
+/* globals describe, it */
+var assert = require('assert');
+var t = require('../index');
+var fromJSON = require('../lib/fromJSON');
+var util = require('./util');
+
+describe('fromJSON', function () {
+
+  // don't pollute the MyDate type
+  var MyDate = t.refinement(t.Date, function () { return true; }, 'MyDate');
+  var date = new Date(1973, 10, 30);
+  function jsonify(x) {
+    return JSON.parse(JSON.stringify(x));
+  }
+
+  MyDate.fromJSON = function (s) {
+    t.assert(t.String.is(s));
+    return new Date(s);
+  };
+
+  it('should throw with a bad type argument', function () {
+    util.throwsWithMessage(function () {
+      fromJSON();
+    }, '[tcomb] Invalid argument type undefined supplied to fromJSON(value, type) (expected a type)');
+  });
+
+  it('should handle a static fromJSON function attached to the type', function () {
+    var MyType = t.refinement(t.String, function () { return true; });
+    MyType.fromJSON = function (s) {
+      return s.length;
+    };
+    assert.equal(fromJSON('aaa', MyType), 3);
+  });
+
+  it('should handle maybe', function () {
+    var MyType = t.maybe(MyDate);
+    assert.strictEqual(fromJSON(null, MyType), null);
+    assert.deepEqual(fromJSON(jsonify(date), MyType), date);
+  });
+
+  it('should handle refinement', function () {
+    var MyType = t.refinement(MyDate, function (d) {
+      return d.getTime() >= date.getTime();
+    }, 'MyType');
+    assert.equal(fromJSON(jsonify(date), MyType).getTime(), date.getTime());
+    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)');
+  });
+
+  it('should handle struct', function () {
+    var MyType = t.struct({
+      name: t.String,
+      birthDate: MyDate
+    }, 'MyType');
+
+    util.throwsWithMessage(function () {
+      fromJSON(null, MyType);
+    }, '[tcomb] Invalid argument value null supplied to fromJSON(value, type) (expected an object for type MyType)');
+
+    var source = {
+      name: 'Giulio',
+      birthDate: date
+    };
+    var json = jsonify(source);
+    var actual = fromJSON(json, MyType);
+    assert.ok(actual instanceof MyType);
+    assert.deepEqual(actual, source);
+  });
+
+  it('should handle list', function () {
+    var MyType = t.list(MyDate, 'MyType');
+
+    util.throwsWithMessage(function () {
+      fromJSON(null, MyType);
+    }, '[tcomb] Invalid argument value null supplied to fromJSON(value, type) (expected an array for type MyType)');
+
+    var source = [new Date(2016, 10, 30), new Date(2073, 10, 30)];
+    var json = jsonify(source);
+    assert.deepEqual(fromJSON(json, MyType), source);
+  });
+
+  it('should handle union', function () {
+    var MyType = t.union([t.Number, MyDate], 'MyType');
+
+    MyType.dispatch = function (x) {
+      if (t.Number.is(x)) { return t.Number; }
+      if (MyDate.is(x) || t.String.is(x)) { return MyDate; }
+    };
+
+    util.throwsWithMessage(function () {
+      fromJSON(null, MyType);
+    }, '[tcomb] Invalid argument value null supplied to fromJSON(value, type) (no constructor returned by dispatch of union MyType)');
+
+    var source = new Date(2016, 10, 30);
+    var json = jsonify(source);
+    assert.deepEqual(fromJSON(json, MyType), source);
+    source = 1;
+    json = jsonify(source);
+    assert.deepEqual(fromJSON(json, MyType), source);
+  });
+
+  it('should handle tuple', function () {
+    var MyType = t.tuple([t.String, MyDate], 'MyType');
+
+    util.throwsWithMessage(function () {
+      fromJSON(null, MyType);
+    }, '[tcomb] Invalid argument value null supplied to fromJSON(value, type) (expected an array for type MyType)');
+
+    var source = ['s', new Date(2016, 10, 30)];
+    var json = jsonify(source);
+    assert.deepEqual(fromJSON(json, MyType), source);
+  });
+
+  it('should handle dict', function () {
+    var MyType = t.dict(t.String, MyDate, 'MyType');
+
+    util.throwsWithMessage(function () {
+      fromJSON(null, MyType);
+    }, '[tcomb] Invalid argument value null supplied to fromJSON(value, type) (expected an object for type MyType)');
+
+    var source = {a: new Date(2016, 10, 30)};
+    var json = jsonify(source);
+    assert.deepEqual(fromJSON(json, MyType), source);
+  });
+
+});