Repository: gcanti/tcomb
Pull request #204 (2016-05-12 15:03:32)

add support for strictness (structs and interfaces). ref #203


Patch:
diff --git a/lib/interface.js b/lib/interface.js
index 127e8be..d47791d 100644
--- a/lib/interface.js
+++ b/lib/interface.js
@@ -2,7 +2,9 @@ var assert = require('./assert');
 var isTypeName = require('./isTypeName');
 var String = require('./String');
 var Function = require('./Function');
+var isBoolean = require('./isBoolean');
 var isObject = require('./isObject');
+var isNil = require('./isNil');
 var create = require('./create');
 var getTypeName = require('./getTypeName');
 var dict = require('./dict');
@@ -13,11 +15,26 @@ var createExtend = require('./createExtend');
 
 var extend = createExtend(inter);
 
-function inter(props, name) {
+function getOptions(options) {
+  if (!isObject(options)) {
+    options = isNil(options) ? {} : { name: options };
+  }
+  if (!options.hasOwnProperty('strict')) {
+    options.strict = inter.strict;
+  }
+  return options;
+}
+
+function inter(props, options) {
+
+  options = getOptions(options);
+  var name = options.name;
+  var strict = options.strict;
 
   if (process.env.NODE_ENV !== 'production') {
-    assert(dict(String, Function).is(props), function () { return 'Invalid argument props ' + assert.stringify(props) + ' supplied to interface(props, [name]) combinator (expected a dictionary String -> Type)'; });
-    assert(isTypeName(name), function () { return 'Invalid argument name ' + assert.stringify(name) + ' supplied to interface(props, [name]) combinator (expected a string)'; });
+    assert(dict(String, Function).is(props), function () { return 'Invalid argument props ' + assert.stringify(props) + ' supplied to interface(props, [options]) combinator (expected a dictionary String -> Type)'; });
+    assert(isTypeName(name), function () { return 'Invalid argument name ' + assert.stringify(name) + ' supplied to interface(props, [options]) combinator (expected a string)'; });
+    assert(isBoolean(strict), function () { return 'Invalid argument strict ' + assert.stringify(strict) + ' supplied to struct(props, [options]) combinator (expected a boolean)'; });
   }
 
   var displayName = name || getDefaultInterfaceName(props);
@@ -34,6 +51,14 @@ function inter(props, name) {
     if (process.env.NODE_ENV !== 'production') {
       path = path || [displayName];
       assert(isObject(value), function () { return 'Invalid value ' + assert.stringify(value) + ' supplied to ' + path.join('/') + ' (expected an object)'; });
+      // strictness
+      if (strict) {
+        for (k in value) {
+          if (value.hasOwnProperty(k)) {
+            assert(props.hasOwnProperty(k), function () { return 'Invalid additional prop "' + k + '" supplied to ' + path.join('/'); });
+          }
+        }
+      }
     }
 
     var idempotent = true;
@@ -62,7 +87,8 @@ function inter(props, name) {
     kind: 'interface',
     props: props,
     name: name,
-    identity: identity
+    identity: identity,
+    strict: strict
   };
 
   Interface.displayName = displayName;
@@ -80,7 +106,7 @@ function inter(props, name) {
   };
 
   Interface.update = function (instance, patch) {
-    return new Interface(assert.update(instance, patch));
+    return Interface(assert.update(instance, patch));
   };
 
   Interface.extend = function (xs, name) {
@@ -90,6 +116,8 @@ function inter(props, name) {
   return Interface;
 }
 
+inter.strict = false;
+inter.getOptions = getOptions;
 inter.getDefaultName = getDefaultInterfaceName;
 inter.extend = extend;
 module.exports = inter;
diff --git a/lib/struct.js b/lib/struct.js
index be70323..d81ad9c 100644
--- a/lib/struct.js
+++ b/lib/struct.js
@@ -2,7 +2,9 @@ var assert = require('./assert');
 var isTypeName = require('./isTypeName');
 var String = require('./String');
 var Function = require('./Function');
+var isBoolean = require('./isBoolean');
 var isObject = require('./isObject');
+var isNil = require('./isNil');
 var create = require('./create');
 var getTypeName = require('./getTypeName');
 var dict = require('./dict');
@@ -15,11 +17,26 @@ function getDefaultName(props) {
 
 var extend = createExtend(struct);
 
-function struct(props, name) {
+function getOptions(options) {
+  if (!isObject(options)) {
+    options = isNil(options) ? {} : { name: options };
+  }
+  if (!options.hasOwnProperty('strict')) {
+    options.strict = struct.strict;
+  }
+  return options;
+}
+
+function struct(props, options) {
+
+  options = getOptions(options);
+  var name = options.name;
+  var strict = options.strict;
 
   if (process.env.NODE_ENV !== 'production') {
-    assert(dict(String, Function).is(props), function () { return 'Invalid argument props ' + assert.stringify(props) + ' supplied to struct(props, [name]) combinator (expected a dictionary String -> Type)'; });
-    assert(isTypeName(name), function () { return 'Invalid argument name ' + assert.stringify(name) + ' supplied to struct(props, [name]) combinator (expected a string)'; });
+    assert(dict(String, Function).is(props), function () { return 'Invalid argument props ' + assert.stringify(props) + ' supplied to struct(props, [options]) combinator (expected a dictionary String -> Type)'; });
+    assert(isTypeName(name), function () { return 'Invalid argument name ' + assert.stringify(name) + ' supplied to struct(props, [options]) combinator (expected a string)'; });
+    assert(isBoolean(strict), function () { return 'Invalid argument strict ' + assert.stringify(strict) + ' supplied to struct(props, [options]) combinator (expected a boolean)'; });
   }
 
   var displayName = name || getDefaultName(props);
@@ -33,6 +50,14 @@ function struct(props, name) {
     if (process.env.NODE_ENV !== 'production') {
       path = path || [displayName];
       assert(isObject(value), function () { return 'Invalid value ' + assert.stringify(value) + ' supplied to ' + path.join('/') + ' (expected an object)'; });
+      // strictness
+      if (strict) {
+        for (k in value) {
+          if (value.hasOwnProperty(k)) {
+            assert(props.hasOwnProperty(k), function () { return 'Invalid additional prop "' + k + '" supplied to ' + path.join('/'); });
+          }
+        }
+      }
     }
 
     if (!(this instanceof Struct)) { // `new` is optional
@@ -57,7 +82,8 @@ function struct(props, name) {
     kind: 'struct',
     props: props,
     name: name,
-    identity: false
+    identity: false,
+    strict: strict
   };
 
   Struct.displayName = displayName;
@@ -77,6 +103,8 @@ function struct(props, name) {
   return Struct;
 }
 
+struct.strict = false;
+struct.getOptions = getOptions;
 struct.getDefaultName = getDefaultName;
 struct.extend = extend;
 module.exports = struct;


Test patch:
diff --git a/test/interface.js b/test/interface.js
index a125112..2a7583d 100644
--- a/test/interface.js
+++ b/test/interface.js
@@ -20,15 +20,42 @@ describe('t.interface(props, [name])', function () {
   describe('combinator', function () {
 
     it('should throw if used with wrong arguments', function () {
+
       throwsWithMessage(function () {
         t.inter();
-      }, '[tcomb] Invalid argument props undefined supplied to interface(props, [name]) combinator (expected a dictionary String -> Type)');
+      }, '[tcomb] Invalid argument props undefined supplied to interface(props, [options]) combinator (expected a dictionary String -> Type)');
+
       throwsWithMessage(function () {
         t.inter({a: null});
-      }, '[tcomb] Invalid argument props {\n  "a": null\n} supplied to interface(props, [name]) combinator (expected a dictionary String -> Type)');
+      }, '[tcomb] Invalid argument props {\n  "a": null\n} supplied to interface(props, [options]) combinator (expected a dictionary String -> Type)');
+
       throwsWithMessage(function () {
         t.inter({}, 1);
-      }, '[tcomb] Invalid argument name 1 supplied to interface(props, [name]) combinator (expected a string)');
+      }, '[tcomb] Invalid argument name 1 supplied to interface(props, [options]) combinator (expected a string)');
+
+      throwsWithMessage(function () {
+        t.inter({}, {strict: 1});
+      }, '[tcomb] Invalid argument strict 1 supplied to struct(props, [options]) combinator (expected a boolean)');
+
+    });
+
+  });
+
+  describe('inter.getOptions', function () {
+
+    it('should handle options', function () {
+      assert.deepEqual(t.inter.getOptions(), { strict: false });
+      assert.deepEqual(t.inter.getOptions({}), { strict: false });
+      assert.deepEqual(t.inter.getOptions('Person'), { strict: false, name: 'Person' });
+      assert.deepEqual(t.inter.getOptions({ strict: false }), { strict: false });
+      assert.deepEqual(t.inter.getOptions({ strict: true }), { strict: true });
+      t.inter.strict = true;
+      assert.deepEqual(t.inter.getOptions(), { strict: true });
+      assert.deepEqual(t.inter.getOptions({}), { strict: true });
+      assert.deepEqual(t.inter.getOptions('Person'), { strict: true, name: 'Person' });
+      assert.deepEqual(t.inter.getOptions({ strict: false }), { strict: false });
+      assert.deepEqual(t.inter.getOptions({ strict: true }), { strict: true });
+      t.inter.strict = false;
     });
 
   });
@@ -62,6 +89,47 @@ describe('t.interface(props, [name])', function () {
       assert.equal(Point.is(hi.point), true);
     });
 
+    it('should handle strict option', function () {
+      var Person = t.inter({
+        name: t.String,
+        surname: t.maybe(t.String)
+      }, { name: 'Person', strict: true });
+
+      assert.strictEqual(Person.meta.name, 'Person');
+      assert.strictEqual(Person.meta.strict, true);
+
+      throwsWithMessage(function () {
+        Person({ name: 'Giulio', age: 42 });
+      }, '[tcomb] Invalid additional prop "age" supplied to Person');
+
+      throwsWithMessage(function () {
+        // simulating a typo on a maybe prop
+        Person({ name: 'Giulio', sur: 'Canti' });
+      }, '[tcomb] Invalid additional prop "sur" supplied to Person');
+
+      function Input(name) {
+        this.name = name;
+      }
+      Input.prototype.method = function () {};
+      assert.doesNotThrow(function () {
+        Person(new Input('Giulio'));
+      });
+    });
+
+    it('should handle global strict option', function () {
+      t.inter.strict = true;
+      var Person = t.inter({
+        name: t.String,
+        surname: t.maybe(t.String)
+      }, 'Person');
+
+      throwsWithMessage(function () {
+        Person({ name: 'Giulio', age: 42 });
+        t.inter.strict = false;
+      }, '[tcomb] Invalid additional prop "age" supplied to Person');
+      t.inter.strict = false;
+    });
+
   });
 
   describe('interface.extend', function () {
diff --git a/test/struct.js b/test/struct.js
index bfd6b2c..4327832 100644
--- a/test/struct.js
+++ b/test/struct.js
@@ -16,15 +16,19 @@ describe('t.struct(props, [name])', function () {
 
       throwsWithMessage(function () {
         t.struct();
-      }, '[tcomb] Invalid argument props undefined supplied to struct(props, [name]) combinator (expected a dictionary String -> Type)');
+      }, '[tcomb] Invalid argument props undefined supplied to struct(props, [options]) combinator (expected a dictionary String -> Type)');
 
       throwsWithMessage(function () {
         t.struct({a: null});
-      }, '[tcomb] Invalid argument props {\n  "a": null\n} supplied to struct(props, [name]) combinator (expected a dictionary String -> Type)');
+      }, '[tcomb] Invalid argument props {\n  "a": null\n} supplied to struct(props, [options]) combinator (expected a dictionary String -> Type)');
 
       throwsWithMessage(function () {
         t.struct({}, 1);
-      }, '[tcomb] Invalid argument name 1 supplied to struct(props, [name]) combinator (expected a string)');
+      }, '[tcomb] Invalid argument name 1 supplied to struct(props, [options]) combinator (expected a string)');
+
+      throwsWithMessage(function () {
+        t.struct({}, {strict: 1});
+      }, '[tcomb] Invalid argument strict 1 supplied to struct(props, [options]) combinator (expected a boolean)');
 
     });
 
@@ -44,6 +48,25 @@ describe('t.struct(props, [name])', function () {
     });
   });
 
+  describe('struct.getOptions', function () {
+
+    it('should handle options', function () {
+      assert.deepEqual(t.struct.getOptions(), { strict: false });
+      assert.deepEqual(t.struct.getOptions({}), { strict: false });
+      assert.deepEqual(t.struct.getOptions('Person'), { strict: false, name: 'Person' });
+      assert.deepEqual(t.struct.getOptions({ strict: false }), { strict: false });
+      assert.deepEqual(t.struct.getOptions({ strict: true }), { strict: true });
+      t.struct.strict = true;
+      assert.deepEqual(t.struct.getOptions(), { strict: true });
+      assert.deepEqual(t.struct.getOptions({}), { strict: true });
+      assert.deepEqual(t.struct.getOptions('Person'), { strict: true, name: 'Person' });
+      assert.deepEqual(t.struct.getOptions({ strict: false }), { strict: false });
+      assert.deepEqual(t.struct.getOptions({ strict: true }), { strict: true });
+      t.struct.strict = false;
+    });
+
+  });
+
   describe('constructor', function () {
 
     it('should be idempotent', function () {
@@ -64,6 +87,61 @@ describe('t.struct(props, [name])', function () {
       }, '[tcomb] Invalid value undefined supplied to Struct{x: Number, y: Number}/x: Number');
     });
 
+    it('should handle strict option', function () {
+      var Person = t.struct({
+        name: t.String,
+        surname: t.maybe(t.String)
+      }, { name: 'Person', strict: true });
+
+      assert.strictEqual(Person.meta.name, 'Person');
+      assert.strictEqual(Person.meta.strict, true);
+
+      throwsWithMessage(function () {
+        new Person({ name: 'Giulio', age: 42 });
+      }, '[tcomb] Invalid additional prop "age" supplied to Person');
+
+      throwsWithMessage(function () {
+        // simulating a typo on a maybe prop
+        new Person({ name: 'Giulio', sur: 'Canti' });
+      }, '[tcomb] Invalid additional prop "sur" supplied to Person');
+
+      function Input(name) {
+        this.name = name;
+      }
+      Input.prototype.method = function () {};
+      assert.doesNotThrow(function () {
+        new Person(new Input('Giulio'));
+      });
+    });
+
+    it('should handle global strict option', function () {
+      t.struct.strict = true;
+      var Person = t.struct({
+        name: t.String,
+        surname: t.maybe(t.String)
+      }, 'Person');
+
+      throwsWithMessage(function () {
+        new Person({ name: 'Giulio', age: 42 });
+        t.struct.strict = false;
+      }, '[tcomb] Invalid additional prop "age" supplied to Person');
+      t.struct.strict = false;
+    });
+
+    it('how to handle unions of structs when global strict is true', function () {
+      var T1 = t.struct({}, { strict: false });
+      var T2 = t.struct({}, { strict: false });
+      var U = t.union([T1, T2]);
+      U.dispatch = function (x) {
+        switch (x.type) {
+          case '1': return T1;
+          case '2': return T2;
+        }
+      };
+      var x = U({type: '2'});
+      assert.strictEqual(x instanceof T2, true);
+    });
+
   });
 
   describe('#is(x)', function () {