Coverage for src/prosemark/domain/materialize_result.py: 100%

46 statements  

« prev     ^ index     » next       coverage.py v7.8.0, created at 2025-09-24 18:08 +0000

1"""MaterializeResult value object for successful placeholder materialization.""" 

2 

3import re 

4from dataclasses import dataclass 

5 

6from prosemark.domain.models import NodeId 

7 

8 

9@dataclass(frozen=True) 

10class MaterializeResult: 

11 """Represents a successful individual placeholder materialization. 

12 

13 This value object captures all the information about a successful 

14 materialization operation, including the generated node ID and file paths. 

15 

16 Args: 

17 display_title: Title of the materialized placeholder 

18 node_id: Generated UUIDv7 identifier for the new node 

19 file_paths: Created file paths (main and notes files) 

20 position: Position in binder hierarchy (e.g., "[0][1]") 

21 

22 Raises: 

23 ValueError: If validation rules are violated during construction 

24 

25 """ 

26 

27 # Expected number of file paths (main and notes) 

28 EXPECTED_FILE_COUNT = 2 

29 

30 display_title: str 

31 node_id: NodeId 

32 file_paths: list[str] 

33 position: str 

34 

35 def __post_init__(self) -> None: 

36 """Validate the materialization result after construction.""" 

37 # Validate display_title is non-empty 

38 if not self.display_title or not self.display_title.strip(): 

39 msg = 'Display title must be non-empty string' 

40 raise ValueError(msg) 

41 

42 # Validate node_id is a valid UUIDv7 

43 if not self._is_valid_uuid7(self.node_id.value): 

44 msg = f'Node ID must be valid UUIDv7, got {self.node_id.value}' 

45 raise ValueError(msg) 

46 

47 # Validate file_paths contains exactly 2 paths 

48 if len(self.file_paths) != self.EXPECTED_FILE_COUNT: 

49 msg = f'File paths must contain exactly 2 paths (main and notes), got {len(self.file_paths)}' 

50 raise ValueError(msg) 

51 

52 # Validate file paths format 

53 expected_main = f'{self.node_id.value}.md' 

54 expected_notes = f'{self.node_id.value}.notes.md' 

55 if expected_main not in self.file_paths or expected_notes not in self.file_paths: 

56 msg = f'File paths must contain {expected_main} and {expected_notes}, got {self.file_paths}' 

57 raise ValueError(msg) 

58 

59 # Validate position format 

60 if not self._is_valid_position(self.position): 

61 msg = f"Position must follow '[n][m]...' pattern, got {self.position}" 

62 raise ValueError(msg) 

63 

64 @staticmethod 

65 def _is_valid_uuid7(uuid_str: str) -> bool: 

66 """Check if string is a valid UUIDv7.""" 

67 # UUIDv7 pattern: 8-4-4-4-12 hex characters with version 7 

68 pattern = r'^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$' 

69 return bool(re.match(pattern, uuid_str, re.IGNORECASE)) 

70 

71 @staticmethod 

72 def _is_valid_position(position: str) -> bool: 

73 """Check if position follows the expected format.""" 

74 # Pattern: [0][1][2]... (one or more bracketed integers) 

75 pattern = r'^(\[[0-9]+\])+$' 

76 return bool(re.match(pattern, position)) 

77 

78 @property 

79 def main_file_path(self) -> str: 

80 """Get the main node file path.""" 

81 main_file = f'{self.node_id.value}.md' 

82 return next(path for path in self.file_paths if path == main_file) 

83 

84 @property 

85 def notes_file_path(self) -> str: 

86 """Get the notes file path.""" 

87 notes_file = f'{self.node_id.value}.notes.md' 

88 return next(path for path in self.file_paths if path == notes_file) 

89 

90 def __str__(self) -> str: 

91 """Generate human-readable string representation.""" 

92 return f"Materialized '{self.display_title}' → {self.node_id.value}"