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
« 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."""
3from dataclasses import dataclass
5from prosemark.templates.domain.exceptions.template_exceptions import (
6 InvalidPlaceholderError,
7 InvalidPlaceholderValueError,
8)
9from prosemark.templates.domain.values.placeholder_pattern import PlaceholderPattern
12@dataclass(frozen=True)
13class Placeholder:
14 """Represents a placeholder within a template that requires user input.
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 """
20 name: str
21 pattern_obj: PlaceholderPattern
22 required: bool = True
23 default_value: str | None = None
24 description: str | None = None
26 @property
27 def pattern(self) -> str:
28 """Get the string representation of the pattern."""
29 return self.pattern_obj.raw
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)
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)
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)
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', '')
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)
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
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.
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
80 Returns:
81 New Placeholder instance
83 Raises:
84 InvalidPlaceholderError: If the pattern is invalid
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 )
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.
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
113 Returns:
114 New Placeholder instance
116 Raises:
117 InvalidPlaceholderError: If the name is invalid
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 )
125 @classmethod
126 def from_frontmatter(cls, name: str, frontmatter: dict[str, str], pattern_obj: PlaceholderPattern) -> 'Placeholder':
127 """Create a Placeholder from frontmatter data.
129 Args:
130 name: The placeholder name
131 frontmatter: The frontmatter dictionary containing placeholder metadata
132 pattern_obj: The PlaceholderPattern object for this placeholder
134 Returns:
135 New Placeholder instance
137 """
138 # Check for default value in frontmatter
139 default_key = f'{name}_default'
140 default_value = frontmatter.get(default_key)
142 # Check for description in frontmatter
143 description_key = f'{name}_description'
144 description = frontmatter.get(description_key)
146 # Determine if required (no default value means required)
147 required = default_value is None
149 return cls(
150 name=name, pattern_obj=pattern_obj, required=required, default_value=default_value, description=description
151 )
153 def validate_value(self, value: str) -> bool:
154 """Validate a potential value for this placeholder.
156 Args:
157 value: The value to validate
159 Returns:
160 True if the value is valid for this placeholder
162 Raises:
163 InvalidPlaceholderValueError: If the value is invalid
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))
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)
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
178 def get_effective_value(self, provided_value: str | None = None) -> str:
179 """Get the effective value for this placeholder.
181 Args:
182 provided_value: Value provided by user (optional)
184 Returns:
185 The value to use (provided value, default, or empty string)
187 Raises:
188 InvalidPlaceholderValueError: If required placeholder has no value
190 """
191 if provided_value is not None:
192 self.validate_value(provided_value)
193 return provided_value
195 if self.required:
196 msg = f'Missing value for required placeholder: {self.name}'
197 raise InvalidPlaceholderValueError(msg, placeholder_name=self.name)
199 return self.default_value or ''
202@dataclass(frozen=True)
203class PlaceholderValue:
204 """Represents a value provided for a placeholder during template instantiation."""
206 placeholder_name: str
207 value: str
208 source: str = 'user_input'
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 )
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 )
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)
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'
233 @property
234 def is_default_value(self) -> bool:
235 """Check if this value is a default value."""
236 return self.source == 'default'
238 @property
239 def is_empty(self) -> bool:
240 """Check if this value is empty."""
241 return not self.value.strip()
243 @classmethod
244 def from_user_input(cls, placeholder_name: str, value: str) -> 'PlaceholderValue':
245 """Create a PlaceholderValue from user input.
247 Args:
248 placeholder_name: Name of the placeholder
249 value: Value provided by user
251 Returns:
252 New PlaceholderValue instance
254 """
255 return cls(placeholder_name=placeholder_name, value=value, source='user_input')
257 @classmethod
258 def from_default(cls, placeholder_name: str, value: str) -> 'PlaceholderValue':
259 """Create a PlaceholderValue from a default value.
261 Args:
262 placeholder_name: Name of the placeholder
263 value: Default value
265 Returns:
266 New PlaceholderValue instance
268 """
269 return cls(placeholder_name=placeholder_name, value=value, source='default')
271 @classmethod
272 def from_config(cls, placeholder_name: str, value: str) -> 'PlaceholderValue':
273 """Create a PlaceholderValue from configuration.
275 Args:
276 placeholder_name: Name of the placeholder
277 value: Value from configuration
279 Returns:
280 New PlaceholderValue instance
282 """
283 return cls(placeholder_name=placeholder_name, value=value, source='config')
285 def matches_placeholder(self, placeholder: Placeholder) -> bool:
286 """Check if this value matches the given placeholder.
288 Args:
289 placeholder: Placeholder to check against
291 Returns:
292 True if this value is for the given placeholder
294 """
295 return self.placeholder_name == placeholder.name