Skip to content

Commit c4fabc7

Browse files
Merge pull request #4 from CarstenWickner/path-validation
2 parents 8781e22 + 139cfaa commit c4fabc7

5 files changed

Lines changed: 188 additions & 74 deletions

File tree

package.json

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,10 +33,10 @@
3333
],
3434
"devDependencies": {
3535
"@mdx-js/loader": "~1.5.1",
36-
"@storybook/addon-actions": "~5.2.3",
37-
"@storybook/addon-docs": "~5.2.3",
38-
"@storybook/addon-options": "~5.2.3",
39-
"@storybook/react": "~5.2.3",
36+
"@storybook/addon-actions": "~5.3.1",
37+
"@storybook/addon-docs": "~5.3.1",
38+
"@storybook/addon-options": "~5.3.1",
39+
"@storybook/react": "~5.3.1",
4040
"@types/enzyme": "~3.10.3",
4141
"@types/enzyme-adapter-react-16": "~1.0.5",
4242
"@types/jest": "24.0.23",
@@ -49,6 +49,7 @@
4949
"@wessberg/rollup-plugin-ts": "~1.1.73",
5050
"awesome-typescript-loader": "~5.2.1",
5151
"babel-loader": "~8.0.6",
52+
"babel-preset-react-app": "~9.1.0",
5253
"coveralls": "~3.0.6",
5354
"css-loader": "~3.3.0",
5455
"enzyme": "~3.10.0",

src/component/renderDataUtils.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { FlowContent, FlowGatewayDiverging, FlowModelerProps } from "../types/Fl
22
import { GridCellData, ElementType, ConnectionType } from "../types/GridCellData";
33
import { createElementTree } from "../model/modelUtils";
44
import { FlowElement } from "../model/FlowElement";
5-
import { validatePaths } from "../model/pathValidationUtils";
5+
import { checkForCircularReference, validatePaths } from "../model/pathValidationUtils";
66

77
const getColumnIndexAfter = (element: FlowElement): number => element.getColumnIndex() + (element.getFollowingElements().length > 1 ? 2 : 1);
88

@@ -122,8 +122,10 @@ export const buildRenderData = (
122122
flow: FlowModelerProps["flow"],
123123
verticalAlign: "top" | "bottom"
124124
): { gridCellData: Array<GridCellData>; columnCount: number } => {
125-
validatePaths(flow);
125+
const { firstElementId, elements } = flow;
126+
checkForCircularReference(firstElementId, elements);
126127
const treeRootElement = createElementTree(flow, verticalAlign);
128+
validatePaths(treeRootElement);
127129
const result: Array<GridCellData> = [];
128130
// add single start element
129131
result.push({
@@ -132,7 +134,6 @@ export const buildRenderData = (
132134
rowEndIndex: 1 + treeRootElement.getRowCount(),
133135
type: ElementType.Start
134136
});
135-
const { elements } = flow;
136137
collectGridCellData(treeRootElement, undefined, undefined, elements, 1, result);
137138
// for a more readable resulting html structure, sort the grid elements first from top to bottom and within each row from left to right
138139
result.sort(sortGridCellDataByPosition);

src/model/pathValidationUtils.ts

Lines changed: 92 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,68 +1,114 @@
1+
import { FlowElement } from "./FlowElement";
12
import { isDivergingGateway } from "./modelUtils";
2-
import { FlowModelerProps, FlowContent, FlowGatewayDiverging } from "../types/FlowModelerProps";
33

4-
const recursivelyCollectPaths = (
4+
import { FlowModelerProps } from "../types/FlowModelerProps";
5+
6+
/**
7+
* Check the given input model for circular references, which are specifically disallowed.
8+
*
9+
* @param {string} targetElementId - id of the element to check
10+
* @param {object} elements - collection of all defined elements in the input model
11+
* @param {Array.<string>} currentPath - ids of elements in front of target element
12+
* @throws Error in case of a circular reference being present
13+
*/
14+
export const checkForCircularReference = (
515
targetElementId: string,
6-
elements: { [key: string]: FlowContent | FlowGatewayDiverging },
7-
currentPath: Array<string>,
8-
otherPaths: Array<Array<string>>
16+
elements: FlowModelerProps["flow"]["elements"],
17+
currentPath: Array<string> = []
918
): void => {
19+
const targetElement = elements[targetElementId];
20+
if (!targetElement) {
21+
// path ends, i.e. there was no circular reference here
22+
return;
23+
}
1024
if (currentPath.includes(targetElementId)) {
1125
// no reason to go in circles, stop it right there
1226
throw new Error(`Circular reference to element: ${targetElementId}`);
1327
}
1428
currentPath.push(targetElementId);
15-
const targetElement = elements[targetElementId];
16-
if (!targetElement) {
17-
// current path ends here, i.e. it should be added to the overall list
18-
otherPaths.push(currentPath);
19-
} else if (isDivergingGateway(targetElement)) {
20-
// ensure that there are always at least two sub elements under a gateway to allow for respective End elements to be displayed
21-
let subElements;
22-
if (targetElement.nextElements.length > 1) {
23-
subElements = targetElement.nextElements;
24-
} else if (targetElement.nextElements.length === 1) {
25-
subElements = [...targetElement.nextElements, {}];
26-
} else {
27-
subElements = [{}, {}];
28-
}
29-
subElements.forEach((next) => recursivelyCollectPaths(next.id, elements, currentPath.slice(0), otherPaths));
29+
if (isDivergingGateway(targetElement)) {
30+
// check on each sub-path after the targeted diverging gateway
31+
targetElement.nextElements.forEach((next) => checkForCircularReference(next.id, elements, currentPath.slice(0)));
3032
} else {
31-
recursivelyCollectPaths(targetElement.nextElementId, elements, currentPath, otherPaths);
33+
// continue check after the targeted content element
34+
checkForCircularReference(targetElement.nextElementId, elements, currentPath);
3235
}
3336
};
34-
/*
35-
const filterPathsWithDifferingStart = ([elementId, pathsIncludingElement]: [string, Array<Array<string>>]): boolean => {
36-
if (pathsIncludingElement.length < 2) {
37-
return false;
38-
}
39-
const elementIndex = pathsIncludingElement[0].indexOf(elementId);
40-
if (pathsIncludingElement.some((path) => path.indexOf(elementId) !== elementIndex)) {
41-
return true;
37+
38+
/**
39+
* Check whether the given two parents of the specified child element are neighbours.
40+
*
41+
* @param {FlowElement} child - element being referenced from multiple parents (thereby being preceded by an implicit converging gateway)
42+
* @param {FlowElement} firstParent - leading specific preceding element from which the designated child is being referenced
43+
* @param {FlowElement} secondParent - trailing specific preceding element from which the designated child is being referenced
44+
* @returns {boolean} whether the implicit converging gateway is valid.
45+
*/
46+
const areParentsNeighbours = (child: FlowElement, firstParent: FlowElement, secondParent: FlowElement): boolean => {
47+
// collect path to second element
48+
const topPathToSecond: Array<FlowElement> = [child, secondParent];
49+
let leadingParentOfSecond = secondParent;
50+
while (leadingParentOfSecond.getPrecedingElements().length) {
51+
// in case of converging gateway, always take the top element
52+
leadingParentOfSecond = leadingParentOfSecond.getPrecedingElements()[0];
53+
topPathToSecond.push(leadingParentOfSecond);
4254
}
43-
const leadingPathPart = pathsIncludingElement[0].slice(0, elementIndex);
44-
return pathsIncludingElement.some((path) => leadingPathPart.some((value, index) => value !== path[index]));
55+
// iterate backwards over path to first element until finding a common parent (worst case: the root element)
56+
const bottomPathToFirst: Array<FlowElement> = [child];
57+
let firstBranch = firstParent;
58+
do {
59+
bottomPathToFirst.push(firstBranch);
60+
if (topPathToSecond.indexOf(firstBranch) > -1) {
61+
break;
62+
}
63+
const parents = firstBranch.getPrecedingElements();
64+
firstBranch = parents[parents.length - 1];
65+
// keep going, worst case till the single root element
66+
} while (true);
67+
const commonIndexInPathToSecond = topPathToSecond.indexOf(firstBranch);
68+
const commonSiblings = firstBranch.getFollowingElements();
69+
// check whether the two paths are neighbouring when branching off from their right-most common parent
70+
const firstBranchIndex = commonSiblings.indexOf(bottomPathToFirst[bottomPathToFirst.length - 2]);
71+
const secondBranchIndex = commonSiblings.indexOf(topPathToSecond[commonIndexInPathToSecond - 1]);
72+
return firstBranchIndex + 1 === secondBranchIndex;
4573
};
46-
*/
4774

48-
export const validatePaths = ({ firstElementId, elements }: FlowModelerProps["flow"]): void => {
49-
const paths: Array<Array<string>> = [];
50-
recursivelyCollectPaths(firstElementId, elements, [], paths);
51-
/*
52-
const elementsOnMultiplePaths = Object.keys(elements)
53-
.map((elementId) => [elementId, paths.filter((path) => path.includes(elementId))] as [string, Array<Array<string>>])
54-
.filter(filterPathsWithDifferingStart);
55-
const getIndexOfPath = (path: Array<string>): number => paths.indexOf(path);
56-
const invalidElements = elementsOnMultiplePaths.filter(([, pathsWithOverlap]) => {
57-
const indexes = pathsWithOverlap.map(getIndexOfPath);
58-
return indexes.length && indexes[0] + indexes.length != indexes[indexes.length - 1] + 1;
59-
});
75+
/**
76+
* Check whether the implicit converging gateway in front of the given element is valid, i.e. whether all connection pairs are direct neighbours.
77+
*
78+
* @param {FlowElement} convergingGateway - element being referenced from multiple parents (thereby being preceded by an implicit converging gateway)
79+
* @returns {boolean} whether the implicit converging gateway is invalid (beware the negation!)
80+
*/
81+
const isInvalidConvergingGateway = (convergingGateway: FlowElement): boolean => {
82+
const connectedElements = convergingGateway.getPrecedingElements();
83+
return connectedElements
84+
.slice(1)
85+
.some((nextElement, previousIndex) => !areParentsNeighbours(convergingGateway, connectedElements[previousIndex], nextElement));
86+
};
87+
88+
/**
89+
* Validate that the parsed data model can be properly displayed. Ensuring that only directly neighbouring paths can link to the same element.
90+
*
91+
* @param {FlowElement} treeRootElement - root of the parsed data model to validate
92+
* @throws Error in case of any (implicit) converging gateways connecting non-neighbouring paths
93+
*/
94+
export const validatePaths = (treeRootElement: FlowElement): void => {
95+
// use Set to automatically filter out duplicates and thereby avoid checking the same gateway repeatedly
96+
const convergingGateways = new Set<FlowElement>();
97+
const collectConvergingGateways = (element: FlowElement): void => {
98+
if (element.getId()) {
99+
if (element.getPrecedingElements().length > 1) {
100+
convergingGateways.add(element);
101+
}
102+
element.getFollowingElements().forEach(collectConvergingGateways);
103+
}
104+
};
105+
collectConvergingGateways(treeRootElement);
106+
const invalidElements = Array.from(convergingGateways).filter(isInvalidConvergingGateway);
60107
if (invalidElements.length) {
61108
throw new Error(
62109
`Multiple references only valid from neighbouring paths. Invalid references to: '${invalidElements
63-
.map(([elementId]) => elementId)
110+
.map((gateway) => gateway.getId())
64111
.join("', '")}'`
65112
);
66113
}
67-
*/
68114
};

test/component/renderDataUtils.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -724,7 +724,7 @@ describe("buildRenderData()", () => {
724724
);
725725
expect(execution).toThrowError("Circular reference to element: a");
726726
});
727-
it.skip("throws error for multiple references on non-neighbouring paths", () => {
727+
it("throws error for multiple references on non-neighbouring paths", () => {
728728
const execution = (): never =>
729729
buildRenderData(
730730
{
Lines changed: 86 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,99 @@
1-
import { validatePaths } from "../../src/model/pathValidationUtils";
1+
import { validatePaths, checkForCircularReference } from "../../src/model/pathValidationUtils";
2+
import { createElementTree } from "../../src/model/modelUtils";
3+
import { FlowModelerProps } from "../../src/types/FlowModelerProps";
24

3-
describe("validatePaths()", () => {
4-
const ref = (nextId: string): { nextElementId: string } => ({ nextElementId: nextId });
5+
const ref = (nextId: string): { nextElementId: string } => ({ nextElementId: nextId });
6+
7+
describe("checkForCircularReference()", () => {
58
it.each`
6-
testDescription | flowElements
7-
${"direct self-reference"} | ${{ a: ref("a") }}
8-
${"nested one level"} | ${{ a: ref("b"), b: ref("a") }}
9-
${"nested six levels"} | ${{ a: ref("b"), b: ref("c"), c: ref("d"), d: ref("e"), e: ref("f"), f: ref("g"), g: ref("a") }}
9+
testDescription | flowElements
10+
${"direct self-reference"} | ${{ a: ref("a") }}
11+
${"nested one level"} | ${{ a: ref("b"), b: ref("a") }}
12+
${"nested six levels"} | ${{ a: ref("b"), b: ref("c"), c: ref("d"), d: ref("e"), e: ref("f"), f: ref("g"), g: ref("a") }}
13+
${"behind diverging gateway"} | ${{ a: { nextElements: [{ id: "b" }, { id: "c" }] }, b: {}, c: ref("d"), d: ref("a") }}
14+
${"behind converging gateway"} | ${{ a: { nextElements: [{ id: "b" }, { id: "c" }] }, b: ref("d"), c: ref("d"), d: ref("a") }}
1015
`("throws error for circular reference – $testDescription", ({ flowElements }) => {
11-
const execution = (): never =>
12-
validatePaths({
13-
firstElementId: "a",
14-
elements: flowElements
15-
});
16+
const execution = (): never => checkForCircularReference("a", flowElements);
1617
expect(execution).toThrowError("Circular reference to element: a");
1718
});
18-
it.skip("throws error for multiple references on non-neighbouring paths", () => {
19-
const execution = (): never =>
20-
validatePaths({
21-
firstElementId: "a",
22-
elements: {
19+
it("accepts non-circular references", () => {
20+
const flowElements = {
21+
a: ref("b"),
22+
b: { nextElements: [{ id: "c" }, { id: "d" }, { id: "e" }] },
23+
c: ref("f"),
24+
d: { nextElements: [{ id: "d1" }, { id: "d2" }] },
25+
d1: ref("f"),
26+
d2: ref("f"),
27+
e: ref("f"),
28+
f: {}
29+
};
30+
const execution = (): never => checkForCircularReference("a", flowElements);
31+
expect(execution).not.toThrowError();
32+
});
33+
});
34+
35+
describe("validatePaths()", () => {
36+
describe.each`
37+
testDescription | firstElementId | additionalElements
38+
${"root"} | ${"a"} | ${{}}
39+
${"behind content element"} | ${"root"} | ${{ root: ref("a") }}
40+
${"behind converging gateway"} | ${"root"} | ${{ root: { nextElements: [{ id: "x" }, { id: "y" }] }, x: ref("a"), y: ref("a") }}
41+
`(
42+
"properly performs validation when gateway is $testDescription",
43+
({ firstElementId, additionalElements }: { firstElementId: string; additionalElements: FlowModelerProps["flow"]["elements"] }) => {
44+
it("accepts link between diverging gateway and one of its children", () => {
45+
const elements = {
46+
...additionalElements,
47+
a: { nextElements: [{ id: "b" }, { id: "c" }] },
48+
b: ref("c"),
49+
c: {}
50+
};
51+
const treeRootElement = createElementTree({ firstElementId, elements }, "top");
52+
const execution = (): never => validatePaths(treeRootElement);
53+
expect(execution).not.toThrowError();
54+
});
55+
it("accepts link between neighbouring children of diverging gateway", () => {
56+
const elements = {
57+
...additionalElements,
58+
a: { nextElements: [{ id: "b" }, { id: "c" }] },
59+
b: ref("d"),
60+
c: ref("d"),
61+
d: {}
62+
};
63+
const treeRootElement = createElementTree({ firstElementId, elements }, "top");
64+
const execution = (): never => validatePaths(treeRootElement);
65+
expect(execution).not.toThrowError();
66+
});
67+
it("accepts links between multiple ancestors of nested diverging gateways", () => {
68+
const elements = {
69+
...additionalElements,
70+
a: { nextElements: [{ id: "b" }, { id: "c" }, { id: "d" }, { id: "e" }] },
71+
b: ref("bc"),
72+
c: ref("bc"),
73+
bc: ref("f"),
74+
d: ref("de"),
75+
e: ref("de"),
76+
de: ref("f"),
77+
f: {}
78+
};
79+
const treeRootElement = createElementTree({ firstElementId, elements }, "top");
80+
const execution = (): never => validatePaths(treeRootElement);
81+
expect(execution).not.toThrowError();
82+
});
83+
it("throws error for multiple references on non-neighbouring paths", () => {
84+
const elements = {
85+
...additionalElements,
2386
a: { nextElements: [{ id: "b" }, { id: "c" }, { id: "d" }, { id: "e" }] },
2487
b: ref("f"),
2588
c: {},
2689
d: ref("f"),
2790
e: ref("b"),
2891
f: {}
29-
}
92+
};
93+
const treeRootElement = createElementTree({ firstElementId, elements }, "top");
94+
const execution = (): never => validatePaths(treeRootElement);
95+
expect(execution).toThrowError("Multiple references only valid from neighbouring paths. Invalid references to: 'b', 'f'");
3096
});
31-
expect(execution).toThrowError("Multiple references only valid from neighbouring paths. Invalid references to: 'b', 'f'");
32-
});
97+
}
98+
);
3399
});

0 commit comments

Comments
 (0)