From 8d81c1c91336cb554f8fa98fcf664dd7c6b27c69 Mon Sep 17 00:00:00 2001 From: Daniel M Date: Sat, 1 Jul 2023 15:17:45 -0400 Subject: [PATCH] feat:implement json.merge Fix #181 --- .github/workflows/test.yml | 2 +- fakeredis/stack/_json_mixin.py | 32 ++++++++++++++++++++++++++++---- test/test_json/test_json.py | 28 ++++++++++++++++------------ 3 files changed, 45 insertions(+), 17 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 6dff2b70..97b6676d 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -61,7 +61,7 @@ jobs: lupa: true hypothesis: true - python-version: "3.11" - redis-image: "redis/redis-stack-server:7.0.6-RC3" + redis-image: "redis/redis-stack-server:7.2.0-RC2" redis-py: "4.6.0" lupa: true json: true diff --git a/fakeredis/stack/_json_mixin.py b/fakeredis/stack/_json_mixin.py index 573649d4..6edab756 100644 --- a/fakeredis/stack/_json_mixin.py +++ b/fakeredis/stack/_json_mixin.py @@ -7,7 +7,7 @@ # Standard Library Imports import json from json import JSONDecodeError -from typing import Any, Union +from typing import Any, Union, Dict from jsonpath_ng import Root, JSONPath from jsonpath_ng.exceptions import JsonPathParserError @@ -46,6 +46,21 @@ def _path_is_root(path: JSONPath) -> bool: return path == Root() +def _dict_deep_merge(source: Dict, destination: Dict) -> Dict: + """Deep merge of two dictionaries + """ + for key, value in source.items(): + if value is None and key in destination: + del destination[key] + elif isinstance(value, dict): + node = destination.setdefault(key, {}) + _dict_deep_merge(value, node) + else: + destination[key] = value + + return destination + + class JSONObject: """Argument converter for JSON objects.""" @@ -440,6 +455,15 @@ def json_mset(self, *args): JSONCommandsMixin._json_set(key, path_str, value) return helpers.OK - # @command(name="JSON.MERGE", fixed=(Key(), bytes, bytes), repeat=(bytes,), flags=msgs.FLAG_LEAVE_EMPTY_VAL) - # def json_merge(self, key, path_str, mult_by, *args): - # pass # TODO + @command(name="JSON.MERGE", fixed=(Key(), bytes, JSONObject), repeat=(), flags=msgs.FLAG_LEAVE_EMPTY_VAL) + def json_merge(self, key, path_str: bytes, value: JsonType): + path: JSONPath = _parse_jsonpath(path_str) + if key.value is not None and (type(key.value) is not dict) and not _path_is_root(path): + raise SimpleError(msgs.JSON_WRONG_REDIS_TYPE) + matching = path.find(key.value) + for item in matching: + prev_value = item.value if item is not None else dict() + _dict_deep_merge(value, prev_value) + if len(matching) > 0: + key.updated() + return helpers.OK diff --git a/test/test_json/test_json.py b/test/test_json/test_json.py index 3e2a6f97..4284bf3a 100644 --- a/test/test_json/test_json.py +++ b/test/test_json/test_json.py @@ -558,27 +558,31 @@ def test_nummultby(r: redis.Redis): assert r.json().nummultby("doc1", ".b[0].a", 3) == 6 -@testtools.run_test_if_redispy_ver('above', '5') +@testtools.run_test_if_redispy_ver('above', '4.6') +@pytest.mark.min_server('7.1') def test_json_merge(r: redis.Redis): # Test with root path $ - key = "person_data" - r.json().set(key, Path.root_path(), {"person1": {"personal_data": {"name": "John"}}}, ) - r.json().merge(key, Path.root_path(), {"person1": {"personal_data": {"hobbies": "reading"}}}) - assert r.json().get(key) == {"person1": {"personal_data": {"name": "John", "hobbies": "reading"}}} + assert r.json().set("person_data", "$", {"person1": {"personal_data": {"name": "John"}}}, ) + assert r.json().merge("person_data", "$", {"person1": {"personal_data": {"hobbies": "reading"}}}) + assert r.json().get("person_data") == {"person1": {"personal_data": {"name": "John", "hobbies": "reading"}}} # Test with root path path $.person1.personal_data - r.json().merge(key, "$.person1.personal_data", {"country": "Israel"}) - assert r.json().get(key) == { - "person1": {"personal_data": {"name": "John", "hobbies": "reading", "country": "Israel"}}} + assert r.json().merge("person_data", "$.person1.personal_data", {"country": "Israel"}) + assert r.json().get("person_data") == {"person1": { + "personal_data": {"name": "John", "hobbies": "reading", "country": "Israel"} + }} # Test with null value to delete a value - r.json().merge("person_data", "$.person1.personal_data", {"name": None}) - assert r.json().get(key) == {"person1": {"personal_data": {"country": "Israel", "hobbies": "reading"}}} + assert r.json().merge("person_data", "$.person1.personal_data", {"name": None}) + assert r.json().get("person_data") == { + "person1": {"personal_data": {"country": "Israel", "hobbies": "reading"}} + } -@testtools.run_test_if_redispy_ver('above', '5') +@testtools.run_test_if_redispy_ver('above', '4.6') +@pytest.mark.min_server('7.1') def test_mset(r: redis.Redis): - r.json().mset("1", Path.root_path(), 1, "2", Path.root_path(), 2) + r.json().mset([("1", Path.root_path(), 1), ("2", Path.root_path(), 2)]) assert r.json().mget(["1"], Path.root_path()) == [1] assert r.json().mget(["1", "2"], Path.root_path()) == [1, 2]