stampit-org / stamp-specification

The Stamp Specification: Composables
434 stars 16 forks source link

Stamp Specification v1.6

Gitter Greenkeeper Badge

Introduction

This specification exists in order to define a standard format for composable factory functions (called stamps), and ensure compatibility between different stamp implementations.

Status

The specification is currently used by the following officially supported implementations:

Reading Function Signatures

This document uses the Rtype specification for function signatures:

(param: Type) => ReturnType

Composable

interface Composable: Stamp|Descriptor

A composable is one of:

Stamp

A stamp is a composable factory function that returns object instances based on its descriptor.

stamp(options?: Object, ...args?: [...Any]) => instance: object
const newObject = stamp();

Stamps have a method called .compose():

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

When called the .compose() method creates new stamp using the current stamp as a base, composed with a list of composables passed as arguments:

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.

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() pure function (optional)

(...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.

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

Implementation details

Stamp

Stamp(options?: Object, ...args?: [...Any]) => Instance: Object

Creates object instances. Take an options object and return the resulting instance.

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

Creates stamps.

A method exposed by all stamps, identical to compose(), except it prepends this to the stamp parameters. Stamp descriptor properties are attached to the .compose method, e.g. stamp.compose.methods.

The Stamp Descriptor

interface Descriptor {
  methods?: Object,
  properties?: Object,
  deepProperties?: Object,
  propertyDescriptors?: Object,
  staticProperties?: Object,
  staticDeepProperties?: Object,
  staticPropertyDescriptors?: Object,
  initializers?: [...Function],
  composers?: [...Function],
  configuration?: Object,
  deepConfiguration?: Object
}

The names and definitions of the fixed properties that form the stamp descriptor. The stamp descriptor properties are made available on each stamp as stamp.compose.*

Composing Descriptors

Descriptors are composed together to create new descriptors with the following rules:

Copying by assignment

The special property assignment algorithm shallow merges the following properties:

Deep merging

Special deep merging algorithm should be used when merging descriptors.

Values:

Keys:

Priority Rules

It is possible for properties to collide, between both stamps, and between different properties of the same stamp. This is often expected behavior, so it must not throw.

Same descriptor property, different stamps: Last in wins.

Different descriptor properties, one or more stamps:

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:

const db = dbStamp({
  host: 'localhost',
  port: 3000,
  onConnect() {
    console.log('Database connection established.');
  }
});

const queue = messageQueueStamp({
  host: 'localhost',
  port: 5000,
  onComplete() {
    console.log('Message queue connection established.');
  }
});

If you tried to compose these directly, they would conflict with each other, but it's easy to namespace the options at compose time:

const DbQueue = compose({
  initializers: [({db, queue}, { instance }) => {
    instance.db = dbStamp({
      host: db.host,
      port: db.port,
      onConnect: db.onConnect
    });
    instance.queue = messageQueueStamp({
      host: queue.host,
      port: queue.port,
      onConnect: queue.onConnect
    });
  }]
});

myDBQueue = DbQueue({
  db: {
    host: 'localhost',
    port: 3000,
    onConnect () {
      console.log('Database connection established.');
    }
  },
  queue: {
    host: 'localhost',
    port: 5000,
    onConnect () {
      console.log('Message queue connection established.');
    }
  }
});

Initializer Parameters

Initializers have the following signature:

(options: Object, { instance: Object, stamp: Stamp, args: Array }) => instance?: Object

Note that if no options object is passed to the factory function, an empty object will be passed to initializers.

Composer Parameters

Composers have the following signature:

({ stamp: Stamp, composables: Composable[] }) => stamp?: Object

Note that it's not recommended to return new stamps from a composer. Instead, it's better to mutate the passed stamp.


Similarities With Promises (aka Thenables)


Contributing

To contribute, please refer to the Contributing guide.