From 90ab253208112a1c26d79c1c1f66cdf86378efc4 Mon Sep 17 00:00:00 2001 From: Subhash Bhushan Date: Sat, 2 Oct 2021 10:21:12 -0700 Subject: [PATCH] Fix embedded VO's to_dict output `to_dict` of embedded VO was not extracting dict-appropriate values from enclosed fields. This commit fixes the issue and adds relevenat test cases. --- src/protean/core/entity.py | 4 ++- src/protean/fields/embedded.py | 20 +++++++----- tests/value_object/test_to_dict.py | 50 ++++++++++++++++++++++++++++++ 3 files changed, 66 insertions(+), 8 deletions(-) create mode 100644 tests/value_object/test_to_dict.py diff --git a/src/protean/core/entity.py b/src/protean/core/entity.py index 6e627141..25623ecd 100644 --- a/src/protean/core/entity.py +++ b/src/protean/core/entity.py @@ -391,7 +391,9 @@ def to_dict(self): getattr(self, field_name, None) ) elif isinstance(field_obj, ValueObject): - field_values.update(field_obj.as_dict(getattr(self, field_name, None))) + value = field_obj.as_dict(getattr(self, field_name, None)) + if value: + field_values.update(value) return field_values diff --git a/src/protean/fields/embedded.py b/src/protean/fields/embedded.py index fa62013d..f633c022 100644 --- a/src/protean/fields/embedded.py +++ b/src/protean/fields/embedded.py @@ -8,13 +8,13 @@ class _ShadowField(Field): """Shadow Attribute Field to back Value Object Fields""" - def __init__(self, owner, field_name, field_type, **kwargs): + def __init__(self, owner, field_name, field_obj, **kwargs): """Preserve link to owner, and original field type for later reference""" super().__init__(**kwargs) self.owner = owner self.field_name = field_name - self.field_type = field_type + self.field_obj = field_obj def __set__(self, instance, value): """Override `__set__` to update owner field and silently fail to update values. @@ -52,7 +52,7 @@ def __init__(self, value_object_cls, *args, **kwargs): self.embedded_fields[field_name] = _ShadowField( self, field_name, - field_obj.__class__, + field_obj, # FIXME Pass all other kwargs here # Because we want the shadow field to mimic the behavior of the actual field # Which means that ShadowField somehow has to become an Integer, Float, String, etc. @@ -102,10 +102,16 @@ def _cast_to_type(self, value): def as_dict(self, value): """Return JSON-compatible value of self""" - return { - field_obj.attribute_name: getattr(value, field_name) - for field_name, field_obj in self.embedded_fields.items() - } + return ( + { + shadow_field_obj.attribute_name: shadow_field_obj.field_obj.as_dict( + getattr(value, field_name, None) + ) + for field_name, shadow_field_obj in self.embedded_fields.items() + } + if value + else None + ) def __set__(self, instance, value): """Override `__set__` to coordinate between value object and its embedded fields""" diff --git a/tests/value_object/test_to_dict.py b/tests/value_object/test_to_dict.py new file mode 100644 index 00000000..f4df79cf --- /dev/null +++ b/tests/value_object/test_to_dict.py @@ -0,0 +1,50 @@ +from datetime import datetime +from protean.core.value_object import BaseValueObject + +from protean.fields.basic import DateTime, String +from protean.fields.embedded import ValueObject +from protean import BaseAggregate + + +class SimpleVO(BaseValueObject): + foo = String() + bar = String() + + +class VOWithDateTime(BaseValueObject): + foo = String() + now = DateTime() + + +class SimpleVOEntity(BaseAggregate): + vo = ValueObject(SimpleVO) + + +class EntityWithDateTimeVO(BaseAggregate): + vo = ValueObject(VOWithDateTime) + + +class TestAsDict: + def test_empty_simple_vo(self): + simple = SimpleVOEntity(id=12) + assert simple.to_dict() == {"id": 12} + + def test_simple_vo_dict(self): + vo = SimpleVO(foo="foo", bar="bar") + assert vo.to_dict() == {"foo": "foo", "bar": "bar"} + + def test_embedded_simple_vo(self): + vo = SimpleVO(foo="foo", bar="bar") + simple = SimpleVOEntity(id=12, vo=vo) + assert simple.to_dict() == {"id": 12, "vo_foo": "foo", "vo_bar": "bar"} + + def test_datetime_vo_dict(self): + now = datetime.utcnow() + vo = VOWithDateTime(foo="foo", now=now) + assert vo.to_dict() == {"foo": "foo", "now": str(now)} + + def test_embedded_datetime_vo(self): + now = datetime.utcnow() + vo = VOWithDateTime(foo="foo", now=now) + simple = EntityWithDateTimeVO(id=12, vo=vo) + assert simple.to_dict() == {"id": 12, "vo_foo": "foo", "vo_now": str(now)}