lml-main
Version:
This is now a mono repository published into many standalone packages.
1,391 lines (1,218 loc) • 43.8 kB
JavaScript
/**
* typed-function
*
* Type checking for JavaScript functions
*
* https://github.com/josdejong/typed-function
*/
'use strict';
(function (root, factory) {
if (typeof define === 'function' && define.amd) {
// AMD. Register as an anonymous module.
define([], factory);
} else if (typeof exports === 'object') {
// OldNode. Does not work with strict CommonJS, but
// only CommonJS-like environments that support module.exports,
// like OldNode.
module.exports = factory();
} else {
// Browser globals (root is window)
root.typed = factory();
}
}(this, function () {
// factory function to create a new instance of typed-function
// TODO: allow passing configuration, types, tests via the factory function
function create() {
/**
* Get a type test function for a specific data type
* @param {string} name Name of a data type like 'number' or 'string'
* @returns {Function(obj: *) : boolean} Returns a type testing function.
* Throws an error for an unknown type.
*/
function getTypeTest(name) {
var test;
for (var i = 0; i < typed.types.length; i++) {
var entry = typed.types[i];
if (entry.name === name) {
test = entry.test;
break;
}
}
if (!test) {
var hint;
for (i = 0; i < typed.types.length; i++) {
entry = typed.types[i];
if (entry.name.toLowerCase() == name.toLowerCase()) {
hint = entry.name;
break;
}
}
throw new Error('Unknown type "' + name + '"' +
(hint ? ('. Did you mean "' + hint + '"?') : ''));
}
return test;
}
/**
* Retrieve the function name from a set of functions, and check
* whether the name of all functions match (if given)
* @param {Array.<function>} fns
*/
function getName (fns) {
var name = '';
for (var i = 0; i < fns.length; i++) {
var fn = fns[i];
// merge function name when this is a typed function
if (fn.signatures && fn.name != '') {
if (name == '') {
name = fn.name;
}
else if (name != fn.name) {
var err = new Error('Function names do not match (expected: ' + name + ', actual: ' + fn.name + ')');
err.data = {
actual: fn.name,
expected: name
};
throw err;
}
}
}
return name;
}
/**
* Create an ArgumentsError. Creates messages like:
*
* Unexpected type of argument (expected: ..., actual: ..., index: ...)
* Too few arguments (expected: ..., index: ...)
* Too many arguments (expected: ..., actual: ...)
*
* @param {String} fn Function name
* @param {number} argCount Number of arguments
* @param {Number} index Current argument index
* @param {*} actual Current argument
* @param {string} [expected] An optional, comma separated string with
* expected types on given index
* @extends Error
*/
function createError(fn, argCount, index, actual, expected) {
var actualType = getTypeOf(actual);
var _expected = expected ? expected.split(',') : null;
var _fn = (fn || 'unnamed');
var anyType = _expected && contains(_expected, 'any');
var message;
var data = {
fn: fn,
index: index,
actual: actualType,
expected: _expected
};
if (_expected) {
if (argCount > index && !anyType) {
// unexpected type
message = 'Unexpected type of argument in function ' + _fn +
' (expected: ' + _expected.join(' or ') + ', actual: ' + actualType + ', index: ' + index + ')';
}
else {
// too few arguments
message = 'Too few arguments in function ' + _fn +
' (expected: ' + _expected.join(' or ') + ', index: ' + index + ')';
}
}
else {
// too many arguments
message = 'Too many arguments in function ' + _fn +
' (expected: ' + index + ', actual: ' + argCount + ')'
}
var err = new TypeError(message);
err.data = data;
return err;
}
/**
* Collection with function references (local shortcuts to functions)
* @constructor
* @param {string} [name='refs'] Optional name for the refs, used to generate
* JavaScript code
*/
function Refs(name) {
this.name = name || 'refs';
this.categories = {};
}
/**
* Add a function reference.
* @param {Function} fn
* @param {string} [category='fn'] A function category, like 'fn' or 'signature'
* @returns {string} Returns the function name, for example 'fn0' or 'signature2'
*/
Refs.prototype.add = function (fn, category) {
var cat = category || 'fn';
if (!this.categories[cat]) this.categories[cat] = [];
var index = this.categories[cat].indexOf(fn);
if (index == -1) {
index = this.categories[cat].length;
this.categories[cat].push(fn);
}
return cat + index;
};
/**
* Create code lines for all function references
* @returns {string} Returns the code containing all function references
*/
Refs.prototype.toCode = function () {
var code = [];
var path = this.name + '.categories';
var categories = this.categories;
for (var cat in categories) {
if (categories.hasOwnProperty(cat)) {
var category = categories[cat];
for (var i = 0; i < category.length; i++) {
code.push('var ' + cat + i + ' = ' + path + '[\'' + cat + '\'][' + i + '];');
}
}
}
return code.join('\n');
};
/**
* A function parameter
* @param {string | string[] | Param} types A parameter type like 'string',
* 'number | boolean'
* @param {boolean} [varArgs=false] Variable arguments if true
* @constructor
*/
function Param(types, varArgs) {
// parse the types, can be a string with types separated by pipe characters |
if (typeof types === 'string') {
// parse variable arguments operator (ellipses '...number')
var _types = types.trim();
var _varArgs = _types.substr(0, 3) === '...';
if (_varArgs) {
_types = _types.substr(3);
}
if (_types === '') {
this.types = ['any'];
}
else {
this.types = _types.split('|');
for (var i = 0; i < this.types.length; i++) {
this.types[i] = this.types[i].trim();
}
}
}
else if (Array.isArray(types)) {
this.types = types;
}
else if (types instanceof Param) {
return types.clone();
}
else {
throw new Error('String or Array expected');
}
// can hold a type to which to convert when handling this parameter
this.conversions = [];
// TODO: implement better API for conversions, be able to add conversions via constructor (support a new type Object?)
// variable arguments
this.varArgs = _varArgs || varArgs || false;
// check for any type arguments
this.anyType = this.types.indexOf('any') !== -1;
}
/**
* Order Params
* any type ('any') will be ordered last, and object as second last (as other
* types may be an object as well, like Array).
*
* @param {Param} a
* @param {Param} b
* @returns {number} Returns 1 if a > b, -1 if a < b, and else 0.
*/
Param.compare = function (a, b) {
// TODO: simplify parameter comparison, it's a mess
if (a.anyType) return 1;
if (b.anyType) return -1;
if (contains(a.types, 'Object')) return 1;
if (contains(b.types, 'Object')) return -1;
if (a.hasConversions()) {
if (b.hasConversions()) {
var i, ac, bc;
for (i = 0; i < a.conversions.length; i++) {
if (a.conversions[i] !== undefined) {
ac = a.conversions[i];
break;
}
}
for (i = 0; i < b.conversions.length; i++) {
if (b.conversions[i] !== undefined) {
bc = b.conversions[i];
break;
}
}
return typed.conversions.indexOf(ac) - typed.conversions.indexOf(bc);
}
else {
return 1;
}
}
else {
if (b.hasConversions()) {
return -1;
}
else {
// both params have no conversions
var ai, bi;
for (i = 0; i < typed.types.length; i++) {
if (typed.types[i].name === a.types[0]) {
ai = i;
break;
}
}
for (i = 0; i < typed.types.length; i++) {
if (typed.types[i].name === b.types[0]) {
bi = i;
break;
}
}
return ai - bi;
}
}
};
/**
* Test whether this parameters types overlap an other parameters types.
* Will not match ['any'] with ['number']
* @param {Param} other
* @return {boolean} Returns true when there are overlapping types
*/
Param.prototype.overlapping = function (other) {
for (var i = 0; i < this.types.length; i++) {
if (contains(other.types, this.types[i])) {
return true;
}
}
return false;
};
/**
* Test whether this parameters types matches an other parameters types.
* When any of the two parameters contains `any`, true is returned
* @param {Param} other
* @return {boolean} Returns true when there are matching types
*/
Param.prototype.matches = function (other) {
return this.anyType || other.anyType || this.overlapping(other);
};
/**
* Create a clone of this param
* @returns {Param} Returns a cloned version of this param
*/
Param.prototype.clone = function () {
var param = new Param(this.types.slice(), this.varArgs);
param.conversions = this.conversions.slice();
return param;
};
/**
* Test whether this parameter contains conversions
* @returns {boolean} Returns true if the parameter contains one or
* multiple conversions.
*/
Param.prototype.hasConversions = function () {
return this.conversions.length > 0;
};
/**
* Tests whether this parameters contains any of the provided types
* @param {Object} types A Map with types, like {'number': true}
* @returns {boolean} Returns true when the parameter contains any
* of the provided types
*/
Param.prototype.contains = function (types) {
for (var i = 0; i < this.types.length; i++) {
if (types[this.types[i]]) {
return true;
}
}
return false;
};
/**
* Return a string representation of this params types, like 'string' or
* 'number | boolean' or '...number'
* @param {boolean} [toConversion] If true, the returned types string
* contains the types where the parameter
* will convert to. If false (default)
* the "from" types are returned
* @returns {string}
*/
Param.prototype.toString = function (toConversion) {
var types = [];
var keys = {};
for (var i = 0; i < this.types.length; i++) {
var conversion = this.conversions[i];
var type = toConversion && conversion ? conversion.to : this.types[i];
if (!(type in keys)) {
keys[type] = true;
types.push(type);
}
}
return (this.varArgs ? '...' : '') + types.join('|');
};
/**
* A function signature
* @param {string | string[] | Param[]} params
* Array with the type(s) of each parameter,
* or a comma separated string with types
* @param {Function} fn The actual function
* @constructor
*/
function Signature(params, fn) {
var _params;
if (typeof params === 'string') {
_params = (params !== '') ? params.split(',') : [];
}
else if (Array.isArray(params)) {
_params = params;
}
else {
throw new Error('string or Array expected');
}
this.params = new Array(_params.length);
this.anyType = false;
this.varArgs = false;
for (var i = 0; i < _params.length; i++) {
var param = new Param(_params[i]);
this.params[i] = param;
if (param.anyType) {
this.anyType = true;
}
if (i === _params.length - 1) {
// the last argument
this.varArgs = param.varArgs;
}
else {
// non-last argument
if (param.varArgs) {
throw new SyntaxError('Unexpected variable arguments operator "..."');
}
}
}
this.fn = fn;
}
/**
* Create a clone of this signature
* @returns {Signature} Returns a cloned version of this signature
*/
Signature.prototype.clone = function () {
return new Signature(this.params.slice(), this.fn);
};
/**
* Expand a signature: split params with union types in separate signatures
* For example split a Signature "string | number" into two signatures.
* @return {Signature[]} Returns an array with signatures (at least one)
*/
Signature.prototype.expand = function () {
var signatures = [];
function recurse(signature, path) {
if (path.length < signature.params.length) {
var i, newParam, conversion;
var param = signature.params[path.length];
if (param.varArgs) {
// a variable argument. do not split the types in the parameter
newParam = param.clone();
// add conversions to the parameter
// recurse for all conversions
for (i = 0; i < typed.conversions.length; i++) {
conversion = typed.conversions[i];
if (!contains(param.types, conversion.from) && contains(param.types, conversion.to)) {
var j = newParam.types.length;
newParam.types[j] = conversion.from;
newParam.conversions[j] = conversion;
}
}
recurse(signature, path.concat(newParam));
}
else {
// split each type in the parameter
for (i = 0; i < param.types.length; i++) {
recurse(signature, path.concat(new Param(param.types[i])));
}
// recurse for all conversions
for (i = 0; i < typed.conversions.length; i++) {
conversion = typed.conversions[i];
if (!contains(param.types, conversion.from) && contains(param.types, conversion.to)) {
newParam = new Param(conversion.from);
newParam.conversions[0] = conversion;
recurse(signature, path.concat(newParam));
}
}
}
}
else {
signatures.push(new Signature(path, signature.fn));
}
}
recurse(this, []);
return signatures;
};
/**
* Compare two signatures.
*
* When two params are equal and contain conversions, they will be sorted
* by lowest index of the first conversions.
*
* @param {Signature} a
* @param {Signature} b
* @returns {number} Returns 1 if a > b, -1 if a < b, and else 0.
*/
Signature.compare = function (a, b) {
if (a.params.length > b.params.length) return 1;
if (a.params.length < b.params.length) return -1;
// count the number of conversions
var i;
var len = a.params.length; // a and b have equal amount of params
var ac = 0;
var bc = 0;
for (i = 0; i < len; i++) {
if (a.params[i].hasConversions()) ac++;
if (b.params[i].hasConversions()) bc++;
}
if (ac > bc) return 1;
if (ac < bc) return -1;
// compare the order per parameter
for (i = 0; i < a.params.length; i++) {
var cmp = Param.compare(a.params[i], b.params[i]);
if (cmp !== 0) {
return cmp;
}
}
return 0;
};
/**
* Test whether any of the signatures parameters has conversions
* @return {boolean} Returns true when any of the parameters contains
* conversions.
*/
Signature.prototype.hasConversions = function () {
for (var i = 0; i < this.params.length; i++) {
if (this.params[i].hasConversions()) {
return true;
}
}
return false;
};
/**
* Test whether this signature should be ignored.
* Checks whether any of the parameters contains a type listed in
* typed.ignore
* @return {boolean} Returns true when the signature should be ignored
*/
Signature.prototype.ignore = function () {
// create a map with ignored types
var types = {};
for (var i = 0; i < typed.ignore.length; i++) {
types[typed.ignore[i]] = true;
}
// test whether any of the parameters contains this type
for (i = 0; i < this.params.length; i++) {
if (this.params[i].contains(types)) {
return true;
}
}
return false;
};
/**
* Test whether the path of this signature matches a given path.
* @param {Param[]} params
*/
Signature.prototype.paramsStartWith = function (params) {
if (params.length === 0) {
return true;
}
var aLast = last(this.params);
var bLast = last(params);
for (var i = 0; i < params.length; i++) {
var a = this.params[i] || (aLast.varArgs ? aLast: null);
var b = params[i] || (bLast.varArgs ? bLast: null);
if (!a || !b || !a.matches(b)) {
return false;
}
}
return true;
};
/**
* Generate the code to invoke this signature
* @param {Refs} refs
* @param {string} prefix
* @returns {string} Returns code
*/
Signature.prototype.toCode = function (refs, prefix) {
var code = [];
var args = new Array(this.params.length);
for (var i = 0; i < this.params.length; i++) {
var param = this.params[i];
var conversion = param.conversions[0];
if (param.varArgs) {
args[i] = 'varArgs';
}
else if (conversion) {
args[i] = refs.add(conversion.convert, 'convert') + '(arg' + i + ')';
}
else {
args[i] = 'arg' + i;
}
}
var ref = this.fn ? refs.add(this.fn, 'signature') : undefined;
if (ref) {
return prefix + 'return ' + ref + '(' + args.join(', ') + '); // signature: ' + this.params.join(', ');
}
return code.join('\n');
};
/**
* Return a string representation of the signature
* @returns {string}
*/
Signature.prototype.toString = function () {
return this.params.join(', ');
};
/**
* A group of signatures with the same parameter on given index
* @param {Param[]} path
* @param {Signature} [signature]
* @param {Node[]} childs
* @param {boolean} [fallThrough=false]
* @constructor
*/
function Node(path, signature, childs, fallThrough) {
this.path = path || [];
this.param = path[path.length - 1] || null;
this.signature = signature || null;
this.childs = childs || [];
this.fallThrough = fallThrough || false;
}
/**
* Generate code for this group of signatures
* @param {Refs} refs
* @param {string} prefix
* @returns {string} Returns the code as string
*/
Node.prototype.toCode = function (refs, prefix) {
// TODO: split this function in multiple functions, it's too large
var code = [];
if (this.param) {
var index = this.path.length - 1;
var conversion = this.param.conversions[0];
var comment = '// type: ' + (conversion ?
(conversion.from + ' (convert to ' + conversion.to + ')') :
this.param);
// non-root node (path is non-empty)
if (this.param.varArgs) {
if (this.param.anyType) {
// variable arguments with any type
code.push(prefix + 'if (arguments.length > ' + index + ') {');
code.push(prefix + ' var varArgs = [];');
code.push(prefix + ' for (var i = ' + index + '; i < arguments.length; i++) {');
code.push(prefix + ' varArgs.push(arguments[i]);');
code.push(prefix + ' }');
code.push(this.signature.toCode(refs, prefix + ' '));
code.push(prefix + '}');
}
else {
// variable arguments with a fixed type
var getTests = function (types, arg) {
var tests = [];
for (var i = 0; i < types.length; i++) {
tests[i] = refs.add(getTypeTest(types[i]), 'test') + '(' + arg + ')';
}
return tests.join(' || ');
}.bind(this);
var allTypes = this.param.types;
var exactTypes = [];
for (var i = 0; i < allTypes.length; i++) {
if (this.param.conversions[i] === undefined) {
exactTypes.push(allTypes[i]);
}
}
code.push(prefix + 'if (' + getTests(allTypes, 'arg' + index) + ') { ' + comment);
code.push(prefix + ' var varArgs = [arg' + index + '];');
code.push(prefix + ' for (var i = ' + (index + 1) + '; i < arguments.length; i++) {');
code.push(prefix + ' if (' + getTests(exactTypes, 'arguments[i]') + ') {');
code.push(prefix + ' varArgs.push(arguments[i]);');
for (var i = 0; i < allTypes.length; i++) {
var conversion_i = this.param.conversions[i];
if (conversion_i) {
var test = refs.add(getTypeTest(allTypes[i]), 'test');
var convert = refs.add(conversion_i.convert, 'convert');
code.push(prefix + ' }');
code.push(prefix + ' else if (' + test + '(arguments[i])) {');
code.push(prefix + ' varArgs.push(' + convert + '(arguments[i]));');
}
}
code.push(prefix + ' } else {');
code.push(prefix + ' throw createError(name, arguments.length, i, arguments[i], \'' + exactTypes.join(',') + '\');');
code.push(prefix + ' }');
code.push(prefix + ' }');
code.push(this.signature.toCode(refs, prefix + ' '));
code.push(prefix + '}');
}
}
else {
if (this.param.anyType) {
// any type
code.push(prefix + '// type: any');
code.push(this._innerCode(refs, prefix));
}
else {
// regular type
var type = this.param.types[0];
var test = type !== 'any' ? refs.add(getTypeTest(type), 'test') : null;
code.push(prefix + 'if (' + test + '(arg' + index + ')) { ' + comment);
code.push(this._innerCode(refs, prefix + ' '));
code.push(prefix + '}');
}
}
}
else {
// root node (path is empty)
code.push(this._innerCode(refs, prefix));
}
return code.join('\n');
};
/**
* Generate inner code for this group of signatures.
* This is a helper function of Node.prototype.toCode
* @param {Refs} refs
* @param {string} prefix
* @returns {string} Returns the inner code as string
* @private
*/
Node.prototype._innerCode = function (refs, prefix) {
var code = [];
var i;
if (this.signature) {
code.push(prefix + 'if (arguments.length === ' + this.path.length + ') {');
code.push(this.signature.toCode(refs, prefix + ' '));
code.push(prefix + '}');
}
for (i = 0; i < this.childs.length; i++) {
code.push(this.childs[i].toCode(refs, prefix));
}
// TODO: shouldn't the this.param.anyType check be redundant
if (!this.fallThrough || (this.param && this.param.anyType)) {
var exceptions = this._exceptions(refs, prefix);
if (exceptions) {
code.push(exceptions);
}
}
return code.join('\n');
};
/**
* Generate code to throw exceptions
* @param {Refs} refs
* @param {string} prefix
* @returns {string} Returns the inner code as string
* @private
*/
Node.prototype._exceptions = function (refs, prefix) {
var index = this.path.length;
if (this.childs.length === 0) {
// TODO: can this condition be simplified? (we have a fall-through here)
return [
prefix + 'if (arguments.length > ' + index + ') {',
prefix + ' throw createError(name, arguments.length, ' + index + ', arguments[' + index + ']);',
prefix + '}'
].join('\n');
}
else {
var keys = {};
var types = [];
for (var i = 0; i < this.childs.length; i++) {
var node = this.childs[i];
if (node.param) {
for (var j = 0; j < node.param.types.length; j++) {
var type = node.param.types[j];
if (!(type in keys) && !node.param.conversions[j]) {
keys[type] = true;
types.push(type);
}
}
}
}
return prefix + 'throw createError(name, arguments.length, ' + index + ', arguments[' + index + '], \'' + types.join(',') + '\');';
}
};
/**
* Split all raw signatures into an array with expanded Signatures
* @param {Object.<string, Function>} rawSignatures
* @return {Signature[]} Returns an array with expanded signatures
*/
function parseSignatures(rawSignatures) {
// FIXME: need to have deterministic ordering of signatures, do not create via object
var signature;
var keys = {};
var signatures = [];
var i;
for (var types in rawSignatures) {
if (rawSignatures.hasOwnProperty(types)) {
var fn = rawSignatures[types];
signature = new Signature(types, fn);
if (signature.ignore()) {
continue;
}
var expanded = signature.expand();
for (i = 0; i < expanded.length; i++) {
var signature_i = expanded[i];
var key = signature_i.toString();
var existing = keys[key];
if (!existing) {
keys[key] = signature_i;
}
else {
var cmp = Signature.compare(signature_i, existing);
if (cmp < 0) {
// override if sorted first
keys[key] = signature_i;
}
else if (cmp === 0) {
throw new Error('Signature "' + key + '" is defined twice');
}
// else: just ignore
}
}
}
}
// convert from map to array
for (key in keys) {
if (keys.hasOwnProperty(key)) {
signatures.push(keys[key]);
}
}
// order the signatures
signatures.sort(function (a, b) {
return Signature.compare(a, b);
});
// filter redundant conversions from signatures with varArgs
// TODO: simplify this loop or move it to a separate function
for (i = 0; i < signatures.length; i++) {
signature = signatures[i];
if (signature.varArgs) {
var index = signature.params.length - 1;
var param = signature.params[index];
var t = 0;
while (t < param.types.length) {
if (param.conversions[t]) {
var type = param.types[t];
for (var j = 0; j < signatures.length; j++) {
var other = signatures[j];
var p = other.params[index];
if (other !== signature &&
p &&
contains(p.types, type) && !p.conversions[index]) {
// this (conversion) type already exists, remove it
param.types.splice(t, 1);
param.conversions.splice(t, 1);
t--;
break;
}
}
}
t++;
}
}
}
return signatures;
}
/**
* Filter all any type signatures
* @param {Signature[]} signatures
* @return {Signature[]} Returns only any type signatures
*/
function filterAnyTypeSignatures (signatures) {
var filtered = [];
for (var i = 0; i < signatures.length; i++) {
if (signatures[i].anyType) {
filtered.push(signatures[i]);
}
}
return filtered;
}
/**
* create a map with normalized signatures as key and the function as value
* @param {Signature[]} signatures An array with split signatures
* @return {Object.<string, Function>} Returns a map with normalized
* signatures as key, and the function
* as value.
*/
function mapSignatures(signatures) {
var normalized = {};
for (var i = 0; i < signatures.length; i++) {
var signature = signatures[i];
if (signature.fn && !signature.hasConversions()) {
var params = signature.params.join(',');
normalized[params] = signature.fn;
}
}
return normalized;
}
/**
* Parse signatures recursively in a node tree.
* @param {Signature[]} signatures Array with expanded signatures
* @param {Param[]} path Traversed path of parameter types
* @param {Signature[]} anys
* @return {Node} Returns a node tree
*/
function parseTree(signatures, path, anys) {
var i, signature;
var index = path.length;
var nodeSignature;
var filtered = [];
for (i = 0; i < signatures.length; i++) {
signature = signatures[i];
// filter the first signature with the correct number of params
if (signature.params.length === index && !nodeSignature) {
nodeSignature = signature;
}
if (signature.params[index] != undefined) {
filtered.push(signature);
}
}
// sort the filtered signatures by param
filtered.sort(function (a, b) {
return Param.compare(a.params[index], b.params[index]);
});
// recurse over the signatures
var entries = [];
for (i = 0; i < filtered.length; i++) {
signature = filtered[i];
// group signatures with the same param at current index
var param = signature.params[index];
// TODO: replace the next filter loop
var existing = entries.filter(function (entry) {
return entry.param.overlapping(param);
})[0];
//var existing;
//for (var j = 0; j < entries.length; j++) {
// if (entries[j].param.overlapping(param)) {
// existing = entries[j];
// break;
// }
//}
if (existing) {
if (existing.param.varArgs) {
throw new Error('Conflicting types "' + existing.param + '" and "' + param + '"');
}
existing.signatures.push(signature);
}
else {
entries.push({
param: param,
signatures: [signature]
});
}
}
// find all any type signature that can still match our current path
var matchingAnys = [];
for (i = 0; i < anys.length; i++) {
if (anys[i].paramsStartWith(path)) {
matchingAnys.push(anys[i]);
}
}
// see if there are any type signatures that don't match any of the
// signatures that we have in our tree, i.e. we have alternative
// matching signature(s) outside of our current tree and we should
// fall through to them instead of throwing an exception
var fallThrough = false;
for (i = 0; i < matchingAnys.length; i++) {
if (!contains(signatures, matchingAnys[i])) {
fallThrough = true;
break;
}
}
// parse the childs
var childs = new Array(entries.length);
for (i = 0; i < entries.length; i++) {
var entry = entries[i];
childs[i] = parseTree(entry.signatures, path.concat(entry.param), matchingAnys)
}
return new Node(path, nodeSignature, childs, fallThrough);
}
/**
* Generate an array like ['arg0', 'arg1', 'arg2']
* @param {number} count Number of arguments to generate
* @returns {Array} Returns an array with argument names
*/
function getArgs(count) {
// create an array with all argument names
var args = [];
for (var i = 0; i < count; i++) {
args[i] = 'arg' + i;
}
return args;
}
/**
* Compose a function from sub-functions each handling a single type signature.
* Signatures:
* typed(signature: string, fn: function)
* typed(name: string, signature: string, fn: function)
* typed(signatures: Object.<string, function>)
* typed(name: string, signatures: Object.<string, function>)
*
* @param {string | null} name
* @param {Object.<string, Function>} signatures
* @return {Function} Returns the typed function
* @private
*/
function _typed(name, signatures) {
var refs = new Refs();
// parse signatures, expand them
var _signatures = parseSignatures(signatures);
if (_signatures.length == 0) {
throw new Error('No signatures provided');
}
// filter all any type signatures
var anys = filterAnyTypeSignatures(_signatures);
// parse signatures into a node tree
var node = parseTree(_signatures, [], anys);
//var util = require('util');
//console.log('ROOT');
//console.log(util.inspect(node, { depth: null }));
// generate code for the typed function
// safeName is a conservative replacement of characters
// to prevend being able to inject JS code at the place of the function name
// the name is useful for stack trackes therefore we want have it there
var code = [];
var safeName = (name || '').replace(/[^a-zA-Z0-9_$]/g, '_')
var args = getArgs(maxParams(_signatures));
code.push('function ' + safeName + '(' + args.join(', ') + ') {');
code.push(' "use strict";');
code.push(' var name = ' + JSON.stringify(name || '') + ';');
code.push(node.toCode(refs, ' ', false));
code.push('}');
// generate body for the factory function
var body = [
refs.toCode(),
'return ' + code.join('\n')
].join('\n');
// evaluate the JavaScript code and attach function references
var factory = (new Function(refs.name, 'createError', body));
var fn = factory(refs, createError);
//console.log('FN\n' + fn.toString()); // TODO: cleanup
// attach the signatures with sub-functions to the constructed function
fn.signatures = mapSignatures(_signatures);
return fn;
}
/**
* Calculate the maximum number of parameters in givens signatures
* @param {Signature[]} signatures
* @returns {number} The maximum number of parameters
*/
function maxParams(signatures) {
var max = 0;
for (var i = 0; i < signatures.length; i++) {
var len = signatures[i].params.length;
if (len > max) {
max = len;
}
}
return max;
}
/**
* Get the type of a value
* @param {*} x
* @returns {string} Returns a string with the type of value
*/
function getTypeOf(x) {
var obj;
for (var i = 0; i < typed.types.length; i++) {
var entry = typed.types[i];
if (entry.name === 'Object') {
// Array and Date are also Object, so test for Object afterwards
obj = entry;
}
else {
if (entry.test(x)) return entry.name;
}
}
// at last, test whether an object
if (obj && obj.test(x)) return obj.name;
return 'unknown';
}
/**
* Test whether an array contains some item
* @param {Array} array
* @param {*} item
* @return {boolean} Returns true if array contains item, false if not.
*/
function contains(array, item) {
return array.indexOf(item) !== -1;
}
/**
* Returns the last item in the array
* @param {Array} array
* @return {*} item
*/
function last (array) {
return array[array.length - 1];
}
// data type tests
var types = [
{ name: 'number', test: function (x) { return typeof x === 'number' } },
{ name: 'string', test: function (x) { return typeof x === 'string' } },
{ name: 'boolean', test: function (x) { return typeof x === 'boolean' } },
{ name: 'Function', test: function (x) { return typeof x === 'function'} },
{ name: 'Array', test: Array.isArray },
{ name: 'Date', test: function (x) { return x instanceof Date } },
{ name: 'RegExp', test: function (x) { return x instanceof RegExp } },
{ name: 'Object', test: function (x) { return typeof x === 'object' } },
{ name: 'null', test: function (x) { return x === null } },
{ name: 'undefined', test: function (x) { return x === undefined } }
];
// configuration
var config = {};
// type conversions. Order is important
var conversions = [];
// types to be ignored
var ignore = [];
// temporary object for holding types and conversions, for constructing
// the `typed` function itself
// TODO: find a more elegant solution for this
var typed = {
config: config,
types: types,
conversions: conversions,
ignore: ignore
};
/**
* Construct the typed function itself with various signatures
*
* Signatures:
*
* typed(signatures: Object.<string, function>)
* typed(name: string, signatures: Object.<string, function>)
*/
typed = _typed('typed', {
'Object': function (signatures) {
var fns = [];
for (var signature in signatures) {
if (signatures.hasOwnProperty(signature)) {
fns.push(signatures[signature]);
}
}
var name = getName(fns);
return _typed(name, signatures);
},
'string, Object': _typed,
// TODO: add a signature 'Array.<function>'
'...Function': function (fns) {
var err;
var name = getName(fns);
var signatures = {};
for (var i = 0; i < fns.length; i++) {
var fn = fns[i];
// test whether this is a typed-function
if (!(typeof fn.signatures === 'object')) {
err = new TypeError('Function is no typed-function (index: ' + i + ')');
err.data = {index: i};
throw err;
}
// merge the signatures
for (var signature in fn.signatures) {
if (fn.signatures.hasOwnProperty(signature)) {
if (signatures.hasOwnProperty(signature)) {
if (fn.signatures[signature] !== signatures[signature]) {
err = new Error('Signature "' + signature + '" is defined twice');
err.data = {signature: signature};
throw err;
}
// else: both signatures point to the same function, that's fine
}
else {
signatures[signature] = fn.signatures[signature];
}
}
}
}
return _typed(name, signatures);
}
});
/**
* Find a specific signature from a (composed) typed function, for
* example:
*
* typed.find(fn, ['number', 'string'])
* typed.find(fn, 'number, string')
*
* Function find only only works for exact matches.
*
* @param {Function} fn A typed-function
* @param {string | string[]} signature Signature to be found, can be
* an array or a comma separated string.
* @return {Function} Returns the matching signature, or
* throws an errror when no signature
* is found.
*/
function find (fn, signature) {
if (!fn.signatures) {
throw new TypeError('Function is no typed-function');
}
// normalize input
var arr;
if (typeof signature === 'string') {
arr = signature.split(',');
for (var i = 0; i < arr.length; i++) {
arr[i] = arr[i].trim();
}
}
else if (Array.isArray(signature)) {
arr = signature;
}
else {
throw new TypeError('String array or a comma separated string expected');
}
var str = arr.join(',');
// find an exact match
var match = fn.signatures[str];
if (match) {
return match;
}
// TODO: extend find to match non-exact signatures
throw new TypeError('Signature not found (signature: ' + (fn.name || 'unnamed') + '(' + arr.join(', ') + '))');
}
/**
* Convert a given value to another data type.
* @param {*} value
* @param {string} type
*/
function convert (value, type) {
var from = getTypeOf(value);
// check conversion is needed
if (type === from) {
return value;
}
for (var i = 0; i < typed.conversions.length; i++) {
var conversion = typed.conversions[i];
if (conversion.from === from && conversion.to === type) {
return conversion.convert(value);
}
}
throw new Error('Cannot convert from ' + from + ' to ' + type);
}
// attach types and conversions to the final `typed` function
typed.config = config;
typed.types = types;
typed.conversions = conversions;
typed.ignore = ignore;
typed.create = create;
typed.find = find;
typed.convert = convert;
// add a type
typed.addType = function (type) {
if (!type || typeof type.name !== 'string' || typeof type.test !== 'function') {
throw new TypeError('Object with properties {name: string, test: function} expected');
}
typed.types.push(type);
};
// add a conversion
typed.addConversion = function (conversion) {
if (!conversion
|| typeof conversion.from !== 'string'
|| typeof conversion.to !== 'string'
|| typeof conversion.convert !== 'function') {
throw new TypeError('Object with properties {from: string, to: string, convert: function} expected');
}
typed.conversions.push(conversion);
};
return typed;
}
return create();
}));