diff --git a/BUILD.bazel b/BUILD.bazel index 2c20fd5e4..33c4927a9 100644 --- a/BUILD.bazel +++ b/BUILD.bazel @@ -77,8 +77,10 @@ cc_library( "include/nlohmann/detail/view/materialize.hpp", "include/nlohmann/detail/view/node.hpp", "include/nlohmann/detail/view/number.hpp", + "include/nlohmann/detail/view/pointer.hpp", "include/nlohmann/detail/view/scan.hpp", "include/nlohmann/detail/view/string_ref.hpp", + "include/nlohmann/detail/view/value.hpp", "include/nlohmann/json.hpp", "include/nlohmann/json_fwd.hpp", "include/nlohmann/json_view.hpp", diff --git a/include/nlohmann/detail/view/errors.hpp b/include/nlohmann/detail/view/errors.hpp index ff5816efd..44d8a6438 100644 --- a/include/nlohmann/detail/view/errors.hpp +++ b/include/nlohmann/detail/view/errors.hpp @@ -40,6 +40,12 @@ namespace view NLOHMANN_VIEW_THROW(invalid_iterator::create(id, msg, nullptr)); } +/// a parse error without a position (as those of json_pointer) +[[noreturn]] NLOHMANN_VIEW_NOINLINE inline void throw_parse_error(int id, const std::string& msg) +{ + NLOHMANN_VIEW_THROW(parse_error::create(id, 0, msg, nullptr)); +} + /*! @brief throw the exception BasicJsonType::parse would throw for this input diff --git a/include/nlohmann/detail/view/pointer.hpp b/include/nlohmann/detail/view/pointer.hpp new file mode 100644 index 000000000..2c7b1e4db --- /dev/null +++ b/include/nlohmann/detail/view/pointer.hpp @@ -0,0 +1,171 @@ +// __ _____ _____ _____ +// __| | __| | | | JSON for Modern C++ +// | | |__ | | | | | | version 3.12.0 +// |_____|_____|_____|_|___| https://github.com/nlohmann/json +// +// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann +// SPDX-License-Identifier: MIT + +#pragma once + +#include // size_t +#include // uint64_t +#include // numeric_limits +#include // string, to_string + +#include +#include +#include + +NLOHMANN_JSON_NAMESPACE_BEGIN +namespace detail +{ +namespace view +{ + +/// what resolving a JSON pointer does where it cannot continue +enum class pointer_mode +{ + unchecked, ///< as const basic_json::operator[]: a discarded view where basic_json's behavior is undefined + checked, ///< as basic_json::at(): out_of_range.401/403 + value, ///< as basic_json::value(): no out_of_range exceptions (the default value is used) + contains, ///< as basic_json::contains(): no exceptions at all +}; + +/// the outcome of reading an array index from a reference token +enum class index_status +{ + ok, + leading_zero, ///< parse_error.106 + not_number, ///< parse_error.109 + unresolved, ///< out_of_range.404 + too_large, ///< out_of_range.410 +}; + +/// reads an array index like json_pointer::array_index (RFC 6901, Sect. 4), +/// but reports errors instead of throwing them +template +index_status array_index(const StringType& s, std::size_t& idx) noexcept +{ + if (s.size() > 1 && s[0] == '0') + { + return index_status::leading_zero; + } + if (s.size() > 1 && !(s[0] >= '1' && s[0] <= '9')) + { + return index_status::not_number; + } + if (s.empty()) + { + return index_status::unresolved; + } + std::uint64_t v = 0; + for (std::size_t i = 0; i < s.size(); ++i) + { + const auto d = static_cast(static_cast(s[i])) - '0'; + if (d > 9 || v > ((std::numeric_limits::max)() - d) / 10) + { + return index_status::unresolved; // not a number, or beyond unsigned long long + } + v = (v * 10) + d; + } + if (v >= static_cast((std::numeric_limits::max)())) + { + return index_status::too_large; + } + idx = static_cast(v); + return index_status::ok; +} + +/// throws the exception json_pointer::array_index throws for this status +template +[[noreturn]] NLOHMANN_VIEW_NOINLINE void throw_array_index_error(index_status status, const StringType& s) +{ + switch (status) + { + case index_status::leading_zero: + throw_parse_error(106, concat("array index '", s, "' must not begin with '0'")); + case index_status::not_number: + throw_parse_error(109, concat("array index '", s, "' is not a number")); + case index_status::too_large: + throw_out_of_range(410, concat("array index ", s, " exceeds size_type")); // LCOV_EXCL_LINE + case index_status::unresolved: + case index_status::ok: + default: + throw_out_of_range(404, concat("unresolved reference token '", s, "'")); + } +} + +/*! +@brief resolve the reference tokens of a JSON pointer, starting at a view + +The exceptions are those basic_json throws for the same pointer; where +basic_json's behavior is undefined (a missing key or an index out of range +with const operator[]), the result is a discarded view. +*/ +template +View resolve_pointer(View cur, const Tokens& tokens, pointer_mode mode) +{ + using string_view_t = typename View::string_view_t; + const bool throwing = mode == pointer_mode::unchecked || mode == pointer_mode::checked; + for (const auto& token : tokens) + { + if (cur.is_object()) + { + const auto it = cur.find(string_view_t(token.data(), token.size())); + if (it == cur.end()) + { + if (mode == pointer_mode::checked) + { + throw_out_of_range(403, concat("key '", token, "' not found")); + } + return View(); + } + cur = *it; + } + else if (cur.is_array()) + { + if (token.size() == 1 && token[0] == '-') + { + if (throwing) + { + throw_out_of_range(402, concat("array index '-' (", std::to_string(cur.size()), ") is out of range")); + } + return View(); + } + std::size_t idx = 0; + const index_status status = array_index(token, idx); + if (status != index_status::ok) + { + const bool parse_error = status == index_status::leading_zero || status == index_status::not_number; + if (throwing || (mode == pointer_mode::value && parse_error)) + { + throw_array_index_error(status, token); + } + return View(); + } + if (idx >= cur.size()) + { + if (mode == pointer_mode::checked) + { + throw_out_of_range(401, concat("array index ", std::to_string(idx), " is out of range")); + } + return View(); + } + cur = cur[idx]; + } + else + { + if (throwing) + { + throw_out_of_range(404, concat("unresolved reference token '", token, "'")); + } + return View(); + } + } + return cur; +} + +} // namespace view +} // namespace detail +NLOHMANN_JSON_NAMESPACE_END diff --git a/include/nlohmann/detail/view/value.hpp b/include/nlohmann/detail/view/value.hpp new file mode 100644 index 000000000..64b05b254 --- /dev/null +++ b/include/nlohmann/detail/view/value.hpp @@ -0,0 +1,108 @@ +// __ _____ _____ _____ +// __| | __| | | | JSON for Modern C++ +// | | |__ | | | | | | version 3.12.0 +// |_____|_____|_____|_|___| https://github.com/nlohmann/json +// +// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann +// SPDX-License-Identifier: MIT + +#pragma once + +#include // int64_t +#include // map +#include // basic_string +#include // enable_if, is_constructible +#include // unordered_map +#include // vector + +#include +#include +#include +#include +#include +#include + +NLOHMANN_JSON_NAMESPACE_BEGIN +namespace detail +{ +namespace view +{ + +/// selects a conversion by its target type +template +struct value_tag {}; + +/*! +@brief the number or boolean of a node converted to an arithmetic type + +As basic_json's get() for arithmetic types: integers and floats are +converted with static_cast, booleans give 0 or 1, and other types throw +type_error.302. +*/ +template +T arithmetic_value(const document_data& d, const node& n) +{ + switch (static_cast(n.kind)) + { + case value_t::number_unsigned: + return static_cast(static_cast(integer_bits(n))); + case value_t::number_integer: + return static_cast(static_cast(static_cast(integer_bits(n)))); + case value_t::number_float: + return static_cast(float_value(d.str(n), n)); + case value_t::boolean: + return static_cast((n.flags & node_flags::is_true) != 0); + case value_t::null: + case value_t::object: + case value_t::array: + case value_t::string: + case value_t::binary: + case value_t::discarded: + default: + throw_type_error(302, "type must be number, but is ", value_type_name(static_cast(n.kind))); + } +} + +/// std::vector from an array, element by element (type_error.302 otherwise) +template +std::vector vector_value(const View& v) +{ + if (NLOHMANN_VIEW_UNLIKELY(!v.is_array())) + { + throw_type_error(302, "type must be array, but is ", v.type_name()); + } + std::vector r; + r.reserve(v.size()); + for (const View e : v) + { + r.push_back(e.template get()); + } + return r; +} + +/// a map with string keys from an object; with duplicate keys, the last +/// value is kept, as parse() does (type_error.302 for other types) +template +Map map_value(const View& v) +{ + if (NLOHMANN_VIEW_UNLIKELY(!v.is_object())) + { + throw_type_error(302, "type must be object, but is ", v.type_name()); + } + Map r; + for (auto it = v.begin(); it != v.end(); ++it) + { + const auto key = it.key(); + r[typename Map::key_type(key.data(), key.size())] = it.value().template get(); + } + return r; +} + +/// whether a map type is read member by member (its keys are made from +/// characters and a length); other maps go through basic_json +template +struct is_string_key : std::is_constructible {}; + +} // namespace view +} // namespace detail +NLOHMANN_JSON_NAMESPACE_END diff --git a/include/nlohmann/json_view.hpp b/include/nlohmann/json_view.hpp index 63509e48d..c82a06d9a 100644 --- a/include/nlohmann/json_view.hpp +++ b/include/nlohmann/json_view.hpp @@ -27,11 +27,14 @@ #include // size_t #include // memcpy, strlen #include // distance, input_iterator_tag, iterator_traits +#include // map #include // unique_ptr #include // string #include // tuple_element, tuple_size -#include // enable_if, integral_constant, is_base_of, is_integral, is_same, remove_cv, remove_extent +#include // decay, enable_if, integral_constant, is_arithmetic, is_base_of, is_integral, is_same, remove_cv, remove_extent +#include // unordered_map #include // forward, move +#include // vector #include @@ -49,7 +52,9 @@ #include #include #include +#include #include +#include NLOHMANN_JSON_NAMESPACE_BEGIN @@ -268,6 +273,14 @@ class basic_json_view return operator[](static_cast(idx)); } + /// the value a JSON pointer refers to; a discarded view if a key is + /// missing or an index is out of range. Other errors throw what const + /// basic_json::operator[] throws. + basic_json_view operator[](const json_pointer& ptr) const + { + return detail::view::resolve_pointer(*this, detail::json_pointer_access::reference_tokens(ptr), detail::view::pointer_mode::unchecked); + } + /// the value of the member with this key (the first one, should the key /// occur more than once). Throws type_error.304 if this is not an object, /// and out_of_range.403 if there is no such member. @@ -315,6 +328,51 @@ class basic_json_view return at(static_cast(idx)); } + /// the value a JSON pointer refers to; throws what basic_json::at() + /// throws if it cannot be resolved + basic_json_view at(const json_pointer& ptr) const + { + return detail::view::resolve_pointer(*this, detail::json_pointer_access::reference_tokens(ptr), detail::view::pointer_mode::checked); + } + + /// the member with this key converted to T, or the default value if there + /// is no such member (the first one, should the key occur more than + /// once). Throws type_error.306 if this is not an object. + template < typename T, typename std::enable_if < !std::is_same::type, const char*>::value, int >::type = 0 > + T value(string_view_t key, const T& default_value) const + { + if (NLOHMANN_VIEW_UNLIKELY(!is_object())) + { + detail::view::throw_type_error(306, "cannot use value() with ", type_name()); + } + const basic_json_view r = lookup(key); + return r ? r.template get() : default_value; + } + + string_t value(string_view_t key, const char* default_value) const + { + return value(key, string_t(default_value)); + } + + /// the value a JSON pointer refers to converted to T, or the default + /// value if the pointer cannot be resolved. Throws type_error.306 if this + /// is neither an object nor an array. + template < typename T, typename std::enable_if < !std::is_same::type, const char*>::value, int >::type = 0 > + T value(const json_pointer& ptr, const T& default_value) const + { + if (NLOHMANN_VIEW_UNLIKELY(!is_structured())) + { + detail::view::throw_type_error(306, "cannot use value() with ", type_name()); + } + const basic_json_view r = detail::view::resolve_pointer(*this, detail::json_pointer_access::reference_tokens(ptr), detail::view::pointer_mode::value); + return r ? r.template get() : default_value; + } + + string_t value(const json_pointer& ptr, const char* default_value) const + { + return value(ptr, string_t(default_value)); + } + /// the first element or member value; a primitive value itself. Throws /// invalid_iterator.214 for null, discarded views, and empty containers. basic_json_view front() const @@ -381,6 +439,13 @@ class basic_json_view return contains(string_view_t(key.data(), key.size())); } + /// whether a JSON pointer can be resolved (never throws, as + /// basic_json::contains()) + bool contains(const json_pointer& ptr) const + { + return static_cast(detail::view::resolve_pointer(*this, detail::json_pointer_access::reference_tokens(ptr), detail::view::pointer_mode::contains)); + } + /// 1 if this is an object with a member with this key, else 0 (duplicate /// keys count once) size_type count(string_view_t key) const @@ -438,6 +503,50 @@ class basic_json_view return detail::view::view_items(*this); } + //////////////// + // conversion // + //////////////// + + /// the value converted to T, as BasicJsonType::get(): arithmetic types, + /// strings (string_view_t without a copy), std::nullptr_t, std::vector, + /// maps with string keys, and views are converted directly; other types + /// through materialize().get() + template + T get() const + { + return get_impl(detail::view::value_tag {}, detail::priority_tag<2> {}); + } + + template + T& get_to(T& v) const + { + v = get(); + return v; + } + + /// the string, without a copy; valid as long as the view is. Throws + /// type_error.302 for other types. + string_view_t get_string() const + { + if (NLOHMANN_VIEW_UNLIKELY(!is_string())) + { + detail::view::throw_type_error(302, "type must be string, but is ", type_name()); + } + return string_view_t(m_doc->str(*m_node), m_node->len); + } + + /// the text of a number as it appears in the source (e.g. "1.50", "1E2", + /// or an integer with more digits than any number type holds). Throws + /// type_error.302 for other types. + string_view_t number_token() const + { + if (NLOHMANN_VIEW_UNLIKELY(!is_number())) + { + detail::view::throw_type_error(302, "type must be number, but is ", type_name()); + } + return string_view_t(m_doc->str(*m_node), detail::view::number_length(*m_node)); + } + ///////////////// // materialize // ///////////////// @@ -478,6 +587,83 @@ class basic_json_view return k != nullptr ? basic_json_view(m_doc, k + 1) : basic_json_view(); } + // --- get() dispatch --- + + bool get_impl(detail::view::value_tag /*unused*/, detail::priority_tag<2> /*unused*/) const + { + if (NLOHMANN_VIEW_UNLIKELY(!is_boolean())) + { + detail::view::throw_type_error(302, "type must be boolean, but is ", type_name()); + } + return (m_node->flags & detail::view::node_flags::is_true) != 0; + } + + template < typename T, typename std::enable_if < std::is_arithmetic::value && !std::is_same::value, int >::type = 0 > + T get_impl(detail::view::value_tag /*unused*/, detail::priority_tag<2> /*unused*/) const + { + if (NLOHMANN_VIEW_UNLIKELY(m_node == nullptr)) + { + detail::view::throw_type_error(302, "type must be number, but is ", type_name()); + } + return detail::view::arithmetic_value(*m_doc, *m_node); + } + + std::nullptr_t get_impl(detail::view::value_tag /*unused*/, detail::priority_tag<2> /*unused*/) const + { + if (NLOHMANN_VIEW_UNLIKELY(!is_null())) + { + detail::view::throw_type_error(302, "type must be null, but is ", type_name()); + } + return nullptr; + } + + string_view_t get_impl(detail::view::value_tag /*unused*/, detail::priority_tag<2> /*unused*/) const + { + return get_string(); + } + + template + std::basic_string get_impl(detail::view::value_tag> /*unused*/, detail::priority_tag<2> /*unused*/) const + { + const string_view_t s = get_string(); + return std::basic_string(s.data(), s.size()); + } + + BasicJsonType get_impl(detail::view::value_tag /*unused*/, detail::priority_tag<2> /*unused*/) const + { + return materialize(); + } + + basic_json_view get_impl(detail::view::value_tag /*unused*/, detail::priority_tag<2> /*unused*/) const noexcept + { + return *this; + } + + template + std::vector get_impl(detail::view::value_tag> /*unused*/, detail::priority_tag<2> /*unused*/) const + { + return detail::view::vector_value(*this); + } + + template::value, int>::type = 0> + std::map get_impl(detail::view::value_tag> /*unused*/, detail::priority_tag<2> /*unused*/) const + { + return detail::view::map_value>(*this); + } + + template::value, int>::type = 0> + std::unordered_map get_impl(detail::view::value_tag> /*unused*/, detail::priority_tag<2> /*unused*/) const + { + return detail::view::map_value>(*this); + } + + /// everything else through the BasicJsonType value (from_json included) + template + T get_impl(detail::view::value_tag /*unused*/, detail::priority_tag<0> /*unused*/) const + { + return materialize().template get(); + } + const document_data* m_doc = nullptr; const node* m_node = nullptr; }; diff --git a/single_include/nlohmann/json_view.hpp b/single_include/nlohmann/json_view.hpp index becfeea2e..ef8a86835 100644 --- a/single_include/nlohmann/json_view.hpp +++ b/single_include/nlohmann/json_view.hpp @@ -27,11 +27,14 @@ #include // size_t #include // memcpy, strlen #include // distance, input_iterator_tag, iterator_traits +#include // map #include // unique_ptr #include // string #include // tuple_element, tuple_size -#include // enable_if, integral_constant, is_base_of, is_integral, is_same, remove_cv, remove_extent +#include // decay, enable_if, integral_constant, is_arithmetic, is_base_of, is_integral, is_same, remove_cv, remove_extent +#include // unordered_map #include // forward, move +#include // vector #include @@ -1608,6 +1611,12 @@ namespace view NLOHMANN_VIEW_THROW(invalid_iterator::create(id, msg, nullptr)); } +/// a parse error without a position (as those of json_pointer) +[[noreturn]] NLOHMANN_VIEW_NOINLINE inline void throw_parse_error(int id, const std::string& msg) +{ + NLOHMANN_VIEW_THROW(parse_error::create(id, 0, msg, nullptr)); +} + /*! @brief throw the exception BasicJsonType::parse would throw for this input @@ -2372,6 +2381,181 @@ NLOHMANN_JSON_NAMESPACE_END // #include +// #include +// __ _____ _____ _____ +// __| | __| | | | JSON for Modern C++ +// | | |__ | | | | | | version 3.12.0 +// |_____|_____|_____|_|___| https://github.com/nlohmann/json +// +// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann +// SPDX-License-Identifier: MIT + + + +#include // size_t +#include // uint64_t +#include // numeric_limits +#include // string, to_string + +// #include +// #include + +// #include + + +NLOHMANN_JSON_NAMESPACE_BEGIN +namespace detail +{ +namespace view +{ + +/// what resolving a JSON pointer does where it cannot continue +enum class pointer_mode +{ + unchecked, ///< as const basic_json::operator[]: a discarded view where basic_json's behavior is undefined + checked, ///< as basic_json::at(): out_of_range.401/403 + value, ///< as basic_json::value(): no out_of_range exceptions (the default value is used) + contains, ///< as basic_json::contains(): no exceptions at all +}; + +/// the outcome of reading an array index from a reference token +enum class index_status +{ + ok, + leading_zero, ///< parse_error.106 + not_number, ///< parse_error.109 + unresolved, ///< out_of_range.404 + too_large, ///< out_of_range.410 +}; + +/// reads an array index like json_pointer::array_index (RFC 6901, Sect. 4), +/// but reports errors instead of throwing them +template +index_status array_index(const StringType& s, std::size_t& idx) noexcept +{ + if (s.size() > 1 && s[0] == '0') + { + return index_status::leading_zero; + } + if (s.size() > 1 && !(s[0] >= '1' && s[0] <= '9')) + { + return index_status::not_number; + } + if (s.empty()) + { + return index_status::unresolved; + } + std::uint64_t v = 0; + for (std::size_t i = 0; i < s.size(); ++i) + { + const auto d = static_cast(static_cast(s[i])) - '0'; + if (d > 9 || v > ((std::numeric_limits::max)() - d) / 10) + { + return index_status::unresolved; // not a number, or beyond unsigned long long + } + v = (v * 10) + d; + } + if (v >= static_cast((std::numeric_limits::max)())) + { + return index_status::too_large; + } + idx = static_cast(v); + return index_status::ok; +} + +/// throws the exception json_pointer::array_index throws for this status +template +[[noreturn]] NLOHMANN_VIEW_NOINLINE void throw_array_index_error(index_status status, const StringType& s) +{ + switch (status) + { + case index_status::leading_zero: + throw_parse_error(106, concat("array index '", s, "' must not begin with '0'")); + case index_status::not_number: + throw_parse_error(109, concat("array index '", s, "' is not a number")); + case index_status::too_large: + throw_out_of_range(410, concat("array index ", s, " exceeds size_type")); // LCOV_EXCL_LINE + case index_status::unresolved: + case index_status::ok: + default: + throw_out_of_range(404, concat("unresolved reference token '", s, "'")); + } +} + +/*! +@brief resolve the reference tokens of a JSON pointer, starting at a view + +The exceptions are those basic_json throws for the same pointer; where +basic_json's behavior is undefined (a missing key or an index out of range +with const operator[]), the result is a discarded view. +*/ +template +View resolve_pointer(View cur, const Tokens& tokens, pointer_mode mode) +{ + using string_view_t = typename View::string_view_t; + const bool throwing = mode == pointer_mode::unchecked || mode == pointer_mode::checked; + for (const auto& token : tokens) + { + if (cur.is_object()) + { + const auto it = cur.find(string_view_t(token.data(), token.size())); + if (it == cur.end()) + { + if (mode == pointer_mode::checked) + { + throw_out_of_range(403, concat("key '", token, "' not found")); + } + return View(); + } + cur = *it; + } + else if (cur.is_array()) + { + if (token.size() == 1 && token[0] == '-') + { + if (throwing) + { + throw_out_of_range(402, concat("array index '-' (", std::to_string(cur.size()), ") is out of range")); + } + return View(); + } + std::size_t idx = 0; + const index_status status = array_index(token, idx); + if (status != index_status::ok) + { + const bool parse_error = status == index_status::leading_zero || status == index_status::not_number; + if (throwing || (mode == pointer_mode::value && parse_error)) + { + throw_array_index_error(status, token); + } + return View(); + } + if (idx >= cur.size()) + { + if (mode == pointer_mode::checked) + { + throw_out_of_range(401, concat("array index ", std::to_string(idx), " is out of range")); + } + return View(); + } + cur = cur[idx]; + } + else + { + if (throwing) + { + throw_out_of_range(404, concat("unresolved reference token '", token, "'")); + } + return View(); + } + } + return cur; +} + +} // namespace view +} // namespace detail +NLOHMANN_JSON_NAMESPACE_END + // #include // __ _____ _____ _____ // __| | __| | | | JSON for Modern C++ @@ -2486,6 +2670,121 @@ class string_ref } // namespace detail NLOHMANN_JSON_NAMESPACE_END +// #include +// __ _____ _____ _____ +// __| | __| | | | JSON for Modern C++ +// | | |__ | | | | | | version 3.12.0 +// |_____|_____|_____|_|___| https://github.com/nlohmann/json +// +// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann +// SPDX-License-Identifier: MIT + + + +#include // int64_t +#include // map +#include // basic_string +#include // enable_if, is_constructible +#include // unordered_map +#include // vector + +// #include +// #include + +// #include + +// #include + +// #include + +// #include + + +NLOHMANN_JSON_NAMESPACE_BEGIN +namespace detail +{ +namespace view +{ + +/// selects a conversion by its target type +template +struct value_tag {}; + +/*! +@brief the number or boolean of a node converted to an arithmetic type + +As basic_json's get() for arithmetic types: integers and floats are +converted with static_cast, booleans give 0 or 1, and other types throw +type_error.302. +*/ +template +T arithmetic_value(const document_data& d, const node& n) +{ + switch (static_cast(n.kind)) + { + case value_t::number_unsigned: + return static_cast(static_cast(integer_bits(n))); + case value_t::number_integer: + return static_cast(static_cast(static_cast(integer_bits(n)))); + case value_t::number_float: + return static_cast(float_value(d.str(n), n)); + case value_t::boolean: + return static_cast((n.flags & node_flags::is_true) != 0); + case value_t::null: + case value_t::object: + case value_t::array: + case value_t::string: + case value_t::binary: + case value_t::discarded: + default: + throw_type_error(302, "type must be number, but is ", value_type_name(static_cast(n.kind))); + } +} + +/// std::vector from an array, element by element (type_error.302 otherwise) +template +std::vector vector_value(const View& v) +{ + if (NLOHMANN_VIEW_UNLIKELY(!v.is_array())) + { + throw_type_error(302, "type must be array, but is ", v.type_name()); + } + std::vector r; + r.reserve(v.size()); + for (const View e : v) + { + r.push_back(e.template get()); + } + return r; +} + +/// a map with string keys from an object; with duplicate keys, the last +/// value is kept, as parse() does (type_error.302 for other types) +template +Map map_value(const View& v) +{ + if (NLOHMANN_VIEW_UNLIKELY(!v.is_object())) + { + throw_type_error(302, "type must be object, but is ", v.type_name()); + } + Map r; + for (auto it = v.begin(); it != v.end(); ++it) + { + const auto key = it.key(); + r[typename Map::key_type(key.data(), key.size())] = it.value().template get(); + } + return r; +} + +/// whether a map type is read member by member (its keys are made from +/// characters and a length); other maps go through basic_json +template +struct is_string_key : std::is_constructible {}; + +} // namespace view +} // namespace detail +NLOHMANN_JSON_NAMESPACE_END + NLOHMANN_JSON_NAMESPACE_BEGIN @@ -2704,6 +3003,14 @@ class basic_json_view return operator[](static_cast(idx)); } + /// the value a JSON pointer refers to; a discarded view if a key is + /// missing or an index is out of range. Other errors throw what const + /// basic_json::operator[] throws. + basic_json_view operator[](const json_pointer& ptr) const + { + return detail::view::resolve_pointer(*this, detail::json_pointer_access::reference_tokens(ptr), detail::view::pointer_mode::unchecked); + } + /// the value of the member with this key (the first one, should the key /// occur more than once). Throws type_error.304 if this is not an object, /// and out_of_range.403 if there is no such member. @@ -2751,6 +3058,51 @@ class basic_json_view return at(static_cast(idx)); } + /// the value a JSON pointer refers to; throws what basic_json::at() + /// throws if it cannot be resolved + basic_json_view at(const json_pointer& ptr) const + { + return detail::view::resolve_pointer(*this, detail::json_pointer_access::reference_tokens(ptr), detail::view::pointer_mode::checked); + } + + /// the member with this key converted to T, or the default value if there + /// is no such member (the first one, should the key occur more than + /// once). Throws type_error.306 if this is not an object. + template < typename T, typename std::enable_if < !std::is_same::type, const char*>::value, int >::type = 0 > + T value(string_view_t key, const T& default_value) const + { + if (NLOHMANN_VIEW_UNLIKELY(!is_object())) + { + detail::view::throw_type_error(306, "cannot use value() with ", type_name()); + } + const basic_json_view r = lookup(key); + return r ? r.template get() : default_value; + } + + string_t value(string_view_t key, const char* default_value) const + { + return value(key, string_t(default_value)); + } + + /// the value a JSON pointer refers to converted to T, or the default + /// value if the pointer cannot be resolved. Throws type_error.306 if this + /// is neither an object nor an array. + template < typename T, typename std::enable_if < !std::is_same::type, const char*>::value, int >::type = 0 > + T value(const json_pointer& ptr, const T& default_value) const + { + if (NLOHMANN_VIEW_UNLIKELY(!is_structured())) + { + detail::view::throw_type_error(306, "cannot use value() with ", type_name()); + } + const basic_json_view r = detail::view::resolve_pointer(*this, detail::json_pointer_access::reference_tokens(ptr), detail::view::pointer_mode::value); + return r ? r.template get() : default_value; + } + + string_t value(const json_pointer& ptr, const char* default_value) const + { + return value(ptr, string_t(default_value)); + } + /// the first element or member value; a primitive value itself. Throws /// invalid_iterator.214 for null, discarded views, and empty containers. basic_json_view front() const @@ -2817,6 +3169,13 @@ class basic_json_view return contains(string_view_t(key.data(), key.size())); } + /// whether a JSON pointer can be resolved (never throws, as + /// basic_json::contains()) + bool contains(const json_pointer& ptr) const + { + return static_cast(detail::view::resolve_pointer(*this, detail::json_pointer_access::reference_tokens(ptr), detail::view::pointer_mode::contains)); + } + /// 1 if this is an object with a member with this key, else 0 (duplicate /// keys count once) size_type count(string_view_t key) const @@ -2874,6 +3233,50 @@ class basic_json_view return detail::view::view_items(*this); } + //////////////// + // conversion // + //////////////// + + /// the value converted to T, as BasicJsonType::get(): arithmetic types, + /// strings (string_view_t without a copy), std::nullptr_t, std::vector, + /// maps with string keys, and views are converted directly; other types + /// through materialize().get() + template + T get() const + { + return get_impl(detail::view::value_tag {}, detail::priority_tag<2> {}); + } + + template + T& get_to(T& v) const + { + v = get(); + return v; + } + + /// the string, without a copy; valid as long as the view is. Throws + /// type_error.302 for other types. + string_view_t get_string() const + { + if (NLOHMANN_VIEW_UNLIKELY(!is_string())) + { + detail::view::throw_type_error(302, "type must be string, but is ", type_name()); + } + return string_view_t(m_doc->str(*m_node), m_node->len); + } + + /// the text of a number as it appears in the source (e.g. "1.50", "1E2", + /// or an integer with more digits than any number type holds). Throws + /// type_error.302 for other types. + string_view_t number_token() const + { + if (NLOHMANN_VIEW_UNLIKELY(!is_number())) + { + detail::view::throw_type_error(302, "type must be number, but is ", type_name()); + } + return string_view_t(m_doc->str(*m_node), detail::view::number_length(*m_node)); + } + ///////////////// // materialize // ///////////////// @@ -2914,6 +3317,83 @@ class basic_json_view return k != nullptr ? basic_json_view(m_doc, k + 1) : basic_json_view(); } + // --- get() dispatch --- + + bool get_impl(detail::view::value_tag /*unused*/, detail::priority_tag<2> /*unused*/) const + { + if (NLOHMANN_VIEW_UNLIKELY(!is_boolean())) + { + detail::view::throw_type_error(302, "type must be boolean, but is ", type_name()); + } + return (m_node->flags & detail::view::node_flags::is_true) != 0; + } + + template < typename T, typename std::enable_if < std::is_arithmetic::value && !std::is_same::value, int >::type = 0 > + T get_impl(detail::view::value_tag /*unused*/, detail::priority_tag<2> /*unused*/) const + { + if (NLOHMANN_VIEW_UNLIKELY(m_node == nullptr)) + { + detail::view::throw_type_error(302, "type must be number, but is ", type_name()); + } + return detail::view::arithmetic_value(*m_doc, *m_node); + } + + std::nullptr_t get_impl(detail::view::value_tag /*unused*/, detail::priority_tag<2> /*unused*/) const + { + if (NLOHMANN_VIEW_UNLIKELY(!is_null())) + { + detail::view::throw_type_error(302, "type must be null, but is ", type_name()); + } + return nullptr; + } + + string_view_t get_impl(detail::view::value_tag /*unused*/, detail::priority_tag<2> /*unused*/) const + { + return get_string(); + } + + template + std::basic_string get_impl(detail::view::value_tag> /*unused*/, detail::priority_tag<2> /*unused*/) const + { + const string_view_t s = get_string(); + return std::basic_string(s.data(), s.size()); + } + + BasicJsonType get_impl(detail::view::value_tag /*unused*/, detail::priority_tag<2> /*unused*/) const + { + return materialize(); + } + + basic_json_view get_impl(detail::view::value_tag /*unused*/, detail::priority_tag<2> /*unused*/) const noexcept + { + return *this; + } + + template + std::vector get_impl(detail::view::value_tag> /*unused*/, detail::priority_tag<2> /*unused*/) const + { + return detail::view::vector_value(*this); + } + + template::value, int>::type = 0> + std::map get_impl(detail::view::value_tag> /*unused*/, detail::priority_tag<2> /*unused*/) const + { + return detail::view::map_value>(*this); + } + + template::value, int>::type = 0> + std::unordered_map get_impl(detail::view::value_tag> /*unused*/, detail::priority_tag<2> /*unused*/) const + { + return detail::view::map_value>(*this); + } + + /// everything else through the BasicJsonType value (from_json included) + template + T get_impl(detail::view::value_tag /*unused*/, detail::priority_tag<0> /*unused*/) const + { + return materialize().template get(); + } + const document_data* m_doc = nullptr; const node* m_node = nullptr; }; diff --git a/tests/src/unit-json_view.cpp b/tests/src/unit-json_view.cpp index 3e17f9ed1..e2d2530eb 100644 --- a/tests/src/unit-json_view.cpp +++ b/tests/src/unit-json_view.cpp @@ -17,12 +17,18 @@ using nlohmann::ordered_json_document; using nlohmann::ordered_json_view; #include +#include +#include #include +#include +#include #include #include +#include #include #include #include +#include #include #include @@ -649,3 +655,368 @@ TEST_CASE("json_view element access and iteration") #endif } } + +namespace +{ +// an exception message without the context that basic_json adds with +// JSON_DIAGNOSTICS ("(/path) ") and JSON_DIAGNOSTIC_POSITIONS ("(bytes 1-2) "); +// the view's exceptions have no such context +std::string without_path(std::string msg) +{ + for (const char* prefix : + {"] (/", "] (bytes " + }) + { + const std::size_t open = msg.find(prefix); + if (open != std::string::npos) + { + msg.erase(open + 2, msg.find(") ", open) + 2 - (open + 2)); + } + } + return msg; +} + +bool has_duplicate_keys(const ordered_json_view& v) +{ + if (v.is_object() && v.size() != v.materialize().size()) + { + return true; + } + for (const ordered_json_view e : v) + { + if (e.is_structured() && has_duplicate_keys(e)) + { + return true; + } + } + return false; +} + +// compares the conversions of a view with those of ordered_json +void check_values(const ordered_json_view& v, const ordered_json& j, const std::string& text) +{ + CHECK(v.get() == j); + switch (j.type()) + { + case json::value_t::number_integer: + case json::value_t::number_unsigned: + case json::value_t::number_float: + { + // (converting a float out of range of the target type is undefined) + if (j.is_number_unsigned()) + { + CHECK(v.get() == j.get()); + } + else if (j.is_number_integer()) + { + CHECK(v.get() == j.get()); + } + const double a = v.get(); + const double b = j.get(); + CHECK(std::memcmp(&a, &b, sizeof(double)) == 0); + if (std::abs(b) < 1e9) + { + CHECK(v.get() == j.get()); + } + const auto token = v.number_token(); + CHECK(text.compare(v.source_offset(), token.size(), token.data(), token.size()) == 0); + break; + } + case json::value_t::string: + CHECK(v.get() == j.get()); + CHECK(std::string(v.get_string().data(), v.get_string().size()) == j.get()); + break; + case json::value_t::boolean: + CHECK(v.get() == j.get()); + CHECK(v.get() == j.get()); + break; + case json::value_t::null: + CHECK(v.get() == nullptr); + break; + case json::value_t::array: + CHECK(v.get>() == j.get>()); + break; + case json::value_t::object: + CHECK((v.get>() == j.get>())); + break; + case json::value_t::binary: + case json::value_t::discarded: + default: + break; + } + + // conversions to the wrong type throw what basic_json throws + if (!j.is_number()) + { + CHECK(exception_of([&] { static_cast(v.get()); }) == without_path(exception_of([&] { static_cast(j.get()); }))); + } + CHECK(exception_of([&] { static_cast(v.get()); }) == without_path(exception_of([&] { static_cast(j.get()); }))); + CHECK(exception_of([&] { static_cast(v.get()); }) == without_path(exception_of([&] { static_cast(j.get()); }))); + CHECK(exception_of([&] { static_cast(v.get()); }) == without_path(exception_of([&] { static_cast(j.get()); }))); + if (!j.is_array()) + { + CHECK(exception_of([&] { static_cast(v.get>()); }) == without_path(exception_of([&] { static_cast(j.get>()); }))); + } + if (!j.is_object()) + { + CHECK(exception_of([&] { static_cast(v.get>()); }) == without_path(exception_of([&] { static_cast(j.get>()); }))); + } + + if (v.is_array()) + { + std::size_t i = 0; + for (const ordered_json_view e : v) + { + check_values(e, j[i++], text); + } + } + else if (v.is_object()) + { + for (auto it = v.begin(); it != v.end(); ++it) + { + const std::string key(it.key().data(), it.key().size()); + if (v.size() == j.size()) // (no duplicate keys) + { + check_values(it.value(), j[key], text); + } + } + } +} + +struct record +{ + std::string name{}; + int count = 0; +}; + +void from_json(const json& j, record& r) +{ + j.at("name").get_to(r.name); + j.at("count").get_to(r.count); +} +} // namespace + +TEST_CASE("json_view values") +{ + SECTION("generated documents") + { + generator g; + for (int i = 0; i < 2000; ++i) + { + std::string text; + g.value(text, 0); + CAPTURE(text); + const ordered_json_document d = ordered_json_document::parse(text); + check_values(d.root(), ordered_json::parse(text), text); + } + } + + SECTION("floats are converted as parse() converts them") + { + std::mt19937_64 rng(5295); + std::vector tokens = {"0.1", "-0.0", "1e308", "1.7976931348623157e308", "2.2250738585072011e-308", "4.9e-324", "5e-324", + "0.1000000000000000055511151231257827021181583404541015625", "123456789012345678901234567890", + "9007199254740993", "1.00000000000000011102230246251565404236316680908203125", "7.2057594037927933e16" + }; + for (int i = 0; i < 20000; ++i) + { + const std::uint64_t bits = rng(); + double d = 0; + std::memcpy(&d, &bits, sizeof(d)); + if (!std::isfinite(d)) + { + continue; + } + std::array buf{}; + switch (i % 5) // NOLINT(hicpp-multiway-paths-covered) + { + case 0: + std::snprintf(buf.data(), buf.size(), "%.17g", d); // NOLINT(cppcoreguidelines-pro-type-vararg,hicpp-vararg) + break; + case 1: + std::snprintf(buf.data(), buf.size(), "%.15g", d); // NOLINT(cppcoreguidelines-pro-type-vararg,hicpp-vararg) + break; + case 2: + std::snprintf(buf.data(), buf.size(), "%.3e", d); // NOLINT(cppcoreguidelines-pro-type-vararg,hicpp-vararg) + break; + case 3: + std::snprintf(buf.data(), buf.size(), "%.25g", d); // NOLINT(cppcoreguidelines-pro-type-vararg,hicpp-vararg) + break; + default: + std::snprintf(buf.data(), buf.size(), "%.0f", d); // NOLINT(cppcoreguidelines-pro-type-vararg,hicpp-vararg) + break; + } + tokens.emplace_back(buf.data()); + } + using json_float = nlohmann::basic_json; + for (const auto& token : tokens) + { + CAPTURE(token); + const std::string text = "[" + token + "]"; + const double a = json_document::parse(text).root()[0].get(); + const double b = json::parse(text)[0].get(); + CHECK(std::memcmp(&a, &b, sizeof(double)) == 0); + if (std::abs(b) < 1e38) + { + const float fa = nlohmann::basic_json_document::parse(text).root()[0].get(); + const float fb = json_float::parse(text)[0].get(); + CHECK(std::memcmp(&fa, &fb, sizeof(float)) == 0); + } + } + } + + SECTION("number tokens") + { + const json_document d = json_document::parse(R"([1.50, 1E2, -0, 123456789012345678901234567890, -12, 7, "x"])"); + const json_view v = d.root(); + CHECK(v[0].number_token() == "1.50"); + CHECK(v[1].number_token() == "1E2"); + CHECK(v[2].number_token() == "-0"); + CHECK(v[3].number_token() == "123456789012345678901234567890"); + CHECK(v[4].number_token() == "-12"); + CHECK(v[5].number_token() == "7"); + CHECK_THROWS_WITH_AS(v[6].number_token(), "[json.exception.type_error.302] type must be number, but is string", json::type_error&); + CHECK_THROWS_WITH_AS(v.get_string(), "[json.exception.type_error.302] type must be string, but is array", json::type_error&); + } + + SECTION("conversions") + { + const std::string text = R"({"name": "widget", "count": 3, "tags": ["a", "b\n"], "sizes": {"s": 1, "m": 2}, "pair": [1, "x"]})"; + const json_document d = json_document::parse(text); + const json_view v = d.root(); + const json j = json::parse(text); + + // user types with from_json, and other types, through basic_json + const record r = v.get(); + CHECK(r.name == "widget"); + CHECK(r.count == 3); + CHECK((v["pair"].get>() == j["pair"].get>())); + CHECK(v["tags"].get>() == j["tags"].get>()); + CHECK((v["sizes"].get>() == j["sizes"].get>())); + CHECK(v["tags"].get>() == std::vector {"a", "b\n"}); + + // views of the elements + const auto views = v["tags"].get>(); + CHECK(views.size() == 2); + CHECK(views[1].get_string() == "b\n"); + const auto members = v.get>(); + CHECK(members.at("count").get() == 3); + CHECK(v.get()["name"].get_string() == "widget"); + + // strings without a copy point into the source text + CHECK(v["name"].get_string().data() == text.data() + text.find("widget")); +#ifdef JSON_HAS_CPP_17 + CHECK(v["name"].get() == "widget"); +#endif + + std::string name; + int count = 0; + CHECK(&v["name"].get_to(name) == &name); + v["count"].get_to(count); + CHECK(name == "widget"); + CHECK(count == 3); + + // a duplicate key: the last value, as parse() + CHECK((json_document::parse(R"({"a":1,"a":2})").root().get>() == std::map {{"a", 2}})); + + const json_view invalid; + CHECK_THROWS_WITH_AS(invalid.get(), "[json.exception.type_error.302] type must be number, but is discarded", json::type_error&); + CHECK(invalid.get().is_discarded()); + } + + SECTION("value") + { + const json_document d = json_document::parse(R"({"n": 1, "s": "text", "o": {"x": [10, 20]}})"); + const json_view v = d.root(); + const json j = v.materialize(); + CHECK(v.value("n", 0) == j.value("n", 0)); + CHECK(v.value("missing", 42) == j.value("missing", 42)); + CHECK(v.value("s", "default") == j.value("s", "default")); + CHECK(v.value("missing", "default") == j.value("missing", "default")); + CHECK(v.value(std::string("n"), 2.5) == j.value(std::string("n"), 2.5)); + CHECK(v.value(json::json_pointer("/o/x/1"), 0) == j.value(json::json_pointer("/o/x/1"), 0)); + CHECK(v.value(json::json_pointer("/o/x/5"), 0) == j.value(json::json_pointer("/o/x/5"), 0)); + CHECK(v.value(json::json_pointer("/o/y"), "none") == j.value(json::json_pointer("/o/y"), "none")); + // with a JSON pointer, arrays can be asked as well + CHECK(v["o"]["x"].value(json::json_pointer("/1"), 0) == j["o"]["x"].value(json::json_pointer("/1"), 0)); + CHECK(v["o"]["x"].value(json::json_pointer("/7"), 3) == j["o"]["x"].value(json::json_pointer("/7"), 3)); + CHECK(exception_of([&] { static_cast(v["o"]["x"].value("k", 0)); }) == without_path(exception_of([&] { static_cast(j["o"]["x"].value("k", 0)); }))); + CHECK(exception_of([&] { static_cast(v.value("s", 0)); }) == without_path(exception_of([&] { static_cast(j.value("s", 0)); }))); + CHECK(exception_of([&] { static_cast(v["n"].value("x", 0)); }) == without_path(exception_of([&] { static_cast(j["n"].value("x", 0)); }))); + CHECK(exception_of([&] { static_cast(v["n"].value(json::json_pointer("/x"), 0)); }) == without_path(exception_of([&] { static_cast(j["n"].value(json::json_pointer("/x"), 0)); }))); + } +} + +TEST_CASE("json_view JSON pointers") +{ + SECTION("every value of generated documents") + { + generator g; + for (int i = 0; i < 1000; ++i) + { + std::string text; + g.value(text, 0); + const ordered_json_document d = ordered_json_document::parse(text); + if (has_duplicate_keys(d.root())) + { + continue; + } + CAPTURE(text); + const ordered_json j = ordered_json::parse(text); + const ordered_json flat = j.flatten(); + for (const auto& leaf : flat.items()) + { + // the leaf and each of its parents + for (ordered_json::json_pointer p(leaf.key());; p = p.parent_pointer()) + { + CAPTURE(p.to_string()); + CHECK(d.root()[p].materialize() == j[p]); + CHECK(d.root().at(p).materialize() == j.at(p)); + CHECK(d.root().contains(p)); + if (p.empty()) + { + break; + } + } + } + } + } + + SECTION("errors are those of basic_json") + { + const std::string text = R"({"a": [1, {"b": null}], "c": "s", "": {"": 0}, "a~b": 1, "c/d": 2})"; + const json_document d = json_document::parse(text); + const json_view v = d.root(); + const json j = v.materialize(); + for (const char* pointer : + {"", "/", "//", "/a", "/a/0", "/a/1/b", "/a/-", "/a/01", "/a/00", "/a/1a", "/a/a", "/a/", "/a/2", "/a/99", "/a/99999999999999999999", + "/a/18446744073709551615", "/a/-1", "/a/+1", "/a/ 1", "/x", "/c/x", "/a/0/x", "/a/1/b/c", "/a~0b", "/c~1d", "/c~1d/x", "/a/1/-" + }) + { + CAPTURE(pointer); + const json::json_pointer p(pointer); + const std::string at_error = without_path(exception_of([&] { static_cast(j.at(p)); })); + CHECK(exception_of([&] { static_cast(v.at(p)); }) == at_error); + if (at_error.empty()) + { + CHECK(v.at(p).materialize() == j.at(p)); + CHECK(v[p].materialize() == j[p]); + } + else if (at_error.find("out_of_range.401") != std::string::npos || at_error.find("out_of_range.403") != std::string::npos) + { + // undefined behavior for const basic_json::operator[] + CHECK(!v[p]); + } + else + { + CHECK(exception_of([&] { static_cast(v[p]); }) == without_path(exception_of([&] { static_cast(j[p]); }))); + } + // (basic_json::contains() throws out_of_range.404 for an empty + // array index token, although it is not meant to throw; the view + // answers false) + const std::string contains_error = exception_of([&] { static_cast(j.contains(p)); }); + CHECK(v.contains(p) == (contains_error.empty() && j.contains(p))); + CHECK(exception_of([&] { static_cast(v.value(p, 5)); }) == without_path(exception_of([&] { static_cast(j.value(p, 5)); }))); + } + } +}