Skip to content

Commit 4389734

Browse files
committed
Introduce Mermaid exporter
Signed-off-by: Ricardo Zanini <[email protected]>
1 parent a14cc21 commit 4389734

33 files changed

+1553
-0
lines changed

mermaid/pom.xml

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<project xmlns="http://maven.apache.org/POM/4.0.0"
3+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
4+
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
5+
<modelVersion>4.0.0</modelVersion>
6+
<parent>
7+
<groupId>io.serverlessworkflow</groupId>
8+
<artifactId>serverlessworkflow-parent</artifactId>
9+
<version>8.0.0-SNAPSHOT</version>
10+
</parent>
11+
12+
<artifactId>serverlessworkflow-mermaid</artifactId>
13+
<name>Serverless Workflow :: Mermaid</name>
14+
<description>Export a Workflow Definition as a Mermaid Flowchart</description>
15+
16+
<properties>
17+
<maven.compiler.source>17</maven.compiler.source>
18+
<maven.compiler.target>17</maven.compiler.target>
19+
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
20+
</properties>
21+
22+
<dependencies>
23+
<dependency>
24+
<groupId>io.serverlessworkflow</groupId>
25+
<artifactId>serverlessworkflow-types</artifactId>
26+
</dependency>
27+
<dependency>
28+
<groupId>io.serverlessworkflow</groupId>
29+
<artifactId>serverlessworkflow-api</artifactId>
30+
</dependency>
31+
32+
<!-- Test Dependencies -->
33+
<dependency>
34+
<groupId>org.junit.jupiter</groupId>
35+
<artifactId>junit-jupiter-api</artifactId>
36+
<scope>test</scope>
37+
</dependency>
38+
<dependency>
39+
<groupId>org.mockito</groupId>
40+
<artifactId>mockito-core</artifactId>
41+
<scope>test</scope>
42+
</dependency>
43+
<dependency>
44+
<groupId>org.assertj</groupId>
45+
<artifactId>assertj-core</artifactId>
46+
<scope>test</scope>
47+
</dependency>
48+
</dependencies>
49+
50+
</project>
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
/*
2+
* Copyright 2020-Present The Serverless Workflow Specification Authors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package io.serverlessworkflow.mermaid;
17+
18+
public class DefaultNodeRenderer implements NodeRenderer {
19+
20+
private final Node node;
21+
protected String renderedArrow = "-->";
22+
23+
public DefaultNodeRenderer(Node node) {
24+
this.node = node;
25+
}
26+
27+
protected final Node getNode() {
28+
return node;
29+
}
30+
31+
@Override
32+
public void setRenderedArrow(String renderedArrow) {
33+
this.renderedArrow = renderedArrow;
34+
}
35+
36+
public void render(StringBuilder sb, int level) {
37+
sb.append(ind(level))
38+
.append(node.id)
39+
.append("@{ shape: ")
40+
.append(node.type.mermaidShape())
41+
.append(", label: \"")
42+
.append(NodeRenderer.escNodeLabel(node.label))
43+
.append("\" }\n");
44+
this.renderBody(sb, level);
45+
this.renderNext(sb, level);
46+
}
47+
48+
protected void renderBody(StringBuilder sb, int level) {
49+
if (!this.node.branches.isEmpty()) {
50+
MermaidRenderer.render(this.getNode().getBranches(), sb, level + 1);
51+
}
52+
}
53+
54+
protected void renderNext(StringBuilder sb, int level) {
55+
if (node.getNext() != null) {
56+
sb.append(ind(level))
57+
.append(node.getId())
58+
.append(renderedArrow)
59+
.append(node.getNext().getId())
60+
.append("\n");
61+
}
62+
}
63+
64+
protected String ind(int level) {
65+
return " ".repeat(level * 4); // 4 spaces per level
66+
}
67+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
/*
2+
* Copyright 2020-Present The Serverless Workflow Specification Authors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package io.serverlessworkflow.mermaid;
17+
18+
import io.serverlessworkflow.api.types.EmitTask;
19+
import io.serverlessworkflow.api.types.TaskItem;
20+
21+
public class EmitNode extends TaskNode {
22+
23+
public EmitNode(TaskItem task) {
24+
super("emit", task, NodeType.EMIT);
25+
26+
if (task.getTask().getEmitTask() == null) {
27+
throw new IllegalStateException("Emit node must have a emit task");
28+
}
29+
30+
EmitTask emitTask = task.getTask().getEmitTask();
31+
32+
if (emitTask.getEmit().getEvent() == null) {
33+
return;
34+
}
35+
36+
this.label = String.format("emit: **%s**", emitTask.getEmit().getEvent().getWith().getType());
37+
}
38+
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
/*
2+
* Copyright 2020-Present The Serverless Workflow Specification Authors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package io.serverlessworkflow.mermaid;
17+
18+
import io.serverlessworkflow.api.types.ForTask;
19+
import io.serverlessworkflow.api.types.TaskItem;
20+
21+
public class ForNode extends TaskSubgraphNode {
22+
23+
ForNode(TaskItem task) {
24+
super(task, String.format("for: %s", task.getName()));
25+
26+
if (task.getTask().getForTask() == null) {
27+
throw new IllegalStateException("For node must have a for task");
28+
}
29+
30+
final ForTask forTask = task.getTask().getForTask();
31+
32+
if (forTask.getDo().isEmpty()) {
33+
return;
34+
}
35+
36+
String noteLabel =
37+
String.format(
38+
"• each: %s<br/>• in: %s<br/> • at: %s",
39+
forTask.getFor().getEach(), forTask.getFor().getIn(), forTask.getFor().getAt());
40+
Node note = NodeBuilder.note(noteLabel);
41+
this.addBranch(note.getId(), note);
42+
43+
Node loop = NodeBuilder.split();
44+
this.addBranch(loop.getId(), loop);
45+
46+
this.branches.putAll(new MermaidGraph().build(forTask.getDo()));
47+
final Node firstTask = this.branches.get(forTask.getDo().get(0).getName());
48+
49+
note.setNext(loop);
50+
loop.setNext(firstTask);
51+
52+
String lastForTask = forTask.getDo().get(forTask.getDo().size() - 1).getName();
53+
String renderedArrow = "-. |next| .->";
54+
if (forTask.getWhile() != null && !forTask.getWhile().isEmpty()) {
55+
renderedArrow = "-. |while: " + NodeRenderer.escNodeLabel(forTask.getWhile()) + "| .->";
56+
}
57+
58+
this.getBranches().get(lastForTask).withNext(loop).setRenderedArrow(renderedArrow);
59+
}
60+
}
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
/*
2+
* Copyright 2020-Present The Serverless Workflow Specification Authors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package io.serverlessworkflow.mermaid;
17+
18+
import io.serverlessworkflow.api.types.ForkTask;
19+
import io.serverlessworkflow.api.types.TaskItem;
20+
import java.util.LinkedHashMap;
21+
import java.util.List;
22+
import java.util.Map;
23+
24+
public class ForkNode extends TaskSubgraphNode {
25+
26+
public ForkNode(TaskItem task) {
27+
super(task, String.format("fork: %s", task.getName()));
28+
29+
if (task.getTask().getForkTask() == null) {
30+
throw new IllegalStateException("Fork node must have a fork task");
31+
}
32+
33+
ForkTask fork = task.getTask().getForkTask();
34+
this.setDirection("LR");
35+
36+
// Split and join badges
37+
SplitNode split = NodeBuilder.split();
38+
String competeLabel = fork.getFork().isCompete() ? "ANY" : "ALL";
39+
Node join = new Node(Ids.newId(), competeLabel, NodeType.JUNCTION);
40+
this.addBranch(split.getId(), split);
41+
this.addBranch(join.getId(), join);
42+
43+
// Build each branch as its own (sub)graph
44+
List<TaskItem> branches = fork.getFork().getBranches();
45+
Map<String, Node> branchRoots = new LinkedHashMap<>();
46+
for (TaskItem branchTask : branches) {
47+
// render branch as a titled subgraph with its inner tasks
48+
String branchTitle = branchTask.getName();
49+
Node branchNode;
50+
51+
if (branchTask.getTask().getDoTask() != null) {
52+
branchNode =
53+
new TaskSubgraphNode(branchTask, branchTitle)
54+
.withBranches(branchTask.getTask().getDoTask().getDo());
55+
} else {
56+
branchNode = NodeBuilder.task(branchTask);
57+
}
58+
branchRoots.put(branchTitle, branchNode);
59+
this.addBranch(branchTitle, branchNode);
60+
}
61+
62+
for (TaskItem branchRoot : branches) {
63+
String name = branchRoot.getName();
64+
Node branch = branchRoots.get(name);
65+
split.addNext(branch);
66+
branch.setNext(join);
67+
branch.setRenderedArrow("-- |" + competeLabel + "| -->");
68+
}
69+
}
70+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
/*
2+
* Copyright 2020-Present The Serverless Workflow Specification Authors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package io.serverlessworkflow.mermaid;
17+
18+
import java.util.concurrent.ThreadLocalRandom;
19+
import java.util.concurrent.atomic.AtomicInteger;
20+
21+
public final class Ids {
22+
private final String salt = Integer.toString(ThreadLocalRandom.current().nextInt(), 36);
23+
private final AtomicInteger seq = new AtomicInteger();
24+
25+
private String build() {
26+
return "n_" + salt + "_" + Integer.toString(seq.getAndIncrement(), 36);
27+
}
28+
29+
public static String newId() {
30+
return new Ids().build();
31+
}
32+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
/*
2+
* Copyright 2020-Present The Serverless Workflow Specification Authors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package io.serverlessworkflow.mermaid;
17+
18+
import io.serverlessworkflow.api.types.SubscriptionIterator;
19+
20+
public class IteratorNode extends SubgraphNode {
21+
22+
public IteratorNode(String label, SubscriptionIterator iterator) {
23+
super(Ids.newId(), label);
24+
25+
if (iterator.getDo().isEmpty()) {
26+
return;
27+
}
28+
29+
Node note = NodeBuilder.note(String.format("• at: %s", iterator.getAt()));
30+
this.addBranch(note.getId(), note);
31+
32+
Node loop = NodeBuilder.junction();
33+
this.addBranch(loop.getId(), loop);
34+
35+
this.branches.putAll(new MermaidGraph().build(iterator.getDo()));
36+
final Node firstTask = this.branches.get(iterator.getDo().get(0).getName());
37+
38+
note.setNext(loop);
39+
loop.setNext(firstTask);
40+
41+
String lastForTask = iterator.getDo().get(iterator.getDo().size() - 1).getName();
42+
String renderedArrow = "-. |next| .->";
43+
44+
this.getBranches().get(lastForTask).withNext(loop).setRenderedArrow(renderedArrow);
45+
}
46+
}

0 commit comments

Comments
 (0)