json/test/src/unit-json_patch.cpp
2018-10-30 21:30:20 +01:00

1295 lines
44 KiB
C++

/*
__ _____ _____ _____
__| | __| | | | JSON for Modern C++ (test suite)
| | |__ | | | | | | version 3.4.0
|_____|_____|_____|_|___| https://github.com/nlohmann/json
Licensed under the MIT License <http://opensource.org/licenses/MIT>.
SPDX-License-Identifier: MIT
Copyright (c) 2013-2018 Niels Lohmann <http://nlohmann.me>.
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 <nlohmann/json.hpp>
using nlohmann::json;
#include <fstream>
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));
}
}
}
}
}