C4 architecture with Structurizr
py-doc reads a Structurizr workspace, writes Markdown chapters from it, draws its views, joins the result with hand-written chapters, and opens the model in a live editor.
Model, pictures and text
- Model:
structurizr-cli export -format json(in the tool container, or the program named byc4.cli) resolves the DSL, so py-doc has no DSL parser of its own. Aworkspace.jsonis read directly and needs no Java. Exports are cached in.py-doc/c4by the hash of the DSL and every file it includes (!include,!docs,!adrs). The versions tested are listed inpy_doc/c4/structurizr.py. A field the format has dropped is named in the error. - Pictures: views are drawn as Structurizr draws them, in a browser. py-doc starts Structurizr Local on a private Docker network, opens it with headless Chromium in the tool container, writes
<view key>.pngand.svg, and stops Local if it started it. A failing view, an empty view, a missing picture or a missing engine is an error with a non-zero exit status that names the view. Nothing is ever written as a stand-in picture. - Text: one Markdown page per level: context, containers, components, relationships, deployment, dynamic views, and an element catalogue with a glossary, then the workspace’s
!docsand!adrs. Output is sorted and has no times. Each page says that it is generated and from which file.
c4:
cli: structurizr-cli # optional: use this program instead of the one in the tool container
image: structurizr/structurizr@sha256:... # optional: pin Structurizr Local
port: 8080
documents:
architecture:
title: Sensor Node Architecture
sources:
- file: intro.md
- structurizr: architecture/workspace.dsl # or workspace.json
levels: [context, containers, components, deployment]
systems: [Sensor Node]
heading_offset: 1
diagrams: {engine: browser, format: png} # or {engine: source, format: mermaid}
- file: decisions.md
outputs:
docx: {}
pptx: {split_level: 2}
With engine: source, the views are put in the text as plantuml or mermaid blocks under a line that says what they are, and the diagram step draws them. Generated pages are rebuilt only when the workspace or these options change. For PowerPoint, every view is a level 2 heading with its picture, so split_level: 2 gives one slide per view.
Commands
| Command | Does |
|---|---|
py-doc c4 check PATH |
Reads each workspace and checks the model: C4001 element without description, C4002 container without technology, C4003 relationship without description, C4004 element with no relationships. Severities are set under lint.rules. --strict fails on warnings |
py-doc c4 export PATH -o DIR -f FORMAT... |
-f (--formats) takes png, svg, plantuml, c4plantuml, mermaid, dot, static and markdown. --views KEY... limits the pictures, --levels the pages, --dry-run lists the files and writes nothing, --url uses a Local that is already running (reachable from the container) |
py-doc c4 lock [--tag TAG] |
Pulls the Structurizr Local image and records its digest in c4-images.lock, so builds use the same image everywhere. Without a lock file, py-doc warns and uses the tag |
py-doc c4 up PATH |
Opens the workspace in Structurizr Local to view it and arrange its views, until Ctrl+C. See Live editing |
py-doc c4 down, status |
Stops a detached Local, or says whether one is running |
PATH is a .dsl or workspace.json file, or a folder: workspaces below it are found in sorted order, skipping hidden and vendor folders and files that are only included. In a folder that has both, the .dsl is the workspace and the workspace.json beside it holds its layouts.
py-doc lint also runs the model rules for each structurizr: source in py-doc.yml. The export cache lives in .py-doc/c4/json at the project root, whatever the working folder. py-c4-exporter still works: it prints a rename note and runs py-doc c4 export with the same flags.
Live editing
py-doc c4 up architecture/workspace.dsl # until Ctrl+C
py-doc c4 up architecture/workspace.dsl --detach # leave it running; py-doc c4 down stops it
c4 up starts Structurizr Local with the project folder itself, opens http://localhost:8080/workspace/1/diagrams in your browser (--no-browser does not), and keeps running until Ctrl+C, when it stops Local.
- Changes to the DSL show when the page is reloaded.
- Layouts you arrange in the diagram editor (the pencil button, then the save button) are saved to
workspace.jsonbeside the.dsl. Structurizr keeps manual positions there because the DSL has no syntax for them. Commitworkspace.jsonwith the.dsl: when Local reads the DSL again, it takes the positions from the JSON, so a renamed element keeps its place. A view withautoLayoutis laid out again on every load, so removeautoLayoutfrom the views you arrange by hand. - Builds use the saved layouts: when a document draws a
workspace.dsl, itsworkspace.jsongoes with it. - The project is never changed by a build: drawing works on a copy of the workspace folder, so only
c4 upwritesworkspace.json. Local also keeps a search index and logs in.structurizr/beside the workspace, whichpy-doc initadds to.gitignore. - Local runs as your user, so the files it writes are yours. Local opens only
workspace.dslorworkspace.json, so a workspace with another name is shown from a copy, and its layouts are not kept (a warning says so). - On Windows, WSL would stop Docker soon after
c4 upreturned. py-doc keeps the distro running for as long as Local runs, andc4 downends that too.
If Local does not answer, check that Docker is running and that the port is free (c4.port in py-doc.yml changes it), then run py-doc c4 down and try again.