Skip to content

Commit 8f77245

Browse files
refactor: Enhance parameter field mapping logic for improved validation and support
- Refined the `_create_parameter_field_mapping` method to support exact matching and plural-to-singular mapping, improving API design clarity. - Updated error handling to provide clearer messages for unmatched parameters and added rules to prevent ambiguity in parameter names. - Enhanced unit tests to cover various mapping scenarios, including order independence, validation, and edge cases, ensuring robust functionality.
1 parent dc27eba commit 8f77245

File tree

2 files changed

+74
-88
lines changed

2 files changed

+74
-88
lines changed

py_spring_model/py_spring_model_rest/service/curd_repository_implementation_service/crud_repository_implementation_service.py

Lines changed: 37 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -113,26 +113,23 @@ def _implemenmt_query(self, repository_type: Type[CrudRepository]) -> None:
113113
def _create_parameter_field_mapping(self, param_names: list[str], field_names: list[str]) -> dict[str, str]:
114114
"""
115115
Create a mapping between parameter names and field names.
116-
This allows for more readable API design where parameter names can be plural
117-
while still mapping to singular field names.
118116
119-
The method validates that parameter names correspond to field names and provides
120-
clear error messages for mismatches.
117+
Supports exact matching and common plural-to-singular mapping:
118+
- Exact match: 'name' -> 'name'
119+
- Plural to singular: 'names' -> 'name', 'ages' -> 'age'
120+
121+
Rules:
122+
1. Parameter names must match field names exactly, OR
123+
2. Parameter names can be plural forms of field names (add 's')
124+
3. No ambiguity allowed (e.g., can't have both 'name' and 'names' as fields)
121125
122126
Examples:
123-
- param_names: ['names'], field_names: ['name'] -> {'names': 'name'}
124-
- param_names: ['ages'], field_names: ['age'] -> {'ages': 'age'}
125127
- param_names: ['name', 'age'], field_names: ['name', 'age'] -> {'name': 'name', 'age': 'age'}
128+
- param_names: ['names', 'ages'], field_names: ['name', 'age'] -> {'names': 'name', 'ages': 'age'}
129+
- param_names: ['name', 'ages'], field_names: ['name', 'age'] -> {'name': 'name', 'ages': 'age'}
126130
"""
127-
if len(param_names) != len(field_names):
128-
raise ValueError(
129-
f"Parameter count mismatch. Expected {len(field_names)} parameters for fields {field_names}, "
130-
f"but got {len(param_names)} parameters: {param_names}"
131-
)
132-
133131
mapping = {}
134132
unmatched_params = []
135-
unmatched_fields = []
136133

137134
# Create a set of field names for efficient lookup
138135
field_set = set(field_names)
@@ -143,69 +140,53 @@ def _create_parameter_field_mapping(self, param_names: list[str], field_names: l
143140
mapping[param_name] = param_name
144141
continue
145142

146-
# Try singular/plural variations
147-
singular_match = None
148-
plural_match = None
149-
150-
# Check if param_name is plural and field_name is singular
143+
# Try plural-to-singular mapping (only if param ends with 's' and is longer than 1 char)
151144
if param_name.endswith('s') and len(param_name) > 1:
152-
singular_candidate = param_name[:-1]
145+
# Handle special cases for words ending with 's'
146+
if param_name.endswith('ies'):
147+
# words ending with 'ies' -> 'y' (e.g., 'categories' -> 'category')
148+
singular_candidate = param_name[:-3] + 'y'
149+
elif param_name.endswith('ses'):
150+
# words ending with 'ses' -> 's' (e.g., 'statuses' -> 'status')
151+
singular_candidate = param_name[:-2]
152+
else:
153+
# regular plural: remove 's'
154+
singular_candidate = param_name[:-1]
155+
153156
if singular_candidate in field_set:
154-
singular_match = singular_candidate
155-
156-
# Check if param_name is singular and field_name is plural
157-
elif not param_name.endswith('s'):
158-
plural_candidate = param_name + 's'
159-
if plural_candidate in field_set:
160-
plural_match = plural_candidate
157+
# Check for ambiguity: make sure we don't have both singular and plural forms as fields
158+
plural_candidate = singular_candidate + 's'
159+
if plural_candidate not in field_set:
160+
mapping[param_name] = singular_candidate
161+
continue
161162

162-
# Use the best match found
163-
if singular_match:
164-
mapping[param_name] = singular_match
165-
elif plural_match:
166-
mapping[param_name] = plural_match
167-
else:
168-
unmatched_params.append(param_name)
163+
# No match found
164+
unmatched_params.append(param_name)
169165

170-
# Check for unmatched fields
171-
mapped_fields = set(mapping.values())
172-
for field_name in field_names:
173-
if field_name not in mapped_fields:
174-
unmatched_fields.append(field_name)
175-
176-
# Report any mismatches
177-
if unmatched_params or unmatched_fields:
166+
# Check if all parameters were mapped
167+
if unmatched_params:
178168
error_msg = "Parameter to field mapping failed:\n"
179-
if unmatched_params:
180-
error_msg += f" Unmatched parameters: {unmatched_params}\n"
181-
if unmatched_fields:
182-
error_msg += f" Unmatched fields: {unmatched_fields}\n"
169+
error_msg += f" Unmatched parameters: {unmatched_params}\n"
183170
error_msg += f" Available fields: {field_names}\n"
184-
error_msg += f" Provided parameters: {param_names}"
171+
error_msg += f" Provided parameters: {param_names}\n"
172+
error_msg += " Note: Parameters must exactly match field names or be plural forms (add 's')"
185173
raise ValueError(error_msg)
186174

187175
return mapping
188176

189177
def create_implementation_wrapper(self, query: _Query, model_type: Type[PySpringModel], original_func_annotations: dict[str, Any], param_to_field_mapping: dict[str, str]) -> Callable[..., Any]:
190178
def wrapper(*args, **kwargs) -> Any:
191179
if len(query.required_fields) > 0:
192-
# Map parameter names to field names
180+
# Simple mapping: parameter names must match field names exactly
193181
field_kwargs = {}
194182
for param_name, value in kwargs.items():
195183
if param_name in param_to_field_mapping:
196184
field_name = param_to_field_mapping[param_name]
197185
field_kwargs[field_name] = value
198186
else:
199-
# Fallback: use parameter name as field name
200-
field_kwargs[param_name] = value
201-
202-
# Check if all required fields are present
203-
if set(query.required_fields) != set(field_kwargs.keys()):
204-
raise ValueError(
205-
f"Invalid number of keyword arguments. Expected {query.required_fields}, received {list(kwargs.keys())}."
206-
)
187+
raise ValueError(f"Unknown parameter '{param_name}'. Expected parameters: {list(param_to_field_mapping.keys())}")
207188

208-
# Execute the query with mapped field names
189+
# Execute the query
209190
sql_statement = self._get_sql_statement(model_type, query, field_kwargs)
210191
result = self._session_execute(sql_statement, query.is_one_result)
211192
logger.info(f"Executing query with params: {kwargs}")

tests/test_crud_repository_implementation_service.py

Lines changed: 37 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -174,64 +174,69 @@ def test_in_operator_empty_list_returns_no_results(self, user_repository: UserRe
174174
results = user_repository.find_all_by_status_in(status=[])
175175
assert len(results) == 0
176176

177-
def test_parameter_field_mapping_order_independence(self, implementation_service: CrudRepositoryImplementationService):
178-
"""Test that parameter field mapping works regardless of parameter order"""
179-
# Test case 1: Parameters in different order than fields
180-
param_names = ['age', 'name'] # Different order
181-
field_names = ['name', 'age'] # From method name
177+
def test_parameter_field_mapping_simple(self, implementation_service: CrudRepositoryImplementationService):
178+
"""Test that parameter field mapping works with exact name matching and plural support"""
179+
# Test case 1: Exact match
180+
param_names = ['name', 'age']
181+
field_names = ['name', 'age']
182+
mapping = implementation_service._create_parameter_field_mapping(param_names, field_names)
183+
assert mapping == {'name': 'name', 'age': 'age'}
184+
185+
# Test case 2: Different order but same names
186+
param_names = ['age', 'name']
187+
field_names = ['name', 'age']
182188
mapping = implementation_service._create_parameter_field_mapping(param_names, field_names)
183189
assert mapping == {'age': 'age', 'name': 'name'}
184190

185-
# Test case 2: Plural parameters mapping to singular fields
191+
# Test case 3: Plural parameters mapping to singular fields
186192
param_names = ['names', 'ages']
187193
field_names = ['name', 'age']
188194
mapping = implementation_service._create_parameter_field_mapping(param_names, field_names)
189195
assert mapping == {'names': 'name', 'ages': 'age'}
190196

191-
# Test case 3: Mixed singular and plural
197+
# Test case 4: Mixed singular and plural
192198
param_names = ['name', 'ages']
193199
field_names = ['name', 'age']
194200
mapping = implementation_service._create_parameter_field_mapping(param_names, field_names)
195201
assert mapping == {'name': 'name', 'ages': 'age'}
196202

197203
def test_parameter_field_mapping_validation(self, implementation_service: CrudRepositoryImplementationService):
198204
"""Test that parameter field mapping properly validates and reports errors"""
199-
# Test case 1: Parameter count mismatch
200-
with pytest.raises(ValueError, match="Parameter count mismatch"):
201-
implementation_service._create_parameter_field_mapping(['name'], ['name', 'age'])
202-
203-
# Test case 2: Unmatched parameters
205+
# Test case 1: Unmatched parameters (no exact match or plural form)
204206
with pytest.raises(ValueError, match="Unmatched parameters"):
205-
implementation_service._create_parameter_field_mapping(['invalid_param'], ['name'])
207+
implementation_service._create_parameter_field_mapping(['username'], ['name'])
206208

207-
# Test case 3: Unmatched fields (when we have more fields than parameters)
208-
with pytest.raises(ValueError, match="Parameter count mismatch"):
209-
implementation_service._create_parameter_field_mapping(['name'], ['name', 'age'])
209+
# Test case 2: Parameter that can't be mapped to plural (ambiguous case)
210+
# This should work because 'statuses' exists as an exact match
211+
mapping = implementation_service._create_parameter_field_mapping(['statuses'], ['status', 'statuses'])
212+
assert mapping == {'statuses': 'statuses'}
210213

211-
# Test case 4: Unmatched fields (when we have more parameters than fields)
212-
with pytest.raises(ValueError, match="Parameter count mismatch"):
213-
implementation_service._create_parameter_field_mapping(['name', 'age'], ['name'])
214-
215-
# Test case 5: Ambiguous plural mapping (same count but no match)
214+
# Test case 3: Single character ending with 's' (should not be treated as plural)
216215
with pytest.raises(ValueError, match="Unmatched parameters"):
217-
implementation_service._create_parameter_field_mapping(['statuses'], ['status'])
216+
implementation_service._create_parameter_field_mapping(['s'], ['name'])
218217

219218
def test_parameter_field_mapping_edge_cases(self, implementation_service: CrudRepositoryImplementationService):
220219
"""Test edge cases in parameter field mapping"""
221-
# Test case 1: Single character parameter ending with 's'
222-
param_names = ['s'] # This should not be treated as plural
223-
field_names = ['s']
220+
# Test case 1: Single parameter
221+
param_names = ['name']
222+
field_names = ['name']
223+
mapping = implementation_service._create_parameter_field_mapping(param_names, field_names)
224+
assert mapping == {'name': 'name'}
225+
226+
# Test case 2: Empty lists
227+
param_names = []
228+
field_names = []
224229
mapping = implementation_service._create_parameter_field_mapping(param_names, field_names)
225-
assert mapping == {'s': 's'}
230+
assert mapping == {}
226231

227-
# Test case 2: Parameter ending with 's' but not plural
228-
param_names = ['status'] # 'status' is already singular
232+
# Test case 3: Parameter ending with 's' but not plural (like 'status')
233+
param_names = ['status']
229234
field_names = ['status']
230235
mapping = implementation_service._create_parameter_field_mapping(param_names, field_names)
231236
assert mapping == {'status': 'status'}
232237

233-
# Test case 3: Exact matches take precedence
234-
param_names = ['names', 'name']
235-
field_names = ['name', 'names']
238+
# Test case 4: Plural of word ending with 's' (like 'statuses')
239+
param_names = ['statuses']
240+
field_names = ['status']
236241
mapping = implementation_service._create_parameter_field_mapping(param_names, field_names)
237-
assert mapping == {'names': 'names', 'name': 'name'}
242+
assert mapping == {'statuses': 'status'}

0 commit comments

Comments
 (0)