Skip to content
This repository was archived by the owner on Mar 28, 2026. It is now read-only.

Commit b412261

Browse files
structurizr-dsl: Adds the ability to define a PlantUML/Mermaid image view that is an export of a workspace view.
1 parent 34cd149 commit b412261

6 files changed

Lines changed: 117 additions & 25 deletions

File tree

changelog.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
- structurizr-dsl: Fixes https://github.com/structurizr/java/issues/346 (`// comment \` joins lines).
1111
- structurizr-dsl: Anonymous identifiers for relationships (i.e. relationships not assigned to an identifier) are excluded from the model, and therefore also excluded from the serialised JSON.
1212
- structurizr-dsl: Adds a way to configure whether the DSL source is retained via a workspace property named `structurizr.dsl.source` - `true` (default) or `false`.
13+
- structurizr-dsl: Adds the ability to define a PlantUML/Mermaid image view that is an export of a workspace view.
1314

1415
## 3.0.0 (19th September 2024)
1516

structurizr-dsl/build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ dependencies {
22

33
api project(':structurizr-client')
44
api project(':structurizr-import')
5+
api project(':structurizr-export')
56
api project(':structurizr-component')
67

78
testImplementation 'org.codehaus.groovy:groovy-jsr223:3.0.22'

structurizr-dsl/src/main/java/com/structurizr/dsl/ImageViewContentParser.java

Lines changed: 38 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,25 @@
11
package com.structurizr.dsl;
22

3+
import com.structurizr.export.mermaid.MermaidDiagramExporter;
4+
import com.structurizr.export.plantuml.StructurizrPlantUMLExporter;
35
import com.structurizr.importer.diagrams.kroki.KrokiImporter;
46
import com.structurizr.importer.diagrams.mermaid.MermaidImporter;
57
import com.structurizr.importer.diagrams.plantuml.PlantUMLImporter;
68
import com.structurizr.util.ImageUtils;
79
import com.structurizr.util.Url;
810
import com.structurizr.view.ImageView;
11+
import com.structurizr.view.ModelView;
12+
import com.structurizr.view.View;
913

1014
import java.io.File;
1115

1216
final class ImageViewContentParser extends AbstractParser {
1317

14-
private static final String PLANTUML_GRAMMAR = "plantuml <file|url>";
15-
private static final String MERMAID_GRAMMAR = "mermaid <file|url>";
18+
private static final String PLANTUML_GRAMMAR = "plantuml <file|url|viewKey>";
19+
private static final String MERMAID_GRAMMAR = "mermaid <file|url|viewKey>";
1620
private static final String KROKI_GRAMMAR = "kroki <format> <file|url>";
1721
private static final String IMAGE_GRAMMAR = "image <file|url>";
1822

19-
private static final int TITLE_INDEX = 1;
20-
private static final int DESCRIPTION_INDEX = 1;
21-
2223
private static final int PLANTUML_SOURCE_INDEX = 1;
2324
private static final int MERMAID_SOURCE_INDEX = 1;
2425
private static final int KROKI_FORMAT_INDEX = 1;
@@ -32,7 +33,7 @@ final class ImageViewContentParser extends AbstractParser {
3233
}
3334

3435
void parsePlantUML(ImageViewDslContext context, File dslFile, Tokens tokens) {
35-
// plantuml <file|url>
36+
// plantuml <file|url|viewKey>
3637

3738
if (tokens.hasMoreThan(PLANTUML_SOURCE_INDEX)) {
3839
throw new RuntimeException("Too many tokens, expected: " + PLANTUML_GRAMMAR);
@@ -44,16 +45,23 @@ void parsePlantUML(ImageViewDslContext context, File dslFile, Tokens tokens) {
4445
String source = tokens.get(PLANTUML_SOURCE_INDEX);
4546

4647
try {
47-
if (Url.isUrl(source)) {
48-
RemoteContent content = readFromUrl(source);
49-
new PlantUMLImporter().importDiagram(context.getView(), content.getContent());
50-
context.getView().setTitle(source.substring(source.lastIndexOf("/")+1));
48+
View viewWithKey = context.getWorkspace().getViews().getViewWithKey(source);
49+
if (viewWithKey instanceof ModelView) {
50+
StructurizrPlantUMLExporter exporter = new StructurizrPlantUMLExporter();
51+
String plantuml = exporter.export((ModelView)viewWithKey).getDefinition();
52+
new PlantUMLImporter().importDiagram(context.getView(), plantuml);
5153
} else {
52-
if (!restricted) {
53-
File file = new File(dslFile.getParentFile(), source);
54-
new PlantUMLImporter().importDiagram(context.getView(), file);
54+
if (Url.isUrl(source)) {
55+
RemoteContent content = readFromUrl(source);
56+
new PlantUMLImporter().importDiagram(context.getView(), content.getContent());
57+
context.getView().setTitle(source.substring(source.lastIndexOf("/") + 1));
5558
} else {
56-
throw new RuntimeException("PlantUML source must be specified as a URL when running in restricted mode");
59+
if (!restricted) {
60+
File file = new File(dslFile.getParentFile(), source);
61+
new PlantUMLImporter().importDiagram(context.getView(), file);
62+
} else {
63+
throw new RuntimeException("PlantUML source must be specified as a URL when running in restricted mode");
64+
}
5765
}
5866
}
5967
} catch (Exception e) {
@@ -66,7 +74,7 @@ void parsePlantUML(ImageViewDslContext context, File dslFile, Tokens tokens) {
6674
}
6775

6876
void parseMermaid(ImageViewDslContext context, File dslFile, Tokens tokens) {
69-
// mermaid <file|url>
77+
// mermaid <file|url|viewKey>
7078

7179
if (tokens.hasMoreThan(MERMAID_SOURCE_INDEX)) {
7280
throw new RuntimeException("Too many tokens, expected: " + MERMAID_GRAMMAR);
@@ -78,16 +86,23 @@ void parseMermaid(ImageViewDslContext context, File dslFile, Tokens tokens) {
7886
String source = tokens.get(MERMAID_SOURCE_INDEX);
7987

8088
try {
81-
if (Url.isUrl(source)) {
82-
RemoteContent content = readFromUrl(source);
83-
new MermaidImporter().importDiagram(context.getView(), content.getContent());
84-
context.getView().setTitle(source.substring(source.lastIndexOf("/")+1));
89+
View viewWithKey = context.getWorkspace().getViews().getViewWithKey(source);
90+
if (viewWithKey instanceof ModelView) {
91+
MermaidDiagramExporter exporter = new MermaidDiagramExporter();
92+
String mermaid = exporter.export((ModelView)viewWithKey).getDefinition();
93+
new MermaidImporter().importDiagram(context.getView(), mermaid);
8594
} else {
86-
if (!restricted) {
87-
File file = new File(dslFile.getParentFile(), source);
88-
new MermaidImporter().importDiagram(context.getView(), file);
95+
if (Url.isUrl(source)) {
96+
RemoteContent content = readFromUrl(source);
97+
new MermaidImporter().importDiagram(context.getView(), content.getContent());
98+
context.getView().setTitle(source.substring(source.lastIndexOf("/") + 1));
8999
} else {
90-
throw new RuntimeException("Mermaid source must be specified as a URL when running in restricted mode");
100+
if (!restricted) {
101+
File file = new File(dslFile.getParentFile(), source);
102+
new MermaidImporter().importDiagram(context.getView(), file);
103+
} else {
104+
throw new RuntimeException("Mermaid source must be specified as a URL when running in restricted mode");
105+
}
91106
}
92107
}
93108
} catch (Exception e) {

structurizr-dsl/src/test/java/com/structurizr/dsl/ImageViewContentParserTests.java

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package com.structurizr.dsl;
22

3+
import com.structurizr.importer.diagrams.mermaid.MermaidImporter;
4+
import com.structurizr.importer.diagrams.plantuml.PlantUMLImporter;
35
import com.structurizr.view.ImageView;
46
import org.junit.jupiter.api.BeforeEach;
57
import org.junit.jupiter.api.Test;
@@ -17,6 +19,19 @@ void setUp() {
1719
imageView = workspace.getViews().createImageView("key");
1820
}
1921

22+
@Test
23+
void test_parsePlantUML_ThrowsAnException_WithTooFewTokens() {
24+
try {
25+
ImageViewDslContext context = new ImageViewDslContext(imageView);
26+
context.setWorkspace(workspace);
27+
parser = new ImageViewContentParser(true);
28+
parser.parsePlantUML(context, null, tokens("plantuml"));
29+
fail();
30+
} catch (Exception e) {
31+
assertEquals("Expected: plantuml <file|url|viewKey>", e.getMessage());
32+
}
33+
}
34+
2035
@Test
2136
void test_parsePlantUML_ThrowsAnException_WhenUsingAFileNameInRestrictedMode() {
2237
try {
@@ -30,6 +45,32 @@ void test_parsePlantUML_ThrowsAnException_WhenUsingAFileNameInRestrictedMode() {
3045
}
3146
}
3247

48+
@Test
49+
void test_parsePlantUML_WithViewKey() {
50+
ImageViewDslContext context = new ImageViewDslContext(imageView);
51+
context.setWorkspace(workspace);
52+
workspace.getModel().addSoftwareSystem("A");
53+
workspace.getViews().createSystemLandscapeView("SystemLandscape", "Description").addAllElements();
54+
workspace.getViews().getConfiguration().addProperty(PlantUMLImporter.PLANTUML_URL_PROPERTY, "https://plantuml.com/plantuml");
55+
56+
parser = new ImageViewContentParser(true);
57+
parser.parsePlantUML(context, null, tokens("plantuml", "SystemLandscape"));
58+
assertEquals("https://plantuml.com/plantuml/svg/HP2nJiD038RtUmghF00f6oYD6f2OO2eI0p2Od9EUUh4Zdwkq8DwTkrB0bZpylsL_zZePgkt7w18P99fGqKI1XSbPi4YmEIQZ4HwGVUfm8kTC9Z21Tp6J4NnGwYm8EvTsWSk44JuT0AhAV2zic_11iAoovAd7VRGdEbWRmy0ZiK6N2sbsPyNfENZRmbLLkaSyF59AED1vGkM-dDi6Jv2HbCIE1UT_Qm517YBLTTiq9uXRx7Q3ofxzdSHys8K_HNOAsLchJb6wHJtfMRt6abbDM_Go1nwWnvYeGFnjWiLgrRvodJBXpR9gNZRIsupw-xUt-h9OpG9-c311wzoQsEUdVmC0", imageView.getContent());
59+
}
60+
61+
@Test
62+
void test_parseMermaid_ThrowsAnException_WithTooFewTokens() {
63+
try {
64+
ImageViewDslContext context = new ImageViewDslContext(imageView);
65+
context.setWorkspace(workspace);
66+
parser = new ImageViewContentParser(true);
67+
parser.parseMermaid(context, null, tokens("plantuml"));
68+
fail();
69+
} catch (Exception e) {
70+
assertEquals("Expected: mermaid <file|url|viewKey>", e.getMessage());
71+
}
72+
}
73+
3374
@Test
3475
void test_parseMermaid_ThrowsAnException_WhenUsingAFileNameInRestrictedMode() {
3576
try {
@@ -43,6 +84,19 @@ void test_parseMermaid_ThrowsAnException_WhenUsingAFileNameInRestrictedMode() {
4384
}
4485
}
4586

87+
@Test
88+
void test_parseMermaid_WithViewKey() {
89+
ImageViewDslContext context = new ImageViewDslContext(imageView);
90+
context.setWorkspace(workspace);
91+
workspace.getModel().addSoftwareSystem("A");
92+
workspace.getViews().createSystemLandscapeView("SystemLandscape", "Description").addAllElements();
93+
workspace.getViews().getConfiguration().addProperty(MermaidImporter.MERMAID_URL_PROPERTY, "https://mermaid.ink");
94+
95+
parser = new ImageViewContentParser(true);
96+
parser.parseMermaid(context, null, tokens("mermaid", "SystemLandscape"));
97+
assertEquals("https://mermaid.ink/svg/pako:eJxlkMtuwjAQRX9lNAhlE9SwqupCpLLuLt0RFiYeJxZ-RLYppYh_bxJHVR93NrM4c3U0N8DGCUKGred9B2-72gJoZU9VvGoCQZKfdQSptGYLOaW2IxPOx3QiFB8WA_saq2uIZOCVWxEa3lONhxEd4FQ2kz_L8hC9O9HvboD10LYR6j1dbjPpbFxdSLVdZHB0WmTly-ZhAMp_VFCfxOCxWD6D4b5VdhVdz6DoP7JyXzkZL9wTJNVD6vjjuZ4NxZRvwyc-Tt447TxbFFOSL1mBOaAhb7gSyG4YOzLjV-f_4f3-BQMfekI=", imageView.getContent());
98+
}
99+
46100
@Test
47101
void test_parseKroki_ThrowsAnException_WhenUsingAFileNameInRestrictedMode() {
48102
try {

structurizr-export/src/main/java/com/structurizr/export/AbstractDiagramExporter.java

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,26 @@ public final Collection<Diagram> export(Workspace workspace) {
8080
return diagrams;
8181
}
8282

83+
public Diagram export(ModelView view) {
84+
if (view instanceof SystemLandscapeView) {
85+
return export((SystemLandscapeView)view);
86+
} else if (view instanceof SystemContextView) {
87+
return export((SystemContextView)view);
88+
} else if (view instanceof ContainerView) {
89+
return export((ContainerView)view);
90+
} else if (view instanceof ComponentView) {
91+
return export((ComponentView)view);
92+
} else if (view instanceof DynamicView) {
93+
return export((DynamicView)view);
94+
} else if (view instanceof DeploymentView) {
95+
return export((DeploymentView)view);
96+
} else if (view instanceof CustomView) {
97+
return export((CustomView)view);
98+
} else {
99+
throw new RuntimeException(view.getClass().getName() + " is not supported");
100+
}
101+
}
102+
83103
public Diagram export(CustomView view) {
84104
Diagram diagram = export(view, null);
85105

structurizr-import/src/test/java/com/structurizr/importer/diagrams/plantuml/PlantUMLEncoderTests.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,9 @@ public class PlantUMLEncoderTests {
1313
@Test
1414
public void encode() throws Exception {
1515
File file = new File("./src/test/resources/diagrams/plantuml/with-title.puml");
16-
String mermaid = new String(Files.readAllBytes(file.toPath()), StandardCharsets.UTF_8);
17-
assertEquals("SoWkIImgAStDuIh9BCb9LGXEBInDpKjELKZ9J4mlIinLIAr8p2t8IULooazIqBLJSCp914fQAMIavkJaSpcavgK0zG80", new PlantUMLEncoder().encode(mermaid));
16+
String plantuml = new String(Files.readAllBytes(file.toPath()), StandardCharsets.UTF_8);
17+
String encodedPlantuml = new PlantUMLEncoder().encode(plantuml);
18+
assertEquals("SoWkIImgAStDuIh9BCb9LGXEBInDpKjELKZ9J4mlIinLIAr8p2t8IULooazIqBLJSCp914fQAMIavkJaSpcavgK0zG80", encodedPlantuml);
1819
}
1920

2021
}

0 commit comments

Comments
 (0)