Coverage for archdocs/diagram_model.py: 100%

35 statements  

« prev     ^ index     » next       coverage.py v7.15.4, created at 2026-08-18 22:03 +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# How a shape is spelled in mermaid is `mermaid_syntax`'s knowledge: the model only names them. 

13@typing.final 

14class NodeShape(enum.Enum): 

15 plain_node = enum.auto() 

16 service_node = enum.auto() 

17 

18 

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

20@typing.final 

21class NodeGroup(enum.Enum): 

22 inbound_api = "Inbound API" 

23 messaging_and_tasks = "Messaging & tasks" 

24 outbound_calls = "Outbound calls" 

25 data_stores = "Data stores" 

26 configuration = "Configuration" 

27 

28 

29@typing.final 

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

31class DiagramNode: 

32 defined_node_id: str 

33 node_label: str 

34 node_group: NodeGroup | None = None 

35 node_shape: NodeShape = NodeShape.plain_node 

36 

37 

38@typing.final 

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

40class DiagramEdge: 

41 source_node: DiagramNode 

42 target_node: DiagramNode 

43 edge_label: str = "" 

44 

45 

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

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

48 

49 

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

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

52 

53 

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

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

56 return DiagramNode( 

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

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

59 node_shape=NodeShape.service_node, 

60 ) 

61 

62 

63EXTERNAL_CLIENT_NODE: typing.Final = DiagramNode( 

64 defined_node_id=settings.EXTERNAL_CLIENT_NODE_ID, 

65 node_label=settings.EXTERNAL_CLIENT_TITLE_FOR_SCHEMA, 

66 node_group=NodeGroup.inbound_api, 

67) 

68EXTERNAL_API_NODE: typing.Final = DiagramNode( 

69 defined_node_id=settings.EXTERNAL_API_NODE_ID, 

70 node_label=settings.EXTERNAL_API_TITLE_FOR_SCHEMA, 

71 node_group=NodeGroup.outbound_calls, 

72)