Fun with Stamps. Episode 8. Tracking and overriding composition

Vasyl Boroviak
5 min readJun 19, 2016

Hello. I’m developer Vasyl Boroviak and welcome to the eighth episode of Vasyl Boroviak presents Fun with Stamps.

UPD: A simpler composition tracking is explained in
Ep 11. Interfering Composition.

Sometimes you need to compose stamps in a certain non-standard way. For example, you might want to make sure there is no method collision when you compose. Or, maybe you want to debug which stamps were used to create a particular stamp. Etc.

You can do that by wrapping and overriding the compose() function.

Overriding composition

Let’s practice. Let’s write some code which would ensure there is no static method collisions.

We will wrap the compose() standalone function:

import compose from 'stamp-specification'; // real NPM modulefunction avoidStaticCollisionsCompose(...args) { // the wrapper.
checkNoStaticCollisions(args); // see below
return compose(...args);
}

And try using it:

const MyStamp = compose({staticProperties: {myMethod() {}}});
const myDescriptor = {staticProperties: {myMethod() {}}};
avoidStaticCollisionsCompose(MyStamp, myDescriptor); // will throw

The last line will throw the exception - “Error: Collision detected: myMethod”.

The checkNoStaticCollisions() function can be implemented something like that (which is not really important for us right now, but anyways, here is the full source code):

import _ from 'lodash';function getDescriptor(obj) {
if (_.isFunction(obj) && _.isFunction(obj.compose)) {
return obj.compose; // "obj" is a stamp
}
if (_.isObject(obj)) {
return obj; // "obj" is a POJO
}
return {}; // "obj" is a rubbish we don't care about
}
function checkNoStaticCollisions(args) {
const allValues = {};
args.map(getDescriptor).map(arg => arg.staticProperties)
.forEach(arg => {
_.assignWith(allValues, arg, (dstValue, srcValue, key) => {
if (!_.isUndefined(dstValue) && dstValue !== srcValue) {
throw new Error(`Collision detected: ${key}`);
}
}
});
}

But there is one problem with this avoidStaticCollisionsCompose() wrapper. The collisions won’t be checked when the .compose() method is used. Like so:

avoidStaticCollisionsCompose()   // even if the wrapper is used,
.compose(MyStamp, myDescriptor); // it doesn't throw any more :(

But no worries!

From the 6-th episode of Fun with Stamp you learned about statics — properties on stamps. We can use it to override the .compose() static method! Here:

function avoidStaticCollisionsCompose(...args) {
checkNoStaticCollisions(args);
args.push({staticProperties: {
compose: avoidStaticCollisionsCompose // overriding .compose()
}});
return compose.apply(this, args); // `this` must be passed too
}

As you can see, we are forcing all new stamps to have the different .compose() method. We are adding a new static property to each new stamp. The property references the avoidStaticCollisionsCompose() itself!

So, now you can use the avoidStaticCollisionsCompose() same way as the regular compose() function.

I call that trick the

INFECTED COMPOSE.

Fun fact. The stampit module is an infected compose itself. :) It forcefully adds many static properties to your stamps.

Tracking composition

Now you are going to see a way how to track (or debug) stamp composition.

One more infected compose below. It will stop debugger when a composition happens. And it will print everything being composed.

function debuggerCompose(...args) {
debugger;
console.log('Composing these:', args);
args.push({staticProperties: {
compose: debuggerCompose
}});
return compose.apply(this, args);
}
debuggerCompose(1, {}, 'whatever', SomeStamp); // will print these

Decomposing stamps

Another example. If you want your objects to contain the list of stamps it was composed of, then you can simply collect them in the deepConfiguration. Like that:

import compose from 'stamp-specification'; // real NPM module
import _ from 'lodash';
function initializer(options, {stamp}) {
const sources = _.uniq(stamp.compose.deepConfiguration.sources);
this.getMySources = () => sources; // <- creating method
}
function sourceCollectingCompose(...args) {
const additional = {
staticProperties: {compose: sourceCollectingCompose},
deepConfiguration: {
sources: _.clone(args) // <- collecting composables
},
initializers: [initializer]
};
return compose.apply(this, [additional].concat(args));
}

As you can see we are infecting each stamp with three more metadata.

  • First one is the infection part — the static .compose() method.
  • Second one is the collection of composable sources (descriptors and stamps) inside the deepConfiguration.sources array.
  • Third one is the initializer, which adds new .getMySources() method to each object. The method returns the data collected in the deepConfiguration.sources.

Below is the usage of our new infected compose. I would like to remind that Stamps are immutable. Every compose call creates a new Stamp.

const Stamp1 = compose({properties: {foo: 42}});
const Stamp2 = compose({methods: {bar: ()=>{}}});
const Descr3 = {deepProperties: {baz: []}};
const All = sourceCollectingCompose(Stamp1) // <- INFECTED STAMP
.compose(Stamp2) // <- INFECTED STAMP TOO
.compose(Descr3); // <- INFECTED STAMP TOO
const obj = All();
console.log(obj.getMySources()); // accessing Stamp1, Stamp2, Descr3

Will print:

[
{ [Function: Stamp] compose: { [Function] properties: [Object] } },
{ [Function: Stamp] compose: { [Function] methods: [Object] } },
{ deepProperties: { baz: [] } }
]

Amazing! Objects know what they consist of!

Un-infecting stamps

To cure stamps you need to override composition one more time and replace the .compose with the original compose function implementation.

import compose from 'stamp-specification'; // originalfunction pureCompose(...args) {
args.push({staticProperties: {
compose: compose // <- referencing the original compose
}});
return compose.apply(this, args);
}
const CuredStamp = pureCompose(InfectedStamp); // un-infected stamp

Double infected stamps

But what if you don’t want to remove first infection? What if you want to add another infection on top of the existing?

Easy!

Here is the first infection.

import compose from 'stamp-specification'; // originalfunction firstInfectionCompose(...args) {
console.log('First infection works!');
args.push({staticProperties: {compose: firstInfectionCompose}}); return compose.apply(this, args);
}
const InfectedStamp = firstInfectionCompose();
InfectedStamp.compose();

The code above will print:

First infection works!
First infection works!

Now, we need to wrap the first infected compose with another wrapper.

import compose from 'stamp-specification'; // original// detaching the first infected compose implementation from a stamp
const
firstCompose = InfectedStamp.compose;
function secondInfectionCompose(...args) {
// calling the detached first infected compose
const
TemporaryStamp = firstCompose.apply(this, args);
// Now calling the second infection logic
console.log('Second infection works!');
return compose(TemporaryStamp, {staticProperties: {
compose: secondInfectionCompose
}});
}
const DoubleInfectedStamp = secondInfectionCompose();
DoubleInfectedStamp.compose();

Will print:

Second infection works!
First infection works!
Second infection works!
First infection works!

The infected compose is a very powerful feature. But, please, remember.

With great power comes great responsibility

UPD: A simpler composition tracking is explained in
Ep 11. Interfering Composition.

--

--