src/model/Action.js (100 lines of code) (raw):

import _ from 'lodash'; import EventDispatcher from './EventDispatcher'; /** * Port descriptor. * * @typedef {object} PortDesc * @property {string} [type] - Arbitrary type identifier (user-defined). * @property {*} [default] - Default value of the port. * @property {boolean} [multi=false] - Specifies if multiple incoming connections are allowed. */ /** * Clean-up port description and provide default values. * * @private * @param {PortDesc} portDesc - Port description. * @returns {PortDesc} */ function createPort(portDesc) { return _.defaults(_.pick(portDesc, [ 'type', 'default', 'expression', 'multi', ]), { multi: false, }); } function isPrimitive(obj) { return obj !== Object(obj); } function isSubset(subObject, bigObject) { return bigObject === subObject || (!isPrimitive(subObject) && bigObject && !_.some(subObject, (val, key) => !isSubset(val, bigObject[key]))); } const PORT_PROHIBITION_ERROR = 'Ports are not allowed for this action'; /** * Class representing a generic action to be referenced in specific steps. * * @extends EventDispatcher * @example * // Create an action with an input variable 'name' and an output 'response'. * const helloAction = new Action('hello', { * i: { * name: { * type: 'String', * }, * }, * o: { * response: { * type: 'File', * default: 'stdout()', * }, * }, * data: { * command: 'echo \'Hello ${name}!\'', * }, * }); */ class Action extends EventDispatcher { /** * Create a generic action. * * @param {string} name - Action name. Must be unique in a {@link Workflow}. * @param {object=} desc - Action description. * @param {Object.<string, PortDesc>} [desc.i={}] - Dictionary of input ports descriptions. * @param {Object.<string, PortDesc>} [desc.o={}] - Dictionary of output ports descriptions. * @param {Object.<string, *>} [desc.data={}] - Metadata associated with the action (i.e. arbitrary user-defined * action details). */ constructor(name, desc = {}) { super(); if (_.isUndefined(name)) { throw new Error('Action must have a name'); } /** * Action name. * @type {string} */ this.name = name; /** * Flag that allow or prohibit the port addition to the action * Note if it set to true, trying to create or add any port lead to the exception * @type {boolean} */ this.canHavePorts = _.isUndefined(desc.canHavePorts) ? true : Boolean(desc.canHavePorts); if (!this.canHavePorts && (!_.isUndefined(desc.i) || !_.isUndefined(desc.o))) { throw new Error(PORT_PROHIBITION_ERROR); } /** * Dictionary of input ports descriptions. * @type {Object.<string, PortDesc>} */ this.i = _.mapValues(desc.i || {}, createPort); /** * Dictionary of output ports descriptions. * @type {Object.<string, PortDesc>} */ this.o = _.mapValues(desc.o || {}, createPort); /** * Metadata associated with the action (i.e. arbitrary user-defined action details). * @type {Object.<string, *>} */ this.data = _.assign({}, desc.data); } /** * Adds new ports or modifies existing ones. * * @param {object} desc - Ports to add or modify. * @param {Object.<string, PortDesc>} [desc.i={}] - Dictionary of input ports descriptions. * @param {Object.<string, PortDesc>} [desc.o={}] - Dictionary of output ports descriptions. * @example * helloAction.addPorts({ * i: { * name: { default: 'World' }, * unused: { type: 'bar', multi: true }, * }, * o: { * str: { type: 'String' }, * }, * }); */ addPorts(desc) { if (!this.canHavePorts) { throw new Error(PORT_PROHIBITION_ERROR); } let changed = false; const newIn = _.mapValues(desc.i || {}, createPort); if (!isSubset(newIn, this.i)) { _.merge(this.i, newIn); changed = true; } const newOut = _.mapValues(desc.o || {}, createPort); if (!isSubset(newOut, this.o)) { _.merge(this.o, newOut); changed = true; } if (changed) { this.trigger('changed'); } } /** * Removes ports. * * @param {object} ports - Ports to remove. * @param {Object.<string, string[]>} [ports.i=[]] - Array of input ports names. * @param {Object.<string, string[]>} [ports.o=[]] - Array of output ports names. * @example * helloAction.removePorts({ i: ['unused'], o: ['str'] }); */ removePorts(ports) { if (!this.canHavePorts) { throw new Error(PORT_PROHIBITION_ERROR); } let changed = false; const removePort = (portsMap, remList) => { _.forEach(remList, (portName) => { if (portsMap[portName]) { changed = true; delete portsMap[portName]; } }); }; removePort(this.i, ports.i || []); removePort(this.o, ports.o || []); if (changed) { this.trigger('changed'); } } _renamePort(oldName, newName, isInput) { if (!this.canHavePorts) { throw new Error(PORT_PROHIBITION_ERROR); } const ports = isInput ? this.i : this.o; if (!ports[oldName]) { throw new Error(`Port with name ${oldName} does not exist!`); } if (newName === oldName) { return; } if (ports[newName]) { throw new Error(`Port with name ${newName} already exists!`); } ports[newName] = ports[oldName]; delete ports[oldName]; this.trigger('port-rename', oldName, newName, isInput); } /** * Renames single input port. * @param {string} oldName - current name. * @param {string} newName - new name. * @example * helloAction.renameIPort('name', 'theName'); */ renameIPort(oldName, newName) { this._renamePort(oldName, newName, true); } /** * Renames single output port * @param {string} oldName - current name. * @param {string} newName - new name. * @example * helloAction.renameOPort('response', 'result'); */ renameOPort(oldName, newName) { this._renamePort(oldName, newName, false); } } // Workaround for JetBrains inspector bug. // It doesn't understand "@extends" tag together with "export default class ... extends ...". export default Action;