Skip to content

Commit

Permalink
[#351] ✨ Use JSON Merge Patch when doing a partial update on records
Browse files Browse the repository at this point in the history
  • Loading branch information
Viicos committed Dec 12, 2023
1 parent 55fbbee commit 7481d91
Show file tree
Hide file tree
Showing 5 changed files with 69 additions and 5 deletions.
6 changes: 6 additions & 0 deletions src/objects/api/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

from .fields import ObjectSlugRelatedField, ObjectTypeField, ObjectUrlField
from .validators import GeometryValidator, IsImmutableValidator, JsonSchemaValidator
from .utils import merge_patch


class ObjectRecordSerializer(serializers.ModelSerializer):
Expand Down Expand Up @@ -129,6 +130,11 @@ def update(self, instance, validated_data):
# in case of PATCH
if "version" not in validated_data:
validated_data["version"] = instance.version
if "data" in validated_data:
# Apply JSON Merge Patch for record data
validated_data["data"] = merge_patch(
instance.data, validated_data["data"]
)

record = super().create(validated_data)
return record
Expand Down
21 changes: 20 additions & 1 deletion src/objects/api/utils.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from datetime import date
from typing import Union
from typing import Any, Dict, Union

from djchoices import DjangoChoices

Expand Down Expand Up @@ -43,3 +43,22 @@ def display_choice_values_for_help_text(choices: DjangoChoices) -> str:
items.append(item)

return "\n".join(items)


def merge_patch(target: Any, patch: Any) -> Dict[str, Any]:
"""An implementation of https://datatracker.ietf.org/doc/html/rfc7396 - JSON Merge Patch"""
if not isinstance(patch, dict):
return patch

if not isinstance(target, dict):
# Ignore the contents and set it to an empty dict
target = {}
for k, v in patch.items():
if v is None:
if k in target:
# remove the key/value pair from target
del target[k]
else:
target[k] = merge_patch(target.get(k), v)

return target
29 changes: 29 additions & 0 deletions src/objects/tests/test_merge_patch.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
from unittest import TestCase

from objects.api.utils import merge_patch


class MergePatchTests(TestCase):
def test_merge_patch(self):

test_data = [
({"a": "b"}, {"a": "c"}, {"a": "c"}),
({"a": "b"}, {"b": "c"}, {"a": "b", "b": "c"}),
({"a": "b"}, {"a": None}, {}),
({"a": "b", "b": "c"}, {"a": None}, {"b": "c"}),
({"a": ["b"]}, {"a": "c"}, {"a": "c"}),
({"a": "c"}, {"a": ["b"]}, {"a": ["b"]}),
({"a": {"b": "c"}}, {"a": {"b": "d", "c": None}}, {"a": {"b": "d"}}),
({"a": [{"b": "c"}]}, {"a": [1]}, {"a": [1]}),
(["a", "b"], ["c", "d"], ["c", "d"]),
({"a": "b"}, ["c"], ["c"]),
({"a": "foo"}, None, None),
({"a": "foo"}, "bar", "bar"),
({"e": None}, {"a": 1}, {"e": None, "a": 1}),
([1, 2], {"a": "b", "c": None}, {"a": "b"}),
({}, {"a": {"bb": {"ccc": None}}}, {"a": {"bb": {}}}),
]

for target, patch, expected in test_data:
with self.subTest():
self.assertEqual(merge_patch(target, patch), expected)
9 changes: 7 additions & 2 deletions src/objects/tests/v1/test_object_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -205,7 +205,10 @@ def test_patch_object_record(self, m):
)

initial_record = ObjectRecordFactory.create(
version=1, object__object_type=self.object_type, start_at=date.today()
version=1,
object__object_type=self.object_type,
start_at=date.today(),
data={"name": "Name", "diameter": 20},
)
object = initial_record.object

Expand All @@ -229,8 +232,10 @@ def test_patch_object_record(self, m):
current_record = object.current_record

self.assertEqual(current_record.version, initial_record.version)
# The actual behavior of the data merging is in test_merge_patch.py:
self.assertEqual(
current_record.data, {"plantDate": "2020-04-12", "diameter": 30}
current_record.data,
{"plantDate": "2020-04-12", "diameter": 30, "name": "Name"},
)
self.assertEqual(current_record.start_at, date(2020, 1, 1))
self.assertEqual(current_record.registration_at, date(2020, 8, 8))
Expand Down
9 changes: 7 additions & 2 deletions src/objects/tests/v2/test_object_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -227,7 +227,10 @@ def test_patch_object_record(self, m):
)

initial_record = ObjectRecordFactory.create(
version=1, object__object_type=self.object_type, start_at=date.today()
version=1,
object__object_type=self.object_type,
start_at=date.today(),
data={"name": "Name", "diameter": 20},
)
object = initial_record.object

Expand All @@ -251,8 +254,10 @@ def test_patch_object_record(self, m):
current_record = object.current_record

self.assertEqual(current_record.version, initial_record.version)
# The actual behavior of the data merging is in test_merge_patch.py:
self.assertEqual(
current_record.data, {"plantDate": "2020-04-12", "diameter": 30}
current_record.data,
{"plantDate": "2020-04-12", "diameter": 30, "name": "Name"},
)
self.assertEqual(current_record.start_at, date(2020, 1, 1))
self.assertEqual(current_record.registration_at, date(2020, 8, 8))
Expand Down

0 comments on commit 7481d91

Please sign in to comment.