Skip to content

Commit

Permalink
Merge pull request #71 from stampit-org/replaceable-compose-method
Browse files Browse the repository at this point in the history
Make the .compose replaceable via staticProperties.
  • Loading branch information
koresar committed Feb 24, 2016
2 parents 09d2612 + 723f86a commit a39e687
Show file tree
Hide file tree
Showing 4 changed files with 120 additions and 16 deletions.
2 changes: 1 addition & 1 deletion .eslintrc
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@
"no-unreachable": [2],
"no-unused-expressions": [2],
"no-unused-vars": [1, {"vars": "all", "args": "after-used"}],
"no-use-before-define": [2],
"no-use-before-define": [0],
"no-void": [0],
"no-warning-comments": [0, {"terms": ["todo", "fixme", "xxx"], "location": "start"}],
"no-with": [2],
Expand Down
30 changes: 26 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,20 +62,42 @@ const combinedStamp = baseStamp.compose(composable1, composable2, composable3);
The `.compose()` method doubles as the stamp's descriptor. In other words, descriptor properties are attached to the stamp `.compose()` method, e.g. `stamp.compose.methods`.


#### Overriding `.compose()` method

It is possible to override the `.compose()` method of a stamp using `staticProperties`. Handy for debugging purposes.

```js
import differentComposeImplementation from 'different-compose-implementation';
const composeOverriddenStamp = stamp.compose({
staticProperties: {
compose: differentComposeImplementation
}
});
```


### Descriptor

**Composable descriptor** (or just **descriptor**) is a meta data object which contains the information necessary to create an object instance.



### Standalone `compose()` function (optional)
### Standalone `compose()` pure function (optional)

```js
(...args?: Composable[]) => Stamp
```

**Creates stamps.** Take any number of stamps or descriptors. Return a new stamp that encapsulates combined behavior. If nothing is passed in, it returns an empty stamp.

#### Detached `compose()` method

The `.compose()` method of any stamp can be detached and used as a standalone `compose()` pure function.

```js
const compose = thirdPartyStamp.compose;
const myStamp = compose(myComposable1, myComposable2);
```

## Implementation details

Expand Down Expand Up @@ -151,7 +173,7 @@ It is possible for properties to collide, between both stamps, and between diffe
**Different descriptor properties, one or more stamps:**

* Shallow properties override deep properties
* Descriptors override everything
* Property Descriptors override everything

#### Configuration

Expand Down Expand Up @@ -181,7 +203,7 @@ const myStamp = compose(config, warnOnCollisions);
```


### Stamp Options
### Stamp Arguments

It is recommended that stamps only take one argument: The stamp `options` argument. There are no reserved properties and no special meaning. However, using multiple arguments for a stamp could create conflicts where multiple stamps expect the same argument to mean different things. Using named parameters, it's possible for stamp creator to resolve conflicts with `options` namespacing. For example, if you want to compose a database connection stamp with a message queue stamp:

Expand Down Expand Up @@ -261,6 +283,6 @@ Initializers have the following signature:
* *Thenable* ~ *Composable*.
* `.then` ~ `.compose`.
* *Promise* ~ *Stamp*.
* `new Promise(function(resolve, reject))` ~ `compose(...stampsOrDescriptors)`
* `new Promise(function(resolve, reject))` ~ `compose(...composables)`

-----
19 changes: 9 additions & 10 deletions examples/compose.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@ const isFunction = obj => typeof obj === 'function';
const isObject = obj => !!obj && (typeof obj === 'function' || typeof obj === 'object');
const isDescriptor = isObject;

const createStamp = (composeMethod) => {
const createStamp = (descriptor) => {
const {
methods, properties, deepProperties, propertyDescriptors, initializers,
staticProperties, staticDeepProperties, staticPropertyDescriptors
} = composeMethod;
} = descriptor;

const Stamp = function Stamp(options, ...args) {
let obj = Object.create(methods || {});
Expand All @@ -34,7 +34,12 @@ const createStamp = (composeMethod) => {
merge(Stamp, staticDeepProperties);
assign(Stamp, staticProperties);
if (staticPropertyDescriptors) Object.defineProperties(Stamp, staticPropertyDescriptors);
Stamp.compose = composeMethod;

const composeImplementation = isFunction(Stamp.compose) ? Stamp.compose : compose;
Stamp.compose = function () {
return composeImplementation.apply(this, arguments);
};
assign(Stamp.compose, descriptor);

return Stamp;
};
Expand Down Expand Up @@ -63,13 +68,7 @@ function mergeInComposable(dstDescriptor, src) {
}

function compose(...composables) {
let composeMethod = function (...args) {
return compose(composeMethod, ...args);
};

composeMethod = composables.reduce(mergeInComposable, composeMethod);

return createStamp(composeMethod);
return createStamp(composables.reduce(mergeInComposable, mergeInComposable({}, this)));
}

export default compose;
85 changes: 84 additions & 1 deletion test/compose-tests.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,94 @@ import compose from '../examples/compose';

test('compose ignores non objects', assert => {
const stamp = compose(0, 'a', null, undefined, {}, NaN, /regexp/);
const subject = _.values(stamp.compose).filter(value => !_.isEmpty(value)).length;
const subject = _.values(stamp.compose).filter(_.negate(_.isEmpty)).length;
const expected = 0;

assert.equal(subject, expected,
'should not add any descriptor data');

assert.end();
});

test('compose in order', assert => {
const initOrder = [];
const getInitDescriptor = (value) => {
return {initializers: [() => initOrder.push(value)]};
};

const stamp = compose(
compose(getInitDescriptor(0)),
compose(getInitDescriptor(1), getInitDescriptor(2))
.compose(getInitDescriptor(3), getInitDescriptor(4)),
getInitDescriptor(5)
);
stamp();
const expected = [0, 1, 2, 3, 4, 5];

assert.deepEqual(initOrder, expected,
'should compose in proper order');

assert.end();
});

test('compose is detachable', assert => {
const detachedCompose = compose().compose;

assert.notEqual(compose, detachedCompose,
'stamp .compose function must be a different object to "compose"');

assert.end();
});

test('detached compose does not inherit previous descriptor', assert => {
const detachedCompose = compose({properties: {foo: 1}}).compose;
const obj = detachedCompose()();
const expected = undefined;

assert.equal(obj.foo, expected,
'detached compose method should not inherit parent descriptor data');

assert.end();
});

test('compose is replaceable', assert => {
let counter = 0;
function newCompose() {
counter++;
return compose({staticProperties: {compose: newCompose}}, this, arguments);
}
newCompose().compose().compose();
const expected = 3;

assert.equal(counter, expected,
'should inherit new compose function');

assert.end();
});

test('replaced compose method is always a new object', assert => {
function newCompose() {
return compose({staticProperties: {compose: newCompose}}, this, arguments);
}
const stamp1 = newCompose();
const compose1 = stamp1.compose;
const stamp2 = stamp1.compose();
const compose2 = stamp2.compose;

assert.notEqual(compose1, compose2, 'should be different objects');

assert.end();
});

test('replaced compose method is always a function', assert => {
function newCompose() {
return compose({staticProperties: {compose: newCompose}}, this, arguments);
}
const overridenCompose = newCompose().compose().compose;
const actual = _.isFunction(overridenCompose);
const expected = true;

assert.equal(actual, expected, 'should be a function');

assert.end();
});

0 comments on commit a39e687

Please sign in to comment.