2014-12-29 13:27:17 -08:00

344 lines
7.5 KiB
JavaScript

// 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 = $traceurRuntime.type;
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) {
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) {
return '[' + value.map(prettyPrint).join(', ') + ']';
}
var properties = Object.keys(value);
return '{' + properties.map((p) => p + ': ' + prettyPrint(value[p])).join(', ') + '}';
}
return value.__assertName || value.name || value.toString();
}
function isType(value, T, errors) {
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 = define('string', function(value) {
return typeof value === 'string';
});
// function string() {}
// string.assert = function(value) {
// return typeof value === 'string';
// };
var boolean = define('boolean', function(value) {
return typeof value === 'boolean';
});
// function boolean() {}
// boolean.assert = function(value) {
// return typeof value === 'boolean';
// };
var number = define('number', function(value) {
return typeof value === 'number';
});
// function number() {}
// number.assert = 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 item of value) {
assert(item).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 property of properties) {
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 type of types) {
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;
// 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}