npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

carnaval

v0.2.17

Published

Minimalist, zero dependency, encoding / decoding class ↔ json for [node](https://nodejs.org).

Downloads

35

Readme

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

Note that if you declare class properties, you need to override default constructor:

const Domain = carnaval.Domain;

class Friend extends Domain {
    name;
    age;

    constructor(data) {
        super();
        Domain.assign(this, data);
    }
    
    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