Repository: gcanti/tcomb
Pull request #211 (2016-05-31 06:28:53)

add support for default props


Patch:
diff --git a/docs/API.md b/docs/API.md
index f813d08..0b3b19f 100644
--- a/docs/API.md
+++ b/docs/API.md
@@ -210,7 +210,8 @@ Person({ name: 'Giulio', age: 'a string' }); // => throws
 ```js
 type Options = {
   name?: string,
-  strict?: boolean
+  strict?: boolean,
+  defaultProps?: Object
 };
 
 (props: {[key: string]: TcombType;}, options?: string | Options) => TcombType
@@ -235,9 +236,11 @@ const point = Point({ x: 1, y: 2 });
 ```js
 {
   kind: 'struct',
-  name: name,
+  name: options.name,
   identity: false,
-  props: props
+  props: props,
+  strict: options.strict,
+  defaultProps: options.defaultProps
 }
 ```
 
@@ -278,8 +281,13 @@ Every struct constructor owns an `extend` function:
 ```js
 type Props = {[key: String]: Type};
 type Mixin = Props | TcombStruct | TcombInterface | refinement(Mixin);
+type Options = {
+  name?: string,
+  strict?: boolean,
+  defaultProps?: Object
+};
 
-extend(mixins: Mixin | Array<Mixin>, name?: string) => TcombStruct
+extend(mixins: Mixin | Array<Mixin>, options?: : string | Options) => TcombStruct
 ```
 
 **Example**
@@ -323,13 +331,13 @@ Cube.prototype.getVolume = function () {
 const Wrong = Point.extend({ x: t.String }); // => throws '[tcomb] Invalid call to mixin(target, source, [overwrite]): cannot overwrite property "x" of target object'
 ```
 
-Alternatively you can use the `t.struct.extend(mixins: Array<Mixin>, name?: string) => TcombType` function:
+Alternatively you can use the `t.struct.extend(mixins: Array<Mixin>, options?: string | Options) => TcombType` function:
 
 ```js
 const Point3D = t.struct.extend([Point, { z: t.Number }], 'Point3D');
 ```
 
-**Note**. The implementation uses the top level function `extend(combinator, mixins, name)` defined in `tcomb/lib/extend`
+**Note**. The implementation uses the top level function `extend(combinator, mixins, options)` defined in `tcomb/lib/extend`
 
 # The `tuple` combinator
 
@@ -582,9 +590,10 @@ doSerialize(bar.point); // => ok
 ```js
 {
   kind: 'interface',
-  name: name,
+  name: options.name,
   identity: ...depends on props,
-  props: props
+  props: props,
+  strict: options.strict
 }
 ```
 
@@ -612,8 +621,12 @@ Every interface constructor owns an `extend` function:
 ```js
 type Props = {[key: String]: Type};
 type Mixin = Props | TcombStruct | TcombInterface | refinement(Mixin);
+type Options = {
+  name?: string,
+  strict?: boolean
+};
 
-extend(mixins: Mixin | Array<Mixin>, name?: string) => TcombStruct
+extend(mixins: Mixin | Array<Mixin>, options?: string | Options) => TcombStruct
 ```
 
 **Example**
@@ -635,13 +648,13 @@ const E = A.extend([B, MixinC, MixinD]);
 const Wrong = Point.extend({ x: t.String }); // => throws '[tcomb] Invalid call to mixin(target, source, [overwrite]): cannot overwrite property "x" of target object'
 ```
 
-Alternatively you can use the `t.interface.extend(mixins: Array<Mixin>, name?: string) => TcombType` function:
+Alternatively you can use the `t.interface.extend(mixins: Array<Mixin>, options?: string | Options) => TcombType` function:
 
 ```js
 const Point3D = t.interface.extend([Point, { z: t.Number }], 'Point3D');
 ```
 
-**Note**. The implementation uses the top level function `extend(combinator, mixins, name)` defined in `tcomb/lib/extend`
+**Note**. The implementation uses the top level function `extend(combinator, mixins, options)` defined in `tcomb/lib/extend`
 
 # The `func` combinator
 
diff --git a/lib/extend.js b/lib/extend.js
index 3648cd3..b12d779 100644
--- a/lib/extend.js
+++ b/lib/extend.js
@@ -41,29 +41,37 @@ function getProps(type) {
   return isObject(type) ? type : type.meta.props;
 }
 
+function getDefaultProps(type) {
+  return isObject(type) ? null : type.meta.defaultProps;
+}
+
 function pushAll(arr, elements) {
   Array.prototype.push.apply(arr, elements);
 }
 
-function extend(combinator, mixins, name) {
+function extend(combinator, mixins, options) {
   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'; });
+    assert(isFunction(combinator), function () { return 'Invalid argument combinator supplied to extend(combinator, mixins, options), expected a function'; });
+    assert(isArray(mixins), function () { return 'Invalid argument mixins supplied to extend(combinator, mixins, options), expected an array'; });
   }
   var props = {};
   var prototype = {};
   var predicates = [];
+  var defaultProps = {};
   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)'; });
+      assert(isObject(unrefinedType) || isStruct(unrefinedType) || isInterface(unrefinedType), function () { return 'Invalid argument mixins[' + i + '] supplied to extend(combinator, mixins, options), expected an object, struct, interface or a refinement (of struct or interface)'; });
     }
     pushAll(predicates, decomposition.predicates);
     mixin(props, getProps(unrefinedType));
     mixin(prototype, unrefinedType.prototype);
+    mixin(defaultProps, getDefaultProps(unrefinedType));
   });
-  var result = compose(predicates, combinator(props, name));
+  options = combinator.getOptions(options);
+  mixin(options.defaultProps, defaultProps);
+  var result = compose(predicates, combinator(props, options));
   mixin(result.prototype, prototype);
   return result;
 }
diff --git a/lib/struct.js b/lib/struct.js
index 4511a84..30945a3 100644
--- a/lib/struct.js
+++ b/lib/struct.js
@@ -26,6 +26,9 @@ function getOptions(options) {
   if (!options.hasOwnProperty('strict')) {
     options.strict = struct.strict;
   }
+  if (!options.hasOwnProperty('defaultProps')) {
+    options.defaultProps = {};
+  }
   return options;
 }
 
@@ -34,11 +37,13 @@ function struct(props, options) {
   options = getOptions(options);
   var name = options.name;
   var strict = options.strict;
+  var defaultProps = options.defaultProps;
 
   if (process.env.NODE_ENV !== 'production') {
     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)'; });
+    assert(isObject(defaultProps), function () { return 'Invalid argument defaultProps ' + assert.stringify(defaultProps) + ' supplied to struct(props, [options]) combinator (expected an object)'; });
   }
 
   var displayName = name || getDefaultName(props);
@@ -70,6 +75,10 @@ function struct(props, options) {
       if (props.hasOwnProperty(k)) {
         var expected = props[k];
         var actual = value[k];
+        // apply defaults
+        if (actual === undefined) {
+          actual = defaultProps[k];
+        }
         this[k] = create(expected, actual, ( process.env.NODE_ENV !== 'production' ? path.concat(k + ': ' + getTypeName(expected)) : null ));
       }
     }
@@ -85,7 +94,8 @@ function struct(props, options) {
     props: props,
     name: name,
     identity: false,
-    strict: strict
+    strict: strict,
+    defaultProps: defaultProps
   };
 
   Struct.displayName = displayName;


Test patch:
diff --git a/test/extend.js b/test/extend.js
index 20cd485..8ddf8f8 100644
--- a/test/extend.js
+++ b/test/extend.js
@@ -18,10 +18,39 @@ 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');
+    }, '[tcomb] Invalid argument mixins supplied to extend(combinator, mixins, options), 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)');
+    }, '[tcomb] Invalid argument mixins[0] supplied to extend(combinator, mixins, options), expected an object, struct, interface or a refinement (of struct or interface)');
+  });
+
+  it('should handle defaultProps', function () {
+    var A1 = t.struct({ a: t.String }, { defaultProps: { a: 'default-a' } });
+    var B1 = t.struct({ b: t.String }, { defaultProps: { b: 'default-b' } });
+    var C1 = t.struct.extend([A1, B1, { z: t.Number }], { defaultProps: { z: 1 } });
+    assert.deepEqual(C1.meta.defaultProps, {
+      a: 'default-a',
+      b: 'default-b',
+      z: 1
+    });
+    assert.deepEqual(C1({}), {
+      a: 'default-a',
+      b: 'default-b',
+      z: 1
+    });
+
+    var A2 = t.struct({ a: t.String }, { defaultProps: { a: 'default-a' } });
+    var B2 = t.struct({ b: t.String }, { defaultProps: { b: 'default-b' } });
+    var C2 = t.struct.extend([A2, B2, { z: t.Number }], 'C2');
+    assert.deepEqual(C2.meta.defaultProps, {
+      a: 'default-a',
+      b: 'default-b'
+    });
+    assert.deepEqual(C2({ z: 2 }), {
+      a: 'default-a',
+      b: 'default-b',
+      z: 2
+    });
   });
 
   describe('struct.extend(mixins, [name])', function () {
diff --git a/test/struct.js b/test/struct.js
index a0a6e29..ae2a161 100644
--- a/test/struct.js
+++ b/test/struct.js
@@ -37,17 +37,17 @@ 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 });
+      assert.deepEqual(t.struct.getOptions(), { strict: false, defaultProps: {} });
+      assert.deepEqual(t.struct.getOptions({}), { strict: false, defaultProps: {} });
+      assert.deepEqual(t.struct.getOptions('Person'), { strict: false, name: 'Person', defaultProps: {} });
+      assert.deepEqual(t.struct.getOptions({ strict: false }), { strict: false, defaultProps: {} });
+      assert.deepEqual(t.struct.getOptions({ strict: true }), { strict: true, defaultProps: {} });
       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 });
+      assert.deepEqual(t.struct.getOptions(), { strict: true, defaultProps: {} });
+      assert.deepEqual(t.struct.getOptions({}), { strict: true, defaultProps: {} });
+      assert.deepEqual(t.struct.getOptions('Person'), { strict: true, name: 'Person', defaultProps: {} });
+      assert.deepEqual(t.struct.getOptions({ strict: false }), { strict: false, defaultProps: {} });
+      assert.deepEqual(t.struct.getOptions({ strict: true }), { strict: true, defaultProps: {} });
       t.struct.strict = false;
     });
 
@@ -153,4 +153,45 @@ describe('t.struct(props, [name])', function () {
 
   });
 
+  describe('default props', function () {
+
+    it('should throw if used with wrong arguments', function () {
+      throwsWithMessage(function () {
+        t.struct({}, { defaultProps: 1 });
+      }, '[tcomb] Invalid argument defaultProps 1 supplied to struct(props, [options]) combinator (expected an object)');
+    });
+
+    it('should handle the defaultProps option', function () {
+      var T = t.struct({
+        name: t.String,
+        surname: t.String
+      }, { defaultProps: { surname: 'Canti' } });
+      assert.doesNotThrow(function () {
+        T({ name: 'Giulio' });
+      });
+    });
+
+    it('should apply defaults if a props is undefined', function () {
+      var T = t.struct({
+        name: t.maybe(t.String)
+      }, { defaultProps: { name: 'Giulio' } });
+      assert.strictEqual(T({}).name, 'Giulio');
+      assert.strictEqual(T({ name: undefined }).name, 'Giulio');
+      assert.strictEqual(T({ name: null }).name, null);
+    });
+
+    it('should apply the default after an update', function () {
+      var T = t.struct({
+        name: t.String,
+        surname: t.String
+      }, { defaultProps: { surname: 'Canti' } });
+      var p1 = T({ name: 'Giulio' });
+      var p2 = T.update(p1, {
+        surname: { $set: undefined }
+      });
+      assert.strictEqual(p2.name, 'Giulio');
+    });
+
+  });
+
 });