Last active
December 31, 2016 00:20
-
-
Save 3mcd/cff30892287af4128028d9556cb3428a to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
const $rest = Symbol('rest'); | |
const $normal = Symbol('normal'); | |
const $optional = Symbol('optional'); | |
const $pattern = Symbol('pattern'); | |
const eq = a => b => a === b; | |
const toString = val => Object.prototype.toString.call(val).split(' ')[1].slice(0, -1); | |
const isString = a => typeof a === 'string'; | |
const isArray = a => Array.isArray(a); | |
const isFunction = a => typeof a === 'function'; | |
const isRestParam = param => getPattern(param) === $rest; | |
const isNormalParam = param => getPattern(param) === $normal; | |
const isOptionalParam = param => getPattern(param) === $optional; | |
const defineProperty = Object.defineProperty; | |
const typeMap = { | |
Symbol, | |
String, | |
Number, | |
Boolean, | |
Array, | |
Object, | |
Map, | |
Set, | |
WeakMap, | |
WeakSet, | |
null, | |
undefined | |
}; | |
const register = (name, ctor) => typeMap[name] = ctor; | |
const getPattern = param => param[$pattern]; | |
const arePolymorphs = (param1, param2) => { | |
const { ctor: ctor1 } = param1; | |
const { ctor: ctor2 } = param2; | |
const a1 = isArray(ctor1); | |
const a2 = isArray(ctor2); | |
if (a1 && a2) { | |
return ctor1.some(fn => ctor2.some(eq(fn))); | |
} | |
if (a1) { | |
return ctor1.some(eq(ctor2)); | |
} | |
if (a2) { | |
return ctor2.some(eq(ctor1)); | |
} | |
return ctor1 === ctor2; | |
}; | |
const param = (pattern, ctor) => { | |
let name; | |
if (ctor instanceof Function) { | |
name = ctor.name; | |
} else if (isArray(ctor)) { | |
name = ctor.map(fn => param(pattern, fn)).map(param => param.name).join(', '); | |
} else { | |
throw new Error('A type must be one or more constructor functions.'); | |
} | |
return { [$pattern]: pattern, ctor, name }; | |
}; | |
const paramTypeInvalidError = (param, arg, i) => ({ | |
err(fn) { | |
return `Arg ${i} to ${fn.name}() was invalid. Expected ${param.name}, got ${toString(arg)}.`; | |
} | |
}); | |
const paramFollowingRestSameTypeError = (param1, param2, i) => ({ | |
err(fn) { | |
return `Params ${i} and ${i + 1} of ${fn.name}() are invalid. A param cannot immediately follow a rest param of the same type. In this case, multiple ${param1.name} params were followed by a single ${param2.name} param.`; | |
} | |
}); | |
const optionalParamFollowedBySameTypeError = (param, i) => ({ | |
err(fn) { | |
return `Params ${i} and ${i + 1} of ${fn.name}() are invalid. An optional param cannot immediately follow another param of the same type. In this case, a ${param.name} was followed by an optional ${param.name}.` | |
} | |
}); | |
const optionalParamTypeInvalidError = (param, arg, i) => ({ | |
err(fn) { | |
return `Arg ${i} to ${fn.name}() was invalid. Expected optional ${param.name}, got ${toString(arg)}.`; | |
} | |
}); | |
const extraArgumentError = (arg, i) => ({ | |
err(fn) { | |
return `Arg ${i} passed to ${fn.name}() was extraneous. Expected nothing, got ${toString(arg)}.`; | |
} | |
}); | |
function* validator (sig) { | |
const len = sig.length; | |
for (let i = 0; i < len; i++) { | |
let param = sig[i]; | |
let nextParam = sig[i + 1]; | |
if (isRestParam(param)) { | |
if (nextParam && arePolymorphs(param, nextParam)) { | |
yield paramFollowingRestSameTypeError(param, nextParam, i); | |
} | |
} else if (isOptionalParam(param)) { | |
if (nextParam && arePolymorphs(param, nextParam)) { | |
yield optionalParamFollowedBySameTypeError(param, i); | |
} | |
} | |
} | |
} | |
function* runtimeValidator (sig, args) { | |
sig = [...sig]; | |
let i = 0; | |
const len = args.length; | |
while (param = sig.shift()) { | |
let arg = args[i]; | |
let nextParam = sig[0]; | |
if (isRestParam(param)) { | |
while (validateArg(param, args[i])) { | |
i++; | |
} | |
if (!nextParam && i < len) { | |
yield paramTypeInvalidError(param, arg, i); | |
} | |
} else if (validateArg(param, arg)) { | |
i++; | |
} else if (isOptionalParam(param) && !nextParam) { | |
yield optionalParamTypeInvalidError(param, arg, i); | |
i++; | |
} else if (!isOptionalParam(param)) { | |
yield paramTypeInvalidError(param, arg, i); | |
i++; | |
} | |
} | |
while (i < len) { | |
yield extraArgumentError(args[i], i); | |
i++; | |
} | |
} | |
const toParam = obj => obj[$pattern] ? obj : param($normal, obj); | |
const isInstance = (val, ctor) => { | |
const type = typeof val; | |
if (ctor == void(0)) return true; | |
else if (ctor === Number && type === 'number') return true; | |
else if (ctor === String && type === 'string') return true; | |
else if (ctor === Boolean && type === 'boolean') return true; | |
else if (ctor === Object && isArray(val)) return false; | |
return val instanceof ctor; | |
}; | |
const validateArg = (param, arg) => { | |
const { ctor } = param; | |
if (isArray(ctor)) { | |
return ctor.some(fn => isInstance(arg, fn)); | |
} | |
return isInstance(arg, ctor); | |
}; | |
const buildErrorMessage = errors => | |
errors.reduce((msg, e) => { | |
return msg + `\n\t${e}`; | |
}, ''); | |
const handleErrors = (errors, fn) => { | |
if (errors.length > 0) { | |
throw new Error(buildErrorMessage(errors.map(x => x.err(fn)))); | |
} | |
}; | |
const createGuard = sig => | |
fn => { | |
const guard = function Object (...args) { | |
const errors = [...runtimeValidator(sig, args)]; | |
handleErrors(errors, guard); | |
return fn.call(this, ...args); | |
}; | |
const errors = [...validator(sig)]; | |
handleErrors(errors, guard); | |
defineProperty(Object, 'name', { value: fn.name || '<anonymous>' }); | |
Object.prototype = fn.prototype; | |
return Object; | |
}; | |
const rest = (...args) => param($rest, args); | |
const optional = (...args) => param($optional, args); | |
const getType = str => typeMap[str]; | |
const REGEX_POLY = /<(\w+(\|\s*?\w+)*)>/g; | |
const REGEX_REST = /^\.\.\./; | |
const REGEX_OPTIONAL = /\?$/; | |
const getPolyParams = p => { | |
let ctor = [p]; | |
const res = REGEX_POLY.exec(p); | |
REGEX_POLY.lastIndex = 0; | |
if (res !== null) { | |
ctor = res[1].split('|').map(x => x.trim()); | |
} | |
return ctor; | |
}; | |
function guard (sig, ...types) { | |
const asTag = isArray(sig) && !!sig.raw; | |
if (asTag) { | |
let params = sig[0].split(',').map(p => p.trim()); | |
sig = params.map(p => { | |
let ctor = getPolyParams(p); | |
let pattern = $normal; | |
if (REGEX_REST.test(p)) { | |
pattern = $rest; | |
} else if (REGEX_OPTIONAL.test(p)) { | |
pattern = $optional; | |
ctor = ctor.map(x => x.slice(0, -1)); | |
} | |
ctor = ctor.map(getType); | |
return param(pattern, ctor); | |
}); | |
} else { | |
sig = [...arguments]; | |
} | |
return createGuard(sig.map(toParam)); | |
} | |
function Person (first, last, ...args) { | |
let dependents = args.filter(x => x instanceof Person); | |
this.first = first; | |
this.last = last; | |
this.dependents = dependents; | |
let salary = args.pop(); | |
this.salary = typeof salary === 'number' ? salary : 10000; | |
} | |
Person = guard(String, String, rest(Person), optional(Number))(Person); | |
var child = new Person('Eric', 'McDaniel', 200); | |
var parent = new Person('Wayne', 'McDaniel', child, 200); | |
const silly = guard(rest(String, Number), optional(Boolean)) | |
( | |
function Test (...args) { | |
console.log(args); | |
} | |
); | |
const silly2 = guard`...<String|Number>, Boolean`( | |
function Test (...args) { | |
console.log(args); | |
} | |
); | |
console.log(silly2(1, 1, '2', 3, false)); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment