Last active
August 29, 2015 14:26
-
-
Save a-s-o/d9edfb76bea72bc5e1e2 to your computer and use it in GitHub Desktop.
cerebral action draft implementation
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
// Action definition signature | |
const Definition = t.struct({ | |
fn: t.Func, | |
displayName: t.maybe(t.Str), | |
inputs: t.maybe(t.Obj), | |
exits: t.maybe(t.Obj), | |
sync: t.maybe(t.Bool) | |
}); | |
cerebral.action = function cerebral$action (action) { | |
/* eslint no-new-func: 0 */ | |
const defaults = { | |
inputs: {}, | |
exits: { | |
success: _.identity, | |
error (ex) { | |
console.error(ex.name, ex.message); | |
return ex; | |
} | |
} | |
}; | |
// Validate definition according to | |
const definition = _.merge(defaults, Definition(action)); | |
const fn = definition.fn; | |
const name = definition.displayName || fn.name; | |
// Named wrapper so that actual function name is displayed | |
// in cerebral debugger and stack traces | |
const namedFunction = (funcName, func) => { | |
return new Function('func', `return function ${funcName} () { | |
return func.apply(this,arguments); | |
}`)(func); | |
}; | |
//////////// | |
// Types // | |
//////////// | |
const checkInputs = createValidation( | |
`${name}:input`, t.struct(definition.inputs || {}) | |
); | |
const exits = _.mapValues(definition.exits, (def, key) => { | |
// Allow functions as possible exit validations | |
// for convenience | |
// | |
// signature [value] -> value | |
// throw TypeError if invalid | |
if (_.isFunction(def)) return def; | |
// Get a list of specified output keys | |
const allowedKeys = Object.keys(def); | |
// Label for debugging (example: someAction:exit:result) | |
const label = `${name}:exit:${key}`; | |
const validate = createValidation(label, t.struct(def)); | |
return (value = {}) => { | |
t.assert(t.Obj.is(value), '[${label}] Returned value is not an object'); | |
// tcomb validation only checks the keys that | |
// are specified in the `def`, other keys are | |
// ignored | |
validate(value); | |
// Only allow specified keys to be returned from actions | |
// since the returned object is merged by cerebral. This keeps | |
// actions from overwriting preceeding args unintentionaly | |
// | |
// Keys can still be overwritten if needed by explicitly | |
// declaring them in the exit type definition (i.e. `def`) | |
const extra = Object | |
.keys(value) | |
.filter(x => allowedKeys.indexOf(x) === -1); | |
if (extra.length) { | |
const args = JSON.stringify(_.pick(value, extra)); | |
t.fail(`[${label}] Extra args supplied ${args}`); | |
} | |
// Return the original args object | |
return value; | |
}; | |
}); | |
// The default exit is important when dealing with sync actions | |
// since they just return without actually calling a specific exit | |
// In these cases, use the defaultExit | |
const defaultExit = definition.defaultExit || 'success'; | |
if (!exits[defaultExit]) exits[defaultExit] = _.identity; | |
////////// | |
// Sync // | |
////////// | |
if (fn.length <= 2 || definition.sync === true) { | |
return namedFunction(name, (args, state) => { | |
try { | |
const result = fn(checkInputs(args), state) || {}; | |
return exits[defaultExit](result); | |
} catch (ex) { | |
// Work around until proper error handling is | |
// implemented by cerebral (for now the | |
// exception is processed or rethrown from | |
// the error exit) | |
return exits.error(ex, state); | |
} | |
}); | |
} | |
/////////// | |
// Async // | |
/////////// | |
return namedFunction(name, (args, state, promise) => { | |
// Support node style callback | |
// Example: | |
// next(errorResoponse); | |
// next(null, defaultExitResponse); | |
function next (err, result) { | |
if (err) next.error(err); | |
else next[defaultExit](result); | |
} | |
// Support arbritrary paths | |
// Example: | |
// next.success() | |
// next.bar() | |
// next.foo() | |
_.forEach(exits, function makeCallbacks (exit, exitName) { | |
next[exitName] = value => { | |
try { | |
if (exit === exits.error) throw value; | |
promise.resolve( exit(value) ); | |
} catch (ex) { | |
promise.reject( exits.error(ex, state) ); | |
} | |
}; | |
}); | |
// Run the fn with the generated callbacks | |
fn(checkInputs(args), state, next); | |
}); | |
}; | |
// Helper: Validation Factory | |
function createValidation (label, type) { | |
return function validate (value) { | |
const check = t.validate(value, type); | |
if (!check.isValid()) { | |
t.fail(`[${label}] ${check.firstError().message}`); | |
} | |
return value; | |
}; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment