Skip to content

Commit

Permalink
Add Fields documentation
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 committed May 13, 2024
1 parent eda5428 commit 876d232
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 876d232

Please sign in to comment.