Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions conformance/json-patch-vectors.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
{ "name": "move", "doc": {"a": 1, "b": 2}, "patch": [{"op": "move", "from": "/a", "path": "/c"}], "expected": {"b": 2, "c": 1} },
{ "name": "copy", "doc": {"a": {"x": 1}}, "patch": [{"op": "copy", "from": "/a", "path": "/b"}], "expected": {"a": {"x": 1}, "b": {"x": 1}} },
{ "name": "test then replace", "doc": {"a": 1}, "patch": [{"op": "test", "path": "/a", "value": 1}, {"op": "replace", "path": "/a", "value": 2}], "expected": {"a": 2} },
{ "name": "test bool true", "doc": {"a": true}, "patch": [{"op": "test", "path": "/a", "value": true}, {"op": "replace", "path": "/a", "value": false}], "expected": {"a": false} },
{ "name": "test nested bool", "doc": {"a": {"b": [true, false]}}, "patch": [{"op": "test", "path": "/a", "value": {"b": [true, false]}}], "expected": {"a": {"b": [true, false]}} },
{ "name": "escaped ~1 (slash) key", "doc": {"a/b": 1}, "patch": [{"op": "replace", "path": "/a~1b", "value": 2}], "expected": {"a/b": 2} },
{ "name": "escaped ~0 (tilde) key", "doc": {"a~b": 1}, "patch": [{"op": "replace", "path": "/a~0b", "value": 2}], "expected": {"a~b": 2} },
{ "name": "nested replace", "doc": {"a": {"b": [0, 0]}}, "patch": [{"op": "replace", "path": "/a/b/1", "value": 9}], "expected": {"a": {"b": [0, 9]}} },
Expand All @@ -27,6 +29,11 @@
{ "name": "remove array out of range", "doc": {"a": [1]}, "patch": [{"op": "remove", "path": "/a/5"}] },
{ "name": "add into missing intermediate", "doc": {"a": {}}, "patch": [{"op": "add", "path": "/a/b/c", "value": 1}] },
{ "name": "test mismatch", "doc": {"a": 1}, "patch": [{"op": "test", "path": "/a", "value": 2}] },
{ "name": "test bool true vs int 1", "doc": {"a": 1}, "patch": [{"op": "test", "path": "/a", "value": true}] },
{ "name": "test int 1 vs bool true", "doc": {"a": true}, "patch": [{"op": "test", "path": "/a", "value": 1}] },
{ "name": "test bool false vs int 0", "doc": {"a": 0}, "patch": [{"op": "test", "path": "/a", "value": false}] },
{ "name": "test int 0 vs bool false", "doc": {"a": false}, "patch": [{"op": "test", "path": "/a", "value": 0}] },
{ "name": "test nested bool vs int", "doc": {"a": {"b": 1}}, "patch": [{"op": "test", "path": "/a", "value": {"b": true}}] },
{ "name": "move into own child", "doc": {"a": {"b": 1}}, "patch": [{"op": "move", "from": "/a", "path": "/a/c"}] },
{ "name": "unknown op", "doc": {"a": 1}, "patch": [{"op": "frobnicate", "path": "/a", "value": 2}] },
{ "name": "remove root", "doc": {"a": 1}, "patch": [{"op": "remove", "path": ""}] },
Expand Down
25 changes: 24 additions & 1 deletion packages/chap-analytics/chap_analytics/_patch.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,29 @@ def _get(doc: Any, path: str) -> Any:
return parent[key]


def _json_equal(a: Any, b: Any) -> bool:
"""JSON-type-aware deep equality for the RFC 6902 ``test`` operation.

Python's ``==`` treats ``True == 1`` and ``False == 0`` as equal because
``bool`` is a subclass of ``int``. JSON distinguishes them: ``true`` is a
boolean, ``1`` is a number. The TypeScript reference's ``deepEqual``
handles this via ``typeof``; this function is the Python equivalent.
"""
if isinstance(a, bool) != isinstance(b, bool):
return False
if a is None or b is None:
return a is b
if isinstance(a, dict):
if not isinstance(b, dict) or len(a) != len(b):
return False
return all(k in b and _json_equal(a[k], b[k]) for k in a)
if isinstance(a, list):
if not isinstance(b, list) or len(a) != len(b):
return False
return all(_json_equal(x, y) for x, y in zip(a, b))
return a == b


def _apply_one(doc: Any, op: dict) -> Any: # noqa: C901 - one branch per RFC 6902 operation
kind = op.get("op")
path = op.get("path", "")
Expand Down Expand Up @@ -180,7 +203,7 @@ def _apply_one(doc: Any, op: dict) -> Any: # noqa: C901 - one branch per RFC 69
if kind == "test":
if "value" not in op:
raise PatchError("'test' requires 'value'")
if _get(doc, path) != op["value"]:
if not _json_equal(_get(doc, path), op["value"]):
raise PatchError(f"'test' failed at {path}")
return doc

Expand Down
25 changes: 24 additions & 1 deletion packages/coordinator-py/chap_coordinator/patch.py
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,29 @@ def _get(doc: Any, path: str) -> Any:
return parent[key]


def _json_equal(a: Any, b: Any) -> bool:
"""JSON-type-aware deep equality for the RFC 6902 ``test`` operation.

Python's ``==`` treats ``True == 1`` and ``False == 0`` as equal because
``bool`` is a subclass of ``int``. JSON distinguishes them: ``true`` is a
boolean, ``1`` is a number. The TypeScript reference's ``deepEqual``
handles this via ``typeof``; this function is the Python equivalent.
"""
if isinstance(a, bool) != isinstance(b, bool):
return False
if a is None or b is None:
return a is b
if isinstance(a, dict):
if not isinstance(b, dict) or len(a) != len(b):
return False
return all(k in b and _json_equal(a[k], b[k]) for k in a)
if isinstance(a, list):
if not isinstance(b, list) or len(a) != len(b):
return False
return all(_json_equal(x, y) for x, y in zip(a, b))
return a == b


def _apply_one(doc: Any, op: dict) -> Any:
"""Apply a single op in place, returning the document."""
kind = op.get("op")
Expand Down Expand Up @@ -212,7 +235,7 @@ def _apply_one(doc: Any, op: dict) -> Any:
if "value" not in op:
raise PatchError("'test' requires 'value'")
actual = _get(doc, path)
if actual != op["value"]:
if not _json_equal(actual, op["value"]):
raise PatchError(f"'test' failed at {path}")
return doc

Expand Down
Loading