The composer module makes it possible to compose actions programmatically. The module is typically used as illustrated in samples/demo.js:
const composer = require('@openwhisk/composer') // author action composition const app = composer.if('authenticate', /* then */ 'welcome', /* else */ 'login') // compile action composition composer.compile(app, 'demo.json')
When using the programming shell fsh
however, there is no need to instantiate the module or invoke compile explicitly. The demo.js
file can be shortened to:
composer.if('authenticate', /* then */ 'welcome', /* else */ 'login')
and used as follows:
$ fsh app create demo demo.js
This example program composes actions authenticate
, welcome
, and login
using the composer.if
composition method. The composer.compile
method produces a JSON object encoding the composition and optionally writes the object to a file. The compiled composition is shown in samples/demo.json.
To deploy and run this composition and others, please refer to README.md.
All composition methods return a composition object. Composition objects must be compiled using the composer.compile
method before export or invocation. Compiled compositions objects are JSON objects that obey the specification described in FORMAT.md. They can be converted to and from strings using the JSON.stringify
and JSON.parse
methods. In contrast, the format of composition objects is not specified and may change in the future.
composer.compile(composition, filename)
compiles the composition object to its JSON representation and writes the JSON object to the file filename
if specified. It returns the compiled composition object.
Composition methods compose tasks. A task is one of the following:
Type | Description | Examples |
---|---|---|
function task | a JavaScript function expression | params => params or function (params) { return params } |
action task | an OpenWhisk action | '/whisk.system/utils/echo' |
composition | a composition | composer.retry(3, 'connect') |
compiled composition | a compiled composition | composer.compile(composer.retry(3, 'connect')) |
Function expressions occurring in action 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 composition method as discussed below.
Actions are specified by name. Fully qualified names and aliases are supported following the usual conventions.
A function task applies the function to the parameter object. An action task the invokes the action with the given name on the parameter object. A composition task or compiled composition task applies the composition to the parameter object.
A task is a function that consumes a JSON dictionary (the input parameter object) and produces a JSON dictionary (the output parameter object). An output parameter object of a task with an error
field is an error object. A task fails if it produces an error object.
Values returned by constant and function tasks are converted to JSON using JSON.stringify
followed by JSON.parse
. Values other than JSON dictionaries are replaced with a dictionary with a unique value
field with the converted value. For instance, the task 42
outputs the JSON dictionary { value: 42 }
.
By convention, an error object returned by a task is stripped from all fields except from the error
field. For instance, the task () => ({ error: 'KO', message: 'OK' })
outputs the JSON dictionary { error: 'KO' }
. This is to be consistent with the OpenWhisk action semantics, e.g., the action with code function main() { return { error: 'KO', message: 'OK' } }
outputs { error: 'KO' }
.
The following composition methods are currently supported:
Composition | Description | Example |
---|---|---|
task | single task | composer.task('sayHi', { input: 'userInfo' }) |
value | constant value | composer.value({ message: 'Hello World!' }) |
sequence | sequence | composer.sequence('getLocation', 'getWeatherForLocation') |
let | variables | composer.let('n', 42, ...) |
if | conditional | composer.if('authenticate', /* then */ 'welcome', /* else */ 'login') |
while | loop | composer.while('needMoreData', 'fetchMoreData') |
try | error handling | try('DivideByN', /* catch */ 'NaN') |
repeat | repetition | repeat(42, 'sayHi') |
retry | error recovery | retry(3, 'connect') |
retain | parameter retention | composer.retain('validateInput') |
composer.task(task, options)
is a composition with a single task task. The optional options parameter may alter the task behavior as follows:
If options.merge evaluates to true the output parameter object for the composition is obtained by merging the output parameter object for the task into the input parameter object (unless the task produces an error object).
For instance, the composition composer.task(42, { merge: true })
invoked on the input parameter object { value: 0, message: 'OK' }
outputs { value: 42, message: 'OK' }
.
Alternatively if options.output is defined the output parameter object for the composition is obtained by assigning the output parameter object for the task to the options.output field of the input parameter object for the composition. Additionally, if options.input is defined the input parameter for the task is only the value of the field options.input of the input parameter object for the composition as opposed to the full input parameter object.
For instance, the composition composer.task(({n}) => ({ n: n+1 }), { input: 'in', output: 'out' })
invoked on the input parameter object { in: { n: 42 } }
outputs { in: { n: 42 }, out: { n: 43 } }
.
If the value of the options.input field is not a JSON dictionary is it replaced with a dictionary with a unique field value
with the field's value.
composer.value(json)
outputs json if it is a JSON dictionary. If json is not a JSON dictionary is it replaced with a dictionary with a unique field value
with value json.
The json value may be computed at composition time. For instance, the following composition captures the composition time:
composer.value(Date())
composer.sequence(task_1, task_2, ...)
runs a sequence of tasks (possibly empty).
The input parameter object for the composition is the input parameter object of the first task in the sequence. The output parameter object of one task in the sequence is the input parameter object for the next task in the sequence. The output parameter object of the last task in the sequence is the output parameter object for the composition.
If one of the tasks fails, the remainder of the sequence is not executed. The output parameter object for the composition is the error object produced by the failed task.
An empty sequence behaves as a sequence with a single function task 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.let(name, value, task_1_, _task_2_, ...)
declares a new variable with name name and initial value value and runs a sequence of tasks in the scope of this definition.
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. 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 task task
repeatedly n
times.
composer.let('i', n, composer.while(() => i-- > 0, task))
Observe the first argument to the let
composition is the quoted variable name 'i'
, whereas occurrences of the variable in the let
body are not quoted. We recommend expanding let
compositions as follows to avoid confusing code editors:
let i = 'i'; composer.let(i, n, composer.while(() => i-- > 0, task))
Variables declared with composer.let
are not visible to invoked actions. However, they may be passed as parameters to actions as for instance:
let n = 'n'; composer.let(n, 42, () => ({n}), '/whisk.system/utils/echo', (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 n
field of the output parameter object is assigned back to variable n
.
composer.if(condition, consequent, alternate)
runs either the consequent task if the condition evaluates to true or the alternate task if not. The condition task and consequent task or alternate task are all invoked on the input parameter object for the composition. The output parameter object of the condition task is discarded.
A condition task evaluates to true if and only if it produces a JSON dictionary with a field value
with the value true
(i.e., JSON's true
value). 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 task). One should write instead: (params) => params.n > 0
.
The alternate task may be omitted.
If condition fails, neither branch is executed.
For examples, the following composition divides parameter n
by two if it is even.
composer.if(({n}) => n % 2 == 0, ({n}) => ({ n: n / 2 }))
composer.while(condition, task)
runs task repeatedly while condition evaluates to true. The condition task is evaluated before any execution of task. See composer.if for a discussion of conditions.
A failure of condition or task interrupts the execution.
composer.try(task, handler)
runs task with error handler handler.
A task failure triggers the execution of handler with the error object as its input parameter object.
composer.repeat(count, task)
runs task count times. It is equivalent to a sequence with count task(s).
composer.retry(count, task)
runs task and retries task up to count times if it fails. The output parameter object for the composition is either the output parameter object of the successful task invocation or the error object produced by the last task invocation.
composer.retain(task[, flag])
runs task 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 task.
If task fails and flag is true, then the output parameter object for the composition is the combination of the input parameters with the error object. If task fails and flag is false or absent, then the output parameter object for the composition is only the error object.