Repository: gcanti/tcomb
Pull request #202 (2016-05-08 08:27:32)

Add support for extending refinements, ref #179


Patch:
diff --git a/lib/createExtend.js b/lib/createExtend.js
deleted file mode 100644
index 55715e0..0000000
--- a/lib/createExtend.js
+++ /dev/null
@@ -1,32 +0,0 @@
-var assert = require('./assert');
-var isObject = require('./isObject');
-var isArray = require('./isArray');
-var mixin = require('./mixin');
-var isStruct = require('./isStruct');
-var isInterface = require('./isInterface');
-
-function createExtend(ctor) {
-  return function extend(mixins, name) {
-    if (process.env.NODE_ENV !== 'production') {
-      assert(isArray(mixins) && mixins.every(function (x) {
-        return isObject(x) || isStruct(x) || isInterface(x);
-      }), function () { return 'Invalid argument mixins supplied to extend(mixins, name), expected an array of objects or structs'; });
-    }
-    var props = {};
-    var prototype = {};
-    mixins.forEach(function (x) {
-      if (isObject(x)) {
-        mixin(props, x);
-      }
-      else {
-        mixin(props, x.meta.props);
-        mixin(prototype, x.prototype);
-      }
-    });
-    var ret = ctor(props, name);
-    mixin(ret.prototype, prototype);
-    return ret;
-  };
-}
-
-module.exports = createExtend;
\ No newline at end of file
diff --git a/lib/extend.js b/lib/extend.js
new file mode 100644
index 0000000..3648cd3
--- /dev/null
+++ b/lib/extend.js
@@ -0,0 +1,71 @@
+var assert = require('./assert');
+var isFunction = require('./isFunction');
+var isArray = require('./isArray');
+var mixin = require('./mixin');
+var isStruct = require('./isStruct');
+var isInterface = require('./isInterface');
+var isObject = require('./isObject');
+var isType = require('./isType');
+var refinement = require('./refinement');
+
+function isRefinement(type) {
+  return isType(type) && type.meta.kind === 'subtype';
+}
+
+function getPredicates(type) {
+  return isRefinement(type) ?
+    [type.meta.predicate].concat(getPredicates(type.meta.type)) :
+    [];
+}
+
+function getUnrefinedType(type) {
+  return isRefinement(type) ?
+    getUnrefinedType(type.meta.type) :
+    type;
+}
+
+function decompose(type) {
+  return {
+    predicates: getPredicates(type),
+    unrefinedType: getUnrefinedType(type)
+  };
+}
+
+function compose(predicates, unrefinedType) {
+  return predicates.reduce(function (type, predicate) {
+    return refinement(type, predicate);
+  }, unrefinedType);
+}
+
+function getProps(type) {
+  return isObject(type) ? type : type.meta.props;
+}
+
+function pushAll(arr, elements) {
+  Array.prototype.push.apply(arr, elements);
+}
+
+function extend(combinator, mixins, name) {
+  if (process.env.NODE_ENV !== 'production') {
+    assert(isFunction(combinator), function () { return 'Invalid argument combinator supplied to extend(combinator, mixins, name), expected a function'; });
+    assert(isArray(mixins), function () { return 'Invalid argument mixins supplied to extend(combinator, mixins, name), expected an array'; });
+  }
+  var props = {};
+  var prototype = {};
+  var predicates = [];
+  mixins.forEach(function (x, i) {
+    var decomposition = decompose(x);
+    var unrefinedType = decomposition.unrefinedType;
+    if (process.env.NODE_ENV !== 'production') {
+      assert(isObject(unrefinedType) || isStruct(unrefinedType) || isInterface(unrefinedType), function () { return 'Invalid argument mixins[' + i + '] supplied to extend(combinator, mixins, name), expected an object, struct, interface or a refinement (of struct or interface)'; });
+    }
+    pushAll(predicates, decomposition.predicates);
+    mixin(props, getProps(unrefinedType));
+    mixin(prototype, unrefinedType.prototype);
+  });
+  var result = compose(predicates, combinator(props, name));
+  mixin(result.prototype, prototype);
+  return result;
+}
+
+module.exports = extend;
\ No newline at end of file
diff --git a/lib/interface.js b/lib/interface.js
index d47791d..a3d35b5 100644
--- a/lib/interface.js
+++ b/lib/interface.js
@@ -11,9 +11,11 @@ var dict = require('./dict');
 var getDefaultInterfaceName = require('./getDefaultInterfaceName');
 var isIdentity = require('./isIdentity');
 var is = require('./is');
-var createExtend = require('./createExtend');
+var extend = require('./extend');
 
-var extend = createExtend(inter);
+function extendInterface(mixins, name) {
+  return extend(inter, mixins, name);
+}
 
 function getOptions(options) {
   if (!isObject(options)) {
@@ -110,7 +112,7 @@ function inter(props, options) {
   };
 
   Interface.extend = function (xs, name) {
-    return extend([Interface].concat(xs), name);
+    return extendInterface([Interface].concat(xs), name);
   };
 
   return Interface;
@@ -119,5 +121,5 @@ function inter(props, options) {
 inter.strict = false;
 inter.getOptions = getOptions;
 inter.getDefaultName = getDefaultInterfaceName;
-inter.extend = extend;
+inter.extend = extendInterface;
 module.exports = inter;
diff --git a/lib/mixin.js b/lib/mixin.js
index 7e630bb..cc5041a 100644
--- a/lib/mixin.js
+++ b/lib/mixin.js
@@ -8,7 +8,7 @@ module.exports = function mixin(target, source, overwrite) {
     if (source.hasOwnProperty(k)) {
       if (overwrite !== true) {
         if (process.env.NODE_ENV !== 'production') {
-          assert(!target.hasOwnProperty(k), function () { return 'Invalid call to mixin(target, source, [overwrite]): cannot overwrite property "' + k + '" of target object'; });
+          assert(!target.hasOwnProperty(k) || target[k] === source[k], function () { return 'Invalid call to mixin(target, source, [overwrite]): cannot overwrite property "' + k + '" of target object'; });
         }
       }
       target[k] = source[k];
diff --git a/lib/struct.js b/lib/struct.js
index d81ad9c..4511a84 100644
--- a/lib/struct.js
+++ b/lib/struct.js
@@ -9,13 +9,15 @@ var create = require('./create');
 var getTypeName = require('./getTypeName');
 var dict = require('./dict');
 var getDefaultInterfaceName = require('./getDefaultInterfaceName');
-var createExtend = require('./createExtend');
+var extend = require('./extend');
 
 function getDefaultName(props) {
   return 'Struct' + getDefaultInterfaceName(props);
 }
 
-var extend = createExtend(struct);
+function extendStruct(mixins, name) {
+  return extend(struct, mixins, name);
+}
 
 function getOptions(options) {
   if (!isObject(options)) {
@@ -97,7 +99,7 @@ function struct(props, options) {
   };
 
   Struct.extend = function (xs, name) {
-    return extend([Struct].concat(xs), name);
+    return extendStruct([Struct].concat(xs), name);
   };
 
   return Struct;
@@ -106,5 +108,5 @@ function struct(props, options) {
 struct.strict = false;
 struct.getOptions = getOptions;
 struct.getDefaultName = getDefaultName;
-struct.extend = extend;
+struct.extend = extendStruct;
 module.exports = struct;


Test patch:
diff --git a/test/extend.js b/test/extend.js
new file mode 100644
index 0000000..20cd485
--- /dev/null
+++ b/test/extend.js
@@ -0,0 +1,229 @@
+/* globals describe, it */
+var assert = require('assert');
+var t = require('../index');
+var throwsWithMessage = require('./util').throwsWithMessage;
+
+var Point = t.struct({
+  x: t.Number,
+  y: t.Number
+}, 'Point');
+
+var PointInterface = t.inter({
+  x: t.Number,
+  y: t.Number
+});
+
+describe('extend(combinator, mixins, [name])', function () {
+
+  it('should throw if used with wrong arguments', function () {
+    throwsWithMessage(function () {
+      t.struct.extend();
+    }, '[tcomb] Invalid argument mixins supplied to extend(combinator, mixins, name), expected an array');
+    throwsWithMessage(function () {
+      t.struct.extend([1]);
+    }, '[tcomb] Invalid argument mixins[0] supplied to extend(combinator, mixins, name), expected an object, struct, interface or a refinement (of struct or interface)');
+  });
+
+  describe('struct.extend(mixins, [name])', function () {
+
+    it('should handle an array of mixins', function () {
+      var Point3D = t.struct.extend([Point, { z: t.Number }], 'Point3D');
+      assert.deepEqual(Point3D.meta.name, 'Point3D', 'name');
+      assert.deepEqual(Point3D.meta.props.x, t.Number, 'x');
+      assert.deepEqual(Point3D.meta.props.y, t.Number, 'y');
+      assert.deepEqual(Point3D.meta.props.z, t.Number, 'z');
+    });
+
+  });
+
+  describe('<Struct>.extend(mixins, [name])', function () {
+
+    it('should extend an existing struct', function () {
+      var Point3D = Point.extend({ z: t.Number }, 'Point3D');
+      assert.strictEqual(Point3D.meta.name, 'Point3D', 'name');
+      assert.strictEqual(Point3D.meta.props.x, t.Number, 'x');
+      assert.strictEqual(Point3D.meta.props.y, t.Number, 'y');
+      assert.strictEqual(Point3D.meta.props.z, t.Number, 'z');
+    });
+
+    it('should handle an array of mixins', function () {
+      var Type = t.struct({ a: t.String }, 'Type');
+      var Mixin = [{ b: t.Number, c: t.Boolean }];
+      var NewType = Type.extend(Mixin, 'NewType');
+      assert.strictEqual(t.getTypeName(NewType), 'NewType');
+      assert.strictEqual(NewType.meta.props.a, t.String);
+      assert.strictEqual(NewType.meta.props.b, t.Number);
+      assert.strictEqual(NewType.meta.props.c, t.Boolean);
+    });
+
+    it('should handle an array of objects / structs / interfaces as mixins', function () {
+      var A = t.struct({ a: t.String }, 'A');
+      var B = t.struct({ b: t.String }, 'B');
+      var C = t.struct({ c: t.String }, 'C');
+      var MixinD = { d: t.String };
+      var I = t.inter({ i: t.String }, 'I');
+      I.prototype.imethod = function() {};
+      var E = A.extend([B, C, MixinD, I]);
+      assert.deepEqual(E.meta.props, {
+        a: t.String,
+        b: t.String,
+        c: t.String,
+        d: t.String,
+        i: t.String
+      });
+      assert.equal(E.prototype.imethod === I.prototype.imethod, true);
+    });
+
+    it('should support prototypal inheritance', function () {
+      var Rectangle = t.struct({
+        w: t.Number,
+        h: t.Number
+      }, 'Rectangle');
+      Rectangle.prototype.area = function () {
+        return this.w * this.h;
+      };
+      var Cube = Rectangle.extend({
+        l: t.Number
+      });
+      Cube.prototype.volume = function () {
+        return this.area() * this.l;
+      };
+
+      assert(typeof Rectangle.prototype.area === 'function');
+      assert(typeof Cube.prototype.area === 'function');
+      assert(undefined === Rectangle.prototype.volume);
+      assert(typeof Cube.prototype.volume === 'function');
+      assert(Cube.prototype.constructor === Cube);
+
+      var c = new Cube({w: 2, h: 2, l: 2});
+      assert.deepEqual(c.volume(), 8);
+    });
+
+    it('should support multiple prototypal inheritance', function () {
+      var A = t.struct({ a: t.Str }, 'A');
+      A.prototype.amethod = function () {};
+      var B = t.struct({ b: t.Str }, 'B');
+      B.prototype.bmethod = function () {};
+      var C = t.struct({ c: t.Str }, 'C');
+      C.prototype.cmethod = function () {};
+      var Z = C.extend([A, B], 'Z');
+      var z = new Z({ a: 'a', b: 'b', c: 'c' });
+      assert.strictEqual(Z.meta.name, 'Z');
+      assert.strictEqual(t.Function.is(z.cmethod), true);
+      assert.strictEqual(t.Function.is(z.bmethod), true);
+      assert.strictEqual(t.Function.is(z.amethod), true);
+    });
+
+  });
+
+  describe('interface.extend', function () {
+    it('should handle an array of mixins', function () {
+      var Point3DInterface = t.inter.extend([PointInterface, { z: t.Number }], 'Point3DInterface');
+      assert.strictEqual(Point3DInterface.meta.name, 'Point3DInterface', 'name');
+      assert.strictEqual(Point3DInterface.meta.props.x, t.Number, 'x');
+      assert.strictEqual(Point3DInterface.meta.props.y, t.Number, 'y');
+      assert.strictEqual(Point3DInterface.meta.props.z, t.Number, 'z');
+    });
+  });
+
+  describe('<Interface>.extend(xs, [name])', function () {
+
+    it('should extend an existing interface', function () {
+      var Point3DInterface = PointInterface.extend({ z: t.Number }, 'Point3DInterface');
+      assert.strictEqual(Point3DInterface.meta.name, 'Point3DInterface', 'name');
+      assert.strictEqual(Point3DInterface.meta.props.x, t.Number, 'x');
+      assert.strictEqual(Point3DInterface.meta.props.y, t.Number, 'y');
+      assert.strictEqual(Point3DInterface.meta.props.z, t.Number, 'z');
+    });
+
+    it('should handle an array as argument', function () {
+      var Mixin = [{ b: t.Number, c: t.Boolean }];
+      var NewInterface = PointInterface.extend(Mixin, 'NewInterface');
+      assert.strictEqual(t.getTypeName(NewInterface), 'NewInterface');
+      assert.strictEqual(NewInterface.meta.props.x, t.Number);
+      assert.strictEqual(NewInterface.meta.props.y, t.Number);
+      assert.strictEqual(NewInterface.meta.props.b, t.Number);
+      assert.strictEqual(NewInterface.meta.props.c, t.Boolean);
+    });
+
+  });
+
+  describe('refinements', function () {
+
+    it('should extend refinements', function () {
+      var PersonContact = t.struct({
+        cell: t.maybe(t.String),
+        work: t.maybe(t.String)
+      });
+
+      function atLeastOne (x) {
+        return Boolean(x.cell || x.work);
+      }
+
+      var PersonContactStrict = t.refinement(PersonContact, atLeastOne);
+
+      var mixin = {
+        role: t.maybe(t.String),
+        department: t.maybe(t.String)
+      };
+
+      function atLeastOne$(x) {
+        return Boolean(x.role || x.department);
+      }
+
+      var Role = t.struct(mixin);
+      Role.prototype.getRole = function () {
+        return this.role + ' ' + this.department;
+      };
+      var RoleStrict = t.refinement(Role, atLeastOne$);
+
+      var CompanyContact = t.struct.extend([PersonContactStrict, mixin]);
+      assert.strictEqual(CompanyContact.meta.kind, 'subtype');
+      assert.strictEqual(CompanyContact.meta.predicate, atLeastOne);
+      assert.strictEqual(CompanyContact.meta.type.meta.kind, 'struct');
+      assert.deepEqual(Object.keys(CompanyContact.meta.type.meta.props), [
+        'cell',
+        'work',
+        'role',
+        'department'
+      ]);
+
+      CompanyContact = t.struct.extend([PersonContactStrict, t.inter(mixin)]);
+      assert.strictEqual(CompanyContact.meta.kind, 'subtype');
+      assert.strictEqual(CompanyContact.meta.predicate, atLeastOne);
+      assert.strictEqual(CompanyContact.meta.type.meta.kind, 'struct');
+      assert.deepEqual(Object.keys(CompanyContact.meta.type.meta.props), [
+        'cell',
+        'work',
+        'role',
+        'department'
+      ]);
+
+      CompanyContact = t.inter.extend([PersonContactStrict, mixin]);
+      assert.strictEqual(CompanyContact.meta.kind, 'subtype');
+      assert.strictEqual(CompanyContact.meta.predicate, atLeastOne);
+      assert.strictEqual(CompanyContact.meta.type.meta.kind, 'interface');
+      assert.deepEqual(Object.keys(CompanyContact.meta.type.meta.props), [
+        'cell',
+        'work',
+        'role',
+        'department'
+      ]);
+
+      var RolePersonContact = PersonContact.extend(RoleStrict);
+      assert.strictEqual(RolePersonContact.meta.kind, 'subtype');
+      assert.strictEqual(RolePersonContact.meta.predicate, atLeastOne$);
+      assert.strictEqual(RolePersonContact.meta.type.meta.kind, 'struct');
+      assert.deepEqual(Object.keys(RolePersonContact.meta.type.meta.props), [
+        'cell',
+        'work',
+        'role',
+        'department'
+      ]);
+      assert.strictEqual(RolePersonContact.prototype.getRole, Role.prototype.getRole);
+
+    });
+
+  });
+
+});
diff --git a/test/interface.js b/test/interface.js
index 2a7583d..3325cf7 100644
--- a/test/interface.js
+++ b/test/interface.js
@@ -132,16 +132,6 @@ describe('t.interface(props, [name])', function () {
 
   });
 
-  describe('interface.extend', function () {
-    it('should handle an array of mixins', function () {
-      var Point3DInterface = t.inter.extend([PointInterface, {z: t.Number}], 'Point3DInterface');
-      assert.deepEqual(Point3DInterface.meta.name, 'Point3DInterface', 'name');
-      assert.deepEqual(Point3DInterface.meta.props.x, t.Number, 'x');
-      assert.deepEqual(Point3DInterface.meta.props.y, t.Number, 'y');
-      assert.deepEqual(Point3DInterface.meta.props.z, t.Number, 'z');
-    });
-  });
-
   describe('#is(x)', function () {
 
     it('should return true when x is an instance of the interface', function () {
@@ -185,27 +175,4 @@ describe('t.interface(props, [name])', function () {
 
   });
 
-  describe('#extend(xs, [name])', function () {
-
-    it('should extend an existing interface', function () {
-      var Point3DInterface = PointInterface.extend({z: t.Number}, 'Point3DInterface');
-      assert.deepEqual(Point3DInterface.meta.name, 'Point3DInterface', 'name');
-      assert.deepEqual(Point3DInterface.meta.props.x, t.Number, 'x');
-      assert.deepEqual(Point3DInterface.meta.props.y, t.Number, 'y');
-      assert.deepEqual(Point3DInterface.meta.props.z, t.Number, 'z');
-    });
-
-    it('should handle an array as argument', function () {
-      var Mixin = [{b: t.Number, c: t.Boolean}];
-      var NewInterface = PointInterface.extend(Mixin, 'NewInterface');
-      assert.deepEqual(t.getTypeName(NewInterface), 'NewInterface');
-      assert.deepEqual(NewInterface.meta.props.x, t.Number);
-      assert.deepEqual(NewInterface.meta.props.y, t.Number);
-      assert.deepEqual(NewInterface.meta.props.b, t.Number);
-      assert.deepEqual(NewInterface.meta.props.c, t.Boolean);
-    });
-
-  });
-
-
 });
diff --git a/test/struct.js b/test/struct.js
index 4327832..a0a6e29 100644
--- a/test/struct.js
+++ b/test/struct.js
@@ -34,20 +34,6 @@ describe('t.struct(props, [name])', function () {
 
   });
 
-  describe('struct.extend', function () {
-    it('should handle an array of mixins', function () {
-      var Point = t.struct({
-        x: t.Number,
-        y: t.Number
-      }, 'Point');
-      var Point3D = t.struct.extend([Point, {z: t.Number}], 'Point3D');
-      assert.deepEqual(Point3D.meta.name, 'Point3D', 'name');
-      assert.deepEqual(Point3D.meta.props.x, t.Number, 'x');
-      assert.deepEqual(Point3D.meta.props.y, t.Number, 'y');
-      assert.deepEqual(Point3D.meta.props.z, t.Number, 'z');
-    });
-  });
-
   describe('struct.getOptions', function () {
 
     it('should handle options', function () {
@@ -167,88 +153,4 @@ describe('t.struct(props, [name])', function () {
 
   });
 
-  describe('#extend(xs, [name])', function () {
-
-    it('should extend an existing struct', function () {
-      var Point = t.struct({
-        x: t.Number,
-        y: t.Number
-      }, 'Point');
-      var Point3D = Point.extend({z: t.Number}, 'Point3D');
-      assert.deepEqual(Point3D.meta.name, 'Point3D', 'name');
-      assert.deepEqual(Point3D.meta.props.x, t.Number, 'x');
-      assert.deepEqual(Point3D.meta.props.y, t.Number, 'y');
-      assert.deepEqual(Point3D.meta.props.z, t.Number, 'z');
-    });
-
-    it('should handle an array as argument', function () {
-      var Type = t.struct({a: t.String}, 'Type');
-      var Mixin = [{b: t.Number, c: t.Boolean}];
-      var NewType = Type.extend(Mixin, 'NewType');
-      assert.deepEqual(t.getTypeName(NewType), 'NewType');
-      assert.deepEqual(NewType.meta.props.a, t.String);
-      assert.deepEqual(NewType.meta.props.b, t.Number);
-      assert.deepEqual(NewType.meta.props.c, t.Boolean);
-    });
-
-    it('should handle an array of objects or structs or interfaces as argument', function () {
-      var A = t.struct({a: t.String}, 'A');
-      var B = t.struct({b: t.String}, 'B');
-      var C = t.struct({c: t.String}, 'C');
-      var MixinD = {d: t.String};
-      var I = t.inter({i: t.String}, 'I');
-      I.prototype.imethod = function() {};
-      var E = A.extend([B, C, MixinD, I]);
-      assert.deepEqual(E.meta.props, {
-        a: t.String,
-        b: t.String,
-        c: t.String,
-        d: t.String,
-        i: t.String
-      });
-      assert.equal(E.prototype.imethod === I.prototype.imethod, true);
-    });
-
-    it('should support prototypal inheritance', function () {
-      var Rectangle = t.struct({
-        w: t.Number,
-        h: t.Number
-      }, 'Rectangle');
-      Rectangle.prototype.area = function () {
-        return this.w * this.h;
-      };
-      var Cube = Rectangle.extend({
-        l: t.Number
-      });
-      Cube.prototype.volume = function () {
-        return this.area() * this.l;
-      };
-
-      assert(typeof Rectangle.prototype.area === 'function');
-      assert(typeof Cube.prototype.area === 'function');
-      assert(undefined === Rectangle.prototype.volume);
-      assert(typeof Cube.prototype.volume === 'function');
-      assert(Cube.prototype.constructor === Cube);
-
-      var c = new Cube({w: 2, h: 2, l: 2});
-      assert.deepEqual(c.volume(), 8);
-    });
-
-    it('should support multiple prototypal inheritance', function () {
-      var A = t.struct({ a: t.Str }, 'A');
-      A.prototype.amethod = function () {};
-      var B = t.struct({ b: t.Str }, 'B');
-      B.prototype.bmethod = function () {};
-      var C = t.struct({ c: t.Str }, 'C');
-      C.prototype.cmethod = function () {};
-      var Z = C.extend([A, B], 'Z');
-      var z = new Z({ a: 'a', b: 'b', c: 'c' });
-      assert.strictEqual(Z.meta.name, 'Z');
-      assert.strictEqual(t.Function.is(z.cmethod), true);
-      assert.strictEqual(t.Function.is(z.bmethod), true);
-      assert.strictEqual(t.Function.is(z.amethod), true);
-    });
-
-  });
-
 });