From 70fc5835cb4758b73fdab800380a7e3246ff79f7 Mon Sep 17 00:00:00 2001 From: Niels Date: Mon, 18 Apr 2016 22:41:36 +0200 Subject: [PATCH 01/17] started implementing JSON Patch (RFC 6902) --- src/json.hpp | 95 +++++++++++++++++++ src/json.hpp.re2c | 95 +++++++++++++++++++ test/unit.cpp | 226 ++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 416 insertions(+) diff --git a/src/json.hpp b/src/json.hpp index 57a8f4c7..421b9995 100644 --- a/src/json.hpp +++ b/src/json.hpp @@ -9492,6 +9492,101 @@ basic_json_parser_63: } /// @} + + /*! + @brief applies a JSON patch + + @param[in] patch JSON patch document + @return patched document + + @note The original JSON value is not changed; that is, the patch is + applied to a copy of the value. + + @sa [RFC 6902](https://tools.ietf.org/html/rfc6902) + */ + basic_json apply_patch(const basic_json& patch) const + { + basic_json result = *this; + + if (not patch.is_array()) + { + // a JSON patch must be an array of objects + throw std::domain_error("JSON patch must be an array of objects"); + } + + for (const auto& val : patch) + { + if (not val.is_object()) + { + throw std::domain_error("JSON patch must be an array of objects"); + } + + // collect members + const auto it_op = val.m_value.object->find("op"); + const auto it_path = val.m_value.object->find("path"); + const auto it_value = val.m_value.object->find("value"); + + if (it_op == val.m_value.object->end() or not it_op->second.is_string()) + { + throw std::domain_error("operation must have a string 'op' member"); + } + + if (it_path == val.m_value.object->end() or not it_op->second.is_string()) + { + throw std::domain_error("operation must have a string 'path' member"); + } + + const std::string op = it_op->second; + const std::string path = it_path->second; + const json_pointer ptr(path); + + if (op == "add") + { + if (it_value == val.m_value.object->end()) + { + throw std::domain_error("'add' operation must have member 'value'"); + } + + result[ptr] = it_value->second; + } + else if (op == "remove") + { + } + else if (op == "replace") + { + if (it_value == val.m_value.object->end()) + { + throw std::domain_error("'replace' operation must have member 'value'"); + } + } + else if (op == "move") + { + } + else if (op == "copy") + { + } + else if (op == "test") + { + if (it_value == val.m_value.object->end()) + { + throw std::domain_error("'test' operation must have member 'value'"); + } + + if (result.at(ptr) != it_value->second) + { + throw std::domain_error("unsuccessful: " + val.dump()); + } + } + else + { + // op must be "add", "remove", "replace", "move", + // "copy", or "test" + throw std::domain_error("operation value '" + op + "' is invalid"); + } + } + + return result; + } }; diff --git a/src/json.hpp.re2c b/src/json.hpp.re2c index cdd96ee9..3f839737 100644 --- a/src/json.hpp.re2c +++ b/src/json.hpp.re2c @@ -8802,6 +8802,101 @@ class basic_json } /// @} + + /*! + @brief applies a JSON patch + + @param[in] patch JSON patch document + @return patched document + + @note The original JSON value is not changed; that is, the patch is + applied to a copy of the value. + + @sa [RFC 6902](https://tools.ietf.org/html/rfc6902) + */ + basic_json apply_patch(const basic_json& patch) const + { + basic_json result = *this; + + if (not patch.is_array()) + { + // a JSON patch must be an array of objects + throw std::domain_error("JSON patch must be an array of objects"); + } + + for (const auto& val : patch) + { + if (not val.is_object()) + { + throw std::domain_error("JSON patch must be an array of objects"); + } + + // collect members + const auto it_op = val.m_value.object->find("op"); + const auto it_path = val.m_value.object->find("path"); + const auto it_value = val.m_value.object->find("value"); + + if (it_op == val.m_value.object->end() or not it_op->second.is_string()) + { + throw std::domain_error("operation must have a string 'op' member"); + } + + if (it_path == val.m_value.object->end() or not it_op->second.is_string()) + { + throw std::domain_error("operation must have a string 'path' member"); + } + + const std::string op = it_op->second; + const std::string path = it_path->second; + const json_pointer ptr(path); + + if (op == "add") + { + if (it_value == val.m_value.object->end()) + { + throw std::domain_error("'add' operation must have member 'value'"); + } + + result[ptr] = it_value->second; + } + else if (op == "remove") + { + } + else if (op == "replace") + { + if (it_value == val.m_value.object->end()) + { + throw std::domain_error("'replace' operation must have member 'value'"); + } + } + else if (op == "move") + { + } + else if (op == "copy") + { + } + else if (op == "test") + { + if (it_value == val.m_value.object->end()) + { + throw std::domain_error("'test' operation must have member 'value'"); + } + + if (result.at(ptr) != it_value->second) + { + throw std::domain_error("unsuccessful: " + val.dump()); + } + } + else + { + // op must be "add", "remove", "replace", "move", + // "copy", or "test" + throw std::domain_error("operation value '" + op + "' is invalid"); + } + } + + return result; + } }; diff --git a/test/unit.cpp b/test/unit.cpp index 2666e111..7a91efd7 100644 --- a/test/unit.cpp +++ b/test/unit.cpp @@ -12391,6 +12391,232 @@ TEST_CASE("JSON pointers") } } +TEST_CASE("JSON patch") +{ + SECTION("examples from RFC 6902") + { + SECTION("example 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.apply_patch(patch) == expected); + } + + SECTION("example 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.apply_patch(patch)); + // check if patched document is unchanged + CHECK(doc.apply_patch(patch) == doc); + } + + SECTION("example 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.apply_patch(patch), std::domain_error); + CHECK_THROWS_WITH(doc.apply_patch(patch), "unsuccessful: " + patch[0].dump()); + } + + SECTION("example 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.apply_patch(patch) == expected); + } + + SECTION("example 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.apply_patch(patch) == expected); + } + + SECTION("example 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.apply_patch(patch), std::out_of_range); + CHECK_THROWS_WITH(doc.apply_patch(patch), "unresolved reference token 'bat'"); + } + + SECTION("example 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.apply_patch(patch) == expected); + } + + SECTION("example 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.apply_patch(patch), std::domain_error); + CHECK_THROWS_WITH(doc.apply_patch(patch), "unsuccessful: " + patch[0].dump()); + } + + SECTION("example 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.apply_patch(patch) == expected); + } + } +} + TEST_CASE("regression tests") { SECTION("issue #60 - Double quotation mark is not parsed correctly") From fa03cf0c63544a358c8b92db8fe85317a826174a Mon Sep 17 00:00:00 2001 From: Niels Date: Wed, 20 Apr 2016 15:41:33 +0200 Subject: [PATCH 02/17] replace and copy --- src/json.hpp | 19 +++++++++++++ src/json.hpp.re2c | 19 +++++++++++++ test/unit.cpp | 70 +++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 108 insertions(+) diff --git a/src/json.hpp b/src/json.hpp index 421b9995..acde03bf 100644 --- a/src/json.hpp +++ b/src/json.hpp @@ -9525,6 +9525,7 @@ basic_json_parser_63: const auto it_op = val.m_value.object->find("op"); const auto it_path = val.m_value.object->find("path"); const auto it_value = val.m_value.object->find("value"); + const auto it_from = val.m_value.object->find("from"); if (it_op == val.m_value.object->end() or not it_op->second.is_string()) { @@ -9558,12 +9559,30 @@ basic_json_parser_63: { throw std::domain_error("'replace' operation must have member 'value'"); } + + result.at(ptr) = it_value->second; } else if (op == "move") { + if (it_from == val.m_value.object->end()) + { + throw std::domain_error("'move' operation must have member 'from'"); + } + + const std::string from_path = it_from->second; + const json_pointer from_ptr(from_path); } else if (op == "copy") { + if (it_from == val.m_value.object->end()) + { + throw std::domain_error("'copy' operation must have member 'from'"); + } + + const std::string from_path = it_from->second; + const json_pointer from_ptr(from_path); + + result[ptr] = result.at(from_ptr); } else if (op == "test") { diff --git a/src/json.hpp.re2c b/src/json.hpp.re2c index 3f839737..d161f38d 100644 --- a/src/json.hpp.re2c +++ b/src/json.hpp.re2c @@ -8835,6 +8835,7 @@ class basic_json const auto it_op = val.m_value.object->find("op"); const auto it_path = val.m_value.object->find("path"); const auto it_value = val.m_value.object->find("value"); + const auto it_from = val.m_value.object->find("from"); if (it_op == val.m_value.object->end() or not it_op->second.is_string()) { @@ -8868,12 +8869,30 @@ class basic_json { throw std::domain_error("'replace' operation must have member 'value'"); } + + result.at(ptr) = it_value->second; } else if (op == "move") { + if (it_from == val.m_value.object->end()) + { + throw std::domain_error("'move' operation must have member 'from'"); + } + + const std::string from_path = it_from->second; + const json_pointer from_ptr(from_path); } else if (op == "copy") { + if (it_from == val.m_value.object->end()) + { + throw std::domain_error("'copy' operation must have member 'from'"); + } + + const std::string from_path = it_from->second; + const json_pointer from_ptr(from_path); + + result[ptr] = result.at(from_ptr); } else if (op == "test") { diff --git a/test/unit.cpp b/test/unit.cpp index 7a91efd7..7b675462 100644 --- a/test/unit.cpp +++ b/test/unit.cpp @@ -12421,6 +12421,34 @@ TEST_CASE("JSON patch") CHECK(doc.apply_patch(patch) == expected); } + SECTION("example 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.apply_patch(patch) == expected); + } + SECTION("example A.8 - Testing a Value: Success") { // An example target JSON document: @@ -12615,6 +12643,48 @@ TEST_CASE("JSON patch") CHECK(doc.apply_patch(patch) == expected); } } + + SECTION("own examples") + { + 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; + + json expected = R"( + { + "foo": { + "bar": "baz", + "waldo": "fred" + }, + "qux": { + "corge": "grault", + "thud": "fred" + } + } + )"_json; + + // check if patched value is as expected + CHECK(doc.apply_patch(patch) == expected); + } + } } TEST_CASE("regression tests") From 397ada22d35cf2ebc168a48f264218c069f13d74 Mon Sep 17 00:00:00 2001 From: Niels Date: Wed, 20 Apr 2016 16:52:00 +0200 Subject: [PATCH 03/17] implemented remove --- src/json.hpp | 26 +++++++++++++++++++++++-- src/json.hpp.re2c | 26 +++++++++++++++++++++++-- test/unit.cpp | 49 +++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 97 insertions(+), 4 deletions(-) diff --git a/src/json.hpp b/src/json.hpp index acde03bf..de6c6ec1 100644 --- a/src/json.hpp +++ b/src/json.hpp @@ -8969,6 +8969,18 @@ basic_json_parser_63: : reference_tokens(split(s)) {} + std::string pop_back() + { + if (reference_tokens.empty()) + { + throw std::domain_error("JSON pointer has no parent"); + } + + auto last = reference_tokens.back(); + reference_tokens.pop_back(); + return last; + } + private: /*! @brief create and return a reference to the pointed to value @@ -9420,7 +9432,7 @@ basic_json_parser_63: private: /// the reference tokens - const std::vector reference_tokens {}; + std::vector reference_tokens {}; }; //////////////////////////// @@ -9539,7 +9551,7 @@ basic_json_parser_63: const std::string op = it_op->second; const std::string path = it_path->second; - const json_pointer ptr(path); + json_pointer ptr(path); if (op == "add") { @@ -9552,6 +9564,16 @@ basic_json_parser_63: } else if (op == "remove") { + const auto last_path = ptr.pop_back(); + basic_json& parent = result.at(ptr); + if (parent.is_object()) + { + parent.erase(parent.find(last_path)); + } + else if (parent.is_array()) + { + parent.erase(parent.begin() + std::stoi(last_path)); + } } else if (op == "replace") { diff --git a/src/json.hpp.re2c b/src/json.hpp.re2c index d161f38d..073baf36 100644 --- a/src/json.hpp.re2c +++ b/src/json.hpp.re2c @@ -8279,6 +8279,18 @@ class basic_json : reference_tokens(split(s)) {} + std::string pop_back() + { + if (reference_tokens.empty()) + { + throw std::domain_error("JSON pointer has no parent"); + } + + auto last = reference_tokens.back(); + reference_tokens.pop_back(); + return last; + } + private: /*! @brief create and return a reference to the pointed to value @@ -8730,7 +8742,7 @@ class basic_json private: /// the reference tokens - const std::vector reference_tokens {}; + std::vector reference_tokens {}; }; //////////////////////////// @@ -8849,7 +8861,7 @@ class basic_json const std::string op = it_op->second; const std::string path = it_path->second; - const json_pointer ptr(path); + json_pointer ptr(path); if (op == "add") { @@ -8862,6 +8874,16 @@ class basic_json } else if (op == "remove") { + const auto last_path = ptr.pop_back(); + basic_json& parent = result.at(ptr); + if (parent.is_object()) + { + parent.erase(parent.find(last_path)); + } + else if (parent.is_array()) + { + parent.erase(parent.begin() + std::stoi(last_path)); + } } else if (op == "replace") { diff --git a/test/unit.cpp b/test/unit.cpp index 7b675462..6f9ad8f5 100644 --- a/test/unit.cpp +++ b/test/unit.cpp @@ -12421,6 +12421,55 @@ TEST_CASE("JSON patch") CHECK(doc.apply_patch(patch) == expected); } + SECTION("example 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.apply_patch(patch) == expected); + } + + SECTION("example 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.apply_patch(patch) == expected); + } + SECTION("example A.5 - Replacing a Value") { // An example target JSON document: From 855cf2307bb7c4ac874cb5eaac74f1c4abf01428 Mon Sep 17 00:00:00 2001 From: Niels Date: Sun, 24 Apr 2016 16:51:06 +0200 Subject: [PATCH 04/17] extended "add" to cope with arrays --- src/json.hpp | 20 +++++++++++++++++++- src/json.hpp.re2c | 20 +++++++++++++++++++- test/unit.cpp | 28 +++++++++++++++++++++++++++- 3 files changed, 65 insertions(+), 3 deletions(-) diff --git a/src/json.hpp b/src/json.hpp index de6c6ec1..c1bf6648 100644 --- a/src/json.hpp +++ b/src/json.hpp @@ -9560,7 +9560,25 @@ basic_json_parser_63: throw std::domain_error("'add' operation must have member 'value'"); } - result[ptr] = it_value->second; + const auto last_path = ptr.pop_back(); + basic_json& parent = result.at(ptr); + + if (parent.is_object()) + { + parent[last_path] = it_value->second; + } + else if (parent.is_array()) + { + if (last_path == "-") + { + parent.push_back(it_value->second); + } + else + { + parent.insert(parent.begin() + std::stoi(last_path), + it_value->second); + } + } } else if (op == "remove") { diff --git a/src/json.hpp.re2c b/src/json.hpp.re2c index 073baf36..aa02bbea 100644 --- a/src/json.hpp.re2c +++ b/src/json.hpp.re2c @@ -8870,7 +8870,25 @@ class basic_json throw std::domain_error("'add' operation must have member 'value'"); } - result[ptr] = it_value->second; + const auto last_path = ptr.pop_back(); + basic_json& parent = result.at(ptr); + + if (parent.is_object()) + { + parent[last_path] = it_value->second; + } + else if (parent.is_array()) + { + if (last_path == "-") + { + parent.push_back(it_value->second); + } + else + { + parent.insert(parent.begin() + std::stoi(last_path), + it_value->second); + } + } } else if (op == "remove") { diff --git a/test/unit.cpp b/test/unit.cpp index 6f9ad8f5..277daed1 100644 --- a/test/unit.cpp +++ b/test/unit.cpp @@ -12421,6 +12421,29 @@ TEST_CASE("JSON patch") CHECK(doc.apply_patch(patch) == expected); } + SECTION("example 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.apply_patch(patch) == expected); + } + SECTION("example A.3 - Removing an Object Member") { // An example target JSON document: @@ -12616,9 +12639,12 @@ TEST_CASE("JSON patch") // an existing object, nor a member of an existing array. CHECK_THROWS_AS(doc.apply_patch(patch), std::out_of_range); - CHECK_THROWS_WITH(doc.apply_patch(patch), "unresolved reference token 'bat'"); + CHECK_THROWS_WITH(doc.apply_patch(patch), "key 'baz' not found"); } + // A.13. Invalid JSON Patch Document + // not applicable + SECTION("example A.14 - Escape Ordering") { // An example target JSON document: From 09e9f6dcd4b61005e0b37f364cdadeda4d6ea9a1 Mon Sep 17 00:00:00 2001 From: Niels Date: Sun, 24 Apr 2016 17:43:27 +0200 Subject: [PATCH 05/17] implemented "move" --- src/json.hpp | 75 ++++++++++++++++++++++++++++------------------- src/json.hpp.re2c | 75 ++++++++++++++++++++++++++++------------------- test/unit.cpp | 62 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 152 insertions(+), 60 deletions(-) diff --git a/src/json.hpp b/src/json.hpp index c1bf6648..8d5291ce 100644 --- a/src/json.hpp +++ b/src/json.hpp @@ -9526,6 +9526,44 @@ basic_json_parser_63: throw std::domain_error("JSON patch must be an array of objects"); } + const auto operation_add = [&result](json_pointer & ptr, + basic_json & value) + { + const auto last_path = ptr.pop_back(); + basic_json& parent = result.at(ptr); + + if (parent.is_object()) + { + parent[last_path] = value; + } + else if (parent.is_array()) + { + if (last_path == "-") + { + parent.push_back(value); + } + else + { + parent.insert(parent.begin() + std::stoi(last_path), + value); + } + } + }; + + const auto operation_remove = [&result](json_pointer & ptr) + { + const auto last_path = ptr.pop_back(); + basic_json& parent = result.at(ptr); + if (parent.is_object()) + { + parent.erase(parent.find(last_path)); + } + else if (parent.is_array()) + { + parent.erase(parent.begin() + std::stoi(last_path)); + } + }; + for (const auto& val : patch) { if (not val.is_object()) @@ -9560,38 +9598,11 @@ basic_json_parser_63: throw std::domain_error("'add' operation must have member 'value'"); } - const auto last_path = ptr.pop_back(); - basic_json& parent = result.at(ptr); - - if (parent.is_object()) - { - parent[last_path] = it_value->second; - } - else if (parent.is_array()) - { - if (last_path == "-") - { - parent.push_back(it_value->second); - } - else - { - parent.insert(parent.begin() + std::stoi(last_path), - it_value->second); - } - } + operation_add(ptr, it_value->second); } else if (op == "remove") { - const auto last_path = ptr.pop_back(); - basic_json& parent = result.at(ptr); - if (parent.is_object()) - { - parent.erase(parent.find(last_path)); - } - else if (parent.is_array()) - { - parent.erase(parent.begin() + std::stoi(last_path)); - } + operation_remove(ptr); } else if (op == "replace") { @@ -9610,7 +9621,11 @@ basic_json_parser_63: } const std::string from_path = it_from->second; - const json_pointer from_ptr(from_path); + json_pointer from_ptr(from_path); + basic_json v = result[from_ptr]; + + operation_remove(from_ptr); + operation_add(ptr, v); } else if (op == "copy") { diff --git a/src/json.hpp.re2c b/src/json.hpp.re2c index aa02bbea..37feeec6 100644 --- a/src/json.hpp.re2c +++ b/src/json.hpp.re2c @@ -8836,6 +8836,44 @@ class basic_json throw std::domain_error("JSON patch must be an array of objects"); } + const auto operation_add = [&result](json_pointer & ptr, + basic_json & value) + { + const auto last_path = ptr.pop_back(); + basic_json& parent = result.at(ptr); + + if (parent.is_object()) + { + parent[last_path] = value; + } + else if (parent.is_array()) + { + if (last_path == "-") + { + parent.push_back(value); + } + else + { + parent.insert(parent.begin() + std::stoi(last_path), + value); + } + } + }; + + const auto operation_remove = [&result](json_pointer & ptr) + { + const auto last_path = ptr.pop_back(); + basic_json& parent = result.at(ptr); + if (parent.is_object()) + { + parent.erase(parent.find(last_path)); + } + else if (parent.is_array()) + { + parent.erase(parent.begin() + std::stoi(last_path)); + } + }; + for (const auto& val : patch) { if (not val.is_object()) @@ -8870,38 +8908,11 @@ class basic_json throw std::domain_error("'add' operation must have member 'value'"); } - const auto last_path = ptr.pop_back(); - basic_json& parent = result.at(ptr); - - if (parent.is_object()) - { - parent[last_path] = it_value->second; - } - else if (parent.is_array()) - { - if (last_path == "-") - { - parent.push_back(it_value->second); - } - else - { - parent.insert(parent.begin() + std::stoi(last_path), - it_value->second); - } - } + operation_add(ptr, it_value->second); } else if (op == "remove") { - const auto last_path = ptr.pop_back(); - basic_json& parent = result.at(ptr); - if (parent.is_object()) - { - parent.erase(parent.find(last_path)); - } - else if (parent.is_array()) - { - parent.erase(parent.begin() + std::stoi(last_path)); - } + operation_remove(ptr); } else if (op == "replace") { @@ -8920,7 +8931,11 @@ class basic_json } const std::string from_path = it_from->second; - const json_pointer from_ptr(from_path); + json_pointer from_ptr(from_path); + basic_json v = result[from_ptr]; + + operation_remove(from_ptr); + operation_add(ptr, v); } else if (op == "copy") { diff --git a/test/unit.cpp b/test/unit.cpp index 277daed1..907e68bf 100644 --- a/test/unit.cpp +++ b/test/unit.cpp @@ -12521,6 +12521,68 @@ TEST_CASE("JSON patch") CHECK(doc.apply_patch(patch) == expected); } + SECTION("example 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.apply_patch(patch) == expected); + } + + SECTION("example 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.apply_patch(patch) == expected); + } + SECTION("example A.8 - Testing a Value: Success") { // An example target JSON document: From fb54e212b666ecc37272740278384a8bc9fc2f5f Mon Sep 17 00:00:00 2001 From: Niels Date: Sun, 24 Apr 2016 19:03:33 +0200 Subject: [PATCH 06/17] clean up and added tests --- src/json.hpp | 120 ++++++++++++++--------------- src/json.hpp.re2c | 120 ++++++++++++++--------------- test/unit.cpp | 192 ++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 310 insertions(+), 122 deletions(-) diff --git a/src/json.hpp b/src/json.hpp index 8d5291ce..b7a6f64a 100644 --- a/src/json.hpp +++ b/src/json.hpp @@ -9518,42 +9518,44 @@ basic_json_parser_63: */ basic_json apply_patch(const basic_json& patch) const { + // make a working copy to apply the patch to basic_json result = *this; - if (not patch.is_array()) - { - // a JSON patch must be an array of objects - throw std::domain_error("JSON patch must be an array of objects"); - } - - const auto operation_add = [&result](json_pointer & ptr, - basic_json & value) + // wrapper for "add" operation; add value at ptr + const auto operation_add = [&result](json_pointer & ptr, basic_json value) { + // get reference to parent of JSON pointer ptr const auto last_path = ptr.pop_back(); basic_json& parent = result.at(ptr); if (parent.is_object()) { + // use operator[] to add value parent[last_path] = value; } else if (parent.is_array()) { if (last_path == "-") { + // special case: append to back parent.push_back(value); } else { - parent.insert(parent.begin() + std::stoi(last_path), - value); + // default case: insert add offset + parent.insert(parent.begin() + std::stoi(last_path), value); } } }; + // wrapper for "remove" operation; remove value at ptr const auto operation_remove = [&result](json_pointer & ptr) { + // get reference to parent of JSON pointer ptr const auto last_path = ptr.pop_back(); basic_json& parent = result.at(ptr); + + // remove child if (parent.is_object()) { parent.erase(parent.find(last_path)); @@ -9564,41 +9566,57 @@ basic_json_parser_63: } }; + // type check + if (not patch.is_array()) + { + // a JSON patch must be an array of objects + throw std::domain_error("JSON patch must be an array of objects"); + } + + // iterate and apply th eoperations for (const auto& val : patch) { + // wrapper to get a value for an operation + const auto get_value = [&val](const std::string & op, + const std::string & member, + bool string_type = false) -> basic_json& + { + // find value + auto it = val.m_value.object->find(member); + + // context-sensitive error message + const auto error_msg = (op == "op") ? "operation" : "operation '" + op + "'"; + + // check if desired value is present + if (it == val.m_value.object->end()) + { + throw std::domain_error(error_msg + " must have member '" + member + "'"); + } + + // check if result is of type string + if (string_type and not it->second.is_string()) + { + throw std::domain_error(error_msg + " must have string member '" + member + "'"); + } + + // no error: return value + return it->second; + }; + + // type check if (not val.is_object()) { throw std::domain_error("JSON patch must be an array of objects"); } - // collect members - const auto it_op = val.m_value.object->find("op"); - const auto it_path = val.m_value.object->find("path"); - const auto it_value = val.m_value.object->find("value"); - const auto it_from = val.m_value.object->find("from"); - - if (it_op == val.m_value.object->end() or not it_op->second.is_string()) - { - throw std::domain_error("operation must have a string 'op' member"); - } - - if (it_path == val.m_value.object->end() or not it_op->second.is_string()) - { - throw std::domain_error("operation must have a string 'path' member"); - } - - const std::string op = it_op->second; - const std::string path = it_path->second; - json_pointer ptr(path); + // collect mandatory members + const std::string op = get_value("op", "op", true); + const std::string path = get_value(op, "path", true); + json_pointer ptr(get_value(op, "path", true)); if (op == "add") { - if (it_value == val.m_value.object->end()) - { - throw std::domain_error("'add' operation must have member 'value'"); - } - - operation_add(ptr, it_value->second); + operation_add(ptr, get_value("add", "value")); } else if (op == "remove") { @@ -9606,21 +9624,11 @@ basic_json_parser_63: } else if (op == "replace") { - if (it_value == val.m_value.object->end()) - { - throw std::domain_error("'replace' operation must have member 'value'"); - } - - result.at(ptr) = it_value->second; + result.at(ptr) = get_value("replace", "value"); } else if (op == "move") { - if (it_from == val.m_value.object->end()) - { - throw std::domain_error("'move' operation must have member 'from'"); - } - - const std::string from_path = it_from->second; + const std::string from_path = get_value("move", "from", true); json_pointer from_ptr(from_path); basic_json v = result[from_ptr]; @@ -9629,32 +9637,22 @@ basic_json_parser_63: } else if (op == "copy") { - if (it_from == val.m_value.object->end()) - { - throw std::domain_error("'copy' operation must have member 'from'"); - } - - const std::string from_path = it_from->second; + const std::string from_path = get_value("copy", "from", true);; const json_pointer from_ptr(from_path); result[ptr] = result.at(from_ptr); } else if (op == "test") { - if (it_value == val.m_value.object->end()) - { - throw std::domain_error("'test' operation must have member 'value'"); - } - - if (result.at(ptr) != it_value->second) + if (result.at(ptr) != get_value("test", "value")) { throw std::domain_error("unsuccessful: " + val.dump()); } } else { - // op must be "add", "remove", "replace", "move", - // "copy", or "test" + // op must be "add", "remove", "replace", "move", "copy", or + // "test" throw std::domain_error("operation value '" + op + "' is invalid"); } } diff --git a/src/json.hpp.re2c b/src/json.hpp.re2c index 37feeec6..c4c87f5d 100644 --- a/src/json.hpp.re2c +++ b/src/json.hpp.re2c @@ -8828,42 +8828,44 @@ class basic_json */ basic_json apply_patch(const basic_json& patch) const { + // make a working copy to apply the patch to basic_json result = *this; - if (not patch.is_array()) - { - // a JSON patch must be an array of objects - throw std::domain_error("JSON patch must be an array of objects"); - } - - const auto operation_add = [&result](json_pointer & ptr, - basic_json & value) + // wrapper for "add" operation; add value at ptr + const auto operation_add = [&result](json_pointer & ptr, basic_json value) { + // get reference to parent of JSON pointer ptr const auto last_path = ptr.pop_back(); basic_json& parent = result.at(ptr); if (parent.is_object()) { + // use operator[] to add value parent[last_path] = value; } else if (parent.is_array()) { if (last_path == "-") { + // special case: append to back parent.push_back(value); } else { - parent.insert(parent.begin() + std::stoi(last_path), - value); + // default case: insert add offset + parent.insert(parent.begin() + std::stoi(last_path), value); } } }; + // wrapper for "remove" operation; remove value at ptr const auto operation_remove = [&result](json_pointer & ptr) { + // get reference to parent of JSON pointer ptr const auto last_path = ptr.pop_back(); basic_json& parent = result.at(ptr); + + // remove child if (parent.is_object()) { parent.erase(parent.find(last_path)); @@ -8874,41 +8876,57 @@ class basic_json } }; + // type check + if (not patch.is_array()) + { + // a JSON patch must be an array of objects + throw std::domain_error("JSON patch must be an array of objects"); + } + + // iterate and apply th eoperations for (const auto& val : patch) { + // wrapper to get a value for an operation + const auto get_value = [&val](const std::string & op, + const std::string & member, + bool string_type = false) -> basic_json& + { + // find value + auto it = val.m_value.object->find(member); + + // context-sensitive error message + const auto error_msg = (op == "op") ? "operation" : "operation '" + op + "'"; + + // check if desired value is present + if (it == val.m_value.object->end()) + { + throw std::domain_error(error_msg + " must have member '" + member + "'"); + } + + // check if result is of type string + if (string_type and not it->second.is_string()) + { + throw std::domain_error(error_msg + " must have string member '" + member + "'"); + } + + // no error: return value + return it->second; + }; + + // type check if (not val.is_object()) { throw std::domain_error("JSON patch must be an array of objects"); } - // collect members - const auto it_op = val.m_value.object->find("op"); - const auto it_path = val.m_value.object->find("path"); - const auto it_value = val.m_value.object->find("value"); - const auto it_from = val.m_value.object->find("from"); - - if (it_op == val.m_value.object->end() or not it_op->second.is_string()) - { - throw std::domain_error("operation must have a string 'op' member"); - } - - if (it_path == val.m_value.object->end() or not it_op->second.is_string()) - { - throw std::domain_error("operation must have a string 'path' member"); - } - - const std::string op = it_op->second; - const std::string path = it_path->second; - json_pointer ptr(path); + // collect mandatory members + const std::string op = get_value("op", "op", true); + const std::string path = get_value(op, "path", true); + json_pointer ptr(get_value(op, "path", true)); if (op == "add") { - if (it_value == val.m_value.object->end()) - { - throw std::domain_error("'add' operation must have member 'value'"); - } - - operation_add(ptr, it_value->second); + operation_add(ptr, get_value("add", "value")); } else if (op == "remove") { @@ -8916,21 +8934,11 @@ class basic_json } else if (op == "replace") { - if (it_value == val.m_value.object->end()) - { - throw std::domain_error("'replace' operation must have member 'value'"); - } - - result.at(ptr) = it_value->second; + result.at(ptr) = get_value("replace", "value"); } else if (op == "move") { - if (it_from == val.m_value.object->end()) - { - throw std::domain_error("'move' operation must have member 'from'"); - } - - const std::string from_path = it_from->second; + const std::string from_path = get_value("move", "from", true); json_pointer from_ptr(from_path); basic_json v = result[from_ptr]; @@ -8939,32 +8947,22 @@ class basic_json } else if (op == "copy") { - if (it_from == val.m_value.object->end()) - { - throw std::domain_error("'copy' operation must have member 'from'"); - } - - const std::string from_path = it_from->second; + const std::string from_path = get_value("copy", "from", true);; const json_pointer from_ptr(from_path); result[ptr] = result.at(from_ptr); } else if (op == "test") { - if (it_value == val.m_value.object->end()) - { - throw std::domain_error("'test' operation must have member 'value'"); - } - - if (result.at(ptr) != it_value->second) + if (result.at(ptr) != get_value("test", "value")) { throw std::domain_error("unsuccessful: " + val.dump()); } } else { - // op must be "add", "remove", "replace", "move", - // "copy", or "test" + // op must be "add", "remove", "replace", "move", "copy", or + // "test" throw std::domain_error("operation value '" + op + "' is invalid"); } } diff --git a/test/unit.cpp b/test/unit.cpp index 907e68bf..664648cb 100644 --- a/test/unit.cpp +++ b/test/unit.cpp @@ -12822,6 +12822,198 @@ TEST_CASE("JSON patch") CHECK(doc.apply_patch(patch) == expected); } } + + SECTION("errors") + { + SECTION("unknown operation") + { + SECTION("missing 'op'") + { + json j; + json patch = {{{"foo", "bar"}}}; + CHECK_THROWS_AS(j.apply_patch(patch), std::domain_error); + CHECK_THROWS_WITH(j.apply_patch(patch), "operation must have member 'op'"); + } + + SECTION("non-string 'op'") + { + json j; + json patch = {{{"op", 1}}}; + CHECK_THROWS_AS(j.apply_patch(patch), std::domain_error); + CHECK_THROWS_WITH(j.apply_patch(patch), "operation must have string member 'op'"); + } + } + + SECTION("add") + { + SECTION("missing 'path'") + { + json j; + json patch = {{{"op", "add"}}}; + CHECK_THROWS_AS(j.apply_patch(patch), std::domain_error); + CHECK_THROWS_WITH(j.apply_patch(patch), "operation 'add' must have member 'path'"); + } + + SECTION("non-string 'path'") + { + json j; + json patch = {{{"op", "add"}, {"path", 1}}}; + CHECK_THROWS_AS(j.apply_patch(patch), std::domain_error); + CHECK_THROWS_WITH(j.apply_patch(patch), "operation 'add' must have string member 'path'"); + } + + SECTION("missing 'value'") + { + json j; + json patch = {{{"op", "add"}, {"path", ""}}}; + CHECK_THROWS_AS(j.apply_patch(patch), std::domain_error); + CHECK_THROWS_WITH(j.apply_patch(patch), "operation 'add' must have member 'value'"); + } + } + + SECTION("remove") + { + SECTION("missing 'path'") + { + json j; + json patch = {{{"op", "remove"}}}; + CHECK_THROWS_AS(j.apply_patch(patch), std::domain_error); + CHECK_THROWS_WITH(j.apply_patch(patch), "operation 'remove' must have member 'path'"); + } + + SECTION("non-string 'path'") + { + json j; + json patch = {{{"op", "remove"}, {"path", 1}}}; + CHECK_THROWS_AS(j.apply_patch(patch), std::domain_error); + CHECK_THROWS_WITH(j.apply_patch(patch), "operation 'remove' must have string member 'path'"); + } + } + + SECTION("replace") + { + SECTION("missing 'path'") + { + json j; + json patch = {{{"op", "replace"}}}; + CHECK_THROWS_AS(j.apply_patch(patch), std::domain_error); + CHECK_THROWS_WITH(j.apply_patch(patch), "operation 'replace' must have member 'path'"); + } + + SECTION("non-string 'path'") + { + json j; + json patch = {{{"op", "replace"}, {"path", 1}}}; + CHECK_THROWS_AS(j.apply_patch(patch), std::domain_error); + CHECK_THROWS_WITH(j.apply_patch(patch), "operation 'replace' must have string member 'path'"); + } + + SECTION("missing 'value'") + { + json j; + json patch = {{{"op", "replace"}, {"path", ""}}}; + CHECK_THROWS_AS(j.apply_patch(patch), std::domain_error); + CHECK_THROWS_WITH(j.apply_patch(patch), "operation 'replace' must have member 'value'"); + } + } + + SECTION("move") + { + SECTION("missing 'path'") + { + json j; + json patch = {{{"op", "move"}}}; + CHECK_THROWS_AS(j.apply_patch(patch), std::domain_error); + CHECK_THROWS_WITH(j.apply_patch(patch), "operation 'move' must have member 'path'"); + } + + SECTION("non-string 'path'") + { + json j; + json patch = {{{"op", "move"}, {"path", 1}}}; + CHECK_THROWS_AS(j.apply_patch(patch), std::domain_error); + CHECK_THROWS_WITH(j.apply_patch(patch), "operation 'move' must have string member 'path'"); + } + + SECTION("missing 'from'") + { + json j; + json patch = {{{"op", "move"}, {"path", ""}}}; + CHECK_THROWS_AS(j.apply_patch(patch), std::domain_error); + CHECK_THROWS_WITH(j.apply_patch(patch), "operation 'move' must have member 'from'"); + } + + SECTION("non-string 'from'") + { + json j; + json patch = {{{"op", "move"}, {"path", ""}, {"from", 1}}}; + CHECK_THROWS_AS(j.apply_patch(patch), std::domain_error); + CHECK_THROWS_WITH(j.apply_patch(patch), "operation 'move' must have string member 'from'"); + } + } + + SECTION("copy") + { + SECTION("missing 'path'") + { + json j; + json patch = {{{"op", "copy"}}}; + CHECK_THROWS_AS(j.apply_patch(patch), std::domain_error); + CHECK_THROWS_WITH(j.apply_patch(patch), "operation 'copy' must have member 'path'"); + } + + SECTION("non-string 'path'") + { + json j; + json patch = {{{"op", "copy"}, {"path", 1}}}; + CHECK_THROWS_AS(j.apply_patch(patch), std::domain_error); + CHECK_THROWS_WITH(j.apply_patch(patch), "operation 'copy' must have string member 'path'"); + } + + SECTION("missing 'from'") + { + json j; + json patch = {{{"op", "copy"}, {"path", ""}}}; + CHECK_THROWS_AS(j.apply_patch(patch), std::domain_error); + CHECK_THROWS_WITH(j.apply_patch(patch), "operation 'copy' must have member 'from'"); + } + + SECTION("non-string 'from'") + { + json j; + json patch = {{{"op", "copy"}, {"path", ""}, {"from", 1}}}; + CHECK_THROWS_AS(j.apply_patch(patch), std::domain_error); + CHECK_THROWS_WITH(j.apply_patch(patch), "operation 'copy' must have string member 'from'"); + } + } + + SECTION("test") + { + SECTION("missing 'path'") + { + json j; + json patch = {{{"op", "test"}}}; + CHECK_THROWS_AS(j.apply_patch(patch), std::domain_error); + CHECK_THROWS_WITH(j.apply_patch(patch), "operation 'test' must have member 'path'"); + } + + SECTION("non-string 'path'") + { + json j; + json patch = {{{"op", "test"}, {"path", 1}}}; + CHECK_THROWS_AS(j.apply_patch(patch), std::domain_error); + CHECK_THROWS_WITH(j.apply_patch(patch), "operation 'test' must have string member 'path'"); + } + + SECTION("missing 'value'") + { + json j; + json patch = {{{"op", "test"}, {"path", ""}}}; + CHECK_THROWS_AS(j.apply_patch(patch), std::domain_error); + CHECK_THROWS_WITH(j.apply_patch(patch), "operation 'test' must have member 'value'"); + } + } + } } TEST_CASE("regression tests") From 8d4cf5ef8d0f3ad5bd7efb32dc7d3b44acc386e9 Mon Sep 17 00:00:00 2001 From: Niels Date: Sun, 24 Apr 2016 19:04:10 +0200 Subject: [PATCH 07/17] oops --- src/json.hpp | 2 +- src/json.hpp.re2c | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/json.hpp b/src/json.hpp index b7a6f64a..0a1db1c3 100644 --- a/src/json.hpp +++ b/src/json.hpp @@ -9612,7 +9612,7 @@ basic_json_parser_63: // collect mandatory members const std::string op = get_value("op", "op", true); const std::string path = get_value(op, "path", true); - json_pointer ptr(get_value(op, "path", true)); + json_pointer ptr(path); if (op == "add") { diff --git a/src/json.hpp.re2c b/src/json.hpp.re2c index c4c87f5d..52d76148 100644 --- a/src/json.hpp.re2c +++ b/src/json.hpp.re2c @@ -8922,7 +8922,7 @@ class basic_json // collect mandatory members const std::string op = get_value("op", "op", true); const std::string path = get_value(op, "path", true); - json_pointer ptr(get_value(op, "path", true)); + json_pointer ptr(path); if (op == "add") { From 96cfe7463fa4faf32a2e618065955fc4ef43ff57 Mon Sep 17 00:00:00 2001 From: Niels Date: Sun, 24 Apr 2016 19:09:12 +0200 Subject: [PATCH 08/17] fixed some warnings --- src/json.hpp | 16 ++++++++-------- src/json.hpp.re2c | 16 ++++++++-------- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/src/json.hpp b/src/json.hpp index 0a1db1c3..bfe45ada 100644 --- a/src/json.hpp +++ b/src/json.hpp @@ -9522,7 +9522,7 @@ basic_json_parser_63: basic_json result = *this; // wrapper for "add" operation; add value at ptr - const auto operation_add = [&result](json_pointer & ptr, basic_json value) + const auto operation_add = [&result](json_pointer & ptr, basic_json val) { // get reference to parent of JSON pointer ptr const auto last_path = ptr.pop_back(); @@ -9531,19 +9531,19 @@ basic_json_parser_63: if (parent.is_object()) { // use operator[] to add value - parent[last_path] = value; + parent[last_path] = val; } else if (parent.is_array()) { if (last_path == "-") { // special case: append to back - parent.push_back(value); + parent.push_back(val); } else { // default case: insert add offset - parent.insert(parent.begin() + std::stoi(last_path), value); + parent.insert(parent.begin() + std::stoi(last_path), val); } } }; @@ -9579,7 +9579,7 @@ basic_json_parser_63: // wrapper to get a value for an operation const auto get_value = [&val](const std::string & op, const std::string & member, - bool string_type = false) -> basic_json& + bool string_type) -> basic_json& { // find value auto it = val.m_value.object->find(member); @@ -9616,7 +9616,7 @@ basic_json_parser_63: if (op == "add") { - operation_add(ptr, get_value("add", "value")); + operation_add(ptr, get_value("add", "value", false)); } else if (op == "remove") { @@ -9624,7 +9624,7 @@ basic_json_parser_63: } else if (op == "replace") { - result.at(ptr) = get_value("replace", "value"); + result.at(ptr) = get_value("replace", "value", false); } else if (op == "move") { @@ -9644,7 +9644,7 @@ basic_json_parser_63: } else if (op == "test") { - if (result.at(ptr) != get_value("test", "value")) + if (result.at(ptr) != get_value("test", "value", false)) { throw std::domain_error("unsuccessful: " + val.dump()); } diff --git a/src/json.hpp.re2c b/src/json.hpp.re2c index 52d76148..af6df6ed 100644 --- a/src/json.hpp.re2c +++ b/src/json.hpp.re2c @@ -8832,7 +8832,7 @@ class basic_json basic_json result = *this; // wrapper for "add" operation; add value at ptr - const auto operation_add = [&result](json_pointer & ptr, basic_json value) + const auto operation_add = [&result](json_pointer & ptr, basic_json val) { // get reference to parent of JSON pointer ptr const auto last_path = ptr.pop_back(); @@ -8841,19 +8841,19 @@ class basic_json if (parent.is_object()) { // use operator[] to add value - parent[last_path] = value; + parent[last_path] = val; } else if (parent.is_array()) { if (last_path == "-") { // special case: append to back - parent.push_back(value); + parent.push_back(val); } else { // default case: insert add offset - parent.insert(parent.begin() + std::stoi(last_path), value); + parent.insert(parent.begin() + std::stoi(last_path), val); } } }; @@ -8889,7 +8889,7 @@ class basic_json // wrapper to get a value for an operation const auto get_value = [&val](const std::string & op, const std::string & member, - bool string_type = false) -> basic_json& + bool string_type) -> basic_json& { // find value auto it = val.m_value.object->find(member); @@ -8926,7 +8926,7 @@ class basic_json if (op == "add") { - operation_add(ptr, get_value("add", "value")); + operation_add(ptr, get_value("add", "value", false)); } else if (op == "remove") { @@ -8934,7 +8934,7 @@ class basic_json } else if (op == "replace") { - result.at(ptr) = get_value("replace", "value"); + result.at(ptr) = get_value("replace", "value", false); } else if (op == "move") { @@ -8954,7 +8954,7 @@ class basic_json } else if (op == "test") { - if (result.at(ptr) != get_value("test", "value")) + if (result.at(ptr) != get_value("test", "value", false)) { throw std::domain_error("unsuccessful: " + val.dump()); } From 5e0bf75d6056eee48976f9c4e86ed52461d289aa Mon Sep 17 00:00:00 2001 From: Niels Date: Mon, 25 Apr 2016 23:17:04 +0200 Subject: [PATCH 09/17] cleanup, test, and diff --- README.md | 2 +- doc/examples/diff.cpp | 34 ++ doc/examples/diff.link | 1 + doc/examples/diff.output | 25 + doc/examples/patch.cpp | 30 + doc/examples/patch.link | 1 + doc/examples/patch.output | 11 + src/json.hpp | 753 +++++++++++++++++------- src/json.hpp.re2c | 753 +++++++++++++++++------- test/unit.cpp | 1174 +++++++++++++++++++++++++++---------- 10 files changed, 2068 insertions(+), 716 deletions(-) create mode 100644 doc/examples/diff.cpp create mode 100644 doc/examples/diff.link create mode 100644 doc/examples/diff.output create mode 100644 doc/examples/patch.cpp create mode 100644 doc/examples/patch.link create mode 100644 doc/examples/patch.output diff --git a/README.md b/README.md index 2dd60bf1..8f4779f9 100644 --- a/README.md +++ b/README.md @@ -428,7 +428,7 @@ $ make $ ./json_unit "*" =============================================================================== -All tests passed (3344416 assertions in 30 test cases) +All tests passed (3344554 assertions in 31 test cases) ``` For more information, have a look at the file [.travis.yml](https://github.com/nlohmann/json/blob/master/.travis.yml). diff --git a/doc/examples/diff.cpp b/doc/examples/diff.cpp new file mode 100644 index 00000000..d81a58db --- /dev/null +++ b/doc/examples/diff.cpp @@ -0,0 +1,34 @@ +#include + +using json = nlohmann::json; + +int main() +{ + // the source document + json source = R"( + { + "baz": "qux", + "foo": "bar" + } + )"_json; + + // the target document + json target = R"( + { + "baz": "boo", + "hello": [ + "world" + ] + } + )"_json; + + // create the patch + json patch = json::diff(source, target); + + // roundtrip + json patched_source = source.patch(patch); + + // output patch and roundtrip result + std::cout << std::setw(4) << patch << "\n\n" + << std::setw(4) << patched_source << std::endl; +} diff --git a/doc/examples/diff.link b/doc/examples/diff.link new file mode 100644 index 00000000..c3e3fa4d --- /dev/null +++ b/doc/examples/diff.link @@ -0,0 +1 @@ +online \ No newline at end of file diff --git a/doc/examples/diff.output b/doc/examples/diff.output new file mode 100644 index 00000000..7dc79791 --- /dev/null +++ b/doc/examples/diff.output @@ -0,0 +1,25 @@ +[ + { + "op": "replace", + "path": "/baz", + "value": "boo" + }, + { + "op": "remove", + "path": "/foo" + }, + { + "op": "add", + "path": "/hello", + "value": [ + "world" + ] + } +] + +{ + "baz": "boo", + "hello": [ + "world" + ] +} diff --git a/doc/examples/patch.cpp b/doc/examples/patch.cpp new file mode 100644 index 00000000..24a52d59 --- /dev/null +++ b/doc/examples/patch.cpp @@ -0,0 +1,30 @@ +#include + +using json = nlohmann::json; + +int main() +{ + // 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; + + // apply the patch + json patched_doc = doc.patch(patch); + + // output original and patched document + std::cout << std::setw(4) << doc << "\n\n" + << std::setw(4) << patched_doc << std::endl; +} diff --git a/doc/examples/patch.link b/doc/examples/patch.link new file mode 100644 index 00000000..5d5032b7 --- /dev/null +++ b/doc/examples/patch.link @@ -0,0 +1 @@ +online \ No newline at end of file diff --git a/doc/examples/patch.output b/doc/examples/patch.output new file mode 100644 index 00000000..eb558fe2 --- /dev/null +++ b/doc/examples/patch.output @@ -0,0 +1,11 @@ +{ + "baz": "qux", + "foo": "bar" +} + +{ + "baz": "boo", + "hello": [ + "world" + ] +} diff --git a/src/json.hpp b/src/json.hpp index bfe45ada..1785f182 100644 --- a/src/json.hpp +++ b/src/json.hpp @@ -3595,121 +3595,6 @@ class basic_json } } - /*! - @brief access specified element via JSON Pointer - - Uses a JSON pointer to retrieve a reference to the respective JSON value. - No bound checking is performed. Similar to - @ref operator[](const typename object_t::key_type&), `null` values - are created in arrays and objects if necessary. - - In particular: - - If the JSON pointer points to an object key that does not exist, it - is created an filled with a `null` value before a reference to it - is returned. - - If the JSON pointer points to an array index that does not exist, it - is created an filled with a `null` value before a reference to it - is returned. All indices between the current maximum and the given - index are also filled with `null`. - - The special value `-` is treated as a synonym for the index past the - end. - - @param[in] ptr a JSON pointer - - @return reference to the element pointed to by @a ptr - - @complexity Constant. - - @throw std::out_of_range if the JSON pointer can not be resolved - @throw std::domain_error if an array index begins with '0' - @throw std::invalid_argument if an array index was not a number - - @liveexample{The behavior is shown in the example.,operatorjson_pointer} - - @since version 2.0.0 - */ - reference operator[](const json_pointer& ptr) - { - return ptr.get_unchecked(this); - } - - /*! - @brief access specified element via JSON Pointer - - Uses a JSON pointer to retrieve a reference to the respective JSON value. - No bound checking is performed. The function does not change the JSON - value; no `null` values are created. In particular, the the special value - `-` yields an exception. - - @param[in] ptr JSON pointer to the desired element - - @return const reference to the element pointed to by @a ptr - - @complexity Constant. - - @throw std::out_of_range if the JSON pointer can not be resolved - @throw std::domain_error if an array index begins with '0' - @throw std::invalid_argument if an array index was not a number - - @liveexample{The behavior is shown in the example.,operatorjson_pointer_const} - - @since version 2.0.0 - */ - const_reference operator[](const json_pointer& ptr) const - { - return ptr.get_unchecked(this); - } - - /*! - @brief access specified element via JSON Pointer - - Returns a reference to the element at with specified JSON pointer @a ptr, - with bounds checking. - - @param[in] ptr JSON pointer to the desired element - - @return reference to the element pointed to by @a ptr - - @complexity Constant. - - @throw std::out_of_range if the JSON pointer can not be resolved - @throw std::domain_error if an array index begins with '0' - @throw std::invalid_argument if an array index was not a number - - @liveexample{The behavior is shown in the example.,at_json_pointer} - - @since version 2.0.0 - */ - reference at(const json_pointer& ptr) - { - return ptr.get_checked(this); - } - - /*! - @brief access specified element via JSON Pointer - - Returns a const reference to the element at with specified JSON pointer - @a ptr, with bounds checking. - - @param[in] ptr JSON pointer to the desired element - - @return reference to the element pointed to by @a ptr - - @complexity Constant. - - @throw std::out_of_range if the JSON pointer can not be resolved - @throw std::domain_error if an array index begins with '0' - @throw std::invalid_argument if an array index was not a number - - @liveexample{The behavior is shown in the example.,at_json_pointer_const} - - @since version 2.0.0 - */ - const_reference at(const json_pointer& ptr) const - { - return ptr.get_checked(this); - } - /*! @brief access specified object element with default value @@ -4145,8 +4030,8 @@ class basic_json @throw std::domain_error when called on a type other than JSON array; example: `"cannot use erase() with null"` - @throw std::out_of_range when `idx >= size()`; example: `"index out of - range"` + @throw std::out_of_range when `idx >= size()`; example: `"array index 17 + is out of range"` @complexity Linear in distance between @a idx and the end of the container. @@ -4167,7 +4052,7 @@ class basic_json { if (idx >= size()) { - throw std::out_of_range("index out of range"); + throw std::out_of_range("array index " + std::to_string(idx) + " is out of range"); } assert(m_value.array != nullptr); @@ -8969,9 +8854,17 @@ basic_json_parser_63: : reference_tokens(split(s)) {} + /// test for inequality + bool operator!=(const json_pointer& rhs) const + { + return reference_tokens != rhs.reference_tokens; + } + + private: + /// remove and return last reference pointer std::string pop_back() { - if (reference_tokens.empty()) + if (is_root()) { throw std::domain_error("JSON pointer has no parent"); } @@ -8981,7 +8874,24 @@ basic_json_parser_63: return last; } - private: + /// return whether pointer points to the root document + bool is_root() const + { + return reference_tokens.empty(); + } + + json_pointer top() const + { + if (is_root()) + { + throw std::domain_error("JSON pointer has no parent"); + } + + json_pointer result = *this; + result.reference_tokens = {reference_tokens[0]}; + return result; + } + /*! @brief create and return a reference to the pointed to value */ @@ -9020,7 +8930,7 @@ basic_json_parser_63: case value_t::array: { // create an entry in the array - result = &result->operator[](static_cast(std::stoi(reference_token))); + result = &result->operator[](static_cast(std::stoi(reference_token))); break; } @@ -9083,7 +8993,7 @@ basic_json_parser_63: else { // convert array index to number; unchecked access - ptr = &ptr->operator[](static_cast(std::stoi(reference_token))); + ptr = &ptr->operator[](static_cast(std::stoi(reference_token))); } break; } @@ -9128,7 +9038,7 @@ basic_json_parser_63: } // note: at performs range check - ptr = &ptr->at(static_cast(std::stoi(reference_token))); + ptr = &ptr->at(static_cast(std::stoi(reference_token))); break; } @@ -9180,7 +9090,7 @@ basic_json_parser_63: } // use unchecked array access - ptr = &ptr->operator[](static_cast(std::stoi(reference_token))); + ptr = &ptr->operator[](static_cast(std::stoi(reference_token))); break; } @@ -9224,7 +9134,7 @@ basic_json_parser_63: } // note: at performs range check - ptr = &ptr->at(static_cast(std::stoi(reference_token))); + ptr = &ptr->at(static_cast(std::stoi(reference_token))); break; } @@ -9291,12 +9201,8 @@ basic_json_parser_63: } } - // first transform any occurrence of the sequence '~1' to '/' - replace_substring(reference_token, "~1", "/"); - // then transform any occurrence of the sequence '~0' to '~' - replace_substring(reference_token, "~0", "~"); - // finally, store the reference token + unescape(reference_token); result.push_back(reference_token); } @@ -9332,6 +9238,24 @@ basic_json_parser_63: ); } + /// escape tilde and slash + static std::string escape(std::string s) + { + // escape "~"" to "~0" and "/" to "~1" + replace_substring(s, "~", "~0"); + replace_substring(s, "/", "~1"); + return s; + } + + /// unescape tilde and slash + static void unescape(std::string& s) + { + // first transform any occurrence of the sequence '~1' to '/' + replace_substring(s, "~1", "/"); + // then transform any occurrence of the sequence '~0' to '~' + replace_substring(s, "~0", "~"); + } + /*! @param[in] reference_string the reference string to the current value @param[in] value the value to consider @@ -9339,7 +9263,7 @@ basic_json_parser_63: @note Empty objects or arrays are flattened to `null`. */ - static void flatten(const std::string reference_string, + static void flatten(const std::string& reference_string, const basic_json& value, basic_json& result) { @@ -9376,12 +9300,7 @@ basic_json_parser_63: // iterate object and use keys as reference string for (const auto& element : *value.m_value.object) { - // escape "~"" to "~0" and "/" to "~1" - std::string key(element.first); - replace_substring(key, "~", "~0"); - replace_substring(key, "/", "~1"); - - flatten(reference_string + "/" + key, + flatten(reference_string + "/" + escape(element.first), element.second, result); } } @@ -9435,13 +9354,128 @@ basic_json_parser_63: std::vector reference_tokens {}; }; - //////////////////////////// - // JSON Pointer functions // - //////////////////////////// + ////////////////////////// + // JSON Pointer support // + ////////////////////////// /// @name JSON Pointer functions /// @{ + /*! + @brief access specified element via JSON Pointer + + Uses a JSON pointer to retrieve a reference to the respective JSON value. + No bound checking is performed. Similar to + @ref operator[](const typename object_t::key_type&), `null` values + are created in arrays and objects if necessary. + + In particular: + - If the JSON pointer points to an object key that does not exist, it + is created an filled with a `null` value before a reference to it + is returned. + - If the JSON pointer points to an array index that does not exist, it + is created an filled with a `null` value before a reference to it + is returned. All indices between the current maximum and the given + index are also filled with `null`. + - The special value `-` is treated as a synonym for the index past the + end. + + @param[in] ptr a JSON pointer + + @return reference to the element pointed to by @a ptr + + @complexity Constant. + + @throw std::out_of_range if the JSON pointer can not be resolved + @throw std::domain_error if an array index begins with '0' + @throw std::invalid_argument if an array index was not a number + + @liveexample{The behavior is shown in the example.,operatorjson_pointer} + + @since version 2.0.0 + */ + reference operator[](const json_pointer& ptr) + { + return ptr.get_unchecked(this); + } + + /*! + @brief access specified element via JSON Pointer + + Uses a JSON pointer to retrieve a reference to the respective JSON value. + No bound checking is performed. The function does not change the JSON + value; no `null` values are created. In particular, the the special value + `-` yields an exception. + + @param[in] ptr JSON pointer to the desired element + + @return const reference to the element pointed to by @a ptr + + @complexity Constant. + + @throw std::out_of_range if the JSON pointer can not be resolved + @throw std::domain_error if an array index begins with '0' + @throw std::invalid_argument if an array index was not a number + + @liveexample{The behavior is shown in the example.,operatorjson_pointer_const} + + @since version 2.0.0 + */ + const_reference operator[](const json_pointer& ptr) const + { + return ptr.get_unchecked(this); + } + + /*! + @brief access specified element via JSON Pointer + + Returns a reference to the element at with specified JSON pointer @a ptr, + with bounds checking. + + @param[in] ptr JSON pointer to the desired element + + @return reference to the element pointed to by @a ptr + + @complexity Constant. + + @throw std::out_of_range if the JSON pointer can not be resolved + @throw std::domain_error if an array index begins with '0' + @throw std::invalid_argument if an array index was not a number + + @liveexample{The behavior is shown in the example.,at_json_pointer} + + @since version 2.0.0 + */ + reference at(const json_pointer& ptr) + { + return ptr.get_checked(this); + } + + /*! + @brief access specified element via JSON Pointer + + Returns a const reference to the element at with specified JSON pointer + @a ptr, with bounds checking. + + @param[in] ptr JSON pointer to the desired element + + @return reference to the element pointed to by @a ptr + + @complexity Constant. + + @throw std::out_of_range if the JSON pointer can not be resolved + @throw std::domain_error if an array index begins with '0' + @throw std::invalid_argument if an array index was not a number + + @liveexample{The behavior is shown in the example.,at_json_pointer_const} + + @since version 2.0.0 + */ + const_reference at(const json_pointer& ptr) const + { + return ptr.get_checked(this); + } + /*! @brief return flattened JSON value @@ -9505,45 +9539,146 @@ basic_json_parser_63: /// @} + ////////////////////////// + // JSON Patch functions // + ////////////////////////// + + /// @name JSON Patch functions + /// @{ + /*! @brief applies a JSON patch + [JSON Patch](http://jsonpatch.com) defines a JSON document structure for + expressing a sequence of operations to apply to a JSON) document. With + this funcion, a JSON Patch is applied to the current JSON value by + executing all operations from the patch. + @param[in] patch JSON patch document @return patched document - @note The original JSON value is not changed; that is, the patch is - applied to a copy of the value. + @note The application of a patch is atomic: Either all operations succeed + and the patched document is returned or an exception is thrown. In + any case, the original value is not changed: the patch is applied + to a copy of the value. - @sa [RFC 6902](https://tools.ietf.org/html/rfc6902) + @throw std::out_of_range if a JSON pointer inside the patch could not + be resolved successfully in the current JSON value; example: `"key baz + not found"` + @throw invalid_argument if the JSON patch is malformed (e.g., mandatory + attributes are missing); example: `"operation add must have member path"` + + @complexity Linear in the size of the JSON value and the length of the + JSON patch. As usually only a fraction of the JSON value is affected by + the patch, the complexity can usually be neglected. + + @liveexample{The following code shows how a JSON patch is applied to a + value.,patch} + + @sa @ref diff -- create a JSON patch by comparing two JSON values + + @sa [RFC 6902 (JSON Patch)](https://tools.ietf.org/html/rfc6902) + @sa [RFC 6901 (JSON Pointer)](https://tools.ietf.org/html/rfc6901) + + @since version 2.0.0 */ - basic_json apply_patch(const basic_json& patch) const + basic_json patch(const basic_json& patch) const { // make a working copy to apply the patch to basic_json result = *this; + // the valid JSON Patch operations + enum class patch_operations {add, remove, replace, move, copy, test, invalid}; + + const auto get_op = [](const std::string op) + { + if (op == "add") + { + return patch_operations::add; + } + if (op == "remove") + { + return patch_operations::remove; + } + if (op == "replace") + { + return patch_operations::replace; + } + if (op == "move") + { + return patch_operations::move; + } + if (op == "copy") + { + return patch_operations::copy; + } + if (op == "test") + { + return patch_operations::test; + } + + return patch_operations::invalid; + }; + // wrapper for "add" operation; add value at ptr const auto operation_add = [&result](json_pointer & ptr, basic_json val) { - // get reference to parent of JSON pointer ptr - const auto last_path = ptr.pop_back(); - basic_json& parent = result.at(ptr); - - if (parent.is_object()) + // adding to the root of the target document means replacing it + if (ptr.is_root()) { - // use operator[] to add value - parent[last_path] = val; + result = val; } - else if (parent.is_array()) + else { - if (last_path == "-") + // make sure the top element of the pointer exists + json_pointer top_pointer = ptr.top(); + if (top_pointer != ptr) { - // special case: append to back - parent.push_back(val); + basic_json& x = result.at(top_pointer); } - else + + // get reference to parent of JSON pointer ptr + const auto last_path = ptr.pop_back(); + basic_json& parent = result[ptr]; + + switch (parent.m_type) { - // default case: insert add offset - parent.insert(parent.begin() + std::stoi(last_path), val); + case value_t::null: + case value_t::object: + { + // use operator[] to add value + parent[last_path] = val; + break; + } + + case value_t::array: + { + if (last_path == "-") + { + // special case: append to back + parent.push_back(val); + } + else + { + const auto idx = std::stoi(last_path); + if (static_cast(idx) > parent.size()) + { + // avoid undefined behavior + throw std::out_of_range("array index " + std::to_string(idx) + " is out of range"); + } + else + { + // default case: insert add offset + parent.insert(parent.begin() + static_cast(idx), val); + } + } + break; + } + + default: + { + throw std::domain_error("unexpected parent type " + parent.type_name()); + } } } }; @@ -9558,11 +9693,21 @@ basic_json_parser_63: // remove child if (parent.is_object()) { - parent.erase(parent.find(last_path)); + // perform range check + auto it = parent.find(last_path); + if (it != parent.end()) + { + parent.erase(it); + } + else + { + throw std::out_of_range("key '" + last_path + "' not found"); + } } else if (parent.is_array()) { - parent.erase(parent.begin() + std::stoi(last_path)); + // note erase performs range check + parent.erase(static_cast(std::stoi(last_path))); } }; @@ -9570,7 +9715,7 @@ basic_json_parser_63: if (not patch.is_array()) { // a JSON patch must be an array of objects - throw std::domain_error("JSON patch must be an array of objects"); + throw std::invalid_argument("JSON patch must be an array of objects"); } // iterate and apply th eoperations @@ -9590,13 +9735,13 @@ basic_json_parser_63: // check if desired value is present if (it == val.m_value.object->end()) { - throw std::domain_error(error_msg + " must have member '" + member + "'"); + throw std::invalid_argument(error_msg + " must have member '" + member + "'"); } // check if result is of type string if (string_type and not it->second.is_string()) { - throw std::domain_error(error_msg + " must have string member '" + member + "'"); + throw std::invalid_argument(error_msg + " must have string member '" + member + "'"); } // no error: return value @@ -9606,7 +9751,7 @@ basic_json_parser_63: // type check if (not val.is_object()) { - throw std::domain_error("JSON patch must be an array of objects"); + throw std::invalid_argument("JSON patch must be an array of objects"); } // collect mandatory members @@ -9614,51 +9759,251 @@ basic_json_parser_63: const std::string path = get_value(op, "path", true); json_pointer ptr(path); - if (op == "add") + switch (get_op(op)) { - operation_add(ptr, get_value("add", "value", false)); - } - else if (op == "remove") - { - operation_remove(ptr); - } - else if (op == "replace") - { - result.at(ptr) = get_value("replace", "value", false); - } - else if (op == "move") - { - const std::string from_path = get_value("move", "from", true); - json_pointer from_ptr(from_path); - basic_json v = result[from_ptr]; - - operation_remove(from_ptr); - operation_add(ptr, v); - } - else if (op == "copy") - { - const std::string from_path = get_value("copy", "from", true);; - const json_pointer from_ptr(from_path); - - result[ptr] = result.at(from_ptr); - } - else if (op == "test") - { - if (result.at(ptr) != get_value("test", "value", false)) + case patch_operations::add: { - throw std::domain_error("unsuccessful: " + val.dump()); + operation_add(ptr, get_value("add", "value", false)); + break; + } + + case patch_operations::remove: + { + operation_remove(ptr); + break; + } + + case patch_operations::replace: + { + // the "path" location must exist - use at() + result.at(ptr) = get_value("replace", "value", false); + break; + } + + case patch_operations::move: + { + const std::string from_path = get_value("move", "from", true); + json_pointer from_ptr(from_path); + + // the "from" location must exist - use at() + basic_json v = result.at(from_ptr); + + // The move operation is functionally identical to a + // "remove" operation on the "from" location, followed + // immediately by an "add" operation at the target + // location with the value that was just removed. + operation_remove(from_ptr); + operation_add(ptr, v); + break; + } + + case patch_operations::copy: + { + const std::string from_path = get_value("copy", "from", true);; + const json_pointer from_ptr(from_path); + + // the "from" location must exist - use at() + result[ptr] = result.at(from_ptr); + break; + } + + case patch_operations::test: + { + bool success = false; + try + { + // check if "value" matches the one at "path" + // the "path" location must exist - use at() + success = (result.at(ptr) == get_value("test", "value", false)); + } + catch (std::out_of_range&) + { + // ignore out of range errors: success remains false + } + + // throw an exception if test fails + if (not success) + { + throw std::domain_error("unsuccessful: " + val.dump()); + } + + break; + } + + case patch_operations::invalid: + { + // op must be "add", "remove", "replace", "move", "copy", or + // "test" + throw std::invalid_argument("operation value '" + op + "' is invalid"); } - } - else - { - // op must be "add", "remove", "replace", "move", "copy", or - // "test" - throw std::domain_error("operation value '" + op + "' is invalid"); } } return result; } + + /*! + @brief creates a diff as a JSON patch + + Creates a [JSON Patch](http://jsonpatch.com) so that value @a source can + be changed into the value @a target by calling @ref patch function. + + @invariant For two JSON values @a source and @a target, the following code + yields always `true`: + @code {.cpp} + source.patch(diff(source, target)) == target; + @endcode + + @note Currently, only `remove`, `add`, and `replace` operations are + generated. + + @param[in] source JSON value to copare from + @param[in] target JSON value to copare against + @param[in] path helper value to create JSON pointers + + @return a JSON patch to convert the @a source to @a target + + @complexity Linear in the lengths of @a source and @a target. + + @liveexample{The following code shows how a JSON patch is created as a + diff for two JSON values.,diff} + + @sa @ref patch -- apply a JSON patch + + @sa [RFC 6902 (JSON Patch)](https://tools.ietf.org/html/rfc6902) + + @since version 2.0.0 + */ + static basic_json diff(const basic_json& source, + const basic_json& target, + std::string path = "") noexcept + { + // the patch + basic_json result(value_t::array); + + // if the values are the same, return empty patch + if (source == target) + { + return result; + } + + if (source.type() != target.type()) + { + // different types: replace value + result.push_back( + { + {"op", "replace"}, + {"path", path}, + {"value", target} + }); + } + else + { + switch (source.type()) + { + case value_t::array: + { + // first pass: traverse common elements + size_t i = 0; + while (i < source.size() and i < target.size()) + { + // recursive call to compare array values at index i + auto temp_diff = diff(source[i], target[i], path + "/" + std::to_string(i)); + result.insert(result.end(), temp_diff.begin(), temp_diff.end()); + ++i; + } + + // i now reached the end of at least one array + // in a second pass, traverse the remaining elements + + // remove my remaining elements + while (i < source.size()) + { + result.push_back(object( + { + {"op", "remove"}, + {"path", path + "/" + std::to_string(i)} + })); + ++i; + } + + // add other remaining elements + while (i < target.size()) + { + result.push_back( + { + {"op", "add"}, + {"path", path + "/" + std::to_string(i)}, + {"value", target[i]} + }); + ++i; + } + + break; + } + + case value_t::object: + { + // first pass: traverse this object's elements + for (auto it = source.begin(); it != source.end(); ++it) + { + // escape the key name to be used in a JSON patch + const auto key = json_pointer::escape(it.key()); + + if (target.find(it.key()) != target.end()) + { + // recursive call to compare object values at key it + auto temp_diff = diff(it.value(), target[it.key()], path + "/" + key); + result.insert(result.end(), temp_diff.begin(), temp_diff.end()); + } + else + { + // found a key that is not in o -> remove it + result.push_back(object( + { + {"op", "remove"}, + {"path", path + "/" + key} + })); + } + } + + // second pass: traverse other object's elements + for (auto it = target.begin(); it != target.end(); ++it) + { + if (source.find(it.key()) == source.end()) + { + // found a key that is not in this -> add it + const auto key = json_pointer::escape(it.key()); + result.push_back( + { + {"op", "add"}, + {"path", path + "/" + key}, + {"value", it.value()} + }); + } + } + + break; + } + + default: + { + // both primitive type: replace value + result.push_back( + { + {"op", "replace"}, + {"path", path}, + {"value", target} + }); + break; + } + } + } + + return result; + } + + /// @} }; @@ -9678,9 +10023,9 @@ using json = basic_json<>; } -///////////////////////// -// nonmember functions // -///////////////////////// +/////////////////////// +// nonmember support // +/////////////////////// // specialization of std::swap, and std::hash namespace std diff --git a/src/json.hpp.re2c b/src/json.hpp.re2c index af6df6ed..51b72167 100644 --- a/src/json.hpp.re2c +++ b/src/json.hpp.re2c @@ -3595,121 +3595,6 @@ class basic_json } } - /*! - @brief access specified element via JSON Pointer - - Uses a JSON pointer to retrieve a reference to the respective JSON value. - No bound checking is performed. Similar to - @ref operator[](const typename object_t::key_type&), `null` values - are created in arrays and objects if necessary. - - In particular: - - If the JSON pointer points to an object key that does not exist, it - is created an filled with a `null` value before a reference to it - is returned. - - If the JSON pointer points to an array index that does not exist, it - is created an filled with a `null` value before a reference to it - is returned. All indices between the current maximum and the given - index are also filled with `null`. - - The special value `-` is treated as a synonym for the index past the - end. - - @param[in] ptr a JSON pointer - - @return reference to the element pointed to by @a ptr - - @complexity Constant. - - @throw std::out_of_range if the JSON pointer can not be resolved - @throw std::domain_error if an array index begins with '0' - @throw std::invalid_argument if an array index was not a number - - @liveexample{The behavior is shown in the example.,operatorjson_pointer} - - @since version 2.0.0 - */ - reference operator[](const json_pointer& ptr) - { - return ptr.get_unchecked(this); - } - - /*! - @brief access specified element via JSON Pointer - - Uses a JSON pointer to retrieve a reference to the respective JSON value. - No bound checking is performed. The function does not change the JSON - value; no `null` values are created. In particular, the the special value - `-` yields an exception. - - @param[in] ptr JSON pointer to the desired element - - @return const reference to the element pointed to by @a ptr - - @complexity Constant. - - @throw std::out_of_range if the JSON pointer can not be resolved - @throw std::domain_error if an array index begins with '0' - @throw std::invalid_argument if an array index was not a number - - @liveexample{The behavior is shown in the example.,operatorjson_pointer_const} - - @since version 2.0.0 - */ - const_reference operator[](const json_pointer& ptr) const - { - return ptr.get_unchecked(this); - } - - /*! - @brief access specified element via JSON Pointer - - Returns a reference to the element at with specified JSON pointer @a ptr, - with bounds checking. - - @param[in] ptr JSON pointer to the desired element - - @return reference to the element pointed to by @a ptr - - @complexity Constant. - - @throw std::out_of_range if the JSON pointer can not be resolved - @throw std::domain_error if an array index begins with '0' - @throw std::invalid_argument if an array index was not a number - - @liveexample{The behavior is shown in the example.,at_json_pointer} - - @since version 2.0.0 - */ - reference at(const json_pointer& ptr) - { - return ptr.get_checked(this); - } - - /*! - @brief access specified element via JSON Pointer - - Returns a const reference to the element at with specified JSON pointer - @a ptr, with bounds checking. - - @param[in] ptr JSON pointer to the desired element - - @return reference to the element pointed to by @a ptr - - @complexity Constant. - - @throw std::out_of_range if the JSON pointer can not be resolved - @throw std::domain_error if an array index begins with '0' - @throw std::invalid_argument if an array index was not a number - - @liveexample{The behavior is shown in the example.,at_json_pointer_const} - - @since version 2.0.0 - */ - const_reference at(const json_pointer& ptr) const - { - return ptr.get_checked(this); - } - /*! @brief access specified object element with default value @@ -4145,8 +4030,8 @@ class basic_json @throw std::domain_error when called on a type other than JSON array; example: `"cannot use erase() with null"` - @throw std::out_of_range when `idx >= size()`; example: `"index out of - range"` + @throw std::out_of_range when `idx >= size()`; example: `"array index 17 + is out of range"` @complexity Linear in distance between @a idx and the end of the container. @@ -4167,7 +4052,7 @@ class basic_json { if (idx >= size()) { - throw std::out_of_range("index out of range"); + throw std::out_of_range("array index " + std::to_string(idx) + " is out of range"); } assert(m_value.array != nullptr); @@ -8279,9 +8164,17 @@ class basic_json : reference_tokens(split(s)) {} + /// test for inequality + bool operator!=(const json_pointer& rhs) const + { + return reference_tokens != rhs.reference_tokens; + } + + private: + /// remove and return last reference pointer std::string pop_back() { - if (reference_tokens.empty()) + if (is_root()) { throw std::domain_error("JSON pointer has no parent"); } @@ -8291,7 +8184,24 @@ class basic_json return last; } - private: + /// return whether pointer points to the root document + bool is_root() const + { + return reference_tokens.empty(); + } + + json_pointer top() const + { + if (is_root()) + { + throw std::domain_error("JSON pointer has no parent"); + } + + json_pointer result = *this; + result.reference_tokens = {reference_tokens[0]}; + return result; + } + /*! @brief create and return a reference to the pointed to value */ @@ -8330,7 +8240,7 @@ class basic_json case value_t::array: { // create an entry in the array - result = &result->operator[](static_cast(std::stoi(reference_token))); + result = &result->operator[](static_cast(std::stoi(reference_token))); break; } @@ -8393,7 +8303,7 @@ class basic_json else { // convert array index to number; unchecked access - ptr = &ptr->operator[](static_cast(std::stoi(reference_token))); + ptr = &ptr->operator[](static_cast(std::stoi(reference_token))); } break; } @@ -8438,7 +8348,7 @@ class basic_json } // note: at performs range check - ptr = &ptr->at(static_cast(std::stoi(reference_token))); + ptr = &ptr->at(static_cast(std::stoi(reference_token))); break; } @@ -8490,7 +8400,7 @@ class basic_json } // use unchecked array access - ptr = &ptr->operator[](static_cast(std::stoi(reference_token))); + ptr = &ptr->operator[](static_cast(std::stoi(reference_token))); break; } @@ -8534,7 +8444,7 @@ class basic_json } // note: at performs range check - ptr = &ptr->at(static_cast(std::stoi(reference_token))); + ptr = &ptr->at(static_cast(std::stoi(reference_token))); break; } @@ -8601,12 +8511,8 @@ class basic_json } } - // first transform any occurrence of the sequence '~1' to '/' - replace_substring(reference_token, "~1", "/"); - // then transform any occurrence of the sequence '~0' to '~' - replace_substring(reference_token, "~0", "~"); - // finally, store the reference token + unescape(reference_token); result.push_back(reference_token); } @@ -8642,6 +8548,24 @@ class basic_json ); } + /// escape tilde and slash + static std::string escape(std::string s) + { + // escape "~"" to "~0" and "/" to "~1" + replace_substring(s, "~", "~0"); + replace_substring(s, "/", "~1"); + return s; + } + + /// unescape tilde and slash + static void unescape(std::string& s) + { + // first transform any occurrence of the sequence '~1' to '/' + replace_substring(s, "~1", "/"); + // then transform any occurrence of the sequence '~0' to '~' + replace_substring(s, "~0", "~"); + } + /*! @param[in] reference_string the reference string to the current value @param[in] value the value to consider @@ -8649,7 +8573,7 @@ class basic_json @note Empty objects or arrays are flattened to `null`. */ - static void flatten(const std::string reference_string, + static void flatten(const std::string& reference_string, const basic_json& value, basic_json& result) { @@ -8686,12 +8610,7 @@ class basic_json // iterate object and use keys as reference string for (const auto& element : *value.m_value.object) { - // escape "~"" to "~0" and "/" to "~1" - std::string key(element.first); - replace_substring(key, "~", "~0"); - replace_substring(key, "/", "~1"); - - flatten(reference_string + "/" + key, + flatten(reference_string + "/" + escape(element.first), element.second, result); } } @@ -8745,13 +8664,128 @@ class basic_json std::vector reference_tokens {}; }; - //////////////////////////// - // JSON Pointer functions // - //////////////////////////// + ////////////////////////// + // JSON Pointer support // + ////////////////////////// /// @name JSON Pointer functions /// @{ + /*! + @brief access specified element via JSON Pointer + + Uses a JSON pointer to retrieve a reference to the respective JSON value. + No bound checking is performed. Similar to + @ref operator[](const typename object_t::key_type&), `null` values + are created in arrays and objects if necessary. + + In particular: + - If the JSON pointer points to an object key that does not exist, it + is created an filled with a `null` value before a reference to it + is returned. + - If the JSON pointer points to an array index that does not exist, it + is created an filled with a `null` value before a reference to it + is returned. All indices between the current maximum and the given + index are also filled with `null`. + - The special value `-` is treated as a synonym for the index past the + end. + + @param[in] ptr a JSON pointer + + @return reference to the element pointed to by @a ptr + + @complexity Constant. + + @throw std::out_of_range if the JSON pointer can not be resolved + @throw std::domain_error if an array index begins with '0' + @throw std::invalid_argument if an array index was not a number + + @liveexample{The behavior is shown in the example.,operatorjson_pointer} + + @since version 2.0.0 + */ + reference operator[](const json_pointer& ptr) + { + return ptr.get_unchecked(this); + } + + /*! + @brief access specified element via JSON Pointer + + Uses a JSON pointer to retrieve a reference to the respective JSON value. + No bound checking is performed. The function does not change the JSON + value; no `null` values are created. In particular, the the special value + `-` yields an exception. + + @param[in] ptr JSON pointer to the desired element + + @return const reference to the element pointed to by @a ptr + + @complexity Constant. + + @throw std::out_of_range if the JSON pointer can not be resolved + @throw std::domain_error if an array index begins with '0' + @throw std::invalid_argument if an array index was not a number + + @liveexample{The behavior is shown in the example.,operatorjson_pointer_const} + + @since version 2.0.0 + */ + const_reference operator[](const json_pointer& ptr) const + { + return ptr.get_unchecked(this); + } + + /*! + @brief access specified element via JSON Pointer + + Returns a reference to the element at with specified JSON pointer @a ptr, + with bounds checking. + + @param[in] ptr JSON pointer to the desired element + + @return reference to the element pointed to by @a ptr + + @complexity Constant. + + @throw std::out_of_range if the JSON pointer can not be resolved + @throw std::domain_error if an array index begins with '0' + @throw std::invalid_argument if an array index was not a number + + @liveexample{The behavior is shown in the example.,at_json_pointer} + + @since version 2.0.0 + */ + reference at(const json_pointer& ptr) + { + return ptr.get_checked(this); + } + + /*! + @brief access specified element via JSON Pointer + + Returns a const reference to the element at with specified JSON pointer + @a ptr, with bounds checking. + + @param[in] ptr JSON pointer to the desired element + + @return reference to the element pointed to by @a ptr + + @complexity Constant. + + @throw std::out_of_range if the JSON pointer can not be resolved + @throw std::domain_error if an array index begins with '0' + @throw std::invalid_argument if an array index was not a number + + @liveexample{The behavior is shown in the example.,at_json_pointer_const} + + @since version 2.0.0 + */ + const_reference at(const json_pointer& ptr) const + { + return ptr.get_checked(this); + } + /*! @brief return flattened JSON value @@ -8815,45 +8849,146 @@ class basic_json /// @} + ////////////////////////// + // JSON Patch functions // + ////////////////////////// + + /// @name JSON Patch functions + /// @{ + /*! @brief applies a JSON patch + [JSON Patch](http://jsonpatch.com) defines a JSON document structure for + expressing a sequence of operations to apply to a JSON) document. With + this funcion, a JSON Patch is applied to the current JSON value by + executing all operations from the patch. + @param[in] patch JSON patch document @return patched document - @note The original JSON value is not changed; that is, the patch is - applied to a copy of the value. + @note The application of a patch is atomic: Either all operations succeed + and the patched document is returned or an exception is thrown. In + any case, the original value is not changed: the patch is applied + to a copy of the value. - @sa [RFC 6902](https://tools.ietf.org/html/rfc6902) + @throw std::out_of_range if a JSON pointer inside the patch could not + be resolved successfully in the current JSON value; example: `"key baz + not found"` + @throw invalid_argument if the JSON patch is malformed (e.g., mandatory + attributes are missing); example: `"operation add must have member path"` + + @complexity Linear in the size of the JSON value and the length of the + JSON patch. As usually only a fraction of the JSON value is affected by + the patch, the complexity can usually be neglected. + + @liveexample{The following code shows how a JSON patch is applied to a + value.,patch} + + @sa @ref diff -- create a JSON patch by comparing two JSON values + + @sa [RFC 6902 (JSON Patch)](https://tools.ietf.org/html/rfc6902) + @sa [RFC 6901 (JSON Pointer)](https://tools.ietf.org/html/rfc6901) + + @since version 2.0.0 */ - basic_json apply_patch(const basic_json& patch) const + basic_json patch(const basic_json& patch) const { // make a working copy to apply the patch to basic_json result = *this; + // the valid JSON Patch operations + enum class patch_operations {add, remove, replace, move, copy, test, invalid}; + + const auto get_op = [](const std::string op) + { + if (op == "add") + { + return patch_operations::add; + } + if (op == "remove") + { + return patch_operations::remove; + } + if (op == "replace") + { + return patch_operations::replace; + } + if (op == "move") + { + return patch_operations::move; + } + if (op == "copy") + { + return patch_operations::copy; + } + if (op == "test") + { + return patch_operations::test; + } + + return patch_operations::invalid; + }; + // wrapper for "add" operation; add value at ptr const auto operation_add = [&result](json_pointer & ptr, basic_json val) { - // get reference to parent of JSON pointer ptr - const auto last_path = ptr.pop_back(); - basic_json& parent = result.at(ptr); - - if (parent.is_object()) + // adding to the root of the target document means replacing it + if (ptr.is_root()) { - // use operator[] to add value - parent[last_path] = val; + result = val; } - else if (parent.is_array()) + else { - if (last_path == "-") + // make sure the top element of the pointer exists + json_pointer top_pointer = ptr.top(); + if (top_pointer != ptr) { - // special case: append to back - parent.push_back(val); + basic_json& x = result.at(top_pointer); } - else + + // get reference to parent of JSON pointer ptr + const auto last_path = ptr.pop_back(); + basic_json& parent = result[ptr]; + + switch (parent.m_type) { - // default case: insert add offset - parent.insert(parent.begin() + std::stoi(last_path), val); + case value_t::null: + case value_t::object: + { + // use operator[] to add value + parent[last_path] = val; + break; + } + + case value_t::array: + { + if (last_path == "-") + { + // special case: append to back + parent.push_back(val); + } + else + { + const auto idx = std::stoi(last_path); + if (static_cast(idx) > parent.size()) + { + // avoid undefined behavior + throw std::out_of_range("array index " + std::to_string(idx) + " is out of range"); + } + else + { + // default case: insert add offset + parent.insert(parent.begin() + static_cast(idx), val); + } + } + break; + } + + default: + { + throw std::domain_error("unexpected parent type " + parent.type_name()); + } } } }; @@ -8868,11 +9003,21 @@ class basic_json // remove child if (parent.is_object()) { - parent.erase(parent.find(last_path)); + // perform range check + auto it = parent.find(last_path); + if (it != parent.end()) + { + parent.erase(it); + } + else + { + throw std::out_of_range("key '" + last_path + "' not found"); + } } else if (parent.is_array()) { - parent.erase(parent.begin() + std::stoi(last_path)); + // note erase performs range check + parent.erase(static_cast(std::stoi(last_path))); } }; @@ -8880,7 +9025,7 @@ class basic_json if (not patch.is_array()) { // a JSON patch must be an array of objects - throw std::domain_error("JSON patch must be an array of objects"); + throw std::invalid_argument("JSON patch must be an array of objects"); } // iterate and apply th eoperations @@ -8900,13 +9045,13 @@ class basic_json // check if desired value is present if (it == val.m_value.object->end()) { - throw std::domain_error(error_msg + " must have member '" + member + "'"); + throw std::invalid_argument(error_msg + " must have member '" + member + "'"); } // check if result is of type string if (string_type and not it->second.is_string()) { - throw std::domain_error(error_msg + " must have string member '" + member + "'"); + throw std::invalid_argument(error_msg + " must have string member '" + member + "'"); } // no error: return value @@ -8916,7 +9061,7 @@ class basic_json // type check if (not val.is_object()) { - throw std::domain_error("JSON patch must be an array of objects"); + throw std::invalid_argument("JSON patch must be an array of objects"); } // collect mandatory members @@ -8924,51 +9069,251 @@ class basic_json const std::string path = get_value(op, "path", true); json_pointer ptr(path); - if (op == "add") + switch (get_op(op)) { - operation_add(ptr, get_value("add", "value", false)); - } - else if (op == "remove") - { - operation_remove(ptr); - } - else if (op == "replace") - { - result.at(ptr) = get_value("replace", "value", false); - } - else if (op == "move") - { - const std::string from_path = get_value("move", "from", true); - json_pointer from_ptr(from_path); - basic_json v = result[from_ptr]; - - operation_remove(from_ptr); - operation_add(ptr, v); - } - else if (op == "copy") - { - const std::string from_path = get_value("copy", "from", true);; - const json_pointer from_ptr(from_path); - - result[ptr] = result.at(from_ptr); - } - else if (op == "test") - { - if (result.at(ptr) != get_value("test", "value", false)) + case patch_operations::add: { - throw std::domain_error("unsuccessful: " + val.dump()); + operation_add(ptr, get_value("add", "value", false)); + break; + } + + case patch_operations::remove: + { + operation_remove(ptr); + break; + } + + case patch_operations::replace: + { + // the "path" location must exist - use at() + result.at(ptr) = get_value("replace", "value", false); + break; + } + + case patch_operations::move: + { + const std::string from_path = get_value("move", "from", true); + json_pointer from_ptr(from_path); + + // the "from" location must exist - use at() + basic_json v = result.at(from_ptr); + + // The move operation is functionally identical to a + // "remove" operation on the "from" location, followed + // immediately by an "add" operation at the target + // location with the value that was just removed. + operation_remove(from_ptr); + operation_add(ptr, v); + break; + } + + case patch_operations::copy: + { + const std::string from_path = get_value("copy", "from", true);; + const json_pointer from_ptr(from_path); + + // the "from" location must exist - use at() + result[ptr] = result.at(from_ptr); + break; + } + + case patch_operations::test: + { + bool success = false; + try + { + // check if "value" matches the one at "path" + // the "path" location must exist - use at() + success = (result.at(ptr) == get_value("test", "value", false)); + } + catch (std::out_of_range&) + { + // ignore out of range errors: success remains false + } + + // throw an exception if test fails + if (not success) + { + throw std::domain_error("unsuccessful: " + val.dump()); + } + + break; + } + + case patch_operations::invalid: + { + // op must be "add", "remove", "replace", "move", "copy", or + // "test" + throw std::invalid_argument("operation value '" + op + "' is invalid"); } - } - else - { - // op must be "add", "remove", "replace", "move", "copy", or - // "test" - throw std::domain_error("operation value '" + op + "' is invalid"); } } return result; } + + /*! + @brief creates a diff as a JSON patch + + Creates a [JSON Patch](http://jsonpatch.com) so that value @a source can + be changed into the value @a target by calling @ref patch function. + + @invariant For two JSON values @a source and @a target, the following code + yields always `true`: + @code {.cpp} + source.patch(diff(source, target)) == target; + @endcode + + @note Currently, only `remove`, `add`, and `replace` operations are + generated. + + @param[in] source JSON value to copare from + @param[in] target JSON value to copare against + @param[in] path helper value to create JSON pointers + + @return a JSON patch to convert the @a source to @a target + + @complexity Linear in the lengths of @a source and @a target. + + @liveexample{The following code shows how a JSON patch is created as a + diff for two JSON values.,diff} + + @sa @ref patch -- apply a JSON patch + + @sa [RFC 6902 (JSON Patch)](https://tools.ietf.org/html/rfc6902) + + @since version 2.0.0 + */ + static basic_json diff(const basic_json& source, + const basic_json& target, + std::string path = "") noexcept + { + // the patch + basic_json result(value_t::array); + + // if the values are the same, return empty patch + if (source == target) + { + return result; + } + + if (source.type() != target.type()) + { + // different types: replace value + result.push_back( + { + {"op", "replace"}, + {"path", path}, + {"value", target} + }); + } + else + { + switch (source.type()) + { + case value_t::array: + { + // first pass: traverse common elements + size_t i = 0; + while (i < source.size() and i < target.size()) + { + // recursive call to compare array values at index i + auto temp_diff = diff(source[i], target[i], path + "/" + std::to_string(i)); + result.insert(result.end(), temp_diff.begin(), temp_diff.end()); + ++i; + } + + // i now reached the end of at least one array + // in a second pass, traverse the remaining elements + + // remove my remaining elements + while (i < source.size()) + { + result.push_back(object( + { + {"op", "remove"}, + {"path", path + "/" + std::to_string(i)} + })); + ++i; + } + + // add other remaining elements + while (i < target.size()) + { + result.push_back( + { + {"op", "add"}, + {"path", path + "/" + std::to_string(i)}, + {"value", target[i]} + }); + ++i; + } + + break; + } + + case value_t::object: + { + // first pass: traverse this object's elements + for (auto it = source.begin(); it != source.end(); ++it) + { + // escape the key name to be used in a JSON patch + const auto key = json_pointer::escape(it.key()); + + if (target.find(it.key()) != target.end()) + { + // recursive call to compare object values at key it + auto temp_diff = diff(it.value(), target[it.key()], path + "/" + key); + result.insert(result.end(), temp_diff.begin(), temp_diff.end()); + } + else + { + // found a key that is not in o -> remove it + result.push_back(object( + { + {"op", "remove"}, + {"path", path + "/" + key} + })); + } + } + + // second pass: traverse other object's elements + for (auto it = target.begin(); it != target.end(); ++it) + { + if (source.find(it.key()) == source.end()) + { + // found a key that is not in this -> add it + const auto key = json_pointer::escape(it.key()); + result.push_back( + { + {"op", "add"}, + {"path", path + "/" + key}, + {"value", it.value()} + }); + } + } + + break; + } + + default: + { + // both primitive type: replace value + result.push_back( + { + {"op", "replace"}, + {"path", path}, + {"value", target} + }); + break; + } + } + } + + return result; + } + + /// @} }; @@ -8988,9 +9333,9 @@ using json = basic_json<>; } -///////////////////////// -// nonmember functions // -///////////////////////// +/////////////////////// +// nonmember support // +/////////////////////// // specialization of std::swap, and std::hash namespace std diff --git a/test/unit.cpp b/test/unit.cpp index 664648cb..f6c49883 100644 --- a/test/unit.cpp +++ b/test/unit.cpp @@ -3455,7 +3455,7 @@ TEST_CASE("element access") { json jarray = {1, 1u, true, nullptr, "string", 42.23, json::object(), {1, 2, 3}}; CHECK_THROWS_AS(jarray.erase(8), std::out_of_range); - CHECK_THROWS_WITH(jarray.erase(8), "index out of range"); + CHECK_THROWS_WITH(jarray.erase(8), "array index 8 is out of range"); } } @@ -12395,304 +12395,384 @@ TEST_CASE("JSON patch") { SECTION("examples from RFC 6902") { - SECTION("example A.1 - Adding an Object Member") + SECTION("4. Operations") { - // An example target JSON document: - json doc = R"( - { "foo": "bar"} - )"_json; + // 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; - // 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.apply_patch(patch) == expected); + // check if the operation objects are equivalent + CHECK(op1 == op2); + CHECK(op1 == op3); } - SECTION("example A.2 - Adding an Array Element") + SECTION("4.1 add") { - // An example target JSON document: - json doc = R"( - { "foo": [ "bar", "baz" ] } - )"_json; + json patch = R"([{ "op": "add", "path": "/a/b/c", "value": [ "foo", "bar" ] }])"_json; - // A JSON Patch document: - json patch = R"( - [ - { "op": "add", "path": "/foo/1", "value": "qux" } - ] - )"_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; - // The resulting JSON document: - json expected = R"( - { "foo": [ "bar", "qux", "baz" ] } - )"_json; - - // check if patched value is as expected - CHECK(doc.apply_patch(patch) == expected); - } - - SECTION("example A.3 - Removing an Object Member") - { - // An example target JSON document: - json doc = R"( + // is not an error, because "a" exists, and "b" will be added to + // its value. + CHECK_NOTHROW(doc1.patch(patch)); + CHECK(doc1.patch(patch) == 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.apply_patch(patch) == expected); - } - - SECTION("example 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.apply_patch(patch) == expected); - } - - SECTION("example 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.apply_patch(patch) == expected); - } - - SECTION("example A.6 - Moving a Value") - { - // An example target JSON document: - json doc = R"( - { - "foo": { - "bar": "baz", - "waldo": "fred" - }, - "qux": { - "corge": "grault" + "a": { + "foo": 1, + "b": { + "c": [ "foo", "bar" ] + } } } - )"_json; + )"_json); + + // 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), std::out_of_range); + CHECK_THROWS_WITH(doc2.patch(patch), "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": "move", "from": "/foo/waldo", "path": "/qux/thud" } - ] - )"_json; + [ + { "op": "add", "path": "/baz", "value": "qux" } + ] + )"_json; // The resulting JSON document: json expected = R"( - { - "foo": { - "bar": "baz" - }, - "qux": { - "corge": "grault", - "thud": "fred" + { + "baz": "qux", + "foo": "bar" } - } - )"_json; + )"_json; // check if patched value is as expected - CHECK(doc.apply_patch(patch) == expected); + CHECK(doc.patch(patch) == expected); + + // check roundtrip + CHECK(doc.patch(json::diff(doc, expected)) == expected); } - SECTION("example A.7 - Moving a Value") + SECTION("A.2. Adding an Array Element") { // An example target JSON document: json doc = R"( - { "foo": [ "all", "grass", "cows", "eat" ] } - )"_json; + { "foo": [ "bar", "baz" ] } + )"_json; // A JSON Patch document: json patch = R"( - [ - { "op": "move", "from": "/foo/1", "path": "/foo/3" } - ] - )"_json; + [ + { "op": "add", "path": "/foo/1", "value": "qux" } + ] + )"_json; // The resulting JSON document: json expected = R"( - { "foo": [ "all", "cows", "eat", "grass" ] } - )"_json; + { "foo": [ "bar", "qux", "baz" ] } + )"_json; // check if patched value is as expected - CHECK(doc.apply_patch(patch) == expected); + CHECK(doc.patch(patch) == expected); + + // check roundtrip + CHECK(doc.patch(json::diff(doc, expected)) == expected); } - SECTION("example A.8 - Testing a Value: Success") + SECTION("A.3. Removing an Object Member") { // An example target JSON document: json doc = R"( - { - "baz": "qux", - "foo": [ "a", 2, "c" ] - } - )"_json; + { + "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; + [ + { "op": "test", "path": "/baz", "value": "qux" }, + { "op": "test", "path": "/foo/1", "value": 2 } + ] + )"_json; // check if evaluation does not throw - CHECK_NOTHROW(doc.apply_patch(patch)); + CHECK_NOTHROW(doc.patch(patch)); // check if patched document is unchanged - CHECK(doc.apply_patch(patch) == doc); + CHECK(doc.patch(patch) == doc); } - SECTION("example A.9 - Testing a Value: Error") + SECTION("A.9. Testing a Value: Error") { // An example target JSON document: json doc = R"( - { "baz": "qux" } - )"_json; + { "baz": "qux" } + )"_json; // A JSON Patch document that will result in an error condition: json patch = R"( - [ - { "op": "test", "path": "/baz", "value": "bar" } - ] - )"_json; + [ + { "op": "test", "path": "/baz", "value": "bar" } + ] + )"_json; // check that evaluation throws - CHECK_THROWS_AS(doc.apply_patch(patch), std::domain_error); - CHECK_THROWS_WITH(doc.apply_patch(patch), "unsuccessful: " + patch[0].dump()); + CHECK_THROWS_AS(doc.patch(patch), std::domain_error); + CHECK_THROWS_WITH(doc.patch(patch), "unsuccessful: " + patch[0].dump()); } - SECTION("example A.10 - Adding a Nested Member Object") + SECTION("A.10. Adding a Nested Member Object") { // An example target JSON document: json doc = R"( - { "foo": "bar" } - )"_json; + { "foo": "bar" } + )"_json; // A JSON Patch document: json patch = R"( - [ - { "op": "add", "path": "/child", "value": { "grandchild": { } } } - ] - )"_json; + [ + { "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.apply_patch(patch) == expected); - } - - SECTION("example 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; + "child": { + "grandchild": { + } + } + } + )"_json; // check if patched value is as expected - CHECK(doc.apply_patch(patch) == expected); + CHECK(doc.patch(patch) == expected); + + // check roundtrip + CHECK(doc.patch(json::diff(doc, expected)) == expected); } - SECTION("example A.12 - Adding to a Nonexistent Target") + SECTION("A.11. Ignoring Unrecognized Elements") { // An example target JSON document: json doc = R"( - { "foo": "bar" } - )"_json; + { "foo": "bar" } + )"_json; // A JSON Patch document: json patch = R"( - [ - { "op": "add", "path": "/baz/bat", "value": "qux" } - ] - )"_json; + [ + { "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 @@ -12700,89 +12780,150 @@ TEST_CASE("JSON patch") // 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.apply_patch(patch), std::out_of_range); - CHECK_THROWS_WITH(doc.apply_patch(patch), "key 'baz' not found"); + CHECK_THROWS_AS(doc.patch(patch), std::out_of_range); + CHECK_THROWS_WITH(doc.patch(patch), "key 'baz' not found"); } - // A.13. Invalid JSON Patch Document + // A.13. Invalid JSON Patch Document // not applicable - SECTION("example A.14 - Escape Ordering") + SECTION("A.14. Escape Ordering") { // An example target JSON document: json doc = R"( - { - "/": 9, - "~1": 10 - } - )"_json; + { + "/": 9, + "~1": 10 + } + )"_json; // A JSON Patch document: json patch = R"( - [ - {"op": "test", "path": "/~01", "value": 10} - ] - )"_json; + [ + {"op": "test", "path": "/~01", "value": 10} + ] + )"_json; json expected = R"( - { - "/": 9, - "~1": 10 - } - )"_json; + { + "/": 9, + "~1": 10 + } + )"_json; // check if patched value is as expected - CHECK(doc.apply_patch(patch) == expected); + CHECK(doc.patch(patch) == expected); + + // check roundtrip + CHECK(doc.patch(json::diff(doc, expected)) == expected); } - SECTION("example A.15 - Comparing Strings and Numbers") + SECTION("A.15. Comparing Strings and Numbers") { // An example target JSON document: json doc = R"( - { - "/": 9, - "~1": 10 - } - )"_json; + { + "/": 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; + [ + {"op": "test", "path": "/~01", "value": "10"} + ] + )"_json; // check that evaluation throws - CHECK_THROWS_AS(doc.apply_patch(patch), std::domain_error); - CHECK_THROWS_WITH(doc.apply_patch(patch), "unsuccessful: " + patch[0].dump()); + CHECK_THROWS_AS(doc.patch(patch), std::domain_error); + CHECK_THROWS_WITH(doc.patch(patch), "unsuccessful: " + patch[0].dump()); } - SECTION("example A.16 - Adding an Array Value") + SECTION("A.16. Adding an Array Value") { // An example target JSON document: json doc = R"( - { "foo": ["bar"] } - )"_json; + { "foo": ["bar"] } + )"_json; // A JSON Patch document: json patch = R"( - [ - { "op": "add", "path": "/foo/-", "value": ["abc", "def"] } - ] - )"_json; + [ + { "op": "add", "path": "/foo/-", "value": ["abc", "def"] } + ] + )"_json; // The resulting JSON document: json expected = R"( - { "foo": ["bar", ["abc", "def"]] } - )"_json; + { "foo": ["bar", ["abc", "def"]] } + )"_json; // check if patched value is as expected - CHECK(doc.apply_patch(patch) == 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: @@ -12805,6 +12946,7 @@ TEST_CASE("JSON patch") ] )"_json; + // The resulting JSON document: json expected = R"( { "foo": { @@ -12819,7 +12961,60 @@ TEST_CASE("JSON patch") )"_json; // check if patched value is as expected - CHECK(doc.apply_patch(patch) == 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); + } } } @@ -12827,20 +13022,44 @@ TEST_CASE("JSON patch") { SECTION("unknown operation") { + SECTION("not an array") + { + json j; + json patch = {{"op", "add"}, {"path", ""}, {"value", 1}}; + CHECK_THROWS_AS(j.patch(patch), std::invalid_argument); + CHECK_THROWS_WITH(j.patch(patch), "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), std::invalid_argument); + CHECK_THROWS_WITH(j.patch(patch), "JSON patch must be an array of objects"); + } + SECTION("missing 'op'") { json j; json patch = {{{"foo", "bar"}}}; - CHECK_THROWS_AS(j.apply_patch(patch), std::domain_error); - CHECK_THROWS_WITH(j.apply_patch(patch), "operation must have member 'op'"); + CHECK_THROWS_AS(j.patch(patch), std::invalid_argument); + CHECK_THROWS_WITH(j.patch(patch), "operation must have member 'op'"); } SECTION("non-string 'op'") { json j; json patch = {{{"op", 1}}}; - CHECK_THROWS_AS(j.apply_patch(patch), std::domain_error); - CHECK_THROWS_WITH(j.apply_patch(patch), "operation must have string member 'op'"); + CHECK_THROWS_AS(j.patch(patch), std::invalid_argument); + CHECK_THROWS_WITH(j.patch(patch), "operation must have string member 'op'"); + } + + SECTION("invalid operation") + { + json j; + json patch = {{{"op", "foo"}, {"path", ""}}}; + CHECK_THROWS_AS(j.patch(patch), std::invalid_argument); + CHECK_THROWS_WITH(j.patch(patch), "operation value 'foo' is invalid"); } } @@ -12850,24 +13069,32 @@ TEST_CASE("JSON patch") { json j; json patch = {{{"op", "add"}}}; - CHECK_THROWS_AS(j.apply_patch(patch), std::domain_error); - CHECK_THROWS_WITH(j.apply_patch(patch), "operation 'add' must have member 'path'"); + CHECK_THROWS_AS(j.patch(patch), std::invalid_argument); + CHECK_THROWS_WITH(j.patch(patch), "operation 'add' must have member 'path'"); } SECTION("non-string 'path'") { json j; json patch = {{{"op", "add"}, {"path", 1}}}; - CHECK_THROWS_AS(j.apply_patch(patch), std::domain_error); - CHECK_THROWS_WITH(j.apply_patch(patch), "operation 'add' must have string member 'path'"); + CHECK_THROWS_AS(j.patch(patch), std::invalid_argument); + CHECK_THROWS_WITH(j.patch(patch), "operation 'add' must have string member 'path'"); } SECTION("missing 'value'") { json j; json patch = {{{"op", "add"}, {"path", ""}}}; - CHECK_THROWS_AS(j.apply_patch(patch), std::domain_error); - CHECK_THROWS_WITH(j.apply_patch(patch), "operation 'add' must have member 'value'"); + CHECK_THROWS_AS(j.patch(patch), std::invalid_argument); + CHECK_THROWS_WITH(j.patch(patch), "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), std::out_of_range); + CHECK_THROWS_WITH(j.patch(patch), "array index 4 is out of range"); } } @@ -12877,16 +13104,40 @@ TEST_CASE("JSON patch") { json j; json patch = {{{"op", "remove"}}}; - CHECK_THROWS_AS(j.apply_patch(patch), std::domain_error); - CHECK_THROWS_WITH(j.apply_patch(patch), "operation 'remove' must have member 'path'"); + CHECK_THROWS_AS(j.patch(patch), std::invalid_argument); + CHECK_THROWS_WITH(j.patch(patch), "operation 'remove' must have member 'path'"); } SECTION("non-string 'path'") { json j; json patch = {{{"op", "remove"}, {"path", 1}}}; - CHECK_THROWS_AS(j.apply_patch(patch), std::domain_error); - CHECK_THROWS_WITH(j.apply_patch(patch), "operation 'remove' must have string member 'path'"); + CHECK_THROWS_AS(j.patch(patch), std::invalid_argument); + CHECK_THROWS_WITH(j.patch(patch), "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), std::out_of_range); + CHECK_THROWS_WITH(j.patch(patch), "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), std::out_of_range); + CHECK_THROWS_WITH(j.patch(patch), "key 'baz' not found"); + } + + SECTION("root element as target location") + { + json j = "string"; + json patch = {{{"op", "remove"}, {"path", ""}}}; + CHECK_THROWS_AS(j.patch(patch), std::domain_error); + CHECK_THROWS_WITH(j.patch(patch), "JSON pointer has no parent"); } } @@ -12896,24 +13147,40 @@ TEST_CASE("JSON patch") { json j; json patch = {{{"op", "replace"}}}; - CHECK_THROWS_AS(j.apply_patch(patch), std::domain_error); - CHECK_THROWS_WITH(j.apply_patch(patch), "operation 'replace' must have member 'path'"); + CHECK_THROWS_AS(j.patch(patch), std::invalid_argument); + CHECK_THROWS_WITH(j.patch(patch), "operation 'replace' must have member 'path'"); } SECTION("non-string 'path'") { json j; json patch = {{{"op", "replace"}, {"path", 1}}}; - CHECK_THROWS_AS(j.apply_patch(patch), std::domain_error); - CHECK_THROWS_WITH(j.apply_patch(patch), "operation 'replace' must have string member 'path'"); + CHECK_THROWS_AS(j.patch(patch), std::invalid_argument); + CHECK_THROWS_WITH(j.patch(patch), "operation 'replace' must have string member 'path'"); } SECTION("missing 'value'") { json j; json patch = {{{"op", "replace"}, {"path", ""}}}; - CHECK_THROWS_AS(j.apply_patch(patch), std::domain_error); - CHECK_THROWS_WITH(j.apply_patch(patch), "operation 'replace' must have member 'value'"); + CHECK_THROWS_AS(j.patch(patch), std::invalid_argument); + CHECK_THROWS_WITH(j.patch(patch), "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), std::out_of_range); + CHECK_THROWS_WITH(j.patch(patch), "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), std::out_of_range); + CHECK_THROWS_WITH(j.patch(patch), "key 'baz' not found"); } } @@ -12923,32 +13190,48 @@ TEST_CASE("JSON patch") { json j; json patch = {{{"op", "move"}}}; - CHECK_THROWS_AS(j.apply_patch(patch), std::domain_error); - CHECK_THROWS_WITH(j.apply_patch(patch), "operation 'move' must have member 'path'"); + CHECK_THROWS_AS(j.patch(patch), std::invalid_argument); + CHECK_THROWS_WITH(j.patch(patch), "operation 'move' must have member 'path'"); } SECTION("non-string 'path'") { json j; json patch = {{{"op", "move"}, {"path", 1}}}; - CHECK_THROWS_AS(j.apply_patch(patch), std::domain_error); - CHECK_THROWS_WITH(j.apply_patch(patch), "operation 'move' must have string member 'path'"); + CHECK_THROWS_AS(j.patch(patch), std::invalid_argument); + CHECK_THROWS_WITH(j.patch(patch), "operation 'move' must have string member 'path'"); } SECTION("missing 'from'") { json j; json patch = {{{"op", "move"}, {"path", ""}}}; - CHECK_THROWS_AS(j.apply_patch(patch), std::domain_error); - CHECK_THROWS_WITH(j.apply_patch(patch), "operation 'move' must have member 'from'"); + CHECK_THROWS_AS(j.patch(patch), std::invalid_argument); + CHECK_THROWS_WITH(j.patch(patch), "operation 'move' must have member 'from'"); } SECTION("non-string 'from'") { json j; json patch = {{{"op", "move"}, {"path", ""}, {"from", 1}}}; - CHECK_THROWS_AS(j.apply_patch(patch), std::domain_error); - CHECK_THROWS_WITH(j.apply_patch(patch), "operation 'move' must have string member 'from'"); + CHECK_THROWS_AS(j.patch(patch), std::invalid_argument); + CHECK_THROWS_WITH(j.patch(patch), "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), std::out_of_range); + CHECK_THROWS_WITH(j.patch(patch), "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), std::out_of_range); + CHECK_THROWS_WITH(j.patch(patch), "key 'baz' not found"); } } @@ -12958,32 +13241,48 @@ TEST_CASE("JSON patch") { json j; json patch = {{{"op", "copy"}}}; - CHECK_THROWS_AS(j.apply_patch(patch), std::domain_error); - CHECK_THROWS_WITH(j.apply_patch(patch), "operation 'copy' must have member 'path'"); + CHECK_THROWS_AS(j.patch(patch), std::invalid_argument); + CHECK_THROWS_WITH(j.patch(patch), "operation 'copy' must have member 'path'"); } SECTION("non-string 'path'") { json j; json patch = {{{"op", "copy"}, {"path", 1}}}; - CHECK_THROWS_AS(j.apply_patch(patch), std::domain_error); - CHECK_THROWS_WITH(j.apply_patch(patch), "operation 'copy' must have string member 'path'"); + CHECK_THROWS_AS(j.patch(patch), std::invalid_argument); + CHECK_THROWS_WITH(j.patch(patch), "operation 'copy' must have string member 'path'"); } SECTION("missing 'from'") { json j; json patch = {{{"op", "copy"}, {"path", ""}}}; - CHECK_THROWS_AS(j.apply_patch(patch), std::domain_error); - CHECK_THROWS_WITH(j.apply_patch(patch), "operation 'copy' must have member 'from'"); + CHECK_THROWS_AS(j.patch(patch), std::invalid_argument); + CHECK_THROWS_WITH(j.patch(patch), "operation 'copy' must have member 'from'"); } SECTION("non-string 'from'") { json j; json patch = {{{"op", "copy"}, {"path", ""}, {"from", 1}}}; - CHECK_THROWS_AS(j.apply_patch(patch), std::domain_error); - CHECK_THROWS_WITH(j.apply_patch(patch), "operation 'copy' must have string member 'from'"); + CHECK_THROWS_AS(j.patch(patch), std::invalid_argument); + CHECK_THROWS_WITH(j.patch(patch), "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), std::out_of_range); + CHECK_THROWS_WITH(j.patch(patch), "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), std::out_of_range); + CHECK_THROWS_WITH(j.patch(patch), "key 'baz' not found"); } } @@ -12993,27 +13292,288 @@ TEST_CASE("JSON patch") { json j; json patch = {{{"op", "test"}}}; - CHECK_THROWS_AS(j.apply_patch(patch), std::domain_error); - CHECK_THROWS_WITH(j.apply_patch(patch), "operation 'test' must have member 'path'"); + CHECK_THROWS_AS(j.patch(patch), std::invalid_argument); + CHECK_THROWS_WITH(j.patch(patch), "operation 'test' must have member 'path'"); } SECTION("non-string 'path'") { json j; json patch = {{{"op", "test"}, {"path", 1}}}; - CHECK_THROWS_AS(j.apply_patch(patch), std::domain_error); - CHECK_THROWS_WITH(j.apply_patch(patch), "operation 'test' must have string member 'path'"); + CHECK_THROWS_AS(j.patch(patch), std::invalid_argument); + CHECK_THROWS_WITH(j.patch(patch), "operation 'test' must have string member 'path'"); } SECTION("missing 'value'") { json j; json patch = {{{"op", "test"}, {"path", ""}}}; - CHECK_THROWS_AS(j.apply_patch(patch), std::domain_error); - CHECK_THROWS_WITH(j.apply_patch(patch), "operation 'test' must have member 'value'"); + CHECK_THROWS_AS(j.patch(patch), std::invalid_argument); + CHECK_THROWS_WITH(j.patch(patch), "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), std::domain_error); + CHECK_THROWS_WITH(doc.patch(patch), "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)); + } + } } TEST_CASE("regression tests") From 1d3b4dd1583d1eeacf7df3c0f9ad2f356b378712 Mon Sep 17 00:00:00 2001 From: Niels Date: Sat, 30 Apr 2016 00:03:47 +0200 Subject: [PATCH 10/17] added test cases --- ChangeLog.md | 5 ++- doc/examples/json_pointer__to_string.cpp | 34 +++++++++++++++++ doc/examples/json_pointer__to_string.link | 1 + doc/examples/json_pointer__to_string.output | 12 ++++++ src/json.hpp | 37 +++++++++++++++++-- src/json.hpp.re2c | 37 +++++++++++++++++-- test/unit.cpp | 41 +++++++++++++++++++++ 7 files changed, 160 insertions(+), 7 deletions(-) create mode 100644 doc/examples/json_pointer__to_string.cpp create mode 100644 doc/examples/json_pointer__to_string.link create mode 100644 doc/examples/json_pointer__to_string.output diff --git a/ChangeLog.md b/ChangeLog.md index 0acd10f5..75827603 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -5,10 +5,12 @@ All notable changes to this project will be documented in this file. This projec [Full Changelog](https://github.com/nlohmann/json/compare/v1.1.0...HEAD) +- Additional integration options [\#237](https://github.com/nlohmann/json/issues/237) +- Can't use basic\_json::iterator as a base iterator for std::move\_iterator [\#233](https://github.com/nlohmann/json/issues/233) - Provide a FAQ [\#163](https://github.com/nlohmann/json/issues/163) - Create PULL\_REQUEST\_TEMPLATE.md [\#213](https://github.com/nlohmann/json/pull/213) ([whackashoe](https://github.com/whackashoe)) - fixed noexcept; added constexpr [\#208](https://github.com/nlohmann/json/pull/208) ([nlohmann](https://github.com/nlohmann)) -- Add support for afl-fuzz testing [\#207](https://github.com/nlohmann/json/pull/207) ([msm-](https://github.com/msm-)) +- Add support for afl-fuzz testing [\#207](https://github.com/nlohmann/json/pull/207) ([mykter](https://github.com/mykter)) - Issue \#178 - Extending support to full uint64\_t/int64\_t range and unsigned type \(updated\) [\#193](https://github.com/nlohmann/json/pull/193) ([twelsby](https://github.com/twelsby)) - double values are serialized with commas as decimal points [\#228](https://github.com/nlohmann/json/issues/228) @@ -26,6 +28,7 @@ All notable changes to this project will be documented in this file. This projec - Conflicting typedef of ssize\_t on Windows 32 bit when using Boost.Python [\#204](https://github.com/nlohmann/json/issues/204) - Integer conversion to unsigned [\#178](https://github.com/nlohmann/json/issues/178) +- Implement additional integration options [\#238](https://github.com/nlohmann/json/pull/238) ([robertmrk](https://github.com/robertmrk)) - make serialization locale-independent [\#232](https://github.com/nlohmann/json/pull/232) ([nlohmann](https://github.com/nlohmann)) - fixes \#223 by updating README.md [\#227](https://github.com/nlohmann/json/pull/227) ([kevin--](https://github.com/kevin--)) - Use namespace std for int64\_t and uint64\_t [\#226](https://github.com/nlohmann/json/pull/226) ([lv-zheng](https://github.com/lv-zheng)) diff --git a/doc/examples/json_pointer__to_string.cpp b/doc/examples/json_pointer__to_string.cpp new file mode 100644 index 00000000..4cb053c5 --- /dev/null +++ b/doc/examples/json_pointer__to_string.cpp @@ -0,0 +1,34 @@ +#include + +using json = nlohmann::json; + +int main() +{ + // different JSON Pointers + json::json_pointer ptr1(""); + json::json_pointer ptr2("/foo"); + json::json_pointer ptr3("/foo/0"); + json::json_pointer ptr4("/"); + json::json_pointer ptr5("/a~1b"); + json::json_pointer ptr6("/c%d"); + json::json_pointer ptr7("/e^f"); + json::json_pointer ptr8("/g|h"); + json::json_pointer ptr9("/i\\j"); + json::json_pointer ptr10("/k\"l"); + json::json_pointer ptr11("/ "); + json::json_pointer ptr12("/m~0n"); + + + std::cout << ptr1.to_string() << '\n' + << ptr2.to_string() << '\n' + << ptr3.to_string() << '\n' + << ptr4.to_string() << '\n' + << ptr5.to_string() << '\n' + << ptr6.to_string() << '\n' + << ptr7.to_string() << '\n' + << ptr8.to_string() << '\n' + << ptr9.to_string() << '\n' + << ptr10.to_string() << '\n' + << ptr11.to_string() << '\n' + << ptr12.to_string() << std::endl; +} diff --git a/doc/examples/json_pointer__to_string.link b/doc/examples/json_pointer__to_string.link new file mode 100644 index 00000000..407609b2 --- /dev/null +++ b/doc/examples/json_pointer__to_string.link @@ -0,0 +1 @@ +online \ No newline at end of file diff --git a/doc/examples/json_pointer__to_string.output b/doc/examples/json_pointer__to_string.output new file mode 100644 index 00000000..c4b5ea8f --- /dev/null +++ b/doc/examples/json_pointer__to_string.output @@ -0,0 +1,12 @@ + +/foo +/foo/0 +/ +/a~1b +/c%d +/e^f +/g|h +/i\j +/k"l +/ +/m~0n diff --git a/src/json.hpp b/src/json.hpp index 1785f182..6b22e89d 100644 --- a/src/json.hpp +++ b/src/json.hpp @@ -8818,6 +8818,10 @@ basic_json_parser_63: /*! @brief JSON Pointer + A JSON pointer defines a string syntax for identifying a specific value + within a JSON document. It can be used with functions `at` and + `operator[]`. Furthermore, JSON pointers are the base for JSON patches. + @sa [RFC 6901](https://tools.ietf.org/html/rfc6901) @since version 2.0.0 @@ -8854,10 +8858,37 @@ basic_json_parser_63: : reference_tokens(split(s)) {} - /// test for inequality - bool operator!=(const json_pointer& rhs) const + /*! + @brief return a string representation of the JSON pointer + + @invariant For each JSON pointer `ptr`, it holds: + @code {.cpp} + ptr == json_pointer(ptr.to_string()); + @endcode + + @return a string representation of the JSON pointer + + @liveexample{The example shows the result of `to_string`., + json_pointer__to_string} + + @since version 2.0.0 + */ + std::string to_string() const noexcept { - return reference_tokens != rhs.reference_tokens; + std::string result; + + for (const auto& reference_token : reference_tokens) + { + result += "/" + escape(reference_token); + } + + return result; + } + + /// @copydoc to_string() + operator std::string() const + { + return to_string(); } private: diff --git a/src/json.hpp.re2c b/src/json.hpp.re2c index 51b72167..3dab33bb 100644 --- a/src/json.hpp.re2c +++ b/src/json.hpp.re2c @@ -8128,6 +8128,10 @@ class basic_json /*! @brief JSON Pointer + A JSON pointer defines a string syntax for identifying a specific value + within a JSON document. It can be used with functions `at` and + `operator[]`. Furthermore, JSON pointers are the base for JSON patches. + @sa [RFC 6901](https://tools.ietf.org/html/rfc6901) @since version 2.0.0 @@ -8164,10 +8168,37 @@ class basic_json : reference_tokens(split(s)) {} - /// test for inequality - bool operator!=(const json_pointer& rhs) const + /*! + @brief return a string representation of the JSON pointer + + @invariant For each JSON pointer `ptr`, it holds: + @code {.cpp} + ptr == json_pointer(ptr.to_string()); + @endcode + + @return a string representation of the JSON pointer + + @liveexample{The example shows the result of `to_string`., + json_pointer__to_string} + + @since version 2.0.0 + */ + std::string to_string() const noexcept { - return reference_tokens != rhs.reference_tokens; + std::string result; + + for (const auto& reference_token : reference_tokens) + { + result += "/" + escape(reference_token); + } + + return result; + } + + /// @copydoc to_string() + operator std::string() const + { + return to_string(); } private: diff --git a/test/unit.cpp b/test/unit.cpp index f6c49883..d038b867 100644 --- a/test/unit.cpp +++ b/test/unit.cpp @@ -12035,6 +12035,36 @@ TEST_CASE("Unicode", "[hide]") // the array has 1112064 + 1 elemnts (a terminating "null" value) CHECK(j.size() == 1112065); + + SECTION("check JSON Pointers") + { + for (auto s : j) + { + // skip non-string JSON values + if (not s.is_string()) + { + continue; + } + + std::string ptr = s; + + // tilde must be followed by 0 or 1 + if (ptr == "~") + { + ptr += "0"; + } + + // JSON Pointers must begin with "/" + ptr = "/" + ptr; + + CHECK_NOTHROW(json::json_pointer("/" + ptr)); + + // check escape/unescape roundtrip + auto escaped = json::json_pointer::escape(ptr); + json::json_pointer::unescape(escaped); + CHECK(escaped == ptr); + } + } } SECTION("ignore byte-order-mark") @@ -12389,6 +12419,17 @@ TEST_CASE("JSON pointers") json j_object(json::value_t::object); CHECK(j_object.flatten().unflatten() == json()); } + + SECTION("string representation") + { + for (auto ptr : + {"", "/foo", "/foo/0", "/", "/a~1b", "/c%d", "/e^f", "/g|h", "/i\\j", "/k\"l", "/ ", "/m~0n" + }) + { + CHECK(json::json_pointer(ptr).to_string() == ptr); + CHECK(json::json_pointer(ptr) == ptr); + } + } } TEST_CASE("JSON patch") From f4ecceab19442e46813de74c3f1571e16ceffa42 Mon Sep 17 00:00:00 2001 From: Niels Date: Sat, 30 Apr 2016 00:12:19 +0200 Subject: [PATCH 11/17] cleanup --- test/unit.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/test/unit.cpp b/test/unit.cpp index d038b867..2ad28a74 100644 --- a/test/unit.cpp +++ b/test/unit.cpp @@ -12427,7 +12427,6 @@ TEST_CASE("JSON pointers") }) { CHECK(json::json_pointer(ptr).to_string() == ptr); - CHECK(json::json_pointer(ptr) == ptr); } } } From be16d005e4e7167c23344e78b72b5229706d347d Mon Sep 17 00:00:00 2001 From: Niels Date: Sat, 30 Apr 2016 00:25:11 +0200 Subject: [PATCH 12/17] get compiler versions --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index 882f98d2..979c5aab 100644 --- a/.travis.yml +++ b/.travis.yml @@ -109,6 +109,7 @@ matrix: env: COMPILER=clang script: + - $COMPILER --version - make CXX=$COMPILER CXXFLAGS="-lstdc++" - ./json_unit "*" - if [ `which valgrind` ]; then From 7c04bc3db659cfd02bbe1903bea4447a672aed83 Mon Sep 17 00:00:00 2001 From: Niels Date: Sat, 30 Apr 2016 00:43:33 +0200 Subject: [PATCH 13/17] added name call --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index 979c5aab..d1748dff 100644 --- a/.travis.yml +++ b/.travis.yml @@ -109,6 +109,7 @@ matrix: env: COMPILER=clang script: + - uname -a - $COMPILER --version - make CXX=$COMPILER CXXFLAGS="-lstdc++" - ./json_unit "*" From 9170740f45b033c9fbd06f084890e18bffec091e Mon Sep 17 00:00:00 2001 From: Niels Date: Sat, 30 Apr 2016 01:01:41 +0200 Subject: [PATCH 14/17] added documentation on Travis compilers --- README.md | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/README.md b/README.md index 90be576c..1f1e0927 100644 --- a/README.md +++ b/README.md @@ -48,6 +48,8 @@ Though it's 2016 already, the support for C++11 is still a bit sparse. Currently - Clang 3.4 - 3.9 (and possibly later) - Microsoft Visual C++ 14.0 RC (and possibly later) +The code is currently checked with Travis for GCC 4.9, GCC + I would be happy to learn about other compilers/versions. Please note: @@ -63,6 +65,24 @@ Please note: - For GCC running on MinGW or Android SDK, the error `'to_string' is not a member of 'std'` (or similarly, for `strtod`) may occur. Note this is not an issue with the code, but rather with the compiler itself. On Android, see above to build with a newer environment. For MinGW, please refer to [this site](http://tehsausage.com/mingw-to-string) and [this discussion](https://github.com/nlohmann/json/issues/136) for information on how to fix this bug. +The following compilers are currently used in [continuous integration](https://travis-ci.org/nlohmann/json): + +| Compiler | Operating System | Version String | +|-----------------|------------------------------|----------------| +| GCC 4.9.3 | Ubuntu 14.04.3 LTS | g++-4.9 (Ubuntu 4.9.3-8ubuntu2~14.04) 4.9.3 | +| GCC 5.3.0 | Ubuntu 14.04.3 LTS | g++-5 (Ubuntu 5.3.0-3ubuntu1~14.04) 5.3.0 20151204 | +| Clang 3.6.2 | Ubuntu 14.04.3 LTS | Ubuntu clang version 3.6.2-svn240577-1~exp1 (branches/release_36) (based on LLVM 3.6.2) | +| Clang 3.7.1 | Ubuntu 14.04.3 LTS | Ubuntu clang version 3.7.1-svn253571-1~exp1 (branches/release_37) (based on LLVM 3.7.1) | +| Clang 3.8.1 | Ubuntu 14.04.3 LTS | clang version 3.8.1-svn265380-1~exp1 (branches/release_38) | +| Clang 3.9.0 | Ubuntu 14.04.3 LTS | clang version 3.9.0-svn267478-1~exp1 (trunk) | +| Clang Xcode 6.1 | Darwin Kernel Version 13.4.0 | Apple LLVM version 6.0 (clang-600.0.54) (based on LLVM 3.5svn) | +| Clang Xcode 6.2 | Darwin Kernel Version 13.4.0 | Apple LLVM version 6.0 (clang-600.0.57) (based on LLVM 3.5svn) | +| Clang Xcode 6.3 | Darwin Kernel Version 14.3.0 | Apple LLVM version 6.1.0 (clang-602.0.49) (based on LLVM 3.6.0svn) | +| Clang Xcode 6.4 | Darwin Kernel Version 14.3.0 | Apple LLVM version 6.1.0 (clang-602.0.53) (based on LLVM 3.6.0svn) | +| Clang Xcode 7.1 | Darwin Kernel Version 14.5.0 | Apple LLVM version 7.0.0 (clang-700.1.76) | +| Clang Xcode 7.2 | Darwin Kernel Version 15.0.0 | Apple LLVM version 7.0.2 (clang-700.1.81) | +| Clang Xcode 7.3 | Darwin Kernel Version 14.5.0 | Apple LLVM version 7.3.0 (clang-703.0.29) | + ## Examples Here are some examples to give you an idea how to use the class. From a21f8b0c77646f3eb225bc98ed3f9ca586101068 Mon Sep 17 00:00:00 2001 From: Niels Date: Sat, 30 Apr 2016 01:21:35 +0200 Subject: [PATCH 15/17] added AppVeyor information --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 1f1e0927..28794d9c 100644 --- a/README.md +++ b/README.md @@ -82,6 +82,7 @@ The following compilers are currently used in [continuous integration](https://t | Clang Xcode 7.1 | Darwin Kernel Version 14.5.0 | Apple LLVM version 7.0.0 (clang-700.1.76) | | Clang Xcode 7.2 | Darwin Kernel Version 15.0.0 | Apple LLVM version 7.0.2 (clang-700.1.81) | | Clang Xcode 7.3 | Darwin Kernel Version 14.5.0 | Apple LLVM version 7.3.0 (clang-703.0.29) | +| Visual Studio 14 2015 | Windows Server 2012 R2 (x64) | Microsoft (R) Build Engine version 14.0.25123.0 | ## Examples From ea84a85b132b2e9bdf1e61aa55b657eba5e254f1 Mon Sep 17 00:00:00 2001 From: Niels Date: Sat, 30 Apr 2016 10:39:03 +0200 Subject: [PATCH 16/17] simplified flatten/unflatten examples --- README.md | 2 +- doc/examples/flatten.cpp | 9 +-------- doc/examples/flatten.link | 2 +- doc/examples/flatten.output | 5 ----- doc/examples/unflatten.cpp | 4 ---- doc/examples/unflatten.link | 2 +- doc/examples/unflatten.output | 6 +----- 7 files changed, 5 insertions(+), 25 deletions(-) diff --git a/README.md b/README.md index 28794d9c..1f501b73 100644 --- a/README.md +++ b/README.md @@ -449,7 +449,7 @@ $ make $ ./json_unit "*" =============================================================================== -All tests passed (3344554 assertions in 31 test cases) +All tests passed (5568699 assertions in 31 test cases) ``` For more information, have a look at the file [.travis.yml](https://github.com/nlohmann/json/blob/master/.travis.yml). diff --git a/doc/examples/flatten.cpp b/doc/examples/flatten.cpp index 0601f8a3..ace53a21 100644 --- a/doc/examples/flatten.cpp +++ b/doc/examples/flatten.cpp @@ -20,18 +20,11 @@ int main() { "object", { {"currency", "USD"}, - {"value", 42.99}, - {"", "empty string"}, - {"/", "slash"}, - {"~", "tilde"}, - {"~1", "tilde1"} + {"value", 42.99} } } }; // call flatten() std::cout << std::setw(4) << j.flatten() << '\n'; - - // flatten for a primitive value - std::cout << j["pi"].flatten() << '\n'; } diff --git a/doc/examples/flatten.link b/doc/examples/flatten.link index 0fe78bbb..817d0627 100644 --- a/doc/examples/flatten.link +++ b/doc/examples/flatten.link @@ -1 +1 @@ -online \ No newline at end of file +online \ No newline at end of file diff --git a/doc/examples/flatten.output b/doc/examples/flatten.output index fedfc8ef..33bd4c4b 100644 --- a/doc/examples/flatten.output +++ b/doc/examples/flatten.output @@ -6,12 +6,7 @@ "/list/2": 2, "/name": "Niels", "/nothing": null, - "/object/": "empty string", "/object/currency": "USD", "/object/value": 42.99, - "/object/~0": "tilde", - "/object/~01": "tilde1", - "/object/~1": "slash", "/pi": 3.141 } -{"":3.141} diff --git a/doc/examples/unflatten.cpp b/doc/examples/unflatten.cpp index 39c674c9..e2b9b6b8 100644 --- a/doc/examples/unflatten.cpp +++ b/doc/examples/unflatten.cpp @@ -14,12 +14,8 @@ int main() {"/list/2", 2}, {"/name", "Niels"}, {"/nothing", nullptr}, - {"/object/", "empty string"}, {"/object/currency", "USD"}, {"/object/value", 42.99}, - {"/object/~0", "tilde"}, - {"/object/~01", "tilde1"}, - {"/object/~1", "slash"}, {"/pi", 3.141} }; diff --git a/doc/examples/unflatten.link b/doc/examples/unflatten.link index bc7594a0..d564ab01 100644 --- a/doc/examples/unflatten.link +++ b/doc/examples/unflatten.link @@ -1 +1 @@ -online \ No newline at end of file +online \ No newline at end of file diff --git a/doc/examples/unflatten.output b/doc/examples/unflatten.output index f57c9c9a..ed48385a 100644 --- a/doc/examples/unflatten.output +++ b/doc/examples/unflatten.output @@ -11,12 +11,8 @@ "name": "Niels", "nothing": null, "object": { - "": "empty string", - "/": "slash", "currency": "USD", - "value": 42.99, - "~": "tilde", - "~1": "tilde1" + "value": 42.99 }, "pi": 3.141 } From c04275966f8e1d2aba154074ef823b5973e1cb68 Mon Sep 17 00:00:00 2001 From: Niels Date: Sat, 7 May 2016 18:33:43 +0200 Subject: [PATCH 17/17] improved test coverage --- README.md | 2 +- src/json.hpp | 3 ++- src/json.hpp.re2c | 3 ++- test/unit.cpp | 6 ++++++ 4 files changed, 11 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 1f501b73..f8cb20d6 100644 --- a/README.md +++ b/README.md @@ -449,7 +449,7 @@ $ make $ ./json_unit "*" =============================================================================== -All tests passed (5568699 assertions in 31 test cases) +All tests passed (5568703 assertions in 31 test cases) ``` For more information, have a look at the file [.travis.yml](https://github.com/nlohmann/json/blob/master/.travis.yml). diff --git a/src/json.hpp b/src/json.hpp index 6b22e89d..932ab9e5 100644 --- a/src/json.hpp +++ b/src/json.hpp @@ -9708,7 +9708,8 @@ basic_json_parser_63: default: { - throw std::domain_error("unexpected parent type " + parent.type_name()); + // if there exists a parent it cannot be primitive + assert(false); // LCOV_EXCL_LINE } } } diff --git a/src/json.hpp.re2c b/src/json.hpp.re2c index 3dab33bb..77a1eea9 100644 --- a/src/json.hpp.re2c +++ b/src/json.hpp.re2c @@ -9018,7 +9018,8 @@ class basic_json default: { - throw std::domain_error("unexpected parent type " + parent.type_name()); + // if there exists a parent it cannot be primitive + assert(false); // LCOV_EXCL_LINE } } } diff --git a/test/unit.cpp b/test/unit.cpp index 2ad28a74..de00166f 100644 --- a/test/unit.cpp +++ b/test/unit.cpp @@ -12094,6 +12094,12 @@ TEST_CASE("JSON pointers") CHECK_THROWS_AS(json::json_pointer("/~"), std::domain_error); CHECK_THROWS_WITH(json::json_pointer("/~"), "escape error: '~' must be followed with '0' or '1'"); + + json::json_pointer p; + CHECK_THROWS_AS(p.top(), std::domain_error); + CHECK_THROWS_WITH(p.top(), "JSON pointer has no parent"); + CHECK_THROWS_AS(p.pop_back(), std::domain_error); + CHECK_THROWS_WITH(p.pop_back(), "JSON pointer has no parent"); } SECTION("examples from RFC 6901")