UNPKG

typify

Version:

Runtime type-checking for JavaScript.

324 lines (274 loc) 10.5 kB
/* * typify * https://github.com/phadej/typify * * Copyright (c) 2013 Oleg Grenrus * Licensed under the MIT license. */ "use strict"; var VERSION = [0, 2, 10]; var utils = require("./utils.js"); var p = require("./predicates.js"); var A = require("./aparser.js"); var c = require("./checkableCompiler.js"); var cons = require("./checkableConstructors.js"); var show = require("./show.js"); var parseCheckableType = require("./checkableParser").parse; var compileCheckableType = c.compile; var compileCheckableTypeRecursive = c.compileRecursive; var functionP = require("./functionParser.js").functionP; // Few almost predicates // :: *... -> * function throwAlways() { throw new Error("this shouldn't been called"); } var functionTypeCheckRe = /^([a-zA-Z_][a-zA-Z0-9_]*|"[^"]*"|'[^']*'|[0-9]+|\*|\?|\||&|\(|\)|\{|\}|::|:|,|=>|->|\.\.\.|\s+)*$/; var functionTypeTokenRe = /([a-zA-Z_][a-zA-Z0-9_]*|"[^"]*"|'[^']*'|[0-9]+|\*|\?|\||&|\(|\)|\{|\}|::|:|,|=>|->|\.\.\.)/g; // Function type parsing, checks pre-compiling & pretty-printing // :: checkable -> *... -> boolean function optional(parsed) { if (parsed.type === "any") { return true; } if (parsed.type === "opt") { return true; } if (parsed.type === "alt") { return parsed.options.some(optional); } return false; } // :: functionType -> nat | infinity function maxParamsF(parsed) { return parsed.rest === undefined ? parsed.params.length : Infinity; } // :: functionType -> nat function minParamsF(parsed) { var result = parsed.params.length; for (var i = result - 1; i >= 0; i--) { if (!optional(parsed.params[i])) { break; } result = i; } return result; } // :: Environment -> map (array checkable) -> map (array fn) function compileContext(environment, context) { return utils.mapValues(context, function (v) { return utils.map(v, compileCheckableType.bind(undefined, environment, context)); }); } // :: string -> functionType function parseFunctionType(type) { if (!functionTypeCheckRe.test(type)) { throw new TypeError("invalid function type: " + type); } var tokens = type.match(functionTypeTokenRe); var parsed = A.parse(functionP, tokens); if (parsed === undefined) { throw new TypeError("invalid function type: " + type); } return parsed; } // :: Environment -> functionType -> * function compileFunctionType(environment, parsed) { return { name: parsed.name, context: compileContext(environment, parsed.context), params: utils.map(parsed.params, compileCheckableType.bind(undefined, environment, parsed.context)), rest: parsed.rest && compileCheckableType(environment, parsed.context, parsed.rest), result: compileCheckableType(environment, parsed.context, parsed.result), minParams: minParamsF(parsed), maxParams: maxParamsF(parsed), }; } // :: map (array fn) -> fn -> string -> *... -> boolean function contextCheckGeneric(context, compiled, varname) { var options = context[varname]; var args = utils.slice(arguments, 3); for (var i = 0; i < options.length; i++) { var option = options[i]; var res = option.apply(undefined, [compiled].concat(args)); if (res) { context[varname] = [option]; return true; } } return false; } // Decorate function with type-signature check function decorate(environment, type, method) { var parsed = parseFunctionType(type); var compiled = compileFunctionType(environment, parsed); return function () { // check there are enough parameters if (arguments.length < compiled.minParams || arguments.length > compiled.maxParams) { if (compiled.minParams === compiled.maxParams) { throw new TypeError("function " + compiled.name + " expects " + compiled.maxParams + " arguments, " + arguments.length + " given"); } else { throw new TypeError("function " + compiled.name + " expects " + compiled.minParams + "-" + compiled.maxParams + " arguments, " + arguments.length + " given"); } } var contextCheckUn = contextCheckGeneric.bind(undefined, utils.copyObj(compiled.context)); var contextCheck = utils.y(contextCheckUn); // check that parameters are of right type for (var i = 0; i < arguments.length; i++) { var argCheck = i < compiled.params.length ? compiled.params[i] : compiled.rest; var argType = i < compiled.params.length ? parsed.params[i] : parsed.rest; if (!argCheck(contextCheck, arguments[i])) { // TODO: str checkable type throw new TypeError("type of " + parsed.name + " " + (i + 1) + ". parameter is not `" + show.checkable(argType) + "` in context `" + show.context(parsed.context) + "` -- " + JSON.stringify(arguments[i])); } } // call original function var r = method.apply(this, arguments); // check type of return value if (!compiled.result(contextCheck, r)) { // TODO: str checkable type throw new TypeError("type of `" + parsed.name + "` return value is not `" + show.checkable(parsed.result) + "` in context `" + show.context(parsed.context) + "` -- " + r); } // return return r; }; } function parse(definition, closed) { if (p.isString(definition)) { return parseCheckableType(definition); } else if (p.isFunction(definition)) { return cons.user(definition); } else if (p.isArray(definition)) { var options = utils.map(definition, parse); return cons.alt(options); } else /* if (p.isObject(definition)) */ { var fields = utils.mapValues(definition, parse); return cons.record(fields, closed); } } // Check checkable type function check(environment, type, variable) { if (arguments.length !== 2 && arguments.length !== 3) { throw new TypeError("check takes 1 or 2 arguments, " + (arguments.length - 1) + " provided"); } var parsed = parseCheckableType(type); // console.log(parsed); // console.log(JSON.stringify(parsed, null)); var compiled = compileCheckableType(environment, {}, parsed); // using empty context switch (arguments.length) { case 2: return function (variable1) { return compiled(throwAlways, variable1) === true; }; case 3: return compiled(throwAlways, variable) === true; } } function assert(environment, type, variable) { if (arguments.length !== 2 && arguments.length !== 3) { throw new TypeError("assert takes 1 or 2 arguments, " + (arguments.length - 1) + " provided"); } var parsed = parseCheckableType(type); // console.log(parsed); // console.log(JSON.stringify(parsed, null)); var compiled = compileCheckableType(environment, {}, parsed); // using empty context switch (arguments.length) { case 2: return function (variable1) { var result1 = compiled(throwAlways, variable1); if (result1 !== true) { throw new TypeError(result1); } }; case 3: var result = compiled(throwAlways, variable); if (result !== true) { throw new TypeError(result); } } } // Add single parsable type // :: Environment -> map checkable -> undefined function addParsedTypes(environment, parsed, closed) { var names = Object.keys(parsed); names.forEach(function (name) { if (environment.has(name)) { throw new Error(name + " is already defined"); } }); var compiled = utils.mapValues(parsed, compileCheckableTypeRecursive.bind(undefined, environment, {}, names)); var checks = utils.mapValues(compiled, function (check2) { return check2.bind(undefined, compiled, throwAlways); }); environment.add(checks); } function addType(environment, name, definition, closed) { var parsed = {}; closed = !!closed; parsed[name] = parse(definition, closed); return addParsedTypes(environment, parsed); } // Or many simultanouslty function mutual(environment, definitions) { var parsed = utils.mapValues(definitions, parse); return addParsedTypes(environment, parsed); } function adt(environment, name, definitions) { if (utils.has(definitions, name)) { throw new Error("adt and it's constructor cannot has the same name"); } var constructors = Object.keys(definitions); var parsed = utils.mapValues(definitions, parse); parsed[name] = parse(constructors); return addParsedTypes(environment, parsed); } function instance(environment, name, cls) { return addType(environment, name, function (arg) { return arg instanceof cls; }); } function wrap(environment, module, signatures) { for (var fn in signatures) { module[fn] = decorate(environment, fn + " :: " + signatures[fn], module[fn]); } return module; } var buildInTypes = require("./builtin.js"); // typify: instance Environment // :: -> undefined function Environment() { this.types = {}; } Environment.prototype.has = function environmentHas(type) { return utils.has(this.types, type) || utils.has(buildInTypes, type); }; Environment.prototype.get = function environmentGet(type) { return this.types[type] || buildInTypes[type]; }; Environment.prototype.add = function environmentAdd(checks) { Object.keys(checks).forEach(function (type) { this.types[type] = checks[type]; }, this); }; // typify public API type signatures var TYPE_SIGNATURES = { // TODO: change fn to multi type and deprecate alias & record type: "string -> fn -> *", // TODO: support alternative function signatures // TODO: support specifying required but "any" parameter // check: "string -> * -> boolean", alias: "string -> string -> *", record: "string -> map string -> boolean? -> *", mutual: "map string -> *", instance: "string -> fn -> *", wrap: "* -> map string -> *", adt: "string -> map string -> *", }; // Create typify // We could want use prototype-style, instead of closure, but we cannot make callable objects. // TODO: add reference function create() { var env = new Environment(); var typify = decorate.bind(undefined, env); typify.type = addType.bind(undefined, env); typify.alias = addType.bind(undefined, env); typify.record = addType.bind(undefined, env); typify.mutual = mutual.bind(undefined, env); typify.adt = adt.bind(undefined, env); typify.instance = instance.bind(undefined, env); typify.check = check.bind(undefined, env); typify.assert = assert.bind(undefined, env); typify.wrap = wrap.bind(undefined, env); typify.version = VERSION; // also add recursive create // make recursive environments or just possible to merge types from old? typify.create = create; return typify.wrap(typify, TYPE_SIGNATURES); } // Export stuff module.exports = create();