mirror of
https://github.com/nlohmann/json.git
synced 2026-09-27 04:25:46 +00:00
* test: cover JSON_NO_IO, JSON_THROW/TRY/CATCH_USER, JSON_SKIP_LIBRARY_VERSION_CHECK, and JSON_DisableEnumSerialization in CI (#5423) These four supported configuration macros were never actually compiled anywhere in the test matrix: - JSON_NO_IO and the JSON_THROW_USER/JSON_TRY_USER/JSON_CATCH_USER trio are exercised together in a new tests/src/unit-no_io_and_user_exceptions.cpp, which is automatically picked up by the existing unit-*.cpp test glob and thus built across the whole standard test matrix. - JSON_SKIP_LIBRARY_VERSION_CHECK is exercised by a new, dedicated tests/src/skip_library_version_check.cpp, compiled directly by the new ci_test_skiplibraryversioncheck target in cmake/ci.cmake: the scenario it simulates (mixing two differently-versioned inclusions of the library) unavoidably triggers the compiler's own "macro redefined" warning, which would fail under the library's own -Weverything/-Werror unit test matrix for a reason unrelated to the macro under test. - JSON_DisableEnumSerialization already had #if-guarded tests in several unit-*.cpp files (from #4384), but no CMake target ever actually set the JSON_DisableEnumSerialization CMake option, so that guarded code was never compiled. Add ci_test_disableenumserialization, mirroring the existing ci_test_noimplicitconversions/ci_test_noglobaludls targets. Building the full test suite with this option on surfaced one real, narrow gap: get<T>() on std::vector<std::byte> (used by unit-regression2.cpp's custom BinaryType tests) relies on std::byte being handled via enum serialization, so add the same #if-guard convention to the two affected SECTIONs there. Both new CI targets are added to the ci_cmake_options matrix in .github/workflows/ubuntu.yml, alongside the existing ci_test_* targets. Signed-off-by: Niels Lohmann <mail@nlohmann.me> * test: cover multi-digit widths and bare alignment in std::formatter<json> (#5423) Every existing std::formatter spec with a width used a single digit (e.g. "{:2}"), so the width-parsing loop's accumulation of a second/third digit was never exercised; add multi-digit width cases. Likewise, every existing spec with an alignment character also had an explicit fill character, so the bare-alignment branch (e.g. "{:<}", with no fill) was never exercised; add cases asserting it keeps the default space indent character. Signed-off-by: Niels Lohmann <mail@nlohmann.me> * test: add coverage for patch_inplace() (#5423) patch_inplace() had no unit test at all. Add a happy-path case mirroring an existing patch() example, and -- more importantly -- pin its distinguishing contract versus patch(): when a multi-operation JSON Patch fails partway through, patch_inplace() (which mutates the document directly, operation by operation) leaves whatever operations already succeeded applied, whereas patch() (which applies the patch to an internal copy that is discarded on exception) leaves the original completely untouched either way. Verified empirically against the current implementation before writing the assertions. Signed-off-by: Niels Lohmann <mail@nlohmann.me> * test: fix duplicate TEST_CASE name in unit-no-mem-leak-on-adl-serialize.cpp (#5423) Two distinct TEST_CASEs were both named "check_for_mem_leak_on_adl_to_json-2". doctest allows duplicate names, so both still ran, but it makes --test-case=<name> filtering and reporting ambiguous. Rename the second one to "-3", continuing the existing "-1"/"-2" sequence. Signed-off-by: Niels Lohmann <mail@nlohmann.me> * test: add direct coverage for the std::u8string to_json overload (#5423) The ADL to_json overload for std::basic_string<char8_t, ...> was only ever reached indirectly, via std::filesystem::path::u8string(). Add a test that constructs a json value directly from a std::u8string, gated the same way as the overload itself (include/nlohmann/detail/conversions/to_json.hpp): behind both the std::filesystem::path feature guard and __cpp_lib_char8_t, since the overload only exists when both are satisfied. Signed-off-by: Niels Lohmann <mail@nlohmann.me> * test: verify move semantics of byte_container_with_subtype's rvalue constructors (#5423) The two rvalue-reference constructors were never distinguished from their const-lvalue-reference twins by any test. Add a "move semantics" section that constructs from an rvalue std::vector, checks the resulting container keeps the exact same buffer address as the source (a stronger check than just observing the source ended up empty, since a copy-then-clear could do that too), and confirms the source vector was left empty. Signed-off-by: Niels Lohmann <mail@nlohmann.me> * Guard patch_inplace() partial-application test against JSON_NOEXCEPTION The "distinguishing contract vs patch(): partial application on failure" test relies on doc.patch_inplace(patch) actually throwing so the partially-applied state can be observed right after the throw point. Under ci_test_noexceptions, JSON_THROW() calls std::abort() instead of throwing, and doctest's --no-throw test filter (which that CI job passes) makes CHECK_THROWS_AS() a no-op that never even evaluates its expression -- so patch_inplace() is never called and the follow-up assertions fail against the untouched original document. Guard the whole SECTION with #if !defined(JSON_NOEXCEPTION), following the same convention already used elsewhere in the test suite (e.g. unit-class_parser.cpp) for exception-dependent tests. Signed-off-by: Niels Lohmann <mail@nlohmann.me> * Fix MSVC C2220 in the std::u8string conversion test MSVC's C5321 ("nonstandard extension used: encoding '\xNN' as a multi-byte utf-8 character") is promoted to a hard error by our MSVC CI configs. It fires because the test composed a non-ASCII UTF-8 sequence inside a u8"" literal using raw \x byte escapes; MSVC treats that as nonstandard and suggests using \u universal-character-names instead, which every compiler agrees on and which compiles down to the exact same encoded bytes. Signed-off-by: Niels Lohmann <mail@nlohmann.me> * Guard the JSON_THROW_USER test against JSON_NOEXCEPTION and GCC's -Wunused-result Two independent CI configurations failed to build/run this new test: - ci_test_noexceptions runs the whole suite with -DJSON_NOEXCEPTION and doctest's "--no-throw" filter, which compiles CHECK_THROWS_AS() down to a no-op that never even invokes the guarded expression. Since this test's whole point is to observe json_throw_user_call_count after json::parse()/at() actually throw, it can't be meaningfully run under that filter (our JSON_THROW_USER override still throws real exceptions regardless of JSON_NOEXCEPTION, but the assertion never gets a chance to run). Guard the TEST_CASE with #if !defined(JSON_NOEXCEPTION), mirroring the existing precedent in unit-json_patch.cpp. - ci_test_gcc and ci_test_standards_gcc(11) failed with -Werror=unused-result on the discarded json::parse() return value. json::parse() is marked warn_unused_result, and unlike a real [[nodiscard]] attribute, GCC does not consider that satisfied by doctest's (void)-cast around the expression in C++11 mode. Assign the result to a discarded local instead, matching the established `json _ = json::parse(...)` idiom already used throughout unit-class_parser.cpp. Signed-off-by: Niels Lohmann <mail@nlohmann.me> * Suppress a clang-tidy false positive on an intentional defensive copy performance-unnecessary-copy-initialization suggests copy_for_patch could be a reference since it's never modified -- but the copy is the point: it guards against a hypothetical regression where patch() mutates its receiver, which a reference could never catch (the follow-up assertion would just compare `original` to itself). Signed-off-by: Niels Lohmann <mail@nlohmann.me> * Fix clang-tidy findings in the JSON_NO_IO/JSON_THROW_USER test - bugprone-macro-parentheses: wrap the JSON_THROW_USER macro argument in parentheses at the throw site. - modernize-raw-string-literal: switch two escaped JSON string literals to raw string literals. Signed-off-by: Niels Lohmann <mail@nlohmann.me> --------- Signed-off-by: Niels Lohmann <mail@nlohmann.me>
1754 lines
67 KiB
C++
1754 lines
67 KiB
C++
// __ _____ _____ _____
|
|
// __| | __| | | | 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-License-Identifier: MIT
|
|
|
|
#include "doctest_compatibility.h"
|
|
|
|
#include <nlohmann/json.hpp>
|
|
using nlohmann::json;
|
|
#ifdef JSON_TEST_NO_GLOBAL_UDLS
|
|
using namespace nlohmann::literals; // NOLINT(google-build-using-namespace)
|
|
#endif
|
|
|
|
#include <fstream>
|
|
#include "make_test_data_available.hpp"
|
|
|
|
TEST_CASE("JSON patch")
|
|
{
|
|
SECTION("examples from RFC 6902")
|
|
{
|
|
SECTION("4. Operations")
|
|
{
|
|
// the ordering of members in JSON objects is not significant:
|
|
const json op1 = R"({ "op": "add", "path": "/a/b/c", "value": "foo" })"_json;
|
|
const json op2 = R"({ "path": "/a/b/c", "op": "add", "value": "foo" })"_json;
|
|
const json op3 = R"({ "value": "foo", "path": "/a/b/c", "op": "add" })"_json;
|
|
|
|
// check if the operation objects are equivalent
|
|
CHECK(op1 == op2);
|
|
CHECK(op1 == op3);
|
|
}
|
|
|
|
SECTION("4.1 add")
|
|
{
|
|
json const patch1 = R"([{ "op": "add", "path": "/a/b", "value": [ "foo", "bar" ] }])"_json;
|
|
|
|
// However, the object itself or an array containing it does need
|
|
// to exist, and it remains an error for that not to be the case.
|
|
// For example, an "add" with a target location of "/a/b" starting
|
|
// with this document
|
|
json const doc1 = R"({ "a": { "foo": 1 } })"_json;
|
|
|
|
// is not an error, because "a" exists, and "b" will be added to
|
|
// its value.
|
|
CHECK_NOTHROW(doc1.patch(patch1));
|
|
auto doc1_ans = R"(
|
|
{
|
|
"a": {
|
|
"foo": 1,
|
|
"b": [ "foo", "bar" ]
|
|
}
|
|
}
|
|
)"_json;
|
|
CHECK(doc1.patch(patch1) == doc1_ans);
|
|
|
|
// It is an error in this document:
|
|
json const doc2 = R"({ "q": { "bar": 2 } })"_json;
|
|
|
|
// because "a" does not exist.
|
|
#if JSON_DIAGNOSTIC_POSITIONS
|
|
CHECK_THROWS_WITH_AS(doc2.patch(patch1), "[json.exception.out_of_range.403] (bytes 0-21) key 'a' not found", json::out_of_range&);
|
|
#else
|
|
CHECK_THROWS_WITH_AS(doc2.patch(patch1), "[json.exception.out_of_range.403] key 'a' not found", json::out_of_range&);
|
|
#endif
|
|
|
|
json const doc3 = R"({ "a": {} })"_json;
|
|
json const patch2 = R"([{ "op": "add", "path": "/a/b/c", "value": 1 }])"_json;
|
|
|
|
// should cause an error because "b" does not exist in doc3
|
|
#if JSON_DIAGNOSTICS
|
|
CHECK_THROWS_WITH_AS(doc3.patch(patch2), "[json.exception.out_of_range.403] (/a) key 'b' not found", json::out_of_range&);
|
|
#elif JSON_DIAGNOSTIC_POSITIONS
|
|
CHECK_THROWS_WITH_AS(doc3.patch(patch2), "[json.exception.out_of_range.403] (bytes 7-9) key 'b' not found", json::out_of_range&);
|
|
#else
|
|
CHECK_THROWS_WITH_AS(doc3.patch(patch2), "[json.exception.out_of_range.403] key 'b' not found", json::out_of_range&);
|
|
#endif
|
|
}
|
|
|
|
SECTION("4.2 remove")
|
|
{
|
|
// If removing an element from an array, any elements above the
|
|
// specified index are shifted one position to the left.
|
|
json const doc = {1, 2, 3, 4};
|
|
json const patch = {{{"op", "remove"}, {"path", "/1"}}};
|
|
CHECK(doc.patch(patch) == json({1, 3, 4}));
|
|
}
|
|
|
|
SECTION("A.1. Adding an Object Member")
|
|
{
|
|
// An example target JSON document:
|
|
json const doc = R"(
|
|
{ "foo": "bar"}
|
|
)"_json;
|
|
|
|
// A JSON Patch document:
|
|
json const patch = R"(
|
|
[
|
|
{ "op": "add", "path": "/baz", "value": "qux" }
|
|
]
|
|
)"_json;
|
|
|
|
// The resulting JSON document:
|
|
json expected = R"(
|
|
{
|
|
"baz": "qux",
|
|
"foo": "bar"
|
|
}
|
|
)"_json;
|
|
|
|
// check if patched value is as expected
|
|
CHECK(doc.patch(patch) == expected);
|
|
|
|
// check roundtrip
|
|
CHECK(doc.patch(json::diff(doc, expected)) == expected);
|
|
}
|
|
|
|
SECTION("A.2. Adding an Array Element")
|
|
{
|
|
// An example target JSON document:
|
|
json const doc = R"(
|
|
{ "foo": [ "bar", "baz" ] }
|
|
)"_json;
|
|
|
|
// A JSON Patch document:
|
|
json const patch = R"(
|
|
[
|
|
{ "op": "add", "path": "/foo/1", "value": "qux" }
|
|
]
|
|
)"_json;
|
|
|
|
// The resulting JSON document:
|
|
json expected = R"(
|
|
{ "foo": [ "bar", "qux", "baz" ] }
|
|
)"_json;
|
|
|
|
// check if patched value is as expected
|
|
CHECK(doc.patch(patch) == expected);
|
|
|
|
// check roundtrip
|
|
CHECK(doc.patch(json::diff(doc, expected)) == expected);
|
|
}
|
|
|
|
SECTION("A.3. Removing an Object Member")
|
|
{
|
|
// An example target JSON document:
|
|
json const doc = R"(
|
|
{
|
|
"baz": "qux",
|
|
"foo": "bar"
|
|
}
|
|
)"_json;
|
|
|
|
// A JSON Patch document:
|
|
json const patch = R"(
|
|
[
|
|
{ "op": "remove", "path": "/baz" }
|
|
]
|
|
)"_json;
|
|
|
|
// The resulting JSON document:
|
|
json expected = R"(
|
|
{ "foo": "bar" }
|
|
)"_json;
|
|
|
|
// check if patched value is as expected
|
|
CHECK(doc.patch(patch) == expected);
|
|
|
|
// check roundtrip
|
|
CHECK(doc.patch(json::diff(doc, expected)) == expected);
|
|
}
|
|
|
|
SECTION("A.4. Removing an Array Element")
|
|
{
|
|
// An example target JSON document:
|
|
json const doc = R"(
|
|
{ "foo": [ "bar", "qux", "baz" ] }
|
|
)"_json;
|
|
|
|
// A JSON Patch document:
|
|
json const patch = R"(
|
|
[
|
|
{ "op": "remove", "path": "/foo/1" }
|
|
]
|
|
)"_json;
|
|
|
|
// The resulting JSON document:
|
|
json expected = R"(
|
|
{ "foo": [ "bar", "baz" ] }
|
|
)"_json;
|
|
|
|
// check if patched value is as expected
|
|
CHECK(doc.patch(patch) == expected);
|
|
|
|
// check roundtrip
|
|
CHECK(doc.patch(json::diff(doc, expected)) == expected);
|
|
}
|
|
|
|
SECTION("A.5. Replacing a Value")
|
|
{
|
|
// An example target JSON document:
|
|
json const doc = R"(
|
|
{
|
|
"baz": "qux",
|
|
"foo": "bar"
|
|
}
|
|
)"_json;
|
|
|
|
// A JSON Patch document:
|
|
json const patch = R"(
|
|
[
|
|
{ "op": "replace", "path": "/baz", "value": "boo" }
|
|
]
|
|
)"_json;
|
|
|
|
json expected = R"(
|
|
{
|
|
"baz": "boo",
|
|
"foo": "bar"
|
|
}
|
|
)"_json;
|
|
|
|
// check if patched value is as expected
|
|
CHECK(doc.patch(patch) == expected);
|
|
|
|
// check roundtrip
|
|
CHECK(doc.patch(json::diff(doc, expected)) == expected);
|
|
}
|
|
|
|
SECTION("A.6. Moving a Value")
|
|
{
|
|
// An example target JSON document:
|
|
json const doc = R"(
|
|
{
|
|
"foo": {
|
|
"bar": "baz",
|
|
"waldo": "fred"
|
|
},
|
|
"qux": {
|
|
"corge": "grault"
|
|
}
|
|
}
|
|
)"_json;
|
|
|
|
// A JSON Patch document:
|
|
json const patch = R"(
|
|
[
|
|
{ "op": "move", "from": "/foo/waldo", "path": "/qux/thud" }
|
|
]
|
|
)"_json;
|
|
|
|
// The resulting JSON document:
|
|
json expected = R"(
|
|
{
|
|
"foo": {
|
|
"bar": "baz"
|
|
},
|
|
"qux": {
|
|
"corge": "grault",
|
|
"thud": "fred"
|
|
}
|
|
}
|
|
)"_json;
|
|
|
|
// check if patched value is as expected
|
|
CHECK(doc.patch(patch) == expected);
|
|
|
|
// check roundtrip
|
|
CHECK(doc.patch(json::diff(doc, expected)) == expected);
|
|
}
|
|
|
|
SECTION("A.7. Moving a Value")
|
|
{
|
|
// An example target JSON document:
|
|
json const doc = R"(
|
|
{ "foo": [ "all", "grass", "cows", "eat" ] }
|
|
)"_json;
|
|
|
|
// A JSON Patch document:
|
|
json const patch = R"(
|
|
[
|
|
{ "op": "move", "from": "/foo/1", "path": "/foo/3" }
|
|
]
|
|
)"_json;
|
|
|
|
// The resulting JSON document:
|
|
json expected = R"(
|
|
{ "foo": [ "all", "cows", "eat", "grass" ] }
|
|
)"_json;
|
|
|
|
// check if patched value is as expected
|
|
CHECK(doc.patch(patch) == expected);
|
|
|
|
// check roundtrip
|
|
CHECK(doc.patch(json::diff(doc, expected)) == expected);
|
|
}
|
|
|
|
SECTION("A.8. Testing a Value: Success")
|
|
{
|
|
// An example target JSON document:
|
|
json doc = R"(
|
|
{
|
|
"baz": "qux",
|
|
"foo": [ "a", 2, "c" ]
|
|
}
|
|
)"_json;
|
|
|
|
// A JSON Patch document that will result in successful evaluation:
|
|
json const patch = R"(
|
|
[
|
|
{ "op": "test", "path": "/baz", "value": "qux" },
|
|
{ "op": "test", "path": "/foo/1", "value": 2 }
|
|
]
|
|
)"_json;
|
|
|
|
// check if evaluation does not throw
|
|
CHECK_NOTHROW(doc.patch(patch));
|
|
// check if patched document is unchanged
|
|
CHECK(doc.patch(patch) == doc);
|
|
}
|
|
|
|
SECTION("A.9. Testing a Value: Error")
|
|
{
|
|
// An example target JSON document:
|
|
json const doc = R"(
|
|
{ "baz": "qux" }
|
|
)"_json;
|
|
|
|
// A JSON Patch document that will result in an error condition:
|
|
json patch = R"(
|
|
[
|
|
{ "op": "test", "path": "/baz", "value": "bar" }
|
|
]
|
|
)"_json;
|
|
|
|
// check that evaluation throws
|
|
CHECK_THROWS_AS(doc.patch(patch), json::other_error&);
|
|
#if JSON_DIAGNOSTICS
|
|
CHECK_THROWS_WITH_STD_STR(doc.patch(patch), "[json.exception.other_error.501] (/0) unsuccessful: " + patch[0].dump());
|
|
#elif JSON_DIAGNOSTIC_POSITIONS
|
|
CHECK_THROWS_WITH_STD_STR(doc.patch(patch), "[json.exception.other_error.501] (bytes 47-95) unsuccessful: " + patch[0].dump());
|
|
#else
|
|
CHECK_THROWS_WITH_STD_STR(doc.patch(patch), "[json.exception.other_error.501] unsuccessful: " + patch[0].dump());
|
|
#endif
|
|
}
|
|
|
|
SECTION("A.10. Adding a Nested Member Object")
|
|
{
|
|
// An example target JSON document:
|
|
json const doc = R"(
|
|
{ "foo": "bar" }
|
|
)"_json;
|
|
|
|
// A JSON Patch document:
|
|
json const patch = R"(
|
|
[
|
|
{ "op": "add", "path": "/child", "value": { "grandchild": { } } }
|
|
]
|
|
)"_json;
|
|
|
|
// The resulting JSON document:
|
|
json expected = R"(
|
|
{
|
|
"foo": "bar",
|
|
"child": {
|
|
"grandchild": {
|
|
}
|
|
}
|
|
}
|
|
)"_json;
|
|
|
|
// check if patched value is as expected
|
|
CHECK(doc.patch(patch) == expected);
|
|
|
|
// check roundtrip
|
|
CHECK(doc.patch(json::diff(doc, expected)) == expected);
|
|
}
|
|
|
|
SECTION("A.11. Ignoring Unrecognized Elements")
|
|
{
|
|
// An example target JSON document:
|
|
json const doc = R"(
|
|
{ "foo": "bar" }
|
|
)"_json;
|
|
|
|
// A JSON Patch document:
|
|
json const patch = R"(
|
|
[
|
|
{ "op": "add", "path": "/baz", "value": "qux", "xyz": 123 }
|
|
]
|
|
)"_json;
|
|
|
|
json expected = R"(
|
|
{
|
|
"foo": "bar",
|
|
"baz": "qux"
|
|
}
|
|
)"_json;
|
|
|
|
// check if patched value is as expected
|
|
CHECK(doc.patch(patch) == expected);
|
|
|
|
// check roundtrip
|
|
CHECK(doc.patch(json::diff(doc, expected)) == expected);
|
|
}
|
|
|
|
SECTION("A.12. Adding to a Nonexistent Target")
|
|
{
|
|
// An example target JSON document:
|
|
json const doc = R"(
|
|
{ "foo": "bar" }
|
|
)"_json;
|
|
|
|
// A JSON Patch document:
|
|
json const patch = R"(
|
|
[
|
|
{ "op": "add", "path": "/baz/bat", "value": "qux" }
|
|
]
|
|
)"_json;
|
|
|
|
// This JSON Patch document, applied to the target JSON document
|
|
// above, would result in an error (therefore, it would not be
|
|
// applied), because the "add" operation's target location that
|
|
// references neither the root of the document, nor a member of
|
|
// an existing object, nor a member of an existing array.
|
|
#if JSON_DIAGNOSTIC_POSITIONS
|
|
CHECK_THROWS_WITH_AS(doc.patch(patch), "[json.exception.out_of_range.403] (bytes 21-37) key 'baz' not found", json::out_of_range&);
|
|
#else
|
|
CHECK_THROWS_WITH_AS(doc.patch(patch), "[json.exception.out_of_range.403] key 'baz' not found", json::out_of_range&);
|
|
#endif
|
|
}
|
|
|
|
// A.13. Invalid JSON Patch Document
|
|
// not applicable
|
|
|
|
SECTION("A.14. Escape Ordering")
|
|
{
|
|
// An example target JSON document:
|
|
json const doc = R"(
|
|
{
|
|
"/": 9,
|
|
"~1": 10
|
|
}
|
|
)"_json;
|
|
|
|
// A JSON Patch document:
|
|
json const patch = R"(
|
|
[
|
|
{"op": "test", "path": "/~01", "value": 10}
|
|
]
|
|
)"_json;
|
|
|
|
json expected = R"(
|
|
{
|
|
"/": 9,
|
|
"~1": 10
|
|
}
|
|
)"_json;
|
|
|
|
// check if patched value is as expected
|
|
CHECK(doc.patch(patch) == expected);
|
|
|
|
// check roundtrip
|
|
CHECK(doc.patch(json::diff(doc, expected)) == expected);
|
|
}
|
|
|
|
SECTION("A.15. Comparing Strings and Numbers")
|
|
{
|
|
// An example target JSON document:
|
|
json const doc = R"(
|
|
{
|
|
"/": 9,
|
|
"~1": 10
|
|
}
|
|
)"_json;
|
|
|
|
// A JSON Patch document that will result in an error condition:
|
|
json patch = R"(
|
|
[
|
|
{"op": "test", "path": "/~01", "value": "10"}
|
|
]
|
|
)"_json;
|
|
|
|
// check that evaluation throws
|
|
CHECK_THROWS_AS(doc.patch(patch), json::other_error&);
|
|
#if JSON_DIAGNOSTICS
|
|
CHECK_THROWS_WITH_STD_STR(doc.patch(patch), "[json.exception.other_error.501] (/0) unsuccessful: " + patch[0].dump());
|
|
#elif JSON_DIAGNOSTIC_POSITIONS
|
|
CHECK_THROWS_WITH_STD_STR(doc.patch(patch), "[json.exception.other_error.501] (bytes 47-92) unsuccessful: " + patch[0].dump());
|
|
#else
|
|
CHECK_THROWS_WITH_STD_STR(doc.patch(patch), "[json.exception.other_error.501] unsuccessful: " + patch[0].dump());
|
|
#endif
|
|
}
|
|
|
|
SECTION("A.16. Adding an Array Value")
|
|
{
|
|
// An example target JSON document:
|
|
json const doc = R"(
|
|
{ "foo": ["bar"] }
|
|
)"_json;
|
|
|
|
// A JSON Patch document:
|
|
json const patch = R"(
|
|
[
|
|
{ "op": "add", "path": "/foo/-", "value": ["abc", "def"] }
|
|
]
|
|
)"_json;
|
|
|
|
// The resulting JSON document:
|
|
json expected = R"(
|
|
{ "foo": ["bar", ["abc", "def"]] }
|
|
)"_json;
|
|
|
|
// check if patched value is as expected
|
|
CHECK(doc.patch(patch) == expected);
|
|
|
|
// check roundtrip
|
|
CHECK(doc.patch(json::diff(doc, expected)) == expected);
|
|
}
|
|
}
|
|
|
|
SECTION("own examples")
|
|
{
|
|
SECTION("add")
|
|
{
|
|
SECTION("add to the root element")
|
|
{
|
|
// If the path is the root of the target document - the
|
|
// specified value becomes the entire content of the target
|
|
// document.
|
|
|
|
// An example target JSON document:
|
|
json const doc = 17;
|
|
|
|
// A JSON Patch document:
|
|
json const patch = R"(
|
|
[
|
|
{ "op": "add", "path": "", "value": [1,2,3] }
|
|
]
|
|
)"_json;
|
|
|
|
// The resulting JSON document:
|
|
json expected = {1, 2, 3};
|
|
|
|
// check if patched value is as expected
|
|
CHECK(doc.patch(patch) == expected);
|
|
|
|
// check roundtrip
|
|
CHECK(doc.patch(json::diff(doc, expected)) == expected);
|
|
}
|
|
|
|
SECTION("add to end of the array")
|
|
{
|
|
// The specified index MUST NOT be greater than the number of
|
|
// elements in the array. The example below uses and index of
|
|
// exactly the number of elements in the array which is legal.
|
|
|
|
// An example target JSON document:
|
|
json const doc = {0, 1, 2};
|
|
|
|
// A JSON Patch document:
|
|
json const patch = R"(
|
|
[
|
|
{ "op": "add", "path": "/3", "value": 3 }
|
|
]
|
|
)"_json;
|
|
|
|
// The resulting JSON document:
|
|
json expected = {0, 1, 2, 3};
|
|
|
|
// check if patched value is as expected
|
|
CHECK(doc.patch(patch) == expected);
|
|
|
|
// check roundtrip
|
|
CHECK(doc.patch(json::diff(doc, expected)) == expected);
|
|
}
|
|
}
|
|
|
|
SECTION("copy")
|
|
{
|
|
// An example target JSON document:
|
|
json const doc = R"(
|
|
{
|
|
"foo": {
|
|
"bar": "baz",
|
|
"waldo": "fred"
|
|
},
|
|
"qux": {
|
|
"corge": "grault"
|
|
}
|
|
}
|
|
)"_json;
|
|
|
|
// A JSON Patch document:
|
|
json const patch = R"(
|
|
[
|
|
{ "op": "copy", "from": "/foo/waldo", "path": "/qux/thud" }
|
|
]
|
|
)"_json;
|
|
|
|
// The resulting JSON document:
|
|
json expected = R"(
|
|
{
|
|
"foo": {
|
|
"bar": "baz",
|
|
"waldo": "fred"
|
|
},
|
|
"qux": {
|
|
"corge": "grault",
|
|
"thud": "fred"
|
|
}
|
|
}
|
|
)"_json;
|
|
|
|
// check if patched value is as expected
|
|
CHECK(doc.patch(patch) == expected);
|
|
|
|
// check roundtrip
|
|
CHECK(doc.patch(json::diff(doc, expected)) == expected);
|
|
}
|
|
|
|
SECTION("replace")
|
|
{
|
|
json const j = "string";
|
|
json const patch = {{{"op", "replace"}, {"path", ""}, {"value", 1}}};
|
|
CHECK(j.patch(patch) == json(1));
|
|
}
|
|
|
|
SECTION("documentation GIF")
|
|
{
|
|
{
|
|
// a JSON patch
|
|
json const p1 = R"(
|
|
[{"op": "add", "path": "/GB", "value": "London"}]
|
|
)"_json;
|
|
|
|
// a JSON value
|
|
json const source = R"(
|
|
{"D": "Berlin", "F": "Paris"}
|
|
)"_json;
|
|
|
|
// apply the patch
|
|
const json target = source.patch(p1);
|
|
// target = { "D": "Berlin", "F": "Paris", "GB": "London" }
|
|
CHECK(target == R"({ "D": "Berlin", "F": "Paris", "GB": "London" })"_json);
|
|
|
|
// create a diff from two JSONs
|
|
const json p2 = json::diff(target, source); // NOLINT(readability-suspicious-call-argument)
|
|
// p2 = [{"op": "delete", "path": "/GB"}]
|
|
CHECK(p2 == R"([{"op":"remove","path":"/GB"}])"_json);
|
|
}
|
|
{
|
|
// a JSON value
|
|
json j = {"good", "bad", "ugly"};
|
|
|
|
// a JSON pointer
|
|
auto ptr = json::json_pointer("/2");
|
|
|
|
// use to access elements
|
|
j[ptr] = {{"it", "cattivo"}};
|
|
CHECK(j == R"(["good","bad",{"it":"cattivo"}])"_json);
|
|
|
|
// use user-defined string literal
|
|
j["/2/en"_json_pointer] = "ugly";
|
|
CHECK(j == R"(["good","bad",{"en":"ugly","it":"cattivo"}])"_json);
|
|
|
|
const json flat = j.flatten();
|
|
CHECK(flat == R"({"/0":"good","/1":"bad","/2/en":"ugly","/2/it":"cattivo"})"_json);
|
|
}
|
|
}
|
|
}
|
|
|
|
SECTION("patch_inplace")
|
|
{
|
|
SECTION("happy path: patch_inplace mirrors patch() on success")
|
|
{
|
|
// mirrors "A.5. Replacing a Value" above, but applies the patch with
|
|
// patch_inplace() to a mutable copy instead of using patch()'s
|
|
// returned copy
|
|
json doc = R"(
|
|
{
|
|
"baz": "qux",
|
|
"foo": "bar"
|
|
}
|
|
)"_json;
|
|
|
|
json const patch = R"(
|
|
[
|
|
{ "op": "replace", "path": "/baz", "value": "boo" }
|
|
]
|
|
)"_json;
|
|
|
|
json const expected = R"(
|
|
{
|
|
"baz": "boo",
|
|
"foo": "bar"
|
|
}
|
|
)"_json;
|
|
|
|
doc.patch_inplace(patch);
|
|
CHECK(doc == expected);
|
|
}
|
|
|
|
// this test relies on the "test" operation actually throwing so the
|
|
// partial-application state can be observed right after the throw
|
|
// point; under JSON_NOEXCEPTION, JSON_THROW() calls std::abort()
|
|
// instead (there is no C++ exception to throw), and doctest's
|
|
// CHECK_THROWS_AS() is compiled out to a no-op that never even
|
|
// invokes the given expression (see doctest's "--no-throw" test
|
|
// filter, which ci_test_noexceptions passes) -- so patch()/
|
|
// patch_inplace() would never be called at all and the follow-up
|
|
// state assertions below would fail against the untouched original
|
|
#if !defined(JSON_NOEXCEPTION)
|
|
SECTION("distinguishing contract vs patch(): partial application on failure")
|
|
{
|
|
// Unlike patch(), which is all-or-nothing because it applies the
|
|
// patch to an internal copy that is simply discarded when an
|
|
// exception is thrown (leaving the original untouched no matter
|
|
// what), patch_inplace() mutates the document it is called on
|
|
// directly and immediately, operation by operation. So if a JSON
|
|
// Patch fails partway through, whatever operations already
|
|
// succeeded remain applied -- the document is left in a partially
|
|
// patched state. This is empirically verified current behavior,
|
|
// not just documented intent, and is pinned here as such.
|
|
json const original = R"(
|
|
{
|
|
"baz": "qux",
|
|
"foo": "bar"
|
|
}
|
|
)"_json;
|
|
|
|
// the first operation ("replace") succeeds; the second ("test")
|
|
// fails because the value at "/baz" no longer (and never did)
|
|
// equal "not boo"
|
|
json const patch = R"(
|
|
[
|
|
{ "op": "replace", "path": "/baz", "value": "boo" },
|
|
{ "op": "test", "path": "/baz", "value": "not boo" }
|
|
]
|
|
)"_json;
|
|
|
|
// patch() never modifies the object it is called on -- it always
|
|
// operates on (and returns) a separate copy, so the original is
|
|
// left completely untouched, regardless of success or failure.
|
|
// copy_for_patch is intentionally a real copy, not a reference
|
|
// to `original`: the whole point of this check is to catch a
|
|
// hypothetical future regression where patch() *does* mutate its
|
|
// receiver. Using a reference here would make the assertion
|
|
// below compare `original` to itself -- trivially true even if
|
|
// such a bug existed -- which is exactly what a static analyzer
|
|
// can't see when it suggests "this copy is never modified, use
|
|
// a reference instead".
|
|
json copy_for_patch = original; // NOLINT(performance-unnecessary-copy-initialization)
|
|
CHECK_THROWS_AS(copy_for_patch.patch(patch), json::other_error&);
|
|
CHECK(copy_for_patch == original);
|
|
|
|
// patch_inplace(), in contrast, already applied the successful
|
|
// "replace" operation to the document before the "test" operation
|
|
// threw -- that change is not rolled back
|
|
json doc = original;
|
|
CHECK_THROWS_AS(doc.patch_inplace(patch), json::other_error&);
|
|
CHECK(doc != original);
|
|
CHECK(doc.at("baz") == "boo");
|
|
CHECK(doc.at("foo") == "bar");
|
|
}
|
|
#endif // !defined(JSON_NOEXCEPTION)
|
|
}
|
|
|
|
SECTION("errors")
|
|
{
|
|
SECTION("unknown operation")
|
|
{
|
|
SECTION("not an array")
|
|
{
|
|
json const j;
|
|
json const patch = {{"op", "add"}, {"path", ""}, {"value", 1}};
|
|
CHECK_THROWS_WITH_AS(j.patch(patch), "[json.exception.parse_error.104] parse error: JSON patch must be an array of objects", json::parse_error&);
|
|
}
|
|
|
|
SECTION("not an array of objects")
|
|
{
|
|
json const j;
|
|
json const patch = {"op", "add", "path", "", "value", 1};
|
|
#if JSON_DIAGNOSTICS
|
|
CHECK_THROWS_WITH_AS(j.patch(patch), "[json.exception.parse_error.104] parse error: (/0) JSON patch must be an array of objects", json::parse_error&);
|
|
#else
|
|
CHECK_THROWS_WITH_AS(j.patch(patch), "[json.exception.parse_error.104] parse error: JSON patch must be an array of objects", json::parse_error&);
|
|
#endif
|
|
}
|
|
|
|
SECTION("missing 'op'")
|
|
{
|
|
json const j;
|
|
json const patch = {{{"foo", "bar"}}};
|
|
#if JSON_DIAGNOSTICS
|
|
CHECK_THROWS_WITH_AS(j.patch(patch), "[json.exception.parse_error.105] parse error: (/0) operation must have member 'op'", json::parse_error&);
|
|
#else
|
|
CHECK_THROWS_WITH_AS(j.patch(patch), "[json.exception.parse_error.105] parse error: operation must have member 'op'", json::parse_error&);
|
|
#endif
|
|
}
|
|
|
|
SECTION("non-string 'op'")
|
|
{
|
|
json const j;
|
|
json const patch = {{{"op", 1}}};
|
|
#if JSON_DIAGNOSTICS
|
|
CHECK_THROWS_WITH_AS(j.patch(patch), "[json.exception.parse_error.105] parse error: (/0) operation must have string member 'op'", json::parse_error&);
|
|
#else
|
|
CHECK_THROWS_WITH_AS(j.patch(patch), "[json.exception.parse_error.105] parse error: operation must have string member 'op'", json::parse_error&);
|
|
#endif
|
|
}
|
|
|
|
SECTION("invalid operation")
|
|
{
|
|
json const j;
|
|
json const patch = {{{"op", "foo"}, {"path", ""}}};
|
|
#if JSON_DIAGNOSTICS
|
|
CHECK_THROWS_WITH_AS(j.patch(patch), "[json.exception.parse_error.105] parse error: (/0) operation value 'foo' is invalid", json::parse_error&);
|
|
#else
|
|
CHECK_THROWS_WITH_AS(j.patch(patch), "[json.exception.parse_error.105] parse error: operation value 'foo' is invalid", json::parse_error&);
|
|
#endif
|
|
}
|
|
}
|
|
|
|
SECTION("add")
|
|
{
|
|
SECTION("missing 'path'")
|
|
{
|
|
json const j;
|
|
json const patch = {{{"op", "add"}}};
|
|
#if JSON_DIAGNOSTICS
|
|
CHECK_THROWS_WITH_AS(j.patch(patch), "[json.exception.parse_error.105] parse error: (/0) operation 'add' must have member 'path'", json::parse_error&);
|
|
#else
|
|
CHECK_THROWS_WITH_AS(j.patch(patch), "[json.exception.parse_error.105] parse error: operation 'add' must have member 'path'", json::parse_error&);
|
|
#endif
|
|
}
|
|
|
|
SECTION("non-string 'path'")
|
|
{
|
|
json const j;
|
|
json const patch = {{{"op", "add"}, {"path", 1}}};
|
|
#if JSON_DIAGNOSTICS
|
|
CHECK_THROWS_WITH_AS(j.patch(patch), "[json.exception.parse_error.105] parse error: (/0) operation 'add' must have string member 'path'", json::parse_error&);
|
|
#else
|
|
CHECK_THROWS_WITH_AS(j.patch(patch), "[json.exception.parse_error.105] parse error: operation 'add' must have string member 'path'", json::parse_error&);
|
|
#endif
|
|
}
|
|
|
|
SECTION("missing 'value'")
|
|
{
|
|
json const j;
|
|
json const patch = {{{"op", "add"}, {"path", ""}}};
|
|
#if JSON_DIAGNOSTICS
|
|
CHECK_THROWS_WITH_AS(j.patch(patch), "[json.exception.parse_error.105] parse error: (/0) operation 'add' must have member 'value'", json::parse_error&);
|
|
#else
|
|
CHECK_THROWS_WITH_AS(j.patch(patch), "[json.exception.parse_error.105] parse error: operation 'add' must have member 'value'", json::parse_error&);
|
|
#endif
|
|
}
|
|
|
|
SECTION("invalid array index")
|
|
{
|
|
json const j = {1, 2};
|
|
json const patch = {{{"op", "add"}, {"path", "/4"}, {"value", 4}}};
|
|
CHECK_THROWS_WITH_AS(j.patch(patch), "[json.exception.out_of_range.401] array index 4 is out of range", json::out_of_range&);
|
|
}
|
|
}
|
|
|
|
SECTION("remove")
|
|
{
|
|
SECTION("missing 'path'")
|
|
{
|
|
json const j;
|
|
json const patch = {{{"op", "remove"}}};
|
|
#if JSON_DIAGNOSTICS
|
|
CHECK_THROWS_WITH_AS(j.patch(patch), "[json.exception.parse_error.105] parse error: (/0) operation 'remove' must have member 'path'", json::parse_error&);
|
|
#else
|
|
CHECK_THROWS_WITH_AS(j.patch(patch), "[json.exception.parse_error.105] parse error: operation 'remove' must have member 'path'", json::parse_error&);
|
|
#endif
|
|
}
|
|
|
|
SECTION("non-string 'path'")
|
|
{
|
|
json const j;
|
|
json const patch = {{{"op", "remove"}, {"path", 1}}};
|
|
#if JSON_DIAGNOSTICS
|
|
CHECK_THROWS_WITH_AS(j.patch(patch), "[json.exception.parse_error.105] parse error: (/0) operation 'remove' must have string member 'path'", json::parse_error&);
|
|
#else
|
|
CHECK_THROWS_WITH_AS(j.patch(patch), "[json.exception.parse_error.105] parse error: operation 'remove' must have string member 'path'", json::parse_error&);
|
|
#endif
|
|
}
|
|
|
|
SECTION("nonexisting target location (array)")
|
|
{
|
|
json const j = {1, 2, 3};
|
|
json const patch = {{{"op", "remove"}, {"path", "/17"}}};
|
|
CHECK_THROWS_WITH_AS(j.patch(patch), "[json.exception.out_of_range.401] array index 17 is out of range", json::out_of_range&);
|
|
}
|
|
|
|
SECTION("nonexisting target location (object)")
|
|
{
|
|
json const j = {{"foo", 1}, {"bar", 2}};
|
|
json const patch = {{{"op", "remove"}, {"path", "/baz"}}};
|
|
CHECK_THROWS_WITH_AS(j.patch(patch), "[json.exception.out_of_range.403] key 'baz' not found", json::out_of_range&);
|
|
}
|
|
|
|
SECTION("root element as target location")
|
|
{
|
|
json const j = "string";
|
|
json const patch = {{{"op", "remove"}, {"path", ""}}};
|
|
CHECK_THROWS_WITH_AS(j.patch(patch), "[json.exception.out_of_range.405] JSON pointer has no parent", json::out_of_range&);
|
|
}
|
|
}
|
|
|
|
SECTION("replace")
|
|
{
|
|
SECTION("missing 'path'")
|
|
{
|
|
json const j;
|
|
json const patch = {{{"op", "replace"}}};
|
|
#if JSON_DIAGNOSTICS
|
|
CHECK_THROWS_WITH_AS(j.patch(patch), "[json.exception.parse_error.105] parse error: (/0) operation 'replace' must have member 'path'", json::parse_error&);
|
|
#else
|
|
CHECK_THROWS_WITH_AS(j.patch(patch), "[json.exception.parse_error.105] parse error: operation 'replace' must have member 'path'", json::parse_error&);
|
|
#endif
|
|
}
|
|
|
|
SECTION("non-string 'path'")
|
|
{
|
|
json const j;
|
|
json const patch = {{{"op", "replace"}, {"path", 1}}};
|
|
#if JSON_DIAGNOSTICS
|
|
CHECK_THROWS_WITH_AS(j.patch(patch), "[json.exception.parse_error.105] parse error: (/0) operation 'replace' must have string member 'path'", json::parse_error&);
|
|
#else
|
|
CHECK_THROWS_WITH_AS(j.patch(patch), "[json.exception.parse_error.105] parse error: operation 'replace' must have string member 'path'", json::parse_error&);
|
|
#endif
|
|
}
|
|
|
|
SECTION("missing 'value'")
|
|
{
|
|
json const j;
|
|
json const patch = {{{"op", "replace"}, {"path", ""}}};
|
|
#if JSON_DIAGNOSTICS
|
|
CHECK_THROWS_WITH_AS(j.patch(patch), "[json.exception.parse_error.105] parse error: (/0) operation 'replace' must have member 'value'", json::parse_error&);
|
|
#else
|
|
CHECK_THROWS_WITH_AS(j.patch(patch), "[json.exception.parse_error.105] parse error: operation 'replace' must have member 'value'", json::parse_error&);
|
|
#endif
|
|
}
|
|
|
|
SECTION("nonexisting target location (array)")
|
|
{
|
|
json const j = {1, 2, 3};
|
|
json const patch = {{{"op", "replace"}, {"path", "/17"}, {"value", 19}}};
|
|
CHECK_THROWS_WITH_AS(j.patch(patch), "[json.exception.out_of_range.401] array index 17 is out of range", json::out_of_range&);
|
|
}
|
|
|
|
SECTION("nonexisting target location (object)")
|
|
{
|
|
json const j = {{"foo", 1}, {"bar", 2}};
|
|
json const patch = {{{"op", "replace"}, {"path", "/baz"}, {"value", 3}}};
|
|
CHECK_THROWS_WITH_AS(j.patch(patch), "[json.exception.out_of_range.403] key 'baz' not found", json::out_of_range&);
|
|
}
|
|
}
|
|
|
|
SECTION("move")
|
|
{
|
|
SECTION("missing 'path'")
|
|
{
|
|
json const j;
|
|
json const patch = {{{"op", "move"}}};
|
|
#if JSON_DIAGNOSTICS
|
|
CHECK_THROWS_WITH_AS(j.patch(patch), "[json.exception.parse_error.105] parse error: (/0) operation 'move' must have member 'path'", json::parse_error&);
|
|
#else
|
|
CHECK_THROWS_WITH_AS(j.patch(patch), "[json.exception.parse_error.105] parse error: operation 'move' must have member 'path'", json::parse_error&);
|
|
#endif
|
|
}
|
|
|
|
SECTION("non-string 'path'")
|
|
{
|
|
json const j;
|
|
json const patch = {{{"op", "move"}, {"path", 1}}};
|
|
#if JSON_DIAGNOSTICS
|
|
CHECK_THROWS_WITH_AS(j.patch(patch), "[json.exception.parse_error.105] parse error: (/0) operation 'move' must have string member 'path'", json::parse_error&);
|
|
#else
|
|
CHECK_THROWS_WITH_AS(j.patch(patch), "[json.exception.parse_error.105] parse error: operation 'move' must have string member 'path'", json::parse_error&);
|
|
#endif
|
|
}
|
|
|
|
SECTION("missing 'from'")
|
|
{
|
|
json const j;
|
|
json const patch = {{{"op", "move"}, {"path", ""}}};
|
|
CHECK_THROWS_AS(j.patch(patch), json::parse_error&);
|
|
#if JSON_DIAGNOSTICS
|
|
CHECK_THROWS_WITH_AS(j.patch(patch), "[json.exception.parse_error.105] parse error: (/0) operation 'move' must have member 'from'", json::parse_error&);
|
|
#else
|
|
CHECK_THROWS_WITH_AS(j.patch(patch), "[json.exception.parse_error.105] parse error: operation 'move' must have member 'from'", json::parse_error&);
|
|
#endif
|
|
}
|
|
|
|
SECTION("non-string 'from'")
|
|
{
|
|
json const j;
|
|
json const patch = {{{"op", "move"}, {"path", ""}, {"from", 1}}};
|
|
CHECK_THROWS_AS(j.patch(patch), json::parse_error&);
|
|
#if JSON_DIAGNOSTICS
|
|
CHECK_THROWS_WITH_AS(j.patch(patch), "[json.exception.parse_error.105] parse error: (/0) operation 'move' must have string member 'from'", json::parse_error&);
|
|
#else
|
|
CHECK_THROWS_WITH_AS(j.patch(patch), "[json.exception.parse_error.105] parse error: operation 'move' must have string member 'from'", json::parse_error&);
|
|
#endif
|
|
}
|
|
|
|
SECTION("nonexisting from location (array)")
|
|
{
|
|
json const j = {1, 2, 3};
|
|
json const patch = {{{"op", "move"}, {"path", "/0"}, {"from", "/5"}}};
|
|
CHECK_THROWS_WITH_AS(j.patch(patch), "[json.exception.out_of_range.401] array index 5 is out of range", json::out_of_range&);
|
|
}
|
|
|
|
SECTION("nonexisting from location (object)")
|
|
{
|
|
json const j = {{"foo", 1}, {"bar", 2}};
|
|
json const patch = {{{"op", "move"}, {"path", "/baz"}, {"from", "/baz"}}};
|
|
CHECK_THROWS_WITH_AS(j.patch(patch), "[json.exception.out_of_range.403] key 'baz' not found", json::out_of_range&);
|
|
}
|
|
}
|
|
|
|
SECTION("copy")
|
|
{
|
|
SECTION("missing 'path'")
|
|
{
|
|
json const j;
|
|
json const patch = {{{"op", "copy"}}};
|
|
#if JSON_DIAGNOSTICS
|
|
CHECK_THROWS_WITH_AS(j.patch(patch), "[json.exception.parse_error.105] parse error: (/0) operation 'copy' must have member 'path'", json::parse_error&);
|
|
#else
|
|
CHECK_THROWS_WITH_AS(j.patch(patch), "[json.exception.parse_error.105] parse error: operation 'copy' must have member 'path'", json::parse_error&);
|
|
#endif
|
|
}
|
|
|
|
SECTION("non-string 'path'")
|
|
{
|
|
json const j;
|
|
json const patch = {{{"op", "copy"}, {"path", 1}}};
|
|
#if JSON_DIAGNOSTICS
|
|
CHECK_THROWS_WITH_AS(j.patch(patch), "[json.exception.parse_error.105] parse error: (/0) operation 'copy' must have string member 'path'", json::parse_error&);
|
|
#else
|
|
CHECK_THROWS_WITH_AS(j.patch(patch), "[json.exception.parse_error.105] parse error: operation 'copy' must have string member 'path'", json::parse_error&);
|
|
#endif
|
|
}
|
|
|
|
SECTION("missing 'from'")
|
|
{
|
|
json const j;
|
|
json const patch = {{{"op", "copy"}, {"path", ""}}};
|
|
#if JSON_DIAGNOSTICS
|
|
CHECK_THROWS_WITH_AS(j.patch(patch), "[json.exception.parse_error.105] parse error: (/0) operation 'copy' must have member 'from'", json::parse_error&);
|
|
#else
|
|
CHECK_THROWS_WITH_AS(j.patch(patch), "[json.exception.parse_error.105] parse error: operation 'copy' must have member 'from'", json::parse_error&);
|
|
#endif
|
|
}
|
|
|
|
SECTION("non-string 'from'")
|
|
{
|
|
json const j;
|
|
json const patch = {{{"op", "copy"}, {"path", ""}, {"from", 1}}};
|
|
#if JSON_DIAGNOSTICS
|
|
CHECK_THROWS_WITH_AS(j.patch(patch), "[json.exception.parse_error.105] parse error: (/0) operation 'copy' must have string member 'from'", json::parse_error&);
|
|
#else
|
|
CHECK_THROWS_WITH_AS(j.patch(patch), "[json.exception.parse_error.105] parse error: operation 'copy' must have string member 'from'", json::parse_error&);
|
|
#endif
|
|
}
|
|
|
|
SECTION("nonexisting from location (array)")
|
|
{
|
|
json const j = {1, 2, 3};
|
|
json const patch = {{{"op", "copy"}, {"path", "/0"}, {"from", "/5"}}};
|
|
CHECK_THROWS_WITH_AS(j.patch(patch), "[json.exception.out_of_range.401] array index 5 is out of range", json::out_of_range&);
|
|
}
|
|
|
|
SECTION("nonexisting from location (object)")
|
|
{
|
|
json const j = {{"foo", 1}, {"bar", 2}};
|
|
json const patch = {{{"op", "copy"}, {"path", "/fob"}, {"from", "/baz"}}};
|
|
CHECK_THROWS_WITH_AS(j.patch(patch), "[json.exception.out_of_range.403] key 'baz' not found", json::out_of_range&);
|
|
}
|
|
}
|
|
|
|
SECTION("test")
|
|
{
|
|
SECTION("missing 'path'")
|
|
{
|
|
json const j;
|
|
json const patch = {{{"op", "test"}}};
|
|
#if JSON_DIAGNOSTICS
|
|
CHECK_THROWS_WITH_AS(j.patch(patch), "[json.exception.parse_error.105] parse error: (/0) operation 'test' must have member 'path'", json::parse_error&);
|
|
#else
|
|
CHECK_THROWS_WITH_AS(j.patch(patch), "[json.exception.parse_error.105] parse error: operation 'test' must have member 'path'", json::parse_error&);
|
|
#endif
|
|
}
|
|
|
|
SECTION("non-string 'path'")
|
|
{
|
|
json const j;
|
|
json const patch = {{{"op", "test"}, {"path", 1}}};
|
|
#if JSON_DIAGNOSTICS
|
|
CHECK_THROWS_WITH_AS(j.patch(patch), "[json.exception.parse_error.105] parse error: (/0) operation 'test' must have string member 'path'", json::parse_error&);
|
|
#else
|
|
CHECK_THROWS_WITH_AS(j.patch(patch), "[json.exception.parse_error.105] parse error: operation 'test' must have string member 'path'", json::parse_error&);
|
|
#endif
|
|
}
|
|
|
|
SECTION("missing 'value'")
|
|
{
|
|
json const j;
|
|
json const patch = {{{"op", "test"}, {"path", ""}}};
|
|
#if JSON_DIAGNOSTICS
|
|
CHECK_THROWS_WITH_AS(j.patch(patch), "[json.exception.parse_error.105] parse error: (/0) operation 'test' must have member 'value'", json::parse_error&);
|
|
#else
|
|
CHECK_THROWS_WITH_AS(j.patch(patch), "[json.exception.parse_error.105] parse error: operation 'test' must have member 'value'", json::parse_error&);
|
|
#endif
|
|
}
|
|
}
|
|
}
|
|
|
|
SECTION("Examples from jsonpatch.com")
|
|
{
|
|
SECTION("Simple Example")
|
|
{
|
|
// The original document
|
|
json const doc = R"(
|
|
{
|
|
"baz": "qux",
|
|
"foo": "bar"
|
|
}
|
|
)"_json;
|
|
|
|
// The patch
|
|
json const patch = R"(
|
|
[
|
|
{ "op": "replace", "path": "/baz", "value": "boo" },
|
|
{ "op": "add", "path": "/hello", "value": ["world"] },
|
|
{ "op": "remove", "path": "/foo"}
|
|
]
|
|
)"_json;
|
|
|
|
// The result
|
|
json result = R"(
|
|
{
|
|
"baz": "boo",
|
|
"hello": ["world"]
|
|
}
|
|
)"_json;
|
|
|
|
// check if patched value is as expected
|
|
CHECK(doc.patch(patch) == result);
|
|
|
|
// check roundtrip
|
|
CHECK(doc.patch(json::diff(doc, result)) == result);
|
|
}
|
|
|
|
SECTION("Operations")
|
|
{
|
|
// The original document
|
|
json const doc = R"(
|
|
{
|
|
"biscuits": [
|
|
{"name":"Digestive"},
|
|
{"name": "Choco Liebniz"}
|
|
]
|
|
}
|
|
)"_json;
|
|
|
|
SECTION("add")
|
|
{
|
|
// The patch
|
|
json const patch = R"(
|
|
[
|
|
{"op": "add", "path": "/biscuits/1", "value": {"name": "Ginger Nut"}}
|
|
]
|
|
)"_json;
|
|
|
|
// The result
|
|
json result = R"(
|
|
{
|
|
"biscuits": [
|
|
{"name": "Digestive"},
|
|
{"name": "Ginger Nut"},
|
|
{"name": "Choco Liebniz"}
|
|
]
|
|
}
|
|
)"_json;
|
|
|
|
// check if patched value is as expected
|
|
CHECK(doc.patch(patch) == result);
|
|
|
|
// check roundtrip
|
|
CHECK(doc.patch(json::diff(doc, result)) == result);
|
|
}
|
|
|
|
SECTION("remove")
|
|
{
|
|
// The patch
|
|
json const patch = R"(
|
|
[
|
|
{"op": "remove", "path": "/biscuits"}
|
|
]
|
|
)"_json;
|
|
|
|
// The result
|
|
json result = R"(
|
|
{}
|
|
)"_json;
|
|
|
|
// check if patched value is as expected
|
|
CHECK(doc.patch(patch) == result);
|
|
|
|
// check roundtrip
|
|
CHECK(doc.patch(json::diff(doc, result)) == result);
|
|
}
|
|
|
|
SECTION("replace")
|
|
{
|
|
// The patch
|
|
json const patch = R"(
|
|
[
|
|
{"op": "replace", "path": "/biscuits/0/name", "value": "Chocolate Digestive"}
|
|
]
|
|
)"_json;
|
|
|
|
// The result
|
|
json result = R"(
|
|
{
|
|
"biscuits": [
|
|
{"name": "Chocolate Digestive"},
|
|
{"name": "Choco Liebniz"}
|
|
]
|
|
}
|
|
)"_json;
|
|
|
|
// check if patched value is as expected
|
|
CHECK(doc.patch(patch) == result);
|
|
|
|
// check roundtrip
|
|
CHECK(doc.patch(json::diff(doc, result)) == result);
|
|
}
|
|
|
|
SECTION("copy")
|
|
{
|
|
// The patch
|
|
json const patch = R"(
|
|
[
|
|
{"op": "copy", "from": "/biscuits/0", "path": "/best_biscuit"}
|
|
]
|
|
)"_json;
|
|
|
|
// The result
|
|
json result = R"(
|
|
{
|
|
"biscuits": [
|
|
{"name": "Digestive"},
|
|
{"name": "Choco Liebniz"}
|
|
],
|
|
"best_biscuit": {
|
|
"name": "Digestive"
|
|
}
|
|
}
|
|
)"_json;
|
|
|
|
// check if patched value is as expected
|
|
CHECK(doc.patch(patch) == result);
|
|
|
|
// check roundtrip
|
|
CHECK(doc.patch(json::diff(doc, result)) == result);
|
|
}
|
|
|
|
SECTION("move")
|
|
{
|
|
// The patch
|
|
json const patch = R"(
|
|
[
|
|
{"op": "move", "from": "/biscuits", "path": "/cookies"}
|
|
]
|
|
)"_json;
|
|
|
|
// The result
|
|
json result = R"(
|
|
{
|
|
"cookies": [
|
|
{"name": "Digestive"},
|
|
{"name": "Choco Liebniz"}
|
|
]
|
|
}
|
|
)"_json;
|
|
|
|
// check if patched value is as expected
|
|
CHECK(doc.patch(patch) == result);
|
|
|
|
// check roundtrip
|
|
CHECK(doc.patch(json::diff(doc, result)) == result);
|
|
}
|
|
|
|
SECTION("test")
|
|
{
|
|
// The patch
|
|
json patch = R"(
|
|
[
|
|
{"op": "test", "path": "/best_biscuit/name", "value": "Choco Liebniz"}
|
|
]
|
|
)"_json;
|
|
|
|
// the test will fail
|
|
CHECK_THROWS_AS(doc.patch(patch), json::other_error&);
|
|
#if JSON_DIAGNOSTICS
|
|
CHECK_THROWS_WITH_STD_STR(doc.patch(patch), "[json.exception.other_error.501] (/0) unsuccessful: " + patch[0].dump());
|
|
#elif JSON_DIAGNOSTIC_POSITIONS
|
|
CHECK_THROWS_WITH_STD_STR(doc.patch(patch), "[json.exception.other_error.501] (bytes 47-117) unsuccessful: " + patch[0].dump());
|
|
#else
|
|
CHECK_THROWS_WITH_STD_STR(doc.patch(patch), "[json.exception.other_error.501] unsuccessful: " + patch[0].dump());
|
|
#endif
|
|
}
|
|
}
|
|
}
|
|
|
|
SECTION("Examples from bruth.github.io/jsonpatch-js")
|
|
{
|
|
SECTION("add")
|
|
{
|
|
CHECK(R"( {} )"_json.patch(
|
|
R"( [{"op": "add", "path": "/foo", "value": "bar"}] )"_json
|
|
) == R"( {"foo": "bar"} )"_json);
|
|
|
|
CHECK(R"( {"foo": [1, 3]} )"_json.patch(
|
|
R"( [{"op": "add", "path": "/foo", "value": "bar"}] )"_json
|
|
) == R"( {"foo": "bar"} )"_json);
|
|
|
|
CHECK(R"( {"foo": [{}]} )"_json.patch(
|
|
R"( [{"op": "add", "path": "/foo/0/bar", "value": "baz"}] )"_json
|
|
) == R"( {"foo": [{"bar": "baz"}]} )"_json);
|
|
}
|
|
|
|
SECTION("remove")
|
|
{
|
|
CHECK(R"( {"foo": "bar"} )"_json.patch(
|
|
R"( [{"op": "remove", "path": "/foo"}] )"_json
|
|
) == R"( {} )"_json);
|
|
|
|
CHECK(R"( {"foo": [1, 2, 3]} )"_json.patch(
|
|
R"( [{"op": "remove", "path": "/foo/1"}] )"_json
|
|
) == R"( {"foo": [1, 3]} )"_json);
|
|
|
|
CHECK(R"( {"foo": [{"bar": "baz"}]} )"_json.patch(
|
|
R"( [{"op": "remove", "path": "/foo/0/bar"}] )"_json
|
|
) == R"( {"foo": [{}]} )"_json);
|
|
}
|
|
|
|
SECTION("replace")
|
|
{
|
|
CHECK(R"( {"foo": "bar"} )"_json.patch(
|
|
R"( [{"op": "replace", "path": "/foo", "value": 1}] )"_json
|
|
) == R"( {"foo": 1} )"_json);
|
|
|
|
CHECK(R"( {"foo": [1, 2, 3]} )"_json.patch(
|
|
R"( [{"op": "replace", "path": "/foo/1", "value": 4}] )"_json
|
|
) == R"( {"foo": [1, 4, 3]} )"_json);
|
|
|
|
CHECK(R"( {"foo": [{"bar": "baz"}]} )"_json.patch(
|
|
R"( [{"op": "replace", "path": "/foo/0/bar", "value": 1}] )"_json
|
|
) == R"( {"foo": [{"bar": 1}]} )"_json);
|
|
}
|
|
|
|
SECTION("move")
|
|
{
|
|
CHECK(R"( {"foo": [1, 2, 3]} )"_json.patch(
|
|
R"( [{"op": "move", "from": "/foo", "path": "/bar"}] )"_json
|
|
) == R"( {"bar": [1, 2, 3]} )"_json);
|
|
}
|
|
|
|
SECTION("copy")
|
|
{
|
|
CHECK(R"( {"foo": [1, 2, 3]} )"_json.patch(
|
|
R"( [{"op": "copy", "from": "/foo/1", "path": "/bar"}] )"_json
|
|
) == R"( {"foo": [1, 2, 3], "bar": 2} )"_json);
|
|
}
|
|
|
|
SECTION("copy")
|
|
{
|
|
CHECK_NOTHROW(R"( {"foo": "bar"} )"_json.patch(
|
|
R"( [{"op": "test", "path": "/foo", "value": "bar"}] )"_json));
|
|
}
|
|
}
|
|
|
|
SECTION("Tests from github.com/json-patch/json-patch-tests")
|
|
{
|
|
for (const auto* filename :
|
|
{
|
|
TEST_DATA_DIRECTORY "/json-patch-tests/spec_tests.json",
|
|
TEST_DATA_DIRECTORY "/json-patch-tests/tests.json"
|
|
})
|
|
{
|
|
CAPTURE(filename)
|
|
std::ifstream f(filename);
|
|
json const suite = json::parse(f);
|
|
|
|
for (const auto& test : suite)
|
|
{
|
|
INFO_WITH_TEMP(test.value("comment", ""));
|
|
|
|
// skip tests marked as disabled
|
|
if (test.value("disabled", false))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
const auto& doc = test["doc"];
|
|
const auto& patch = test["patch"];
|
|
|
|
if (test.count("error") == 0) // NOLINT(readability-container-contains)
|
|
{
|
|
// if an expected value is given, use it; use doc otherwise
|
|
const auto& expected = test.value("expected", doc);
|
|
CHECK(doc.patch(patch) == expected);
|
|
}
|
|
else
|
|
{
|
|
CHECK_THROWS(doc.patch(patch));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
TEST_CASE("JSON patch - add to a primitive parent (regression #4292)")
|
|
{
|
|
// Regression test for https://github.com/nlohmann/json/issues/4292
|
|
//
|
|
// An "add" operation whose parent location resolves to a primitive
|
|
// (non-container) value must be rejected with a catchable exception.
|
|
// Previously this hit JSON_ASSERT(false) in operation_add, which aborts
|
|
// the process in debug builds and silently dropped the operation (leaving
|
|
// a wrong result) when assertions were compiled out (NDEBUG). It now
|
|
// throws out_of_range.411.
|
|
//
|
|
// The documents below are constructed programmatically (not parsed) so
|
|
// they carry no byte positions; the JSON_DIAGNOSTICS path prefix is
|
|
// handled by the guards. The exact message with positions is covered in
|
|
// unit-diagnostic-positions.cpp.
|
|
|
|
SECTION("string parent")
|
|
{
|
|
json const doc = {{"foo", {{"bar", "a string"}}}};
|
|
json const patch = {{{"op", "add"}, {"path", "/foo/bar/baz"}, {"value", 1}}};
|
|
#if JSON_DIAGNOSTICS
|
|
CHECK_THROWS_WITH_AS(doc.patch(patch), "[json.exception.out_of_range.411] (/foo/bar) cannot add value: the JSON Patch 'add' 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.411] cannot add value: the JSON Patch 'add' target's parent is of type string, but must be an object or array", json::out_of_range&);
|
|
#endif
|
|
}
|
|
|
|
SECTION("number parent")
|
|
{
|
|
json const doc = {{"foo", 1}};
|
|
json const patch = {{{"op", "add"}, {"path", "/foo/bar"}, {"value", 2}}};
|
|
#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 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.411] cannot add value: the JSON Patch 'add' target's parent is of type number, but must be an object or array", json::out_of_range&);
|
|
#endif
|
|
}
|
|
|
|
SECTION("original two-step sequence from the issue")
|
|
{
|
|
// The user's two-step patch from #4292: first turn /xyz/1 into a
|
|
// string, then try to add a member inside that string.
|
|
json const doc = R"( { "xyz": [ { "lmn": "214", "nnp": "001" } ] } )"_json;
|
|
json const patch = R"(
|
|
[
|
|
{ "op": "add", "path": "/xyz/1", "value": "" },
|
|
{ "op": "add", "path": "/xyz/1/lmn", "value": "214" }
|
|
]
|
|
)"_json;
|
|
|
|
CHECK_THROWS_AS(doc.patch(patch), json::out_of_range&);
|
|
}
|
|
}
|
|
|
|
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")
|
|
{
|
|
SECTION("array shrunk to empty")
|
|
{
|
|
json const source = {0, 1, 2, 3, 4};
|
|
json const target = json::array();
|
|
|
|
json const patch = json::diff(source, target);
|
|
|
|
json const expected = R"(
|
|
[
|
|
{"op": "remove", "path": "/4"},
|
|
{"op": "remove", "path": "/3"},
|
|
{"op": "remove", "path": "/2"},
|
|
{"op": "remove", "path": "/1"},
|
|
{"op": "remove", "path": "/0"}
|
|
]
|
|
)"_json;
|
|
|
|
CHECK(patch == expected);
|
|
CHECK(source.patch(patch) == target);
|
|
}
|
|
|
|
SECTION("array partially shrunk, after a replacement at a common index")
|
|
{
|
|
json const source = {0, 1, 2, 3, 4};
|
|
json const target = {0, 9};
|
|
|
|
json const patch = json::diff(source, target);
|
|
|
|
// the replacement comes first, then the removals, highest index first
|
|
json const expected = R"(
|
|
[
|
|
{"op": "replace", "path": "/1", "value": 9},
|
|
{"op": "remove", "path": "/4"},
|
|
{"op": "remove", "path": "/3"},
|
|
{"op": "remove", "path": "/2"}
|
|
]
|
|
)"_json;
|
|
|
|
CHECK(patch == expected);
|
|
CHECK(source.patch(patch) == target);
|
|
}
|
|
|
|
SECTION("nested array shrunk")
|
|
{
|
|
json const source = {{"a", {0, 1, 2}}};
|
|
json const target = {{"a", json::array()}};
|
|
|
|
json const patch = json::diff(source, target);
|
|
|
|
json const expected = R"(
|
|
[
|
|
{"op": "remove", "path": "/a/2"},
|
|
{"op": "remove", "path": "/a/1"},
|
|
{"op": "remove", "path": "/a/0"}
|
|
]
|
|
)"_json;
|
|
|
|
CHECK(patch == expected);
|
|
CHECK(source.patch(patch) == target);
|
|
}
|
|
|
|
SECTION("many removals still round-trip")
|
|
{
|
|
json source = json::array();
|
|
for (int i = 0; i < 1000; ++i)
|
|
{
|
|
source.push_back(i);
|
|
}
|
|
json const target = json::array();
|
|
|
|
json const patch = json::diff(source, target);
|
|
|
|
CHECK(patch.size() == 1000);
|
|
CHECK(patch.front().at("path") == "/999");
|
|
CHECK(patch.back().at("path") == "/0");
|
|
CHECK(source.patch(patch) == target);
|
|
}
|
|
}
|