UNPKG

guards

Version:

Data type & structure checking, runtime analog of types

344 lines (330 loc) 12.4 kB
/* vim:set ts=2 sw=2 sts=2 expandtab */ /*jshint asi: true undef: true es5: true node: true devel: true forin: false */ /*global define: true */ (typeof define !== "function" ? function($){ $(require, exports, module); } : define)(function(require, exports, module, undefined) { var slice = Array.prototype.slice var stringify = Object.prototype.toString function compose() { var funcs = slice.call(arguments) return function composed() { var args = slice.call(arguments) var i = funcs.length while (0 <= --i) args = [ funcs[i].apply(this, args) ] return args[0] } } var isArray = Array.isArray || function isArray(value) { return stringify.call(value) === '[object Array]' } function isSchema(value) { return value && typeof value === 'object' && !isArray(value) } function reference(value) { return value } exports.version = "0.2.0"; /** * # Guard # * * Function takes `isValid` function `defaults` function or value and `message` * as an argument and returns guard function that may be used to guard values. * Each value passed to the guard will be validated using given `isValid` if it * returns `true` value is passed through, otherwise `TypeError` is thrown with * a given message. If guard is called without an argument, `defaults` is used. * If defaults is a function it's value is used otherwise `defaults` is used * itself. * @param {Function} isValid * Function that will be used by guard to validate values. It will be called * with `value` argument that needs to be validated. If function returns * `true` value is valid if `false` it's not. * @param {Function|*} defaults * Function that returns default value or a default value itself to be used * when guard is called without arguments. * @param {String} [message="Unexpected value: `{{value}}`"] * Optional `message` argument may be passed that will be used as a template * for a `TypeError` message that is thrown by generated guards when called * with invalid values. */ /** * # Guard # * * Guard function that may be called with a `value` to be set to a guarded * variable. If `value` is invalid `TypeError` is thrown. Optionally second * `name` argument may be passed, which is useful when guards are used for * object properties. In such case `name` argument just name of object * property and will be used to give a better error messages. * @param {Object|String|Number|function} value * Value to be validated. * @param {String} name * Name of the property that is being guarded. */ function Guard(isValid, defaults, message) { message = message || "Unexpected value: `{{value}}`" return function guard(value, name) { value = value !== undefined ? value : typeof defaults === 'function' ? defaults() : defaults if (!isValid(value)) throw new TypeError(message.replace("{{name}}", name). replace("{{value}}", value). replace("{{type}}", typeof value)); return value } } exports.Guard = Guard /** * # Schema # * * Schema is useful for defining guards for data objects that have particular * structure. Function takes `descriptor` argument that is a map of guards * guarding same named properties of the value being validated. Scheme may * contain guards for a primitive values like `String` and `Number` and also * guards for more complex data structures defined by other `Schema`s, or to * put it other way `Schema` may contain guards that were created by `Schema` * itself which allows defining deeply nested data structures. * * Generated guard will accept only object `values` as an argument. All the * non-guarded properties (that are not present in the `descriptor`) of the * `value` will be stripped out. All the missing properties of the `value` * will be replaced / assembled from the defaults if associated guards provide * fallback mechanism to default value. * * @param {Object} descriptor * Object containing guards for the associated (same named properties) of * the guarded object `value`. * @param {String} [message] * Optional error message template that will be a message of a `TypeError` * that is will be thrown if returned guard is invoked with a wrong `value` * type (other then "object" or "undefined"). If `message` contains * `"{{value}}"` and `"{{type}}"` strings they are going to be replaced with * an actual `value` and it's type. * * ## Examples ## * * var guards = require("guards"); * var Point = guards.Schema({ * x: guards.Number(0), * y: guards.Number(0) * }); * * new Point * // { x: 0, y: 0 } * * Point({ x: 17, z: 50 }) * // { x: 17, y: 0 } * * Point({ x: "5" }) * // TypeError: Number expected instead of string `5` * * Point("{ y: 6 }") * // TypeError: Object expected instead of string `{ y: 6 }` * * * var Segment = guards.Schema({ * start: Point, * end: Point, * opacity: guards.Number(1) * }) * * new Segment * // { start: { x: 0, y: 0 }, end: { x: 0, y: 0 }, opacity: 1 } * * Segment({ end: { x: 17 }, opacity: 0.5 }) * // { start: { x: 0, y: 0 }, end: { x: 17, y: 0 }, opacity: 0.5 } * * Segment({ start: 17 }); * // TypeError: Object expected instead of number `17` * */ exports.Schema = function Schema(schema, message) { message = message || 'Object expected instead of {{type}} `{{value}}`' return compose(function guard(value) { var validated = {} for (var key in schema) validated[key] = schema[key](value[key], key) return validated }, Guard(isSchema, Object, message)) } /** * # Array # * * Array can be used to define guards for an arrays containing elements of some * type or schema. Function takes `guard` as an argument that will guard all the * elements of the `value` array that is passed to the returned guard. * * @param {Function} guard * Guard that is going to be used to verify elements of the array. It can * be any guard created by `String`, `Schema`, `Array` or any custom guard * as well. * @param {String} [message] * Optional error message template that will be a message of a `TypeError` * that is will be thrown if returned guard is invoked with a wrong `value` * type (other then "array" or "undefined"). If `message` contains * `"{{value}}"` and `"{{type}}"` strings they are going to be replaced with * an actual `value` and it's type. * * ## Examples ## * * var guards = require("guards") * var Words = guards.Array(guards.String('')) * * Words([ "foo", "bar" ]) * // [ 'foo', 'bar' ] * * Words([ "foo", 9 ]) * // TypeError: String expected instead of number `9` * * var Point = guards.Schema({ * x: guards.Number(0), * y: guards.Number(0) * }) * var Points = guards.Array(Point) * * new Points([{}, { x: 2, y: 8 }]) * // [ { x: 0, y: 0 }, { x: 2, y: 8 } ] * * Points({ x: 2, y: 8 }) * // TypeError: Array expected instead of object `[object Object]` * * * var Graph = guards.Array(Points) * * new Graph * // [] * * Graph([ * [{ x: 17, foo: "bar" }, { x: 16 }], * [{ y: 4 }], * [] * ]) * // [ [ { x: 17, y: 0 }, { x: 16, y: 0 } ], [ { x: 0, y: 4 } ], [] ] */ exports.Array = function Array(type, message) { var Type = [].constructor message = message || 'Array expected instead of {{type}} `{{value}}`' type = type || reference return compose(function guard(value) { return value.map(type) }, Guard(isArray, Type, message)) } /** * # Tuple # * * Tuple can be used to define guards for an arrays containing predefined * amount of elements guarded by specific guards. Tuple guards are something * in between Array and Schema guards. Function takes array of guards as an * argument that will be used to validate same indexed elements of the `value` * array that is passed to the returned guard. * * @param {Function[]} guards * Guards that are going to be used to verify elements of the array. It can * be any guard created by `String`, `Schema`, `Array` or any custom guard * as well. * @param {String} [message] * Optional error message template that will be a message of a `TypeError` * that is will be thrown if returned guard is invoked with a wrong `value` * type (other then "array" or "undefined"). If `message` contains * `"{{value}}"` and `"{{type}}"` strings they are going to be replaced with * an actual `value` and it's type. * * ## Examples ## * * var guards = require("guards"); * var Point = guards.Schema({ * x: guards.Number(0), * y: guards.Number(0) * }); * var Segment = guards.Schema({ * start: Point, * end: Point, * opacity: guards.Number(1) * }); * var Triangle = guards.Tuple([ Segment, Segment, Segment ]); * * var t1 = Triangle(); * // [ { start: { x: 0, y: 0 }, end: { x: 0, y: 0 }, opacity: 1 }, * // { start: { x: 0, y: 0 }, end: { x: 0, y: 0 }, opacity: 1 }, * // { start: { x: 0, y: 0 }, end: { x: 0, y: 0 }, opacity: 1 } * // ] * * var t2 = Triangle([ * { opacity: 0, foo: "bar" }, * { start: { x: 2 } } * ]); * // [ { start: { x: 0, y: 0 }, end: { x: 0, y: 0 }, opacity: 0 }, * // { start: { x: 2, y: 0 }, end: { x: 0, y: 0 }, opacity: 1 }, * // { start: { x: 0, y: 0 }, end: { x: 0, y: 0 }, opacity: 1 } * // ] * * var t2 = Triangle("foo"); * // TypeError: Array expected instead of string `foo` * * var t3 = Triangle([{ start: { x: '3' } } ]); * // TypeError: Number expected instead of string `3` * * var Pointer = guards.Tuple([ Point, Segment ]); * * var p1 = Pointer(); * // [ { x: 0, y: 0 }, { start: { x: 0, y: 0 }, end: { x: 0, y: 0 }, opacity: 1 } ] * * var p2 = Pointer([ { x: 17 }, { opacity: 0 } ]); * // [ { x: 17, y: 0 }, { start: { x: 0, y: 0 }, end: { x: 0, y: 0 }, opacity: 0 } ] * * var p3 = Pointer([ { foo: "bar" }, { baz: "bla" }, "foo" ]); * // [ { x: 0, y: 0 }, { start: { x: 0, y: 0 }, end: { x: 0, y: 0 }, opacity: 1 } ] */ exports.Tuple = function Tuple(schema, message) { message = message || 'Array expected instead of {{type}} `{{value}}`' return compose(function guard(value) { return schema.map(function(guard, index) { return guard(value[index], index) }) }, Guard(isArray, Array, message)) } /** * # AnyOf # * * `AnyOf` can be used to define guards that validates `value`s that must * satisfy just one of many guards. This is handy in specific specific * scenarios were valid `value` may have different types or schemas. * Function takes any number guards as an arguments and returns composed * guard, which when called will try to validate a given `value` with a given * guards in an order they were passed, the first validate `value` is returned * as result, unless non will validate in which case `TypeError` is thrown. * * @params {Function} guard * Guards used for validations. * @returns {Function} * * ## Examples ## * * var guards = require("guards") * var ObjectPoint = guards.Schema({ * x: guards.Number(0), * y: guards.Number(0) * }) * var ArrayPoint = guards.Tuple.extend([ * guards.Number(0), * guards.Number(0) * ]) * var Point = guards.AnyOf([ ObjectPoint, ArrayPoint ]) * * Point([ 1 ]) * // [ 1, 0 ] * * Point({ y: 15 }) * // { x: 0, y: 15 } * * Point(1, 2) * // TypeError: Passed value: `1` has invalid type or structure */ exports.AnyOf = function AnyOf(guards, message) { message = message || 'Passed value: `{{value}}` has invalid type or structure' return function guard(value, name) { var i = 0, ii = guards.length while(i < ii) { try { return guards[i++](value, name) } catch (error) { /* swallow */ } } throw new TypeError(message.replace("{{name}}", name). replace("{{value}}", value). replace("{{type}}", typeof value)) } } });