modelico-immutable
v19.0.5
Published
Universal library for serialisable immutable models
Downloads
9
Maintainers
Readme
Modélico [moˈðe.li.ko] is a universal-JS library for serialisable immutable models.
Note: babel-polyfill might be required
for browsers other than Chrome, Firefox and Edge. Additionally, IE 9 & 10 also require
a getPrototypeOf
polyfill as the one in es5-sham.
See browser tests for more details.
Installation
npm i modelico
To use it in the browser, grab the minified or the development files.
Run the current tests directly on your target browsers to see what setup is right for you:
Quick intro
The goal is to parse JSON strings like the following into JavaScript custom objects
{
"name": "Robbie"
}
so that we can do things like this:
const pet1 = M.fromJSON(Animal, petJson);
pet1.speak(); //=> 'my name is Robbie!'
// pet1 will not be mutated
const pet2 = pet1.set('name', 'Bane');
pet2.name(); //=> 'Bane'
pet1.name(); //=> 'Robbie'
M.fromJSON
is a simpler way to do the following:
const { _ } = M.metadata;
const pet1 = JSON.parse(petJson, _(Animal).reviver);
Here is how Animal
would look like:
const M = require('modelico');
const { string } = M.metadata;
class Animal extends M.Base {
constructor(fields) {
super(Animal, fields);
}
speak() {
const name = this.name();
return (name === '') ?
`I don't have a name` :
`My name is ${name}!`;
}
static innerTypes() {
return Object.freeze({
name: string()
});
}
}
A more complex example
The previous example features a standalone class. Let's look at a more involved example that builds on top of that:
{
"givenName": "Javier",
"familyName": "Cejudo",
"pets": [
{
"name": "Robbie"
}
]
}
Notice that the data contains a list of pets (Animal
).
Again, our goal is to parse JSON into JavaScript classes to be able to do things like
const person1 = M.fromJSON(Person, personJson);
person1.fullName(); //=> 'Javier Cejudo'
person1.pets().inner()[0].speak(); //=> 'my name is Robbie!'
Note: pets() returns a Modelico.List
, hence the need to use .inner()
to grab the underlying array. See the proxies section
for a way to use methods and properties of the inner structure directly.
We are going to need a Person
class much like the Animal
class we have already defined.
const M = require('modelico');
const { _, string, list } = M.metadata;
class Person extends M.Base {
constructor(fields) {
super(Person, fields);
}
fullName() {
return [this.givenName(), this.familyName()].join(' ').trim();
}
static innerTypes() {
return Object.freeze({
givenName: string(),
familyName: string(),
pets: list(_(Animal))
});
}
}
A note on immutability
Following the examples above:
const person2 = person1.set('givenName', 'Javi');
// person2 is a clone of person with the givenName
// set to 'Javi', but person is not mutated
person2.fullName(); //=> 'Javi Cejudo'
person1.fullName(); //=> 'Javier Cejudo'
const person3 = person1.setPath(['pets', 0, 'name'], 'Bane');
person3.pets().inner()[0].name(); //=> 'Bane'
person1.pets().inner()[0].name(); //=> 'Robbie'
Optional / null values
In the examples above, a pet with a null
or missing name
would cause a
TypeError
while reviving.
const pet = M.fromJSON(Animal, '{"name": null}');
//=> TypeError: no value for key "name"
To support missing properties or null
values, you can declare the property
as a Maybe
:
const M = require('modelico');
const { string, maybe } = M.metadata;
class Animal extends M.Base {
// ... same as before
static innerTypes() {
return Object.freeze({
name: maybe(string())
});
}
}
Then, we can use it as follows:
const pet = M.fromJSON(Animal, '{"name": null}');
pet.name().isEmpty(); //=> true
pet.name().getOrElse('Bane'); //=> Bane
ES2015 proxies
Most built-in types in Modélico (List, Set, Map, EnumMap and Date)
are wrappers around native structures. By default, it is necessary to
retrieve those structures to access their properties and methods
(eg. list.inner().length
).
However, if your environment supports ES2015 proxies, Modélico provides utilities to get around this:
const M = Modelico;
const p = M.proxyMap;
const defaultMap = M.Map.fromObject({a: 1, b: 2, c: 3});
const proxiedMap = p(defaultMap);
// without proxies
defaultMap.inner().get('b'); //=> 2
defaultMap.inner().size; //=> 3
// with proxies
proxiedMap.get('b'); //=> 2
proxiedMap.size; //=> 3
Please note that native methods that modify the structure in place will instead return a new modelico object:
const proxiedMap2 = proxiedMap.delete('b');
proxiedMap.size; //=> 3 (still)
proxiedMap2.size; //=> 2
See proxy tests for more details.
ES5 classes
To support legacy browsers without transpiling, Modélico can be used
with ES5-style classes. In the case of our Animal
class:
(function(M) {
var m = M.metadata;
function Animal(fields) {
M.Base.factory(Animal, fields, this);
}
Animal.prototype = Object.create(M.Base.prototype);
Animal.prototype.constructor = Animal;
Animal.prototype.speak = function() {
var name = this.name();
return (name === '') ?
"I don't have a name" :
'My name is ' + name + '!';
};
Animal.innerTypes = function() {
return Object.freeze({
name: m.string()
});
}
}(window.Modelico));
Acknowledgments :bow:
Inspired by Immutable.js, Gson and initially designed to cover the same use cases as an internal Skiddoo tool by Jaie Wilson.