mirror of
https://github.com/nlohmann/json.git
synced 2026-09-08 17:27:59 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f7f778c5f4 | ||
|
|
47bd3e67cb | ||
|
|
95c1df59ef | ||
|
|
f3f7a16dfc | ||
|
|
f826b1e9f9 | ||
|
|
eca15ca0e5 |
@@ -34,6 +34,10 @@ Strong guarantee: if an exception is thrown, there are no changes in the JSON va
|
|||||||
("add", "remove", "move")
|
("add", "remove", "move")
|
||||||
- Throws [`out_of_range.411`](../../home/exceptions.md#jsonexceptionout_of_range411) if an "add" operation's target
|
- Throws [`out_of_range.411`](../../home/exceptions.md#jsonexceptionout_of_range411) if an "add" operation's target
|
||||||
location has a parent that is neither an object nor an array.
|
location has a parent that is neither an object nor an array.
|
||||||
|
- Throws [`out_of_range.413`](../../home/exceptions.md#jsonexceptionout_of_range413) if a "remove" operation's target
|
||||||
|
location has a parent that is neither an object nor an array.
|
||||||
|
- 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
|
- Throws [`other_error.501`](../../home/exceptions.md#jsonexceptionother_error501) if "test" operation was
|
||||||
unsuccessful.
|
unsuccessful.
|
||||||
|
|
||||||
@@ -75,3 +79,7 @@ is thrown. In any case, the original value is not changed: the patch is applied
|
|||||||
- Added in version 2.0.0.
|
- 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
|
- 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.
|
target location has a non-object/non-array parent 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
|
||||||
|
prefix of its "path" location instead of silently producing a corrupted result in version 3.13.0.
|
||||||
|
|||||||
@@ -30,6 +30,10 @@ No guarantees, value may be corrupted by an unsuccessful patch operation.
|
|||||||
("add", "remove", "move")
|
("add", "remove", "move")
|
||||||
- Throws [`out_of_range.411`](../../home/exceptions.md#jsonexceptionout_of_range411) if an "add" operation's target
|
- Throws [`out_of_range.411`](../../home/exceptions.md#jsonexceptionout_of_range411) if an "add" operation's target
|
||||||
location has a parent that is neither an object nor an array.
|
location has a parent that is neither an object nor an array.
|
||||||
|
- Throws [`out_of_range.413`](../../home/exceptions.md#jsonexceptionout_of_range413) if a "remove" operation's target
|
||||||
|
location has a parent that is neither an object nor an array.
|
||||||
|
- 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
|
- Throws [`other_error.501`](../../home/exceptions.md#jsonexceptionother_error501) if "test" operation was
|
||||||
unsuccessful.
|
unsuccessful.
|
||||||
|
|
||||||
@@ -72,3 +76,7 @@ function throws an exception.
|
|||||||
- Added in version 3.11.0.
|
- 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
|
- 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.
|
target location has a non-object/non-array parent 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
|
||||||
|
prefix of its "path" location instead of silently producing a corrupted result in version 3.13.0.
|
||||||
|
|||||||
@@ -933,6 +933,34 @@ BSON stores the length of documents, arrays, strings, and binary values in a sig
|
|||||||
[`to_bson`](../api/basic_json/to_bson.md) produced documents with negative length prefixes that
|
[`to_bson`](../api/basic_json/to_bson.md) produced documents with negative length prefixes that
|
||||||
[`from_bson`](../api/basic_json/from_bson.md) rejected.
|
[`from_bson`](../api/basic_json/from_bson.md) rejected.
|
||||||
|
|
||||||
|
### json.exception.out_of_range.413
|
||||||
|
|
||||||
|
A JSON Patch `remove` operation cannot be applied because the target location's parent is neither an object nor an array. Per [RFC 6902](https://datatracker.ietf.org/doc/html/rfc6902), a `remove` target must reference a member of an existing object or an element of an existing array; a primitive value (string, number, boolean, etc.) or `null` has no members or elements to remove.
|
||||||
|
|
||||||
|
!!! failure "Example message"
|
||||||
|
|
||||||
|
```
|
||||||
|
cannot remove value: the JSON Patch 'remove' target's parent is of type number, but must be an object or array
|
||||||
|
```
|
||||||
|
|
||||||
|
!!! note
|
||||||
|
|
||||||
|
This exception was added in version 3.13.0. Before that, this situation was silently ignored (the `remove` operation had no effect).
|
||||||
|
|
||||||
|
### json.exception.out_of_range.414
|
||||||
|
|
||||||
|
A JSON Patch `move` operation's `"from"` location is a proper prefix of its `"path"` location. Per [RFC 6902](https://datatracker.ietf.org/doc/html/rfc6902) (section 4.4), a location cannot be moved into one of its own children.
|
||||||
|
|
||||||
|
!!! failure "Example message"
|
||||||
|
|
||||||
|
```
|
||||||
|
cannot move value: 'from' path '/0' is a proper prefix of 'path' '/0/0'
|
||||||
|
```
|
||||||
|
|
||||||
|
!!! note
|
||||||
|
|
||||||
|
This exception was added in version 3.13.0. Before that, this situation could succeed with a corrupted result: for an array target, removing the "from" element before the "add" step shifted subsequent indices, so "path" silently re-resolved to a different element than intended.
|
||||||
|
|
||||||
## Further exceptions
|
## Further exceptions
|
||||||
|
|
||||||
This exception is thrown in case of errors that cannot be classified with the
|
This exception is thrown in case of errors that cannot be classified with the
|
||||||
|
|||||||
@@ -4939,6 +4939,36 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
|
|||||||
// note erase performs range check
|
// note erase performs range check
|
||||||
parent.erase(json_pointer::template array_index<basic_json_t>(last_path));
|
parent.erase(json_pointer::template array_index<basic_json_t>(last_path));
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// the parent of a "remove" target must be an object or array
|
||||||
|
// (see #5396)
|
||||||
|
JSON_THROW(out_of_range::create(413, detail::concat("cannot remove value: the JSON Patch 'remove' target's parent is of type ", parent.type_name(), ", but must be an object or array"), &parent));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// RFC 6902 (section 4.4) forbids "from" from being a proper prefix
|
||||||
|
// of "path" for a "move" operation: a location cannot be moved into
|
||||||
|
// one of its own children. Compares reference tokens (already
|
||||||
|
// unescaped by json_pointer's parser) rather than the raw pointer
|
||||||
|
// strings, since a token may itself contain an escaped '/' or '~'
|
||||||
|
// that would defeat a naive string-prefix comparison. "from" equal
|
||||||
|
// to "path" is *not* a proper prefix and must return false.
|
||||||
|
const auto is_proper_prefix = [](const json_pointer & from, const json_pointer & to)
|
||||||
|
{
|
||||||
|
const auto from_size = from.reference_tokens.size();
|
||||||
|
if (from_size >= to.reference_tokens.size())
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
for (std::size_t i = 0; i < from_size; ++i)
|
||||||
|
{
|
||||||
|
if (!(from.reference_tokens[i] == to.reference_tokens[i]))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
// type check: top level value must be an array
|
// type check: top level value must be an array
|
||||||
@@ -5016,6 +5046,11 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
|
|||||||
const auto from_path = get_value("move", "from", true).template get<string_t>();
|
const auto from_path = get_value("move", "from", true).template get<string_t>();
|
||||||
json_pointer from_ptr(from_path);
|
json_pointer from_ptr(from_path);
|
||||||
|
|
||||||
|
if (JSON_HEDLEY_UNLIKELY(is_proper_prefix(from_ptr, ptr)))
|
||||||
|
{
|
||||||
|
JSON_THROW(out_of_range::create(414, detail::concat("cannot move value: 'from' path '", from_path, "' is a proper prefix of 'path' '", path, "'"), &result));
|
||||||
|
}
|
||||||
|
|
||||||
// the "from" location must exist - use at()
|
// the "from" location must exist - use at()
|
||||||
basic_json const v = result.at(from_ptr);
|
basic_json const v = result.at(from_ptr);
|
||||||
|
|
||||||
|
|||||||
@@ -26367,6 +26367,36 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
|
|||||||
// note erase performs range check
|
// note erase performs range check
|
||||||
parent.erase(json_pointer::template array_index<basic_json_t>(last_path));
|
parent.erase(json_pointer::template array_index<basic_json_t>(last_path));
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// the parent of a "remove" target must be an object or array
|
||||||
|
// (see #5396)
|
||||||
|
JSON_THROW(out_of_range::create(413, detail::concat("cannot remove value: the JSON Patch 'remove' target's parent is of type ", parent.type_name(), ", but must be an object or array"), &parent));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// RFC 6902 (section 4.4) forbids "from" from being a proper prefix
|
||||||
|
// of "path" for a "move" operation: a location cannot be moved into
|
||||||
|
// one of its own children. Compares reference tokens (already
|
||||||
|
// unescaped by json_pointer's parser) rather than the raw pointer
|
||||||
|
// strings, since a token may itself contain an escaped '/' or '~'
|
||||||
|
// that would defeat a naive string-prefix comparison. "from" equal
|
||||||
|
// to "path" is *not* a proper prefix and must return false.
|
||||||
|
const auto is_proper_prefix = [](const json_pointer & from, const json_pointer & to)
|
||||||
|
{
|
||||||
|
const auto from_size = from.reference_tokens.size();
|
||||||
|
if (from_size >= to.reference_tokens.size())
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
for (std::size_t i = 0; i < from_size; ++i)
|
||||||
|
{
|
||||||
|
if (!(from.reference_tokens[i] == to.reference_tokens[i]))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
// type check: top level value must be an array
|
// type check: top level value must be an array
|
||||||
@@ -26444,6 +26474,11 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
|
|||||||
const auto from_path = get_value("move", "from", true).template get<string_t>();
|
const auto from_path = get_value("move", "from", true).template get<string_t>();
|
||||||
json_pointer from_ptr(from_path);
|
json_pointer from_ptr(from_path);
|
||||||
|
|
||||||
|
if (JSON_HEDLEY_UNLIKELY(is_proper_prefix(from_ptr, ptr)))
|
||||||
|
{
|
||||||
|
JSON_THROW(out_of_range::create(414, detail::concat("cannot move value: 'from' path '", from_path, "' is a proper prefix of 'path' '", path, "'"), &result));
|
||||||
|
}
|
||||||
|
|
||||||
// the "from" location must exist - use at()
|
// the "from" location must exist - use at()
|
||||||
basic_json const v = result.at(from_ptr);
|
basic_json const v = result.at(from_ptr);
|
||||||
|
|
||||||
|
|||||||
@@ -1389,6 +1389,192 @@ TEST_CASE("JSON patch - add to a primitive parent (regression #4292)")
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TEST_CASE("JSON patch - remove with primitive or null parent (regression #5396)")
|
||||||
|
{
|
||||||
|
// Regression test for https://github.com/nlohmann/json/issues/5396
|
||||||
|
//
|
||||||
|
// RFC 6902 (§4.2) requires the target location of a "remove" operation
|
||||||
|
// to exist. When the target's parent resolves to a primitive value or
|
||||||
|
// null, the operation must fail. Previously operation_remove silently
|
||||||
|
// did nothing in this case (neither the "is_object" nor the "is_array"
|
||||||
|
// branch matched, and there was no final "else"), so the patch appeared
|
||||||
|
// to succeed without changing the document. It now throws
|
||||||
|
// out_of_range.413.
|
||||||
|
|
||||||
|
SECTION("parent is a primitive (number)")
|
||||||
|
{
|
||||||
|
json const doc = {{"a", 1}};
|
||||||
|
json const patch = {{{"op", "remove"}, {"path", "/a/b"}}};
|
||||||
|
#if JSON_DIAGNOSTICS
|
||||||
|
CHECK_THROWS_WITH_AS(doc.patch(patch), "[json.exception.out_of_range.413] (/a) cannot remove value: the JSON Patch 'remove' target's parent is of type number, but must be an object or array", json::out_of_range&);
|
||||||
|
#else
|
||||||
|
CHECK_THROWS_WITH_AS(doc.patch(patch), "[json.exception.out_of_range.413] cannot remove value: the JSON Patch 'remove' target's parent is of type number, but must be an object or array", json::out_of_range&);
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
SECTION("parent is a primitive (string)")
|
||||||
|
{
|
||||||
|
json const doc = {{"foo", {{"bar", "a string"}}}};
|
||||||
|
json const patch = {{{"op", "remove"}, {"path", "/foo/bar/baz"}}};
|
||||||
|
#if JSON_DIAGNOSTICS
|
||||||
|
CHECK_THROWS_WITH_AS(doc.patch(patch), "[json.exception.out_of_range.413] (/foo/bar) cannot remove value: the JSON Patch 'remove' target's parent is of type string, but must be an object or array", json::out_of_range&);
|
||||||
|
#else
|
||||||
|
CHECK_THROWS_WITH_AS(doc.patch(patch), "[json.exception.out_of_range.413] cannot remove value: the JSON Patch 'remove' target's parent is of type string, but must be an object or array", json::out_of_range&);
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
SECTION("top-level document is null")
|
||||||
|
{
|
||||||
|
json const doc = nullptr;
|
||||||
|
json const patch = {{{"op", "remove"}, {"path", "/a"}}};
|
||||||
|
CHECK_THROWS_WITH_AS(doc.patch(patch), "[json.exception.out_of_range.413] cannot remove value: the JSON Patch 'remove' target's parent is of type null, but must be an object or array", json::out_of_range&);
|
||||||
|
}
|
||||||
|
|
||||||
|
SECTION("legitimate removes still work")
|
||||||
|
{
|
||||||
|
// object member
|
||||||
|
json const doc1 = {{"a", 1}, {"b", 2}};
|
||||||
|
json const patch1 = {{{"op", "remove"}, {"path", "/a"}}};
|
||||||
|
CHECK(doc1.patch(patch1) == json({{"b", 2}}));
|
||||||
|
|
||||||
|
// array element
|
||||||
|
json const doc2 = R"([1, 2, 3])"_json;
|
||||||
|
json const patch2 = {{{"op", "remove"}, {"path", "/1"}}};
|
||||||
|
CHECK(doc2.patch(patch2) == R"([1, 3])"_json);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("JSON patch - move where 'from' is a proper prefix of 'path' (regression #5397)")
|
||||||
|
{
|
||||||
|
// Regression test for https://github.com/nlohmann/json/issues/5397
|
||||||
|
//
|
||||||
|
// RFC 6902 (§4.4) forbids "from" from being a proper prefix of "path"
|
||||||
|
// for a "move" operation: "a location cannot be moved into one of its
|
||||||
|
// children." "move" is implemented as remove-then-add; for an object
|
||||||
|
// target this happened to throw anyway as a side effect of the "add"
|
||||||
|
// step re-resolving through the now-removed parent, but for an array
|
||||||
|
// target the removal shifted subsequent indices, so "path" silently
|
||||||
|
// re-resolved to a different element and the operation "succeeded"
|
||||||
|
// with a corrupted result. It now throws out_of_range.414 for both
|
||||||
|
// object and array targets.
|
||||||
|
|
||||||
|
SECTION("array target (from the issue)")
|
||||||
|
{
|
||||||
|
json const doc = R"([[1,2],[3]])"_json;
|
||||||
|
json const patch = {{{"op", "move"}, {"from", "/0"}, {"path", "/0/0"}}};
|
||||||
|
#if JSON_DIAGNOSTIC_POSITIONS
|
||||||
|
CHECK_THROWS_WITH_AS(doc.patch(patch), "[json.exception.out_of_range.414] (bytes 0-11) cannot move value: 'from' path '/0' is a proper prefix of 'path' '/0/0'", json::out_of_range&);
|
||||||
|
#else
|
||||||
|
CHECK_THROWS_WITH_AS(doc.patch(patch), "[json.exception.out_of_range.414] cannot move value: 'from' path '/0' is a proper prefix of 'path' '/0/0'", json::out_of_range&);
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
SECTION("object target")
|
||||||
|
{
|
||||||
|
json const doc = R"({"a": {"b": 1}})"_json;
|
||||||
|
json const patch = {{{"op", "move"}, {"from", "/a"}, {"path", "/a/b"}}};
|
||||||
|
#if JSON_DIAGNOSTIC_POSITIONS
|
||||||
|
CHECK_THROWS_WITH_AS(doc.patch(patch), "[json.exception.out_of_range.414] (bytes 0-15) cannot move value: 'from' path '/a' is a proper prefix of 'path' '/a/b'", json::out_of_range&);
|
||||||
|
#else
|
||||||
|
CHECK_THROWS_WITH_AS(doc.patch(patch), "[json.exception.out_of_range.414] cannot move value: 'from' path '/a' is a proper prefix of 'path' '/a/b'", json::out_of_range&);
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
SECTION("from == path is not a proper prefix and must not be rejected")
|
||||||
|
{
|
||||||
|
// "from" equal to "path" is a no-op move; it is not a *proper*
|
||||||
|
// prefix relationship, so this new check must not reject it.
|
||||||
|
json const doc = R"({"a": 1, "b": 2})"_json;
|
||||||
|
json const patch = {{{"op", "move"}, {"from", "/a"}, {"path", "/a"}}};
|
||||||
|
CHECK(doc.patch(patch) == doc);
|
||||||
|
}
|
||||||
|
|
||||||
|
SECTION("raw string prefix that is not a pointer-token prefix must be allowed")
|
||||||
|
{
|
||||||
|
// "/ab" is a string-prefix of "/abc/x" as raw text, but "ab" and
|
||||||
|
// "abc" are different reference tokens, so this is NOT a
|
||||||
|
// pointer-token prefix relationship and the move must succeed.
|
||||||
|
// This is the key case proving the check compares tokens, not
|
||||||
|
// raw pointer text (a naive std::string prefix/rfind check on
|
||||||
|
// the undecoded pointer would wrongly reject this).
|
||||||
|
json const doc = R"({"ab": 1, "abc": {"x": 2}})"_json;
|
||||||
|
json const patch = {{{"op", "move"}, {"from", "/ab"}, {"path", "/abc/x"}}};
|
||||||
|
json const result = R"({"abc": {"x": 1}})"_json;
|
||||||
|
CHECK(doc.patch(patch) == result);
|
||||||
|
}
|
||||||
|
|
||||||
|
SECTION("escaped reference tokens are compared unescaped")
|
||||||
|
{
|
||||||
|
// "from" is the single token "a/b" (escaped as "a~1b"); "path"
|
||||||
|
// addresses member "x" of that same value, so "from" is a
|
||||||
|
// proper (token-level) prefix of "path" and must be rejected.
|
||||||
|
json const doc = R"({"a/b": {"x": 1}})"_json;
|
||||||
|
json const patch = {{{"op", "move"}, {"from", "/a~1b"}, {"path", "/a~1b/x"}}};
|
||||||
|
#if JSON_DIAGNOSTIC_POSITIONS
|
||||||
|
CHECK_THROWS_WITH_AS(doc.patch(patch), "[json.exception.out_of_range.414] (bytes 0-17) cannot move value: 'from' path '/a~1b' is a proper prefix of 'path' '/a~1b/x'", json::out_of_range&);
|
||||||
|
#else
|
||||||
|
CHECK_THROWS_WITH_AS(doc.patch(patch), "[json.exception.out_of_range.414] cannot move value: 'from' path '/a~1b' is a proper prefix of 'path' '/a~1b/x'", json::out_of_range&);
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
SECTION("ordinary valid moves still work")
|
||||||
|
{
|
||||||
|
// unrelated top-level members
|
||||||
|
json const doc1 = R"({"a": 1, "b": 2})"_json;
|
||||||
|
json const patch1 = {{{"op", "move"}, {"from", "/a"}, {"path", "/c"}}};
|
||||||
|
CHECK(doc1.patch(patch1) == R"({"b": 2, "c": 1})"_json);
|
||||||
|
|
||||||
|
// sibling paths that share a textual prefix but are unrelated
|
||||||
|
json const doc2 = R"({"a": {"x": 1}, "b": {"y": 2}})"_json;
|
||||||
|
json const patch2 = {{{"op", "move"}, {"from", "/a/x"}, {"path", "/b/z"}}};
|
||||||
|
CHECK(doc2.patch(patch2) == R"({"a": {}, "b": {"y": 2, "z": 1}})"_json);
|
||||||
|
|
||||||
|
// "path" is a proper prefix of "from" (the reverse relationship,
|
||||||
|
// which RFC 6902 does not forbid)
|
||||||
|
json const doc3 = R"({"a": {"b": 1}})"_json;
|
||||||
|
json const patch3 = {{{"op", "move"}, {"from", "/a/b"}, {"path", "/a"}}};
|
||||||
|
CHECK(doc3.patch(patch3) == R"({"a": 1})"_json);
|
||||||
|
}
|
||||||
|
|
||||||
|
SECTION("root 'from' is a proper prefix of every non-root 'path'")
|
||||||
|
{
|
||||||
|
// the whole document is a proper prefix of any location inside it
|
||||||
|
json const doc = R"({"a": 1})"_json;
|
||||||
|
json const patch = {{{"op", "move"}, {"from", ""}, {"path", "/a"}}};
|
||||||
|
#if JSON_DIAGNOSTIC_POSITIONS
|
||||||
|
CHECK_THROWS_WITH_AS(doc.patch(patch), "[json.exception.out_of_range.414] (bytes 0-8) cannot move value: 'from' path '' is a proper prefix of 'path' '/a'", json::out_of_range&);
|
||||||
|
#else
|
||||||
|
CHECK_THROWS_WITH_AS(doc.patch(patch), "[json.exception.out_of_range.414] cannot move value: 'from' path '' is a proper prefix of 'path' '/a'", json::out_of_range&);
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
SECTION("root 'path' is never a proper prefix violation for a non-root 'from'")
|
||||||
|
{
|
||||||
|
// the reverse of the above: moving a non-root location to the root
|
||||||
|
// is the "path is a prefix of from" relationship, which RFC 6902
|
||||||
|
// permits (already covered generally above; this pins the root
|
||||||
|
// case specifically, since root is the one path with no reference
|
||||||
|
// tokens at all)
|
||||||
|
json const doc = R"({"a": {"b": 1}})"_json;
|
||||||
|
json const patch = {{{"op", "move"}, {"from", "/a"}, {"path", ""}}};
|
||||||
|
CHECK(doc.patch(patch) == R"({"b": 1})"_json);
|
||||||
|
}
|
||||||
|
|
||||||
|
SECTION("the array-append token '-' is an ordinary child token")
|
||||||
|
{
|
||||||
|
// "-" (append-to-array) addresses a location *inside* the array,
|
||||||
|
// so "from" pointing at the array is still a proper prefix of
|
||||||
|
// "path" ending in "-" and must be rejected like any other child.
|
||||||
|
json const doc = R"({"a": [1, 2]})"_json;
|
||||||
|
json const patch = {{{"op", "move"}, {"from", "/a"}, {"path", "/a/-"}}};
|
||||||
|
#if JSON_DIAGNOSTIC_POSITIONS
|
||||||
|
CHECK_THROWS_WITH_AS(doc.patch(patch), "[json.exception.out_of_range.414] (bytes 0-13) cannot move value: 'from' path '/a' is a proper prefix of 'path' '/a/-'", json::out_of_range&);
|
||||||
|
#else
|
||||||
|
CHECK_THROWS_WITH_AS(doc.patch(patch), "[json.exception.out_of_range.414] cannot move value: 'from' path '/a' is a proper prefix of 'path' '/a/-'", json::out_of_range&);
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
TEST_CASE("JSON patch - diff emits array removals in descending index order")
|
TEST_CASE("JSON patch - diff emits array removals in descending index order")
|
||||||
{
|
{
|
||||||
SECTION("array shrunk to empty")
|
SECTION("array shrunk to empty")
|
||||||
|
|||||||
@@ -1,489 +0,0 @@
|
|||||||
// __ _____ _____ _____
|
|
||||||
// __| | __| | | | JSON for Modern C++ (supporting code)
|
|
||||||
// | | |__ | | | | | | version 3.12.0
|
|
||||||
// |_____|_____|_____|_|___| https://github.com/nlohmann/json
|
|
||||||
//
|
|
||||||
// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann <https://nlohmann.me>
|
|
||||||
// SPDX-FileCopyrightText: 2018 Vitaliy Manushkin <agri@akamo.info>
|
|
||||||
// SPDX-License-Identifier: MIT
|
|
||||||
|
|
||||||
// This file closes a test-coverage gap described in GitHub issue #5421:
|
|
||||||
// nlohmann::ordered_json (and other non-default basic_json specializations,
|
|
||||||
// such as the alt_string-based one from unit-alt-string.cpp) were never
|
|
||||||
// exercised through the binary formats (CBOR/MessagePack/UBJSON/BSON/BJData)
|
|
||||||
// or through flatten()/unflatten()/diff()/patch()/merge_patch().
|
|
||||||
|
|
||||||
#include "doctest_compatibility.h"
|
|
||||||
|
|
||||||
#include <nlohmann/json.hpp>
|
|
||||||
|
|
||||||
#include <cstdint>
|
|
||||||
#include <string>
|
|
||||||
#include <utility>
|
|
||||||
#include <vector>
|
|
||||||
|
|
||||||
using nlohmann::json;
|
|
||||||
using nlohmann::ordered_json;
|
|
||||||
|
|
||||||
/////////////////////////////////////////////////////////////////////////////
|
|
||||||
// alt_json: a second, independent copy of the custom-string_t basic_json
|
|
||||||
// specialization defined in unit-alt-string.cpp.
|
|
||||||
//
|
|
||||||
// It is duplicated here (rather than shared via a header) because every
|
|
||||||
// unit-*.cpp file in this test suite is compiled into its own standalone
|
|
||||||
// executable (see tests/CMakeLists.txt), so there is no ODR concern in
|
|
||||||
// having the same class name defined in multiple translation units.
|
|
||||||
//
|
|
||||||
// Two members had to be added relative to the original alt_string
|
|
||||||
// (a constructor from std::string, and a find(char, pos) overload) because
|
|
||||||
// the original type was never used with the binary writers/readers before
|
|
||||||
// this file: BSON's array/document writer converts std::to_string() results
|
|
||||||
// and checks for embedded NUL characters via find(char), and the UBJSON/BSON
|
|
||||||
// high-precision-number path constructs the SAX string_t argument from a
|
|
||||||
// std::string. Neither path is exercised anywhere else in the test suite for
|
|
||||||
// this type, which is presumably why the gap was never noticed.
|
|
||||||
/////////////////////////////////////////////////////////////////////////////
|
|
||||||
|
|
||||||
class alt_string;
|
|
||||||
bool operator<(const char* op1, const alt_string& op2) noexcept; // NOLINT(misc-use-internal-linkage)
|
|
||||||
void int_to_string(alt_string& target, std::size_t value); // NOLINT(misc-use-internal-linkage)
|
|
||||||
|
|
||||||
class alt_string
|
|
||||||
{
|
|
||||||
public:
|
|
||||||
using value_type = std::string::value_type;
|
|
||||||
|
|
||||||
static constexpr auto npos = (std::numeric_limits<std::size_t>::max)();
|
|
||||||
|
|
||||||
alt_string(const char* str): str_impl(str) {}
|
|
||||||
alt_string(const char* str, std::size_t count): str_impl(str, count) {}
|
|
||||||
alt_string(std::string str): str_impl(std::move(str)) {}
|
|
||||||
alt_string(size_t count, char chr): str_impl(count, chr) {}
|
|
||||||
alt_string() = default;
|
|
||||||
|
|
||||||
alt_string& append(char ch)
|
|
||||||
{
|
|
||||||
str_impl.push_back(ch);
|
|
||||||
return *this;
|
|
||||||
}
|
|
||||||
|
|
||||||
alt_string& append(const alt_string& str)
|
|
||||||
{
|
|
||||||
str_impl.append(str.str_impl);
|
|
||||||
return *this;
|
|
||||||
}
|
|
||||||
|
|
||||||
alt_string& append(const char* s, std::size_t length)
|
|
||||||
{
|
|
||||||
str_impl.append(s, length);
|
|
||||||
return *this;
|
|
||||||
}
|
|
||||||
|
|
||||||
void push_back(char c)
|
|
||||||
{
|
|
||||||
str_impl.push_back(c);
|
|
||||||
}
|
|
||||||
|
|
||||||
template <typename op_type>
|
|
||||||
bool operator==(const op_type& op) const
|
|
||||||
{
|
|
||||||
return str_impl == op;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool operator==(const alt_string& op) const
|
|
||||||
{
|
|
||||||
return str_impl == op.str_impl;
|
|
||||||
}
|
|
||||||
|
|
||||||
template <typename op_type>
|
|
||||||
bool operator!=(const op_type& op) const
|
|
||||||
{
|
|
||||||
return str_impl != op;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool operator!=(const alt_string& op) const
|
|
||||||
{
|
|
||||||
return str_impl != op.str_impl;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::size_t size() const noexcept
|
|
||||||
{
|
|
||||||
return str_impl.size();
|
|
||||||
}
|
|
||||||
|
|
||||||
void resize(std::size_t n)
|
|
||||||
{
|
|
||||||
str_impl.resize(n);
|
|
||||||
}
|
|
||||||
|
|
||||||
void resize(std::size_t n, char c)
|
|
||||||
{
|
|
||||||
str_impl.resize(n, c);
|
|
||||||
}
|
|
||||||
|
|
||||||
template <typename op_type>
|
|
||||||
bool operator<(const op_type& op) const noexcept
|
|
||||||
{
|
|
||||||
return str_impl < op;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool operator<(const alt_string& op) const noexcept
|
|
||||||
{
|
|
||||||
return str_impl < op.str_impl;
|
|
||||||
}
|
|
||||||
|
|
||||||
const char* c_str() const
|
|
||||||
{
|
|
||||||
return str_impl.c_str();
|
|
||||||
}
|
|
||||||
|
|
||||||
char& operator[](std::size_t index)
|
|
||||||
{
|
|
||||||
return str_impl[index];
|
|
||||||
}
|
|
||||||
|
|
||||||
const char& operator[](std::size_t index) const
|
|
||||||
{
|
|
||||||
return str_impl[index];
|
|
||||||
}
|
|
||||||
|
|
||||||
char& back()
|
|
||||||
{
|
|
||||||
return str_impl.back();
|
|
||||||
}
|
|
||||||
|
|
||||||
const char& back() const
|
|
||||||
{
|
|
||||||
return str_impl.back();
|
|
||||||
}
|
|
||||||
|
|
||||||
void clear()
|
|
||||||
{
|
|
||||||
str_impl.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
const value_type* data() const
|
|
||||||
{
|
|
||||||
return str_impl.data();
|
|
||||||
}
|
|
||||||
|
|
||||||
bool empty() const
|
|
||||||
{
|
|
||||||
return str_impl.empty();
|
|
||||||
}
|
|
||||||
|
|
||||||
std::size_t find(const alt_string& str, std::size_t pos = 0) const
|
|
||||||
{
|
|
||||||
return str_impl.find(str.str_impl, pos);
|
|
||||||
}
|
|
||||||
|
|
||||||
// needed by binary_writer's BSON support, which probes string keys for
|
|
||||||
// embedded NUL characters via find(char)
|
|
||||||
std::size_t find(char c, std::size_t pos = 0) const
|
|
||||||
{
|
|
||||||
return str_impl.find(c, pos);
|
|
||||||
}
|
|
||||||
|
|
||||||
std::size_t find_first_of(char c, std::size_t pos = 0) const
|
|
||||||
{
|
|
||||||
return str_impl.find_first_of(c, pos);
|
|
||||||
}
|
|
||||||
|
|
||||||
alt_string substr(std::size_t pos = 0, std::size_t count = npos) const
|
|
||||||
{
|
|
||||||
const std::string s = str_impl.substr(pos, count);
|
|
||||||
return {s.data(), s.size()};
|
|
||||||
}
|
|
||||||
|
|
||||||
alt_string& replace(std::size_t pos, std::size_t count, const alt_string& str)
|
|
||||||
{
|
|
||||||
str_impl.replace(pos, count, str.str_impl);
|
|
||||||
return *this;
|
|
||||||
}
|
|
||||||
|
|
||||||
void reserve(std::size_t new_cap = 0)
|
|
||||||
{
|
|
||||||
str_impl.reserve(new_cap);
|
|
||||||
}
|
|
||||||
|
|
||||||
private:
|
|
||||||
std::string str_impl {}; // NOLINT(readability-redundant-member-init)
|
|
||||||
|
|
||||||
friend bool operator<(const char* /*op1*/, const alt_string& /*op2*/) noexcept;
|
|
||||||
};
|
|
||||||
|
|
||||||
void int_to_string(alt_string& target, std::size_t value)
|
|
||||||
{
|
|
||||||
target = std::to_string(value).c_str();
|
|
||||||
}
|
|
||||||
|
|
||||||
using alt_json = nlohmann::basic_json <
|
|
||||||
std::map,
|
|
||||||
std::vector,
|
|
||||||
alt_string,
|
|
||||||
bool,
|
|
||||||
std::int64_t,
|
|
||||||
std::uint64_t,
|
|
||||||
double,
|
|
||||||
std::allocator,
|
|
||||||
nlohmann::adl_serializer >;
|
|
||||||
|
|
||||||
bool operator<(const char* op1, const alt_string& op2) noexcept
|
|
||||||
{
|
|
||||||
return op1 < op2.str_impl;
|
|
||||||
}
|
|
||||||
|
|
||||||
namespace
|
|
||||||
{
|
|
||||||
|
|
||||||
// collects the object keys of j, in iteration order
|
|
||||||
std::vector<std::string> collect_keys(const ordered_json& j)
|
|
||||||
{
|
|
||||||
std::vector<std::string> result;
|
|
||||||
for (auto it = j.cbegin(); it != j.cend(); ++it)
|
|
||||||
{
|
|
||||||
result.push_back(it.key());
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
// a nested object/array value with keys inserted in non-alphabetical order,
|
|
||||||
// used to check both round-trip equality and (for ordered_json) that
|
|
||||||
// insertion order survives a trip through a binary format
|
|
||||||
ordered_json make_rich_ordered_json()
|
|
||||||
{
|
|
||||||
ordered_json j;
|
|
||||||
j["zebra"] = 1;
|
|
||||||
j["apple"] = ordered_json::array({1, 2, 3});
|
|
||||||
j["mango"]["z_nested"] = true;
|
|
||||||
j["mango"]["a_nested"] = nullptr;
|
|
||||||
j["banana"] = "some text";
|
|
||||||
j["cherry"] = 3.14;
|
|
||||||
return j;
|
|
||||||
}
|
|
||||||
|
|
||||||
alt_json make_rich_alt_json()
|
|
||||||
{
|
|
||||||
alt_json j;
|
|
||||||
j["zebra"] = 1;
|
|
||||||
j["apple"] = alt_json::array({1, 2, 3});
|
|
||||||
j["mango"]["z_nested"] = true;
|
|
||||||
j["mango"]["a_nested"] = nullptr;
|
|
||||||
j["banana"] = "some text";
|
|
||||||
j["cherry"] = 3.14;
|
|
||||||
return j;
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace
|
|
||||||
|
|
||||||
TEST_CASE("ordered_json across binary formats")
|
|
||||||
{
|
|
||||||
const ordered_json original = make_rich_ordered_json();
|
|
||||||
const std::vector<std::string> original_keys = collect_keys(original);
|
|
||||||
const std::vector<std::string> original_mango_keys = collect_keys(original["mango"]);
|
|
||||||
|
|
||||||
SECTION("CBOR")
|
|
||||||
{
|
|
||||||
const auto bytes = ordered_json::to_cbor(original);
|
|
||||||
const auto restored = ordered_json::from_cbor(bytes);
|
|
||||||
CHECK(restored == original);
|
|
||||||
CHECK(collect_keys(restored) == original_keys);
|
|
||||||
CHECK(collect_keys(restored["mango"]) == original_mango_keys);
|
|
||||||
}
|
|
||||||
|
|
||||||
SECTION("MessagePack")
|
|
||||||
{
|
|
||||||
const auto bytes = ordered_json::to_msgpack(original);
|
|
||||||
const auto restored = ordered_json::from_msgpack(bytes);
|
|
||||||
CHECK(restored == original);
|
|
||||||
CHECK(collect_keys(restored) == original_keys);
|
|
||||||
CHECK(collect_keys(restored["mango"]) == original_mango_keys);
|
|
||||||
}
|
|
||||||
|
|
||||||
SECTION("UBJSON")
|
|
||||||
{
|
|
||||||
const auto bytes = ordered_json::to_ubjson(original);
|
|
||||||
const auto restored = ordered_json::from_ubjson(bytes);
|
|
||||||
CHECK(restored == original);
|
|
||||||
CHECK(collect_keys(restored) == original_keys);
|
|
||||||
CHECK(collect_keys(restored["mango"]) == original_mango_keys);
|
|
||||||
}
|
|
||||||
|
|
||||||
SECTION("BSON")
|
|
||||||
{
|
|
||||||
const auto bytes = ordered_json::to_bson(original);
|
|
||||||
const auto restored = ordered_json::from_bson(bytes);
|
|
||||||
CHECK(restored == original);
|
|
||||||
CHECK(collect_keys(restored) == original_keys);
|
|
||||||
CHECK(collect_keys(restored["mango"]) == original_mango_keys);
|
|
||||||
}
|
|
||||||
|
|
||||||
SECTION("BJData")
|
|
||||||
{
|
|
||||||
const auto bytes = ordered_json::to_bjdata(original);
|
|
||||||
const auto restored = ordered_json::from_bjdata(bytes);
|
|
||||||
CHECK(restored == original);
|
|
||||||
CHECK(collect_keys(restored) == original_keys);
|
|
||||||
CHECK(collect_keys(restored["mango"]) == original_mango_keys);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
TEST_CASE("alt_json (custom string_t) across binary formats")
|
|
||||||
{
|
|
||||||
const alt_json original = make_rich_alt_json();
|
|
||||||
|
|
||||||
SECTION("CBOR")
|
|
||||||
{
|
|
||||||
const auto bytes = alt_json::to_cbor(original);
|
|
||||||
const auto restored = alt_json::from_cbor(bytes);
|
|
||||||
CHECK(restored == original);
|
|
||||||
}
|
|
||||||
|
|
||||||
SECTION("MessagePack")
|
|
||||||
{
|
|
||||||
const auto bytes = alt_json::to_msgpack(original);
|
|
||||||
const auto restored = alt_json::from_msgpack(bytes);
|
|
||||||
CHECK(restored == original);
|
|
||||||
}
|
|
||||||
|
|
||||||
SECTION("UBJSON")
|
|
||||||
{
|
|
||||||
const auto bytes = alt_json::to_ubjson(original);
|
|
||||||
const auto restored = alt_json::from_ubjson(bytes);
|
|
||||||
CHECK(restored == original);
|
|
||||||
}
|
|
||||||
|
|
||||||
SECTION("BSON")
|
|
||||||
{
|
|
||||||
const auto bytes = alt_json::to_bson(original);
|
|
||||||
const auto restored = alt_json::from_bson(bytes);
|
|
||||||
CHECK(restored == original);
|
|
||||||
}
|
|
||||||
|
|
||||||
SECTION("BJData")
|
|
||||||
{
|
|
||||||
const auto bytes = alt_json::to_bjdata(original);
|
|
||||||
const auto restored = alt_json::from_bjdata(bytes);
|
|
||||||
CHECK(restored == original);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
TEST_CASE("ordered_json operator== is sensitive to key order")
|
|
||||||
{
|
|
||||||
// Unlike nlohmann::json (whose object_t is a std::map, so equality never
|
|
||||||
// depends on insertion order), ordered_json's object_t (ordered_map) is a
|
|
||||||
// std::vector<std::pair<Key, T>> under the hood, and does not define its
|
|
||||||
// own operator==: it inherits std::vector's element-wise comparison. As a
|
|
||||||
// result, two ordered_json objects holding the very same key/value pairs
|
|
||||||
// in different insertion order compare *unequal*. This is the property
|
|
||||||
// that makes the round-trip `CHECK(restored == original)` checks above a
|
|
||||||
// meaningful order-preservation check by themselves (the explicit
|
|
||||||
// collect_keys() comparisons make that check explicit/readable, and
|
|
||||||
// guard against this operator== behavior ever changing).
|
|
||||||
ordered_json a;
|
|
||||||
a["x"] = 1;
|
|
||||||
a["y"] = 2;
|
|
||||||
|
|
||||||
ordered_json b;
|
|
||||||
b["y"] = 2;
|
|
||||||
b["x"] = 1;
|
|
||||||
|
|
||||||
CHECK(a.size() == b.size());
|
|
||||||
CHECK(a["x"] == b["x"]);
|
|
||||||
CHECK(a["y"] == b["y"]);
|
|
||||||
CHECK_FALSE(a == b);
|
|
||||||
}
|
|
||||||
|
|
||||||
TEST_CASE("duplicate keys in a binary-encoded object")
|
|
||||||
{
|
|
||||||
// CBOR encoding of a map with two entries under the same key "a": {"a": 1, "a": 2}
|
|
||||||
const std::vector<std::uint8_t> cbor_bytes
|
|
||||||
{
|
|
||||||
0xA2, 0x61, 'a', 0x01, 0x61, 'a', 0x02
|
|
||||||
};
|
|
||||||
|
|
||||||
// Both json (std::map, via operator[]) and ordered_json (ordered_map, via
|
|
||||||
// operator[]) build binary-decoded objects by looking up/creating the
|
|
||||||
// entry for each incoming key and then assigning the value into it. This
|
|
||||||
// means a repeated key does *not* produce two entries in either case;
|
|
||||||
// instead, the *first* occurrence's position is kept (relevant only for
|
|
||||||
// ordered_json) while the *last* occurrence's value wins (for both) --
|
|
||||||
// this matches operator[]'s "assign the referenced slot" semantics, and
|
|
||||||
// is worth noting because it differs from the initializer-list
|
|
||||||
// construction path (`ordered_json{{"a",1},{"a",2}}`), which builds
|
|
||||||
// through insert()/emplace() and therefore keeps the *first* value, not
|
|
||||||
// the last (see the "There are no dup keys..." case in
|
|
||||||
// unit-ordered_json.cpp).
|
|
||||||
const auto j = json::from_cbor(cbor_bytes);
|
|
||||||
const auto oj = ordered_json::from_cbor(cbor_bytes);
|
|
||||||
|
|
||||||
CHECK(j.size() == 1);
|
|
||||||
CHECK(oj.size() == 1);
|
|
||||||
CHECK(j["a"] == 2);
|
|
||||||
CHECK(oj["a"] == 2);
|
|
||||||
CHECK(j == json(oj));
|
|
||||||
}
|
|
||||||
|
|
||||||
TEST_CASE("ordered_json through flatten/unflatten")
|
|
||||||
{
|
|
||||||
const ordered_json original = make_rich_ordered_json();
|
|
||||||
const std::vector<std::string> original_keys = collect_keys(original);
|
|
||||||
const std::vector<std::string> original_mango_keys = collect_keys(original["mango"]);
|
|
||||||
|
|
||||||
const ordered_json flat = original.flatten();
|
|
||||||
const ordered_json unflattened = flat.unflatten();
|
|
||||||
|
|
||||||
CHECK(unflattened == original);
|
|
||||||
// flatten() walks the value depth-first in iteration order and
|
|
||||||
// unflatten() re-inserts each flattened key via operator[] in the flat
|
|
||||||
// object's iteration order, so for ordered_json the original key order
|
|
||||||
// (both top-level and nested) is preserved end-to-end.
|
|
||||||
CHECK(collect_keys(unflattened) == original_keys);
|
|
||||||
CHECK(collect_keys(unflattened["mango"]) == original_mango_keys);
|
|
||||||
}
|
|
||||||
|
|
||||||
TEST_CASE("ordered_json through diff/patch/patch_inplace")
|
|
||||||
{
|
|
||||||
ordered_json original;
|
|
||||||
original["one"] = 1;
|
|
||||||
original["two"] = 2;
|
|
||||||
original["three"] = 3;
|
|
||||||
|
|
||||||
ordered_json target = original;
|
|
||||||
target["one"] = 100; // replace
|
|
||||||
target.erase("two"); // remove
|
|
||||||
target["four"] = 4; // add
|
|
||||||
|
|
||||||
const ordered_json patch = ordered_json::diff(original, target);
|
|
||||||
|
|
||||||
SECTION("patch")
|
|
||||||
{
|
|
||||||
const ordered_json patched = original.patch(patch);
|
|
||||||
CHECK(patched == target);
|
|
||||||
}
|
|
||||||
|
|
||||||
SECTION("patch_inplace")
|
|
||||||
{
|
|
||||||
ordered_json copy = original;
|
|
||||||
copy.patch_inplace(patch);
|
|
||||||
CHECK(copy == target);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
TEST_CASE("ordered_json through merge_patch")
|
|
||||||
{
|
|
||||||
ordered_json original;
|
|
||||||
original["a"] = 1;
|
|
||||||
original["b"] = 2;
|
|
||||||
|
|
||||||
const ordered_json patch = {{"b", nullptr}, {"c", 3}};
|
|
||||||
|
|
||||||
original.merge_patch(patch);
|
|
||||||
|
|
||||||
ordered_json expected;
|
|
||||||
expected["a"] = 1;
|
|
||||||
expected["c"] = 3;
|
|
||||||
|
|
||||||
CHECK(original == expected);
|
|
||||||
CHECK(collect_keys(original) == collect_keys(expected));
|
|
||||||
}
|
|
||||||
@@ -17,7 +17,6 @@
|
|||||||
|
|
||||||
#include <nlohmann/json.hpp>
|
#include <nlohmann/json.hpp>
|
||||||
using json = nlohmann::json;
|
using json = nlohmann::json;
|
||||||
using ordered_json = nlohmann::ordered_json;
|
|
||||||
|
|
||||||
// JSON_HAS_CPP_20 (do not remove; see note at top of file)
|
// JSON_HAS_CPP_20 (do not remove; see note at top of file)
|
||||||
#if JSON_HAS_STD_FORMAT
|
#if JSON_HAS_STD_FORMAT
|
||||||
@@ -94,16 +93,4 @@ TEST_CASE("std::formatter<nlohmann::json>")
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST_CASE("std::formatter<nlohmann::ordered_json>")
|
|
||||||
{
|
|
||||||
// spot-check a non-default basic_json instantiation, since the formatter
|
|
||||||
// is written against the generic NLOHMANN_BASIC_JSON_TPL_DECLARATION
|
|
||||||
// template and must actually instantiate (and behave correctly) for
|
|
||||||
// template arguments other than nlohmann::json
|
|
||||||
const ordered_json j = {{"foo", 1}, {"bar", {1, 2, 3}}};
|
|
||||||
CHECK(std::format("{}", j) == j.dump());
|
|
||||||
CHECK(std::format("{:#}", j) == j.dump(4));
|
|
||||||
CHECK(std::format("{:2}", j) == j.dump(2));
|
|
||||||
}
|
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
Reference in New Issue
Block a user