| /* |
| * Licensed to the Apache Software Foundation (ASF) under one or more |
| * contributor license agreements. See the NOTICE file distributed with |
| * this work for additional information regarding copyright ownership. |
| * The ASF licenses this file to You 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. |
| */ |
| |
| 'use strict' |
| |
| const fqn = require('./fqn') |
| const fs = require('fs') |
| const util = require('util') |
| |
| const version = require('./package.json').version |
| |
| const isObject = obj => typeof obj === 'object' && obj !== null && !Array.isArray(obj) |
| |
| // error class |
| class ComposerError extends Error { |
| constructor (message, argument) { |
| super(message + (argument !== undefined ? '\nArgument value: ' + util.inspect(argument) : '')) |
| } |
| } |
| |
| const composer = { util: { declare, version } } |
| |
| const lowerer = { |
| literal (value) { |
| return composer.let({ value }, () => value) |
| }, |
| |
| retain (...components) { |
| let params = null |
| return composer.let( |
| { params }, |
| composer.finally( |
| args => { params = args }, |
| composer.seq(composer.mask(...components), |
| result => ({ params, result })))) |
| }, |
| |
| retain_catch (...components) { |
| return composer.seq( |
| composer.retain( |
| composer.finally( |
| composer.seq(...components), |
| result => ({ result }))), |
| ({ params, result }) => ({ params, result: result.result })) |
| }, |
| |
| if (test, consequent, alternate) { |
| let params = null |
| return composer.let( |
| { params }, |
| composer.finally( |
| args => { params = args }, |
| composer.if_nosave( |
| composer.mask(test), |
| composer.finally(() => params, composer.mask(consequent)), |
| composer.finally(() => params, composer.mask(alternate))))) |
| }, |
| |
| while (test, body) { |
| let params = null |
| return composer.let( |
| { params }, |
| composer.finally( |
| args => { params = args }, |
| composer.seq(composer.while_nosave( |
| composer.mask(test), |
| composer.finally(() => params, composer.seq(composer.mask(body), args => { params = args }))), |
| () => params))) |
| }, |
| |
| dowhile (body, test) { |
| let params = null |
| return composer.let( |
| { params }, |
| composer.finally( |
| args => { params = args }, |
| composer.seq(composer.dowhile_nosave( |
| composer.finally(() => params, composer.seq(composer.mask(body), args => { params = args })), |
| composer.mask(test)), |
| () => params))) |
| }, |
| |
| repeat (count, ...components) { |
| return composer.let( |
| { count }, |
| composer.while( |
| () => count-- > 0, |
| composer.mask(...components))) |
| }, |
| |
| retry (count, ...components) { |
| return composer.let( |
| { count }, |
| params => ({ params }), |
| composer.dowhile( |
| composer.finally(({ params }) => params, composer.mask(composer.retain_catch(...components))), |
| ({ result }) => result.error !== undefined && count-- > 0), |
| ({ result }) => result) |
| }, |
| |
| merge (...components) { |
| return composer.seq(composer.retain(...components), ({ params, result }) => Object.assign(params, result)) |
| } |
| } |
| |
| // apply f to all fields of type composition |
| function visit (composition, f) { |
| composition = Object.assign({}, composition) // copy |
| const combinator = composition['.combinator']() |
| if (combinator.components) { |
| composition.components = composition.components.map(f) |
| } |
| for (let arg of combinator.args || []) { |
| if (arg.type === undefined && composition[arg.name] !== undefined) { |
| composition[arg.name] = f(composition[arg.name], arg.name) |
| } |
| } |
| return new Composition(composition) |
| } |
| |
| // recursively label combinators with the json path |
| function label (composition) { |
| const label = path => (composition, name, array) => { |
| const p = path + (name !== undefined ? (array === undefined ? `.${name}` : `[${name}]`) : '') |
| composition = visit(composition, label(p)) // copy |
| composition.path = p |
| return composition |
| } |
| return label('')(composition) |
| } |
| |
| // derive combinator methods from combinator table |
| // check argument count and map argument positions to argument names |
| // delegate to Composition constructor for the rest of the validation |
| function declare (combinators, prefix) { |
| if (arguments.length > 2) throw new ComposerError('Too many arguments in "declare"') |
| if (!isObject(combinators)) throw new ComposerError('Invalid argument "combinators" in "declare"', combinators) |
| if (prefix !== undefined && typeof prefix !== 'string') throw new ComposerError('Invalid argument "prefix" in "declare"', prefix) |
| const composer = {} |
| for (let key in combinators) { |
| const type = prefix ? prefix + '.' + key : key |
| const combinator = combinators[key] |
| if (!isObject(combinator) || (combinator.args !== undefined && !Array.isArray(combinator.args))) { |
| throw new ComposerError(`Invalid "${type}" combinator specification in "declare"`, combinator) |
| } |
| for (let arg of combinator.args || []) { |
| if (typeof arg.name !== 'string') throw new ComposerError(`Invalid "${type}" combinator specification in "declare"`, combinator) |
| } |
| composer[key] = function () { |
| const composition = { type, '.combinator': () => combinator } |
| const skip = (combinator.args && combinator.args.length) || 0 |
| if (!combinator.components && (arguments.length > skip)) { |
| throw new ComposerError(`Too many arguments in "${type}" combinator`) |
| } |
| for (let i = 0; i < skip; ++i) { |
| composition[combinator.args[i].name] = arguments[i] |
| } |
| if (combinator.components) { |
| composition.components = Array.prototype.slice.call(arguments, skip) |
| } |
| return new Composition(composition) |
| } |
| } |
| return composer |
| } |
| |
| // 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) { |
| const combinator = composition['.combinator']() |
| Object.assign(this, composition) |
| for (let arg of combinator.args || []) { |
| if (composition[arg.name] === undefined && arg.optional && arg.type !== undefined) continue |
| switch (arg.type) { |
| case undefined: |
| try { |
| this[arg.name] = composer.task(arg.optional ? composition[arg.name] || null : composition[arg.name]) |
| } catch (error) { |
| throw new ComposerError(`Invalid argument "${arg.name}" in "${composition.type} combinator"`, composition[arg.name]) |
| } |
| break |
| case 'name': |
| try { |
| this[arg.name] = fqn(composition[arg.name]) |
| } catch (error) { |
| throw new ComposerError(`${error.message} in "${composition.type} combinator"`, composition[arg.name]) |
| } |
| break |
| case 'value': |
| if (typeof composition[arg.name] === 'function' || composition[arg.name] === undefined) { |
| throw new ComposerError(`Invalid argument "${arg.name}" in "${composition.type} combinator"`, composition[arg.name]) |
| } |
| break |
| case 'object': |
| if (!isObject(composition[arg.name])) { |
| throw new ComposerError(`Invalid argument "${arg.name}" in "${composition.type} combinator"`, composition[arg.name]) |
| } |
| break |
| default: |
| if ('' + typeof composition[arg.name] !== arg.type) { |
| throw new ComposerError(`Invalid argument "${arg.name}" in "${composition.type} combinator"`, composition[arg.name]) |
| } |
| } |
| } |
| if (combinator.components) this.components = (composition.components || []).map(obj => composer.task(obj)) |
| return this |
| } |
| |
| // compile composition |
| compile () { |
| if (arguments.length > 0) throw new ComposerError('Too many arguments in "compile"') |
| |
| const actions = [] |
| |
| const flatten = composition => { |
| composition = visit(composition, flatten) |
| if (composition.type === 'action' && composition.action) { |
| actions.push({ name: composition.name, action: composition.action }) |
| delete composition.action |
| } |
| return composition |
| } |
| |
| const obj = { composition: label(flatten(this)).lower(), ast: this, version } |
| if (actions.length > 0) obj.actions = actions |
| return obj |
| } |
| |
| // recursively lower combinators to the desired set of combinators (including primitive combinators) |
| lower (combinators = []) { |
| if (arguments.length > 1) throw new ComposerError('Too many arguments in "lower"') |
| if (!Array.isArray(combinators)) throw new ComposerError('Invalid argument "combinators" in "lower"', combinators) |
| |
| const lower = composition => { |
| // repeatedly lower root combinator |
| while (composition['.combinator']().def) { |
| const path = composition.path |
| const combinator = composition['.combinator']() |
| if (Array.isArray(combinators) && combinators.indexOf(composition.type) >= 0) break |
| // map argument names to positions |
| const args = [] |
| const skip = (combinator.args && combinator.args.length) || 0 |
| for (let i = 0; i < skip; i++) args.push(composition[combinator.args[i].name]) |
| if (combinator.components) args.push(...composition.components) |
| composition = combinator.def(...args) |
| if (path !== undefined) composition.path = path // preserve path |
| } |
| // lower nested combinators |
| return visit(composition, lower) |
| } |
| |
| return lower(this) |
| } |
| } |
| |
| // primitive combinators |
| const combinators = { |
| sequence: { components: true }, |
| if_nosave: { args: [{ name: 'test' }, { name: 'consequent' }, { name: 'alternate', optional: true }] }, |
| while_nosave: { args: [{ name: 'test' }, { name: 'body' }] }, |
| dowhile_nosave: { args: [{ name: 'body' }, { name: 'test' }] }, |
| try: { args: [{ name: 'body' }, { name: 'handler' }] }, |
| finally: { args: [{ name: 'body' }, { name: 'finalizer' }] }, |
| let: { args: [{ name: 'declarations', type: 'object' }], components: true }, |
| mask: { components: true }, |
| action: { args: [{ name: 'name', type: 'name' }, { name: 'action', type: 'object', optional: true }] }, |
| function: { args: [{ name: 'function', type: 'object' }] }, |
| async: { components: true } |
| } |
| |
| Object.assign(composer, declare(combinators)) |
| |
| // derived combinators |
| const extra = { |
| empty: { def: composer.sequence }, |
| seq: { components: true, def: composer.sequence }, |
| if: { args: [{ name: 'test' }, { name: 'consequent' }, { name: 'alternate', optional: true }], def: lowerer.if }, |
| while: { args: [{ name: 'test' }, { name: 'body' }], def: lowerer.while }, |
| dowhile: { args: [{ name: 'body' }, { name: 'test' }], def: lowerer.dowhile }, |
| repeat: { args: [{ name: 'count', type: 'number' }], components: true, def: lowerer.repeat }, |
| retry: { args: [{ name: 'count', type: 'number' }], components: true, def: lowerer.retry }, |
| retain: { components: true, def: lowerer.retain }, |
| retain_catch: { components: true, def: lowerer.retain_catch }, |
| value: { args: [{ name: 'value', type: 'value' }], def: lowerer.literal }, |
| literal: { args: [{ name: 'value', type: 'value' }], def: lowerer.literal }, |
| merge: { components: true, def: lowerer.merge } |
| } |
| |
| Object.assign(composer, declare(extra)) |
| |
| // add or override definitions of some combinators |
| Object.assign(composer, { |
| // detect task type and create corresponding composition object |
| task (task) { |
| if (arguments.length > 1) throw new ComposerError('Too many arguments in "task" combinator') |
| if (task === undefined) throw new ComposerError('Invalid argument in "task" combinator', task) |
| if (task === null) return composer.empty() |
| if (task instanceof Composition) return task |
| if (typeof task === 'function') return composer.function(task) |
| if (typeof task === 'string') return composer.action(task) |
| throw new ComposerError('Invalid argument "task" in "task" combinator', task) |
| }, |
| |
| // function combinator: stringify function code |
| function (fun) { |
| if (arguments.length > 1) throw new ComposerError('Too many arguments in "function" combinator') |
| if (typeof fun === 'function') { |
| fun = `${fun}` |
| if (fun.indexOf('[native code]') !== -1) throw new ComposerError('Cannot capture native function in "function" combinator', fun) |
| } |
| if (typeof fun === 'string') { |
| fun = { kind: 'nodejs:default', code: fun } |
| } |
| if (!isObject(fun)) throw new ComposerError('Invalid argument "function" in "function" combinator', fun) |
| return new Composition({ type: 'function', function: { exec: fun }, '.combinator': () => combinators.function }) |
| }, |
| |
| // action combinator |
| action (name, options = {}) { |
| if (arguments.length > 2) throw new ComposerError('Too many arguments in "action" combinator') |
| if (!isObject(options)) throw new ComposerError('Invalid argument "options" in "action" combinator', options) |
| let exec |
| if (Array.isArray(options.sequence)) { // native sequence |
| exec = { kind: 'sequence', components: options.sequence.map(fqn) } |
| } else if (typeof options.filename === 'string') { // read action code from file |
| exec = fs.readFileSync(options.filename, { encoding: 'utf8' }) |
| } else 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 in "action" combinator', options.action) |
| } else if (typeof options.action === 'string' || isObject(options.action)) { |
| exec = options.action |
| } |
| if (typeof exec === 'string') { |
| exec = { kind: 'nodejs:default', code: exec } |
| } |
| const composition = { type: 'action', name, '.combinator': () => combinators.action } |
| if (exec) composition.action = { exec } |
| return new Composition(composition) |
| }, |
| |
| // recursively deserialize composition |
| parse (composition) { |
| if (arguments.length > 1) throw new ComposerError('Too many arguments in "parse" combinator') |
| if (!isObject(composition)) throw new ComposerError('Invalid argument "composition" in "parse" combinator', composition) |
| const combinator = typeof composition['.combinator'] === 'function' ? composition['.combinator']() : combinators[composition.type] |
| if (!isObject(combinator)) throw new ComposerError('Invalid composition type in "parse" combinator', composition) |
| return visit(Object.assign({ '.combinator': () => combinator }, composition), composition => composer.parse(composition)) |
| } |
| }) |
| |
| module.exports = composer |