-
Notifications
You must be signed in to change notification settings - Fork 10
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
This commit allows
List
fields to manage an array of Value Objects.
Specific changes: - Update `List` field to support Value Object content type - Allow ValueObjects to output dict even when outside an entity or aggregate - Support serialization of Value Objects in SQLAlchemy with a custom JSON serializer This will come handy when embedding List of Value Objects in containers like Events, which need to enclose all information related to the event within themselves.
- Loading branch information
Showing
13 changed files
with
558 additions
and
60 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,34 @@ | ||
# Container Fields | ||
|
||
## `ValueObject` | ||
|
||
Represents a field that holds a value object. This field is used to embed a | ||
Value Object within an entity. | ||
|
||
**Arguments** | ||
|
||
- **`value_object_cls`**: The class of the value object to be embedded. | ||
|
||
```python hl_lines="7-15 20" | ||
{! docs_src/guides/domain-definition/fields/container-fields/003.py !} | ||
``` | ||
|
||
You can provide an instance of the Value Object as input to the value object | ||
field: | ||
|
||
```shell hl_lines="2 8" | ||
In [1]: account = Account( | ||
...: balance=Balance(currency="USD", amount=100.0), | ||
...: name="Checking" | ||
...: ) | ||
|
||
In [2]: account.to_dict() | ||
Out[2]: | ||
{'balance': {'currency': 'USD', 'amount': 100.0}, | ||
'name': 'Checking', | ||
'id': '513b8a78-e00f-45ce-bb6f-11ef0cccbec6'} | ||
``` | ||
|
||
## `List` | ||
|
||
A field that represents a list of values. | ||
|
@@ -18,8 +47,8 @@ Accepted field types are `Boolean`, `Date`, `DateTime`, `Float`, `Identifier`, | |
specifying `pickled=True`. Databases that don’t support lists simply store | ||
the field as a python object. | ||
|
||
```python hl_lines="9" | ||
{! docs_src/guides/domain-definition/fields/simple-fields/001.py !} | ||
```python hl_lines="10" | ||
{! docs_src/guides/domain-definition/fields/container-fields/001.py !} | ||
``` | ||
|
||
The value is provided as a `list`, and the values in the `list` are validated | ||
|
@@ -40,6 +69,86 @@ ERROR: Error during initialization: {'roles': ['Invalid value [1, 2]']} | |
ValidationError: {'roles': ['Invalid value [1, 2]']} | ||
``` | ||
|
||
### List of Value Objects | ||
|
||
A `List` field can even hold a list of `ValueObject` instances. The content of | ||
the `List` will be persisted as a list of dicts, so the field will behave | ||
essentially like `List(Dict())` when it comes to persistence. However, it will | ||
have the added benefit of a validation structure of content within the List. | ||
|
||
```python hl_lines="7-12 19" | ||
{! docs_src/guides/domain-definition/fields/container-fields/004.py !} | ||
``` | ||
|
||
```shell | ||
In [1]: order = Order( | ||
...: customer=Customer( | ||
...: name="John Doe", | ||
...: email="[email protected]", | ||
...: addresses=[ | ||
...: Address(street="123 Main St", city="Anytown", state="CA", country="USA"), | ||
...: Address(street="321 Side St", city="Anytown", state="CA", country="USA"), | ||
...: ], | ||
...: ) | ||
...: ) | ||
|
||
In [2]: order.to_dict() | ||
Out[2]: | ||
{'customer': {'name': 'John Doe', | ||
'email': '[email protected]', | ||
'addresses': [{'street': '123 Main St', | ||
'city': 'Anytown', | ||
'state': 'CA', | ||
'country': 'USA'}, | ||
{'street': '321 Side St', | ||
'city': 'Anytown', | ||
'state': 'CA', | ||
'country': 'USA'}], | ||
'id': 'f5c5a750-e9fe-47db-877e-44b7c0ca1dfc'}, | ||
'id': '4a9538bf-1eb1-4621-8ced-86bcc4362a51'} | ||
|
||
In [3]: domain.repository_for(Order).add(order) | ||
Out[3]: <Order: Order object (id: 4a9538bf-1eb1-4621-8ced-86bcc4362a51)> | ||
|
||
In [4]: retrieved_order = domain.repository_for(Order).get(order.id) | ||
|
||
In [5]: len(retrieved_order.customer.addresses) | ||
Out[5]: 2 | ||
``` | ||
|
||
Note that unlike `HasMany` fields, you have to supply a new entire list of | ||
Value Objects if you want to update the field. Appendind to the list will not | ||
work. | ||
|
||
```shell | ||
In [6]: retrieved_order.customer.addresses.append( | ||
...: Address(street="456 Side St", city="Anytown", state="CA", country="USA") | ||
...: ) | ||
|
||
In [7]: domain.repository_for(Order).add(retrieved_order) | ||
Out[7]: <Order: Order object (id: 4a9538bf-1eb1-4621-8ced-86bcc4362a51)> | ||
|
||
In [8]: updated_order = domain.repository_for(Order).get(order.id) | ||
|
||
In [9]: len(updated_order.customer.addresses) | ||
Out[9]: 2 | ||
# This did not work! | ||
In [10]: updated_order.customer.addresses = [ | ||
...: Address(street="123 Main St", city="Anytown", state="CA", country="USA"), | ||
...: Address(street="321 Side St", city="Anytown", state="CA", country="USA"), | ||
...: Address(street="456 Side St", city="Anytown", state="CA", country="USA"), | ||
...: ] | ||
|
||
In [11]: domain.repository_for(Order).add(updated_order) | ||
Out[11]: <Order: Order object (id: 4a9538bf-1eb1-4621-8ced-86bcc4362a51)> | ||
|
||
In [12]: refreshed_order = domain.repository_for(Order).get(order.id) | ||
|
||
In [13]: len(refreshed_order.customer.addresses) | ||
Out[13]: 3 | ||
# This worked! | ||
``` | ||
|
||
## `Dict` | ||
|
||
A field that represents a dictionary. | ||
|
@@ -74,32 +183,3 @@ Out[2]: | |
by default. You can force it to store the pickled value as a Python object | ||
by specifying pickled=True. Databases that don’t support lists simply store | ||
the field as a python object. | ||
|
||
## `ValueObject` | ||
|
||
Represents a field that holds a value object. This field is used to embed a | ||
Value Object within an entity. | ||
|
||
**Arguments** | ||
|
||
- **`value_object_cls`**: The class of the value object to be embedded. | ||
|
||
```python hl_lines="7-15 20" | ||
{! docs_src/guides/domain-definition/fields/container-fields/003.py !} | ||
``` | ||
|
||
You can provide an instance of the Value Object as input to the value object | ||
field: | ||
|
||
```shell hl_lines="2 8" | ||
In [1]: account = Account( | ||
...: balance=Balance(currency="USD", amount=100.0), | ||
...: name="Checking" | ||
...: ) | ||
|
||
In [2]: account.to_dict() | ||
Out[2]: | ||
{'balance': {'currency': 'USD', 'amount': 100.0}, | ||
'name': 'Checking', | ||
'id': '513b8a78-e00f-45ce-bb6f-11ef0cccbec6'} | ||
``` |
24 changes: 24 additions & 0 deletions
24
docs_src/guides/domain-definition/fields/container-fields/004.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
from protean import Domain | ||
from protean.fields import HasOne, String, List, ValueObject | ||
|
||
domain = Domain(__file__, load_toml=False) | ||
|
||
|
||
@domain.value_object | ||
class Address: | ||
street = String(max_length=100) | ||
city = String(max_length=25) | ||
state = String(max_length=25) | ||
country = String(max_length=25) | ||
|
||
|
||
@domain.entity(part_of="Order") | ||
class Customer: | ||
name = String(max_length=50, required=True) | ||
email = String(max_length=254, required=True) | ||
addresses = List(content_type=ValueObject(Address)) | ||
|
||
|
||
@domain.aggregate | ||
class Order: | ||
customer = HasOne(Customer) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -112,9 +112,10 @@ def test_array_content_type_validation(test_domain): | |
{"email": "[email protected]", "roles": [1.0, 2.0]}, | ||
{"email": "[email protected]", "roles": [datetime.now(UTC)]}, | ||
]: | ||
with pytest.raises(ValidationError) as exception: | ||
try: | ||
ArrayUser(**kwargs) | ||
assert exception.value.messages["roles"][0].startswith("Invalid value") | ||
except ValidationError: | ||
pytest.fail("Failed to convert integers into strings in List field type") | ||
|
||
model_cls = test_domain.repository_for(IntegerArrayUser)._model | ||
user = IntegerArrayUser(email="[email protected]", roles=[1, 2]) | ||
|
@@ -126,7 +127,6 @@ def test_array_content_type_validation(test_domain): | |
|
||
for kwargs in [ | ||
{"email": "[email protected]", "roles": ["ADMIN", "USER"]}, | ||
{"email": "[email protected]", "roles": ["1", "2"]}, | ||
{"email": "[email protected]", "roles": [datetime.now(UTC)]}, | ||
]: | ||
with pytest.raises(ValidationError) as exception: | ||
|
Oops, something went wrong.