Skip to content

Commit dbe5d11

Browse files
structurizr-dsl: PlantUML, Mermaid, and Kroki image views can now be defined by an inline source block.
1 parent c9d20b3 commit dbe5d11

File tree

9 files changed

+178
-53
lines changed

9 files changed

+178
-53
lines changed

changelog.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
- structurizr-dsl: Fixes https://github.com/structurizr/java/issues/435 (Relationship archetype not applied to implicit-source relationships).
1414
- structurizr-dsl: Adds support for removing relationships between software system instance/container instances, with a view to redefining them via infrastructure nodes.
1515
- structurizr-dsl: Adds support for a `jump` property on relationship styles.
16+
- structurizr-dsl: PlantUML, Mermaid, and Kroki image views can now be defined by an inline source block.
1617
- structurizr-import: Adds support for `plantuml.inline`, `mermaid.inline`, and `kroki.inline` properties to inline the resulting PNG/SVG file into the workspace.
1718
- structurizr-inspection: Adds a way to disable inspections via a workspace property named `structurizr.inspection` (`false` to disable).
1819
- structurizr-inspection: Default inspector adds a summary of error/warning/info/ignore counts as workspace properties.

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,4 +21,9 @@ int getLineNumber() {
2121
return lineNumber;
2222
}
2323

24+
@Override
25+
public String toString() {
26+
return source;
27+
}
28+
2429
}

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

Lines changed: 73 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,9 @@
1515

1616
final class ImageViewContentParser extends AbstractParser {
1717

18-
private static final String PLANTUML_GRAMMAR = "plantuml <file|url|viewKey>";
19-
private static final String MERMAID_GRAMMAR = "mermaid <file|url|viewKey>";
20-
private static final String KROKI_GRAMMAR = "kroki <format> <file|url>";
18+
private static final String PLANTUML_GRAMMAR = "plantuml <source|file|url|viewKey>";
19+
private static final String MERMAID_GRAMMAR = "mermaid <source|file|url|viewKey>";
20+
private static final String KROKI_GRAMMAR = "kroki <format> <source|file|url>";
2121
private static final String IMAGE_GRAMMAR = "image <file|url>";
2222

2323
private static final int PLANTUML_SOURCE_INDEX = 1;
@@ -33,7 +33,7 @@ final class ImageViewContentParser extends AbstractParser {
3333
}
3434

3535
void parsePlantUML(ImageViewDslContext context, File dslFile, Tokens tokens) {
36-
// plantuml <file|url|viewKey>
36+
// plantuml <source|file|url|viewKey>
3737

3838
if (tokens.hasMoreThan(PLANTUML_SOURCE_INDEX)) {
3939
throw new RuntimeException("Too many tokens, expected: " + PLANTUML_GRAMMAR);
@@ -45,22 +45,31 @@ void parsePlantUML(ImageViewDslContext context, File dslFile, Tokens tokens) {
4545
String source = tokens.get(PLANTUML_SOURCE_INDEX);
4646

4747
try {
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);
48+
if (source.contains("\n")) {
49+
// inline source
50+
new PlantUMLImporter().importDiagram(context.getView(), source);
5351
} else {
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));
52+
View viewWithKey = context.getWorkspace().getViews().getViewWithKey(source);
53+
if (viewWithKey instanceof ModelView) {
54+
StructurizrPlantUMLExporter exporter = new StructurizrPlantUMLExporter();
55+
String plantuml = exporter.export((ModelView) viewWithKey).getDefinition();
56+
new PlantUMLImporter().importDiagram(context.getView(), plantuml);
5857
} else {
59-
if (!restricted) {
60-
File file = new File(dslFile.getParentFile(), source);
61-
new PlantUMLImporter().importDiagram(context.getView(), file);
58+
if (Url.isUrl(source)) {
59+
RemoteContent content = readFromUrl(source);
60+
new PlantUMLImporter().importDiagram(context.getView(), content.getContent());
61+
context.getView().setTitle(source.substring(source.lastIndexOf("/") + 1));
6262
} else {
63-
throw new RuntimeException("PlantUML source must be specified as a URL when running in restricted mode");
63+
if (!restricted) {
64+
File file = new File(dslFile.getParentFile(), source);
65+
if (file.exists()) {
66+
new PlantUMLImporter().importDiagram(context.getView(), file);
67+
} else {
68+
throw new RuntimeException("The file at " + file.getAbsolutePath() + " does not exist");
69+
}
70+
} else {
71+
throw new RuntimeException("PlantUML source must be specified as a URL when running in restricted mode");
72+
}
6473
}
6574
}
6675
}
@@ -74,7 +83,7 @@ void parsePlantUML(ImageViewDslContext context, File dslFile, Tokens tokens) {
7483
}
7584

7685
void parseMermaid(ImageViewDslContext context, File dslFile, Tokens tokens) {
77-
// mermaid <file|url|viewKey>
86+
// mermaid <source|file|url|viewKey>
7887

7988
if (tokens.hasMoreThan(MERMAID_SOURCE_INDEX)) {
8089
throw new RuntimeException("Too many tokens, expected: " + MERMAID_GRAMMAR);
@@ -86,22 +95,31 @@ void parseMermaid(ImageViewDslContext context, File dslFile, Tokens tokens) {
8695
String source = tokens.get(MERMAID_SOURCE_INDEX);
8796

8897
try {
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);
98+
if (source.contains("\n")) {
99+
// inline source
100+
new MermaidImporter().importDiagram(context.getView(), source);
94101
} else {
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));
102+
View viewWithKey = context.getWorkspace().getViews().getViewWithKey(source);
103+
if (viewWithKey instanceof ModelView) {
104+
MermaidDiagramExporter exporter = new MermaidDiagramExporter();
105+
String mermaid = exporter.export((ModelView) viewWithKey).getDefinition();
106+
new MermaidImporter().importDiagram(context.getView(), mermaid);
99107
} else {
100-
if (!restricted) {
101-
File file = new File(dslFile.getParentFile(), source);
102-
new MermaidImporter().importDiagram(context.getView(), file);
108+
if (Url.isUrl(source)) {
109+
RemoteContent content = readFromUrl(source);
110+
new MermaidImporter().importDiagram(context.getView(), content.getContent());
111+
context.getView().setTitle(source.substring(source.lastIndexOf("/") + 1));
103112
} else {
104-
throw new RuntimeException("Mermaid source must be specified as a URL when running in restricted mode");
113+
if (!restricted) {
114+
File file = new File(dslFile.getParentFile(), source);
115+
if (file.exists()) {
116+
new MermaidImporter().importDiagram(context.getView(), file);
117+
} else {
118+
throw new RuntimeException("The file at " + file.getAbsolutePath() + " does not exist");
119+
}
120+
} else {
121+
throw new RuntimeException("Mermaid source must be specified as a URL when running in restricted mode");
122+
}
105123
}
106124
}
107125
}
@@ -115,7 +133,7 @@ void parseMermaid(ImageViewDslContext context, File dslFile, Tokens tokens) {
115133
}
116134

117135
void parseKroki(ImageViewDslContext context, File dslFile, Tokens tokens) {
118-
// kroki <format> <file|url>
136+
// kroki <format> <source|file|url>
119137

120138
if (tokens.hasMoreThan(KROKI_SOURCE_INDEX)) {
121139
throw new RuntimeException("Too many tokens, expected: " + KROKI_GRAMMAR);
@@ -128,16 +146,25 @@ void parseKroki(ImageViewDslContext context, File dslFile, Tokens tokens) {
128146
String source = tokens.get(KROKI_SOURCE_INDEX);
129147

130148
try {
131-
if (Url.isUrl(source)) {
132-
RemoteContent content = readFromUrl(source);
133-
new KrokiImporter().importDiagram(context.getView(), format, content.getContent());
134-
context.getView().setTitle(source.substring(source.lastIndexOf("/")+1));
149+
if (source.contains("\n")) {
150+
// inline source
151+
new KrokiImporter().importDiagram(context.getView(), format, source);
135152
} else {
136-
if (!restricted) {
137-
File file = new File(dslFile.getParentFile(), source);
138-
new KrokiImporter().importDiagram(context.getView(), format, file);
153+
if (Url.isUrl(source)) {
154+
RemoteContent content = readFromUrl(source);
155+
new KrokiImporter().importDiagram(context.getView(), format, content.getContent());
156+
context.getView().setTitle(source.substring(source.lastIndexOf("/") + 1));
139157
} else {
140-
throw new RuntimeException("Kroki source must be specified as a URL when running in restricted mode");
158+
if (!restricted) {
159+
File file = new File(dslFile.getParentFile(), source);
160+
if (file.exists()) {
161+
new KrokiImporter().importDiagram(context.getView(), format, file);
162+
} else {
163+
throw new RuntimeException("The file at " + file.getAbsolutePath() + " does not exist");
164+
}
165+
} else {
166+
throw new RuntimeException("Kroki source must be specified as a URL when running in restricted mode");
167+
}
141168
}
142169
}
143170
} catch (Exception e) {
@@ -168,8 +195,12 @@ void parseImage(ImageViewDslContext context, File dslFile, Tokens tokens) {
168195
} else {
169196
if (!restricted) {
170197
File file = new File(dslFile.getParentFile(), source);
171-
context.getView().setContent(ImageUtils.getImageAsDataUri(file));
172-
context.getView().setTitle(file.getName());
198+
if (file.exists()) {
199+
context.getView().setContent(ImageUtils.getImageAsDataUri(file));
200+
context.getView().setTitle(file.getName());
201+
} else {
202+
throw new RuntimeException("The file at " + file.getAbsolutePath() + " does not exist");
203+
}
173204
} else {
174205
throw new RuntimeException("Images must be specified as a URL when running in restricted mode");
175206
}

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

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ public final class StructurizrDslParser extends StructurizrDslTokens {
3636
private static final String TEXT_BLOCK_MARKER = "\"\"\"";
3737

3838
private static final Pattern STRING_SUBSTITUTION_PATTERN = Pattern.compile("(\\$\\{[a-zA-Z0-9-_.]+?})");
39+
private static final String STRING_SUBSTITUTION_TEMPLATE = "${%s}";
3940

4041
private static final String STRUCTURIZR_DSL_IDENTIFIER_PROPERTY_NAME = "structurizr.dsl.identifier";
4142

@@ -1289,7 +1290,7 @@ private List<DslLine> preProcessLines(List<String> lines) {
12891290
for (String line : lines) {
12901291
if (textBlock) {
12911292
if (line.endsWith(TEXT_BLOCK_MARKER)) {
1292-
buf.append("\"");
1293+
buf.append(TEXT_BLOCK_MARKER);
12931294
textBlock = false;
12941295
textBlockLeadingSpace = -1;
12951296
lineComplete = true;
@@ -1304,14 +1305,18 @@ private List<DslLine> preProcessLines(List<String> lines) {
13041305
}
13051306
}
13061307
}
1307-
buf.append(line, textBlockLeadingSpace, line.length());
1308-
buf.append("\n");
1308+
if (StringUtils.isNullOrEmpty(line)) {
1309+
buf.append("\n");
1310+
} else {
1311+
buf.append(line, textBlockLeadingSpace, line.length());
1312+
buf.append("\n");
1313+
}
13091314
}
13101315
} else if (!COMMENT_PATTERN.matcher(line).matches() && line.endsWith(MULTI_LINE_SEPARATOR)) {
13111316
buf.append(line, 0, line.length() - 1);
13121317
lineComplete = false;
13131318
} else if (!COMMENT_PATTERN.matcher(line).matches() && line.endsWith(TEXT_BLOCK_MARKER)) {
1314-
buf.append(line, 0, line.length() - 2);
1319+
buf.append(line, 0, line.length());
13151320
lineComplete = false;
13161321
textBlock = true;
13171322
} else {
@@ -1324,7 +1329,19 @@ private List<DslLine> preProcessLines(List<String> lines) {
13241329
}
13251330

13261331
if (lineComplete) {
1327-
dslLines.add(new DslLine(buf.toString(), lineNumber));
1332+
// replace the text block with a constant (that will become substituted later)
1333+
// (this makes it possible for text blocks to include double-quote characters)
1334+
String s = buf.toString();
1335+
if (s.endsWith(TEXT_BLOCK_MARKER)) {
1336+
String[] parts = s.split(TEXT_BLOCK_MARKER);
1337+
String constantName = UUID.randomUUID().toString();
1338+
String constantValue = parts[1].substring(0, parts[1].length() - 1); // remove final line break
1339+
addConstant(constantName, constantValue);
1340+
dslLines.add(new DslLine(parts[0] + "\"" + String.format(STRING_SUBSTITUTION_TEMPLATE, constantName) + "\"", lineNumber));
1341+
} else {
1342+
dslLines.add(new DslLine(buf.toString(), lineNumber));
1343+
}
1344+
13281345
buf = new StringBuilder();
13291346
}
13301347

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ final class Tokens {
1111
}
1212

1313
String get(int index) {
14-
return tokens.get(index).trim().replaceAll("\\\\\"", "\"").trim().replaceAll("\\\\n", "\n");
14+
return tokens.get(index).replaceAll("\\\\\"", "\"").replaceAll("\\\\n", "\n");
1515
}
1616

1717
void remove(int index) {

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

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1129,6 +1129,30 @@ void test_imageViews_ViaUrls() throws Exception {
11291129
assertEquals("image/svg+xml", svgView.getContentType());
11301130
}
11311131

1132+
@Test
1133+
void test_imageViews_ViaSource() throws Exception {
1134+
StructurizrDslParser parser = new StructurizrDslParser();
1135+
parser.parse(new File("src/test/resources/dsl/image-views/workspace-via-source.dsl"));
1136+
1137+
Workspace workspace = parser.getWorkspace();
1138+
assertEquals(3, workspace.getViews().getImageViews().size());
1139+
1140+
ImageView plantumlView = (ImageView)workspace.getViews().getViewWithKey("plantuml");
1141+
assertNull(plantumlView.getTitle());
1142+
assertEquals("http://localhost:7777/svg/SoWkIImgAStDuNBAJrBGjLDmpCbCJbMmKiX8pSd9vt98pKi1IW80", plantumlView.getContent());
1143+
assertEquals("image/svg+xml", plantumlView.getContentType());
1144+
1145+
ImageView mermaidView = (ImageView)workspace.getViews().getViewWithKey("mermaid");
1146+
assertNull(mermaidView.getTitle());
1147+
assertEquals("http://localhost:8888/svg/Zmxvd2NoYXJ0IFRECiAgICBTdGFydCAtLT4gU3RvcA==", mermaidView.getContent());
1148+
assertEquals("image/svg+xml", mermaidView.getContentType());
1149+
1150+
ImageView krokiView = (ImageView)workspace.getViews().getViewWithKey("kroki");
1151+
assertNull(krokiView.getTitle());
1152+
assertEquals("http://localhost:9999/graphviz/png/eNpLyUwvSizIUHBXqPZIzcnJ17ULzy_KSanlAgB1EAjQ", krokiView.getContent());
1153+
assertEquals("image/png", krokiView.getContentType());
1154+
}
1155+
11321156
@Test
11331157
void test_EmptyDeploymentEnvironment() throws Exception {
11341158
StructurizrDslParser parser = new StructurizrDslParser();

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

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ void test_parsePlantUML_ThrowsAnException_WithTooFewTokens() {
2828
parser.parsePlantUML(context, null, tokens("plantuml"));
2929
fail();
3030
} catch (Exception e) {
31-
assertEquals("Expected: plantuml <file|url|viewKey>", e.getMessage());
31+
assertEquals("Expected: plantuml <source|file|url|viewKey>", e.getMessage());
3232
}
3333
}
3434

@@ -64,10 +64,10 @@ void test_parseMermaid_ThrowsAnException_WithTooFewTokens() {
6464
ImageViewDslContext context = new ImageViewDslContext(imageView);
6565
context.setWorkspace(workspace);
6666
parser = new ImageViewContentParser(true);
67-
parser.parseMermaid(context, null, tokens("plantuml"));
67+
parser.parseMermaid(context, null, tokens("mermaid"));
6868
fail();
6969
} catch (Exception e) {
70-
assertEquals("Expected: mermaid <file|url|viewKey>", e.getMessage());
70+
assertEquals("Expected: mermaid <source|file|url|viewKey>", e.getMessage());
7171
}
7272
}
7373

@@ -97,6 +97,19 @@ void test_parseMermaid_WithViewKey() {
9797
assertEquals("https://mermaid.ink/svg/pako:eJxlkMtuwjAQRX9lNAhlE9SwqupCpLLuLt0RFiYeJxZ-RLYppYh_bxJHVR93NrM4c3U0N8DGCUKGred9B2-72gJoZU9VvGoCQZKfdQSptGYLOaW2IxPOx3QiFB8WA_saq2uIZOCVWxEa3lONhxEd4FQ2kz_L8hC9O9HvboD10LYR6j1dbjPpbFxdSLVdZHB0WmTly-ZhAMp_VFCfxOCxWD6D4b5VdhVdz6DoP7JyXzkZL9wTJNVD6vjjuZ4NxZRvwyc-Tt447TxbFFOSL1mBOaAhb7gSyG4YOzLjV-f_4f3-BQMfekI=", imageView.getContent());
9898
}
9999

100+
@Test
101+
void test_parseKroki_ThrowsAnException_WithTooFewTokens() {
102+
try {
103+
ImageViewDslContext context = new ImageViewDslContext(imageView);
104+
context.setWorkspace(workspace);
105+
parser = new ImageViewContentParser(true);
106+
parser.parseKroki(context, null, tokens("kroki"));
107+
fail();
108+
} catch (Exception e) {
109+
assertEquals("Expected: kroki <format> <source|file|url>", e.getMessage());
110+
}
111+
}
112+
100113
@Test
101114
void test_parseKroki_ThrowsAnException_WhenUsingAFileNameInRestrictedMode() {
102115
try {

structurizr-dsl/src/test/resources/dsl/image-view.dsl

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@ workspace {
22

33
views {
44
image * "Image" {
5-
image image.png
6-
}
5+
image image.png
76
}
7+
}
88

99
}

0 commit comments

Comments
 (0)