Skip to content

Commit 538767b

Browse files
committed
feat: add support for grouping elements
1 parent 3646417 commit 538767b

File tree

6 files changed

+395
-3
lines changed

6 files changed

+395
-3
lines changed

CHANGELOG.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ History
44

55
Next Release
66
------------
7+
* Feat: Add support for grouping elements (#72)
78

89
0.4.0 (2021-02-05)
910
------------------
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
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+
"""Provide a superclass for all elements that can be included in a group."""
15+
16+
17+
from abc import ABC
18+
from typing import Optional
19+
20+
from .element import Element, ElementIO
21+
22+
23+
__all__ = ("GroupableElementIO", "GroupableElement")
24+
25+
26+
class GroupableElementIO(ElementIO, ABC):
27+
"""
28+
Define a superclass for all elements that can be included in a group.
29+
30+
Attributes:
31+
group (str): The name of thegroup in which this element should be included.
32+
"""
33+
34+
group: Optional[str] = ""
35+
36+
37+
class GroupableElement(Element, ABC):
38+
"""
39+
Define a superclass for all elements that can be included in a group.
40+
41+
Attributes:
42+
group (str): The name of thegroup in which this element should be included.
43+
"""
44+
45+
def __init__(self, *, group: str = "", **kwargs):
46+
"""Initialise a GroupableElement."""
47+
super().__init__(**kwargs)
48+
self.group = group
49+
50+
@classmethod
51+
def hydrate_arguments(cls, io: GroupableElementIO) -> dict:
52+
"""Hydrate an ElementIO into the constructor arguments for Element."""
53+
return {
54+
**super().hydrate_arguments(io),
55+
"group": io.group,
56+
}

src/structurizr/model/static_structure_element.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,8 @@
1919
from abc import ABC
2020
from typing import TYPE_CHECKING, Optional
2121

22-
from .element import Element, ElementIO
22+
from .element import Element
23+
from .groupable_element import GroupableElement, GroupableElementIO
2324

2425

2526
if TYPE_CHECKING: # pragma: no cover
@@ -29,7 +30,7 @@
2930
__all__ = ("StaticStructureElementIO", "StaticStructureElement")
3031

3132

32-
class StaticStructureElementIO(ElementIO, ABC):
33+
class StaticStructureElementIO(GroupableElementIO, ABC):
3334
"""
3435
Define a superclass for all static structure model elements.
3536
@@ -41,7 +42,7 @@ class StaticStructureElementIO(ElementIO, ABC):
4142
pass
4243

4344

44-
class StaticStructureElement(Element, ABC):
45+
class StaticStructureElement(GroupableElement, ABC):
4546
"""
4647
Define a superclass for all static structure model elements.
4748
Lines changed: 252 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,252 @@
1+
{
2+
"id" : 0,
3+
"name" : "Grouping",
4+
"description" : "Based on https://github.com/structurizr/dsl/blob/master/examples/groups.dsl",
5+
"properties" : {
6+
"structurizr.dsl" : "d29ya3NwYWNlIHsKCiAgICBtb2RlbCB7CiAgICAgICAgZ3JvdXAgIkNvbnN1bWVycyAtIEdyb3VwIDEiIHsKICAgICAgICAgICAgY29uc3VtZXJBID0gc29mdHdhcmVTeXN0ZW0gIkNvbnN1bWVyIEEiCiAgICAgICAgICAgIGNvbnN1bWVyQiA9IHNvZnR3YXJlU3lzdGVtICJDb25zdW1lciBCIgogICAgICAgIH0KICAgICAgICBncm91cCAiQ29uc3VtZXJzIC0gR3JvdXAgMiIgewogICAgICAgICAgICBjb25zdW1lckMgPSBzb2Z0d2FyZVN5c3RlbSAiQ29uc3VtZXIgQyIKICAgICAgICAgICAgY29uc3VtZXJEID0gc29mdHdhcmVTeXN0ZW0gIkNvbnN1bWVyIEQiCiAgICAgICAgfQoKICAgICAgICBzb2Z0d2FyZVN5c3RlbSA9IHNvZnR3YXJlU3lzdGVtICJTb2Z0d2FyZSBTeXN0ZW0iICJNeSBzb2Z0d2FyZSBzeXN0ZW0uIiB7CiAgICAgICAgICAgIGdyb3VwICJTZXJ2aWNlIDEiIHsKICAgICAgICAgICAgICAgIHNlcnZpY2UxRGF0YWJhc2UgPSBjb250YWluZXIgIlNlcnZpY2UgMSBEYXRhYmFzZSIKICAgICAgICAgICAgICAgIHNlcnZpY2UxQXBpID0gY29udGFpbmVyICJTZXJ2aWNlIDEgQVBJIgogICAgICAgICAgICB9CiAgICAgICAgICAgIGdyb3VwICJTZXJ2aWNlIDIiIHsKICAgICAgICAgICAgICAgIHNlcnZpY2UyRGF0YWJhc2UgPSBjb250YWluZXIgIlNlcnZpY2UgMiBEYXRhYmFzZSIKICAgICAgICAgICAgICAgIHNlcnZpY2UyQXBpID0gY29udGFpbmVyICJTZXJ2aWNlIDIgQVBJIgogICAgICAgICAgICB9CiAgICAgICAgfQoKICAgICAgICBjb25zdW1lckEgLT4gc2VydmljZTFBcGkgIlVzZXMiCiAgICAgICAgY29uc3VtZXJCIC0+IHNlcnZpY2UxQXBpICJVc2VzIgogICAgICAgIGNvbnN1bWVyQyAtPiBzZXJ2aWNlMUFwaSAiVXNlcyIKICAgICAgICBjb25zdW1lckQgLT4gc2VydmljZTFBcGkgIlVzZXMiCgogICAgICAgIHNlcnZpY2UxQXBpIC0+IHNlcnZpY2UxRGF0YWJhc2UgIlJlYWRzIGZyb20gYW5kIHdyaXRlcyB0byIKICAgICAgICBzZXJ2aWNlMkFwaSAtPiBzZXJ2aWNlMkRhdGFiYXNlICJSZWFkcyBmcm9tIGFuZCB3cml0ZXMgdG8iCiAgICAgICAgc2VydmljZTFBcGkgLT4gc2VydmljZTJBcGkgIlVzZXMiCiAgICB9CgogICAgdmlld3MgewogICAgICAgIHN5c3RlbUNvbnRleHQgc29mdHdhcmVTeXN0ZW0gIlN5c3RlbUNvbnRleHQiIHsKICAgICAgICAgICAgaW5jbHVkZSAqCiAgICAgICAgICAgIGF1dG9MYXlvdXQKICAgICAgICB9CgogICAgICAgIGNvbnRhaW5lciBzb2Z0d2FyZVN5c3RlbSAiQ29udGFpbmVycyIgewogICAgICAgICAgICBpbmNsdWRlICoKICAgICAgICAgICAgYXV0b2xheW91dAogICAgICAgIH0KCiAgICAgICAgc3R5bGVzIHsKICAgICAgICAgICAgZWxlbWVudCAiUGVyc29uIiB7CiAgICAgICAgICAgICAgICBzaGFwZSBwZXJzb24KICAgICAgICAgICAgfQogICAgICAgIH0KICAgIH0KICAgIAp9Cg=="
7+
},
8+
"configuration" : { },
9+
"model" : {
10+
"softwareSystems" : [ {
11+
"id" : "1",
12+
"tags" : "Element,Software System",
13+
"name" : "Consumer A",
14+
"relationships" : [ {
15+
"id" : "10",
16+
"tags" : "Relationship",
17+
"sourceId" : "1",
18+
"destinationId" : "7",
19+
"description" : "Uses"
20+
}, {
21+
"id" : "11",
22+
"tags" : "Relationship",
23+
"sourceId" : "1",
24+
"destinationId" : "5",
25+
"description" : "Uses"
26+
} ],
27+
"group" : "Consumers - Group 1",
28+
"location" : "Unspecified"
29+
}, {
30+
"id" : "2",
31+
"tags" : "Element,Software System",
32+
"name" : "Consumer B",
33+
"relationships" : [ {
34+
"id" : "12",
35+
"tags" : "Relationship",
36+
"sourceId" : "2",
37+
"destinationId" : "7",
38+
"description" : "Uses"
39+
}, {
40+
"id" : "13",
41+
"tags" : "Relationship",
42+
"sourceId" : "2",
43+
"destinationId" : "5",
44+
"description" : "Uses"
45+
} ],
46+
"group" : "Consumers - Group 1",
47+
"location" : "Unspecified"
48+
}, {
49+
"id" : "3",
50+
"tags" : "Element,Software System",
51+
"name" : "Consumer C",
52+
"relationships" : [ {
53+
"id" : "14",
54+
"tags" : "Relationship",
55+
"sourceId" : "3",
56+
"destinationId" : "7",
57+
"description" : "Uses"
58+
}, {
59+
"id" : "15",
60+
"tags" : "Relationship",
61+
"sourceId" : "3",
62+
"destinationId" : "5",
63+
"description" : "Uses"
64+
} ],
65+
"group" : "Consumers - Group 2",
66+
"location" : "Unspecified"
67+
}, {
68+
"id" : "4",
69+
"tags" : "Element,Software System",
70+
"name" : "Consumer D",
71+
"relationships" : [ {
72+
"id" : "16",
73+
"tags" : "Relationship",
74+
"sourceId" : "4",
75+
"destinationId" : "7",
76+
"description" : "Uses"
77+
}, {
78+
"id" : "17",
79+
"tags" : "Relationship",
80+
"sourceId" : "4",
81+
"destinationId" : "5",
82+
"description" : "Uses"
83+
} ],
84+
"group" : "Consumers - Group 2",
85+
"location" : "Unspecified"
86+
}, {
87+
"id" : "5",
88+
"tags" : "Element,Software System",
89+
"name" : "Software System",
90+
"description" : "My software system.",
91+
"location" : "Unspecified",
92+
"containers" : [ {
93+
"id" : "6",
94+
"tags" : "Element,Container",
95+
"name" : "Service 1 Database",
96+
"group" : "Service 1"
97+
}, {
98+
"id" : "9",
99+
"tags" : "Element,Container",
100+
"name" : "Service 2 API",
101+
"relationships" : [ {
102+
"id" : "19",
103+
"tags" : "Relationship",
104+
"sourceId" : "9",
105+
"destinationId" : "8",
106+
"description" : "Reads from and writes to"
107+
} ],
108+
"group" : "Service 2"
109+
}, {
110+
"id" : "7",
111+
"tags" : "Element,Container",
112+
"name" : "Service 1 API",
113+
"relationships" : [ {
114+
"id" : "18",
115+
"tags" : "Relationship",
116+
"sourceId" : "7",
117+
"destinationId" : "6",
118+
"description" : "Reads from and writes to"
119+
}, {
120+
"id" : "20",
121+
"tags" : "Relationship",
122+
"sourceId" : "7",
123+
"destinationId" : "9",
124+
"description" : "Uses"
125+
} ],
126+
"group" : "Service 1"
127+
}, {
128+
"id" : "8",
129+
"tags" : "Element,Container",
130+
"name" : "Service 2 Database",
131+
"group" : "Service 2"
132+
} ]
133+
} ]
134+
},
135+
"documentation" : { },
136+
"views" : {
137+
"systemContextViews" : [ {
138+
"softwareSystemId" : "5",
139+
"key" : "SystemContext",
140+
"automaticLayout" : {
141+
"implementation" : "Graphviz",
142+
"rankDirection" : "TopBottom",
143+
"rankSeparation" : 300,
144+
"nodeSeparation" : 300,
145+
"edgeSeparation" : 0,
146+
"vertices" : false
147+
},
148+
"enterpriseBoundaryVisible" : true,
149+
"elements" : [ {
150+
"id" : "1",
151+
"x" : 0,
152+
"y" : 0
153+
}, {
154+
"id" : "2",
155+
"x" : 0,
156+
"y" : 0
157+
}, {
158+
"id" : "3",
159+
"x" : 0,
160+
"y" : 0
161+
}, {
162+
"id" : "4",
163+
"x" : 0,
164+
"y" : 0
165+
}, {
166+
"id" : "5",
167+
"x" : 0,
168+
"y" : 0
169+
} ],
170+
"relationships" : [ {
171+
"id" : "17"
172+
}, {
173+
"id" : "15"
174+
}, {
175+
"id" : "13"
176+
}, {
177+
"id" : "11"
178+
} ]
179+
} ],
180+
"containerViews" : [ {
181+
"softwareSystemId" : "5",
182+
"key" : "Containers",
183+
"automaticLayout" : {
184+
"implementation" : "Graphviz",
185+
"rankDirection" : "TopBottom",
186+
"rankSeparation" : 300,
187+
"nodeSeparation" : 300,
188+
"edgeSeparation" : 0,
189+
"vertices" : false
190+
},
191+
"externalSoftwareSystemBoundariesVisible" : true,
192+
"elements" : [ {
193+
"id" : "1",
194+
"x" : 0,
195+
"y" : 0
196+
}, {
197+
"id" : "2",
198+
"x" : 0,
199+
"y" : 0
200+
}, {
201+
"id" : "3",
202+
"x" : 0,
203+
"y" : 0
204+
}, {
205+
"id" : "4",
206+
"x" : 0,
207+
"y" : 0
208+
}, {
209+
"id" : "6",
210+
"x" : 0,
211+
"y" : 0
212+
}, {
213+
"id" : "7",
214+
"x" : 0,
215+
"y" : 0
216+
}, {
217+
"id" : "8",
218+
"x" : 0,
219+
"y" : 0
220+
}, {
221+
"id" : "9",
222+
"x" : 0,
223+
"y" : 0
224+
} ],
225+
"relationships" : [ {
226+
"id" : "18"
227+
}, {
228+
"id" : "16"
229+
}, {
230+
"id" : "14"
231+
}, {
232+
"id" : "12"
233+
}, {
234+
"id" : "20"
235+
}, {
236+
"id" : "10"
237+
}, {
238+
"id" : "19"
239+
} ]
240+
} ],
241+
"configuration" : {
242+
"branding" : { },
243+
"styles" : {
244+
"elements" : [ {
245+
"tag" : "Person",
246+
"shape" : "Person"
247+
} ]
248+
},
249+
"terminology" : { }
250+
}
251+
}
252+
}

tests/integration/test_grouping.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
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+
"""Ensure grouping works with example workspace."""
14+
15+
from pathlib import Path
16+
17+
from structurizr import Workspace
18+
19+
20+
DEFINITIONS = Path(__file__).parent / "data" / "workspace_definition"
21+
22+
23+
def test_loading_workspace_with_groups():
24+
"""Check loading an example workspace with groupings defined."""
25+
path = DEFINITIONS / "Grouping.json"
26+
27+
workspace = Workspace.load(path)
28+
consumer_a = workspace.model.get_element("1")
29+
consumer_d = workspace.model.get_element("4")
30+
assert consumer_a.name == "Consumer A"
31+
assert consumer_a.group == "Consumers - Group 1"
32+
assert consumer_d.name == "Consumer D"
33+
assert consumer_d.group == "Consumers - Group 2"
34+
35+
service_2_api = workspace.model.get_element("9")
36+
assert service_2_api.name == "Service 2 API"
37+
assert service_2_api.group == "Service 2"

0 commit comments

Comments
 (0)