From cfb917ed8b005bd142716c62fcbd66029b3a5674 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A1t=C3=A9=20Szendr=C5=91?= Date: Mon, 2 Feb 2026 10:42:55 +0000 Subject: [PATCH 1/2] Handle template variable validation edge cases --- afp/validators.py | 11 ++++++++--- tests/test_validators.py | 25 ++++++++++++++++--------- 2 files changed, 24 insertions(+), 12 deletions(-) diff --git a/afp/validators.py b/afp/validators.py index 8b28844..4877a1c 100644 --- a/afp/validators.py +++ b/afp/validators.py @@ -188,14 +188,19 @@ def validate_outcome_space_conditions( f"EdgeCase[{i}]" for i in range(len(edge_case_conditions)) ] for condition, schema in zip(conditions, schemas): - for variable in re.findall(r"{(.+?)}", condition): + for variable in re.findall(r"{(.*?)}", condition): parts = variable.split(".") try: - reduce(getitem, parts, outcome_point_dict) - except KeyError: + referred_value = reduce(getitem, parts, outcome_point_dict) + except (TypeError, KeyError): raise ValueError( f"{schema}: condition: Invalid template variable '{variable}'" ) + if isinstance(referred_value, dict) or isinstance(referred_value, list): # type: ignore + raise ValueError( + f"{schema}: Template variable '{variable}' " + "should not refer to a nested object or list" + ) def validate_symbol(symbol: str, frequency: str, release_date: date) -> None: diff --git a/tests/test_validators.py b/tests/test_validators.py index 4a14b46..8ddc344 100644 --- a/tests/test_validators.py +++ b/tests/test_validators.py @@ -119,15 +119,26 @@ def test_validate_outcome_space_conditions__pass(): }, }, } - base_case_condition = "Reference to {a.b.c}" - edge_case_conditions = ["And to {a.b.d} as well"] validators.validate_outcome_space_conditions( - base_case_condition, edge_case_conditions, dct + "Reference to {a.b.c} should pass", + ["And to {a.b.d} as well", "So as having no template variable"], + dct, ) -def test_validate_outcome_space_conditions__error(): +@pytest.mark.parametrize( + "base_case_condition", + [ + "{a.b.c} and {a.b.e}", + "{c}", + "{a.b.c.d}", + "{a.b}", + "{}", + ], + ids=str, +) +def test_validate_outcome_space_conditions__error(base_case_condition): dct = { "a": { "b": { @@ -136,13 +147,9 @@ def test_validate_outcome_space_conditions__error(): }, }, } - base_case_condition = "Reference to {a.b.e}" - edge_case_conditions = [] with pytest.raises(ValueError): - validators.validate_outcome_space_conditions( - base_case_condition, edge_case_conditions, dct - ) + validators.validate_outcome_space_conditions(base_case_condition, [], dct) @pytest.mark.parametrize( From e42040debf44ddd2d8004b3b1b3befac228dc532 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A1t=C3=A9=20Szendr=C5=91?= Date: Mon, 2 Feb 2026 18:10:18 +0000 Subject: [PATCH 2/2] Include all BaseCase & EdgeCase fields in template variable validation --- afp/schemas.py | 15 ++++++++++++--- afp/validators.py | 25 ++++++++++--------------- tests/test_validators.py | 17 ++++++++++------- 3 files changed, 32 insertions(+), 25 deletions(-) diff --git a/afp/schemas.py b/afp/schemas.py index d2acd7b..260c8af 100644 --- a/afp/schemas.py +++ b/afp/schemas.py @@ -1,6 +1,7 @@ """AFP data structures.""" from decimal import Decimal +from itertools import chain from typing import Annotated, Any, ClassVar, Literal, Self from pydantic import AfterValidator, BeforeValidator, Field, model_validator @@ -321,9 +322,17 @@ def _cross_validate(self) -> Self: self.product.min_price, self.product.max_price, ) - validators.validate_outcome_space_conditions( - self.outcome_space.base_case.condition, - [case.condition for case in self.outcome_space.edge_cases], + validators.validate_outcome_space_template_variables( + [ + self.outcome_space.base_case.condition, + self.outcome_space.base_case.fsp_resolution, + ] + + list( + chain.from_iterable( + [edge_case.condition, edge_case.fsp_resolution] + for edge_case in self.outcome_space.edge_cases + ) + ), self.outcome_point.model_dump(), ) if isinstance(self.outcome_space, OutcomeSpaceTimeSeries) and isinstance( diff --git a/afp/validators.py b/afp/validators.py index 4877a1c..86a162c 100644 --- a/afp/validators.py +++ b/afp/validators.py @@ -3,7 +3,7 @@ from decimal import Decimal from functools import reduce from operator import getitem -from typing import Any +from typing import Any, Iterable import requests from binascii import Error @@ -178,27 +178,22 @@ def validate_oracle_fallback_fsp( ) -def validate_outcome_space_conditions( - base_case_condition: str, - edge_case_conditions: list[str], - outcome_point_dict: dict[Any, Any], +def validate_outcome_space_template_variables( + values: Iterable[str], outcome_point_dict: dict[Any, Any] ) -> None: - conditions = [base_case_condition] + edge_case_conditions - schemas = ["BaseCaseResolution"] + [ - f"EdgeCase[{i}]" for i in range(len(edge_case_conditions)) - ] - for condition, schema in zip(conditions, schemas): - for variable in re.findall(r"{(.*?)}", condition): - parts = variable.split(".") + for value in values: + for variable in re.findall(r"{(.*?)}", value): try: - referred_value = reduce(getitem, parts, outcome_point_dict) + referred_value = reduce( + getitem, variable.split("."), outcome_point_dict + ) except (TypeError, KeyError): raise ValueError( - f"{schema}: condition: Invalid template variable '{variable}'" + f"OutcomeSpace: Invalid template variable '{variable}'" ) if isinstance(referred_value, dict) or isinstance(referred_value, list): # type: ignore raise ValueError( - f"{schema}: Template variable '{variable}' " + f"OutcomeSpace: Template variable '{variable}' " "should not refer to a nested object or list" ) diff --git a/tests/test_validators.py b/tests/test_validators.py index 8ddc344..c6e2e8f 100644 --- a/tests/test_validators.py +++ b/tests/test_validators.py @@ -110,7 +110,7 @@ def test_validate_price_limits__error(): validators.validate_price_limits(Decimal("0.11"), Decimal("0.10")) -def test_validate_outcome_space_conditions__pass(): +def test_validate_outcome_space_template_variables__pass(): dct = { "a": { "b": { @@ -120,15 +120,18 @@ def test_validate_outcome_space_conditions__pass(): }, } - validators.validate_outcome_space_conditions( - "Reference to {a.b.c} should pass", - ["And to {a.b.d} as well", "So as having no template variable"], + validators.validate_outcome_space_template_variables( + [ + "Reference to {a.b.c} should pass", + "And to {a.b.d} as well", + "So as having no template variable", + ], dct, ) @pytest.mark.parametrize( - "base_case_condition", + "value", [ "{a.b.c} and {a.b.e}", "{c}", @@ -138,7 +141,7 @@ def test_validate_outcome_space_conditions__pass(): ], ids=str, ) -def test_validate_outcome_space_conditions__error(base_case_condition): +def test_validate_outcome_space_tempate_variables__error(value): dct = { "a": { "b": { @@ -149,7 +152,7 @@ def test_validate_outcome_space_conditions__error(base_case_condition): } with pytest.raises(ValueError): - validators.validate_outcome_space_conditions(base_case_condition, [], dct) + validators.validate_outcome_space_template_variables([value], dct) @pytest.mark.parametrize(