/* __ _____ _____ _____ __| | __| | | | JSON for Modern C++ (test suite) | | |__ | | | | | | version 3.4.0 |_____|_____|_____|_|___| https://github.com/nlohmann/json Licensed under the MIT License . SPDX-License-Identifier: MIT Copyright (c) 2013-2018 Niels Lohmann . Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ #include "catch.hpp" #include using nlohmann::json; #include 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 patch = R"([{ "op": "add", "path": "/a/b/c", "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(patch)); auto doc1_ans = R"( { "a": { "foo": 1, "b": { "c": [ "foo", "bar" ] } } } )"_json; CHECK(doc1.patch(patch) == doc1_ans); // It is an error in this document: json doc2 = R"({ "q": { "bar": 2 } })"_json; // because "a" does not exist. CHECK_THROWS_AS(doc2.patch(patch), json::out_of_range&); CHECK_THROWS_WITH(doc2.patch(patch), "[json.exception.out_of_range.403] key 'a' not found"); } 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&); CHECK_THROWS_WITH(doc.patch(patch), "[json.exception.other_error.501] unsuccessful: " + patch[0].dump()); } 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_AS(doc.patch(patch), json::out_of_range&); CHECK_THROWS_WITH(doc.patch(patch), "[json.exception.out_of_range.403] key 'baz' not found"); } // 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&); CHECK_THROWS_WITH(doc.patch(patch), "[json.exception.other_error.501] unsuccessful: " + patch[0].dump()); } 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); // 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_AS(j.patch(patch), json::parse_error&); CHECK_THROWS_WITH(j.patch(patch), "[json.exception.parse_error.104] parse error: JSON patch must be an array of objects"); } SECTION("not an array of objects") { json j; json patch = {"op", "add", "path", "", "value", 1}; CHECK_THROWS_AS(j.patch(patch), json::parse_error&); CHECK_THROWS_WITH(j.patch(patch), "[json.exception.parse_error.104] parse error: JSON patch must be an array of objects"); } SECTION("missing 'op'") { json j; json patch = {{{"foo", "bar"}}}; CHECK_THROWS_AS(j.patch(patch), json::parse_error&); CHECK_THROWS_WITH(j.patch(patch), "[json.exception.parse_error.105] parse error: operation must have member 'op'"); } SECTION("non-string 'op'") { json j; json patch = {{{"op", 1}}}; CHECK_THROWS_AS(j.patch(patch), json::parse_error&); CHECK_THROWS_WITH(j.patch(patch), "[json.exception.parse_error.105] parse error: operation must have string member 'op'"); } SECTION("invalid operation") { json j; json patch = {{{"op", "foo"}, {"path", ""}}}; CHECK_THROWS_AS(j.patch(patch), json::parse_error&); CHECK_THROWS_WITH(j.patch(patch), "[json.exception.parse_error.105] parse error: operation value 'foo' is invalid"); } } SECTION("add") { SECTION("missing 'path'") { json j; json patch = {{{"op", "add"}}}; CHECK_THROWS_AS(j.patch(patch), json::parse_error&); CHECK_THROWS_WITH(j.patch(patch), "[json.exception.parse_error.105] parse error: operation 'add' must have member 'path'"); } SECTION("non-string 'path'") { json j; json patch = {{{"op", "add"}, {"path", 1}}}; CHECK_THROWS_AS(j.patch(patch), json::parse_error&); CHECK_THROWS_WITH(j.patch(patch), "[json.exception.parse_error.105] parse error: operation 'add' must have string member 'path'"); } SECTION("missing 'value'") { json j; json patch = {{{"op", "add"}, {"path", ""}}}; CHECK_THROWS_AS(j.patch(patch), json::parse_error&); CHECK_THROWS_WITH(j.patch(patch), "[json.exception.parse_error.105] parse error: operation 'add' must have member 'value'"); } SECTION("invalid array index") { json j = {1, 2}; json patch = {{{"op", "add"}, {"path", "/4"}, {"value", 4}}}; CHECK_THROWS_AS(j.patch(patch), json::out_of_range&); CHECK_THROWS_WITH(j.patch(patch), "[json.exception.out_of_range.401] array index 4 is out of range"); } } SECTION("remove") { SECTION("missing 'path'") { json j; json patch = {{{"op", "remove"}}}; CHECK_THROWS_AS(j.patch(patch), json::parse_error&); CHECK_THROWS_WITH(j.patch(patch), "[json.exception.parse_error.105] parse error: operation 'remove' must have member 'path'"); } SECTION("non-string 'path'") { json j; json patch = {{{"op", "remove"}, {"path", 1}}}; CHECK_THROWS_AS(j.patch(patch), json::parse_error&); CHECK_THROWS_WITH(j.patch(patch), "[json.exception.parse_error.105] parse error: operation 'remove' must have string member 'path'"); } SECTION("nonexisting target location (array)") { json j = {1, 2, 3}; json patch = {{{"op", "remove"}, {"path", "/17"}}}; CHECK_THROWS_AS(j.patch(patch), json::out_of_range&); CHECK_THROWS_WITH(j.patch(patch), "[json.exception.out_of_range.401] array index 17 is out of range"); } SECTION("nonexisting target location (object)") { json j = {{"foo", 1}, {"bar", 2}}; json patch = {{{"op", "remove"}, {"path", "/baz"}}}; CHECK_THROWS_AS(j.patch(patch), json::out_of_range&); CHECK_THROWS_WITH(j.patch(patch), "[json.exception.out_of_range.403] key 'baz' not found"); } SECTION("root element as target location") { json j = "string"; json patch = {{{"op", "remove"}, {"path", ""}}}; CHECK_THROWS_AS(j.patch(patch), json::out_of_range&); CHECK_THROWS_WITH(j.patch(patch), "[json.exception.out_of_range.405] JSON pointer has no parent"); } } SECTION("replace") { SECTION("missing 'path'") { json j; json patch = {{{"op", "replace"}}}; CHECK_THROWS_AS(j.patch(patch), json::parse_error&); CHECK_THROWS_WITH(j.patch(patch), "[json.exception.parse_error.105] parse error: operation 'replace' must have member 'path'"); } SECTION("non-string 'path'") { json j; json patch = {{{"op", "replace"}, {"path", 1}}}; CHECK_THROWS_AS(j.patch(patch), json::parse_error&); CHECK_THROWS_WITH(j.patch(patch), "[json.exception.parse_error.105] parse error: operation 'replace' must have string member 'path'"); } SECTION("missing 'value'") { json j; json patch = {{{"op", "replace"}, {"path", ""}}}; CHECK_THROWS_AS(j.patch(patch), json::parse_error&); CHECK_THROWS_WITH(j.patch(patch), "[json.exception.parse_error.105] parse error: operation 'replace' must have member 'value'"); } SECTION("nonexisting target location (array)") { json j = {1, 2, 3}; json patch = {{{"op", "replace"}, {"path", "/17"}, {"value", 19}}}; CHECK_THROWS_AS(j.patch(patch), json::out_of_range&); CHECK_THROWS_WITH(j.patch(patch), "[json.exception.out_of_range.401] array index 17 is out of range"); } SECTION("nonexisting target location (object)") { json j = {{"foo", 1}, {"bar", 2}}; json patch = {{{"op", "replace"}, {"path", "/baz"}, {"value", 3}}}; CHECK_THROWS_AS(j.patch(patch), json::out_of_range&); CHECK_THROWS_WITH(j.patch(patch), "[json.exception.out_of_range.403] key 'baz' not found"); } } SECTION("move") { SECTION("missing 'path'") { json j; json patch = {{{"op", "move"}}}; CHECK_THROWS_AS(j.patch(patch), json::parse_error&); CHECK_THROWS_WITH(j.patch(patch), "[json.exception.parse_error.105] parse error: operation 'move' must have member 'path'"); } SECTION("non-string 'path'") { json j; json patch = {{{"op", "move"}, {"path", 1}}}; CHECK_THROWS_AS(j.patch(patch), json::parse_error&); CHECK_THROWS_WITH(j.patch(patch), "[json.exception.parse_error.105] parse error: operation 'move' must have string member 'path'"); } SECTION("missing 'from'") { json j; json patch = {{{"op", "move"}, {"path", ""}}}; CHECK_THROWS_AS(j.patch(patch), json::parse_error&); CHECK_THROWS_WITH(j.patch(patch), "[json.exception.parse_error.105] parse error: operation 'move' must have member 'from'"); } SECTION("non-string 'from'") { json j; json patch = {{{"op", "move"}, {"path", ""}, {"from", 1}}}; CHECK_THROWS_AS(j.patch(patch), json::parse_error&); CHECK_THROWS_WITH(j.patch(patch), "[json.exception.parse_error.105] parse error: operation 'move' must have string member 'from'"); } SECTION("nonexisting from location (array)") { json j = {1, 2, 3}; json patch = {{{"op", "move"}, {"path", "/0"}, {"from", "/5"}}}; CHECK_THROWS_AS(j.patch(patch), json::out_of_range&); CHECK_THROWS_WITH(j.patch(patch), "[json.exception.out_of_range.401] array index 5 is out of range"); } SECTION("nonexisting from location (object)") { json j = {{"foo", 1}, {"bar", 2}}; json patch = {{{"op", "move"}, {"path", "/baz"}, {"from", "/baz"}}}; CHECK_THROWS_AS(j.patch(patch), json::out_of_range&); CHECK_THROWS_WITH(j.patch(patch), "[json.exception.out_of_range.403] key 'baz' not found"); } } SECTION("copy") { SECTION("missing 'path'") { json j; json patch = {{{"op", "copy"}}}; CHECK_THROWS_AS(j.patch(patch), json::parse_error&); CHECK_THROWS_WITH(j.patch(patch), "[json.exception.parse_error.105] parse error: operation 'copy' must have member 'path'"); } SECTION("non-string 'path'") { json j; json patch = {{{"op", "copy"}, {"path", 1}}}; CHECK_THROWS_AS(j.patch(patch), json::parse_error&); CHECK_THROWS_WITH(j.patch(patch), "[json.exception.parse_error.105] parse error: operation 'copy' must have string member 'path'"); } SECTION("missing 'from'") { json j; json patch = {{{"op", "copy"}, {"path", ""}}}; CHECK_THROWS_AS(j.patch(patch), json::parse_error&); CHECK_THROWS_WITH(j.patch(patch), "[json.exception.parse_error.105] parse error: operation 'copy' must have member 'from'"); } SECTION("non-string 'from'") { json j; json patch = {{{"op", "copy"}, {"path", ""}, {"from", 1}}}; CHECK_THROWS_AS(j.patch(patch), json::parse_error&); CHECK_THROWS_WITH(j.patch(patch), "[json.exception.parse_error.105] parse error: operation 'copy' must have string member 'from'"); } SECTION("nonexisting from location (array)") { json j = {1, 2, 3}; json patch = {{{"op", "copy"}, {"path", "/0"}, {"from", "/5"}}}; CHECK_THROWS_AS(j.patch(patch), json::out_of_range&); CHECK_THROWS_WITH(j.patch(patch), "[json.exception.out_of_range.401] array index 5 is out of range"); } SECTION("nonexisting from location (object)") { json j = {{"foo", 1}, {"bar", 2}}; json patch = {{{"op", "copy"}, {"path", "/fob"}, {"from", "/baz"}}}; CHECK_THROWS_AS(j.patch(patch), json::out_of_range&); CHECK_THROWS_WITH(j.patch(patch), "[json.exception.out_of_range.403] key 'baz' not found"); } } SECTION("test") { SECTION("missing 'path'") { json j; json patch = {{{"op", "test"}}}; CHECK_THROWS_AS(j.patch(patch), json::parse_error&); CHECK_THROWS_WITH(j.patch(patch), "[json.exception.parse_error.105] parse error: operation 'test' must have member 'path'"); } SECTION("non-string 'path'") { json j; json patch = {{{"op", "test"}, {"path", 1}}}; CHECK_THROWS_AS(j.patch(patch), json::parse_error&); CHECK_THROWS_WITH(j.patch(patch), "[json.exception.parse_error.105] parse error: operation 'test' must have string member 'path'"); } SECTION("missing 'value'") { json j; json patch = {{{"op", "test"}, {"path", ""}}}; CHECK_THROWS_AS(j.patch(patch), json::parse_error&); CHECK_THROWS_WITH(j.patch(patch), "[json.exception.parse_error.105] parse error: operation 'test' must have member 'value'"); } } } 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&); CHECK_THROWS_WITH(doc.patch(patch), "[json.exception.other_error.501] unsuccessful: " + patch[0].dump()); } } } 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 (auto filename : {"test/data/json-patch-tests/spec_tests.json", "test/data/json-patch-tests/tests.json" }) { CAPTURE(filename); std::ifstream f(filename); json suite = json::parse(f); for (const auto& test : suite) { CAPTURE(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)); } } } } }