Skip to content

Commit 9562ac4

Browse files
yt-msMidnighter
authored andcommitted
feat: replicate element relationships onto instances
1 parent 801cdd1 commit 9562ac4

File tree

4 files changed

+204
-18
lines changed

4 files changed

+204
-18
lines changed

src/structurizr/model/deployment_node.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -175,11 +175,16 @@ def add_container(
175175
+ 1
176176
)
177177
instance = ContainerInstance(
178-
container=container, instance_id=instance_id, parent=self
178+
container=container,
179+
instance_id=instance_id,
180+
environment=self.environment,
181+
parent=self,
179182
)
180183
self._container_instances.add(instance)
181184
model = self.model
182185
model += instance
186+
if replicate_relationships:
187+
instance.replicate_element_relationships()
183188
return instance
184189

185190
def add_software_system(
@@ -206,11 +211,16 @@ def add_software_system(
206211
+ 1
207212
)
208213
instance = SoftwareSystemInstance(
209-
software_system=software_system, instance_id=instance_id, parent=self
214+
software_system=software_system,
215+
instance_id=instance_id,
216+
environment=self.environment,
217+
parent=self,
210218
)
211219
self._software_system_instances.add(instance)
212220
model = self.model
213221
model += instance
222+
if replicate_relationships:
223+
instance.replicate_element_relationships()
214224
return instance
215225

216226
def add_infrastructure_node(self, name: str, **kwargs) -> InfrastructureNode:

src/structurizr/model/static_structure_element_instance.py

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717

1818

1919
from abc import ABC
20-
from typing import Iterable, List, Optional
20+
from typing import TYPE_CHECKING, Iterable, List, Optional, Set
2121

2222
from pydantic import Field
2323

@@ -26,6 +26,9 @@
2626
from .static_structure_element import StaticStructureElement
2727

2828

29+
if TYPE_CHECKING:
30+
from .deployment_node import DeploymentNode
31+
2932
__all__ = ("StaticStructureElementInstance", "StaticStructureElementInstanceIO")
3033

3134

@@ -60,6 +63,44 @@ def __init__(
6063
self.health_checks = set(health_checks)
6164
self.parent = parent
6265

66+
def replicate_element_relationships(self):
67+
"""
68+
Replicate relationships from the element of this instance.
69+
70+
This looks at the relationships to and from the element of this instance and
71+
sets up the equivalent relationships between the corresponding instances in
72+
the same environment.
73+
"""
74+
# Find all the element instances in the same deployment environment
75+
element_instances: Set[StaticStructureElementInstance] = {
76+
e
77+
for e in self.model.get_elements()
78+
if isinstance(e, StaticStructureElementInstance)
79+
and e.environment == self.environment
80+
}
81+
82+
for other_element_instance in element_instances:
83+
other_element = other_element_instance.element
84+
85+
for relationship in self.element.relationships:
86+
if relationship.destination is other_element:
87+
self.add_relationship(
88+
destination=other_element_instance,
89+
description=relationship.description,
90+
technology=relationship.technology,
91+
interaction_style=relationship.interaction_style,
92+
linked_relationship_id=relationship.id,
93+
).tags.clear()
94+
for relationship in other_element.relationships:
95+
if relationship.destination is self.element:
96+
other_element_instance.add_relationship(
97+
destination=self,
98+
description=relationship.description,
99+
technology=relationship.technology,
100+
interaction_style=relationship.interaction_style,
101+
linked_relationship_id=relationship.id,
102+
).tags.clear()
103+
63104
@classmethod
64105
def hydrate_arguments(cls, instance_io: StaticStructureElementInstanceIO) -> dict:
65106
"""Build constructor arguments from IO."""
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
# Licensed under the Apache License, Version 2.0 (the "License");
2+
# you may not use this file except in compliance with the License.
3+
# You may obtain a copy of the License at
4+
#
5+
# https://www.apache.org/licenses/LICENSE-2.0
6+
#
7+
# Unless required by applicable law or agreed to in writing, software
8+
# distributed under the License is distributed on an "AS IS" BASIS,
9+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10+
# See the License for the specific language governing permissions and
11+
# limitations under the License.
12+
13+
14+
"""Test the behaviour when replicating relationships to element instances."""
15+
16+
17+
import pytest
18+
19+
from structurizr.model import InteractionStyle, Model
20+
21+
22+
@pytest.fixture(scope="function")
23+
def empty_model() -> Model:
24+
"""Provide an new empty model on demand for test cases to use."""
25+
return Model()
26+
27+
28+
def test_replication_adds_relationships_in_the_same_environment(empty_model: Model):
29+
"""Ensure within the same environment, relationships are replicated."""
30+
# Adapted from the Java ModelTests
31+
model = empty_model
32+
system1 = model.add_software_system("Software System 1")
33+
container1 = system1.add_container("Container 1")
34+
35+
system2 = model.add_software_system("Software System 2")
36+
container2 = system2.add_container("Container 2")
37+
38+
system3 = model.add_software_system("Software System 3")
39+
container3 = system3.add_container("Container 3")
40+
41+
system4 = model.add_software_system("Software System 4")
42+
43+
container1.uses(
44+
container2,
45+
"Uses 1",
46+
"Technology 1",
47+
interaction_style=InteractionStyle.Synchronous,
48+
)
49+
container2.uses(
50+
container3,
51+
"Uses 2",
52+
"Technology 2",
53+
interaction_style=InteractionStyle.Asynchronous,
54+
)
55+
container3.uses(system4)
56+
57+
dev_deployment_node = model.add_deployment_node(
58+
"Deployment Node", environment="Development"
59+
)
60+
container_instance1 = dev_deployment_node.add_container(container1)
61+
container_instance2 = dev_deployment_node.add_container(container2)
62+
container_instance3 = dev_deployment_node.add_container(container3)
63+
system_instance = dev_deployment_node.add_software_system(system4)
64+
65+
# The following live element instances should not affect the relationships of the
66+
# development element instances
67+
live_deployment_node = model.add_deployment_node(
68+
"Deployment Node", environment="Live"
69+
)
70+
live_deployment_node.add_container(container1)
71+
live_deployment_node.add_container(container2)
72+
live_deployment_node.add_container(container3)
73+
live_deployment_node.add_software_system(system4)
74+
75+
assert len(container_instance1.relationships) == 1
76+
relationship = next(iter(container_instance1.relationships))
77+
assert relationship.source is container_instance1
78+
assert relationship.destination is container_instance2
79+
assert relationship.description == "Uses 1"
80+
assert relationship.technology == "Technology 1"
81+
assert relationship.interaction_style == InteractionStyle.Synchronous
82+
assert relationship.tags == set()
83+
84+
assert len(container_instance2.relationships) == 1
85+
relationship = next(iter(container_instance2.relationships))
86+
assert relationship.source is container_instance2
87+
assert relationship.destination is container_instance3
88+
assert relationship.description == "Uses 2"
89+
assert relationship.technology == "Technology 2"
90+
assert relationship.interaction_style == InteractionStyle.Asynchronous
91+
assert relationship.tags == set()
92+
93+
assert len(container_instance3.relationships) == 1
94+
relationship = next(iter(container_instance3.relationships))
95+
assert relationship.source is container_instance3
96+
assert relationship.destination is system_instance
97+
assert relationship.description == "Uses"
98+
assert relationship.technology == ""
99+
assert relationship.interaction_style == InteractionStyle.Synchronous
100+
assert relationship.tags == set()
101+
102+
103+
def test_not_replicating_doesnt_add_relationships(empty_model: Model):
104+
"""Ensure within the same environment, relationships are replicated."""
105+
# Adapted from the Java ModelTests
106+
model = empty_model
107+
system1 = model.add_software_system("Software System 1")
108+
container1 = system1.add_container("Container 1")
109+
110+
system2 = model.add_software_system("Software System 2")
111+
container2 = system2.add_container("Container 2")
112+
113+
system3 = model.add_software_system("Software System 3")
114+
container3 = system3.add_container("Container 3")
115+
116+
system4 = model.add_software_system("Software System 4")
117+
118+
container1.uses(
119+
container2,
120+
"Uses 1",
121+
"Technology 1",
122+
interaction_style=InteractionStyle.Synchronous,
123+
)
124+
container2.uses(
125+
container3,
126+
"Uses 2",
127+
"Technology 2",
128+
interaction_style=InteractionStyle.Asynchronous,
129+
)
130+
container3.uses(system4)
131+
132+
dev_deployment_node = model.add_deployment_node(
133+
"Deployment Node", environment="Development"
134+
)
135+
container_instance1 = dev_deployment_node.add_container(
136+
container1, replicate_relationships=False
137+
)
138+
container_instance2 = dev_deployment_node.add_container(
139+
container2, replicate_relationships=False
140+
)
141+
container_instance3 = dev_deployment_node.add_container(
142+
container3, replicate_relationships=False
143+
)
144+
145+
assert container_instance1.relationships == set()
146+
assert container_instance2.relationships == set()
147+
assert container_instance3.relationships == set()

tests/unit/model/test_deployment_node.py

Lines changed: 3 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ class MockModel:
2424

2525
def __init__(self):
2626
"""Initialize the mock, creating an empty node for tests."""
27-
self.empty_node = DeploymentNode(name="Empty")
27+
self.empty_node = DeploymentNode(name="Empty", environment="Live")
2828
self.empty_node.set_model(self)
2929
self.mock_element = MockElement("element")
3030

@@ -133,6 +133,7 @@ def test_deployment_node_add_container(model_with_node):
133133
instance = node.add_container(container, replicate_relationships=False)
134134

135135
assert instance.container is container
136+
assert instance.environment == "Live"
136137
assert instance.model is model_with_node
137138
assert instance.parent is node
138139
assert instance in node.container_instances
@@ -160,12 +161,6 @@ def test_deployment_node_serialising_container(model_with_node):
160161
assert instance.parent is node2
161162

162163

163-
@pytest.mark.xfail(strict=True)
164-
def test_deployment_node_adding_container_replicating_relationships(model_with_node):
165-
"""Test replicating relationships when adding a container instance."""
166-
raise AssertionError() # Not implemented yet
167-
168-
169164
def test_deployment_node_add_software_system(model_with_node):
170165
"""Test adding a software system to a node to create an instance."""
171166
node = model_with_node.empty_node
@@ -174,6 +169,7 @@ def test_deployment_node_add_software_system(model_with_node):
174169
instance = node.add_software_system(system, replicate_relationships=False)
175170

176171
assert instance.software_system is system
172+
assert instance.environment == "Live"
177173
assert instance.model is model_with_node
178174
assert instance.parent is node
179175
assert instance in node.software_system_instances
@@ -201,14 +197,6 @@ def test_deployment_node_serialising_software_system(model_with_node):
201197
assert instance.parent is node2
202198

203199

204-
@pytest.mark.xfail(strict=True)
205-
def test_deployment_node_adding_software_system_replicating_relationships(
206-
model_with_node,
207-
):
208-
"""Test replicating relationships when adding a software system instance."""
209-
raise AssertionError() # Not implemented yet
210-
211-
212200
def test_deployment_node_add_infrastructure_node(model_with_node):
213201
"""Test adding an infrastructure node to a deployment node."""
214202
node = model_with_node.empty_node

0 commit comments

Comments
 (0)