mirror of https://github.com/mingrammer/diagrams
feat: Basic support for C4 model primitives. (#508)
* Basic support for C4 model primitives. * Use the "rect" shape for nodes With the record shape we used before, graphviz would trip over edges that set constraint=False. * Adopt C4 terminology: Rename Dependency -> Relationship * Adopt C4 terminology: Rename type -> technology * Extract a shared C4Node This makes the code more DRY, but also allows to add company- specific extensions more easily. One need we have is to slightly adapt the terminology. At Spotify, we happen to call `Container` a `Component` for example. This is now easier to implement on top of the shared `C4Node`. * Add "C4" shield to the README * Document how to produce a C4 diagrampull/757/head
parent
e8eb3d83c3
commit
90dd23926b
@ -0,0 +1,97 @@
|
|||||||
|
"""
|
||||||
|
A set of nodes and edges to visualize software architecture using the C4 model.
|
||||||
|
"""
|
||||||
|
import html
|
||||||
|
import textwrap
|
||||||
|
from diagrams import Cluster, Node, Edge
|
||||||
|
|
||||||
|
|
||||||
|
def _format_node_label(name, key, description):
|
||||||
|
"""Create a graphviz label string for a C4 node"""
|
||||||
|
title = f'<font point-size="12"><b>{html.escape(name)}</b></font><br/>'
|
||||||
|
subtitle = f'<font point-size="9">[{html.escape(key)}]<br/></font>' if key else ""
|
||||||
|
text = f'<br/><font point-size="10">{_format_description(description)}</font>' if description else ""
|
||||||
|
return f"<{title}{subtitle}{text}>"
|
||||||
|
|
||||||
|
|
||||||
|
def _format_description(description):
|
||||||
|
"""
|
||||||
|
Formats the description string so it fits into the C4 nodes.
|
||||||
|
|
||||||
|
It line-breaks the description so it fits onto exactly three lines. If there are more
|
||||||
|
than three lines, all further lines are discarded and "..." inserted on the last line to
|
||||||
|
indicate that it was shortened. This will also html-escape the description so it can
|
||||||
|
safely be included in a HTML label.
|
||||||
|
"""
|
||||||
|
wrapper = textwrap.TextWrapper(width=40, max_lines=3)
|
||||||
|
lines = [html.escape(line) for line in wrapper.wrap(description)]
|
||||||
|
lines += [""] * (3 - len(lines)) # fill up with empty lines so it is always three
|
||||||
|
return "<br/>".join(lines)
|
||||||
|
|
||||||
|
|
||||||
|
def _format_edge_label(description):
|
||||||
|
"""Create a graphviz label string for a C4 edge"""
|
||||||
|
wrapper = textwrap.TextWrapper(width=24, max_lines=3)
|
||||||
|
lines = [html.escape(line) for line in wrapper.wrap(description)]
|
||||||
|
text = "<br/>".join(lines)
|
||||||
|
return f'<<font point-size="10">{text}</font>>'
|
||||||
|
|
||||||
|
|
||||||
|
def C4Node(name, technology="", description="", type="Container", **kwargs):
|
||||||
|
key = f"{type}: {technology}" if technology else type
|
||||||
|
node_attributes = {
|
||||||
|
"label": _format_node_label(name, key, description),
|
||||||
|
"labelloc": "c",
|
||||||
|
"shape": "rect",
|
||||||
|
"width": "2.6",
|
||||||
|
"height": "1.6",
|
||||||
|
"fixedsize": "true",
|
||||||
|
"style": "filled",
|
||||||
|
"fillcolor": "dodgerblue3",
|
||||||
|
"fontcolor": "white",
|
||||||
|
}
|
||||||
|
# collapse boxes to a smaller form if they don't have a description
|
||||||
|
if not description:
|
||||||
|
node_attributes.update({"width": "2", "height": "1"})
|
||||||
|
node_attributes.update(kwargs)
|
||||||
|
return Node(**node_attributes)
|
||||||
|
|
||||||
|
|
||||||
|
def Container(name, technology="", description="", **kwargs):
|
||||||
|
return C4Node(name, technology=technology, description=description, type="Container")
|
||||||
|
|
||||||
|
|
||||||
|
def Database(name, technology="", description="", **kwargs):
|
||||||
|
return C4Node(name, technology=technology, description=description, type="Database", shape="cylinder", labelloc="b")
|
||||||
|
|
||||||
|
|
||||||
|
def System(name, description="", external=False, **kwargs):
|
||||||
|
type = "External System" if external else "System"
|
||||||
|
fillcolor = "gray60" if external else "dodgerblue4"
|
||||||
|
return C4Node(name, description=description, type=type, fillcolor=fillcolor)
|
||||||
|
|
||||||
|
|
||||||
|
def Person(name, description="", external=False, **kwargs):
|
||||||
|
type = "External Person" if external else "Person"
|
||||||
|
fillcolor = "gray60" if external else "dodgerblue4"
|
||||||
|
style = "rounded,filled"
|
||||||
|
return C4Node(name, description=description, type=type, fillcolor=fillcolor, style=style)
|
||||||
|
|
||||||
|
|
||||||
|
def SystemBoundary(name, **kwargs):
|
||||||
|
graph_attributes = {
|
||||||
|
"label": html.escape(name),
|
||||||
|
"bgcolor": "white",
|
||||||
|
"margin": "16",
|
||||||
|
"style": "dashed",
|
||||||
|
}
|
||||||
|
graph_attributes.update(kwargs)
|
||||||
|
return Cluster(name, graph_attr=graph_attributes)
|
||||||
|
|
||||||
|
|
||||||
|
def Relationship(label="", **kwargs):
|
||||||
|
edge_attribtues = {"style": "dashed", "color": "gray60"}
|
||||||
|
if label:
|
||||||
|
edge_attribtues.update({"label": _format_edge_label(label)})
|
||||||
|
edge_attribtues.update(kwargs)
|
||||||
|
return Edge(**edge_attribtues)
|
@ -0,0 +1,77 @@
|
|||||||
|
---
|
||||||
|
id: c4
|
||||||
|
title: C4
|
||||||
|
---
|
||||||
|
|
||||||
|
## C4 Diagrams
|
||||||
|
|
||||||
|
[C4](https://c4model.com/) is a standardized model to visualize software architecture.
|
||||||
|
You can generate C4 diagrams by using the node and edge classes from the `diagrams.c4` package:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from diagrams import Diagram
|
||||||
|
from diagrams.c4 import Person, Container, Database, System, SystemBoundary, Relationship
|
||||||
|
|
||||||
|
graph_attr = {
|
||||||
|
"splines": "spline",
|
||||||
|
}
|
||||||
|
|
||||||
|
with Diagram("Container diagram for Internet Banking System", direction="TB", graph_attr=graph_attr):
|
||||||
|
customer = Person(
|
||||||
|
name="Personal Banking Customer", description="A customer of the bank, with personal bank accounts."
|
||||||
|
)
|
||||||
|
|
||||||
|
with SystemBoundary("Internet Banking System"):
|
||||||
|
webapp = Container(
|
||||||
|
name="Web Application",
|
||||||
|
technology="Java and Spring MVC",
|
||||||
|
description="Delivers the static content and the Internet banking single page application.",
|
||||||
|
)
|
||||||
|
|
||||||
|
spa = Container(
|
||||||
|
name="Single-Page Application",
|
||||||
|
technology="Javascript and Angular",
|
||||||
|
description="Provides all of the Internet banking functionality to customers via their web browser.",
|
||||||
|
)
|
||||||
|
|
||||||
|
mobileapp = Container(
|
||||||
|
name="Mobile App",
|
||||||
|
technology="Xamarin",
|
||||||
|
description="Provides a limited subset of the Internet banking functionality to customers via their mobile device.",
|
||||||
|
)
|
||||||
|
|
||||||
|
api = Container(
|
||||||
|
name="API Application",
|
||||||
|
technology="Java and Spring MVC",
|
||||||
|
description="Provides Internet banking functionality via a JSON/HTTPS API.",
|
||||||
|
)
|
||||||
|
|
||||||
|
database = Database(
|
||||||
|
name="Database",
|
||||||
|
technology="Oracle Database Schema",
|
||||||
|
description="Stores user registration information, hashed authentication credentials, access logs, etc.",
|
||||||
|
)
|
||||||
|
|
||||||
|
email = System(name="E-mail System", description="The internal Microsoft Exchange e-mail system.", external=True)
|
||||||
|
|
||||||
|
mainframe = System(
|
||||||
|
name="Mainframe Banking System",
|
||||||
|
description="Stores all of the core banking information about customers, accounts, transactions, etc.",
|
||||||
|
external=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
customer >> Relationship("Visits bigbank.com/ib using [HTTPS]") >> webapp
|
||||||
|
customer >> Relationship("Views account balances, and makes payments using") >> [spa, mobileapp]
|
||||||
|
webapp >> Relationship("Delivers to the customer's web browser") >> spa
|
||||||
|
spa >> Relationship("Make API calls to [JSON/HTTPS]") >> api
|
||||||
|
mobileapp >> Relationship("Make API calls to [JSON/HTTPS]") >> api
|
||||||
|
|
||||||
|
api >> Relationship("reads from and writes to") >> database
|
||||||
|
api >> Relationship("Sends email using [SMTP]") >> email
|
||||||
|
api >> Relationship("Makes API calls to [XML/HTTPS]") >> mainframe
|
||||||
|
customer << Relationship("Sends e-mails to") << email
|
||||||
|
```
|
||||||
|
|
||||||
|
It will produce the following diagram:
|
||||||
|
|
||||||
|

|
@ -0,0 +1,64 @@
|
|||||||
|
import os
|
||||||
|
import random
|
||||||
|
import string
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
from diagrams import Diagram
|
||||||
|
from diagrams import setcluster, setdiagram
|
||||||
|
from diagrams.c4 import Person, Container, Database, System, SystemBoundary, Relationship
|
||||||
|
|
||||||
|
|
||||||
|
class C4Test(unittest.TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.name = "diagram-" + "".join([random.choice(string.hexdigits) for n in range(7)])
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
setdiagram(None)
|
||||||
|
setcluster(None)
|
||||||
|
try:
|
||||||
|
os.remove(self.name + ".png")
|
||||||
|
except FileNotFoundError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def test_nodes(self):
|
||||||
|
with Diagram(name=self.name, show=False):
|
||||||
|
person = Person("person", "A person.")
|
||||||
|
container = Container("container", "Java application", "The application.")
|
||||||
|
database = Database("database", "Oracle database", "Stores information.")
|
||||||
|
|
||||||
|
def test_external_nodes(self):
|
||||||
|
with Diagram(name=self.name, show=False):
|
||||||
|
external_person = Person("person", external=True)
|
||||||
|
external_system = System("external", external=True)
|
||||||
|
|
||||||
|
def test_systems(self):
|
||||||
|
with Diagram(name=self.name, show=False):
|
||||||
|
system = System("system", "The internal system.")
|
||||||
|
system_without_description = System("unknown")
|
||||||
|
|
||||||
|
def test_edges(self):
|
||||||
|
with Diagram(name=self.name, show=False):
|
||||||
|
c1 = Container("container1")
|
||||||
|
c2 = Container("container2")
|
||||||
|
|
||||||
|
c1 >> c2
|
||||||
|
|
||||||
|
def test_edges_with_labels(self):
|
||||||
|
with Diagram(name=self.name, show=False):
|
||||||
|
c1 = Container("container1")
|
||||||
|
c2 = Container("container2")
|
||||||
|
|
||||||
|
c1 >> Relationship("depends on") >> c2
|
||||||
|
c1 << Relationship("is depended on by") << c2
|
||||||
|
|
||||||
|
def test_edge_without_constraint(self):
|
||||||
|
with Diagram(name=self.name, show=False):
|
||||||
|
s1 = System("system 1")
|
||||||
|
s2 = System("system 2")
|
||||||
|
|
||||||
|
s1 >> Relationship(constraint="False") >> s2
|
||||||
|
|
||||||
|
def test_cluster(self):
|
||||||
|
with Diagram(name=self.name, show=False):
|
||||||
|
with SystemBoundary("System"):
|
||||||
|
Container("container", "type", "description")
|
After Width: | Height: | Size: 141 KiB |
Loading…
Reference in new issue