0.0.4 • Published 10 years ago

volan v0.0.4

Weekly downloads
2
License
MIT
Repository
github
Last release
10 years ago

volan

Build Status

Meta classes and typechecks in ~100 lines of code for ES5

Volan adds a wrapper around simple javascript function constructor, providing type-checks for javascript's native objects and more.

It does so by converting object properties to es5 getters and setters. It's very similar to typescript's interfaces but with runtime type checking. It implements a simple Meta Object Protocol, much inspired by the Moose Object System for Perl.

( some whitespace has been maintained for readability - don't argue with js-beautify)

Install

    npm install volan --save

Example

    var Volan = require('volan');

    var Point = Volan.create({
        x: Number,

        y: Number,

        get xy(){
            return [this.x, this.y];
        }
    });

    var Circle = Volan.create({
        center: Point,

        radius: Number

        get circumference() {
            return (2 * this.radius) * Math.PI;
        },

        pointInCircle: function( point ) {
            var dx = point.x - this.center.x,
                dy = point.y - this.center.y;

            return (Math.sqrt(dx) - Math.sqrt(dy)) < Math.sqrt(this.radius);
        }
    });

    var c = new Circle({
        center: new Point({ x: 10, y: 10 }),
        radius: 12
    });

    // now let em' bounce...

Custom types, methods and static properties

Volan tries to stick with what you know about javascript. The above example for circle show how a simple method is implemented, together with a simple get.

Methods

Methods are just like native methods: properties of an object. Except, Volan makes them read-only and thus protects them form being overwritten accidentally:

    'use strict';

    var Person = Volan.create({
        firstName: String,
        lastName: String,

        greet: function( salutation ){
            return util.format('%s, %s %s', salutation, this.firstName, this.lastName );
        }
    });

    var me = new Person({ firstName: 'Matthijs', lastName: 'van Henten' });

    me.greet('Howdy'); // Howdy, Matthijs van Henten

    me.greet = 'Ooops, forgot your name?'; // throws in strict mode

Static properties or class constants

Native get and set syntax is untouched. By default, Object.defineProperty defines a read-only property:

    'use strict';

    var x = {};
    Object.defineProperty(x, 'one', { value: 1});

    x.one = 2; // throws an error in strict mode
    console.log( x.one ); // x is still 1

When creating a property with a scalar or function value, Volan follows the same convention:

    'use strict';

    var Foo = Volan.create({
        one: 1
    });

    var x = new Foo();
    x.one = 2; // error in strict mode
    console.log( x.one ); // x is still 1

Typechecking

Volan performs typechecking both during object creation and when setting properties that are writable. Properties are writable and required by default.

This behaviour can be overriden with a little extra syntax.

    'use strict';

    var p = new Point();
    // throws: TypeError: Validation failed for "x", value "undefined" is not a Number

    var MagicPoint = Volan.create({
        x: {
            type: Number,
            value: 42,
            writable: false,
            required: false
        },

        y: {
            type: Number,
            value: 42,
            writable: false,
            required: false
        }
    });

    var p = new MagicPoint();

    console.log( p.x, p.y ); // 42, 42

    p.x = 10;

    // OUCH: TypeError: Cannot assign to read only property 'x' of #<Object>

Native type checks

Volan supports built-in type checks for Boolean|Number|String|RegExp|Array|Object|Date and a special case for pre-compiled regexes:

    'use strict';

    var Thing = Volan.create({
        int: new RegEx(/\d+/)
    });

    var t = new Thing({ int: '1' }); // all right
    var p = new Thing({ int: '1,2' }); // throws an error

When providing a class-like function, typecheck is done by instanceof comparison:

    // Point is a previously defined function that has a prototype

    var Rect = Volan.create({
        pos: Point,

        width: Number,

        height: Number,
    });

    var r = new Rect({ pos: 1, width: 10, height: 10 });
    // throws: TypeError: Validation failed for "pos", value "1" is not a ... (function dump)

    var s = new Rect({ pos: new Point({ x: 1, y: 2 }), width: 10, height: 10 }); // okidoki

Custom typechecks

Volan supports custom typechecks by convention. To distinguish Class-like objects and methods from typechecks, it is required to provide a named function:

    'use strict';

    var Walk = Volan.create({
        silly: function SillyWalk(val){
            return /silly/.test(val);
        }
    });

    var d = new Walk({ silly: 'In the park' });
    // throws: TypeError: Validation failed for "silly", value "In the park" is not a SillyWalk

    var p = new Walk({ silly: 'A silly walk in the park' }); // all ok

A composite typecheck may be written as such:

    'use strict';

    var enum = function( size, type ){
        return EnumTypeCheck(val){
            return val instanceof Array &&
                val.length === size &&
                val.every(function(item){
                    return item instanceof type;
                });
        }
    };

    var Polygon = Volan.create({
        points: enum(3, Point )
    });

Transforming arguments

Sometimes, it is desirable to transform constructor arguments before they are applied.

Volan provides a special hook called __buildargs. This function is only executed once before applying the arguments, allowing you to transform them.

Supposed we'd like the Point example to transform it's first two arguments to respectively x and y.

    var Augumented = Volan.extend( Point, {
        __buildargs: function( x, y ){
            return { x: x, y: y };
        }
    });

    var p = new Augumented( 10, 20 );

    console.log( p.x, p.y );
    // 10, 20 as expected

Note that __buildargs is invoked without the this. __buildargs is removed from the object after construction, but this behaviour cannot be guaranteed for classes extended from vanilla js.

Inheritance

Glad you've asked!

Woperting classes are just plain javascript Function constructors with a prototype. This means they can be extended like any other, and vice versa. As a bonus, the super constructor is called just before the object is fully initialized.

Examples:

    'use strict';

    // back to the classic example...
    var Point = Volan.create({
        x: Number,

        y: Number,

        get xy(){
            return [this.x, this.y];
        }
    });

    // using classic extend...
    $.extend( Point.prototype, {
        move: function(x,y){
            this.x = x;
            this.y = y;
        }
    });

    // adding an alternative constructor
    Point.create = function( x, y ){
        return new Point({ x: x, y: y });
    };

    // extending Volan.extend
    var Point3D = Volan.extend(Point, {
        z: Number,

        get xyz(){
            return [this.x, this.y, this.z];
        }
    });

Inheritance chains

Each class in the chain keeps a reference to it's own super:

    var Point3DInTime = Volan.extend( Point3D, {
        __buildargs: function(x,y,z,t){
          return { x: x, y: y, z: z, t: t };
        },
        t: Number,
    });

    var p = new Point3DInTime( 1, 2, 3, 4 );

    console.log( p.x, p.y, p.z, p.t );
    // 1, 2, 3, 4

Here's an (contrived) example use case for extending non-volan classes:

    var Email = function(){}

    Email.prototype = {
        subject: 'Hello',

        send: function( to ){
            // some logic for sending...
            return this.subject + ' send email to: ' + to;
        }
    };

    var EmailChild = Volan.extend( Email, {
        subject: String,

        to: new RegExp(/.+@.+/), // really lame check for email

        send: function(){
            // need to call super's prototype.send as a method of our own
            return this.__super.prototype.send.call( this, this.to );
        }
    });

    var m = new EmailChild({
        to: 'someone@example.com',
        subject: 'Your new class'
    });

    assert.equal( m.send(), 'Your new class send email to: someone@example.com' );

Isn't this the same as that other project?

I've written similar libraries in the past, called class-wolperting and jackalope. I've abandoned jackalope in favour of native javascript syntax and the use of Object.create.

class-wolperting has been used in production during my time at a company called Whatser. Volan is a rewrite using less dependencies and sacrificing a couple of features I never used in real life. It's also a lot smaller, easier to read and maintain and does not rely on lodash.

Both projects have an almost similar test suite for similar functionality - in fact this library was written entirely TDD style using Woleperting's set of tests.

I intend to use this in client-side projects where size does matter.

What's a volan, wolperting and a jackalope

They're mythical creatures inhibiting some unnatural properties, like flying fish and carnivourus rabbits.

Javascript has a long-standing issue with prototypical inheritance v.s. classical inheritance.

Some may argue that we should abandon the prototype approach altogether, whereas others tend to keep it more functional. In some use cases, using something class-like just fits the bill.

Prototypes have a lower memory footprint then vanilla objects, and function constructors can be type-checked using instanceof.

Lot's of client-side frameworks tend to "extend this" and "extend that", creating a spaghetti of inheritance and interdependency. Using a formal class system with typechecking at least helps with bookkeeping.

Not really funny meme image

0.0.4

10 years ago

0.0.3

10 years ago

0.0.2

10 years ago

0.0.1

10 years ago