diff --git a/index.js b/index.js index 2002a4f..d087391 100644 --- a/index.js +++ b/index.js @@ -4,6 +4,7 @@ module.exports = { rules: { 'processmaker/custom-validation': 'error', 'processmaker/gateway-direction': 'error', + 'processmaker/event-based-gateway': 'error', 'processmaker/call-activity-child-process': 'error', 'processmaker/call-activity-sequence-flow': 'error', 'processmaker/id-required': 'error', diff --git a/rule-tester.js b/rule-tester.js new file mode 100644 index 0000000..0957922 --- /dev/null +++ b/rule-tester.js @@ -0,0 +1,90 @@ +/* global it, describe, beforeEach */ + +const assert = require('assert'); + +const Linter = require('bpmnlint/lib/linter'); + +function createResolver(rule) { + return { + resolveRule: () => Promise.resolve(rule) + }; +} + +function expectEqual(a, b) { + assert.deepStrictEqual(a, b); +} + + +function verify(ruleName, rule, testCases) { + const linterConfig = { + rules: { [ruleName]: 2 } + }; + + describe(`rules/${ruleName}`, function() { + + let linter; + + beforeEach(function() { + linter = new Linter({ + resolver: createResolver(rule) + }); + }); + + + describe('should lint valid', function() { + + testCases.valid.forEach(({ moddleElement }, idx) => ( + + it(`test case #${idx + 1}`, function() { + return ( + Promise.resolve(moddleElement) + .then(moddleRoot => { + return linter.lint(moddleRoot.root, linterConfig); + }) + .then(lintResults => { + expectEqual(lintResults, {}); + }) + ); + }) + + )); + + }); + + + describe('should lint invalid', function() { + + testCases.invalid.forEach(({ moddleElement, report }, idx) => ( + + it(`test case #${idx}`, function() { + + const expectedResult = report instanceof Array ? report : [{ + ...report, + category: 'error' + }]; + + return ( + Promise.resolve(moddleElement) + .then(moddleRoot => { + return linter.lint(moddleRoot.root, linterConfig); + }) + .then(lintResults => { + expectEqual(lintResults, { + [ruleName]: expectedResult + }); + }) + ); + }) + + )); + + }); + + }); + +} + + +module.exports = { + verify +}; diff --git a/rules/event-based-gateway.js b/rules/event-based-gateway.js new file mode 100644 index 0000000..01b9b7b --- /dev/null +++ b/rules/event-based-gateway.js @@ -0,0 +1,44 @@ +const { isAny } = require('bpmnlint-utils'); + +/** + * Rule that validates gateways according to the following rules: + * + * - An Event Based Gateway can only be connected to events. + */ +module.exports = function() { + let gateway = null; + + function outgoingFlowsAreValid(gateway) { + const outgoing = gateway.get('outgoing'); + return outgoing.filter(sequenceFlow => { + const target = sequenceFlow.get('targetRef'); + return !isAny(target, ['bpmn:IntermediateCatchEvent', 'bpmn:EndEvent']); + }).length === 0; + } + + function check(gateway, reporter) { + if (!isAny(gateway, ['bpmn:EventBasedGateway'])) { + return; + } + + const outgoing = gateway.get('outgoing'); + const valid = outgoing.filter(sequenceFlow => { + const target = sequenceFlow.get('targetRef'); + const isValidType = isAny(target, ['bpmn:IntermediateCatchEvent', 'bpmn:EndEvent']); + const onlyOneIncoming = target.get('incoming').length === 1; + if (!isValidType) { + reporter.report(target.id, 'Event Gateways target elements must be Catch Events'); + } + if (!onlyOneIncoming) { + reporter.report(target.id, 'Event Gateway target elements must not have additional incoming Sequence Flows'); + } + return !isValidType || !onlyOneIncoming; + }).length === 0; + + if (!valid) { + reporter.report(gateway.id, 'Event Gateways target elements must be valid Catch Events'); + } + } + + return { check }; +}; diff --git a/test-diagrams/event-based-gateway.invalid.bpmn b/test-diagrams/event-based-gateway.invalid.bpmn new file mode 100644 index 0000000..c1b85d7 --- /dev/null +++ b/test-diagrams/event-based-gateway.invalid.bpmn @@ -0,0 +1,133 @@ + + + + + node_5 + + + node_5 + node_9 + node_11 + node_26 + + + + node_9 + node_10 + node_18 + + PT1H + + + + + node_18 + + + + + node_23 + + + + node_30 + + + node_11 + node_23 + + + + + + node_26 + node_30 + + + + + + node_14 + node_10 + + + + node_14 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test-diagrams/event-based-gateway.valid.bpmn b/test-diagrams/event-based-gateway.valid.bpmn new file mode 100644 index 0000000..80e6ba5 --- /dev/null +++ b/test-diagrams/event-based-gateway.valid.bpmn @@ -0,0 +1,107 @@ + + + + + node_5 + + + node_5 + node_9 + node_11 + node_26 + + + + node_9 + node_18 + + PT1H + + + + + node_11 + node_23 + + + + + node_18 + + + + + node_23 + + + + + node_26 + node_30 + + + + + node_30 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test.js b/test.js index 932e796..729353a 100644 --- a/test.js +++ b/test.js @@ -1,11 +1,12 @@ const readModdle = require('bpmnlint/lib/testers/helper').readModdle; -const RuleTester = require('bpmnlint/lib/testers/rule-tester'); +const RuleTester = require('./rule-tester'); const gatewayDirectionRule = require('./rules/gateway-direction'); const callActivityChildProcessRule = require('./rules/call-activity-child-process'); const callActivitySequenceFlowRule = require('./rules/call-activity-sequence-flow'); const idRequiredRule = require('./rules/id-required'); const signalRefRequiredRule = require('./rules/signal-ref-required'); +const eventBasedGatewayRule = require('./rules/event-based-gateway'); RuleTester.verify('gateway-direction', gatewayDirectionRule, { valid: [ @@ -165,3 +166,38 @@ RuleTester.verify('signal-ref-required', signalRefRequiredRule, { }, ] }); + +RuleTester.verify('event-based-gateway', eventBasedGatewayRule, { + valid: [ + { + moddleElement: readModdle('./test-diagrams/event-based-gateway.valid.bpmn') + }, + ], + invalid: [ + { + moddleElement: readModdle('./test-diagrams/event-based-gateway.invalid.bpmn'), + report: [ + { + "category": "error", + "id": "node_6", + "message": "Event Gateway target elements must not have additional incoming Sequence Flows" + }, + { + "category": "error", + "id": "node_2", + "message": "Event Gateways target elements must be Catch Events" + }, + { + "category": "error", + "id": "node_4", + "message": "Event Gateways target elements must be Catch Events" + }, + { + "category": "error", + "id": "node_3", + "message": "Event Gateways target elements must be valid Catch Events" + } + ] + }, + ] +});