-
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.
Enhance support for different identity types in
Identifier
field (#414
) Enhance support for different identity types in `Identifier` field - Accept a parameter called `identity_type` per Entity, that helps customize the type of identifier for itself - Consider the default `IDENTITY_TYPE` configured at the domain level - Auto-transform identifiers to relevant identity type value - **UUID** and **STRING** (with numerals) to **INT** - **INT** and **UUID** to **STRING** - **STRING** (with valid UUID string) to **UUID** Fixes #413
- Loading branch information
Showing
17 changed files
with
505 additions
and
84 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,7 +1,173 @@ | ||
# Fields (draft) | ||
|
||
Fields are attributes that define the structure of data container elements in | ||
Protean. | ||
As we saw with Aggregates, Entities, and Value Objects, fields are attributes | ||
that define the structure of data container elements. | ||
|
||
This document lists all field types and their options available in Protean, | ||
and their built-in capabilities. | ||
|
||
## Options | ||
|
||
### `required` | ||
|
||
If `True`, the field is not allowed to be blank. Default is `False`. | ||
|
||
```python hl_lines="9" | ||
{! docs_src/guides/domain-definition/fields/001.py !} | ||
``` | ||
|
||
Leaving the field blank or not specifying a value will raise a | ||
`ValidationError`: | ||
|
||
```shell hl_lines="4" | ||
In [1]: p = Person() | ||
ERROR: Error during initialization: {'name': ['is required']} | ||
... | ||
ValidationError: {'name': ['is required']} | ||
``` | ||
|
||
### `identifier` | ||
|
||
If True, the field is the primary identifier for the entity (a la _primary key_ | ||
in RDBMS). | ||
|
||
```python hl_lines="9" | ||
{! docs_src/guides/domain-definition/fields/002.py !} | ||
``` | ||
|
||
The field is validated to be unique and non-blank: | ||
|
||
```shell hl_lines="6 11" | ||
In [1]: from protean.reflection import declared_fields | ||
|
||
In [2]: p = Person(email='[email protected]', name='John Doe') | ||
|
||
In [3]: declared_fields(p) | ||
Out[3]: {'email': String(identifier=True), 'name': String(required=True)} | ||
|
||
In [4]: p = Person(name='John Doe') | ||
ERROR: Error during initialization: {'email': ['is required']} | ||
... | ||
ValidationError: {'email': ['is required']} | ||
``` | ||
|
||
Aggregates and Entities need at least one field to be marked as an identifier. | ||
If you don’t specify one, Protean will automatically add a field called `id` | ||
to act as the primary identifier. This means that you don’t need to explicitly | ||
set `identifier=True` on any of your fields unless you want to override the | ||
default behavior or the name of the field. | ||
|
||
Alternatively, you can use [`Identifier`](#identifier-field) field type for | ||
primary identifier fields. | ||
|
||
By default, Protean dynamically generates UUIDs as values of identifier fields | ||
unless explicitly provided. You can customize the type of value accepted with | ||
`identity-strategy` config parameter. More details are in | ||
[Configuration](../compose-a-domain/configuration.md) section. | ||
|
||
### `default` | ||
|
||
The default value assigned to the field on initialization. | ||
|
||
This can be a value or a callable object. If callable, the function will be | ||
called every time a new object is created. | ||
|
||
```python hl_lines="16" | ||
{! docs_src/guides/domain-definition/fields/003.py !} | ||
``` | ||
|
||
```shell hl_lines="6" | ||
In [1]: post = Post(title='Foo') | ||
|
||
In [2]: post.to_dict() | ||
Out[2]: | ||
{'title': 'Foo', | ||
'created_at': '2024-05-09 00:58:10.781744+00:00', | ||
'id': '4f6b1fef-bc60-44c2-9ba6-6f844e0d31b0'} | ||
``` | ||
|
||
#### Mutable object defaults | ||
|
||
**IMPORTANT**: The default cannot be a mutable object (list, set, dict, entity | ||
instance, etc.), because the reference to the same object would be used as the | ||
default in all instances. Instead, wrap the desired default in a callable. | ||
|
||
For example, to specify a default `list` for `List` field, use a function: | ||
|
||
```python hl_lines="12" | ||
{! docs_src/guides/domain-definition/fields/004.py !} | ||
``` | ||
|
||
Initializing an Adult aggregate will populate the defaults correctly: | ||
|
||
```shell | ||
In [1]: adult = Adult(name="John Doe") | ||
|
||
In [2]: adult.to_dict() | ||
Out[2]: | ||
{'name': 'John Doe', | ||
'topics': ['Music', 'Cinema', 'Politics'], | ||
'id': '14381a6f-b62a-4135-a1d7-d50f68e2afba'} | ||
``` | ||
|
||
#### Lambda expressions | ||
|
||
You can use lambda expressions to specify an anonymous function: | ||
|
||
```python hl_lines="13" | ||
{! docs_src/guides/domain-definition/fields/005.py !} | ||
``` | ||
|
||
```shell hl_lines="4" | ||
In [1]: dice = Dice() | ||
|
||
In [2]: dice.to_dict() | ||
Out[2]: {'sides': 6, 'id': '0536ade5-f3a4-4e94-8139-8024756659a7'} | ||
|
||
In [3]: dice.throw() | ||
Out[3]: 3 | ||
``` | ||
|
||
This is a great option when you want to pass parameters to a function. | ||
|
||
### `unique` | ||
|
||
If True, this field's value must be unique among all entities. | ||
|
||
```python hl_lines="10" | ||
{! docs_src/guides/domain-definition/fields/006.py !} | ||
``` | ||
|
||
Obviously, this field's integrity is enforced at the database layer when an | ||
entity is persisted. If an entity instance specifies a duplicate value in a | ||
field marked `unique`, a `ValidationError` will be raised: | ||
|
||
```shell hl_lines="11" | ||
In [1]: p1 = Person(name='John Doe', email='[email protected]') | ||
|
||
In [2]: domain.repository_for(Person).add(p1) | ||
Out[2]: <Person: Person object (id: b2c592d5-bd78-4e1e-a9d1-eea20ab5374a)> | ||
|
||
In [3]: p2 = Person(name= 'Jane Doe', email='[email protected]') | ||
|
||
In [4]: domain.repository_for(Person).add(p2) | ||
ERROR: Failed saving entity because {'email': ["Person with email '[email protected]' is already present."]} | ||
... | ||
ValidationError: {'email': ["Person with email '[email protected]' is already present."]} | ||
``` | ||
|
||
We will explore more about persistence in the Application Layer. | ||
<!-- FIXME Add link to database persistence and aggregate lifecycle --> | ||
|
||
## Simple Fields | ||
|
||
## Container Fields | ||
|
||
## Associations | ||
|
||
## Embedded Fields | ||
|
||
## Custom Fields | ||
|
||
<!-- Be careful not to choose field names that conflict with the | ||
[Data Container API](../../api/data-containers) like `clean`, `clone`, or | ||
|
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 |
---|---|---|
|
@@ -50,4 +50,6 @@ Brokers | |
## Architecture Patterns | ||
|
||
CQRS | ||
Event Sourcing | ||
Event Sourcing | ||
|
||
## Data Container Elements |
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 |
---|---|---|
@@ -0,0 +1,9 @@ | ||
from protean import Domain | ||
from protean.fields import String | ||
|
||
domain = Domain(__file__) | ||
|
||
|
||
@domain.aggregate | ||
class Person: | ||
name = String(required=True) |
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,10 @@ | ||
from protean import Domain | ||
from protean.fields import String | ||
|
||
domain = Domain(__file__) | ||
|
||
|
||
@domain.aggregate | ||
class Person: | ||
email = String(identifier=True) | ||
name = String(required=True) |
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,16 @@ | ||
from datetime import datetime, timezone | ||
|
||
from protean.domain import Domain | ||
from protean.fields import DateTime, String | ||
|
||
publishing = Domain(__name__) | ||
|
||
|
||
def utc_now(): | ||
return datetime.now(timezone.utc) | ||
|
||
|
||
@publishing.aggregate | ||
class Post: | ||
title = String(max_length=50) | ||
created_at = DateTime(default=utc_now) |
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,14 @@ | ||
from protean.domain import Domain | ||
from protean.fields import List, String | ||
|
||
domain = Domain(__name__) | ||
|
||
|
||
def standard_topics(): | ||
return ["Music", "Cinema", "Politics"] | ||
|
||
|
||
@domain.aggregate | ||
class Adult: | ||
name = String(max_length=255) | ||
topics = List(default=standard_topics) |
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,16 @@ | ||
import random | ||
|
||
from protean.domain import Domain | ||
from protean.fields import Integer | ||
|
||
domain = Domain(__name__) | ||
|
||
dice_sides = [4, 6, 8, 10, 12, 20] | ||
|
||
|
||
@domain.aggregate | ||
class Dice: | ||
sides = Integer(default=lambda: random.choice(dice_sides)) | ||
|
||
def throw(self): | ||
return random.randint(1, self.sides) |
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,10 @@ | ||
from protean import Domain | ||
from protean.fields import String | ||
|
||
domain = Domain(__file__) | ||
|
||
|
||
@domain.aggregate | ||
class Person: | ||
name = String(required=True) | ||
email = String(unique=True) |
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
Oops, something went wrong.