value-object-sindri 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- __init__.py +0 -0
- value_object/__init__.py +31 -0
- value_object/_compat.py +15 -0
- value_object/aggregate.py +315 -0
- value_object/decorators/__init__.py +0 -0
- value_object/decorators/validation.py +28 -0
- value_object/errors/__init__.py +0 -0
- value_object/errors/incorrect_value_type_error.py +12 -0
- value_object/errors/invalid_id_format_error.py +8 -0
- value_object/errors/required_value_error.py +8 -0
- value_object/errors/sindri_validation_error.py +10 -0
- value_object/identifiers/__init__.py +0 -0
- value_object/identifiers/string_uuid.py +55 -0
- value_object/primitives/__init__.py +0 -0
- value_object/primitives/boolean.py +44 -0
- value_object/primitives/float.py +44 -0
- value_object/primitives/integer.py +44 -0
- value_object/primitives/list.py +307 -0
- value_object/primitives/string.py +43 -0
- value_object/py.typed +0 -0
- value_object/value_object.py +314 -0
- value_object_sindri-0.1.0.dist-info/METADATA +122 -0
- value_object_sindri-0.1.0.dist-info/RECORD +26 -0
- value_object_sindri-0.1.0.dist-info/WHEEL +5 -0
- value_object_sindri-0.1.0.dist-info/licenses/LICENSE +22 -0
- value_object_sindri-0.1.0.dist-info/top_level.txt +2 -0
__init__.py
ADDED
|
File without changes
|
value_object/__init__.py
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""Public facade for value object implementations.
|
|
2
|
+
|
|
3
|
+
This module re-exports the most common value objects so they can be
|
|
4
|
+
imported directly from :mod:`value_objects`.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
__version__ = "0.1.0"
|
|
8
|
+
|
|
9
|
+
from value_object.aggregate import Aggregate
|
|
10
|
+
from value_object.decorators.validation import validate
|
|
11
|
+
from value_object.errors.sindri_validation_error import SindriValidationError
|
|
12
|
+
from value_object.identifiers.string_uuid import StringUuid
|
|
13
|
+
from value_object.primitives.boolean import Boolean
|
|
14
|
+
from value_object.primitives.float import Float
|
|
15
|
+
from value_object.primitives.integer import Integer
|
|
16
|
+
from value_object.primitives.list import List
|
|
17
|
+
from value_object.primitives.string import String
|
|
18
|
+
from value_object.value_object import ValueObject
|
|
19
|
+
|
|
20
|
+
__all__ = [
|
|
21
|
+
"Aggregate",
|
|
22
|
+
"validate",
|
|
23
|
+
"StringUuid",
|
|
24
|
+
"Boolean",
|
|
25
|
+
"Float",
|
|
26
|
+
"Integer",
|
|
27
|
+
"List",
|
|
28
|
+
"String",
|
|
29
|
+
"ValueObject",
|
|
30
|
+
"SindriValidationError",
|
|
31
|
+
]
|
value_object/_compat.py
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""Compatibility helpers for typing features across Python versions."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
try:
|
|
6
|
+
from typing import Self
|
|
7
|
+
except ImportError:
|
|
8
|
+
from typing_extensions import Self
|
|
9
|
+
|
|
10
|
+
try:
|
|
11
|
+
from typing import override
|
|
12
|
+
except ImportError:
|
|
13
|
+
from typing_extensions import override
|
|
14
|
+
|
|
15
|
+
__all__ = ["Self", "override"]
|
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
from enum import Enum
|
|
3
|
+
from inspect import Parameter, _empty, signature
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from value_object._compat import Self, override
|
|
7
|
+
from value_object.value_object import ValueObject
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Aggregate(ABC):
|
|
11
|
+
"""
|
|
12
|
+
Abstract base class for implementing aggregates in domain-driven design.
|
|
13
|
+
|
|
14
|
+
Aggregates are clusters of domain objects that can be treated as a single unit.
|
|
15
|
+
They ensure consistency boundaries and encapsulate business rules across
|
|
16
|
+
related entities and value objects.
|
|
17
|
+
|
|
18
|
+
This class provides utilities for:
|
|
19
|
+
- Converting aggregates to/from primitive dictionaries
|
|
20
|
+
- Comparing aggregates for equality
|
|
21
|
+
- String representation for debugging
|
|
22
|
+
- Handling nested value objects and other aggregates
|
|
23
|
+
|
|
24
|
+
Example:
|
|
25
|
+
>>> class User(Aggregate):
|
|
26
|
+
... def __init__(self, user_id: int, name: str, email: str):
|
|
27
|
+
... self.user_id = user_id
|
|
28
|
+
... self.name = name
|
|
29
|
+
... self.email = email
|
|
30
|
+
...
|
|
31
|
+
>>> user = User(1, "John Doe", "john@example.com")
|
|
32
|
+
>>> user.to_primitives()
|
|
33
|
+
{'user_id': 1, 'name': 'John Doe', 'email': 'john@example.com'}
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
@abstractmethod
|
|
37
|
+
def __init__(self) -> None:
|
|
38
|
+
"""
|
|
39
|
+
Initialize the aggregate.
|
|
40
|
+
|
|
41
|
+
This method must be implemented by all concrete aggregate classes.
|
|
42
|
+
It should set up the aggregate's state and ensure all invariants are met.
|
|
43
|
+
|
|
44
|
+
Raises:
|
|
45
|
+
NotImplementedError: Always raised as this is an abstract method.
|
|
46
|
+
|
|
47
|
+
Example:
|
|
48
|
+
>>> class Product(Aggregate):
|
|
49
|
+
... def __init__(self, product_id: str, name: str, price: float):
|
|
50
|
+
... self.product_id = product_id
|
|
51
|
+
... self.name = name
|
|
52
|
+
... self.price = price
|
|
53
|
+
... if price < 0:
|
|
54
|
+
... raise ValueError("Price cannot be negative")
|
|
55
|
+
"""
|
|
56
|
+
raise NotImplementedError
|
|
57
|
+
|
|
58
|
+
@override
|
|
59
|
+
def __repr__(self) -> str:
|
|
60
|
+
"""
|
|
61
|
+
Return a string representation suitable for debugging.
|
|
62
|
+
|
|
63
|
+
Creates a string representation showing all non-private attributes
|
|
64
|
+
in a constructor-like format.
|
|
65
|
+
|
|
66
|
+
Returns:
|
|
67
|
+
A string in the format "ClassName(attr1=value1, attr2=value2, ...)"
|
|
68
|
+
|
|
69
|
+
Example:
|
|
70
|
+
>>> class Order(Aggregate):
|
|
71
|
+
... def __init__(self, order_id: str, customer_id: int, total: float):
|
|
72
|
+
... self.order_id = order_id
|
|
73
|
+
... self.customer_id = customer_id
|
|
74
|
+
... self.total = total
|
|
75
|
+
...
|
|
76
|
+
>>> order = Order("ORD-001", 123, 99.99)
|
|
77
|
+
>>> repr(order)
|
|
78
|
+
"Order(customer_id=123, order_id='ORD-001', total=99.99)"
|
|
79
|
+
"""
|
|
80
|
+
attributes = []
|
|
81
|
+
for key, value in self._to_dict().items():
|
|
82
|
+
attributes.append(f"{key}={value!r}")
|
|
83
|
+
|
|
84
|
+
return f"{self.__class__.__name__}({', '.join(attributes)})"
|
|
85
|
+
|
|
86
|
+
@override
|
|
87
|
+
def __eq__(self, other: Self) -> bool:
|
|
88
|
+
"""
|
|
89
|
+
Check equality with another aggregate of the same type.
|
|
90
|
+
|
|
91
|
+
Two aggregates are considered equal if they are of the same class
|
|
92
|
+
and all their non-private attributes have equal values.
|
|
93
|
+
|
|
94
|
+
Args:
|
|
95
|
+
other: Another aggregate of the same type to compare with.
|
|
96
|
+
|
|
97
|
+
Returns:
|
|
98
|
+
True if both aggregates have equal attributes, False otherwise.
|
|
99
|
+
NotImplemented if comparing with a different type.
|
|
100
|
+
|
|
101
|
+
Example:
|
|
102
|
+
>>> class Category(Aggregate):
|
|
103
|
+
... def __init__(self, cat_id: int, name: str):
|
|
104
|
+
... self.cat_id = cat_id
|
|
105
|
+
... self.name = name
|
|
106
|
+
...
|
|
107
|
+
>>> cat1 = Category(1, "Electronics")
|
|
108
|
+
>>> cat2 = Category(1, "Electronics")
|
|
109
|
+
>>> cat3 = Category(2, "Books")
|
|
110
|
+
>>> cat1 == cat2
|
|
111
|
+
True
|
|
112
|
+
>>> cat1 == cat3
|
|
113
|
+
False
|
|
114
|
+
"""
|
|
115
|
+
if not isinstance(other, self.__class__):
|
|
116
|
+
return NotImplemented
|
|
117
|
+
|
|
118
|
+
return self._to_dict() == other._to_dict()
|
|
119
|
+
|
|
120
|
+
def _to_dict(self, *, ignore_private: bool = True) -> dict[str, Any]:
|
|
121
|
+
"""
|
|
122
|
+
Convert the aggregate to a dictionary representation.
|
|
123
|
+
|
|
124
|
+
Extracts all instance attributes and converts them to a dictionary,
|
|
125
|
+
optionally filtering out private attributes (those starting with
|
|
126
|
+
double underscore and class name).
|
|
127
|
+
|
|
128
|
+
Args:
|
|
129
|
+
ignore_private: Whether to exclude private attributes from the result.
|
|
130
|
+
Defaults to True.
|
|
131
|
+
|
|
132
|
+
Returns:
|
|
133
|
+
A dictionary mapping attribute names to their values.
|
|
134
|
+
|
|
135
|
+
Example:
|
|
136
|
+
>>> class Invoice(Aggregate):
|
|
137
|
+
... def __init__(self, invoice_id: str, amount: float):
|
|
138
|
+
... self.invoice_id = invoice_id
|
|
139
|
+
... self.amount = amount
|
|
140
|
+
... self._calculated_tax = amount * 0.1 # private attribute
|
|
141
|
+
... self.__secret = "hidden" # name-mangled private
|
|
142
|
+
...
|
|
143
|
+
>>> invoice = Invoice("INV-001", 100.0)
|
|
144
|
+
>>> invoice._to_dict()
|
|
145
|
+
{'invoice_id': 'INV-001', 'amount': 100.0, 'calculated_tax': 10.0}
|
|
146
|
+
>>> invoice._to_dict(ignore_private=False)
|
|
147
|
+
{'invoice_id': 'INV-001', 'amount': 100.0, 'calculated_tax': 10.0, 'secret': 'hidden'}
|
|
148
|
+
"""
|
|
149
|
+
dictionary: dict[str, Any] = {}
|
|
150
|
+
for key, value in self.__dict__.items():
|
|
151
|
+
if ignore_private and key.startswith(f"_{self.__class__.__name__}__"):
|
|
152
|
+
continue # ignore private attributes
|
|
153
|
+
|
|
154
|
+
key = key.replace(f"_{self.__class__.__name__}__", "")
|
|
155
|
+
|
|
156
|
+
if key.startswith("_"):
|
|
157
|
+
key = key[1:]
|
|
158
|
+
|
|
159
|
+
dictionary[key] = value
|
|
160
|
+
|
|
161
|
+
return dictionary
|
|
162
|
+
|
|
163
|
+
@classmethod
|
|
164
|
+
def from_primitives(cls, primitives: dict[str, Any]) -> Self:
|
|
165
|
+
"""
|
|
166
|
+
Create an aggregate instance from a dictionary of primitive values.
|
|
167
|
+
|
|
168
|
+
This factory method constructs an aggregate by mapping dictionary keys
|
|
169
|
+
to constructor parameters. All required constructor parameters must be
|
|
170
|
+
present in the primitives dictionary.
|
|
171
|
+
|
|
172
|
+
Args:
|
|
173
|
+
primitives: A dictionary mapping parameter names to their values.
|
|
174
|
+
Must contain all required constructor parameters.
|
|
175
|
+
|
|
176
|
+
Returns:
|
|
177
|
+
A new instance of the aggregate class.
|
|
178
|
+
|
|
179
|
+
Raises:
|
|
180
|
+
TypeError: If primitives is not a dictionary with string keys.
|
|
181
|
+
ValueError: If required parameters are missing or extra parameters are provided.
|
|
182
|
+
|
|
183
|
+
Example:
|
|
184
|
+
>>> class Customer(Aggregate):
|
|
185
|
+
... def __init__(self, customer_id: int, name: str, email: str = None):
|
|
186
|
+
... self.customer_id = customer_id
|
|
187
|
+
... self.name = name
|
|
188
|
+
... self.email = email
|
|
189
|
+
...
|
|
190
|
+
>>> data = {"customer_id": 42, "name": "Alice Smith"}
|
|
191
|
+
>>> customer = Customer.from_primitives(data)
|
|
192
|
+
>>> customer.name
|
|
193
|
+
'Alice Smith'
|
|
194
|
+
>>> customer.customer_id
|
|
195
|
+
42
|
|
196
|
+
>>>
|
|
197
|
+
>>> # Missing required parameter
|
|
198
|
+
>>> Customer.from_primitives({"name": "Bob"}) # Raises ValueError
|
|
199
|
+
>>>
|
|
200
|
+
>>> # Extra parameter
|
|
201
|
+
>>> Customer.from_primitives({"customer_id": 1, "name": "Charlie", "age": 30}) # Raises ValueError
|
|
202
|
+
"""
|
|
203
|
+
if not isinstance(primitives, dict) or not all(isinstance(key, str) for key in primitives):
|
|
204
|
+
raise TypeError(f'{cls.__name__} primitives <<<{primitives}>>> must be a dictionary of strings. Got <<<{type(primitives).__name__}>>> type.') # noqa: E501 # fmt: skip
|
|
205
|
+
|
|
206
|
+
constructor_signature = signature(obj=cls.__init__)
|
|
207
|
+
parameters: dict[str, Parameter] = {parameter.name: parameter for parameter in constructor_signature.parameters.values() if parameter.name != 'self'} # noqa: E501 # fmt: skip
|
|
208
|
+
missing = {name for name, parameter in parameters.items() if parameter.default is _empty and name not in primitives} # noqa: E501 # fmt: skip
|
|
209
|
+
extra = set(primitives) - parameters.keys()
|
|
210
|
+
|
|
211
|
+
if missing or extra:
|
|
212
|
+
cls._raise_value_constructor_parameters_mismatch(primitives=set(primitives), missing=missing, extra=extra)
|
|
213
|
+
|
|
214
|
+
return cls(**primitives)
|
|
215
|
+
|
|
216
|
+
@classmethod
|
|
217
|
+
def _raise_value_constructor_parameters_mismatch(
|
|
218
|
+
cls,
|
|
219
|
+
primitives: set[str],
|
|
220
|
+
missing: set[str],
|
|
221
|
+
extra: set[str],
|
|
222
|
+
) -> None:
|
|
223
|
+
"""
|
|
224
|
+
Raise a detailed ValueError for constructor parameter mismatches.
|
|
225
|
+
|
|
226
|
+
This helper method generates informative error messages when the
|
|
227
|
+
primitives dictionary doesn't match the constructor signature.
|
|
228
|
+
|
|
229
|
+
Args:
|
|
230
|
+
primitives: Set of parameter names provided in the primitives dict.
|
|
231
|
+
missing: Set of required parameter names that are missing.
|
|
232
|
+
extra: Set of parameter names that are not in the constructor.
|
|
233
|
+
|
|
234
|
+
Raises:
|
|
235
|
+
ValueError: Always raised with detailed information about the mismatch.
|
|
236
|
+
|
|
237
|
+
Example:
|
|
238
|
+
>>> class Item(Aggregate):
|
|
239
|
+
... def __init__(self, item_id: str, name: str, price: float):
|
|
240
|
+
... pass
|
|
241
|
+
...
|
|
242
|
+
>>> # This would trigger the error method internally:
|
|
243
|
+
>>> Item.from_primitives({"item_id": "123", "description": "test"})
|
|
244
|
+
Traceback (most recent call last):
|
|
245
|
+
...
|
|
246
|
+
ValueError: Item primitives <<<description, item_id>>> must contain all constructor parameters.
|
|
247
|
+
Missing parameters: <<<name, price>>> and extra parameters: <<<description>>>.
|
|
248
|
+
"""
|
|
249
|
+
primitives_names = ", ".join(sorted(primitives))
|
|
250
|
+
missing_names = ", ".join(sorted(missing))
|
|
251
|
+
extra_names = ", ".join(sorted(extra))
|
|
252
|
+
|
|
253
|
+
raise ValueError(f'{cls.__name__} primitives <<<{primitives_names}>>> must contain all constructor parameters. Missing parameters: <<<{missing_names}>>> and extra parameters: <<<{extra_names}>>>.') # noqa: E501 # fmt: skip
|
|
254
|
+
|
|
255
|
+
def to_primitives(self) -> dict[str, Any]:
|
|
256
|
+
"""
|
|
257
|
+
Convert the aggregate to a dictionary of primitive values.
|
|
258
|
+
|
|
259
|
+
Recursively converts the aggregate and all nested objects (other aggregates,
|
|
260
|
+
value objects, enums) to their primitive representations. This is useful
|
|
261
|
+
for serialization to JSON, database storage, or API responses.
|
|
262
|
+
|
|
263
|
+
Returns:
|
|
264
|
+
A dictionary with primitive values (strings, numbers, booleans, etc.)
|
|
265
|
+
where complex objects have been converted to their primitive forms.
|
|
266
|
+
|
|
267
|
+
Example:
|
|
268
|
+
>>> from enum import Enum
|
|
269
|
+
>>> from value_objects import ValueObject
|
|
270
|
+
>>> from value_objects.aggregate import Aggregate
|
|
271
|
+
>>>
|
|
272
|
+
>>> class Status(Enum):
|
|
273
|
+
... ACTIVE = "active"
|
|
274
|
+
... INACTIVE = "inactive"
|
|
275
|
+
...
|
|
276
|
+
>>> class UserId(ValueObject[int]):
|
|
277
|
+
... pass
|
|
278
|
+
...
|
|
279
|
+
>>> class Account(Aggregate):
|
|
280
|
+
... def __init__(self, user_id: UserId, status: Status, balance: float):
|
|
281
|
+
... self.user_id = user_id
|
|
282
|
+
... self.status = status
|
|
283
|
+
... self.balance = balance
|
|
284
|
+
...
|
|
285
|
+
>>> account = Account(UserId(123), Status.ACTIVE, 1500.50)
|
|
286
|
+
>>> account.to_primitives()
|
|
287
|
+
{'user_id': 123, 'status': 'active', 'balance': 1500.5}
|
|
288
|
+
>>>
|
|
289
|
+
>>> # With nested aggregates
|
|
290
|
+
>>> class Order(Aggregate):
|
|
291
|
+
... def __init__(self, account: Account, item_count: int):
|
|
292
|
+
... self.account = account
|
|
293
|
+
... self.item_count = item_count
|
|
294
|
+
...
|
|
295
|
+
>>> order = Order(account, 3)
|
|
296
|
+
>>> order.to_primitives()
|
|
297
|
+
{'account': {'user_id': 123, 'status': 'active', 'balance': 1500.5}, 'item_count': 3}
|
|
298
|
+
"""
|
|
299
|
+
primitives = self._to_dict()
|
|
300
|
+
for key, value in primitives.items():
|
|
301
|
+
if isinstance(value, Aggregate) or hasattr(value, "to_primitives"):
|
|
302
|
+
value = value.to_primitives()
|
|
303
|
+
|
|
304
|
+
elif isinstance(value, Enum):
|
|
305
|
+
value = value.value
|
|
306
|
+
|
|
307
|
+
elif isinstance(value, ValueObject) or hasattr(value, "value"):
|
|
308
|
+
value = value.value
|
|
309
|
+
|
|
310
|
+
if isinstance(value, Enum):
|
|
311
|
+
value = value.value
|
|
312
|
+
|
|
313
|
+
primitives[key] = value
|
|
314
|
+
|
|
315
|
+
return primitives
|
|
File without changes
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
from collections.abc import Callable
|
|
2
|
+
from typing import Any, TypeVar
|
|
3
|
+
|
|
4
|
+
F = TypeVar("F", bound=Callable[..., Any])
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def validate(func: F | None = None, *, order: int = 0) -> Callable[[F], F] | F:
|
|
8
|
+
"""Mark a method as a validator for ValueObject validation.
|
|
9
|
+
|
|
10
|
+
Arguments:
|
|
11
|
+
func: the function to decorate.
|
|
12
|
+
order: order in which this validator should run relative to other validators in the same class. Lower numbers run first.
|
|
13
|
+
""" # noqa: E501
|
|
14
|
+
|
|
15
|
+
def wrapper(fn: F) -> F:
|
|
16
|
+
if not isinstance(order, int):
|
|
17
|
+
raise TypeError(f"Validation order {order} must be an integer. Got {type(order).__name__} type.")
|
|
18
|
+
if order < 0:
|
|
19
|
+
raise ValueError(f"Validation order {order} must be a positive value.")
|
|
20
|
+
|
|
21
|
+
fn._is_validator = True
|
|
22
|
+
fn._order = order
|
|
23
|
+
return fn
|
|
24
|
+
|
|
25
|
+
if func is not None:
|
|
26
|
+
return wrapper(func)
|
|
27
|
+
|
|
28
|
+
return wrapper
|
|
File without changes
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
from typing import Any, TypeVar
|
|
2
|
+
|
|
3
|
+
from value_object.errors.sindri_validation_error import SindriValidationError
|
|
4
|
+
|
|
5
|
+
T = TypeVar("T")
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class IncorrectValueTypeError(SindriValidationError):
|
|
9
|
+
def __init__(self, value: T, expected_type: type[Any]) -> None:
|
|
10
|
+
super().__init__(
|
|
11
|
+
message=f"Value '{value}' is not of type {expected_type.__name__}",
|
|
12
|
+
)
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
class SindriValidationError(Exception):
|
|
2
|
+
"""Base class for all controlled errors during validation of value objects."""
|
|
3
|
+
|
|
4
|
+
def __init__(self, message: str) -> None:
|
|
5
|
+
self._message = message
|
|
6
|
+
super().__init__(self._message)
|
|
7
|
+
|
|
8
|
+
@property
|
|
9
|
+
def message(self) -> str:
|
|
10
|
+
return self._message
|
|
File without changes
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
from uuid import UUID
|
|
2
|
+
|
|
3
|
+
from value_object.decorators.validation import validate
|
|
4
|
+
from value_object.errors.incorrect_value_type_error import IncorrectValueTypeError
|
|
5
|
+
from value_object.errors.invalid_id_format_error import InvalidIdFormatError
|
|
6
|
+
from value_object.errors.required_value_error import RequiredValueError
|
|
7
|
+
from value_object.value_object import ValueObject
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class StringUuid(ValueObject[str]):
|
|
11
|
+
"""
|
|
12
|
+
A value object that wraps UUID (Universally Unique Identifier) string values with validation.
|
|
13
|
+
|
|
14
|
+
This class provides a specialized implementation for creating value objects that
|
|
15
|
+
represent UUID values in the domain. It ensures that the wrapped value is a
|
|
16
|
+
valid UUID string format and not None.
|
|
17
|
+
|
|
18
|
+
The class includes built-in validation for:
|
|
19
|
+
- Required value (not None)
|
|
20
|
+
- Type checking (must be a string)
|
|
21
|
+
- UUID format validation (must be a valid UUID format)
|
|
22
|
+
|
|
23
|
+
Inherits all functionality from ValueObject including immutability,
|
|
24
|
+
equality comparison, string representation, and hashing.
|
|
25
|
+
|
|
26
|
+
Example:
|
|
27
|
+
```python
|
|
28
|
+
class UserId(StringUuid):
|
|
29
|
+
@validate
|
|
30
|
+
def _validate_version(self) -> None:
|
|
31
|
+
parsed_uuid = UUID(self._value)
|
|
32
|
+
if parsed_uuid.version != 4:
|
|
33
|
+
raise ValueError("Only UUID version 4 allowed")
|
|
34
|
+
|
|
35
|
+
user_id = UserId("123e4567-e89b-12d3-a456-426614174000")
|
|
36
|
+
user_id.value # '123e4567-e89b-12d3-a456-426614174000'
|
|
37
|
+
```
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
@validate
|
|
41
|
+
def _ensure_has_value(self) -> None:
|
|
42
|
+
if self._value is None:
|
|
43
|
+
raise RequiredValueError
|
|
44
|
+
|
|
45
|
+
@validate
|
|
46
|
+
def _ensure_value_is_string(self) -> None:
|
|
47
|
+
if not isinstance(self._value, str):
|
|
48
|
+
raise IncorrectValueTypeError(self._value, str)
|
|
49
|
+
|
|
50
|
+
@validate
|
|
51
|
+
def _ensure_value_has_valid_uuid_format(self) -> None:
|
|
52
|
+
try:
|
|
53
|
+
UUID(self._value)
|
|
54
|
+
except ValueError as error:
|
|
55
|
+
raise InvalidIdFormatError from error
|
|
File without changes
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
from value_object.decorators.validation import validate
|
|
2
|
+
from value_object.errors.incorrect_value_type_error import IncorrectValueTypeError
|
|
3
|
+
from value_object.errors.required_value_error import RequiredValueError
|
|
4
|
+
from value_object.value_object import ValueObject
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class Boolean(ValueObject[bool]):
|
|
8
|
+
"""
|
|
9
|
+
A value object that wraps boolean values with validation.
|
|
10
|
+
|
|
11
|
+
This class provides a base implementation for creating value objects that
|
|
12
|
+
represent boolean values in the domain. It ensures that the wrapped value
|
|
13
|
+
is a valid boolean and not None.
|
|
14
|
+
|
|
15
|
+
The class includes built-in validation for:
|
|
16
|
+
- Required value (not None)
|
|
17
|
+
- Type checking (must be a boolean)
|
|
18
|
+
|
|
19
|
+
Inherits all functionality from ValueObject including immutability,
|
|
20
|
+
equality comparison, string representation, and hashing.
|
|
21
|
+
|
|
22
|
+
Example:
|
|
23
|
+
```python
|
|
24
|
+
class IsActive(Boolean):
|
|
25
|
+
@validate
|
|
26
|
+
def _validate_true_for_premium(self) -> None:
|
|
27
|
+
# Custom business logic can be added here
|
|
28
|
+
pass
|
|
29
|
+
|
|
30
|
+
is_active = IsActive(True)
|
|
31
|
+
is_active.value # True
|
|
32
|
+
str(is_active) # 'True'
|
|
33
|
+
```
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
@validate
|
|
37
|
+
def _ensure_has_value(self) -> None:
|
|
38
|
+
if self._value is None:
|
|
39
|
+
raise RequiredValueError
|
|
40
|
+
|
|
41
|
+
@validate
|
|
42
|
+
def _ensure_value_is_boolean(self) -> None:
|
|
43
|
+
if not isinstance(self._value, bool):
|
|
44
|
+
raise IncorrectValueTypeError(self._value, bool)
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
from value_object.decorators.validation import validate
|
|
2
|
+
from value_object.errors.incorrect_value_type_error import IncorrectValueTypeError
|
|
3
|
+
from value_object.errors.required_value_error import RequiredValueError
|
|
4
|
+
from value_object.value_object import ValueObject
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class Float(ValueObject[float]):
|
|
8
|
+
"""
|
|
9
|
+
A value object that wraps float values with validation.
|
|
10
|
+
|
|
11
|
+
This class provides a base implementation for creating value objects that
|
|
12
|
+
represent float values in the domain. It ensures that the wrapped value
|
|
13
|
+
is a valid float and not None. It accepts both positive and negative values.
|
|
14
|
+
|
|
15
|
+
The class includes built-in validation for:
|
|
16
|
+
- Required value (not None)
|
|
17
|
+
- Type checking (must be a float)
|
|
18
|
+
|
|
19
|
+
Inherits all functionality from ValueObject including immutability,
|
|
20
|
+
equality comparison, string representation, and hashing.
|
|
21
|
+
|
|
22
|
+
Example:
|
|
23
|
+
```python
|
|
24
|
+
class Price(Float):
|
|
25
|
+
@validate
|
|
26
|
+
def _validate_positive(self) -> None:
|
|
27
|
+
if self._value < 0:
|
|
28
|
+
raise ValueError("Price cannot be negative")
|
|
29
|
+
|
|
30
|
+
price = Price(29.99)
|
|
31
|
+
price.value # 29.99
|
|
32
|
+
str(price) # '29.99'
|
|
33
|
+
```
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
@validate
|
|
37
|
+
def _ensure_has_value(self) -> None:
|
|
38
|
+
if self._value is None:
|
|
39
|
+
raise RequiredValueError
|
|
40
|
+
|
|
41
|
+
@validate
|
|
42
|
+
def _ensure_value_is_float(self) -> None:
|
|
43
|
+
if not isinstance(self._value, float):
|
|
44
|
+
raise IncorrectValueTypeError(self._value, float)
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
from value_object.decorators.validation import validate
|
|
2
|
+
from value_object.errors.incorrect_value_type_error import IncorrectValueTypeError
|
|
3
|
+
from value_object.errors.required_value_error import RequiredValueError
|
|
4
|
+
from value_object.value_object import ValueObject
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class Integer(ValueObject[int]):
|
|
8
|
+
"""
|
|
9
|
+
A value object that wraps integer values with validation.
|
|
10
|
+
|
|
11
|
+
This class provides a base implementation for creating value objects that
|
|
12
|
+
represent integer values in the domain. It ensures that the wrapped value
|
|
13
|
+
is a valid integer and not None.
|
|
14
|
+
|
|
15
|
+
The class includes built-in validation for:
|
|
16
|
+
- Required value (not None)
|
|
17
|
+
- Type checking (must be an integer)
|
|
18
|
+
|
|
19
|
+
Inherits all functionality from ValueObject including immutability,
|
|
20
|
+
equality comparison, string representation, and hashing.
|
|
21
|
+
|
|
22
|
+
Example:
|
|
23
|
+
```python
|
|
24
|
+
class Age(Integer):
|
|
25
|
+
@validate
|
|
26
|
+
def _validate_positive(self) -> None:
|
|
27
|
+
if self._value < 0:
|
|
28
|
+
raise ValueError("Age cannot be negative")
|
|
29
|
+
|
|
30
|
+
age = Age(25)
|
|
31
|
+
age.value # 25
|
|
32
|
+
str(age) # '25'
|
|
33
|
+
```
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
@validate
|
|
37
|
+
def _ensure_has_value(self) -> None:
|
|
38
|
+
if self._value is None:
|
|
39
|
+
raise RequiredValueError
|
|
40
|
+
|
|
41
|
+
@validate
|
|
42
|
+
def _ensure_value_is_integer(self) -> None:
|
|
43
|
+
if not isinstance(self._value, int):
|
|
44
|
+
raise IncorrectValueTypeError(self._value, int)
|