Skip to content

Instantly share code, notes, and snippets.

@a-s-o
Last active August 29, 2015 14:26
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save a-s-o/d9edfb76bea72bc5e1e2 to your computer and use it in GitHub Desktop.
Save a-s-o/d9edfb76bea72bc5e1e2 to your computer and use it in GitHub Desktop.
cerebral action draft implementation
// 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