Skip to content

Commit

Permalink
Bugfix - Resolve VO class in Lists
Browse files Browse the repository at this point in the history
If a List field has been declared with content_type as a Value Object and
the value object class was supplied as a string, we should resolve reference
on domain initialization.
  • Loading branch information
subhashb committed Jul 18, 2024
1 parent f831806 commit d6374f1
Show file tree
Hide file tree
Showing 6 changed files with 95 additions and 4 deletions.
13 changes: 13 additions & 0 deletions src/protean/domain/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
NotSupportedError,
)
from protean.fields import HasMany, HasOne, Reference, ValueObject
from protean.fields import List as ProteanList
from protean.globals import g
from protean.reflection import declared_fields, has_fields, id_field
from protean.utils import (
Expand Down Expand Up @@ -510,20 +511,32 @@ def _register_element(self, element_type, element_cls, **opts): # noqa: C901
# 1. Associations
if has_fields(new_cls):
for _, field_obj in declared_fields(new_cls).items():
# Record Association references to resolve later
if isinstance(field_obj, (HasOne, HasMany, Reference)) and isinstance(
field_obj.to_cls, str
):
self._pending_class_resolutions[field_obj.to_cls].append(
("Association", (field_obj, new_cls))
)

# Record Value Object references to resolve later
if isinstance(field_obj, ValueObject) and isinstance(
field_obj.value_object_cls, str
):
self._pending_class_resolutions[field_obj.value_object_cls].append(
("ValueObject", (field_obj, new_cls))
)

# Record Value Object references in List fields to resolve later
if (
isinstance(field_obj, ProteanList)
and isinstance(field_obj.content_type, ValueObject)
and isinstance(field_obj.content_type.value_object_cls, str)
):
self._pending_class_resolutions[
field_obj.content_type.value_object_cls
].append(("ValueObject", (field_obj.content_type, new_cls)))

# 2. Meta Linkages
if element_type in [
DomainObjects.ENTITY,
Expand Down
5 changes: 4 additions & 1 deletion src/protean/fields/embedded.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,10 @@ def _validate_value_object_cls(self, value_object_cls):
raise IncorrectUsageError(
{
"_value_object": [
f"`{value_object_cls.__name__}` is not a valid Value Object"
(
f"`{value_object_cls.__name__}` is not a valid Value Object "
"and cannot be embedded in a Value Object field"
)
]
}
)
Expand Down
32 changes: 30 additions & 2 deletions tests/field/test_list.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@

import pytest

from protean import BaseValueObject
from protean.exceptions import ValidationError
from protean import BaseAggregate, BaseEntity, BaseValueObject
from protean.exceptions import IncorrectUsageError, ValidationError
from protean.fields.basic import (
Boolean,
Date,
Expand All @@ -15,6 +15,7 @@
String,
)
from protean.fields.embedded import ValueObject
from protean.reflection import declared_fields


class TestListFieldContentType:
Expand Down Expand Up @@ -98,6 +99,33 @@ class VO(BaseValueObject):
value = [VO(foo="bar"), VO(foo="baz")]
assert field._cast_to_type(value) == value

def test_list_field_with_invalid_value_object(self):
class VO(BaseEntity):
foo = String()

with pytest.raises(IncorrectUsageError) as exc:
List(content_type=ValueObject(VO))

assert exc.value.messages == {
"_value_object": [
"`VO` is not a valid Value Object and cannot be embedded in "
"a Value Object field"
]
}

def test_list_field_with_value_object_string_is_resolved(self, test_domain):
class VO(BaseValueObject):
foo = String()

class Foo(BaseAggregate):
foos = List(content_type=ValueObject("VO"))

test_domain.register(VO)
test_domain.register(Foo)
test_domain.init(traverse=False)

assert declared_fields(Foo)["foos"].content_type.value_object_cls == VO


class TestListFieldAsDictWithDifferentContentTypes:
def test_list_as_dict_with_string_content_type(self):
Expand Down
4 changes: 3 additions & 1 deletion tests/field/test_vo.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,5 +28,7 @@ class User(BaseAggregate):
address = ValueObject(Address)

assert exc.value.messages == {
"_value_object": ["`Address` is not a valid Value Object"]
"_value_object": [
"`Address` is not a valid Value Object and cannot be embedded in a Value Object field"
]
}
44 changes: 44 additions & 0 deletions tests/views/test_view_persistence.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import pytest

from protean import BaseView
from protean.fields import Identifier, Integer, String


class Person(BaseView):
person_id = Identifier(identifier=True)
first_name = String(max_length=50, required=True)
last_name = String(max_length=50)
age = Integer(default=21)


@pytest.fixture(autouse=True)
def register_elements(test_domain):
test_domain.register(Person)
test_domain.init(traverse=False)


class TestViewPersistence:
def test_view_can_be_persisted(self, test_domain):
person = Person(person_id="1", first_name="John", last_name="Doe", age=25)
test_domain.repository_for(Person).add(person)

refreshed_person = test_domain.repository_for(Person).get(person.person_id)

assert refreshed_person.person_id == "1"
assert refreshed_person.first_name == "John"
assert refreshed_person.last_name == "Doe"
assert refreshed_person.age == 25

def test_view_can_be_updated(self, test_domain):
person = Person(person_id="1", first_name="John", last_name="Doe", age=25)
test_domain.repository_for(Person).add(person)

person.first_name = "Jane"
test_domain.repository_for(Person).add(person)

refreshed_person = test_domain.repository_for(Person).get(person.person_id)

assert refreshed_person.person_id == "1"
assert refreshed_person.first_name == "Jane"
assert refreshed_person.last_name == "Doe"
assert refreshed_person.age == 25
1 change: 1 addition & 0 deletions tests/views/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@ def register_elements(test_domain):
test_domain.register(OrderedPerson, order_by="first_name")
test_domain.register(OrderedPersonSubclass, order_by="last_name")
test_domain.register(Building)
test_domain.init(traverse=False)


class TestViewRegistration:
Expand Down

0 comments on commit d6374f1

Please sign in to comment.