Coverage for src/prosemark/templates/domain/services/template_service.py: 100%
132 statements
« prev ^ index » next coverage.py v7.8.0, created at 2025-10-01 00:05 +0000
« prev ^ index » next coverage.py v7.8.0, created at 2025-10-01 00:05 +0000
1"""Template service providing core business logic for template operations."""
3from typing import Any
5from prosemark.templates.domain.entities.template import Template
6from prosemark.templates.domain.entities.template_directory import TemplateDirectory
7from prosemark.templates.domain.exceptions.template_exceptions import (
8 InvalidPlaceholderValueError,
9 PlaceholderProcessingError,
10 TemplateError,
11 TemplateValidationError,
12)
13from prosemark.templates.ports.template_repository_port import TemplateRepositoryPort
14from prosemark.templates.ports.template_validator_port import TemplateValidatorPort
15from prosemark.templates.ports.user_prompter_port import UserPrompterPort
18class TemplateService:
19 """Service providing template operations and business logic."""
21 def __init__(
22 self,
23 repository: TemplateRepositoryPort,
24 validator: TemplateValidatorPort,
25 prompter: UserPrompterPort,
26 ) -> None:
27 """Initialize template service with required dependencies.
29 Args:
30 repository: Template storage and retrieval interface
31 validator: Template validation interface
32 prompter: User interaction interface
34 """
35 self.repository = repository
36 self.validator = validator
37 self.prompter = prompter
39 def create_from_template(self, template_name: str, placeholder_values: dict[str, str] | None = None) -> str:
40 """Create content from a template with placeholder replacement.
42 Args:
43 template_name: Name of template to use
44 placeholder_values: Optional predefined placeholder values
46 Returns:
47 Generated content with placeholders replaced
49 Raises:
50 TemplateNotFoundError: If template doesn't exist
51 InvalidPlaceholderValueError: If required placeholders missing
52 TemplateValidationError: If template is invalid
53 PlaceholderProcessingError: If placeholder replacement fails
55 """
56 # Load template
57 template = self.repository.get_template(template_name)
59 # Validate template
60 validation_errors = self.validator.validate_template(template)
61 if validation_errors:
62 msg = f'Template validation failed: {"; ".join(validation_errors)}'
63 raise TemplateValidationError(msg, template_path=str(template.path))
65 # Collect all required placeholder values
66 final_values = self._collect_placeholder_values(template, placeholder_values or {})
68 # Replace placeholders and return content
69 try:
70 return template.replace_placeholders(final_values)
71 except TemplateError as e:
72 msg = f"Failed to process placeholders in template '{template_name}': {e}"
73 raise PlaceholderProcessingError(msg, template_path=str(template.path)) from e
75 def create_from_directory_template(
76 self, template_directory_name: str, placeholder_values: dict[str, str] | None = None
77 ) -> dict[str, str]:
78 """Create multiple files from a directory template.
80 Args:
81 template_directory_name: Name of template directory to use
82 placeholder_values: Optional predefined placeholder values
84 Returns:
85 Dictionary mapping relative file paths to generated content
87 Raises:
88 TemplateDirectoryNotFoundError: If template directory doesn't exist
89 InvalidPlaceholderValueError: If required placeholders missing
90 TemplateValidationError: If any template is invalid
91 PlaceholderProcessingError: If placeholder replacement fails
93 """
94 # Load template directory
95 template_directory = self.repository.get_template_directory(template_directory_name)
97 # Validate directory and all templates
98 validation_errors = self.validator.validate_template_directory(template_directory)
99 if validation_errors:
100 msg = f'Template directory validation failed: {"; ".join(validation_errors)}'
101 raise TemplateValidationError(msg, template_path=str(template_directory.path))
103 # Collect all required placeholder values for the entire directory
104 final_values = self._collect_directory_placeholder_values(template_directory, placeholder_values or {})
106 # Replace placeholders in all templates
107 try:
108 return template_directory.replace_placeholders_in_all(final_values)
109 except TemplateError as e:
110 msg = f"Failed to process placeholders in directory template '{template_directory_name}': {e}"
111 raise PlaceholderProcessingError(msg, template_path=str(template_directory.path)) from e
113 def get_template_info(self, template_name: str) -> dict[str, Any]:
114 """Get detailed information about a template.
116 Args:
117 template_name: Name of template to inspect
119 Returns:
120 Dictionary containing template metadata and placeholder information
122 Raises:
123 TemplateNotFoundError: If template doesn't exist
125 """
126 template = self.repository.get_template(template_name)
127 return template.to_dict()
129 def get_directory_template_info(self, template_directory_name: str) -> dict[str, Any]:
130 """Get detailed information about a directory template.
132 Args:
133 template_directory_name: Name of template directory to inspect
135 Returns:
136 Dictionary containing directory metadata and placeholder information
138 Raises:
139 TemplateDirectoryNotFoundError: If template directory doesn't exist
141 """
142 template_directory = self.repository.get_template_directory(template_directory_name)
143 return template_directory.to_dict()
145 def list_templates(self) -> list[str]:
146 """List all available single template names.
148 Returns:
149 List of template names available for use
151 """
152 # Use templates root as search path
153 templates_root = self.repository.get_templates_root()
154 templates = self.repository.list_templates(templates_root)
155 return [template.name for template in templates]
157 def list_template_directories(self) -> list[str]:
158 """List all available template directory names.
160 Returns:
161 List of template directory names available for use
163 """
164 # Use templates root as search path
165 templates_root = self.repository.get_templates_root()
166 template_directories = self.repository.list_template_directories(templates_root)
167 return [template_dir.name for template_dir in template_directories]
169 def create_content_from_single_template(
170 self, template_name: str, placeholder_values: dict[str, str] | None = None, *, interactive: bool = True
171 ) -> dict[str, Any]:
172 """Create content from a single template with optional interactivity.
174 Args:
175 template_name: Name of template to use
176 placeholder_values: Optional predefined placeholder values
177 interactive: Whether to prompt user for missing values
179 Returns:
180 Dictionary with success status and result/error details
182 """
183 try:
184 # Load template
185 template = self.repository.get_template(template_name)
187 # Validate template
188 validation_errors = self.validator.validate_template(template)
189 if validation_errors:
190 error_message = f'Template validation failed: {"; ".join(validation_errors)}'
191 return {
192 'success': False,
193 'template_name': template_name,
194 'error_type': 'TemplateValidationError',
195 'error_message': error_message,
196 'error': error_message, # For backward compatibility with some tests
197 }
199 provided_values = placeholder_values or {}
201 if interactive:
202 # Collect missing values interactively
203 final_values = self._collect_placeholder_values(template, provided_values)
204 else:
205 # Non-interactive mode - validate we have all required values
206 final_values = provided_values.copy()
208 # Check for missing required placeholders
209 missing_required = [
210 placeholder.name
211 for placeholder in template.required_placeholders
212 if placeholder.name not in final_values
213 ]
215 if missing_required:
216 error_message = f'Missing values for required placeholders: {", ".join(missing_required)}'
217 return {
218 'success': False,
219 'template_name': template_name,
220 'error_type': 'InvalidPlaceholderValueError',
221 'error_message': error_message,
222 'error': error_message, # For backward compatibility with some tests
223 }
225 # Add default values for optional placeholders
226 for placeholder in template.optional_placeholders:
227 if placeholder.name not in final_values:
228 final_values[placeholder.name] = placeholder.get_effective_value()
230 # Render template
231 content = template.render(final_values)
233 except TemplateError as e:
234 return {
235 'success': False,
236 'template_name': template_name,
237 'error_type': type(e).__name__,
238 'error_message': str(e),
239 'error': str(e), # For backward compatibility with some tests
240 }
241 else:
242 return {
243 'success': True,
244 'content': content,
245 'template_name': template_name,
246 'placeholder_values': final_values,
247 }
249 def list_all_templates(self) -> dict[str, Any]:
250 """List all available templates (single and directory).
252 Returns:
253 Dictionary with counts and lists of all template types
255 """
256 try:
257 single_templates = self.list_templates()
258 directory_templates = self.list_template_directories()
260 return {
261 'success': True,
262 'total_templates': len(single_templates) + len(directory_templates),
263 'single_templates': {'count': len(single_templates), 'names': single_templates},
264 'directory_templates': {'count': len(directory_templates), 'names': directory_templates},
265 }
267 except (TemplateError, Exception) as e:
268 return {
269 'success': False,
270 'error_type': type(e).__name__,
271 'error_message': str(e),
272 'error': str(e), # For backward compatibility with some tests
273 }
275 def _collect_placeholder_values(self, template: Template, provided_values: dict[str, str]) -> dict[str, str]:
276 """Collect all required placeholder values for a template.
278 Args:
279 template: Template to collect values for
280 provided_values: Values already provided
282 Returns:
283 Complete set of placeholder values
285 Raises:
286 InvalidPlaceholderValueError: If required placeholders missing or invalid
287 UserCancelledError: If user cancels input
289 """
290 final_values = provided_values.copy()
292 # First, add default values for optional placeholders
293 for placeholder in template.placeholders:
294 if placeholder.name not in final_values and not placeholder.required:
295 final_values[placeholder.name] = placeholder.get_effective_value()
297 # Collect missing required placeholders
298 missing_required = [
299 placeholder
300 for placeholder in template.placeholders
301 if placeholder.name not in final_values and placeholder.required
302 ]
304 # If there are missing required placeholders, prompt for them
305 if missing_required:
306 prompted_values = self.prompter.prompt_for_placeholder_values(missing_required)
307 for placeholder_name, placeholder_value in prompted_values.items():
308 final_values[placeholder_name] = placeholder_value.value
310 # Validate all values
311 for placeholder in template.placeholders:
312 if placeholder.name in final_values: # pragma: no branch
313 try:
314 placeholder.validate_value(final_values[placeholder.name])
315 except TemplateError as e:
316 msg = f"Invalid value for placeholder '{placeholder.name}': {e}"
317 raise InvalidPlaceholderValueError(
318 msg, placeholder_name=placeholder.name, provided_value=final_values[placeholder.name]
319 ) from e
321 return final_values
323 def _collect_directory_placeholder_values(
324 self, template_directory: TemplateDirectory, provided_values: dict[str, str]
325 ) -> dict[str, str]:
326 """Collect all required placeholder values for a template directory.
328 Args:
329 template_directory: Template directory to collect values for
330 provided_values: Values already provided
332 Returns:
333 Complete set of placeholder values
335 Raises:
336 InvalidPlaceholderValueError: If required placeholders missing or invalid
337 UserCancelledError: If user cancels input
339 """
340 final_values = provided_values.copy()
342 # First, add default values for optional placeholders
343 for placeholder in template_directory.all_placeholders:
344 if placeholder.name not in final_values and not placeholder.required:
345 final_values[placeholder.name] = placeholder.get_effective_value()
347 # Collect missing required placeholders
348 missing_required = [
349 placeholder
350 for placeholder in template_directory.all_placeholders
351 if placeholder.name not in final_values and placeholder.required
352 ]
354 # If there are missing required placeholders, prompt for them
355 if missing_required:
356 prompted_values = self.prompter.prompt_for_placeholder_values(missing_required)
357 for placeholder_name, placeholder_value in prompted_values.items():
358 final_values[placeholder_name] = placeholder_value.value
360 # Validate all values
361 validation_errors = template_directory.validate_placeholder_values(final_values)
362 if validation_errors:
363 msg = f'Placeholder validation failed: {"; ".join(validation_errors)}'
364 raise InvalidPlaceholderValueError(msg, placeholder_name='multiple')
366 return final_values
368 def create_content_from_directory_template(
369 self,
370 template_directory_name: str,
371 placeholder_values: dict[str, str] | None = None,
372 ) -> dict[str, Any]:
373 """Create content from a directory template.
375 Args:
376 template_directory_name: Name of template directory to use
377 placeholder_values: Optional predefined placeholder values
379 Returns:
380 Dictionary with success status and result/error details
382 """
383 try:
384 content_map = self.create_from_directory_template(template_directory_name, placeholder_values)
385 return {
386 'success': True,
387 'content': content_map,
388 'template_name': template_directory_name,
389 'file_count': len(content_map),
390 'placeholder_values': placeholder_values or {},
391 }
392 except TemplateError as e:
393 return {
394 'success': False,
395 'template_name': template_directory_name,
396 'error_type': type(e).__name__,
397 'error_message': str(e),
398 'error': str(e), # For backward compatibility with some tests
399 }
401 def validate_template(self, template_name: str) -> dict[str, Any]:
402 """Validate a single template.
404 Args:
405 template_name: Name of template to validate
407 Returns:
408 Dictionary with validation results
410 """
411 try:
412 template = self.repository.get_template(template_name)
413 validation_errors = self.validator.validate_template(template)
415 return {'valid': len(validation_errors) == 0, 'template_name': template_name, 'errors': validation_errors}
416 except TemplateError as e:
417 return {
418 'valid': False,
419 'template_name': template_name,
420 'error_type': type(e).__name__,
421 'error_message': str(e),
422 }
424 def validate_directory_template(self, template_directory_name: str) -> dict[str, Any]:
425 """Validate a directory template.
427 Args:
428 template_directory_name: Name of template directory to validate
430 Returns:
431 Dictionary with validation results
433 """
434 try:
435 template_directory = self.repository.get_template_directory(template_directory_name)
436 validation_errors = self.validator.validate_template_directory(template_directory)
438 return {
439 'valid': len(validation_errors) == 0,
440 'template_directory_name': template_directory_name,
441 'errors': validation_errors,
442 }
443 except TemplateError as e:
444 return {
445 'valid': False,
446 'template_directory_name': template_directory_name,
447 'error_type': type(e).__name__,
448 'error_message': str(e),
449 }