-
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.
NOTE: At present, the contents of arrays are not Immutable. Consider using Immutable.js for arrays, and maps.
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({
onError(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',
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.
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);
}
});
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();
This package has not yet undergone performance analysis. It's usually fine for small data structures. If your app needs large arrays or maps of immutable data, consider using Immutable.js for that.