Skip to content

Commit

Permalink
✨ feat: implement validation decorator and refactor value object vali…
Browse files Browse the repository at this point in the history
…dation methods
  • Loading branch information
adriamontoto committed Jan 4, 2025
1 parent a57fc7b commit d8d6973
Show file tree
Hide file tree
Showing 5 changed files with 80 additions and 16 deletions.
29 changes: 16 additions & 13 deletions tests/models/test_value_object_validate_method.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,39 +2,42 @@
Test value object validate method.
"""

from sys import version_info

from object_mother_pattern.mothers import IntegerMother
from pytest import mark, raises as assert_raises

from value_object_pattern import ValueObject

if version_info >= (3, 12):
from typing import override # pragma: no cover
else:
from typing_extensions import override # pragma: no cover
from value_object_pattern import ValueObject, validation


class NaturalValueObject(ValueObject[int]):
"""
NaturalValueObject value object class.
"""

@override
def _validate(self, value: int) -> None:
@validation
def ensure_value_is_integer(self, value: int) -> None:
"""
Validate the value object.
Ensures the value object is an integer.
Args:
value (int): Value object value.
value (int): Value object.
Raises:
TypeError: If the value object is not an integer.
ValueError: If the value object is not a natural number.
"""
if type(value) is not int:
raise TypeError(f'Value object must be an integer, not {type(value).__name__}.')

@validation
def ensure_value_is_natural(self, value: int) -> None:
"""
Ensures the value object is a natural number.
Args:
value (int): Value object.
Raises:
ValueError: If the value object is not a natural number.
"""
if value <= 0:
raise ValueError('Value object must be a natural number.')

Expand Down
6 changes: 5 additions & 1 deletion value_object_pattern/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
__version__ = '2025.01.03'

from .decorators import validation
from .models import ValueObject

__all__ = ('ValueObject',)
__all__ = (
'ValueObject',
'validation',
)
3 changes: 3 additions & 0 deletions value_object_pattern/decorators/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from .value_object_validation import validation

__all__ = ('validation',)
48 changes: 48 additions & 0 deletions value_object_pattern/decorators/value_object_validation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
"""
Validation decorator for value object pattern.
"""

from functools import wraps
from typing import Any, Callable


def validation(function: Callable[..., Any]) -> Callable[..., Any]:
"""
Decorator for validation.
Args:
function (Callable[..., Any]): Function to be execution when the value object is created.
Returns:
Callable[..., Any]: Wrapper function for the validation.
Example:
```python
from value_object_pattern import ValueObject, validation
class IntegerValueObject(ValueObject[int]):
@validation
def ensure_value_is_integer(self, value: int) -> None:
if type(value) is not int:
raise TypeError(f'IntegerValueObject value <<<{value}>>> must be an integer. Got <<<{type(value).__name__}>>> type.')
integer = IntegerValueObject(value='invalid')
# >>> TypeError: IntegerValueObject value <<<invalid>>> must be an integer. Got <<<str>>> type.
```
""" # noqa: E501 # fmt: skip
function._is_validation = True # type: ignore[attr-defined]

@wraps(wrapped=function)
def wrapper(*args: tuple[Any, ...], **kwargs: dict[str, Any]) -> None:
"""
Wrapper for validation.
Args:
*args (tuple[Any, ...]): The arguments for the function.
**kwargs (dict[str, Any]): The keyword arguments for the function.
"""
function(*args, **kwargs)

return wrapper
10 changes: 8 additions & 2 deletions value_object_pattern/models/value_object.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,12 @@
Value object generic type.
"""

import inspect
from abc import ABC
from datetime import date, datetime
from sys import version_info
from typing import Generic, NoReturn, TypeVar
from uuid import UUID

if version_info >= (3, 12):
from typing import override # pragma: no cover
Expand Down Expand Up @@ -120,12 +123,15 @@ def _post_validation_process(self, value: T) -> T:

def _validate(self, value: T) -> None:
"""
This method validates that the value follows the domain rules.
This method validates that the value follows the domain rules, by executing all methods with the `@validation`
decorator.
Args:
value (T): The value object value.
"""
pass
for _, method in inspect.getmembers(object=self, predicate=inspect.ismethod):
if getattr(method, '_is_validation', False):
method(value=value)

@property
def value(self) -> T:
Expand Down

0 comments on commit d8d6973

Please sign in to comment.