From 8e9fb41623aefdedd9b781026c9248e1425022fe Mon Sep 17 00:00:00 2001 From: Timofey Alyakin Date: Tue, 27 Feb 2018 15:58:43 +0300 Subject: [PATCH 01/16] allowing declarations to be placed after other things of current scope --- src/parser/WDL/entities/WDLWorkflow.js | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/src/parser/WDL/entities/WDLWorkflow.js b/src/parser/WDL/entities/WDLWorkflow.js index 3f503bf..46d7e63 100644 --- a/src/parser/WDL/entities/WDLWorkflow.js +++ b/src/parser/WDL/entities/WDLWorkflow.js @@ -46,19 +46,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`); From ce38d57f26f1c713656abf670fdf009bf96e6aaf Mon Sep 17 00:00:00 2001 From: Timofey Alyakin Date: Tue, 27 Feb 2018 16:20:32 +0300 Subject: [PATCH 02/16] updated tests --- test/parser/WDL/parseTest.js | 15 --------------- 1 file changed, 15 deletions(-) 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: { From b81a852e2e0860d19e5aa0e6ca3a281b822b97b5 Mon Sep 17 00:00:00 2001 From: Timofey Alyakin Date: Fri, 2 Mar 2018 15:50:01 +0300 Subject: [PATCH 03/16] fixed Errors like: "Context initialization failed due to WDLParserError: Undeclared call is referenced: 'Utils_ConvertToCram'" when calling 'call ns.name as name' --- src/parser/WDL/parse.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/parser/WDL/parse.js b/src/parser/WDL/parse.js index 61a9211..f7360f0 100644 --- a/src/parser/WDL/parse.js +++ b/src/parser/WDL/parse.js @@ -139,7 +139,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) { @@ -177,7 +177,7 @@ function renameWfAstNames(node, prefix) { 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); From 236fe5e1ec4a52f38ccbc781c88b803e195e1a8a Mon Sep 17 00:00:00 2001 From: Timofey Alyakin Date: Fri, 2 Mar 2018 19:16:55 +0300 Subject: [PATCH 04/16] [ Support for declarations in (call, if, scatter & while) blocks ] * added Declaration instance to model * knowing inputs & declarations apart * declarations in blocks aren't causing app to crash --- src/model/Declaration.js | 36 +++++++++++++++++ src/model/Group.js | 55 ++++++++++++++++++++++++++ src/model/Workflow.js | 6 +++ src/parser/WDL/entities/WDLWorkflow.js | 40 ++++++++++--------- src/parser/WDL/utils/utils.js | 2 +- 5 files changed, 119 insertions(+), 20 deletions(-) create mode 100644 src/model/Declaration.js diff --git a/src/model/Declaration.js b/src/model/Declaration.js new file mode 100644 index 0000000..8c2ed38 --- /dev/null +++ b/src/model/Declaration.js @@ -0,0 +1,36 @@ +import Port from './Port'; +import Action from './Action'; +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 (declaration instanceof Action) { + throw new Error('Group 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..ad00822 100644 --- a/src/model/Group.js +++ b/src/model/Group.js @@ -34,6 +34,61 @@ 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.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; + + root.declarations[declaration.name] = declaration; + this.ownDeclarations[declaration.name] = declaration; + } else if (existingInRoot !== 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.declarations[name]; + const declaration = this.ownDeclarations[name]; + if (declarationInRoot) { + declarationInRoot.parent = null; + delete workflow.declarations[name]; + } + if (declaration) { + declaration.parent = null; + delete this.ownDeclarations[name]; + } } } 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 46d7e63..83fcd25 100644 --- a/src/parser/WDL/entities/WDLWorkflow.js +++ b/src/parser/WDL/entities/WDLWorkflow.js @@ -3,6 +3,7 @@ import _ from 'lodash'; import Workflow from '../../../model/Workflow'; import Step from '../../../model/Step'; import Group from '../../../model/Group'; +import Declaration from '../../../model/Declaration'; import { extractExpression, extractType, extractMetaBlock, WDLParserError } from '../utils/utils'; import * as Constants from '../constants'; @@ -203,19 +204,17 @@ export default class WDLWorkflow { const name = decl.name.source_string; const type = extractType(decl.type); - const obj = {}; - obj[name] = { - type, - }; - - const str = extractExpression(decl.expression).string; - if (str !== '') { - obj[name].default = str; + 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 + parentStep.addDeclaration(new Declaration(name, decl, parentStep)); } - - parentStep.action.addPorts({ - i: obj, - }); } /** @@ -303,12 +302,7 @@ 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) { @@ -372,6 +366,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); } @@ -398,7 +396,11 @@ export default class WDLWorkflow { } 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}'`); } diff --git a/src/parser/WDL/utils/utils.js b/src/parser/WDL/utils/utils.js index b6eddaf..357f810 100644 --- a/src/parser/WDL/utils/utils.js +++ b/src/parser/WDL/utils/utils.js @@ -242,7 +242,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) { From 349930ff18544942f8ed9863dbbe81b4b7eb2145 Mon Sep 17 00:00:00 2001 From: Timofey Alyakin Date: Wed, 7 Mar 2018 14:50:54 +0300 Subject: [PATCH 05/16] [ Support for declarations in (call, if, scatter & while) blocks ] * added VisualDeclaration instance * visualization for Declarations was added to Visualizer * added links expression types like 'FunctionCall', 'ArrayOrMapLookup', 'ArrayLiteral' and so on * changed links type --- src/parser/WDL/entities/WDLWorkflow.js | 72 ++++++++++++- src/parser/WDL/utils/utils.js | 10 +- src/pipeline.scss | 15 +++ src/visual/VisualDeclaration.js | 34 +++++++ src/visual/VisualLink.js | 2 +- src/visual/Visualizer.js | 133 ++++++++++++++++++++++--- 6 files changed, 249 insertions(+), 17 deletions(-) create mode 100644 src/visual/VisualDeclaration.js diff --git a/src/parser/WDL/entities/WDLWorkflow.js b/src/parser/WDL/entities/WDLWorkflow.js index 83fcd25..eada97c 100644 --- a/src/parser/WDL/entities/WDLWorkflow.js +++ b/src/parser/WDL/entities/WDLWorkflow.js @@ -84,6 +84,7 @@ export default class WDLWorkflow { const collection = extractExpression(item.attributes.collection); + // in scatter the item is always an identifier, so it'll always be one port returned from .getPortForBinding() not an array const port = WDLWorkflow.getPortForBinding(this.workflowStep, parent, collection); opts.i[itemName] = {}; @@ -186,7 +187,12 @@ export default class WDLWorkflow { const expression = extractExpression(nodeValue); if (step.i[declaration]) { - step.i[declaration].bind(WDLWorkflow.getPortForBinding(this.workflowStep, parentStep, expression)); + const bindings = WDLWorkflow.getPortForBinding(this.workflowStep, parentStep, expression); + if (bindings && !Array.isArray(bindings)) { + step.i[declaration].bind(bindings); + } else if (bindings) { + _.forEach(bindings, binding => step.i[declaration].bind(binding)); + } } else { throw new WDLParserError(`Undeclared variable trying to be assigned: call '${step.name}' --> '${declaration}'`); } @@ -213,7 +219,15 @@ export default class WDLWorkflow { i: obj, }); } else if (parentStep instanceof Group) { // declaration is a "variable" type - parentStep.addDeclaration(new Declaration(name, decl, parentStep)); + const declaration = new Declaration(name, decl, parentStep); + const bindings = WDLWorkflow.getPortForBinding(this.workflowStep, declaration.step, declaration.expression); + if (bindings && !Array.isArray(bindings)) { + declaration.bind(bindings); + } else if (bindings) { + _.forEach(bindings, binding => declaration.bind(binding)); + } + + parentStep.addDeclaration(declaration); } } @@ -378,6 +392,33 @@ export default class WDLWorkflow { } static getPortForBinding(workflow, parent, expression) { + const accessesTypes = [ + 'ArrayOrMapLookup', + 'FunctionCall', + 'ArrayLiteral', + 'ObjectLiteral', + 'MapLiteral', + 'TupleLiteral', + 'LogicalNot', + 'UnaryPlus', + 'UnaryNegation', + 'Add', + 'Subtract', + 'Multiply', + 'Divide', + 'Remainder', + 'LogicalOr', + 'LogicalAnd', + 'Equals', + 'NotEquals', + 'LessThan', + 'LessThanOrEqual', + 'GreaterThan', + 'GreaterThanOrEqual', + 'TernaryIf', + 'MapLiteralKv', + 'ObjectKV', + ]; let binder = expression.string; if (expression.type === 'MemberAccess') { const rhsPart = expression.accesses[0].rhs; @@ -393,6 +434,33 @@ export default class WDLWorkflow { } else { throw new WDLParserError(`Undeclared call is referenced: '${lhsPart}'`); } + } else 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) { + if (outputStep.o[accesses.rhs]) { + binder.push(outputStep.o[accesses.rhs]); + } else { + throw new WDLParserError(`Undeclared variable is referenced: '${accesses.lhs}.${accesses.rhs}'`); + } + } else { + throw new WDLParserError(`Undeclared call 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 is referenced: '${expression.string}'`); + } + } + }); } else if (expression.type === 'identifier') { const desiredStep = WDLWorkflow.groupNameResolver(parent, expression.string); if (desiredStep) { diff --git a/src/parser/WDL/utils/utils.js b/src/parser/WDL/utils/utils.js index 357f810..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', 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..ba07762 --- /dev/null +++ b/src/visual/VisualDeclaration.js @@ -0,0 +1,34 @@ +import _ from 'lodash'; +import * as joint from 'jointjs'; + +const cDefaultWidth = 100; +const cMinHeight = 100; + +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; + } + + isPortEnabled() { + return true; + } + + _getLabel() { + return this.declaration.name; + } +} 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..a849e37 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)); @@ -429,6 +523,19 @@ export default class Visualizer { 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 }); + 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); } From 40ef7bbdf6284e39e1bfea448230e8de56c501c2 Mon Sep 17 00:00:00 2001 From: Timofey Alyakin Date: Wed, 7 Mar 2018 14:58:20 +0300 Subject: [PATCH 06/16] [ Support for declarations in (call, if, scatter & while) blocks ] * fixed eslint error 'class-method-use-this' --- src/visual/VisualDeclaration.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/visual/VisualDeclaration.js b/src/visual/VisualDeclaration.js index ba07762..fd7391f 100644 --- a/src/visual/VisualDeclaration.js +++ b/src/visual/VisualDeclaration.js @@ -24,6 +24,7 @@ export default class VisualDeclaration extends joint.shapes.pn.Transition { this.declaration = opts.declaration; } + // eslint-disable-next-line class-methods-use-this isPortEnabled() { return true; } From da663f56b4a8a1fd9e3d4981a79224aee98f1f4c Mon Sep 17 00:00:00 2001 From: Timofey Alyakin Date: Mon, 12 Mar 2018 13:37:15 +0300 Subject: [PATCH 07/16] [ Support for declarations in (call, if, scatter & while) blocks ] * WDLWorkflow tests were added --- test/parser/WDL/entities/WDLWorkflowTest.js | 1301 +++++++++++++++++++ 1 file changed, 1301 insertions(+) diff --git a/test/parser/WDL/entities/WDLWorkflowTest.js b/test/parser/WDL/entities/WDLWorkflowTest.js index 36750d2..424f85d 100644 --- a/test/parser/WDL/entities/WDLWorkflowTest.js +++ b/test/parser/WDL/entities/WDLWorkflowTest.js @@ -94,6 +94,1307 @@ describe('parser/WDL/entities/WDLWorkflow', () => { expect(workflow.workflowStep.action.i).to.have.all.keys(['a', 'b']); }); + 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', () => { + 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, + }, + }, + }; + 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: { From d9abc434fb1df08a06a5ac6dccbc06d03430615e Mon Sep 17 00:00:00 2001 From: Timofey Alyakin Date: Mon, 12 Mar 2018 16:08:24 +0300 Subject: [PATCH 08/16] [ Support for declarations in (call, if, scatter & while) blocks ] * Group tests were added --- src/model/Group.js | 14 +- test/model/GroupTest.js | 161 ++++++++++++++++++++ test/parser/WDL/entities/WDLWorkflowTest.js | 2 + 3 files changed, 171 insertions(+), 6 deletions(-) diff --git a/src/model/Group.js b/src/model/Group.js index ad00822..6a4ce84 100644 --- a/src/model/Group.js +++ b/src/model/Group.js @@ -52,7 +52,7 @@ class Group extends Step { */ addDeclaration(declaration) { const root = this.workflow(); - const existingInRoot = root.declarations[declaration.name]; + const existingInRoot = root && root.declarations[declaration.name]; const ownExisting = this.ownDeclarations[declaration.name]; if (!existingInRoot && !ownExisting) { if (declaration.step !== null) { @@ -61,9 +61,11 @@ class Group extends Step { declaration.step = this; - root.declarations[declaration.name] = declaration; + if (root) { + root.declarations[declaration.name] = declaration; + } this.ownDeclarations[declaration.name] = declaration; - } else if (existingInRoot !== declaration) { + } else if ((existingInRoot && existingInRoot !== declaration) || ownExisting !== declaration) { throw new Error(`Cannot add a declaration with the same name ${declaration.name}.`); } return declaration; @@ -79,14 +81,14 @@ class Group extends Step { */ removeDeclaration(name, root = null) { const workflow = root || this.workflow(); - const declarationInRoot = workflow.declarations[name]; + const declarationInRoot = workflow && workflow.declarations[name]; const declaration = this.ownDeclarations[name]; if (declarationInRoot) { - declarationInRoot.parent = null; + declarationInRoot.step = null; delete workflow.declarations[name]; } if (declaration) { - declaration.parent = null; + declaration.step = null; delete this.ownDeclarations[name]; } } 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 424f85d..bf54b91 100644 --- a/test/parser/WDL/entities/WDLWorkflowTest.js +++ b/test/parser/WDL/entities/WDLWorkflowTest.js @@ -327,6 +327,7 @@ describe('parser/WDL/entities/WDLWorkflow', () => { }); 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: { @@ -909,6 +910,7 @@ describe('parser/WDL/entities/WDLWorkflow', () => { }, }, }; + /* 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']); From a9539564d66fec9b0d8385d175cd0eacfe42336f Mon Sep 17 00:00:00 2001 From: Timofey Alyakin Date: Mon, 12 Mar 2018 17:09:04 +0300 Subject: [PATCH 09/16] [ Support for declarations in (call, if, scatter & while) blocks ] * Declaration tests were added --- src/model/Declaration.js | 8 +++++--- test/model/DeclarationTest.js | 30 ++++++++++++++++++++++++++++++ 2 files changed, 35 insertions(+), 3 deletions(-) create mode 100644 test/model/DeclarationTest.js diff --git a/src/model/Declaration.js b/src/model/Declaration.js index 8c2ed38..8858301 100644 --- a/src/model/Declaration.js +++ b/src/model/Declaration.js @@ -1,5 +1,4 @@ import Port from './Port'; -import Action from './Action'; import { extractExpression, extractType } from '../parser/WDL/utils/utils'; /** @@ -14,8 +13,11 @@ export default class Declaration extends Port { * @param {Step} step - Parent step this Declaration belongs to. */ constructor(name, declaration, step) { - if (declaration instanceof Action) { - throw new Error('Group could be created only using declaration object'); + 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, {}); 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); + }); + }); +}); From cbb1d57fd5ab567d5b2ebb0ad77ae47c5070ea68 Mon Sep 17 00:00:00 2001 From: Timofey Alyakin Date: Mon, 12 Mar 2018 19:10:11 +0300 Subject: [PATCH 10/16] [ Support for declarations in (call, if, scatter & while) blocks ] * Coverage increased --- src/model/Group.js | 2 +- test/parser/parseTest.js | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/model/Group.js b/src/model/Group.js index 6a4ce84..356a5e9 100644 --- a/src/model/Group.js +++ b/src/model/Group.js @@ -65,7 +65,7 @@ class Group extends Step { root.declarations[declaration.name] = declaration; } this.ownDeclarations[declaration.name] = declaration; - } else if ((existingInRoot && existingInRoot !== declaration) || ownExisting !== declaration) { + } else if (existingInRoot !== declaration && ownExisting !== declaration) { throw new Error(`Cannot add a declaration with the same name ${declaration.name}.`); } return declaration; 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); }); From 70655bb78f93d1116c4c9fe95f9fc907584de59c Mon Sep 17 00:00:00 2001 From: Timofey Alyakin Date: Wed, 14 Mar 2018 15:04:32 +0300 Subject: [PATCH 11/16] [ Support for declarations in (call, if, scatter & while) blocks ] * the imports resolving feature was updated according to the new declarations --- src/model/Group.js | 2 + src/model/Step.js | 4 +- src/parser/WDL/entities/WDLWorkflow.js | 9 ++ src/parser/WDL/parse.js | 41 +++----- src/parser/WDL/utils/renaming_utils.js | 135 +++++++++++++++++++++++++ 5 files changed, 163 insertions(+), 28 deletions(-) create mode 100644 src/parser/WDL/utils/renaming_utils.js diff --git a/src/model/Group.js b/src/model/Group.js index 356a5e9..d1b410a 100644 --- a/src/model/Group.js +++ b/src/model/Group.js @@ -91,6 +91,8 @@ class Group extends Step { 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/parser/WDL/entities/WDLWorkflow.js b/src/parser/WDL/entities/WDLWorkflow.js index eada97c..01d66cd 100644 --- a/src/parser/WDL/entities/WDLWorkflow.js +++ b/src/parser/WDL/entities/WDLWorkflow.js @@ -4,6 +4,7 @@ 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'; @@ -186,6 +187,14 @@ 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]) { const bindings = WDLWorkflow.getPortForBinding(this.workflowStep, parentStep, expression); if (bindings && !Array.isArray(bindings)) { diff --git a/src/parser/WDL/parse.js b/src/parser/WDL/parse.js index f7360f0..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; @@ -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,7 +165,7 @@ 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) => { @@ -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); +} From 22491d0da11ec0bac976ae39b17feb6ba27001c7 Mon Sep 17 00:00:00 2001 From: Timofey Alyakin Date: Thu, 15 Mar 2018 14:01:49 +0300 Subject: [PATCH 12/16] [ Support for declarations in (call, if, scatter & while) blocks ] * tests added for renaming_utils.js --- test/parser/WDL/utils/renaming_utilsTest.js | 380 ++++++++++++++++++++ 1 file changed, 380 insertions(+) create mode 100644 test/parser/WDL/utils/renaming_utilsTest.js 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'); + }); + }); +}); From e3722aea737d6cab4524d59044714b89d456f785 Mon Sep 17 00:00:00 2001 From: Timofey Alyakin Date: Mon, 19 Mar 2018 13:34:59 +0300 Subject: [PATCH 13/16] [ Support for declarations in (call, if, scatter & while) blocks ] * coverage increased --- src/parser/WDL/entities/WDLWorkflow.js | 30 ++-- test/parser/WDL/entities/WDLWorkflowTest.js | 190 ++++++++++++++++++++ 2 files changed, 201 insertions(+), 19 deletions(-) diff --git a/src/parser/WDL/entities/WDLWorkflow.js b/src/parser/WDL/entities/WDLWorkflow.js index 01d66cd..7b38a4d 100644 --- a/src/parser/WDL/entities/WDLWorkflow.js +++ b/src/parser/WDL/entities/WDLWorkflow.js @@ -85,8 +85,8 @@ export default class WDLWorkflow { const collection = extractExpression(item.attributes.collection); - // in scatter the item is always an identifier, so it'll always be one port returned from .getPortForBinding() not an array - 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).shift(); opts.i[itemName] = {}; opts.i[itemName].type = 'ScatterItem'; @@ -196,12 +196,8 @@ export default class WDLWorkflow { } if (step.i[declaration]) { - const bindings = WDLWorkflow.getPortForBinding(this.workflowStep, parentStep, expression); - if (bindings && !Array.isArray(bindings)) { - step.i[declaration].bind(bindings); - } else if (bindings) { - _.forEach(bindings, binding => step.i[declaration].bind(binding)); - } + 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}'`); } @@ -229,12 +225,8 @@ export default class WDLWorkflow { }); } else if (parentStep instanceof Group) { // declaration is a "variable" type const declaration = new Declaration(name, decl, parentStep); - const bindings = WDLWorkflow.getPortForBinding(this.workflowStep, declaration.step, declaration.expression); - if (bindings && !Array.isArray(bindings)) { - declaration.bind(bindings); - } else if (bindings) { - _.forEach(bindings, binding => declaration.bind(binding)); - } + const bindings = WDLWorkflow.getPortsForBinding(this.workflowStep, declaration.step, declaration.expression); + _.forEach(bindings, binding => declaration.bind(binding)); parentStep.addDeclaration(declaration); } @@ -400,7 +392,7 @@ export default class WDLWorkflow { return undefined; } - static getPortForBinding(workflow, parent, expression) { + static getPortsForBinding(workflow, parent, expression) { const accessesTypes = [ 'ArrayOrMapLookup', 'FunctionCall', @@ -428,7 +420,7 @@ export default class WDLWorkflow { 'MapLiteralKv', 'ObjectKV', ]; - let binder = expression.string; + let binder = [expression.string]; if (expression.type === 'MemberAccess') { const rhsPart = expression.accesses[0].rhs; const lhsPart = expression.accesses[0].lhs; @@ -436,7 +428,7 @@ export default class WDLWorkflow { const outputStep = WDLWorkflow.findStepInStructureRecursively(workflow, lhsPart); if (outputStep) { if (outputStep.o[rhsPart]) { - binder = outputStep.o[rhsPart]; + binder = [outputStep.o[rhsPart]]; } else { throw new WDLParserError(`Undeclared variable is referenced: '${lhsPart}.${rhsPart}'`); } @@ -474,9 +466,9 @@ export default class WDLWorkflow { const desiredStep = WDLWorkflow.groupNameResolver(parent, expression.string); if (desiredStep) { if (desiredStep.i[expression.string]) { - binder = desiredStep.i[expression.string]; + binder = [desiredStep.i[expression.string]]; } else { - binder = desiredStep.declarations[expression.string]; + binder = [desiredStep.declarations[expression.string]]; } } else { throw new WDLParserError(`Undeclared variable is referenced: '${expression.string}'`); diff --git a/test/parser/WDL/entities/WDLWorkflowTest.js b/test/parser/WDL/entities/WDLWorkflowTest.js index bf54b91..2d02559 100644 --- a/test/parser/WDL/entities/WDLWorkflowTest.js +++ b/test/parser/WDL/entities/WDLWorkflowTest.js @@ -94,6 +94,196 @@ describe('parser/WDL/entities/WDLWorkflow', () => { expect(workflow.workflowStep.action.i).to.have.all.keys(['a', 'b']); }); + 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: {}, + ast: { + name: 'Workflow', + attributes: { + 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, + }, + }, + }], + }, + }, + }], + }, + }, + }, + }, + }], + }, + }, + }, + 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: { From 2d7ac6d743ffe2f12f1e29e71d5f7dfdbd3eb690 Mon Sep 17 00:00:00 2001 From: Timofey Alyakin Date: Wed, 21 Mar 2018 19:28:43 +0300 Subject: [PATCH 14/16] [ Support for declarations in (call, if, scatter & while) blocks ] * Fixed console error found in the PR --- src/parser/WDL/entities/WDLWorkflow.js | 63 ++++++++++++++------------ 1 file changed, 34 insertions(+), 29 deletions(-) diff --git a/src/parser/WDL/entities/WDLWorkflow.js b/src/parser/WDL/entities/WDLWorkflow.js index 7b38a4d..d40b578 100644 --- a/src/parser/WDL/entities/WDLWorkflow.js +++ b/src/parser/WDL/entities/WDLWorkflow.js @@ -86,7 +86,7 @@ export default class WDLWorkflow { const collection = extractExpression(item.attributes.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).shift(); + const [port] = WDLWorkflow.getPortsForBinding(this.workflowStep, parent, collection); opts.i[itemName] = {}; opts.i[itemName].type = 'ScatterItem'; @@ -262,33 +262,38 @@ 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; } + const wfOutLinksList = expression.accesses; this.workflowStep.action.addPorts({ o: obj, }); wfOutLinksList.forEach((i) => { - const startStep = WDLWorkflow.findStepInStructureRecursively(this.workflowStep, i.lhs); + let nameFrom = i; + let portFrom; + if (_.isObject(i)) { + nameFrom = i.lhs; + portFrom = i.rhs; + } + const startStep = WDLWorkflow.findStepInStructureRecursively(this.workflowStep, nameFrom); - if (startStep) { - if (startStep.o[i.rhs]) { - this.workflowStep.o[i.to].bind(startStep.o[i.rhs]); + if (startStep && startStep instanceof Step) { + if (startStep.o[portFrom]) { + this.workflowStep.o[name].bind(startStep.o[portFrom]); } else { throw new WDLParserError( `In '${this.workflowStep.name}' - output block undeclared variable is referenced: '${i.lhs}.${i.rhs}'`); + output block undeclared variable is referenced: '${nameFrom}.${portFrom}'`); } + } else if (startStep && startStep instanceof Port) { + this.workflowStep.o[name].bind(startStep); } else { throw new WDLParserError( `In '${this.workflowStep.name}' - output block undeclared call is referenced: '${i.lhs}'`); + output block undeclared call is referenced: '${nameFrom}'`); } }); }); @@ -320,7 +325,7 @@ export default class WDLWorkflow { 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 { @@ -328,6 +333,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}' @@ -337,7 +344,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]); @@ -347,6 +354,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}' @@ -373,6 +382,13 @@ export default class WDLWorkflow { return undefined; }); + if (!result && step.declarations && Object.keys(step.declarations).includes(name)) { + result = step.declarations[name]; + } + if (!result && step.i && Object.keys(step.i).includes(name)) { + result = step.i[name]; + } + return result; } @@ -395,6 +411,7 @@ export default class WDLWorkflow { static getPortsForBinding(workflow, parent, expression) { const accessesTypes = [ 'ArrayOrMapLookup', + 'MemberAccess', 'FunctionCall', 'ArrayLiteral', 'ObjectLiteral', @@ -421,31 +438,19 @@ export default class WDLWorkflow { 'ObjectKV', ]; 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}'`); - } - } else { - throw new WDLParserError(`Undeclared call is referenced: '${lhsPart}'`); - } - } else if (accessesTypes.includes(expression.type) && expression.accesses.length) { + 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) { + if (outputStep && outputStep instanceof Step) { if (outputStep.o[accesses.rhs]) { binder.push(outputStep.o[accesses.rhs]); } else { throw new WDLParserError(`Undeclared variable is referenced: '${accesses.lhs}.${accesses.rhs}'`); } + } else if (outputStep && outputStep instanceof Port) { + binder.push(outputStep); } else { throw new WDLParserError(`Undeclared call is referenced: '${accesses.lhs}'`); } From 3f788114203a67e7ed3ed86c0d5e9fa6b9929ce9 Mon Sep 17 00:00:00 2001 From: Timofey Alyakin Date: Mon, 26 Mar 2018 17:46:24 +0300 Subject: [PATCH 15/16] [ Support for declarations in (call, if, scatter & while) blocks ] * coverage increased --- src/parser/WDL/entities/WDLWorkflow.js | 73 ++-- test/parser/WDL/entities/WDLWorkflowTest.js | 395 +++++++++++++++++--- 2 files changed, 366 insertions(+), 102 deletions(-) diff --git a/src/parser/WDL/entities/WDLWorkflow.js b/src/parser/WDL/entities/WDLWorkflow.js index d40b578..02e9817 100644 --- a/src/parser/WDL/entities/WDLWorkflow.js +++ b/src/parser/WDL/entities/WDLWorkflow.js @@ -265,36 +265,14 @@ export default class WDLWorkflow { if (expression.type !== 'MemberAccess') { obj[name].default = expression.string; } - const wfOutLinksList = expression.accesses; this.workflowStep.action.addPorts({ o: obj, }); - wfOutLinksList.forEach((i) => { - let nameFrom = i; - let portFrom; - if (_.isObject(i)) { - nameFrom = i.lhs; - portFrom = i.rhs; - } - const startStep = WDLWorkflow.findStepInStructureRecursively(this.workflowStep, nameFrom); - - if (startStep && startStep instanceof Step) { - if (startStep.o[portFrom]) { - this.workflowStep.o[name].bind(startStep.o[portFrom]); - } else { - throw new WDLParserError( - `In '${this.workflowStep.name}' - output block undeclared variable is referenced: '${nameFrom}.${portFrom}'`); - } - } else if (startStep && startStep instanceof Port) { - this.workflowStep.o[name].bind(startStep); - } else { - throw new WDLParserError( - `In '${this.workflowStep.name}' - output block undeclared call is referenced: '${nameFrom}'`); - } + const bindings = WDLWorkflow.getPortsForBinding(this.workflowStep, this.workflowStep, expression, true); + _.forEach(bindings, (binding) => { + this.workflowStep.o[name].bind(binding); }); }); } @@ -367,26 +345,28 @@ 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; + } - if (!result && step.declarations && Object.keys(step.declarations).includes(name)) { - result = step.declarations[name]; - } - if (!result && step.i && Object.keys(step.i).includes(name)) { - result = step.i[name]; + return undefined; + }); } return result; @@ -408,7 +388,7 @@ export default class WDLWorkflow { return undefined; } - static getPortsForBinding(workflow, parent, expression) { + static getPortsForBinding(workflow, parent, expression, isWfOutput = false) { const accessesTypes = [ 'ArrayOrMapLookup', 'MemberAccess', @@ -437,6 +417,7 @@ export default class WDLWorkflow { 'MapLiteralKv', 'ObjectKV', ]; + const errorMessAdd = isWfOutput ? `in ${workflow.name} output block ` : ''; let binder = [expression.string]; if (accessesTypes.includes(expression.type) && expression.accesses.length) { binder = []; @@ -447,12 +428,12 @@ export default class WDLWorkflow { if (outputStep.o[accesses.rhs]) { binder.push(outputStep.o[accesses.rhs]); } else { - throw new WDLParserError(`Undeclared variable is referenced: '${accesses.lhs}.${accesses.rhs}'`); + 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 is referenced: '${accesses.lhs}'`); + throw new WDLParserError(`Undeclared call ${errorMessAdd}is referenced: '${accesses.lhs}'`); } } else if (_.isString(accesses)) { const desiredStep = WDLWorkflow.groupNameResolver(parent, accesses); @@ -463,7 +444,7 @@ export default class WDLWorkflow { binder.push(desiredStep.declarations[accesses]); } } else { - throw new WDLParserError(`Undeclared variable is referenced: '${expression.string}'`); + throw new WDLParserError(`Undeclared variable ${errorMessAdd}is referenced: '${expression.string}'`); } } }); @@ -476,7 +457,7 @@ export default class WDLWorkflow { 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/test/parser/WDL/entities/WDLWorkflowTest.js b/test/parser/WDL/entities/WDLWorkflowTest.js index 2d02559..0161324 100644 --- a/test/parser/WDL/entities/WDLWorkflowTest.js +++ b/test/parser/WDL/entities/WDLWorkflowTest.js @@ -94,6 +94,345 @@ 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 }, @@ -172,62 +511,6 @@ describe('parser/WDL/entities/WDLWorkflow', () => { }, }, declarations: {}, - ast: { - name: 'Workflow', - attributes: { - 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, - }, - }, - }], - }, - }, - }], - }, - }, - }, - }, - }], - }, - }, - }, isSubWorkflow: false, }, wf2: { From 7542e4668237e621124486c20dde9031263b50d1 Mon Sep 17 00:00:00 2001 From: Timofey Alyakin Date: Tue, 27 Mar 2018 16:58:50 +0300 Subject: [PATCH 16/16] fixed bug when some declarations were drawn underneath visual groups (e.g. scatter, if, etc.) --- src/visual/VisualDeclaration.js | 23 ++++++++++++++++++++--- src/visual/Visualizer.js | 12 ++++++------ 2 files changed, 26 insertions(+), 9 deletions(-) diff --git a/src/visual/VisualDeclaration.js b/src/visual/VisualDeclaration.js index fd7391f..1aa0d1e 100644 --- a/src/visual/VisualDeclaration.js +++ b/src/visual/VisualDeclaration.js @@ -1,8 +1,9 @@ import _ from 'lodash'; -import * as joint from 'jointjs'; +import joint from 'jointjs'; -const cDefaultWidth = 100; -const cMinHeight = 100; +const cDefaultWidth = 12; +const cMinHeight = 50; +const cPixelPerSymbol = 10; export default class VisualDeclaration extends joint.shapes.pn.Transition { @@ -32,4 +33,20 @@ export default class VisualDeclaration extends joint.shapes.pn.Transition { _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/Visualizer.js b/src/visual/Visualizer.js index a849e37..c17802a 100644 --- a/src/visual/Visualizer.js +++ b/src/visual/Visualizer.js @@ -514,12 +514,12 @@ 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; @@ -527,7 +527,7 @@ export default class Visualizer { _.forEach(opts.step.ownDeclarations, (declaration) => { let visDeclaration = declarations[declaration.name]; if (!visDeclaration) { - visDeclaration = new VisualDeclaration({ declaration }); + visDeclaration = new VisualDeclaration({ declaration, x: opts.x, y: opts.y }); cellsToAdd[cellsToAdd.length] = visDeclaration; declarations[declaration.name] = visDeclaration; visChild.embed(visDeclaration);