diff --git a/src/model/Declaration.js b/src/model/Declaration.js new file mode 100644 index 0000000..8858301 --- /dev/null +++ b/src/model/Declaration.js @@ -0,0 +1,38 @@ +import Port from './Port'; +import { extractExpression, extractType } from '../parser/WDL/utils/utils'; + +/** + * Class representing binding points of workflow steps. + */ +export default class Declaration extends Port { + /** + * Create a Declaration. + * + * @param {string} name - Declaration name + * @param {ast} declaration - Declaration ast tree node + * @param {Step} step - Parent step this Declaration belongs to. + */ + constructor(name, declaration, step) { + if (!name) { + throw new Error('Declaration must have a name'); + } + if (!declaration.expression || !declaration.type) { + throw new Error('Declaration could be created only using declaration object'); + } + super(name, step, {}); + + delete this.desc; + + /** + * Declaration expression extracted object. + * @type {string} + */ + this.expression = extractExpression(declaration.expression); + /** + * Declaration type. + * @type {string} + */ + this.type = extractType(declaration.type); + } + +} diff --git a/src/model/Group.js b/src/model/Group.js index 2b54392..d1b410a 100644 --- a/src/model/Group.js +++ b/src/model/Group.js @@ -34,6 +34,65 @@ class Group extends Step { super(name, new Action(name, config), config); this.type = type || 'default'; + /** + * Dictionary of Group's own declarations. + * @type {Object.} + */ + this.ownDeclarations = {}; + } + + /** + * Add a child step. + * + * @param {Declaration} declaration - Declaration to add. + * @returns {Declaration} Added declaration. May be used for easy declaration creation. + * @throws {Error} Generic exception if a declaration with the same name already exists. + * @example + * const declaration = parent.addDeclaration(new Declaration('declaration', ...)); + */ + addDeclaration(declaration) { + const root = this.workflow(); + const existingInRoot = root && root.declarations[declaration.name]; + const ownExisting = this.ownDeclarations[declaration.name]; + if (!existingInRoot && !ownExisting) { + if (declaration.step !== null) { + declaration.step.removeDeclaration(declaration.name, root); + } + + declaration.step = this; + + if (root) { + root.declarations[declaration.name] = declaration; + } + this.ownDeclarations[declaration.name] = declaration; + } else if (existingInRoot !== declaration && ownExisting !== declaration) { + throw new Error(`Cannot add a declaration with the same name ${declaration.name}.`); + } + return declaration; + } + + /** + * Remove a declaration. + * + * @param {string} name - Declaration to remove. + * @param {Workflow} root - Declaration's workflow + * @example + * parent.removeDeclaration('declaration'); + */ + removeDeclaration(name, root = null) { + const workflow = root || this.workflow(); + const declarationInRoot = workflow && workflow.declarations[name]; + const declaration = this.ownDeclarations[name]; + if (declarationInRoot) { + declarationInRoot.step = null; + delete workflow.declarations[name]; + } + if (declaration) { + declaration.step = null; + delete this.ownDeclarations[name]; + } + + return declaration; } } diff --git a/src/model/Step.js b/src/model/Step.js index a6e96d8..a03261b 100644 --- a/src/model/Step.js +++ b/src/model/Step.js @@ -95,9 +95,9 @@ export default class Step { * Create a workflow step. You should {@link Workflow#add add} it to a workflow or a compound step. * * @param {string} name - Step name. Must be unique in a parent step (e.g. {@link Workflow}). - * @param {Action=} [action={}] action - Action to execute during the step. If no action is specified it will + * @param {Action|{}} action - Action to execute during the step. If no action is specified it will * automatically be created based on the configuration. Multiple steps may share a single action. - * @param {object} [config={}] - Action configuration containing input bindings. + * @param {object} config - Action configuration containing input bindings. * It should include action description in case the action is missing. * */ diff --git a/src/model/Workflow.js b/src/model/Workflow.js index 75059a1..d7f0803 100644 --- a/src/model/Workflow.js +++ b/src/model/Workflow.js @@ -35,6 +35,12 @@ class Workflow extends Group { * @type {Object.} */ this.actions = {}; + /** + * Dictionary of all declarations accessible within workflow. + * @type {Object.} + */ + this.declarations = {}; + if (config.ast) { this.ast = config.ast; } diff --git a/src/parser/WDL/entities/WDLWorkflow.js b/src/parser/WDL/entities/WDLWorkflow.js index 3f503bf..02e9817 100644 --- a/src/parser/WDL/entities/WDLWorkflow.js +++ b/src/parser/WDL/entities/WDLWorkflow.js @@ -3,6 +3,8 @@ import _ from 'lodash'; import Workflow from '../../../model/Workflow'; import Step from '../../../model/Step'; import Group from '../../../model/Group'; +import Declaration from '../../../model/Declaration'; +import Port from '../../../model/Port'; import { extractExpression, extractType, extractMetaBlock, WDLParserError } from '../utils/utils'; import * as Constants from '../constants'; @@ -46,19 +48,13 @@ export default class WDLWorkflow { * @param {list} bodyList - list of the current parsing wdl body node * @param {string} name - current body name * @param {Step} parent - parent step - * @param {list} opts - nodes that prohibited for current stage to parse (in lower case) + * @param {[list]} opts - nodes that prohibited for current stage to parse (in lower case) */ parseBody(bodyList, name, parent, opts = []) { const parentStep = parent || this.workflowStep; - let declarationsPassed = false; bodyList.forEach((item) => { const lcName = item.name.toLowerCase(); if (_.indexOf(opts, lcName) < 0) { - if (lcName !== 'declaration') { - declarationsPassed = true; - } else if (declarationsPassed) { - throw new WDLParserError('Declarations are allowed only before other things of current scope'); - } this.parsingProcessors[lcName].call(this, item, parentStep); } else { throw new WDLParserError(`In ${name} body keys [${opts}] are not allowed`); @@ -89,7 +85,8 @@ export default class WDLWorkflow { const collection = extractExpression(item.attributes.collection); - const port = WDLWorkflow.getPortForBinding(this.workflowStep, parent, collection); + // in scatter the item is always an identifier, so it'll always be one port returned from .getPortsForBinding() + const [port] = WDLWorkflow.getPortsForBinding(this.workflowStep, parent, collection); opts.i[itemName] = {}; opts.i[itemName].type = 'ScatterItem'; @@ -190,8 +187,17 @@ export default class WDLWorkflow { const declaration = node.attributes.key.source_string; const expression = extractExpression(nodeValue); + if (!step.i[declaration] && step instanceof Workflow && step.ownDeclarations[declaration]) { + // remove declaration and add it as an input + const declarationObj = step.removeDeclaration(declaration); + const port = new Port(declaration, step, { type: declarationObj.type }); + _.forEach(declarationObj.outputs, conn => conn.to.bind(port)); + step.i[declaration] = port; + } + if (step.i[declaration]) { - step.i[declaration].bind(WDLWorkflow.getPortForBinding(this.workflowStep, parentStep, expression)); + const bindings = WDLWorkflow.getPortsForBinding(this.workflowStep, parentStep, expression); + _.forEach(bindings, binding => step.i[declaration].bind(binding)); } else { throw new WDLParserError(`Undeclared variable trying to be assigned: call '${step.name}' --> '${declaration}'`); } @@ -209,19 +215,21 @@ export default class WDLWorkflow { const name = decl.name.source_string; const type = extractType(decl.type); - const obj = {}; - obj[name] = { - type, - }; + if (decl.expression === null) { // declaration is an input type + const obj = {}; + obj[name] = { + type, + }; + parentStep.action.addPorts({ + i: obj, + }); + } else if (parentStep instanceof Group) { // declaration is a "variable" type + const declaration = new Declaration(name, decl, parentStep); + const bindings = WDLWorkflow.getPortsForBinding(this.workflowStep, declaration.step, declaration.expression); + _.forEach(bindings, binding => declaration.bind(binding)); - const str = extractExpression(decl.expression).string; - if (str !== '') { - obj[name].default = str; + parentStep.addDeclaration(declaration); } - - parentStep.action.addPorts({ - i: obj, - }); } /** @@ -254,34 +262,17 @@ export default class WDLWorkflow { type, }; - let wfOutLinksList = []; if (expression.type !== 'MemberAccess') { obj[name].default = expression.string; - } else { - expression.accesses.forEach((v) => { v.to = name; }); - wfOutLinksList = expression.accesses; } this.workflowStep.action.addPorts({ o: obj, }); - wfOutLinksList.forEach((i) => { - const startStep = WDLWorkflow.findStepInStructureRecursively(this.workflowStep, i.lhs); - - if (startStep) { - if (startStep.o[i.rhs]) { - this.workflowStep.o[i.to].bind(startStep.o[i.rhs]); - } else { - throw new WDLParserError( - `In '${this.workflowStep.name}' - output block undeclared variable is referenced: '${i.lhs}.${i.rhs}'`); - } - } else { - throw new WDLParserError( - `In '${this.workflowStep.name}' - output block undeclared call is referenced: '${i.lhs}'`); - } + const bindings = WDLWorkflow.getPortsForBinding(this.workflowStep, this.workflowStep, expression, true); + _.forEach(bindings, (binding) => { + this.workflowStep.o[name].bind(binding); }); }); } @@ -309,15 +300,10 @@ export default class WDLWorkflow { }); // WF output connections if (!wildcard) { // syntax: call_name.output_name - const callOutput = fqn.source_string.split('.'); - if (callOutput.length < 2) { - return; - } - const callName = callOutput[0]; - const outputName = callOutput[1]; + const [callName, outputName] = fqn.source_string.split('.'); const startStep = WDLWorkflow.findStepInStructureRecursively(this.workflowStep, callName); - if (startStep) { + if (startStep && startStep instanceof Step) { if (startStep.o[outputName]) { this.workflowStep.o[fqn.source_string].bind(startStep.o[outputName]); } else { @@ -325,6 +311,8 @@ export default class WDLWorkflow { `In '${this.workflowStep.name}' output block undeclared variable is referenced: '${callName}.${outputName}'`); } + } else if (startStep && startStep instanceof Port) { + this.workflowStep.o[fqn.source_string].bind(startStep); } else { throw new WDLParserError( `In '${this.workflowStep.name}' @@ -334,7 +322,7 @@ export default class WDLWorkflow { const callName = fqn.source_string; const startStep = WDLWorkflow.findStepInStructureRecursively(this.workflowStep, callName); - if (startStep) { + if (startStep && startStep instanceof Step) { if (_.size(startStep.o)) { _.forEach(startStep.o, (output, outputName) => { this.workflowStep.o[`${fqn.source_string}.*`].bind(startStep.o[outputName]); @@ -344,6 +332,8 @@ export default class WDLWorkflow { `In '${this.workflowStep.name}' output block undeclared variable is referenced: '${callName}.* (${callName} doesn't have any outputs)`); } + } else if (startStep && startStep instanceof Port) { + this.workflowStep.o[`${fqn.source_string}.*`].bind(startStep); } else { throw new WDLParserError( `In '${this.workflowStep.name}' @@ -355,20 +345,29 @@ export default class WDLWorkflow { static findStepInStructureRecursively(step, name) { let result = null; - _.forEach(step.children, (item, key) => { - if (key === name) { - result = item; - return false; - } + if (step.declarations && Object.keys(step.declarations).includes(name)) { + result = step.declarations[name]; + } + if (!result && step instanceof Group && step.i && Object.keys(step.i).includes(name)) { + result = step.i[name]; + } - result = WDLWorkflow.findStepInStructureRecursively(item, name); + if (!result) { + _.forEach(step.children, (item, key) => { + if (key === name) { + result = item; + return false; + } - if (result) { - return false; - } + result = WDLWorkflow.findStepInStructureRecursively(item, name); - return undefined; - }); + if (result) { + return false; + } + + return undefined; + }); + } return result; } @@ -378,6 +377,10 @@ export default class WDLWorkflow { if (_.has(step.i, portName) || _.has(step.o, portName)) { return step; } + const root = step.workflow(); + if (_.has(root.declarations, portName)) { + return root; + } return WDLWorkflow.groupNameResolver(step.parent, portName); } @@ -385,28 +388,76 @@ export default class WDLWorkflow { return undefined; } - static getPortForBinding(workflow, parent, expression) { - let binder = expression.string; - if (expression.type === 'MemberAccess') { - const rhsPart = expression.accesses[0].rhs; - const lhsPart = expression.accesses[0].lhs; - - const outputStep = WDLWorkflow.findStepInStructureRecursively(workflow, lhsPart); - if (outputStep) { - if (outputStep.o[rhsPart]) { - binder = outputStep.o[rhsPart]; - } else { - throw new WDLParserError(`Undeclared variable is referenced: '${lhsPart}.${rhsPart}'`); + static getPortsForBinding(workflow, parent, expression, isWfOutput = false) { + const accessesTypes = [ + 'ArrayOrMapLookup', + 'MemberAccess', + 'FunctionCall', + 'ArrayLiteral', + 'ObjectLiteral', + 'MapLiteral', + 'TupleLiteral', + 'LogicalNot', + 'UnaryPlus', + 'UnaryNegation', + 'Add', + 'Subtract', + 'Multiply', + 'Divide', + 'Remainder', + 'LogicalOr', + 'LogicalAnd', + 'Equals', + 'NotEquals', + 'LessThan', + 'LessThanOrEqual', + 'GreaterThan', + 'GreaterThanOrEqual', + 'TernaryIf', + 'MapLiteralKv', + 'ObjectKV', + ]; + const errorMessAdd = isWfOutput ? `in ${workflow.name} output block ` : ''; + let binder = [expression.string]; + if (accessesTypes.includes(expression.type) && expression.accesses.length) { + binder = []; + _.forEach(expression.accesses, (accesses) => { + if (_.isObject(accesses)) { + const outputStep = WDLWorkflow.findStepInStructureRecursively(workflow, accesses.lhs); + if (outputStep && outputStep instanceof Step) { + if (outputStep.o[accesses.rhs]) { + binder.push(outputStep.o[accesses.rhs]); + } else { + throw new WDLParserError(`Undeclared variable ${errorMessAdd}is referenced: '${accesses.lhs}.${accesses.rhs}'`); + } + } else if (outputStep && outputStep instanceof Port) { + binder.push(outputStep); + } else { + throw new WDLParserError(`Undeclared call ${errorMessAdd}is referenced: '${accesses.lhs}'`); + } + } else if (_.isString(accesses)) { + const desiredStep = WDLWorkflow.groupNameResolver(parent, accesses); + if (desiredStep) { + if (desiredStep.i[accesses]) { + binder.push(desiredStep.i[accesses]); + } else { + binder.push(desiredStep.declarations[accesses]); + } + } else { + throw new WDLParserError(`Undeclared variable ${errorMessAdd}is referenced: '${expression.string}'`); + } } - } else { - throw new WDLParserError(`Undeclared call is referenced: '${lhsPart}'`); - } + }); } else if (expression.type === 'identifier') { const desiredStep = WDLWorkflow.groupNameResolver(parent, expression.string); if (desiredStep) { - binder = desiredStep.i[expression.string]; + if (desiredStep.i[expression.string]) { + binder = [desiredStep.i[expression.string]]; + } else { + binder = [desiredStep.declarations[expression.string]]; + } } else { - throw new WDLParserError(`Undeclared variable is referenced: '${expression.string}'`); + throw new WDLParserError(`Undeclared variable ${errorMessAdd}is referenced: '${expression.string}'`); } } diff --git a/src/parser/WDL/parse.js b/src/parser/WDL/parse.js index 61a9211..d01df32 100644 --- a/src/parser/WDL/parse.js +++ b/src/parser/WDL/parse.js @@ -4,6 +4,7 @@ import Context from './entities/Context'; import Parser from './hermes/wdl_parser'; import * as Constants from './constants'; import DataService from './../../dataServices/data-service'; +import { renameExpression, replaceSplitter } from './utils/renaming_utils'; function hermesStage(data) { let tokens; @@ -83,19 +84,6 @@ function getImports(ast) { }) : []; } -function replaceDot(name) { - let res; - const splitted = name.split(Constants.NS_SPLITTER); - if (splitted.length > 2) { - const last = splitted.pop(); - res = `${splitted.join(Constants.NS_CONNECTOR)}${Constants.NS_SPLITTER}${last}`; - } else { - res = splitted.join(Constants.NS_CONNECTOR); - } - - return res; -} - function renameCallInput(callInput, prefix, initialCalls) { if (callInput.name.toLowerCase() === Constants.CALL_IO_MAPPING) { const valueType = callInput.attributes.value.name ? callInput.attributes.value.name.toLowerCase() : null; @@ -103,7 +91,7 @@ function renameCallInput(callInput, prefix, initialCalls) { const calls = initialCalls.map(call => call.split(Constants.NS_SPLITTER).pop()); const index = calls.indexOf(callInput.attributes.value.attributes.lhs.source_string); if (index > -1) { - callInput.attributes.value.attributes.lhs.source_string = `${prefix}${replaceDot(initialCalls[index])}`; + callInput.attributes.value.attributes.lhs.source_string = `${prefix}${replaceSplitter(initialCalls[index])}`; } } } @@ -127,7 +115,7 @@ function renameWfOutput(output, prefix, initialCalls) { const calls = initialCalls.map(call => call.split(Constants.NS_SPLITTER).pop()); const index = calls.indexOf(output.attributes.expression.attributes.lhs.source_string); if (index > -1) { - output.attributes.expression.attributes.lhs.source_string = `${prefix}${replaceDot(initialCalls[index])}`; + output.attributes.expression.attributes.lhs.source_string = `${prefix}${replaceSplitter(initialCalls[index])}`; } } break; @@ -139,7 +127,7 @@ function renameWfOutput(output, prefix, initialCalls) { } function renameCallAst(call, prefix, initialCalls) { - if (!initialCalls.includes(call.attributes.task.source_string)) { + if (!initialCalls.includes(call.attributes.task.source_string) && !call.attributes.alias) { initialCalls.push(call.attributes.task.source_string); } if (call.attributes.body && call.attributes.body.attributes && call.attributes.body.attributes.io) { @@ -147,7 +135,7 @@ function renameCallAst(call, prefix, initialCalls) { .map(callInput => renameCallInputs(callInput, prefix, initialCalls)); } - call.attributes.task.source_string = `${prefix}${replaceDot(call.attributes.task.source_string)}`; + call.attributes.task.source_string = `${prefix}${replaceSplitter(call.attributes.task.source_string)}`; return call; } @@ -155,6 +143,10 @@ function renameCallAst(call, prefix, initialCalls) { function renameWfDefinition(definition, prefix, initialCalls) { switch (definition.name.toLowerCase()) { case Constants.DECLARATION: + renameExpression(definition.attributes.name, prefix, initialCalls); + if (definition.attributes.expression) { + renameExpression(definition.attributes.expression, prefix, initialCalls); + } break; case Constants.CALL: definition = renameCallAst(definition, prefix, initialCalls); @@ -173,11 +165,11 @@ function renameWfDefinition(definition, prefix, initialCalls) { } function renameWfAstNames(node, prefix) { - node.attributes.name.source_string = `${prefix}${replaceDot(node.attributes.name.source_string)}`; + node.attributes.name.source_string = `${prefix}${replaceSplitter(node.attributes.name.source_string)}`; const initialCalls = []; // declarations, calls, outputs node.attributes.body.list = node.attributes.body.list.map((definition) => { - if (definition.name.toLowerCase() === Constants.CALL) { + if (definition.name.toLowerCase() === Constants.CALL && !definition.attributes.alias) { initialCalls.push(definition.attributes.task.source_string); } return renameWfDefinition(definition, prefix, initialCalls); @@ -187,7 +179,7 @@ function renameWfAstNames(node, prefix) { } function renameTaskAstNames(node, prefix) { - node.attributes.name.source_string = `${prefix}${replaceDot(node.attributes.name.source_string)}`; + node.attributes.name.source_string = `${prefix}${replaceSplitter(node.attributes.name.source_string)}`; return node; } @@ -273,9 +265,9 @@ function getPreparedSubWDLs(opts) { function clearWfDefinition(definition) { switch (definition.name.toLowerCase()) { case Constants.DECLARATION: - break; - case Constants.CALL: - definition = false; + if (definition.attributes.expression) { + definition.attributes.expression = null; + } break; case Constants.WF_OUTPUTS: definition.attributes.outputs.list = definition.attributes.outputs.list @@ -286,12 +278,9 @@ function clearWfDefinition(definition) { return output; }); break; + case Constants.CALL: default: - definition.attributes.body.list = definition.attributes.body.list.map(def => clearWfDefinition(def)) - .filter(i => !!i && !_.isArray(i)); - if (!definition.attributes.body.list.length) { - definition = false; - } + definition = false; break; } diff --git a/src/parser/WDL/utils/renaming_utils.js b/src/parser/WDL/utils/renaming_utils.js new file mode 100644 index 0000000..2ebeba2 --- /dev/null +++ b/src/parser/WDL/utils/renaming_utils.js @@ -0,0 +1,135 @@ +import * as Constants from '../constants'; + +/** + * Replaces {@link Constants}.{@link NS_SPLITTER} with {@link Constants}.{@link NS_CONNECTOR} in the name string + * @param {String} name - the name string to handle + * */ +export function replaceSplitter(name) { + let res; + const split = name.split(Constants.NS_SPLITTER); + if (split.length > 2) { + const last = split.pop(); + res = `${split.join(Constants.NS_CONNECTOR)}${Constants.NS_SPLITTER}${last}`; + } else { + res = split.join(Constants.NS_CONNECTOR); + } + + return res; +} + +function putEnumeratedExpressions(list, localScope, callback) { + list.forEach(item => callback(item, localScope.prefix, localScope.initialCalls)); +} + +const processors = { + FunctionCall: (scope) => { + putEnumeratedExpressions(scope.attr.params.list, scope, scope.renameExpression); + }, + MemberAccess: (scope) => { + scope.renameExpression(scope.attr.lhs, scope.prefix, scope.initialCalls); + }, + Unary: (scope) => { + scope.renameExpression(scope.attr.expression, scope.prefix, scope.initialCalls); + }, + Binary: (scope) => { + scope.renameExpression(scope.attr.lhs, scope.prefix, scope.initialCalls); + scope.renameExpression(scope.attr.rhs, scope.prefix, scope.initialCalls); + }, + Ternary: (scope) => { + scope.renameExpression(scope.attr.cond, scope.prefix, scope.initialCalls); + scope.renameExpression(scope.attr.iftrue, scope.prefix, scope.initialCalls); + scope.renameExpression(scope.attr.iffalse, scope.prefix, scope.initialCalls); + }, + Kv: (scope) => { + scope.renameExpression(scope.attr.value, scope.prefix, scope.initialCalls); + }, + ArrayOrMapLookup: (scope) => { + scope.renameExpression(scope.attr.lhs, scope.prefix, scope.initialCalls); + scope.renameExpression(scope.attr.rhs, scope.prefix, scope.initialCalls); + }, + ArrayLiteral: (scope) => { + putEnumeratedExpressions(scope.attr.values.list, scope, scope.renameExpression); + }, + ObjectLiteral: (scope) => { + putEnumeratedExpressions(scope.attr.map.list, scope, scope.renameExpression); + }, + MapLiteral: (scope) => { + putEnumeratedExpressions(scope.attr.map.list, scope, scope.renameExpression); + }, + TupleLiteral: (scope) => { + putEnumeratedExpressions(scope.attr.values.list, scope, scope.renameExpression); + }, + Identifier: (scope) => { + const calls = scope.initialCalls.map(call => call.split(Constants.NS_SPLITTER).pop()); + const index = calls.indexOf(scope.ast.source_string); + if (index > -1) { + scope.ast.source_string = `${scope.prefix}${replaceSplitter(scope.initialCalls[index])}`; + } + }, + default: () => { + // no need to rename these types of declarations + }, +}; + +const processorRemap = { + ArrayOrMapLookup: 'ArrayOrMapLookup', + FunctionCall: 'FunctionCall', + MemberAccess: 'MemberAccess', + ArrayLiteral: 'ArrayLiteral', + ObjectLiteral: 'ObjectLiteral', + MapLiteral: 'MapLiteral', + TupleLiteral: 'TupleLiteral', + identifier: 'Identifier', + + string: 'default', + boolean: 'default', + integer: 'default', + float: 'default', + + LogicalNot: 'Unary', + UnaryPlus: 'Unary', + UnaryNegation: 'Unary', + + Add: 'Binary', + Subtract: 'Binary', + Multiply: 'Binary', + Divide: 'Binary', + Remainder: 'Binary', + LogicalOr: 'Binary', + LogicalAnd: 'Binary', + Equals: 'Binary', + NotEquals: 'Binary', + LessThan: 'Binary', + LessThanOrEqual: 'Binary', + GreaterThan: 'Binary', + GreaterThanOrEqual: 'Binary', + + TernaryIf: 'Ternary', + + MapLiteralKv: 'Kv', + ObjectKV: 'Kv', +}; + +/** + * Changes declaration's names in entire ast expression subtree according to prefix and initialCalls parameters + * @param {ast} ast - Ast tree node to be expressed + * @param {String} prefix - name prefix + * @param {[String]} initialCalls - array of initial call's names that need to be renamed + */ +export function renameExpression(ast, prefix, initialCalls) { + if (!ast) { + return; + } + + const attr = ast.attributes; + + const scope = { + ast, + prefix, + initialCalls, + renameExpression, + attr, + }; + + processors[processorRemap[ast.name || ast.str]](scope); +} diff --git a/src/parser/WDL/utils/utils.js b/src/parser/WDL/utils/utils.js index b6eddaf..2ee93ea 100644 --- a/src/parser/WDL/utils/utils.js +++ b/src/parser/WDL/utils/utils.js @@ -57,7 +57,7 @@ const unaryUnScoped = [ function putEnumeratedExpressions(list, localRes, callback) { list.map(item => callback(item)) .forEach((item, idx) => { - localRes.accesses.concat(item.accesses); + localRes.accesses = localRes.accesses.concat(item.accesses); localRes.string += idx === 0 ? item.string : `, ${item.string}`; }); } @@ -162,6 +162,12 @@ const processors = { scope.res.string += ')'; }, + Identifier: (scope) => { + scope.res.string = scope.ast.source_string; + scope.res.type = scope.ast.str; + + scope.res.accesses = scope.res.accesses.concat(scope.ast.source_string); + }, default: (scope) => { if (scope.ast.str === 'string') { scope.res.string = `"${JSON.stringify(scope.ast.source_string).slice(1, -1)}"`; @@ -181,9 +187,9 @@ const processorRemap = { ObjectLiteral: 'ObjectLiteral', MapLiteral: 'MapLiteral', TupleLiteral: 'TupleLiteral', + identifier: 'Identifier', string: 'default', - identifier: 'default', boolean: 'default', integer: 'default', float: 'default', @@ -242,7 +248,7 @@ export function extractExpression(ast) { /** * Build the string from entire ast type subtree - * @param {ast} ast - Ast tree node to be expressed + * @param {ast} declarationNode - Ast tree node to be expressed */ export function extractType(declarationNode) { if (declarationNode === undefined || declarationNode === null) { diff --git a/src/pipeline.scss b/src/pipeline.scss index 9bc4971..c1ca62e 100644 --- a/src/pipeline.scss +++ b/src/pipeline.scss @@ -62,6 +62,21 @@ $port-color-available: $lime-green; @include grabbing; } +.joint-type-visualdeclaration { + .root { + fill: $step-color; + stroke: $step-color; + } + + .label { + font-weight: 500; + font-family: "Source Sans Pro", sans-serif; + font-size: 16px; + text-decoration: none; + text-transform: none; + } +} + .joint-type-visualstep.selected { .body { stroke: $raspberry; diff --git a/src/visual/VisualDeclaration.js b/src/visual/VisualDeclaration.js new file mode 100644 index 0000000..1aa0d1e --- /dev/null +++ b/src/visual/VisualDeclaration.js @@ -0,0 +1,52 @@ +import _ from 'lodash'; +import joint from 'jointjs'; + +const cDefaultWidth = 12; +const cMinHeight = 50; +const cPixelPerSymbol = 10; + +export default class VisualDeclaration extends joint.shapes.pn.Transition { + + constructor(opts = { declaration: null, x: 0, y: 0 }) { + super(_.defaultsDeep(opts, { + position: { + x: (opts.x - cDefaultWidth / 2) || 0, + y: (opts.y - cMinHeight / 2) || 0, + }, + type: 'VisualDeclaration', + attrs: { + '.label': { + text: opts.declaration.name, + }, + }, + })); + + this.step = opts.declaration.step; + this.declaration = opts.declaration; + } + + // eslint-disable-next-line class-methods-use-this + isPortEnabled() { + return true; + } + + _getLabel() { + return this.declaration.name; + } + + /** + * Obtains bounding box os the element. Overrides Model method. + * @param opts options, see joint.shapes.devs.Model.getBBox + * @returns {*} + */ + getBBox(opts) { + const bbox = super.getBBox(opts); + const declaration = this.declaration; + if (declaration) { + const boxWidth = Math.max((cPixelPerSymbol * declaration.name.length), cDefaultWidth); + bbox.x -= boxWidth / 2; + bbox.width = boxWidth; + } + return bbox; + } +} diff --git a/src/visual/VisualLink.js b/src/visual/VisualLink.js index 710cf53..337c765 100644 --- a/src/visual/VisualLink.js +++ b/src/visual/VisualLink.js @@ -5,7 +5,7 @@ import joint from 'jointjs'; * Class that represents graphical link * @private */ -export default class VisualLink extends joint.shapes.devs.Link { +export default class VisualLink extends joint.shapes.pn.Link { constructor(opts, readOnly) { const defaultLinkaAttr = readOnly ? diff --git a/src/visual/Visualizer.js b/src/visual/Visualizer.js index 208962b..c17802a 100644 --- a/src/visual/Visualizer.js +++ b/src/visual/Visualizer.js @@ -2,11 +2,14 @@ import _ from 'lodash'; import joint, { V } from 'jointjs'; import Workflow from '../model/Workflow'; +import Declaration from '../model/Declaration'; +import Port from '../model/Port'; import Paper from './Paper'; import VisualLink from './VisualLink'; import VisualStep from './VisualStep'; import VisualGroup from './VisualGroup'; import VisualWorkflow from './VisualWorkflow'; +import VisualDeclaration from './VisualDeclaration'; import Zoom from './Zoom'; @@ -176,6 +179,7 @@ export default class Visualizer { this._selectionEnabled = true; this._graph = graph; this._children = {}; + this._declarations = {}; this._timer = null; this._step = null; this.clear(); @@ -259,6 +263,7 @@ export default class Visualizer { this._graph.off('add', null, this); this._graph.clear(); this._children = {}; + this._declarations = {}; this._step = null; this.selection = []; } @@ -332,21 +337,76 @@ export default class Visualizer { }); } - _loopPorts(ports, source, cellsToAdd, isEnabled) { - const children = this._children; - const links = this._graph.getConnectedLinks(source); - _.forEach(ports, (port) => { - if (!isEnabled(port.name)) { - return; - } - _.forEach(port.outputs, (conn) => { + get _connectionProcessors() { + return { + declarationToDeclaration: (conn, visDeclaration, links, cellsToAdd) => { + const declarations = this._declarations; + const targetName = conn.to.name; + if (declarations[targetName] && + _.find(links, link => link.conn === conn) === undefined && + declarations[targetName].isPortEnabled(conn.to.step.hasInputPort(conn.to), conn.to.name)) { + const isReadOnly = this._readOnly || this._isChildSubWorkflow(visDeclaration.declaration.step) || + this._isChildSubWorkflow(declarations[targetName].step); + cellsToAdd[cellsToAdd.length] = new VisualLink({ + source: { + id: visDeclaration.id, + }, + target: { + id: declarations[targetName].id, + }, + conn, + }, isReadOnly); + } + }, + declarationToPort: (conn, visDeclaration, links, cellsToAdd) => { + const children = this._children; + const targetName = generateChildName(conn.to.step); + if (children[targetName] && + _.find(links, link => link.conn === conn) === undefined && + children[targetName].isPortEnabled(conn.to.step.hasInputPort(conn.to), conn.to.name)) { + const isReadOnly = this._readOnly || this._isChildSubWorkflow(visDeclaration.declaration.step) || + this._isChildSubWorkflow(children[targetName].step); + cellsToAdd[cellsToAdd.length] = new VisualLink({ + source: { + id: visDeclaration.id, + }, + target: { + id: children[targetName].id, + port: conn.to.name, + }, + conn, + }, isReadOnly); + } + }, + portToDeclaration: (conn, port, source, links, cellsToAdd) => { + const declarations = this._declarations; + const targetDeclarationName = conn.to.name; + if (declarations[targetDeclarationName] && + _.find(links, link => link.conn === conn) === undefined && + declarations[targetDeclarationName].isPortEnabled()) { + const isReadOnly = this._readOnly || this._isChildSubWorkflow(source.step) || + this._isChildSubWorkflow(declarations[targetDeclarationName].declaration.step); + cellsToAdd[cellsToAdd.length] = new VisualLink({ + source: { + id: source.id, + port: port.name, + }, + target: { + id: declarations[targetDeclarationName].id, + }, + conn, + }, isReadOnly); + } + }, + portToPort: (conn, port, source, links, cellsToAdd) => { + const children = this._children; const targetName = generateChildName(conn.to.step); if (children[targetName] && _.find(links, link => link.conn === conn) === undefined && children[targetName].isPortEnabled(conn.to.step.hasInputPort(conn.to), conn.to.name)) { const isReadOnly = this._readOnly || this._isChildSubWorkflow(source.step) || this._isChildSubWorkflow(children[targetName].step); - const link = new VisualLink({ + cellsToAdd[cellsToAdd.length] = new VisualLink({ source: { id: source.id, port: port.name, @@ -357,7 +417,39 @@ export default class Visualizer { }, conn, }, isReadOnly); - cellsToAdd[cellsToAdd.length] = link; + } + }, + }; + } + + _loopPorts(ports, source, visDeclarations, cellsToAdd, isEnabled) { + const links = this._graph.getConnectedLinks(source); + _.forEach(ports, (port) => { + if (!isEnabled(port.name)) { + return; + } + _.forEach(port.outputs, (conn) => { + if (conn.to instanceof Declaration) { + this._connectionProcessors + .portToDeclaration(conn, port, source, links, cellsToAdd); + } else if (conn.to instanceof Port) { + this._connectionProcessors + .portToPort(conn, port, source, links, cellsToAdd); + } + }); + }); + } + + _loopDeclarations(visDeclarations, cellsToAdd) { + _.forEach(visDeclarations, (visDeclaration) => { + const links = this._graph.getConnectedLinks(visDeclaration); + _.forEach(visDeclaration.declaration.outputs, (conn) => { + if (conn.to instanceof Declaration) { + this._connectionProcessors + .declarationToDeclaration(conn, visDeclaration, links, cellsToAdd); + } else if (conn.to instanceof Port) { + this._connectionProcessors + .declarationToPort(conn, visDeclaration, links, cellsToAdd); } }); }); @@ -371,6 +463,8 @@ export default class Visualizer { // validate call <-> step correspondence const children = this._children; + const declarations = this._declarations; + // handle the renames const pickBy = _.pickBy || _.pick; // be prepared for legacy lodash 3.10.1 const renamed = pickBy(children, (vStep, name) => name !== generateChildName(vStep.step)); @@ -420,15 +514,28 @@ export default class Visualizer { const updateOrCreateVisualSteps = (innerStep, parent = null) => { const name = generateChildName(innerStep); let visChild = children[name]; - const opts = this.zoom.fromWidgetToLocal({ - x: this.paper.el.offsetWidth / 2, - y: this.paper.el.offsetHeight / 2, - }); - opts.step = innerStep; if (!visChild) { + const opts = this.zoom.fromWidgetToLocal({ + x: this.paper.el.offsetWidth / 2, + y: this.paper.el.offsetHeight / 2, + }); + opts.step = innerStep; visChild = createVisual(opts); children[name] = visChild; cellsToAdd[cellsToAdd.length] = visChild; + if (_.size(opts.step.ownDeclarations)) { + _.forEach(opts.step.ownDeclarations, (declaration) => { + let visDeclaration = declarations[declaration.name]; + if (!visDeclaration) { + visDeclaration = new VisualDeclaration({ declaration, x: opts.x, y: opts.y }); + cellsToAdd[cellsToAdd.length] = visDeclaration; + declarations[declaration.name] = visDeclaration; + visChild.embed(visDeclaration); + visChild.fit(); + visChild.update(); + } + }); + } if (parent) { parent.embed(visChild); parent.fit(); @@ -452,10 +559,12 @@ export default class Visualizer { updateOrCreateVisualSteps(step); _.forEach(children, (child) => { - this._loopPorts(child.step.o, child, cellsToAdd, portName => child.isPortEnabled(false, portName)); - this._loopPorts(child.step.i, child, cellsToAdd, portName => child.isPortEnabled(true, portName)); + this._loopPorts(child.step.o, child, declarations, cellsToAdd, portName => child.isPortEnabled(false, portName)); + this._loopPorts(child.step.i, child, declarations, cellsToAdd, portName => child.isPortEnabled(true, portName)); }); + this._loopDeclarations(declarations, cellsToAdd); + this._graph.addCells(cellsToAdd); } diff --git a/test/model/DeclarationTest.js b/test/model/DeclarationTest.js new file mode 100644 index 0000000..e759f2c --- /dev/null +++ b/test/model/DeclarationTest.js @@ -0,0 +1,30 @@ +import { expect } from 'chai'; + +import Declaration from '../../src/model/Declaration'; + + +describe('model/Declaration', () => { + + describe('constructor', () => { + + const declarationAst = { + type: { id: 41, str: 'type', source_string: 'Int', line: 2, col: 3 }, + name: { id: 39, str: 'identifier', source_string: 'foo', line: 2, col: 9 }, + expression: { id: 17, str: 'float', source_string: '9', line: 2, col: 15 }, + }; + const name = 'foo'; + + it('requires a name', () => { + expect(() => new Declaration('', declarationAst, null)).to.throw(Error); + expect(() => new Declaration(name, declarationAst, null)).to.not.throw(Error); + expect(new Declaration(name, declarationAst, null).name).to.equal(name); + }); + + it('requires a declaration ast object with expression and type', () => { + expect(() => new Declaration(name)).to.throws(Error); + expect(() => new Declaration(name, {})).to.throws(Error); + expect(() => new Declaration(name, { type: declarationAst.type })).to.throws(Error); + expect(() => new Declaration(name, { expression: declarationAst.expression })).to.throws(Error); + }); + }); +}); diff --git a/test/model/GroupTest.js b/test/model/GroupTest.js index 47bee06..9e08089 100644 --- a/test/model/GroupTest.js +++ b/test/model/GroupTest.js @@ -2,6 +2,8 @@ import { expect } from 'chai'; import Group from '../../src/model/Group'; import Action from '../../src/model/Action'; +import Workflow from '../../src/model/Workflow'; +import Declaration from '../../src/model/Declaration'; describe('model/Group', () => { @@ -44,4 +46,163 @@ describe('model/Group', () => { expect(() => new Group(name, type, new Action(name, config))).to.throw(Error); }); }); + + describe('.addDeclaration()', () => { + + const declarationAst = { + type: { id: 41, str: 'type', source_string: 'Int', line: 2, col: 3 }, + name: { id: 39, str: 'identifier', source_string: 'bar', line: 2, col: 9 }, + expression: { id: 17, str: 'float', source_string: '9', line: 2, col: 15 }, + }; + + it('links a declaration', () => { + const group = new Group('group'); + const declaration = new Declaration('declaration', declarationAst, null); + group.addDeclaration(declaration); + + expect(declaration.step).to.equal(group); + expect(group.ownDeclarations).to.have.all.keys(['declaration']); + expect(group.ownDeclarations).to.have.property('declaration', declaration); + }); + + it('returns added declaration', () => { + const group = new Group('group'); + const declaration = new Declaration('declaration', declarationAst, null); + + expect(group.addDeclaration(declaration)).to.equal(declaration); + }); + + it('allows multiple declarations', () => { + const group = new Group('group'); + const declaration = new Declaration('declaration', declarationAst, null); + const declarationAst2 = { + type: { id: 41, str: 'type', source_string: 'Int', line: 2, col: 3 }, + name: { id: 39, str: 'identifier', source_string: 'baz', line: 2, col: 9 }, + expression: { id: 17, str: 'float', source_string: '10', line: 2, col: 15 }, + }; + const declaration2 = new Declaration('declaration2', declarationAst2, null); + group.addDeclaration(declaration); + group.addDeclaration(declaration2); + + expect(declaration.step).to.equal(group); + expect(declaration2.step).to.equal(group); + expect(group.ownDeclarations).to.have.all.keys(['declaration', 'declaration2']); + expect(group.ownDeclarations).to.have.property('declaration', declaration); + expect(group.ownDeclarations).to.have.property('declaration2', declaration2); + }); + + it('allows a single parent only', () => { + const momGroup = new Group('mom'); + const dadGroup = new Group('dad'); + const declaration = new Declaration('declaration', declarationAst, null); + momGroup.addDeclaration(declaration); + dadGroup.addDeclaration(declaration); + + expect(declaration.step).to.equal(dadGroup); + expect(momGroup.ownDeclarations).to.be.empty; + expect(dadGroup.ownDeclarations).to.have.all.keys(['declaration']); + }); + + it('forbids equal declaration names', () => { + const group = new Group('group'); + const declaration = group.addDeclaration(new Declaration('declaration', declarationAst, null)); + + expect(() => group.addDeclaration(new Declaration('declaration', declarationAst, null))).to.throw(Error); + expect(declaration.step).to.equal(group); + expect(group.ownDeclarations).to.have.all.keys(['declaration']); + expect(group.ownDeclarations).to.have.property('declaration', declaration); + }); + + it('allows adding twice', () => { + const group = new Group('group'); + const declaration = group.addDeclaration(new Declaration('declaration', declarationAst, null)); + + expect(() => group.addDeclaration(declaration)).to.not.throw(Error); + expect(declaration.step).to.equal(group); + expect(group.ownDeclarations).to.have.all.keys(['declaration']); + expect(group.ownDeclarations).to.have.property('declaration', declaration); + }); + + it('adds a declaration to a parent workflow', () => { + const root = new Workflow('root'); + const group = new Group('group'); + root.add(group); + const declaration = group.addDeclaration(new Declaration('declaration', declarationAst, null)); + + expect(root.declarations).to.have.all.keys(['declaration']); + expect(root.declarations).to.have.property('declaration', declaration); + expect(group.ownDeclarations).to.have.all.keys(['declaration']); + expect(group.ownDeclarations).to.have.property('declaration', declaration); + }); + + it('adds declarations to a grandparent workflow (top-to-bottom)', () => { + const root = new Workflow('root'); + const parentGroup = new Group('parentGroup'); + root.add(parentGroup); + const sonGroup = new Group('sonGroup'); + parentGroup.add(sonGroup); + const declaration = sonGroup.addDeclaration(new Declaration('declaration', declarationAst, null)); + + expect(root.declarations).to.have.all.keys(['declaration']); + expect(root.declarations).to.have.property('declaration', declaration); + expect(parentGroup.ownDeclarations).to.be.empty; + expect(parentGroup.ownDeclarations).not.to.have.property('declaration'); + expect(sonGroup.ownDeclarations).to.have.all.keys(['declaration']); + expect(sonGroup.ownDeclarations).to.have.property('declaration', declaration); + }); + + it('adds declarations to a grandparent workflow (bottom-to-top)', () => { + const root = new Workflow('root'); + const parentGroup = new Group('parentGroup'); + const sonGroup = new Group('sonGroup'); + parentGroup.add(sonGroup); + root.add(parentGroup); + const declaration = sonGroup.addDeclaration(new Declaration('declaration', declarationAst, null)); + + expect(root.declarations).to.have.all.keys(['declaration']); + expect(root.declarations).to.have.property('declaration', declaration); + expect(parentGroup.ownDeclarations).to.be.empty; + expect(parentGroup.ownDeclarations).not.to.have.property('declaration'); + expect(sonGroup.ownDeclarations).to.have.all.keys(['declaration']); + expect(sonGroup.ownDeclarations).to.have.property('declaration', declaration); + }); + }); + + describe('.removeDeclaration()', () => { + + const declarationAst = { + type: { id: 41, str: 'type', source_string: 'Int', line: 2, col: 3 }, + name: { id: 39, str: 'identifier', source_string: 'bar', line: 2, col: 9 }, + expression: { id: 17, str: 'float', source_string: '9', line: 2, col: 15 }, + }; + + it('unlinks a declaration', () => { + const group = new Group('group'); + const declaration = new Declaration('declaration', declarationAst, null); + group.addDeclaration(declaration); + group.removeDeclaration('declaration'); + + expect(declaration.step).to.be.null; + expect(group.ownDeclarations).to.be.empty; + }); + + it('allows missing names', () => { + const group = new Group('group'); + + expect(() => group.removeDeclaration('declaration')).to.not.throw(Error); + }); + + it('removes a declaration from a workflow', () => { + const root = new Workflow('root'); + const group = new Group('group'); + const declaration = new Declaration('declaration', declarationAst, null); + root.add(group); + group.addDeclaration(declaration); + group.removeDeclaration('declaration'); + + expect(root.declarations).to.be.empty; + expect(group.ownDeclarations).to.be.empty; + expect(declaration.step).to.be.null; + }); + }); }); diff --git a/test/parser/WDL/entities/WDLWorkflowTest.js b/test/parser/WDL/entities/WDLWorkflowTest.js index 36750d2..0161324 100644 --- a/test/parser/WDL/entities/WDLWorkflowTest.js +++ b/test/parser/WDL/entities/WDLWorkflowTest.js @@ -94,6 +94,1782 @@ describe('parser/WDL/entities/WDLWorkflow', () => { expect(workflow.workflowStep.action.i).to.have.all.keys(['a', 'b']); }); + it('allows MemberAccess to access not only the call statements', () => { + const ast = { + name: { id: 39, str: 'identifier', source_string: 'ContEstMuTect', line: 10, col: 10 }, + body: { + list: [{ + name: 'Declaration', + attributes: { + type: { + name: 'Type', + attributes: { + name: { id: 41, str: 'type', source_string: 'Array', line: 11, col: 3 }, + subtype: { list: [{ id: 41, str: 'type', source_string: 'String', line: 11, col: 9 }] }, + }, + }, + name: { id: 39, str: 'identifier', source_string: 'bams', line: 11, col: 17 }, + expression: null, + }, + }, { + name: 'Declaration', + attributes: { + type: { + name: 'Type', + attributes: { + name: { id: 41, str: 'type', source_string: 'Array', line: 12, col: 3 }, + subtype: { list: [{ id: 41, str: 'type', source_string: 'String', line: 12, col: 9 }] }, + }, + }, + name: { id: 39, str: 'identifier', source_string: 'bais', line: 12, col: 17 }, + expression: null, + }, + }, { + name: 'Declaration', + attributes: { + type: { id: 41, str: 'type', source_string: 'File', line: 14, col: 3 }, + name: { id: 39, str: 'identifier', source_string: 'inputMAFLite', line: 14, col: 8 }, + expression: { id: 27, str: 'string', source_string: 'test', line: 14, col: 23 }, + }, + }, { + name: 'Declaration', + attributes: { + type: { + name: 'Type', + attributes: { + name: { id: 41, str: 'type', source_string: 'Pair', line: 15, col: 3 }, + subtype: { + list: [{ + id: 41, + str: 'type', + source_string: 'Int', + line: 15, + col: 8, + }, { id: 41, str: 'type', source_string: 'String', line: 15, col: 13 }], + }, + }, + }, + name: { id: 39, str: 'identifier', source_string: 'test', line: 15, col: 21 }, + expression: { + name: 'TupleLiteral', + attributes: { + values: { + list: [{ + id: 34, + str: 'integer', + source_string: '1', + line: 15, + col: 29, + }, { id: 27, str: 'string', source_string: 'one', line: 15, col: 32 }], + }, + }, + }, + }, + }, { + name: 'Declaration', + attributes: { + type: { + name: 'Type', + attributes: { + name: { id: 41, str: 'type', source_string: 'Pair', line: 16, col: 3 }, + subtype: { + list: [{ + id: 41, + str: 'type', + source_string: 'Int', + line: 16, + col: 8, + }, { id: 41, str: 'type', source_string: 'String', line: 16, col: 13 }], + }, + }, + }, + name: { id: 39, str: 'identifier', source_string: 'testTwo', line: 16, col: 21 }, + expression: { + name: 'TupleLiteral', + attributes: { + values: { + list: [{ + id: 34, + str: 'integer', + source_string: '2', + line: 16, + col: 32, + }, { id: 27, str: 'string', source_string: 'two', line: 16, col: 35 }], + }, + }, + }, + }, + }, { + name: 'Declaration', + attributes: { + type: { + name: 'Type', + attributes: { + name: { id: 41, str: 'type', source_string: 'Array', line: 17, col: 3 }, + subtype: { + list: [{ + name: 'Type', + attributes: { + name: { id: 41, str: 'type', source_string: 'Pair', line: 17, col: 9 }, + subtype: { + list: [{ + id: 41, + str: 'type', + source_string: 'String', + line: 17, + col: 14, + }, { id: 41, str: 'type', source_string: 'String', line: 17, col: 22 }], + }, + }, + }], + }, + }, + }, + name: { id: 39, str: 'identifier', source_string: 'bams_and_bais', line: 17, col: 31 }, + expression: { + name: 'FunctionCall', + attributes: { + name: { id: 39, str: 'identifier', source_string: 'zip', line: 17, col: 47 }, + params: { + list: [{ + id: 39, + str: 'identifier', + source_string: 'bams', + line: 17, + col: 51, + }, { id: 39, str: 'identifier', source_string: 'bais', line: 17, col: 57 }], + }, + }, + }, + }, + }, { + name: 'Scatter', + attributes: { + item: { + id: 39, + str: 'identifier', + source_string: 'bam_and_bai', + line: 18, + col: 12, + }, + collection: { id: 39, str: 'identifier', source_string: 'bams_and_bais', line: 18, col: 27 }, + body: { + list: [{ + name: 'Call', + attributes: { + task: { id: 40, str: 'fqn', source_string: 'testTask', line: 19, col: 10 }, + alias: null, + body: { + name: 'CallBody', + attributes: { + declarations: { list: [] }, + io: { + list: [{ + name: 'Inputs', + attributes: { + map: { + list: [{ + name: 'IOMapping', + attributes: { + key: { + id: 39, + str: 'identifier', + source_string: 'bam', + line: 21, + col: 9, + }, + value: { + name: 'MemberAccess', + attributes: { + lhs: { + id: 39, + str: 'identifier', + source_string: 'bam_and_bai', + line: 21, + col: 15, + }, + rhs: { + id: 39, + str: 'identifier', + source_string: 'left', + line: 21, + col: 27, + }, + }, + }, + }, + }, { + name: 'IOMapping', + attributes: { + key: { + id: 39, + str: 'identifier', + source_string: 'bai', + line: 22, + col: 9, + }, + value: { + name: 'MemberAccess', + attributes: { + lhs: { + id: 39, + str: 'identifier', + source_string: 'bam_and_bai', + line: 22, + col: 15, + }, + rhs: { + id: 39, + str: 'identifier', + source_string: 'right', + line: 22, + col: 27, + }, + }, + }, + }, + }], + }, + }, + }], + }, + }, + }, + }, + }], + }, + }, + }, { + name: 'WorkflowOutputs', + attributes: { + outputs: { + list: [{ + name: 'WorkflowOutputWildcard', + attributes: { + fqn: { id: 40, str: 'fqn', source_string: 'inputMAFLite', line: 28, col: 5 }, + wildcard: null, + }, + }, { + name: 'WorkflowOutputWildcard', + attributes: { + fqn: { id: 40, str: 'fqn', source_string: 'test', line: 29, col: 5 }, + wildcard: { id: 10, str: 'asterisk', source_string: '*', line: 29, col: 10 }, + }, + }, { + name: 'WorkflowOutputWildcard', + attributes: { + fqn: { + id: 40, + str: 'fqn', + source_string: 'testTwo.right', + line: 30, + col: 5, + }, + wildcard: null, + }, + }], + }, + }, + }], + }, + }; + const context = { + genericTaskCommandMap: [], + actionMap: { + testTask: { + _handlers: {}, + name: 'testTask', + canHavePorts: true, + i: { bam: { type: 'File', multi: false }, bai: { type: 'File', multi: false } }, + o: { out: { type: 'String', default: 'output', multi: false } }, + data: {}, + }, + ContEstMuTect: { + name: 'ContEstMuTect', + action: { + _handlers: { changed: [[null, null]], 'port-rename': [[null, null]] }, + name: 'ContEstMuTect', + canHavePorts: true, + i: {}, + o: {}, + data: {}, + }, + parent: null, + children: {}, + i: { + bams: { type: 'Array[String]' }, + bais: { type: 'Array[String]' }, + inputMAFLite: { type: 'File', default: 'test' }, + test: { type: 'Pair[Int, String]', default: '(1, "one")' }, + testTwo: { type: 'Pair[Int, String]', default: '(2, "two")' }, + bams_and_bais: { type: 'Array[Pair[String, String]]', default: 'zip(bams, bais)' }, + }, + o: { + inputMAFLite: { type: '', multi: false, default: 'inputMAFLite' }, + 'test.*': { type: '', multi: true, default: 'test.*' }, + 'testTwo.right': { type: '', multi: false, default: 'testTwo.right' }, + }, + type: 'workflow', + ownDeclarations: {}, + actions: { + ContEstMuTect: { + _handlers: { changed: [[null, null]], 'port-rename': [[null, null]] }, + name: 'ContEstMuTect', + canHavePorts: true, + i: {}, + o: {}, + data: {}, + }, + }, + declarations: {}, + isSubWorkflow: false, + }, + }, + }; + + const workflow = new WDLWorkflow(ast, context); + + expect(workflow.workflowStep.o).to.have.all.keys(['inputMAFLite', 'test.*', 'testTwo.right']); + }); + + + it('allows to override subWorkflow\'s declarations in call\'s inputs', () => { + const ast = { + name: { id: 39, str: 'identifier', source_string: 'wf1', line: 1, col: 10 }, + body: { + list: [{ + name: 'Declaration', + attributes: { + type: { id: 41, str: 'type', source_string: 'String', line: 2, col: 3 }, + name: { id: 39, str: 'identifier', source_string: 'test', line: 2, col: 10 }, + expression: { id: 27, str: 'string', source_string: 'test', line: 2, col: 17 }, + }, + }, { + name: 'Call', + attributes: { + task: { id: 40, str: 'fqn', source_string: 'wf2', line: 4, col: 8 }, + alias: null, + body: { + name: 'CallBody', + attributes: { + declarations: { list: [] }, + io: { + list: [{ + name: 'Inputs', + attributes: { + map: { + list: [{ + name: 'IOMapping', + attributes: { + key: { + id: 39, + str: 'identifier', + source_string: 'test', + line: 6, + col: 7, + }, + value: { id: 39, str: 'identifier', source_string: 'test', line: 6, col: 14 }, + }, + }], + }, + }, + }], + }, + }, + }, + }, + }], + }, + }; + const context = { + genericTaskCommandMap: [], + actionMap: { + wf1: { + name: 'wf1', + action: { + _handlers: { changed: [[null, null]], 'port-rename': [[null, null]] }, + name: 'wf1', + canHavePorts: true, + i: {}, + o: {}, + data: {}, + }, + parent: null, + children: {}, + i: { test: { type: 'String', default: 'test' } }, + o: {}, + type: 'workflow', + ownDeclarations: {}, + actions: { + wf1: { + _handlers: { changed: [[null, null]], 'port-rename': [[null, null]] }, + name: 'wf1', + canHavePorts: true, + i: {}, + o: {}, + data: {}, + }, + }, + declarations: {}, + isSubWorkflow: false, + }, + wf2: { + name: 'wf2', + action: { + _handlers: { changed: [[null, null]], 'port-rename': [[null, null]] }, + name: 'wf2', + canHavePorts: true, + i: {}, + o: {}, + data: {}, + }, + parent: null, + children: {}, + i: { test: { type: 'String', default: 'test2' } }, + o: {}, + type: 'workflow', + ownDeclarations: {}, + actions: { + wf2: { + _handlers: { changed: [[null, null]], 'port-rename': [[null, null]] }, + name: 'wf2', + canHavePorts: true, + i: {}, + o: {}, + data: {}, + }, + }, + declarations: {}, + ast: { + name: 'Workflow', + attributes: { + name: { id: 39, str: 'identifier', source_string: 'wf2', line: 10, col: 10 }, + body: { + list: [{ + name: 'Declaration', + attributes: { + type: { id: 41, str: 'type', source_string: 'String', line: 11, col: 3 }, + name: { id: 39, str: 'identifier', source_string: 'test', line: 11, col: 10 }, + expression: { id: 27, str: 'string', source_string: 'test2', line: 11, col: 17 }, + }, + }], + }, + }, + }, + isSubWorkflow: false, + }, + }, + }; + + const workflow = new WDLWorkflow(ast, context); + + expect(workflow.workflowStep.children.wf2.i).to.have.all.keys(['test']); + expect(workflow.workflowStep.children.wf2.ownDeclarations).to.be.empty; + }); + + it('resolves connections for call inputs with declaration with multiple inputs', () => { + const ast = { + name: { + id: 39, + str: 'identifier', + source_string: 'foo', + line: 1, + col: 10, + }, + body: { + list: [{ + name: 'Declaration', + attributes: { + type: { id: 41, str: 'type', source_string: 'String', line: 2, col: 3 }, + name: { id: 39, str: 'identifier', source_string: 'bar', line: 2, col: 10 }, + expression: { id: 27, str: 'string', source_string: 'bar', line: 2, col: 16 }, + }, + }, { + name: 'Declaration', + attributes: { + type: { id: 41, str: 'type', source_string: 'String', line: 3, col: 3 }, + name: { id: 39, str: 'identifier', source_string: 'baz', line: 3, col: 10 }, + expression: { id: 27, str: 'string', source_string: 'baz', line: 3, col: 16 }, + }, + }, { + name: 'Call', + attributes: { + task: { id: 40, str: 'fqn', source_string: 'task1', line: 5, col: 8 }, + alias: null, + body: { + name: 'CallBody', + attributes: { + declarations: { list: [] }, + io: { + list: [{ + name: 'Inputs', + attributes: { + map: { + list: [{ + name: 'IOMapping', + attributes: { + key: { + id: 39, + str: 'identifier', + source_string: 'in', + line: 7, + col: 7, + }, + value: { + name: 'FunctionCall', + attributes: { + name: { + id: 39, + str: 'identifier', + source_string: 'select_first', + line: 7, + col: 12, + }, + params: { + list: [{ + name: 'ArrayLiteral', + attributes: { + values: { + list: [{ + id: 39, + str: 'identifier', + source_string: 'bar', + line: 7, + col: 26, + }, { + id: 39, + str: 'identifier', + source_string: 'baz', + line: 7, + col: 31, + }], + }, + }, + }], + }, + }, + }, + }, + }], + }, + }, + }], + }, + }, + }, + }, + }], + }, + }; + const context = { + genericTaskCommandMap: [], + actionMap: { + task1: { + _handlers: {}, + name: 'task1', + canHavePorts: true, + i: { in: { type: 'String', multi: false } }, + o: {}, + data: {}, + }, + foo: { + name: 'foo', + action: { + _handlers: { changed: [[null, null]], 'port-rename': [[null, null]] }, + name: 'foo', + canHavePorts: true, + i: {}, + o: {}, + data: {}, + }, + parent: null, + children: {}, + i: { bar: { type: 'String', default: 'bar' }, baz: { type: 'String', default: 'baz' } }, + o: {}, + type: 'workflow', + ownDeclarations: {}, + actions: { + foo: { + _handlers: { changed: [[null, null]], 'port-rename': [[null, null]] }, + name: 'foo', + canHavePorts: true, + i: {}, + o: {}, + data: {}, + }, + }, + declarations: {}, + ast: { + name: 'Workflow', + attributes: { + name: { id: 39, str: 'identifier', source_string: 'foo', line: 1, col: 10 }, + body: { + list: [{ + name: 'Declaration', + attributes: { + type: { id: 41, str: 'type', source_string: 'String', line: 2, col: 3 }, + name: { id: 39, str: 'identifier', source_string: 'bar', line: 2, col: 10 }, + expression: { id: 27, str: 'string', source_string: 'bar', line: 2, col: 16 }, + }, + }, { + name: 'Declaration', + attributes: { + type: { id: 41, str: 'type', source_string: 'String', line: 3, col: 3 }, + name: { id: 39, str: 'identifier', source_string: 'baz', line: 3, col: 10 }, + expression: { id: 27, str: 'string', source_string: 'baz', line: 3, col: 16 }, + }, + }, { + name: 'Call', + attributes: { + task: { id: 40, str: 'fqn', source_string: 'task1', line: 5, col: 8 }, + alias: null, + body: { + name: 'CallBody', + attributes: { + declarations: { list: [] }, + io: { + list: [{ + name: 'Inputs', + attributes: { + map: { + list: [{ + name: 'IOMapping', + attributes: { + key: { + id: 39, + str: 'identifier', + source_string: 'in', + line: 7, + col: 7, + }, + value: { + name: 'FunctionCall', + attributes: { + name: { + id: 39, + str: 'identifier', + source_string: 'select_first', + line: 7, + col: 12, + }, + params: { + list: [{ + name: 'ArrayLiteral', + attributes: { + values: { + list: [{ + id: 39, + str: 'identifier', + source_string: 'bar', + line: 7, + col: 26, + }, { + id: 39, + str: 'identifier', + source_string: 'baz', + line: 7, + col: 31, + }], + }, + }, + }], + }, + }, + }, + }, + }], + }, + }, + }], + }, + }, + }, + }, + }], + }, + }, + }, + isSubWorkflow: false, + }, + }, + }; + const workflow = new WDLWorkflow(ast, context); + + expect(workflow.workflowStep.children.task1.i.in.inputs[0].from.name).to.equal('bar'); + expect(workflow.workflowStep.children.task1.i.in.inputs[1].from.name).to.equal('baz'); + }); + + it('expect root workflow to have all declarations', () => { + /* eslint-disable no-template-curly-in-string */ + const ast = { + name: { id: 39, str: 'identifier', source_string: 'foo', line: 1, col: 10 }, + body: { + list: [{ + name: 'Declaration', + attributes: { + type: { id: 41, str: 'type', source_string: 'String', line: 2, col: 3 }, + name: { id: 39, str: 'identifier', source_string: 'bar', line: 2, col: 10 }, + expression: { id: 27, str: 'string', source_string: 'bar', line: 2, col: 16 }, + }, + }, { + name: 'Declaration', + attributes: { + type: { id: 41, str: 'type', source_string: 'String', line: 3, col: 3 }, + name: { id: 39, str: 'identifier', source_string: 'baz', line: 3, col: 10 }, + expression: { id: 27, str: 'string', source_string: 'baz', line: 3, col: 16 }, + }, + }, { + name: 'Declaration', + attributes: { + type: { id: 41, str: 'type', source_string: 'String', line: 4, col: 3 }, + name: { id: 39, str: 'identifier', source_string: 'newBar', line: 4, col: 10 }, + expression: { + name: 'Add', + attributes: { + lhs: { + name: 'Add', + attributes: { + lhs: { id: 39, str: 'identifier', source_string: 'bar', line: 4, col: 19 }, + rhs: { id: 39, str: 'identifier', source_string: 'bar', line: 4, col: 25 }, + }, + }, + rhs: { id: 39, str: 'identifier', source_string: 'baz', line: 4, col: 31 }, + }, + }, + }, + }, { + name: 'Declaration', + attributes: { + type: { id: 41, str: 'type', source_string: 'String', line: 5, col: 3 }, + name: { id: 39, str: 'identifier', source_string: 'newBaz', line: 5, col: 10 }, + expression: { id: 39, str: 'identifier', source_string: 'baz', line: 5, col: 19 }, + }, + }, { + name: 'Declaration', + attributes: { + type: { + name: 'Type', + attributes: { + name: { id: 41, str: 'type', source_string: 'Array', line: 6, col: 3 }, + subtype: { list: [{ id: 41, str: 'type', source_string: 'String', line: 6, col: 9 }] }, + }, + }, + name: { id: 39, str: 'identifier', source_string: 'coll', line: 6, col: 17 }, + expression: { + name: 'ArrayLiteral', + attributes: { + values: { + list: [{ + id: 39, + str: 'identifier', + source_string: 'bar', + line: 6, + col: 25, + }, { + id: 39, + str: 'identifier', + source_string: 'baz', + line: 6, + col: 30, + }, { + id: 39, + str: 'identifier', + source_string: 'newBaz', + line: 6, + col: 35, + }, { + id: 39, + str: 'identifier', + source_string: 'newBar', + line: 6, + col: 43, + }], + }, + }, + }, + }, + }, { + name: 'Scatter', + attributes: { + item: { id: 39, str: 'identifier', source_string: 'item', line: 8, col: 12 }, + collection: { id: 39, str: 'identifier', source_string: 'coll', line: 8, col: 20 }, + body: { + list: [{ + name: 'Call', + attributes: { + task: { id: 40, str: 'fqn', source_string: 'task1', line: 9, col: 10 }, + alias: null, + body: { + name: 'CallBody', + attributes: { + declarations: { list: [] }, + io: { + list: [{ + name: 'Inputs', + attributes: { + map: { + list: [{ + name: 'IOMapping', + attributes: { + key: { + id: 39, + str: 'identifier', + source_string: 'in', + line: 11, + col: 9, + }, + value: { + name: 'TernaryIf', + attributes: { + cond: { + name: 'FunctionCall', + attributes: { + name: { + id: 39, + str: 'identifier', + source_string: 'defined', + line: 11, + col: 17, + }, + params: { + list: [{ + id: 39, + str: 'identifier', + source_string: 'item', + line: 11, + col: 25, + }], + }, + }, + }, + iftrue: { + id: 27, + str: 'string', + source_string: '${item} 1', + line: 11, + col: 36, + }, + iffalse: { + id: 27, + str: 'string', + source_string: 'string', + line: 11, + col: 53, + }, + }, + }, + }, + }], + }, + }, + }], + }, + }, + }, + }, + }, { + name: 'Declaration', + attributes: { + type: { id: 41, str: 'type', source_string: 'String', line: 13, col: 5 }, + name: { id: 39, str: 'identifier', source_string: 'res', line: 13, col: 12 }, + expression: { + name: 'FunctionCall', + attributes: { + name: { + id: 39, + str: 'identifier', + source_string: 'select_first', + line: 13, + col: 18, + }, + params: { + list: [{ + name: 'ArrayLiteral', + attributes: { + values: { + list: [{ + name: 'MemberAccess', + attributes: { + lhs: { + id: 39, + str: 'identifier', + source_string: 'task1', + line: 13, + col: 32, + }, + rhs: { + id: 39, + str: 'identifier', + source_string: 'out', + line: 13, + col: 38, + }, + }, + }, { id: 27, str: 'string', source_string: 'test', line: 13, col: 43 }], + }, + }, + }], + }, + }, + }, + }, + }], + }, + }, + }, { + name: 'Call', + attributes: { + task: { id: 40, str: 'fqn', source_string: 'task2', line: 16, col: 8 }, + alias: null, + body: { + name: 'CallBody', + attributes: { + declarations: { list: [] }, + io: { + list: [{ + name: 'Inputs', + attributes: { + map: { + list: [{ + name: 'IOMapping', + attributes: { + key: { + id: 39, + str: 'identifier', + source_string: 'in', + line: 18, + col: 7, + }, + value: { id: 39, str: 'identifier', source_string: 'res', line: 18, col: 12 }, + }, + }], + }, + }, + }], + }, + }, + }, + }, + }], + }, + }; + const context = { + genericTaskCommandMap: [], + actionMap: { + task1: { + _handlers: {}, + name: 'task1', + canHavePorts: true, + i: { in: { type: 'String', multi: false } }, + o: { out: { type: 'String', default: 'output', multi: false } }, + data: {}, + }, + task2: { + _handlers: {}, + name: 'task2', + canHavePorts: true, + i: { in: { type: 'Array[String]', multi: false } }, + o: {}, + data: {}, + }, + foo: { + name: 'foo', + action: { + _handlers: { changed: [[null, null]], 'port-rename': [[null, null]] }, + name: 'foo', + canHavePorts: true, + i: {}, + o: {}, + data: {}, + }, + parent: null, + children: {}, + i: { + bar: { type: 'String', default: 'bar' }, + baz: { type: 'String', default: 'baz' }, + newBar: { type: 'String', default: 'bar + bar + baz' }, + newBaz: { type: 'String', default: 'baz' }, + coll: { type: 'Array[String]', default: '[bar, baz, newBaz, newBar]' }, + }, + o: {}, + type: 'workflow', + ownDeclarations: {}, + actions: { + foo: { + _handlers: { changed: [[null, null]], 'port-rename': [[null, null]] }, + name: 'foo', + canHavePorts: true, + i: {}, + o: {}, + data: {}, + }, + }, + declarations: {}, + ast: { + name: 'Workflow', + attributes: { + name: { id: 39, str: 'identifier', source_string: 'foo', line: 1, col: 10 }, + body: { + list: [{ + name: 'Declaration', + attributes: { + type: { id: 41, str: 'type', source_string: 'String', line: 2, col: 3 }, + name: { id: 39, str: 'identifier', source_string: 'bar', line: 2, col: 10 }, + expression: { id: 27, str: 'string', source_string: 'bar', line: 2, col: 16 }, + }, + }, { + name: 'Declaration', + attributes: { + type: { id: 41, str: 'type', source_string: 'String', line: 3, col: 3 }, + name: { id: 39, str: 'identifier', source_string: 'baz', line: 3, col: 10 }, + expression: { id: 27, str: 'string', source_string: 'baz', line: 3, col: 16 }, + }, + }, { + name: 'Declaration', + attributes: { + type: { id: 41, str: 'type', source_string: 'String', line: 4, col: 3 }, + name: { id: 39, str: 'identifier', source_string: 'newBar', line: 4, col: 10 }, + expression: { + name: 'Add', + attributes: { + lhs: { + name: 'Add', + attributes: { + lhs: { + id: 39, + str: 'identifier', + source_string: 'bar', + line: 4, + col: 19, + }, + rhs: { id: 39, str: 'identifier', source_string: 'bar', line: 4, col: 25 }, + }, + }, + rhs: { id: 39, str: 'identifier', source_string: 'baz', line: 4, col: 31 }, + }, + }, + }, + }, { + name: 'Declaration', + attributes: { + type: { id: 41, str: 'type', source_string: 'String', line: 5, col: 3 }, + name: { id: 39, str: 'identifier', source_string: 'newBaz', line: 5, col: 10 }, + expression: { id: 39, str: 'identifier', source_string: 'baz', line: 5, col: 19 }, + }, + }, { + name: 'Declaration', + attributes: { + type: { + name: 'Type', + attributes: { + name: { id: 41, str: 'type', source_string: 'Array', line: 6, col: 3 }, + subtype: { + list: [{ + id: 41, + str: 'type', + source_string: 'String', + line: 6, + col: 9, + }], + }, + }, + }, + name: { id: 39, str: 'identifier', source_string: 'coll', line: 6, col: 17 }, + expression: { + name: 'ArrayLiteral', + attributes: { + values: { + list: [ + { id: 39, str: 'identifier', source_string: 'bar', line: 6, col: 25 }, + { id: 39, str: 'identifier', source_string: 'baz', line: 6, col: 30 }, + { id: 39, str: 'identifier', source_string: 'newBaz', line: 6, col: 35 }, + { id: 39, str: 'identifier', source_string: 'newBar', line: 6, col: 43 }, + ], + }, + }, + }, + }, + }, { + name: 'Scatter', + attributes: { + item: { + id: 39, + str: 'identifier', + source_string: 'item', + line: 8, + col: 12, + }, + collection: { id: 39, str: 'identifier', source_string: 'coll', line: 8, col: 20 }, + body: { + list: [{ + name: 'Call', + attributes: { + task: { + id: 40, + str: 'fqn', + source_string: 'task1', + line: 9, + col: 10, + }, + alias: null, + body: { + name: 'CallBody', + attributes: { + declarations: { list: [] }, + io: { + list: [{ + name: 'Inputs', + attributes: { + map: { + list: [{ + name: 'IOMapping', + attributes: { + key: { + id: 39, + str: 'identifier', + source_string: 'in', + line: 11, + col: 9, + }, + value: { + name: 'TernaryIf', + attributes: { + cond: { + name: 'FunctionCall', + attributes: { + name: { + id: 39, + str: 'identifier', + source_string: 'defined', + line: 11, + col: 17, + }, + params: { + list: [{ + id: 39, + str: 'identifier', + source_string: 'item', + line: 11, + col: 25, + }], + }, + }, + }, + iftrue: { + id: 27, + str: 'string', + source_string: '${item} 1', + line: 11, + col: 36, + }, + iffalse: { + id: 27, + str: 'string', + source_string: 'string', + line: 11, + col: 53, + }, + }, + }, + }, + }], + }, + }, + }], + }, + }, + }, + }, + }, { + name: 'Declaration', + attributes: { + type: { + id: 41, + str: 'type', + source_string: 'String', + line: 13, + col: 5, + }, + name: { id: 39, str: 'identifier', source_string: 'res', line: 13, col: 12 }, + expression: { + name: 'FunctionCall', + attributes: { + name: { + id: 39, + str: 'identifier', + source_string: 'select_first', + line: 13, + col: 18, + }, + params: { + list: [{ + name: 'ArrayLiteral', + attributes: { + values: { + list: [{ + name: 'MemberAccess', + attributes: { + lhs: { + id: 39, + str: 'identifier', + source_string: 'task1', + line: 13, + col: 32, + }, + rhs: { + id: 39, + str: 'identifier', + source_string: 'out', + line: 13, + col: 38, + }, + }, + }, { id: 27, str: 'string', source_string: 'test', line: 13, col: 43 }], + }, + }, + }], + }, + }, + }, + }, + }], + }, + }, + }, { + name: 'Call', + attributes: { + task: { id: 40, str: 'fqn', source_string: 'task2', line: 16, col: 8 }, + alias: null, + body: { + name: 'CallBody', + attributes: { + declarations: { list: [] }, + io: { + list: [{ + name: 'Inputs', + attributes: { + map: { + list: [{ + name: 'IOMapping', + attributes: { + key: { + id: 39, + str: 'identifier', + source_string: 'in', + line: 18, + col: 7, + }, + value: { + id: 39, + str: 'identifier', + source_string: 'res', + line: 18, + col: 12, + }, + }, + }], + }, + }, + }], + }, + }, + }, + }, + }], + }, + }, + }, + isSubWorkflow: false, + }, + }, + }; + /* eslint-enable no-template-curly-in-string */ + const workflow = new WDLWorkflow(ast, context); + + expect(workflow.workflowStep.declarations).to.have.all.keys(['bar', 'baz', 'newBar', 'newBaz', 'coll', 'res']); + }); + + it('throws an error when some declaration refers to undeclared variable identifier in a function call', () => { + const ast = { + name: { id: 39, str: 'identifier', source_string: 'foo', line: 1, col: 10 }, + body: { + list: [{ + name: 'Declaration', + attributes: { + type: { id: 41, str: 'type', source_string: 'Float', line: 2, col: 3 }, + name: { id: 39, str: 'identifier', source_string: 'bar', line: 2, col: 9 }, + expression: { id: 17, str: 'float', source_string: '9.54', line: 2, col: 15 }, + }, + }, { + name: 'Declaration', + attributes: { + type: { id: 41, str: 'type', source_string: 'Int', line: 3, col: 3 }, + name: { id: 39, str: 'identifier', source_string: 'baz', line: 3, col: 7 }, + expression: { + name: 'FunctionCall', + attributes: { + name: { id: 39, str: 'identifier', source_string: 'ceil', line: 3, col: 13 }, + params: { + list: [{ + id: 39, + str: 'identifier', + source_string: 'undeclaredBar', + line: 3, + col: 18, + }], + }, + }, + }, + }, + }], + }, + }; + const context = { + genericTaskCommandMap: [], + actionMap: { + foo: { + name: 'foo', + action: { + _handlers: { changed: [[null, null]], 'port-rename': [[null, null]] }, + name: 'foo', + canHavePorts: true, + i: {}, + o: {}, + data: {}, + }, + parent: null, + children: {}, + i: { + bar: { type: 'Float', default: '9.54' }, + baz: { type: 'Int', default: 'ceil(undeclaredBar)' }, + }, + o: {}, + type: 'workflow', + ownDeclarations: {}, + actions: { + foo: { + _handlers: { changed: [[null, null]], 'port-rename': [[null, null]] }, + name: 'foo', + canHavePorts: true, + i: {}, + o: {}, + data: {}, + }, + }, + declarations: {}, + ast: { + name: 'Workflow', + attributes: { + name: { id: 39, str: 'identifier', source_string: 'foo', line: 1, col: 10 }, + body: { + list: [{ + name: 'Declaration', + attributes: { + type: { id: 41, str: 'type', source_string: 'Float', line: 2, col: 3 }, + name: { id: 39, str: 'identifier', source_string: 'bar', line: 2, col: 9 }, + expression: { id: 17, str: 'float', source_string: '9.54', line: 2, col: 15 }, + }, + }, { + name: 'Declaration', + attributes: { + type: { id: 41, str: 'type', source_string: 'Int', line: 3, col: 3 }, + name: { id: 39, str: 'identifier', source_string: 'baz', line: 3, col: 7 }, + expression: { + name: 'FunctionCall', + attributes: { + name: { + id: 39, + str: 'identifier', + source_string: 'ceil', + line: 3, + col: 13, + }, + params: { + list: [{ + id: 39, + str: 'identifier', + source_string: 'undeclaredBar', + line: 3, + col: 18, + }], + }, + }, + }, + }, + }], + }, + }, + }, + isSubWorkflow: false, + }, + }, + }; + + expect(() => new WDLWorkflow(ast, context)).to.throws(WDLParserError); + }); + + it('throws an error when some declaration refers to undeclared call in a function call', () => { + const ast = { + name: { id: 39, str: 'identifier', source_string: 'foo', line: 1, col: 10 }, + body: { + list: [{ + name: 'Declaration', + attributes: { + type: { id: 41, str: 'type', source_string: 'Float', line: 2, col: 3 }, + name: { id: 39, str: 'identifier', source_string: 'bar', line: 2, col: 9 }, + expression: { id: 17, str: 'float', source_string: '9.54', line: 2, col: 15 }, + }, + }, { + name: 'Declaration', + attributes: { + type: { id: 41, str: 'type', source_string: 'Int', line: 3, col: 3 }, + name: { id: 39, str: 'identifier', source_string: 'baz', line: 3, col: 7 }, + expression: { + name: 'FunctionCall', + attributes: { + name: { id: 39, str: 'identifier', source_string: 'ceil', line: 3, col: 13 }, + params: { + list: [{ + name: 'MemberAccess', + attributes: { + lhs: { + id: 39, + str: 'identifier', + source_string: 'undeclaredCall', + line: 3, + col: 18, + }, + rhs: { id: 39, str: 'identifier', source_string: 'undeclaredBar', line: 3, col: 33 }, + }, + }], + }, + }, + }, + }, + }], + }, + }; + const context = { + genericTaskCommandMap: [], + actionMap: { + foo: { + name: 'foo', + action: { + _handlers: { changed: [[null, null]], 'port-rename': [[null, null]] }, + name: 'foo', + canHavePorts: true, + i: {}, + o: {}, + data: {}, + }, + parent: null, + children: {}, + i: { + bar: { type: 'Float', default: '9.54' }, + baz: { type: 'Int', default: 'ceil(undeclaredCall.undeclaredBar)' }, + }, + o: {}, + type: 'workflow', + ownDeclarations: {}, + actions: { + foo: { + _handlers: { changed: [[null, null]], 'port-rename': [[null, null]] }, + name: 'foo', + canHavePorts: true, + i: {}, + o: {}, + data: {}, + }, + }, + declarations: {}, + ast: { + name: 'Workflow', + attributes: { + name: { id: 39, str: 'identifier', source_string: 'foo', line: 1, col: 10 }, + body: { + list: [{ + name: 'Declaration', + attributes: { + type: { id: 41, str: 'type', source_string: 'Float', line: 2, col: 3 }, + name: { id: 39, str: 'identifier', source_string: 'bar', line: 2, col: 9 }, + expression: { id: 17, str: 'float', source_string: '9.54', line: 2, col: 15 }, + }, + }, { + name: 'Declaration', + attributes: { + type: { id: 41, str: 'type', source_string: 'Int', line: 3, col: 3 }, + name: { id: 39, str: 'identifier', source_string: 'baz', line: 3, col: 7 }, + expression: { + name: 'FunctionCall', + attributes: { + name: { + id: 39, + str: 'identifier', + source_string: 'ceil', + line: 3, + col: 13, + }, + params: { + list: [{ + name: 'MemberAccess', + attributes: { + lhs: { + id: 39, + str: 'identifier', + source_string: 'undeclaredCall', + line: 3, + col: 18, + }, + rhs: { + id: 39, + str: 'identifier', + source_string: 'undeclaredBar', + line: 3, + col: 33, + }, + }, + }], + }, + }, + }, + }, + }], + }, + }, + }, + isSubWorkflow: false, + }, + }, + }; + + expect(() => new WDLWorkflow(ast, context)).to.throws(WDLParserError); + }); + + it('throws an error when some declaration refers to undeclared member access variable in a function call', () => { + const ast = { + name: { id: 39, str: 'identifier', source_string: 'foo', line: 1, col: 10 }, + body: { + list: [{ + name: 'Declaration', + attributes: { + type: { id: 41, str: 'type', source_string: 'Float', line: 2, col: 3 }, + name: { id: 39, str: 'identifier', source_string: 'bar', line: 2, col: 9 }, + expression: { id: 17, str: 'float', source_string: '9.54', line: 2, col: 15 }, + }, + }, { + name: 'Call', + attributes: { + task: { id: 40, str: 'fqn', source_string: 'task1', line: 3, col: 8 }, + alias: null, + body: { + name: 'CallBody', + attributes: { + declarations: { list: [] }, + io: { + list: [{ + name: 'Inputs', + attributes: { + map: { + list: [{ + name: 'IOMapping', + attributes: { + key: { + id: 39, + str: 'identifier', + source_string: 'in', + line: 5, + col: 7, + }, + value: { id: 39, str: 'identifier', source_string: 'bar', line: 5, col: 12 }, + }, + }], + }, + }, + }], + }, + }, + }, + }, + }, { + name: 'Declaration', + attributes: { + type: { id: 41, str: 'type', source_string: 'Int', line: 7, col: 3 }, + name: { id: 39, str: 'identifier', source_string: 'baz', line: 7, col: 7 }, + expression: { + name: 'FunctionCall', + attributes: { + name: { id: 39, str: 'identifier', source_string: 'ceil', line: 7, col: 13 }, + params: { + list: [{ + name: 'MemberAccess', + attributes: { + lhs: { + id: 39, + str: 'identifier', + source_string: 'task1', + line: 7, + col: 18, + }, + rhs: { + id: 39, + str: 'identifier', + source_string: 'undeclaredOutput', + line: 7, + col: 24, + }, + }, + }], + }, + }, + }, + }, + }], + }, + }; + const context = { + genericTaskCommandMap: [], + actionMap: { + task1: { + _handlers: {}, + name: 'task1', + canHavePorts: true, + i: { in: { type: 'Float', multi: false } }, + o: {}, + data: {}, + }, + foo: { + name: 'foo', + action: { + _handlers: { changed: [[null, null]], 'port-rename': [[null, null]] }, + name: 'foo', + canHavePorts: true, + i: {}, + o: {}, + data: {}, + }, + parent: null, + children: {}, + i: { + bar: { type: 'Float', default: '9.54' }, + baz: { type: 'Int', default: 'ceil(task1.undeclaredOutput)' }, + }, + o: {}, + type: 'workflow', + ownDeclarations: {}, + actions: { + foo: { + _handlers: { changed: [[null, null]], 'port-rename': [[null, null]] }, + name: 'foo', + canHavePorts: true, + i: {}, + o: {}, + data: {}, + }, + }, + declarations: {}, + ast: { + name: 'Workflow', + attributes: { + name: { id: 39, str: 'identifier', source_string: 'foo', line: 1, col: 10 }, + body: { + list: [{ + name: 'Declaration', + attributes: { + type: { id: 41, str: 'type', source_string: 'Float', line: 2, col: 3 }, + name: { id: 39, str: 'identifier', source_string: 'bar', line: 2, col: 9 }, + expression: { id: 17, str: 'float', source_string: '9.54', line: 2, col: 15 }, + }, + }, { + name: 'Call', + attributes: { + task: { id: 40, str: 'fqn', source_string: 'task1', line: 3, col: 8 }, + alias: null, + body: { + name: 'CallBody', + attributes: { + declarations: { list: [] }, + io: { + list: [{ + name: 'Inputs', + attributes: { + map: { + list: [{ + name: 'IOMapping', + attributes: { + key: { + id: 39, + str: 'identifier', + source_string: 'in', + line: 5, + col: 7, + }, + value: { + id: 39, + str: 'identifier', + source_string: 'bar', + line: 5, + col: 12, + }, + }, + }], + }, + }, + }], + }, + }, + }, + }, + }, { + name: 'Declaration', + attributes: { + type: { id: 41, str: 'type', source_string: 'Int', line: 7, col: 3 }, + name: { id: 39, str: 'identifier', source_string: 'baz', line: 7, col: 7 }, + expression: { + name: 'FunctionCall', + attributes: { + name: { + id: 39, + str: 'identifier', + source_string: 'ceil', + line: 7, + col: 13, + }, + params: { + list: [{ + name: 'MemberAccess', + attributes: { + lhs: { + id: 39, + str: 'identifier', + source_string: 'task1', + line: 7, + col: 18, + }, + rhs: { + id: 39, + str: 'identifier', + source_string: 'undeclaredOutput', + line: 7, + col: 24, + }, + }, + }], + }, + }, + }, + }, + }], + }, + }, + }, + isSubWorkflow: false, + }, + }, + }; + + expect(() => new WDLWorkflow(ast, context)).to.throws(WDLParserError); + }); + it('throws error when workflow declarations are obtained not only at start', () => { const ast = { name: { diff --git a/test/parser/WDL/parseTest.js b/test/parser/WDL/parseTest.js index 037afeb..faef003 100644 --- a/test/parser/WDL/parseTest.js +++ b/test/parser/WDL/parseTest.js @@ -26,21 +26,6 @@ workflow foo { return expect(parse(src)).to.be.fulfilled; }); - it('returns with error flag if source syntax is incorrect', () => { - const src = ` -workflow foo { - File a - call b - File c -} - -task b { - File a -}`; - - return expect(parse(src)).to.be.rejected; - }); - it('requires to parse valid wdl script', () => { const flow = new Workflow('example1', { i: { diff --git a/test/parser/WDL/utils/renaming_utilsTest.js b/test/parser/WDL/utils/renaming_utilsTest.js new file mode 100644 index 0000000..9ade7bf --- /dev/null +++ b/test/parser/WDL/utils/renaming_utilsTest.js @@ -0,0 +1,380 @@ +import { expect } from 'chai'; + +import { renameExpression, replaceSplitter } from '../../../../src/parser/WDL/utils/renaming_utils'; + +describe('parser/WDL/renaming_utils', () => { + describe('.replaceSplitter()', () => { + it('expect to replace splitter in source string', () => { + expect(replaceSplitter('ns.name')).to.equal('ns_name'); + expect(replaceSplitter('ns.name.var')).to.equal('ns_name.var'); + }); + }); + + describe('.renameExpression()', () => { + it('requires an empty ast', () => { + const astUndefined = undefined; + renameExpression(astUndefined); + expect(astUndefined).to.equal(undefined); + const astNull = null; + renameExpression(astNull); + expect(astNull).to.equal(null); + const astEmpty = ''; + renameExpression(astEmpty); + expect(astEmpty).to.equal(''); + }); + + it('renames an identifier within expression with map literal and function call', () => { + const ast = { + name: 'MapLiteral', + attributes: { + map: { + list: [ + { + name: 'MapLiteralKv', + attributes: { + key: { + id: 2, + str: 'integer', + source_string: '1', + line: 18, + col: 28, + }, + value: { + id: 18, + str: 'string', + source_string: 'a', + line: 18, + col: 32, + }, + }, + }, + { + name: 'MapLiteralKv', + attributes: { + key: { + id: 2, + str: 'integer', + source_string: '3', + line: 18, + col: 37, + }, + value: { + name: 'FunctionCall', + attributes: { + name: { + id: 14, + str: 'identifier', + source_string: 'read_string', + line: 18, + col: 41, + }, + params: { + list: [ + { + name: 'MemberAccess', + attributes: { + lhs: { + id: 14, + str: 'identifier', + source_string: 'bar', + line: 16, + col: 16, + }, + rhs: { + id: 14, + str: 'identifier', + source_string: 'out1', + line: 16, + col: 20, + }, + }, + }, + { + id: 2, + str: 'integer', + source_string: '2', + line: 18, + col: 56, + }, + ], + }, + }, + }, + }, + }, + ], + }, + }, + }; + const prefix = 'pre_'; + const initialCalls = ['ns.bar', 'ns2.baz']; + + renameExpression(ast, prefix, initialCalls); + + expect(ast.attributes.map.list[1].attributes.value.attributes.params.list[0].attributes.lhs.source_string).to.equal('pre_ns_bar'); + }); + + it('renames multiple identifiers within expression with array and object literals', () => { + const ast = { + name: 'ArrayLiteral', + attributes: { + values: { + list: [ + { + name: 'ObjectLiteral', + attributes: { + map: { + list: [ + { + name: 'ObjectKV', + attributes: { + key: { + id: 14, + str: 'identifier', + source_string: 'o1', + line: 17, + col: 25, + }, + value: { + name: 'MemberAccess', + attributes: { + lhs: { + id: 14, + str: 'identifier', + source_string: 'bar', + line: 17, + col: 30, + }, + rhs: { + id: 14, + str: 'identifier', + source_string: 'out1', + line: 17, + col: 34, + }, + }, + }, + }, + }, + { + name: 'ObjectKV', + attributes: { + key: { + id: 14, + str: 'identifier', + source_string: 'o1', + line: 17, + col: 25, + }, + value: { + name: 'MemberAccess', + attributes: { + lhs: { + id: 14, + str: 'identifier', + source_string: 'bar', + line: 17, + col: 45, + }, + rhs: { + id: 14, + str: 'identifier', + source_string: 'out2', + line: 17, + col: 49, + }, + }, + }, + }, + }, + ], + }, + }, + }, + { + name: 'MemberAccess', + attributes: { + lhs: { + id: 14, + str: 'identifier', + source_string: 'baz', + line: 17, + col: 30, + }, + rhs: { + id: 14, + str: 'identifier', + source_string: 'out', + line: 17, + col: 34, + }, + }, + }, + ], + }, + }, + }; + const prefix = 'pre_'; + const initialCalls = ['ns.bar', 'ns2.baz']; + + renameExpression(ast, prefix, initialCalls); + + expect(ast.attributes.values.list[0].attributes.map.list[0].attributes.value.attributes.lhs.source_string).to.equal('pre_ns_bar'); + expect(ast.attributes.values.list[0].attributes.map.list[1].attributes.value.attributes.lhs.source_string).to.equal('pre_ns_bar'); + expect(ast.attributes.values.list[1].attributes.lhs.source_string).to.equal('pre_ns2_baz'); + }); + + it('renames an identifier within expression with binary and unary operations', () => { + const ast = { + name: 'Add', + attributes: { + lhs: { + name: 'LogicalNot', + attributes: { + expression: { + name: 'MemberAccess', + attributes: { + lhs: { + id: 14, + str: 'identifier', + source_string: 'baz', + line: 16, + col: 16, + }, + rhs: { + id: 14, + str: 'identifier', + source_string: 'out1', + line: 16, + col: 20, + }, + }, + }, + }, + }, + rhs: { + name: 'Multiply', + attributes: { + lhs: { + id: 2, + str: 'integer', + source_string: '5', + line: 14, + col: 17, + }, + rhs: { + id: 2, + str: 'integer', + source_string: '4', + line: 14, + col: 22, + }, + }, + }, + }, + }; + const prefix = 'pre_'; + const initialCalls = ['ns.baz']; + + renameExpression(ast, prefix, initialCalls); + + expect(ast.attributes.lhs.attributes.expression.attributes.lhs.source_string).to.equal('pre_ns_baz'); + }); + + it('renames an identifier within expression with tuple literal and ternary', () => { + const ast = { + name: 'TernaryIf', + attributes: { + cond: { + name: 'TupleLiteral', + attributes: { + values: { + list: [ + { + name: 'LogicalAnd', + attributes: { + lhs: { + name: 'LogicalNot', + attributes: { + expression: { + name: 'MemberAccess', + attributes: { + lhs: { + id: 14, + str: 'identifier', + source_string: 'baz', + line: 16, + col: 16, + }, + rhs: { + id: 14, + str: 'identifier', + source_string: 'out1', + line: 16, + col: 20, + }, + }, + }, + }, + }, + rhs: { + name: 'MemberAccess', + attributes: { + lhs: { + id: 14, + str: 'identifier', + source_string: 'foo', + line: 16, + col: 16, + }, + rhs: { + id: 14, + str: 'identifier', + source_string: 'out1', + line: 16, + col: 20, + }, + }, + }, + }, + }, + ], + }, + }, + }, + iftrue: { + name: 'LogicalNot', + attributes: { + expression: { + id: 2, + str: 'integer', + source_string: '2', + line: 14, + col: 12, + }, + }, + }, + iffalse: { + name: 'LogicalNot', + attributes: { + expression: { + id: 2, + str: 'integer', + source_string: '3', + line: 14, + col: 12, + }, + }, + }, + }, + }; + const prefix = 'pre_'; + const initialCalls = ['ns.foo', 'ns.bar', 'ns.baz']; + + renameExpression(ast, prefix, initialCalls); + + expect(ast.attributes.cond.attributes.values.list[0].attributes.lhs.attributes.expression.attributes.lhs.source_string).to.equal('pre_ns_baz'); + expect(ast.attributes.cond.attributes.values.list[0].attributes.rhs.attributes.lhs.source_string).to.equal('pre_ns_foo'); + }); + }); +}); diff --git a/test/parser/parseTest.js b/test/parser/parseTest.js index 4cf7232..b43b040 100644 --- a/test/parser/parseTest.js +++ b/test/parser/parseTest.js @@ -65,8 +65,11 @@ workflow RootWorkflow { resolve(data); } }); - }).then(data => parse(wdl, { zipFile: data, subWfDetailing: '*', recursionDepth: 10 }))).to.be.fulfilled); + }).then(data => parse(wdl, { zipFile: data, subWfDetailing: ['*'], recursionDepth: 10 }))).to.be.fulfilled); it('returns with error if source zip is incorrect', () => expect(parse(wdl, { zipFile: 'test' })).to.be.rejectedWith('Error')); + + it('returns with error if no wdl presented', () => + expect(parse('')).to.be.rejected); });