From 86e77c1eb5bd5dc1f3890ed8a1272e968b301362 Mon Sep 17 00:00:00 2001 From: Subhash Bhushan Date: Fri, 10 May 2024 08:01:49 -0700 Subject: [PATCH] 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 --- docs/guides/domain-definition/fields.md | 170 +++++++++++++++++- docs/guides/domain-definition/index.md | 4 +- docs_src/guides/domain-definition/008.py | 2 +- .../guides/domain-definition/fields/001.py | 9 + .../guides/domain-definition/fields/002.py | 10 ++ .../guides/domain-definition/fields/003.py | 16 ++ .../guides/domain-definition/fields/004.py | 14 ++ .../guides/domain-definition/fields/005.py | 16 ++ .../guides/domain-definition/fields/006.py | 10 ++ src/protean/fields/base.py | 45 +++-- src/protean/fields/basic.py | 41 ++++- tests/dao/test_basics.py | 13 -- tests/field/test_identifier.py | 98 +++++++++- tests/field/test_repr.py | 87 +++++++++ tests/field/test_string.py | 28 --- tests/field/test_text.py | 18 -- tests/views/tests.py | 8 +- 17 files changed, 505 insertions(+), 84 deletions(-) create mode 100644 docs_src/guides/domain-definition/fields/001.py create mode 100644 docs_src/guides/domain-definition/fields/002.py create mode 100644 docs_src/guides/domain-definition/fields/003.py create mode 100644 docs_src/guides/domain-definition/fields/004.py create mode 100644 docs_src/guides/domain-definition/fields/005.py create mode 100644 docs_src/guides/domain-definition/fields/006.py create mode 100644 tests/field/test_repr.py diff --git a/docs/guides/domain-definition/fields.md b/docs/guides/domain-definition/fields.md index fd1b923a..c4991978 100644 --- a/docs/guides/domain-definition/fields.md +++ b/docs/guides/domain-definition/fields.md @@ -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='john.doe@example.com', 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='john.doe@example.com') + +In [2]: domain.repository_for(Person).add(p1) +Out[2]: + +In [3]: p2 = Person(name= 'Jane Doe', email='john.doe@example.com') + +In [4]: domain.repository_for(Person).add(p2) +ERROR: Failed saving entity because {'email': ["Person with email 'john.doe@example.com' is already present."]} +... +ValidationError: {'email': ["Person with email 'john.doe@example.com' is already present."]} +``` + +We will explore more about persistence in the Application Layer. + + +## Simple Fields + +## Container Fields + +## Associations + +## Embedded Fields + +## Custom Fields