UNPKG

dbc

Version:

Design by contract and type checking assertions

343 lines (311 loc) 11.6 kB
// dbc // === // `dbc` is a small library for design-by-contract style defensive coding in javascript. // (function () { var mode , messages = []; var _ = this._ || require('underscore'); dbc = { // Public API // ========== // check // --- // Check asserts that an object `o` meets a specification `spec`. // If `o` does not satisfy `spec` then an exception is thrown. // eg. // // dbc.check({ // name: 'John', // age: 22 // }, { // name: [{validator:'type',args:['string']}], // age: [{validator:'type',args:['number']}], // }); check: function(o, spec, message) { message = message || ''; mode = 'check'; try { applyValidators.call(this, o, spec); } catch (e) { throw new Error(message ? message + ': ' + e.message : e.message); } }, // validate // ----- // Validate is the same as check, except that errors are returned // as an array of messages instead of throwing an exception. validate: function (o, spec) { mode = 'validate'; applyValidators.call(this, o, spec); return messages; }, // wrap // ---- // Wrap returns a wrapped function that applies 'specs' validators to the // functions arguments and 'returnSpec' validators to the return value. // eg // // var add = dbc.wrap(function (f,s) { // return f + s; // }, { // // validators for the first arg // 0: [{validator:'type', args:['number']}], // // validators for the second arg // 1:[{validator:'type', args:['number']}] // }, // // validators for the return value // [{validator: 'type', args:['number']}]); wrap: function(original, specs, returnSpec) { return function () { var r; checkArgs(arguments); r = original.apply(this,arguments); checkReturn(); return r; function checkArgs(args) { _.each(_.keys(specs || {}), function (k,index) { dbc.check( {k: args[index] }, { k: specs[k] }); }); } function checkReturn() { if (returnSpec) { dbc.check({ret: r}, {ret: returnSpec}); } } }; }, // makeConstructor // ---- // MakeConstructor generates a constructor from a spec. The constructor // validates that the created objects meet the spec. // ie // // var Person = dbc.MakeConstructor({ // name: [{validator:'type',args:'string'}], // age: [{validator:'type',args:'number'}] // }); // Person.prototype.canDrive = dbc.wrap(function () { // return this.age > 16; // }, null, [{validator:'type',args:'number'}]); // var p = new Person('John', 22); // p.canDrive(); // true makeConstructor: function(spec) { var f = function (prps) { var c = this; _.each(_.keys(spec), function (key) { c[key] = prps[key]; }); dbc.check(c, f.__spec); }; f.__spec = spec; return f; }, // custom // --- // The `custom` validator applies a custom predicate. // eg // // dbc.check({ // number: 7 // },{ // number: [{validator:'custom',args:[function isEven(n) { // return n % 2 === 0; // }]}] // }); custom: function(v, test, message) { if (!isExisting(v)) return; if (!test(v)) { storeMessage(message || 'failed custom function condition for value ' + v); } }, // assert // --- // Assert is for simple boolean assertions. eg // // dbc.assert(6 === 9, 'Six should equal nine'); assert: function(condition, message) { if (!condition) { storeMessage(message); } }, // type // ---- // Type asserts the type of a value using JavaScript's `typeof` operator. // In addition to the JavaScript types (undefined, object, boolean, number, string, function) you can also use `array`. eg // // dbc.check({ // name:'John' // }, { // name: [{validator:'type',args: ['string']}] // }); type: function (v, type, message) { if (type.charAt(type.length-1) == '?') { if (!isExisting(v)) return; type = type.substring(0, type.length-1); } message = message || 'Expected type of ' + type + ' but was ' + typeof v; if (type == 'array') { if (!_.isArray(v)) { storeMessage(message) } return; } if (typeof v == 'undefined' || v == null) { storeMessage('Expected type of ' + type + ' but was null or undefined'); } if ((typeof v) != type) { storeMessage(message); } }, // required // --- // Required asserts that a value is not null or undefined. This validator does // not require any args. eg // // dbc.check({ // name:'John' // }, { // name: [{validator:'required'}] // }); required: function(v, message) { if (!isExisting(v)) { storeMessage(message || 'expected a defined value'); } }, // isArray // --- // IsArray asserts that a value is an array. This validator does // not require any args. eg // // dbc.check({ // colours: ['red','green','blue'] // }, { // colours: [{validator:'isArray'}] // }); isArray: function (v, message) { if (isExisting(v) && !_.isArray(v)) { storeMessage(message || 'expected an array') } }, // isEnumerable // --- // IsEnumerable asserts that a value has a forEach function. This validator does // not require any args. eg // // dbc.check({ // colours: ['red','green','blue'] // }, { // colours: [{validator:'isEnumerable'}] // }); isEnumerable: function (v, message) { if (isExisting(v) && typeof v.forEach !== 'function') { storeMessage(message || 'expected an object with a forEach function'); } }, // isNonEmptyCollection // --- // isNonEmptyCollection asserts that a value has a length property greater than zero. This validator does // not require any args. eg // // dbc.check({ // divs: $('div') // assuming jQuery // }, { // divs: [{validator:'isNonEmptyCollection'}] // }); isNonEmptyCollection: function (v, message) { if (!isExisting(v)) return; if (!(typeof v.length === 'number' && v.length > 0)) { throw new Error(message || 'expected collection with length > 0'); } }, // isFunction // --- // isFunction asserts that a value is a function. This validator does // not require any args. eg // // dbc.check({ // square: function (n) { return n * n; } // }, { // square: [{validator:'isFunction'}] // }); isFunction: function(f, message) { this.type(f, 'function', message || 'expected a function'); }, // isObject // --- // isObject asserts that a value is an object. This validator does // not require any args. eg // // dbc.check({ // thing: {} // }, { // thing: [{validator:'isObject'}] // }); isObject: function(o, message) { this.type(o, 'object', message || 'expected an object'); }, // isInstance // --- // isInstance asserts the constructor of an object. eg // // dbc.check({ // john: new Person('John') // }, { // john: [{validator:'isInstance',args:[Person]}] // }); isInstance: function(o, type, message) { if (isExisting(o) && !(o instanceof type)) { storeMessage(message || "expected " + o + " to be an instance of " + type.name); } }, // functionArity // --- // functionArity asserts the arity of a function. eg // // dbc.check({ // add: function (f,s) { return f + s; } // }, { // add: [{validator:'functionArity',args:[2]}] // }); functionArity: function (f, arity, message) { if (!isExisting(f)) return; this.isFunction(f, 'cannot check arity of an object that is not a function'); if (f.length !== arity) { storeMessage(message || 'Function arity is ' + f.length + '. Expected ' + arity); } } }; function isExisting(v) { return typeof v !== "undefined" && v !== null; } function applyValidators(o, spec) { var specKeys = _.keys(spec), dbc = this; specKeys.forEach(function (key) { var validators = spec[key]; validators.forEach(function(validator) { dbc[validator.validator].apply(dbc, [o[key]].concat(validator.args || [])) }); }); } function storeMessage(message) { if (mode === 'validate') { messages.push(message); return; } throw new Error(message); } if (typeof define !== "undefined" && define !== null) { define('dbc', [], function () { return dbc; }); } else if (typeof window !== "undefined" && window !== null) { window.dbc = dbc; } if (typeof module !== "undefined" && module !== null) { module.exports = dbc; } })();