Composer makes it easy to define new composition templates. In essence, a composition template is just a composition with one or more variable arguments. In this document, we discuss key concepts of composition templates and describe a few useful templates. To use any of these templates, simply paste the template code into your composition file.
The following composition templates invokes composition composition
and returns the input dictionary augmented with the fields of the output dictionary, overwriting existing fields on name clashes:
function merge(composition) { return composer.seq(composer.retain(composition), ({ params, result }) => Object.assign(params, result)) }
To instantiate a composition template, simply apply the Javascript function to concrete arguments as in:
merge(({ n }) => ({ nPlusOne: n + 1 }))
For example, invoking this composition on input { n: 42 }
outputs { n: 42, nPlusOne: 43 }
.
The composer
object itself may be extended with the new template using monkey patching:
composer.merge = composition => composition.seq(composer.retain(composition), ({ params, result }) => Object.assign(params, result))
Many predefined combinators are actually templates over more primitive combinators. For instance the retain
combinator is essentially defined as follows:
function retain(composition) { return composer.let( { params: null }, args => { params = args }, composer.mask(composition), result => ({ params, result })) }
This implementation first declares a variable named params
using composer.let
. It then saves the input dictionary by assigning it to params
. Next it runs composition
. It produces the final result by combining the output dictionary (bound to result
) with the input dictionary (bound to params
).
It is important to notice the use of the mask
combinator here. Since this implementation introduces a variable named params
and invokes the parameter composition
in the scope of the params
variable, this variable declaration may clash with another declaration of params
in the user code. By wrapping the composition
invocation with mask
we ensure that the params
variable declared in this template is hidden from composition
. Thanks to mask
, the following composition correctly writes 'Hi there!'
to the standard output:
composer.let({ params: 'Hi there!' }, retain(() => { console.log(params) }))
Because composer
variables are dynamically scoped, without the mask
combinator, the action log would show the value of the params
variable declared in the template instead of the expected value.
The composer
module does not support parallel execution at this time but we can fake it as follows:
composer.par = (f, g) => composer.let( { input: null, left: null }, args => { input = args }, composer.mask(f), args => { left = args; return input }, composer.mask(g), right => ({ left, right }))
This code pretends to execute compositions f
and g
in parallel. They both receive the same input dictionary. The output dictionary for the composition has two fields: left
carries the result of f
and right
carries the result of g
. In fact the two composition are executed in sequence but let
, mask
, and a few Javascript function can flow the data as if running in parallel.
This implementation does not handle exceptions in f
or g
. This is left as an exercise to the reader.
This apply
combinator makes it possible to invoke a composition
on a field
of the input dictionary, leaving other fields unchanged.
// example.js composer.apply = (field, composition) => composer.let( { field }, composer.retain(p => p[field], composer.mask(composition)), p => { p.params[field] = p.result; return p.params }) composer.apply('payload', p => { p.n++ })
compose example.js --deploy example ok: created action /_/example
wsk action invoke example -r -p payload '{"n":1,"p":42}' { "payload": { "n": 2, "p": 42 } }
In this example, the let
combinator is used to capture the desired field name: it binds the field
variable to the field name.
This forward
combinator excludes the specified fields
from the input dictionary for composition
and restores them afterwards. It is useful for instance to hide secrets from a composition.
composer.forward = (fields, composition) => composer.let( { fields }, composer.retain(p => require('lodash').omit(p, ...fields), composer.mask(composition)), ({ params, result }) => Object.assign(result, require('lodash').pick(params, ...fields))) composer.forward(['user', 'password'], untrustedComposition)
The following combinators make it possible to bind a field of the parameter object to the value of the homonymous variable and vice versa.
composer.inject = v => composer.seq(composer.let({ v }, params => { params[v] = eval(v) })) composer.extract = v => composer.seq(composer.let({ v }, params => { eval(`${v} = params[v]`); delete params[v] })) composer.let({ token: null }, 'getSecretToken', composer.extract('token'), untrustedAction, composer.inject('token'), trustedAction)