typed-function.js (43896B)
1 /** 2 * typed-function 3 * 4 * Type checking for JavaScript functions 5 * 6 * https://github.com/josdejong/typed-function 7 */ 8 'use strict'; 9 10 (function (root, factory) { 11 if (typeof define === 'function' && define.amd) { 12 // AMD. Register as an anonymous module. 13 define([], factory); 14 } else if (typeof exports === 'object') { 15 // OldNode. Does not work with strict CommonJS, but 16 // only CommonJS-like environments that support module.exports, 17 // like OldNode. 18 module.exports = factory(); 19 } else { 20 // Browser globals (root is window) 21 root.typed = factory(); 22 } 23 }(this, function () { 24 25 function ok () { 26 return true; 27 } 28 29 function notOk () { 30 return false; 31 } 32 33 function undef () { 34 return undefined; 35 } 36 37 /** 38 * @typedef {{ 39 * params: Param[], 40 * fn: function 41 * }} Signature 42 * 43 * @typedef {{ 44 * types: Type[], 45 * restParam: boolean 46 * }} Param 47 * 48 * @typedef {{ 49 * name: string, 50 * typeIndex: number, 51 * test: function, 52 * conversion?: ConversionDef, 53 * conversionIndex: number, 54 * }} Type 55 * 56 * @typedef {{ 57 * from: string, 58 * to: string, 59 * convert: function (*) : * 60 * }} ConversionDef 61 * 62 * @typedef {{ 63 * name: string, 64 * test: function(*) : boolean 65 * }} TypeDef 66 */ 67 68 // create a new instance of typed-function 69 function create () { 70 // data type tests 71 var _types = [ 72 { name: 'number', test: function (x) { return typeof x === 'number' } }, 73 { name: 'string', test: function (x) { return typeof x === 'string' } }, 74 { name: 'boolean', test: function (x) { return typeof x === 'boolean' } }, 75 { name: 'Function', test: function (x) { return typeof x === 'function'} }, 76 { name: 'Array', test: Array.isArray }, 77 { name: 'Date', test: function (x) { return x instanceof Date } }, 78 { name: 'RegExp', test: function (x) { return x instanceof RegExp } }, 79 { name: 'Object', test: function (x) { 80 return typeof x === 'object' && x !== null && x.constructor === Object 81 }}, 82 { name: 'null', test: function (x) { return x === null } }, 83 { name: 'undefined', test: function (x) { return x === undefined } } 84 ]; 85 86 var anyType = { 87 name: 'any', 88 test: ok 89 } 90 91 // types which need to be ignored 92 var _ignore = []; 93 94 // type conversions 95 var _conversions = []; 96 97 // This is a temporary object, will be replaced with a typed function at the end 98 var typed = { 99 types: _types, 100 conversions: _conversions, 101 ignore: _ignore 102 }; 103 104 /** 105 * Find the test function for a type 106 * @param {String} typeName 107 * @return {TypeDef} Returns the type definition when found, 108 * Throws a TypeError otherwise 109 */ 110 function findTypeByName (typeName) { 111 var entry = findInArray(typed.types, function (entry) { 112 return entry.name === typeName; 113 }); 114 115 if (entry) { 116 return entry; 117 } 118 119 if (typeName === 'any') { // special baked-in case 'any' 120 return anyType; 121 } 122 123 var hint = findInArray(typed.types, function (entry) { 124 return entry.name.toLowerCase() === typeName.toLowerCase(); 125 }); 126 127 throw new TypeError('Unknown type "' + typeName + '"' + 128 (hint ? ('. Did you mean "' + hint.name + '"?') : '')); 129 } 130 131 /** 132 * Find the index of a type definition. Handles special case 'any' 133 * @param {TypeDef} type 134 * @return {number} 135 */ 136 function findTypeIndex(type) { 137 if (type === anyType) { 138 return 999; 139 } 140 141 return typed.types.indexOf(type); 142 } 143 144 /** 145 * Find a type that matches a value. 146 * @param {*} value 147 * @return {string} Returns the name of the first type for which 148 * the type test matches the value. 149 */ 150 function findTypeName(value) { 151 var entry = findInArray(typed.types, function (entry) { 152 return entry.test(value); 153 }); 154 155 if (entry) { 156 return entry.name; 157 } 158 159 throw new TypeError('Value has unknown type. Value: ' + value); 160 } 161 162 /** 163 * Find a specific signature from a (composed) typed function, for example: 164 * 165 * typed.find(fn, ['number', 'string']) 166 * typed.find(fn, 'number, string') 167 * 168 * Function find only only works for exact matches. 169 * 170 * @param {Function} fn A typed-function 171 * @param {string | string[]} signature Signature to be found, can be 172 * an array or a comma separated string. 173 * @return {Function} Returns the matching signature, or 174 * throws an error when no signature 175 * is found. 176 */ 177 function find (fn, signature) { 178 if (!fn.signatures) { 179 throw new TypeError('Function is no typed-function'); 180 } 181 182 // normalize input 183 var arr; 184 if (typeof signature === 'string') { 185 arr = signature.split(','); 186 for (var i = 0; i < arr.length; i++) { 187 arr[i] = arr[i].trim(); 188 } 189 } 190 else if (Array.isArray(signature)) { 191 arr = signature; 192 } 193 else { 194 throw new TypeError('String array or a comma separated string expected'); 195 } 196 197 var str = arr.join(','); 198 199 // find an exact match 200 var match = fn.signatures[str]; 201 if (match) { 202 return match; 203 } 204 205 // TODO: extend find to match non-exact signatures 206 207 throw new TypeError('Signature not found (signature: ' + (fn.name || 'unnamed') + '(' + arr.join(', ') + '))'); 208 } 209 210 /** 211 * Convert a given value to another data type. 212 * @param {*} value 213 * @param {string} type 214 */ 215 function convert (value, type) { 216 var from = findTypeName(value); 217 218 // check conversion is needed 219 if (type === from) { 220 return value; 221 } 222 223 for (var i = 0; i < typed.conversions.length; i++) { 224 var conversion = typed.conversions[i]; 225 if (conversion.from === from && conversion.to === type) { 226 return conversion.convert(value); 227 } 228 } 229 230 throw new Error('Cannot convert from ' + from + ' to ' + type); 231 } 232 233 /** 234 * Stringify parameters in a normalized way 235 * @param {Param[]} params 236 * @return {string} 237 */ 238 function stringifyParams (params) { 239 return params 240 .map(function (param) { 241 var typeNames = param.types.map(getTypeName); 242 243 return (param.restParam ? '...' : '') + typeNames.join('|'); 244 }) 245 .join(','); 246 } 247 248 /** 249 * Parse a parameter, like "...number | boolean" 250 * @param {string} param 251 * @param {ConversionDef[]} conversions 252 * @return {Param} param 253 */ 254 function parseParam (param, conversions) { 255 var restParam = param.indexOf('...') === 0; 256 var types = (!restParam) 257 ? param 258 : (param.length > 3) 259 ? param.slice(3) 260 : 'any'; 261 262 var typeNames = types.split('|').map(trim) 263 .filter(notEmpty) 264 .filter(notIgnore); 265 266 var matchingConversions = filterConversions(conversions, typeNames); 267 268 var exactTypes = typeNames.map(function (typeName) { 269 var type = findTypeByName(typeName); 270 271 return { 272 name: typeName, 273 typeIndex: findTypeIndex(type), 274 test: type.test, 275 conversion: null, 276 conversionIndex: -1 277 }; 278 }); 279 280 var convertibleTypes = matchingConversions.map(function (conversion) { 281 var type = findTypeByName(conversion.from); 282 283 return { 284 name: conversion.from, 285 typeIndex: findTypeIndex(type), 286 test: type.test, 287 conversion: conversion, 288 conversionIndex: conversions.indexOf(conversion) 289 }; 290 }); 291 292 return { 293 types: exactTypes.concat(convertibleTypes), 294 restParam: restParam 295 }; 296 } 297 298 /** 299 * Parse a signature with comma separated parameters, 300 * like "number | boolean, ...string" 301 * @param {string} signature 302 * @param {function} fn 303 * @param {ConversionDef[]} conversions 304 * @return {Signature | null} signature 305 */ 306 function parseSignature (signature, fn, conversions) { 307 var params = []; 308 309 if (signature.trim() !== '') { 310 params = signature 311 .split(',') 312 .map(trim) 313 .map(function (param, index, array) { 314 var parsedParam = parseParam(param, conversions); 315 316 if (parsedParam.restParam && (index !== array.length - 1)) { 317 throw new SyntaxError('Unexpected rest parameter "' + param + '": ' + 318 'only allowed for the last parameter'); 319 } 320 321 return parsedParam; 322 }); 323 } 324 325 if (params.some(isInvalidParam)) { 326 // invalid signature: at least one parameter has no types 327 // (they may have been filtered) 328 return null; 329 } 330 331 return { 332 params: params, 333 fn: fn 334 }; 335 } 336 337 /** 338 * Test whether a set of params contains a restParam 339 * @param {Param[]} params 340 * @return {boolean} Returns true when the last parameter is a restParam 341 */ 342 function hasRestParam(params) { 343 var param = last(params) 344 return param ? param.restParam : false; 345 } 346 347 /** 348 * Test whether a parameter contains conversions 349 * @param {Param} param 350 * @return {boolean} Returns true when at least one of the parameters 351 * contains a conversion. 352 */ 353 function hasConversions(param) { 354 return param.types.some(function (type) { 355 return type.conversion != null; 356 }); 357 } 358 359 /** 360 * Create a type test for a single parameter, which can have one or multiple 361 * types. 362 * @param {Param} param 363 * @return {function(x: *) : boolean} Returns a test function 364 */ 365 function compileTest(param) { 366 if (!param || param.types.length === 0) { 367 // nothing to do 368 return ok; 369 } 370 else if (param.types.length === 1) { 371 return findTypeByName(param.types[0].name).test; 372 } 373 else if (param.types.length === 2) { 374 var test0 = findTypeByName(param.types[0].name).test; 375 var test1 = findTypeByName(param.types[1].name).test; 376 return function or(x) { 377 return test0(x) || test1(x); 378 } 379 } 380 else { // param.types.length > 2 381 var tests = param.types.map(function (type) { 382 return findTypeByName(type.name).test; 383 }) 384 return function or(x) { 385 for (var i = 0; i < tests.length; i++) { 386 if (tests[i](x)) { 387 return true; 388 } 389 } 390 return false; 391 } 392 } 393 } 394 395 /** 396 * Create a test for all parameters of a signature 397 * @param {Param[]} params 398 * @return {function(args: Array<*>) : boolean} 399 */ 400 function compileTests(params) { 401 var tests, test0, test1; 402 403 if (hasRestParam(params)) { 404 // variable arguments like '...number' 405 tests = initial(params).map(compileTest); 406 var varIndex = tests.length; 407 var lastTest = compileTest(last(params)); 408 var testRestParam = function (args) { 409 for (var i = varIndex; i < args.length; i++) { 410 if (!lastTest(args[i])) { 411 return false; 412 } 413 } 414 return true; 415 } 416 417 return function testArgs(args) { 418 for (var i = 0; i < tests.length; i++) { 419 if (!tests[i](args[i])) { 420 return false; 421 } 422 } 423 return testRestParam(args) && (args.length >= varIndex + 1); 424 }; 425 } 426 else { 427 // no variable arguments 428 if (params.length === 0) { 429 return function testArgs(args) { 430 return args.length === 0; 431 }; 432 } 433 else if (params.length === 1) { 434 test0 = compileTest(params[0]); 435 return function testArgs(args) { 436 return test0(args[0]) && args.length === 1; 437 }; 438 } 439 else if (params.length === 2) { 440 test0 = compileTest(params[0]); 441 test1 = compileTest(params[1]); 442 return function testArgs(args) { 443 return test0(args[0]) && test1(args[1]) && args.length === 2; 444 }; 445 } 446 else { // arguments.length > 2 447 tests = params.map(compileTest); 448 return function testArgs(args) { 449 for (var i = 0; i < tests.length; i++) { 450 if (!tests[i](args[i])) { 451 return false; 452 } 453 } 454 return args.length === tests.length; 455 }; 456 } 457 } 458 } 459 460 /** 461 * Find the parameter at a specific index of a signature. 462 * Handles rest parameters. 463 * @param {Signature} signature 464 * @param {number} index 465 * @return {Param | null} Returns the matching parameter when found, 466 * null otherwise. 467 */ 468 function getParamAtIndex(signature, index) { 469 return index < signature.params.length 470 ? signature.params[index] 471 : hasRestParam(signature.params) 472 ? last(signature.params) 473 : null 474 } 475 476 /** 477 * Get all type names of a parameter 478 * @param {Signature} signature 479 * @param {number} index 480 * @param {boolean} excludeConversions 481 * @return {string[]} Returns an array with type names 482 */ 483 function getExpectedTypeNames (signature, index, excludeConversions) { 484 var param = getParamAtIndex(signature, index); 485 var types = param 486 ? excludeConversions 487 ? param.types.filter(isExactType) 488 : param.types 489 : []; 490 491 return types.map(getTypeName); 492 } 493 494 /** 495 * Returns the name of a type 496 * @param {Type} type 497 * @return {string} Returns the type name 498 */ 499 function getTypeName(type) { 500 return type.name; 501 } 502 503 /** 504 * Test whether a type is an exact type or conversion 505 * @param {Type} type 506 * @return {boolean} Returns true when 507 */ 508 function isExactType(type) { 509 return type.conversion === null || type.conversion === undefined; 510 } 511 512 /** 513 * Helper function for creating error messages: create an array with 514 * all available types on a specific argument index. 515 * @param {Signature[]} signatures 516 * @param {number} index 517 * @return {string[]} Returns an array with available types 518 */ 519 function mergeExpectedParams(signatures, index) { 520 var typeNames = uniq(flatMap(signatures, function (signature) { 521 return getExpectedTypeNames(signature, index, false); 522 })); 523 524 return (typeNames.indexOf('any') !== -1) ? ['any'] : typeNames; 525 } 526 527 /** 528 * Create 529 * @param {string} name The name of the function 530 * @param {array.<*>} args The actual arguments passed to the function 531 * @param {Signature[]} signatures A list with available signatures 532 * @return {TypeError} Returns a type error with additional data 533 * attached to it in the property `data` 534 */ 535 function createError(name, args, signatures) { 536 var err, expected; 537 var _name = name || 'unnamed'; 538 539 // test for wrong type at some index 540 var matchingSignatures = signatures; 541 var index; 542 for (index = 0; index < args.length; index++) { 543 var nextMatchingDefs = matchingSignatures.filter(function (signature) { 544 var test = compileTest(getParamAtIndex(signature, index)); 545 return (index < signature.params.length || hasRestParam(signature.params)) && 546 test(args[index]); 547 }); 548 549 if (nextMatchingDefs.length === 0) { 550 // no matching signatures anymore, throw error "wrong type" 551 expected = mergeExpectedParams(matchingSignatures, index); 552 if (expected.length > 0) { 553 var actualType = findTypeName(args[index]); 554 555 err = new TypeError('Unexpected type of argument in function ' + _name + 556 ' (expected: ' + expected.join(' or ') + 557 ', actual: ' + actualType + ', index: ' + index + ')'); 558 err.data = { 559 category: 'wrongType', 560 fn: _name, 561 index: index, 562 actual: actualType, 563 expected: expected 564 } 565 return err; 566 } 567 } 568 else { 569 matchingSignatures = nextMatchingDefs; 570 } 571 } 572 573 // test for too few arguments 574 var lengths = matchingSignatures.map(function (signature) { 575 return hasRestParam(signature.params) ? Infinity : signature.params.length; 576 }); 577 if (args.length < Math.min.apply(null, lengths)) { 578 expected = mergeExpectedParams(matchingSignatures, index); 579 err = new TypeError('Too few arguments in function ' + _name + 580 ' (expected: ' + expected.join(' or ') + 581 ', index: ' + args.length + ')'); 582 err.data = { 583 category: 'tooFewArgs', 584 fn: _name, 585 index: args.length, 586 expected: expected 587 } 588 return err; 589 } 590 591 // test for too many arguments 592 var maxLength = Math.max.apply(null, lengths); 593 if (args.length > maxLength) { 594 err = new TypeError('Too many arguments in function ' + _name + 595 ' (expected: ' + maxLength + ', actual: ' + args.length + ')'); 596 err.data = { 597 category: 'tooManyArgs', 598 fn: _name, 599 index: args.length, 600 expectedLength: maxLength 601 } 602 return err; 603 } 604 605 err = new TypeError('Arguments of type "' + args.join(', ') + 606 '" do not match any of the defined signatures of function ' + _name + '.'); 607 err.data = { 608 category: 'mismatch', 609 actual: args.map(findTypeName) 610 } 611 return err; 612 } 613 614 /** 615 * Find the lowest index of all exact types of a parameter (no conversions) 616 * @param {Param} param 617 * @return {number} Returns the index of the lowest type in typed.types 618 */ 619 function getLowestTypeIndex (param) { 620 var min = 999; 621 622 for (var i = 0; i < param.types.length; i++) { 623 if (isExactType(param.types[i])) { 624 min = Math.min(min, param.types[i].typeIndex); 625 } 626 } 627 628 return min; 629 } 630 631 /** 632 * Find the lowest index of the conversion of all types of the parameter 633 * having a conversion 634 * @param {Param} param 635 * @return {number} Returns the lowest index of the conversions of this type 636 */ 637 function getLowestConversionIndex (param) { 638 var min = 999; 639 640 for (var i = 0; i < param.types.length; i++) { 641 if (!isExactType(param.types[i])) { 642 min = Math.min(min, param.types[i].conversionIndex); 643 } 644 } 645 646 return min; 647 } 648 649 /** 650 * Compare two params 651 * @param {Param} param1 652 * @param {Param} param2 653 * @return {number} returns a negative number when param1 must get a lower 654 * index than param2, a positive number when the opposite, 655 * or zero when both are equal 656 */ 657 function compareParams (param1, param2) { 658 var c; 659 660 // compare having a rest parameter or not 661 c = param1.restParam - param2.restParam; 662 if (c !== 0) { 663 return c; 664 } 665 666 // compare having conversions or not 667 c = hasConversions(param1) - hasConversions(param2); 668 if (c !== 0) { 669 return c; 670 } 671 672 // compare the index of the types 673 c = getLowestTypeIndex(param1) - getLowestTypeIndex(param2); 674 if (c !== 0) { 675 return c; 676 } 677 678 // compare the index of any conversion 679 return getLowestConversionIndex(param1) - getLowestConversionIndex(param2); 680 } 681 682 /** 683 * Compare two signatures 684 * @param {Signature} signature1 685 * @param {Signature} signature2 686 * @return {number} returns a negative number when param1 must get a lower 687 * index than param2, a positive number when the opposite, 688 * or zero when both are equal 689 */ 690 function compareSignatures (signature1, signature2) { 691 var len = Math.min(signature1.params.length, signature2.params.length); 692 var i; 693 var c; 694 695 // compare whether the params have conversions at all or not 696 c = signature1.params.some(hasConversions) - signature2.params.some(hasConversions) 697 if (c !== 0) { 698 return c; 699 } 700 701 // next compare whether the params have conversions one by one 702 for (i = 0; i < len; i++) { 703 c = hasConversions(signature1.params[i]) - hasConversions(signature2.params[i]); 704 if (c !== 0) { 705 return c; 706 } 707 } 708 709 // compare the types of the params one by one 710 for (i = 0; i < len; i++) { 711 c = compareParams(signature1.params[i], signature2.params[i]); 712 if (c !== 0) { 713 return c; 714 } 715 } 716 717 // compare the number of params 718 return signature1.params.length - signature2.params.length; 719 } 720 721 /** 722 * Get params containing all types that can be converted to the defined types. 723 * 724 * @param {ConversionDef[]} conversions 725 * @param {string[]} typeNames 726 * @return {ConversionDef[]} Returns the conversions that are available 727 * for every type (if any) 728 */ 729 function filterConversions(conversions, typeNames) { 730 var matches = {}; 731 732 conversions.forEach(function (conversion) { 733 if (typeNames.indexOf(conversion.from) === -1 && 734 typeNames.indexOf(conversion.to) !== -1 && 735 !matches[conversion.from]) { 736 matches[conversion.from] = conversion; 737 } 738 }); 739 740 return Object.keys(matches).map(function (from) { 741 return matches[from]; 742 }); 743 } 744 745 /** 746 * Preprocess arguments before calling the original function: 747 * - if needed convert the parameters 748 * - in case of rest parameters, move the rest parameters into an Array 749 * @param {Param[]} params 750 * @param {function} fn 751 * @return {function} Returns a wrapped function 752 */ 753 function compileArgsPreprocessing(params, fn) { 754 var fnConvert = fn; 755 756 // TODO: can we make this wrapper function smarter/simpler? 757 758 if (params.some(hasConversions)) { 759 var restParam = hasRestParam(params); 760 var compiledConversions = params.map(compileArgConversion) 761 762 fnConvert = function convertArgs() { 763 var args = []; 764 var last = restParam ? arguments.length - 1 : arguments.length; 765 for (var i = 0; i < last; i++) { 766 args[i] = compiledConversions[i](arguments[i]); 767 } 768 if (restParam) { 769 args[last] = arguments[last].map(compiledConversions[last]); 770 } 771 772 return fn.apply(this, args); 773 } 774 } 775 776 var fnPreprocess = fnConvert; 777 if (hasRestParam(params)) { 778 var offset = params.length - 1; 779 780 fnPreprocess = function preprocessRestParams () { 781 return fnConvert.apply(this, 782 slice(arguments, 0, offset).concat([slice(arguments, offset)])); 783 } 784 } 785 786 return fnPreprocess; 787 } 788 789 /** 790 * Compile conversion for a parameter to the right type 791 * @param {Param} param 792 * @return {function} Returns the wrapped function that will convert arguments 793 * 794 */ 795 function compileArgConversion(param) { 796 var test0, test1, conversion0, conversion1; 797 var tests = []; 798 var conversions = []; 799 800 param.types.forEach(function (type) { 801 if (type.conversion) { 802 tests.push(findTypeByName(type.conversion.from).test); 803 conversions.push(type.conversion.convert); 804 } 805 }); 806 807 // create optimized conversion functions depending on the number of conversions 808 switch (conversions.length) { 809 case 0: 810 return function convertArg(arg) { 811 return arg; 812 } 813 814 case 1: 815 test0 = tests[0] 816 conversion0 = conversions[0]; 817 return function convertArg(arg) { 818 if (test0(arg)) { 819 return conversion0(arg) 820 } 821 return arg; 822 } 823 824 case 2: 825 test0 = tests[0] 826 test1 = tests[1] 827 conversion0 = conversions[0]; 828 conversion1 = conversions[1]; 829 return function convertArg(arg) { 830 if (test0(arg)) { 831 return conversion0(arg) 832 } 833 if (test1(arg)) { 834 return conversion1(arg) 835 } 836 return arg; 837 } 838 839 default: 840 return function convertArg(arg) { 841 for (var i = 0; i < conversions.length; i++) { 842 if (tests[i](arg)) { 843 return conversions[i](arg); 844 } 845 } 846 return arg; 847 } 848 } 849 } 850 851 /** 852 * Convert an array with signatures into a map with signatures, 853 * where signatures with union types are split into separate signatures 854 * 855 * Throws an error when there are conflicting types 856 * 857 * @param {Signature[]} signatures 858 * @return {Object.<string, function>} Returns a map with signatures 859 * as key and the original function 860 * of this signature as value. 861 */ 862 function createSignaturesMap(signatures) { 863 var signaturesMap = {}; 864 signatures.forEach(function (signature) { 865 if (!signature.params.some(hasConversions)) { 866 splitParams(signature.params, true).forEach(function (params) { 867 signaturesMap[stringifyParams(params)] = signature.fn; 868 }); 869 } 870 }); 871 872 return signaturesMap; 873 } 874 875 /** 876 * Split params with union types in to separate params. 877 * 878 * For example: 879 * 880 * splitParams([['Array', 'Object'], ['string', 'RegExp']) 881 * // returns: 882 * // [ 883 * // ['Array', 'string'], 884 * // ['Array', 'RegExp'], 885 * // ['Object', 'string'], 886 * // ['Object', 'RegExp'] 887 * // ] 888 * 889 * @param {Param[]} params 890 * @param {boolean} ignoreConversionTypes 891 * @return {Param[]} 892 */ 893 function splitParams(params, ignoreConversionTypes) { 894 function _splitParams(params, index, types) { 895 if (index < params.length) { 896 var param = params[index] 897 var filteredTypes = ignoreConversionTypes 898 ? param.types.filter(isExactType) 899 : param.types; 900 var typeGroups 901 902 if (param.restParam) { 903 // split the types of a rest parameter in two: 904 // one with only exact types, and one with exact types and conversions 905 var exactTypes = filteredTypes.filter(isExactType) 906 typeGroups = exactTypes.length < filteredTypes.length 907 ? [exactTypes, filteredTypes] 908 : [filteredTypes] 909 910 } 911 else { 912 // split all the types of a regular parameter into one type per group 913 typeGroups = filteredTypes.map(function (type) { 914 return [type] 915 }) 916 } 917 918 // recurse over the groups with types 919 return flatMap(typeGroups, function (typeGroup) { 920 return _splitParams(params, index + 1, types.concat([typeGroup])); 921 }); 922 923 } 924 else { 925 // we've reached the end of the parameters. Now build a new Param 926 var splittedParams = types.map(function (type, typeIndex) { 927 return { 928 types: type, 929 restParam: (typeIndex === params.length - 1) && hasRestParam(params) 930 } 931 }); 932 933 return [splittedParams]; 934 } 935 } 936 937 return _splitParams(params, 0, []); 938 } 939 940 /** 941 * Test whether two signatures have a conflicting signature 942 * @param {Signature} signature1 943 * @param {Signature} signature2 944 * @return {boolean} Returns true when the signatures conflict, false otherwise. 945 */ 946 function hasConflictingParams(signature1, signature2) { 947 var ii = Math.max(signature1.params.length, signature2.params.length); 948 949 for (var i = 0; i < ii; i++) { 950 var typesNames1 = getExpectedTypeNames(signature1, i, true); 951 var typesNames2 = getExpectedTypeNames(signature2, i, true); 952 953 if (!hasOverlap(typesNames1, typesNames2)) { 954 return false; 955 } 956 } 957 958 var len1 = signature1.params.length; 959 var len2 = signature2.params.length; 960 var restParam1 = hasRestParam(signature1.params); 961 var restParam2 = hasRestParam(signature2.params); 962 963 return restParam1 964 ? restParam2 ? (len1 === len2) : (len2 >= len1) 965 : restParam2 ? (len1 >= len2) : (len1 === len2) 966 } 967 968 /** 969 * Create a typed function 970 * @param {String} name The name for the typed function 971 * @param {Object.<string, function>} signaturesMap 972 * An object with one or 973 * multiple signatures as key, and the 974 * function corresponding to the 975 * signature as value. 976 * @return {function} Returns the created typed function. 977 */ 978 function createTypedFunction(name, signaturesMap) { 979 if (Object.keys(signaturesMap).length === 0) { 980 throw new SyntaxError('No signatures provided'); 981 } 982 983 // parse the signatures, and check for conflicts 984 var parsedSignatures = []; 985 Object.keys(signaturesMap) 986 .map(function (signature) { 987 return parseSignature(signature, signaturesMap[signature], typed.conversions); 988 }) 989 .filter(notNull) 990 .forEach(function (parsedSignature) { 991 // check whether this parameter conflicts with already parsed signatures 992 var conflictingSignature = findInArray(parsedSignatures, function (s) { 993 return hasConflictingParams(s, parsedSignature) 994 }); 995 if (conflictingSignature) { 996 throw new TypeError('Conflicting signatures "' + 997 stringifyParams(conflictingSignature.params) + '" and "' + 998 stringifyParams(parsedSignature.params) + '".'); 999 } 1000 1001 parsedSignatures.push(parsedSignature); 1002 }); 1003 1004 // split and filter the types of the signatures, and then order them 1005 var signatures = flatMap(parsedSignatures, function (parsedSignature) { 1006 var params = parsedSignature ? splitParams(parsedSignature.params, false) : [] 1007 1008 return params.map(function (params) { 1009 return { 1010 params: params, 1011 fn: parsedSignature.fn 1012 }; 1013 }); 1014 }).filter(notNull); 1015 1016 signatures.sort(compareSignatures); 1017 1018 // we create a highly optimized checks for the first couple of signatures with max 2 arguments 1019 var ok0 = signatures[0] && signatures[0].params.length <= 2 && !hasRestParam(signatures[0].params); 1020 var ok1 = signatures[1] && signatures[1].params.length <= 2 && !hasRestParam(signatures[1].params); 1021 var ok2 = signatures[2] && signatures[2].params.length <= 2 && !hasRestParam(signatures[2].params); 1022 var ok3 = signatures[3] && signatures[3].params.length <= 2 && !hasRestParam(signatures[3].params); 1023 var ok4 = signatures[4] && signatures[4].params.length <= 2 && !hasRestParam(signatures[4].params); 1024 var ok5 = signatures[5] && signatures[5].params.length <= 2 && !hasRestParam(signatures[5].params); 1025 var allOk = ok0 && ok1 && ok2 && ok3 && ok4 && ok5; 1026 1027 // compile the tests 1028 var tests = signatures.map(function (signature) { 1029 return compileTests(signature.params); 1030 }); 1031 1032 var test00 = ok0 ? compileTest(signatures[0].params[0]) : notOk; 1033 var test10 = ok1 ? compileTest(signatures[1].params[0]) : notOk; 1034 var test20 = ok2 ? compileTest(signatures[2].params[0]) : notOk; 1035 var test30 = ok3 ? compileTest(signatures[3].params[0]) : notOk; 1036 var test40 = ok4 ? compileTest(signatures[4].params[0]) : notOk; 1037 var test50 = ok5 ? compileTest(signatures[5].params[0]) : notOk; 1038 1039 var test01 = ok0 ? compileTest(signatures[0].params[1]) : notOk; 1040 var test11 = ok1 ? compileTest(signatures[1].params[1]) : notOk; 1041 var test21 = ok2 ? compileTest(signatures[2].params[1]) : notOk; 1042 var test31 = ok3 ? compileTest(signatures[3].params[1]) : notOk; 1043 var test41 = ok4 ? compileTest(signatures[4].params[1]) : notOk; 1044 var test51 = ok5 ? compileTest(signatures[5].params[1]) : notOk; 1045 1046 // compile the functions 1047 var fns = signatures.map(function(signature) { 1048 return compileArgsPreprocessing(signature.params, signature.fn); 1049 }); 1050 1051 var fn0 = ok0 ? fns[0] : undef; 1052 var fn1 = ok1 ? fns[1] : undef; 1053 var fn2 = ok2 ? fns[2] : undef; 1054 var fn3 = ok3 ? fns[3] : undef; 1055 var fn4 = ok4 ? fns[4] : undef; 1056 var fn5 = ok5 ? fns[5] : undef; 1057 1058 var len0 = ok0 ? signatures[0].params.length : -1; 1059 var len1 = ok1 ? signatures[1].params.length : -1; 1060 var len2 = ok2 ? signatures[2].params.length : -1; 1061 var len3 = ok3 ? signatures[3].params.length : -1; 1062 var len4 = ok4 ? signatures[4].params.length : -1; 1063 var len5 = ok5 ? signatures[5].params.length : -1; 1064 1065 // simple and generic, but also slow 1066 var iStart = allOk ? 6 : 0; 1067 var iEnd = signatures.length; 1068 var generic = function generic() { 1069 'use strict'; 1070 1071 for (var i = iStart; i < iEnd; i++) { 1072 if (tests[i](arguments)) { 1073 return fns[i].apply(this, arguments); 1074 } 1075 } 1076 1077 return typed.onMismatch(name, arguments, signatures); 1078 } 1079 1080 // create the typed function 1081 // fast, specialized version. Falls back to the slower, generic one if needed 1082 var fn = function fn(arg0, arg1) { 1083 'use strict'; 1084 1085 if (arguments.length === len0 && test00(arg0) && test01(arg1)) { return fn0.apply(fn, arguments); } 1086 if (arguments.length === len1 && test10(arg0) && test11(arg1)) { return fn1.apply(fn, arguments); } 1087 if (arguments.length === len2 && test20(arg0) && test21(arg1)) { return fn2.apply(fn, arguments); } 1088 if (arguments.length === len3 && test30(arg0) && test31(arg1)) { return fn3.apply(fn, arguments); } 1089 if (arguments.length === len4 && test40(arg0) && test41(arg1)) { return fn4.apply(fn, arguments); } 1090 if (arguments.length === len5 && test50(arg0) && test51(arg1)) { return fn5.apply(fn, arguments); } 1091 1092 return generic.apply(fn, arguments); 1093 } 1094 1095 // attach name the typed function 1096 try { 1097 Object.defineProperty(fn, 'name', {value: name}); 1098 } 1099 catch (err) { 1100 // old browsers do not support Object.defineProperty and some don't support setting the name property 1101 // the function name is not essential for the functioning, it's mostly useful for debugging, 1102 // so it's fine to have unnamed functions. 1103 } 1104 1105 // attach signatures to the function 1106 fn.signatures = createSignaturesMap(signatures); 1107 1108 return fn; 1109 } 1110 1111 /** 1112 * Action to take on mismatch 1113 * @param {string} name Name of function that was attempted to be called 1114 * @param {Array} args Actual arguments to the call 1115 * @param {Array} signatures Known signatures of the named typed-function 1116 */ 1117 function _onMismatch(name, args, signatures) { 1118 throw createError(name, args, signatures); 1119 } 1120 1121 /** 1122 * Test whether a type should be NOT be ignored 1123 * @param {string} typeName 1124 * @return {boolean} 1125 */ 1126 function notIgnore(typeName) { 1127 return typed.ignore.indexOf(typeName) === -1; 1128 } 1129 1130 /** 1131 * trim a string 1132 * @param {string} str 1133 * @return {string} 1134 */ 1135 function trim(str) { 1136 return str.trim(); 1137 } 1138 1139 /** 1140 * Test whether a string is not empty 1141 * @param {string} str 1142 * @return {boolean} 1143 */ 1144 function notEmpty(str) { 1145 return !!str; 1146 } 1147 1148 /** 1149 * test whether a value is not strict equal to null 1150 * @param {*} value 1151 * @return {boolean} 1152 */ 1153 function notNull(value) { 1154 return value !== null; 1155 } 1156 1157 /** 1158 * Test whether a parameter has no types defined 1159 * @param {Param} param 1160 * @return {boolean} 1161 */ 1162 function isInvalidParam (param) { 1163 return param.types.length === 0; 1164 } 1165 1166 /** 1167 * Return all but the last items of an array 1168 * @param {Array} arr 1169 * @return {Array} 1170 */ 1171 function initial(arr) { 1172 return arr.slice(0, arr.length - 1); 1173 } 1174 1175 /** 1176 * return the last item of an array 1177 * @param {Array} arr 1178 * @return {*} 1179 */ 1180 function last(arr) { 1181 return arr[arr.length - 1]; 1182 } 1183 1184 /** 1185 * Slice an array or function Arguments 1186 * @param {Array | Arguments | IArguments} arr 1187 * @param {number} start 1188 * @param {number} [end] 1189 * @return {Array} 1190 */ 1191 function slice(arr, start, end) { 1192 return Array.prototype.slice.call(arr, start, end); 1193 } 1194 1195 /** 1196 * Test whether an array contains some item 1197 * @param {Array} array 1198 * @param {*} item 1199 * @return {boolean} Returns true if array contains item, false if not. 1200 */ 1201 function contains(array, item) { 1202 return array.indexOf(item) !== -1; 1203 } 1204 1205 /** 1206 * Test whether two arrays have overlapping items 1207 * @param {Array} array1 1208 * @param {Array} array2 1209 * @return {boolean} Returns true when at least one item exists in both arrays 1210 */ 1211 function hasOverlap(array1, array2) { 1212 for (var i = 0; i < array1.length; i++) { 1213 if (contains(array2, array1[i])) { 1214 return true; 1215 } 1216 } 1217 1218 return false; 1219 } 1220 1221 /** 1222 * Return the first item from an array for which test(arr[i]) returns true 1223 * @param {Array} arr 1224 * @param {function} test 1225 * @return {* | undefined} Returns the first matching item 1226 * or undefined when there is no match 1227 */ 1228 function findInArray(arr, test) { 1229 for (var i = 0; i < arr.length; i++) { 1230 if (test(arr[i])) { 1231 return arr[i]; 1232 } 1233 } 1234 return undefined; 1235 } 1236 1237 /** 1238 * Filter unique items of an array with strings 1239 * @param {string[]} arr 1240 * @return {string[]} 1241 */ 1242 function uniq(arr) { 1243 var entries = {} 1244 for (var i = 0; i < arr.length; i++) { 1245 entries[arr[i]] = true; 1246 } 1247 return Object.keys(entries); 1248 } 1249 1250 /** 1251 * Flat map the result invoking a callback for every item in an array. 1252 * https://gist.github.com/samgiles/762ee337dff48623e729 1253 * @param {Array} arr 1254 * @param {function} callback 1255 * @return {Array} 1256 */ 1257 function flatMap(arr, callback) { 1258 return Array.prototype.concat.apply([], arr.map(callback)); 1259 } 1260 1261 /** 1262 * Retrieve the function name from a set of typed functions, 1263 * and check whether the name of all functions match (if given) 1264 * @param {function[]} fns 1265 */ 1266 function getName (fns) { 1267 var name = ''; 1268 1269 for (var i = 0; i < fns.length; i++) { 1270 var fn = fns[i]; 1271 1272 // check whether the names are the same when defined 1273 if ((typeof fn.signatures === 'object' || typeof fn.signature === 'string') && fn.name !== '') { 1274 if (name === '') { 1275 name = fn.name; 1276 } 1277 else if (name !== fn.name) { 1278 var err = new Error('Function names do not match (expected: ' + name + ', actual: ' + fn.name + ')'); 1279 err.data = { 1280 actual: fn.name, 1281 expected: name 1282 }; 1283 throw err; 1284 } 1285 } 1286 } 1287 1288 return name; 1289 } 1290 1291 // extract and merge all signatures of a list with typed functions 1292 function extractSignatures(fns) { 1293 var err; 1294 var signaturesMap = {}; 1295 1296 function validateUnique(_signature, _fn) { 1297 if (signaturesMap.hasOwnProperty(_signature) && _fn !== signaturesMap[_signature]) { 1298 err = new Error('Signature "' + _signature + '" is defined twice'); 1299 err.data = {signature: _signature}; 1300 throw err; 1301 // else: both signatures point to the same function, that's fine 1302 } 1303 } 1304 1305 for (var i = 0; i < fns.length; i++) { 1306 var fn = fns[i]; 1307 1308 // test whether this is a typed-function 1309 if (typeof fn.signatures === 'object') { 1310 // merge the signatures 1311 for (var signature in fn.signatures) { 1312 if (fn.signatures.hasOwnProperty(signature)) { 1313 validateUnique(signature, fn.signatures[signature]); 1314 signaturesMap[signature] = fn.signatures[signature]; 1315 } 1316 } 1317 } 1318 else if (typeof fn.signature === 'string') { 1319 validateUnique(fn.signature, fn); 1320 signaturesMap[fn.signature] = fn; 1321 } 1322 else { 1323 err = new TypeError('Function is no typed-function (index: ' + i + ')'); 1324 err.data = {index: i}; 1325 throw err; 1326 } 1327 } 1328 1329 return signaturesMap; 1330 } 1331 1332 typed = createTypedFunction('typed', { 1333 'string, Object': createTypedFunction, 1334 'Object': function (signaturesMap) { 1335 // find existing name 1336 var fns = []; 1337 for (var signature in signaturesMap) { 1338 if (signaturesMap.hasOwnProperty(signature)) { 1339 fns.push(signaturesMap[signature]); 1340 } 1341 } 1342 var name = getName(fns); 1343 return createTypedFunction(name, signaturesMap); 1344 }, 1345 '...Function': function (fns) { 1346 return createTypedFunction(getName(fns), extractSignatures(fns)); 1347 }, 1348 'string, ...Function': function (name, fns) { 1349 return createTypedFunction(name, extractSignatures(fns)); 1350 } 1351 }); 1352 1353 typed.create = create; 1354 typed.types = _types; 1355 typed.conversions = _conversions; 1356 typed.ignore = _ignore; 1357 typed.onMismatch = _onMismatch; 1358 typed.throwMismatchError = _onMismatch; 1359 typed.createError = createError; 1360 typed.convert = convert; 1361 typed.find = find; 1362 1363 /** 1364 * add a type 1365 * @param {{name: string, test: function}} type 1366 * @param {boolean} [beforeObjectTest=true] 1367 * If true, the new test will be inserted before 1368 * the test with name 'Object' (if any), since 1369 * tests for Object match Array and classes too. 1370 */ 1371 typed.addType = function (type, beforeObjectTest) { 1372 if (!type || typeof type.name !== 'string' || typeof type.test !== 'function') { 1373 throw new TypeError('Object with properties {name: string, test: function} expected'); 1374 } 1375 1376 if (beforeObjectTest !== false) { 1377 for (var i = 0; i < typed.types.length; i++) { 1378 if (typed.types[i].name === 'Object') { 1379 typed.types.splice(i, 0, type); 1380 return 1381 } 1382 } 1383 } 1384 1385 typed.types.push(type); 1386 }; 1387 1388 // add a conversion 1389 typed.addConversion = function (conversion) { 1390 if (!conversion 1391 || typeof conversion.from !== 'string' 1392 || typeof conversion.to !== 'string' 1393 || typeof conversion.convert !== 'function') { 1394 throw new TypeError('Object with properties {from: string, to: string, convert: function} expected'); 1395 } 1396 1397 typed.conversions.push(conversion); 1398 }; 1399 1400 return typed; 1401 } 1402 1403 return create(); 1404 }));