@ibm-functions/composer
Advanced tools
Comparing version 0.7.0 to 0.8.0
956
composer.js
/* | ||
* Copyright 2017-2018 IBM Corporation | ||
* Licensed to the Apache Software Foundation (ASF) under one or more | ||
* contributor license agreements. See the NOTICE file distributed with | ||
* this work for additional information regarding copyright ownership. | ||
* The ASF licenses this file to You under the Apache License, Version 2.0 | ||
* (the "License"); you may not use this file except in compliance with | ||
* the License. You may obtain a copy of the License at | ||
* | ||
* Licensed under the Apache License, Version 2.0 (the "License"); | ||
* you may not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* http://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* http://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, software | ||
@@ -19,9 +20,6 @@ * distributed under the License is distributed on an "AS IS" BASIS, | ||
const fqn = require('openwhisk-fqn') | ||
const fs = require('fs') | ||
const os = require('os') | ||
const path = require('path') | ||
const semver = require('semver') | ||
const util = require('util') | ||
// read composer version number | ||
const version = require('./package.json').version | ||
@@ -31,695 +29,345 @@ | ||
// combinator signatures | ||
const combinators = {} | ||
// error class | ||
class ComposerError extends Error { | ||
constructor(message, argument) { | ||
super(message + (argument !== undefined ? '\nArgument: ' + util.inspect(argument) : '')) | ||
} | ||
constructor (message, argument) { | ||
super(message + (argument !== undefined ? '\nArgument value: ' + util.inspect(argument) : '')) | ||
} | ||
} | ||
// registered plugins | ||
const plugins = [] | ||
const composer = { util: { declare, version } } | ||
const composer = {} | ||
Object.assign(composer, { | ||
// detect task type and create corresponding composition object | ||
task(task) { | ||
if (arguments.length > 1) throw new ComposerError('Too many arguments') | ||
if (task === null) return composer.empty() | ||
if (task instanceof Composition) return task | ||
if (typeof task === 'function') return composer.function(task) | ||
if (typeof task === 'string') return composer.action(task) | ||
throw new ComposerError('Invalid argument', task) | ||
}, | ||
// function combinator: stringify function code | ||
function(fun) { | ||
if (arguments.length > 1) 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 (!isObject(fun)) throw new ComposerError('Invalid argument', fun) | ||
return new Composition({ type: 'function', function: { exec: fun } }) | ||
}, | ||
// action combinator | ||
action(name, options = {}) { | ||
if (arguments.length > 2) throw new ComposerError('Too many arguments') | ||
if (!isObject(options)) throw new ComposerError('Invalid argument', options) | ||
name = composer.util.canonical(name) // throws ComposerError if name is not valid | ||
let exec | ||
if (Array.isArray(options.sequence)) { // native sequence | ||
exec = { kind: 'sequence', components: options.sequence.map(canonical) } | ||
} else if (typeof options.filename === 'string') { // read action code from file | ||
exec = fs.readFileSync(options.filename, { encoding: 'utf8' }) | ||
} else if (typeof options.action === 'function') { // capture function | ||
exec = `const main = ${options.action}` | ||
if (exec.indexOf('[native code]') !== -1) throw new ComposerError('Cannot capture native function', options.action) | ||
} else if (typeof options.action === 'string' || isObject(options.action)) { | ||
exec = options.action | ||
} | ||
if (typeof exec === 'string') { | ||
exec = { kind: 'nodejs:default', code: exec } | ||
} | ||
const composition = { type: 'action', name } | ||
if (exec) composition.action = { exec } | ||
return new Composition(composition) | ||
}, | ||
}) | ||
const lowerer = { | ||
empty() { | ||
return composer.sequence() | ||
}, | ||
literal (value) { | ||
return composer.let({ value }, () => value) | ||
}, | ||
seq({ components }) { | ||
return composer.sequence(...components) | ||
}, | ||
retain (...components) { | ||
let params = null | ||
return composer.let( | ||
{ params }, | ||
composer.finally( | ||
args => { params = args }, | ||
composer.seq(composer.mask(...components), | ||
result => ({ params, result })))) | ||
}, | ||
value({ value }) { | ||
return composer.literal(value) | ||
}, | ||
retain_catch (...components) { | ||
return composer.seq( | ||
composer.retain( | ||
composer.finally( | ||
composer.seq(...components), | ||
result => ({ result }))), | ||
({ params, result }) => ({ params, result: result.result })) | ||
}, | ||
literal({ value }) { | ||
return composer.let({ value }, composer.function('() => value')) | ||
}, | ||
if (test, consequent, alternate) { | ||
let params = null | ||
return composer.let( | ||
{ params }, | ||
composer.finally( | ||
args => { params = args }, | ||
composer.if_nosave( | ||
composer.mask(test), | ||
composer.finally(() => params, composer.mask(consequent)), | ||
composer.finally(() => params, composer.mask(alternate))))) | ||
}, | ||
retain({ components }) { | ||
return composer.let( | ||
{ params: null }, | ||
composer.finally( | ||
args => { params = args }, | ||
composer.seq(composer.mask(...components), | ||
result => ({ params, result })))) | ||
}, | ||
while (test, body) { | ||
let params = null | ||
return composer.let( | ||
{ params }, | ||
composer.finally( | ||
args => { params = args }, | ||
composer.seq(composer.while_nosave( | ||
composer.mask(test), | ||
composer.finally(() => params, composer.seq(composer.mask(body), args => { params = args }))), | ||
() => params))) | ||
}, | ||
retain_catch({ components }) { | ||
return composer.seq( | ||
composer.retain( | ||
composer.finally( | ||
composer.seq(...components), | ||
result => ({ result }))), | ||
({ params, result }) => ({ params, result: result.result })) | ||
}, | ||
dowhile (body, test) { | ||
let params = null | ||
return composer.let( | ||
{ params }, | ||
composer.finally( | ||
args => { params = args }, | ||
composer.seq(composer.dowhile_nosave( | ||
composer.finally(() => params, composer.seq(composer.mask(body), args => { params = args })), | ||
composer.mask(test)), | ||
() => params))) | ||
}, | ||
if({ test, consequent, alternate }) { | ||
return composer.let( | ||
{ params: null }, | ||
composer.finally( | ||
args => { params = args }, | ||
composer.if_nosave( | ||
composer.mask(test), | ||
composer.finally(() => params, composer.mask(consequent)), | ||
composer.finally(() => params, composer.mask(alternate))))) | ||
}, | ||
repeat (count, ...components) { | ||
return composer.let( | ||
{ count }, | ||
composer.while( | ||
() => count-- > 0, | ||
composer.mask(...components))) | ||
}, | ||
while({ test, body }) { | ||
return composer.let( | ||
{ params: null }, | ||
composer.finally( | ||
args => { params = args }, | ||
composer.seq(composer.while_nosave( | ||
composer.mask(test), | ||
composer.finally(() => params, composer.seq(composer.mask(body), args => { params = args }))), | ||
() => params))) | ||
}, | ||
retry (count, ...components) { | ||
return composer.let( | ||
{ count }, | ||
params => ({ params }), | ||
composer.dowhile( | ||
composer.finally(({ params }) => params, composer.mask(composer.retain_catch(...components))), | ||
({ result }) => result.error !== undefined && count-- > 0), | ||
({ result }) => result) | ||
}, | ||
dowhile({ body, test }) { | ||
return composer.let( | ||
{ params: null }, | ||
composer.finally( | ||
args => { params = args }, | ||
composer.seq(composer.dowhile_nosave( | ||
composer.finally(() => params, composer.seq(composer.mask(body), args => { params = args })), | ||
composer.mask(test)), | ||
() => params))) | ||
}, | ||
repeat({ count, components }) { | ||
return composer.let( | ||
{ count }, | ||
composer.while( | ||
composer.function('() => count-- > 0'), | ||
composer.mask(...components))) | ||
}, | ||
retry({ count, components }) { | ||
return composer.let( | ||
{ count }, | ||
params => ({ params }), | ||
composer.dowhile( | ||
composer.finally(({ params }) => params, composer.mask(composer.retain_catch(...components))), | ||
composer.function('({ result }) => result.error !== undefined && count-- > 0')), | ||
({ result }) => result) | ||
}, | ||
merge (...components) { | ||
return composer.seq(composer.retain(...components), ({ params, result }) => Object.assign(params, result)) | ||
} | ||
} | ||
// recursively compile composition composition into { composition, actions } | ||
function flatten(composition) { | ||
if (arguments.length > 1) throw new ComposerError('Too many arguments') | ||
if (!(composition instanceof Composition)) throw new ComposerError('Invalid argument', composition) | ||
const actions = [] | ||
const flatten = composition => { | ||
composition = new Composition(composition) // copy | ||
composition.visit(flatten) | ||
if (composition.type === 'action' && composition.action) { | ||
actions.push({ name: composition.name, action: composition.action }) | ||
delete composition.action | ||
} | ||
return composition | ||
// apply f to all fields of type composition | ||
function visit (composition, f) { | ||
composition = Object.assign({}, composition) // copy | ||
const combinator = composition['.combinator']() | ||
if (combinator.components) { | ||
composition.components = composition.components.map(f) | ||
} | ||
for (let arg of combinator.args || []) { | ||
if (arg.type === undefined && composition[arg.name] !== undefined) { | ||
composition[arg.name] = f(composition[arg.name], arg.name) | ||
} | ||
} | ||
return new Composition(composition) | ||
} | ||
composition = flatten(composition) | ||
return { composition, actions } | ||
// recursively label combinators with the json path | ||
function label (composition) { | ||
const label = path => (composition, name, array) => { | ||
const p = path + (name !== undefined ? (array === undefined ? `.${name}` : `[${name}]`) : '') | ||
composition = visit(composition, label(p)) // copy | ||
composition.path = p | ||
return composition | ||
} | ||
return label('')(composition) | ||
} | ||
// synthesize conductor action code from composition | ||
function synthesize(composition) { | ||
if (arguments.length > 1) throw new ComposerError('Too many arguments') | ||
if (!(composition instanceof Composition)) throw new ComposerError('Invalid argument', composition) | ||
let code = `const main=(${main})(` | ||
for (let plugin of plugins) { | ||
code += `{plugin:new(${plugin.constructor})()` | ||
if (plugin.configure) code += `,config:${JSON.stringify(plugin.configure())}` | ||
code += '},' | ||
// derive combinator methods from combinator table | ||
// check argument count and map argument positions to argument names | ||
// delegate to Composition constructor for the rest of the validation | ||
function declare (combinators, prefix) { | ||
if (arguments.length > 2) throw new ComposerError('Too many arguments in "declare"') | ||
if (!isObject(combinators)) throw new ComposerError('Invalid argument "combinators" in "declare"', combinators) | ||
if (prefix !== undefined && typeof prefix !== 'string') throw new ComposerError('Invalid argument "prefix" in "declare"', prefix) | ||
const composer = {} | ||
for (let key in combinators) { | ||
const type = prefix ? prefix + '.' + key : key | ||
const combinator = combinators[key] | ||
if (!isObject(combinator) || (combinator.args !== undefined && !Array.isArray(combinator.args))) { | ||
throw new ComposerError(`Invalid "${type}" combinator specification in "declare"`, combinator) | ||
} | ||
code = require('uglify-es').minify(`${code})`, { output: { max_line_len: 127 } }).code | ||
code = `// generated by composer v${composer.util.version}\n\nconst composition = ${JSON.stringify(composer.util.lower(label(composition)), null, 4)}\n\n// do not edit below this point\n\n${code}` // invoke conductor on composition | ||
return { exec: { kind: 'nodejs:default', code }, annotations: [{ key: 'conductor', value: composition }, { key: 'composer', value: version }] } | ||
for (let arg of combinator.args || []) { | ||
if (typeof arg.name !== 'string') throw new ComposerError(`Invalid "${type}" combinator specification in "declare"`, combinator) | ||
} | ||
composer[key] = function () { | ||
const composition = { type, '.combinator': () => combinator } | ||
const skip = (combinator.args && combinator.args.length) || 0 | ||
if (!combinator.components && (arguments.length > skip)) { | ||
throw new ComposerError(`Too many arguments in "${type}" combinator`) | ||
} | ||
for (let i = 0; i < skip; ++i) { | ||
composition[combinator.args[i].name] = arguments[i] | ||
} | ||
if (combinator.components) { | ||
composition.components = Array.prototype.slice.call(arguments, skip) | ||
} | ||
return new Composition(composition) | ||
} | ||
} | ||
return composer | ||
} | ||
composer.util = { | ||
// return the signatures of the combinators | ||
get combinators() { | ||
return combinators | ||
}, | ||
// recursively deserialize composition | ||
deserialize(composition) { | ||
if (arguments.length > 1) throw new ComposerError('Too many arguments') | ||
composition = new Composition(composition) // copy | ||
composition.visit(composition => composer.util.deserialize(composition)) | ||
return composition | ||
}, | ||
// recursively lower combinators to the desired set of combinators (including primitive combinators) | ||
lower(composition, combinators = []) { | ||
if (arguments.length > 2) throw new ComposerError('Too many arguments') | ||
if (!(composition instanceof Composition)) throw new ComposerError('Invalid argument', composition) | ||
if (typeof combinators === 'string') { // lower to combinators of specific composer version | ||
combinators = Object.keys(composer.util.combinators).filter(key => semver.gte(combinators, composer.util.combinators[key].since)) | ||
} | ||
if (!Array.isArray(combinators)) throw new ComposerError('Invalid argument', combinators) | ||
const lower = composition => { | ||
composition = new Composition(composition) // copy | ||
// repeatedly lower root combinator | ||
while (combinators.indexOf(composition.type) < 0 && lowerer[composition.type]) { | ||
const path = composition.path | ||
composition = lowerer[composition.type](composition) | ||
if (path !== undefined) composition.path = path // preserve path | ||
} | ||
// lower nested combinators | ||
composition.visit(lower) | ||
return composition | ||
} | ||
return lower(composition) | ||
}, | ||
// register plugin | ||
register(plugin) { | ||
plugins.push(plugin) | ||
}, | ||
/** | ||
* 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 | ||
*/ | ||
canonical(name) { | ||
if (typeof name !== 'string') throw new ComposerError('Name must be a string') | ||
if (name.trim().length == 0) throw new ComposerError('Name is not valid') | ||
name = name.trim() | ||
const delimiter = '/' | ||
const parts = name.split(delimiter) | ||
const n = parts.length | ||
const 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') }) | ||
const newName = parts.join(delimiter) | ||
if (leadingSlash) return newName | ||
else if (n < 3) return `${delimiter}_${delimiter}${newName}` | ||
else return `${delimiter}${newName}` | ||
}, | ||
// encode composition as an action table | ||
encode(name, composition, combinators) { | ||
if (arguments.length > 3) throw new ComposerError('Too many arguments') | ||
name = composer.util.canonical(name) // throws ComposerError if name is not valid | ||
if (!(composition instanceof Composition)) throw new ComposerError('Invalid argument', composition) | ||
if (combinators) composition = composer.util.lower(composition, combinators) | ||
const table = flatten(composition) | ||
table.actions.push({ name, action: synthesize(table.composition) }) | ||
return table.actions | ||
}, | ||
// return composer version | ||
get version() { | ||
return version | ||
}, | ||
// return enhanced openwhisk client capable of deploying compositions | ||
openwhisk(options) { | ||
// try to extract apihost and key first from whisk property file file and then from process.env | ||
let apihost | ||
let api_key | ||
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 | ||
}, | ||
// derive combinator methods from combinator table | ||
declare(combinators) { | ||
Object.assign(composer.util.combinators, combinators) | ||
for (let type in combinators) { | ||
const combinator = combinators[type] | ||
// do not overwrite existing combinators | ||
composer[type] = composer[type] || function () { | ||
const composition = { type } | ||
const skip = combinator.args && combinator.args.length || 0 | ||
if (!combinator.components && (arguments.length > skip)) { | ||
throw new ComposerError('Too many arguments') | ||
} | ||
for (let i = 0; i < skip; ++i) { | ||
const arg = combinator.args[i] | ||
const argument = arguments[i] | ||
if (argument === undefined && arg.optional && arg.type !== undefined) continue | ||
switch (arg.type) { | ||
case undefined: | ||
composition[arg._] = composer.task(arg.optional ? argument || null : argument) | ||
continue | ||
case 'value': | ||
if (typeof argument === 'function') throw new ComposerError('Invalid argument', argument) | ||
composition[arg._] = argument === undefined ? {} : argument | ||
continue | ||
case 'object': | ||
if (argument === null || Array.isArray(argument)) throw new ComposerError('Invalid argument', argument) | ||
default: | ||
if (typeof argument !== arg.type) throw new ComposerError('Invalid argument', argument) | ||
composition[arg._] = argument | ||
} | ||
} | ||
if (combinator.components) { | ||
composition.components = Array.prototype.slice.call(arguments, skip).map(obj => composer.task(obj)) | ||
} | ||
return new Composition(composition) | ||
} | ||
} | ||
}, | ||
get lowerer() { | ||
return lowerer | ||
}, | ||
} | ||
composer.util.frontend = composer.util.openwhisk | ||
// composition class | ||
class Composition { | ||
// weaker instanceof to tolerate multiple instances of this class | ||
static [Symbol.hasInstance](instance) { | ||
return instance.constructor && instance.constructor.name === Composition.name | ||
} | ||
// weaker instanceof to tolerate multiple instances of this class | ||
static [Symbol.hasInstance] (instance) { | ||
return instance.constructor && instance.constructor.name === Composition.name | ||
} | ||
// construct a composition object with the specified fields | ||
constructor(composition) { | ||
if (!isObject(composition) || composer.util.combinators[composition.type] === undefined) throw new ComposerError('Invalid argument', composition) | ||
const combinator = composer.util.combinators[composition.type] | ||
if (combinator.components && composition.components === undefined) throw new ComposerError('Invalid argument', composition) | ||
for (let arg of combinator.args || []) { | ||
if (!arg.optional && composition[arg._] === undefined) throw new ComposerError('Invalid argument', composition) | ||
} | ||
return Object.assign(this, composition) | ||
// construct a composition object with the specified fields | ||
constructor (composition) { | ||
const combinator = composition['.combinator']() | ||
Object.assign(this, composition) | ||
for (let arg of combinator.args || []) { | ||
if (composition[arg.name] === undefined && arg.optional && arg.type !== undefined) continue | ||
switch (arg.type) { | ||
case undefined: | ||
try { | ||
this[arg.name] = composer.task(arg.optional ? composition[arg.name] || null : composition[arg.name]) | ||
} catch (error) { | ||
throw new ComposerError(`Invalid argument "${arg.name}" in "${composition.type} combinator"`, composition[arg.name]) | ||
} | ||
break | ||
case 'name': | ||
try { | ||
this[arg.name] = fqn(composition[arg.name]) | ||
} catch (error) { | ||
throw new ComposerError(`${error.message} in "${composition.type} combinator"`, composition[arg.name]) | ||
} | ||
break | ||
case 'value': | ||
if (typeof composition[arg.name] === 'function' || composition[arg.name] === undefined) { | ||
throw new ComposerError(`Invalid argument "${arg.name}" in "${composition.type} combinator"`, composition[arg.name]) | ||
} | ||
break | ||
case 'object': | ||
if (!isObject(composition[arg.name])) { | ||
throw new ComposerError(`Invalid argument "${arg.name}" in "${composition.type} combinator"`, composition[arg.name]) | ||
} | ||
break | ||
default: | ||
if ('' + typeof composition[arg.name] !== arg.type) { | ||
throw new ComposerError(`Invalid argument "${arg.name}" in "${composition.type} combinator"`, composition[arg.name]) | ||
} | ||
} | ||
} | ||
if (combinator.components) this.components = (composition.components || []).map(obj => composer.task(obj)) | ||
return this | ||
} | ||
// apply f to all fields of type composition | ||
visit(f) { | ||
const combinator = composer.util.combinators[this.type] | ||
if (combinator.components) { | ||
this.components = this.components.map(f) | ||
} | ||
for (let arg of combinator.args || []) { | ||
if (arg.type === undefined && this[arg._] !== undefined) { | ||
this[arg._] = f(this[arg._], arg._) | ||
} | ||
} | ||
// compile composition | ||
compile () { | ||
if (arguments.length > 0) throw new ComposerError('Too many arguments in "compile"') | ||
const actions = [] | ||
const flatten = composition => { | ||
composition = visit(composition, flatten) | ||
if (composition.type === 'action' && composition.action) { | ||
actions.push({ name: composition.name, action: composition.action }) | ||
delete composition.action | ||
} | ||
return composition | ||
} | ||
} | ||
Composition.composer = composer | ||
const obj = { composition: label(flatten(this)).lower(), ast: this, version } | ||
if (actions.length > 0) obj.actions = actions | ||
return obj | ||
} | ||
composer.util.declare({ | ||
empty: { since: '0.4.0' }, | ||
seq: { components: true, since: '0.4.0' }, | ||
sequence: { components: true, since: '0.4.0' }, | ||
if: { args: [{ _: 'test' }, { _: 'consequent' }, { _: 'alternate', optional: true }], since: '0.4.0' }, | ||
if_nosave: { args: [{ _: 'test' }, { _: 'consequent' }, { _: 'alternate', optional: true }], since: '0.4.0' }, | ||
while: { args: [{ _: 'test' }, { _: 'body' }], since: '0.4.0' }, | ||
while_nosave: { args: [{ _: 'test' }, { _: 'body' }], since: '0.4.0' }, | ||
dowhile: { args: [{ _: 'body' }, { _: 'test' }], since: '0.4.0' }, | ||
dowhile_nosave: { args: [{ _: 'body' }, { _: 'test' }], since: '0.4.0' }, | ||
try: { args: [{ _: 'body' }, { _: 'handler' }], since: '0.4.0' }, | ||
finally: { args: [{ _: 'body' }, { _: 'finalizer' }], since: '0.4.0' }, | ||
retain: { components: true, since: '0.4.0' }, | ||
retain_catch: { components: true, since: '0.4.0' }, | ||
let: { args: [{ _: 'declarations', type: 'object' }], components: true, since: '0.4.0' }, | ||
mask: { components: true, since: '0.4.0' }, | ||
action: { args: [{ _: 'name', type: 'string' }, { _: 'action', type: 'object', optional: true }], since: '0.4.0' }, | ||
repeat: { args: [{ _: 'count', type: 'number' }], components: true, since: '0.4.0' }, | ||
retry: { args: [{ _: 'count', type: 'number' }], components: true, since: '0.4.0' }, | ||
value: { args: [{ _: 'value', type: 'value' }], since: '0.4.0' }, | ||
literal: { args: [{ _: 'value', type: 'value' }], since: '0.4.0' }, | ||
function: { args: [{ _: 'function', type: 'object' }], since: '0.4.0' }, | ||
async: { args: [{ _: 'body' }], since: '0.6.0' }, | ||
}) | ||
// recursively lower combinators to the desired set of combinators (including primitive combinators) | ||
lower (combinators = []) { | ||
if (arguments.length > 1) throw new ComposerError('Too many arguments in "lower"') | ||
if (!Array.isArray(combinators)) throw new ComposerError('Invalid argument "combinators" in "lower"', combinators) | ||
// management class for compositions | ||
class Compositions { | ||
constructor(wsk) { | ||
this.actions = wsk.actions | ||
const lower = composition => { | ||
// repeatedly lower root combinator | ||
while (composition['.combinator']().def) { | ||
const path = composition.path | ||
const combinator = composition['.combinator']() | ||
if (Array.isArray(combinators) && combinators.indexOf(composition.type) >= 0) break | ||
// map argument names to positions | ||
const args = [] | ||
const skip = (combinator.args && combinator.args.length) || 0 | ||
for (let i = 0; i < skip; i++) args.push(composition[combinator.args[i].name]) | ||
if (combinator.components) args.push(...composition.components) | ||
composition = combinator.def(...args) | ||
if (path !== undefined) composition.path = path // preserve path | ||
} | ||
// lower nested combinators | ||
return visit(composition, lower) | ||
} | ||
deploy({ name, composition, combinators }) { | ||
const actions = composer.util.encode(name, composition, combinators) | ||
return actions.reduce((promise, action) => promise.then(() => this.actions.delete(action).catch(() => { })) | ||
.then(() => this.actions.update(action)), Promise.resolve()) | ||
.then(() => actions) | ||
} | ||
return lower(this) | ||
} | ||
} | ||
// recursively label combinators with the json path | ||
function label(composition) { | ||
if (arguments.length > 1) throw new ComposerError('Too many arguments') | ||
if (!(composition instanceof Composition)) throw new ComposerError('Invalid argument', composition) | ||
const label = path => (composition, name, array) => { | ||
composition = new Composition(composition) // copy | ||
composition.path = path + (name !== undefined ? (array === undefined ? `.${name}` : `[${name}]`) : '') | ||
// label nested combinators | ||
composition.visit(label(composition.path)) | ||
return composition | ||
} | ||
return label('')(composition) | ||
// primitive combinators | ||
const combinators = { | ||
sequence: { components: true }, | ||
if_nosave: { args: [{ name: 'test' }, { name: 'consequent' }, { name: 'alternate', optional: true }] }, | ||
while_nosave: { args: [{ name: 'test' }, { name: 'body' }] }, | ||
dowhile_nosave: { args: [{ name: 'body' }, { name: 'test' }] }, | ||
try: { args: [{ name: 'body' }, { name: 'handler' }] }, | ||
finally: { args: [{ name: 'body' }, { name: 'finalizer' }] }, | ||
let: { args: [{ name: 'declarations', type: 'object' }], components: true }, | ||
mask: { components: true }, | ||
action: { args: [{ name: 'name', type: 'name' }, { name: 'action', type: 'object', optional: true }] }, | ||
function: { args: [{ name: 'function', type: 'object' }] }, | ||
async: { components: true } | ||
} | ||
// runtime code | ||
function main() { | ||
const isObject = obj => typeof obj === 'object' && obj !== null && !Array.isArray(obj) | ||
Object.assign(composer, declare(combinators)) | ||
// compile ast to fsm | ||
const compiler = { | ||
sequence(node) { | ||
return [{ type: 'pass', path: node.path }, ...compile(...node.components)] | ||
}, | ||
// derived combinators | ||
const extra = { | ||
empty: { def: composer.sequence }, | ||
seq: { components: true, def: composer.sequence }, | ||
if: { args: [{ name: 'test' }, { name: 'consequent' }, { name: 'alternate', optional: true }], def: lowerer.if }, | ||
while: { args: [{ name: 'test' }, { name: 'body' }], def: lowerer.while }, | ||
dowhile: { args: [{ name: 'body' }, { name: 'test' }], def: lowerer.dowhile }, | ||
repeat: { args: [{ name: 'count', type: 'number' }], components: true, def: lowerer.repeat }, | ||
retry: { args: [{ name: 'count', type: 'number' }], components: true, def: lowerer.retry }, | ||
retain: { components: true, def: lowerer.retain }, | ||
retain_catch: { components: true, def: lowerer.retain_catch }, | ||
value: { args: [{ name: 'value', type: 'value' }], def: lowerer.literal }, | ||
literal: { args: [{ name: 'value', type: 'value' }], def: lowerer.literal }, | ||
merge: { components: true, def: lowerer.merge } | ||
} | ||
action(node) { | ||
return [{ type: 'action', name: node.name, path: node.path }] | ||
}, | ||
Object.assign(composer, declare(extra)) | ||
async(node) { | ||
const body = compile(node.body) | ||
return [{ type: 'async', path: node.path, return: body.length + 2 }, ...body, { type: 'stop' }, { type: 'pass' }] | ||
}, | ||
// add or override definitions of some combinators | ||
Object.assign(composer, { | ||
// detect task type and create corresponding composition object | ||
task (task) { | ||
if (arguments.length > 1) throw new ComposerError('Too many arguments in "task" combinator') | ||
if (task === undefined) throw new ComposerError('Invalid argument in "task" combinator', task) | ||
if (task === null) return composer.empty() | ||
if (task instanceof Composition) return task | ||
if (typeof task === 'function') return composer.function(task) | ||
if (typeof task === 'string') return composer.action(task) | ||
throw new ComposerError('Invalid argument "task" in "task" combinator', task) | ||
}, | ||
function(node) { | ||
return [{ type: 'function', exec: node.function.exec, path: node.path }] | ||
}, | ||
finally(node) { | ||
const finalizer = compile(node.finalizer) | ||
const fsm = [{ type: 'try', path: node.path }, ...compile(node.body), { type: 'exit' }, ...finalizer] | ||
fsm[0].catch = fsm.length - finalizer.length | ||
return fsm | ||
}, | ||
let(node) { | ||
return [{ type: 'let', let: node.declarations, path: node.path }, ...compile(...node.components), { type: 'exit' }] | ||
}, | ||
mask(node) { | ||
return [{ type: 'let', let: null, path: node.path }, ...compile(...node.components), { type: 'exit' }] | ||
}, | ||
try(node) { | ||
const handler = [...compile(node.handler), { type: 'pass' }] | ||
const fsm = [{ type: 'try', path: node.path }, ...compile(node.body), { type: 'exit' }, ...handler] | ||
fsm[0].catch = fsm.length - handler.length | ||
fsm[fsm.length - handler.length - 1].next = handler.length | ||
return fsm | ||
}, | ||
if_nosave(node) { | ||
const consequent = compile(node.consequent) | ||
const alternate = [...compile(node.alternate), { type: 'pass' }] | ||
const fsm = [{ type: 'pass', path: node.path }, ...compile(node.test), { type: 'choice', then: 1, else: consequent.length + 1 }, ...consequent, ...alternate] | ||
fsm[fsm.length - alternate.length - 1].next = alternate.length | ||
return fsm | ||
}, | ||
while_nosave(node) { | ||
const body = compile(node.body) | ||
const fsm = [{ type: 'pass', path: node.path }, ...compile(node.test), { type: 'choice', then: 1, else: body.length + 1 }, ...body, { type: 'pass' }] | ||
fsm[fsm.length - 2].next = 2 - fsm.length | ||
return fsm | ||
}, | ||
dowhile_nosave(node) { | ||
const fsm = [{ type: 'pass', path: node.path }, ...compile(node.body), ...compile(node.test), { type: 'choice', else: 1 }, { type: 'pass' }] | ||
fsm[fsm.length - 2].then = 2 - fsm.length | ||
return fsm | ||
}, | ||
// function combinator: stringify function code | ||
function (fun) { | ||
if (arguments.length > 1) throw new ComposerError('Too many arguments in "function" combinator') | ||
if (typeof fun === 'function') { | ||
fun = `${fun}` | ||
if (fun.indexOf('[native code]') !== -1) throw new ComposerError('Cannot capture native function in "function" combinator', fun) | ||
} | ||
function compile(node) { | ||
if (arguments.length === 0) return [{ type: 'empty' }] | ||
if (arguments.length === 1) return compiler[node.type](node) | ||
return Array.prototype.reduce.call(arguments, (fsm, node) => { fsm.push(...compile(node)); return fsm }, []) | ||
if (typeof fun === 'string') { | ||
fun = { kind: 'nodejs:default', code: fun } | ||
} | ||
if (!isObject(fun)) throw new ComposerError('Invalid argument "function" in "function" combinator', fun) | ||
return new Composition({ type: 'function', function: { exec: fun }, '.combinator': () => combinators.function }) | ||
}, | ||
const openwhisk = require('openwhisk') | ||
let wsk | ||
const conductor = { | ||
choice({ p, node, index }) { | ||
p.s.state = index + (p.params.value ? node.then : node.else) | ||
}, | ||
try({ p, node, index }) { | ||
p.s.stack.unshift({ catch: index + node.catch }) | ||
}, | ||
let({ p, node, index }) { | ||
p.s.stack.unshift({ let: JSON.parse(JSON.stringify(node.let)) }) | ||
}, | ||
exit({ p, node, index }) { | ||
if (p.s.stack.length === 0) return internalError(`State ${index} attempted to pop from an empty stack`) | ||
p.s.stack.shift() | ||
}, | ||
action({ p, node, index }) { | ||
return { action: node.name, params: p.params, state: { $resume: p.s } } | ||
}, | ||
function({ p, node, index }) { | ||
return Promise.resolve().then(() => run(node.exec.code, p)) | ||
.catch(error => { | ||
console.error(error) | ||
return { error: `An exception was caught at state ${index} (see log for details)` } | ||
}) | ||
.then(result => { | ||
if (typeof result === 'function') result = { error: `State ${index} evaluated to a function` } | ||
// if a function has only side effects and no return value, return params | ||
p.params = JSON.parse(JSON.stringify(result === undefined ? p.params : result)) | ||
inspect(p) | ||
return step(p) | ||
}) | ||
}, | ||
empty({ p, node, index }) { | ||
inspect(p) | ||
}, | ||
pass({ p, node, index }) { | ||
}, | ||
async({ p, node, index, inspect, step }) { | ||
if (!wsk) wsk = openwhisk({ ignore_certs: true }) | ||
p.params.$resume = { state: p.s.state } | ||
p.s.state = index + node.return | ||
return wsk.actions.invoke({ name: process.env.__OW_ACTION_NAME, params: p.params }) | ||
.catch(error => { | ||
console.error(error) | ||
return { error: `An exception was caught at state ${index} (see log for details)` } | ||
}) | ||
.then(result => { | ||
p.params = result | ||
inspect(p) | ||
return step(p) | ||
}) | ||
}, | ||
stop({ p, node, index, inspect, step }) { | ||
p.s.state = -1 | ||
}, | ||
// action combinator | ||
action (name, options = {}) { | ||
if (arguments.length > 2) throw new ComposerError('Too many arguments in "action" combinator') | ||
if (!isObject(options)) throw new ComposerError('Invalid argument "options" in "action" combinator', options) | ||
let exec | ||
if (Array.isArray(options.sequence)) { // native sequence | ||
exec = { kind: 'sequence', components: options.sequence.map(fqn) } | ||
} else if (typeof options.filename === 'string') { // read action code from file | ||
exec = fs.readFileSync(options.filename, { encoding: 'utf8' }) | ||
} else if (typeof options.action === 'function') { // capture function | ||
exec = `const main = ${options.action}` | ||
if (exec.indexOf('[native code]') !== -1) throw new ComposerError('Cannot capture native function in "action" combinator', options.action) | ||
} else if (typeof options.action === 'string' || isObject(options.action)) { | ||
exec = options.action | ||
} | ||
const finishers = [] | ||
for (let { plugin, config } of arguments) { | ||
if (plugin.compiler) Object.assign(compiler, plugin.compiler({ compile })) | ||
if (plugin.conductor) { | ||
Object.assign(conductor, plugin.conductor(config)) | ||
if (conductor._finish) { | ||
finishers.push(conductor._finish) | ||
delete conductor._finish | ||
} | ||
} | ||
if (typeof exec === 'string') { | ||
exec = { kind: 'nodejs:default', code: exec } | ||
} | ||
const composition = { type: 'action', name, '.combinator': () => combinators.action } | ||
if (exec) composition.action = { exec } | ||
return new Composition(composition) | ||
}, | ||
const fsm = compile(composition) | ||
// recursively deserialize composition | ||
parse (composition) { | ||
if (arguments.length > 1) throw new ComposerError('Too many arguments in "parse" combinator') | ||
if (!isObject(composition)) throw new ComposerError('Invalid argument "composition" in "parse" combinator', composition) | ||
const combinator = typeof composition['.combinator'] === 'function' ? composition['.combinator']() : combinators[composition.type] | ||
if (!isObject(combinator)) throw new ComposerError('Invalid composition type in "parse" combinator', composition) | ||
return visit(Object.assign({ '.combinator': () => combinator }, composition), composition => composer.parse(composition)) | ||
} | ||
}) | ||
// 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)) | ||
// wrap params if not a dictionary, branch to error handler if error | ||
function inspect(p) { | ||
if (!isObject(p.params)) p.params = { value: p.params } | ||
if (p.params.error !== undefined) { | ||
p.params = { error: p.params.error } // discard all fields but the error field | ||
p.s.state = -1 // abort unless there is a handler in the stack | ||
while (p.s.stack.length > 0) { | ||
if ((p.s.state = p.s.stack.shift().catch || -1) >= 0) break | ||
} | ||
} | ||
} | ||
// run function f on current stack | ||
function run(f, p) { | ||
// handle let/mask pairs | ||
const view = [] | ||
let n = 0 | ||
for (let frame of p.s.stack) { | ||
if (frame.let === null) { | ||
n++ | ||
} else if (frame.let !== undefined) { | ||
if (n === 0) { | ||
view.push(frame) | ||
} else { | ||
n-- | ||
} | ||
} | ||
} | ||
// update value of topmost matching symbol on stack if any | ||
function set(symbol, value) { | ||
const element = view.find(element => element.let !== undefined && element.let[symbol] !== undefined) | ||
if (element !== undefined) element.let[symbol] = JSON.parse(JSON.stringify(value)) | ||
} | ||
// collapse stack for invocation | ||
const env = view.reduceRight((acc, cur) => cur.let ? 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 (1, eval)(main)(p.params, env) | ||
} finally { | ||
for (const name in env) set(name, env[name]) | ||
} | ||
} | ||
function step(p) { | ||
// final state, return composition result | ||
if (p.s.state < 0 || p.s.state >= fsm.length) { | ||
console.log(`Entering final state`) | ||
console.log(JSON.stringify(p.params)) | ||
return finishers.reduce((promise, _finish) => promise.then(() => _finish(p)), Promise.resolve()) | ||
.then(() => p.params.error ? p.params : { params: p.params }) | ||
} | ||
// process one state | ||
const node = fsm[p.s.state] // json definition for index state | ||
if (node.path !== undefined) console.log(`Entering composition${node.path}`) | ||
const index = p.s.state // current state | ||
p.s.state = p.s.state + (node.next || 1) // default next state | ||
return conductor[node.type]({ p, index, node, inspect, step }) || step(p) | ||
} | ||
return params => Promise.resolve().then(() => invoke(params)).catch(internalError) | ||
// do invocation | ||
function invoke(params) { | ||
const p = { s: { state: 0, stack: [] }, params } // initial state | ||
if (params.$resume !== undefined) { | ||
if (!isObject(params.$resume)) return badRequest('The type of optional $resume parameter must be object') | ||
const resuming = params.$resume.stack | ||
Object.assign(p.s, params.$resume) | ||
if (typeof p.s.state !== 'number') return badRequest('The type of optional $resume.state parameter must be number') | ||
if (!Array.isArray(p.s.stack)) return badRequest('The type of optional $resume.stack parameter must be an array') | ||
delete params.$resume | ||
if (resuming) inspect(p) // handle error objects when resuming | ||
} | ||
return step(p) | ||
} | ||
} | ||
module.exports = composer |
@@ -0,1 +1,20 @@ | ||
<!-- | ||
# | ||
# Licensed to the Apache Software Foundation (ASF) under one or more | ||
# contributor license agreements. See the NOTICE file distributed with | ||
# this work for additional information regarding copyright ownership. | ||
# The ASF licenses this file to You under the Apache License, Version 2.0 | ||
# (the "License"); you may not use this file except in compliance with | ||
# the License. You may obtain a copy of the License at | ||
# | ||
# http://www.apache.org/licenses/LICENSE-2.0 | ||
# | ||
# Unless required by applicable law or agreed to in writing, software | ||
# distributed under the License is distributed on an "AS IS" BASIS, | ||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
# See the License for the specific language governing permissions and | ||
# limitations under the License. | ||
# | ||
--> | ||
# Combinators | ||
@@ -6,22 +25,25 @@ | ||
| 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!' })` | | ||
| --:| --- | --- | | ||
| [`action`](#action) | named action | `composer.action('echo')` | | ||
| [`async`](#async) | asynchronous invocation | `composer.async('compress', 'upload')` | | ||
| [`dowhile` and `dowhile_nosave`](#dowhile) | loop at least once | `composer.dowhile('fetchData', 'needMoreData')` | | ||
| [`empty`](#empty) | empty sequence | `composer.empty()` | ||
| [`sequence` or `seq`](#sequence) | sequence | `composer.sequence('hello', 'bye')` | | ||
| [`task`](#task) | single task | `composer.task('echo')` | ||
| [`finally`](#finally) | finalization | `composer.finally('tryThis', 'doThatAlways')` | | ||
| [`function`](#function) | Javascript function | `composer.function(({ x, y }) => ({ product: x * y }))` | | ||
| [`if` and `if_nosave`](#if) | conditional | `composer.if('authenticate', 'success', 'failure')` | | ||
| [`let`](#let) | variable declarations | `composer.let({ count: 3, message: 'hello' }, ...)` | | ||
| [`literal` or `value`](#literal) | constant value | `composer.literal({ message: 'Hello, World!' })` | | ||
| [`mask`](#mask) | variable hiding | `composer.let({ n }, composer.while(_ => n-- > 0, composer.mask(composition)))` | | ||
| [`if` and `if_nosave`](#if) | conditional | `composer.if('authenticate', 'success', 'failure')` | | ||
| [`while` and `while_nosave`](#while) | loop | `composer.while('notEnough', 'doMore')` | | ||
| [`dowhile` and `dowhile_nosave`](#dowhile) | loop at least once | `composer.dowhile('fetchData', 'needMoreData')` | | ||
| [`merge`](#merge) | data augmentation | `composer.merge('hash')` | | ||
| [`repeat`](#repeat) | counted loop | `composer.repeat(3, 'hello')` | | ||
| [`retain` and `retain_catch`](#retain) | persistence | `composer.retain('validateInput')` | | ||
| [`retry`](#retry) | error recovery | `composer.retry(3, 'connect')` | | ||
| [`sequence` or `seq`](#sequence) | sequence | `composer.sequence('hello', 'bye')` | | ||
| [`task`](#task) | single task | `composer.task('echo')` | ||
| [`try`](#try) | error handling | `composer.try('divideByN', 'NaN')` | | ||
| [`finally`](#finally) | finalization | `composer.finally('tryThis', 'doThatAlways')` | | ||
| [`retry`](#retry) | error recovery | `composer.retry(3, 'connect')` | | ||
| [`retain` and `retain_catch`](#retain) | persistence | `composer.retain('validateInput')` | | ||
| [`async`](#async) | asynchronous invocation | `composer.async('sendMessage')` | | ||
| [`while` and `while_nosave`](#while) | loop | `composer.while('notEnough', 'doMore')` | | ||
The `action`, `function`, and `literal` combinators construct compositions respectively from actions, functions, and constant values. The other combinators combine existing compositions to produce new compositions. | ||
The `action`, `function`, and `literal` combinators construct compositions | ||
respectively from OpenWhisk actions, Javascript functions, and constant values. | ||
The other combinators combine existing compositions to produce new compositions. | ||
@@ -35,11 +57,12 @@ ## Shorthands | ||
## Primitive combinators | ||
Some of these combinators are _derived_ combinators: they are equivalent to combinations of other combinators. The `composer` module offers a `composer.lower` method (see [COMPOSER.md](#COMPOSER.md)) that can eliminate derived combinators from a composition, producing an equivalent composition made only of _primitive_ combinators. | ||
## Action | ||
`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. | ||
`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. | ||
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. | ||
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. | ||
@@ -52,3 +75,4 @@ Examples: | ||
``` | ||
The optional `options` dictionary makes it possible to provide a definition for the action being composed. | ||
The optional `options` dictionary makes it possible to provide a definition for | ||
the action being composed. | ||
```javascript | ||
@@ -82,7 +106,12 @@ // specify the code for the action as a function | ||
``` | ||
The action may be defined by providing the code for the action as a string, as a Javascript function, or as a file name. Alternatively, a sequence action may be defined by providing the list of sequenced actions. The code (specified as a string) may be annotated with the kind of the action runtime. | ||
The action may be defined by providing the code for the action as a string, as a | ||
Javascript function, or as a file name. Alternatively, a sequence action may be | ||
defined by providing the list of sequenced actions. The code (specified as a | ||
string) may be annotated with the kind of the action runtime. | ||
### Environment capture in actions | ||
Javascript functions used to define actions cannot capture any part of their declaration environment. The following code is not correct as the declaration of `name` would not be available at invocation time: | ||
Javascript functions used to define actions cannot capture any part of their | ||
declaration environment. The following code is not correct as the declaration of | ||
`name` would not be available at invocation time: | ||
```javascript | ||
@@ -92,3 +121,4 @@ let name = 'Dave' | ||
``` | ||
In contrast, the following code is correct as it resolves `name`'s value at composition time. | ||
In contrast, the following code is correct as it resolves `name`'s value at | ||
composition time. | ||
```javascript | ||
@@ -101,7 +131,17 @@ let name = 'Dave' | ||
`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`. | ||
`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`. | ||
@@ -119,5 +159,8 @@ Examples: | ||
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. | ||
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 [let](#let) combinator discussed | ||
below. | ||
The following is not legal: | ||
The following code is not correct: | ||
```javascript | ||
@@ -127,3 +170,3 @@ let name = 'Dave' | ||
``` | ||
The following is legal: | ||
The following code is correct: | ||
```javascript | ||
@@ -135,5 +178,11 @@ composer.let({ name: 'Dave' }, composer.function(params => ({ message: 'Hello ' + name }))) | ||
`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. | ||
`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: | ||
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 | ||
@@ -145,19 +194,38 @@ composer.sequence( | ||
JSON values cannot represent functions. Applying `composer.literal` to a value of type `'function'` will result in an error. Functions embedded in a `value` of type `'object'`, e.g., `{ f: p => p, n: 42 }` will be silently omitted from the JSON dictionary. In other words, `composer.literal({ f: p => p, n: 42 })` will output `{ n: 42 }`. | ||
JSON values cannot represent functions. Applying `composer.literal` to a value | ||
of type `'function'` will result in an error. Functions embedded in a `value` of | ||
type `'object'`, e.g., `{ f: p => p, n: 42 }` will be silently omitted from the | ||
JSON dictionary. In other words, `composer.literal({ f: p => p, n: 42 })` will | ||
output `{ n: 42 }`. | ||
In general, a function can be embedded in a composition either by using the `composer.function` combinator, or by embedding the source code for the function as a string and later using `eval` to evaluate the function code. | ||
In general, a function can be embedded in a composition either by using the | ||
`composer.function` combinator, or by embedding the source code for the function | ||
as a string and later using `eval` to evaluate the function code. | ||
## Empty | ||
## Sequence | ||
`composer.empty()` is a shorthand for the empty sequence `composer.sequence()`. It is typically used to make it clear that a composition, e.g., a branch of an `if` combinator, is intentionally doing nothing. | ||
`composer.sequence(composition_1, composition_2, ...)` or it synonymous | ||
`composer.seq(composition_1, composition_2, ...)` chain a series of compositions | ||
(possibly empty). | ||
## Sequence | ||
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. | ||
`composer.sequence(composition_1, composition_2, ...)` chains a series of compositions (possibly empty). | ||
If one of the components fails (i.e., returns an error object), the remainder of | ||
the sequence is not executed. The output parameter object for the composition is | ||
the error object produced by the failed component. | ||
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. | ||
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. | ||
If one of the components fails (i.e., returns an error object), the remainder of the sequence is not executed. The output parameter object for the composition is the error object produced by the failed component. | ||
## Empty | ||
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. | ||
`composer.empty()` is a shorthand for the empty sequence `composer.sequence()`. | ||
It is typically used to make it clear that a composition, e.g., a branch of an | ||
`if` combinator, is intentionally doing nothing. | ||
@@ -170,13 +238,27 @@ ## Task | ||
`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 a sequence of compositions in the scope of these declarations. | ||
`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 a sequence of compositions in the scope of these | ||
declarations. | ||
The initial values must be valid JSON values. In particular, `composer.let({ foo: undefined })` is incorrect as `undefined` is not representable by a JSON value. On the other hand, `composer.let({ foo: null })` is correct. For the same reason, initial values cannot be functions, e.g., `composer.let({ foo: params => params })` is incorrect. | ||
The initial values must be valid JSON values. In particular, `composer.let({foo: | ||
undefined }, composition)` is incorrect as `undefined` is not representable by a | ||
JSON value. Use `composer.let({ foo: null }, composition)` instead. For the same | ||
reason, initial values cannot be functions, e.g., `composer.let({ foo: params => | ||
params }, composition)` is incorrect. | ||
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. | ||
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. | ||
For example, the following composition invokes composition `composition` repeatedly `n` times. | ||
For example, the following composition invokes composition `composition` | ||
repeatedly `n` times. | ||
```javascript | ||
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: | ||
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 | ||
@@ -186,53 +268,104 @@ composer.let({ n: 42 }, () => ({ n }), 'increment', params => { n = params.n }) | ||
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`. | ||
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`. | ||
## Mask | ||
`composer.mask(composition)` is meant to be used in combination with the `let` combinator. It makes it possible to hide the innermost enclosing `let` combinator from _composition_. It is typically used to define composition templates that need to introduce variables. | ||
`composer.mask(composition_1, composition_2, ...)` is meant to be used in | ||
combination with the `let` combinator. It runs a sequence of compositions | ||
excluding from their scope the variables declared by the innermost enclosing | ||
`let`. It is typically used to define composition templates that need to | ||
introduce variables. | ||
For instance, the following function is a possible implementation of a repeat loop: | ||
For instance, the following function is a possible implementation of a repeat | ||
loop: | ||
```javascript | ||
function loop(n, composition) { | ||
return .let({ n }, composer.while(() => n-- > 0, composer.mask(composition))) | ||
return composer.let({ n }, composer.while(() => n-- > 0, composer.mask(composition))) | ||
} | ||
``` | ||
This function takes two parameters: the number of iterations _n_ and the _composition_ to repeat _n_ times. Here, the `mask` combinator makes sure that this declaration of _n_ is not visible to _composition_. Thanks to `mask`, the following example correctly returns `{ value: 12 }`. | ||
This function takes two parameters: the number of iterations _n_ and the | ||
_composition_ to repeat _n_ times. Here, the `mask` combinator makes sure that | ||
this declaration of _n_ is not visible to _composition_. Thanks to `mask`, the | ||
following example correctly returns `{ value: 12 }`. | ||
```javascript | ||
composer.let({ n: 0 }, loop(3, loop(4, () => ++n))) | ||
``` | ||
While composer variables are dynamically scoped, the `mask` combinator alleviates the biggest concern with dynamic scoping: incidental name collision. | ||
While composer variables are dynamically scoped, judicious use of the `mask` | ||
combinator can prevent incidental name collision. | ||
## If | ||
`composer.if(condition, consequent, [alternate])` runs either the _consequent_ composition if the _condition_ evaluates to true or the _alternate_ composition if not. | ||
`composer.if(condition, consequent, [alternate])` runs either the _consequent_ | ||
composition if the _condition_ evaluates to true or the _alternate_ composition | ||
if not. | ||
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. | ||
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. | ||
The _alternate_ composition may be omitted. If _condition_ fails, neither branch is executed. | ||
The _alternate_ composition may be omitted. If _condition_ fails, neither branch | ||
is executed. | ||
The output parameter object of the _condition_ composition is discarded, one the choice of a branch has been made and the _consequent_ composition or _alternate_ composition is invoked on the input parameter object for the composition. For example, the following composition divides parameter `n` by two if `n` is even: | ||
The output parameter object of the _condition_ composition is discarded, one the | ||
choice of a branch has been made and the _consequent_ composition or _alternate_ | ||
composition is invoked on the input parameter object for the composition. For | ||
example, the following composition divides parameter `n` by two if `n` is even: | ||
```javascript | ||
composer.if(params => params.n % 2 === 0, params => { params.n /= 2 }) | ||
``` | ||
The `if_nosave` combinator is similar but it does not preserve the input parameter object, i.e., the _consequent_ composition or _alternate_ composition is invoked on the output parameter object of _condition_. The following example also divides parameter `n` by two if `n` is even: | ||
The `if_nosave` combinator is similar but it does not preserve the input | ||
parameter object, i.e., the _consequent_ composition or _alternate_ composition | ||
is invoked on the output parameter object of _condition_. The following example | ||
also divides parameter `n` by two if `n` is even: | ||
```javascript | ||
composer.if_nosave(params => { params.value = params.n % 2 === 0 }, params => { params.n /= 2 }) | ||
``` | ||
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. | ||
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. | ||
While, the `if` combinator 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 `if_nosave` combinator omits the parameter save, hence preserving the parameter size limit. | ||
While, the `if` combinator 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 `if_nosave` combinator omits the parameter save, | ||
hence preserving the parameter size limit. | ||
## While | ||
`composer.while(condition, body)` 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. | ||
`composer.while(condition, body)` 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. | ||
A failure of _condition_ or _body_ interrupts the execution. The composition returns the error object from the failed component. | ||
A failure of _condition_ or _body_ interrupts the execution. The composition | ||
returns the error object from the failed component. | ||
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 `while_nosave` combinator is used, 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. | ||
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 `while_nosave` | ||
combinator is used, 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. | ||
For instance, the following composition invoked on dictionary `{ n: 28 }` returns `{ n: 7 }`: | ||
For instance, the following composition invoked on dictionary `{ n: 28 }` | ||
returns `{ n: 7 }`: | ||
```javascript | ||
composer.while(params => params.n % 2 === 0, params => { params.n /= 2 }) | ||
``` | ||
For instance, the following composition invoked on dictionary `{ n: 28 }` returns `{ n: 7, value: false }`: | ||
For instance, the following composition invoked on dictionary `{ n: 28 }` | ||
returns `{ n: 7, value: false }`: | ||
```javascript | ||
@@ -244,9 +377,13 @@ composer.while_nosave(params => { params.value = params.n % 2 === 0 }, params => { params.n /= 2 }) | ||
`composer.dowhile(condition, body)` is similar to `composer.while(body, condition)` except that _body_ is invoked before _condition_ is evaluated, hence _body_ is always invoked at least once. | ||
`composer.dowhile(condition, body)` is similar to `composer.while(body, | ||
condition)` except that _body_ is invoked before _condition_ is evaluated, hence | ||
_body_ is always invoked at least once. | ||
Like `while_nosave`, `dowhile_nosave` does not implicitly preserve the parameter object while evaluating _condition_. | ||
Like `while_nosave`, `dowhile_nosave` does not implicitly preserve the parameter | ||
object while evaluating _condition_. | ||
## Repeat | ||
`composer.repeat(count, body)` invokes _body_ _count_ times. | ||
`composer.repeat(count, composition_1, composition_2, ...)` invokes a sequence | ||
of compositions _count_ times. | ||
@@ -257,3 +394,4 @@ ## Try | ||
If _body_ returns an error object, _handler_ is invoked with this error object as its input parameter object. Otherwise, _handler_ is not run. | ||
If _body_ returns an error object, _handler_ is invoked with this error object | ||
as its input parameter object. Otherwise, _handler_ is not run. | ||
@@ -264,16 +402,41 @@ ## Finally | ||
The _finalizer_ is invoked in sequence after _body_ even if _body_ returns an error object. | ||
The _finalizer_ is invoked in sequence after _body_ even if _body_ returns an | ||
error object. The output parameter object of _body_ (error object or not) is the | ||
input parameter object of _finalizer_. | ||
## Retry | ||
`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. | ||
`composer.retry(count, composition_1, composition_2, ...)` runs a sequence of | ||
compositions retrying the sequence up to _count_ times if it fails. The output | ||
parameter object for the composition is either the output parameter object of | ||
the successful sequence invocation or the error object produced by the last | ||
sequence invocation. | ||
## Retain | ||
`composer.retain(body)` 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_. | ||
`composer.retain(composition_1, composition_2, ...)` runs a sequence of | ||
compositions 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 the sequence. | ||
If _body_ fails, the output of the `retain` combinator is only the error object (i.e., the input parameter object is not preserved). In constrast, the `retain_catch` combinator always outputs `{ params, result }`, even if `result` is an error result. | ||
If the sequence fails, the output of the `retain` combinator is only the error | ||
object (i.e., the input parameter object is not preserved). In contrast, the | ||
`retain_catch` combinator always outputs `{ params, result }`, even if `result` | ||
is an error object. | ||
## Merge | ||
`composer.merge(composition_1, composition_2, ...)` runs a sequence of | ||
compositions on the input parameter object and merge the output parameter object | ||
of the sequence into the input parameter object. In other words, | ||
`composer.merge(composition_1, composition_2, ...)` is a shorthand for: | ||
``` | ||
composer.seq(composer.retain(composition_1, composition_2, ...), ({ params, result }) => Object.assign(params, result)) | ||
``` | ||
## Async | ||
`composer.async(body)` runs the _body_ composition asynchronously. It spawns _body_ but does not wait for it to execute. It immediately returns a dictionary with a single field named `activationId` identifying the invocation of _body_. | ||
`composer.async(composition_1, composition_2, ...)` runs a sequence of | ||
compositions asynchronously. It invokes the sequence but does not wait for it to | ||
execute. It immediately returns a dictionary that includes a field named | ||
`activationId` with the activation id for the sequence invocation. |
@@ -0,8 +1,31 @@ | ||
<!-- | ||
# | ||
# Licensed to the Apache Software Foundation (ASF) under one or more | ||
# contributor license agreements. See the NOTICE file distributed with | ||
# this work for additional information regarding copyright ownership. | ||
# The ASF licenses this file to You under the Apache License, Version 2.0 | ||
# (the "License"); you may not use this file except in compliance with | ||
# the License. You may obtain a copy of the License at | ||
# | ||
# http://www.apache.org/licenses/LICENSE-2.0 | ||
# | ||
# Unless required by applicable law or agreed to in writing, software | ||
# distributed under the License is distributed on an "AS IS" BASIS, | ||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
# See the License for the specific language governing permissions and | ||
# limitations under the License. | ||
# | ||
--> | ||
# Compositions | ||
Composer makes it possible to assemble actions into rich workflows called _compositions_. An example composition is described in [../README.md](../README.md). | ||
Composer makes it possible to assemble actions into rich workflows called | ||
_compositions_. An example composition is described in | ||
[../README.md](../README.md). | ||
## Control flow | ||
Compositions can express the control flow of typical a sequential imperative programming language: sequences, conditionals, loops, error handling. This control flow is specified using _combinator_ methods such as: | ||
Compositions can express the control flow of typical a sequential imperative | ||
programming language: sequences, conditionals, loops, structured error handling. | ||
This control flow is specified using _combinator_ methods such as: | ||
- `composer.sequence(firstAction, secondAction)` | ||
@@ -16,25 +39,50 @@ - `composer.if(conditionAction, consequentAction, alternateAction)` | ||
Combinators return composition objects, i.e., instances of the `Composition` class. | ||
Combinators return composition objects, i.e., instances of the `Composition` | ||
class. | ||
## Parameter objects and error objects | ||
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. | ||
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. | ||
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' }`. | ||
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' }`. | ||
Error objects play a specific role as they interrupt the normal flow of execution, akin to exceptions in traditional programming languages. For instance, if a component of a sequence returns an error object, the remainder of the sequence is not executed. Moreover, if the sequence is enclosed in an error handling composition like a `composer.try(sequence, handler)` combinator, the execution continues with the error handler. | ||
Error objects play a specific role as they interrupt the normal flow of | ||
execution, akin to exceptions in traditional programming languages. For | ||
instance, if a component of a sequence returns an error object, the remainder of | ||
the sequence is not executed. Moreover, if the sequence is enclosed in an error | ||
handling composition like a `composer.try(sequence, handler)` combinator, the | ||
execution continues with the error handler. | ||
## Data flow | ||
The invocation of a composition triggers a series of computations (possibly empty, e.g., for the empty sequence) obtained by chaining the components of the composition along the path of execution. The input parameter object for the composition is the input parameter object of the first component in the chain. The output parameter object of a component in the chain is typically the input parameter object for the next component if any or the output parameter object for the composition if this is the final component in the chain. | ||
The invocation of a composition triggers a series of computations (possibly | ||
empty, e.g., for the empty sequence) obtained by chaining the components of the | ||
composition along the path of execution. The input parameter object for the | ||
composition is the input parameter object of the first component in the chain. | ||
The output parameter object of a component in the chain is typically the input | ||
parameter object for the next component if any or the output parameter object | ||
for the composition if this is the final component in the chain. | ||
For example, the composition `composer.sequence('triple', 'increment')` invokes the `increment` action on the output of the `triple` action. | ||
For example, the composition `composer.sequence('triple', 'increment')` invokes | ||
the `increment` action on the output of the `triple` action. | ||
Some combinators however are designed to alter the default flow of data. For instance, the `composer.retain(myAction)` composition returns a combination of the input parameter object and the output parameter object of `myAction`. | ||
Some combinators however are designed to alter the default flow of data. For | ||
instance, the `composer.merge('myAction')` composition merges the input and | ||
output parameter objects of `myAction`. | ||
## Components | ||
Components of a compositions can be actions, Javascript functions, or compositions. | ||
Components of a compositions can be actions, Javascript functions, or | ||
compositions. | ||
Javascript functions can be viewed as simple, anonymous actions that do not need to be deployed and managed separately from the composition they belong to. Functions are typically used to alter a parameter object between two actions that expect different schemas, as in: | ||
Javascript functions can be viewed as simple, anonymous actions that do not need | ||
to be deployed and managed separately from the composition they belong to. | ||
Functions are typically used to alter a parameter object between two actions | ||
that expect different schemas, as in: | ||
```javascript | ||
@@ -47,11 +95,17 @@ composer.sequence('getUserNameAndPassword', params => ({ key = btoa(params.user + ':' + params.password) }), 'authenticate') | ||
``` | ||
Compositions can reference other compositions by name. For instance, assuming we deploy the sequential composition of the `triple` and `increment` actions as the composition `tripleAndIncrement`, the following code behaves identically to the previous example: | ||
Compositions can reference other compositions by name. For instance, assuming we | ||
deploy the sequential composition of the `triple` and `increment` actions as the | ||
composition `tripleAndIncrement`, the following code behaves identically to the | ||
previous example: | ||
```javascript | ||
composer.if('isEven', 'half', 'tripleAndIncrement') | ||
``` | ||
The behavior of this last composition would be altered if we redefine the `tripleAndIncrement` composition to do something else, whereas the first example would not be affected. | ||
The behavior of this last composition would be altered if we redefine the | ||
`tripleAndIncrement` composition to do something else, whereas the first example | ||
would not be affected. | ||
## Nested declarations | ||
## Embedded action definitions | ||
A composition can embed the definitions of none, some, or all the composed actions as illustrated in [demo.js](../samples/demo.js): | ||
A composition can embed the definitions of none, some, or all the composed | ||
actions as illustrated in [demo.js](../samples/demo.js): | ||
```javascript | ||
@@ -61,3 +115,3 @@ composer.if( | ||
composer.action('success', { action: function () { return { message: 'success' } } }), | ||
composer.action('failure', { action: function () { return { message: 'failure' } } })) | ||
composer.action('failure', { action: function () { return { message: 'failure' } } })) | ||
) | ||
@@ -67,9 +121,11 @@ ``` | ||
## Serialization and deserialization | ||
Compositions objects can be serialized to JSON dictionaries by invoking `JSON.stringify` on them. Serialized compositions can be deserialized to composition objects using the `composer.deserialize(serializedComposition)` method. The JSON format is documented in [FORMAT.md](FORMAT.md). | ||
In short, the JSON dictionary for a composition contains a representation of the syntax tree for this composition as well as the definition of all the actions embedded inside the composition. | ||
## Conductor actions | ||
Compositions are implemented by means of OpenWhisk [conductor actions](https://github.com/apache/incubator-openwhisk/blob/master/docs/conductors.md). The conductor actions are implicitly synthesized when compositions are deployed using the `compose` command or the `composer.deploy` method. Alternatively, the `composer.encode` method can encode compositions without deploying them. See [COMPOSER.md](COMPOSER.md) for details. | ||
Compositions are implemented by means of OpenWhisk [conductor | ||
actions](https://github.com/apache/incubator-openwhisk/blob/master/docs/conductors.md). | ||
Compositions have all the attributes and capabilities of an action, e.g., | ||
default parameters, limits, blocking invocation, web export. Execution | ||
[traces](https://github.com/apache/incubator-openwhisk/blob/master/docs/conductors.md#activations) | ||
and | ||
[limits](https://github.com/apache/incubator-openwhisk/blob/master/docs/conductors.md#limits) | ||
of compositions follow from conductor actions. |
@@ -0,14 +1,30 @@ | ||
<!-- | ||
# | ||
# Licensed to the Apache Software Foundation (ASF) under one or more | ||
# contributor license agreements. See the NOTICE file distributed with | ||
# this work for additional information regarding copyright ownership. | ||
# The ASF licenses this file to You under the Apache License, Version 2.0 | ||
# (the "License"); you may not use this file except in compliance with | ||
# the License. You may obtain a copy of the License at | ||
# | ||
# http://www.apache.org/licenses/LICENSE-2.0 | ||
# | ||
# Unless required by applicable law or agreed to in writing, software | ||
# distributed under the License is distributed on an "AS IS" BASIS, | ||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
# See the License for the specific language governing permissions and | ||
# limitations under the License. | ||
# | ||
--> | ||
# Composer Package | ||
The Composer package consists of: | ||
* the [composer](../composer.js) Node.js module for authoring, deploying, and invoking compositions, | ||
* the [compose](../bin/compose) command for managing compositions from the command line. | ||
* the [composer](../composer.js) Node.js module for authoring compositions, | ||
* the [compose](../bin/compose.js) and [deploy](../bin/deploy.js) commands for | ||
managing compositions from the command line. | ||
The documentation for the Composer package is organized as follows: | ||
- [COMPOSITIONS.md](COMPOSITIONS.md) gives a brief introduction to compositions. | ||
- [COMPOSER.md](COMPOSER.md) documents the `composer` module. | ||
- [COMPOSE.md](COMPOSE.md) documents the `compose` command. | ||
- [COMBINATORS.md](COMBINATORS.md) documents the methods of the `composer` object. | ||
- [FORMAT.md](FORMAT.md) documents the JSON format for encoding compositions. | ||
- [TEMPLATES.md](TEMPLATES.md) demonstrates various composition templates. | ||
- The [tutorials](tutorials) folder includes various tutorials. | ||
- [COMBINATORS.md](COMBINATORS.md) explains the composition constructs. | ||
- [COMMANDS.md](COMMANDS.md) describes the `compose` and `deploy` commands. |
{ | ||
"name": "@ibm-functions/composer", | ||
"version": "0.7.0", | ||
"description": "Composer is an IBM Cloud Functions programming model for composing individual functions into larger applications.", | ||
"version": "0.8.0", | ||
"description": "Composer is a new programming model for composing cloud functions built on Apache OpenWhisk.", | ||
"homepage": "https://github.com/ibm-functions/composer", | ||
"main": "composer.js", | ||
"scripts": { | ||
"test": "mocha" | ||
"test": "standard && mocha" | ||
}, | ||
"bin": { | ||
"compose": "./bin/compose" | ||
"compose": "./bin/compose.js", | ||
"deploy": "./bin/deploy.js" | ||
}, | ||
@@ -16,5 +17,5 @@ "files": [ | ||
"composer.js", | ||
"conductor.js", | ||
"docs/*.md", | ||
"samples/", | ||
"test/" | ||
"samples/" | ||
], | ||
@@ -26,7 +27,5 @@ "repository": { | ||
"keywords": [ | ||
"ibm", | ||
"functions", | ||
"serverless", | ||
"composer", | ||
"bluemix", | ||
"openwhisk" | ||
@@ -37,7 +36,9 @@ ], | ||
"openwhisk": "^3.11.0", | ||
"semver": "^5.3.0", | ||
"uglify-es": "^3.3.9" | ||
"openwhisk-fqn": "^0.0.2", | ||
"terser": "^3.8.2" | ||
}, | ||
"devDependencies": { | ||
"mocha": "^5.2.0" | ||
"mocha": "^5.2.0", | ||
"pre-commit": "^1.2.2", | ||
"standard": "^12.0.1" | ||
}, | ||
@@ -44,0 +45,0 @@ "author": { |
149
README.md
@@ -0,1 +1,20 @@ | ||
<!-- | ||
# | ||
# Licensed to the Apache Software Foundation (ASF) under one or more | ||
# contributor license agreements. See the NOTICE file distributed with | ||
# this work for additional information regarding copyright ownership. | ||
# The ASF licenses this file to You under the Apache License, Version 2.0 | ||
# (the "License"); you may not use this file except in compliance with | ||
# the License. You may obtain a copy of the License at | ||
# | ||
# http://www.apache.org/licenses/LICENSE-2.0 | ||
# | ||
# Unless required by applicable law or agreed to in writing, software | ||
# distributed under the License is distributed on an "AS IS" BASIS, | ||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
# See the License for the specific language governing permissions and | ||
# limitations under the License. | ||
# | ||
--> | ||
# @ibm-functions/composer | ||
@@ -8,38 +27,20 @@ | ||
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 is a new programming model for composing cloud functions 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. | ||
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](docs/COMPOSE.md) command | ||
(for deployment) and the [OpenWhisk | ||
CLI](https://console.bluemix.net/openwhisk/learn/cli) (for configuration, | ||
invocation, and life-cycle management). | ||
**In contrast to earlier releases of Composer, a Redis server is not required to | ||
run compositions**. Composer now synthesizes OpenWhisk [conductor | ||
Composer 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). | ||
of an action, e.g., default parameters, limits, blocking invocation, web export. | ||
This repository includes: | ||
* the [composer](docs/COMPOSER.md) Node.js module for authoring compositions using | ||
* the [composer](composer.js) Node.js module for authoring compositions using | ||
JavaScript, | ||
* the [compose](docs/COMPOSE.md) command for deploying compositions, | ||
* the [compose](bin/compose.js) and [deploy](bin/deploy.js) | ||
[commands](docs/COMMANDS.md) for compiling and deploying compositions, | ||
* [documentation](docs), [examples](samples), and [tests](test). | ||
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 | ||
@@ -50,11 +51,6 @@ | ||
``` | ||
npm install @ibm-functions/composer | ||
npm install -g @ibm-functions/composer | ||
``` | ||
We recommend to also install the package globally (with `-g` option) if you intend to | ||
use the `compose` command to define and deploy compositions. | ||
``` | ||
npm -g install @ibm-functions/composer | ||
``` | ||
Shell embeds the Composer package, so there is no need to install | ||
Composer explicitly when using Shell. | ||
We recommend to install the package globally (with `-g` option) if you intend to | ||
use the `compose` and `deploy` commands to compile and deploy compositions. | ||
@@ -73,9 +69,9 @@ ## Defining a composition | ||
``` | ||
Compositions compose actions using _combinator_ methods. These methods | ||
implement the typical control-flow constructs of a sequential imperative | ||
programming language. 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. | ||
Compositions compose actions using [combinator](docs/COMBINATORS.md) methods. | ||
These methods implement the typical control-flow constructs of a sequential | ||
imperative programming language. 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. | ||
@@ -91,5 +87,6 @@ This composition includes the definitions of the three composed actions. If the | ||
One way to deploy a composition is to use the [compose](docs/COMPOSE.md) command: | ||
One way to deploy a composition is to use the `compose` and `deploy` commands: | ||
``` | ||
compose demo.js --deploy demo | ||
compose demo.js > demo.json | ||
deploy demo demo.json -w | ||
``` | ||
@@ -99,5 +96,7 @@ ``` | ||
``` | ||
The `compose` command synthesizes and deploys an action named `demo` that | ||
implements the composition. It also deploys the composed actions if definitions | ||
are provided for them. | ||
The `compose` command compiles the composition code to a portable JSON format. | ||
The `deploy` command deploys the JSON-encoded composition creating an action | ||
with the given name. It also deploys the composed actions if definitions are | ||
provided for them. The `-w` option authorizes the `deploy` command to overwrite | ||
existing definitions. | ||
@@ -124,3 +123,3 @@ ## Running a composition | ||
``` | ||
## Execution traces | ||
### Execution traces | ||
@@ -152,53 +151,1 @@ This invocation creates a trace, i.e., a series of activation records: | ||
explains execution traces in greater details. | ||
## 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) | ||
* [Data Flows in Serverless Cloud-Native | ||
Applications](http://heidloff.net/article/serverless-data-flows) | ||
* [Transforming JSON Data in Serverless | ||
Applications](http://heidloff.net/article/transforming-json-serverless) | ||
## 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). |
/* | ||
* Copyright 2017 IBM Corporation | ||
* Licensed to the Apache Software Foundation (ASF) under one or more | ||
* contributor license agreements. See the NOTICE file distributed with | ||
* this work for additional information regarding copyright ownership. | ||
* The ASF licenses this file to You under the Apache License, Version 2.0 | ||
* (the "License"); you may not use this file except in compliance with | ||
* the License. You may obtain a copy of the License at | ||
* | ||
* Licensed under the Apache License, Version 2.0 (the "License"); | ||
* you may not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* http://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* http://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, software | ||
@@ -20,4 +21,4 @@ * distributed under the License is distributed on an "AS IS" BASIS, | ||
module.exports = composer.if( | ||
composer.action('authenticate', { action: function ({ password }) { return { value: password === 'abc123' } } }), | ||
composer.action('success', { action: function () { return { message: 'success' } } }), | ||
composer.action('failure', { action: function () { return { message: 'failure' } } })) | ||
composer.action('authenticate', { action: function ({ password }) { return { value: password === 'abc123' } } }), | ||
composer.action('success', { action: function () { return { message: 'success' } } }), | ||
composer.action('failure', { action: function () { return { message: 'failure' } } })) |
{ | ||
"type": "if", | ||
"test": { | ||
"type": "action", | ||
"name": "/_/authenticate", | ||
"action": { | ||
"exec": { | ||
"kind": "nodejs:default", | ||
"code": "const main = function ({ password }) { return { value: password === 'abc123' } }" | ||
"composition": { | ||
"type": "let", | ||
"declarations": { | ||
"params": null | ||
}, | ||
"components": [ | ||
{ | ||
"type": "finally", | ||
"body": { | ||
"type": "function", | ||
"function": { | ||
"exec": { | ||
"kind": "nodejs:default", | ||
"code": "args => { params = args }" | ||
} | ||
} | ||
}, | ||
"finalizer": { | ||
"type": "if_nosave", | ||
"test": { | ||
"type": "mask", | ||
"components": [ | ||
{ | ||
"type": "action", | ||
"name": "/_/authenticate", | ||
"path": ".test" | ||
} | ||
] | ||
}, | ||
"consequent": { | ||
"type": "finally", | ||
"body": { | ||
"type": "function", | ||
"function": { | ||
"exec": { | ||
"kind": "nodejs:default", | ||
"code": "() => params" | ||
} | ||
} | ||
}, | ||
"finalizer": { | ||
"type": "mask", | ||
"components": [ | ||
{ | ||
"type": "action", | ||
"name": "/_/success", | ||
"path": ".consequent" | ||
} | ||
] | ||
} | ||
}, | ||
"alternate": { | ||
"type": "finally", | ||
"body": { | ||
"type": "function", | ||
"function": { | ||
"exec": { | ||
"kind": "nodejs:default", | ||
"code": "() => params" | ||
} | ||
} | ||
}, | ||
"finalizer": { | ||
"type": "mask", | ||
"components": [ | ||
{ | ||
"type": "action", | ||
"name": "/_/failure", | ||
"path": ".alternate" | ||
} | ||
] | ||
} | ||
} | ||
} | ||
} | ||
} | ||
], | ||
"path": "" | ||
}, | ||
"consequent": { | ||
"type": "action", | ||
"name": "/_/success", | ||
"action": { | ||
"exec": { | ||
"kind": "nodejs:default", | ||
"code": "const main = function () { return { message: 'success' } }" | ||
"ast": { | ||
"type": "if", | ||
"test": { | ||
"type": "action", | ||
"name": "/_/authenticate", | ||
"action": { | ||
"exec": { | ||
"kind": "nodejs:default", | ||
"code": "const main = function ({ password }) { return { value: password === 'abc123' } }" | ||
} | ||
} | ||
}, | ||
"consequent": { | ||
"type": "action", | ||
"name": "/_/success", | ||
"action": { | ||
"exec": { | ||
"kind": "nodejs:default", | ||
"code": "const main = function () { return { message: 'success' } }" | ||
} | ||
} | ||
}, | ||
"alternate": { | ||
"type": "action", | ||
"name": "/_/failure", | ||
"action": { | ||
"exec": { | ||
"kind": "nodejs:default", | ||
"code": "const main = function () { return { message: 'failure' } }" | ||
} | ||
} | ||
} | ||
}, | ||
"alternate": { | ||
"type": "action", | ||
"name": "/_/failure", | ||
"action": { | ||
"exec": { | ||
"kind": "nodejs:default", | ||
"code": "const main = function () { return { message: 'failure' } }" | ||
"version": "0.8.0", | ||
"actions": [ | ||
{ | ||
"name": "/_/authenticate", | ||
"action": { | ||
"exec": { | ||
"kind": "nodejs:default", | ||
"code": "const main = function ({ password }) { return { value: password === 'abc123' } }" | ||
} | ||
} | ||
}, | ||
{ | ||
"name": "/_/success", | ||
"action": { | ||
"exec": { | ||
"kind": "nodejs:default", | ||
"code": "const main = function () { return { message: 'success' } }" | ||
} | ||
} | ||
}, | ||
{ | ||
"name": "/_/failure", | ||
"action": { | ||
"exec": { | ||
"kind": "nodejs:default", | ||
"code": "const main = function () { return { message: '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
Debug access
Supply chain riskUses debug, reflection and dynamic code execution features.
Found 1 instance in 1 package
Dynamic require
Supply chain riskDynamic require can indicate the package is performing dangerous or unsafe dynamic code execution.
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
Dynamic require
Supply chain riskDynamic require can indicate the package is performing dangerous or unsafe dynamic code execution.
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
Minified code
QualityThis package contains minified code. This may be harmless in some cases where minified code is included in packaged libraries, however packages on npm should not minify code.
Found 1 instance in 1 package
1
89244
3
13
922
145
13
+ Addedopenwhisk-fqn@^0.0.2
+ Addedterser@^3.8.2
+ Addedbuffer-from@1.1.2(transitive)
+ Addedcommander@2.20.3(transitive)
+ Addedopenwhisk-fqn@0.0.2(transitive)
+ Addedsource-map-support@0.5.21(transitive)
+ Addedterser@3.17.0(transitive)
- Removedsemver@^5.3.0
- Removeduglify-es@^3.3.9
- Removedcommander@2.14.1(transitive)
- Removedsemver@5.7.2(transitive)
- Removeduglify-es@3.3.10(transitive)