@ibm-functions/composer
Advanced tools
Comparing version 0.1.1 to 0.2.0
657
composer.js
/* | ||
* Copyright 2017 IBM Corporation | ||
* Copyright 2017-2018 IBM Corporation | ||
* | ||
@@ -19,263 +19,502 @@ * Licensed under the Apache License, Version 2.0 (the "License"); | ||
const clone = require('clone') | ||
// composer module | ||
const fs = require('fs') | ||
const os = require('os') | ||
const path = require('path') | ||
const util = require('util') | ||
const fs = require('fs') | ||
const uglify = require('uglify-es') | ||
class ComposerError extends Error { | ||
constructor(message, cause) { | ||
super(message) | ||
const index = this.stack.indexOf('\n') | ||
this.stack = this.stack.substring(0, index) + '\nCause: ' + util.inspect(cause) + this.stack.substring(index) | ||
constructor(message, argument) { | ||
super(message + (typeof argument !== 'undefined' ? '\nArgument: ' + util.inspect(argument) : '')) | ||
} | ||
} | ||
function chain(front, back) { | ||
front.States.push(...back.States) | ||
front.Exit.Next = back.Entry | ||
front.Exit = back.Exit | ||
return front | ||
/** | ||
* Validates options and converts to JSON | ||
*/ | ||
function validate(options) { | ||
if (options == null) return | ||
if (typeof options !== 'object' || Array.isArray(options)) throw new ComposerError('Invalid options', options) | ||
options = JSON.stringify(options) | ||
if (options === '{}') return | ||
return JSON.parse(options) | ||
} | ||
function push(id) { | ||
const Entry = { Type: 'Push', id } | ||
return { Entry, States: [Entry], Exit: Entry } | ||
/** | ||
* Encodes a composition as an action by injecting the conductor code | ||
*/ | ||
function encode({ name, action }) { | ||
if (action.exec.kind !== 'composition') return { name, action } | ||
const code = `${conductor}(eval,${JSON.stringify(action.exec.composition)})\n` // invoke conductor on composition | ||
return { name, action: { exec: { kind: 'nodejs:default', code }, annotations: [{ key: 'conductor', value: action.exec.composition }] } } | ||
} | ||
function pop(id) { | ||
const Entry = { Type: 'Pop', id } | ||
return { Entry, States: [Entry], Exit: Entry } | ||
/** | ||
* Parses a (possibly fully qualified) resource name and validates it. If it's not a fully qualified name, | ||
* then attempts to qualify it. | ||
* | ||
* Examples string to namespace, [package/]action name | ||
* foo => /_/foo | ||
* pkg/foo => /_/pkg/foo | ||
* /ns/foo => /ns/foo | ||
* /ns/pkg/foo => /ns/pkg/foo | ||
*/ | ||
function parseActionName(name) { | ||
if (typeof name !== 'string' || name.trim().length == 0) throw new ComposerError('Name is not specified') | ||
name = name.trim() | ||
let delimiter = '/' | ||
let parts = name.split(delimiter) | ||
let n = parts.length | ||
let leadingSlash = name[0] == delimiter | ||
// no more than /ns/p/a | ||
if (n < 1 || n > 4 || (leadingSlash && n == 2) || (!leadingSlash && n == 4)) throw new ComposerError('Name is not valid') | ||
// skip leading slash, all parts must be non empty (could tighten this check to match EntityName regex) | ||
parts.forEach(function (part, i) { if (i > 0 && part.trim().length == 0) throw new ComposerError('Name is not valid') }) | ||
let newName = parts.join(delimiter) | ||
if (leadingSlash) return newName | ||
else if (n < 3) return `${delimiter}_${delimiter}${newName}` | ||
else return `${delimiter}${newName}` | ||
} | ||
function begin(id, symbol, value) { | ||
const Entry = { Type: 'Let', Symbol: symbol, Value: value, id } | ||
return { Entry, States: [Entry], Exit: Entry } | ||
class Composition { | ||
constructor(composition, options, actions = []) { | ||
// collect actions defined in nested composition | ||
Object.keys(composition).forEach(key => { | ||
if (composition[key] instanceof Composition) { | ||
// TODO: check for duplicate entries | ||
actions.push(...composition[key].actions || []) | ||
composition[key] = composition[key].composition | ||
} | ||
}) | ||
if (actions.length > 0) this.actions = actions | ||
options = validate(options) | ||
if (typeof options !== 'undefined') composition = Object.assign({ options }, composition) | ||
// flatten composition array | ||
this.composition = Array.isArray(composition) ? [].concat(...composition) : [composition] | ||
} | ||
/** Names the composition and returns a composition which invokes the named composition */ | ||
named(name) { | ||
if (arguments.length > 1) throw new ComposerError('Too many arguments') | ||
if (typeof name !== 'string') throw new ComposerError('Invalid argument', name) | ||
name = parseActionName(name) | ||
if (this.actions && this.actions.findIndex(action => action.name === name) !== -1) throw new ComposerError('Duplicate action name', name) | ||
const actions = (this.actions || []).concat({ name, action: { exec: { kind: 'composition', composition: this.composition } } }) | ||
return new Composition({ type: 'action', name }, null, actions) | ||
} | ||
/** Encodes all compositions as actions by injecting the conductor code in them */ | ||
encode(name) { | ||
if (arguments.length > 1) throw new ComposerError('Too many arguments') | ||
if (typeof name !== 'undefined' && typeof name !== 'string') throw new ComposerError('Invalid argument', name) | ||
const obj = typeof name === 'string' ? this.named(name) : this | ||
if (obj.composition.length !== 1 || obj.composition[0].type !== 'action') throw new ComposerError('Cannot encode anonymous composition') | ||
return new Composition(obj.composition, null, obj.actions.map(encode)) | ||
} | ||
} | ||
function end(id) { | ||
const Entry = { Type: 'End', id } | ||
return { Entry, States: [Entry], Exit: Entry } | ||
class Compositions { | ||
constructor(wsk) { | ||
this.actions = wsk.actions | ||
} | ||
deploy(composition, name) { | ||
if (arguments.length > 2) throw new ComposerError('Too many arguments') | ||
if (!(composition instanceof Composition)) throw new ComposerError('Invalid argument', composition) | ||
const obj = composition.encode(name) | ||
return obj.actions.reduce((promise, action) => promise.then(() => this.actions.delete(action).catch(() => { })) | ||
.then(() => this.actions.update(action)), Promise.resolve()) | ||
.then(() => composition) | ||
} | ||
} | ||
const isObject = obj => typeof (obj) === 'object' && obj !== null && !Array.isArray(obj) | ||
class Composer { | ||
openwhisk(options) { | ||
// try to extract apihost and key first from whisk property file file and then from process.env | ||
let apihost | ||
let api_key | ||
class Composer { | ||
task(obj, options) { | ||
if (options != null && options.output) return this.assign(options.output, obj, options.input) | ||
if (options != null && options.merge) return this.sequence(this.retain(obj), ({ params, result }) => Object.assign({}, params, result)) | ||
const id = {} | ||
let Entry | ||
if (obj == null) { // identity function (must throw errors if any) | ||
Entry = { Type: 'Task', Helper: 'null', Function: 'params => params', id } | ||
} else if (typeof obj === 'object' && typeof obj.Entry === 'object' && Array.isArray(obj.States) && typeof obj.Exit === 'object') { // an action composition | ||
return clone(obj) | ||
} else if (typeof obj === 'object' && typeof obj.Entry === 'string' && typeof obj.States === 'object' && typeof obj.Exit === 'string') { // a compiled composition | ||
return this.decompile(obj) | ||
} else if (typeof obj === 'function') { // function | ||
Entry = { Type: 'Task', Function: obj.toString(), id } | ||
} else if (typeof obj === 'string') { // action | ||
Entry = { Type: 'Task', Action: obj, id } | ||
} else if (typeof obj === 'object' && typeof obj.Helper !== 'undefined' && typeof obj.Function === 'string') { //helper function | ||
Entry = { Type: 'Task', Function: obj.Function, Helper: obj.Helper, id } | ||
} else { // error | ||
throw new ComposerError('Invalid composition argument', obj) | ||
} | ||
return { Entry, States: [Entry], Exit: Entry } | ||
try { | ||
const wskpropsPath = process.env.WSK_CONFIG_FILE || path.join(os.homedir(), '.wskprops') | ||
const lines = fs.readFileSync(wskpropsPath, { encoding: 'utf8' }).split('\n') | ||
for (let line of lines) { | ||
let parts = line.trim().split('=') | ||
if (parts.length === 2) { | ||
if (parts[0] === 'APIHOST') { | ||
apihost = parts[1] | ||
} else if (parts[0] === 'AUTH') { | ||
api_key = parts[1] | ||
} | ||
} | ||
} | ||
} catch (error) { } | ||
if (process.env.__OW_API_HOST) apihost = process.env.__OW_API_HOST | ||
if (process.env.__OW_API_KEY) api_key = process.env.__OW_API_KEY | ||
const wsk = require('openwhisk')(Object.assign({ apihost, api_key }, options)) | ||
wsk.compositions = new Compositions(wsk) | ||
return wsk | ||
} | ||
sequence() { | ||
if (arguments.length == 0) return this.task() | ||
return Array.prototype.map.call(arguments, x => this.task(x), this).reduce(chain) | ||
seq() { | ||
return this.sequence(...arguments) | ||
} | ||
if(test, consequent, alternate) { | ||
if (test == null || consequent == null) throw new ComposerError('Missing arguments in composition', arguments) | ||
const id = {} | ||
test = chain(push(id), this.task(test)) | ||
consequent = this.task(consequent) | ||
alternate = this.task(alternate) | ||
const Exit = { Type: 'Pass', id } | ||
const choice = { Type: 'Choice', Then: consequent.Entry, Else: alternate.Entry, id } | ||
test.States.push(choice) | ||
test.States.push(...consequent.States) | ||
test.States.push(...alternate.States) | ||
test.Exit.Next = choice | ||
consequent.Exit.Next = Exit | ||
alternate.Exit.Next = Exit | ||
test.States.push(Exit) | ||
test.Exit = Exit | ||
return test | ||
value() { | ||
return this.literal(...arguments) | ||
} | ||
while(test, body) { | ||
if (test == null || body == null) throw new ComposerError('Missing arguments in composition', arguments) | ||
const id = {} | ||
test = chain(push(id), this.task(test)) | ||
body = this.task(body) | ||
const Exit = { Type: 'Pass', id } | ||
const choice = { Type: 'Choice', Then: body.Entry, Else: Exit, id } | ||
test.States.push(choice) | ||
test.States.push(...body.States) | ||
test.Exit.Next = choice | ||
body.Exit.Next = test.Entry | ||
test.States.push(Exit) | ||
test.Exit = Exit | ||
return test | ||
/** Takes a serialized Composition and returns a Composition instance */ | ||
deserialize({ composition, actions }) { | ||
return new Composition(composition, null, actions) | ||
} | ||
try(body, handler) { | ||
if (body == null || handler == null) throw new ComposerError('Missing arguments in composition', arguments) | ||
const id = {} | ||
body = this.task(body) | ||
handler = this.task(handler) | ||
const Exit = { Type: 'Pass', id } | ||
const Entry = { Type: 'Try', Next: body.Entry, Handler: handler.Entry, id } | ||
const pop = { Type: 'Catch', Next: Exit, id } | ||
const States = [Entry] | ||
States.push(...body.States, pop, ...handler.States, Exit) | ||
body.Exit.Next = pop | ||
handler.Exit.Next = Exit | ||
return { Entry, States, Exit } | ||
task(obj) { | ||
if (arguments.length > 1) throw new ComposerError('Too many arguments') | ||
if (obj == null) return this.seq() | ||
if (obj instanceof Composition) return obj | ||
if (typeof obj === 'function') return this.function(obj) | ||
if (typeof obj === 'string') return this.action(obj) | ||
throw new ComposerError('Invalid argument', obj) | ||
} | ||
retain(body, flag = false) { | ||
if (body == null) throw new ComposerError('Missing arguments in composition', arguments) | ||
if (typeof flag !== 'boolean') throw new ComposerError('Invalid retain flag', flag) | ||
sequence() { // varargs, no options | ||
return new Composition(Array.prototype.map.call(arguments, obj => this.task(obj), this)) | ||
} | ||
const id = {} | ||
if (!flag) return chain(push(id), chain(this.task(body), pop(id))) | ||
if(test, consequent, alternate, options) { | ||
if (arguments.length > 4) throw new ComposerError('Too many arguments') | ||
return new Composition({ type: 'if', test: this.task(test), consequent: this.task(consequent), alternate: this.task(alternate) }, options) | ||
} | ||
let helperFunc_1 = { 'Helper': 'retain_1', 'Function': 'params => ({params})' } | ||
let helperFunc_3 = { 'Helper': 'retain_3', 'Function': 'params => ({params})' } | ||
let helperFunc_2 = { 'Helper': 'retain_2', 'Function': 'params => ({ params: params.params, result: params.result.params })' } | ||
while(test, body, options) { | ||
if (arguments.length > 3) throw new ComposerError('Too many arguments') | ||
return new Composition({ type: 'while', test: this.task(test), body: this.task(body) }, options) | ||
} | ||
return this.sequence( | ||
this.retain( | ||
this.try( | ||
this.sequence( | ||
body, | ||
helperFunc_1 | ||
), | ||
helperFunc_3 | ||
) | ||
), | ||
helperFunc_2 | ||
) | ||
dowhile(body, test, options) { | ||
if (arguments.length > 3) throw new ComposerError('Too many arguments') | ||
return new Composition({ type: 'dowhile', test: this.task(test), body: this.task(body) }, options) | ||
} | ||
assign(dest, body, source, flag = false) { | ||
if (dest == null || body == null) throw new ComposerError('Missing arguments in composition', arguments) | ||
if (typeof flag !== 'boolean') throw new ComposerError('Invalid assign flag', flag) | ||
try(body, handler, options) { | ||
if (arguments.length > 3) throw new ComposerError('Too many arguments') | ||
return new Composition({ type: 'try', body: this.task(body), handler: this.task(handler) }, options) | ||
} | ||
let helperFunc_1 = { 'Helper': 'assign_1', 'Function': 'params => params[source]' }; | ||
let helperFunc_2 = { 'Helper': 'assign_2', 'Function': 'params => { params.params[dest] = params.result; return params.params }' }; | ||
finally(body, finalizer, options) { | ||
if (arguments.length > 3) throw new ComposerError('Too many arguments') | ||
return new Composition({ type: 'finally', body: this.task(body), finalizer: this.task(finalizer) }, options) | ||
} | ||
const t = source ? this.let('source', source, this.retain(this.sequence(helperFunc_1, body), flag)) : this.retain(body, flag) | ||
return this.let('dest', dest, t, helperFunc_2) | ||
let(declarations) { // varargs, no options | ||
if (typeof declarations !== 'object' || declarations === null) throw new ComposerError('Invalid argument', declarations) | ||
return new Composition({ type: 'let', declarations, body: this.seq(...Array.prototype.slice.call(arguments, 1)) }) | ||
} | ||
let(arg1, arg2) { | ||
if (arg1 == null) throw new ComposerError('Missing arguments in composition', arguments) | ||
if (typeof arg1 === 'string') { | ||
const id = {} | ||
return chain(begin(id, arg1, arg2), chain(this.sequence(...Array.prototype.slice.call(arguments, 2)), end(id))) | ||
} else if (isObject(arg1)) { | ||
const enter = [] | ||
const exit = [] | ||
for (const name in arg1) { | ||
const id = {} | ||
enter.push(begin(id, name, arg1[name])) | ||
exit.unshift(end(id)) | ||
} | ||
if (enter.length == 0) return this.sequence(...Array.prototype.slice.call(arguments, 1)) | ||
return chain(enter.reduce(chain), chain(this.sequence(...Array.prototype.slice.call(arguments, 1)), exit.reduce(chain))) | ||
} else { | ||
throw new ComposerError('Invalid first let argument', arg1) | ||
literal(value, options) { | ||
if (arguments.length > 2) throw new ComposerError('Too many arguments') | ||
if (typeof value === 'function') throw new ComposerError('Invalid argument', value) | ||
return new Composition({ type: 'literal', value: typeof value === 'undefined' ? {} : value }, options) | ||
} | ||
function(fun, options) { | ||
if (arguments.length > 2) throw new ComposerError('Too many arguments') | ||
if (typeof fun === 'function') { | ||
fun = `${fun}` | ||
if (fun.indexOf('[native code]') !== -1) throw new ComposerError('Cannot capture native function', fun) | ||
} | ||
if (typeof fun === 'string') { | ||
fun = { kind: 'nodejs:default', code: fun } | ||
} | ||
if (typeof fun !== 'object' || fun === null) throw new ComposerError('Invalid argument', fun) | ||
return new Composition({ type: 'function', exec: fun }, options) | ||
} | ||
retry(count, body) { | ||
if (body == null) throw new ComposerError('Missing arguments in composition', arguments) | ||
if (typeof count !== 'number') throw new ComposerError('Invalid retry count', count) | ||
action(name, options) { | ||
if (arguments.length > 2) throw new ComposerError('Too many arguments') | ||
name = parseActionName(name) // throws ComposerError if name is not valid | ||
let exec | ||
if (options && Array.isArray(options.sequence)) { // native sequence | ||
const components = options.sequence.map(a => a.indexOf('/') == -1 ? `/_/${a}` : a) | ||
exec = { kind: 'sequence', components } | ||
delete options.sequence | ||
} | ||
if (options && typeof options.filename === 'string') { // read action code from file | ||
options.action = fs.readFileSync(options.filename, { encoding: 'utf8' }) | ||
delete options.filename | ||
} | ||
if (options && typeof options.action === 'function') { | ||
options.action = `${options.action}` | ||
if (options.action.indexOf('[native code]') !== -1) throw new ComposerError('Cannot capture native function', options.action) | ||
} | ||
if (options && typeof options.action === 'string') { | ||
options.action = { kind: 'nodejs:default', code: options.action } | ||
} | ||
if (options && typeof options.action === 'object' && options.action !== null) { | ||
exec = options.action | ||
delete options.action | ||
} | ||
return new Composition({ type: 'action', name }, options, exec ? [{ name, action: { exec } }] : []) | ||
} | ||
let helperFunc_1 = { 'Helper': 'retry_1', 'Function': "params => typeof params.result.error !== 'undefined' && count-- > 0" } | ||
let helperFunc_2 = { 'Helper': 'retry_2', 'Function': 'params => params.params' } | ||
let helperFunc_3 = { 'Helper': 'retry_3', 'Function': 'params => params.result' } | ||
retain(body, options) { | ||
if (arguments.length > 2) throw new ComposerError('Too many arguments') | ||
if (options && typeof options.filter === 'function') { | ||
// return { params: filter(params), result: body(params) } | ||
const filter = options.filter | ||
delete options.filter | ||
options.field = 'result' | ||
return this.seq(this.retain(filter), this.retain(this.finally(this.function(({ params }) => params, { helper: 'retain_3' }), body), options)) | ||
} | ||
if (options && typeof options.catch === 'boolean' && options.catch) { | ||
// return { params, result: body(params) } even if result is an error | ||
delete options.catch | ||
return this.seq( | ||
this.retain(this.finally(body, this.function(result => ({ result }), { helper: 'retain_1' })), options), | ||
this.function(({ params, result }) => ({ params, result: result.result }), { helper: 'retain_2' })) | ||
} | ||
if (options && typeof options.field !== 'undefined' && typeof options.field !== 'string') throw new ComposerError('Invalid options', options) | ||
// return new Composition({ params, result: body(params) } if no error, otherwise body(params) | ||
return new Composition({ type: 'retain', body: this.task(body) }, options) | ||
} | ||
return this.let('count', count, | ||
this.retain(body, true), | ||
this.while( | ||
helperFunc_1, | ||
this.sequence(helperFunc_2, this.retain(body, true))), | ||
helperFunc_3) | ||
repeat(count) { // varargs, no options | ||
if (typeof count !== 'number') throw new ComposerError('Invalid argument', count) | ||
return this.let({ count }, this.while(this.function(() => count-- > 0, { helper: 'repeat_1' }), this.seq(...Array.prototype.slice.call(arguments, 1)))) | ||
} | ||
repeat(count, body) { | ||
if (body == null) throw new ComposerError('Missing arguments in composition', arguments) | ||
if (typeof count !== 'number') throw new ComposerError('Invalid repeat count', count) | ||
let helperFunc_1 = { 'Helper': 'repeat_1', 'Function': '() => count-- > 0' } | ||
return this.let('count', count, this.while(helperFunc_1, body)) | ||
retry(count) { // varargs, no options | ||
if (typeof count !== 'number') throw new ComposerError('Invalid argument', count) | ||
const attempt = this.retain(this.seq(...Array.prototype.slice.call(arguments, 1)), { catch: true }) | ||
return this.let({ count }, | ||
this.function(params => ({ params }), { helper: 'retry_1' }), | ||
this.dowhile( | ||
this.finally(this.function(({ params }) => params, { helper: 'retry_2' }), attempt), | ||
this.function(({ result }) => typeof result.error !== 'undefined' && count-- > 0, { helper: 'retry_3' })), | ||
this.function(({ result }) => result, { helper: 'retry_4' })) | ||
} | ||
} | ||
value(json) { | ||
const id = {} | ||
if (typeof json === 'function') throw new ComposerError('Value cannot be a function', json.toString()) | ||
const Entry = { Type: 'Task', Value: typeof json === 'undefined' ? {} : json, id } | ||
return { Entry, States: [Entry], Exit: Entry } | ||
module.exports = new Composer() | ||
// conductor action | ||
const conductor = `const main=(${uglify.minify(`${init}`).code})` | ||
function init(__eval__, composition) { | ||
function chain(front, back) { | ||
front.slice(-1)[0].next = 1 | ||
front.push(...back) | ||
return front | ||
} | ||
compile(obj, filename) { | ||
if (typeof obj !== 'object' || typeof obj.Entry !== 'object' || !Array.isArray(obj.States) || typeof obj.Exit !== 'object') { | ||
throw new ComposerError('Invalid argument to compile', obj) | ||
function compile(json, path = '') { | ||
if (Array.isArray(json)) { | ||
if (json.length === 0) return [{ type: 'pass', path }] | ||
return json.map((json, index) => compile(json, path + '[' + index + ']')).reduce(chain) | ||
} | ||
obj = clone(obj) | ||
const States = {} | ||
let Entry | ||
let Exit | ||
let Count = 0 | ||
obj.States.forEach(state => { | ||
if (typeof state.id.id === 'undefined') state.id.id = Count++ | ||
}) | ||
obj.States.forEach(state => { | ||
const id = (state.Type === 'Task' ? state.Action && 'action' || state.Function && 'function' || state.Value && 'value' : state.Type.toLowerCase()) + '_' + state.id.id | ||
States[id] = state | ||
state.id = id | ||
if (state === obj.Entry) Entry = id | ||
if (state === obj.Exit) Exit = id | ||
}) | ||
obj.States.forEach(state => { | ||
if (state.Next) state.Next = state.Next.id | ||
if (state.Then) state.Then = state.Then.id | ||
if (state.Else) state.Else = state.Else.id | ||
if (state.Handler) state.Handler = state.Handler.id | ||
}) | ||
obj.States.forEach(state => { | ||
delete state.id | ||
}) | ||
const app = { Entry, States, Exit } | ||
if (filename) fs.writeFileSync(filename, JSON.stringify(app, null, 4), { encoding: 'utf8' }) | ||
return app | ||
const options = json.options || {} | ||
switch (json.type) { | ||
case 'action': | ||
return [{ type: 'action', name: json.name, path }] | ||
case 'function': | ||
return [{ type: 'function', exec: json.exec, path }] | ||
case 'literal': | ||
return [{ type: 'literal', value: json.value, path }] | ||
case 'finally': | ||
var body = compile(json.body, path + '.body') | ||
const finalizer = compile(json.finalizer, path + '.finalizer') | ||
var fsm = [[{ type: 'try', path }], body, [{ type: 'exit', path }], finalizer].reduce(chain) | ||
fsm[0].catch = fsm.length - finalizer.length | ||
return fsm | ||
case 'let': | ||
var body = compile(json.body, path + '.body') | ||
return [[{ type: 'let', let: json.declarations, path }], body, [{ type: 'exit', path }]].reduce(chain) | ||
case 'retain': | ||
var body = compile(json.body, path + '.body') | ||
var fsm = [[{ type: 'push', path }], body, [{ type: 'pop', collect: true, path }]].reduce(chain) | ||
if (options.field) fsm[0].field = options.field | ||
return fsm | ||
case 'try': | ||
var body = compile(json.body, path + '.body') | ||
const handler = chain(compile(json.handler, path + '.handler'), [{ type: 'pass', path }]) | ||
var fsm = [[{ type: 'try', path }], body].reduce(chain) | ||
fsm[0].catch = fsm.length | ||
fsm.slice(-1)[0].next = handler.length | ||
fsm.push(...handler) | ||
return fsm | ||
case 'if': | ||
var consequent = compile(json.consequent, path + '.consequent') | ||
var alternate = chain(compile(json.alternate, path + '.alternate'), [{ type: 'pass', path }]) | ||
if (!options.nosave) consequent = chain([{ type: 'pop', path }], consequent) | ||
if (!options.nosave) alternate = chain([{ type: 'pop', path }], alternate) | ||
var fsm = chain(compile(json.test, path + '.test'), [{ type: 'choice', then: 1, else: consequent.length + 1, path }]) | ||
if (!options.nosave) fsm = chain([{ type: 'push', path }], fsm) | ||
consequent.slice(-1)[0].next = alternate.length | ||
fsm.push(...consequent) | ||
fsm.push(...alternate) | ||
return fsm | ||
case 'while': | ||
var consequent = compile(json.body, path + '.body') | ||
var alternate = [{ type: 'pass', path }] | ||
if (!options.nosave) consequent = chain([{ type: 'pop', path }], consequent) | ||
if (!options.nosave) alternate = chain([{ type: 'pop', path }], alternate) | ||
var fsm = chain(compile(json.test, path + '.test'), [{ type: 'choice', then: 1, else: consequent.length + 1, path }]) | ||
if (!options.nosave) fsm = chain([{ type: 'push', path }], fsm) | ||
consequent.slice(-1)[0].next = 1 - fsm.length - consequent.length | ||
fsm.push(...consequent) | ||
fsm.push(...alternate) | ||
return fsm | ||
case 'dowhile': | ||
var test = compile(json.test, path + '.test') | ||
if (!options.nosave) test = chain([{ type: 'push', path }], test) | ||
var fsm = [compile(json.body, path + '.body'), test, [{ type: 'choice', then: 1, else: 2, path }]].reduce(chain) | ||
if (options.nosave) { | ||
fsm.slice(-1)[0].then = 1 - fsm.length | ||
fsm.slice(-1)[0].else = 1 | ||
} else { | ||
fsm.push({ type: 'pop', path }) | ||
fsm.slice(-1)[0].next = 1 - fsm.length | ||
} | ||
var alternate = [{ type: 'pass', path }] | ||
if (!options.nosave) alternate = chain([{ type: 'pop', path }], alternate) | ||
fsm.push(...alternate) | ||
return fsm | ||
} | ||
} | ||
decompile(obj) { | ||
if (typeof obj !== 'object' || typeof obj.Entry !== 'string' || typeof obj.States !== 'object' || typeof obj.Exit !== 'string') { | ||
throw new ComposerError('Invalid argument to decompile', obj) | ||
const fsm = compile(composition) | ||
const isObject = obj => typeof obj === 'object' && obj !== null && !Array.isArray(obj) | ||
// encode error object | ||
const encodeError = error => ({ | ||
code: typeof error.code === 'number' && error.code || 500, | ||
error: (typeof error.error === 'string' && error.error) || error.message || (typeof error === 'string' && error) || 'An internal error occurred' | ||
}) | ||
// error status codes | ||
const badRequest = error => Promise.reject({ code: 400, error }) | ||
const internalError = error => Promise.reject(encodeError(error)) | ||
return params => Promise.resolve().then(() => invoke(params)).catch(internalError) | ||
// do invocation | ||
function invoke(params) { | ||
// initial state and stack | ||
let state = 0 | ||
let stack = [] | ||
// restore state and stack when resuming | ||
if (typeof params.$resume !== 'undefined') { | ||
if (!isObject(params.$resume)) return badRequest('The type of optional $resume parameter must be object') | ||
state = params.$resume.state | ||
stack = params.$resume.stack | ||
if (typeof state !== 'undefined' && typeof state !== 'number') return badRequest('The type of optional $resume.state parameter must be number') | ||
if (!Array.isArray(stack)) return badRequest('The type of $resume.stack must be an array') | ||
delete params.$resume | ||
inspect() // handle error objects when resuming | ||
} | ||
obj = clone(obj) | ||
const States = [] | ||
const ids = [] | ||
for (const name in obj.States) { | ||
const state = obj.States[name] | ||
if (state.Next) state.Next = obj.States[state.Next] | ||
if (state.Then) state.Then = obj.States[state.Then] | ||
if (state.Else) state.Else = obj.States[state.Else] | ||
if (state.Handler) state.Handler = obj.States[state.Handler] | ||
const id = parseInt(name.substring(name.lastIndexOf('_') + 1)) | ||
state.id = ids[id] = typeof ids[id] !== 'undefined' ? ids[id] : {} | ||
States.push(state) | ||
// wrap params if not a dictionary, branch to error handler if error | ||
function inspect() { | ||
if (!isObject(params)) params = { value: params } | ||
if (typeof params.error !== 'undefined') { | ||
params = { error: params.error } // discard all fields but the error field | ||
state = undefined // abort unless there is a handler in the stack | ||
while (stack.length > 0) { | ||
if (typeof (state = stack.shift().catch) === 'number') break | ||
} | ||
} | ||
} | ||
return { Entry: obj.States[obj.Entry], States, Exit: obj.States[obj.Exit] } | ||
// run function f on current stack | ||
function run(f) { | ||
// update value of topmost matching symbol on stack if any | ||
function set(symbol, value) { | ||
const element = stack.find(element => typeof element.let !== 'undefined' && typeof element.let[symbol] !== 'undefined') | ||
if (typeof element !== 'undefined') element.let[symbol] = JSON.parse(JSON.stringify(value)) | ||
} | ||
// collapse stack for invocation | ||
const env = stack.reduceRight((acc, cur) => typeof cur.let === 'object' ? Object.assign(acc, cur.let) : acc, {}) | ||
let main = '(function(){try{' | ||
for (const name in env) main += `var ${name}=arguments[1]['${name}'];` | ||
main += `return eval((${f}))(arguments[0])}finally{` | ||
for (const name in env) main += `arguments[1]['${name}']=${name};` | ||
main += '}})' | ||
try { | ||
return __eval__(main)(params, env) | ||
} finally { | ||
for (const name in env) set(name, env[name]) | ||
} | ||
} | ||
while (true) { | ||
// final state, return composition result | ||
if (typeof state === 'undefined') { | ||
console.log(`Entering final state`) | ||
console.log(JSON.stringify(params)) | ||
if (params.error) return params; else return { params } | ||
} | ||
// process one state | ||
const json = fsm[state] // json definition for current state | ||
console.log(`Entering state ${state} at path fsm${json.path}`) | ||
const current = state | ||
state = typeof json.next === 'undefined' ? undefined : current + json.next // default next state | ||
switch (json.type) { | ||
case 'choice': | ||
state = current + (params.value ? json.then : json.else) | ||
break | ||
case 'try': | ||
stack.unshift({ catch: current + json.catch }) | ||
break | ||
case 'let': | ||
stack.unshift({ let: JSON.parse(JSON.stringify(json.let)) }) | ||
break | ||
case 'exit': | ||
if (stack.length === 0) return internalError(`State ${current} attempted to pop from an empty stack`) | ||
stack.shift() | ||
break | ||
case 'push': | ||
stack.unshift(JSON.parse(JSON.stringify({ params: json.field ? params[json.field] : params }))) | ||
break | ||
case 'pop': | ||
if (stack.length === 0) return internalError(`State ${current} attempted to pop from an empty stack`) | ||
params = json.collect ? { params: stack.shift().params, result: params } : stack.shift().params | ||
break | ||
case 'action': | ||
return { action: json.name, params, state: { $resume: { state, stack } } } // invoke continuation | ||
break | ||
case 'literal': | ||
params = JSON.parse(JSON.stringify(json.value)) | ||
inspect() | ||
break | ||
case 'function': | ||
let result | ||
try { | ||
result = run(json.exec.code) | ||
} catch (error) { | ||
console.error(error) | ||
result = { error: `An exception was caught at state ${current} (see log for details)` } | ||
} | ||
if (typeof result === 'function') result = { error: `State ${current} evaluated to a function` } | ||
// if a function has only side effects and no return value, return params | ||
params = JSON.parse(JSON.stringify(typeof result === 'undefined' ? params : result)) | ||
inspect() | ||
break | ||
case 'pass': | ||
inspect() | ||
break | ||
default: | ||
return internalError(`State ${current} has an unknown type`) | ||
} | ||
} | ||
} | ||
} | ||
module.exports = new Composer() |
# JSON Format | ||
This document provides a specification of the JSON format for encoding action compositions and its semantics. | ||
__TODO__: document the `Let` and `End` states for variable declarations. | ||
## Principles | ||
An action composition is a kind of [_finite state machine_](https://en.wikipedia.org/wiki/Finite-state_machine) (FSM) with one initial state and one final state. One execution of the action composition (a _trace_) consists of a finite sequence of states starting with the initial state. It is possible for the trace to end at a state other than the final state because of errors or timeouts. | ||
Each state has a unique [_Type_](#state-types) that characterizes the behavior of the state. For example, a `Task` state can specify an OpenWhisk action to run, a `Choice` state can select a next state among two possible successor states. | ||
The input parameter object for the action composition is the input parameter object for the first state of the composition. The output parameter object of the last state in the trace is the output parameter object of the composition (unless a failure occurs). The output parameter object of one state is the input object parameter for the next state in the trace. | ||
An output parameter object of a `Task` state with an `error` field is an _error object_. Error objects interrupt the normal flow of execution. They are processed by the current error handler if any or abort the execution. | ||
In addition to the implicit flow of parameter objects from state to state, an action composition has access to a stack of objects that can be manipulated explicitly using `Push` states and `Pop` states but is also used implicitly by other types of states like `Try` and `Catch`. The stack is initially empty. | ||
## Specification | ||
An action composition is specified by means of a JSON object. The JSON object has three mandatory fields: | ||
* the object field `States` lists the states in the composition, | ||
* the string field `Entry` is the name of the initial state of the composition, | ||
* the string field `Exit` is the name of the final state of the composition. | ||
Additional fields are ignored if present. | ||
Each field of the `States` object describes a state. The state name is the field name. State names are case sensitive and must be pairwise distinct. Each state has a string field [`Type`](#state-types) that characterizes the behavior of the state. For example, a `Task` state can specify via a string field `Action` an OpenWhisk action to run. Most states can specify a successor state via the string field `Next`. | ||
### Example | ||
A sequence of two actions `foo` and `bar` can be encoded as the following: | ||
```json | ||
{ | ||
"Entry": "first_state", | ||
"Exit": "last_state", | ||
"States": { | ||
"first_state": { | ||
"Type": "Task", | ||
"Action": "foo", | ||
"Next": "last_state" | ||
}, | ||
"last_state": { | ||
"Type": "Task", | ||
"Action": "bar" | ||
} | ||
} | ||
} | ||
``` | ||
### Well-formedness | ||
A JSON object is a _well-formed_ action composition if it complies with all the requirements specified in this document. For instance mandatory fields must be present with the required types. The execution of an ill-formed composition may fail in unspecified ways. | ||
### State Types | ||
Each state has a mandatory string field `Type` and possibly additional fields depending on the type of the state. The supported types are `Pass`, `Task`, `Choice`, `Push`, `Pop`, `Try`, and `Catch`. | ||
Every state except for the final state must specify one or two potential successor states. `Choice` states have two potential successor states specified by the string fields `Then` and `Else`. Other non-final states have a single potential successor state specified by the string field `Next`. The final state cannot be a `Choice` state and cannot have a `Next` field. In an execution trace, a state is always followed by one of its potential successors. | ||
The following fields must be specified for each type of state. Other fields are ignored. | ||
| | Pass | Task | Choice | Push | Pop | Try | Catch | | ||
| ------------------------------- |:---------:|:---------:|:---------:|:---------:|:---------:|:---------:|:---------:| | ||
| Type | X | X | X | X | X | X | X | | ||
| Next _(unless state is final)_ | X | X | | X | X | X | X | | ||
| Then | | | X | | | | | | ||
| Else | | | X | | | | | | ||
| Handler | | | | | | X | | | ||
| _kind name_ | | X | | | | | | | ||
The values of the `Next`, `Then`, `Else`, and `Handler` fields must be state names, i.e., names of fields of the `States` object. The `Task` state must specify a task to execute by providing a field named according to its _kind_. The possible field names are `Action`, `Function`, `Value`. | ||
#### Pass State | ||
The Pass state is the identity function on the parameter object. The execution continues with the `Next` state if defined (even if the parameter object is an error object) or terminates if there is no `Next` state (final state). | ||
##### Examples | ||
```json | ||
"intermediate_state": { | ||
"Type": "Pass", | ||
"Next": "next_state" | ||
} | ||
``` | ||
```json | ||
"final_state": { | ||
"Type": "Pass" | ||
} | ||
``` | ||
#### Task State | ||
The `Task` states must contain either a string field named `Action` or `Function` or a JSON object field named `Value`. | ||
* An `Action` task runs the OpenWhisk action with the specified name. | ||
* A `Function` task evaluates the specified Javascript function expression. | ||
* A `Value` task returns the specified JSON object. The input parameter object is discarded. The output parameter object is the value of the `Value` field. | ||
Function expressions occurring in action compositions cannot capture any part of their environment and must return a JSON object. The two syntax `params => params` and `function (params) { return params }` are supported. A `Task` state with a `Function` field invokes the specified function expression on the input parameter object. The output parameter object is the JSON object returned by the function. | ||
If the output parameter object of a `Task` state is not an error object, the execution continues with the `Next` state if defined (non-final state) or terminates if not (final state). If the output parameter object of a `Task` state is an error object, the executions continues with the current error handler if any (see [Try and Catch States](#try-and-catch-states)) or terminates if none. In essence, a `Task` state implicitly throws error objects instead of returning them. | ||
| Output object is | not an error object | an error object | | ||
| ---- |:----:|:----:| | ||
| Transitions to | `Next` state if defined<br> or terminates if not defined | current error handler if any<br>or terminates if no error handler | | ||
When transitioning to an error handler, all the objects pushed to the stack (`Push` state) since the `Try` state that introduced this error handler are popped from the stack. The error handler is also popped from the stack. | ||
A failure to invoke an action, for instance because the action with the specified name does not exist, produces an output parameter object with an `error` field describing the error. Since this is an error object, the executions continues with the current error handler if any or terminates if none. | ||
##### Examples | ||
```json | ||
"action_state": { | ||
"Type": "Task", | ||
"Action": "myAction" | ||
} | ||
``` | ||
```json | ||
"function_state": { | ||
"Type": "Task", | ||
"Function": "params => { params.count++; return params }" | ||
} | ||
``` | ||
```json | ||
"value_state": { | ||
"Type": "Task", | ||
"Value": { | ||
"error": "divide by zero" | ||
} | ||
} | ||
``` | ||
#### Push and Pop States | ||
The `Push` state pushes a clone of the current parameter object to the top of the stack. The execution continues with the `Next` state if defined or terminates if not (final state). The output parameter object of the `Push` state is its input parameter object (no change). | ||
The `Pop` state pops the object at the top of the stack and returns an object with two object fields `result` and `params`, where `result` is the input parameter object and `params` is the object popped from the top of the stack. The execution continues with the `Next` state if defined or terminates if not (final state). | ||
Obviously the stack must not be empty when entering a `Pop` state. Moreover, the object at the top of the stack must have been pushed onto the stack using a `Push` state. | ||
The field names `result` and `params` are chosen so that a sequential composition of three states of type `Push`, `Task`, and `Pop` in this order returns an object where the `params` field contains the input parameter object for the composition and the `result` field contains the output parameter object of the `Task` state. | ||
##### Example | ||
```json | ||
"push_state": { | ||
"Type": "Push", | ||
"Next": "function_state" | ||
}, | ||
"function_state": { | ||
"Type": "Task", | ||
"Function": "params => { params.count++; return params }", | ||
"Next": "pop_state" | ||
}, | ||
"pop_state": { | ||
"Type": "Pop" | ||
} | ||
``` | ||
#### Choice State | ||
The `Choice` state decides among two potential successor states. The execution continues with the `Then` state if the `value` field of the input parameter object is defined and holds JSON's `true` value. It continues with the `Else` state otherwise. | ||
The `Choice` state pops and returns the object at the top of the stack discarding the input parameter object. The `Choice` state is typically used in a sequential composition of three states of type `Push`, `Task`, and `Choice` in this order so that the input parameter object for the composition is also the input parameter object for the `Then` or `Else` state. | ||
Obviously the stack must not be empty when entering a `Choice` state. Moreover, the object at the top of the stack must have been pushed onto the stack using a `Push` state. | ||
##### Example | ||
```json | ||
"push_state": { | ||
"Type": "Push", | ||
"Next": "condition_state" | ||
}, | ||
"condition_state": { | ||
"Type": "Task", | ||
"Function": "params => ({ value: params.count % 2 == 0 })", | ||
"Next": "choice_state" | ||
}, | ||
"choice_state": { | ||
"Type": "Choice", | ||
"Then": "even_state", | ||
"Else": "odd_state" | ||
} | ||
``` | ||
#### Try and Catch States | ||
The `Try` and `Catch` states manage error handlers, i.e., error handling states. The `Try` state pushes a new error handling state whose name is given by its string field `Handler` onto the stack. The `Catch` state pops the handling state at the top of the stack. The topmost handling state is the current handling state that is transitioned to when a `Task` state produces an error object. | ||
The execution of a `Try` or `Catch` state continues with the `Next` state if defined or terminates if not (final state). The output parameter object of the `Try` or `Catch` state is its input parameter object (no change). | ||
Obviously the stack must not be empty when entering a `Catch` state. Moreover, the topmost stack element must have been created using a `Try` state. | ||
##### Example | ||
```json | ||
"try_state": { | ||
"Type": "Try", | ||
"Handler": "handler_state", | ||
"Next": "function_state" | ||
}, | ||
"function_state": { | ||
"Type": "Task", | ||
"Function": "params => (params.den == 0 ? { error: 'divide by 0' } : { result: params.num / params.den })", | ||
"Next": "catch_state" | ||
}, | ||
"catch_state": { | ||
"Type": "Catch", | ||
"Next": "output_state" | ||
}, | ||
"output_state": { | ||
"Type": "Task", | ||
"Function": "params => ({ message: 'Ratio: ' + params.result })", | ||
"Next": "final_state" | ||
}, | ||
"handler_state": { | ||
"Type": "Task", | ||
"Function": "params => ({ message: 'Error: ' + params.error })", | ||
"Next": "final_state" | ||
}, | ||
"final_state": { | ||
"Type": "Pass" | ||
} | ||
``` | ||
This document will soon provide a specification of the JSON format for encoding compositions. |
@@ -1,569 +0,360 @@ | ||
# Introduction to Serverless Composition | ||
# Composer Reference | ||
Composer is an [IBM Cloud Functions](https://ibm.biz/openwhisk) | ||
programming model for composing individual functions into larger | ||
applications. Compositions, informally named _apps_, run in the cloud | ||
using automatically managed compute and memory resources. Composer is | ||
an extension of the function-as-a-service computing model, and enables | ||
stateful computation, control flow, and rich patterns of data flow. | ||
The [`composer`](../composer.js) Node.js module makes it possible define action [compositions](#example) using [combinators](#combinators) and [deploy](#deployment) them. | ||
Composer has two parts. The first is a library for describing | ||
compositions, programmatically. The library is currently available in | ||
Node.js. The second is a runtime that executes the composition. We | ||
will explain these components in greater detail, but first, we will | ||
introduce you to the programming environment for compositions. | ||
## Installation | ||
Programming for the serverless cloud is a uniquely new experience. For | ||
this reason, we have developed a unified environment that offers the | ||
benefits and familiarity of a command line interface, with | ||
visualization and a graphical interface to assist in certain | ||
tasks. This environment is offered through a new tool called `fsh`: | ||
_the functions programming shell for the IBM Cloud_. | ||
To install the `composer` module use the Node Package Manager: | ||
``` | ||
npm -g install @ibm-functions/composer | ||
``` | ||
We recommend to install the module globally (with `-g` option) so the `compose` | ||
command is added to the path. Otherwise, it can be found in the `bin` folder of | ||
the module installation. | ||
- [Programming shell quick start](#programming-shell-quick-start) | ||
- [Installing the shell](#installing-the-shell) | ||
- [Tour of the command line tool](#tour-of-the-programming-shell) | ||
- [Getting setup to run compositions](#before-you-run-your-first-app) | ||
## Example | ||
- Your first composition | ||
- [Create a composition](#your-first-composition) | ||
- [Preview your composition](#previewing-a-composition) | ||
- [Deploy and run](#running-your-first-app) | ||
- [Visualize an execution](#visualizing-sessions) | ||
- [Composing OpenWhisk actions](#composing-openwhisk-actions) | ||
- [Compositions by example](#compositions-by-example) | ||
- [if-then-else](#if-combinator) | ||
- [try-catch](#try-combinator) | ||
- [data forwarding](#nesting-and-forwarding) | ||
- [scoped variables](#variables-and-scoping) | ||
- The Composer programming model | ||
- [The composition library](COMPOSER.md) | ||
- [The underlying composition model](FORMAT.md) | ||
- [The execution model](CONDUCTOR.md) | ||
## Programming shell quick start | ||
The programming shell for functions and compositions is a new | ||
developer experience with fluid migration between a conventional | ||
command line tool and a graphical interface. It is also the | ||
environment for developing and working with serverless compositions. | ||
### Installing the shell | ||
The programming shell is currently distributed through the [Node | ||
package manager](https://www.npmjs.com/package/@ibm-functions/shell). | ||
```bash | ||
$ npm install -g @ibm-functions/shell | ||
A composition is typically defined by means of a Javascript file as illustrated | ||
in [samples/demo.js](samples/demo.js): | ||
```javascript | ||
composer.if( | ||
composer.action('authenticate', { action: function main({ password }) { return { value: password === 'abc123' } } }), | ||
composer.action('success', { action: function main() { return { message: 'success' } } }), | ||
composer.action('failure', { action: function main() { return { message: 'failure' } } })) | ||
``` | ||
Composer offers traditional control-flow concepts as methods. These methods | ||
are called _combinators_. This example composition composes three actions named | ||
`authenticate`, `success`, and `failure` using the `composer.if` combinator, | ||
which implements the usual conditional construct. It take three actions (or | ||
compositions) as parameters. It invokes the first one and, depending on the | ||
result of this invocation, invokes either the second or third action. | ||
We roll out frequent updates and bug fixes. You can check for new | ||
releases via `fsh version -u`. | ||
This composition includes the definitions of the three composed actions. If the | ||
actions are defined and deployed elsewhere, the composition code can be shorten | ||
to: | ||
```javascript | ||
composer.if('authenticate', 'success', 'failure') | ||
``` | ||
$ fsh version -u | ||
You are currently on version 1.3.219 | ||
Checking for updates... you are up to date! | ||
To deploy this composition use the `compose` command: | ||
``` | ||
We recommend updating the shell via the same `npm install` command | ||
shown earlier. Refer to [`npm` troubleshooting](npm.md) if your | ||
installation fails. | ||
### Tour of the programming shell | ||
At the end of the installation, you can run the programming shell form your terminal. It is | ||
typically installed in `/usr/local/bin/fsh`. | ||
compose demo.js --deploy demo | ||
``` | ||
$ fsh | ||
Welcome to the IBM Cloud Functions Shell | ||
The `compose` command synthesizes and deploy an action named `demo` that | ||
implements the composition. It also deploys the composed actions if definitions | ||
are provided for them. | ||
Usage information: | ||
fsh about [ Display version information ] | ||
fsh help [ Show more detailed help, with tutorials ] | ||
fsh shell [ Open graphical shell ] | ||
fsh run <script.fsh> [ Execute commands from a file ] | ||
fsh app init [ Initialize state management ] | ||
fsh app preview <file.js|file.json> [ Prototype a composition, with visualization help ] | ||
fsh app list [ List deployed compositions ] | ||
fsh app create <name> <file.js|file.json> [ Deploy a composition ] | ||
fsh app update <name> <file.js|file.json> [ Update or deploy composition ] | ||
fsh app delete <name> [ Undeploy a composition ] | ||
fsh app invoke <name> [ Invoke a composition and wait for its response ] | ||
fsh app async <name> [ Asynchronously invoke a composition ] | ||
fsh session list [ List recent app invocations ] | ||
fsh session get <sessionId> [ Graphically display the result and flow of a session ] | ||
fsh session result <sessionId> [ Print the return value of a session ] | ||
fsh session kill <sessionId> [ Kill a live session ] | ||
fsh session purge <sessionId> [ Purge the state of a completed session ] | ||
The `demo` composition may be invoked like any action, for instance using the | ||
OpenWhisk CLI: | ||
``` | ||
The commands above allow you to create/update/delete a composition, | ||
visualize the computation, invoke the app, inspect the result and | ||
the dynamic execution graph. | ||
### Before you run your first app | ||
Composer allows you to orchestrate the execution of several cloud | ||
functions, and further, to describe the dataflow between them. Its | ||
model of computing automatically manages the state of the application | ||
as it executes, and determines which functions to execute at any given | ||
transition. This automatically managed state requires a backing | ||
store, and the current implementation of Composer uses | ||
[Redis](https://redis.io/) for this purpose. | ||
Before running an app, you must have a valid IBM Cloud (i.e., Bluemix) | ||
[account](https://ibm.biz/openwhisk), or deploy [Apache | ||
OpenWhisk](https://github.com/apache/incubator-openwhisk) | ||
locally. This is needed because Composer builds on and extends Apache | ||
OpenWhisk, which powers IBM Cloud Functions. | ||
* _Using composer with IBM Cloud Functions:_ you need an IBM Cloud | ||
[account](https://ibm.biz/openwhisk), and a valid access token which | ||
you can get using [`bx login`](https://console.bluemix.net/openwhisk/learn/cli). | ||
_Tip:_ you do not need to perform the login operations if you simply | ||
want to locally build and preview a composition. The setup described | ||
here is strictly required for actually deploying and running a | ||
composition in the IBM Cloud. | ||
wsk action invoke demo -r -p password passw0rd | ||
``` | ||
$ bx login -a api.ng.bluemix.net -o yourBluemixOrg -s yourBluemixSpace | ||
```json | ||
{ | ||
"message": "failure" | ||
} | ||
``` | ||
* _Using composer with Apache OpenWhisk:_ you need a valid | ||
`$HOME/.wskprops` file and a locally deployed OpenWhisk instance. | ||
The shell initializes the backing store with `fsh app init`: | ||
## Activation Records | ||
An invocation of a composition creates a series of activation records: | ||
``` | ||
$ fsh app init --url redis://user:password@hostname:port | ||
Waiting for redis [Done] | ||
Successfully initialized the required services. You may now create compositions. | ||
wsk action invoke demo -p password passw0rd | ||
``` | ||
For Openwhisk, the actual command is shown below. | ||
``` | ||
$ fsh app init --url redis://192.168.99.100:6379 | ||
ok: invoked /_/demo with id 4f91f9ed0d874aaa91f9ed0d87baaa07 | ||
``` | ||
For the IBM Cloud, you can provision a [Redis instance | ||
yourself](redis.md) and retrieve its service keys to initialize the | ||
shell in a similar way. Alternatively, you can use an _experimental_ | ||
auto-provisioning feature via `fsh app init --auto`. Note that | ||
[charges will | ||
apply](https://console.bluemix.net/catalog/services/compose-for-redis) | ||
for the provisioned Redis instance. | ||
The initialization step creates a package in your namespace called | ||
`bluemix.redis` which includes useful administrative operations. Read | ||
more about Redis provisioning [here](redis.md). | ||
## Your first composition | ||
Compositions are described using a [Node.js library](COMPOSER.md) | ||
which offers an SDK for describing control structures. We call these | ||
_combinators_. The simplest combinator constructs a sequence. Here is | ||
a composition snippet to create your first app: it creates a sequence | ||
with just one function that is inlined for convenience. _You may user | ||
your favorite editor to compose apps._ When finished, save your code | ||
to a file with the extension `.js`. | ||
```javascript | ||
composer.sequence(args => ({msg: `hello ${args.name}!`})) | ||
``` | ||
You use the `composer` to construct an application, then _compile_ it | ||
into a [finite state machine (FSM)](FORMAT.md) representation, encoded | ||
as a JSON object. While you can author a composition directly as an | ||
FSM, it is far more convenient and natural to program at the level of | ||
the Node.js library instead. It is the FSM that is used to create the | ||
app in the IBM Cloud. [Later examples](#compositions-by-example) we will | ||
show how to create more elaborate compositions using `if-then-else`, | ||
`try-catch`, and `while` combinators to name a few. | ||
_Advanced Tip:_ It is possible to compile the FSM without using the | ||
shell, and instead using `node` directly. In this case, you must | ||
import the `composer` and `compile` the FSM explicitly as shown below. | ||
```javascript | ||
$ npm install @openwhisk/composer | ||
$ node | ||
const composer = require('@openwhisk/composer') | ||
const app = composer.sequence(args => ({msg: `hello ${args.name}!`})) | ||
composer.compile(app, 'hello.json') | ||
wsk activation list | ||
``` | ||
``` | ||
activations | ||
fd89b99a90a1462a89b99a90a1d62a8e demo | ||
eaec119273d94087ac119273d90087d0 failure | ||
3624ad829d4044afa4ad829d40e4af60 demo | ||
a1f58ade9b1e4c26b58ade9b1e4c2614 authenticate | ||
3624ad829d4044afa4ad829d40e4af60 demo | ||
4f91f9ed0d874aaa91f9ed0d87baaa07 demo | ||
``` | ||
The entry with the earliest start time (`4f91f9ed0d874aaa91f9ed0d87baaa07`) summarizes the invocation of the composition while other entries record later activations caused by the composition invocation. There is one entry for each invocation of a composed action (`a1f58ade9b1e4c26b58ade9b1e4c2614` and `eaec119273d94087ac119273d90087d0`). The remaining entries record the beginning and end of the composition as well as the transitions between the composed actions. | ||
## Previewing a composition | ||
Compositions are implemented by means of OpenWhisk conductor actions. The [documentation of conductor actions](https://github.com/apache/incubator-openwhisk/blob/master/docs/conductors.md) discusses activation records in greater details. | ||
The programming shell offers a visual representation of a composition | ||
to quickly validate if the app represents the desired control flow | ||
structure, before actually deploying any assets to the cloud. This | ||
code snippet is bundled with the shell as | ||
[`@demos/hello.js`](https://github.com/ibm-functions/shell/blob/master/app/demos/hello.js). | ||
## Deployment | ||
```bash | ||
$ fsh app preview @demos/hello.js | ||
The `compose` command when not invoked with the `--deploy` option returns the composition encoded as a JSON dictionary: | ||
``` | ||
|<img src="hello-composition.png" width="50%" title="Hello app">| | ||
|:--:| | ||
|Composition preview showing the control flow for the app.| | ||
You can view the actual JSON description of the FSM by clicking on the | ||
corresponding tab in the shell UI. | ||
_Tip:_ The shell watches the file you are editing and automatically | ||
updates the view as you compose. You can use this active preview mode | ||
to incrementally build your application, sanity checking your control | ||
flow as you go. | ||
## Running your first app | ||
You create and invoke apps in a manner similar to working with | ||
OpenWhisk actions. | ||
```bash | ||
$ fsh app create hello @demos/hello.js | ||
$ fsh app invoke hello -p name composer | ||
compose demo.js | ||
``` | ||
```json | ||
{ | ||
msg: 'hello composer!' | ||
"actions": [ | ||
{ | ||
"name": "/_/authenticate", | ||
"action": { | ||
"exec": { | ||
"kind": "nodejs:default", | ||
"code": "function main({ password }) { return { value: password === 'abc123' } }" | ||
} | ||
} | ||
}, | ||
{ | ||
"name": "/_/success", | ||
"action": { | ||
"exec": { | ||
"kind": "nodejs:default", | ||
"code": "function main() { return { message: 'success' } }" | ||
} | ||
} | ||
}, | ||
{ | ||
"name": "/_/failure", | ||
"action": { | ||
"exec": { | ||
"kind": "nodejs:default", | ||
"code": "function main() { return { message: 'failure' } }" | ||
} | ||
} | ||
} | ||
], | ||
"composition": [ | ||
{ | ||
"type": "if", | ||
"test": [ | ||
{ | ||
"type": "action", | ||
"name": "/_/authenticate" | ||
} | ||
], | ||
"consequent": [ | ||
{ | ||
"type": "action", | ||
"name": "/_/success" | ||
} | ||
], | ||
"alternate": [ | ||
{ | ||
"type": "action", | ||
"name": "/_/failure" | ||
} | ||
] | ||
} | ||
] | ||
} | ||
``` | ||
The JSON format is documented in [FORMAT.md](FORMAT.md). The format is meant to be stable, self-contained, language-independent, and human-readable. The JSON dictionary includes the definition for the composition as well as definitions of nested actions and compositions (if any). | ||
_Tip:_ If you have an action already named `hello`, the shell will | ||
report a name conflict. Use a different name for your app. Apps are | ||
stored as OpenWhisk actions, and hence the naming restrictions for | ||
OpenWhisk apply. | ||
A JSON-encoded composition may be deployed using the `compose` command: | ||
``` | ||
compose demo.js > demo.json | ||
compose demo.json --deploy demo | ||
``` | ||
The `compose` command can also produce the code of the conductor action generated for the composition: | ||
``` | ||
compose demo.js --encode | ||
``` | ||
```javascript | ||
const main=(function init(e,t){function r(e,t){return e.slice(-1)[0].next=1,e.push(...t),e}const a=function e(t,a=""){if(Array.isArray(t))return 0===t.length?[{type:"pass",path:a}]:t.map((t,r)=>e(t,a+"["+r+"]")).reduce(r);const n=t.options||{};switch(t.type){case"action":return[{type:"action",name:t.name,path:a}];case"function":return[{type:"function",exec:t.exec,path:a}];case"literal":return[{type:"literal",value:t.value,path:a}];case"finally":var s=e(t.body,a+".body");const l=e(t.finalizer,a+".finalizer");return(o=[[{type:"try",path:a}],s,[{type:"exit",path:a}],l].reduce(r))[0].catch=o.length-l.length,o;case"let":return s=e(t.body,a+".body"),[[{type:"let",let:t.declarations,path:a}],s,[{type:"exit",path:a}]].reduce(r);case"retain":s=e(t.body,a+".body");var o=[[{type:"push",path:a}],s,[{type:"pop",collect:!0,path:a}]].reduce(r);return n.field&&(o[0].field=n.field),o;case"try":s=e(t.body,a+".body");const h=r(e(t.handler,a+".handler"),[{type:"pass",path:a}]);return(o=[[{type:"try",path:a}],s].reduce(r))[0].catch=o.length,o.slice(-1)[0].next=h.length,o.push(...h),o;case"if":var p=e(t.consequent,a+".consequent"),c=r(e(t.alternate,a+".alternate"),[{type:"pass",path:a}]);return n.nosave||(p=r([{type:"pop",path:a}],p)),n.nosave||(c=r([{type:"pop",path:a}],c)),o=r(e(t.test,a+".test"),[{type:"choice",then:1,else:p.length+1,path:a}]),n.nosave||(o=r([{type:"push",path:a}],o)),p.slice(-1)[0].next=c.length,o.push(...p),o.push(...c),o;case"while":return p=e(t.body,a+".body"),c=[{type:"pass",path:a}],n.nosave||(p=r([{type:"pop",path:a}],p)),n.nosave||(c=r([{type:"pop",path:a}],c)),o=r(e(t.test,a+".test"),[{type:"choice",then:1,else:p.length+1,path:a}]),n.nosave||(o=r([{type:"push",path:a}],o)),p.slice(-1)[0].next=1-o.length-p.length,o.push(...p),o.push(...c),o;case"dowhile":var i=e(t.test,a+".test");return n.nosave||(i=r([{type:"push",path:a}],i)),o=[e(t.body,a+".body"),i,[{type:"choice",then:1,else:2,path:a}]].reduce(r),n.nosave?(o.slice(-1)[0].then=1-o.length,o.slice(-1)[0].else=1):(o.push({type:"pop",path:a}),o.slice(-1)[0].next=1-o.length),c=[{type:"pass",path:a}],n.nosave||(c=r([{type:"pop",path:a}],c)),o.push(...c),o}}(t),n=e=>"object"==typeof e&&null!==e&&!Array.isArray(e),s=e=>Promise.reject({code:400,error:e}),o=e=>Promise.reject((e=>({code:"number"==typeof e.code&&e.code||500,error:"string"==typeof e.error&&e.error||e.message||"string"==typeof e&&e||"An internal error occurred"}))(e));return t=>Promise.resolve().then(()=>(function(t){let r=0,p=[];if(void 0!==t.$resume){if(!n(t.$resume))return s("The type of optional $resume parameter must be object");if(r=t.$resume.state,p=t.$resume.stack,void 0!==r&&"number"!=typeof r)return s("The type of optional $resume.state parameter must be number");if(!Array.isArray(p))return s("The type of $resume.stack must be an array");delete t.$resume,c()}function c(){if(n(t)||(t={value:t}),void 0!==t.error)for(t={error:t.error},r=void 0;p.length>0&&"number"!=typeof(r=p.shift().catch););}function i(r){function a(e,t){const r=p.find(t=>void 0!==t.let&&void 0!==t.let[e]);void 0!==r&&(r.let[e]=JSON.parse(JSON.stringify(t)))}const n=p.reduceRight((e,t)=>"object"==typeof t.let?Object.assign(e,t.let):e,{});let s="(function(){try{";for(const e in n)s+=`var ${e}=arguments[1]['${e}'];`;s+=`return eval((${r}))(arguments[0])}finally{`;for(const e in n)s+=`arguments[1]['${e}']=${e};`;s+="}})";try{return e(s)(t,n)}finally{for(const e in n)a(e,n[e])}}for(;;){if(void 0===r)return console.log("Entering final state"),console.log(JSON.stringify(t)),t.error?t:{params:t};const e=a[r];console.log(`Entering state ${r} at path fsm${e.path}`);const n=r;switch(r=void 0===e.next?void 0:n+e.next,e.type){case"choice":r=n+(t.value?e.then:e.else);break;case"try":p.unshift({catch:n+e.catch});break;case"let":p.unshift({let:JSON.parse(JSON.stringify(e.let))});break;case"exit":if(0===p.length)return o(`State ${n} attempted to pop from an empty stack`);p.shift();break;case"push":p.unshift(JSON.parse(JSON.stringify({params:e.field?t[e.field]:t})));break;case"pop":if(0===p.length)return o(`State ${n} attempted to pop from an empty stack`);t=e.collect?{params:p.shift().params,result:t}:p.shift().params;break;case"action":return{action:e.name,params:t,state:{$resume:{state:r,stack:p}}};case"literal":t=JSON.parse(JSON.stringify(e.value)),c();break;case"function":let a;try{a=i(e.exec.code)}catch(e){console.error(e),a={error:`An exception was caught at state ${n} (see log for details)`}}"function"==typeof a&&(a={error:`State ${n} evaluated to a function`}),t=JSON.parse(JSON.stringify(void 0===a?t:a)),c();break;case"pass":c();break;default:return o(`State ${n} has an unknown type`)}}})(t)).catch(o)})(eval,[{"type":"if","test":[{"type":"action","name":"/_/authenticate"}],"consequent":[{"type":"action","name":"/_/success"}],"alternate":[{"type":"action","name":"/_/failure"}]}]) | ||
``` | ||
This code may be deployed using the OpenWhisk CLI: | ||
``` | ||
compose demo.js > demo-conductor.js | ||
wsk action create demo demo-conductor.js -a conductor true | ||
``` | ||
In contrast to the JSON format, the conductor action code does not include definitions for nested actions or compositions. | ||
All app activations are asynchronous and non-blocking. The immediate | ||
result of an invocation is a _session id_, which you may use to query | ||
the app for its final output. For development convenience, the shell | ||
implements a client-side poll to provide a the final output of the | ||
app, if it is ready within 30 seconds. Otherwise, you may use the | ||
session id to retrieve the output; in this way, working with a session | ||
id is similar to working with an activation id when invoking an | ||
action. | ||
## Parameter Objects and Error Objects | ||
_Tip:_ You may disable the client-side poll by using `app async` | ||
instead of `app invoke`. The session id is returned immediately when | ||
it is available. | ||
A composition, like any action, accepts a JSON dictionary (the _input parameter object_) and produces a JSON dictionary (the _output parameter object_). An output parameter object with an `error` field is an _error object_. A composition _fails_ if it produces an error object. | ||
```bash | ||
$ fsh session list # lists all recent sessions | ||
$ fsh session result <id> # retrieves the JSON output of the app as text | ||
``` | ||
By convention, an error object returned by a composition is stripped from all fields except from the `error` field. This behavior is consistent with the OpenWhisk action semantics, e.g., the action with code `function main() { return { error: 'KO', message: 'OK' } }` outputs `{ error: 'KO' }`. | ||
_Note:_ Sessions only persist for up to 24 hours, and expire automatically. | ||
## Combinators | ||
## Visualizing sessions | ||
The `composer` module offers a number of combinators to define compositions: | ||
The shell can also summarize the dynamic execution flow of an app, as | ||
described by the app session id. | ||
| Combinator | Description | Example | | ||
| --:| --- | --- | | ||
| [`action`](#action) | action | `composer.action('echo')` | | ||
| [`function`](#function) | function | `composer.function(({ x, y }) => ({ product: x * y }))` | | ||
| [`literal` or `value`](#literal) | constant value | `composer.literal({ message: 'Hello, World!' })` | | ||
| [`sequence` or `seq`](#sequence) | sequence | `composer.sequence('hello', 'bye')` | | ||
| [`let`](#let) | variable declarations | `composer.let({ count: 3, message: 'hello' }, ...)` | | ||
| [`if`](#if) | conditional | `composer.if('authenticate', 'success', 'failure')` | | ||
| [`while`](#while) | loop | `composer.while('notEnough', 'doMore')` | | ||
| [`dowhile`](#dowhile) | loop at least once | `composer.dowhile('fetchData', 'needMoreData')` | | ||
| [`repeat`](#repeat) | counted loop | `composer.repeat(3, 'hello')` | | ||
| [`try`](#try) | error handling | `composer.try('divideByN', 'NaN')` | | ||
| [`finally`](#finally) | finalization | `composer.finally('tryThis', 'doThatAlways')` | | ||
| [`retry`](#retry) | error recovery | `composer.retry(3, 'connect')` | | ||
| [`retain`](#retain) | persistence | `composer.retain('validateInput')` | | ||
```bash | ||
$ fsh session get <id> | ||
``` | ||
The `action`, `function`, and `literal` combinators and their synonymous construct compositions respectively from actions, functions, and constant values. The other combinators combine existing compositions to produce new compositions. | ||
|<img src="hello-session.png" width="50%" title="Hello session">| | ||
|:--:| | ||
|An example session.| | ||
Where a composition is expected, the following shorthands are permitted: | ||
- `name` of type `string` stands for `composer.action(name)`, | ||
- `fun` of type `function` stands for `composer.function(fun)`, | ||
- `null` stands for the empty sequence `composer.sequence()`. | ||
The session view uses a green color scheme for a successful | ||
activation, and red for a failed activation (i.e., the result of the | ||
function or app is an `error`.) The `Entry` and `Exit` nodes are the | ||
logical start and end states of the activation. Hovering over the | ||
nodes will typically show the result of the underlying function or | ||
app. | ||
### Action | ||
## Composing OpenWhisk actions | ||
`composer.action(name, [options])` is a composition with a single action named _name_. It invokes the action named _name_ on the input parameter object for the composition and returns the output parameter object of this action invocation. | ||
Combinators accept either inline Node.js functions or actions by name. | ||
For the latter, you may use a fully qualified name of an action (i.e., | ||
`/namespace[/package]/action`) or its short name. Here is an example | ||
using the `date` action from the `/whisk.system/utils` package. | ||
The action _name_ may specify the namespace and/or package containing the action following the usual OpenWhisk grammar. If no namespace is specified, the default namespace is assumed. If no package is specified, the default package is assumed. | ||
Examples: | ||
```javascript | ||
composer.sequence('/whisk.system/utils/date') | ||
composer.action('hello') | ||
composer.action('myPackage/myAction') | ||
composer.action('/whisk.system/utils/echo') | ||
``` | ||
The optional `options` dictionary makes it possible to provide a definition for the action being composed: | ||
```javascript | ||
// specify the code for the action | ||
composer.action('hello', { action: function main() { return { message: 'hello' } } }) | ||
composer.action('hello', { action: "function main() { return { message: 'hello' } }" }) | ||
composer.action('hello', { | ||
action: { | ||
kind: 'nodejs:default', | ||
code: "function main() { return { message: 'hello' } }" | ||
} | ||
}) | ||
A composition which refers to actions by name will not run correctly | ||
if there are missing referenced entities. The `app preview` will | ||
highlight any missing entities. As an example, preview the built-in | ||
[`@demos/if.js`](https://github.com/ibm-functions/shell/blob/master/app/demos/if.js) | ||
composition, which is [described in the next | ||
section](#if-combinator). The control flow graph should be | ||
self-explanatory. An action is gray when it is not yet deployed, and | ||
blue otherwise. | ||
// specify a file containing the code for the action | ||
composer.action('hello', { filename: 'hello.js' }) | ||
```bash | ||
$ fsh app preview @demos/if.js | ||
// define an action sequence | ||
composer.action('helloAndBye', { sequence: ['hello', 'bye'] }) | ||
``` | ||
|<img src="if-preview.png" title="if combinator preview" width="50%">| | ||
|:--:| | ||
|Control flow graph for `if` combinator. An action that is not yet deployed is gray, and blue otherwise.| | ||
### Function | ||
To create and deploy the actions, you may use the `wsk` CLI or the | ||
OpenWhisk API directly. For added convenience, `fsh` uses [`npm | ||
openwhisk`](http://github.com/openwhisk/) and can create actions | ||
directly. Its command structure for creating an action will be | ||
familiar to `wsk` users (but does not offer full parity). You may | ||
find it convenient to use `fsh` directly instead for everything, | ||
including to create and update actions. Read more about [`fsh` vs | ||
`wsk`](fsh.md). | ||
`composer.function(fun)` is a composition with a single Javascript function _fun_. It applies the specified function to the input parameter object for the composition. | ||
- If the function returns a value of type `function`, the composition returns an error object. | ||
- If the function throws an exception, the composition returns an error object. The exception is logged as part of the conductor action invocation. | ||
- If the function returns a value of type other than function, the value is first converted to a JSON value using `JSON.stringify` followed by `JSON.parse`. If the resulting JSON value is not a JSON dictionary, the JSON value is then wrapped into a `{ value }` dictionary. The composition returns the final JSON dictionary. | ||
- If the function does not return a value and does not throw an exception, the composition returns the input parameter object for the composition converted to a JSON dictionary using `JSON.stringify` followed by `JSON.parse`. | ||
## Compositions by example | ||
Examples: | ||
```javascript | ||
composer.function(params => ({ message: 'Hello ' + params.name })) | ||
composer.function(function (params) { return { error: 'error' } }) | ||
You now have the basic tools to build a serverless composition, invoke | ||
it, and inspect its execution and result. This section will introduce | ||
you to more combinators for creating richer control and data flow. | ||
function product({ x, y }) { return { product: x * y } } | ||
composer.function(product) | ||
``` | ||
The following composition methods are currently supported. The rest of the document will show you example compositions using some of these combinators. The rest of the combinators are covered in the [reference manual](COMPOSER.md). | ||
#### Environment capture | ||
| Composition | Description | Example | | ||
| --:| --- | --- | | ||
| [`task`](COMPOSER.md#composertasktask-options) | single task | `composer.task('sayHi', { input: 'userInfo' })` | | ||
| [`value`](COMPOSER.md#composervaluejson) | constant value | `composer.value({ message: 'Hello World!' })` | | ||
| [`sequence`](COMPOSER.md#composersequencetask_1-task_2-) | sequence | `composer.sequence('getLocation', 'getWeatherForLocation')` | | ||
| [`let`](COMPOSER.md#composerletname-value-task_1-task_2-) | variables | `composer.let('n', 42, ...)` | | ||
| [`if`](COMPOSER.md#composerifcondition-consequent-alternate) | conditional | `composer.if('authenticate', /* then */ 'welcome', /* else */ 'login')` | | ||
| [`while`](COMPOSER.md#composerwhilecondition-task) | loop | `composer.while('needMoreData', 'fetchMoreData')` | | ||
| [`try`](COMPOSER.md#composertrytask-handler) | error handling | `try('DivideByN', /* catch */ 'NaN')` | | ||
| [`repeat`](COMPOSER.md#composerrepeatcount-task) | repetition | `repeat(42, 'sayHi')` | | ||
| [`retry`](COMPOSER.md#composerretrycount-task) | error recovery | `retry(3, 'connect')` | | ||
| [`retain`](COMPOSER.md#composerretaintask-flag) | parameter retention | `composer.retain('validateInput')` | | ||
Functions intended for compositions cannot capture any part of their declaration environment. They may however access and mutate variables in an environment consisting of the variables declared by the [composer.let](#composerletname-value-composition_1-composition_2-) combinator discussed below. | ||
### `if` combinator | ||
An `if` combinator allows you to describe a conditional flow with a | ||
`then` and optional `else` branch. This is convenient for | ||
short-circuiting a sequence for example, or taking data-dependent | ||
paths in the control flow. | ||
Here is a short example. Say you have a function `welcome` which | ||
generates an HTML page. | ||
The following is not legal: | ||
```javascript | ||
let welcome = args => ({ html: `<html><body>welcome ${args.name}!</body></html>` }) | ||
let name = 'Dave' | ||
composer.function(params => ({ message: 'Hello ' + name })) | ||
``` | ||
In order to use this function as part of an authenticated API, we can | ||
modify the function itself to introduce authentication middleware. Or, | ||
we can compose it with an authentication function. | ||
The following is legal: | ||
```javascript | ||
let authenticate = args => ({ value: args.token === "secret" }) | ||
composer.let({ name: 'Dave' }, composer.function(params => ({ message: 'Hello ' + name }))) | ||
``` | ||
For illustration purposes, `authenticate` is a simple token based | ||
checker. If the token equals the secret value, return `true`, and | ||
`false` otherwise. In a real scenario, this function may delegate to a | ||
third party service or identity provider. | ||
### Literal | ||
Let's add a third function, this one to deal with the | ||
non-authenticated case and return a different HTML page, perhaps | ||
informing the client to try again with the proper secret. | ||
`composer.literal(value)` and its synonymous `composer.value(value)` output a constant JSON dictionary. This dictionary is obtained by first converting the _value_ argument to JSON using `JSON.stringify` followed by `JSON.parse`. If the resulting JSON value is not a JSON dictionary, the JSON value is then wrapped into a `{ value }` dictionary. | ||
The _value_ argument may be computed at composition time. For instance, the following composition captures the date at the time the composition is encoded to JSON: | ||
```javascript | ||
let login = args => ({ html: `<html><body>please say the magic word.</body></html>` }) | ||
composer.literal(Date()) | ||
``` | ||
The `if` combinator composes these three functions as you might | ||
expect. This example is bundled in the shell samples as | ||
[`@demos/if.js`](https://github.com/ibm-functions/shell/blob/master/app/demos/if.js). | ||
### Sequence | ||
```javascript | ||
composer.if( | ||
/* cond */ 'authenticate', | ||
/* then */ 'welcome', | ||
/* else */ 'login') | ||
``` | ||
`composer.sequence(composition_1, composition_2, ...)` chains a series of compositions (possibly empty). | ||
```bash | ||
# create required actions | ||
$ fsh action create authenticate @demos/authenticate.js | ||
$ fsh action create welcome @demos/welcome.js | ||
$ fsh action create login @demos/login.js | ||
The input parameter object for the composition is the input parameter object of the first composition in the sequence. The output parameter object of one composition in the sequence is the input parameter object for the next composition in the sequence. The output parameter object of the last composition in the sequence is the output parameter object for the composition. | ||
# create app | ||
$ fsh app create if @demos/if.js | ||
If one of the components fails, the remainder of the sequence is not executed. The output parameter object for the composition is the error object produced by the failed component. | ||
# invoke app, with no secret parameter | ||
$ fsh app invoke if | ||
{ | ||
html: "<html><body>please say the magic word.</body></html>" | ||
} | ||
An empty sequence behaves as a sequence with a single function `params => params`. The output parameter object for the empty sequence is its input parameter object unless it is an error object, in which case, as usual, the error object only contains the `error` field of the input parameter object. | ||
# now invoke with secret parameter | ||
$ fsh app invoke if -p token secret -p name if-combinator | ||
{ | ||
html: "<html><body>welcome if-combinator!</body></html>" | ||
} | ||
``` | ||
### Let | ||
Each of the activations will have a different session id, which are reported by listing the available sessions. | ||
```bash | ||
$ fsh session list | ||
sessionId app start status | ||
339c82e5e1ad45cd9c82e5e1ada5cd24 if 10/6/2017, 6:53:28 PM done | ||
9c361e9d06364064b61e9d0636e06482 if 10/6/2017, 6:53:21 PM done | ||
``` | ||
`composer.let({ name_1: value_1, name_2: value_2, ... }, composition_1_, _composition_2_, ...)` declares one or more variables with the given names and initial values, and runs runs a sequence of compositions in the scope of these declarations. | ||
### `try` combinator | ||
Variables declared with `composer.let` may be accessed and mutated by functions __running__ as part of the following sequence (irrespective of their place of definition). In other words, name resolution is [dynamic](https://en.wikipedia.org/wiki/Name_resolution_(programming_languages)#Static_versus_dynamic). If a variable declaration is nested inside a declaration of a variable with the same name, the innermost declaration masks the earlier declarations. | ||
Another common composition pattern is for error handling and | ||
recovery. Composer offers a `try` combinator that is analogous to | ||
`try-catch`. | ||
A example to illustrate using `try` is a schema or data validation | ||
action. Let `validate` be an action which checks if a string is base64 | ||
encoded, and which throws an exception if the input is not valid. A | ||
`try` combinator allows an error handler to rewrite the result, as | ||
one example, to suite the particular usage scenario in the app. | ||
For example, the following composition invokes composition `composition` repeatedly `n` times. | ||
```javascript | ||
composer.try( | ||
/* try */ 'validate', | ||
/* catch */ args => ({ ok: false })) | ||
composer.let({ i: n }, composer.while(() => i-- > 0, composition)) | ||
``` | ||
Variables declared with `composer.let` are not visible to invoked actions. However, they may be passed as parameters to actions as for instance in: | ||
```javascript | ||
composer.let({ n: 42 }, () => ({ n }), 'increment', params => { n = params.n }) | ||
``` | ||
The `validate` action is available as [`@demos/validate.js`](https://github.com/ibm-functions/shell/blob/master/app/demos/validate.js) and the | ||
composition as [`@demos/try.js`](https://github.com/ibm-functions/shell/blob/master/app/demos/try.js) for your convenience. | ||
In this example, the variable `n` is exposed to the invoked action as a field of the input parameter object. Moreover, the value of the field `n` of the output parameter object is assigned back to variable `n`. | ||
```bash | ||
# create validate action | ||
$ fsh action create validate @demos/validate.js | ||
### If | ||
# create app | ||
$ fsh app create try @demos/try.js | ||
`composer.if(condition, consequent, [alternate], [options])` runs either the _consequent_ composition if the _condition_ evaluates to true or the _alternate_ composition if not. | ||
# invoke app with valid parameter | ||
$ fsh app invoke try -p str aGVsbG8gdHJ5IQ== | ||
{ | ||
ok: true | ||
} | ||
A _condition_ composition evaluates to true if and only if it produces a JSON dictionary with a field `value` with value `true`. Other fields are ignored. Because JSON values other than dictionaries are implicitly lifted to dictionaries with a `value` field, _condition_ may be a Javascript function returning a Boolean value. An expression such as `params.n > 0` is not a valid condition (or in general a valid composition). One should write instead `params => params.n > 0`. The input parameter object for the composition is the input parameter object for the _condition_ composition. | ||
# and now for the failing case | ||
$ fsh app invoke try -p str bogus | ||
{ | ||
ok: false | ||
} | ||
``` | ||
The _alternate_ composition may be omitted. If _condition_ fails, neither branch is executed. | ||
It is worth looking at the execution of the second app invoke where | ||
the catch handler is invoked. | ||
The optional `options` dictionary supports a `nosave` option. If `options.nosave` is thruthy, the _consequent_ composition or _alternate_ composition is invoked on the output parameter object of the _condition_ composition. Otherwise, the output parameter object of the _condition_ composition is discarded and the _consequent_ composition or _alternate_ composition is invoked on the input parameter object for the composition. For example, the following compositions divide parameter `n` by two if `n` is even: | ||
```javascript | ||
composer.if(params => params.n % 2 === 0, params => { params.n /= 2 }) | ||
composer.if(params => { params.value = params.n % 2 === 0 }, params => { params.n /= 2 }, null, { nosave: true }) | ||
``` | ||
$ fsh session get --last try | ||
``` | ||
In the first example, the condition function simply returns a Boolean value. The consequent function uses the saved input parameter object to compute `n`'s value. In the second example, the condition function adds a `value` field to the input parameter object. The consequent function applies to the resulting object. In particular, in the second example, the output parameter object for the condition includes the `value` field. | ||
|<img src="try-session.png" title="try session with exception" width="50%">| | ||
|:--:| | ||
|Session execution for `try` where the handler is invoked.| | ||
While, the default `nosave == false` behavior is typically more convenient, preserving the input parameter object is not free as it counts toward the parameter size limit for OpenWhisk actions. In essence, the limit on the size of parameter objects processed during the evaluation of the condition is reduced by the size of the saved parameter object. The `nosave` option omits the parameter save, hence preserving the parameter size limit. | ||
### While | ||
Notice that the `validate` action failed, as expected. This is | ||
visually recognized by the red-colored action, and the hover text which | ||
shows the action result containing the error. The app result is | ||
successful however, as the handler rewrites the exception into a | ||
different result. | ||
`composer.while(condition, body, [options])` runs _body_ repeatedly while _condition_ evaluates to true. The _condition_ composition is evaluated before any execution of the _body_ composition. See [composer.if](#composerifcondition-consequent-alternate) for a discussion of conditions. | ||
## Nesting and forwarding | ||
A failure of _condition_ or _body_ interrupts the execution. The composition returns the error object from the failed component. | ||
An important property of the combinators is that they nest. This | ||
encourages modularity and composition reuse. The example that follows | ||
illustrates both composition nesting, and data forwarding. The example | ||
builds on the `try` app described in the previous section. Here, after | ||
the validate task, we extend the composition with a base64 decoder to | ||
render the input `str` in plain text. | ||
Like `composer.if`, `composer.while` supports a `nosave` option. By default, the output parameter object of the _condition_ composition is discarded and the input parameter object for the _body_ composition is either the input parameter object for the whole composition the first time around or the output parameter object of the previous iteration of _body_. However if `options.nosave` is thruthy, the input parameter object for _body_ is the output parameter object of _condition_. Moreover, the output parameter object for the whole composition is the output parameter object of the last _condition_ evaluation. | ||
Recall that the result of the `validate` task is `{ok: true}`, | ||
not the `str` argument that it processed. So we need a way to forward | ||
`str` around this action. In other words, we _retain_ the input | ||
arguments to `validate`, and pass them to the next action in the | ||
sequence. Composer offers a combinator for just this purpose. Below | ||
is the composition showing the inner sequence with the data forwarding | ||
combinator `retain`. | ||
For instance, the following composition invoked on dictionary `{ n: 28 }` outputs `{ n: 7 }`: | ||
```javascript | ||
composer.try( | ||
composer.sequence( | ||
composer.retain('validate'), | ||
args => ({ text: new Buffer(args.params.str, 'base64').toString() })), | ||
args => ({ ok: false })) | ||
composer.while(params => params.n % 2 === 0, params => { params.n /= 2 }) | ||
``` | ||
For instance, the following composition invoked on dictionary `{ n: 28 }` outputs `{ n: 7, value: false }`: | ||
```javascript | ||
composer.while(params => { params.value = params.n % 2 === 0 }, params => { params.n /= 2 }, { nosave: true }) | ||
``` | ||
The `retain` combinator produces an output with two fields: `params` | ||
and `result`. The former is the input parameter of the | ||
composition. The latter is the output of `validate`. The control and | ||
dataflow for this composition is shown below, and is available in the | ||
shell as | ||
[`@demos/retain.js`](https://github.com/ibm-functions/shell/blob/master/app/demos/retain.js). | ||
### Dowhile | ||
```bash | ||
$ fsh app preview @demos/retain.js | ||
``` | ||
`composer.dowhile(condition, body, [options])` is similar to `composer.while(body, condition, [options])` except that _body_ is invoked before _condition_ is evaluated, hence _body_ is always invoked at least once. | ||
|<img src="retain.png" title="retain combinator and nesting" width="50%">| | ||
|:--:| | ||
|Control flow graph showing the `retain` combinator and the implied dataflow around `validate`.| | ||
### Repeat | ||
The app will now produce the decoded text as its final output. | ||
`composer.repeat(count, body)` invokes _body_ _count_ times. | ||
```bash | ||
# create app | ||
$ fsh app create try @demos/retain.js | ||
### Try | ||
# invoke app with valid parameter | ||
> fsh app invoke retain -p str aGVsbG8gdHJ5IQ== | ||
{ | ||
text: "hello try!" | ||
} | ||
`composer.try(body, handler)` runs _body_ with error handler _handler_. | ||
# and now for the failing case | ||
> fsh app invoke retain -p str bogus | ||
{ | ||
ok: false | ||
} | ||
``` | ||
If _body_ outputs an error object, _handler_ is invoked with this error object as its input parameter object. Otherwise, _handler_ is not run. | ||
## Variables and scoping | ||
### Finally | ||
The composer allows you to introduce variables within a composition, | ||
and to limit their scope. This is useful when you have to introduce | ||
service keys and credentials for example. A scoped variable is defined | ||
using `let`. The example below illustrates how you might introduce a | ||
"secret" for a specific task without its value escaping to other | ||
compositions or functions. | ||
`composer.finally(body, finalizer)` runs _body_ and then _finalizer_. | ||
```javascript | ||
composer.sequence( | ||
composer.let({secret: 42}, | ||
composer.task(_ => ({ ok: secret === 42 }))), | ||
composer.task(_ => ({ ok: (typeof secret === 'undefined') }))) | ||
``` | ||
The _finalizer_ is invoked in sequence after _body_ even if _body_ returns an error object. | ||
The composition will execute successfully only if `secret` is not | ||
leaked to the final task in the composition, while the value is | ||
available inside the task nested within the `let`. | ||
### Retry | ||
```bash | ||
$ fsh app create let @demos/let.js | ||
$ fsh app invoke let | ||
{ | ||
ok: true | ||
} | ||
``` | ||
`composer.retry(count, body)` runs _body_ and retries _body_ up to _count_ times if it fails. The output parameter object for the composition is either the output parameter object of the successful _body_ invocation or the error object produced by the last _body_ invocation. | ||
## Other combinators | ||
### Retain | ||
The examples shown here illustrate the more common combinators you | ||
may use to create serverless compositions. There are more combinators | ||
available in the Composer library. Refer to the [Composer reference | ||
manual](COMPOSER.md) for more details. | ||
`composer.retain(body, [options])` runs _body_ on the input parameter object producing an object with two fields `params` and `result` such that `params` is the input parameter object of the composition and `result` is the output parameter object of _body_. | ||
An `options` dictionary object may be specified to alter the default behavior of `composer.retain` in the following ways: | ||
- If `options.catch` is thruthy, the `retain` combinator behavior will be the same even if _body_ returns an error object. Otherwise, if _body_ fails, the output of the `retain` combinator is only the error object (i.e., the input parameter object is not preserved). | ||
- If `options.filter` is a function, the combinator only persists the result of the function application to the input parameter object. | ||
- If `options.field` is a string, the combinator only persists the value of the field of the input parameter object with the given name. |
{ | ||
"name": "@ibm-functions/composer", | ||
"version": "0.1.1", | ||
"version": "0.2.0", | ||
"description": "Composer is an IBM Cloud Functions programming model for composing individual functions into larger applications.", | ||
@@ -10,2 +10,12 @@ "homepage": "https://github.com/ibm-functions/composer", | ||
}, | ||
"bin": { | ||
"compose": "./bin/compose" | ||
}, | ||
"files": [ | ||
"bin/", | ||
"composer.js", | ||
"docs/*.md", | ||
"samples/", | ||
"test/" | ||
], | ||
"repository": { | ||
@@ -24,8 +34,8 @@ "type": "git", | ||
"dependencies": { | ||
"redis": "^2.8.0", | ||
"clone": "^2.1.1" | ||
"minimist": "^1.2.0", | ||
"openwhisk": "^3.11.0", | ||
"uglify-es": "^3.3.9" | ||
}, | ||
"devDependencies": { | ||
"mocha": "^3.5.0", | ||
"openwhisk": "git://github.com/starpit/openwhisk-client-js.git#add_client_timeout" | ||
"mocha": "^3.5.0" | ||
}, | ||
@@ -51,2 +61,2 @@ "author": { | ||
"license": "Apache-2.0" | ||
} | ||
} |
173
README.md
@@ -1,45 +0,142 @@ | ||
Composer is a new programming model from [IBM | ||
Research](https://ibm.biz/serverless-research) for composing [IBM | ||
Cloud Functions](https://ibm.biz/openwhisk), built on [Apache | ||
OpenWhisk](https://github.com/apache/incubator-openwhisk). Composer | ||
extends Functions and sequences with more powerful control flow and | ||
automatic state management. With it, developers can build even more | ||
serverless applications including using it for IoT, with workflow | ||
orchestration, conversation services, and devops automation, to name a | ||
few examples. | ||
# @ibm-functions/composer | ||
Composer helps you express cloud-native apps that are serverless by | ||
construction: scale automatically, and pay as you go and not for idle | ||
time. Programming compositions for IBM Cloud Functions is done via the | ||
[functions shell](https://github.com/ibm-functions/shell), which | ||
offers a CLI and graphical interface for fast, incremental, iterative, | ||
and local development of serverless apps. Some additional highlights | ||
of the shell include: | ||
[![Travis](https://travis-ci.org/ibm-functions/composer.svg?branch=master)](https://travis-ci.org/ibm-functions/composer) | ||
[![License](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0) | ||
[![Join | ||
Slack](https://img.shields.io/badge/join-slack-9B69A0.svg)](http://slack.openwhisk.org/) | ||
* Edit your code and program using your favorite text editor, rather than using a drag-n-drop UI | ||
* Validate your compositions with readily accessible visualizations, without switching tools or using a browser | ||
* Deploy and invoke compositions using familiar CLI commands | ||
* Debug your invocations with either familiar CLI commands or readily accessible visualizations | ||
Composer is a new programming model from [IBM | ||
Research](https://ibm.biz/serverless-research) for composing [IBM Cloud | ||
Functions](https://ibm.biz/openwhisk), built on [Apache | ||
OpenWhisk](https://github.com/apache/incubator-openwhisk). With Composer, | ||
developers can build even more serverless applications including using it for | ||
IoT, with workflow orchestration, conversation services, and devops automation, | ||
to name a few examples. | ||
Composer and shell are currently available as IBM Research | ||
previews. We are excited about both and are looking forward to what | ||
compositions you build and run using [IBM Cloud | ||
Functions](https://ibm.biz/openwhisk) or directly on [Apache | ||
OpenWhisk](https://github.com/apache/incubator-openwhisk). | ||
Programming compositions for IBM Cloud Functions is supported by a new developer | ||
tool called [IBM Cloud Shell](https://github.com/ibm-functions/shell), or just | ||
_Shell_. Shell offers a CLI and graphical interface for fast, incremental, | ||
iterative, and local development of serverless applications. While we recommend | ||
using Shell, Shell is not required to work with compositions. Compositions may | ||
be managed using a combination of the Composer [compose](bin/compose) shell | ||
script (for deployment) and the [OpenWhisk | ||
CLI](https://console.bluemix.net/openwhisk/learn/cli) (for configuration, | ||
invocation, and life-cycle management). | ||
We welcome your feedback and criticism. Find bugs and we will squash | ||
them. And will be grateful for your help. As an early adopter, you | ||
will also be among the first to experience even more features planned | ||
for the weeks ahead. We look forward to your feedback and encourage | ||
you to [join us on slack](http://ibm.biz/composer-users). | ||
**In contrast to earlier releases of Composer, a REDIS server is not required to | ||
run compositions**. Composer now synthesizes OpenWhisk [conductor | ||
actions](https://github.com/apache/incubator-openwhisk/blob/master/docs/conductors.md) | ||
to implement compositions. Compositions have all the attributes and capabilities | ||
of an action (e.g., default parameters, limits, blocking invocation, web | ||
export). | ||
This repository includes: | ||
* the [composer](composer.js) Node.js module for authoring compositions using | ||
JavaScript, | ||
* the [compose](bin/compose) shell script for deploying compositions, | ||
* [documentation](docs), [examples](samples), and [tests](test). | ||
* [tutorial](docs) for getting started with Composer in the [docs](docs) folder, | ||
* [composer](composer.js) node.js module to author compositions using JavaScript, | ||
* [conductor](conductor.js) action code to orchestrate the execution of compositions, | ||
* [manager](manager.js) node.js module to query the state of compositions, | ||
* [test-harness](test-harness.js) helper module for testing composer, | ||
* [redis-promise](redis-promise.js) helper module that implements a promisified redis client for node.js, | ||
* example compositions in the [samples](samples) folder, | ||
* unit tests in the [test](test) folder. | ||
Composer and Shell are currently available as _IBM Research previews_. As | ||
Composer and Shell continue to evolve, it may be necessary to redeploy existing | ||
compositions to take advantage of new capabilities. However existing | ||
compositions should continue to run fine without redeployment. | ||
## Installation | ||
To install the `composer` module use the Node Package Manager: | ||
``` | ||
npm -g install @ibm-functions/composer | ||
``` | ||
We recommend to install the module globally (with `-g` option) so the `compose` | ||
command is added to the path. Otherwise, it can be found in the `bin` folder of | ||
the module installation. | ||
## Example | ||
A composition is typically defined by means of a Javascript file as illustrated | ||
in [samples/demo.js](samples/demo.js): | ||
```javascript | ||
composer.if( | ||
composer.action('authenticate', { action: function main({ password }) { return { value: password === 'abc123' } } }), | ||
composer.action('success', { action: function main() { return { message: 'success' } } }), | ||
composer.action('failure', { action: function main() { return { message: 'failure' } } })) | ||
``` | ||
Composer offers traditional control-flow concepts as methods. These methods | ||
are called _combinators_. This example composition composes three actions named | ||
`authenticate`, `success`, and `failure` using the `composer.if` combinator, | ||
which implements the usual conditional construct. It take three actions (or | ||
compositions) as parameters. It invokes the first one and, depending on the | ||
result of this invocation, invokes either the second or third action. | ||
This composition includes the definitions of the three composed actions. If the | ||
actions are defined and deployed elsewhere, the composition code can be shorten | ||
to: | ||
```javascript | ||
composer.if('authenticate', 'success', 'failure') | ||
``` | ||
To deploy this composition use the `compose` command: | ||
``` | ||
compose demo.js --deploy demo | ||
``` | ||
The `compose` command synthesizes and deploy an action named `demo` that | ||
implements the composition. It also deploys the composed actions if definitions | ||
are provided for them. | ||
The `demo` composition may be invoked like any action, for instance using the | ||
OpenWhisk CLI: | ||
``` | ||
wsk action invoke demo -r -p password passw0rd | ||
``` | ||
```json | ||
{ | ||
"message": "failure" | ||
} | ||
``` | ||
## Getting started | ||
* [Introduction to Serverless Composition](docs/tutorials/introduction/README.md): | ||
Setting up your programming environment and getting started with Shell and | ||
Composer. | ||
* [Building a Translation Slack Bot with Serverless | ||
Composition](docs/tutorials/translateBot/README.md): A more advanced tutorial | ||
using Composition to build a serverless Slack chatbot that does language | ||
translation. | ||
* [Composer Reference](docs/README.md): A comprehensive reference manual for | ||
the Node.js programmer. | ||
## Videos | ||
* The [IBM Cloud Shell YouTube | ||
channel](https://www.youtube.com/channel/UCcu16nIMNclSujJWDOgUI_g) hosts demo | ||
videos of IBM Cloud Shell, including editing a composition [using a built-in | ||
editor](https://youtu.be/1wmkSYl7EDM) or [an external | ||
editor](https://youtu.be/psqoysnVgE4), and [visualizing a composition's | ||
execution](https://youtu.be/jTaHgDQDZnQ). | ||
* Watch [our presentation at | ||
Serverlessconf'17](https://acloud.guru/series/serverlessconf/view/ibm-cloud-functions) | ||
about Composer and Shell. | ||
* [Conductor Actions and Composer | ||
v2](https://urldefense.proofpoint.com/v2/url?u=https-3A__youtu.be_qkqenC5b1kE&d=DwIGaQ&c=jf_iaSHvJObTbx-siA1ZOg&r=C3zA0dhyHjF4WaOy8EW8kQHtYUl9-dKPdS8OrjFeQmE&m=vCx7thSf3YtT7x3Pe2DaLYw-dcjU1hNIfDkTM_21ObA&s=MGh9y3vSvssj1xTzwEurJ6TewdE7Dr2Ycs10Tix8sNg&e=) | ||
(29:30 minutes into the video): A discussion of the composition runtime. | ||
## Blog posts | ||
* [Serverless Composition with IBM Cloud | ||
Functions](https://www.raymondcamden.com/2017/10/09/serverless-composition-with-ibm-cloud-functions/) | ||
* [Building Your First Serverless Composition with IBM Cloud | ||
Functions](https://www.raymondcamden.com/2017/10/18/building-your-first-serverless-composition-with-ibm-cloud-functions/) | ||
* [Upgrading Serverless Superman to IBM | ||
Composer](https://www.raymondcamden.com/2017/10/20/upgrading-serverless-superman-to-ibm-composer/) | ||
* [Calling Multiple Serverless Actions and Retaining Values with IBM | ||
Composer](https://www.raymondcamden.com/2017/10/25/calling-multiple-serverless-actions-and-retaining-values-with-ibm-composer/) | ||
* [Serverless Try/Catch/Finally with IBM | ||
Composer](https://www.raymondcamden.com/2017/11/22/serverless-trycatchfinally-with-ibm-composer/) | ||
* [Composing functions into | ||
applications](https://medium.com/openwhisk/composing-functions-into-applications-70d3200d0fac) | ||
* [A composition story: using IBM Cloud Functions to relay SMS to | ||
email](https://medium.com/openwhisk/a-composition-story-using-ibm-cloud-functions-to-relay-sms-to-email-d67fc65d29c) | ||
## Contributions | ||
We are looking forward to your feedback and criticism. We encourage you to [join | ||
us on slack](http://ibm.biz/composer-users). File bugs and we will squash them. | ||
We welcome contributions to Composer and Shell. See | ||
[CONTRIBUTING.md](CONTRIBUTING.md). |
@@ -17,10 +17,5 @@ /* | ||
'use strict' | ||
const composer = require('@openwhisk/composer') | ||
// author action composition | ||
const app = composer.if('authenticate', /* then */ 'welcome', /* else */ 'login') | ||
// compile action composition | ||
composer.compile(app, 'demo.json') | ||
composer.if( | ||
composer.action('authenticate', { action: function main({ password }) { return { value: password === 'abc123' } } }), | ||
composer.action('success', { action: function main() { return { message: 'success' } } }), | ||
composer.action('failure', { action: function main() { return { message: 'failure' } } })) |
{ | ||
"Entry": "push_0", | ||
"States": { | ||
"push_0": { | ||
"Type": "Push", | ||
"Next": "action_1" | ||
"actions": [ | ||
{ | ||
"name": "/_/authenticate", | ||
"action": { | ||
"exec": { | ||
"kind": "nodejs:default", | ||
"code": "function main({ password }) { return { value: password === 'abc123' } }" | ||
} | ||
} | ||
}, | ||
"action_1": { | ||
"Type": "Task", | ||
"Action": "authenticate", | ||
"Next": "choice_0" | ||
{ | ||
"name": "/_/success", | ||
"action": { | ||
"exec": { | ||
"kind": "nodejs:default", | ||
"code": "function main() { return { message: 'success' } }" | ||
} | ||
} | ||
}, | ||
"choice_0": { | ||
"Type": "Choice", | ||
"Then": "action_2", | ||
"Else": "action_3" | ||
}, | ||
"action_2": { | ||
"Type": "Task", | ||
"Action": "welcome", | ||
"Next": "pass_0" | ||
}, | ||
"action_3": { | ||
"Type": "Task", | ||
"Action": "login", | ||
"Next": "pass_0" | ||
}, | ||
"pass_0": { | ||
"Type": "Pass" | ||
{ | ||
"name": "/_/failure", | ||
"action": { | ||
"exec": { | ||
"kind": "nodejs:default", | ||
"code": "function main() { return { message: 'failure' } }" | ||
} | ||
} | ||
} | ||
}, | ||
"Exit": "pass_0" | ||
} | ||
], | ||
"composition": [ | ||
{ | ||
"type": "if", | ||
"test": [ | ||
{ | ||
"type": "action", | ||
"name": "/_/authenticate" | ||
} | ||
], | ||
"consequent": [ | ||
{ | ||
"type": "action", | ||
"name": "/_/success" | ||
} | ||
], | ||
"alternate": [ | ||
{ | ||
"type": "action", | ||
"name": "/_/failure" | ||
} | ||
] | ||
} | ||
] | ||
} |
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
Uses eval
Supply chain riskPackage uses dynamic code execution (e.g., eval()), which is a dangerous practice. This can prevent the code from running in certain environments and increases the risk that the code may contain exploits or malicious behavior.
Found 1 instance in 1 package
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 4 instances in 1 package
1
1078
143
9
1
102293
3
10
+ Addedminimist@^1.2.0
+ Addedopenwhisk@^3.11.0
+ Addeduglify-es@^3.3.9
+ Addedasync-retry@1.3.3(transitive)
+ Addedcommander@2.14.1(transitive)
+ Addediconv-lite@0.6.3(transitive)
+ Addedminimist@1.2.8(transitive)
+ Addedneedle@3.3.1(transitive)
+ Addedopenwhisk@3.21.8(transitive)
+ Addedretry@0.13.1(transitive)
+ Addedsafer-buffer@2.1.2(transitive)
+ Addedsax@1.4.1(transitive)
+ Addedsource-map@0.6.1(transitive)
+ Addeduglify-es@3.3.10(transitive)
- Removedclone@^2.1.1
- Removedredis@^2.8.0
- Removedclone@2.1.2(transitive)
- Removeddouble-ended-queue@2.1.0-0(transitive)
- Removedredis@2.8.0(transitive)
- Removedredis-commands@1.7.0(transitive)
- Removedredis-parser@2.6.0(transitive)