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

Commit ed4e6ee

Browse files
committed
Initial commit
0 parents  commit ed4e6ee

23 files changed

+2424
-0
lines changed

.travis.yml

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
language: node_js
2+
node_js:
3+
- "6"
4+
5+
env:
6+
global:
7+
# - __OW_API_HOST="OpenWhisk host"
8+
# - __OW_API_KEY="OpenWhisk API key"
9+
# - REDIS="Redis url"

README.md

+45
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
Composer is a new programming model from [IBM
2+
Research](https://ibm.biz/serverless-research) for composing [IBM
3+
Cloud Functions](https://ibm.biz/openwhisk), built on [Apache
4+
OpenWhisk](https://github.com/apache/incubator-openwhisk). Composer
5+
extends Functions and sequences with more powerful control flow and
6+
automatic state management. With it, developers can build even more
7+
serverless applications including using it for IoT, with workflow
8+
orchestration, conversation services, and devops automation, to name a
9+
few examples.
10+
11+
Composer helps you express cloud-native apps that are serverless by
12+
construction: scale automatically, and pay as you go and not for idle
13+
time. Programming compositions for IBM Cloud Functions is done via the
14+
[functions shell](https://github.com/ibm-functions/shell), which
15+
offers a CLI and graphical interface for fast, incremental, iterative,
16+
and local development of serverless apps. Some additional highlights
17+
of the shell include:
18+
19+
* Edit your code and program using your favorite text editor, rather than using a drag-n-drop UI
20+
* Validate your compositions with readily accessible visualizations, without switching tools or using a browser
21+
* Deploy and invoke compositions using familiar CLI commands
22+
* Debug your invocations with either familiar CLI commands or readily accessible visualizations
23+
24+
Composer and shell are currently available as IBM Research
25+
previews. We are excited about both and are looking forward to what
26+
compositions you build and run using [IBM Cloud
27+
Functions](https://ibm.biz/openwhisk) or directly on [Apache
28+
OpenWhisk](https://github.com/apache/incubator-openwhisk).
29+
30+
We welcome your feedback and criticism. Find bugs and we will squash
31+
them. And will be grateful for your help. As an early adopter, you
32+
will also be among the first to experience even more features planned
33+
for the weeks ahead. We look forward to your feedback and encourage
34+
you to [join us on slack](http://ibm.biz/composer-users).
35+
36+
This repository includes:
37+
38+
* [tutorial](docs) for getting started with Composer in the [docs](docs) folder,
39+
* [composer](composer.js) node.js module to author compositions using JavaScript,
40+
* [conductor](conductor.js) action code to orchestrate the execution of compositions,
41+
* [manager](manager.js) node.js module to query the state of compositions,
42+
* [test-harness](test-harness.js) helper module for testing composer,
43+
* [redis-promise](redis-promise.js) helper module that implements a promisified redis client for node.js,
44+
* example compositions in the [samples](samples) folder,
45+
* unit tests in the [test](test) folder.

composer.js

+280
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,280 @@
1+
/*
2+
* Copyright 2017 IBM Corporation
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
'use strict'
18+
19+
const clone = require('clone')
20+
const util = require('util')
21+
const fs = require('fs')
22+
23+
class ComposerError extends Error {
24+
constructor(message, cause) {
25+
super(message)
26+
const index = this.stack.indexOf('\n')
27+
this.stack = this.stack.substring(0, index) + '\nCause: ' + util.inspect(cause) + this.stack.substring(index)
28+
}
29+
}
30+
31+
function chain(front, back) {
32+
front.States.push(...back.States)
33+
front.Exit.Next = back.Entry
34+
front.Exit = back.Exit
35+
return front
36+
}
37+
38+
function push(id) {
39+
const Entry = { Type: 'Push', id }
40+
return { Entry, States: [Entry], Exit: Entry }
41+
}
42+
43+
function pop(id) {
44+
const Entry = { Type: 'Pop', id }
45+
return { Entry, States: [Entry], Exit: Entry }
46+
}
47+
48+
function begin(id, symbol, value) {
49+
const Entry = { Type: 'Let', Symbol: symbol, Value: value, id }
50+
return { Entry, States: [Entry], Exit: Entry }
51+
}
52+
53+
function end(id) {
54+
const Entry = { Type: 'End', id }
55+
return { Entry, States: [Entry], Exit: Entry }
56+
}
57+
58+
const isObject = obj => typeof (obj) === 'object' && obj !== null && !Array.isArray(obj)
59+
60+
class Composer {
61+
task(obj, options) {
62+
if (options != null && options.output) return this.assign(options.output, obj, options.input)
63+
if (options != null && options.merge) return this.sequence(this.retain(obj), ({ params, result }) => Object.assign({}, params, result))
64+
const id = {}
65+
let Entry
66+
if (obj == null) { // identity function (must throw errors if any)
67+
Entry = { Type: 'Task', Helper: 'null', Function: 'params => params', id }
68+
} else if (typeof obj === 'object' && typeof obj.Entry === 'object' && Array.isArray(obj.States) && typeof obj.Exit === 'object') { // an action composition
69+
return clone(obj)
70+
} else if (typeof obj === 'object' && typeof obj.Entry === 'string' && typeof obj.States === 'object' && typeof obj.Exit === 'string') { // a compiled composition
71+
return this.decompile(obj)
72+
} else if (typeof obj === 'function') { // function
73+
Entry = { Type: 'Task', Function: obj.toString(), id }
74+
} else if (typeof obj === 'string') { // action
75+
Entry = { Type: 'Task', Action: obj, id }
76+
} else if (typeof obj === 'object' && typeof obj.Helper !== 'undefined' && typeof obj.Function === 'string') { //helper function
77+
Entry = { Type: 'Task', Function: obj.Function, Helper: obj.Helper, id }
78+
} else { // error
79+
throw new ComposerError('Invalid composition argument', obj)
80+
}
81+
return { Entry, States: [Entry], Exit: Entry }
82+
}
83+
84+
sequence() {
85+
if (arguments.length == 0) return this.task()
86+
return Array.prototype.map.call(arguments, x => this.task(x), this).reduce(chain)
87+
}
88+
89+
if(test, consequent, alternate) {
90+
if (test == null || consequent == null) throw new ComposerError('Missing arguments in composition', arguments)
91+
const id = {}
92+
test = chain(push(id), this.task(test))
93+
consequent = this.task(consequent)
94+
alternate = this.task(alternate)
95+
const Exit = { Type: 'Pass', id }
96+
const choice = { Type: 'Choice', Then: consequent.Entry, Else: alternate.Entry, id }
97+
test.States.push(choice)
98+
test.States.push(...consequent.States)
99+
test.States.push(...alternate.States)
100+
test.Exit.Next = choice
101+
consequent.Exit.Next = Exit
102+
alternate.Exit.Next = Exit
103+
test.States.push(Exit)
104+
test.Exit = Exit
105+
return test
106+
}
107+
108+
while(test, body) {
109+
if (test == null || body == null) throw new ComposerError('Missing arguments in composition', arguments)
110+
const id = {}
111+
test = chain(push(id), this.task(test))
112+
body = this.task(body)
113+
const Exit = { Type: 'Pass', id }
114+
const choice = { Type: 'Choice', Then: body.Entry, Else: Exit, id }
115+
test.States.push(choice)
116+
test.States.push(...body.States)
117+
test.Exit.Next = choice
118+
body.Exit.Next = test.Entry
119+
test.States.push(Exit)
120+
test.Exit = Exit
121+
return test
122+
}
123+
124+
try(body, handler) {
125+
if (body == null || handler == null) throw new ComposerError('Missing arguments in composition', arguments)
126+
const id = {}
127+
body = this.task(body)
128+
handler = this.task(handler)
129+
const Exit = { Type: 'Pass', id }
130+
const Entry = { Type: 'Try', Next: body.Entry, Handler: handler.Entry, id }
131+
const pop = { Type: 'Catch', Next: Exit, id }
132+
const States = [Entry]
133+
States.push(...body.States, pop, ...handler.States, Exit)
134+
body.Exit.Next = pop
135+
handler.Exit.Next = Exit
136+
return { Entry, States, Exit }
137+
}
138+
139+
retain(body, flag = false) {
140+
if (body == null) throw new ComposerError('Missing arguments in composition', arguments)
141+
if (typeof flag !== 'boolean') throw new ComposerError('Invalid retain flag', flag)
142+
143+
const id = {}
144+
if (!flag) return chain(push(id), chain(this.task(body), pop(id)))
145+
146+
let helperFunc_1 = { 'Helper': 'retain_1', 'Function': 'params => ({params})' }
147+
let helperFunc_3 = { 'Helper': 'retain_3', 'Function': 'params => ({params})' }
148+
let helperFunc_2 = { 'Helper': 'retain_2', 'Function': 'params => ({ params: params.params, result: params.result.params })' }
149+
150+
return this.sequence(
151+
this.retain(
152+
this.try(
153+
this.sequence(
154+
body,
155+
helperFunc_1
156+
),
157+
helperFunc_3
158+
)
159+
),
160+
helperFunc_2
161+
)
162+
}
163+
164+
assign(dest, body, source, flag = false) {
165+
if (dest == null || body == null) throw new ComposerError('Missing arguments in composition', arguments)
166+
if (typeof flag !== 'boolean') throw new ComposerError('Invalid assign flag', flag)
167+
168+
let helperFunc_1 = { 'Helper': 'assign_1', 'Function': 'params => params[source]' };
169+
let helperFunc_2 = { 'Helper': 'assign_2', 'Function': 'params => { params.params[dest] = params.result; return params.params }' };
170+
171+
const t = source ? this.let('source', source, this.retain(this.sequence(helperFunc_1, body), flag)) : this.retain(body, flag)
172+
return this.let('dest', dest, t, helperFunc_2)
173+
}
174+
175+
let(arg1, arg2) {
176+
if (arg1 == null) throw new ComposerError('Missing arguments in composition', arguments)
177+
if (typeof arg1 === 'string') {
178+
const id = {}
179+
return chain(begin(id, arg1, arg2), chain(this.sequence(...Array.prototype.slice.call(arguments, 2)), end(id)))
180+
} else if (isObject(arg1)) {
181+
const enter = []
182+
const exit = []
183+
for (const name in arg1) {
184+
const id = {}
185+
enter.push(begin(id, name, arg1[name]))
186+
exit.unshift(end(id))
187+
}
188+
if (enter.length == 0) return this.sequence(...Array.prototype.slice.call(arguments, 1))
189+
return chain(enter.reduce(chain), chain(this.sequence(...Array.prototype.slice.call(arguments, 1)), exit.reduce(chain)))
190+
} else {
191+
throw new ComposerError('Invalid first let argument', arg1)
192+
}
193+
}
194+
195+
retry(count, body) {
196+
if (body == null) throw new ComposerError('Missing arguments in composition', arguments)
197+
if (typeof count !== 'number') throw new ComposerError('Invalid retry count', count)
198+
199+
let helperFunc_1 = { 'Helper': 'retry_1', 'Function': "params => typeof params.result.error !== 'undefined' && count-- > 0" }
200+
let helperFunc_2 = { 'Helper': 'retry_2', 'Function': 'params => params.params' }
201+
let helperFunc_3 = { 'Helper': 'retry_3', 'Function': 'params => params.result' }
202+
203+
return this.let('count', count,
204+
this.retain(body, true),
205+
this.while(
206+
helperFunc_1,
207+
this.sequence(helperFunc_2, this.retain(body, true))),
208+
helperFunc_3)
209+
}
210+
211+
repeat(count, body) {
212+
if (body == null) throw new ComposerError('Missing arguments in composition', arguments)
213+
if (typeof count !== 'number') throw new ComposerError('Invalid repeat count', count)
214+
215+
let helperFunc_1 = { 'Helper': 'repeat_1', 'Function': '() => count-- > 0' }
216+
return this.let('count', count, this.while(helperFunc_1, body))
217+
}
218+
219+
value(json) {
220+
const id = {}
221+
if (typeof json === 'function') throw new ComposerError('Value cannot be a function', json.toString())
222+
const Entry = { Type: 'Task', Value: typeof json === 'undefined' ? {} : json, id }
223+
return { Entry, States: [Entry], Exit: Entry }
224+
}
225+
226+
compile(obj, filename) {
227+
if (typeof obj !== 'object' || typeof obj.Entry !== 'object' || !Array.isArray(obj.States) || typeof obj.Exit !== 'object') {
228+
throw new ComposerError('Invalid argument to compile', obj)
229+
}
230+
obj = clone(obj)
231+
const States = {}
232+
let Entry
233+
let Exit
234+
let Count = 0
235+
obj.States.forEach(state => {
236+
if (typeof state.id.id === 'undefined') state.id.id = Count++
237+
})
238+
obj.States.forEach(state => {
239+
const id = (state.Type === 'Task' ? state.Action && 'action' || state.Function && 'function' || state.Value && 'value' : state.Type.toLowerCase()) + '_' + state.id.id
240+
States[id] = state
241+
state.id = id
242+
if (state === obj.Entry) Entry = id
243+
if (state === obj.Exit) Exit = id
244+
})
245+
obj.States.forEach(state => {
246+
if (state.Next) state.Next = state.Next.id
247+
if (state.Then) state.Then = state.Then.id
248+
if (state.Else) state.Else = state.Else.id
249+
if (state.Handler) state.Handler = state.Handler.id
250+
})
251+
obj.States.forEach(state => {
252+
delete state.id
253+
})
254+
const app = { Entry, States, Exit }
255+
if (filename) fs.writeFileSync(filename, JSON.stringify(app, null, 4), { encoding: 'utf8' })
256+
return app
257+
}
258+
259+
decompile(obj) {
260+
if (typeof obj !== 'object' || typeof obj.Entry !== 'string' || typeof obj.States !== 'object' || typeof obj.Exit !== 'string') {
261+
throw new ComposerError('Invalid argument to decompile', obj)
262+
}
263+
obj = clone(obj)
264+
const States = []
265+
const ids = []
266+
for (const name in obj.States) {
267+
const state = obj.States[name]
268+
if (state.Next) state.Next = obj.States[state.Next]
269+
if (state.Then) state.Then = obj.States[state.Then]
270+
if (state.Else) state.Else = obj.States[state.Else]
271+
if (state.Handler) state.Handler = obj.States[state.Handler]
272+
const id = parseInt(name.substring(name.lastIndexOf('_') + 1))
273+
state.id = ids[id] = typeof ids[id] !== 'undefined' ? ids[id] : {}
274+
States.push(state)
275+
}
276+
return { Entry: obj.States[obj.Entry], States, Exit: obj.States[obj.Exit] }
277+
}
278+
}
279+
280+
module.exports = new Composer()

0 commit comments

Comments
 (0)