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

1"""Template service providing core business logic for template operations.""" 

2 

3from typing import Any 

4 

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 

16 

17 

18class TemplateService: 

19 """Service providing template operations and business logic.""" 

20 

21 def __init__( 

22 self, 

23 repository: TemplateRepositoryPort, 

24 validator: TemplateValidatorPort, 

25 prompter: UserPrompterPort, 

26 ) -> None: 

27 """Initialize template service with required dependencies. 

28 

29 Args: 

30 repository: Template storage and retrieval interface 

31 validator: Template validation interface 

32 prompter: User interaction interface 

33 

34 """ 

35 self.repository = repository 

36 self.validator = validator 

37 self.prompter = prompter 

38 

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. 

41 

42 Args: 

43 template_name: Name of template to use 

44 placeholder_values: Optional predefined placeholder values 

45 

46 Returns: 

47 Generated content with placeholders replaced 

48 

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 

54 

55 """ 

56 # Load template 

57 template = self.repository.get_template(template_name) 

58 

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

64 

65 # Collect all required placeholder values 

66 final_values = self._collect_placeholder_values(template, placeholder_values or {}) 

67 

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 

74 

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. 

79 

80 Args: 

81 template_directory_name: Name of template directory to use 

82 placeholder_values: Optional predefined placeholder values 

83 

84 Returns: 

85 Dictionary mapping relative file paths to generated content 

86 

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 

92 

93 """ 

94 # Load template directory 

95 template_directory = self.repository.get_template_directory(template_directory_name) 

96 

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

102 

103 # Collect all required placeholder values for the entire directory 

104 final_values = self._collect_directory_placeholder_values(template_directory, placeholder_values or {}) 

105 

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 

112 

113 def get_template_info(self, template_name: str) -> dict[str, Any]: 

114 """Get detailed information about a template. 

115 

116 Args: 

117 template_name: Name of template to inspect 

118 

119 Returns: 

120 Dictionary containing template metadata and placeholder information 

121 

122 Raises: 

123 TemplateNotFoundError: If template doesn't exist 

124 

125 """ 

126 template = self.repository.get_template(template_name) 

127 return template.to_dict() 

128 

129 def get_directory_template_info(self, template_directory_name: str) -> dict[str, Any]: 

130 """Get detailed information about a directory template. 

131 

132 Args: 

133 template_directory_name: Name of template directory to inspect 

134 

135 Returns: 

136 Dictionary containing directory metadata and placeholder information 

137 

138 Raises: 

139 TemplateDirectoryNotFoundError: If template directory doesn't exist 

140 

141 """ 

142 template_directory = self.repository.get_template_directory(template_directory_name) 

143 return template_directory.to_dict() 

144 

145 def list_templates(self) -> list[str]: 

146 """List all available single template names. 

147 

148 Returns: 

149 List of template names available for use 

150 

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] 

156 

157 def list_template_directories(self) -> list[str]: 

158 """List all available template directory names. 

159 

160 Returns: 

161 List of template directory names available for use 

162 

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] 

168 

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. 

173 

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 

178 

179 Returns: 

180 Dictionary with success status and result/error details 

181 

182 """ 

183 try: 

184 # Load template 

185 template = self.repository.get_template(template_name) 

186 

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 } 

198 

199 provided_values = placeholder_values or {} 

200 

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() 

207 

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 ] 

214 

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 } 

224 

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() 

229 

230 # Render template 

231 content = template.render(final_values) 

232 

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 } 

248 

249 def list_all_templates(self) -> dict[str, Any]: 

250 """List all available templates (single and directory). 

251 

252 Returns: 

253 Dictionary with counts and lists of all template types 

254 

255 """ 

256 try: 

257 single_templates = self.list_templates() 

258 directory_templates = self.list_template_directories() 

259 

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 } 

266 

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 } 

274 

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. 

277 

278 Args: 

279 template: Template to collect values for 

280 provided_values: Values already provided 

281 

282 Returns: 

283 Complete set of placeholder values 

284 

285 Raises: 

286 InvalidPlaceholderValueError: If required placeholders missing or invalid 

287 UserCancelledError: If user cancels input 

288 

289 """ 

290 final_values = provided_values.copy() 

291 

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() 

296 

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 ] 

303 

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 

309 

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 

320 

321 return final_values 

322 

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. 

327 

328 Args: 

329 template_directory: Template directory to collect values for 

330 provided_values: Values already provided 

331 

332 Returns: 

333 Complete set of placeholder values 

334 

335 Raises: 

336 InvalidPlaceholderValueError: If required placeholders missing or invalid 

337 UserCancelledError: If user cancels input 

338 

339 """ 

340 final_values = provided_values.copy() 

341 

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() 

346 

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 ] 

353 

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 

359 

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') 

365 

366 return final_values 

367 

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. 

374 

375 Args: 

376 template_directory_name: Name of template directory to use 

377 placeholder_values: Optional predefined placeholder values 

378 

379 Returns: 

380 Dictionary with success status and result/error details 

381 

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 } 

400 

401 def validate_template(self, template_name: str) -> dict[str, Any]: 

402 """Validate a single template. 

403 

404 Args: 

405 template_name: Name of template to validate 

406 

407 Returns: 

408 Dictionary with validation results 

409 

410 """ 

411 try: 

412 template = self.repository.get_template(template_name) 

413 validation_errors = self.validator.validate_template(template) 

414 

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 } 

423 

424 def validate_directory_template(self, template_directory_name: str) -> dict[str, Any]: 

425 """Validate a directory template. 

426 

427 Args: 

428 template_directory_name: Name of template directory to validate 

429 

430 Returns: 

431 Dictionary with validation results 

432 

433 """ 

434 try: 

435 template_directory = self.repository.get_template_directory(template_directory_name) 

436 validation_errors = self.validator.validate_template_directory(template_directory) 

437 

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 }