@ibm-functions/composer
Advanced tools
Comparing version 0.4.0 to 0.5.0
724
composer.js
@@ -19,9 +19,10 @@ /* | ||
// compiler code shared between composer and conductor (to permit client-side and server-side lowering) | ||
function compiler() { | ||
function main() { | ||
const fs = require('fs') | ||
const util = require('util') | ||
const semver = require('semver') | ||
// standard combinators | ||
const isObject = obj => typeof obj === 'object' && obj !== null && !Array.isArray(obj) | ||
// default combinators | ||
const combinators = { | ||
@@ -43,4 +44,4 @@ empty: { 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' }, | ||
composition: { args: [{ _: 'name', type: 'string' }, { _: 'composition' }], since: '0.4.0' }, | ||
action: { args: [{ _: 'name', type: 'string' }, { _: 'options', type: 'object', optional: true }], since: '0.4.0' }, | ||
composition: { args: [{ _: 'name', type: 'string' }, { _: 'composition' }, { _: 'options', type: 'object', optional: true }], since: '0.4.0' }, | ||
repeat: { args: [{ _: 'count', type: 'number' }], components: true, since: '0.4.0' }, | ||
@@ -53,3 +54,3 @@ retry: { args: [{ _: 'count', type: 'number' }], components: true, since: '0.4.0' }, | ||
// composer error class | ||
// error class | ||
class ComposerError extends Error { | ||
@@ -74,3 +75,3 @@ constructor(message, argument) { | ||
// apply f to all fields of type composition | ||
visit(f) { | ||
visit(combinators, f) { | ||
const combinator = combinators[this.type] | ||
@@ -88,4 +89,7 @@ if (combinator.components) { | ||
// compiler class | ||
class Compiler { | ||
// registered plugins | ||
const plugins = [] | ||
// composer & lowerer | ||
const composer = { | ||
// detect task type and create corresponding composition object | ||
@@ -99,3 +103,3 @@ task(task) { | ||
throw new ComposerError('Invalid argument', task) | ||
} | ||
}, | ||
@@ -112,6 +116,44 @@ // function combinator: stringify function code | ||
} | ||
if (typeof fun !== 'object' || fun === null) throw new ComposerError('Invalid argument', fun) | ||
if (!isObject(fun)) throw new ComposerError('Invalid argument', fun) | ||
return new Composition({ type: 'function', function: { exec: fun } }) | ||
} | ||
}, | ||
// enhanced action combinator: mangle name, capture code | ||
action(name, options = {}) { | ||
if (arguments.length > 2) throw new ComposerError('Too many arguments') | ||
if (!isObject(options)) throw new ComposerError('Invalid argument', options) | ||
name = parseActionName(name) // throws ComposerError if name is not valid | ||
let exec | ||
if (Array.isArray(options.sequence)) { // native sequence | ||
exec = { kind: 'sequence', components: options.sequence.map(parseActionName) } | ||
} | ||
if (typeof options.filename === 'string') { // read action code from file | ||
exec = fs.readFileSync(options.filename, { encoding: 'utf8' }) | ||
} | ||
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) | ||
} | ||
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 } | ||
if (options.async) composition.async = true | ||
return new Composition(composition) | ||
}, | ||
// enhanced composition combinator: mangle name | ||
composition(name, composition, options = {}) { | ||
if (arguments.length > 3) throw new ComposerError('Too many arguments') | ||
if (!isObject(options)) throw new ComposerError('Invalid argument', options) | ||
name = parseActionName(name) | ||
const obj = { type: 'composition', name, composition: this.task(composition) } | ||
if (options.async) obj.async = true | ||
return new Composition(obj) | ||
}, | ||
// lowering | ||
@@ -121,15 +163,15 @@ | ||
return this.sequence() | ||
} | ||
}, | ||
_seq(composition) { | ||
return this.sequence(...composition.components) | ||
} | ||
}, | ||
_value(composition) { | ||
return this._literal(composition) | ||
} | ||
}, | ||
_literal(composition) { | ||
return this.let({ value: composition.value }, () => value) | ||
} | ||
}, | ||
@@ -142,3 +184,3 @@ _retain(composition) { | ||
result => ({ params, result })) | ||
} | ||
}, | ||
@@ -152,3 +194,3 @@ _retain_catch(composition) { | ||
({ params, result }) => ({ params, result: result.result })) | ||
} | ||
}, | ||
@@ -163,3 +205,3 @@ _if(composition) { | ||
this.seq(() => params, this.mask(composition.alternate)))) | ||
} | ||
}, | ||
@@ -174,3 +216,3 @@ _while(composition) { | ||
() => params) | ||
} | ||
}, | ||
@@ -185,3 +227,3 @@ _dowhile(composition) { | ||
() => params) | ||
} | ||
}, | ||
@@ -194,3 +236,3 @@ _repeat(composition) { | ||
this.mask(this.seq(...composition.components)))) | ||
} | ||
}, | ||
@@ -205,46 +247,6 @@ _retry(composition) { | ||
({ result }) => result) | ||
} | ||
}, | ||
// define combinator methods for the standard combinators | ||
static init() { | ||
for (let type in combinators) { | ||
const combinator = combinators[type] | ||
// do not overwrite hand-written combinators | ||
Compiler.prototype[type] = Compiler.prototype[type] || function () { | ||
const composition = new 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 = arg.optional ? arguments[i] || null : arguments[i] | ||
switch (arg.type) { | ||
case undefined: | ||
composition[arg._] = this.task(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 => this.task(obj)) | ||
} | ||
return composition | ||
} | ||
} | ||
} | ||
combinators: {}, | ||
// return combinator list | ||
get combinators() { | ||
return combinators | ||
} | ||
// recursively deserialize composition | ||
@@ -254,5 +256,5 @@ deserialize(composition) { | ||
composition = new Composition(composition) // copy | ||
composition.visit(composition => this.deserialize(composition)) | ||
composition.visit(this.combinators, composition => this.deserialize(composition)) | ||
return composition | ||
} | ||
}, | ||
@@ -268,3 +270,3 @@ // label combinators with the json path | ||
// label nested combinators | ||
composition.visit(label(composition.path)) | ||
composition.visit(this.combinators, label(composition.path)) | ||
return composition | ||
@@ -274,3 +276,3 @@ } | ||
return label('')(composition) | ||
} | ||
}, | ||
@@ -282,7 +284,6 @@ // recursively label and lower combinators to the desired set of combinators (including primitive combinators) | ||
if (!Array.isArray(combinators) && typeof combinators !== 'boolean' && typeof combinators !== 'string') throw new ComposerError('Invalid argument', combinators) | ||
if (combinators === false) return composition // no lowering | ||
if (combinators === true || combinators === '') combinators = [] // maximal lowering | ||
if (typeof combinators === 'string') { // lower to combinators of specific composer version | ||
combinators = Object.keys(this.combinators).filter(key => semver.gte(combinators, this.combinators[key].since)) | ||
combinators = Object.keys(this.combinators.filter(key => semver.gte(combinators, this.combinators[key].since))) | ||
} | ||
@@ -299,3 +300,3 @@ | ||
// lower nested combinators | ||
composition.visit(lower) | ||
composition.visit(this.combinators, lower) | ||
return composition | ||
@@ -305,27 +306,13 @@ } | ||
return lower(composition) | ||
} | ||
}, | ||
// register plugin | ||
register(plugin) { | ||
if (plugin.combinators) init(plugin.combinators()) | ||
if (plugin.composer) Object.assign(this, plugin.composer({ ComposerError, Composition })) | ||
plugins.push(plugin) | ||
return this | ||
}, | ||
} | ||
Compiler.init() | ||
return { ComposerError, Composition, Compiler } | ||
} | ||
// composer module | ||
function composer() { | ||
const fs = require('fs') | ||
const os = require('os') | ||
const path = require('path') | ||
const { minify } = require('uglify-es') | ||
// read composer version number | ||
const { version } = require('./package.json') | ||
// initialize compiler | ||
const { ComposerError, Composition, Compiler } = compiler() | ||
// capture compiler and conductor code (omitting composer code) | ||
const conductorCode = minify(`const main=(${conductor})(${compiler}())`, { output: { max_line_len: 127 }, mangle: { reserved: [Composition.name] } }).code | ||
/** | ||
@@ -342,8 +329,9 @@ * Parses a (possibly fully qualified) resource name and validates it. If it's not a fully qualified name, | ||
function parseActionName(name) { | ||
if (typeof name !== 'string' || name.trim().length == 0) throw new ComposerError('Name is not specified') | ||
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() | ||
let delimiter = '/' | ||
let parts = name.split(delimiter) | ||
let n = parts.length | ||
let leadingSlash = name[0] == delimiter | ||
const delimiter = '/' | ||
const parts = name.split(delimiter) | ||
const n = parts.length | ||
const leadingSlash = name[0] == delimiter | ||
// no more than /ns/p/a | ||
@@ -353,3 +341,3 @@ if (n < 1 || n > 4 || (leadingSlash && n == 2) || (!leadingSlash && n == 4)) throw new ComposerError('Name is not valid') | ||
parts.forEach(function (part, i) { if (i > 0 && part.trim().length == 0) throw new ComposerError('Name is not valid') }) | ||
let newName = parts.join(delimiter) | ||
const newName = parts.join(delimiter) | ||
if (leadingSlash) return newName | ||
@@ -360,168 +348,195 @@ else if (n < 3) return `${delimiter}_${delimiter}${newName}` | ||
// management class for compositions | ||
class Compositions { | ||
constructor(wsk, composer) { | ||
this.actions = wsk.actions | ||
this.composer = composer | ||
// derive combinator methods from combinator table | ||
function init(combinators) { | ||
Object.assign(composer.combinators, combinators) | ||
for (let type in combinators) { | ||
const combinator = combinators[type] | ||
// do not overwrite existing combinators | ||
composer[type] = composer[type] || function () { | ||
const composition = new 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 = arg.optional ? arguments[i] || null : arguments[i] | ||
switch (arg.type) { | ||
case undefined: | ||
composition[arg._] = composer.task(argument) | ||
continue | ||
case 'value': | ||
if (typeof argument === 'function') throw new ComposerError('Invalid argument', argument) | ||
composition[arg._] = argument === undefined ? {} : argument | ||
continue | ||
case 'object': | ||
if (!isObject(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 composition | ||
} | ||
} | ||
deploy(composition, combinators) { | ||
if (arguments.length > 2) throw new ComposerError('Too many arguments') | ||
if (!(composition instanceof Composition)) throw new ComposerError('Invalid argument', composition) | ||
if (composition.type !== 'composition') throw new ComposerError('Cannot deploy anonymous composition') | ||
const obj = this.composer.encode(composition, combinators) | ||
return obj.actions.reduce((promise, action) => promise.then(() => this.actions.delete(action).catch(() => { })) | ||
.then(() => this.actions.update(action)), Promise.resolve()) | ||
.then(() => obj) | ||
} | ||
} | ||
// enhanced client-side compiler | ||
class Composer extends Compiler { | ||
// enhanced action combinator: mangle name, capture code | ||
action(name, options = {}) { | ||
if (arguments.length > 2) throw new ComposerError('Too many arguments') | ||
name = parseActionName(name) // throws ComposerError if name is not valid | ||
let exec | ||
if (Array.isArray(options.sequence)) { // native sequence | ||
exec = { kind: 'sequence', components: options.sequence.map(parseActionName) } | ||
init(combinators) | ||
// client-side stuff | ||
function client() { | ||
const os = require('os') | ||
const path = require('path') | ||
const minify = require('uglify-es').minify | ||
// read composer version number | ||
const version = require('./package.json').version | ||
// management class for compositions | ||
class Compositions { | ||
constructor(wsk, composer) { | ||
this.actions = wsk.actions | ||
this.composer = composer | ||
} | ||
if (typeof options.filename === 'string') { // read action code from file | ||
exec = fs.readFileSync(options.filename, { encoding: 'utf8' }) | ||
deploy(composition, combinators) { | ||
if (arguments.length > 2) throw new ComposerError('Too many arguments') | ||
if (!(composition instanceof Composition)) throw new ComposerError('Invalid argument', composition) | ||
if (composition.type !== 'composition') throw new ComposerError('Cannot deploy anonymous composition') | ||
const obj = this.composer.encode(composition, combinators) | ||
return obj.actions.reduce((promise, action) => promise.then(() => this.actions.delete(action).catch(() => { })) | ||
.then(() => this.actions.update(action)), Promise.resolve()) | ||
.then(() => obj) | ||
} | ||
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) | ||
} | ||
if (typeof options.action === 'string' || typeof options.action === 'object' && options.action !== null && !Array.isArray(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) | ||
} | ||
// enhanced composition combinator: mangle name | ||
composition(name, composition) { | ||
if (arguments.length > 2) throw new ComposerError('Too many arguments') | ||
if (typeof name !== 'string') throw new ComposerError('Invalid argument', name) | ||
name = parseActionName(name) | ||
return new Composition({ type: 'composition', name, composition: this.task(composition) }) | ||
} | ||
// client-side only methods | ||
Object.assign(composer, { | ||
// 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 | ||
// 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') | ||
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] | ||
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) { } | ||
} 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 | ||
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, this) | ||
return wsk | ||
} | ||
const wsk = require('openwhisk')(Object.assign({ apihost, api_key }, options)) | ||
wsk.compositions = new Compositions(wsk, this) | ||
return wsk | ||
}, | ||
// recursively encode composition into { composition, actions } by encoding nested compositions into actions and extracting nested action definitions | ||
encode(composition, combinators = []) { // lower non-primitive combinators by default | ||
if (arguments.length > 2) throw new ComposerError('Too many arguments') | ||
if (!(composition instanceof Composition)) throw new ComposerError('Invalid argument', composition) | ||
// recursively encode composition into { composition, actions } by encoding nested compositions into actions and extracting nested action definitions | ||
encode(composition, combinators = []) { // lower non-primitive combinators by default | ||
if (arguments.length > 2) throw new ComposerError('Too many arguments') | ||
if (!(composition instanceof Composition)) throw new ComposerError('Invalid argument', composition) | ||
composition = this.lower(composition, combinators) | ||
composition = this.lower(composition, combinators) | ||
const actions = [] | ||
const actions = [] | ||
const encode = composition => { | ||
composition = new Composition(composition) // copy | ||
composition.visit(encode) | ||
if (composition.type === 'composition') { | ||
const code = `// generated by composer v${version}\n\nconst composition = ${JSON.stringify(encode(composition.composition), null, 4)}\n\n// do not edit below this point\n\n${conductorCode}` // invoke conductor on composition | ||
composition.action = { exec: { kind: 'nodejs:default', code }, annotations: [{ key: 'conductor', value: composition.composition }, { key: 'composer', value: version }] } | ||
delete composition.composition | ||
composition.type = 'action' | ||
const encode = composition => { | ||
composition = new Composition(composition) // copy | ||
composition.visit(this.combinators, encode) | ||
if (composition.type === 'composition') { | ||
let code = `const main=(${main})().server(` | ||
for (let plugin of plugins) { | ||
code += `{plugin:new(${plugin.constructor})()` | ||
if (plugin.configure) code += `,config:${JSON.stringify(plugin.configure())}` | ||
code += '},' | ||
} | ||
code = minify(`${code})`, { output: { max_line_len: 127 } }).code | ||
code = `// generated by composer v${version}\n\nconst composition = ${JSON.stringify(encode(composition.composition), null, 4)}\n\n// do not edit below this point\n\n${code}` // invoke conductor on composition | ||
composition.action = { exec: { kind: 'nodejs:default', code }, annotations: [{ key: 'conductor', value: composition.composition }, { key: 'composer', value: version }] } | ||
delete composition.composition | ||
composition.type = 'action' | ||
} | ||
if (composition.type === 'action' && composition.action) { | ||
actions.push({ name: composition.name, action: composition.action }) | ||
delete composition.action | ||
} | ||
return composition | ||
} | ||
if (composition.type === 'action' && composition.action) { | ||
actions.push({ name: composition.name, action: composition.action }) | ||
delete composition.action | ||
} | ||
return composition | ||
composition = encode(composition) | ||
return { composition, actions } | ||
}, | ||
// return composer version | ||
get version() { | ||
return version | ||
} | ||
}) | ||
composition = encode(composition) | ||
return { composition, actions } | ||
} | ||
return composer | ||
} | ||
get version() { | ||
return version | ||
// server-side stuff | ||
function server() { | ||
function chain(front, back) { | ||
front.slice(-1)[0].next = 1 | ||
front.push(...back) | ||
return front | ||
} | ||
} | ||
return new Composer() | ||
} | ||
const compiler = { | ||
compile(node) { | ||
if (arguments.length === 0) return [{ type: 'empty' }] | ||
if (arguments.length === 1) return this[node.type](node) | ||
return Array.prototype.map.call(arguments, node => this.compile(node)).reduce(chain) | ||
}, | ||
module.exports = composer() | ||
sequence(node) { | ||
return chain([{ type: 'pass', path: node.path }], this.compile(...node.components)) | ||
}, | ||
// conductor action | ||
action(node) { | ||
return [{ type: 'action', name: node.name, async: node.async, path: node.path }] | ||
}, | ||
function conductor({ Compiler }) { | ||
const compiler = new Compiler() | ||
function(node) { | ||
return [{ type: 'function', exec: node.function.exec, path: node.path }] | ||
}, | ||
this.require = require | ||
finally(node) { | ||
var body = this.compile(node.body) | ||
const finalizer = this.compile(node.finalizer) | ||
var fsm = [[{ type: 'try', path: node.path }], body, [{ type: 'exit' }], finalizer].reduce(chain) | ||
fsm[0].catch = fsm.length - finalizer.length | ||
return fsm | ||
}, | ||
function chain(front, back) { | ||
front.slice(-1)[0].next = 1 | ||
front.push(...back) | ||
return front | ||
} | ||
let(node) { | ||
var body = this.compile(...node.components) | ||
return [[{ type: 'let', let: node.declarations, path: node.path }], body, [{ type: 'exit' }]].reduce(chain) | ||
}, | ||
function sequence(components) { | ||
if (components.length === 0) return [{ type: 'empty' }] | ||
return components.map(compile).reduce(chain) | ||
} | ||
mask(node) { | ||
var body = this.compile(...node.components) | ||
return [[{ type: 'let', let: null, path: node.path }], body, [{ type: 'exit' }]].reduce(chain) | ||
}, | ||
function compile(json) { | ||
const path = json.path | ||
switch (json.type) { | ||
case 'sequence': | ||
return chain([{ type: 'pass', path }], sequence(json.components)) | ||
case 'action': | ||
return [{ type: 'action', name: json.name, path }] | ||
case 'function': | ||
return [{ type: 'function', exec: json.function.exec, path }] | ||
case 'finally': | ||
var body = compile(json.body) | ||
const finalizer = compile(json.finalizer) | ||
var fsm = [[{ type: 'try', path }], body, [{ type: 'exit' }], finalizer].reduce(chain) | ||
fsm[0].catch = fsm.length - finalizer.length | ||
return fsm | ||
case 'let': | ||
var body = sequence(json.components) | ||
return [[{ type: 'let', let: json.declarations, path }], body, [{ type: 'exit' }]].reduce(chain) | ||
case 'mask': | ||
var body = sequence(json.components) | ||
return [[{ type: 'let', let: null, path }], body, [{ type: 'exit' }]].reduce(chain) | ||
case 'try': | ||
var body = compile(json.body) | ||
const handler = chain(compile(json.handler), [{ type: 'pass' }]) | ||
var fsm = [[{ type: 'try', path }], body, [{ type: 'exit' }]].reduce(chain) | ||
try(node) { | ||
var body = this.compile(node.body) | ||
const handler = chain(this.compile(node.handler), [{ type: 'pass' }]) | ||
var fsm = [[{ type: 'try', path: node.path }], body, [{ type: 'exit' }]].reduce(chain) | ||
fsm[0].catch = fsm.length | ||
@@ -531,6 +546,8 @@ fsm.slice(-1)[0].next = handler.length | ||
return fsm | ||
case 'if_nosave': | ||
var consequent = compile(json.consequent) | ||
var alternate = chain(compile(json.alternate), [{ type: 'pass' }]) | ||
var fsm = [[{ type: 'pass', path }], compile(json.test), [{ type: 'choice', then: 1, else: consequent.length + 1 }]].reduce(chain) | ||
}, | ||
if_nosave(node) { | ||
var consequent = this.compile(node.consequent) | ||
var alternate = chain(this.compile(node.alternate), [{ type: 'pass' }]) | ||
var fsm = [[{ type: 'pass', path: node.path }], this.compile(node.test), [{ type: 'choice', then: 1, else: consequent.length + 1 }]].reduce(chain) | ||
consequent.slice(-1)[0].next = alternate.length | ||
@@ -540,6 +557,8 @@ fsm.push(...consequent) | ||
return fsm | ||
case 'while_nosave': | ||
var consequent = compile(json.body) | ||
}, | ||
while_nosave(node) { | ||
var consequent = this.compile(node.body) | ||
var alternate = [{ type: 'pass' }] | ||
var fsm = [[{ type: 'pass', path }], compile(json.test), [{ type: 'choice', then: 1, else: consequent.length + 1 }]].reduce(chain) | ||
var fsm = [[{ type: 'pass', path: node.path }], this.compile(node.test), [{ type: 'choice', then: 1, else: consequent.length + 1 }]].reduce(chain) | ||
consequent.slice(-1)[0].next = 1 - fsm.length - consequent.length | ||
@@ -549,5 +568,7 @@ fsm.push(...consequent) | ||
return fsm | ||
case 'dowhile_nosave': | ||
var test = compile(json.test) | ||
var fsm = [[{ type: 'pass', path }], compile(json.body), test, [{ type: 'choice', then: 1, else: 2 }]].reduce(chain) | ||
}, | ||
dowhile_nosave(node) { | ||
var test = this.compile(node.test) | ||
var fsm = [[{ type: 'pass', path: node.path }], this.compile(node.body), test, [{ type: 'choice', then: 1, else: 2 }]].reduce(chain) | ||
fsm.slice(-1)[0].then = 1 - fsm.length | ||
@@ -558,46 +579,101 @@ fsm.slice(-1)[0].else = 1 | ||
return fsm | ||
}, | ||
} | ||
} | ||
const fsm = compile(compiler.lower(compiler.label(compiler.deserialize(composition)))) | ||
const openwhisk = require('openwhisk') | ||
let wsk | ||
const isObject = obj => typeof obj === 'object' && obj !== null && !Array.isArray(obj) | ||
const conductor = { | ||
choice({ p, node, index }) { | ||
p.s.state = index + (p.params.value ? node.then : node.else) | ||
}, | ||
// 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' | ||
}) | ||
try({ p, node, index }) { | ||
p.s.stack.unshift({ catch: index + node.catch }) | ||
}, | ||
// error status codes | ||
const badRequest = error => Promise.reject({ code: 400, error }) | ||
const internalError = error => Promise.reject(encodeError(error)) | ||
let({ p, node, index }) { | ||
p.s.stack.unshift({ let: JSON.parse(JSON.stringify(node.let)) }) | ||
}, | ||
return params => Promise.resolve().then(() => invoke(params)).catch(internalError) | ||
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() | ||
}, | ||
// do invocation | ||
function invoke(params) { | ||
// initial state and stack | ||
let state = 0 | ||
let stack = [] | ||
action({ p, node, index }) { | ||
if (node.async) { | ||
if (!wsk) wsk = openwhisk({ ignore_certs: true }) | ||
return wsk.actions.invoke({ name: node.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) | ||
}) | ||
} | ||
return { action: node.name, params: p.params, state: { $resume: p.s } } | ||
}, | ||
// restore state and stack when resuming | ||
if (params.$resume !== undefined) { | ||
if (!isObject(params.$resume)) return badRequest('The type of optional $resume parameter must be object') | ||
state = params.$resume.state | ||
stack = params.$resume.stack | ||
if (state !== undefined && typeof state !== 'number') return badRequest('The type of optional $resume.state parameter must be number') | ||
if (!Array.isArray(stack)) return badRequest('The type of $resume.stack must be an array') | ||
delete params.$resume | ||
inspect() // handle error objects when resuming | ||
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 }) { | ||
}, | ||
} | ||
const finishers = [] | ||
for ({ plugin, config } of arguments) { | ||
composer.register(plugin) | ||
if (plugin.compiler) Object.assign(compiler, plugin.compiler()) | ||
if (plugin.conductor) { | ||
const r = plugin.conductor(config) | ||
if (r._finish) { | ||
finishers.push(r._finish) | ||
delete r._finish | ||
} | ||
Object.assign(conductor, r) | ||
} | ||
} | ||
const fsm = compiler.compile(composer.lower(composer.label(composer.deserialize(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() { | ||
if (!isObject(params)) params = { value: params } | ||
if (params.error !== undefined) { | ||
params = { error: params.error } // discard all fields but the error field | ||
state = undefined // abort unless there is a handler in the stack | ||
while (stack.length > 0) { | ||
if (typeof (state = stack.shift().catch) === 'number') break | ||
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 = undefined // abort unless there is a handler in the stack | ||
while (p.s.stack.length > 0) { | ||
if (typeof (p.s.state = p.s.stack.shift().catch) === 'number') break | ||
} | ||
@@ -608,7 +684,9 @@ } | ||
// run function f on current stack | ||
function run(f) { | ||
function run(f, p) { | ||
this.require = require | ||
// handle let/mask pairs | ||
const view = [] | ||
let n = 0 | ||
for (let frame of stack) { | ||
for (let frame of p.s.stack) { | ||
if (frame.let === null) { | ||
@@ -632,3 +710,3 @@ n++ | ||
// collapse stack for invocation | ||
const env = view.reduceRight((acc, cur) => typeof cur.let === 'object' ? Object.assign(acc, cur.let) : acc, {}) | ||
const env = view.reduceRight((acc, cur) => cur.let ? Object.assign(acc, cur.let) : acc, {}) | ||
let main = '(function(){try{' | ||
@@ -640,3 +718,3 @@ for (const name in env) main += `var ${name}=arguments[1]['${name}'];` | ||
try { | ||
return (1, eval)(main)(params, env) | ||
return (1, eval)(main)(p.params, env) | ||
} finally { | ||
@@ -647,55 +725,43 @@ for (const name in env) set(name, env[name]) | ||
while (true) { | ||
function step(p) { | ||
// final state, return composition result | ||
if (state === undefined) { | ||
if (p.s.state === undefined) { | ||
console.log(`Entering final state`) | ||
console.log(JSON.stringify(params)) | ||
if (params.error) return params; else return { params } | ||
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 json = fsm[state] // json definition for current state | ||
if (json.path !== undefined) console.log(`Entering composition${json.path}`) | ||
const current = state | ||
state = json.next === undefined ? undefined : current + json.next // default next state | ||
switch (json.type) { | ||
case 'choice': | ||
state = current + (params.value ? json.then : json.else) | ||
break | ||
case 'try': | ||
stack.unshift({ catch: current + json.catch }) | ||
break | ||
case 'let': | ||
stack.unshift({ let: JSON.parse(JSON.stringify(json.let)) }) | ||
break | ||
case 'exit': | ||
if (stack.length === 0) return internalError(`State ${current} attempted to pop from an empty stack`) | ||
stack.shift() | ||
break | ||
case 'action': | ||
return { action: json.name, params, state: { $resume: { state, stack } } } // invoke continuation | ||
break | ||
case 'function': | ||
let result | ||
try { | ||
result = run(json.exec.code) | ||
} catch (error) { | ||
console.error(error) | ||
result = { error: `An exception was caught at state ${current} (see log for details)` } | ||
} | ||
if (typeof result === 'function') result = { error: `State ${current} evaluated to a function` } | ||
// if a function has only side effects and no return value, return params | ||
params = JSON.parse(JSON.stringify(result === undefined ? params : result)) | ||
inspect() | ||
break | ||
case 'empty': | ||
inspect() | ||
break | ||
case 'pass': | ||
break | ||
default: | ||
return internalError(`State ${current} has an unknown type`) | ||
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 // save current state for logging purposes | ||
p.s.state = node.next === undefined ? undefined : p.s.state + node.next // 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 (resuming) p.s.state = params.$resume.state // undef | ||
if (p.s.state !== undefined && 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 $resume.stack must be an array') | ||
delete params.$resume | ||
if (resuming) inspect(p) // handle error objects when resuming | ||
} | ||
return step(p) | ||
} | ||
} | ||
return { client, server } | ||
} | ||
module.exports = main().client() |
@@ -78,5 +78,5 @@ # Combinators | ||
``` | ||
The action may de 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 | ||
### Environment capture in actions | ||
@@ -111,3 +111,3 @@ 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: | ||
### Environment capture | ||
### Environment capture in functions | ||
@@ -114,0 +114,0 @@ 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. |
@@ -18,2 +18,4 @@ # Compose Command | ||
--deploy NAME deploy the composition with name NAME | ||
--entity NAME output the conductor action definition for the composition (giving name NAME to the composition) | ||
--entities NAME convert the composition into an array of action definition (giving name NAME to the composition) | ||
--encode output the conductor action code for the composition | ||
@@ -25,11 +27,14 @@ Flags: | ||
-i, --insecure bypass certificate checking | ||
-v, --version output the composer version | ||
``` | ||
The `compose` command requires either a Javascript file that evaluates to a composition (for example [demo.js](../samples/demo.js)) or a JSON file that encodes a composition (for example [demo.json](../samples/demo.json)). The JSON format is documented in [FORMAT.md](FORMAT.md). | ||
The `compose` command has three modes of operation: | ||
The `compose` command has several modes of operation: | ||
- By default or when the `--json` option is specified, the command returns the composition encoded as a JSON dictionary. | ||
- When the `--deploy` option is specified, the command deploys the composition given the desired name for the composition. | ||
- When the `--encode` option is specified, the command returns the Javascript code for the [conductor action](https://github.com/apache/incubator-openwhisk/blob/master/docs/conductors.md) for the composition. | ||
- When the `--entity` option is specified, the command returns the complete conductor action definition as a JSON dictionary. | ||
- When the `--entities` option is specified, the command returns an array of action definitions including not only the conductor action for the composition, but possibly also the nested action definitions. | ||
## JSON format | ||
## JSON option | ||
@@ -81,4 +86,119 @@ By default, the `compose` command evaluates the composition code and outputs the resulting JSON dictionary: | ||
## Deployment | ||
## Entity option | ||
With the `--entity` option the `compose` command returns the conductor action definition for the composition. | ||
``` | ||
compose demo.js --entity demo | ||
``` | ||
```json | ||
{ | ||
"name": "/_/demo", | ||
"action": { | ||
"exec": { | ||
"kind": "nodejs:default", | ||
"code": "..." | ||
}, | ||
"annotations": [ | ||
{ | ||
"key": "conductor", | ||
"value": { | ||
"type": "if", | ||
"test": { | ||
"type": "action", | ||
"name": "/_/authenticate" | ||
}, | ||
"consequent": { | ||
"type": "action", | ||
"name": "/_/success" | ||
}, | ||
"alternate": { | ||
"type": "action", | ||
"name": "/_/failure" | ||
} | ||
} | ||
}, | ||
{ | ||
"key": "composer", | ||
"value": "0.4.0" | ||
} | ||
] | ||
} | ||
} | ||
``` | ||
## Entities option | ||
With the `--entities` option the `compose` command returns not only the conductor action definition for the composition but also the definitions of nested actions and compositions. | ||
``` | ||
compose demo.js --entities demo | ||
``` | ||
```json | ||
[ | ||
{ | ||
"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' } }" | ||
} | ||
} | ||
}, | ||
{ | ||
"name": "/_/demo", | ||
"action": { | ||
"exec": { | ||
"kind": "nodejs:default", | ||
"code": "..." | ||
}, | ||
"annotations": [ | ||
{ | ||
"key": "conductor", | ||
"value": { | ||
"type": "if", | ||
"test": { | ||
"type": "action", | ||
"name": "/_/authenticate" | ||
}, | ||
"consequent": { | ||
"type": "action", | ||
"name": "/_/success" | ||
}, | ||
"alternate": { | ||
"type": "action", | ||
"name": "/_/failure" | ||
} | ||
} | ||
}, | ||
{ | ||
"key": "composer", | ||
"value": "0.4.0" | ||
} | ||
] | ||
} | ||
} | ||
] | ||
``` | ||
## Deploy option | ||
The `--deploy` option makes it possible to deploy a composition (Javascript or JSON) given the desired name for the composition: | ||
@@ -121,3 +241,3 @@ ``` | ||
## Code generation | ||
## Encode option | ||
@@ -124,0 +244,0 @@ The `compose` command returns the code of the conductor action for the composition (Javascript or JSON) when invoked with the `--encode` option. |
@@ -13,2 +13,3 @@ # Composer Package | ||
- [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. |
{ | ||
"name": "@ibm-functions/composer", | ||
"version": "0.4.0", | ||
"version": "0.5.0", | ||
"description": "Composer is an IBM Cloud Functions programming model for composing individual functions into larger applications.", | ||
@@ -39,3 +39,3 @@ "homepage": "https://github.com/ibm-functions/composer", | ||
"devDependencies": { | ||
"mocha": "^3.5.0" | ||
"mocha": "^5.2.0" | ||
}, | ||
@@ -42,0 +42,0 @@ "author": { |
const assert = require('assert') | ||
const composer = require('../composer') | ||
const name = 'TestAction' | ||
const compositionName = 'TestComposition' | ||
const wsk = composer.openwhisk({ ignore_certs: process.env.IGNORE_CERTS && process.env.IGNORE_CERTS !== 'false' && process.env.IGNORE_CERTS !== '0' }) | ||
@@ -34,6 +35,11 @@ | ||
it('action must return activationId', function () { | ||
return invoke(composer.action('isNotOne', { async: true }), { n: 1 }).then(activation => assert.ok(activation.response.result.activationId)) | ||
}) | ||
it('action name must parse to fully qualified', function () { | ||
let combos = [ | ||
{ n: '', s: false, e: 'Name is not specified' }, | ||
{ n: ' ', s: false, e: 'Name is not specified' }, | ||
{ n: 42, s: false, e: 'Name must be a string' }, | ||
{ n: '', s: false, e: 'Name is not valid' }, | ||
{ n: ' ', s: false, e: 'Name is not valid' }, | ||
{ n: '/', s: false, e: 'Name is not valid' }, | ||
@@ -68,5 +74,33 @@ { n: '//', s: false, e: 'Name is not valid' }, | ||
it('invalid options', function () { | ||
try { | ||
invoke(composer.action('foo', 42)) | ||
assert.fail() | ||
} catch (error) { | ||
assert.ok(error.message.startsWith('Invalid argument')) | ||
} | ||
}) | ||
it('too many arguments', function () { | ||
try { | ||
invoke(composer.action('foo', {}, 'foo')) | ||
assert.fail() | ||
} catch (error) { | ||
assert.ok(error.message.startsWith('Too many arguments')) | ||
} | ||
}) | ||
}) | ||
describe('compositions', function () { | ||
it('composition must return true', function () { | ||
return invoke(composer.composition(compositionName, composer.action('isNotOne')), { n: 0 }).then(activation => assert.deepEqual(activation.response.result, { value: true })) | ||
}) | ||
it('action must return activationId', function () { | ||
return invoke(composer.composition(compositionName, composer.action('isNotOne'), { async: true }), { n: 1 }).then(activation => assert.ok(activation.response.result.activationId)) | ||
}) | ||
it('invalid argument', function () { | ||
try { | ||
invoke(composer.function(42)) | ||
invoke(composer.composition(compositionName, 42)) | ||
assert.fail() | ||
@@ -78,5 +112,14 @@ } catch (error) { | ||
it('invalid options', function () { | ||
try { | ||
invoke(composer.composition(compositionName, 'foo', 42)) | ||
assert.fail() | ||
} catch (error) { | ||
assert.ok(error.message.startsWith('Invalid argument')) | ||
} | ||
}) | ||
it('too many arguments', function () { | ||
try { | ||
invoke(composer.function('foo', 'foo')) | ||
invoke(composer.composition(compositionName, 'foo', {}, 'foo')) | ||
assert.fail() | ||
@@ -142,2 +185,6 @@ } catch (error) { | ||
it('function may return a promise', function () { | ||
return invoke(composer.function(({ n }) => Promise.resolve(n % 2 === 0)), { n: 4 }).then(activation => assert.deepEqual(activation.response.result, { value: true })) | ||
}) | ||
it('invalid argument', function () { | ||
@@ -144,0 +191,0 @@ try { |
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
150693
17
1333