Skip to content

Instantly share code, notes, and snippets.

@3mcd
Last active December 31, 2016 00:20
Show Gist options
  • Save 3mcd/cff30892287af4128028d9556cb3428a to your computer and use it in GitHub Desktop.
Save 3mcd/cff30892287af4128028d9556cb3428a to your computer and use it in GitHub Desktop.
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