0.2.14 • Published 3 months ago

carnaval v0.2.14

Weekly downloads
4
License
Apache-2.0
Repository
github
Last release
3 months ago

Carnaval

Minimalist, zero dependency, encoding / decoding class ↔ json for node.

class Friend {
    constructor(data) {
        Object.assign(this, data);
    }
    get props() {
        return {
            name: String,
            age: Number
        };
    }
}

const Mapping = require('carnaval').Mapping;
const mapping = Mapping.map(Friend);

mapping.decode({name: 'Joe', age: 27}).then(friend => { .. });
mapping.encode(new Friend({name: 'Joe', age: 27})).then(json => { .. });

Installation

This is a node module available through the npm registry. Before installing, download and install Node.js 6.0.0 or higher.

$ npm install carnaval

Features

  • Encode & decode objects with a constructor / class
  • Handle deep objects and arrays
  • Configurable through middlewares & providers
  • Options to mask / merge instances for update scenarios
  • Provide a domain class to inherit from

Usage

Props

To properly encode / decode / mask a class, this class should list its attributes through a props literal. One key per attribute (ex. name), one value for this attribute type (ex. String). Types should be function / constructor, their names are relevant for deep encoding / decoding.

Mapping

A mapping defines the way a json literal will be mapped to a class object (with decode) or mapped from it (with encode). In both cases, this returns a Promise. By default, every class' props will be duplicated (deep copy).

const mapping = Mapping.map(Friend);

mapping.decode({name: 'Joe', age: 27}).then(friend => { /* new Friend({name: 'Joe', age: 27}) */ });
mapping.encode(new Friend({name: 'Joe', age: 27})).then(json => { /* {name: 'Joe', age: 27} */ });

A mapping can hide some props of its target class on encode, decode, or both.

const mapping = Mapping.map(Friend).with({
    size: {set: false}
});

mapping.decode({name: 'Joe', age: 27}).then(friend => { /* new Friend({name: 'Joe'}) */ });
const mapping = Mapping.map(Friend).with({
    size: {get: false}
});

mapping.encode(new Friend({name: 'Joe', age: 27})).then(json => { /* {name: 'Joe'} */ });

A mapping can transform some props; set is called when decoding to a class object, get is called when encoding from it. Both take the prop value as first parameter, and the full decoded / encoded object as second parameter.

const mapping = Mapping.map(Friend).with({
    name: {
        set: (value, json) => value && value.trim(),
        get: (value, object) => value && value.toUpperCase()
    }
});

mapping.decode({name: ' Joe '}).then(friend => { /* new Friend({name: 'Joe'}) */ });
mapping.encode(new Friend({name: 'Joe'})).then(friend => { /* {name: 'JOE'} */ });

Renaming properties

A mapping can rename some props of its target class.

const mapping = Mapping.map(Friend).with({
    name: {alias: 'firstName'}
});

mapping.decode({firstName: 'Joe', age: 27}).then(friend => { /* new Friend({name: 'Joe'}) */ });
mapping.encode(new Friend({name: 'Joe', age: 27})).then(json => { /* {firstName: 'Joe'} */ });

Deep properties and arrays

Sub objects are handled automatically (as literal or as class instance). Array types are handled though a type in brackets (ex. [String]).

class Group {
    constructor(data) {
        Object.assign(this, data);
    }
    get props() {
        return {
            rights: {public: Boolean},
            owner: Friend,
            tags: [String]
        };
    }
}

const mapping = Mapping.map(Group);

mapping.decode({
    rights: {public: true},
    owner: {name: 'Joe', age: 27}}
    tags: ['music', 'culture']
)).then(group => {
    // new Group({
    //     rights: {public: true},
    //     owner: new Friend({name: 'Joe', age: 27}),
    //     tags: ['music', 'culture']
    // }
});

Sub properties can also be transformed.

const mapping = Mapping.map(Group).with({
    owner: {
        name: {
            set: value => value && value.toUpperCase()
        }
    },
    tags: {
        get: value => value && value.toUpperCase()
    }
});

mapping.decode({owner: {name: 'Joe'}, tags: ['music']).then(owner => { /* new Group({owner: new Friend({name: 'JOE'}), tags: ['music']})) */ });
mapping.encode(new Group({owner: new Friend({name: 'Joe'}, tags: ['music'])).then(json => { /* {owner: {name: 'Joe'}, tags: ['MUSIC']} */ });

Middlewares

Encoding and decoding transforms can be supplemented by middlewares beforeEncode, beforeDecode, afterEncode and afterDecode. For example, decoding json may need a validation or a query to a database while encoding an object to json may add some formatted property.

const validate = object => {
    if (object.age < 18)
        throw new Error('must be an adult of full age');
};

const formattedName = json => {
    json.formattedName = json.name && json.name.toUpperCase();
};

const mapping = Mapping.map(Friend)
.afterDecode(object => validate(object))
.afterEncode(json => formattedName(json));

Middlewares are available before and after transforms. Before, they work on copies. After, they work on the proper result.

They may return nothing, a value or a Promise. If they return a value of the Promise of a value, this value will be passed to the next middleware and as a transform's result if this was the last middleware. If they don't return any value, the previous value will be passed to the next middleware and as a transform's result if this was the last middleware.

Providers

Helpers can be transmitted to custom get / set and to middlewares through providers. Once a provider is configured, it is given to every custom beforeEncode, beforeDecode, afterEncode, afterDecode, get, and set.

const mapping = Mapping.map(Friend)
.providers({
    upperCase: value => value.toUpperCase()
})
.afterEncode((json, providers) => {
    json.formattedName = providers.upperCase(json.name)
});

Defaults

Defaults permissions can be configured to avoid repetition. All props are readen and writen if not stated otherwise. Valid flags are 'rw', 'r-','-w' and '--'.

const mapping = Mapping.map(Friend)
.defaults({
    permissions: 'r-'
});

mapping.decode({name: 'Joe', age: 27}).then(friend => { /* new Friend({name: 'Joe'}) */ });

Casting to primitive

By default, String, Number, Boolean and Date are parsed. This can be turned off through normalize.

const mapping = Mapping.map(Friend);
const mappingNotCasted = Mapping.map(Friend).normalize(false);

mapping.decode({age: '27'}).then(friend => { /* new Friend({age: 27}) */ });
mappingNotCasted.decode({age: '27'}).then(friend => { /* new Friend({age: '27'}) */ });

Mask

Used after mapping json to class, masks ease update scenarios. A mask defines the way an instance can be overriden by another one (of the same type) according to a 'readonly' schema (thus erasing on the new instance the attributes from the original that should not be changed).

const mask = Mask.cover(Friend).with({age: true})
const touched = mask.settle(new Friend({name: 'Joe', age: 27}), new Friend({name: 'Jack', age: 33})); /* settle(dest, source) */

friend; /* {name: 'Joe', age: 33} */
touched; /* {name: true} */

const mask = Mask.cover(Friend).except({age: true})
const touched = mask.settle(new Friend({name: 'Joe', age: 27}), new Friend({name: 'Jack', age: 33})); /* settle(dest, source) */

friend; /* {name: 'Jack', age: 27} */
touched; /* {age: true} */

A touched literal is returned a list of the updated attributes.

Domain

A domain class is supplied optionally to inherit from, in order to ease props definition and creation (its constructor copy only defined props in the instance attributes on new).

const Domain = carnaval.Domain;

class Friend extends Domain {
    get props() {
        return {
            name: String,
            age: Number
        };
    }
}

Domain validation

A rules attributes can be used to define a validation schema. A custom validate implentation can then handle all the checks. An ajv implentation is providen as an example.

class Friend extends Domain {
    get props() {
        return {
            name: String,
            age: Number
        };
    }
    get rules() {
        return {
            name: {required: true}
        }
    }
}

const mapping = Mapping.map(Friend).afterDecode(object => validate(object));
mapping.decode({}) // depending on validate, this will throw an error
0.2.14

3 months ago

0.2.13

3 months ago

0.2.12

9 months ago

0.2.11

9 months ago

0.2.10

11 months ago

0.2.9

11 months ago

0.2.8

11 months ago

0.2.7

2 years ago

0.2.6

2 years ago

0.2.5

2 years ago

0.2.4

2 years ago

0.2.3

3 years ago

0.2.2

3 years ago

0.2.1

3 years ago

0.2.0

3 years ago

0.1.3

3 years ago

0.1.2

3 years ago

0.1.1

3 years ago

0.1.0

3 years ago

0.0.0

7 years ago