Skip to content

Commit c527e09

Browse files
Adds support for removing relationships between software system instance/container instances, with a view to redefining them via infrastructure nodes.
1 parent d76d18c commit c527e09

13 files changed

+678
-9
lines changed

changelog.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
- structurizr-dsl: Adds an 'instanceOf' keyword (an alternative for `softwareSystemInstance` and `containerInstance`).
1111
- structurizr-dsl: Relationships to/from software system/container instances can be now defined by using the software system/container identifier.
1212
- structurizr-dsl: Fixes https://github.com/structurizr/java/issues/435 (Relationship archetype not applied to implicit-source relationships).
13+
- structurizr-dsl: Adds support for removing relationships between software system instance/container instances, with a view to redefining them via infrastructure nodes.
1314

1415
## v4.1.0 (28th May 2025)
1516

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,11 +38,11 @@ protected Relationship createRelationship(Element sourceElement, String descript
3838
return relationship;
3939
}
4040

41-
protected Set<Element> findSoftwareSystemInstances(SoftwareSystem softwareSystem, String deploymentEnvironment) {
41+
protected Set<StaticStructureElementInstance> findSoftwareSystemInstances(SoftwareSystem softwareSystem, String deploymentEnvironment) {
4242
return softwareSystem.getModel().getElements().stream().filter(e -> e instanceof SoftwareSystemInstance).map(e -> (SoftwareSystemInstance) e).filter(ssi -> ssi.getSoftwareSystem().equals(softwareSystem) && ssi.getEnvironment().equals(deploymentEnvironment)).collect(Collectors.toSet());
4343
}
4444

45-
protected Set<Element> findContainerInstances(Container container, String deploymentEnvironment) {
45+
protected Set<StaticStructureElementInstance> findContainerInstances(Container container, String deploymentEnvironment) {
4646
return container.getModel().getElements().stream().filter(e -> e instanceof ContainerInstance).map(e -> (ContainerInstance) e).filter(ci -> ci.getContainer().equals(container) && ci.getEnvironment().equals(deploymentEnvironment)).collect(Collectors.toSet());
4747
}
4848

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
package com.structurizr.dsl;
22

3-
final class DeploymentEnvironmentDslContext extends DslContext implements GroupableDslContext {
3+
class DeploymentEnvironmentDslContext extends DslContext implements GroupableDslContext {
44

55
private final DeploymentEnvironment environment;
66
private final ElementGroup group;

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

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -43,11 +43,19 @@ Set<Relationship> parse(DslContext context, Tokens tokens, Archetype archetype)
4343
String description = archetype.getDescription();
4444
if (tokens.includes(DESCRIPTION_INDEX)) {
4545
description = tokens.get(DESCRIPTION_INDEX);
46+
} else {
47+
if (context instanceof NoRelationshipInDeploymentEnvironmentDslContext) {
48+
description = ((NoRelationshipInDeploymentEnvironmentDslContext)context).getRelationship().getDescription();
49+
}
4650
}
4751

4852
String technology = archetype.getTechnology();
4953
if (tokens.includes(TECHNOLOGY_INDEX)) {
5054
technology = tokens.get(TECHNOLOGY_INDEX);
55+
} else {
56+
if (context instanceof NoRelationshipInDeploymentEnvironmentDslContext) {
57+
technology = ((NoRelationshipInDeploymentEnvironmentDslContext)context).getRelationship().getTechnology();
58+
}
5159
}
5260

5361
List<String> tags = new ArrayList<>(archetype.getTags());
@@ -68,20 +76,20 @@ Set<Relationship> parse(DslContext context, Tokens tokens, Archetype archetype)
6876

6977
if (sourceElement instanceof SoftwareSystem) {
7078
// find the software system instances in the deployment environment
71-
sourceElements = findSoftwareSystemInstances((SoftwareSystem)sourceElement, deploymentEnvironment);
79+
sourceElements.addAll(findSoftwareSystemInstances((SoftwareSystem)sourceElement, deploymentEnvironment));
7280
} else if (sourceElement instanceof Container) {
7381
// find the container instances in the deployment environment
74-
sourceElements = findContainerInstances((Container)sourceElement, deploymentEnvironment);
82+
sourceElements.addAll(findContainerInstances((Container)sourceElement, deploymentEnvironment));
7583
} else {
7684
sourceElements.add(sourceElement);
7785
}
7886

7987
if (destinationElement instanceof SoftwareSystem) {
8088
// find the software system instances in the deployment environment
81-
destinationElements = findSoftwareSystemInstances((SoftwareSystem)destinationElement, deploymentEnvironment);
89+
destinationElements.addAll(findSoftwareSystemInstances((SoftwareSystem)destinationElement, deploymentEnvironment));
8290
} else if (destinationElement instanceof Container) {
8391
// find the container instances in the deployment environment
84-
destinationElements = findContainerInstances((Container)destinationElement, deploymentEnvironment);
92+
destinationElements.addAll(findContainerInstances((Container)destinationElement, deploymentEnvironment));
8593
} else {
8694
destinationElements.add(destinationElement);
8795
}

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,10 +63,10 @@ Set<Relationship> parse(ElementDslContext context, Tokens tokens, Archetype arch
6363

6464
if (destinationElement instanceof SoftwareSystem) {
6565
// find the software system instances in the deployment environment
66-
destinationElements = findSoftwareSystemInstances((SoftwareSystem)destinationElement, deploymentEnvironment);
66+
destinationElements.addAll(findSoftwareSystemInstances((SoftwareSystem)destinationElement, deploymentEnvironment));
6767
} else if (destinationElement instanceof Container) {
6868
// find the container instances in the deployment environment
69-
destinationElements = findContainerInstances((Container)destinationElement, deploymentEnvironment);
69+
destinationElements.addAll(findContainerInstances((Container)destinationElement, deploymentEnvironment));
7070
} else {
7171
destinationElements.add(destinationElement);
7272
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package com.structurizr.dsl;
2+
3+
import com.structurizr.model.Relationship;
4+
5+
final class NoRelationshipInDeploymentEnvironmentDslContext extends DeploymentEnvironmentDslContext {
6+
7+
private final Relationship relationship;
8+
9+
NoRelationshipInDeploymentEnvironmentDslContext(DeploymentEnvironmentDslContext parent, Relationship relationship) {
10+
super(parent.getEnvironment().getName(), parent.getGroup());
11+
12+
this.relationship = relationship;
13+
}
14+
15+
Relationship getRelationship() {
16+
return relationship;
17+
}
18+
19+
@Override
20+
protected String[] getPermittedTokens() {
21+
return new String[] {
22+
StructurizrDslTokens.RELATIONSHIP_TOKEN
23+
};
24+
}
25+
26+
}
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
package com.structurizr.dsl;
2+
3+
import com.structurizr.model.*;
4+
5+
import java.util.*;
6+
7+
final class NoRelationshipParser extends AbstractRelationshipParser {
8+
9+
private static final String GRAMMAR = "<identifier> -/> <identifier> [description]";
10+
11+
private static final int SOURCE_IDENTIFIER_INDEX = 0;
12+
private static final int DESTINATION_IDENTIFIER_INDEX = 2;
13+
private static final int DESCRIPTION_IDENTIFIER_INDEX = 3;
14+
15+
Set<Relationship> parse(DeploymentEnvironmentDslContext context, Tokens tokens) {
16+
// <identifier> -/> <identifier> [description]
17+
18+
Set<Relationship> relationships = new HashSet<>();
19+
20+
if (tokens.hasMoreThan(DESCRIPTION_IDENTIFIER_INDEX)) {
21+
throw new RuntimeException("Too many tokens, expected: " + GRAMMAR);
22+
}
23+
24+
if (!tokens.includes(DESTINATION_IDENTIFIER_INDEX)) {
25+
throw new RuntimeException("Not enough tokens, expected: " + GRAMMAR);
26+
}
27+
28+
String sourceId = tokens.get(SOURCE_IDENTIFIER_INDEX);
29+
Element sourceElement = context.getElement(sourceId);
30+
Set<StaticStructureElementInstance> sourceElements = new HashSet<>();
31+
32+
if (sourceElement == null) {
33+
throw new RuntimeException("The source element \"" + sourceId + "\" does not exist");
34+
} else if (sourceElement instanceof SoftwareSystem) {
35+
sourceElements = findSoftwareSystemInstances((SoftwareSystem)sourceElement, context.getEnvironment().getName());
36+
} else if (sourceElement instanceof Container) {
37+
sourceElements = findContainerInstances((Container)sourceElement, context.getEnvironment().getName());
38+
} else if (sourceElement instanceof StaticStructureElementInstance) {
39+
sourceElements.add((StaticStructureElementInstance)sourceElement);
40+
} else {
41+
throw new RuntimeException("The source element \"" + sourceId + "\" is not valid - expecting a software system, software system instance, container, or container instance");
42+
}
43+
44+
String destinationId = tokens.get(DESTINATION_IDENTIFIER_INDEX);
45+
Element destinationElement = context.getElement(destinationId);
46+
Set<StaticStructureElementInstance> destinationElements = new HashSet<>();
47+
48+
if (destinationElement == null) {
49+
throw new RuntimeException("The destination element \"" + destinationId + "\" does not exist");
50+
} else if (destinationElement instanceof SoftwareSystem) {
51+
destinationElements = findSoftwareSystemInstances((SoftwareSystem)destinationElement, context.getEnvironment().getName());
52+
} else if (destinationElement instanceof Container) {
53+
destinationElements = findContainerInstances((Container)destinationElement, context.getEnvironment().getName());
54+
} else if (destinationElement instanceof StaticStructureElementInstance) {
55+
destinationElements.add((StaticStructureElementInstance)destinationElement);
56+
} else {
57+
throw new RuntimeException("The destination element \"" + destinationId + "\" is not valid - expecting a software system, software system instance, container, or container instance");
58+
}
59+
60+
String description = null;
61+
62+
if (tokens.includes(DESCRIPTION_IDENTIFIER_INDEX)) {
63+
description = tokens.get(DESCRIPTION_IDENTIFIER_INDEX);
64+
}
65+
66+
int count = 0;
67+
for (Element se : sourceElements) {
68+
for (Element de : destinationElements) {
69+
Relationship relationship;
70+
71+
do {
72+
if (description != null) {
73+
relationship = se.getEfferentRelationshipWith(de, description);
74+
} else {
75+
relationship = se.getEfferentRelationshipWith(de);
76+
}
77+
78+
if (relationship != null && relationship.getLinkedRelationshipId() != null) {
79+
context.getWorkspace().remove(relationship);
80+
relationships.add(relationship);
81+
count++;
82+
}
83+
} while (relationship != null);
84+
}
85+
}
86+
87+
if (count == 0) {
88+
if (description != null) {
89+
throw new RuntimeException("A relationship between \"" + sourceId + "\" and \"" + destinationId + "\" with description \"" + description + "\" does not exist");
90+
} else {
91+
throw new RuntimeException("A relationship between \"" + sourceId + "\" and \"" + destinationId + "\" does not exist");
92+
}
93+
}
94+
95+
return relationships;
96+
}
97+
98+
}

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

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -323,6 +323,19 @@ void parse(List<String> lines, File dslFile, boolean fragment, boolean includeIn
323323
} else if (inContext(ExternalScriptDslContext.class)) {
324324
new ScriptParser().parseParameter(getContext(ExternalScriptDslContext.class), tokens);
325325

326+
} else if (tokens.size() >= 4 && tokens.get(1).equals(NO_RELATIONSHIP_TOKEN) && shouldStartContext(tokens) && inContext(DeploymentEnvironmentDslContext.class)) {
327+
// source -/> destination {
328+
// or
329+
// source -/> destination "description" {
330+
331+
// remove source -> destination (between instances) in the deployment model
332+
Set<Relationship> relationships = new NoRelationshipParser().parse(getContext(DeploymentEnvironmentDslContext.class), tokens.withoutContextStartToken());
333+
334+
// find the static element -> static element relationship that the removed relationships were based upon
335+
Relationship relationship = workspace.getModel().getRelationship(relationships.iterator().next().getLinkedRelationshipId());
336+
337+
startContext(new NoRelationshipInDeploymentEnvironmentDslContext(getContext(DeploymentEnvironmentDslContext.class), relationship));
338+
326339
} else if (tokens.size() > 2 && isRelationshipKeywordOrArchetype(tokens.get(1)) && (inContext(ModelDslContext.class) || inContext(DeploymentEnvironmentDslContext.class) || inContext(ElementDslContext.class))) {
327340
// explicit without archetype: a -> b
328341
// explicit with archetype: a --https-> b

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ class StructurizrDslTokens {
1111
static final String PERSON_TOKEN = "person";
1212
static final String SOFTWARE_SYSTEM_TOKEN = "softwareSystem";
1313
static final String RELATIONSHIP_TOKEN = "->";
14+
static final String NO_RELATIONSHIP_TOKEN = "-/>";
1415
static final String CONTAINER_TOKEN = "container";
1516
static final String COMPONENT_TOKEN = "component";
1617
static final String GROUP_TOKEN = "group";

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

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1753,4 +1753,53 @@ void test_colorSchemes() throws Exception {
17531753
assertEquals("#ffffff", relationshipStyle.getColor());
17541754
}
17551755

1756+
@Test
1757+
void test_noRelationship() throws Exception {
1758+
StructurizrDslParser parser = new StructurizrDslParser();
1759+
parser.parse(new File("src/test/resources/dsl/no-relationship.dsl"));
1760+
1761+
Workspace workspace = parser.getWorkspace();
1762+
1763+
Container ui = (Container)parser.getIdentifiersRegister().getElement("ss.ui");
1764+
Container backend = (Container)parser.getIdentifiersRegister().getElement("ss.backend");
1765+
1766+
// environment One: ui -> backend
1767+
ContainerInstance uiInstance = workspace.getModel().getElements().stream().filter(e -> e instanceof ContainerInstance).map(e -> (ContainerInstance)e).filter(ci -> ci.getEnvironment().equals("One") && ci.getContainer().equals(ui)).findFirst().get();
1768+
ContainerInstance backendInstance = workspace.getModel().getElements().stream().filter(e -> e instanceof ContainerInstance).map(e -> (ContainerInstance)e).filter(ci -> ci.getEnvironment().equals("One") && ci.getContainer().equals(backend)).findFirst().get();
1769+
1770+
assertNotNull(uiInstance);
1771+
assertNotNull(backendInstance);
1772+
assertTrue(uiInstance.hasEfferentRelationshipWith(backendInstance));
1773+
1774+
// environment Two: ui -> load balancer -> backend
1775+
uiInstance = workspace.getModel().getElements().stream().filter(e -> e instanceof ContainerInstance).map(e -> (ContainerInstance)e).filter(ci -> ci.getEnvironment().equals("Two") && ci.getContainer().equals(ui)).findFirst().get();
1776+
backendInstance = workspace.getModel().getElements().stream().filter(e -> e instanceof ContainerInstance).map(e -> (ContainerInstance)e).filter(ci -> ci.getEnvironment().equals("Two") && ci.getContainer().equals(backend)).findFirst().get();
1777+
InfrastructureNode loadBalancer = workspace.getModel().getElements().stream().filter(e -> e instanceof InfrastructureNode).map(e -> (InfrastructureNode)e).filter(ci -> ci.getEnvironment().equals("Two")).findFirst().get();
1778+
1779+
assertNotNull(uiInstance);
1780+
assertNotNull(backendInstance);
1781+
assertFalse(uiInstance.hasEfferentRelationshipWith(backendInstance));
1782+
1783+
assertEquals("Makes API requests to", uiInstance.getEfferentRelationshipWith(loadBalancer).getDescription());
1784+
assertEquals("JSON/HTTPS", uiInstance.getEfferentRelationshipWith(loadBalancer).getTechnology());
1785+
1786+
assertEquals("Forwards API requests to", loadBalancer.getEfferentRelationshipWith(backendInstance).getDescription());
1787+
assertEquals("", loadBalancer.getEfferentRelationshipWith(backendInstance).getTechnology());
1788+
1789+
// environment Three: ui -> load balancer -> backend
1790+
uiInstance = workspace.getModel().getElements().stream().filter(e -> e instanceof ContainerInstance).map(e -> (ContainerInstance)e).filter(ci -> ci.getEnvironment().equals("Three") && ci.getContainer().equals(ui)).findFirst().get();
1791+
backendInstance = workspace.getModel().getElements().stream().filter(e -> e instanceof ContainerInstance).map(e -> (ContainerInstance)e).filter(ci -> ci.getEnvironment().equals("Three") && ci.getContainer().equals(backend)).findFirst().get();
1792+
loadBalancer = workspace.getModel().getElements().stream().filter(e -> e instanceof InfrastructureNode).map(e -> (InfrastructureNode)e).filter(ci -> ci.getEnvironment().equals("Three")).findFirst().get();
1793+
1794+
assertNotNull(uiInstance);
1795+
assertNotNull(backendInstance);
1796+
assertFalse(uiInstance.hasEfferentRelationshipWith(backendInstance));
1797+
1798+
assertEquals("Makes API requests to", uiInstance.getEfferentRelationshipWith(loadBalancer).getDescription());
1799+
assertEquals("JSON/HTTPS", uiInstance.getEfferentRelationshipWith(loadBalancer).getTechnology());
1800+
1801+
assertEquals("Forwards API requests to", loadBalancer.getEfferentRelationshipWith(backendInstance).getDescription());
1802+
assertEquals("", loadBalancer.getEfferentRelationshipWith(backendInstance).getTechnology());
1803+
}
1804+
17561805
}

0 commit comments

Comments
 (0)