mirror of
https://github.com/nlohmann/json.git
synced 2026-10-02 06:55:16 +00:00
* Drop stale LCOV_EXCL_LINE from the json_pointer out_of_range.410 throw The comment said the size_type overflow check in array_index() is only triggered on special platforms like 32-bit, and the throw was excluded from coverage. On 64-bit platforms the check is true for SIZE_MAX itself, and unit-json_pointer.cpp has asserted that case four times since #5395, so the line is executed in the coverage job. Reword the comment and remove the exclusion marker so the coverage report notices if the tests stop reaching it. Part of #5725 Signed-off-by: Niels Lohmann <mail@nlohmann.me> * Name all three C-array check aliases in the enum-macro NOLINTs NLOHMANN_JSON_SERIALIZE_ENUM(_STRICT) suppressed the c-array warning under modernize-avoid-c-arrays only, but clang-tidy emits the same diagnostic under the aliases cppcoreguidelines-avoid-c-arrays and hicpp-avoid-c-arrays too. Any user running those checks got a false positive at every macro expansion, and our own tests needed a local NOLINT at each call site to work around it. Name all three aliases in the four macro comments instead, and drop the now-redundant c-array names from the five test call-site NOLINTs. Comment-only change; behavior, the public API, and the ABI do not change. Ran make amalgamate. Part of #5725 Signed-off-by: Niels Lohmann <mail@nlohmann.me> * Include doctest as a SYSTEM directory instead of disabling warnings for all tests test_main added -Wno-deprecated and -Wno-float-equal as PUBLIC compile options for every non-MSVC compiler, so they were applied to every translation unit, library headers included, and silenced the CI warnings meant to check the library's own -Wfloat-equal pragmas. The only code that actually needed the suppression was the vendored doctest.h, which was included as a normal (non-SYSTEM) directory. Include thirdparty/doctest as SYSTEM for test_main, matching what tests/abi/CMakeLists.txt already does, and drop the two suppressions from both targets. Verified locally that unit-comparison, unit-conversions and unit-constructor1 compile clean with -Werror -Weverything and doctest as -isystem, and that CMake still configures with JSON_BuildTests=ON. Part of #5725 Signed-off-by: Niels Lohmann <mail@nlohmann.me> * Remove the no-op ci_clang_analyze target ci_clang_analyze configured the build with the real compiler and only then wrapped ninja with scan-build. scan-build intercepts compiles by overriding CC/CXX, but build.ninja already had the compiler path baked in from the configure step, so every run bypassed the analyzer: CI logs show "No bugs found" after a normal build, never an analysis. The job also used Debian's frozen clang-tools-14 rather than the image's own clang, and CLANG_ANALYZER_CHECKS still named three valist.* checkers that current clang merged into security.VAList. ci_clang_tidy already runs every clang-analyzer-* check (via .clang-tidy's "Checks: '*'") with warnings as errors, so nothing is lost by removing the dead job. Delete ci_clang_analyze, CLANG_ANALYZER_CHECKS and the SCAN_BUILD_TOOL lookup from cmake/ci.cmake, drop it from the ubuntu.yml ci_static_analysis_clang matrix, and drop the now-unused clang-tools apt package (iwyu stays for ci_single_binaries). Reword quality_assurance.md and assurance_case.md, which described the dead job as a working control, to say the Clang Static Analyzer checks run through clang-tidy. Verified that `cmake -DJSON_CI=ON` still configures cleanly and that ci_clang_analyze no longer appears in the generated build or in any CMake/workflow file. Part of #5725 Signed-off-by: Niels Lohmann <mail@nlohmann.me> * Re-enable portability-template-virtual-member-function; remove redundant forwards .clang-tidy disabled three checks "to get the CI going" (#4489, 2024-11-13): portability-template-virtual-member-function, bugprone-use-after-move and its alias hicpp-invalid-access-moved. portability-template-virtual-member-function only flagged output_stream_adapter::write_character/write_characters; annotate both with NOLINT and re-enable the check. bugprone-use-after-move flagged several double forwards that have no effect at runtime: - from_json.hpp calls std::forward<BasicJsonType>(j).at(Idx) inside pack expansions; at() has no ref-qualified overloads and always returns an lvalue reference, so the forward is a no-op. Replace with plain j.at(Idx) in all four places. - the move constructor forwards the whole object to its base class and then reads other's members. That is item 9 of #5724 (together with its cppcheck suppressions) and is left to that change. - input_adapters.hpp forwards the container twice on purpose, so the begin/end iterator types match adapter_type; annotate with NOLINT and a comment instead of changing behavior. The check still flags the move constructor (see above) and two sites in at(KeyType&&) (both overloads, json.hpp, in the throw's string_t(std::forward<KeyType>(key)) after find(std::forward<KeyType>(key))). Open PR #5689 rewrites that hunk, so bugprone-use-after-move (and hicpp-invalid-access-moved) stay disabled for now, with a comment explaining why; re-enable them once #5689 and the #5724 move-constructor change have landed. Also resolve the portability-avoid-pragma-once TODO: single_include never has #pragma once (amalgamate.py strips it) and every supported compiler accepts it in include/, so keep it disabled with an explanatory comment instead of a TODO. Fix the stale "json.hpp, around line 1265" comment in unit-class_parser.cpp, which now points at the move constructor's actual line. Behavior, the public API and the ABI do not change. Verified with clang-tidy 22.1.8 that portability-template-virtual-member-function now reports nothing, that bugprone-use-after-move/ hicpp-invalid-access-moved report only the known at(KeyType&&) and move-constructor sites, and that unit-custom-base-class, unit-constructor1, unit-conversions, unit-element_access2, unit-class_parser and unit-diagnostic-positions (JSON_DIAGNOSTIC_POSITIONS=1) compile under ASan/UBSan and pass with the same assertion counts as before. Ran make amalgamate. Part of #5725 Signed-off-by: Niels Lohmann <mail@nlohmann.me> * Fix stale and malformed NOLINT comments json_sax.hpp named "-warnings-as-errors" in the NOLINT list on the two JSON_ASSERT(false) lines; that is the suffix clang-tidy appends to a diagnostic tag under WarningsAsErrors, not a check name, and every other JSON_ASSERT(false) omits it. unit-capacity.cpp carried 30 "// NOLINT(misc-const-correctness)" comments on "json j = ...;" declarations that are all used with non-const members afterwards, so the check has nothing to report there. unit-constructor2.cpp used a blanket "// NOLINT: access after move is OK here" on a use-after-move that hides every check on the line; naming bugprone-use-after-move and hicpp-invalid-access-moved keeps the intent once those checks are re-enabled (#5724). Signed-off-by: Niels Lohmann <mail@nlohmann.me> #5725 item 10 * Remove stale .clang-tidy entries -google-runtime-references disabled a check that neither clang-tidy 22.1.8 nor 23.1.2 lists under --list-checks -checks='*'; it was removed upstream. The commented-out HeaderFilterRegex line has been unused since the active HeaderFilterRegex was introduced in #2561 (2021). Signed-off-by: Niels Lohmann <mail@nlohmann.me> #5725 item 11 * Remove the GCC C++20 -Wignored-attributes pragma in json.hpp The pragma (added in #5164) claimed to work around the C++ modules redefinition errors of #5103, but #5103 is about hard errors (e.g. "redefinition of std::__is_constant_evaluated()", conflicting std::integral_constant) that ignoring a warning cannot suppress; they are traced to GCC PR 124430 and reproduce with <map> or <string> instead of json.hpp too. A GCC 16.2 -std=gnu++20 -fmodules build following #5103's repro steps still fails with the pragma in place, and a build of all test TUs with GCC_CXXFLAGS (which enable -Wignored-attributes) and the pragma removed produces no such warning. The block only hid a warning class from GCC C++20 users while suggesting #5103 was handled. Overlaps #5610, whose hunks touch the closing half of this pragma to insert the json_literals.hpp include. Signed-off-by: Niels Lohmann <mail@nlohmann.me> #5725 item 9 * Fix stale doxygen comments hidden by the -Wdocumentation pragma macro_scope.hpp ignores -Wdocumentation and -Wdocumentation-unknown-command for the whole library, which also hides genuine documentation mistakes: - detail::unescape() documented "@return unescaped string" but returns void and unescapes its argument in place; reworded to "@param[in,out] s string to unescape in place" and dropped the bogus @return. - basic_json::get()'s copy-conversion overload wrote "converted to @tparam ValueType" inside @return, which Doxygen and Clang parse as a second, malformed @tparam; changed to "@a ValueType", matching the two other get() overloads a few lines above that already use it. This narrows the gap the -Wdocumentation pragma needs to cover; fully replacing the Doxygen-only commands it also hides (item 2c) is left for after #5267. Signed-off-by: Niels Lohmann <mail@nlohmann.me> #5725 item 2 * Fix -Wextra-semi-stmt at its actual source, not assert() clang_flags.cmake blamed the global -Wno-extra-semi-stmt on assert(), but assert() expands to an expression under glibc and libc++ and does not trigger this warning. unit-assert_macro.cpp overrides JSON_ASSERT with "{if (!(x)) ++assert_counter; }", a bare block followed by a semicolon at every JSON_ASSERT(...) call site in the library; that was the actual source of 151 of the 208 -Wextra-semi-stmt sites found in a Clang 22 -Weverything sweep of the test suite with the flag removed. Switched to the standard do/while(false) macro idiom, which does not expand to a statement-plus-semicolon, and corrected the comment to name the remaining source instead: vendored Doctest's CAPTURE(x) shim, which already ends in a semicolon. Verified with clang++ -Wextra-semi-stmt (plus the file's other CI ignores) that unit-assert_macro.cpp now compiles without any -Wextra-semi-stmt diagnostic. Signed-off-by: Niels Lohmann <mail@nlohmann.me> #5725 item 8 (step 1 of 2; step 2 covers the CAPTURE() call sites) * Drop the redundant semicolon from CAPTURE() call sites; remove -Wno-extra-semi-stmt doctest_compatibility.h defines CAPTURE(x) as DOCTEST_CAPTURE(x); (with a trailing semicolon baked into the macro), specifically so call sites do not need to add one themselves; most of the ~267 call sites already follow that convention. The remaining 64 call sites across 20 files wrote "CAPTURE(x);" anyway, turning into a statement plus an empty statement and triggering -Wextra-semi-stmt. Dropped the redundant semicolon at each of those sites. With item 6 having already made vendored Doctest a SYSTEM include, and this the last known source of -Wextra-semi-stmt findings, removed the flag from clang_flags.cmake entirely. Verified with clang++ -Wextra-semi-stmt (plus the file's other CI ignores) that all 20 touched files, plus a file with no CAPTURE() use (unit-json_pointer.cpp), compile without any -Wextra-semi-stmt diagnostic. Signed-off-by: Niels Lohmann <mail@nlohmann.me> #5725 item 8 (step 2 of 2) * Switch ci_static_analysis_clang off the frozen LLVM 22 dev image ubuntu.yml pinned the clang-tidy/clang-tidy-sanitizer/single-binaries job to silkeh/clang:dev, a tag last pushed 2026-02-18 that reports "clang version 22.0.0 (...+20251015...)", a pre-release snapshot from before the LLVM 22 release; the maintainer now updates dev-unstable, 22, and latest instead. Switched to silkeh/clang:22, matching the other clang jobs on :latest. Verified with clang-tidy 22.1.8 (the image's actual version) against this repository's .clang-tidy and library headers what the release image newly reports compared to :dev: - readability-redundant-typename fires at ~250 sites across the _cpp20-relevant conversion/to_chars headers; the library targets C++11 and keeps the typenames, so the check is disabled in .clang-tidy, matching how the file already handles checks that don't fit a C++11 codebase. - misc-anonymous-namespace-in-header fires on the two anonymous namespaces in from_json.hpp and to_json.hpp; added the alias to their existing NOLINT (cert-dcl59-cpp, fuchsia-header-anon-namespaces, google-build-namespaces). - bugprone-std-namespace-modification fires on every addition to namespace std: the std::hash, std::formatter and std::swap overloads in json.hpp, and the std::tuple_size/std::tuple_element specializations in iteration_proxy.hpp (this last file is not named in #5725's item 5, found by actually running clang-tidy 22.1.8 against the current tree). All six are legal, deliberate additions to namespace std (explicit/partial specializations of std types, or the pre-C++20 std::swap overload); annotated each with the check name next to its existing cert-dcl58-cpp NOLINT. - modernize-avoid-c-style-cast reported nothing new. Also added clang++-22/21, clang-tidy-22/21, g++-16 and gcov-16 to the find_program search lists in ci.cmake so a local "maximal warnings" configure prefers the current toolchain version over an older one on PATH. #5725 item 5 Signed-off-by: Niels Lohmann <mail@nlohmann.me> * Regenerate cmake/gcc_flags.cmake for GCC 16.2.0 GCC_CXXFLAGS was generated for GCC 15.1.0, but ci_test_gcc and ci_test_gcc_cxx{11..26} now run in gcc:latest, currently GCC 16.2.0, so the "maximal warnings" job was missing warnings introduced since 15.1.0 while carrying entries GCC 16 treats as duplicates or no-ops. Regenerated with https://github.com/nlohmann/gcc_flags (patched locally to not crash on an option whose "-x c++ <opt> -" probe fails before it reads stdin, e.g. -Wabi=; the tool otherwise raises BrokenPipeError instead of recording the option as an error) run against g++ 16.2.0 in the official gcc:16 Docker image, keeping the documented -Wno-* exclusions and the same alphabetical placement scheme as before. Also added three GCC 16 warnings the generator cannot discover on its own because it only probes value ranges/lists it finds in the -Q option name itself, not in the enum choices --help=warnings documents separately: - -Wbidi-chars=any, -Wleading-whitespace=spaces: manually verified these compile cleanly with g++ 16.2.0. - -Wstrict-flex-arrays: deliberately NOT added, unlike the other two. Without -fstrict-flex-arrays (which the library does not enable, as it would change codegen for flexible array members), GCC prints "'-Wstrict-flex-arrays' is ignored when '-fstrict-flex-arrays' is not present" on every translation unit, and under our -Werror that note itself aborts the build. This differs from the harmless no-op warnings already kept in the file (-Whsa, -Wsynth, -Wunreachable-code, -Wunsafe-loop-optimizations), which emit nothing; #5725 item 7 named -Wstrict-flex-arrays as one of the flags GCC 16 adds, but did not anticipate this failure mode. Verified: compiled the library header and a representative set of test translation units (including ones touched by items 1, 3, 8, 9, 10 of this issue) with the regenerated GCC_CXXFLAGS plus -Werror under g++ 16.2.0 at -std=c++11 through -std=c++26, with zero warnings; ran the full local test suite (129/129 passing, unrelated to this compiler) as a regression check. CI must still confirm the actual ci_test_gcc / ci_test_standards_gcc targets end to end, since this was verified with direct g++ invocations rather than through the CMake/ CXXFLAGS environment-variable plumbing in ci.cmake. #5725 item 7 Signed-off-by: Niels Lohmann <mail@nlohmann.me> * Avoid std::basic_string<CharType> for non-character output_adapter CharType output_adapter<CharType, StringType> defaulted StringType to std::basic_string<CharType>, and (with JSON_NO_IO undefined) always declared a std::basic_ostream<CharType>&-taking constructor. For CharType with no non-deprecated std::char_traits specialization (only std::uint8_t is ever used this way, by the binary writers), simply naming either type - as an unused default template argument, or as an unused, never-called constructor's parameter type - instantiates std::char_traits<CharType> merely to name it, which some standard libraries mark deprecated: with the library-wide -Wdocumentation pragma (item 2's other half, left for a later commit) temporarily removed, an Apple clang 21 / libc++ TU calling json::to_cbor(j, vec) with std::vector<std::uint8_t>& got one -Wdeprecated-declarations warning per binary writer at the old output_adapters.hpp:193. Replaced the eager std::basic_string<CharType> / std::basic_ostream <CharType> defaults with a bool-tagged partial specialization (not std::conditional, which requires naming both branches' types up front regardless of which is selected, reproducing the same warning) that only ever names std::basic_string<CharType> / std::basic_ostream <CharType> when CharType is actually one of char, wchar_t, char16_t, char32_t, or (with __cpp_lib_char8_t) char8_t. For any other CharType, output_adapter's StringType and ostream-constructor parameter fall back to two distinct empty placeholder types, kept distinct so the two constructor overloads do not collide into a single redeclaration. Public API / behavior: passing a std::basic_string<std::uint8_t>& or std::basic_ostream<std::uint8_t>& directly to a binary writer's output_adapter now fails to compile instead of compiling with a deprecation warning; this was neither documented nor tested. All documented uses (std::vector<CharType>, std::basic_ostream<CharType> and StringType for character CharType) are unaffected. Verified with Apple clang 21 / libc++, with the two -Wdocumentation* "ignored" pragma lines in macro_scope.hpp temporarily removed and -std=c++11/c++20 plus the project's -Weverything flag set: calling to_cbor/to_msgpack/to_ubjson/to_bjdata/to_bson/to_bon8 on a std::vector<std::uint8_t> now produces no char_traits<unsigned char> (or any other) deprecation warning, while the char-based string- and ostream-adapter paths, and a to_cbor/from_cbor round trip, still compile and run correctly; also verified with GCC 16.2.0. Ran the full local test suite, including the binary-format unit tests (unit-cbor, unit-msgpack, unit-ubjson, unit-bjdata, unit-bson, unit-bon8, unit-binary_writer_sinks, unit-binary_formats, unit-custom-binary-type): 129/129 passing. #5725 item 2 (step a) Signed-off-by: Niels Lohmann <mail@nlohmann.me> * Remove the library-wide -Wdocumentation pragma; fix what it hid macro_scope.hpp / macro_unscope.hpp pushed and popped a Clang diagnostic region over the entire library that ignored -Wdocumentation and -Wdocumentation-unknown-command. Removed both pragmas and fixed every finding a full -Wdocumentation (which implies -Wdocumentation-unknown-command and -Wdocumentation-deprecated-sync) build reports, so the library now compiles clean under Clang's documentation checks without a blanket suppression. Overlaps #5267, which is still open and edits a nearby doc block (json.hpp's get()/get_impl() @return, already fixed in the item 2 step (b) commit of this branch); this commit does not touch that block again. Unknown Doxygen alias commands (Doxyfile removed in #3071, so these were never rendered by anything) rewritten as plain prose, keeping the same information: - @requirement REQ-JSON-01 / REQ-JSON-02 (iter_impl.hpp, json_reverse_iterator.hpp): now "This class satisfies the following concept requirements (REQ-JSON-0N):". - @liveexample{prose,example-id} (three sites in json.hpp): kept the prose, dropped the command wrapper and the trailing example-id (docs/mkdocs/docs/examples/*.cpp still exist and are used directly by the rendered docs, not through this in-header alias) and unescaped the "\," commas that were only needed for the old alias's comma-separated argument syntax. - @complexity X (json.hpp x4, json_pointer.hpp x2, serializer.hpp x1): now "Complexity: X". Backslash sequences Clang's comment lexer tried to parse as commands, escaped to render as literal backslashes: - lexer.hpp get_codepoint(): two `\u` occurrences. - binary_reader.hpp get_bson_cstr() / get_bson_cstr_bulk(): two `\x00` occurrences. - serializer.hpp: three `\uXXXX` occurrences (constructor @param, append_codepoint_to_string_buffer() @brief, and the ensure_ascii member comment). One finding remained after all of the above: Clang reports "declaration is marked with '@deprecated' command but does not have a deprecation attribute" on the deprecated sax_parse(span_input_adapter&&, ...) overload, even though JSON_HEDLEY_DEPRECATED_FOR does expand to __attribute__((deprecated(...))) for Clang. Several isolated reproductions of this exact declaration shape - doc comment, template<>, two stacked __attribute__ macros, an overload set sharing the name - did not reproduce the warning, so this looks like a Clang comment/declaration-association quirk specific to this overload inside the much larger basic_json class template, not an actual documentation defect. Rather than keep the pragma library-wide for one Clang false positive, added a tightly scoped -Wdocumentation-deprecated-sync push/pop around just that overload. Verified with Apple clang 21 and the project's actual -Weverything flag set (cmake/clang_flags.cmake) on the full header at -std=c++11 and -std=c++20: zero -Wdocumentation* diagnostics. Also compiled clean with GCC 16.2.0 (the pragmas are already __clang__-gated, so this only confirms no unrelated breakage). Ran make check-amalgamation and the full local test suite: 129/129 passing. #5725 item 2 (step c) Signed-off-by: Niels Lohmann <mail@nlohmann.me> * Take the JSON value by const reference in the array and tuple from_json paths Review feedback on #5737 (gregmarr): once the no-op std::forward calls are gone, the forwarding references have no purpose. from_json_fn passes the value as const BasicJsonType&, so these functions were only ever instantiated with a const lvalue anyway. The std::array, std::pair and std::tuple overloads of from_json and their helpers now take const BasicJsonType& and pass j on unchanged. Because the deduced BasicJsonType is now the plain type, tuple_type and the static_assert name const BasicJsonType& explicitly, so the reference checks are unchanged: get<std::tuple<const std::string&>>() still works, and get<std::tuple<std::string&>>() still fails the same static_assert. from_json_tuple_get_impl keeps its forwarding reference, since tuple_type calls it through std::declval. Behavior, the public API and the ABI do not change. unit-conversions, unit-constructor1, unit-udt, unit-udt_macro, unit-regression1/2/3, unit-deserialization, unit-noexcept, unit-items, unit-allocator, unit-custom-object-type, unit-ordered_json2 and unit-brace-init-copy-semantics pass at C++11, C++17 and C++20 with unchanged assertion counts. Ran make amalgamate. Signed-off-by: Niels Lohmann <mail@nlohmann.me> --------- Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2054 lines
80 KiB
C++
2054 lines
80 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 <string>
|
|
#include <vector>
|
|
#include "make_test_data_available.hpp"
|
|
|
|
namespace
|
|
{
|
|
// alternating objects and arrays nested `depth` levels deep, with members that
|
|
// depend on `variant` at some levels, so diffing two variants yields
|
|
// operations on many levels: replacing the innermost value, adding, removing,
|
|
// and (for ordered_json) reordering members, and changing array lengths
|
|
template<typename BasicJsonType>
|
|
BasicJsonType nested(const std::size_t depth, const int variant)
|
|
{
|
|
BasicJsonType value = variant;
|
|
for (std::size_t i = 0; i < depth; ++i)
|
|
{
|
|
if (i % 2 == 0)
|
|
{
|
|
BasicJsonType object = BasicJsonType::object();
|
|
if ((i + static_cast<std::size_t>(variant)) % 7 == 0)
|
|
{
|
|
object["x"] = i;
|
|
}
|
|
if (variant == 2 && i % 11 == 0)
|
|
{
|
|
object["z"] = "z";
|
|
}
|
|
object["a"] = std::move(value);
|
|
if (variant == 1 && i % 5 == 0)
|
|
{
|
|
object["y"] = 1;
|
|
}
|
|
value = std::move(object);
|
|
}
|
|
else
|
|
{
|
|
BasicJsonType array = BasicJsonType::array({std::move(value)});
|
|
if ((i + static_cast<std::size_t>(variant)) % 3 == 0)
|
|
{
|
|
array.push_back(i);
|
|
}
|
|
value = std::move(array);
|
|
}
|
|
}
|
|
return value;
|
|
}
|
|
|
|
// a path of `depth` reference tokens, as nested() nests its values
|
|
std::string nested_path(const std::size_t depth)
|
|
{
|
|
std::string path;
|
|
for (std::size_t i = depth; i > 0; --i)
|
|
{
|
|
path += (i - 1) % 2 == 0 ? "/a" : "/0";
|
|
}
|
|
return path;
|
|
}
|
|
} // namespace
|
|
|
|
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);
|
|
}
|
|
}
|
|
|
|
TEST_CASE("JSON patch: diff of deeply nested values")
|
|
{
|
|
SECTION("the diff reproduces the target at every depth")
|
|
{
|
|
// depths on either side of the nesting depth up to which diff()
|
|
// recurses (detail::recursion_depth_limit(), 128); not every depth up
|
|
// to 300, as the test would then time out under Valgrind
|
|
std::vector<std::size_t> depths;
|
|
for (std::size_t depth = 0; depth <= 16; ++depth)
|
|
{
|
|
depths.push_back(depth);
|
|
}
|
|
for (std::size_t depth = 120; depth <= 136; ++depth)
|
|
{
|
|
depths.push_back(depth);
|
|
}
|
|
depths.push_back(300);
|
|
|
|
for (const auto depth : depths)
|
|
{
|
|
CAPTURE(depth)
|
|
for (int from = 0; from < 3; ++from)
|
|
{
|
|
for (int to = 0; to < 3; ++to)
|
|
{
|
|
CAPTURE(from)
|
|
CAPTURE(to)
|
|
const auto source = nested<json>(depth, from);
|
|
const auto target = nested<json>(depth, to);
|
|
const auto patch = json::diff(source, target);
|
|
CHECK(source.patch(patch) == target);
|
|
CHECK(patch.empty() == (from == to));
|
|
|
|
const auto ordered_source = nested<nlohmann::ordered_json>(depth, from);
|
|
const auto ordered_target = nested<nlohmann::ordered_json>(depth, to);
|
|
CHECK(ordered_source.patch(nlohmann::ordered_json::diff(ordered_source, ordered_target)) == ordered_target);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
SECTION("a difference only in the innermost value is one replace operation")
|
|
{
|
|
for (std::size_t depth = 0; depth <= 300; ++depth)
|
|
{
|
|
CAPTURE(depth)
|
|
json source = 1;
|
|
json target = 2;
|
|
for (std::size_t i = 0; i < depth; ++i)
|
|
{
|
|
source = i % 2 == 0 ? json::object({{"a", std::move(source)}}) : json::array({std::move(source)});
|
|
target = i % 2 == 0 ? json::object({{"a", std::move(target)}}) : json::array({std::move(target)});
|
|
}
|
|
CHECK(json::diff(source, target, "/root") == json::array({{{"op", "replace"}, {"path", "/root" + nested_path(depth)}, {"value", 2}}}));
|
|
}
|
|
}
|
|
|
|
SECTION("values nested too deeply for the call stack (#5393)")
|
|
{
|
|
// diff() used to recurse once per nesting level, and compared the
|
|
// values with operator== on every level. The values are only
|
|
// parsed and diffed, never copied or compared, since those recurse
|
|
// too.
|
|
const std::size_t depth = 100000;
|
|
for (const bool objects :
|
|
{
|
|
false, true
|
|
})
|
|
{
|
|
CAPTURE(objects)
|
|
std::string source_text;
|
|
std::string target_text;
|
|
std::string equal_text;
|
|
std::string path;
|
|
for (std::size_t i = 0; i < depth; ++i)
|
|
{
|
|
source_text += objects ? "{\"a\":" : "[";
|
|
path += objects ? "/a" : "/0";
|
|
}
|
|
target_text = source_text + "2";
|
|
equal_text = source_text + "1";
|
|
source_text += "1";
|
|
const std::string closing(depth, objects ? '}' : ']');
|
|
const auto source = json::parse(source_text + closing);
|
|
|
|
const auto patch = json::diff(source, json::parse(target_text + closing));
|
|
REQUIRE(patch.size() == 1);
|
|
CHECK(patch[0]["op"] == "replace");
|
|
CHECK(patch[0]["path"] == path);
|
|
CHECK(patch[0]["value"] == 2);
|
|
|
|
CHECK(json::diff(source, json::parse(equal_text + closing)).empty());
|
|
}
|
|
}
|
|
}
|
|
|
|
TEST_CASE("JSON patch - diff() takes the fast path for non-reorderable object types (regression #5639)")
|
|
{
|
|
// #5465 added an order check to diff()'s object handling so a
|
|
// member-by-member diff is only used when it would also reproduce
|
|
// target's member *order* -- needed for ordered_json, whose object_t
|
|
// keeps insertion order and whose patch() "add" op appends a new
|
|
// member at the end. For json's default object_t (std::map, which
|
|
// orders members by key regardless of insertion history), that check
|
|
// could still fail: a new key that sorts before an existing common key
|
|
// makes target's iteration interleave the new key between common keys,
|
|
// even though nothing else about the object changed. That sent the
|
|
// whole object through the slow (remove-every-member,
|
|
// re-add-every-member) path instead of the minimal one.
|
|
SECTION("json: added key sorts before an existing common key")
|
|
{
|
|
const json source = {{"a", 1}, {"c", {{"x", 1}, {"y", 2}}}};
|
|
const json target = {{"a", 1}, {"b", 0}, {"c", {{"x", 1}, {"y", 2}}}};
|
|
|
|
const json patch = json::diff(source, target);
|
|
|
|
// only the new key is added; "a" and "c" are left alone instead of
|
|
// being removed and re-added
|
|
const json expected = R"([{"op": "add", "path": "/b", "value": 0}])"_json;
|
|
CHECK(patch == expected);
|
|
CHECK(source.patch(patch) == target);
|
|
}
|
|
|
|
SECTION("ordered_json: reordering behavior from #5465 is unchanged")
|
|
{
|
|
using nlohmann::ordered_json;
|
|
|
|
// same key/value shape as the json case above, but for ordered_json
|
|
// the *target*'s member order must be reproduced, so the slow path
|
|
// is still required here.
|
|
ordered_json source;
|
|
source["a"] = 1;
|
|
source["c"] = ordered_json{{"x", 1}, {"y", 2}};
|
|
|
|
ordered_json target;
|
|
target["a"] = 1;
|
|
target["b"] = 0;
|
|
target["c"] = ordered_json{{"x", 1}, {"y", 2}};
|
|
|
|
const ordered_json patch = ordered_json::diff(source, target);
|
|
|
|
// unlike the json case: every member is still removed and re-added
|
|
// so the result ends up in target's order (2 removes + 3 adds)
|
|
CHECK(patch.size() == 5);
|
|
CHECK(source.patch(patch) == target);
|
|
}
|
|
}
|
|
|
|
TEST_CASE("JSON patch - every operation on ordered_json")
|
|
{
|
|
using nlohmann::ordered_json;
|
|
|
|
const ordered_json doc = {{"foo", "bar"}, {"arr", {1, 2, 3}}, {"obj", {{"a", 1}}}};
|
|
|
|
SECTION("successful operations")
|
|
{
|
|
const ordered_json patch = ordered_json::parse(R"([
|
|
{"op": "add", "path": "/obj/b", "value": 2},
|
|
{"op": "add", "path": "/arr/1", "value": 9},
|
|
{"op": "add", "path": "/arr/-", "value": 4},
|
|
{"op": "remove", "path": "/arr/0"},
|
|
{"op": "remove", "path": "/obj/a"},
|
|
{"op": "replace", "path": "/foo", "value": "baz"},
|
|
{"op": "move", "from": "/foo", "path": "/moved"},
|
|
{"op": "copy", "from": "/obj", "path": "/copied"},
|
|
{"op": "test", "path": "/copied/b", "value": 2}
|
|
])");
|
|
|
|
const ordered_json expected = ordered_json::parse(R"({
|
|
"arr": [9, 2, 3, 4], "obj": {"b": 2}, "moved": "baz", "copied": {"b": 2}
|
|
})");
|
|
|
|
CHECK(doc.patch(patch) == expected);
|
|
|
|
// adding to the root replaces the document
|
|
CHECK(doc.patch(ordered_json::parse(R"([{"op": "add", "path": "", "value": [1]}])")) == ordered_json({1}));
|
|
}
|
|
|
|
SECTION("failing operations")
|
|
{
|
|
ordered_json _;
|
|
#if JSON_DIAGNOSTICS
|
|
CHECK_THROWS_WITH_AS(_ = doc.patch(ordered_json::parse(R"([{"op": "add", "path": "/arr/4", "value": 1}])")),
|
|
"[json.exception.out_of_range.401] (/arr) array index 4 is out of range", ordered_json::out_of_range&);
|
|
#else
|
|
CHECK_THROWS_WITH_AS(_ = doc.patch(ordered_json::parse(R"([{"op": "add", "path": "/arr/4", "value": 1}])")),
|
|
"[json.exception.out_of_range.401] array index 4 is out of range", ordered_json::out_of_range&);
|
|
#endif
|
|
CHECK_THROWS_WITH_AS(_ = doc.patch(ordered_json::parse(R"([{"op": "add", "path": "/nope/x", "value": 1}])")),
|
|
"[json.exception.out_of_range.403] key 'nope' not found", ordered_json::out_of_range&);
|
|
CHECK_THROWS_WITH_AS(_ = doc.patch(ordered_json::parse(R"([{"op": "remove", "path": "/obj/nope"}])")),
|
|
"[json.exception.out_of_range.403] key 'nope' not found", ordered_json::out_of_range&);
|
|
#if JSON_DIAGNOSTICS
|
|
CHECK_THROWS_WITH_AS(_ = doc.patch(ordered_json::parse(R"([{"op": "remove", "path": "/arr/3"}])")),
|
|
"[json.exception.out_of_range.401] (/arr) array index 3 is out of range", ordered_json::out_of_range&);
|
|
#else
|
|
CHECK_THROWS_WITH_AS(_ = doc.patch(ordered_json::parse(R"([{"op": "remove", "path": "/arr/3"}])")),
|
|
"[json.exception.out_of_range.401] array index 3 is out of range", ordered_json::out_of_range&);
|
|
#endif
|
|
#if JSON_DIAGNOSTICS
|
|
CHECK_THROWS_WITH_AS(_ = doc.patch(ordered_json::parse(R"([{"op": "test", "path": "/foo", "value": "qux"}])")),
|
|
"[json.exception.other_error.501] (/0) unsuccessful: {\"op\":\"test\",\"path\":\"/foo\",\"value\":\"qux\"}", ordered_json::other_error&);
|
|
#elif JSON_DIAGNOSTIC_POSITIONS
|
|
CHECK_THROWS_WITH_AS(_ = doc.patch(ordered_json::parse(R"([{"op": "test", "path": "/foo", "value": "qux"}])")),
|
|
"[json.exception.other_error.501] (bytes 1-47) unsuccessful: {\"op\":\"test\",\"path\":\"/foo\",\"value\":\"qux\"}", ordered_json::other_error&);
|
|
#else
|
|
CHECK_THROWS_WITH_AS(_ = doc.patch(ordered_json::parse(R"([{"op": "test", "path": "/foo", "value": "qux"}])")),
|
|
"[json.exception.other_error.501] unsuccessful: {\"op\":\"test\",\"path\":\"/foo\",\"value\":\"qux\"}", ordered_json::other_error&);
|
|
#endif
|
|
#if JSON_DIAGNOSTICS
|
|
CHECK_THROWS_WITH_AS(_ = doc.patch(ordered_json::parse(R"([{"op": "add", "path": "/foo"}])")),
|
|
"[json.exception.parse_error.105] parse error: (/0) operation 'add' must have member 'value'", ordered_json::parse_error&);
|
|
#elif JSON_DIAGNOSTIC_POSITIONS
|
|
CHECK_THROWS_WITH_AS(_ = doc.patch(ordered_json::parse(R"([{"op": "add", "path": "/foo"}])")),
|
|
"[json.exception.parse_error.105] parse error: (bytes 1-30) operation 'add' must have member 'value'", ordered_json::parse_error&);
|
|
#else
|
|
CHECK_THROWS_WITH_AS(_ = doc.patch(ordered_json::parse(R"([{"op": "add", "path": "/foo"}])")),
|
|
"[json.exception.parse_error.105] parse error: operation 'add' must have member 'value'", ordered_json::parse_error&);
|
|
#endif
|
|
CHECK_THROWS_WITH_AS(_ = doc.patch(ordered_json::parse(R"([{"op": "move", "from": "/obj", "path": "/obj/a/b"}])")),
|
|
"[json.exception.out_of_range.414] cannot move value: 'from' path '/obj' is a proper prefix of 'path' '/obj/a/b'", ordered_json::out_of_range&);
|
|
}
|
|
|
|
SECTION("diff reproduces the target")
|
|
{
|
|
const ordered_json source = {{"a", 1}, {"b", 2}, {"c", {{"x", 1}}}, {"l", {1, 2, 3}}};
|
|
const std::vector<ordered_json> targets =
|
|
{
|
|
// a key removed, a key added, a nested change, a shorter array
|
|
{{"a", 1}, {"c", {{"x", 2}}}, {"l", {1}}, {"d", 4}},
|
|
// the same keys in another order
|
|
{{"c", {{"x", 1}}}, {"a", 1}, {"b", 2}, {"l", {1, 2, 3}}},
|
|
// new keys ahead of the common ones
|
|
{{"new", true}, {"a", 1}, {"b", 3}, {"c", {{"x", 1}}}, {"l", {1, 2, 3}}},
|
|
};
|
|
for (const auto& target : targets)
|
|
{
|
|
CAPTURE(target.dump())
|
|
CHECK(source.patch(ordered_json::diff(source, target)) == target);
|
|
}
|
|
}
|
|
}
|