mirror of
https://github.com/nlohmann/json.git
synced 2026-10-26 15:58:55 +00:00
Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1d4baf429e | ||
|
|
cdbd969fe4 |
No files matched your search
@@ -2,17 +2,27 @@
|
||||
|
||||
```cpp
|
||||
static basic_json diff(const basic_json& source,
|
||||
const basic_json& target);
|
||||
const basic_json& target,
|
||||
const string_t& path = "");
|
||||
```
|
||||
|
||||
Creates a [JSON Patch](http://jsonpatch.com) so that value `source` can be changed into the value `target` by calling
|
||||
[`patch`](patch.md) function.
|
||||
|
||||
For two JSON values `source` and `target`, the following code always yields `#!cpp true`:
|
||||
For two JSON values `source` and `target`, the following code yields `#!cpp true`:
|
||||
```cpp
|
||||
source.patch(diff(source, target)) == target;
|
||||
```
|
||||
|
||||
The equality holds up to [`operator==`](operator_eq.md), so it has the following exceptions:
|
||||
|
||||
- A value that is not equal to itself cannot round-trip. This applies to `NaN` (also nested in an array or object) and
|
||||
to [discarded](is_discarded.md) values: `diff` emits a `replace` operation for them. A `NaN` in the patch is
|
||||
serialized as `#!json null`, so a patch that was dumped and parsed again would set `#!json null`.
|
||||
- Numbers that compare equal produce no operation, even if their types or signs differ: `diff(1, 1.0)`,
|
||||
`diff(1u, 1)` (unsigned and signed integer) and `diff(0.0, -0.0)` all return `#!json []`. The patched value is then
|
||||
the `source` and keeps its number type.
|
||||
|
||||
## Parameters
|
||||
|
||||
`source` (in)
|
||||
@@ -21,6 +31,12 @@ source.patch(diff(source, target)) == target;
|
||||
`target` (in)
|
||||
: JSON value to compare against
|
||||
|
||||
`path` (in)
|
||||
: a prefix that is put verbatim in front of the `path` member of every operation (default: `""`, which addresses the
|
||||
root of the value). The prefix is neither escaped nor validated: `#!cpp diff(1, 2, "abc")` yields an operation with
|
||||
the `path` `abc`, which is not a valid JSON Pointer. Use it with a valid JSON Pointer such as `/root` and escape
|
||||
`~` and `/` in its reference tokens (see [JSON Pointer](../../features/json_pointer.md)).
|
||||
|
||||
## Return value
|
||||
|
||||
a JSON patch to convert the `source` to `target`
|
||||
@@ -31,11 +47,32 @@ Strong guarantee: `source` and `target` are never modified.
|
||||
|
||||
## Complexity
|
||||
|
||||
Linear in the lengths of `source` and `target`.
|
||||
Linear in the total number of values (including nested ones) in `source` and `target`, times the cost of a key-based
|
||||
object operation (looking up a key in an object): logarithmic for [`json`](../json.md), and linear for
|
||||
[`ordered_json`](../ordered_json.md) (see [`ordered_map` complexity](../ordered_map.md#complexity)). For
|
||||
`ordered_json`, diffing two objects with `n` and `m` members therefore takes O((n+m)²) when their keys do not line up
|
||||
(e.g., a key was added or the keys are in a different order). The size of the patch is proportional to the number of
|
||||
operations times the length of their paths, so a small change deep inside a value yields a long `path`.
|
||||
|
||||
## Notes
|
||||
|
||||
Currently, only `remove`, `add`, and `replace` operations are generated.
|
||||
- Currently, only `remove`, `add`, and `replace` operations are generated.
|
||||
- The patch is correct, but not guaranteed to be minimal. For instance, `diff([1,2,3], [0,1,2,3])` yields three
|
||||
`replace` operations and one `add` operation instead of a single insertion.
|
||||
- Differences between two arrays are reported index by index. Elements that were added at the end of the array are added
|
||||
with `add` operations on the path `/-` (appended in order), and elements that were removed are removed with `remove`
|
||||
operations from the highest index down to the lowest, so that the remaining indices stay valid.
|
||||
- For objects, the operations for the members of `source` come first (`replace`, `remove`, or the operations for nested
|
||||
values, in the iteration order of `source`), followed by `add` operations for the members that only `target` has.
|
||||
- For [`ordered_json`](../ordered_json.md), objects with the same keys in a different order are different. As a patch
|
||||
cannot reorder members, `diff` removes all keys of the `source` object and adds them again in the order of the
|
||||
`target`.
|
||||
- If `source` and `target` differ as a whole (e.g., different types), the patch consists of a single `replace`
|
||||
operation for the root of the value, i.e., with the `path` `""` (or the given `path` prefix).
|
||||
- The `value` members of the patch hold copies of values of `target`. A [binary](../../features/binary_values.md) value
|
||||
has no JSON representation: when a patch that contains one is serialized, it becomes an object such as
|
||||
`#!json {"bytes":[1,3],"subtype":null}`, so a parsed patch no longer holds a binary value (and would set that object
|
||||
instead).
|
||||
|
||||
## Examples
|
||||
|
||||
|
||||
@@ -40,7 +40,7 @@ Strong guarantee: if an exception is thrown, there are no changes in the JSON va
|
||||
where an existing element is required (the "path" of "replace", the "from" of "move" and "copy"); example:
|
||||
`"array index '-' (3) is out of range"`.
|
||||
- Throws [`out_of_range.403`](../../home/exceptions.md#jsonexceptionout_of_range403) if a JSON pointer inside the patch
|
||||
could not be resolved successfully in the current JSON value; example: `"key baz not found"`.
|
||||
could not be resolved successfully in the current JSON value; example: `"key 'baz' not found"`.
|
||||
- Throws [`out_of_range.404`](../../home/exceptions.md#jsonexceptionout_of_range404) if a reference token of a JSON
|
||||
pointer inside the patch cannot be resolved, e.g., `-` in a "remove" operation or `1a` for an array; example:
|
||||
`"unresolved reference token '-'"`.
|
||||
@@ -53,7 +53,10 @@ Strong guarantee: if an exception is thrown, there are no changes in the JSON va
|
||||
- Throws [`out_of_range.414`](../../home/exceptions.md#jsonexceptionout_of_range414) if a "move" operation's "from"
|
||||
location is a proper prefix of its "path" location.
|
||||
- Throws [`other_error.501`](../../home/exceptions.md#jsonexceptionother_error501) if "test" operation was
|
||||
unsuccessful.
|
||||
unsuccessful. A "test" operation whose "path" does not exist is unsuccessful, too: the failed lookup is reported as a
|
||||
failed test, even if the operation has no "value" member (which would otherwise be
|
||||
[`parse_error.105`](../../home/exceptions.md#jsonexceptionparse_error105)). Which of the two errors is reported for
|
||||
this combination depends on the order in which the operands of a comparison are evaluated, so do not rely on it.
|
||||
|
||||
## Complexity
|
||||
|
||||
@@ -65,6 +68,10 @@ the JSON value, the complexity can usually be neglected.
|
||||
The application of a patch is atomic: Either all operations succeed and the patched document is returned or an exception
|
||||
is thrown. In any case, the original value is not changed: the patch is applied to a copy of the value.
|
||||
|
||||
Duplicate member names inside an operation object are resolved by the parser before the patch is applied (the last one
|
||||
is kept, see [`object_t`](object_t.md#behavior)), so `#!json {"op": "add", "op": "remove", "path": "/a"}` is a
|
||||
`remove` operation.
|
||||
|
||||
## Examples
|
||||
|
||||
??? example "Example: apply a JSON patch"
|
||||
@@ -108,6 +115,7 @@ is thrown. In any case, the original value is not changed: the patch is applied
|
||||
- Added in version 2.0.0.
|
||||
- Added [`out_of_range.411`](../../home/exceptions.md#jsonexceptionout_of_range411) and stopped relying on an internal assertion when an "add" operation's
|
||||
target location has a non-object/non-array parent in version 3.13.0.
|
||||
This includes a `null` parent, which was previously silently converted to an object, in version 3.13.0.
|
||||
- Added [`out_of_range.413`](../../home/exceptions.md#jsonexceptionout_of_range413) and stopped silently ignoring a "remove" operation whose target
|
||||
location has a non-object/non-array parent in version 3.13.0.
|
||||
- Added [`out_of_range.414`](../../home/exceptions.md#jsonexceptionout_of_range414) and rejected a "move" operation whose "from" location is a proper
|
||||
|
||||
@@ -36,7 +36,7 @@ No guarantees, value may be corrupted by an unsuccessful patch operation.
|
||||
where an existing element is required (the "path" of "replace", the "from" of "move" and "copy"); example:
|
||||
`"array index '-' (3) is out of range"`.
|
||||
- Throws [`out_of_range.403`](../../home/exceptions.md#jsonexceptionout_of_range403) if a JSON pointer inside the patch
|
||||
could not be resolved successfully in the current JSON value; example: `"key baz not found"`.
|
||||
could not be resolved successfully in the current JSON value; example: `"key 'baz' not found"`.
|
||||
- Throws [`out_of_range.404`](../../home/exceptions.md#jsonexceptionout_of_range404) if a reference token of a JSON
|
||||
pointer inside the patch cannot be resolved, e.g., `-` in a "remove" operation or `1a` for an array; example:
|
||||
`"unresolved reference token '-'"`.
|
||||
@@ -49,7 +49,10 @@ No guarantees, value may be corrupted by an unsuccessful patch operation.
|
||||
- Throws [`out_of_range.414`](../../home/exceptions.md#jsonexceptionout_of_range414) if a "move" operation's "from"
|
||||
location is a proper prefix of its "path" location.
|
||||
- Throws [`other_error.501`](../../home/exceptions.md#jsonexceptionother_error501) if "test" operation was
|
||||
unsuccessful.
|
||||
unsuccessful. A "test" operation whose "path" does not exist is unsuccessful, too: the failed lookup is reported as a
|
||||
failed test, even if the operation has no "value" member (which would otherwise be
|
||||
[`parse_error.105`](../../home/exceptions.md#jsonexceptionparse_error105)). Which of the two errors is reported for
|
||||
this combination depends on the order in which the operands of a comparison are evaluated, so do not rely on it.
|
||||
|
||||
## Complexity
|
||||
|
||||
@@ -62,15 +65,9 @@ Unlike [`patch`](patch.md), `patch_inplace` applies the operation "in place" and
|
||||
That makes it faster for large documents by avoiding the copy. However, the JSON value might be corrupted if the
|
||||
function throws an exception.
|
||||
|
||||
The JSON patch may be the JSON value itself (`#!cpp j.patch_inplace(j)`): the operations are then read from a copy of
|
||||
the patch made before the first operation is applied, so the result is the same as that of
|
||||
`#!cpp j = j.patch(j)`, and the exception safety is the same as above.
|
||||
|
||||
!!! danger "Undefined behavior"
|
||||
|
||||
The JSON patch must not be a value stored inside the JSON value (e.g., `#!cpp j.patch_inplace(j["patch"])`), as the
|
||||
operations may change or destroy it while it is read. Pass a copy (`#!cpp j.patch_inplace(json(j["patch"]))`) or use
|
||||
[`patch`](patch.md) instead.
|
||||
Duplicate member names inside an operation object are resolved by the parser before the patch is applied (the last one
|
||||
is kept, see [`object_t`](object_t.md#behavior)), so `#!json {"op": "add", "op": "remove", "path": "/a"}` is a
|
||||
`remove` operation.
|
||||
|
||||
## Examples
|
||||
|
||||
@@ -116,6 +113,7 @@ the patch made before the first operation is applied, so the result is the same
|
||||
- Added in version 3.11.0.
|
||||
- Added [`out_of_range.411`](../../home/exceptions.md#jsonexceptionout_of_range411) and stopped relying on an internal assertion when an "add" operation's
|
||||
target location has a non-object/non-array parent in version 3.13.0.
|
||||
This includes a `null` parent, which was previously silently converted to an object, in version 3.13.0.
|
||||
- Added [`out_of_range.413`](../../home/exceptions.md#jsonexceptionout_of_range413) and stopped silently ignoring a "remove" operation whose target
|
||||
location has a non-object/non-array parent in version 3.13.0.
|
||||
- Added [`out_of_range.414`](../../home/exceptions.md#jsonexceptionout_of_range414) and rejected a "move" operation whose "from" location is a proper
|
||||
@@ -123,4 +121,3 @@ the patch made before the first operation is applied, so the result is the same
|
||||
- Throws [`parse_error.109`](../../home/exceptions.md#jsonexceptionparse_error109) instead of
|
||||
[`out_of_range.404`](../../home/exceptions.md#jsonexceptionout_of_range404) for a one-character array index that is
|
||||
not a digit (e.g., `/x`) in version 3.13.0, as it already did for longer ones.
|
||||
- Applying a JSON value as patch to itself no longer results in undefined behavior in version 3.13.0.
|
||||
@@ -18,8 +18,7 @@ json_pointer& operator/=(std::size_t array_idx)
|
||||
## Parameters
|
||||
|
||||
`ptr` (in)
|
||||
: JSON pointer to append; may be the JSON pointer itself (`#!cpp p /= p`), which then appends a copy of its
|
||||
reference tokens
|
||||
: JSON pointer to append
|
||||
|
||||
`token` (in)
|
||||
: reference token to append
|
||||
@@ -69,7 +68,6 @@ json_pointer& operator/=(std::size_t array_idx)
|
||||
|
||||
## Version history
|
||||
|
||||
1. Added in version 3.6.0. Appending a JSON pointer to itself no longer results in undefined behavior in version
|
||||
3.13.0.
|
||||
1. Added in version 3.6.0.
|
||||
2. Added in version 3.6.0. Changed type of `token` to `string_t` in version 3.11.0.
|
||||
3. Added in version 3.6.0.
|
||||
@@ -13,6 +13,12 @@ to the current JSON value by executing all operations from the patch, yielding t
|
||||
If the document is large and the original value is no longer needed,
|
||||
[`patch_inplace`](../api/basic_json/patch_inplace.md) applies the same operations in place instead.
|
||||
|
||||
!!! note "Duplicate member names"
|
||||
|
||||
Duplicate member names inside an operation object are resolved by the parser before the patch is applied (the last
|
||||
one is kept, see [`object_t`](../api/basic_json/object_t.md#behavior)), so
|
||||
`#!json {"op": "add", "op": "remove", "path": "/a"}` is a `remove` operation.
|
||||
|
||||
??? example "Example: apply a JSON Patch"
|
||||
|
||||
The following code shows how a JSON patch is applied to a value.
|
||||
@@ -41,12 +47,23 @@ flowchart LR
|
||||
|
||||
!!! success "Invariant"
|
||||
|
||||
For two JSON values *source* and *target*, the following code yields always true:
|
||||
For two JSON values *source* and *target*, the following code yields true:
|
||||
|
||||
```cpp
|
||||
source.patch(diff(source, target)) == target;
|
||||
```
|
||||
|
||||
The equality holds up to [`operator==`](../api/basic_json/operator_eq.md), with these exceptions:
|
||||
|
||||
- `NaN` and [discarded](../api/basic_json/is_discarded.md) values are not equal to themselves, so the invariant
|
||||
cannot hold for them. `diff` emits a `replace` operation for such a value, and a `NaN` in a patch is serialized
|
||||
as `#!json null`.
|
||||
- Numbers that compare equal produce no operation even if their types differ: `diff(1, 1.0)`, `diff(1u, 1)`, and
|
||||
`diff(0.0, -0.0)` return `#!json []`, so the patched value keeps the number type of *source*.
|
||||
- A [binary value](binary_values.md) in a patch is serialized as an object
|
||||
(`#!json {"bytes":[...],"subtype":null}`), so a patch that was dumped and parsed again no longer holds a binary
|
||||
value.
|
||||
|
||||
??? example "Example: create a JSON Patch from the difference of two values"
|
||||
|
||||
The following code shows how a JSON patch is created as a diff for two JSON values.
|
||||
|
||||
@@ -1100,6 +1100,8 @@ A JSON Patch `add` operation cannot be applied because the target location's par
|
||||
|
||||
This exception was added in version 3.13.0. Before that, this situation hit an internal assertion (aborting the program in debug builds) or was silently ignored when assertions were disabled.
|
||||
|
||||
A `null` parent is rejected as well. Before version 3.13.0, an `add` into a `null` parent silently converted it to an object (e.g., `add` at `/foo/bar` turned `{"foo":null}` into `{"foo":{"bar":1}}`).
|
||||
|
||||
### json.exception.out_of_range.412
|
||||
|
||||
BSON stores the length of documents, arrays, strings, and binary values in a signed 32-bit integer, and MessagePack
|
||||
|
||||
@@ -243,13 +243,6 @@ class json_pointer
|
||||
/// @sa https://json.nlohmann.me/api/json_pointer/operator_slasheq/
|
||||
json_pointer& operator/=(const json_pointer& ptr)
|
||||
{
|
||||
// insert() must not be given iterators into the vector it inserts
|
||||
// into: append a copy when a pointer is appended to itself
|
||||
if (JSON_HEDLEY_UNLIKELY(&ptr == this))
|
||||
{
|
||||
return *this /= json_pointer(ptr);
|
||||
}
|
||||
|
||||
reference_tokens.insert(reference_tokens.end(),
|
||||
ptr.reference_tokens.begin(),
|
||||
ptr.reference_tokens.end());
|
||||
|
||||
@@ -6950,15 +6950,6 @@ public:
|
||||
/// @sa https://json.nlohmann.me/api/basic_json/patch/
|
||||
void patch_inplace(const basic_json& json_patch)
|
||||
{
|
||||
// the operations below change *this while iterating json_patch: apply
|
||||
// a copy when a value is applied as patch to itself
|
||||
if (JSON_HEDLEY_UNLIKELY(&json_patch == this))
|
||||
{
|
||||
const basic_json json_patch_copy = json_patch;
|
||||
patch_inplace(json_patch_copy);
|
||||
return;
|
||||
}
|
||||
|
||||
basic_json& result = *this;
|
||||
// the valid JSON Patch operations
|
||||
enum class patch_operations {add, remove, replace, move, copy, test, invalid};
|
||||
@@ -7012,7 +7003,6 @@ public:
|
||||
|
||||
switch (parent.m_data.m_type)
|
||||
{
|
||||
case value_t::null:
|
||||
case value_t::object:
|
||||
{
|
||||
// use operator[] to add value
|
||||
@@ -7044,6 +7034,7 @@ public:
|
||||
|
||||
// the parent of an "add" target must be an object or array
|
||||
// (see #4292)
|
||||
case value_t::null:
|
||||
case value_t::string:
|
||||
case value_t::boolean:
|
||||
case value_t::number_integer:
|
||||
|
||||
Generated
+1
-17
@@ -23828,13 +23828,6 @@ class json_pointer
|
||||
/// @sa https://json.nlohmann.me/api/json_pointer/operator_slasheq/
|
||||
json_pointer& operator/=(const json_pointer& ptr)
|
||||
{
|
||||
// insert() must not be given iterators into the vector it inserts
|
||||
// into: append a copy when a pointer is appended to itself
|
||||
if (JSON_HEDLEY_UNLIKELY(&ptr == this))
|
||||
{
|
||||
return *this /= json_pointer(ptr);
|
||||
}
|
||||
|
||||
reference_tokens.insert(reference_tokens.end(),
|
||||
ptr.reference_tokens.begin(),
|
||||
ptr.reference_tokens.end());
|
||||
@@ -42747,15 +42740,6 @@ public:
|
||||
/// @sa https://json.nlohmann.me/api/basic_json/patch/
|
||||
void patch_inplace(const basic_json& json_patch)
|
||||
{
|
||||
// the operations below change *this while iterating json_patch: apply
|
||||
// a copy when a value is applied as patch to itself
|
||||
if (JSON_HEDLEY_UNLIKELY(&json_patch == this))
|
||||
{
|
||||
const basic_json json_patch_copy = json_patch;
|
||||
patch_inplace(json_patch_copy);
|
||||
return;
|
||||
}
|
||||
|
||||
basic_json& result = *this;
|
||||
// the valid JSON Patch operations
|
||||
enum class patch_operations {add, remove, replace, move, copy, test, invalid};
|
||||
@@ -42809,7 +42793,6 @@ public:
|
||||
|
||||
switch (parent.m_data.m_type)
|
||||
{
|
||||
case value_t::null:
|
||||
case value_t::object:
|
||||
{
|
||||
// use operator[] to add value
|
||||
@@ -42841,6 +42824,7 @@ public:
|
||||
|
||||
// the parent of an "add" target must be an object or array
|
||||
// (see #4292)
|
||||
case value_t::null:
|
||||
case value_t::string:
|
||||
case value_t::boolean:
|
||||
case value_t::number_integer:
|
||||
|
||||
+223
-104
@@ -15,6 +15,7 @@ using nlohmann::json;
|
||||
#endif
|
||||
|
||||
#include <fstream>
|
||||
#include <limits>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include "make_test_data_available.hpp"
|
||||
@@ -490,7 +491,9 @@ TEST_CASE("JSON patch")
|
||||
}
|
||||
|
||||
// A.13. Invalid JSON Patch Document
|
||||
// not applicable
|
||||
// not applicable: the library cannot tell the duplicate "op" members
|
||||
// apart, because the parser resolves them (the last one is kept); see
|
||||
// "duplicate member names in an operation" below
|
||||
|
||||
SECTION("A.14. Escape Ordering")
|
||||
{
|
||||
@@ -823,109 +826,6 @@ TEST_CASE("JSON patch")
|
||||
CHECK(doc.at("foo") == "bar");
|
||||
}
|
||||
#endif // !defined(JSON_NOEXCEPTION)
|
||||
|
||||
SECTION("applying a value as patch to itself")
|
||||
{
|
||||
// a JSON patch is an array, so an array of operations can be
|
||||
// applied to itself; the operations are read from the patch as
|
||||
// it was before the first operation, as patch() does it
|
||||
const json copy_op = {{"op", "copy"}, {"from", "/0"}, {"path", "/-"}};
|
||||
json copy_ops = json::array();
|
||||
for (int i = 0; i < 5; ++i)
|
||||
{
|
||||
copy_ops.push_back(copy_op);
|
||||
}
|
||||
|
||||
json expected_copies = copy_ops;
|
||||
for (int i = 0; i < 5; ++i)
|
||||
{
|
||||
expected_copies.push_back(copy_op);
|
||||
}
|
||||
|
||||
SECTION("copy operations without spare capacity")
|
||||
{
|
||||
json doc = copy_ops;
|
||||
doc.get_ref<json::array_t&>().shrink_to_fit();
|
||||
CHECK(doc.patch(doc) == expected_copies);
|
||||
|
||||
doc.patch_inplace(doc);
|
||||
CHECK(doc == expected_copies);
|
||||
}
|
||||
|
||||
SECTION("copy operations with spare capacity")
|
||||
{
|
||||
json doc = copy_ops;
|
||||
doc.get_ref<json::array_t&>().reserve(100);
|
||||
CHECK(doc.patch(doc) == expected_copies);
|
||||
|
||||
doc.patch_inplace(doc);
|
||||
CHECK(doc == expected_copies);
|
||||
}
|
||||
|
||||
SECTION("remove operations")
|
||||
{
|
||||
json doc = R"([
|
||||
{"op": "remove", "path": "/0"},
|
||||
{"op": "remove", "path": "/0"},
|
||||
{"op": "remove", "path": "/0"}
|
||||
])"_json;
|
||||
CHECK(doc.patch(doc) == json::array());
|
||||
|
||||
doc.patch_inplace(doc);
|
||||
CHECK(doc == json::array());
|
||||
}
|
||||
|
||||
SECTION("operations that read a changed element")
|
||||
{
|
||||
// the second operation is still a "test" operation, although
|
||||
// the first one replaced it in the document
|
||||
json doc = R"([
|
||||
{"op": "replace", "path": "/1", "value": 1},
|
||||
{"op": "test", "path": "/1", "value": 1},
|
||||
{"op": "add", "path": "/-", "value": 2}
|
||||
])"_json;
|
||||
const json expected = R"([
|
||||
{"op": "replace", "path": "/1", "value": 1},
|
||||
1,
|
||||
{"op": "add", "path": "/-", "value": 2},
|
||||
2
|
||||
])"_json;
|
||||
CHECK(doc.patch(doc) == expected);
|
||||
|
||||
doc.patch_inplace(doc);
|
||||
CHECK(doc == expected);
|
||||
}
|
||||
|
||||
SECTION("replacing the root")
|
||||
{
|
||||
json doc = R"([
|
||||
{"op": "add", "path": "", "value": [1, 2]},
|
||||
{"op": "add", "path": "/-", "value": 3},
|
||||
{"op": "replace", "path": "", "value": {"a": 4}}
|
||||
])"_json;
|
||||
const json expected = {{"a", 4}};
|
||||
CHECK(doc.patch(doc) == expected);
|
||||
|
||||
doc.patch_inplace(doc);
|
||||
CHECK(doc == expected);
|
||||
}
|
||||
|
||||
#if !defined(JSON_NOEXCEPTION)
|
||||
SECTION("failing operation")
|
||||
{
|
||||
// the operations before the failing one remain applied
|
||||
json doc = R"([
|
||||
{"op": "add", "path": "/-", "value": 1},
|
||||
{"op": "test", "path": "/0/op", "value": "remove"}
|
||||
])"_json;
|
||||
CHECK_THROWS_AS(doc.patch(doc), json::other_error&);
|
||||
|
||||
CHECK_THROWS_AS(doc.patch_inplace(doc), json::other_error&);
|
||||
CHECK(doc.size() == 3);
|
||||
CHECK(doc.back() == 1);
|
||||
}
|
||||
#endif // !defined(JSON_NOEXCEPTION)
|
||||
}
|
||||
}
|
||||
|
||||
SECTION("errors")
|
||||
@@ -1618,6 +1518,24 @@ TEST_CASE("JSON patch - add to a primitive parent (regression #4292)")
|
||||
#endif
|
||||
}
|
||||
|
||||
SECTION("null parent")
|
||||
{
|
||||
json const doc = {{"foo", nullptr}};
|
||||
json const patch = {{{"op", "add"}, {"path", "/foo/bar"}, {"value", 1}}};
|
||||
#if JSON_DIAGNOSTICS
|
||||
CHECK_THROWS_WITH_AS(doc.patch(patch), "[json.exception.out_of_range.411] (/foo) cannot add value: the JSON Patch 'add' target's parent is of type null, but must be an object or array", json::out_of_range&);
|
||||
#else
|
||||
CHECK_THROWS_WITH_AS(doc.patch(patch), "[json.exception.out_of_range.411] cannot add value: the JSON Patch 'add' target's parent is of type null, but must be an object or array", json::out_of_range&);
|
||||
#endif
|
||||
}
|
||||
|
||||
SECTION("null document")
|
||||
{
|
||||
json const doc = nullptr;
|
||||
json const patch = {{{"op", "add"}, {"path", "/-"}, {"value", 1}}};
|
||||
CHECK_THROWS_WITH_AS(doc.patch(patch), "[json.exception.out_of_range.411] cannot add value: the JSON Patch 'add' target's parent is of type null, but must be an object or array", json::out_of_range&);
|
||||
}
|
||||
|
||||
SECTION("number parent")
|
||||
{
|
||||
json const doc = {{"foo", 1}};
|
||||
@@ -2206,3 +2124,204 @@ TEST_CASE_TEMPLATE("JSON patch - diff round trip on hand-made documents", Json,
|
||||
roundtrip(Json(1), Json::parse("[1]"));
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("JSON patch - behavior documented in the API reference")
|
||||
{
|
||||
SECTION("duplicate member names in an operation")
|
||||
{
|
||||
// RFC 6902, A.13: the parser resolves the duplicates (the last one
|
||||
// is kept), so this is a "remove" and not an "add"
|
||||
const json patch = json::parse(R"([{"op": "add", "op": "remove", "path": "/a"}])");
|
||||
CHECK(patch == json::parse(R"([{"op": "remove", "path": "/a"}])"));
|
||||
CHECK(json({{"a", 1}, {"b", 2}}).patch(patch) == json({{"b", 2}}));
|
||||
}
|
||||
|
||||
SECTION("add with a leading zero in an array index")
|
||||
{
|
||||
const json patch = R"([{"op": "add", "path": "/01", "value": 1}])"_json;
|
||||
json doc = json::array();
|
||||
#if !defined(JSON_NOEXCEPTION)
|
||||
CHECK_THROWS_AS(doc.patch(patch), json::parse_error&);
|
||||
CHECK_THROWS_AS(doc.patch_inplace(patch), json::parse_error&);
|
||||
#endif
|
||||
CHECK(doc == json::array());
|
||||
}
|
||||
|
||||
SECTION("test compares numbers by value")
|
||||
{
|
||||
const json doc = R"({"a": 1, "b": 0, "c": 1.5})"_json;
|
||||
CHECK(doc.patch(R"([{"op": "test", "path": "/a", "value": 1.0}])"_json) == doc);
|
||||
CHECK(doc.patch(R"([{"op": "test", "path": "/b", "value": -0.0}])"_json) == doc);
|
||||
CHECK(doc.patch(R"([{"op": "test", "path": "/c", "value": 1.5}])"_json) == doc);
|
||||
CHECK_THROWS_AS(doc.patch(R"([{"op": "test", "path": "/a", "value": 2}])"_json), json::other_error&);
|
||||
CHECK_THROWS_AS(doc.patch(R"([{"op": "test", "path": "/a", "value": "1"}])"_json), json::other_error&);
|
||||
}
|
||||
|
||||
SECTION("test ignores the order of object members, but not the order of array elements")
|
||||
{
|
||||
const json doc = R"({"o": {"x": 1, "y": 2}, "a": [1, 2]})"_json;
|
||||
CHECK(doc.patch(R"([{"op": "test", "path": "/o", "value": {"y": 2, "x": 1}}])"_json) == doc);
|
||||
CHECK(doc.patch(R"([{"op": "test", "path": "/a", "value": [1, 2]}])"_json) == doc);
|
||||
CHECK_THROWS_AS(doc.patch(R"([{"op": "test", "path": "/a", "value": [2, 1]}])"_json), json::other_error&);
|
||||
}
|
||||
|
||||
SECTION("test on a path that does not exist")
|
||||
{
|
||||
const json doc = R"({"a": 1})"_json;
|
||||
// with a "value" member, the failed lookup is a failed test
|
||||
CHECK_THROWS_AS(doc.patch(R"([{"op": "test", "path": "/b", "value": 1}])"_json), json::other_error&);
|
||||
CHECK_THROWS_AS(doc.patch(R"([{"op": "test", "path": "/b", "value": null}])"_json), json::other_error&);
|
||||
// without it, an exception is thrown, too; which one depends on the
|
||||
// order of evaluation of the operands of ==, so it is not checked
|
||||
CHECK_THROWS(doc.patch(R"([{"op": "test", "path": "/b"}])"_json));
|
||||
}
|
||||
|
||||
SECTION("patch() has the strong guarantee")
|
||||
{
|
||||
const json doc = R"({"a": [1, 2], "o": {"k": "v"}})"_json;
|
||||
const json before = doc;
|
||||
|
||||
const std::vector<json> failing =
|
||||
{
|
||||
// remove of a member that does not exist
|
||||
R"([{"op": "add", "path": "/n", "value": 1}, {"op": "remove", "path": "/missing"}])"_json,
|
||||
// replace of a member that does not exist
|
||||
R"([{"op": "remove", "path": "/a/0"}, {"op": "replace", "path": "/missing", "value": 1}])"_json,
|
||||
// move from a member that does not exist
|
||||
R"([{"op": "add", "path": "/n", "value": 1}, {"op": "move", "from": "/missing", "path": "/m"}])"_json,
|
||||
// copy from a member that does not exist
|
||||
R"([{"op": "add", "path": "/n", "value": 1}, {"op": "copy", "from": "/missing", "path": "/m"}])"_json,
|
||||
// out-of-range array index
|
||||
R"([{"op": "add", "path": "/n", "value": 1}, {"op": "add", "path": "/a/5", "value": 1}])"_json,
|
||||
// failing test after a successful operation
|
||||
R"([{"op": "remove", "path": "/o"}, {"op": "test", "path": "/a", "value": []}])"_json,
|
||||
};
|
||||
for (const auto& patch : failing)
|
||||
{
|
||||
CAPTURE(patch.dump())
|
||||
CHECK_THROWS_AS(doc.patch(patch), json::exception&);
|
||||
CHECK(doc == before);
|
||||
|
||||
json copy = doc;
|
||||
CHECK_THROWS_AS(copy.patch(patch), json::exception&);
|
||||
CHECK(copy == before);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("JSON patch - diff behavior documented in the API reference")
|
||||
{
|
||||
SECTION("path is a verbatim prefix")
|
||||
{
|
||||
// not escaped and not validated
|
||||
CHECK(json::diff(1, 2) == R"([{"op": "replace", "path": "", "value": 2}])"_json);
|
||||
CHECK(json::diff(1, 2, "abc") == R"([{"op": "replace", "path": "abc", "value": 2}])"_json);
|
||||
CHECK(json::diff(1, 2, "/root") == R"([{"op": "replace", "path": "/root", "value": 2}])"_json);
|
||||
// an escaped token in a non-root prefix is kept as given
|
||||
CHECK(json::diff(R"({"a": 1})"_json, R"({"a": 2})"_json, "/x~1y/~0")
|
||||
== R"([{"op": "replace", "path": "/x~1y/~0/a", "value": 2}])"_json);
|
||||
CHECK(json::diff(R"({"a": 1})"_json, R"({"a": 2})"_json, "/p")
|
||||
== R"([{"op": "replace", "path": "/p/a", "value": 2}])"_json);
|
||||
}
|
||||
|
||||
SECTION("keys are escaped in the paths")
|
||||
{
|
||||
const json source = json::object();
|
||||
const json target =
|
||||
{
|
||||
{"", 0}, {"/", 1}, {"~", 2}, {"~1", 3}, {"~01", 4}
|
||||
};
|
||||
const json patch = json::diff(source, target);
|
||||
REQUIRE(patch.size() == 5);
|
||||
|
||||
// the order of the operations is not checked
|
||||
json paths = json::object();
|
||||
for (const auto& op : patch)
|
||||
{
|
||||
CHECK(op["op"] == "add");
|
||||
paths[op["path"].get<std::string>()] = op["value"];
|
||||
}
|
||||
CHECK(paths == json({{"/", 0}, {"/~1", 1}, {"/~0", 2}, {"/~01", 3}, {"/~001", 4}}));
|
||||
CHECK(source.patch(patch) == target);
|
||||
}
|
||||
|
||||
SECTION("NaN is not equal to itself")
|
||||
{
|
||||
const json nan = std::numeric_limits<json::number_float_t>::quiet_NaN();
|
||||
const json patch = json::diff(nan, nan);
|
||||
CHECK(patch.size() == 1);
|
||||
CHECK(patch[0]["op"] == "replace");
|
||||
CHECK(patch[0]["path"] == "");
|
||||
CHECK(patch[0]["value"].is_number_float());
|
||||
CHECK(nan.patch(patch) != nan);
|
||||
|
||||
// a NaN in a patch is serialized as null
|
||||
CHECK(json::parse(patch.dump())[0]["value"].is_null());
|
||||
|
||||
const json nested = {{"a", {1, nan}}};
|
||||
const json nested_patch = json::diff(nested, nested);
|
||||
CHECK(nested_patch.size() == 1);
|
||||
CHECK(nested_patch[0]["path"] == "/a/1");
|
||||
|
||||
// infinity is equal to itself
|
||||
const json inf = std::numeric_limits<json::number_float_t>::infinity();
|
||||
CHECK(json::diff(inf, inf).empty());
|
||||
}
|
||||
|
||||
SECTION("discarded values are not equal to themselves")
|
||||
{
|
||||
const json discarded(json::value_t::discarded);
|
||||
const json patch = json::diff(discarded, discarded);
|
||||
CHECK(patch.size() == 1);
|
||||
CHECK(patch[0]["op"] == "replace");
|
||||
CHECK(patch[0]["path"] == "");
|
||||
CHECK(patch[0]["value"].is_discarded());
|
||||
CHECK(discarded.patch(patch) != discarded);
|
||||
}
|
||||
|
||||
SECTION("numbers that compare equal produce no operation")
|
||||
{
|
||||
CHECK(json::diff(json(1), json(1.0)).empty());
|
||||
CHECK(json::diff(json(1.0), json(1)).empty());
|
||||
CHECK(json::diff(json(1U), json(1)).empty());
|
||||
CHECK(json::diff(json(1), json(1U)).empty());
|
||||
CHECK(json::diff(json(0.0), json(-0.0)).empty());
|
||||
|
||||
// the patched value keeps the number type of the source
|
||||
const json source = 1;
|
||||
const json patched = source.patch(json::diff(source, json(1.0)));
|
||||
CHECK(patched.is_number_integer());
|
||||
CHECK(patched == json(1.0));
|
||||
|
||||
// different values are replaced
|
||||
CHECK(json::diff(json(1), json(2.0)) == R"([{"op": "replace", "path": "", "value": 2.0}])"_json);
|
||||
CHECK(json::diff(json(1), json(-1)) == R"([{"op": "replace", "path": "", "value": -1}])"_json);
|
||||
CHECK(json::diff(json(1.0), json(1.5)) == R"([{"op": "replace", "path": "", "value": 1.5}])"_json);
|
||||
CHECK(json::diff(json(1), json("1")) == R"([{"op": "replace", "path": "", "value": "1"}])"_json);
|
||||
}
|
||||
|
||||
SECTION("binary values")
|
||||
{
|
||||
const json a = json::binary({1, 2});
|
||||
const json b = json::binary({1, 3});
|
||||
CHECK(json::diff(a, a).empty());
|
||||
|
||||
const json patch = json::diff(a, b);
|
||||
CHECK(patch.size() == 1);
|
||||
CHECK(patch[0]["op"] == "replace");
|
||||
CHECK(patch[0]["path"] == "");
|
||||
CHECK(patch[0]["value"].is_binary());
|
||||
CHECK(a.patch(patch) == b);
|
||||
|
||||
// serialized, the binary value is an object
|
||||
const json parsed = json::parse(patch.dump());
|
||||
CHECK(parsed[0]["value"].is_object());
|
||||
CHECK(parsed[0]["value"] == R"({"bytes": [1, 3], "subtype": null})"_json);
|
||||
}
|
||||
|
||||
SECTION("a difference as a whole is a replace of the root")
|
||||
{
|
||||
CHECK(json::diff(json::array({1}), json::object()) == R"([{"op": "replace", "path": "", "value": {}}])"_json);
|
||||
CHECK(json::diff(json(nullptr), json::array()) == R"([{"op": "replace", "path": "", "value": []}])"_json);
|
||||
}
|
||||
}
|
||||
@@ -779,44 +779,6 @@ TEST_CASE("JSON pointers")
|
||||
CHECK(ptr.to_string() == "/object/~1");
|
||||
}
|
||||
|
||||
SECTION("appending a pointer to itself")
|
||||
{
|
||||
// operator/= must not pass iterators into its own token vector to
|
||||
// insert(); each pointer is appended to itself and must yield the
|
||||
// pointer whose string representation is the original one twice
|
||||
const std::vector<std::string> pointers =
|
||||
{
|
||||
"",
|
||||
"/",
|
||||
"/a",
|
||||
"/a/b",
|
||||
"/foo/0/-",
|
||||
"/a~1b/~0c/~01",
|
||||
"/1/2/3/4/5/6/7/8/9/10/11/12/13/14/15/16/17"
|
||||
};
|
||||
|
||||
for (const auto& s : pointers)
|
||||
{
|
||||
CAPTURE(s)
|
||||
json::json_pointer ptr(s);
|
||||
const json::json_pointer expected(s + s);
|
||||
|
||||
json::json_pointer& ret = (ptr /= ptr);
|
||||
CHECK(&ret == &ptr);
|
||||
CHECK(ptr == expected);
|
||||
CHECK(ptr.to_string() == s + s);
|
||||
|
||||
// appending to itself again doubles the pointer once more
|
||||
ptr /= ptr;
|
||||
CHECK(ptr.to_string() == s + s + s + s);
|
||||
|
||||
// operator/ appends to a copy and leaves the operand unchanged
|
||||
const json::json_pointer original(s);
|
||||
CHECK((original / original) == expected);
|
||||
CHECK(original.to_string() == s);
|
||||
}
|
||||
}
|
||||
|
||||
SECTION("equality comparison")
|
||||
{
|
||||
std::string ptr_string{"/foo/bar"};
|
||||
|
||||
Reference in new issue
Block a user