_
| |
| |___ _ __ ___ ___ ___
_ | / __| '_ ` _ \ / _ \ / _ \
| |__| \__ \ | | | | | (_) | (_) |
\____/|___/_| |_| |_|\___/ \___/
Jsmoo (JavaScript Minimalist Object Orientation), it's a library that allows you to define consistent Classes and Roles with a simple API. It's inpired for Perl libraries Moo and Moose, and also from Perl6. It provides type validation for attributes (isa
), presence validation (required
), defaults (default
), role composition (does
and Role
) and much more!.
If you want some slaides of a presentation about this lib, here you have the link
With npm:
$> npm install --save jsmoo
Without Jsmoo:
class Client extends Jsmoo {
constructor({name, surname, age = 18}) {
if (!name) throw new Error('... some error ...');
if (typeof name !== 'string') throw new Error('... some error ...');
if (typeof age !== 'number') throw new Error('... some error ...');
if (typeof surname !== 'string') throw new Error('... some error ...');
this.name = name;
this.surname = surname;
this.age = age;
}
fullName() {
return `${this.name} ${this.surname}`;
}
}
const client = new Client({name: 'Pepito', surname: 'Grillo'});
console.log(client.fullName());
// => Pepito Grillo
With Jsmoo:
import Jsmoo from 'jsmoo';
class Client extends Jsmoo {
fullName() {
return `${this.name} ${this.surname}`;
}
}
// Define the attributes and options
Client.has({
name: { is: 'rw', isa: 'string', required: true },
surname: { is: 'rw', isa: 'string' },
age: { is: 'rw', isa: 'number', default: 18 },
});
const client = new Client({name: 'Pepito', surname: 'Grillo'});
console.log(client.fullName());
// => 'Pepito Grillo'
The example without Jsmoo it's not the same of the one with Jsmoo, because to write the access validation is
is so much code :) but you get the point, no?
The module itself exports more than one module:
import Jsmoo, { Role, before, after } from 'jsmoo';
The way the Classes are initialized is with a plain Object, where the keys are the attributes defined on the has
.
If you define this function on you class, will be called before the initialization arguments are passed to the constructor, here you can redefine this arguements as you want, the return
from this function will be the ones the constructor will use to initialize the object.
Example:
class File extends from Jsmoo {
beforeInitialize(args) {
if (!args.extension) {
args.extension = args.filename.split('.')[-1];
}
return args;
}
}
File.has({
extension: { is: 'ro', isa: 'string', required: true },
filename: { is: 'ro', isa: 'string', required: true },
});
const file = new File({filename: 'photo.jpg'});
console.log(file.extension);
// => 'jpg'
If you define this function on you class, will be called after the initialization without any arguments here you have access to the this
of the Class.
Example:
class File extends from Jsmoo {
afterInitialize() {
console.log(this.filename);
}
}
File.has({
filename: { is: 'ro', isa: 'string', required: true },
});
const file = new File({filename: 'photo.jpg'});
// => 'photo.jpg'
You can use this function to register some callback or validation.
API: before(rootObject, beforeThis, beforeFunction)
The before
function is called before the specified function. The result of it is totally ignored, but you can throw an error to stop the execution if you need too.
import Jsmoo, { before } from 'jsmoo';
class Client extends from Jsmoo {
save() {
// Save the client (fake)
db.insert(this);
}
}
Client.has({
name: { is: 'rw', isa: 'string', predicate: 1},
surname: { is: 'rw', isa: 'string', predicate: 1},
});
before(Client.prototype, 'save', function() {
if (this.hasSurname() && !this.hasName()) {
throw new TypeError('Need name if surname');
}
});
const client = new Client({ surname: 'Grillo' });
client.save();
It's really useful to perform validations/callbacks, for example a before save will do something before calling the save function.
The after
function is called after the specified function, the result of it is totally ignored.
import Jsmoo, { after } from 'jsmoo';
class Client extends from Jsmoo {
save() {
// Save the client (fake)
db.insert(this);
}
}
Client.has({
name: { is: 'rw', isa: 'string' },
});
after(Client.prototype, 'save', function() {
Mailer.send(this)
});
const client = new Client({ surname: 'Grillo' });
client.save();
It's really useful to perform validations/callbacks, for example a after create will do something after the function create is called.
Has provides the core functionallity of this module, define the attributes of the Class as easy as possible as clear as possible. This method is a static
method of the Class that has extended from Jsmoo
.
It expects a Object as parameters and each key of this object will become an attribute of the class. The configuration of the attribute is the value of the attribute key.
Example:
class File extends from Jsmoo { }
File.has({
filename: { is: 'ro' }
});
This is the most basic configuration, the attributes filename
and his configuration { is: 'ro'}
.
It defines the accesability of the attribute, it's the only configuration REQUIRED on the attribute, it can have the following values:
rw
: The attribute can be setted with new values (Read Write)ro
: You can not change the value of this attribute (Read Only)
If you try to change a ro
attribute it will raise an error.
It defines the type of the attribute, it can have the following values:
string
orString
number
orNumber
array
orArray
boolean
orBoolean
object
orObject
Maybe[type]
validates the type but it wont throw error if it'sundefined
ornull
- Your types
- Custom validations
Each of this types is defined as string on the isa
except for the 'Custom validations' which are functions that validates the value. For custom validations each time a value es setted to the attribute it'll run this validations, the result of those is ignored, the only way to stop the execution is to throw an error.
Example:
class Client extends Jsmoo { }
function isEven(value) {
if (value % 2 !== 0) throw new Error('Not even value')
}
Client.has({
name: { is: 'rw', isa: 'string' },
age: { is: 'rw', isa: 'number' },
address: { is: 'rw', isa: 'object' },
valid: { is: 'rw', isa: 'boolean'},
city: { is: 'rw', isa: 'City' }, // Your types
even: { is: 'rw', isa: isEven }, // Your custom validation
number: { is: 'rw', isa: 'Maybe[number]' }, // Can be undefined or null
});
const city = new City();
const client = new Client({
name: 'Pepito',
age: 45,
address: {},
valid: true,
city: city,
even: 2,
});
It defines a default value of an attribute only if no one is given in the initialization, it can be a simple value or a function, the function has the this
context of the Class but if you try to access some attribute that it's also default, you may, or may not, get the value you expect, if you want this behavior you shoud define the attributte you want to access as lazy.
Example:
class Client extends Jsmoo {}
Client.has({
email: { is: 'rw' }
name: { is: 'rw', default() { return this.email.split('@')[0] },
valid: { is: 'rw', isa: 'boolean', default: true },
created: { is: 'rw', default() { return new Date }},
});
const client = new Client({
email: '[email protected]'
});
client.name // pepitogrillo
client.valid // true
client.created // Date
It describes the attribute as required
as a boolean value, which means that it must be (if true) one of the parameters on initialization time, if it's not present it will fail loudly.
Example:
class Client extends Jsmoo {}
Client.has({
name: { is: 'rw', required: true }
})
The attributes defined as lazy
will be instanciated only when the attribute is called.
Example:
class Client extends Jsmoo {}
Clint.has({
name: { is: 'rw', lazy: true }
});
This is useful in combination with default or builder because you can use it to catch heaby operations like DB queryies.
Created a function (has${attributeName}
if it start with _ then _has${attributeName}
) to validate if the value is defined, wich means the values is not undefined
or null
Example:
class Client extends Jsmoo {}
Clint.has({
name: { is: 'rw', predicate: true }
});
let obj = new Client({ name: 'value' });
obj.hasName()
// => true
obj.name = undefined;
obj.hasName()
// => false
Created a function (clear${attributeName}
if it start with _ then _clear${attributeName}
) to clear the value, which means removing the attribute from the internal store.
Example:
class Client extends Jsmoo {}
Clint.has({
name: { is: 'rw', clearer: true }
});
let obj = new Client({ name: 'value' });
obj.name
// => value
obj.clearName();
obj.name
// => undefined
Defines a function to build the attribute if not initialized, if it has a Boolean value it will call the function build${attributeName}
(if it start with _ then _build${attributeName}
) but you can override this by passing a string with the name of the builder function that you want, this function would have the this
context of the class.
Example:
class Client extends Jsmoo {}
Clint.has({
name: { is: 'rw', builder: true },
age: { is: 'rw', builder: 'buildAgeForUser' },
});
Client.prototype.buildAgeForUser = function() {}
Client.prototype.buildName = function() {}
This is very useful to use it with Role compositions and the role defined the builder and then the Ojbect with the Role has to define the custom implementation.
API: function(newValue, oldValue) {}
It creates a handle that will trigger after the attribute is setted. This includes the constructor but not default
ond builder
. This handle will recieve the oldValue
and the newValue
. It can be defined with a boolean value, in which case would call a function with the name of the attribute like this trigger${attributeName}
(if is starts with _ then _trigger${attributeName}
. Or it can be defined with a funciton.
Example:
class Client extends Jsmoo {}
Client.has({
name: { is: 'rw', trigger: 1 },
age: { is: 'rw', trigger: triggerForAge },
surname: { is: 'rw', trigger: trigger(newValue, oldValue) {} },
});
Client.prototype.triggerName = function (newValue, oldValue) { }
function triggerForAge (newValue, oldValue) { }
Very useful to register some kind of callbacks to some attributes after they change.
API: function(value) {}
It takes a function and coerce the attribute. Which means it may transform the value to another one.
Example:
class Client extends Jsmoo {}
function stringToNumber(value) {
if (typeof value === 'string') {
return value * 1;
}
return value
}
Client.has({
age: { is: 'rw', isa: 'number', coerce: stringToNumber },
});
const client = new Client({ age: '25' });
client.age
# 18
typeof client.age
# number
It's usefull to transform no objects to objects, or different types (string => integer)
It's the way to acomplish composition, there are some rules for Role composition:
- Only
Roles
can be composed. - Roles can
override
existing attributes with the+
sign. - Classes can
override
existing attributes with the sign+
sign. - If one of the overrided attributes is not declated (with has) before the declaration of the override it will fail loudly, basically if you try to
+name
andname
is not defiend by the Role then it fails.. - If a function is defined in the main Class, the Role will not override it.
The instance and class functions will be composed to the main Class and also the attributes defined with has
on the Role.
Example:
//------- address_role.js
import Jsmoo, { Role } from 'jsmoo';
class AddressRole extends Role {
static staticFunction() {
return 'static'
}
instanceFunction() {
return this.name
}
}
AddressRole.has({
address: { is: 'rw', default: 'C/ To Pepi' }
})
export default AddressRole;
//------- person.js
import Jsmoo, { Role } from 'jsmoo';
import AddressRole from './address_role';
class Person extends Jsmoo {}
Person.does(AddressRole)
Person.has({
name: { is: 'rw' },
'+address': { default: 'C/ Pepi To' },
})
Person.staticFunction()
// => 'static'
let person = new Person({ name: 'Pepito' })
person.instanceFunction()
// => 'Pepito'
person.address
// => 'C/ Pepi To'
This function is present in all the Classes extending of Jsmoo as a instance function, it returns all the attributes setted with has
of the Class:
Example;
class Client extends Jsmoo {}
Client.has({
name: { is: 'rw' },
age: { is: 'rw', default: 18 },
});
const client = new Client({
name: 'Pepito'
});
console.log(client.getAttribute());
// => { name: 'Pepito', age: 18 }
Roles are the way to achive composition, they are similar to the Jsmoo class but with some differences:
- They are the only ones that can be composed with
does
. - Roles can not be initialized.
Roles also have the has
static function to define attributes, wich then will be extended to the main Jsmoo Class.
They work the same way of a Jsmoo Class.
import Jsmoo, { Role } from 'jsmoo'
class Document extends Role {}
Document.has({
_id: { is: 'rw', isa: 'number' }
})
class Person extends Jsmoo {}
Person.with(Document)
const person = new Person({ _id: 23 })
console.log(person._id)
// => 23
The use of Role is up to you hehe, basically is to abstract some code that is used in other classes, like the logic to query or serialize to a DB (like a ORM)