Skip to content

Commit

Permalink
structurizr-dsl: Adds the ability to use the group keyword inside a…
Browse files Browse the repository at this point in the history
… component definition, to set the group name of that component.
  • Loading branch information
simonbrowndotje committed Feb 17, 2025
1 parent 4e27be6 commit 5b64d91
Show file tree
Hide file tree
Showing 7 changed files with 134 additions and 25 deletions.
3 changes: 2 additions & 1 deletion changelog.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
# Changelog

## (unreleased)
## v4.0.0 (unreleased)

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

## 3.2.1 (10th December 2024)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ protected String[] getPermittedTokens() {
StructurizrDslTokens.URL_TOKEN,
StructurizrDslTokens.PROPERTIES_TOKEN,
StructurizrDslTokens.PERSPECTIVES_TOKEN,
StructurizrDslTokens.GROUP_TOKEN,
StructurizrDslTokens.RELATIONSHIP_TOKEN
};
}
Expand Down
41 changes: 36 additions & 5 deletions structurizr-dsl/src/main/java/com/structurizr/dsl/GroupParser.java
Original file line number Diff line number Diff line change
@@ -1,29 +1,32 @@
package com.structurizr.dsl;

import com.structurizr.model.Component;
import com.structurizr.util.StringUtils;

class GroupParser {

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

private static final String GRAMMAR = "group <name> {";
private static final String GRAMMAR_AS_CONTEXT = "group <name> {";
private static final String GRAMMAR_AS_PROPERTY = "group <name>";

private final static int NAME_INDEX = 1;
private final static int BRACE_INDEX = 2;

ElementGroup parse(GroupableDslContext dslContext, Tokens tokens) {

ElementGroup parseContext(GroupableDslContext dslContext, Tokens tokens) {
// group <name> {

if (tokens.hasMoreThan(BRACE_INDEX)) {
throw new RuntimeException("Too many tokens, expected: " + GRAMMAR);
throw new RuntimeException("Too many tokens, expected: " + GRAMMAR_AS_CONTEXT);
}

if (!tokens.includes(BRACE_INDEX)) {
throw new RuntimeException("Expected: " + GRAMMAR);
throw new RuntimeException("Expected: " + GRAMMAR_AS_CONTEXT);
}

if (!DslContext.CONTEXT_START_TOKEN.equalsIgnoreCase(tokens.get(BRACE_INDEX))) {
throw new RuntimeException("Expected: " + GRAMMAR);
throw new RuntimeException("Expected: " + GRAMMAR_AS_CONTEXT);
}

ElementGroup group;
Expand All @@ -42,4 +45,32 @@ ElementGroup parse(GroupableDslContext dslContext, Tokens tokens) {
return group;
}

void parseProperty(ComponentDslContext dslContext, Tokens tokens) {
// group <name>

if (tokens.includes(BRACE_INDEX)) {
throw new RuntimeException("Too many tokens, expected: " + GRAMMAR_AS_PROPERTY);
}

if (!tokens.includes(NAME_INDEX)) {
throw new RuntimeException("Expected: " + GRAMMAR_AS_PROPERTY);
}

String group = tokens.get(NAME_INDEX);

Component component = dslContext.getComponent();
String existingGroup = component.getGroup();

if (!StringUtils.isNullOrEmpty(existingGroup)) {
String groupSeparator = dslContext.getWorkspace().getModel().getProperties().getOrDefault(STRUCTURIZR_GROUP_SEPARATOR_PROPERTY_NAME, "");
if (StringUtils.isNullOrEmpty(groupSeparator)) {
throw new RuntimeException("To use nested groups, please define a model property named " + STRUCTURIZR_GROUP_SEPARATOR_PROPERTY_NAME);
}

group = existingGroup + groupSeparator + group;
}

component.setGroup(group);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -522,32 +522,32 @@ void parse(List<String> lines, File dslFile, boolean fragment, boolean includeIn
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)");

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

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

SoftwareSystem softwareSystem = getContext(SoftwareSystemDslContext.class).getSoftwareSystem();
group.setParent(softwareSystem);
startContext(new SoftwareSystemDslContext(softwareSystem, group));
registerIdentifier(identifier, group);
} else if (isElementKeywordOrArchetype(firstToken, GROUP_TOKEN) && inContext(ContainerDslContext.class)) {
ElementGroup group = new GroupParser().parse(getContext(ContainerDslContext.class), tokens);
ElementGroup group = new GroupParser().parseContext(getContext(ContainerDslContext.class), tokens);

Container container = getContext(ContainerDslContext.class).getContainer();
group.setParent(container);
startContext(new ContainerDslContext(container, group));
registerIdentifier(identifier, group);
} else if (isElementKeywordOrArchetype(firstToken, GROUP_TOKEN) && inContext(DeploymentEnvironmentDslContext.class)) {
ElementGroup group = new GroupParser().parse(getContext(DeploymentEnvironmentDslContext.class), tokens);
ElementGroup group = new GroupParser().parseContext(getContext(DeploymentEnvironmentDslContext.class), tokens);

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

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

} else if (GROUP_TOKEN.equalsIgnoreCase(firstToken) && inContext(ComponentDslContext.class)) {
new GroupParser().parseProperty(getContext(ComponentDslContext.class), tokens);

} else if (WORKSPACE_TOKEN.equalsIgnoreCase(firstToken) && contextStack.empty()) {
if (parsedTokens.contains(WORKSPACE_TOKEN)) {
throw new RuntimeException("Multiple workspaces are not permitted in a DSL definition");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -641,6 +641,12 @@ void test_nested_groups() throws Exception {
Container aApi = a.getContainerWithName("A API");
assertEquals("Capability 1/Service A", aApi.getGroup());

Component aApiEndpoint = aApi.getComponentWithName("API Endpoint");
assertEquals("a-api.jar/API Layer", aApiEndpoint.getGroup());

Component aApiRepository = aApi.getComponentWithName("Repository");
assertEquals("a-api.jar/Data Layer", aApiRepository.getGroup());

Container aDatabase = a.getContainerWithName("A Database");
assertEquals("Capability 1/Service A", aDatabase.getGroup());

Expand Down
Original file line number Diff line number Diff line change
@@ -1,72 +1,130 @@
package com.structurizr.dsl;

import com.structurizr.model.Component;
import org.junit.jupiter.api.Test;

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

class GroupParserTests extends AbstractTests {

private GroupParser parser = new GroupParser();
private final GroupParser parser = new GroupParser();

@Test
void parse_ThrowsAnException_WhenThereAreTooManyTokens() {
void parseContext_ThrowsAnException_WhenThereAreTooManyTokens() {
try {
parser.parse(null, tokens("group", "name", "{", "extra"));
parser.parseContext(null, tokens("group", "name", "{", "extra"));
fail();
} catch (Exception e) {
assertEquals("Too many tokens, expected: group <name> {", e.getMessage());
}
}

@Test
void parse_ThrowsAnException_WhenTheNameIsMissing() {
void parseContext_ThrowsAnException_WhenTheNameIsMissing() {
try {
parser.parse(null, tokens("group"));
parser.parseContext(null, tokens("group"));
fail();
} catch (Exception e) {
assertEquals("Expected: group <name> {", e.getMessage());
}
}

@Test
void parse_ThrowsAnException_WhenTheBraceIsMissing() {
void parseContext_ThrowsAnException_WhenTheBraceIsMissing() {
try {
parser.parse(null, tokens("group", "Name", "foo"));
parser.parseContext(null, tokens("group", "Name", "foo"));
fail();
} catch (Exception e) {
assertEquals("Expected: group <name> {", e.getMessage());
}
}

@Test
void parse() {
ElementGroup group = parser.parse(context(), tokens("group", "Group 1", "{"));
void parseContext() {
ElementGroup group = parser.parseContext(context(), tokens("group", "Group 1", "{"));
assertEquals("Group 1", group.getName());
assertTrue(group.getElements().isEmpty());
}

@Test
void parse_NestedGroup_ThrowsAnExceptionWhenNestedGroupsAreNotConfigured() {
void parseContext_NestedGroup_ThrowsAnExceptionWhenNestedGroupsAreNotConfigured() {
ModelDslContext context = new ModelDslContext(new ElementGroup("Group 1"));
context.setWorkspace(workspace);

try {
parser.parse(context, tokens("group", "Group 2", "{"));
parser.parseContext(context, tokens("group", "Group 2", "{"));
fail();
} catch (Exception e) {
assertEquals("To use nested groups, please define a model property named structurizr.groupSeparator", e.getMessage());
}
}

@Test
void parse_NestedGroup() {
void parseContext_NestedGroup() {
workspace.getModel().addProperty("structurizr.groupSeparator", "/");
ModelDslContext context = new ModelDslContext(new ElementGroup("Group 1"));
context.setWorkspace(workspace);

ElementGroup group = parser.parse(context, tokens("group", "Group 2", "{"));
ElementGroup group = parser.parseContext(context, tokens("group", "Group 2", "{"));
assertEquals("Group 1/Group 2", group.getName());
assertTrue(group.getElements().isEmpty());
}

@Test
void parseProperty_ThrowsAnException_WhenThereAreTooManyTokens() {
try {
parser.parseProperty(null, tokens("group", "name", "extra"));
fail();
} catch (Exception e) {
assertEquals("Too many tokens, expected: group <name>", e.getMessage());
}
}

@Test
void parseProperty_ThrowsAnException_WhenTheNameIsMissing() {
try {
parser.parseProperty(null, tokens("group"));
fail();
} catch (Exception e) {
assertEquals("Expected: group <name>", e.getMessage());
}
}

@Test
void parseProperty() {
Component component = workspace.getModel().addSoftwareSystem("Name").addContainer("Name").addComponent("Name");
ComponentDslContext context = new ComponentDslContext(component);
context.setWorkspace(workspace);

parser.parseProperty(context, tokens("group", "Group 1"));
assertEquals("Group 1", component.getGroup());
}

@Test
void parseProperty_NestedGroup_ThrowsAnExceptionWhenNestedGroupsAreNotConfigured() {
Component component = workspace.getModel().addSoftwareSystem("Name").addContainer("Name").addComponent("Name");
component.setGroup("Group 1");
ComponentDslContext context = new ComponentDslContext(component);
context.setWorkspace(workspace);

try {
parser.parseProperty(context, tokens("group", "Group 2"));
fail();
} catch (Exception e) {
assertEquals("To use nested groups, please define a model property named structurizr.groupSeparator", e.getMessage());
}
}

@Test
void parseProperty_NestedGroup() {
workspace.getModel().addProperty("structurizr.groupSeparator", "/");
Component component = workspace.getModel().addSoftwareSystem("Name").addContainer("Name").addComponent("Name");
component.setGroup("Group 1");
ComponentDslContext context = new ComponentDslContext(component);
context.setWorkspace(workspace);

parser.parseProperty(context, tokens("group", "Group 2"));
assertEquals("Group 1/Group 2", component.getGroup());
}

}
11 changes: 10 additions & 1 deletion structurizr-dsl/src/test/resources/dsl/groups-nested.dsl
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,16 @@ workspace {
a = softwareSystem "A" {
group "Capability 1" {
group "Service A" {
container "A API"
container "A API" {
group "a-api.jar" {
component "API Endpoint" {
group "API Layer"
}
component "Repository" {
group "Data Layer"
}
}
}
container "A Database"
}
group "Service B" {
Expand Down

0 comments on commit 5b64d91

Please sign in to comment.