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

1"""Domain policies for binder integrity validation. 

2 

3This module provides pure functions that validate binder integrity constraints 

4to ensure the system maintains consistent and valid binder state throughout 

5all operations. 

6 

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""" 

13 

14from typing import TYPE_CHECKING 

15 

16from prosemark.exceptions import BinderIntegrityError 

17 

18if TYPE_CHECKING: # pragma: no cover 

19 from prosemark.domain.models import BinderItem, NodeId 

20 

21 

22def validate_no_duplicate_ids(items: list['BinderItem']) -> None: 

23 """Validate that no duplicate NodeId values exist within the binder tree. 

24 

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. 

28 

29 Args: 

30 items: List of root-level BinderItem objects to validate 

31 

32 Raises: 

33 BinderIntegrityError: If duplicate NodeId values are found in the tree 

34 

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 

42 

43 >>> # Invalid case - duplicate NodeIds 

44 >>> duplicate = BinderItem(id=id1, display_title='Duplicate', children=[]) 

45 >>> validate_no_duplicate_ids([item1, duplicate]) # Raises BinderIntegrityError 

46 

47 """ 

48 seen_node_ids = set['NodeId']() 

49 

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) 

57 

58 for child in item.children: 

59 _collect_node_ids(child) 

60 

61 for item in items: 

62 _collect_node_ids(item) 

63 

64 

65def validate_tree_structure(items: list['BinderItem']) -> None: 

66 """Validate that all referenced nodes maintain valid hierarchical relationships. 

67 

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. 

71 

72 Args: 

73 items: List of root-level BinderItem objects to validate 

74 

75 Raises: 

76 BinderIntegrityError: If tree structure violations are detected 

77 

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 

83 

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 

88 

89 """ 

90 

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 

97 

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 

102 

103 # Recursively validate children 

104 for child in item.children: 

105 _validate_item_structure(child) 

106 

107 for item in items: 

108 _validate_item_structure(item) 

109 

110 

111def validate_placeholder_handling(items: list['BinderItem']) -> None: 

112 """Validate that placeholder nodes (without IDs) are properly handled. 

113 

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. 

117 

118 Args: 

119 items: List of root-level BinderItem objects to validate 

120 

121 Raises: 

122 BinderIntegrityError: If placeholder handling violations are detected 

123 

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 

129 

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 

134 

135 """ 

136 

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) 

144 

145 # Recursively validate children 

146 for child in item.children: 

147 _validate_placeholder_item(child) 

148 

149 for item in items: 

150 _validate_placeholder_item(item)