-
Notifications
You must be signed in to change notification settings - Fork 1
Immutable
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.
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;
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;
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({});
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
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
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
}
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({});
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();
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'
});
// 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.
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'
});
// 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);
}
});
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'
});
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);
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);
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();