Skip to content

Commit

Permalink
Merge pull request #96 from ericelliott/stamp-function-accept-refs
Browse files Browse the repository at this point in the history
Stamp function accept refs
  • Loading branch information
koresar committed Jun 11, 2015
2 parents 0bfa98e + 7036c87 commit 406afa4
Show file tree
Hide file tree
Showing 4 changed files with 95 additions and 56 deletions.
30 changes: 19 additions & 11 deletions README.md
Expand Up @@ -13,12 +13,12 @@ Looking for a deep dive into prototypal OO, stamps, and the Two Pillars of JavaS
**v1, stable,** in production use with millions of monthly users. There will be no breaking changes in the 1.x line.

**v2, current stable**. Breaking changes:
* `stampit()` now receives options object (`{methods,refs,init,props}`) instead of multiple arguments.
* `stampit()` now receives options object (`{methods,refs,init,props,static}`) instead of multiple arguments.
* All chaining methods return new stamps instead of self-mutating `this` stamp.
* `state()` always shallow merge properties. It was not doing so in a single rare case.
* Instead of factory arguments the `enclose()` functions now recieve the following object `{ instance, stamp, args }`.
* Instead of factory arguments the `enclose()` functions now receives the following object `{ instance, stamp, args }`.

There is a slight chance these changes affect your current codebase. If so, we would recommend you to update to v2 becuase it is more powerful. See [advances examples](https://github.com/ericelliott/stampit/blob/master/ADVANCED_EXAMPLES.md).
There is a slight chance these changes affect your current codebase. If so, we would recommend you to update to v2 because it is more powerful. See [advances examples](https://github.com/ericelliott/stampit/blob/master/ADVANCED_EXAMPLES.md).


## Contribute
Expand Down Expand Up @@ -54,16 +54,18 @@ or by [downloading the latest release](https://github.com/ericelliott/stampit/re

* Create factory functions (called stamps) which stamp out new objects. All of the new objects inherit all of the prescribed behavior.

* Assign properties by passing a references object to the stamp (factory function).

* Compose stamps together to create new stamps.

* Inherit methods and default state.

* Supports composable private state and privileged methods.

* References are copied across for each instance.

* Properties are deeply merged and cloned for each instance, so it won't be accidentally shared.

* Initializers are called for each new instance. Provides wide extensibility to stamp behavior.

* For the curious - it's great for [learning about prototypal OO](http://ericleads.com/2013/02/fluent-javascript-three-different-kinds-of-prototypal-oo/). It mixes three major types of prototypes:
Expand All @@ -83,11 +85,13 @@ Stamp composition takes advantage of three different kinds of prototypal inherit

When invoked the stamp factory function creates and returns object instances assigning:
```js
var myStamp = stampit().
methods({ doSomething: function(){} }). // methods each new object instance will have
refs({ myObj: myObjByRef }). // properties to be set by reference to object instances
var DbAuthStamp = stampit().
methods({ authorize: function(){} }). // methods each new object instance will have
refs({user: {name: 'guest', pwd: ''}}). // properties to be set by reference to object instances
init(function(context){ }). // init function(s) to be called when an object instance is created
props({ foo: {bar: 'bam'} }); // properties to be deeply merged to object instances
props({db: {host: 'localhost'}}); // properties to be deeply merged to object instances

var dbAuthorizer = DbAuthStamp({ user: adminUserCredentials });
```

### How are Stamps Different from Classes?
Expand Down Expand Up @@ -224,7 +228,7 @@ stamp.static({
});
```

## More chaining
## Chaining methods

Chaining stamps *always* creates new stamps.

Expand Down Expand Up @@ -464,7 +468,8 @@ MyStamp.create().originalStamp === MyStamp; // true

### stamp.props() ###

Take n objects and deep merge them to the properties. Creates new stamp.
Take n objects and deep merge them safely to the properties. Creates new stamp.
Note: the merge algorithm will not change any existing `refs` data of a resulting object instance.
* @return {Object} stamp The new stamp based on the original `this` stamp.


Expand All @@ -478,9 +483,12 @@ Combining overrides properties with last-in priority.

### stamp.create([properties] [,arg2] [,arg3...]) ###

Alias to `stamp([properties] [,arg2] [,arg3...])`.

Just like calling `stamp()`, `stamp.create()` invokes the stamp
and returns a new object instance. The first argument is an object
containing properties you wish to set on the new objects.
The properties are copied by reference using standard mixin/extend/assign algorithm.

The remaining arguments are passed to all `.init()`
functions. **WARNING** Avoid using two different `.init()`
Expand Down
65 changes: 51 additions & 14 deletions mixer.js
Expand Up @@ -3,12 +3,13 @@ var forOwn = require('lodash/object/forOwn');
var forIn = require('lodash/object/forIn');
var deepClone = require('lodash/lang/cloneDeep');
var isObject = require('lodash/lang/isObject');
var isUndefined = require('lodash/lang/isUndefined');
var isFunction = require('lodash/lang/isFunction');

/**
* Creates mixin functions of all kinds.
* @param {object} opts Options.
* @param {function(object, string)} opts.filter Function which filters value and key.
* @param {function(object, object)} opts.filter Function which filters value and key.
* @param {boolean} opts.chain Loop through prototype properties too.
* @param {function(object)} opts.getTarget Converts an object object to a target.
* @param {function(object, object)} opts.getValue Converts src and dst values to a new value.
Expand All @@ -26,32 +27,30 @@ var mixer = function (opts) {
var loop = opts.chain ? forIn : forOwn;
var i = 0,
n = arguments.length,
obj;
target = opts.getTarget ? opts.getTarget(target) : target;
obj,
filter = opts.filter,
getValue = opts.getValue,
mergeValue = function mergeValue(val, key) {
if (filter && !filter(val, target[key])) {
return;
}

var mergeValue = function mergeValue(val, key) {
if (opts.filter && !opts.filter(val, key)) {
return;
}
target[key] = getValue ? getValue(val, target[key]) : val;
};

this[key] = opts.getValue ? opts.getValue(val, this[key]) : val;
};
target = opts.getTarget ? opts.getTarget(target) : target;

while (++i < n) {
obj = arguments[i];
if (obj) {
loop(
obj,
mergeValue,
target);
loop(obj, mergeValue);
}
}
return target;
};
};

var merge = mixer({
getTarget: deepClone,
/* jshint ignore:start */
getValue: mergeSourceToTarget
/* jshint ignore:end */
Expand Down Expand Up @@ -94,6 +93,34 @@ module.exports.mixinChainFunctions = mixer({
*/
module.exports.merge = merge;


var mergeUnique = mixer({
filter: function (srcVal, targetVal) {
return isUndefined(targetVal) || (isObject(srcVal) && isObject(targetVal));
},
/* jshint ignore:start */
getValue: mergeUniqueSourceToTarget
/* jshint ignore:end */
});

function mergeUniqueSourceToTarget(srcVal, targetVal) {
if (isObject(srcVal) && isObject(targetVal)) {
// inception, deep merge objects
return mergeUnique(targetVal, srcVal);
} else if (isUndefined(targetVal)) {
// make sure arrays, regexp, date, objects are cloned
return deepClone(srcVal);
} else {
return targetVal;
}
}

/**
* Just like regular object merge, but do not override destination object data.
*/
module.exports.mergeUnique = mergeUnique;


/**
* merge objects including prototype chain properties.
*/
Expand All @@ -103,3 +130,13 @@ module.exports.mergeChainNonFunctions = mixer({
getValue: mergeSourceToTarget,
chain: true
});

/**
* merge unique properties of objects including prototype chain properties.
*/
module.exports.mergeUniqueChainNonFunctions = mixer({
filter: function (val) { return !isFunction(val); },
getTarget: deepClone,
getValue: mergeUniqueSourceToTarget,
chain: true
});
23 changes: 11 additions & 12 deletions stampit.js
Expand Up @@ -10,7 +10,6 @@
var forEach = require('lodash/collection/forEach');
var map = require('lodash/collection/map');
var forOwn = require('lodash/object/forOwn');
var deepClone = require('lodash/lang/cloneDeep');
var isFunction = require('lodash/lang/isFunction');
var isArray = Array.isArray;
var isObject = require('lodash/lang/isObject');
Expand Down Expand Up @@ -104,15 +103,15 @@ function compose(factories) {
* @param {Object} [options.init] A closure (function) used to create private data and privileged methods.
* @param {Object} [options.props] An object to be deeply cloned into each newly stamped object.
* @param {Object} [options.static] An object to be mixed into each `this` and derived stamps (not objects!).
* @return {Function} factory A factory to produce objectss.
* @return {Function} factory.create Just like calling the factory function.
* @return {Function(refs)} factory A factory to produce objects.
* @return {Function(refs)} factory.create Just like calling the factory function.
* @return {Object} factory.fixed An object map containing the stamp components.
* @return {Function} factory.methods Add methods to the stamp. Chainable.
* @return {Function} factory.refs Add references to the stamp. Chainable.
* @return {Function} factory.init Add a closure which called on object instantiation. Chainable.
* @return {Function} factory.props Add deeply cloned properties to the produced objects. Chainable.
* @return {Function} factory.compose Combine several stamps into single. Chainable.
* @return {Function} factory.static Add properties to the stamp (not objects!). Chainable.
* @return {Function(methods)} factory.methods Add methods to the stamp. Chainable.
* @return {Function(refs)} factory.refs Add references to the stamp. Chainable.
* @return {Function(Function(context))} factory.init Add a closure which called on object instantiation. Chainable.
* @return {Function(props)} factory.props Add deeply cloned properties to the produced objects. Chainable.
* @return {Function(stamps)} factory.compose Combine several stamps into single. Chainable.
* @return {Function(statics)} factory.static Add properties to the stamp (not objects!). Chainable.
*/
stampit = function stampit(options) {
var fixed = {methods: {}, refs: {}, init: [], props: {}, static: {}};
Expand All @@ -126,9 +125,9 @@ stampit = function stampit(options) {
addStatic(fixed, options.static);
}

var factory = function Factory(properties, args) {
properties = properties ? mixer.merge({}, fixed.props, properties) : deepClone(fixed.props);
var instance = mixer.mixin(create(fixed.methods), fixed.refs, properties); // props are taking over refs
var factory = function Factory(refs, args) {
var instance = mixer.mixin(create(fixed.methods), fixed.refs, refs);
mixer.mergeUnique(instance, fixed.props); // props are safely merged into refs

if (fixed.init.length > 0) {
args = slice(arguments, 1, arguments.length);
Expand Down
33 changes: 14 additions & 19 deletions test/props-safety.js
Expand Up @@ -26,27 +26,22 @@ test('Stamp props deep cloned for object created', function (t) {
t.end();
});

test('stamp(props) deep merge into object created', function (t) {
var deep = { foo: 'foo', bar: 'bar' };
var stamp1 = stampit().props({ deep: deep, foo: 'foo', bar: 'bar' });
var stamp2 = stampit({ props: { deep: deep, foo: 'foo', bar: 'bar' } });
test('stamp(refs) deep merges props into refs', function (t) {
var deepInProps = { deepProp1: 'should not be merged', deepProp2: 'merge me!' };
var stamp1 = stampit().props({ deep: deepInProps, shallow1: 'should not be merged', shallow2: 'merge me!' });
var stamp2 = stampit({ props: { deep: deepInProps, shallow1: 'should not be merged', shallow2: 'merge me!' } });

var deep2 = { foo: 'override', baz: 'baz' };
var o1 = stamp1({ deep: deep2, foo: 'override', baz: 'baz' });
var o2 = stamp2({ deep: deep2, foo: 'override', baz: 'baz' });
var o1 = stamp1({ deep: { deepProp1: 'leave me as is' }, shallow1: 'leave me as is' });
var o2 = stamp2({ deep: { deepProp1: 'leave me as is' }, shallow1: 'leave me as is' });

t.equal(o1.foo, 'override');
t.equal(o1.bar, 'bar');
t.equal(o1.baz, 'baz');
t.equal(o2.foo, 'override');
t.equal(o2.bar, 'bar');
t.equal(o2.baz, 'baz');
t.equal(o1.deep.foo, 'override');
t.equal(o1.deep.bar, 'bar');
t.equal(o1.deep.baz, 'baz');
t.equal(o2.deep.foo, 'override');
t.equal(o2.deep.bar, 'bar');
t.equal(o2.deep.baz, 'baz');
t.equal(o1.shallow1, 'leave me as is', 'A conflicting shallow reference must not be touched by props');
t.equal(o1.shallow2, 'merge me!', 'A non conflicting shallow reference must be merged from props');
t.equal(o2.shallow1, 'leave me as is', 'A conflicting shallow reference must not be touched by props');
t.equal(o2.shallow2, 'merge me!', 'A non conflicting shallow reference must be merged from props');
t.equal(o1.deep.deepProp1, 'leave me as is', 'A conflicting deep property in refs must not be touched by props');
t.equal(o1.deep.deepProp2, 'merge me!', 'A non conflicting deep property must be merged from props');
t.equal(o2.deep.deepProp1, 'leave me as is', 'A conflicting deep property in refs must not be touched by props');
t.equal(o2.deep.deepProp2, 'merge me!', 'A non conflicting deep property must be merged from props');

t.end();
});
Expand Down

0 comments on commit 406afa4

Please sign in to comment.