Coverage for src/prosemark/domain/policies.py: 100%
29 statements
« prev ^ index » next coverage.py v7.8.0, created at 2025-09-24 18:08 +0000
« prev ^ index » next coverage.py v7.8.0, created at 2025-09-24 18:08 +0000
1"""Domain policies for binder integrity validation.
3This module provides pure functions that validate binder integrity constraints
4to ensure the system maintains consistent and valid binder state throughout
5all operations.
7All policies follow functional programming principles:
8- Pure functions with no side effects
9- Take domain objects and return validation results
10- Raise appropriate domain exceptions for violations
11- Composable and reusable across different use cases
12"""
14from typing import TYPE_CHECKING
16from prosemark.exceptions import BinderIntegrityError
18if TYPE_CHECKING: # pragma: no cover
19 from prosemark.domain.models import BinderItem, NodeId
22def validate_no_duplicate_ids(items: list['BinderItem']) -> None:
23 """Validate that no duplicate NodeId values exist within the binder tree.
25 This policy ensures tree integrity by preventing duplicate NodeId references
26 that could lead to ambiguous node resolution and data inconsistencies.
27 Placeholder items with None id are allowed and ignored.
29 Args:
30 items: List of root-level BinderItem objects to validate
32 Raises:
33 BinderIntegrityError: If duplicate NodeId values are found in the tree
35 Examples:
36 >>> # Valid case - unique NodeIds
37 >>> id1 = NodeId('0192f0c1-2345-7123-8abc-def012345678')
38 >>> id2 = NodeId('0192f0c1-2345-7456-8abc-def012345678')
39 >>> item1 = BinderItem(id=id1, display_title='Chapter 1', children=[])
40 >>> item2 = BinderItem(id=id2, display_title='Chapter 2', children=[])
41 >>> validate_no_duplicate_ids([item1, item2]) # No exception
43 >>> # Invalid case - duplicate NodeIds
44 >>> duplicate = BinderItem(id=id1, display_title='Duplicate', children=[])
45 >>> validate_no_duplicate_ids([item1, duplicate]) # Raises BinderIntegrityError
47 """
48 seen_node_ids = set['NodeId']()
50 def _collect_node_ids(item: 'BinderItem') -> None:
51 """Recursively collect all NodeIds and check for duplicates."""
52 if item.id is not None:
53 if item.id in seen_node_ids:
54 msg = f'Duplicate NodeId found in tree: {item.id}'
55 raise BinderIntegrityError(msg, item.id)
56 seen_node_ids.add(item.id)
58 for child in item.children:
59 _collect_node_ids(child)
61 for item in items:
62 _collect_node_ids(item)
65def validate_tree_structure(items: list['BinderItem']) -> None:
66 """Validate that all referenced nodes maintain valid hierarchical relationships.
68 This policy ensures tree structure integrity by validating that the
69 hierarchical structure is well-formed and maintains proper parent-child
70 relationships. Currently validates basic structural integrity.
72 Args:
73 items: List of root-level BinderItem objects to validate
75 Raises:
76 BinderIntegrityError: If tree structure violations are detected
78 Examples:
79 >>> # Valid hierarchical structure
80 >>> child = BinderItem(id=NodeId('...'), display_title='Chapter 1', children=[])
81 >>> parent = BinderItem(id=NodeId('...'), display_title='Part 1', children=[child])
82 >>> validate_tree_structure([parent]) # No exception
84 >>> # Valid flat structure
85 >>> item1 = BinderItem(id=NodeId('...'), display_title='Chapter 1', children=[])
86 >>> item2 = BinderItem(id=NodeId('...'), display_title='Chapter 2', children=[])
87 >>> validate_tree_structure([item1, item2]) # No exception
89 """
91 def _validate_item_structure(item: 'BinderItem') -> None:
92 """Recursively validate the structure of each item and its children."""
93 # Basic structure validation - item should have a display_title
94 if not isinstance(item.display_title, str): # pragma: no cover
95 msg = 'BinderItem display_title must be a string'
96 raise BinderIntegrityError(msg, item) # pragma: no cover
98 # Validate children list is properly formed
99 if not isinstance(item.children, list): # pragma: no cover
100 msg = 'BinderItem children must be a list'
101 raise BinderIntegrityError(msg, item) # pragma: no cover
103 # Recursively validate children
104 for child in item.children:
105 _validate_item_structure(child)
107 for item in items:
108 _validate_item_structure(item)
111def validate_placeholder_handling(items: list['BinderItem']) -> None:
112 """Validate that placeholder nodes (without IDs) are properly handled.
114 This policy ensures that placeholder items with None id are properly
115 supported throughout the tree structure. Placeholders are valid items
116 that represent future or organizational nodes without actual content.
118 Args:
119 items: List of root-level BinderItem objects to validate
121 Raises:
122 BinderIntegrityError: If placeholder handling violations are detected
124 Examples:
125 >>> # Valid placeholders
126 >>> placeholder1 = BinderItem(id=None, display_title='Future Section', children=[])
127 >>> placeholder2 = BinderItem(id=None, display_title='New Chapter', children=[])
128 >>> validate_placeholder_handling([placeholder1, placeholder2]) # No exception
130 >>> # Valid mixed items
131 >>> regular = BinderItem(id=NodeId('...'), display_title='Chapter 1', children=[])
132 >>> placeholder = BinderItem(id=None, display_title='Future', children=[])
133 >>> validate_placeholder_handling([regular, placeholder]) # No exception
135 """
137 def _validate_placeholder_item(item: 'BinderItem') -> None:
138 """Recursively validate placeholder handling for each item."""
139 # Placeholder validation - None id is explicitly allowed
140 if item.id is None and (not item.display_title or not isinstance(item.display_title, str)):
141 # Placeholders must still have valid display titles
142 msg = 'Placeholder items must have valid display titles'
143 raise BinderIntegrityError(msg, item)
145 # Recursively validate children
146 for child in item.children:
147 _validate_placeholder_item(child)
149 for item in items:
150 _validate_placeholder_item(item)