Fun with Stamps. Episode 6. Statics — properties on stamps

Vasyl Boroviak
6 min readMay 23, 2016

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

Stamps have “static” properties similar to static properties in classes.

Class:

class MyClass {
static staticMethod() {
return 'classic static';
}
}
class MySubClass extends MyClass {} // inheritingMySubClass.staticMethod(); // "classic static"

Stamp:

import compose from 'stamp-specification';const MyStamp = compose({staticProperties: {
staticMethod() {
return 'stamp static';
}
}};
const MySubStamp = MyStamp.compose(); // inheritingMySubStamp.staticMethod(); // "stamp static"

As you can see from the example above, stamps inherit static properties the same way classes do. Although, statics in stamps have several more features.

Implementing a Slack API client stamp

Imagine that you are developing a Slack API client stamp — SlackApi. The user of your stamp must provide the team property. Otherwise, there is no sense creating an instance of the stamp.

const myObj = SlackApi(); // should throw 'Error: "team" is missing'const myObj = SlackApi({team: 'Sheldon'}); // should be OK

With stamps you would want to move that assertion logic to a separate stamp:

const TeamArgumentChecker = compose({initializers: [
function ({team} = {}) {
if (typeof team !== 'string')
throw new Error('"team" is missing');
}
]});
const SlackApi = compose(..., TeamArgumentChecker);SlackApi(); // throws 'Error: "team" is missing'

Same way, the accessToken is a mandatory property too. We have to create a very similar AccessTokenArgumentChecker stamp.

const AccessTokenArgumentChecker = compose({initializers: [
function ({accessToken} = {}) {
if (typeof accessToken !== 'string')
throw new Error('"accessToken" is missing');
}
]});
const SlackApi = compose(..., AccessTokenArgumentChecker);

But that is too much copy-paste. What if we could simplify the code to:

const SlackApi = compose(...)
.compose(ArgumentChecker).checkArguments({ // chaining!!!
team: 'string',
accessToken: 'string
});

So that the .checkArguments() returns a new stamp, just like the compose() call. (In fact, checkArguments will be calling this.compose() internally.) Although, it is not necessary for static functions to return stamps. You are free in what your static functions do. Apply some fantasy my dear reader. :)

The .checkArguments() would be the place we moved the copy-pasted logic to. It’ll be be clear further down in the article. Stay with me.

This is very easy to implement. Let’s do it step by step.

First, the ArgumentChecker stamp should add checkArguments static function to the resulting stamp:

const ArgumentChecker = compose({staticProperties: {
checkArguments() {
// ???
}
}});

Now, the checkArguments should return a new stamp using the compose(). By appending this. in front of the compose() we will make sure the new stamp is based on the current stamp.

const ArgumentChecker = compose({staticProperties: {
checkArguments() {
return this.compose( // `this` references the stamp itself
// ???
);
}
}});

We should add a new initializer to the this.compose() function:

const ArgumentChecker = compose({staticProperties: {
checkArguments() {
return this.compose({initializers: [ // adding an initializer
function (options = {}) { // initializer receiving options
// ???
}
]});
}
}});

The initializer would iterate over the passed keyValueMap:

const ArgumentChecker = compose({staticProperties: {
checkArguments(keyValueMap) { // passed key-value map
return this.compose({initializers: [
function (options = {}) {
Object.keys(keyValueMap).forEach(key => { // iterating
// ???
});
}
]});
}
}});

And check each key-value pair:

const ArgumentChecker = compose({staticProperties: {
checkArguments(keyValueMap) {
return this.compose({initializers: [
function (options = {}) {
Object.keys(keyValueMap).forEach(key => {
if (typeof options[key] !== keyValueMap[key]) // checking
throw new Error(`"${key}" is missing`); // and throwing
});
}
]});
}
}});

And done!

Here it is working:

const SlackApi = compose(ArgumentChecker) // composing with it
.checkArguments({ // setting up the checker
team: 'string',
accessToken: 'string
})
.compose({initializers: [
function ({team, accessToken}) {
// In the next line we are 100% sure team and token were passed
console.log(`Team: ${team}, Access Token: ${accessToken}`);
}
]});
SlackApi(); // 'Error: "team" is missing'
SlackApi({team: 'Sheldon'}); // 'Error: "accessToken" is missing'
SlackApi({team: 'Sheldon', accessToken: 'TWFuIGlzIGRp'}); // OK !!!

Will print:

Team: Sheldon, Access Token: TWFuIGlzIGRp

The tricky part here is that the checking logic must be composed before the usage logic. We want the check-initializer invoked earlier than the use-initializer.

Optimizing chaining with the “configuration”

There is a light problem with the ArgumentChecker stamp above. Take this code for example:

const SlackApi = compose(..., ArgumentChecker)
.checkArguments({team: 'string'})
.checkArguments({accessToken: 'string'})
.checkArguments({apiVersion: 'number'});

It will add three initializers to the SlackApi stamp. Not a real problem actually, but we’ll solve it to demonstrate stamp powers. Below you’ll see how to make it a single initializer.

Reminder: each stamp carries its metadata (aka stamp descriptor) in the stamp.compose.* object.

Configuration

Some theory first. The practical code is further down. Stay with me!

Each stamp can have a configuration:

const SomeConfiguration = compose({configuration: {
some: 'data 1'
}});
const MoarConfiguration = compose({configuration: {
moar: 'data 2'
}});
const HaveBoth = compose(
SomeConfiguration, MoarConfiguration);
console.log(HaveBoth.compose.configuration); // accessing metadata

Will print:

{ some: ‘data 1’, moar: ‘data 2’ }

Also, stamps can have deeply merged configuration (using JavaScript deep object merging):

const SomeDeepConfiguration = compose({deepConfiguration: {
some: ['data']
}});
const MoarDeepConfiguration = compose({deepConfiguration: {
some: ['moar data']
}});
const HaveBoth = compose(
SomeDeepConfiguration, MoarDeepConfiguration);
console.log(HaveBoth.compose.deepConfiguration);

Will print:

{ some: [ 'data', 'moar data' ] }

Second argument of initializers

Each initializer can access the stamp which was invoked to create the object. This means that initializers have access to the stamp descriptor (metadata) via the stamp.compose.* object.

It’s in the second argument of any initializer out there (see specification).

const PrintMyConf = compose({initializers: [
function (options, {stamp}) { // second argument
const conf = stamp.compose.configuration;
console.log('Hey, look, here is my configuration:', conf);
}
]});
const ConfigurationX = compose({configuration: {X: 'I am X'}})
.compose(PrintMyConf);
ConfigurationX();

Will print the following:

Hey, look, here is my configuration: { X: 'I am X' }

Statics+Configuration=power

Let’s rewrite the ArgumentChecker to collect the keyValueMap inside the deepConfiguration.

const ArgumentChecker = compose({
staticProperties: {
checkArguments(keyValueMap) {
// deep merge all the pairs to the ArgumentChecker object
return this.compose({deepConfiguration: {
ArgumentChecker: keyValueMap
}});
}
},
initializers: [function (options = {}, {stamp}) {
// take the map of key-value pairs and iterate over it
const map = stamp.compose.deepConfiguration.ArgumentChecker;
Object.keys(map).forEach(key => {
if (typeof options[key] !== map[key])
throw new Error(`"${key}" is missing`);
});
}]
});

The usage is exactly the same:

const SlackApi = compose(ArgumentChecker)
.checkArguments({team: 'string'})
.checkArguments({accessToken: 'string'})
.checkArguments({apiVersion: 'number'})
.compose({initializers: [
function ({team, accessToken, apiVersion}) {
console.log(`${team}, ${accessToken}, v${apiVersion}`);
}
]});
SlackApi(); // 'Error: "team" is missing'
SlackApi({team: 'Sheldon'}); // 'Error: "accessToken" is missing'
SlackApi({team: 'Sheldon', accessToken: 'TWFuIGlzIGRp'});
// 'Error: "apiVersion" is missing'
const slackClient = SlackApi({ // all good !!!
team: 'Sheldon',
accessToken: 'TWFuIGlzIGRp',
apiVersion: 2
});

Will print:

Sheldon, TWFuIGlzIGRp, v2

Stampit v3

Stampit is the compose() function on steroids.

Let’s implement the same ArgumentChecker stamp using Stampit v3 handy API. Namely the .statics (aka staticProperties), .deepConf (aka deepConfiguration), and .init (aka initializers).

const ArgumentChecker = stampit() // <- creating a new empty stamp
.statics({ // statics
checkArguments(keyValueMap) {
return this.deepConf({ArgumentChecker: keyValueMap}); //deepConf
}
})
.init(function (options = {}, {stamp}) { // init
const map = stamp.compose.deepConfiguration.ArgumentChecker;
Object.keys(map).forEach(key => {
if (typeof options[key] !== map[key])
throw new Error(`"${key}" is missing`);
});
});

Shorter, isn’t it?

To blow your mind a little I’m going to say that .statics(), .deepConf(), and .init() are static functions themselves. ;) Stampit simply adds them without you asking.

Conclusion

We have just built a universal purpose stamp — ArgumentChecker. You can use it today with your stamps.

Statics is a useful multipurpose feature. The ways we compose stamps are limited only by our imagination.

--

--