Coverage for src/prosemark/templates/domain/services/placeholder_service.py: 100%
125 statements
« prev ^ index » next coverage.py v7.8.0, created at 2025-09-30 23:09 +0000
« prev ^ index » next coverage.py v7.8.0, created at 2025-09-30 23:09 +0000
1"""Placeholder service providing business logic for placeholder operations."""
3from typing import Any
5from prosemark.templates.domain.entities.placeholder import Placeholder, PlaceholderValue
6from prosemark.templates.domain.exceptions.template_exceptions import (
7 InvalidPlaceholderError,
8 InvalidPlaceholderValueError,
9)
10from prosemark.templates.domain.values.placeholder_pattern import PlaceholderPattern
13class PlaceholderService:
14 """Service providing placeholder operations and validation logic."""
16 @staticmethod
17 def create_placeholder_from_pattern(
18 pattern: str,
19 frontmatter: dict[str, Any] | None = None,
20 *,
21 required: bool = True,
22 default_value: str | None = None,
23 description: str | None = None,
24 ) -> Placeholder:
25 """Create a Placeholder from a pattern string.
27 Args:
28 pattern: The placeholder pattern (e.g., "{{variable_name}}")
29 frontmatter: Optional frontmatter dictionary for placeholder configuration
30 required: Whether this placeholder requires user input (ignored if frontmatter provided)
31 default_value: Optional default value if not required (ignored if frontmatter provided)
32 description: Optional human-readable description (ignored if frontmatter provided)
34 Returns:
35 New Placeholder instance
37 Raises:
38 InvalidPlaceholderError: If the pattern is invalid or inconsistent
40 """
41 try:
42 if frontmatter:
43 # Use frontmatter to configure the placeholder
44 pattern_obj = PlaceholderPattern(pattern)
45 return Placeholder.from_frontmatter(pattern_obj.name, frontmatter, pattern_obj)
46 # Use provided arguments
47 return Placeholder.from_pattern(
48 pattern, required=required, default_value=default_value, description=description
49 )
50 except Exception as e:
51 msg = f"Failed to create placeholder from pattern '{pattern}': {e}"
52 raise InvalidPlaceholderError(msg, placeholder_pattern=pattern) from e
54 @staticmethod
55 def create_placeholder_from_name(
56 name: str,
57 *,
58 required: bool = True,
59 default_value: str | None = None,
60 description: str | None = None,
61 ) -> Placeholder:
62 """Create a Placeholder from a variable name.
64 Args:
65 name: The variable name (without braces)
66 required: Whether this placeholder requires user input
67 default_value: Optional default value if not required
68 description: Optional human-readable description
70 Returns:
71 New Placeholder instance
73 Raises:
74 InvalidPlaceholderError: If the name is invalid
76 """
77 try:
78 return Placeholder.from_name(name, required=required, default_value=default_value, description=description)
79 except Exception as e:
80 msg = f"Failed to create placeholder from name '{name}': {e}"
81 raise InvalidPlaceholderError(msg, placeholder_pattern=f'{ { {name}} } ') from e
83 @staticmethod
84 def validate_placeholder_pattern(pattern: str) -> bool:
85 """Validate that a pattern is a valid placeholder.
87 Args:
88 pattern: The pattern to validate
90 Returns:
91 True if the pattern is valid, False otherwise
93 """
94 try:
95 PlaceholderPattern(pattern)
96 except InvalidPlaceholderError:
97 return False
98 else:
99 return True
101 @staticmethod
102 def extract_placeholders_from_text(text: str, frontmatter: dict[str, Any] | None = None) -> list[Placeholder]:
103 """Extract all placeholder patterns from text.
105 Args:
106 text: The text to extract placeholders from
107 frontmatter: Optional frontmatter dictionary for placeholder configuration
109 Returns:
110 List of unique Placeholder objects found in the text
112 Raises:
113 InvalidPlaceholderError: If any pattern is invalid
115 """
116 # Find all potential placeholder patterns manually and filter valid ones
117 import re
119 potential_patterns = re.findall(r'\{\{[^}]*\}\}', text)
121 # Convert patterns to Placeholder objects, skipping invalid ones
122 placeholders = []
123 seen_names = set()
125 for pattern_str in potential_patterns:
126 try:
127 pattern = PlaceholderPattern(pattern_str)
128 except InvalidPlaceholderError:
129 # Skip invalid patterns as per test requirements
130 continue
131 if pattern.name not in seen_names:
132 if frontmatter:
133 # Use frontmatter to configure placeholder
134 placeholder = Placeholder.from_frontmatter(pattern.name, frontmatter, pattern)
135 else:
136 # Default configuration
137 placeholder = Placeholder(
138 name=pattern.name,
139 pattern_obj=pattern,
140 required=True, # Default to required
141 default_value=None,
142 description=None,
143 )
144 placeholders.append(placeholder)
145 seen_names.add(pattern.name)
147 return placeholders
149 @staticmethod
150 def get_placeholder_names_from_text(text: str) -> set[str]:
151 """Extract just the placeholder names from text.
153 Args:
154 text: The text to extract placeholder names from
156 Returns:
157 Set of unique placeholder names found in the text
159 """
160 # Find all potential placeholder patterns manually and filter valid ones
161 import re
163 potential_patterns = re.findall(r'\{\{[^}]*\}\}', text)
165 names = set()
166 for pattern_str in potential_patterns:
167 try:
168 pattern = PlaceholderPattern(pattern_str)
169 names.add(pattern.name)
170 except InvalidPlaceholderError:
171 # Skip invalid patterns
172 continue
174 return names
176 def replace_placeholders_in_text(self, text: str, placeholder_values: dict[str, str]) -> str:
177 """Replace placeholders in text with provided values.
179 Only replaces placeholders for which values are provided.
180 Missing placeholders are left unchanged.
182 Args:
183 text: The text to replace placeholders in
184 placeholder_values: Dictionary mapping placeholder names to values
186 Returns:
187 Text with available placeholders replaced
189 Raises:
190 InvalidPlaceholderError: If any placeholder pattern is invalid
191 InvalidPlaceholderValueError: If any value is invalid
193 """
194 result = text
196 # Extract all placeholders from the text
197 placeholders = self.extract_placeholders_from_text(text)
199 # Replace each placeholder if a value is available
200 for placeholder in placeholders:
201 if placeholder.name in placeholder_values:
202 value = placeholder_values[placeholder.name]
203 result = self.replace_placeholder_in_text(result, placeholder, value)
205 return result
207 @staticmethod
208 def merge_placeholder_lists(placeholder_lists: list[list[Placeholder]]) -> list[Placeholder]:
209 """Merge multiple placeholder lists into a single list.
211 Args:
212 placeholder_lists: List of placeholder lists to merge
214 Returns:
215 Single merged list with duplicates removed
217 Raises:
218 ValueError: If conflicting placeholder definitions are found
220 """
221 merged_placeholders: dict[str, Placeholder] = {}
223 for placeholder_list in placeholder_lists:
224 for placeholder in placeholder_list:
225 if placeholder.name in merged_placeholders:
226 existing = merged_placeholders[placeholder.name]
227 # Check if they are identical
228 if (
229 existing.required != placeholder.required
230 or existing.default_value != placeholder.default_value
231 or existing.description != placeholder.description
232 ):
233 msg = f"Conflicting placeholder definitions for '{placeholder.name}'"
234 raise ValueError(msg)
235 # If identical, keep existing one
236 else:
237 merged_placeholders[placeholder.name] = placeholder
239 return list(merged_placeholders.values())
241 @staticmethod
242 def validate_placeholder_value(placeholder: Placeholder, value: str) -> bool:
243 """Validate a value against a placeholder's requirements.
245 Args:
246 placeholder: The placeholder to validate against
247 value: The value to validate
249 Returns:
250 True if the value is valid
252 Raises:
253 InvalidPlaceholderValueError: If the value is invalid
255 """
256 try:
257 return placeholder.validate_value(value)
258 except Exception as e:
259 msg = f"Value validation failed for placeholder '{placeholder.name}': {e}"
260 raise InvalidPlaceholderValueError(msg, placeholder_name=placeholder.name, provided_value=value) from e
262 @staticmethod
263 def create_placeholder_value(placeholder_name: str, value: str, source: str = 'user_input') -> PlaceholderValue:
264 """Create a PlaceholderValue object.
266 Args:
267 placeholder_name: Name of the placeholder
268 value: Value provided
269 source: Source of the value ('user_input', 'default', 'config', 'computed')
271 Returns:
272 New PlaceholderValue instance
274 Raises:
275 InvalidPlaceholderValueError: If the value or source is invalid
277 """
278 try:
279 return PlaceholderValue(placeholder_name=placeholder_name, value=value, source=source)
280 except Exception as e:
281 msg = f"Failed to create placeholder value for '{placeholder_name}': {e}"
282 raise InvalidPlaceholderValueError(msg, placeholder_name=placeholder_name, provided_value=value) from e
284 @staticmethod
285 def get_effective_value(placeholder: Placeholder, provided_value: str | None = None) -> str:
286 """Get the effective value for a placeholder.
288 Args:
289 placeholder: The placeholder to get value for
290 provided_value: Value provided by user (optional)
292 Returns:
293 The value to use (provided value, default, or empty string)
295 Raises:
296 InvalidPlaceholderValueError: If required placeholder has no value
298 """
299 try:
300 return placeholder.get_effective_value(provided_value)
301 except Exception as e:
302 msg = f"Failed to get effective value for placeholder '{placeholder.name}': {e}"
303 raise InvalidPlaceholderValueError(
304 msg, placeholder_name=placeholder.name, provided_value=provided_value
305 ) from e
307 def replace_placeholder_in_text(self, text: str, placeholder: Placeholder, value: str) -> str:
308 """Replace a placeholder in text with a value.
310 Args:
311 text: The text to replace placeholder in
312 placeholder: The placeholder to replace
313 value: The replacement value
315 Returns:
316 Text with placeholder replaced
318 Raises:
319 InvalidPlaceholderValueError: If value is invalid
320 InvalidPlaceholderError: If placeholder pattern is invalid
322 """
323 # Validate the value first
324 self.validate_placeholder_value(placeholder, value)
326 try:
327 return placeholder.pattern_obj.replace_in_text(text, value)
328 except Exception as e:
329 msg = f"Failed to replace placeholder '{placeholder.name}' in text: {e}"
330 raise InvalidPlaceholderError(msg, placeholder_pattern=placeholder.pattern) from e
332 def replace_all_placeholders_in_text(self, text: str, placeholder_values: dict[str, str]) -> str:
333 """Replace all placeholders in text with provided values.
335 Args:
336 text: The text to replace placeholders in
337 placeholder_values: Dictionary mapping placeholder names to values
339 Returns:
340 Text with all placeholders replaced
342 Raises:
343 InvalidPlaceholderError: If any placeholder pattern is invalid
344 InvalidPlaceholderValueError: If any value is invalid
346 """
347 result = text
349 # Extract all placeholders from the text
350 placeholders = self.extract_placeholders_from_text(text)
352 # Replace each placeholder
353 for placeholder in placeholders:
354 if placeholder.name in placeholder_values:
355 value = placeholder_values[placeholder.name]
356 result = self.replace_placeholder_in_text(result, placeholder, value)
357 elif placeholder.required:
358 msg = f"Missing value for required placeholder '{placeholder.name}'"
359 raise InvalidPlaceholderValueError(msg, placeholder_name=placeholder.name)
360 else:
361 # Use default value for optional placeholder
362 default_value = placeholder.get_effective_value()
363 result = self.replace_placeholder_in_text(result, placeholder, default_value)
365 return result
367 @staticmethod
368 def get_placeholder_summary(placeholders: list[Placeholder]) -> dict[str, Any]:
369 """Get a summary of placeholder information.
371 Args:
372 placeholders: List of placeholders to summarize
374 Returns:
375 Dictionary containing placeholder summary information
377 """
378 required_placeholders = [p for p in placeholders if p.required]
379 optional_placeholders = [p for p in placeholders if not p.required]
381 return {
382 'total_count': len(placeholders),
383 'required_count': len(required_placeholders),
384 'optional_count': len(optional_placeholders),
385 'required_names': [p.name for p in required_placeholders],
386 'optional_names': [p.name for p in optional_placeholders],
387 'all_names': [p.name for p in placeholders],
388 'placeholders_with_defaults': [p.name for p in placeholders if p.has_default],
389 'placeholders_with_descriptions': [p.name for p in placeholders if p.description],
390 }