Skip to content
This repository was archived by the owner on Jan 17, 2025. It is now read-only.

Commit 2ff54e4

Browse files
authored
Add documentation and examples (#51)
1 parent e93d30b commit 2ff54e4

File tree

11 files changed

+470
-386
lines changed

11 files changed

+470
-386
lines changed

README.md

+7-7
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ tool called [IBM Cloud Shell](https://github.com/ibm-functions/shell), or just
1818
_Shell_. Shell offers a CLI and graphical interface for fast, incremental,
1919
iterative, and local development of serverless applications. While we recommend
2020
using Shell, Shell is not required to work with compositions. Compositions may
21-
be managed using a combination of the Composer [compose](bin/compose) command
21+
be managed using a combination of the Composer [compose](docs/COMPOSE.md) command
2222
(for deployment) and the [OpenWhisk
2323
CLI](https://console.bluemix.net/openwhisk/learn/cli) (for configuration,
2424
invocation, and life-cycle management).
@@ -31,9 +31,9 @@ of an action (e.g., default parameters, limits, blocking invocation, web
3131
export).
3232

3333
This repository includes:
34-
* the [composer](composer.js) Node.js module for authoring compositions using
34+
* the [composer](docs/COMPOSER.md) Node.js module for authoring compositions using
3535
JavaScript,
36-
* the [compose](bin/compose) command for deploying compositions,
36+
* the [compose](docs/COMPOSE.md) command for deploying compositions,
3737
* [documentation](docs), [examples](samples), and [tests](test).
3838

3939
Composer and Shell are currently available as _IBM Research previews_. As
@@ -60,9 +60,9 @@ A composition is typically defined by means of a Javascript expression as
6060
illustrated in [samples/demo.js](samples/demo.js):
6161
```javascript
6262
composer.if(
63-
composer.action('authenticate', { action: function main({ password }) { return { value: password === 'abc123' } } }),
64-
composer.action('success', { action: function main() { return { message: 'success' } } }),
65-
composer.action('failure', { action: function main() { return { message: 'failure' } } }))
63+
composer.action('authenticate', { action: function ({ password }) { return { value: password === 'abc123' } } }),
64+
composer.action('success', { action: function () { return { message: 'success' } } }),
65+
composer.action('failure', { action: function () { return { message: 'failure' } } }))
6666
```
6767
Compositions compose actions using _combinator_ methods. These methods
6868
implement the typical control-flow constructs of a sequential imperative
@@ -81,7 +81,7 @@ composer.if('authenticate', 'success', 'failure')
8181

8282
## Deploying a composition
8383

84-
One way to deploy a composition is to use the `compose` command:
84+
One way to deploy a composition is to use the [compose](docs/COMPOSE.md) command:
8585
```
8686
compose demo.js --deploy demo
8787
```

composer.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,6 @@ class Composition {
109109
if (arguments.length > 1) throw new ComposerError('Too many arguments')
110110
if (typeof name !== 'undefined' && typeof name !== 'string') throw new ComposerError('Invalid argument', name)
111111
const obj = typeof name === 'string' ? this.named(name) : this
112-
if (obj.composition.length !== 1 || obj.composition[0].type !== 'action') throw new ComposerError('Cannot encode anonymous composition')
113112
return new Composition(obj.composition, null, obj.actions.map(encode))
114113
}
115114
}
@@ -123,6 +122,7 @@ class Compositions {
123122
if (arguments.length > 2) throw new ComposerError('Too many arguments')
124123
if (!(composition instanceof Composition)) throw new ComposerError('Invalid argument', composition)
125124
const obj = composition.encode(name)
125+
if (obj.composition.length !== 1 || obj.composition[0].type !== 'action') throw new ComposerError('Cannot deploy anonymous composition')
126126
return obj.actions.reduce((promise, action) => promise.then(() => this.actions.delete(action).catch(() => { }))
127127
.then(() => this.actions.update(action)), Promise.resolve())
128128
}

docs/COMBINATORS.md

+220
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,220 @@
1+
# Combinators
2+
3+
The `composer` module offers a number of combinators to define compositions:
4+
5+
| Combinator | Description | Example |
6+
| --:| --- | --- |
7+
| [`action`](#action) | action | `composer.action('echo')` |
8+
| [`function`](#function) | function | `composer.function(({ x, y }) => ({ product: x * y }))` |
9+
| [`literal` or `value`](#literal) | constant value | `composer.literal({ message: 'Hello, World!' })` |
10+
| [`sequence` or `seq`](#sequence) | sequence | `composer.sequence('hello', 'bye')` |
11+
| [`let`](#let) | variable declarations | `composer.let({ count: 3, message: 'hello' }, ...)` |
12+
| [`if`](#if) | conditional | `composer.if('authenticate', 'success', 'failure')` |
13+
| [`while`](#while) | loop | `composer.while('notEnough', 'doMore')` |
14+
| [`dowhile`](#dowhile) | loop at least once | `composer.dowhile('fetchData', 'needMoreData')` |
15+
| [`repeat`](#repeat) | counted loop | `composer.repeat(3, 'hello')` |
16+
| [`try`](#try) | error handling | `composer.try('divideByN', 'NaN')` |
17+
| [`finally`](#finally) | finalization | `composer.finally('tryThis', 'doThatAlways')` |
18+
| [`retry`](#retry) | error recovery | `composer.retry(3, 'connect')` |
19+
| [`retain`](#retain) | persistence | `composer.retain('validateInput')` |
20+
21+
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.
22+
23+
## Shorthands
24+
25+
Where a composition is expected, the following shorthands are permitted:
26+
- `name` of type `string` stands for `composer.action(name)`,
27+
- `fun` of type `function` stands for `composer.function(fun)`,
28+
- `null` stands for the empty sequence `composer.sequence()`.
29+
30+
## Action
31+
32+
`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.
33+
34+
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.
35+
36+
Examples:
37+
```javascript
38+
composer.action('hello')
39+
composer.action('myPackage/myAction')
40+
composer.action('/whisk.system/utils/echo')
41+
```
42+
The optional `options` dictionary makes it possible to provide a definition for the action being composed.
43+
```javascript
44+
// specify the code for the action as a function
45+
composer.action('hello', { action: function () { return { message: 'hello' } } })
46+
47+
// specify the code for the action as a function reference
48+
function hello() {
49+
return { message: 'hello' }
50+
}
51+
composer.action('hello', { action: hello })
52+
53+
// specify the code for the action as a string
54+
composer.action('hello', { action: "const message = 'hello'; function main() { return { message } }" })
55+
56+
57+
// specify the code and runtime for the action
58+
composer.action('hello', {
59+
action: {
60+
kind: 'nodejs:8',
61+
code: "function () { return { message: 'hello' } }"
62+
}
63+
})
64+
65+
// specify a file containing the code for the action
66+
composer.action('hello', { filename: 'hello.js' })
67+
68+
// specify a sequence of actions
69+
composer.action('helloAndBye', { sequence: ['hello', 'bye'] })
70+
```
71+
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.
72+
73+
### Environment capture
74+
75+
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:
76+
```javascript
77+
let name = 'Dave'
78+
composer.action('hello', { action: function main() { return { message: 'Hello ' + name } } })
79+
```
80+
In contrast, the following code is correct as it resolves `name`'s value at composition time.
81+
```javascript
82+
let name = 'Dave'
83+
composer.action('hello', { action: `function main() { return { message: 'Hello ' + '${name}' } }` })
84+
```
85+
86+
## Function
87+
88+
`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.
89+
- If the function returns a value of type `function`, the composition returns an error object.
90+
- If the function throws an exception, the composition returns an error object. The exception is logged as part of the conductor action invocation.
91+
- 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.
92+
- 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`.
93+
94+
Examples:
95+
```javascript
96+
composer.function(params => ({ message: 'Hello ' + params.name }))
97+
composer.function(function () { return { error: 'error' } })
98+
99+
function product({ x, y }) { return { product: x * y } }
100+
composer.function(product)
101+
```
102+
103+
### Environment capture
104+
105+
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.
106+
107+
The following is not legal:
108+
```javascript
109+
let name = 'Dave'
110+
composer.function(params => ({ message: 'Hello ' + name }))
111+
```
112+
The following is legal:
113+
```javascript
114+
composer.let({ name: 'Dave' }, composer.function(params => ({ message: 'Hello ' + name })))
115+
```
116+
117+
## Literal
118+
119+
`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.
120+
121+
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:
122+
```javascript
123+
composer.sequence(
124+
composer.literal(Date()),
125+
composer.action('log', { action: params => ({ message: 'Composition time: ' + params.value }) }))
126+
```
127+
128+
## Sequence
129+
130+
`composer.sequence(composition_1, composition_2, ...)` chains a series of compositions (possibly empty).
131+
132+
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.
133+
134+
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.
135+
136+
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.
137+
138+
## Let
139+
140+
`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.
141+
142+
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.
143+
144+
For example, the following composition invokes composition `composition` repeatedly `n` times.
145+
```javascript
146+
composer.let({ i: n }, composer.while(() => i-- > 0, composition))
147+
```
148+
Variables declared with `composer.let` are not visible to invoked actions. However, they may be passed as parameters to actions as for instance in:
149+
```javascript
150+
composer.let({ n: 42 }, () => ({ n }), 'increment', params => { n = params.n })
151+
```
152+
153+
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`.
154+
155+
## If
156+
157+
`composer.if(condition, consequent, [alternate], [options])` runs either the _consequent_ composition if the _condition_ evaluates to true or the _alternate_ composition if not.
158+
159+
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.
160+
161+
The _alternate_ composition may be omitted. If _condition_ fails, neither branch is executed.
162+
163+
The optional `options` dictionary supports a `nosave` option. If `options.nosave` is thruthy, the _consequent_ composition or _alternate_ composition is invoked on the output parameter object of the _condition_ composition. Otherwise, the output parameter object of the _condition_ composition is discarded and the _consequent_ composition or _alternate_ composition is invoked on the input parameter object for the composition. For example, the following compositions divide parameter `n` by two if `n` is even:
164+
```javascript
165+
composer.if(params => params.n % 2 === 0, params => { params.n /= 2 })
166+
composer.if(params => { params.value = params.n % 2 === 0 }, params => { params.n /= 2 }, null, { nosave: true })
167+
```
168+
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.
169+
170+
While, the default `nosave == false` behavior 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 `nosave` option omits the parameter save, hence preserving the parameter size limit.
171+
172+
## While
173+
174+
`composer.while(condition, body, [options])` 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.
175+
176+
A failure of _condition_ or _body_ interrupts the execution. The composition returns the error object from the failed component.
177+
178+
Like `composer.if`, `composer.while` supports a `nosave` option. By default, 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 `options.nosave` is thruthy, 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.
179+
180+
For instance, the following composition invoked on dictionary `{ n: 28 }` returns `{ n: 7 }`:
181+
```javascript
182+
composer.while(params => params.n % 2 === 0, params => { params.n /= 2 })
183+
```
184+
For instance, the following composition invoked on dictionary `{ n: 28 }` returns `{ n: 7, value: false }`:
185+
```javascript
186+
composer.while(params => { params.value = params.n % 2 === 0 }, params => { params.n /= 2 }, { nosave: true })
187+
```
188+
189+
## Dowhile
190+
191+
`composer.dowhile(condition, body, [options])` is similar to `composer.while(body, condition, [options])` except that _body_ is invoked before _condition_ is evaluated, hence _body_ is always invoked at least once.
192+
193+
## Repeat
194+
195+
`composer.repeat(count, body)` invokes _body_ _count_ times.
196+
197+
## Try
198+
199+
`composer.try(body, handler)` runs _body_ with error handler _handler_.
200+
201+
If _body_ returns an error object, _handler_ is invoked with this error object as its input parameter object. Otherwise, _handler_ is not run.
202+
203+
## Finally
204+
205+
`composer.finally(body, finalizer)` runs _body_ and then _finalizer_.
206+
207+
The _finalizer_ is invoked in sequence after _body_ even if _body_ returns an error object.
208+
209+
## Retry
210+
211+
`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.
212+
213+
## Retain
214+
215+
`composer.retain(body, [options])` 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_.
216+
217+
An `options` dictionary object may be specified to alter the default behavior of `composer.retain` in the following ways:
218+
- If `options.catch` is thruthy, the `retain` combinator behavior will be the same even if _body_ returns an error object. Otherwise, if _body_ fails, the output of the `retain` combinator is only the error object (i.e., the input parameter object is not preserved).
219+
- If `options.filter` is a function, the combinator only persists the result of the function application to the input parameter object.
220+
- If `options.field` is a string, the combinator only persists the value of the field of the input parameter object with the given name.

0 commit comments

Comments
 (0)