Skip to content

Commit 5b64d91

Browse files
structurizr-dsl: Adds the ability to use the group keyword inside a component definition, to set the group name of that component.
1 parent 4e27be6 commit 5b64d91

File tree

7 files changed

+134
-25
lines changed

7 files changed

+134
-25
lines changed

changelog.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
# Changelog
22

3-
## (unreleased)
3+
## v4.0.0 (unreleased)
44

55
- structurizr-dsl: Fixes https://github.com/structurizr/java/issues/374 (!identifiers hierarchical isn't propagated when extending a workspace).
6+
- structurizr-dsl: Adds the ability to use the `group` keyword inside a component definition, to set the group name of that component.
67

78
## 3.2.1 (10th December 2024)
89

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ protected String[] getPermittedTokens() {
3737
StructurizrDslTokens.URL_TOKEN,
3838
StructurizrDslTokens.PROPERTIES_TOKEN,
3939
StructurizrDslTokens.PERSPECTIVES_TOKEN,
40+
StructurizrDslTokens.GROUP_TOKEN,
4041
StructurizrDslTokens.RELATIONSHIP_TOKEN
4142
};
4243
}

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

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

3+
import com.structurizr.model.Component;
34
import com.structurizr.util.StringUtils;
45

56
class GroupParser {
67

78
private static final String STRUCTURIZR_GROUP_SEPARATOR_PROPERTY_NAME = "structurizr.groupSeparator";
89

9-
private static final String GRAMMAR = "group <name> {";
10+
private static final String GRAMMAR_AS_CONTEXT = "group <name> {";
11+
private static final String GRAMMAR_AS_PROPERTY = "group <name>";
1012

1113
private final static int NAME_INDEX = 1;
1214
private final static int BRACE_INDEX = 2;
1315

14-
ElementGroup parse(GroupableDslContext dslContext, Tokens tokens) {
16+
17+
ElementGroup parseContext(GroupableDslContext dslContext, Tokens tokens) {
1518
// group <name> {
1619

1720
if (tokens.hasMoreThan(BRACE_INDEX)) {
18-
throw new RuntimeException("Too many tokens, expected: " + GRAMMAR);
21+
throw new RuntimeException("Too many tokens, expected: " + GRAMMAR_AS_CONTEXT);
1922
}
2023

2124
if (!tokens.includes(BRACE_INDEX)) {
22-
throw new RuntimeException("Expected: " + GRAMMAR);
25+
throw new RuntimeException("Expected: " + GRAMMAR_AS_CONTEXT);
2326
}
2427

2528
if (!DslContext.CONTEXT_START_TOKEN.equalsIgnoreCase(tokens.get(BRACE_INDEX))) {
26-
throw new RuntimeException("Expected: " + GRAMMAR);
29+
throw new RuntimeException("Expected: " + GRAMMAR_AS_CONTEXT);
2730
}
2831

2932
ElementGroup group;
@@ -42,4 +45,32 @@ ElementGroup parse(GroupableDslContext dslContext, Tokens tokens) {
4245
return group;
4346
}
4447

48+
void parseProperty(ComponentDslContext dslContext, Tokens tokens) {
49+
// group <name>
50+
51+
if (tokens.includes(BRACE_INDEX)) {
52+
throw new RuntimeException("Too many tokens, expected: " + GRAMMAR_AS_PROPERTY);
53+
}
54+
55+
if (!tokens.includes(NAME_INDEX)) {
56+
throw new RuntimeException("Expected: " + GRAMMAR_AS_PROPERTY);
57+
}
58+
59+
String group = tokens.get(NAME_INDEX);
60+
61+
Component component = dslContext.getComponent();
62+
String existingGroup = component.getGroup();
63+
64+
if (!StringUtils.isNullOrEmpty(existingGroup)) {
65+
String groupSeparator = dslContext.getWorkspace().getModel().getProperties().getOrDefault(STRUCTURIZR_GROUP_SEPARATOR_PROPERTY_NAME, "");
66+
if (StringUtils.isNullOrEmpty(groupSeparator)) {
67+
throw new RuntimeException("To use nested groups, please define a model property named " + STRUCTURIZR_GROUP_SEPARATOR_PROPERTY_NAME);
68+
}
69+
70+
group = existingGroup + groupSeparator + group;
71+
}
72+
73+
component.setGroup(group);
74+
}
75+
4576
}

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

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -522,32 +522,32 @@ void parse(List<String> lines, File dslFile, boolean fragment, boolean includeIn
522522
throw new RuntimeException("The enterprise keyword was previously deprecated, and has now been removed - please use group instead (https://docs.structurizr.com/dsl/language#group)");
523523

524524
} else if (isElementKeywordOrArchetype(firstToken, GROUP_TOKEN) && inContext(ModelDslContext.class)) {
525-
ElementGroup group = new GroupParser().parse(getContext(ModelDslContext.class), tokens);
525+
ElementGroup group = new GroupParser().parseContext(getContext(ModelDslContext.class), tokens);
526526

527527
startContext(new ModelDslContext(group));
528528
registerIdentifier(identifier, group);
529529
} else if (isElementKeywordOrArchetype(firstToken, GROUP_TOKEN) && inContext(SoftwareSystemDslContext.class)) {
530-
ElementGroup group = new GroupParser().parse(getContext(SoftwareSystemDslContext.class), tokens);
530+
ElementGroup group = new GroupParser().parseContext(getContext(SoftwareSystemDslContext.class), tokens);
531531

532532
SoftwareSystem softwareSystem = getContext(SoftwareSystemDslContext.class).getSoftwareSystem();
533533
group.setParent(softwareSystem);
534534
startContext(new SoftwareSystemDslContext(softwareSystem, group));
535535
registerIdentifier(identifier, group);
536536
} else if (isElementKeywordOrArchetype(firstToken, GROUP_TOKEN) && inContext(ContainerDslContext.class)) {
537-
ElementGroup group = new GroupParser().parse(getContext(ContainerDslContext.class), tokens);
537+
ElementGroup group = new GroupParser().parseContext(getContext(ContainerDslContext.class), tokens);
538538

539539
Container container = getContext(ContainerDslContext.class).getContainer();
540540
group.setParent(container);
541541
startContext(new ContainerDslContext(container, group));
542542
registerIdentifier(identifier, group);
543543
} else if (isElementKeywordOrArchetype(firstToken, GROUP_TOKEN) && inContext(DeploymentEnvironmentDslContext.class)) {
544-
ElementGroup group = new GroupParser().parse(getContext(DeploymentEnvironmentDslContext.class), tokens);
544+
ElementGroup group = new GroupParser().parseContext(getContext(DeploymentEnvironmentDslContext.class), tokens);
545545

546546
String environment = getContext(DeploymentEnvironmentDslContext.class).getEnvironment();
547547
startContext(new DeploymentEnvironmentDslContext(environment, group));
548548
registerIdentifier(identifier, group);
549549
} else if (isElementKeywordOrArchetype(firstToken, GROUP_TOKEN) && inContext(DeploymentNodeDslContext.class)) {
550-
ElementGroup group = new GroupParser().parse(getContext(DeploymentNodeDslContext.class), tokens);
550+
ElementGroup group = new GroupParser().parseContext(getContext(DeploymentNodeDslContext.class), tokens);
551551

552552
DeploymentNode deploymentNode = getContext(DeploymentNodeDslContext.class).getDeploymentNode();
553553
startContext(new DeploymentNodeDslContext(deploymentNode, group));
@@ -630,6 +630,9 @@ void parse(List<String> lines, File dslFile, boolean fragment, boolean includeIn
630630
} else if (inContext(PerspectivesDslContext.class)) {
631631
new PerspectiveParser().parse(getContext(PerspectivesDslContext.class), tokens);
632632

633+
} else if (GROUP_TOKEN.equalsIgnoreCase(firstToken) && inContext(ComponentDslContext.class)) {
634+
new GroupParser().parseProperty(getContext(ComponentDslContext.class), tokens);
635+
633636
} else if (WORKSPACE_TOKEN.equalsIgnoreCase(firstToken) && contextStack.empty()) {
634637
if (parsedTokens.contains(WORKSPACE_TOKEN)) {
635638
throw new RuntimeException("Multiple workspaces are not permitted in a DSL definition");

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -641,6 +641,12 @@ void test_nested_groups() throws Exception {
641641
Container aApi = a.getContainerWithName("A API");
642642
assertEquals("Capability 1/Service A", aApi.getGroup());
643643

644+
Component aApiEndpoint = aApi.getComponentWithName("API Endpoint");
645+
assertEquals("a-api.jar/API Layer", aApiEndpoint.getGroup());
646+
647+
Component aApiRepository = aApi.getComponentWithName("Repository");
648+
assertEquals("a-api.jar/Data Layer", aApiRepository.getGroup());
649+
644650
Container aDatabase = a.getContainerWithName("A Database");
645651
assertEquals("Capability 1/Service A", aDatabase.getGroup());
646652

Lines changed: 71 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,72 +1,130 @@
11
package com.structurizr.dsl;
22

3+
import com.structurizr.model.Component;
34
import org.junit.jupiter.api.Test;
45

56
import static org.junit.jupiter.api.Assertions.*;
67

78
class GroupParserTests extends AbstractTests {
89

9-
private GroupParser parser = new GroupParser();
10+
private final GroupParser parser = new GroupParser();
1011

1112
@Test
12-
void parse_ThrowsAnException_WhenThereAreTooManyTokens() {
13+
void parseContext_ThrowsAnException_WhenThereAreTooManyTokens() {
1314
try {
14-
parser.parse(null, tokens("group", "name", "{", "extra"));
15+
parser.parseContext(null, tokens("group", "name", "{", "extra"));
1516
fail();
1617
} catch (Exception e) {
1718
assertEquals("Too many tokens, expected: group <name> {", e.getMessage());
1819
}
1920
}
2021

2122
@Test
22-
void parse_ThrowsAnException_WhenTheNameIsMissing() {
23+
void parseContext_ThrowsAnException_WhenTheNameIsMissing() {
2324
try {
24-
parser.parse(null, tokens("group"));
25+
parser.parseContext(null, tokens("group"));
2526
fail();
2627
} catch (Exception e) {
2728
assertEquals("Expected: group <name> {", e.getMessage());
2829
}
2930
}
3031

3132
@Test
32-
void parse_ThrowsAnException_WhenTheBraceIsMissing() {
33+
void parseContext_ThrowsAnException_WhenTheBraceIsMissing() {
3334
try {
34-
parser.parse(null, tokens("group", "Name", "foo"));
35+
parser.parseContext(null, tokens("group", "Name", "foo"));
3536
fail();
3637
} catch (Exception e) {
3738
assertEquals("Expected: group <name> {", e.getMessage());
3839
}
3940
}
4041

4142
@Test
42-
void parse() {
43-
ElementGroup group = parser.parse(context(), tokens("group", "Group 1", "{"));
43+
void parseContext() {
44+
ElementGroup group = parser.parseContext(context(), tokens("group", "Group 1", "{"));
4445
assertEquals("Group 1", group.getName());
4546
assertTrue(group.getElements().isEmpty());
4647
}
4748

4849
@Test
49-
void parse_NestedGroup_ThrowsAnExceptionWhenNestedGroupsAreNotConfigured() {
50+
void parseContext_NestedGroup_ThrowsAnExceptionWhenNestedGroupsAreNotConfigured() {
5051
ModelDslContext context = new ModelDslContext(new ElementGroup("Group 1"));
5152
context.setWorkspace(workspace);
5253

5354
try {
54-
parser.parse(context, tokens("group", "Group 2", "{"));
55+
parser.parseContext(context, tokens("group", "Group 2", "{"));
5556
fail();
5657
} catch (Exception e) {
5758
assertEquals("To use nested groups, please define a model property named structurizr.groupSeparator", e.getMessage());
5859
}
5960
}
6061

6162
@Test
62-
void parse_NestedGroup() {
63+
void parseContext_NestedGroup() {
6364
workspace.getModel().addProperty("structurizr.groupSeparator", "/");
6465
ModelDslContext context = new ModelDslContext(new ElementGroup("Group 1"));
6566
context.setWorkspace(workspace);
6667

67-
ElementGroup group = parser.parse(context, tokens("group", "Group 2", "{"));
68+
ElementGroup group = parser.parseContext(context, tokens("group", "Group 2", "{"));
6869
assertEquals("Group 1/Group 2", group.getName());
6970
assertTrue(group.getElements().isEmpty());
7071
}
7172

73+
@Test
74+
void parseProperty_ThrowsAnException_WhenThereAreTooManyTokens() {
75+
try {
76+
parser.parseProperty(null, tokens("group", "name", "extra"));
77+
fail();
78+
} catch (Exception e) {
79+
assertEquals("Too many tokens, expected: group <name>", e.getMessage());
80+
}
81+
}
82+
83+
@Test
84+
void parseProperty_ThrowsAnException_WhenTheNameIsMissing() {
85+
try {
86+
parser.parseProperty(null, tokens("group"));
87+
fail();
88+
} catch (Exception e) {
89+
assertEquals("Expected: group <name>", e.getMessage());
90+
}
91+
}
92+
93+
@Test
94+
void parseProperty() {
95+
Component component = workspace.getModel().addSoftwareSystem("Name").addContainer("Name").addComponent("Name");
96+
ComponentDslContext context = new ComponentDslContext(component);
97+
context.setWorkspace(workspace);
98+
99+
parser.parseProperty(context, tokens("group", "Group 1"));
100+
assertEquals("Group 1", component.getGroup());
101+
}
102+
103+
@Test
104+
void parseProperty_NestedGroup_ThrowsAnExceptionWhenNestedGroupsAreNotConfigured() {
105+
Component component = workspace.getModel().addSoftwareSystem("Name").addContainer("Name").addComponent("Name");
106+
component.setGroup("Group 1");
107+
ComponentDslContext context = new ComponentDslContext(component);
108+
context.setWorkspace(workspace);
109+
110+
try {
111+
parser.parseProperty(context, tokens("group", "Group 2"));
112+
fail();
113+
} catch (Exception e) {
114+
assertEquals("To use nested groups, please define a model property named structurizr.groupSeparator", e.getMessage());
115+
}
116+
}
117+
118+
@Test
119+
void parseProperty_NestedGroup() {
120+
workspace.getModel().addProperty("structurizr.groupSeparator", "/");
121+
Component component = workspace.getModel().addSoftwareSystem("Name").addContainer("Name").addComponent("Name");
122+
component.setGroup("Group 1");
123+
ComponentDslContext context = new ComponentDslContext(component);
124+
context.setWorkspace(workspace);
125+
126+
parser.parseProperty(context, tokens("group", "Group 2"));
127+
assertEquals("Group 1/Group 2", component.getGroup());
128+
}
129+
72130
}

structurizr-dsl/src/test/resources/dsl/groups-nested.dsl

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,16 @@ workspace {
1010
a = softwareSystem "A" {
1111
group "Capability 1" {
1212
group "Service A" {
13-
container "A API"
13+
container "A API" {
14+
group "a-api.jar" {
15+
component "API Endpoint" {
16+
group "API Layer"
17+
}
18+
component "Repository" {
19+
group "Data Layer"
20+
}
21+
}
22+
}
1423
container "A Database"
1524
}
1625
group "Service B" {

0 commit comments

Comments
 (0)