diff --git a/doc/examples/contains_json_pointer.cpp b/doc/examples/contains_json_pointer.cpp new file mode 100644 index 00000000..ed45fcee --- /dev/null +++ b/doc/examples/contains_json_pointer.cpp @@ -0,0 +1,45 @@ +#include +#include + +using json = nlohmann::json; + +int main() +{ + // create a JSON value + json j = + { + {"number", 1}, {"string", "foo"}, {"array", {1, 2}} + }; + + std::cout << std::boolalpha + << j.contains("/number"_json_pointer) << '\n' + << j.contains("/string"_json_pointer) << '\n' + << j.contains("/string"_json_pointer) << '\n' + << j.contains("/array"_json_pointer) << '\n' + << j.contains("/array/1"_json_pointer) << '\n' + << j.contains("/array/-"_json_pointer) << '\n' + << j.contains("/array/4"_json_pointer) << '\n' + << j.contains("/baz"_json_pointer) << std::endl; + + // out_of_range.106 + try + { + // try to use an array index with leading '0' + j.contains("/array/01"_json_pointer); + } + catch (json::parse_error& e) + { + std::cout << e.what() << '\n'; + } + + // out_of_range.109 + try + { + // try to use an array index that is not a number + j.contains("/array/one"_json_pointer); + } + catch (json::parse_error& e) + { + std::cout << e.what() << '\n'; + } +} diff --git a/doc/examples/contains_json_pointer.link b/doc/examples/contains_json_pointer.link new file mode 100644 index 00000000..778e151f --- /dev/null +++ b/doc/examples/contains_json_pointer.link @@ -0,0 +1 @@ +online \ No newline at end of file diff --git a/doc/examples/contains_json_pointer.output b/doc/examples/contains_json_pointer.output new file mode 100644 index 00000000..82ec3675 --- /dev/null +++ b/doc/examples/contains_json_pointer.output @@ -0,0 +1,10 @@ +true +true +true +true +true +false +false +false +[json.exception.parse_error.106] parse error: array index '01' must not begin with '0' +[json.exception.parse_error.109] parse error: array index 'one' is not a number diff --git a/include/nlohmann/detail/json_pointer.hpp b/include/nlohmann/detail/json_pointer.hpp index 465e5165..51c51efa 100644 --- a/include/nlohmann/detail/json_pointer.hpp +++ b/include/nlohmann/detail/json_pointer.hpp @@ -2,6 +2,7 @@ #include // all_of #include // assert +#include // isdigit #include // accumulate #include // string #include // move @@ -369,7 +370,7 @@ class json_pointer // j which will be overwritten by a primitive value for (const auto& reference_token : reference_tokens) { - switch (result->m_type) + switch (result->type()) { case detail::value_t::null: { @@ -446,14 +447,14 @@ class json_pointer for (const auto& reference_token : reference_tokens) { // convert null values to arrays or objects before continuing - if (ptr->m_type == detail::value_t::null) + if (ptr->is_null()) { // check if reference token is a number const bool nums = std::all_of(reference_token.begin(), reference_token.end(), - [](const char x) + [](const unsigned char x) { - return x >= '0' and x <= '9'; + return std::isdigit(x); }); // change value to array for numbers or "-" or to object otherwise @@ -462,7 +463,7 @@ class json_pointer : detail::value_t::object; } - switch (ptr->m_type) + switch (ptr->type()) { case detail::value_t::object: { @@ -521,7 +522,7 @@ class json_pointer using size_type = typename BasicJsonType::size_type; for (const auto& reference_token : reference_tokens) { - switch (ptr->m_type) + switch (ptr->type()) { case detail::value_t::object: { @@ -586,7 +587,7 @@ class json_pointer using size_type = typename BasicJsonType::size_type; for (const auto& reference_token : reference_tokens) { - switch (ptr->m_type) + switch (ptr->type()) { case detail::value_t::object: { @@ -645,7 +646,7 @@ class json_pointer using size_type = typename BasicJsonType::size_type; for (const auto& reference_token : reference_tokens) { - switch (ptr->m_type) + switch (ptr->type()) { case detail::value_t::object: { @@ -692,6 +693,77 @@ class json_pointer return *ptr; } + /*! + @throw parse_error.106 if an array index begins with '0' + @throw parse_error.109 if an array index was not a number + */ + bool contains(const BasicJsonType* ptr) const + { + using size_type = typename BasicJsonType::size_type; + for (const auto& reference_token : reference_tokens) + { + switch (ptr->type()) + { + case detail::value_t::object: + { + if (not ptr->contains(reference_token)) + { + // we did not find the key in the object + return false; + } + + ptr = &ptr->operator[](reference_token); + break; + } + + case detail::value_t::array: + { + if (JSON_UNLIKELY(reference_token == "-")) + { + // "-" always fails the range check + return false; + } + + // error condition (cf. RFC 6901, Sect. 4) + if (JSON_UNLIKELY(reference_token.size() > 1 and reference_token[0] == '0')) + { + JSON_THROW(detail::parse_error::create(106, 0, + "array index '" + reference_token + + "' must not begin with '0'")); + } + + JSON_TRY + { + const auto idx = static_cast(array_index(reference_token)); + if (idx >= ptr->size()) + { + // index out of range + return false; + } + + ptr = &ptr->operator[](idx); + break; + } + JSON_CATCH(std::invalid_argument&) + { + JSON_THROW(detail::parse_error::create(109, 0, "array index '" + reference_token + "' is not a number")); + } + break; + } + + default: + { + // we do not expect primitive values if there is still a + // reference token to process + return false; + } + } + } + + // no reference token left means we found a primitive value + return true; + } + /*! @brief split the string input to reference tokens @@ -813,7 +885,7 @@ class json_pointer const BasicJsonType& value, BasicJsonType& result) { - switch (value.m_type) + switch (value.type()) { case detail::value_t::array: { diff --git a/include/nlohmann/json.hpp b/include/nlohmann/json.hpp index 58db19d7..bef79c2c 100644 --- a/include/nlohmann/json.hpp +++ b/include/nlohmann/json.hpp @@ -4001,15 +4001,48 @@ class basic_json @liveexample{The following code shows an example for `contains()`.,contains} @sa @ref find(KeyT&&) -- returns an iterator to an object element + @sa @ref contains(const json_pointer&) const -- checks the existence for a JSON pointer @since version 3.6.0 */ - template - bool contains(KeyT&& key) const + template::value, int>::type = 0> + bool contains(KeyT && key) const { return is_object() and m_value.object->find(std::forward(key)) != m_value.object->end(); } + /*! + @brief check the existence of an element in a JSON object given a JSON pointer + + Check wehther the given JSON pointer @a ptr can be resolved in the current + JSON value. + + @note This method can be executed on any JSON value type. + + @param[in] ptr JSON pointer to check its existence. + + @return true if the JSON pointer can be resolved to a stored value, false + otherwise. + + @post If `j.contains(ptr)` returns true, it is safe to call `j[ptr]`. + + @throw parse_error.106 if an array index begins with '0' + @throw parse_error.109 if an array index was not a number + + @complexity Logarithmic in the size of the JSON object. + + @liveexample{The following code shows an example for `contains()`.,contains_json_pointer} + + @sa @ref contains(KeyT &&) const -- checks the existence of a key + + @since version 3.7.0 + */ + bool contains(const json_pointer& ptr) const + { + return ptr.contains(this); + } + /// @} diff --git a/single_include/nlohmann/json.hpp b/single_include/nlohmann/json.hpp index bb2dacbc..872a9b69 100644 --- a/single_include/nlohmann/json.hpp +++ b/single_include/nlohmann/json.hpp @@ -8445,6 +8445,7 @@ class json_reverse_iterator : public std::reverse_iterator #include // all_of #include // assert +#include // isdigit #include // accumulate #include // string #include // move @@ -8815,7 +8816,7 @@ class json_pointer // j which will be overwritten by a primitive value for (const auto& reference_token : reference_tokens) { - switch (result->m_type) + switch (result->type()) { case detail::value_t::null: { @@ -8892,14 +8893,14 @@ class json_pointer for (const auto& reference_token : reference_tokens) { // convert null values to arrays or objects before continuing - if (ptr->m_type == detail::value_t::null) + if (ptr->is_null()) { // check if reference token is a number const bool nums = std::all_of(reference_token.begin(), reference_token.end(), - [](const char x) + [](const unsigned char x) { - return x >= '0' and x <= '9'; + return std::isdigit(x); }); // change value to array for numbers or "-" or to object otherwise @@ -8908,7 +8909,7 @@ class json_pointer : detail::value_t::object; } - switch (ptr->m_type) + switch (ptr->type()) { case detail::value_t::object: { @@ -8967,7 +8968,7 @@ class json_pointer using size_type = typename BasicJsonType::size_type; for (const auto& reference_token : reference_tokens) { - switch (ptr->m_type) + switch (ptr->type()) { case detail::value_t::object: { @@ -9032,7 +9033,7 @@ class json_pointer using size_type = typename BasicJsonType::size_type; for (const auto& reference_token : reference_tokens) { - switch (ptr->m_type) + switch (ptr->type()) { case detail::value_t::object: { @@ -9091,7 +9092,7 @@ class json_pointer using size_type = typename BasicJsonType::size_type; for (const auto& reference_token : reference_tokens) { - switch (ptr->m_type) + switch (ptr->type()) { case detail::value_t::object: { @@ -9138,6 +9139,77 @@ class json_pointer return *ptr; } + /*! + @throw parse_error.106 if an array index begins with '0' + @throw parse_error.109 if an array index was not a number + */ + bool contains(const BasicJsonType* ptr) const + { + using size_type = typename BasicJsonType::size_type; + for (const auto& reference_token : reference_tokens) + { + switch (ptr->type()) + { + case detail::value_t::object: + { + if (not ptr->contains(reference_token)) + { + // we did not find the key in the object + return false; + } + + ptr = &ptr->operator[](reference_token); + break; + } + + case detail::value_t::array: + { + if (JSON_UNLIKELY(reference_token == "-")) + { + // "-" always fails the range check + return false; + } + + // error condition (cf. RFC 6901, Sect. 4) + if (JSON_UNLIKELY(reference_token.size() > 1 and reference_token[0] == '0')) + { + JSON_THROW(detail::parse_error::create(106, 0, + "array index '" + reference_token + + "' must not begin with '0'")); + } + + JSON_TRY + { + const auto idx = static_cast(array_index(reference_token)); + if (idx >= ptr->size()) + { + // index out of range + return false; + } + + ptr = &ptr->operator[](idx); + break; + } + JSON_CATCH(std::invalid_argument&) + { + JSON_THROW(detail::parse_error::create(109, 0, "array index '" + reference_token + "' is not a number")); + } + break; + } + + default: + { + // we do not expect primitive values if there is still a + // reference token to process + return false; + } + } + } + + // no reference token left means we found a primitive value + return true; + } + /*! @brief split the string input to reference tokens @@ -9259,7 +9331,7 @@ class json_pointer const BasicJsonType& value, BasicJsonType& result) { - switch (value.m_type) + switch (value.type()) { case detail::value_t::array: { @@ -16802,15 +16874,48 @@ class basic_json @liveexample{The following code shows an example for `contains()`.,contains} @sa @ref find(KeyT&&) -- returns an iterator to an object element + @sa @ref contains(const json_pointer&) const -- checks the existence for a JSON pointer @since version 3.6.0 */ - template - bool contains(KeyT&& key) const + template::value, int>::type = 0> + bool contains(KeyT && key) const { return is_object() and m_value.object->find(std::forward(key)) != m_value.object->end(); } + /*! + @brief check the existence of an element in a JSON object given a JSON pointer + + Check wehther the given JSON pointer @a ptr can be resolved in the current + JSON value. + + @note This method can be executed on any JSON value type. + + @param[in] ptr JSON pointer to check its existence. + + @return true if the JSON pointer can be resolved to a stored value, false + otherwise. + + @post If `j.contains(ptr)` returns true, it is safe to call `j[ptr]`. + + @throw parse_error.106 if an array index begins with '0' + @throw parse_error.109 if an array index was not a number + + @complexity Logarithmic in the size of the JSON object. + + @liveexample{The following code shows an example for `contains()`.,contains_json_pointer} + + @sa @ref contains(KeyT &&) const -- checks the existence of a key + + @since version 3.7.0 + */ + bool contains(const json_pointer& ptr) const + { + return ptr.contains(this); + } + /// @} diff --git a/test/src/unit-json_pointer.cpp b/test/src/unit-json_pointer.cpp index 6e6a1d79..598f36b0 100644 --- a/test/src/unit-json_pointer.cpp +++ b/test/src/unit-json_pointer.cpp @@ -90,12 +90,18 @@ TEST_CASE("JSON pointers") // the whole document CHECK(j[json::json_pointer()] == j); CHECK(j[json::json_pointer("")] == j); + CHECK(j.contains(json::json_pointer())); + CHECK(j.contains(json::json_pointer(""))); // array access CHECK(j[json::json_pointer("/foo")] == j["foo"]); + CHECK(j.contains(json::json_pointer("/foo"))); CHECK(j[json::json_pointer("/foo/0")] == j["foo"][0]); CHECK(j[json::json_pointer("/foo/1")] == j["foo"][1]); CHECK(j["/foo/1"_json_pointer] == j["foo"][1]); + CHECK(j.contains(json::json_pointer("/foo/0"))); + CHECK(j.contains(json::json_pointer("/foo/1"))); + CHECK(not j.contains(json::json_pointer("/foo/-"))); // checked array access CHECK(j.at(json::json_pointer("/foo/0")) == j["foo"][0]); @@ -103,6 +109,8 @@ TEST_CASE("JSON pointers") // empty string access CHECK(j[json::json_pointer("/")] == j[""]); + CHECK(j.contains(json::json_pointer(""))); + CHECK(j.contains(json::json_pointer("/"))); // other cases CHECK(j[json::json_pointer("/ ")] == j[" "]); @@ -112,6 +120,14 @@ TEST_CASE("JSON pointers") CHECK(j[json::json_pointer("/i\\j")] == j["i\\j"]); CHECK(j[json::json_pointer("/k\"l")] == j["k\"l"]); + // contains + CHECK(j.contains(json::json_pointer("/ "))); + CHECK(j.contains(json::json_pointer("/c%d"))); + CHECK(j.contains(json::json_pointer("/e^f"))); + CHECK(j.contains(json::json_pointer("/g|h"))); + CHECK(j.contains(json::json_pointer("/i\\j"))); + CHECK(j.contains(json::json_pointer("/k\"l"))); + // checked access CHECK(j.at(json::json_pointer("/ ")) == j[" "]); CHECK(j.at(json::json_pointer("/c%d")) == j["c%d"]); @@ -123,14 +139,24 @@ TEST_CASE("JSON pointers") // escaped access CHECK(j[json::json_pointer("/a~1b")] == j["a/b"]); CHECK(j[json::json_pointer("/m~0n")] == j["m~n"]); + CHECK(j.contains(json::json_pointer("/a~1b"))); + CHECK(j.contains(json::json_pointer("/m~0n"))); // unescaped access // access to nonexisting values yield object creation + CHECK(not j.contains(json::json_pointer("/a/b"))); CHECK_NOTHROW(j[json::json_pointer("/a/b")] = 42); + CHECK(j.contains(json::json_pointer("/a/b"))); CHECK(j["a"]["b"] == json(42)); + + CHECK(not j.contains(json::json_pointer("/a/c/1"))); CHECK_NOTHROW(j[json::json_pointer("/a/c/1")] = 42); CHECK(j["a"]["c"] == json({nullptr, 42})); + CHECK(j.contains(json::json_pointer("/a/c/1"))); + + CHECK(not j.contains(json::json_pointer("/a/d/-"))); CHECK_NOTHROW(j[json::json_pointer("/a/d/-")] = 42); + CHECK(not j.contains(json::json_pointer("/a/d/-"))); CHECK(j["a"]["d"] == json::array({42})); // "/a/b" works for JSON {"a": {"b": 42}} CHECK(json({{"a", {{"b", 42}}}})[json::json_pointer("/a/b")] == json(42)); @@ -143,6 +169,7 @@ TEST_CASE("JSON pointers") CHECK_THROWS_AS(j_primitive.at("/foo"_json_pointer), json::out_of_range&); CHECK_THROWS_WITH(j_primitive.at("/foo"_json_pointer), "[json.exception.out_of_range.404] unresolved reference token 'foo'"); + CHECK(not j_primitive.contains(json::json_pointer("/foo"))); } SECTION("const access") @@ -233,11 +260,16 @@ TEST_CASE("JSON pointers") // the whole document CHECK(j[""_json_pointer] == j); + CHECK(j.contains(""_json_pointer)); // array access CHECK(j["/foo"_json_pointer] == j["foo"]); CHECK(j["/foo/0"_json_pointer] == j["foo"][0]); CHECK(j["/foo/1"_json_pointer] == j["foo"][1]); + CHECK(j.contains("/foo"_json_pointer)); + CHECK(j.contains("/foo/0"_json_pointer)); + CHECK(j.contains("/foo/1"_json_pointer)); + CHECK(not j.contains("/foo/-"_json_pointer)); } } @@ -278,6 +310,12 @@ TEST_CASE("JSON pointers") CHECK_THROWS_AS(j_const.at("/01"_json_pointer), json::parse_error&); CHECK_THROWS_WITH(j_const.at("/01"_json_pointer), "[json.exception.parse_error.106] parse error: array index '01' must not begin with '0'"); + CHECK_THROWS_AS(j.contains("/01"_json_pointer), json::parse_error&); + CHECK_THROWS_WITH(j.contains("/01"_json_pointer), + "[json.exception.parse_error.106] parse error: array index '01' must not begin with '0'"); + CHECK_THROWS_AS(j_const.contains("/01"_json_pointer), json::parse_error&); + CHECK_THROWS_WITH(j_const.contains("/01"_json_pointer), + "[json.exception.parse_error.106] parse error: array index '01' must not begin with '0'"); // error with incorrect numbers CHECK_THROWS_AS(j["/one"_json_pointer] = 1, json::parse_error&); @@ -294,6 +332,13 @@ TEST_CASE("JSON pointers") CHECK_THROWS_WITH(j_const.at("/one"_json_pointer) == 1, "[json.exception.parse_error.109] parse error: array index 'one' is not a number"); + CHECK_THROWS_AS(j.contains("/one"_json_pointer), json::parse_error&); + CHECK_THROWS_WITH(j.contains("/one"_json_pointer), + "[json.exception.parse_error.109] parse error: array index 'one' is not a number"); + CHECK_THROWS_AS(j_const.contains("/one"_json_pointer), json::parse_error&); + CHECK_THROWS_WITH(j_const.contains("/one"_json_pointer), + "[json.exception.parse_error.109] parse error: array index 'one' is not a number"); + CHECK_THROWS_AS(json({{"/list/0", 1}, {"/list/1", 2}, {"/list/three", 3}}).unflatten(), json::parse_error&); CHECK_THROWS_WITH(json({{"/list/0", 1}, {"/list/1", 2}, {"/list/three", 3}}).unflatten(), "[json.exception.parse_error.109] parse error: array index 'three' is not a number"); @@ -306,6 +351,7 @@ TEST_CASE("JSON pointers") CHECK_THROWS_AS(j_const["/-"_json_pointer], json::out_of_range&); CHECK_THROWS_WITH(j_const["/-"_json_pointer], "[json.exception.out_of_range.402] array index '-' (3) is out of range"); + CHECK(not j_const.contains("/-"_json_pointer)); // error when using "-" with at CHECK_THROWS_AS(j.at("/-"_json_pointer), json::out_of_range&); @@ -314,6 +360,7 @@ TEST_CASE("JSON pointers") CHECK_THROWS_AS(j_const.at("/-"_json_pointer), json::out_of_range&); CHECK_THROWS_WITH(j_const.at("/-"_json_pointer), "[json.exception.out_of_range.402] array index '-' (3) is out of range"); + CHECK(not j_const.contains("/-"_json_pointer)); } SECTION("const access") @@ -329,11 +376,13 @@ TEST_CASE("JSON pointers") CHECK_THROWS_AS(j.at("/3"_json_pointer), json::out_of_range&); CHECK_THROWS_WITH(j.at("/3"_json_pointer), "[json.exception.out_of_range.401] array index 3 is out of range"); + CHECK(not j.contains("/3"_json_pointer)); // assign to nonexisting index (with gap) CHECK_THROWS_AS(j.at("/5"_json_pointer), json::out_of_range&); CHECK_THROWS_WITH(j.at("/5"_json_pointer), "[json.exception.out_of_range.401] array index 5 is out of range"); + CHECK(not j.contains("/5"_json_pointer)); // assign to "-" CHECK_THROWS_AS(j["/-"_json_pointer], json::out_of_range&); @@ -342,8 +391,8 @@ TEST_CASE("JSON pointers") CHECK_THROWS_AS(j.at("/-"_json_pointer), json::out_of_range&); CHECK_THROWS_WITH(j.at("/-"_json_pointer), "[json.exception.out_of_range.402] array index '-' (3) is out of range"); + CHECK(not j.contains("/-"_json_pointer)); } - } SECTION("flatten")