New Case Study:See how Anthropic automated 95% of dependency reviews with Socket.Learn More
Socket
Sign inDemoInstall
Socket

@ibm-functions/composer

Package Overview
Dependencies
Maintainers
6
Versions
19
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@ibm-functions/composer - npm Package Compare versions

Comparing version 0.7.0 to 0.8.0

bin/compose.js

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": {

@@ -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' } }"
}
}
}
}
]
}
SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc