Basic support for C4 model primitives.

pull/508/head
Marc Bruggmann 5 years ago
parent ac4e502277
commit b4729f9194

@ -0,0 +1,113 @@
"""
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, type, 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(type)}]<br/></font>' if type 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 Container(name, type, description, **kwargs):
node_attributes = {
"label": _format_node_label(name, type, description),
"shape": "record",
"width": "2.6",
"height": "1.6",
"fixedsize": "true",
"style": "filled",
"fillcolor": "dodgerblue3",
"fontcolor": "white",
}
node_attributes.update(kwargs)
return Node(**node_attributes)
def Database(name, type, description, **kwargs):
node_attributes = {
"label": _format_node_label(name, type, description),
"shape": "cylinder",
"width": "2.6",
"height": "1.6",
"fixedsize": "true",
"style": "filled",
"fillcolor": "dodgerblue3",
"fontcolor": "white",
}
node_attributes.update(kwargs)
return Node(**node_attributes)
def System(name, description="", external=False, **kwargs):
type = "External System" if external else "System"
node_attributes = {
"label": _format_node_label(name, type, description),
"shape": "record",
"width": "2.6",
"height": "1.6",
"fixedsize": "true",
"style": "filled",
"fillcolor": "gray60" if external else "dodgerblue4",
"fontcolor": "white",
}
# collapse system 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 Person(name, description, **kwargs):
node_attributes = {
"label": _format_node_label(name, "", description),
"shape": "record",
"width": "2.6",
"height": "1.6",
"fixedsize": "true",
"style": "rounded,filled",
"fillcolor": "dodgerblue4",
"fontcolor": "white",
}
node_attributes.update(kwargs)
return Node(**node_attributes)
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 Dependency(label, **kwargs):
edge_attribtues = {"label": _format_edge_label(label), "style": "dashed", "color": "gray60"}
edge_attribtues.update(kwargs)
return Edge(**edge_attribtues)

@ -0,0 +1,50 @@
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, Dependency
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.")
person >> container >> database
def test_systems(self):
with Diagram(name=self.name, show=False):
system = System("system", "The internal system.")
system_without_description = System("unknown")
external_system = System("external", "The external system.", external=True)
system >> system_without_description >> external_system
def test_edges(self):
with Diagram(name=self.name, show=False):
c1 = Container("container1", "type", "description")
c2 = Container("container2", "type", "description")
c1 >> Dependency("depends on") >> c2
c1 << Dependency("is dependend on") << c2
def test_cluster(self):
with Diagram(name=self.name, show=False):
with SystemBoundary("System"):
Container("container", "type", "description")
Loading…
Cancel
Save