var _global = typeof window === 'object' ? window : global; // TODO(vojta): // - extract into multiple files // - different error types // - simplify/humanize error messages // - throw when invalid input (such as odd number of args into assert.argumentTypes) var POSITION_NAME = ['', '1st', '2nd', '3rd']; function argPositionName(i) { var position = (i / 2) + 1; return POSITION_NAME[position] || (position + 'th'); } var primitives; var genericType; if (typeof $traceurRuntime === 'object') { primitives = $traceurRuntime.type; genericType = $traceurRuntime.genericType; } else { // Allow to work without traceur runtime as well! primitives = { any: {name: 'any'}, boolean: {name: 'boolean'}, number: {name: 'number'}, string: {name: 'string'}, symbol: {name: 'symbol'}, void: {name: 'void'} }; genericType = function(type, args) { return { type: type, args: args } } } Object.keys(primitives).forEach(function(name) { primitives[name].__assertName = name; }); export function proxy(){ } function assertArgumentTypes(...params) { var actual, type; var currentArgErrors; var errors = []; var msg; for (var i = 0, l = params.length; i < l; i = i + 2) { actual = params[i]; type = params[i + 1]; currentArgErrors = []; // currentStack = []; // if (!isType(actual, type, currentArgErrors)) { // console.log(JSON.stringify(errors, null, ' ')); // TODO(vojta): print "an instance of" only if T starts with uppercase. errors.push(argPositionName(i) + ' argument has to be an instance of ' + prettyPrint(type) + ', got ' + prettyPrint(actual)); if (currentArgErrors.length) { errors.push(currentArgErrors); } } } if (errors.length) { throw new Error('Invalid arguments given!\n' + formatErrors(errors)); } } function prettyPrint(value, depth) { if (typeof(depth) === 'undefined') { depth = 0; } if (depth++ > 3) { return '[...]'; } if (typeof value === 'undefined') { return 'undefined'; } if (typeof value === 'string') { return '"' + value + '"'; } if (typeof value === 'boolean') { return value.toString(); } if (value === null) { return 'null'; } if (typeof value === 'object') { if (value.__assertName) { return value.__assertName; } if (value.map && typeof value.map === 'function') { return '[' + value.map((v) => prettyPrint(v, depth)).join(', ') + ']'; } var properties = Object.keys(value); var suffix = '}'; if (properties.length > 20) { properties.length = 20; suffix = ', ... }'; } return '{' + properties.map((p) => p + ': ' + prettyPrint(value[p], depth)).join(', ') + suffix; } return value.__assertName || value.name || value.toString(); } function isType(value, T, errors) { if (T && T.type) { // needed for generics. // TODO(tbosch): read out T.args and do assertions based on them as well! T = T.type; } if (T === primitives.void) { return typeof value === 'undefined'; } if (_isProxy(value)) { return true; } if (T === primitives.any || value === null) { return true; } if (T === primitives.string) { return typeof value === 'string'; } if (T === primitives.number) { return typeof value === 'number'; } if (T === primitives.boolean) { return typeof value === 'boolean'; } // var parentStack = currentStack; // currentStack = []; // shouldnt this create new stack? if (typeof T.assert === 'function') { var parentStack = currentStack; var isValid; currentStack = errors; try { isValid = T.assert(value) ; } catch (e) { fail(e.message); isValid = false; } currentStack = parentStack; if (typeof isValid === 'undefined') { isValid = errors.length === 0; } return isValid; // if (!currentStack.length) { // currentStack = parentStack; // return []; // } // var res = currentStack; // currentStack = parentStack; // return ['not instance of ' + prettyPrint(T), res]; } return value instanceof T; // if (!(value instanceof T)) { // fail('not instance of ' + prettyPrint(T)); // } // var res = currentStack; // currentStack = parentStack; // return res; } function _isProxy(obj) { if (!obj || !obj.constructor || !obj.constructor.annotations) return false; return obj.constructor.annotations.filter((a) => a instanceof proxy).length > 0; } function formatErrors(errors, indent = ' ') { return errors.map((e) => { if (typeof e === 'string') return indent + '- ' + e; return formatErrors(e, indent + ' '); }).join('\n'); } // assert a type of given value and throw if does not pass function type(actual, T) { var errors = []; // currentStack = []; if (!isType(actual, T, errors)) { // console.log(JSON.stringify(errors, null, ' ')); // TODO(vojta): print "an instance of" only if T starts with uppercase. var msg = 'Expected an instance of ' + prettyPrint(T) + ', got ' + prettyPrint(actual) + '!'; if (errors.length) { msg += '\n' + formatErrors(errors); } throw new Error(msg); } return actual; } function returnType(actual, T) { var errors = []; // currentStack = []; if (!isType(actual, T, errors)) { // console.log(JSON.stringify(errors, null, ' ')); // TODO(vojta): print "an instance of" only if T starts with uppercase. var msg = 'Expected to return an instance of ' + prettyPrint(T) + ', got ' + prettyPrint(actual) + '!'; if (errors.length) { msg += '\n' + formatErrors(errors); } throw new Error(msg); } return actual; } // TODO(vojta): define these with DSL? var string = type.string = define('string', function(value) { return typeof value === 'string'; }); var boolean = type.boolean = define('boolean', function(value) { return typeof value === 'boolean'; }); var number = type.number = define('number', function(value) { return typeof value === 'number'; }); function arrayOf(...types) { return assert.define('array of ' + types.map(prettyPrint).join('/'), function(value) { if (assert(value).is(Array)) { for (var i = 0; i < value.length; i++) { assert(value[i]).is(...types); } } }); } function structure(definition) { var properties = Object.keys(definition); return assert.define('object with properties ' + properties.join(', '), function(value) { if (assert(value).is(Object)) { for (var i = 0; i < properties.length; i++) { var property = properties[i]; assert(value[property]).is(definition[property]); } } }) } // I'm sorry, bad global state... to make the API nice ;-) var currentStack = []; function fail(message) { currentStack.push(message); } function define(classOrName, check) { var cls = classOrName; if (typeof classOrName === 'string') { cls = function() {}; cls.__assertName = classOrName; } cls.assert = function(value) { // var parentStack = currentStack; // currentStack = []; return check(value); // if (currentStack.length) { // parentStack.push(currentStack) // } // currentStack = parentStack; }; return cls; } function assert(value) { return { is: function is(...types) { // var errors = [] var allErrors = []; var errors; for (var i = 0; i < types.length; i++) { var type = types[i]; errors = []; if (isType(value, type, errors)) { return true; } // if no errors, merge multiple "is not instance of " into x/y/z ? allErrors.push(prettyPrint(value) + ' is not instance of ' + prettyPrint(type)) if (errors.length) { allErrors.push(errors); } } // if (types.length > 1) { // currentStack.push(['has to be ' + types.map(prettyPrint).join(' or '), ...allErrors]); // } else { currentStack.push(...allErrors); // } return false; } }; } // PUBLIC API // asserting API // throw if no type provided assert.type = type; for (var prop in primitives) { assert.type[prop] = primitives[prop]; } assert.genericType = genericType; // throw if odd number of args assert.argumentTypes = assertArgumentTypes; assert.returnType = returnType; // define AP; assert.define = define; assert.fail = fail; // primitive value type; assert.string = string; assert.number = number; assert.boolean = boolean; // custom types assert.arrayOf = arrayOf; assert.structure = structure; export {assert}