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:
|
||||
|
||||
![c4](/img/c4.png)
|
@ -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