UNPKG

ducktype

Version:

Flexible data validation using a duck type interface

703 lines (627 loc) 18 kB
/** * ducktype * * Data validation using a ducktype interface. For JavaScript and Node.js. * * Load * var ducktype = require('ducktype'); * * Syntax: * ducktype(type) * ducktype(type, options) * ducktype(type1, type2, ...) * ducktype(type1, type2, ..., options) * * Where: * type is a type description * options is an object with properties: * name: String (optional) * optional: Boolean (optional) * nullable: Boolean (optional) */ (function () { 'use strict'; /** * Duck type constructor * @param {{name: String, test: Function}} options * @constructor DuckType */ function DuckType(options) { this.name = options.name; this.test = options.test; } /** * Test whether an object matches this DuckType. * This function should be overwritten by the DuckType implementation * @param {*} object * @returns {boolean} match */ DuckType.prototype.test = function (object) { return false; }; /** * Test whether an object matches this DuckType. * Throws a TypeError when the object does not match. * @param {*} object */ DuckType.prototype.validate = function (object) { if (!this.test(object)) { throw new TypeError(object + ' is not a valid ' + (this.name || 'type')); } }; /** * Create a wrapper around the provided function. The wrapper first validates * the function arguments, and throws a TypeError if not correct. * When correct, the function will be executed. * @param {Function} fn * @returns {Function} wrapper */ DuckType.prototype.wrap = function (fn) { var ducktype = this // TODO: test whether this DuckType is an Array // Alter the behavior of the ducktype in case of a test with zero or one arguments return function ducktypeWrapper() { ducktype.validate(arguments); return fn.apply(fn, arguments); }; }; // The object basic contains all basic types var basic = {}; // type Array basic.array = new DuckType({ name: 'Array', test: function isArray(object) { return (Array.isArray(object) || ((object != null) && (object.toString() === '[object Arguments]'))); } }); // type Boolean basic.boolean = new DuckType({ name: 'Boolean', test: function isBoolean(object) { return ((object instanceof Boolean) || (typeof object === 'boolean')); } }); // type Date basic.date = new DuckType({ name: 'Date', test: function isDate(object) { return (object instanceof Date); } }); // type Function basic.function = new DuckType({ name: 'Function', test: function isFunction(object) { return ((object instanceof Function) || (typeof object === 'function')); } }); // type Number basic.number = new DuckType({ name: 'Number', test: function isNumber(object) { return ((object instanceof Number) || (typeof object === 'number')); } }); // type Object basic.object = new DuckType({ name: 'Object', test: function isObject(object) { return ((object instanceof Object) && (object.constructor === Object)); } }); // type RegExp basic.regexp = new DuckType({ name: 'RegExp', test: function isRegExp(object) { return (object instanceof RegExp); } }); // type String basic.string = new DuckType({ name: 'String', test: function isString(object) { return ((object instanceof String) || (typeof object === 'string')); } }); // type null basic['null'] = new DuckType({ name: 'null', test: function isNull(object) { return (object === null); } }); // type undefined basic['undefined'] = new DuckType({ name: 'undefined', test: function isUndefined(object) { return (object === undefined); } }); // type url basic.url = new DuckType({ name: 'url', test: isUrl }); // type email // Be careful: when changing the regexp, double check whether it is still secure. // test with https://www.npmjs.com/package/vuln-regex-detector var emailRegExp = /^[a-zA-Z][\w.-]*[a-zA-Z0-9]@\w+\.[a-zA-Z]+$/; basic.email = new DuckType({ name: 'email', test: function (object) { return emailRegExp.test(object); } }); // type integer basic['integer'] = new DuckType({ name: 'integer', test: function isInteger (object) { return ((object instanceof Number) || (typeof object === 'number')) && (object === parseInt(object)); } }); // TODO: add types like phone number, postcode, ... /** * Create a ducktype handling an object * @param {Object} type * @param {{name: String}} [options] * @returns {*} */ function createObject (type, options) { // retrieve the test functions for each of the objects properties var tests = {}, prop; for (prop in type) { if (type.hasOwnProperty(prop)) { tests[prop] = ducktype(type[prop]).test; } } // non-empty object var isObject = basic.object.test; return new DuckType({ name: options && options.name || null, test: function test (object) { var prop; // test whether we have an object if (!isObject(object)) { return false; } // test each of the defined properties for (prop in tests) { if (tests.hasOwnProperty(prop)) { if (!tests[prop](object[prop])) { return false; } } } return true; } }); } /** * Create a ducktype handling an array * @param {Array} type An array with multiple elements * @param {{name: String}} [options] * @returns {*} */ function createArray (type, options) { // multiple childs, fixed length var tests = []; var isArray = basic.array.test; for (var i = 0, ii = type.length; i < ii; i++) { tests[i] = ducktype(type[i]).test; } // create the ducktype return new DuckType({ name: options && options.name || null, test: function test (object) { // test whether object is an array if (!isArray(object)) { return false; } // test for correct length if (object.length !== tests.length) { return false; } // test all childs of the array for (var i = 0, ii = object.length; i < ii; i++) { if (!tests[i](object[i])) { return false; } } return true; } }); // TODO: create an option length, length.min, length.max for the array. // length can be an integer or a function } /** * Create a ducktype handling an array * @param {Array} type An array containing one element * @param {{name: String}} [options] * @returns {*} */ function createArrayRepeat (type, options) { // a single child, repeat for each child var childTest = ducktype(type[0]).test; // create the ducktype return new DuckType({ name: options && options.name || null, test: function test (object) { // test whether object is an array if (!Array.isArray(object)) { return false; } // test all childs of the array for (var i = 0, ii = object.length; i < ii; i++) { if (!childTest(object[i])) { return false; } } return true; } }); // TODO: create an option length, length.min, length.max for the array. // length can be an integer or a function } /** * Create a ducktype handling a prototype * @param {Object} type A prototype function * @param {{name: String}} [options] * @returns {*} */ function createPrototype (type, options) { return new DuckType({ name: options && options.name || null, test: function test (object) { return (object instanceof type); } }); } /** * Create a ducktype handling a combination of types * @param {Array} types * @param {{name: String}} [options] * @returns {*} */ function createCombi (types, options) { var tests = types.map(function (type) { return ducktype(type).test; }); return new DuckType({ name: options && options.name || null, test: function test (object) { for (var i = 0, ii = tests.length; i < ii; i++) { if (tests[i](object)) { return true; } } return false; } }); } /** * Create a ducktype from a test function * @param {Function} test A test function, returning true when a provided * object matches, or else returns false. * @param {{name: String}} [options] * @returns {*} */ function createFunction (test, options) { return new DuckType({ name: options && options.name || null, test: test }); } /** * Create a ducktype from a regular expression. The created ducktype * will check whether the provided object is a String, * and matches with the regular expression * @param {RegExp} regexp A regular expression * @param {{name: String}} [options] * @returns {*} */ function createRegExp (regexp, options) { return new DuckType({ name: options && options.name || null, test: function (object) { return ((object instanceof String) || typeof(object) === 'string') && regexp.test(object); } }); } // TODO: document ducktype.construct(function) and ducktype.construct(regexp) /** * Create a new duck type. Syntax: * ducktype(type) * ducktype(type, options) * ducktype(type1, type2, ...) * ducktype(type1, type2, ..., options) * * Where: * type is a type description * options is an object with properties: * name: String (optional) * optional: Boolean (optional) * nullable: Boolean (optional) * * @param {*...} args * @return {DuckType} ducktype */ function ducktype (args) { // TODO: implement support for ducktype(test: Function) to create a custom type // TODO: implement support for ducktype(test: RegExp) to create a custom type var i, ii; var newDucktype; var type = null; var types = null; var options = null; var test; // process arguments if (arguments.length === 0) { throw new SyntaxError('Parameter type missing'); } else if (arguments.length === 1) { type = arguments[0]; } else { types = []; for (i = 0, ii = arguments.length; i < ii; i++) { if ((i === ii - 1) && arguments[i].constructor === Object) { options = arguments[i]; } else { types[i] = arguments[i]; } } if (types.length === 1) { type = types[0]; types = null; } } // create a duck type if (types) { newDucktype = createCombi(types, options); } else if (type === Array) { newDucktype = basic.array; } else if (type === Boolean) { newDucktype = basic.boolean; } else if (type === Date) { newDucktype = basic.date; } else if (type === Function) { newDucktype = basic.function; } else if (type === Number) { newDucktype = basic.number; } else if (type === Object) { newDucktype = basic.object; } else if (type === String) { newDucktype = basic.string; } else if (type === RegExp) { newDucktype = basic.regexp; } else if (type === null) { newDucktype = basic['null']; } else if (type === undefined) { newDucktype = basic['undefined']; } else if (type instanceof DuckType) { newDucktype = type; // already a duck type } else if (Array.isArray(type)) { if (type.length === 0) { newDucktype = basic.array; } else if (type.length === 1) { newDucktype = createArrayRepeat(type, options); } else { newDucktype = createArray(type, options); } } else if ((type instanceof Object) && (type.constructor === Object)) { if (Object.keys(type).length === 0) { newDucktype = basic.object; } else { newDucktype = createObject(type, options); } } else { newDucktype = createPrototype(type, options); } // process options if (options) { test = newDucktype.test; var constructedTest = null; var tests = []; var name = options.name || newDucktype.name || null; if ((options.optional !== undefined) || (options.nullable !== undefined)) { var optional = (options.optional !== undefined) ? options.optional : false; var nullable = (options.nullable !== undefined) ? options.nullable : false; constructedTest = function (object) { return test(object) || (nullable && (object === null)) || (optional && (object === undefined)); }; tests.push(constructedTest); } else { tests.push(test); } if (options.integer === true) { tests.push(function test_integer (object) { return (object === parseInt(object)); }); } if (options.min !== undefined) { tests.push(function test_min (object) { return (object >= options.min); }); } if (options.max !== undefined) { tests.push(function test_max (object) { return (object <= options.max); }); } if (tests.length === 1) { // a single test newDucktype = new DuckType({ name: name, test: tests[0] }); } else { // multiple tests newDucktype = new DuckType({ name: name, test: function (object) { for (var i = 0, ii = tests.length; i < ii; i++) { if (!tests[i](object)) { return false; } } return true; } }); } } // return the created ducktype return newDucktype; } /** * Create a DuckType from a test function or regular expression * @param {String} [name] * @param {Function | RegExp} test * @return {DuckType} ducktype */ // TODO: document ducktype.construct ducktype.construct = function construct(name, test) { if (arguments.length === 1) { test = arguments[0]; name = null; } if (basic.function.test(test)) { // function return createFunction(test, { name: name }); } else if (basic.regexp.test(test)) { // regexp return createRegExp(test, { name: name }); } else { throw new TypeError('Function or RegExp expected'); } }; // attach each of the basic types to the ducktype function for (var type in basic) { if (basic.hasOwnProperty(type)) { ducktype[type] = basic[type]; } } /** * RegExps. * A URL must match #1 and then at least one of #2/#3. * Use two levels of REs to avoid REDOS. */ var protocolAndDomainRE = /^(?:\w+:)?\/\/(\S+)$/; var localhostDomainRE = /^localhost[:?\d]*(?:[^:?\d]\S*)?$/ var nonLocalhostDomainRE = /^[^\s.]+\.\S{2,}$/; /** * Loosely validate a URL `string`. * * Source: https://github.com/segmentio/is-url * * @param {String} string * @return {Boolean} */ function isUrl(string){ if (typeof string !== 'string') { return false; } var match = string.match(protocolAndDomainRE); if (!match) { return false; } var everythingAfterProtocol = match[1]; if (!everythingAfterProtocol) { return false; } return localhostDomainRE.test(everythingAfterProtocol) || nonLocalhostDomainRE.test(everythingAfterProtocol); } // TODO: implement a parser implements js type annotations // TODO: implement non-strict tests and an option strict /** * Shims for older JavaScript engines */ // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/isArray if(!Array.isArray) { Array.isArray = function (vArg) { return Object.prototype.toString.call(vArg) === '[object Array]'; }; } // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/keys if (!Object.keys) { Object.keys = (function () { var hasOwnProperty = Object.prototype.hasOwnProperty, hasDontEnumBug = !({toString: null}).propertyIsEnumerable('toString'), dontEnums = [ 'toString', 'toLocaleString', 'valueOf', 'hasOwnProperty', 'isPrototypeOf', 'propertyIsEnumerable', 'constructor' ], dontEnumsLength = dontEnums.length; return function (obj) { if (typeof obj !== 'object' && typeof obj !== 'function' || obj === null) throw new TypeError('Object.keys called on non-object'); var result = []; for (var prop in obj) { if (hasOwnProperty.call(obj, prop)) result.push(prop); } if (hasDontEnumBug) { for (var i=0; i < dontEnumsLength; i++) { if (hasOwnProperty.call(obj, dontEnums[i])) result.push(dontEnums[i]); } } return result; }; })(); } /** * CommonJS module exports */ if ((typeof module !== 'undefined') && (typeof module.exports !== 'undefined')) { module.exports = ducktype; } if (typeof exports !== 'undefined') { exports = ducktype; } /** * AMD module exports */ if (typeof(define) === 'function') { define(ducktype); } /** * Browser exports */ if (typeof(window) !== 'undefined') { window['ducktype'] = ducktype; } })();