Coverage for archdocs/diagram_model.py: 100%

35 statements  

« prev     ^ index     » next       coverage.py v7.15.3, created at 2026-08-03 00:00 +0000

1import dataclasses 

2import enum 

3import re as py_re 

4import typing 

5 

6from archdocs import settings 

7 

8 

9_UNSAFE_NODE_ID_PATTERN: typing.Final = py_re.compile(r"[^A-Za-z0-9_]+") 

10 

11 

12@typing.final 

13class NodeShape(enum.Enum): 

14 plain_node = '{defined_node_id}["{node_label}"]' 

15 service_node = '{defined_node_id}{{"{node_label}"}}' 

16 

17 

18# Declaration order is emission order, and mermaid lays a group out where it is written. 

19@typing.final 

20class NodeGroup(enum.Enum): 

21 inbound_api = "Inbound API" 

22 messaging_and_tasks = "Messaging & tasks" 

23 outbound_calls = "Outbound calls" 

24 data_stores = "Data stores" 

25 configuration = "Configuration" 

26 

27 

28@typing.final 

29@dataclasses.dataclass(frozen=True, slots=True, kw_only=True) 

30class DiagramNode: 

31 defined_node_id: str 

32 node_label: str 

33 node_group: NodeGroup | None = None 

34 node_shape: NodeShape = NodeShape.plain_node 

35 

36 

37@typing.final 

38@dataclasses.dataclass(frozen=True, slots=True, kw_only=True) 

39class DiagramEdge: 

40 source_node: DiagramNode 

41 target_node: DiagramNode 

42 edge_label: str = "" 

43 

44 

45def render_node_id(raw_node_name: str) -> str: 

46 return _UNSAFE_NODE_ID_PATTERN.sub("_", raw_node_name).strip("_") 

47 

48 

49def build_diagram_node(raw_node_name: str, node_label: str, node_group: NodeGroup, /) -> DiagramNode: 

50 return DiagramNode(defined_node_id=render_node_id(raw_node_name), node_label=node_label, node_group=node_group) 

51 

52 

53def build_service_node(service_name: str, node_annotations: typing.Iterable[str] = ()) -> DiagramNode: 

54 joined_annotations: typing.Final = ", ".join(filter(None, node_annotations)) 

55 return DiagramNode( 

56 defined_node_id=render_node_id(service_name) or settings.FALLBACK_SERVICE_NODE_ID, 

57 node_label=f"{service_name} ({joined_annotations})" if joined_annotations else service_name, 

58 node_shape=NodeShape.service_node, 

59 ) 

60 

61 

62EXTERNAL_CLIENT_NODE: typing.Final = DiagramNode( 

63 defined_node_id=settings.EXTERNAL_CLIENT_NODE_ID, 

64 node_label=settings.EXTERNAL_CLIENT_TITLE_FOR_SCHEMA, 

65 node_group=NodeGroup.inbound_api, 

66) 

67EXTERNAL_API_NODE: typing.Final = DiagramNode( 

68 defined_node_id=settings.EXTERNAL_API_NODE_ID, 

69 node_label=settings.EXTERNAL_API_TITLE_FOR_SCHEMA, 

70 node_group=NodeGroup.outbound_calls, 

71)