Skip to content
&y edited this page Dec 3, 2016 · 17 revisions

Immutable allows us to generate constructors that validate schema's, using Blueprint, and to render immutable objects.

im•mu•ta•ble (ĭ-myo͞oˈtə-bəl), adj. Not subject or susceptible to change.

When out objects are subject to change, side effects can cause problems. Making our objects immutable, and achieving the Open/Closed Principle in JavaScript can be verbose. polyn's Immutable makes this trivial.

Usage

Node

Immutable is part of the polyn package. To install it:

npm install --save polyn

Then you can require it like so:

var Immutable = require('polyn').Immutable;

Browser

Immutable is part of the polyn package. To install it, download the release folder, or:

bower install --save polyn

Then add a script tag:

<script src="polyn.min.js"></script>

Then it will be available on the window:

var Immutable = window.polyn.Immutable;

Validation

Immutable builds on Blueprint. All Blueprint validation features are avaiable to Immutables.

var Foo = new Immutable({
    num: 'number',
    str: 'string',
    arr: 'array',
    currency: 'money',
    bool: 'bool',
    date: 'datetime',
    regex: 'regexp',
    obj: 'object',
    func: {
        type: 'function',
        args: ['arg1', 'arg2']
    },
    dec: {
        type: 'decimal',
        places: 2
    },
    nullable: {
        type: 'string',
        required: false
    },
    custom: {
        validate: function (val, errors, self) {
            if (val !== 42) {
                errors.push('custom must be 42');
            }            
        }
    }
});

Given the constructor above, the following would pass validation and return an immutable object:

var foo = new Foo({
    num: 42,
    str: 'string',
    arr: [],
    currency: '42.42',
    bool: true,
    date: new Date(),
    regex: /[A-B]/,
    obj: {
        foo: 'bar'
    },
    func: function (arg1, arg2) {},
    dec: 42.42,
    custom: 42
});

However, the next example would return an exception object:

var foo = new Foo({});

Lazy validation

If for some reason, you don't want the Immutable to be validated upon construction, you can defer or omit validation, by setting the __skipValdation property:

var Person = new Immutable({
    name: 'string',
    __skipValdation: true
});

// Now this will not generate an error
var person = new Person({});

If you wish to validate the Immutable later, you can use the validate feature:

var Person = new Immutable({
    name: 'string',
    __skipValdation: true
});

var person = new Person({});

// You can validate asynchronously
Person.validate(person, function (errors, result) {
    console.log('errors:', result.errors); // prints errors: ['...']
    console.log('result:', result.result); // prints result: false
});

// Or synchronously
var result = Person.validate(person);
console.log('errors:', result.errors); // prints errors: ['...']
console.log('result:', result.result); // prints result: false

Single Property Validation

You can also check validation on single properties, using validateProperty, for when it doesn't make sense to validate an entire object.

var Person = new Immutable({
    name: 'string',
    __skipValdation: true
});

var person = new Person({});

// You can validate asynchronously
Person.validateProperty(person, 'name', function (errors, result) {
    console.log('errors:', result.errors); // prints errors: ['...']
    console.log('result:', result.result); // prints result: false
});

// Or synchronously
var result = Person.validateProperty(person, 'name');
console.log('errors:', result.errors); // prints errors: ['...']
console.log('result:', result.result); // prints result: false

Exceptions

When an error occurs, or validation fails, Immutable returns an exception object. It's easy to check for the occurrence for an error, by using the exceptions properties:

{
    type: 'InvalidArgumentException', // types vary by exception
    error: Error('hello world!'),
    messages: ['validation error 1', 'validation error 2'],
    isException: true
}

Here's an example that checks for the existence of an error:

var Person = new Immutable({
    name: 'string'
});

var person = new Person({ name: 'Andy' });

if (person.isException) {
    // do something with the exception
}

Handling Exceptions

Immutable has a hook for exceptions. By default, exceptions are written to the console. You can override this behavior with whatever you want, using configure.

Immutable.configure({
    inError(function (err) {
        throw err.error;
    });
});

var Person = new Immutable({
    name: 'string'
});

// this would throw
var person = new Person({});

Functions and Behaviors

If our models have behaviors (functions), we might not want to expose the Immutable itself, in favor of wrapping it. Here's an example:

var Person = (function () {
    var Ctor, Person;

    Ctor = new Immutable({
        firstName: 'string',
        lastName: 'string',
        sayHello: 'function'
    });

    Person = function (person) {
        person = person || {};
        person.sayHello = function () {
            var greeting = 'Hello, {{first}} {{last}}';
            console.log(greeting.replace(/{{first}}/, this.firstName).replace(/{{last}}/, this.lastName));
        };

        return new Ctor(person);
    };

    return Person;    
}());

var person = new Person({
    firstName: 'Zaphod',
    lastName: 'Beeblebrox'
});

person.sayHello();

Dealing with Mutation

Just because we have an Immutable object doesn't mean we can't change the values of properties. We just can't change them on a given reference. When we need to modify an Immutable object, we can merge it with another object, to create a new Immutable:

var Person = new Immutable({
    firstName: 'string',
    lastName: 'string',
    hasTwoHeads: 'boolean'
});

// creates an instance of Person, with the
// firstName Zaphod, lastName Beeblebrox, and hasTwoHeads equal to false
var zaphod1 = new Person({ 
    firstName: 'Zaphod',
    lastName: 'Beeblebrox',
    hasTwoHeads: false 
});

// creates another instance of Person, with the
// firstName Zaphod, lastName Beeblebrox, and hasTwoHeads equal to true
var zaphod2 = Person.merge(zaphod1, { hasTwoHeads: true });

// Or asynchronously
Person.merge(zaphod1, { hasTwoHeads: true }, function (err, zaphod2) {
    if (!err) {
        console.log(zaphod2);
    }
});

This way, any code that references zaphod1 will experience no side-effects when we change values. We end up with two separate objects.

Getting Plain Old Objects

Sometimes we need a plain old Object. Perhaps we need to update several properties of an Immutable. To do this, we can use the toObject feature:

var Person = new Immutable({
    firstName: 'string',
    lastName: 'string',
    hasTwoHeads: 'boolean'
});

// creates an instance of Person, with the
// firstName Zaphod, lastName Beeblebrox, and hasTwoHeads equal to false
var zaphod1 = new Person({ 
    firstName: 'Zaphod',
    lastName: 'Beeblebrox',
    hasTwoHeads: false 
});

// creates a plain old JavaScript Object with the
// Immutable's properties and values
var updated = Person.toObject(zaphod1);
updated.hasTwoHeads = true;
var zaphod2 = new Person(updated);

// OR asynchronously
Person.toObject(zaphod1, function (err, updated) {
    if (!err) {
        updated.hasTwoHeads = true;
        var zaphod2 = new Person(updated);        
    }
});

Debugging, and Logging to the Console

When logging Immutables to the console, the property values are displayed as [Getter/Setter]. This is not terribly helpful when you're debugging. When you need to see the actual values that are set, use the log feature:

var Person = new Immutable({
    name: 'string'
});

var person = new Person({ name: 'Andy' });

// Prints { name: 'Andy' } to the console
Person.log(person);

It can be difficult to tell where our error messages are coming from, if the property names aren't unique within our app. We can use the __blueprintId to help with that. By setting it to something we understand, all validation error messages will indicate what Blueprint/Immutable they were generated by.

var Person = new Immutable({
    __blueprintId: 'Person',
    name: 'string'
});

Nested Immutables

Like Blueprints, Immutables can be nested. Child objects are converted to Immutables automatically, although you can define them yourself, if you wish.

var Person = new Immutable({
    name: 'string',
    address: {
        street1: 'string',
        street2: { type: 'string', required: false },
        city: 'string',
        stateOrProvince: 'string',
        postalCode: 'string',
        country: 'string'
    }
});
var Person = new Immutable({
    name: 'string',
    address: new Immutable({
        street1: 'string',
        street2: { type: 'string', required: false },
        city: 'string',
        stateOrProvince: 'string',
        postalCode: 'string',
        country: 'string'
    })
});

Nested Immutables can be accessed by the PascalCased version of their property name:

var Person = new Immutable({
    name: 'string',
    address: {
        street1: 'string'
    }
});

var person = new Person({
    name: 'Trillian',
    address: {
        street1: 'Earth'
    }
});

Person.Address.log(person.address);

Schemas

With Immutables, and Blueprints, the schema is the object that we pass to the Immutable or Blueprint constructor. It's the object that tells us that a given property is a string, and another is a number.

Immutables expose their schema through the getSchema function. The schema that is returned is a copy, not a reference, so it is safe to manipulate it.

They also expose their Blueprint through the blueprint property. Note that the blueprint, and it's properties are themselves immutable.

var Person = new Immutable({
    firstName: 'string',
    lastName: 'string'
});

// prints { firstName: 'string', lastName: 'string' }
console.log(Person.getSchema());

// Or asynchronously
Person.getSchema(function (err, schema) {
    console.log(schema);
});

// prints { firstName: [Getter/Setter] ('string'), lastName: [Getter/Setter] ('string') }
console.log(Person.blueprint.props);

Using Schema's for Inheritance

The schema's can be used to achieve sub-type polymorphism. In the following example, we create a Person constructor. Then, by extending the Person schema, we create an Author constructor, which essentially inherits the schema of Person.

var Person = new Immutable({
    firstName: 'string',
    lastName: 'string'
});

var authorSchema = Person.getSchema();
authorSchema.books = 'array';

var Author = new Immutable(authorSchema);

If our constructor should export behaviors (functions), we might want to approach this a different way. There are many ways to approach this. This example wraps the Immutable constructors, so they can

var people = (function () {
    var PersonCtor, authorSchema, AuthorCtor, BasePerson, Person, Author;

    PersonCtor = new Immutable({
        firstName: 'string',
        lastName: 'string',
        type: 'string',
        sayHello: 'function'
    });

    authorSchema = PersonCtor.getSchema();
    authorSchema.books = 'array';

    var AuthorCtor = new Immutable(authorSchema);

    BasePerson = function (person, type, Ctor) {
        person = person || {};
        person.type = type;
        person.sayHello = function () {
            var greeting = 'Hello, {{first}} {{last}}';
            console.log(greeting.replace(/{{first}}/, this.firstName).replace(/{{last}}/, this.lastName));
        };

        return new Ctor(person);
    };

    Person = function (person) {
        return BasePerson(person, 'PERSON', PersonCtor);
    }

    Author = function (author) {
        return BasePerson(author, 'AUTHOR', AuthorCtor);
    }

    return {
        Person: Person,
        Author: Author
    };    
}());

var person, author;

person = new people.Person({
    firstName: 'Zaphod',
    lastName: 'Beeblebrox'
});

author = new people.Author({
    firstName: 'Zaphod',
    lastName: 'Beeblebrox',
    books: ['The Hitchhiker\'s Guide to the Galaxy']
});

person.sayHello();
author.sayHello();