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

1"""Placeholder service providing business logic for placeholder operations.""" 

2 

3from typing import Any 

4 

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 

11 

12 

13class PlaceholderService: 

14 """Service providing placeholder operations and validation logic.""" 

15 

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. 

26 

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) 

33 

34 Returns: 

35 New Placeholder instance 

36 

37 Raises: 

38 InvalidPlaceholderError: If the pattern is invalid or inconsistent 

39 

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 

53 

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. 

63 

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 

69 

70 Returns: 

71 New Placeholder instance 

72 

73 Raises: 

74 InvalidPlaceholderError: If the name is invalid 

75 

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 

82 

83 @staticmethod 

84 def validate_placeholder_pattern(pattern: str) -> bool: 

85 """Validate that a pattern is a valid placeholder. 

86 

87 Args: 

88 pattern: The pattern to validate 

89 

90 Returns: 

91 True if the pattern is valid, False otherwise 

92 

93 """ 

94 try: 

95 PlaceholderPattern(pattern) 

96 except InvalidPlaceholderError: 

97 return False 

98 else: 

99 return True 

100 

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. 

104 

105 Args: 

106 text: The text to extract placeholders from 

107 frontmatter: Optional frontmatter dictionary for placeholder configuration 

108 

109 Returns: 

110 List of unique Placeholder objects found in the text 

111 

112 Raises: 

113 InvalidPlaceholderError: If any pattern is invalid 

114 

115 """ 

116 # Find all potential placeholder patterns manually and filter valid ones 

117 import re 

118 

119 potential_patterns = re.findall(r'\{\{[^}]*\}\}', text) 

120 

121 # Convert patterns to Placeholder objects, skipping invalid ones 

122 placeholders = [] 

123 seen_names = set() 

124 

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) 

146 

147 return placeholders 

148 

149 @staticmethod 

150 def get_placeholder_names_from_text(text: str) -> set[str]: 

151 """Extract just the placeholder names from text. 

152 

153 Args: 

154 text: The text to extract placeholder names from 

155 

156 Returns: 

157 Set of unique placeholder names found in the text 

158 

159 """ 

160 # Find all potential placeholder patterns manually and filter valid ones 

161 import re 

162 

163 potential_patterns = re.findall(r'\{\{[^}]*\}\}', text) 

164 

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 

173 

174 return names 

175 

176 def replace_placeholders_in_text(self, text: str, placeholder_values: dict[str, str]) -> str: 

177 """Replace placeholders in text with provided values. 

178 

179 Only replaces placeholders for which values are provided. 

180 Missing placeholders are left unchanged. 

181 

182 Args: 

183 text: The text to replace placeholders in 

184 placeholder_values: Dictionary mapping placeholder names to values 

185 

186 Returns: 

187 Text with available placeholders replaced 

188 

189 Raises: 

190 InvalidPlaceholderError: If any placeholder pattern is invalid 

191 InvalidPlaceholderValueError: If any value is invalid 

192 

193 """ 

194 result = text 

195 

196 # Extract all placeholders from the text 

197 placeholders = self.extract_placeholders_from_text(text) 

198 

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) 

204 

205 return result 

206 

207 @staticmethod 

208 def merge_placeholder_lists(placeholder_lists: list[list[Placeholder]]) -> list[Placeholder]: 

209 """Merge multiple placeholder lists into a single list. 

210 

211 Args: 

212 placeholder_lists: List of placeholder lists to merge 

213 

214 Returns: 

215 Single merged list with duplicates removed 

216 

217 Raises: 

218 ValueError: If conflicting placeholder definitions are found 

219 

220 """ 

221 merged_placeholders: dict[str, Placeholder] = {} 

222 

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 

238 

239 return list(merged_placeholders.values()) 

240 

241 @staticmethod 

242 def validate_placeholder_value(placeholder: Placeholder, value: str) -> bool: 

243 """Validate a value against a placeholder's requirements. 

244 

245 Args: 

246 placeholder: The placeholder to validate against 

247 value: The value to validate 

248 

249 Returns: 

250 True if the value is valid 

251 

252 Raises: 

253 InvalidPlaceholderValueError: If the value is invalid 

254 

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 

261 

262 @staticmethod 

263 def create_placeholder_value(placeholder_name: str, value: str, source: str = 'user_input') -> PlaceholderValue: 

264 """Create a PlaceholderValue object. 

265 

266 Args: 

267 placeholder_name: Name of the placeholder 

268 value: Value provided 

269 source: Source of the value ('user_input', 'default', 'config', 'computed') 

270 

271 Returns: 

272 New PlaceholderValue instance 

273 

274 Raises: 

275 InvalidPlaceholderValueError: If the value or source is invalid 

276 

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 

283 

284 @staticmethod 

285 def get_effective_value(placeholder: Placeholder, provided_value: str | None = None) -> str: 

286 """Get the effective value for a placeholder. 

287 

288 Args: 

289 placeholder: The placeholder to get value for 

290 provided_value: Value provided by user (optional) 

291 

292 Returns: 

293 The value to use (provided value, default, or empty string) 

294 

295 Raises: 

296 InvalidPlaceholderValueError: If required placeholder has no value 

297 

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 

306 

307 def replace_placeholder_in_text(self, text: str, placeholder: Placeholder, value: str) -> str: 

308 """Replace a placeholder in text with a value. 

309 

310 Args: 

311 text: The text to replace placeholder in 

312 placeholder: The placeholder to replace 

313 value: The replacement value 

314 

315 Returns: 

316 Text with placeholder replaced 

317 

318 Raises: 

319 InvalidPlaceholderValueError: If value is invalid 

320 InvalidPlaceholderError: If placeholder pattern is invalid 

321 

322 """ 

323 # Validate the value first 

324 self.validate_placeholder_value(placeholder, value) 

325 

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 

331 

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. 

334 

335 Args: 

336 text: The text to replace placeholders in 

337 placeholder_values: Dictionary mapping placeholder names to values 

338 

339 Returns: 

340 Text with all placeholders replaced 

341 

342 Raises: 

343 InvalidPlaceholderError: If any placeholder pattern is invalid 

344 InvalidPlaceholderValueError: If any value is invalid 

345 

346 """ 

347 result = text 

348 

349 # Extract all placeholders from the text 

350 placeholders = self.extract_placeholders_from_text(text) 

351 

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) 

364 

365 return result 

366 

367 @staticmethod 

368 def get_placeholder_summary(placeholders: list[Placeholder]) -> dict[str, Any]: 

369 """Get a summary of placeholder information. 

370 

371 Args: 

372 placeholders: List of placeholders to summarize 

373 

374 Returns: 

375 Dictionary containing placeholder summary information 

376 

377 """ 

378 required_placeholders = [p for p in placeholders if p.required] 

379 optional_placeholders = [p for p in placeholders if not p.required] 

380 

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 }