diff --git a/include/boost/json/impl/pointer.ipp b/include/boost/json/impl/pointer.ipp index d07c48082..9f82d1fd0 100644 --- a/include/boost/json/impl/pointer.ipp +++ b/include/boost/json/impl/pointer.ipp @@ -520,6 +520,84 @@ value::set_at_pointer( return try_set_at_pointer(sv, ref, opts).value(); } +bool +value::erase_at_pointer ( + string_view sv, + system::error_code& ec) noexcept +{ + ec.clear(); + if(sv.empty()){ + BOOST_JSON_FAIL(ec, error::missing_slash); + return false; + } + + string_view walk = sv; + string_view last_segment; + while (true) + { + last_segment = detail::next_segment(walk, ec); + if (ec.failed()) + return false; + if (walk.empty()) + break; + } + + string_view const parent_sv( + sv.data(), + static_cast(last_segment.data() - sv.data())); + + value* parent = detail::walk_pointer( + *this, + parent_sv, + ec, + []( object& obj, detail::pointer_token token ) + { + return detail::if_contains_token(obj, token); + }, + []( array& arr, std::size_t index, system::error_code& ec ) -> value* + { + if( ec ) + return nullptr; + + return arr.if_contains(index); + }, + []( value&, string_view) + { + return std::false_type(); + }); + + if (!parent) + return false; + + switch (parent->kind()) + { + case boost::json::kind::object: { + auto& obj = parent->get_object(); + detail::pointer_token const token(last_segment); + key_value_pair* kv = detail::find_in_object(obj, token).first; + if (kv) { + obj.erase(kv); + return true; + } + return false; + } + case boost::json::kind::array: { + auto const index = detail::parse_number_token(last_segment, ec); + auto& arr = parent->get_array(); + if (arr.if_contains(index)){ + arr.erase(arr.begin() + index); + return true; + } + return false; + } + default: { + BOOST_JSON_FAIL(ec, error::value_is_scalar); + return false; + } + } +} + + } // namespace json } // namespace boost diff --git a/include/boost/json/value.hpp b/include/boost/json/value.hpp index 3e93bcf28..33def578b 100644 --- a/include/boost/json/value.hpp +++ b/include/boost/json/value.hpp @@ -3035,6 +3035,19 @@ class value //------------------------------------------------------ + /** Remove an element via JSON Pointer. + + @{ + */ + BOOST_JSON_DECL + bool + erase_at_pointer( + string_view sv, + system::error_code& ec) noexcept; + + /// @} + //------------------------------------------------------ + /** Check if two values are equal. Two values are equal when they are the same kind and their referenced diff --git a/test/pointer.cpp b/test/pointer.cpp index ef4c12bb2..9e3b4ab0a 100644 --- a/test/pointer.cpp +++ b/test/pointer.cpp @@ -8,6 +8,8 @@ // #include +#include +#include #include "test_suite.hpp" @@ -410,5 +412,292 @@ class pointer_test TEST_SUITE(pointer_test, "boost.json.pointer"); +class erase_at_pointer_test +{ + value + testValueArray() const + { + auto raw = R"( +[ + { + "image" : { + "path" : "somewhere", + "size" : 9100 + }, + "comment" : [ + { + "text" : "this is cool", + "timestamp" : 123456, + "likes" : [ + { + "author" : "Coco", + "timestamp" : 123 + }, + { + "author" : "Izzy", + "timestamp" : 456 + } + ] + } + ] + } +] + +)"; + return boost::json::parse(raw); + } + + value + testValueObject() const + { + auto raw = R"( +{ + "comment" : { + "text" : "this is cool", + "timestamp" : 123456, + "likes" : [ + { + "author" : "Coco", + "timestamp" : 123 + }, + { + "author" : "Izzy", + "timestamp" : 456 + } + ] + } +} + + +)"; + return boost::json::parse(raw); + } + + +public: + void testObject1() + { + value json = testValueObject(); + value target = boost::json::parse(R"( +{ + "comment" : { + "text" : "this is cool", + "timestamp" : 123456 + } +} +)"); + boost::system::error_code ec; + bool res = json.erase_at_pointer("/comment/likes", ec); + BOOST_TEST(res); + BOOST_TEST(!ec); + BOOST_TEST(target == json); + } + + void + testArray1(){ + value json = testValueArray(); + value target = boost::json::parse(R"( +[ + { + "image" : { + "path" : "somewhere", + "size" : 9100 + } + } +] +)"); + boost::system::error_code ec; + bool res = json.erase_at_pointer("/0/comment", ec); + BOOST_TEST(res); + BOOST_TEST(!ec); + BOOST_TEST(target == json); + } + + void + testArray2(){ + value json = testValueArray(); + value target = boost::json::parse(R"( +[ + { + "image" : { + "path" : "somewhere", + "size" : 9100 + }, + "comment" : [ + { + "text" : "this is cool", + "timestamp" : 123456, + "likes" : [ + { + "author" : "Coco", + "timestamp" : 123 + } + ] + } + ] + } +] +)"); + boost::system::error_code ec; + bool res = json.erase_at_pointer("/0/comment/0/likes/1", ec); + BOOST_TEST(res); + BOOST_TEST(!ec); + BOOST_TEST(target == json); + + } + + + void + malformedPointer() + { + value const original = testValueArray(); + value copy = original; + + boost::system::error_code ec; + bool res = copy.erase_at_pointer("invalid", ec); + //it should fail + BOOST_TEST(!res); + + BOOST_TEST(ec == error::missing_slash); + //and the json should not have any change + BOOST_TEST(copy == original); + } + + void + testEmptyPointer() + { + value const original = testValueArray(); + value copy = original; + + boost::system::error_code ec; + bool res = copy.erase_at_pointer("", ec); + //it should fail + BOOST_TEST(!res); + + BOOST_TEST(ec); + //and the json should not have any change + BOOST_TEST(copy == original); + } + + void inexistent1() + { + value const original = testValueArray(); + value copy = original; + boost::system::error_code ec; + bool res = copy.erase_at_pointer("/1/image", ec); + + //no deletition + BOOST_TEST(!res); + + //and the json should not have any change + BOOST_TEST(copy == original); + } + + void + inexistent2() + { + value const original = testValueObject(); + value copy = original; + boost::system::error_code ec; + bool res = copy.erase_at_pointer("/something/inexistent", ec); + + //no deletition + BOOST_TEST(!res); + + //and the json should not have any change + BOOST_TEST(copy == original); + } + + void + doubleDelete() + { + value json = testValueArray(); + boost::system::error_code ec; + bool res = json.erase_at_pointer("/0/comment/0/text", ec); + BOOST_TEST(res); + + auto copy = json; + + //already deleted + res = json.erase_at_pointer("/0/comment/0/text", ec); + BOOST_TEST(!res); + + BOOST_TEST(copy == json); + } + + void + chained() + { + value json = testValueArray(); + + value target = boost::json::parse(R"( +[ + { + "image": {}, + "comment": [ + { + "likes": [ + {}, + {} + ] + } + ] + } +] +)"); + + boost::system::error_code ec; + bool res = json.erase_at_pointer("/0/comment/0/text", ec); + BOOST_TEST(res); + + res = json.erase_at_pointer("/0/comment/0/timestamp", ec); + BOOST_TEST(res); + + res = json.erase_at_pointer("/0/comment/0/likes/0/author", ec); + BOOST_TEST(res); + + res = json.erase_at_pointer("/0/comment/0/likes/0/timestamp", ec); + BOOST_TEST(res); + + //the previous element 0 still exists but is empty + res = json.erase_at_pointer("/0/comment/0/likes/0/author", ec); + BOOST_TEST(!res); + + res = json.erase_at_pointer("/0/comment/0/likes/1/author", ec); + BOOST_TEST(res); + + res = json.erase_at_pointer("/0/comment/0/likes/1/timestamp", ec); + BOOST_TEST(res); + + res = json.erase_at_pointer("/0/image/path", ec); + BOOST_TEST(res); + + res = json.erase_at_pointer("/0/image/size", ec); + BOOST_TEST(res); + + res = json.erase_at_pointer("/0/image/invalid", ec); + BOOST_TEST(!res); + + BOOST_TEST(json == target); + } + + void + run() + { + testEmptyPointer(); + malformedPointer(); + testArray1(); + testArray2(); + testObject1(); + inexistent1(); + inexistent2(); + doubleDelete(); + chained(); + } +}; + + +TEST_SUITE(erase_at_pointer_test, "boost.json.erase_at_pointer"); + } // namespace json } // namespace boost