Skip to content

Instantly share code, notes, and snippets.

@axefrog
Last active April 1, 2016 02:04
Show Gist options
  • Select an option

  • Save axefrog/a2c383d939f698503d8e to your computer and use it in GitHub Desktop.

Select an option

Save axefrog/a2c383d939f698503d8e to your computer and use it in GitHub Desktop.
An experiment in functional programming for validating and normalizing a variably-structured options argument
function sanitizeOptions(options) {
if(!options) {
options = {}
}
else if(typeof options === `string`) {
options = { scope: options }
}
else if(Array.isArray(options)) {
options = { include: options }
}
else if(typeof options === `object` && options.constructor === Object) {
options = Object.assign({}, options)
}
else {
throw new Error(`Second argument to isolate() must be a string ` +
`specifying 'scope', an array specifying sources/sinks ` +
`to whitelist for isolation, or an options object of ` +
`{ scope, include|exclude }`)
}
if(!options.scope) {
options.scope = newScope()
}
if(options.include) {
if(options.exclude) {
throw new Error(`Options passed to isolate() may have 'include' or ` +
`'exclude' properties, but not both`)
}
if(typeof options.include === `string`) {
options.include = [options.include]
}
else if(!Array.isArray(options.include) ||
!options.include.every(x => typeof x === `string`)) {
throw new Error(`The 'include' option passed to isolate() should be a ` +
`string or an array of strings`)
}
}
else if(options.exclude) {
if(typeof options.exclude === `string`) {
options.exclude = [options.exclude]
}
else if(!Array.isArray(options.exclude) ||
!options.exclude.every(x => typeof x === `string`)) {
throw new Error(`The 'exclude' option passed to isolate() should be a ` +
`string or an array of strings`)
}
}
return options
}
// ruleset :: [sanitizer, ...]
// sanitizer :: [matcher, ...]
// matcher :: [test, transform]
// test :: x => true|false
// transform :: x => y
const rules = [
[
[isNil, emptyObj],
[isString, objOf(`scope`)],
[isArray, objOf(`include`)],
[isObject, identity],
error(`Second argument to isolate() must be a string specifying ` +
`'scope', an array specifying sources/sinks to whitelist for ` +
`isolation, or an options object of { scope, include|exclude }`)
],[
[`scope`, [
[isNilOrEmpty, () => newScope()],
[not(isString), error(`invalid isolate() options: 'scope' must be a string`)]
]]
],[
[hasAll(`include`, `exclude`), error(`Options passed to isolate() may have 'include' or 'exclude' properties, but not both`)],
[`include`, [
[isNilOrEmpty, void 0],
[isString, arrayOf]
]],
],[
[`exclude`, [
[isNilOrEmpty, void 0],
[isString, arrayOf]
]]
]
];
const any = (...fn) => arg => fn.some(f => f(arg));
const all = (...fn) => arg => fn.every(f => f(arg));
const always = () => true;
const not = fn => arg => !fn(arg);
const pipe = (...fn) => arg => fn.reduce((acc, f) => f(acc), arg);
const keys = arg => Array.from(Object.keys(arg));
const omit = name => arg => keys(arg).filter(not(equals(name)));
const discardNil = name => arg => isNil(arg[name]) ? omit(name)(arg) : arg;
const has = name => arg => name in arg;
const hasAll = names => arg => all(name => name in arg)
const identity = arg => arg;
const assign = (...args) => Object.assign({}, ...args);
const error = msg => () => throw new Error(msg);
const objOf = name => arg => ({ [name]: arg });
const arrayOf = arg => [arg];
const constantOf = arg => () => arg;
const emptyObj = () => ({});
const isEmpty = arg => arg.length == 0;
const isUndefined = arg => arg === void 0;
const isNull = arg => arg === null;
const isNil = any(isUndefined, isNull);
const isFunction = arg => typeof arg === 'function';
const isNilOrEmpty = any(isUndefined, isNull, isEmpty)
const isString = arg => typeof arg === `string`;
const isObject = arg => typeof arg === `object` && arg.constructor === Object;
const isArray = Array.isArray;
const makeSetter = (name, target) => x => assign(target, { [name]: x });
const normalizeResolver = r => isArray(r) ? makeSanitizer(r) : isFunction(r) ? r : constantOf(r);
const normalizeMatcher = matcher => isFunction(matcher) ? [always, matcher] : [matcher[0], normalizeResolver(matcher[1])];
const makeStandardResolver = ([_, resolve]) => x => resolve(x);
const makePropResolver = ([name, resolve]) => x => pipe(x[name], resolve, makeSetter(name, x), discardNil(name));
const makeResolver = matcher => (isString(matcher[0]) ? makePropResolver : makeStandardResolver)(matcher);
const makeTest = matcher => isString(matcher[0]) ? always : matcher[0];
const makeMatcher = def => (([t, r]) => [makeTest(t), makeResolver(r)])(normalizeMatcher(def));
const makeSanitizer = list => (matchers => x => matchers.find(x) || x)(list.map(makeMatcher));
const makeRuleSet = list => (sanitizers => x => sanitizers.reduce((a, s) => s(a), x))(list.map(makeSanitizer));
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment