Skip to content

Commit c6052cc

Browse files
committed
feat: add key conflict validation for component request options
1 parent e38f914 commit c6052cc

File tree

2 files changed

+46
-2
lines changed

2 files changed

+46
-2
lines changed

airbyte_cdk/sources/declarative/requesters/paginators/default_paginator.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
)
2424
from airbyte_cdk.sources.declarative.requesters.request_path import RequestPath
2525
from airbyte_cdk.sources.types import Config, Record, StreamSlice, StreamState
26+
from airbyte_cdk.utils.mapping_helpers import combine_mappings, _validate_multiple_request_options
2627

2728

2829
@dataclass
@@ -112,6 +113,13 @@ def __post_init__(self, parameters: Mapping[str, Any]) -> None:
112113
)
113114
if isinstance(self.url_base, str):
114115
self.url_base = InterpolatedString(string=self.url_base, parameters=parameters)
116+
117+
if self.page_token_option and not isinstance(self.page_token_option, RequestPath):
118+
_validate_multiple_request_options(
119+
self.config,
120+
self.page_size_option,
121+
self.page_token_option,
122+
)
115123

116124
def get_initial_token(self) -> Optional[Any]:
117125
"""

airbyte_cdk/utils/mapping_helpers.py

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
import copy
77
from typing import Any, Dict, List, Mapping, Optional, Union
88

9+
from airbyte_cdk.sources.declarative.requesters.request_option import RequestOption, RequestOptionType
10+
from airbyte_cdk.sources.types import Config
911

1012
def _merge_mappings(
1113
target: Dict[str, Any],
@@ -33,13 +35,13 @@ def _merge_mappings(
3335
if isinstance(target_value, dict) and isinstance(source_value, dict):
3436
# Only body_json supports nested_structures
3537
if not allow_same_value_merge:
36-
raise ValueError(f"Duplicate keys found: {'.'.join(current_path)}")
38+
raise ValueError(f"Request body collision, duplicate keys detected at: {'.'.join(current_path)}. Please ensure that all keys in request are unique.")
3739
# If both are dictionaries, recursively merge them
3840
_merge_mappings(target_value, source_value, current_path, allow_same_value_merge)
3941

4042
elif not allow_same_value_merge or target_value != source_value:
4143
# If same key has different values, that's a conflict
42-
raise ValueError(f"Duplicate keys found: {'.'.join(current_path)}")
44+
raise ValueError(f"Request body collision, duplicate keys detected at: {'.'.join(current_path)}. Please ensure that all keys in request are unique.")
4345
else:
4446
# No conflict, just copy the value (using deepcopy for nested structures)
4547
target[key] = copy.deepcopy(source_value)
@@ -102,3 +104,37 @@ def combine_mappings(
102104
_merge_mappings(result, mapping, allow_same_value_merge=allow_same_value_merge)
103105

104106
return result
107+
108+
def _validate_multiple_request_options(
109+
config: Config,
110+
*request_options: Optional[RequestOption]
111+
) -> None:
112+
"""
113+
Validates that a component with multiple request options does not have conflicting paths.
114+
Uses dummy values for validation since actual values might not be available at init time.
115+
"""
116+
grouped_options: Dict[RequestOptionType, List[RequestOption]] = {}
117+
for option in request_options:
118+
if option:
119+
grouped_options.setdefault(option.inject_into, []).append(option)
120+
121+
for inject_type, options in grouped_options.items():
122+
if len(options) <= 1:
123+
continue
124+
125+
option_dicts: List[Optional[Union[Mapping[str, Any], str]]] = []
126+
for i, option in enumerate(options):
127+
option_dict: Dict[str, Any] = {}
128+
# Use indexed dummy values to ensure we catch conflicts
129+
option.inject_into_request(option_dict, f"dummy_value_{i}", config)
130+
option_dicts.append(option_dict)
131+
132+
try:
133+
combine_mappings(
134+
option_dicts,
135+
allow_same_value_merge=(inject_type == RequestOptionType.body_json)
136+
)
137+
except ValueError as e:
138+
print(e)
139+
raise ValueError(f"Conflict mapping request options: {e}") from e
140+

0 commit comments

Comments
 (0)