Skip to content

Commit a2f4508

Browse files
committed
add conditions to validators
1 parent f398ab6 commit a2f4508

File tree

7 files changed

+134
-9
lines changed

7 files changed

+134
-9
lines changed

airbyte_cdk/sources/declarative/declarative_component_schema.yaml

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4276,6 +4276,16 @@ definitions:
42764276
description: The condition that the specified config value will be evaluated against
42774277
anyOf:
42784278
- "$ref": "#/definitions/ValidateAdheresToSchema"
4279+
condition:
4280+
title: Condition
4281+
description: The condition which will determine if the validation strategy will be applied.
4282+
type: string
4283+
interpolation_context:
4284+
- config
4285+
default: ""
4286+
examples:
4287+
- "{{ config.get('dimensions', False) }}"
4288+
- "{{ config.get('custom_reports', [{}])[0].get('dimensions', [])|length > 0 }}"
42794289
PredicateValidator:
42804290
title: Predicate Validator
42814291
description: Validator that applies a validation strategy to a specified value.
@@ -4310,6 +4320,16 @@ definitions:
43104320
description: The validation strategy to apply to the value.
43114321
anyOf:
43124322
- "$ref": "#/definitions/ValidateAdheresToSchema"
4323+
condition:
4324+
title: Condition
4325+
description: The condition which will determine if the validation strategy will be applied.
4326+
type: string
4327+
interpolation_context:
4328+
- config
4329+
default: ""
4330+
examples:
4331+
- "{{ config.get('dimensions', False) }}"
4332+
- "{{ config.get('custom_reports', [{}])[0].get('dimensions', [])|length > 0 }}"
43134333
ValidateAdheresToSchema:
43144334
title: Validate Adheres To Schema
43154335
description: Validates that a user-provided schema adheres to a specified JSON schema.

airbyte_cdk/sources/declarative/models/declarative_component_schema.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
# Copyright (c) 2025 Airbyte, Inc., all rights reserved.
2+
13
# generated by datamodel-codegen:
24
# filename: declarative_component_schema.yaml
35

@@ -2008,6 +2010,15 @@ class DpathValidator(BaseModel):
20082010
description="The condition that the specified config value will be evaluated against",
20092011
title="Validation Strategy",
20102012
)
2013+
condition: Optional[str] = Field(
2014+
"",
2015+
description="The condition which will determine if the validation strategy will be applied.",
2016+
examples=[
2017+
"{{ config.get('dimensions', False) }}",
2018+
"{{ config.get('custom_reports', [{}])[0].get('dimensions', [])|length > 0 }}",
2019+
],
2020+
title="Condition",
2021+
)
20112022

20122023

20132024
class PredicateValidator(BaseModel):
@@ -2028,6 +2039,15 @@ class PredicateValidator(BaseModel):
20282039
description="The validation strategy to apply to the value.",
20292040
title="Validation Strategy",
20302041
)
2042+
condition: Optional[str] = Field(
2043+
"",
2044+
description="The condition which will determine if the validation strategy will be applied.",
2045+
examples=[
2046+
"{{ config.get('dimensions', False) }}",
2047+
"{{ config.get('custom_reports', [{}])[0].get('dimensions', [])|length > 0 }}",
2048+
],
2049+
title="Condition",
2050+
)
20312051

20322052

20332053
class ConfigAddFields(BaseModel):

airbyte_cdk/sources/declarative/parsers/model_to_component_factory.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -869,20 +869,26 @@ def create_config_remap_field(
869869

870870
def create_dpath_validator(self, model: DpathValidatorModel, config: Config) -> DpathValidator:
871871
strategy = self._create_component_from_model(model.validation_strategy, config)
872+
condition = model.condition or ""
872873

873874
return DpathValidator(
874875
field_path=model.field_path,
875876
strategy=strategy,
877+
config=config,
878+
condition=condition,
876879
)
877880

878881
def create_predicate_validator(
879882
self, model: PredicateValidatorModel, config: Config
880883
) -> PredicateValidator:
881884
strategy = self._create_component_from_model(model.validation_strategy, config)
885+
condition = model.condition or ""
882886

883887
return PredicateValidator(
884888
value=model.value,
885889
strategy=strategy,
890+
config=config,
891+
condition=condition,
886892
)
887893

888894
@staticmethod

airbyte_cdk/sources/declarative/validators/dpath_validator.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,11 @@
77

88
import dpath.util
99

10+
from airbyte_cdk.sources.declarative.interpolation.interpolated_boolean import InterpolatedBoolean
1011
from airbyte_cdk.sources.declarative.interpolation.interpolated_string import InterpolatedString
1112
from airbyte_cdk.sources.declarative.validators.validation_strategy import ValidationStrategy
1213
from airbyte_cdk.sources.declarative.validators.validator import Validator
14+
from airbyte_cdk.sources.types import Config
1315

1416

1517
@dataclass
@@ -21,8 +23,12 @@ class DpathValidator(Validator):
2123

2224
field_path: List[str]
2325
strategy: ValidationStrategy
26+
config: Config
27+
condition: str
2428

2529
def __post_init__(self) -> None:
30+
self._interpolated_condition = InterpolatedBoolean(condition=self.condition, parameters={})
31+
2632
self._field_path = [
2733
InterpolatedString.create(path, parameters={}) for path in self.field_path
2834
]
@@ -39,6 +45,9 @@ def validate(self, input_data: dict[str, Any]) -> None:
3945
:param input_data: Dictionary containing the data to validate
4046
:raises ValueError: If the path doesn't exist or validation fails
4147
"""
48+
if self.condition and not self._interpolated_condition.eval(self.config):
49+
return
50+
4251
path = [path.eval({}) for path in self._field_path]
4352

4453
if len(path) == 0:

airbyte_cdk/sources/declarative/validators/predicate_validator.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@
55
from dataclasses import dataclass
66
from typing import Any
77

8+
from airbyte_cdk.sources.declarative.interpolation.interpolated_boolean import InterpolatedBoolean
89
from airbyte_cdk.sources.declarative.validators.validation_strategy import ValidationStrategy
10+
from airbyte_cdk.sources.types import Config
911

1012

1113
@dataclass
@@ -16,11 +18,19 @@ class PredicateValidator:
1618

1719
value: Any
1820
strategy: ValidationStrategy
21+
config: Config
22+
condition: str
23+
24+
def __post_init__(self) -> None:
25+
self._interpolated_condition = InterpolatedBoolean(condition=self.condition, parameters={})
1926

2027
def validate(self) -> None:
2128
"""
2229
Applies the validation strategy to the value.
2330
2431
:raises ValueError: If validation fails
2532
"""
33+
if self.condition and not self._interpolated_condition.eval(self.config):
34+
return
35+
2636
self.strategy.validate(self.value)

unit_tests/sources/declarative/validators/test_dpath_validator.py

Lines changed: 51 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
# Copyright (c) 2025 Airbyte, Inc., all rights reserved.
2+
13
from unittest import TestCase
24

35
import pytest
@@ -23,7 +25,12 @@ def validate(self, value):
2325
class TestDpathValidator(TestCase):
2426
def test_given_valid_path_and_input_validate_is_successful(self):
2527
strategy = MockValidationStrategy()
26-
validator = DpathValidator(field_path=["user", "profile", "email"], strategy=strategy)
28+
validator = DpathValidator(
29+
field_path=["user", "profile", "email"],
30+
strategy=strategy,
31+
config={},
32+
condition="",
33+
)
2734

2835
test_data = {"user": {"profile": {"email": "[email protected]", "name": "Test User"}}}
2936

@@ -34,7 +41,12 @@ def test_given_valid_path_and_input_validate_is_successful(self):
3441

3542
def test_given_invalid_path_when_validate_then_raise_key_error(self):
3643
strategy = MockValidationStrategy()
37-
validator = DpathValidator(field_path=["user", "profile", "phone"], strategy=strategy)
44+
validator = DpathValidator(
45+
field_path=["user", "profile", "phone"],
46+
strategy=strategy,
47+
config={},
48+
condition="",
49+
)
3850

3951
test_data = {"user": {"profile": {"email": "[email protected]"}}}
4052

@@ -47,7 +59,12 @@ def test_given_invalid_path_when_validate_then_raise_key_error(self):
4759
def test_given_strategy_fails_when_validate_then_raise_value_error(self):
4860
error_message = "Invalid email format"
4961
strategy = MockValidationStrategy(should_fail=True, error_message=error_message)
50-
validator = DpathValidator(field_path=["user", "email"], strategy=strategy)
62+
validator = DpathValidator(
63+
field_path=["user", "email"],
64+
strategy=strategy,
65+
config={},
66+
condition="",
67+
)
5168

5269
test_data = {"user": {"email": "invalid-email"}}
5370

@@ -59,15 +76,25 @@ def test_given_strategy_fails_when_validate_then_raise_value_error(self):
5976

6077
def test_given_empty_path_list_when_validate_then_validate_raises_exception(self):
6178
strategy = MockValidationStrategy()
62-
validator = DpathValidator(field_path=[], strategy=strategy)
79+
validator = DpathValidator(
80+
field_path=[],
81+
strategy=strategy,
82+
config={},
83+
condition="",
84+
)
6385
test_data = {"key": "value"}
6486

6587
with pytest.raises(ValueError):
6688
validator.validate(test_data)
6789

6890
def test_given_empty_input_data_when_validate_then_validate_raises_exception(self):
6991
strategy = MockValidationStrategy()
70-
validator = DpathValidator(field_path=["data", "field"], strategy=strategy)
92+
validator = DpathValidator(
93+
field_path=["data", "field"],
94+
strategy=strategy,
95+
config={},
96+
condition="",
97+
)
7198

7299
test_data = {}
73100

@@ -76,7 +103,12 @@ def test_given_empty_input_data_when_validate_then_validate_raises_exception(sel
76103

77104
def test_path_with_wildcard_when_validate_then_validate_is_successful(self):
78105
strategy = MockValidationStrategy()
79-
validator = DpathValidator(field_path=["users", "*", "email"], strategy=strategy)
106+
validator = DpathValidator(
107+
field_path=["users", "*", "email"],
108+
strategy=strategy,
109+
config={},
110+
condition="",
111+
)
80112

81113
test_data = {
82114
"users": {
@@ -90,3 +122,16 @@ def test_path_with_wildcard_when_validate_then_validate_is_successful(self):
90122
assert strategy.validate_called
91123
assert strategy.validated_value in ["[email protected]", "[email protected]"]
92124
self.assertIn(strategy.validated_value, ["[email protected]", "[email protected]"])
125+
126+
def test_given_condition_is_false_when_validate_then_validate_is_not_called(self):
127+
strategy = MockValidationStrategy()
128+
validator = DpathValidator(
129+
field_path=["user", "profile", "email"],
130+
strategy=strategy,
131+
config={"test": "test"},
132+
condition="{{ not config.get('test') }}",
133+
)
134+
135+
validator.validate({"user": {"profile": {"email": "[email protected]"}}})
136+
137+
assert not strategy.validate_called

unit_tests/sources/declarative/validators/test_predicate_validator.py

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
# Copyright (c) 2025 Airbyte, Inc., all rights reserved.
2+
13
from unittest import TestCase
24

35
import pytest
@@ -24,7 +26,7 @@ class TestPredicateValidator(TestCase):
2426
def test_given_valid_input_validate_is_successful(self):
2527
strategy = MockValidationStrategy()
2628
test_value = "[email protected]"
27-
validator = PredicateValidator(value=test_value, strategy=strategy)
29+
validator = PredicateValidator(value=test_value, strategy=strategy, config={}, condition="")
2830

2931
validator.validate()
3032

@@ -35,7 +37,7 @@ def test_given_invalid_input_when_validate_then_raise_value_error(self):
3537
error_message = "Invalid email format"
3638
strategy = MockValidationStrategy(should_fail=True, error_message=error_message)
3739
test_value = "invalid-email"
38-
validator = PredicateValidator(value=test_value, strategy=strategy)
40+
validator = PredicateValidator(value=test_value, strategy=strategy, config={}, condition="")
3941

4042
with pytest.raises(ValueError) as context:
4143
validator.validate()
@@ -47,9 +49,22 @@ def test_given_invalid_input_when_validate_then_raise_value_error(self):
4749
def test_given_complex_object_when_validate_then_successful(self):
4850
strategy = MockValidationStrategy()
4951
test_value = {"user": {"email": "[email protected]", "name": "Test User"}}
50-
validator = PredicateValidator(value=test_value, strategy=strategy)
52+
validator = PredicateValidator(value=test_value, strategy=strategy, config={}, condition="")
5153

5254
validator.validate()
5355

5456
assert strategy.validate_called
5557
assert strategy.validated_value == test_value
58+
59+
def test_given_condition_is_false_when_validate_then_validate_is_not_called(self):
60+
strategy = MockValidationStrategy()
61+
validator = PredicateValidator(
62+
value="test",
63+
strategy=strategy,
64+
config={"test": "test"},
65+
condition="{{ not config.get('test') }}",
66+
)
67+
68+
validator.validate()
69+
70+
assert not strategy.validate_called

0 commit comments

Comments
 (0)