Skip to content

Commit 64914b1

Browse files
committed
added validations
1 parent 8ce9856 commit 64914b1

7 files changed

Lines changed: 557 additions & 20 deletions

File tree

datamodel/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,9 @@
66
from datamodel.fields import Field, Column, fields
77
from .models import Model
88
from .base import BaseModel
9+
from .exceptions import ValidationError
910
from .version import (
1011
__title__, __description__, __version__, __author__, __author_email__
1112
)
1213

13-
__all__ = ('fields', 'Field', 'Column', 'Model', 'BaseModel', )
14+
__all__ = ('fields', 'Field', 'Column', 'Model', 'BaseModel', 'ValidationError', )

datamodel/converters.pyx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ from uuid import UUID
3030
import asyncpg.pgproto.pgproto as pgproto
3131
from .functions import is_iterable, is_primitive
3232
from .validation import _validation
33+
from .validation cimport _validate_constraints
3334
from .fields import Field
3435
# New converter:
3536
import datamodel.rs_parsers as rc
@@ -1883,7 +1884,6 @@ cpdef dict processing_fields(object obj, list columns):
18831884
errors.update(
18841885
_build_error(name, f"Error parsing *{name}* = *{value}*", ex)
18851886
)
1886-
continue
18871887
elif field_category == 'primitive':
18881888
try:
18891889
newval = parse_basic(_type, value, _encoder)
@@ -1893,7 +1893,6 @@ cpdef dict processing_fields(object obj, list columns):
18931893
errors.update(
18941894
_build_error(name, f"Error parsing {name}: ", ex)
18951895
)
1896-
continue
18971896
elif field_category == 'type':
18981897
# TODO: support multiple types
18991898
pass
@@ -2248,6 +2247,10 @@ cdef object _validation_(
22482247
if error:
22492248
err["error"] = error
22502249
return err
2250+
else:
2251+
# calling validation_constraints:
2252+
if _type in (str, int, float):
2253+
return _validate_constraints(f, name, value, _type, val_type)
22512254
return None
22522255
except ValueError:
22532256
raise

datamodel/validation.pxd

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
# cython: language_level=3, embedsignature=True
2+
# Copyright (C) 2018-present Jesus Lara
3+
#
4+
cdef dict _validate_constraints(object field, str name, object value, object annotated_type, object val_type)

datamodel/validation.pyx

Lines changed: 177 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,16 @@ validators = {
9595
Text: valid_str
9696
}
9797

98+
cdef dict _create_error(str name, object value, object error, object val_type, object annotated_type, object exception = None):
99+
return {
100+
"field": name,
101+
"value": value,
102+
"error": error,
103+
"value_type": val_type,
104+
"annotation": annotated_type,
105+
"exception": exception
106+
}
107+
98108

99109
cdef inline bint is_enum_class(object annotated_type):
100110
cdef int res
@@ -294,6 +304,168 @@ cdef dict _validate_union_field(
294304
errors
295305
)
296306

307+
cdef dict _validate_constraints(
308+
object field,
309+
str name,
310+
object value,
311+
object annotated_type,
312+
object val_type
313+
):
314+
"""
315+
Validates primitive field constraints based on field metadata.
316+
317+
Handles the following validations:
318+
- For strings: length, min_length, max_length
319+
- For numbers: min, max, gt, lt, ge, le, eq
320+
321+
Args:
322+
field: The Field object
323+
name: Field name
324+
value: The value to validate
325+
annotated_type: The annotated type
326+
val_type: The actual value type
327+
328+
Returns:
329+
Empty dict if validation passes, error dict otherwise
330+
"""
331+
# string comparisons
332+
cdef object length = None
333+
cdef object min_length = None
334+
cdef object max_length = None
335+
# integer/float comparisons
336+
cdef object min_val = None
337+
cdef object max_val = None
338+
cdef object ge_val = None
339+
cdef object le_val = None
340+
cdef object eq_val = None
341+
cdef object ne_val = None
342+
cdef object pattern = None
343+
# Skip validation if value is None
344+
if value is None:
345+
return {}
346+
347+
metadata = field.metadata
348+
error = {}
349+
350+
# String validations
351+
if annotated_type is str:
352+
length = metadata.get('length', None)
353+
min_length = metadata.get('min_length', None)
354+
max_length = metadata.get('max_length', None)
355+
pattern = metadata.get('pattern', getattr(field, '_pattern', None))
356+
# Length validation
357+
if length is not None and len(value) != length:
358+
return _create_error(
359+
name,
360+
value,
361+
f"String length must be exactly {length} characters, got {len(value)}",
362+
val_type,
363+
annotated_type
364+
)
365+
366+
# Min length validation
367+
if min_length is not None and len(value) < min_length:
368+
return _create_error(
369+
name,
370+
value,
371+
f"String length must be at least {min_length} characters, got {len(value)}",
372+
val_type,
373+
annotated_type
374+
)
375+
376+
# Max length validation
377+
if max_length is not None and len(value) > max_length:
378+
return _create_error(
379+
name,
380+
value,
381+
f"String length must be at most {max_length} characters, got {len(value)}",
382+
val_type,
383+
annotated_type
384+
)
385+
386+
# Pattern validation
387+
if pattern is not None:
388+
import re
389+
if not re.match(pattern, value):
390+
return _create_error(
391+
name,
392+
value,
393+
f"String does not match pattern {pattern}",
394+
val_type,
395+
annotated_type
396+
)
397+
398+
# Numeric validations (int, float, Decimal)
399+
elif annotated_type in (int, float, Decimal):
400+
min_val = metadata.get('min', getattr(field, 'gt', None))
401+
max_val = metadata.get('max', getattr(field, 'lt', None))
402+
ge_val = getattr(field, 'ge', None)
403+
le_val = getattr(field, 'le', None)
404+
eq_val = getattr(field, 'eq', None)
405+
ne_val = getattr(field, 'ne', None)
406+
# Equal validation
407+
if eq_val is not None and value != eq_val:
408+
return _create_error(
409+
name,
410+
value,
411+
f"Value must be equal to {eq_val}",
412+
val_type,
413+
annotated_type
414+
)
415+
416+
# Not equal validation
417+
if ne_val is not None and value == ne_val:
418+
return _create_error(
419+
name,
420+
value,
421+
f"Value must not be equal to {ne_val}",
422+
val_type,
423+
annotated_type
424+
)
425+
426+
# Minimum value validation (greater than)
427+
if min_val is not None and value <= min_val:
428+
return _create_error(
429+
name,
430+
value,
431+
f"Value must be greater than {min_val}",
432+
val_type,
433+
annotated_type
434+
)
435+
436+
# Maximum value validation (less than)
437+
if max_val is not None and value >= max_val:
438+
return _create_error(
439+
name,
440+
value,
441+
f"Value must be less than {max_val}",
442+
val_type,
443+
annotated_type
444+
)
445+
446+
# Greater than or equal validation
447+
if ge_val is not None and value < ge_val:
448+
return _create_error(
449+
name,
450+
value,
451+
f"Value must be greater than or equal to {ge_val}",
452+
val_type,
453+
annotated_type
454+
)
455+
456+
# Less than or equal validation
457+
if le_val is not None and value > le_val:
458+
return _create_error(
459+
name,
460+
value,
461+
f"Value must be less than or equal to {le_val}",
462+
val_type,
463+
annotated_type
464+
)
465+
466+
# If we've made it here, all validations passed
467+
return {}
468+
297469
cpdef dict _validation(
298470
object F,
299471
str name,
@@ -321,6 +493,11 @@ cpdef dict _validation(
321493
return _create_error(name, value, msg, val_type, annotated_type)
322494
except ValueError:
323495
raise
496+
# Check for primitive type constraints if the value is not None
497+
if F._type_category == 'primitive':
498+
errors = _validate_constraints(F, name, value, annotated_type, val_type)
499+
if errors:
500+
return errors
324501
# check: data type hint
325502
# If field_type is known, short-circuit certain checks
326503
if F.type == Text:
@@ -532,16 +709,6 @@ cpdef dict _validation(
532709
)
533710
return error
534711

535-
cdef dict _create_error(str name, object value, object error, object val_type, object annotated_type, object exception = None):
536-
return {
537-
"field": name,
538-
"value": value,
539-
"error": error,
540-
"value_type": val_type,
541-
"annotation": annotated_type,
542-
"exception": exception
543-
}
544-
545712
# Define a validator function for uint64
546713
def validate_uint64(value: int) -> None:
547714
"""Validate uint64 values.

datamodel/version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
'simple library based on python +3.8 to use Dataclass-syntax'
77
'for interacting with Data'
88
)
9-
__version__ = '0.10.14'
9+
__version__ = '0.10.15'
1010
__copyright__ = 'Copyright (c) 2020-2024 Jesus Lara'
1111
__author__ = 'Jesus Lara'
1212
__author_email__ = 'jesuslarag@gmail.com'

0 commit comments

Comments
 (0)