| // __ _____ _____ _____ |
| // __| | __| | | | JSON for Modern C++ (supporting code) |
| // | | |__ | | | | | | version 3.11.2 |
| // |_____|_____|_____|_|___| https://github.com/nlohmann/json |
| // |
| // SPDX-FileCopyrightText: 2013-2022 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: |
| json op1 = R"({ "op": "add", "path": "/a/b/c", "value": "foo" })"_json; |
| json op2 = R"({ "path": "/a/b/c", "op": "add", "value": "foo" })"_json; |
| 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 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 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 doc2 = R"({ "q": { "bar": 2 } })"_json; |
| |
| // because "a" does not exist. |
| CHECK_THROWS_WITH_AS(doc2.patch(patch1), "[json.exception.out_of_range.403] key 'a' not found", json::out_of_range&); |
| |
| json doc3 = R"({ "a": {} })"_json; |
| json 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&); |
| #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 doc = {1, 2, 3, 4}; |
| json 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 doc = R"( |
| { "foo": "bar"} |
| )"_json; |
| |
| // A JSON Patch document: |
| json 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 doc = R"( |
| { "foo": [ "bar", "baz" ] } |
| )"_json; |
| |
| // A JSON Patch document: |
| json 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 doc = R"( |
| { |
| "baz": "qux", |
| "foo": "bar" |
| } |
| )"_json; |
| |
| // A JSON Patch document: |
| json 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 doc = R"( |
| { "foo": [ "bar", "qux", "baz" ] } |
| )"_json; |
| |
| // A JSON Patch document: |
| json 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 doc = R"( |
| { |
| "baz": "qux", |
| "foo": "bar" |
| } |
| )"_json; |
| |
| // A JSON Patch document: |
| json 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 doc = R"( |
| { |
| "foo": { |
| "bar": "baz", |
| "waldo": "fred" |
| }, |
| "qux": { |
| "corge": "grault" |
| } |
| } |
| )"_json; |
| |
| // A JSON Patch document: |
| json 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 doc = R"( |
| { "foo": [ "all", "grass", "cows", "eat" ] } |
| )"_json; |
| |
| // A JSON Patch document: |
| json 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 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 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()); |
| #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 doc = R"( |
| { "foo": "bar" } |
| )"_json; |
| |
| // A JSON Patch document: |
| json 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 doc = R"( |
| { "foo": "bar" } |
| )"_json; |
| |
| // A JSON Patch document: |
| json 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 doc = R"( |
| { "foo": "bar" } |
| )"_json; |
| |
| // A JSON Patch document: |
| json 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. |
| |
| CHECK_THROWS_WITH_AS(doc.patch(patch), "[json.exception.out_of_range.403] key 'baz' not found", json::out_of_range&); |
| } |
| |
| // A.13. Invalid JSON Patch Document |
| // not applicable |
| |
| SECTION("A.14. Escape Ordering") |
| { |
| // An example target JSON document: |
| json doc = R"( |
| { |
| "/": 9, |
| "~1": 10 |
| } |
| )"_json; |
| |
| // A JSON Patch document: |
| json 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 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()); |
| #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 doc = R"( |
| { "foo": ["bar"] } |
| )"_json; |
| |
| // A JSON Patch document: |
| json 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 doc = 17; |
| |
| // A JSON Patch document: |
| json 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 doc = {0, 1, 2}; |
| |
| // A JSON Patch document: |
| json 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 doc = R"( |
| { |
| "foo": { |
| "bar": "baz", |
| "waldo": "fred" |
| }, |
| "qux": { |
| "corge": "grault" |
| } |
| } |
| )"_json; |
| |
| // A JSON Patch document: |
| json 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 j = "string"; |
| json patch = {{{"op", "replace"}, {"path", ""}, {"value", 1}}}; |
| CHECK(j.patch(patch) == json(1)); |
| } |
| |
| SECTION("documentation GIF") |
| { |
| { |
| // a JSON patch |
| json p1 = R"( |
| [{"op": "add", "path": "/GB", "value": "London"}] |
| )"_json; |
| |
| // a JSON value |
| json source = R"( |
| {"D": "Berlin", "F": "Paris"} |
| )"_json; |
| |
| // apply the patch |
| 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 |
| 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); |
| |
| json flat = j.flatten(); |
| CHECK(flat == R"({"/0":"good","/1":"bad","/2/en":"ugly","/2/it":"cattivo"})"_json); |
| } |
| } |
| } |
| |
| SECTION("errors") |
| { |
| SECTION("unknown operation") |
| { |
| SECTION("not an array") |
| { |
| json j; |
| json 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 j; |
| json 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 j; |
| json 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 j; |
| json 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 j; |
| json 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 j; |
| json 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 j; |
| json 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 j; |
| json 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 j = {1, 2}; |
| json 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 j; |
| json 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 j; |
| json 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 j = {1, 2, 3}; |
| json 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 j = {{"foo", 1}, {"bar", 2}}; |
| json 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 j = "string"; |
| json 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 j; |
| json 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 j; |
| json 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 j; |
| json 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 j = {1, 2, 3}; |
| json 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 j = {{"foo", 1}, {"bar", 2}}; |
| json 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 j; |
| json 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 j; |
| json 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 j; |
| json 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 j; |
| json 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 j = {1, 2, 3}; |
| json 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 j = {{"foo", 1}, {"bar", 2}}; |
| json 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 j; |
| json 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 j; |
| json 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 j; |
| json 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 j; |
| json 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 j = {1, 2, 3}; |
| json 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 j = {{"foo", 1}, {"bar", 2}}; |
| json 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 j; |
| json 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 j; |
| json 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 j; |
| json 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 doc = R"( |
| { |
| "baz": "qux", |
| "foo": "bar" |
| } |
| )"_json; |
| |
| // The patch |
| json 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 doc = R"( |
| { |
| "biscuits": [ |
| {"name":"Digestive"}, |
| {"name": "Choco Liebniz"} |
| ] |
| } |
| )"_json; |
| |
| SECTION("add") |
| { |
| // The patch |
| json 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 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 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 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 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()); |
| #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 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) |
| { |
| // 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)); |
| } |
| } |
| } |
| } |
| } |