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
« prev ^ index » next coverage.py v7.8.0, created at 2025-09-24 18:08 +0000
1"""MaterializeResult value object for successful placeholder materialization."""
3import re
4from dataclasses import dataclass
6from prosemark.domain.models import NodeId
9@dataclass(frozen=True)
10class MaterializeResult:
11 """Represents a successful individual placeholder materialization.
13 This value object captures all the information about a successful
14 materialization operation, including the generated node ID and file paths.
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]")
22 Raises:
23 ValueError: If validation rules are violated during construction
25 """
27 # Expected number of file paths (main and notes)
28 EXPECTED_FILE_COUNT = 2
30 display_title: str
31 node_id: NodeId
32 file_paths: list[str]
33 position: str
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)
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)
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)
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)
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)
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))
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))
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)
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)
90 def __str__(self) -> str:
91 """Generate human-readable string representation."""
92 return f"Materialized '{self.display_title}' → {self.node_id.value}"