Skip to content

Commit

Permalink
structurizr-dsl: Adds the ability to define a PlantUML/Mermaid image …
Browse files Browse the repository at this point in the history
…view that is an export of a workspace view.
  • Loading branch information
simonbrowndotje committed Oct 6, 2024
1 parent 34cd149 commit b412261
Show file tree
Hide file tree
Showing 6 changed files with 117 additions and 25 deletions.
1 change: 1 addition & 0 deletions changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
- structurizr-dsl: Fixes https://github.com/structurizr/java/issues/346 (`// comment \` joins lines).
- 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.
- 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`.
- structurizr-dsl: Adds the ability to define a PlantUML/Mermaid image view that is an export of a workspace view.

## 3.0.0 (19th September 2024)

Expand Down
1 change: 1 addition & 0 deletions structurizr-dsl/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ dependencies {

api project(':structurizr-client')
api project(':structurizr-import')
api project(':structurizr-export')
api project(':structurizr-component')

testImplementation 'org.codehaus.groovy:groovy-jsr223:3.0.22'
Expand Down
Original file line number Diff line number Diff line change
@@ -1,24 +1,25 @@
package com.structurizr.dsl;

import com.structurizr.export.mermaid.MermaidDiagramExporter;
import com.structurizr.export.plantuml.StructurizrPlantUMLExporter;
import com.structurizr.importer.diagrams.kroki.KrokiImporter;
import com.structurizr.importer.diagrams.mermaid.MermaidImporter;
import com.structurizr.importer.diagrams.plantuml.PlantUMLImporter;
import com.structurizr.util.ImageUtils;
import com.structurizr.util.Url;
import com.structurizr.view.ImageView;
import com.structurizr.view.ModelView;
import com.structurizr.view.View;

import java.io.File;

final class ImageViewContentParser extends AbstractParser {

private static final String PLANTUML_GRAMMAR = "plantuml <file|url>";
private static final String MERMAID_GRAMMAR = "mermaid <file|url>";
private static final String PLANTUML_GRAMMAR = "plantuml <file|url|viewKey>";
private static final String MERMAID_GRAMMAR = "mermaid <file|url|viewKey>";
private static final String KROKI_GRAMMAR = "kroki <format> <file|url>";
private static final String IMAGE_GRAMMAR = "image <file|url>";

private static final int TITLE_INDEX = 1;
private static final int DESCRIPTION_INDEX = 1;

private static final int PLANTUML_SOURCE_INDEX = 1;
private static final int MERMAID_SOURCE_INDEX = 1;
private static final int KROKI_FORMAT_INDEX = 1;
Expand All @@ -32,7 +33,7 @@ final class ImageViewContentParser extends AbstractParser {
}

void parsePlantUML(ImageViewDslContext context, File dslFile, Tokens tokens) {
// plantuml <file|url>
// plantuml <file|url|viewKey>

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

try {
if (Url.isUrl(source)) {
RemoteContent content = readFromUrl(source);
new PlantUMLImporter().importDiagram(context.getView(), content.getContent());
context.getView().setTitle(source.substring(source.lastIndexOf("/")+1));
View viewWithKey = context.getWorkspace().getViews().getViewWithKey(source);
if (viewWithKey instanceof ModelView) {
StructurizrPlantUMLExporter exporter = new StructurizrPlantUMLExporter();
String plantuml = exporter.export((ModelView)viewWithKey).getDefinition();
new PlantUMLImporter().importDiagram(context.getView(), plantuml);
} else {
if (!restricted) {
File file = new File(dslFile.getParentFile(), source);
new PlantUMLImporter().importDiagram(context.getView(), file);
if (Url.isUrl(source)) {
RemoteContent content = readFromUrl(source);
new PlantUMLImporter().importDiagram(context.getView(), content.getContent());
context.getView().setTitle(source.substring(source.lastIndexOf("/") + 1));
} else {
throw new RuntimeException("PlantUML source must be specified as a URL when running in restricted mode");
if (!restricted) {
File file = new File(dslFile.getParentFile(), source);
new PlantUMLImporter().importDiagram(context.getView(), file);
} else {
throw new RuntimeException("PlantUML source must be specified as a URL when running in restricted mode");
}
}
}
} catch (Exception e) {
Expand All @@ -66,7 +74,7 @@ void parsePlantUML(ImageViewDslContext context, File dslFile, Tokens tokens) {
}

void parseMermaid(ImageViewDslContext context, File dslFile, Tokens tokens) {
// mermaid <file|url>
// mermaid <file|url|viewKey>

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

try {
if (Url.isUrl(source)) {
RemoteContent content = readFromUrl(source);
new MermaidImporter().importDiagram(context.getView(), content.getContent());
context.getView().setTitle(source.substring(source.lastIndexOf("/")+1));
View viewWithKey = context.getWorkspace().getViews().getViewWithKey(source);
if (viewWithKey instanceof ModelView) {
MermaidDiagramExporter exporter = new MermaidDiagramExporter();
String mermaid = exporter.export((ModelView)viewWithKey).getDefinition();
new MermaidImporter().importDiagram(context.getView(), mermaid);
} else {
if (!restricted) {
File file = new File(dslFile.getParentFile(), source);
new MermaidImporter().importDiagram(context.getView(), file);
if (Url.isUrl(source)) {
RemoteContent content = readFromUrl(source);
new MermaidImporter().importDiagram(context.getView(), content.getContent());
context.getView().setTitle(source.substring(source.lastIndexOf("/") + 1));
} else {
throw new RuntimeException("Mermaid source must be specified as a URL when running in restricted mode");
if (!restricted) {
File file = new File(dslFile.getParentFile(), source);
new MermaidImporter().importDiagram(context.getView(), file);
} else {
throw new RuntimeException("Mermaid source must be specified as a URL when running in restricted mode");
}
}
}
} catch (Exception e) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package com.structurizr.dsl;

import com.structurizr.importer.diagrams.mermaid.MermaidImporter;
import com.structurizr.importer.diagrams.plantuml.PlantUMLImporter;
import com.structurizr.view.ImageView;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
Expand All @@ -17,6 +19,19 @@ void setUp() {
imageView = workspace.getViews().createImageView("key");
}

@Test
void test_parsePlantUML_ThrowsAnException_WithTooFewTokens() {
try {
ImageViewDslContext context = new ImageViewDslContext(imageView);
context.setWorkspace(workspace);
parser = new ImageViewContentParser(true);
parser.parsePlantUML(context, null, tokens("plantuml"));
fail();
} catch (Exception e) {
assertEquals("Expected: plantuml <file|url|viewKey>", e.getMessage());
}
}

@Test
void test_parsePlantUML_ThrowsAnException_WhenUsingAFileNameInRestrictedMode() {
try {
Expand All @@ -30,6 +45,32 @@ void test_parsePlantUML_ThrowsAnException_WhenUsingAFileNameInRestrictedMode() {
}
}

@Test
void test_parsePlantUML_WithViewKey() {
ImageViewDslContext context = new ImageViewDslContext(imageView);
context.setWorkspace(workspace);
workspace.getModel().addSoftwareSystem("A");
workspace.getViews().createSystemLandscapeView("SystemLandscape", "Description").addAllElements();
workspace.getViews().getConfiguration().addProperty(PlantUMLImporter.PLANTUML_URL_PROPERTY, "https://plantuml.com/plantuml");

parser = new ImageViewContentParser(true);
parser.parsePlantUML(context, null, tokens("plantuml", "SystemLandscape"));
assertEquals("https://plantuml.com/plantuml/svg/HP2nJiD038RtUmghF00f6oYD6f2OO2eI0p2Od9EUUh4Zdwkq8DwTkrB0bZpylsL_zZePgkt7w18P99fGqKI1XSbPi4YmEIQZ4HwGVUfm8kTC9Z21Tp6J4NnGwYm8EvTsWSk44JuT0AhAV2zic_11iAoovAd7VRGdEbWRmy0ZiK6N2sbsPyNfENZRmbLLkaSyF59AED1vGkM-dDi6Jv2HbCIE1UT_Qm517YBLTTiq9uXRx7Q3ofxzdSHys8K_HNOAsLchJb6wHJtfMRt6abbDM_Go1nwWnvYeGFnjWiLgrRvodJBXpR9gNZRIsupw-xUt-h9OpG9-c311wzoQsEUdVmC0", imageView.getContent());
}

@Test
void test_parseMermaid_ThrowsAnException_WithTooFewTokens() {
try {
ImageViewDslContext context = new ImageViewDslContext(imageView);
context.setWorkspace(workspace);
parser = new ImageViewContentParser(true);
parser.parseMermaid(context, null, tokens("plantuml"));
fail();
} catch (Exception e) {
assertEquals("Expected: mermaid <file|url|viewKey>", e.getMessage());
}
}

@Test
void test_parseMermaid_ThrowsAnException_WhenUsingAFileNameInRestrictedMode() {
try {
Expand All @@ -43,6 +84,19 @@ void test_parseMermaid_ThrowsAnException_WhenUsingAFileNameInRestrictedMode() {
}
}

@Test
void test_parseMermaid_WithViewKey() {
ImageViewDslContext context = new ImageViewDslContext(imageView);
context.setWorkspace(workspace);
workspace.getModel().addSoftwareSystem("A");
workspace.getViews().createSystemLandscapeView("SystemLandscape", "Description").addAllElements();
workspace.getViews().getConfiguration().addProperty(MermaidImporter.MERMAID_URL_PROPERTY, "https://mermaid.ink");

parser = new ImageViewContentParser(true);
parser.parseMermaid(context, null, tokens("mermaid", "SystemLandscape"));
assertEquals("https://mermaid.ink/svg/pako:eJxlkMtuwjAQRX9lNAhlE9SwqupCpLLuLt0RFiYeJxZ-RLYppYh_bxJHVR93NrM4c3U0N8DGCUKGred9B2-72gJoZU9VvGoCQZKfdQSptGYLOaW2IxPOx3QiFB8WA_saq2uIZOCVWxEa3lONhxEd4FQ2kz_L8hC9O9HvboD10LYR6j1dbjPpbFxdSLVdZHB0WmTly-ZhAMp_VFCfxOCxWD6D4b5VdhVdz6DoP7JyXzkZL9wTJNVD6vjjuZ4NxZRvwyc-Tt447TxbFFOSL1mBOaAhb7gSyG4YOzLjV-f_4f3-BQMfekI=", imageView.getContent());
}

@Test
void test_parseKroki_ThrowsAnException_WhenUsingAFileNameInRestrictedMode() {
try {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,26 @@ public final Collection<Diagram> export(Workspace workspace) {
return diagrams;
}

public Diagram export(ModelView view) {
if (view instanceof SystemLandscapeView) {
return export((SystemLandscapeView)view);
} else if (view instanceof SystemContextView) {
return export((SystemContextView)view);
} else if (view instanceof ContainerView) {
return export((ContainerView)view);
} else if (view instanceof ComponentView) {
return export((ComponentView)view);
} else if (view instanceof DynamicView) {
return export((DynamicView)view);
} else if (view instanceof DeploymentView) {
return export((DeploymentView)view);
} else if (view instanceof CustomView) {
return export((CustomView)view);
} else {
throw new RuntimeException(view.getClass().getName() + " is not supported");
}
}

public Diagram export(CustomView view) {
Diagram diagram = export(view, null);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,9 @@ public class PlantUMLEncoderTests {
@Test
public void encode() throws Exception {
File file = new File("./src/test/resources/diagrams/plantuml/with-title.puml");
String mermaid = new String(Files.readAllBytes(file.toPath()), StandardCharsets.UTF_8);
assertEquals("SoWkIImgAStDuIh9BCb9LGXEBInDpKjELKZ9J4mlIinLIAr8p2t8IULooazIqBLJSCp914fQAMIavkJaSpcavgK0zG80", new PlantUMLEncoder().encode(mermaid));
String plantuml = new String(Files.readAllBytes(file.toPath()), StandardCharsets.UTF_8);
String encodedPlantuml = new PlantUMLEncoder().encode(plantuml);
assertEquals("SoWkIImgAStDuIh9BCb9LGXEBInDpKjELKZ9J4mlIinLIAr8p2t8IULooazIqBLJSCp914fQAMIavkJaSpcavgK0zG80", encodedPlantuml);
}

}

0 comments on commit b412261

Please sign in to comment.