Skip to content

Commit

Permalink
Add Fields documentation (#417)
Browse files Browse the repository at this point in the history
This commit includes markdown and sample files related to fields support in Protean.
  • Loading branch information
subhashb authored May 13, 2024
1 parent eda5428 commit 4146cfd
Show file tree
Hide file tree
Showing 36 changed files with 1,049 additions and 214 deletions.
2 changes: 2 additions & 0 deletions docs/guides/compose-a-domain/activate-domain.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# Activate the domain

Once a domain object is defined, the next step is to activate it.

A `Domain` in protean is always associated with a domain context, which can be
used to bind an domain object implicitly to the current thread or greenlet. We
refer to the act of binding the domain object as **activating the domain**.
Expand Down
174 changes: 0 additions & 174 deletions docs/guides/domain-definition/fields.md

This file was deleted.

101 changes: 101 additions & 0 deletions docs/guides/domain-definition/fields/association-fields.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
# Association Fields

## `HasOne`

Represents an one-to-one association between an aggregate and its entities.
This field is used to define a relationship where an aggregate is associated
with at most one instance of a child entity.

```python hl_lines="10 13"
{! docs_src/guides/domain-definition/fields/association-fields/001.py !}
```

!!!note
If you carefully observe the `HasOne` field declaration, the child entity's
name is a string value! This is usually the way to avoid circular references.
It applies to all aspects of Protean that link two entities - the string
value will be resolved to the class at runtime.

The `Author` entity can now be persisted along with the `Book` aggregate:

```shell hl_lines="3 12-13"
In [1]: book = Book(
...: title="The Great Gatsby",
...: author=Author(name="F. Scott Fitzgerald")
...: )

In [2]: domain.repository_for(Book).add(book)
Out[2]: <Book: Book object (id: a4a642d9-87ed-44de-9889-c687466f171b)>

In [3]: domain.repository_for(Book)._dao.query.all().items[0].to_dict()
Out[3]:
{'title': 'The Great Gatsby',
'author': {'name': 'F. Scott Fitzgerald',
'id': '1f275e92-9872-4d96-b999-4ef0fbe61013'},
'id': 'a4a642d9-87ed-44de-9889-c687466f171b'}
```

!!!note
Protean adds a `Reference` field to child entities to preserve the inverse
relationship - from child entity to aggregate - when persisted. This is
visible if you introspect the fields of the Child Entity.

```shell hl_lines="7 13"
In [1]: from protean.reflection import declared_fields, attributes

In [2]: declared_fields(Author)
Out[2]:
{'name': String(required=True, max_length=50),
'id': Auto(identifier=True),
'book': Reference()}

In [3]: attributes(Author)
Out[3]:
{'name': String(required=True, max_length=50),
'id': Auto(identifier=True),
'book_id': _ReferenceField()}
```

We will further review persistence related aspects around associations in the
Repository section.
<!-- FIXME Link Repository section -->

## `HasMany`

Represents a one-to-many association between two entities. This field is used
to define a relationship where an aggregate has multiple instances of a child
entity.

```python hl_lines="11"
{! docs_src/guides/domain-definition/fields/association-fields/002.py !}
```

Protean provides helper methods that begin with `add_` and `remove_` to add
and remove child entities from the `HasMany` relationship.

```shell hl_lines="4-5 12-13 16 23"
In [1]: post = Post(
...: title="Foo",
...: comments=[
...: Comment(content="Bar"),
...: Comment(content="Baz")
...: ]
...: )

In [2]: post.to_dict()
Out[2]:
{'title': 'Foo',
'comments': [{'content': 'Bar', 'id': '085ed011-15b3-48e3-9363-99a53bc9362a'},
{'content': 'Baz', 'id': '4790cf87-c234-42b6-bb03-1e0599bd6c0f'}],
'id': '29943ac9-a9eb-497b-b6d2-466b30ecd5f5'}

In [3]: post.add_comments(Comment(content="Qux"))

In [4]: post.to_dict()
Out[4]:
{'title': 'Foo',
'comments': [{'content': 'Bar', 'id': '085ed011-15b3-48e3-9363-99a53bc9362a'},
{'content': 'Baz', 'id': '4790cf87-c234-42b6-bb03-1e0599bd6c0f'},
{'content': 'Qux', 'id': 'b1a7aeda-81ca-4d0b-9d7e-6fe0c000b8af'}],
'id': '29943ac9-a9eb-497b-b6d2-466b30ecd5f5'}
```
105 changes: 105 additions & 0 deletions docs/guides/domain-definition/fields/container-fields.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
# Container Fields

## `List`

A field that represents a list of values.

**Optional Arguments**

- **`content_type`**: The type of items in the list. Defaults to `String`.
Accepted field types are `Boolean`, `Date`, `DateTime`, `Float`, `Identifier`,
`Integer`, `String`, and `Text`.
- **`pickled`**: Whether the list should be pickled when stored. Defaults to
`False`.

!!!note
Some database implementations (like Postgresql) can store lists 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.

```python hl_lines="9"
{! docs_src/guides/domain-definition/fields/simple-fields/001.py !}
```

The value is provided as a `list`, and the values in the `list` are validated
to be of the right type.

```shell hl_lines="6 12"
In [1]: user = User(email="[email protected]", roles=['ADMIN', 'EDITOR'])

In [2]: user.to_dict()
Out[2]:
{'email': '[email protected]',
'roles': ['ADMIN', 'EDITOR'],
'id': '582d946b-409b-4b15-b3be-6a90284264b3'}

In [3]: user2 = User(email="[email protected]", roles=[1, 2])
ERROR: Error during initialization: {'roles': ['Invalid value [1, 2]']}
...
ValidationError: {'roles': ['Invalid value [1, 2]']}
```

## `Dict`

A field that represents a dictionary.

**Optional Arguments**

- **`pickled`**: Whether the dict should be pickled when stored. Defaults to
`False`.

```python hl_lines="10"
{! docs_src/guides/domain-definition/fields/container-fields/002.py !}
```

A regular dictionary can be supplied as value to payload:


```shell hl_lines="3 9"
In [1]: event=UserEvent(
...: name="UserRegistered",
...: payload={'name': 'John Doe', 'email': '[email protected]'}
...: )

In [2]: event.to_dict()
Out[2]:
{'name': 'UserRegistered',
'payload': {'name': 'John Doe', 'email': '[email protected]'},
'id': '44e9143f-f4a6-40da-9128-4b6c013420d4'}
```

!!!note
Some database implementations (like Postgresql) can store dicts as JSON
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'}
```
Loading

0 comments on commit 4146cfd

Please sign in to comment.