| # Copyright 2018 IBM Corporation |
| # |
| # Licensed under the Apache License, Version 2.0 (the "License"); |
| # you may not use this file except in compliance with the License. |
| # You may obtain a copy of the License at |
| # |
| # http://www.apache.org/licenses/LICENSE-2.0 |
| # |
| # Unless required by applicable law or agreed to in writing, software |
| # distributed under the License is distributed on an "AS IS" BASIS, |
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| # See the License for the specific language governing permissions and |
| # limitations under the License. |
| |
| import json |
| import os |
| import sys |
| import openwhisk |
| import copy |
| import inspect |
| from composer import __version__ |
| |
| # standard combinators |
| |
| combinators = { |
| 'empty': {'since': '0.4.0'}, |
| 'seq': {'components': True, 'since': '0.4.0'}, |
| 'sequence': {'components': True, 'since': '0.4.0'}, |
| 'if': {'args': [{'_': 'test'}, {'_': 'consequent'}, {'_': 'alternate', 'optional': True}], 'since': '0.4.0'}, |
| 'if_nosave': {'args': [{'_': 'test'}, {'_': 'consequent'}, {'_': 'alternate', 'optional': True}], 'since': '0.4.0'}, |
| 'while': {'args': [{'_': 'test'}, {'_': 'body'}], 'since': '0.4.0'}, |
| 'while_nosave': {'args': [{'_': 'test'}, {'_': 'body'}], 'since': '0.4.0'}, |
| 'dowhile': {'args': [{'_': 'body'}, {'_': 'test'}], 'since': '0.4.0'}, |
| 'dowhile_nosave': {'args': [{'_': 'body'}, {'_': 'test'}], 'since': '0.4.0'}, |
| 'try': {'args': [{'_': 'body'}, {'_': 'handler'}], 'since': '0.4.0'}, |
| 'finally': {'args': [{'_': 'body'}, {'_': 'finalizer'}], 'since': '0.4.0'}, |
| 'retain': {'components': True, 'since': '0.4.0'}, |
| 'retain_catch': {'components': True, 'since': '0.4.0'}, |
| 'let': {'args': [{'_': 'declarations', 'type': 'object'}], 'components': True, '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'}, |
| 'repeat': {'args': [{'_': 'count', 'type': 'int'}], 'components': True, 'since': '0.4.0'}, |
| 'retry': {'args': [{'_': 'count', 'type': 'int'}], 'components': True, 'since': '0.4.0'}, |
| 'value': {'args': [{'_': 'value', 'type': 'value'}], 'since': '0.4.0'}, |
| 'literal': {'args': [{'_': 'value', 'type': 'value'}], 'since': '0.4.0'}, |
| 'function': {'args': [{'_': 'function', 'type': 'object'}], 'since': '0.4.0'} |
| } |
| |
| class ComposerError(Exception): |
| def __init__(self, message, *arguments): |
| self.message = message |
| self.argument = arguments |
| |
| def serialize(obj): |
| return obj.__dict__ |
| |
| class Composition: |
| def __init__(self, **kwargs): |
| if kwargs is not None: |
| for k, v in kwargs.items(): |
| setattr(self, k, v) |
| |
| def __str__(self): |
| return json.dumps(self.__dict__, indent=2, default=serialize) |
| |
| def visit(self, f): |
| ''' apply f to all fields of type composition ''' |
| |
| combinator = combinators[getattr(self, 'type')] |
| if 'components' in combinator: |
| self.components = tuple(map(lambda c: f(c, None), self.components)) |
| |
| if 'args' in combinator: |
| for arg in combinator['args']: |
| if 'type' not in arg: |
| setattr(self, arg['_'], f(getattr(self, arg['_']), arg['_'])) |
| |
| class Compiler: |
| |
| def empty(self): |
| return self._compose('empty', ()) |
| |
| def literal(self, value): |
| return self._compose('literal', (value,)) |
| |
| def seq(self, *arguments): |
| return self._compose('seq', arguments) |
| |
| def sequence(self, *arguments): |
| return self._compose('sequence', arguments) |
| |
| def action(self, name, action=None): |
| return self._compose('action', (name, action)) |
| |
| def when(self, test, consequent, alternate=None): |
| return self._compose('if', (test, consequent, alternate)) |
| |
| def ensure(self, body, finalizer): |
| return self._compose('finally', (body, finalizer)) |
| |
| def task(self, task): |
| '''detect task type and create corresponding composition object''' |
| if task is None: |
| return self.empty() |
| |
| if isinstance(task, Composition): |
| return task |
| |
| if callable(task): |
| return self.function(task) |
| |
| if isinstance(task, str): # python3 only |
| return self.action(task) |
| |
| raise ComposerError('Invalid argument', task) |
| |
| def function(self, fun): |
| ''' function combinator: stringify lambda code ''' |
| if callable(fun): |
| try: |
| fun = inspect.getsource(fun) |
| except OSError: |
| raise ComposerError('Invalid argument', fun) |
| |
| if isinstance(fun, str): |
| fun = { 'kind': 'nodejs:default', 'code': fun } |
| |
| if not isinstance(fun, dict) or fun is None: |
| raise ComposerError('Invalid argument', fun) |
| |
| return Composition(type='function', function={ 'exec': fun }) |
| |
| def _compose(self, type_, arguments): |
| combinator = combinators[type_] |
| skip = len(combinator['args']) if 'args' in combinator else 0 |
| composition = Composition(type=type_) |
| |
| # process declared arguments |
| for i in range(skip): |
| arg = combinator['args'][i] |
| argument = arguments[i] if len(arguments) > i else None |
| |
| if 'type' not in arg: |
| setattr(composition, arg['_'], self.task(argument)) |
| elif arg['type'] == 'value': |
| if type(argument).__name__ == 'function': |
| raise ComposerError('Invalid argument', argument) |
| setattr(composition, arg['_'], argument) |
| else: |
| if type(argument).__name__ != arg['type']: |
| raise ComposerError('Invalid argument', argument) |
| |
| setattr(composition, arg['_'], argument) |
| |
| if 'components' in combinator: |
| setattr(composition, 'components', tuple(map(lambda obj: self.task(obj), arguments[skip:]))) |
| |
| return composition |
| |
| def deserialize(self, composition): |
| ''' recursively deserialize composition ''' |
| |
| composition = copy.copy(composition) |
| composition.visit(lambda composition: self.deserialize(composition)) |
| return composition |
| |
| def label(self, composition): |
| ''' label combinators with the json path ''' |
| |
| if not isinstance(composition, Composition): |
| raise ComposerError('Invalid argument', composition) |
| |
| def label(path): |
| |
| def labeler(composition, name, array) : |
| nonlocal path |
| composition = copy.copy(composition) |
| segment = '' |
| if name is not None: |
| if array is not None: |
| segment = '['+name+']' |
| else: |
| segment = '.'+name |
| |
| setattr(composition, 'path', path + segment) |
| |
| # label nested combinators |
| composition.visit(label(getattr(composition, 'path'))) |
| return composition |
| |
| return labeler |
| |
| return label('')(composition, None, None) |
| |
| |
| def lower(self, composition, combinators = []): |
| ''' recursively label and lower combinators to the desired set of combinators (including primitive combinators) ''' |
| |
| if not isinstance(composition, Composition): |
| raise ComposerError('Invalid argument', composition) |
| |
| # TODO |
| |
| return composition |
| |
| |
| def parse_action_name(name): |
| ''' |
| Parses a (possibly fully qualified) resource name and validates it. If it's not a fully qualified name, |
| then attempts to qualify it. |
| |
| Examples string to namespace, [package/]action name |
| foo => /_/foo |
| pkg/foo => /_/pkg/foo |
| /ns/foo => /ns/foo |
| /ns/pkg/foo => /ns/pkg/foo |
| ''' |
| if not isinstance(name, str): |
| raise ComposerError('Name is not valid') |
| name = name.strip() |
| if len(name) == 0: |
| raise ComposerError('Name is not specified') |
| |
| delimiter = '/' |
| parts = name.split(delimiter) |
| n = len(parts) |
| leadingSlash = name[0] == delimiter if len(name) > 0 else False |
| # no more than /ns/p/a |
| if n < 1 or n > 4 or (leadingSlash and n == 2) or (not leadingSlash and n == 4): |
| raise ComposerError('Name is not valid') |
| |
| # skip leading slash, all parts must be non empty (could tighten this check to match EntityName regex) |
| for part in parts[1:]: |
| if len(part.strip()) == 0: |
| raise ComposerError('Name is not valid') |
| |
| newName = delimiter.join(parts) |
| if leadingSlash: |
| return newName |
| elif n < 3: |
| return delimiter+'_'+delimiter+newName |
| else: |
| return delimiter+newName |
| |
| class Compositions: |
| ''' management class for compositions ''' |
| def __init__(self, wsk, composer): |
| self.actions = wsk.actions |
| self.composer = composer |
| |
| def deploy(self, composition, combinators=None): |
| if not isinstance(composition, Composition): |
| raise ComposerError('Invalid argument', composition) |
| |
| if composition.type != 'composition': |
| raise ComposerError('Cannot deploy anonymous composition') |
| |
| obj = self.composer.encode(composition, combinators) |
| |
| if 'actions' in obj: |
| for action in obj['actions']: |
| self.actions.delete(action) |
| self.actions.update(action) |
| |
| class Composer(Compiler): |
| def action(self, name, options = {}): |
| ''' enhanced action combinator: mangle name, capture code ''' |
| name = parse_action_name(name) # throws ComposerError if name is not valid |
| exec = None |
| if hasattr(options, 'sequence'): # native sequence |
| exec = { 'kind': 'sequence', 'components': tuple(map(parse_action_name, options['sequence'])) } |
| |
| if hasattr(options, 'filename') and isinstance(options['filename'], str): # read action code from file |
| raise ComposerError('read from file not implemented') |
| # 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 hasattr(options, 'action') and (isinstance(options['action'], str) or isinstance(options['action'], dict)): |
| exec = options['action'] |
| |
| if isinstance(exec, str): |
| exec = { 'kind': 'nodejs:default', 'code': exec } |
| |
| return Composition(type='action', exec=exec, name=name) |
| |
| def openwhisk(self, options): |
| ''' return enhanced openwhisk client capable of deploying compositions ''' |
| # try to extract apihost and key first from whisk property file file and then from os.environ |
| |
| wskpropsPath = os.environ['WSK_CONFIG_FILE'] if 'WSK_CONFIG_FILE' in os.environ else os.path.expanduser('~/.wskprops') |
| with open(wskpropsPath) as f: |
| lines = f.readlines() |
| |
| options = dict(options) |
| |
| for line in lines: |
| parts = line.strip().split('=') |
| if len(parts) == 2: |
| if parts[0] == 'APIHOST': |
| options['apihost'] = parts[1] |
| elif parts[0] == 'AUTH': |
| options['api_key'] = parts[1] |
| |
| |
| if '__OW_API_HOST' in os.environ: |
| options['apihost'] = os.environ['__OW_API_HOST'] |
| |
| if '__OW_API_KEY' in os.environ: |
| options['api_key'] = os.environ['__OW_API_KEY'] |
| |
| wsk = openwhisk.Client(options) |
| wsk.compositions = Compositions(wsk, self) |
| return wsk |
| |
| |
| def composition(self, name, composition): |
| ''' enhanced composition combinator: mangle name ''' |
| |
| if not isinstance(name, str): |
| raise ComposerError('Invalid argument', name) |
| |
| name = parse_action_name(name) |
| return Composition(type='composition', name=name, composition= self.task(composition)) |
| |
| |
| def encode(self, composition, combinators=[]): |
| ''' recursively encode composition into { composition, actions } |
| by encoding nested compositions into actions and extracting nested action definitions ''' |
| |
| if not isinstance(composition, Composition): |
| raise ComposerError('Invalid argument', composition) |
| |
| composition = self.lower(composition, combinators) |
| |
| actions = [] |
| |
| def encode(composition, name): |
| composition = copy.copy(composition) |
| composition.visit(encode) |
| if composition.type == 'composition': |
| #code = '// generated by composer v'+__version__+'\n\nconst composition = '+str(encode(composition.composition, ''))+'\n\n// do not edit below this point\n\n'+_conductorCode+'('+_compilerCode+'())' # invoke conductor on composition |
| code = '# generated by composer v'+__version__+'\n\ncomposition = '+str(encode(composition.composition, ''))+'\n\n# do not edit below this point\n\n'+_conductorCode+'('+_compilerCode+'())' # invoke conductor on composition |
| composition.action = { 'exec': { 'kind': 'nodejs:default', 'code':code }, 'annotations': [{ 'key': 'conductor', 'value': str(composition.composition) }, { 'key': 'composer', 'value': __version__ }] } |
| |
| del composition.composition |
| composition.type = 'action' |
| |
| if composition.type == 'action' and hasattr(composition, 'action'): |
| actions.append({ 'name': composition.name, 'action': composition.action, 'serializer': serialize }) |
| del composition.action |
| |
| return composition |
| |
| |
| composition = encode(composition, None) |
| return { 'composition': composition, 'actions': actions } |
| |
| # Use Node js server-side code |
| def conductorPyCode(): |
| with open('workfile') as f: |
| read_data = f.read() |
| |
| # Use Node js server-side code |
| |
| _conductorCode = ''' |
| const main=(function conductor({ Compiler }) { |
| const compiler = new Compiler() |
| |
| this.require = require |
| |
| function chain(front, back) { |
| front.slice(-1)[0].next = 1 |
| front.push(...back) |
| return front |
| } |
| |
| function sequence(components) { |
| if (components.length === 0) return [{ type: 'empty' }] |
| return components.map(compile).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) |
| fsm[0].catch = fsm.length |
| fsm.slice(-1)[0].next = handler.length |
| fsm.push(...handler) |
| 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) |
| consequent.slice(-1)[0].next = alternate.length |
| fsm.push(...consequent) |
| fsm.push(...alternate) |
| return fsm |
| case 'while_nosave': |
| var consequent = compile(json.body) |
| var alternate = [{ type: 'pass' }] |
| var fsm = [[{ type: 'pass', path }], compile(json.test), [{ type: 'choice', then: 1, else: consequent.length + 1 }]].reduce(chain) |
| consequent.slice(-1)[0].next = 1 - fsm.length - consequent.length |
| fsm.push(...consequent) |
| fsm.push(...alternate) |
| 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) |
| fsm.slice(-1)[0].then = 1 - fsm.length |
| fsm.slice(-1)[0].else = 1 |
| var alternate = [{ type: 'pass' }] |
| fsm.push(...alternate) |
| return fsm |
| } |
| } |
| |
| const fsm = compile(compiler.lower(compiler.label(compiler.deserialize(composition)))) |
| |
| const isObject = obj => typeof obj === 'object' && obj !== null && !Array.isArray(obj) |
| |
| // 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)) |
| |
| return params => Promise.resolve().then(() => invoke(params)).catch(internalError) |
| |
| // do invocation |
| function invoke(params) { |
| // initial state and stack |
| let state = 0 |
| let stack = [] |
| |
| // 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 |
| } |
| |
| // 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 |
| } |
| } |
| } |
| |
| // run function f on current stack |
| function run(f) { |
| // handle let/mask pairs |
| const view = [] |
| let n = 0 |
| for (let frame of stack) { |
| if (frame.let === null) { |
| n++ |
| } else if (frame.let !== undefined) { |
| if (n === 0) { |
| view.push(frame) |
| } else { |
| n-- |
| } |
| } |
| } |
| |
| // update value of topmost matching symbol on stack if any |
| function set(symbol, value) { |
| const element = view.find(element => element.let !== undefined && element.let[symbol] !== undefined) |
| if (element !== undefined) element.let[symbol] = JSON.parse(JSON.stringify(value)) |
| } |
| |
| // collapse stack for invocation |
| const env = view.reduceRight((acc, cur) => typeof cur.let === 'object' ? Object.assign(acc, cur.let) : acc, {}) |
| let main = '(function(){try{' |
| for (const name in env) main += `var ${name}=arguments[1]['${name}'];` |
| main += `return eval((${f}))(arguments[0])}finally{` |
| for (const name in env) main += `arguments[1]['${name}']=${name};` |
| main += '}})' |
| try { |
| return (1, eval)(main)(params, env) |
| } finally { |
| for (const name in env) set(name, env[name]) |
| } |
| } |
| |
| while (true) { |
| // final state, return composition result |
| if (state === undefined) { |
| console.log(`Entering final state`) |
| console.log(JSON.stringify(params)) |
| if (params.error) return params; else return { 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`) |
| } |
| } |
| } |
| }) |
| ''' |
| |
| _compilerCode = ''' |
| function compiler() { |
| const util = require('util') |
| const semver = require('semver') |
| |
| // standard combinators |
| const combinators = { |
| empty: { since: '0.4.0' }, |
| seq: { components: true, since: '0.4.0' }, |
| sequence: { components: true, since: '0.4.0' }, |
| if: { args: [{ _: 'test' }, { _: 'consequent' }, { _: 'alternate', optional: true }], since: '0.4.0' }, |
| if_nosave: { args: [{ _: 'test' }, { _: 'consequent' }, { _: 'alternate', optional: true }], since: '0.4.0' }, |
| while: { args: [{ _: 'test' }, { _: 'body' }], since: '0.4.0' }, |
| while_nosave: { args: [{ _: 'test' }, { _: 'body' }], since: '0.4.0' }, |
| dowhile: { args: [{ _: 'body' }, { _: 'test' }], since: '0.4.0' }, |
| dowhile_nosave: { args: [{ _: 'body' }, { _: 'test' }], since: '0.4.0' }, |
| try: { args: [{ _: 'body' }, { _: 'handler' }], since: '0.4.0' }, |
| finally: { args: [{ _: 'body' }, { _: 'finalizer' }], since: '0.4.0' }, |
| retain: { components: true, since: '0.4.0' }, |
| retain_catch: { components: true, since: '0.4.0' }, |
| let: { args: [{ _: 'declarations', type: 'object' }], components: true, 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' }, |
| repeat: { args: [{ _: 'count', type: 'number' }], components: true, since: '0.4.0' }, |
| retry: { args: [{ _: 'count', type: 'number' }], components: true, since: '0.4.0' }, |
| value: { args: [{ _: 'value', type: 'value' }], since: '0.4.0' }, |
| literal: { args: [{ _: 'value', type: 'value' }], since: '0.4.0' }, |
| function: { args: [{ _: 'function', type: 'object' }], since: '0.4.0' } |
| } |
| |
| // composer error class |
| class ComposerError extends Error { |
| constructor(message, argument) { |
| super(message + (argument !== undefined ? '\\nArgument: ' + util.inspect(argument) : '')) |
| } |
| } |
| |
| // composition class |
| class Composition { |
| // weaker instanceof to tolerate multiple instances of this class |
| static [Symbol.hasInstance](instance) { |
| return instance.constructor && instance.constructor.name === Composition.name |
| } |
| |
| // construct a composition object with the specified fields |
| constructor(composition) { |
| return Object.assign(this, composition) |
| } |
| |
| // apply f to all fields of type composition |
| visit(f) { |
| const combinator = combinators[this.type] |
| if (combinator.components) { |
| this.components = this.components.map(f) |
| } |
| for (let arg of combinator.args || []) { |
| if (arg.type === undefined) { |
| this[arg._] = f(this[arg._], arg._) |
| } |
| } |
| } |
| } |
| |
| // compiler class |
| class Compiler { |
| // detect task type and create corresponding composition object |
| task(task) { |
| if (arguments.length > 1) throw new ComposerError('Too many arguments') |
| if (task === null) return this.empty() |
| if (task instanceof Composition) return task |
| if (typeof task === 'function') return this.function(task) |
| if (typeof task === 'string') return this.action(task) |
| throw new ComposerError('Invalid argument', task) |
| } |
| |
| // function combinator: stringify function code |
| function(fun) { |
| if (arguments.length > 1) throw new ComposerError('Too many arguments') |
| if (typeof fun === 'function') { |
| fun = `${fun}` |
| if (fun.indexOf('[native code]') !== -1) throw new ComposerError('Cannot capture native function', fun) |
| } |
| if (typeof fun === 'string') { |
| fun = { kind: 'nodejs:default', code: fun } |
| } |
| if (typeof fun !== 'object' || fun === null) throw new ComposerError('Invalid argument', fun) |
| return new Composition({ type: 'function', function: { exec: fun } }) |
| } |
| |
| // lowering |
| |
| _empty() { |
| 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) |
| } |
| |
| _retain(composition) { |
| return this.let( |
| { params: null }, |
| args => { params = args }, |
| this.mask(...composition.components), |
| result => ({ params, result })) |
| } |
| |
| _retain_catch(composition) { |
| return this.seq( |
| this.retain( |
| this.finally( |
| this.seq(...composition.components), |
| result => ({ result }))), |
| ({ params, result }) => ({ params, result: result.result })) |
| } |
| |
| _if(composition) { |
| return this.let( |
| { params: null }, |
| args => { params = args }, |
| this.if_nosave( |
| this.mask(composition.test), |
| this.seq(() => params, this.mask(composition.consequent)), |
| this.seq(() => params, this.mask(composition.alternate)))) |
| } |
| |
| _while(composition) { |
| return this.let( |
| { params: null }, |
| args => { params = args }, |
| this.while_nosave( |
| this.mask(composition.test), |
| this.seq(() => params, this.mask(composition.body), args => { params = args })), |
| () => params) |
| } |
| |
| _dowhile(composition) { |
| return this.let( |
| { params: null }, |
| args => { params = args }, |
| this.dowhile_nosave( |
| this.seq(() => params, this.mask(composition.body), args => { params = args }), |
| this.mask(composition.test)), |
| () => params) |
| } |
| |
| _repeat(composition) { |
| return this.let( |
| { count: composition.count }, |
| this.while( |
| () => count-- > 0, |
| this.mask(this.seq(...composition.components)))) |
| } |
| |
| _retry(composition) { |
| return this.let( |
| { count: composition.count }, |
| params => ({ params }), |
| this.dowhile( |
| this.finally(({ params }) => params, this.mask(this.retain_catch(...composition.components))), |
| ({ result }) => result.error !== undefined && count-- > 0), |
| ({ 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 |
| } |
| } |
| } |
| |
| // return combinator list |
| get combinators() { |
| return combinators |
| } |
| |
| // recursively deserialize composition |
| deserialize(composition) { |
| if (arguments.length > 1) throw new ComposerError('Too many arguments') |
| composition = new Composition(composition) // copy |
| composition.visit(composition => this.deserialize(composition)) |
| return composition |
| } |
| |
| // label combinators with the json path |
| label(composition) { |
| if (arguments.length > 1) throw new ComposerError('Too many arguments') |
| if (!(composition instanceof Composition)) throw new ComposerError('Invalid argument', composition) |
| |
| const label = path => (composition, name, array) => { |
| composition = new Composition(composition) // copy |
| composition.path = path + (name !== undefined ? (array === undefined ? `.${name}` : `[${name}]`) : '') |
| // label nested combinators |
| composition.visit(label(composition.path)) |
| return composition |
| } |
| |
| return label('')(composition) |
| } |
| |
| // recursively label and lower combinators to the desired set of combinators (including primitive combinators) |
| lower(composition, combinators = []) { |
| if (arguments.length > 2) throw new ComposerError('Too many arguments') |
| if (!(composition instanceof Composition)) throw new ComposerError('Invalid argument', composition) |
| 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)) |
| } |
| |
| const lower = composition => { |
| composition = new Composition(composition) // copy |
| // repeatedly lower root combinator |
| while (combinators.indexOf(composition.type) < 0 && this[`_${composition.type}`]) { |
| const path = composition.path |
| composition = this[`_${composition.type}`](composition) |
| if (path !== undefined) composition.path = path |
| } |
| // lower nested combinators |
| composition.visit(lower) |
| return composition |
| } |
| |
| return lower(composition) |
| } |
| } |
| |
| Compiler.init() |
| |
| return { ComposerError, Composition, Compiler } |
| } |
| ''' |