Skip to content

Commit

Permalink
Initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
asherikov committed Oct 24, 2023
0 parents commit ddc376e
Show file tree
Hide file tree
Showing 43 changed files with 1,255 additions and 0 deletions.
16 changes: 16 additions & 0 deletions .github/workflows/master.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
on: [push, pull_request]

env:
APT_INSTALL: sudo apt install -y --no-install-recommends


jobs:
jammy_test:
runs-on: ubuntu-22.04

steps:
- uses: actions/checkout@v3
- run: make install_deps_apt
- run: make install
- run: make test

1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
build
70 changes: 70 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
PROJECT_NAME=hiearch

APT_INSTALL=sudo apt install -y --no-install-recommends
# DEB_PYTHON_INSTALL_LAYOUT fixes UNKNOWN -> https://github.com/pypa/setuptools/issues/3269
export DEB_PYTHON_INSTALL_LAYOUT=deb_system
export PYTHONDONTWRITEBYTECODE=1

CURRENT_DIR=$(shell pwd)
BUILD_DIR?=${CURRENT_DIR}/build
TEST_NOT=

.DEFAULT:
@echo "Testing $@..."
${TEST_NOT} hiearch -o ${BUILD_DIR}/$@ ./test/$@/*.yaml
mkdir -p ./build/$@
# TODO awkward and fragile
find build/$@/ -iname '*.gv' | sort | xargs --no-run-if-empty -I {} sh -c "sort {} | md5sum > ./build/$@/checksum.build"
find test/$@/ -iname '*.gv' | sort | xargs --no-run-if-empty -I {} sh -c "sort {} | md5sum > ./build/$@/checksum.test"
${TEST_NOT} cmp ./build/$@/checksum.build ./build/$@/checksum.test

venv: builddir
python3 -m venv ${BUILD_DIR}/venv

venv_test: venv
/bin/sh -c ". ${BUILD_DIR}/venv/bin/activate && ${MAKE} reinstall && ${MAKE} test"

test:
@${MAKE} \
01_basic 02_default_view 03_default_view_split 06_multiscope \
07_trivial 08_node_realations 09_tags 10_minimal || (echo "Failure!" && false)
@${MAKE} TEST_NOT=! 04_node_cycle 05_style_cycle || (echo "Failure!" && false)
@echo "Success!"

clean:
rm -Rf .pytest_cache
rm -Rf ${BUILD_DIR}
find ./ -name "__pycache__" | xargs rm -Rf

builddir:
mkdir -p ${BUILD_DIR}

install:
pip install --no-cache-dir ./
#${MAKE} clean

install_edit:
pip install --editable ./
${MAKE} clean

install_deps: builddir
pip-compile --verbose --output-file ./${BUILD_DIR}/requirements.txt pyproject.toml
pip3 install -r ./${BUILD_DIR}/requirements.txt

uninstall: clean
pip uninstall -y ${PROJECT_NAME}

reinstall:
${MAKE} uninstall
${MAKE} install

spell:
hunspell README.md

install_deps_apt:
${APT_INSTALL} graphviz


.PHONY: test

# https://packaging.python.org/en/latest/tutorials/packaging-projects/
216 changes: 216 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,216 @@
Introduction
============

`hiearch` is a CLI utility that generates diagrams from textual descriptions,
a.k.a., "diagrams as code". Unlike many other generators like `graphviz` it is
designed to support hierarchical decomposition and multiple views, in which
sense it is conceptually similar to <https://structurizr.com>. In other words,
each node in a the description is a hierarchy of nodes, that is automatically
expanded, collapsed, or hidden depending on preferences of each particular view
requested by the same description. Currently, `hiearch` uses `graphviz` to
generate individual diagrams corresponding to views, but other backends may be
added in the future.

The main purpose of `hiearch` is graphical representation of complex systems,
but it is meant to be generic and may find other applications. Why would anyone
need another system diagram generator when there is a multitude of tools that
support UML, C4, etc? I believe that the most important aspects of the system
are its decomposition into components and connections between them, `hiearch`
provides just that, nothing more, so that you can focus on documenting your
system rather than fitting it into a specific design framework.


Features
========

- `hiearch` does not use a DSL, but rather accepts a set of input `yaml` files
in arbitrary order. The file contents get composed into a single description,
which in turn gets decomposed into views.

- Description files have flat structure without nesting or inclusion and
contain lists of the following objects: nodes, edges, and views. Hierarchical
relations between nodes are specified using node parameters.

- Unlike `graphviz`, `hiearch` does not have a concept of a subgraphs: each
node may automatically become a subgraph depending on a view.

- `hiearch` is also somewhat stricter than `graphviz`: for example, all nodes
must be defined explicitly and cannot be deduced from edge definitions.

- View is not the same thing as `graphviz` layer
<https://graphviz.org/docs/attrs/layer/>: `graphviz` places all nodes on each
layer and simply makes some of them invisible, which results in awkward
spacing.

- `hiearch` allows nodes to have multiple parent nodes, which is referenced
here as 'multiscoping'. The idea is, of course, to show parents in different
views, for example, to outline system from logical or hardware point of view.
However, it is possible to visualize all parents in the same diagram, which
may be a bit kinky.

- `hiearch` supports label templates, which facilitates automatic generation of
URLs, tables, icon inclusions, etc.


Examples
========

Using hiearch
-------------

```
usage: hiearch [-h] [-o OUTPUT] [-f FORMAT] <filename> [<filename> ...]
Generates diagrams
positional arguments:
<filename> Input files
optional arguments:
-h, --help show this help message and exit
-o OUTPUT, --output OUTPUT
Output directory
-f FORMAT, --format FORMAT
Output format
```



Trivial
-------

<table>
<tr>
<td>
<pre>
nodes:
- id: ["Test 1", test1] # [label, unique id]
edges:
- link: [test1, test1] # [from node id, to node id]
views:
- id: view1 # unique id / output filename
nodes: [test1] # nodes to include
</pre>
</td>
<td align="center">
<img src="test/07_trivial/view1.svg" alt="view1" />
view1
</td>
</tr>
</table>



Node relations
--------------

<table>
<tr>
<td rowspan="3">
<pre>
nodes:
- id: ["Test 1", test1]
- id: ["Test 2", test2]
graphviz: # set graphviz attributes directly
fillcolor: aqua
style: filled
- id: ["Test 3", test3]
scope: test1 # test3 is contained in test1
style: test2 # test3 inherits all test2 attributes
edges:
- link: [test3, test3]
views:
- id: view1
nodes: [test2, test3]
- id: view2 # test1 is shown as subgraph
nodes: [test1, test3]
- id: view3
nodes: [test1, test2]
</pre>
</td>
<td align="center">
<img src="test/08_node_realations/view1.svg" alt="view1" />
view1
</td>
</tr>
<tr>
<td align="center">
<img src="test/08_node_realations/view2.svg" alt="view2" />
view2
</td>
</tr>
<tr>
<td align="center">
<img src="test/08_node_realations/view3.svg" alt="view3" />
view3
</td>
</tr>
</table>


Node selection using tags
-------------------------

<table>
<tr>
<td rowspan="2">
<pre>
nodes:
- id: ["Test 1", test1]
# tags: ["default"] if not specified
- id: ["Test 2", test2]
tags: ["test2_tag"]
edges:
- link: [test1, test1]
- link: [test2, test2]
views:
- id: view1
tags: ["test2_tag"]
- id: view2
tags: ["default"]
</pre>
</td>
<td align="center">
<img src="test/09_tags/view1.svg" alt="view1" />
view1
</td>
</tr>
<tr>
<td align="center">
<img src="test/09_tags/view2.svg" alt="view2" />
view2
</td>
</tr>
</table>

<table>
<tr>
<td>
<pre>
nodes:
- id: ["Test 1", test1]
# if no views are specified,
# a default view is automatically
# added with 'tags: ["default"]'
</pre>
</td>
<td align="center">
<img src="test/10_minimal/default.svg" alt="default" />
view1
</td>
</tr>
</table>



Node selection using relations
------------------------------


Multiscoping
------------


https://docs.structurizr.com/dsl/example
https://c4model.com/#DeploymentDiagram
https://docs.structurizr.com/
1 change: 1 addition & 0 deletions hiearch/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from . import hiearch
31 changes: 31 additions & 0 deletions hiearch/edge.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
def get_key(edge):
return f'{edge["link"][0]}.{edge["link"][1]}'


def parse(yaml_edges, edges, must_exist_nodes):
default = {
'link': ['', ''],
'style': None,
'graphviz': {},
# overriden
'id': None,
'in': None,
'out': None,
'scope_in': None,
'scope_out': None
}


for edge in yaml_edges:
key = get_key(edge)

if key in edges.keys():
raise RuntimeError(f'Duplicate edge id: {key} | file: {filename}')

edge['out'] = edge['link'][0]
edge['in'] = edge['link'][1]
must_exist_nodes.add(edge['in'])
must_exist_nodes.add(edge['out'])

edges[key] = default | edge

Loading

0 comments on commit ddc376e

Please sign in to comment.