Skip to content

xescugc/jsmoo

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Build Status

                         _                           
                        | |                          
                        | |___ _ __ ___   ___   ___  
                    _   | / __| '_ ` _ \ / _ \ / _ \ 
                   | |__| \__ \ | | | | | (_) | (_) |
                    \____/|___/_| |_| |_|\___/ \___/ 
                                                     
                                                     

Jsmoo

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

Installation

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?

API

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.

beforeInitialize

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'

afterInitialize

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.

before

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.

after

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

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'}.

is

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.

isa

It defines the type of the attribute, it can have the following values:

  • string or String
  • number or Number
  • array or Array
  • boolean or Boolean
  • object or Object
  • Maybe[type] validates the type but it wont throw error if it's undefined or null
  • 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,
  });

default

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

required

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 }
  })

lazy

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.

predicate

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

clearer

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

builder

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.

trigger

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.

coerce

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)

does

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 and name 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'

getAttributes

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 }

Role

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)

About

JavaScript Minimalist Object Orientation

Topics

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published