Coverage for src/prosemark/templates/domain/entities/placeholder.py: 100%

98 statements  

« prev     ^ index     » next       coverage.py v7.8.0, created at 2025-09-30 23:09 +0000

1"""Placeholder entity and related value objects for template placeholders.""" 

2 

3from dataclasses import dataclass 

4 

5from prosemark.templates.domain.exceptions.template_exceptions import ( 

6 InvalidPlaceholderError, 

7 InvalidPlaceholderValueError, 

8) 

9from prosemark.templates.domain.values.placeholder_pattern import PlaceholderPattern 

10 

11 

12@dataclass(frozen=True) 

13class Placeholder: 

14 """Represents a placeholder within a template that requires user input. 

15 

16 A placeholder is defined by its pattern (e.g., "{{variable_name}}") and 

17 metadata about how it should be handled during template instantiation. 

18 """ 

19 

20 name: str 

21 pattern_obj: PlaceholderPattern 

22 required: bool = True 

23 default_value: str | None = None 

24 description: str | None = None 

25 

26 @property 

27 def pattern(self) -> str: 

28 """Get the string representation of the pattern.""" 

29 return self.pattern_obj.raw 

30 

31 def __post_init__(self) -> None: 

32 """Validate placeholder properties after initialization.""" 

33 # Validate the pattern 

34 if not self.pattern_obj.name: 

35 msg = f'Invalid placeholder pattern: {self.pattern}' 

36 raise InvalidPlaceholderError(msg, placeholder_pattern=self.pattern) 

37 

38 # Ensure name matches pattern 

39 if self.pattern_obj.name != self.name: 

40 msg = f"Placeholder name '{self.name}' does not match pattern '{self.pattern}'" 

41 raise InvalidPlaceholderError(msg, placeholder_pattern=self.pattern) 

42 

43 # Validate required/default_value consistency 

44 if self.required and self.default_value is not None: 

45 msg = f"Required placeholder '{self.name}' cannot have a default value" 

46 raise InvalidPlaceholderError(msg, placeholder_pattern=self.pattern) 

47 

48 if not self.required and self.default_value is None: 

49 # Optional placeholders should have defaults, but we'll allow None 

50 # and treat it as an empty string default 

51 object.__setattr__(self, 'default_value', '') 

52 

53 # Validate default value if provided 

54 if self.default_value is not None and not isinstance(self.default_value, str): 

55 msg = f"Default value for placeholder '{self.name}' must be a string" 

56 raise InvalidPlaceholderError(msg, placeholder_pattern=self.pattern) 

57 

58 @property 

59 def has_default(self) -> bool: 

60 """Check if this placeholder has a default value.""" 

61 return self.default_value is not None 

62 

63 @classmethod 

64 def from_pattern( 

65 cls, 

66 pattern: str, 

67 *, # keyword-only arguments for clarity and to prevent default behavior confusion 

68 required: bool = True, # Controls whether this placeholder must have a value 

69 default_value: str | None = None, # Optional default if not required 

70 description: str | None = None, # Optional human-readable explanation 

71 ) -> 'Placeholder': 

72 """Create a Placeholder from a pattern string. 

73 

74 Args: 

75 pattern: The placeholder pattern (e.g., "{{variable_name}}") 

76 required: Whether this placeholder requires user input 

77 default_value: Optional default value if not required 

78 description: Optional human-readable description 

79 

80 Returns: 

81 New Placeholder instance 

82 

83 Raises: 

84 InvalidPlaceholderError: If the pattern is invalid 

85 

86 """ 

87 pattern_obj = PlaceholderPattern(pattern) 

88 return cls( 

89 name=pattern_obj.name, 

90 pattern_obj=pattern_obj, 

91 required=required, 

92 default_value=default_value, 

93 description=description, 

94 ) 

95 

96 @classmethod 

97 def from_name( 

98 cls, 

99 name: str, 

100 *, # keyword-only arguments for clarity and to prevent default behavior confusion 

101 required: bool = True, # Controls whether this placeholder must have a value 

102 default_value: str | None = None, # Optional default if not required 

103 description: str | None = None, # Optional human-readable explanation 

104 ) -> 'Placeholder': 

105 """Create a Placeholder from a variable name. 

106 

107 Args: 

108 name: The variable name (without braces) 

109 required: Whether this placeholder requires user input 

110 default_value: Optional default value if not required 

111 description: Optional human-readable description 

112 

113 Returns: 

114 New Placeholder instance 

115 

116 Raises: 

117 InvalidPlaceholderError: If the name is invalid 

118 

119 """ 

120 pattern_obj = PlaceholderPattern.from_name(name) 

121 return cls( 

122 name=name, pattern_obj=pattern_obj, required=required, default_value=default_value, description=description 

123 ) 

124 

125 @classmethod 

126 def from_frontmatter(cls, name: str, frontmatter: dict[str, str], pattern_obj: PlaceholderPattern) -> 'Placeholder': 

127 """Create a Placeholder from frontmatter data. 

128 

129 Args: 

130 name: The placeholder name 

131 frontmatter: The frontmatter dictionary containing placeholder metadata 

132 pattern_obj: The PlaceholderPattern object for this placeholder 

133 

134 Returns: 

135 New Placeholder instance 

136 

137 """ 

138 # Check for default value in frontmatter 

139 default_key = f'{name}_default' 

140 default_value = frontmatter.get(default_key) 

141 

142 # Check for description in frontmatter 

143 description_key = f'{name}_description' 

144 description = frontmatter.get(description_key) 

145 

146 # Determine if required (no default value means required) 

147 required = default_value is None 

148 

149 return cls( 

150 name=name, pattern_obj=pattern_obj, required=required, default_value=default_value, description=description 

151 ) 

152 

153 def validate_value(self, value: str) -> bool: 

154 """Validate a potential value for this placeholder. 

155 

156 Args: 

157 value: The value to validate 

158 

159 Returns: 

160 True if the value is valid for this placeholder 

161 

162 Raises: 

163 InvalidPlaceholderValueError: If the value is invalid 

164 

165 """ 

166 if not isinstance(value, str): 

167 msg = f'Placeholder value must be a string, got {type(value).__name__}' 

168 raise InvalidPlaceholderValueError(msg, placeholder_name=self.name, provided_value=str(value)) 

169 

170 if self.required and not value.strip(): 

171 msg = f'Empty value for required placeholder: {self.name}' 

172 raise InvalidPlaceholderValueError(msg, placeholder_name=self.name, provided_value=value) 

173 

174 # Additional validation could be added here based on placeholder type 

175 # For now, any non-empty string is valid for required placeholders 

176 return True 

177 

178 def get_effective_value(self, provided_value: str | None = None) -> str: 

179 """Get the effective value for this placeholder. 

180 

181 Args: 

182 provided_value: Value provided by user (optional) 

183 

184 Returns: 

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

186 

187 Raises: 

188 InvalidPlaceholderValueError: If required placeholder has no value 

189 

190 """ 

191 if provided_value is not None: 

192 self.validate_value(provided_value) 

193 return provided_value 

194 

195 if self.required: 

196 msg = f'Missing value for required placeholder: {self.name}' 

197 raise InvalidPlaceholderValueError(msg, placeholder_name=self.name) 

198 

199 return self.default_value or '' 

200 

201 

202@dataclass(frozen=True) 

203class PlaceholderValue: 

204 """Represents a value provided for a placeholder during template instantiation.""" 

205 

206 placeholder_name: str 

207 value: str 

208 source: str = 'user_input' 

209 

210 def __post_init__(self) -> None: 

211 """Validate placeholder value properties.""" 

212 if not isinstance(self.placeholder_name, str) or not self.placeholder_name: 

213 raise InvalidPlaceholderValueError( 

214 'Placeholder name must be a non-empty string', placeholder_name=self.placeholder_name 

215 ) 

216 

217 if not isinstance(self.value, str): 

218 msg = f'Placeholder value must be a string, got {type(self.value).__name__}' 

219 raise InvalidPlaceholderValueError( 

220 msg, placeholder_name=self.placeholder_name, provided_value=str(self.value) 

221 ) 

222 

223 valid_sources = {'user_input', 'default', 'config', 'computed'} 

224 if self.source not in valid_sources: 

225 msg = f"Invalid value source '{self.source}'. Must be one of: {', '.join(valid_sources)}" 

226 raise InvalidPlaceholderValueError(msg, placeholder_name=self.placeholder_name) 

227 

228 @property 

229 def is_user_provided(self) -> bool: 

230 """Check if this value was provided by the user.""" 

231 return self.source == 'user_input' 

232 

233 @property 

234 def is_default_value(self) -> bool: 

235 """Check if this value is a default value.""" 

236 return self.source == 'default' 

237 

238 @property 

239 def is_empty(self) -> bool: 

240 """Check if this value is empty.""" 

241 return not self.value.strip() 

242 

243 @classmethod 

244 def from_user_input(cls, placeholder_name: str, value: str) -> 'PlaceholderValue': 

245 """Create a PlaceholderValue from user input. 

246 

247 Args: 

248 placeholder_name: Name of the placeholder 

249 value: Value provided by user 

250 

251 Returns: 

252 New PlaceholderValue instance 

253 

254 """ 

255 return cls(placeholder_name=placeholder_name, value=value, source='user_input') 

256 

257 @classmethod 

258 def from_default(cls, placeholder_name: str, value: str) -> 'PlaceholderValue': 

259 """Create a PlaceholderValue from a default value. 

260 

261 Args: 

262 placeholder_name: Name of the placeholder 

263 value: Default value 

264 

265 Returns: 

266 New PlaceholderValue instance 

267 

268 """ 

269 return cls(placeholder_name=placeholder_name, value=value, source='default') 

270 

271 @classmethod 

272 def from_config(cls, placeholder_name: str, value: str) -> 'PlaceholderValue': 

273 """Create a PlaceholderValue from configuration. 

274 

275 Args: 

276 placeholder_name: Name of the placeholder 

277 value: Value from configuration 

278 

279 Returns: 

280 New PlaceholderValue instance 

281 

282 """ 

283 return cls(placeholder_name=placeholder_name, value=value, source='config') 

284 

285 def matches_placeholder(self, placeholder: Placeholder) -> bool: 

286 """Check if this value matches the given placeholder. 

287 

288 Args: 

289 placeholder: Placeholder to check against 

290 

291 Returns: 

292 True if this value is for the given placeholder 

293 

294 """ 

295 return self.placeholder_name == placeholder.name