Skip to content

Latest commit

 

History

History
545 lines (469 loc) · 17.9 KB

ADVANCED_EXAMPLES.md

File metadata and controls

545 lines (469 loc) · 17.9 KB

This is a set of handy composable stamps, as well as few tips and tricks.

Table of Contents

Self aware objects - instance.getStamp(). 2 ways.

Run the examples below for yourself:

$ git clone https://github.com/stampit-org/stampit.git
$ cd stampit && npm i babel -g
$ babel-node advanced-examples/self-aware.js

You can add .getStamp() function to each object with ease.

First, let's assume you have a following stamp:

const User = stampit();

Attach function to object

Just compose the following stamp to any other stamp.

const SelfAware = stampit.init(({ instance, stamp }) => {
  instance.getStamp = () => stamp;
});

Let's compose it with the User stamp from above:

const SelfAwareUser = User.compose(SelfAware);

Now, let's create a user and call the .getStamp():

const user = SelfAwareUser();
assert.strictEqual(user.getStamp(), SelfAwareUser); // All good

So, now every object instance returns the exact stamp it was built with. Nice!

Attach function to prototype (memory efficient)

Another composable stamp which does the same but in a memory efficient way. It attaches the function to the .prototype of the objects, but not to each one.

const SelfAware = stampit.init(({ instance, stamp }) => {
  if (!stamp.fixed.methods.getStamp) { // Avoid adding the same method to the prototype twice.
    stamp.fixed.methods.getStamp = () => stamp;
  }
});

The stamp.fixed property contains stamp's internal data. The stamp.fixed.methods object is used as all object instances' .prototype.

Compose this new stamp with our User from above:

const SelfAwareUser = User.compose(SelfAware);

Let's test it:

const user = SelfAwareUser();
assert.strictEqual(user.getStamp(), SelfAwareUser); // All good

And again, every new object instance knows which stamp it was made of. Brilliant!


Self cloneable objects - instance.clone(). 3 ways.

Run the examples below for yourself:

$ git clone https://github.com/stampit-org/stampit.git
$ cd stampit && npm i babel -g
$ babel-node advanced-examples/cloneable.js

This is a simple stamp with a single method and a single state property prefix.

const PrependLogger = stampit.methods({
  log(obj) {
    console.log(this.prefix, obj);
  }
}).refs({
  prefix: 'STDOUT: '
});

Using it:

const originalLogger = PrependLogger();
originalLogger.log('hello');

Prints STDOUT: hello

Attach method to each object instance

Let's implement a stamp which allows any object to be safely cloned:

const Cloneable = stampit.init(({ instance, stamp }) => {
  instance.clone = () => stamp(instance);
});

All the properties of the object instance will be copied by reference to the new object when calling the factory - stamp(instance).

Compose it with our PrependLogger from above:

const CloneablePrependlogger = PrependLogger.compose(Cloneable);

Let's create an instance, then clone it, and see the result:

const logger = CloneablePrependlogger({ prefix: 'OUT: ' }); // creating first object
const loggerClone = logger.clone(); // cloning the object.
logger.log('hello'); // OUT: hello
loggerClone.log('hello'); // OUT: hello

Prints

OUT: hello
OUT: hello

The logger and loggerClone work exactly the same. Woah!

Bind method to each object instance

This is how you can implement self cloning different:

const Cloneable = stampit.init(({ instance, stamp }) => {
  instance.clone = stamp.bind(null, instance);
});

Stamp is a regular function, so we simply bound its first argument to the object instance. All the properties of the object instance will be copied by reference to the new object.

Composing it with our PrependLogger from above:

const CloneablePrependlogger = PrependLogger.compose(Cloneable);

Create an instance, then clone it, and see the result:

const logger = CloneablePrependlogger({ prefix: 'OUT: ' }); // creating first object
const loggerClone = logger.clone(); // cloning the object.
logger.log('hello'); // OUT: hello
loggerClone.log('hello'); // OUT: hello

Prints

OUT: hello
OUT: hello

Objects have the same state again. Awesome!

Attach function to prototype (memory efficient)

Let's reimplement the Cloneable stamp so that the clone() function is not attached to every object but to the prototype. This will save us a little bit of memory per object.

const Cloneable = stampit.init(({ instance, stamp }) => {
  if (!stamp.fixed.methods.clone) { // Avoid adding the same method to the prototype twice.
    stamp.fixed.methods.clone = function () { return stamp(this); };
  }
});

The stamp.fixed property contains stamp's internal data. The stamp.fixed.methods object is used as all object instances' .prototype. Compose this new stamp with our PrependLogger from above:

const CloneablePrependlogger = PrependLogger.compose(Cloneable);

Let's see how it works:

const logger = CloneablePrependlogger({ prefix: 'OUT: ' });  // creating first object
const loggerClone = logger.clone(); // cloning the object.
logger.log('hello'); // OUT: hello
loggerClone.log('hello'); // OUT: hello

Prints

OUT: hello
OUT: hello

Memory efficient and safe cloning for each object. Yay!


Delayed object instantiation using Promises

Run the examples below for yourself:

$ git clone https://github.com/stampit-org/stampit.git
$ cd stampit && npm i babel -g
$ babel-node advanced-examples/delayed-instantiation.js

What if you can't create an object right now but have to retrieve data from a server or filesystem?

To solve this we can make any stamp to return Promise instead of object itself.

First, let's assume you have this stamp:

const User = stampit.refs({ entityName: 'user' });

When combined the following stamp will make any existing stamp return a promise instead of an object instance. The promise will always resolve to an object instance.

const AsyncInitializable = stampit.refs({
  db: { user: { getById() { return Promise.resolve({ name: { first: 'John', last: 'Snow' }}) } } } // mocking a DB
}).methods({
  getEntity(id) { // Gets id and return Promise which resolves into DB entity.
    return Promise.resolve(this.db[this.entityName].getById(id));
  }
}).init(function () {
  // If we return anything from an .init() function it becomes our object instance.
  return this.getEntity(this.id);
});

Let's compose it with our User stamp:

const AsyncInitializableUser = AsyncInitializable.compose(User); // The stamp produces promises now.

Create object (ES6):

const userEntity = AsyncInitializableUser({ id: '42' }).then(console.log);

A random stamp received the behavior which creates objects asynchronously. OMG!


Dependency injection tips

Using the node-dm dependency management module you can receive preconfigured objects if you pass a stamp to it. It's possible because stamps are functions.

Self printing behavior. An object will log itself after being created.

const PrintSelf = stampit.init(({ instance }) => {
  console.log(instance);
});

Supply the self printing stamp to the dependency manager:

dm.resolve({db: true, config: true, pi: true}).then(PrintSelf);

Will print all the properties passed to it by the dependency manager module:

{ db: ...
  config: ...
  pi: ... }

Validate before a function call

Run the examples below for yourself:

$ git clone https://github.com/stampit-org/stampit.git
$ cd stampit && npm i babel -g
$ npm i joi
$ babel-node advanced-examples/prevalidate.js

For example you can prevalidate an object instance before a function call.

First, let's assume you have this stamp:

const User = stampit.methods({
  authorize() {
    // dummy implementation. Don't bother. :)
    return this.authorized = (this.user.name === 'john' && this.user.password === '123');
  }
});

It requires the user object to have both name and password set.

Now, let's implement a stamp which validates a state just before a function call.

const JoiPrevalidator = stampit
  .static({ // Adds properties to stamps, not object instances.
    prevalidate(methodName, schema) {
      var prevalidations = this.fixed.refs.prevalidations || {}; // Taking existing validation schemas
      prevalidations[methodName] = schema; // Adding/overriding a validation schema.
      return this.refs({prevalidations}); // Cloning self and (re)assigning a reference.
    }
  })
  .init(function () { // This will be called for each new object instance.
    _.forOwn(this.prevalidations, (value, key) => { // overriding functions
      const actualFunc = this[key];
      this[key] = () => { // Overwrite a real function with ours.
        const result = joi.validate(this, value, {allowUnknown: true});
        if (result.error) {
          throw new Error(`Can't call ${key}(), prevalidation failed: ${result.error}`);
        }

        return actualFunc.apply(this, arguments);
      }
    });
  });

Note, you can validate anything in any way you want and need.

Compose the new validator stamp with our User stamp:

const UserWithValidation = User.compose(JoiPrevalidator) // Adds new method prevalidate() to the stamp.
  .prevalidate('authorize', { // Setup a prevalidation rule using our new "static" function.
    user: { // Joi schema.
      name: joi.string().required(),
      password: joi.string().required()
    }
  });

Let's try it:

const okUser = UserWithValidation({user: {name: 'john', password: '123'}});
okUser.authorize(); // No error. Validation successful.
console.log('Authorized:', okUser.authorized);

const throwingUser = UserWithValidation({user: {name: 'john', password: ''}});
throwingUser.authorize(); // will throw an error because password is absent

Will print Authorized: true and then an error stack. The code throws an error because the password is missing.

You can replace joi validation logic with strummer or is-my-json-valid or any other module.

So, now you have a composable behavior to validate any function just before it's called. Incredible!


EventEmitter without inheritance (convertConstructor)

Run the examples below for yourself:

$ git clone https://github.com/stampit-org/stampit.git
$ cd stampit && npm i babel -g
$ babel-node advanced-examples/event-emitter.js

You can have a stamp which makes any object an EventEmitter without inheriting from it.

const EventEmitter = require('events').EventEmitter;
const EventEmittable = stampit.convertConstructor(EventEmitter);

We have just used a special utility function convertConstructor. It converts classic JavaScript "classes" to a composable stamp.

Let's compose it with any other stamp:

const User = stampit.refs({ name: { first: "(unnamed)", last: "(unnamed)" } });
const EmittableUser = User.compose(EventEmittable);
const user = EmittableUser({ name: { first: "John", last: "Doe" } });

Now, let's subscribe and emit an event.

user.on('name', console.log); // Does not throw exceptions, e.g. "'user' has no method 'on'"
user.emit('name', user.name); // correctly handled by the object.

Will print { first: "John", last: "Doe" }.


Mocking in the unit tests

Run the examples below for yourself:

$ git clone https://github.com/stampit-org/stampit.git
$ cd stampit && npm i babel -g
$ babel-node advanced-examples/mocking.js

Consider the following stamp composition:

const NewStamp = AStamp.compose(FirstStamp, SecondsStamp);

Last composed stamp always wins. This means that SecondStamp will override methods of the AStamp and FirstStamp if case of conflicts. Let's use this feature to override DB calls with mock functions.

Define few following stamps:

/**
 * Implements convertOne() method for future usage.
 */
const DbToApiCommodityConverter = stampit.methods({
  convertOne(entity) {
    var keysMap = {_id: 'id'};
    return _.mapKeys(_.pick(entity, ['category', '_id', 'name', 'price']), (v, k) => keysMap[k] || k);
  }
});

/**
 * Abstract converter. Implements convert() which does argument validation and can convert both arrays and single items.
 * Requires this.convertOne() to be defined.
 */
const Converter = stampit.methods({
  convert(entities) {
    if (!entities) {
      return;
    }

    if (!Array.isArray(entities)) {
      return this.convertOne(entities);
    }

    return _.map(entities, this.convertOne);
  }
});

/**
 * Database querying implementation: findById() and find()
 * Requires this.schema to be defined.
 */
const MongoDb = stampit.methods({
  findById(id) {
    return this.schema.findById(id);
  },

  find(params) {
    return this.schema.find(params);
  }
});

Okay, let's define few business logic functions to retrieve data from the DB using the stamps above:

/**
 * The business logic. Defines getById() and search() which query DB and convert data with this.convert().
 * Requires this.convert(), this.findById(), and this.find() to be defined.
 */
const Commodity = stampit.methods({
  getById(id) {
    return this.findById(id).then(this.convert.bind(this));
  },

  search(fields = {price: {from: 0, to: Infinity}}) {
    return this.find({category: fields.categories, price: {gte: fields.price.from, lte: fields.price.to}})
      .then(this.convert.bind(this));
  }
}).compose(Converter, DbToApiCommodityConverter, MongoDb); // Adding the missing behavior

The usage is quite straightforward.

const commodity = Commodity({
  schema: MongooseCommoditySchema
});

commodity.getById(42).then(console.log);
commodity.find({categories: 'kettle', price: {from: 0, to: 20}}).then(console.log);

Finally, the mocking! All we need to do is to have a stamp with the findById() and find() methods.

const _mockItem = {category: 'kettle', _id: 42, name: 'Samsung Kettle', price: 4.2};
const FakeDb = stampit.methods({
  findById(id) { // Mocking the DB call
    return Promise.resolve(_mockItem);
  },
  find(params) { // Mocking the DB call
    return Promise.resolve([_mockItem]);
  }
});

Let's test.

const MockedCommodity = Commodity.compose(FakeDb);

const commodity = MockedCommodity();
commodity.getById().then(data => {
  assert.equal(data.category, _mockItem.category);
  assert.equal(data.id, _mockItem._id);
  assert.equal(data.name, _mockItem.name);
  assert.equal(data.price, _mockItem.price);
  console.log('getById works!');
}).catch(console.error);
commodity.search().then(data => {
  assert.equal(data.length, 1);
  assert.equal(data[0].category, _mockItem.category);
  assert.equal(data[0].id, _mockItem._id);
  assert.equal(data[0].name, _mockItem.name);
  assert.equal(data[0].price, _mockItem.price);
  console.log('search works!');
}).catch(console.error);

Do you see the idea? The reusable DB mock can be attached to any behavior. Fantastic!


Hacking stamps

Each stamp has the property fixed. It's an object with 4 properties. It's used by stampit in the following order:

  • Stamp.fixed.methods - plain object. Stampit uses it to set new objects' prototype: Object.create(fixed.methods).
  • Stamp.fixed.refs - plain object. Stampit uses it to set new objects' state: _.assign(obj, fixed.refs).
  • Stamp.fixed.props - plain object. Stampit deeply merges it into new objects: _.merge(obj, fixed.props).
  • Stamp.fixed.init - array of functions. Stampit calls them sequentially: fixed.init.forEach(fn => fn.call(obj)).

Run the examples below for yourself:

$ git clone https://github.com/stampit-org/stampit.git
$ cd stampit && npm i babel -g
$ babel-node advanced-examples/hacking.js

Enforced default properties

You can add non-removable "default" state by changing Stamp.fixed.methods. I.e. you can modify object instances' .prototype.

const Stamp = stampit();
Stamp.fixed.methods.data = 1; // fixed.methods is the prototype for each new object.
const instance = Stamp(); // Creating object, it's prototype is set to fixed.methods. It has property 'data'.
console.log(instance.data); // 1

Will print 1. But let's add some state:

const instance2 = Stamp({ data: 2 }); // Creating second object. It'll have property 'data' too.
console.log(instance2.data); // 2

Will print 2. But let's delete this property from the instance.

delete instance2.data; // Deleting 'data' assigned to the instance.
console.log(instance2.data); // 1 <- The .prototype.data is still there.

Will print 1. The data was removed from the object instance, but not from its prototype.