diff --git a/changelog.md b/changelog.md index fee0fefc..96cdedfe 100644 --- a/changelog.md +++ b/changelog.md @@ -11,6 +11,7 @@ - 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. +- structurizr-dsl: Adds support for `url`, `properties`, and `perspectives` nested inside `!elements` and `!relationships`. ## 3.0.0 (19th September 2024) diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/ElementsDslContext.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/ElementsDslContext.java index d0aec535..7e056c15 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/ElementsDslContext.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/ElementsDslContext.java @@ -27,7 +27,14 @@ Set getModelItems() { @Override protected String[] getPermittedTokens() { - return new String[0]; + return new String[] { + StructurizrDslTokens.RELATIONSHIP_TOKEN, + StructurizrDslTokens.TAG_TOKEN, + StructurizrDslTokens.TAGS_TOKEN, + StructurizrDslTokens.URL_TOKEN, + StructurizrDslTokens.PROPERTIES_TOKEN, + StructurizrDslTokens.PERSPECTIVES_TOKEN + }; } } \ No newline at end of file diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/ModelItemParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/ModelItemParser.java index f98cb241..e4c48ac9 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/ModelItemParser.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/ModelItemParser.java @@ -8,10 +8,6 @@ final class ModelItemParser extends AbstractParser { private final static int URL_INDEX = 1; - private final static int PERSPECTIVE_NAME_INDEX = 0; - private final static int PERSPECTIVE_DESCRIPTION_INDEX = 1; - private final static int PERSPECTIVE_VALUE_INDEX = 2; - void parseTags(ModelItemDslContext context, Tokens tokens) { // tags [tags] if (!tokens.includes(TAGS_INDEX)) { @@ -52,26 +48,4 @@ void parseUrl(ModelItemDslContext context, Tokens tokens) { context.getModelItem().setUrl(url); } - void parsePerspective(ModelItemPerspectivesDslContext context, Tokens tokens) { - // [value] - - if (tokens.hasMoreThan(PERSPECTIVE_VALUE_INDEX)) { - throw new RuntimeException("Too many tokens, expected: [value]"); - } - - if (!tokens.includes(PERSPECTIVE_DESCRIPTION_INDEX)) { - throw new RuntimeException("Expected: [value]"); - } - - String name = tokens.get(PERSPECTIVE_NAME_INDEX); - String description = tokens.get(PERSPECTIVE_DESCRIPTION_INDEX); - String value = ""; - - if (tokens.includes(PERSPECTIVE_VALUE_INDEX)) { - value = tokens.get(PERSPECTIVE_VALUE_INDEX); - } - - context.getModelItem().addPerspective(name, description, value); - } - } \ No newline at end of file diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/ModelItemPerspectivesDslContext.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/ModelItemPerspectivesDslContext.java deleted file mode 100644 index 37647a49..00000000 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/ModelItemPerspectivesDslContext.java +++ /dev/null @@ -1,22 +0,0 @@ -package com.structurizr.dsl; - -import com.structurizr.model.ModelItem; - -final class ModelItemPerspectivesDslContext extends DslContext { - - private ModelItem modelItem; - - public ModelItemPerspectivesDslContext(ModelItem modelItem) { - this.modelItem = modelItem; - } - - ModelItem getModelItem() { - return this.modelItem; - } - - @Override - protected String[] getPermittedTokens() { - return new String[0]; - } - -} \ No newline at end of file diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/ModelItemsParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/ModelItemsParser.java index c34ceb33..2c44be60 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/ModelItemsParser.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/ModelItemsParser.java @@ -5,6 +5,7 @@ final class ModelItemsParser extends AbstractParser { private final static int TAGS_INDEX = 1; + private final static int URL_INDEX = 1; void parseTags(ModelItemsDslContext context, Tokens tokens) { // tags [tags] @@ -21,4 +22,20 @@ void parseTags(ModelItemsDslContext context, Tokens tokens) { } } + void parseUrl(ModelItemsDslContext context, Tokens tokens) { + // url + if (tokens.hasMoreThan(URL_INDEX)) { + throw new RuntimeException("Too many tokens, expected: url "); + } + + if (!tokens.includes(URL_INDEX)) { + throw new RuntimeException("Expected: url "); + } + + String url = tokens.get(URL_INDEX); + for (ModelItem modelItem : context.getModelItems()) { + modelItem.setUrl(url); + } + } + } \ No newline at end of file diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/PerspectiveParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/PerspectiveParser.java new file mode 100644 index 00000000..3a38ad3a --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/PerspectiveParser.java @@ -0,0 +1,35 @@ +package com.structurizr.dsl; + +import com.structurizr.model.ModelItem; + +final class PerspectiveParser extends AbstractParser { + + private final static int PERSPECTIVE_NAME_INDEX = 0; + private final static int PERSPECTIVE_DESCRIPTION_INDEX = 1; + private final static int PERSPECTIVE_VALUE_INDEX = 2; + + void parse(PerspectivesDslContext context, Tokens tokens) { + // [value] + + if (tokens.hasMoreThan(PERSPECTIVE_VALUE_INDEX)) { + throw new RuntimeException("Too many tokens, expected: [value]"); + } + + if (!tokens.includes(PERSPECTIVE_DESCRIPTION_INDEX)) { + throw new RuntimeException("Expected: [value]"); + } + + String name = tokens.get(PERSPECTIVE_NAME_INDEX); + String description = tokens.get(PERSPECTIVE_DESCRIPTION_INDEX); + String value = ""; + + if (tokens.includes(PERSPECTIVE_VALUE_INDEX)) { + value = tokens.get(PERSPECTIVE_VALUE_INDEX); + } + + for (ModelItem modelItem : context.getModelItems()) { + modelItem.addPerspective(name, description, value); + } + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/PerspectivesDslContext.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/PerspectivesDslContext.java new file mode 100644 index 00000000..50ff2e6d --- /dev/null +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/PerspectivesDslContext.java @@ -0,0 +1,29 @@ +package com.structurizr.dsl; + +import com.structurizr.model.ModelItem; + +import java.util.ArrayList; +import java.util.Collection; + +final class PerspectivesDslContext extends DslContext { + + private final Collection modelItems = new ArrayList<>(); + + PerspectivesDslContext(ModelItem modelItem) { + this.modelItems.add(modelItem); + } + + PerspectivesDslContext(Collection modelItems) { + this.modelItems.addAll(modelItems); + } + + Collection getModelItems() { + return this.modelItems; + } + + @Override + protected String[] getPermittedTokens() { + return new String[0]; + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/PropertiesDslContext.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/PropertiesDslContext.java index e1f8184f..6b31503d 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/PropertiesDslContext.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/PropertiesDslContext.java @@ -2,16 +2,23 @@ import com.structurizr.PropertyHolder; +import java.util.ArrayList; +import java.util.Collection; + final class PropertiesDslContext extends DslContext { - private PropertyHolder propertyHolder; + private final Collection propertyHolders = new ArrayList<>(); public PropertiesDslContext(PropertyHolder propertyHolder) { - this.propertyHolder = propertyHolder; + this.propertyHolders.add(propertyHolder); + } + + public PropertiesDslContext(Collection propertyHolders) { + this.propertyHolders.addAll(propertyHolders); } - PropertyHolder getPropertyHolder() { - return this.propertyHolder; + Collection getPropertyHolders() { + return this.propertyHolders; } @Override diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/PropertyParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/PropertyParser.java index 2129a2dd..0a1ba6fb 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/PropertyParser.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/PropertyParser.java @@ -1,5 +1,7 @@ package com.structurizr.dsl; +import com.structurizr.PropertyHolder; + final class PropertyParser extends AbstractParser { private final static int PROPERTY_NAME_INDEX = 0; @@ -19,7 +21,9 @@ void parse(PropertiesDslContext context, Tokens tokens) { String name = tokens.get(PROPERTY_NAME_INDEX); String value = tokens.get(PROPERTY_VALUE_INDEX); - context.getPropertyHolder().addProperty(name, value); + for (PropertyHolder propertyHolder : context.getPropertyHolders()) { + propertyHolder.addProperty(name, value); + } } } \ No newline at end of file diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/RelationshipsDslContext.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/RelationshipsDslContext.java index 3797be71..9a830de7 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/RelationshipsDslContext.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/RelationshipsDslContext.java @@ -26,7 +26,13 @@ Set getModelItems() { @Override protected String[] getPermittedTokens() { - return new String[0]; + return new String[] { + StructurizrDslTokens.TAG_TOKEN, + StructurizrDslTokens.TAGS_TOKEN, + StructurizrDslTokens.URL_TOKEN, + StructurizrDslTokens.PROPERTIES_TOKEN, + StructurizrDslTokens.PERSPECTIVES_TOKEN + }; } } \ No newline at end of file diff --git a/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParser.java b/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParser.java index 50f30b35..9581c6d5 100644 --- a/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParser.java +++ b/structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParser.java @@ -1,5 +1,6 @@ package com.structurizr.dsl; +import com.structurizr.PropertyHolder; import com.structurizr.Workspace; import com.structurizr.model.*; import com.structurizr.util.StringUtils; @@ -554,6 +555,9 @@ void parse(List lines, File dslFile, boolean fragment, boolean includeIn } else if (URL_TOKEN.equalsIgnoreCase(firstToken) && inContext(ModelItemDslContext.class) && !isGroup(getContext())) { new ModelItemParser().parseUrl(getContext(ModelItemDslContext.class), tokens); + } else if (URL_TOKEN.equalsIgnoreCase(firstToken) && inContext(ModelItemsDslContext.class)) { + new ModelItemsParser().parseUrl(getContext(ModelItemsDslContext.class), tokens); + } else if (PROPERTIES_TOKEN.equalsIgnoreCase(firstToken) && inContext(WorkspaceDslContext.class)) { startContext(new PropertiesDslContext(workspace)); @@ -566,6 +570,9 @@ void parse(List lines, File dslFile, boolean fragment, boolean includeIn } else if (PROPERTIES_TOKEN.equalsIgnoreCase(firstToken) && inContext(ModelItemDslContext.class) && !isGroup(getContext())) { startContext(new PropertiesDslContext(getContext(ModelItemDslContext.class).getModelItem())); + } else if (PROPERTIES_TOKEN.equalsIgnoreCase(firstToken) && inContext(ModelItemsDslContext.class)) { + startContext(new PropertiesDslContext(getContext(ModelItemsDslContext.class).getModelItems().stream().map(mi -> (PropertyHolder)mi).toList())); + } else if (PROPERTIES_TOKEN.equalsIgnoreCase(firstToken) && inContext(ViewsDslContext.class)) { startContext(new PropertiesDslContext(workspace.getViews().getConfiguration())); @@ -585,10 +592,13 @@ void parse(List lines, File dslFile, boolean fragment, boolean includeIn new PropertyParser().parse(getContext(PropertiesDslContext.class), tokens); } else if (PERSPECTIVES_TOKEN.equalsIgnoreCase(firstToken) && inContext(ModelItemDslContext.class) && !isGroup(getContext())) { - startContext(new ModelItemPerspectivesDslContext(getContext(ModelItemDslContext.class).getModelItem())); + startContext(new PerspectivesDslContext(getContext(ModelItemDslContext.class).getModelItem())); + + } else if (PERSPECTIVES_TOKEN.equalsIgnoreCase(firstToken) && inContext(ModelItemsDslContext.class)) { + startContext(new PerspectivesDslContext(getContext(ModelItemsDslContext.class).getModelItems())); - } else if (inContext(ModelItemPerspectivesDslContext.class)) { - new ModelItemParser().parsePerspective(getContext(ModelItemPerspectivesDslContext.class), tokens); + } else if (inContext(PerspectivesDslContext.class)) { + new PerspectiveParser().parse(getContext(PerspectivesDslContext.class), tokens); } else if (WORKSPACE_TOKEN.equalsIgnoreCase(firstToken) && contextStack.empty()) { if (parsedTokens.contains(WORKSPACE_TOKEN)) { diff --git a/structurizr-dsl/src/test/java/com/structurizr/dsl/ModelItemParserTests.java b/structurizr-dsl/src/test/java/com/structurizr/dsl/ModelItemParserTests.java index 01182692..fb109178 100644 --- a/structurizr-dsl/src/test/java/com/structurizr/dsl/ModelItemParserTests.java +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/ModelItemParserTests.java @@ -1,6 +1,5 @@ package com.structurizr.dsl; -import com.structurizr.model.Perspective; import com.structurizr.model.SoftwareSystem; import org.junit.jupiter.api.Test; @@ -105,61 +104,4 @@ void test_parseUrl_SetsTheUrl_WhenAUrlIsSpecified() { assertEquals("http://example.com", softwareSystem.getUrl()); } - @Test - void test_parsePerspective_ThrowsAnException_WhenThereAreTooManyTokens() { - try { - ModelItemPerspectivesDslContext context = new ModelItemPerspectivesDslContext(null); - parser.parsePerspective(context, tokens("name", "description", "value", "extra")); - fail(); - } catch (Exception e) { - assertEquals("Too many tokens, expected: [value]", e.getMessage()); - } - } - - @Test - void test_parsePerspective_ThrowsAnException_WhenNoNameIsSpecified() { - try { - SoftwareSystem softwareSystem = model.addSoftwareSystem("Name", "Description"); - ModelItemPerspectivesDslContext context = new ModelItemPerspectivesDslContext(softwareSystem); - parser.parsePerspective(context, tokens()); - fail(); - } catch (Exception e) { - assertEquals("Expected: [value]", e.getMessage()); - } - } - - @Test - void test_parsePerspective_ThrowsAnException_WhenNoDescriptionIsSpecified() { - try { - SoftwareSystem softwareSystem = model.addSoftwareSystem("Name", "Description"); - ModelItemPerspectivesDslContext context = new ModelItemPerspectivesDslContext(softwareSystem); - parser.parsePerspective(context, tokens("name")); - fail(); - } catch (Exception e) { - assertEquals("Expected: [value]", e.getMessage()); - } - } - - @Test - void test_parsePerspective_AddsThePerspective_WhenADescriptionIsSpecified() { - SoftwareSystem softwareSystem = model.addSoftwareSystem("Name", "Description"); - ModelItemPerspectivesDslContext context = new ModelItemPerspectivesDslContext(softwareSystem); - parser.parsePerspective(context, tokens("Security", "Description")); - - Perspective perspective = softwareSystem.getPerspectives().stream().filter(p -> p.getName().equals("Security")).findFirst().get(); - assertEquals("Description", perspective.getDescription()); - assertEquals("", perspective.getValue()); - } - - @Test - void test_parsePerspective_AddsThePerspective_WhenADescriptionAndValueIsSpecified() { - SoftwareSystem softwareSystem = model.addSoftwareSystem("Name", "Description"); - ModelItemPerspectivesDslContext context = new ModelItemPerspectivesDslContext(softwareSystem); - parser.parsePerspective(context, tokens("Security", "Description", "Value")); - - Perspective perspective = softwareSystem.getPerspectives().stream().filter(p -> p.getName().equals("Security")).findFirst().get(); - assertEquals("Description", perspective.getDescription()); - assertEquals("Value", perspective.getValue()); - } - } \ No newline at end of file diff --git a/structurizr-dsl/src/test/java/com/structurizr/dsl/PerspectiveParserTests.java b/structurizr-dsl/src/test/java/com/structurizr/dsl/PerspectiveParserTests.java new file mode 100644 index 00000000..3f11603d --- /dev/null +++ b/structurizr-dsl/src/test/java/com/structurizr/dsl/PerspectiveParserTests.java @@ -0,0 +1,72 @@ +package com.structurizr.dsl; + +import com.structurizr.model.ModelItem; +import com.structurizr.model.Perspective; +import com.structurizr.model.SoftwareSystem; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.fail; + +class PerspectiveParserTests extends AbstractTests { + + private final PerspectiveParser parser = new PerspectiveParser(); + + @Test + void test_parsePerspective_ThrowsAnException_WhenThereAreTooManyTokens() { + try { + PerspectivesDslContext context = new PerspectivesDslContext((ModelItem)null); + parser.parse(context, tokens("name", "description", "value", "extra")); + fail(); + } catch (Exception e) { + assertEquals("Too many tokens, expected: [value]", e.getMessage()); + } + } + + @Test + void test_parsePerspective_ThrowsAnException_WhenNoNameIsSpecified() { + try { + SoftwareSystem softwareSystem = model.addSoftwareSystem("Name", "Description"); + PerspectivesDslContext context = new PerspectivesDslContext(softwareSystem); + parser.parse(context, tokens()); + fail(); + } catch (Exception e) { + assertEquals("Expected: [value]", e.getMessage()); + } + } + + @Test + void test_parsePerspective_ThrowsAnException_WhenNoDescriptionIsSpecified() { + try { + SoftwareSystem softwareSystem = model.addSoftwareSystem("Name", "Description"); + PerspectivesDslContext context = new PerspectivesDslContext(softwareSystem); + parser.parse(context, tokens("name")); + fail(); + } catch (Exception e) { + assertEquals("Expected: [value]", e.getMessage()); + } + } + + @Test + void test_parsePerspective_AddsThePerspective_WhenADescriptionIsSpecified() { + SoftwareSystem softwareSystem = model.addSoftwareSystem("Name", "Description"); + PerspectivesDslContext context = new PerspectivesDslContext(softwareSystem); + parser.parse(context, tokens("Security", "Description")); + + Perspective perspective = softwareSystem.getPerspectives().stream().filter(p -> p.getName().equals("Security")).findFirst().get(); + assertEquals("Description", perspective.getDescription()); + assertEquals("", perspective.getValue()); + } + + @Test + void test_parsePerspective_AddsThePerspective_WhenADescriptionAndValueIsSpecified() { + SoftwareSystem softwareSystem = model.addSoftwareSystem("Name", "Description"); + PerspectivesDslContext context = new PerspectivesDslContext(softwareSystem); + parser.parse(context, tokens("Security", "Description", "Value")); + + Perspective perspective = softwareSystem.getPerspectives().stream().filter(p -> p.getName().equals("Security")).findFirst().get(); + assertEquals("Description", perspective.getDescription()); + assertEquals("Value", perspective.getValue()); + } + +} \ No newline at end of file diff --git a/structurizr-dsl/src/test/resources/dsl/test.dsl b/structurizr-dsl/src/test/resources/dsl/test.dsl index 7deeaf86..045aa311 100644 --- a/structurizr-dsl/src/test/resources/dsl/test.dsl +++ b/structurizr-dsl/src/test/resources/dsl/test.dsl @@ -69,7 +69,15 @@ workspace "Name" "Description" { } !elements "element.parent==webApplication && element.technology==Spring MVC Controller" { - tags "Spring MVC Controller" + tag "Tag 1" + tags "Tag 2, Tag 3" + url "https://example.com" + properties { + "type" "Spring MVC Controller" + } + perspectives { + "Owner" "Team A" + } } } @@ -142,6 +150,18 @@ workspace "Name" "Description" { } } + + !relationships "*->*" { + tag "Tag 1" + tags "Tag 2, Tag 3" + url "https://example.com" + properties { + name value + } + perspectives { + name value + } + } } views {