Last active
April 1, 2016 02:04
-
-
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
This file contains hidden or 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
| 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 | |
| } |
This file contains hidden or 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
| // 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] | |
| ]] | |
| ] | |
| ]; |
This file contains hidden or 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 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