Skip to content

Commit b221fe4

Browse files
committed
feat: Refactor validation error handling by replacing enum with constants and improving validation functions
1 parent 8e1c08e commit b221fe4

8 files changed

Lines changed: 655 additions & 109 deletions

File tree

pyproject.toml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,11 @@ ignore = ["venv", "__pycache__", ".git", "tests"]
4646
persistent = true
4747

4848
[tool.pylint.messages_control]
49-
disable = ["broad-exception-caught", "too-many-return-statements"]
49+
disable = [
50+
"broad-exception-caught",
51+
"too-many-return-statements",
52+
"too-many-branches",
53+
]
5054

5155
[tool.pylint.format]
5256
max-line-length = 80

tests/test_to_dict.py

Lines changed: 383 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,383 @@
1+
"""Tests for the to_dict utility function."""
2+
3+
import pytest
4+
5+
from tomlval.errors.error_codes import (
6+
INVALID_ARRAY_ELEMENT,
7+
INVALID_TYPE,
8+
MISSING_KEY,
9+
REGEX_MISMATCH,
10+
VALIDATION_FAILURE,
11+
)
12+
from tomlval.utils.to_dict import _parse_key_path, _set_nested_value, to_dict
13+
14+
15+
class TestToDict:
16+
"""Test cases for the to_dict function."""
17+
18+
def test_empty_dict(self):
19+
"""Test with empty input dictionary."""
20+
result = to_dict({})
21+
assert not result
22+
23+
def test_simple_keys(self):
24+
"""Test with simple flat keys (no nesting)."""
25+
flat_errors = {
26+
"name": INVALID_TYPE,
27+
"age": MISSING_KEY,
28+
"active": VALIDATION_FAILURE,
29+
}
30+
expected = {
31+
"name": INVALID_TYPE,
32+
"age": MISSING_KEY,
33+
"active": VALIDATION_FAILURE,
34+
}
35+
result = to_dict(flat_errors)
36+
assert result == expected
37+
38+
def test_nested_keys(self):
39+
"""Test with dot notation nested keys."""
40+
flat_errors = {
41+
"user.name": INVALID_TYPE,
42+
"user.email": REGEX_MISMATCH,
43+
"config.debug": INVALID_TYPE,
44+
}
45+
expected = {
46+
"user": {"name": INVALID_TYPE, "email": REGEX_MISMATCH},
47+
"config": {"debug": INVALID_TYPE},
48+
}
49+
result = to_dict(flat_errors)
50+
assert result == expected
51+
52+
def test_deep_nesting(self):
53+
"""Test with deeply nested keys."""
54+
flat_errors = {
55+
"app.database.connection.host": MISSING_KEY,
56+
"app.database.connection.port": INVALID_TYPE,
57+
"app.cache.redis.timeout": VALIDATION_FAILURE,
58+
}
59+
expected = {
60+
"app": {
61+
"database": {
62+
"connection": {
63+
"host": MISSING_KEY,
64+
"port": INVALID_TYPE,
65+
}
66+
},
67+
"cache": {"redis": {"timeout": VALIDATION_FAILURE}},
68+
}
69+
}
70+
result = to_dict(flat_errors)
71+
assert result == expected
72+
73+
def test_array_indices(self):
74+
"""Test with array index notation."""
75+
flat_errors = {
76+
"items[0].name": INVALID_TYPE,
77+
"items[0].price": MISSING_KEY,
78+
"items[2].category": VALIDATION_FAILURE,
79+
}
80+
expected = {
81+
"items": [
82+
{"name": INVALID_TYPE, "price": MISSING_KEY},
83+
None,
84+
{"category": VALIDATION_FAILURE},
85+
]
86+
}
87+
result = to_dict(flat_errors)
88+
assert result == expected
89+
90+
def test_array_element_errors(self):
91+
"""Test with errors on array elements themselves."""
92+
flat_errors = {
93+
"tasks[0]": INVALID_ARRAY_ELEMENT,
94+
"tasks[2]": INVALID_TYPE,
95+
"data[1].value": MISSING_KEY,
96+
}
97+
expected = {
98+
"tasks": [INVALID_ARRAY_ELEMENT, None, INVALID_TYPE],
99+
"data": [None, {"value": MISSING_KEY}],
100+
}
101+
result = to_dict(flat_errors)
102+
assert result == expected
103+
104+
def test_mixed_structure(self):
105+
"""Test with mixed dictionary and array structures."""
106+
flat_errors = {
107+
"config.servers[0].host": INVALID_TYPE,
108+
"config.servers[0].port": VALIDATION_FAILURE,
109+
"config.servers[1].ssl.enabled": MISSING_KEY,
110+
"config.debug": INVALID_TYPE,
111+
"users[0].permissions[1]": INVALID_ARRAY_ELEMENT,
112+
}
113+
expected = {
114+
"config": {
115+
"servers": [
116+
{"host": INVALID_TYPE, "port": VALIDATION_FAILURE},
117+
{"ssl": {"enabled": MISSING_KEY}},
118+
],
119+
"debug": INVALID_TYPE,
120+
},
121+
"users": [{"permissions": [None, INVALID_ARRAY_ELEMENT]}],
122+
}
123+
result = to_dict(flat_errors)
124+
assert result == expected
125+
126+
def test_sparse_arrays(self):
127+
"""Test with sparse array indices."""
128+
flat_errors = {
129+
"items[5].name": INVALID_TYPE,
130+
"items[10]": INVALID_ARRAY_ELEMENT,
131+
}
132+
expected = {
133+
"items": [
134+
None,
135+
None,
136+
None,
137+
None,
138+
None,
139+
{"name": INVALID_TYPE},
140+
None,
141+
None,
142+
None,
143+
None,
144+
INVALID_ARRAY_ELEMENT,
145+
]
146+
}
147+
result = to_dict(flat_errors)
148+
assert result == expected
149+
150+
def test_complex_real_world_example(self):
151+
"""Test with a complex real-world example."""
152+
flat_errors = {
153+
"startup_tasks[1].args": INVALID_TYPE,
154+
"startup_tasks[1].timeout": INVALID_TYPE,
155+
"database.connection.pool_size": VALIDATION_FAILURE,
156+
"api.cors.allowed_origins[0]": REGEX_MISMATCH,
157+
"plugins.analytics.config.tracking_id": MISSING_KEY,
158+
"scheduled_tasks[0].enabled": INVALID_TYPE,
159+
}
160+
expected = {
161+
"startup_tasks": [
162+
None,
163+
{"args": INVALID_TYPE, "timeout": INVALID_TYPE},
164+
],
165+
"database": {"connection": {"pool_size": VALIDATION_FAILURE}},
166+
"api": {"cors": {"allowed_origins": [REGEX_MISMATCH]}},
167+
"plugins": {"analytics": {"config": {"tracking_id": MISSING_KEY}}},
168+
"scheduled_tasks": [{"enabled": INVALID_TYPE}],
169+
}
170+
result = to_dict(flat_errors)
171+
assert result == expected
172+
173+
174+
class TestParseKeyPath:
175+
"""Test cases for the _parse_key_path function."""
176+
177+
def test_simple_key(self):
178+
"""Test parsing a simple key."""
179+
result = _parse_key_path("name")
180+
expected = [{"type": "key", "value": "name"}]
181+
assert result == expected
182+
183+
def test_nested_keys(self):
184+
"""Test parsing nested keys with dots."""
185+
result = _parse_key_path("user.profile.name")
186+
expected = [
187+
{"type": "key", "value": "user"},
188+
{"type": "key", "value": "profile"},
189+
{"type": "key", "value": "name"},
190+
]
191+
assert result == expected
192+
193+
def test_array_index(self):
194+
"""Test parsing array index notation."""
195+
result = _parse_key_path("items[0]")
196+
expected = [
197+
{"type": "key", "value": "items"},
198+
{"type": "index", "value": 0},
199+
]
200+
assert result == expected
201+
202+
def test_array_with_nested_key(self):
203+
"""Test parsing array index with nested key."""
204+
result = _parse_key_path("items[0].name")
205+
expected = [
206+
{"type": "key", "value": "items"},
207+
{"type": "index", "value": 0},
208+
{"type": "key", "value": "name"},
209+
]
210+
assert result == expected
211+
212+
def test_multiple_array_indices(self):
213+
"""Test parsing multiple array indices."""
214+
result = _parse_key_path("matrix[1][2]")
215+
expected = [
216+
{"type": "key", "value": "matrix"},
217+
{"type": "index", "value": 1},
218+
{"type": "index", "value": 2},
219+
]
220+
assert result == expected
221+
222+
def test_complex_path(self):
223+
"""Test parsing a complex path with mixed notation."""
224+
result = _parse_key_path("config.servers[0].ssl.certs[1].path")
225+
expected = [
226+
{"type": "key", "value": "config"},
227+
{"type": "key", "value": "servers"},
228+
{"type": "index", "value": 0},
229+
{"type": "key", "value": "ssl"},
230+
{"type": "key", "value": "certs"},
231+
{"type": "index", "value": 1},
232+
{"type": "key", "value": "path"},
233+
]
234+
assert result == expected
235+
236+
def test_invalid_array_index(self):
237+
"""Test parsing invalid array index (non-numeric)."""
238+
result = _parse_key_path("items[invalid]")
239+
expected = [
240+
{"type": "key", "value": "items"},
241+
{"type": "key", "value": "[invalid]"},
242+
]
243+
assert result == expected
244+
245+
def test_unclosed_bracket(self):
246+
"""Test parsing unclosed bracket."""
247+
result = _parse_key_path("items[0")
248+
expected = [
249+
{"type": "key", "value": "items"},
250+
{"type": "key", "value": "[0"},
251+
]
252+
assert result == expected
253+
254+
def test_empty_brackets(self):
255+
"""Test parsing empty brackets."""
256+
result = _parse_key_path("items[]")
257+
expected = [
258+
{"type": "key", "value": "items"},
259+
{"type": "key", "value": "[]"},
260+
]
261+
assert result == expected
262+
263+
264+
class TestSetNestedValue:
265+
"""Test cases for the _set_nested_value function."""
266+
267+
def test_set_simple_key(self):
268+
"""Test setting a simple key value."""
269+
nested = {}
270+
_set_nested_value(nested, "name", INVALID_TYPE)
271+
assert nested == {"name": INVALID_TYPE}
272+
273+
def test_set_nested_key(self):
274+
"""Test setting a nested key value."""
275+
nested = {}
276+
_set_nested_value(nested, "user.name", INVALID_TYPE)
277+
assert nested == {"user": {"name": INVALID_TYPE}}
278+
279+
def test_set_array_element(self):
280+
"""Test setting an array element."""
281+
nested = {}
282+
_set_nested_value(nested, "items[0]", INVALID_ARRAY_ELEMENT)
283+
assert nested == {"items": [INVALID_ARRAY_ELEMENT]}
284+
285+
def test_set_nested_array_element(self):
286+
"""Test setting a nested array element property."""
287+
nested = {}
288+
_set_nested_value(nested, "items[0].name", INVALID_TYPE)
289+
assert nested == {"items": [{"name": INVALID_TYPE}]}
290+
291+
def test_type_error_on_non_list_index_access(self):
292+
"""Test TypeError when trying to access index on non-list."""
293+
nested = {"items": "not-a-list"}
294+
with pytest.raises(
295+
TypeError, match="Expected list but got.*at index access"
296+
):
297+
_set_nested_value(nested, "items[0]", "value")
298+
299+
def test_extend_existing_structure(self):
300+
"""Test extending existing nested structure."""
301+
nested = {"user": {"name": "existing"}}
302+
_set_nested_value(nested, "user.email", REGEX_MISMATCH)
303+
assert nested == {"user": {"name": "existing", "email": REGEX_MISMATCH}}
304+
305+
def test_create_sparse_array(self):
306+
"""Test creating sparse array with gaps."""
307+
nested = {}
308+
_set_nested_value(nested, "items[5].name", INVALID_TYPE)
309+
expected = {
310+
"items": [None, None, None, None, None, {"name": INVALID_TYPE}]
311+
}
312+
assert nested == expected
313+
314+
def test_mixed_operations(self):
315+
"""Test multiple operations on same structure."""
316+
nested = {}
317+
_set_nested_value(nested, "config.debug", INVALID_TYPE)
318+
_set_nested_value(nested, "config.servers[0].host", MISSING_KEY)
319+
_set_nested_value(nested, "config.servers[1]", INVALID_ARRAY_ELEMENT)
320+
321+
expected = {
322+
"config": {
323+
"debug": INVALID_TYPE,
324+
"servers": [{"host": MISSING_KEY}, INVALID_ARRAY_ELEMENT],
325+
}
326+
}
327+
assert nested == expected
328+
329+
330+
class TestEdgeCases:
331+
"""Test edge cases and error conditions."""
332+
333+
def test_overwrite_existing_values(self):
334+
"""Test that existing values get overwritten."""
335+
flat_errors = { # pylint: disable=duplicate-key
336+
"user.name": INVALID_TYPE,
337+
"user.name": VALIDATION_FAILURE,
338+
}
339+
result = to_dict(flat_errors)
340+
assert result == {"user": {"name": VALIDATION_FAILURE}}
341+
342+
def test_numeric_string_keys(self):
343+
"""Test with numeric string keys that aren't array indices."""
344+
flat_errors = {
345+
"config.123": INVALID_TYPE,
346+
"data.456.value": MISSING_KEY,
347+
}
348+
expected = {
349+
"config": {"123": INVALID_TYPE},
350+
"data": {"456": {"value": MISSING_KEY}},
351+
}
352+
result = to_dict(flat_errors)
353+
assert result == expected
354+
355+
def test_special_characters_in_keys(self):
356+
"""Test with special characters in keys."""
357+
flat_errors = {
358+
"config.api-key": INVALID_TYPE,
359+
"data.field_name": MISSING_KEY,
360+
"settings.max@size": VALIDATION_FAILURE,
361+
}
362+
expected = {
363+
"config": {"api-key": INVALID_TYPE},
364+
"data": {"field_name": MISSING_KEY},
365+
"settings": {"max@size": VALIDATION_FAILURE},
366+
}
367+
result = to_dict(flat_errors)
368+
assert result == expected
369+
370+
def test_large_array_indices(self):
371+
"""Test with large array indices."""
372+
flat_errors = {"items[100].name": INVALID_TYPE}
373+
result = to_dict(flat_errors)
374+
assert len(result["items"]) == 101
375+
assert result["items"][100] == {"name": INVALID_TYPE}
376+
assert all(item is None for item in result["items"][:100])
377+
378+
def test_zero_index(self):
379+
"""Test with zero index."""
380+
flat_errors = {"items[0].name": INVALID_TYPE}
381+
expected = {"items": [{"name": INVALID_TYPE}]}
382+
result = to_dict(flat_errors)
383+
assert result == expected

0 commit comments

Comments
 (0)