@@ -90,15 +90,15 @@ def get_token_replacement(token: str, person_data: list[dict], present_attribute
9090 if TokenProcessor .should_replace_with_empty (parsed_token , present_attributes ):
9191 return ""
9292
93- # Check if this is a derived value (has a function like ADD_DAYS)
9493 if parsed_token .function_name :
9594 return TokenProcessor .get_derived_value (parsed_token , person_data , present_attributes , token )
9695
96+ TokenProcessor .validate_target_attribute (parsed_token , token )
97+
9798 found_attribute , key_to_replace = TokenProcessor .find_matching_attribute (parsed_token , person_data )
9899
99100 if not found_attribute or not key_to_replace :
100101 TokenProcessor .handle_token_not_found (parsed_token , token )
101- # handle_token_not_found always raises, but the type checker needs help
102102 msg = "Unreachable"
103103 raise RuntimeError (msg ) # pragma: no cover
104104
@@ -113,6 +113,11 @@ def get_derived_value(
113113 ) -> str :
114114 """Calculate a derived value using the registered handler.
115115
116+ For TARGET level tokens, validates that the condition is allowed before processing.
117+ If the vaccine type is not in person data, returns an empty string.
118+ For derived values, any target attribute name is allowed (e.g., NEXT_BOOKING_AVAILABLE)
119+ since it's just a placeholder that may be surfaced in the future.
120+
116121 Args:
117122 parsed_token: The parsed token containing function information
118123 person_data: List of person attribute dictionaries
@@ -136,20 +141,14 @@ def get_derived_value(
136141 message = f"Unknown function '{ function_name } ' in token '{ token } '."
137142 raise ValueError (message )
138143
139- # For TARGET level tokens, validate the condition is allowed
140144 if parsed_token .attribute_level == TARGET_ATTRIBUTE_LEVEL :
141145 is_allowed_condition = parsed_token .attribute_name in ALLOWED_CONDITIONS .__args__
142- is_allowed_target_attr = parsed_token .attribute_value in ALLOWED_TARGET_ATTRIBUTES
143146
144- # If condition is not allowed, raise error
145147 if not is_allowed_condition :
146148 TokenProcessor .handle_token_not_found (parsed_token , token )
147149
148- # If vaccine type is not in person data but is allowed, return empty
149150 if parsed_token .attribute_name not in present_attributes :
150- if is_allowed_target_attr :
151- return ""
152- TokenProcessor .handle_token_not_found (parsed_token , token )
151+ return ""
153152
154153 try :
155154 target_attribute = parsed_token .attribute_value or parsed_token .attribute_name
@@ -165,14 +164,14 @@ def get_derived_value(
165164 source_attribute = source_attribute ,
166165 function_args = parsed_token .function_args ,
167166 date_format = parsed_token .format ,
167+ attribute_level = parsed_token .attribute_level ,
168168 )
169169
170170 return registry .calculate (
171171 function_name = function_name ,
172172 context = context ,
173173 )
174174 except ValueError as e :
175- # Re-raise with more context
176175 message = f"Error calculating derived value for token '{ token } ': { e } "
177176 raise ValueError (message ) from e
178177
@@ -185,6 +184,26 @@ def should_replace_with_empty(parsed_token: ParsedToken, present_attributes: set
185184
186185 return all ([is_target_level , is_allowed_condition , is_allowed_target_attr , is_attr_not_present ])
187186
187+ @staticmethod
188+ def validate_target_attribute (parsed_token : ParsedToken , token : str ) -> None :
189+ """Validate that target attribute is allowed for non-derived tokens.
190+
191+ For regular (non-derived) tokens, only allow known target attributes.
192+ Derived values with functions can use any custom target attribute name.
193+
194+ Args:
195+ parsed_token: The parsed token to validate
196+ token: The original token string for error messages
197+
198+ Raises:
199+ ValueError: If the target attribute is not in ALLOWED_TARGET_ATTRIBUTES
200+ """
201+ if (
202+ parsed_token .attribute_level == TARGET_ATTRIBUTE_LEVEL
203+ and parsed_token .attribute_value not in ALLOWED_TARGET_ATTRIBUTES
204+ ):
205+ TokenProcessor .handle_token_not_found (parsed_token , token )
206+
188207 @staticmethod
189208 def find_matching_attribute (parsed_token : ParsedToken , person_data : list [dict ]) -> tuple [dict | None , str | None ]:
190209 attribute_level_map = {
0 commit comments