Skip to content

Commit 3af80d1

Browse files
ilaifMidnighter
andcommitted
feat: expand model and software system features
Co-authored-by: Midnighter <[email protected]>
1 parent 68ae29c commit 3af80d1

File tree

2 files changed

+177
-48
lines changed

2 files changed

+177
-48
lines changed

src/structurizr/model/model.py

Lines changed: 151 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,16 @@
1717

1818

1919
import logging
20-
from typing import Any, Iterable, Iterator, List, Optional, Set
20+
from typing import Iterable, List, Optional, ValuesView
2121

2222
from pydantic import Field
2323

24+
from structurizr.model.deployment_node import DeploymentNode, DeploymentNodeIO
25+
2426
from ..abstract_base import AbstractBase
2527
from ..base_model import BaseModel
28+
from .container import Container
29+
from .container_instance import ContainerInstance
2630
from .element import Element
2731
from .enterprise import Enterprise, EnterpriseIO
2832
from .person import Person, PersonIO
@@ -52,18 +56,18 @@ class ModelIO(BaseModel):
5256
"""
5357

5458
enterprise: Optional[EnterpriseIO] = Field(
55-
None, description="The enterprise associated with this model."
59+
default=None, description="The enterprise associated with this model."
5660
)
57-
people: Optional[List[PersonIO]] = Field(
58-
[], description="The set of people belonging to this model."
61+
people: List[PersonIO] = Field(
62+
default=[], description="The set of people belonging to this model."
5963
)
60-
software_systems: Optional[List[SoftwareSystemIO]] = Field(
61-
[],
64+
software_systems: List[SoftwareSystemIO] = Field(
65+
default=[],
6266
alias="softwareSystems",
6367
description="The set of software systems belonging to this model.",
6468
)
65-
deployment_nodes: Optional[List[Any]] = Field(
66-
[],
69+
deployment_nodes: List[DeploymentNodeIO] = Field(
70+
default=[],
6771
alias="deploymentNodes",
6872
description="The set of deployment nodes belonging to this model.",
6973
)
@@ -87,9 +91,8 @@ def __init__(
8791
self,
8892
enterprise: Optional[Enterprise] = None,
8993
people: Optional[Iterable[Person]] = (),
90-
software_systems: Optional[Iterable[SoftwareSystem]] = (),
91-
# TODO
92-
deployment_nodes: Optional[Iterable[Any]] = (),
94+
software_systems: Iterable[SoftwareSystem] = (),
95+
deployment_nodes: Iterable[DeploymentNode] = (),
9396
**kwargs,
9497
) -> None:
9598
"""
@@ -110,24 +113,40 @@ def __init__(
110113
self._id_generator = SequentialIntegerIDGenerator()
111114

112115
def __contains__(self, element: Element):
113-
return (
114-
element in self.people
115-
or element in self.software_systems
116-
or element in self.deployment_nodes
117-
)
116+
return element in self.get_elements()
118117

119118
@classmethod
120119
def hydrate(cls, model_io: ModelIO) -> "Model":
121120
""""""
122-
return Model(
121+
model = cls(
123122
enterprise=Enterprise.hydrate(model_io.enterprise)
124123
if model_io.enterprise is not None
125124
else None,
126-
people=map(Person.hydrate, model_io.people),
127-
software_systems=map(SoftwareSystem.hydrate, model_io.software_systems),
128-
# TODO: deployment nodes, relationships
125+
# TODO: relationships
129126
)
130127

128+
for person_io in model_io.people:
129+
person = Person.hydrate(person_io)
130+
model.add_person(person=person)
131+
132+
for software_system_io in model_io.software_systems:
133+
software_system = SoftwareSystem.hydrate(software_system_io)
134+
model.add_software_system(software_system=software_system)
135+
136+
for deployment_node_io in model_io.deployment_nodes:
137+
deployment_node = DeploymentNode.hydrate(deployment_node_io)
138+
model.add_deployment_node(deployment_node=deployment_node)
139+
140+
for element in model.get_elements():
141+
for relationship in element.relationships: # type: Relationship
142+
relationship.source = model.get_element(relationship.source_id)
143+
relationship.destination = model.get_element(
144+
relationship.destination_id
145+
)
146+
model.add_relationship(relationship)
147+
148+
return model
149+
131150
def add_person(self, person=None, **kwargs) -> Person:
132151
"""
133152
Add a new person to the model.
@@ -171,7 +190,7 @@ def add_software_system(self, software_system=None, **kwargs) -> SoftwareSystem:
171190
SoftwareSystem: Either the same or a new instance, depending on arguments.
172191
173192
Raises:
174-
ValueError: When a person with the same name already exists.
193+
ValueError: When a software system with the same name already exists.
175194
176195
See Also:
177196
SoftwareSystem
@@ -188,17 +207,100 @@ def add_software_system(self, software_system=None, **kwargs) -> SoftwareSystem:
188207
self.software_systems.add(software_system)
189208
return software_system
190209

191-
# def add_deployment_node(self, deployment_node=None,
192-
# **kwargs) -> DeploymentNode:
193-
# """Add a new software system to the model."""
194-
# if deployment_node is None:
195-
# deployment_node = DeploymentNode(**kwargs)
196-
# if deployment_node.id in {d.id for d in self.deployment_nodes}:
197-
# ValueError(
198-
# f"A deployment node with the ID {deployment_node.id} already "
199-
# f"exists in the model.")
200-
# self.deployment_nodes.add(deployment_node)
201-
# return deployment_node
210+
def add_container(
211+
self, container: Optional[Container] = None, **kwargs
212+
) -> Container:
213+
"""
214+
Add a new container to the model.
215+
216+
Args:
217+
container (Container, optional): Either provide a
218+
`Container` instance or
219+
**kwargs: Provide keyword arguments for instantiating a `Container`
220+
(recommended).
221+
222+
Returns:
223+
SoftwareSystem: Either the same or a new instance, depending on arguments.
224+
225+
Raises:
226+
ValueError: When a container with the same name already exists.
227+
228+
See Also:
229+
Container
230+
231+
"""
232+
if container is None:
233+
container = Container(**kwargs)
234+
if any(container.name == c.name for c in container.parent.containers):
235+
ValueError(
236+
f"A container with the name {container.name} already "
237+
f"exists in the model."
238+
)
239+
# TODO (midnighter): Modifying the parent seems like creating an undesired
240+
# tight link here.
241+
container.parent.add(container)
242+
self._add_element(container)
243+
return container
244+
245+
def add_container_instance(
246+
self,
247+
deployment_node: DeploymentNode,
248+
container: Container,
249+
replicate_container_relationships: bool,
250+
) -> ContainerInstance:
251+
"""
252+
Add a new container instance to the model.
253+
254+
Args:
255+
deployment_node (DeploymentNode, optional): `DeploymentNode` instance
256+
container (Container, optional): `Container` instance
257+
258+
Returns:
259+
ContainerInstance: A container instance.
260+
261+
Raises:
262+
ValueError: When a container with the same name already exists.
263+
264+
See Also:
265+
ContainerInstance
266+
267+
"""
268+
if container is None:
269+
raise ValueError("A container must be specified.")
270+
# TODO: implement
271+
# instance_number =
272+
273+
def add_deployment_node(
274+
self, deployment_node: Optional[DeploymentNode] = None, **kwargs
275+
) -> DeploymentNode:
276+
"""
277+
Add a new deployment node to the model.
278+
Args:
279+
deployment_node (DeploymentNode, optional): Either provide a
280+
`DeploymentNode` instance or
281+
**kwargs: Provide keyword arguments for instantiating a `DeploymentNode`
282+
(recommended).
283+
284+
Returns:
285+
DeploymentNode: Either the same or a new instance, depending on arguments.
286+
287+
Raises:
288+
ValueError: When a deployment node with the same name already exists.
289+
290+
See Also:
291+
DeploymentNode
292+
293+
"""
294+
if deployment_node is None:
295+
deployment_node = DeploymentNode(**kwargs)
296+
if deployment_node.id in {d.id for d in self.deployment_nodes}:
297+
ValueError(
298+
f"A deployment node with the ID {deployment_node.id} already "
299+
f"exists in the model."
300+
)
301+
self.deployment_nodes.add(deployment_node)
302+
self._add_element(deployment_node)
303+
return deployment_node
202304

203305
def add_relationship(
204306
self, relationship: Relationship = None, **kwargs
@@ -257,10 +359,19 @@ def get_relationship(self, id: str) -> Optional[Relationship]:
257359
"""
258360
return self._relationships_by_id.get(id)
259361

260-
def get_relationships(self) -> Iterator[Relationship]:
362+
def get_relationships(self) -> ValuesView[Relationship]:
261363
"""Return an iterator over all relationships contained in this model."""
262364
return self._relationships_by_id.values()
263365

366+
def get_elements(self) -> ValuesView[Element]:
367+
return self._elements_by_id.values()
368+
369+
def get_software_system_with_id(self, id: str) -> Optional[SoftwareSystem]:
370+
result = self.get_element(id)
371+
if not isinstance(result, SoftwareSystem):
372+
return None
373+
return result
374+
264375
def _add_element(self, element: Element) -> None:
265376
""""""
266377
if not element.id:
@@ -279,10 +390,16 @@ def _add_relationship(self, relationship: Relationship) -> bool:
279390
if not relationship.id:
280391
relationship.id = self._id_generator.generate_id()
281392
elif (
393+
# TODO(ilaif): @midnighter: not sure this is the best check,
394+
# we should have a global id check?
282395
relationship.id in self._elements_by_id
283396
or relationship.id in self._relationships_by_id
284397
):
285398
raise ValueError(f"The relationship {relationship} has an existing ID.")
399+
relationship.source.add_relationship(relationship)
400+
self._add_relationship_to_internal_structures(relationship)
401+
return True
402+
403+
def _add_relationship_to_internal_structures(self, relationship: Relationship):
286404
self._relationships_by_id[relationship.id] = relationship
287405
self._id_generator.found(relationship.id)
288-
return True

src/structurizr/model/software_system.py

Lines changed: 26 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,14 @@
1616
"""Provide a software system element model."""
1717

1818

19-
from typing import Any, List, Optional, Set
19+
from typing import List, Optional
2020

2121
from pydantic import Field
2222

23+
from .container import Container, ContainerIO
2324
from .location import Location
2425
from .static_structure_element import StaticStructureElement, StaticStructureElementIO
26+
from .tags import Tags
2527

2628

2729
__all__ = ("SoftwareSystem", "SoftwareSystemIO")
@@ -38,11 +40,11 @@ class SoftwareSystemIO(StaticStructureElementIO):
3840
"""
3941

4042
location: Location = Field(
41-
Location.Unspecified, description="The location of this software system."
43+
default=Location.Unspecified,
44+
description="The location of this software system.",
4245
)
43-
# TODO
44-
containers: List[Any] = Field(
45-
[], description="The containers within this software system."
46+
containers: List[ContainerIO] = Field(
47+
default=(), description="The containers within this software system."
4648
)
4749

4850

@@ -56,21 +58,31 @@ class SoftwareSystem(StaticStructureElement):
5658
5759
"""
5860

59-
def __init__(
60-
self,
61-
*,
62-
location: Location = Location.Unspecified,
63-
containers: Optional[Set[Any]] = None,
64-
**kwargs
65-
) -> None:
61+
def __init__(self, *, location: Location = Location.Unspecified, **kwargs) -> None:
6662
""""""
6763
super().__init__(**kwargs)
6864
self.location = location
69-
self.containers = set() if containers is None else containers
65+
self.containers = set()
66+
67+
# TODO: canonical_name
68+
# TODO: parent
69+
70+
self.tags.add(Tags.ELEMENT)
71+
self.tags.add(Tags.SOFTWARE_SYSTEM)
72+
73+
def add(self, container: Container):
74+
self.containers.add(container)
75+
76+
def add_container(self, name: str, description: str, technology: str,) -> Container:
77+
return self.get_model().add_container(
78+
parent=self, name=name, description=description, technology=technology,
79+
)
7080

7181
@classmethod
7282
def hydrate(cls, software_system_io: SoftwareSystemIO) -> "SoftwareSystem":
7383
""""""
7484
return cls(
75-
name=software_system_io.name, description=software_system_io.description
85+
name=software_system_io.name,
86+
description=software_system_io.description,
87+
location=software_system_io.location,
7688
)

0 commit comments

Comments
 (0)