diff --git a/docs/mkdocs/docs/api/basic_json_view/at.md b/docs/mkdocs/docs/api/basic_json_view/at.md index 747b46299..c9f2daa8c 100644 --- a/docs/mkdocs/docs/api/basic_json_view/at.md +++ b/docs/mkdocs/docs/api/basic_json_view/at.md @@ -68,9 +68,10 @@ Strong guarantee: there are no changes to the view or the document it refers to. - Throws [`out_of_range.404`](../../home/exceptions.md#jsonexceptionout_of_range404) if `ptr` cannot be resolved because a reference token is used on a primitive value. -None of these exceptions carry a [`JSON_DIAGNOSTICS`](../macros/json_diagnostics.md) path: the view has no -`BasicJsonType` value to point at, so the exception is created without one, even if `BasicJsonType` was built with -`JSON_DIAGNOSTICS` enabled. +As for `BasicJsonType`, with [`JSON_DIAGNOSTICS`](../macros/json_diagnostics.md) these exceptions name the JSON +Pointer path of the value within the document, and with +[`JSON_DIAGNOSTIC_POSITIONS`](../macros/json_diagnostic_positions.md) its bytes in the source text (where +[`materialize()`](materialize.md) sets them). ## Complexity diff --git a/docs/mkdocs/docs/api/basic_json_view/back.md b/docs/mkdocs/docs/api/basic_json_view/back.md index 473502f0a..17cd36a0e 100644 --- a/docs/mkdocs/docs/api/basic_json_view/back.md +++ b/docs/mkdocs/docs/api/basic_json_view/back.md @@ -20,9 +20,9 @@ Strong guarantee: there are no changes to the view or the document it refers to. Throws [`invalid_iterator.214`](../../home/exceptions.md#jsonexceptioninvalid_iterator214) if the view is [null](is_null.md) or [discarded](is_discarded.md), or if it is an empty array or object. -This exception does not carry a [`JSON_DIAGNOSTICS`](../macros/json_diagnostics.md) path: the view has no -`BasicJsonType` value to point at, so the exception is created without one, even if `BasicJsonType` was built with -`JSON_DIAGNOSTICS` enabled. +As for `BasicJsonType`, with [`JSON_DIAGNOSTICS`](../macros/json_diagnostics.md) the exception names the JSON Pointer +path of the value within the document, and with [`JSON_DIAGNOSTIC_POSITIONS`](../macros/json_diagnostic_positions.md) +its bytes in the source text (where [`materialize()`](materialize.md) sets them). ## Complexity diff --git a/docs/mkdocs/docs/api/basic_json_view/front.md b/docs/mkdocs/docs/api/basic_json_view/front.md index 4cefcbd33..d983f5b3c 100644 --- a/docs/mkdocs/docs/api/basic_json_view/front.md +++ b/docs/mkdocs/docs/api/basic_json_view/front.md @@ -20,9 +20,9 @@ Strong guarantee: there are no changes to the view or the document it refers to. Throws [`invalid_iterator.214`](../../home/exceptions.md#jsonexceptioninvalid_iterator214) if the view is [null](is_null.md) or [discarded](is_discarded.md), or if it is an empty array or object. -This exception does not carry a [`JSON_DIAGNOSTICS`](../macros/json_diagnostics.md) path: the view has no -`BasicJsonType` value to point at, so the exception is created without one, even if `BasicJsonType` was built with -`JSON_DIAGNOSTICS` enabled. +As for `BasicJsonType`, with [`JSON_DIAGNOSTICS`](../macros/json_diagnostics.md) the exception names the JSON Pointer +path of the value within the document, and with [`JSON_DIAGNOSTIC_POSITIONS`](../macros/json_diagnostic_positions.md) +its bytes in the source text (where [`materialize()`](materialize.md) sets them). ## Complexity diff --git a/docs/mkdocs/docs/api/basic_json_view/get.md b/docs/mkdocs/docs/api/basic_json_view/get.md index bb9ac6f46..7ee7952bf 100644 --- a/docs/mkdocs/docs/api/basic_json_view/get.md +++ b/docs/mkdocs/docs/api/basic_json_view/get.md @@ -80,14 +80,13 @@ Strong guarantee: there are no changes to the view or the document it refers to. - For every other `T`: whatever [`materialize().get()`](../basic_json/get.md) throws — typically `type_error.302`, or whatever a user-provided `from_json()` throws. -None of the exceptions thrown directly by this function or by a `from_json()` that reads the view (the first four -bullets above) carry a -[`JSON_DIAGNOSTICS`](../macros/json_diagnostics.md) path: the view has no `BasicJsonType` value to point at. An -exception thrown while converting through `materialize()` (the last bullet) is different: it is thrown by a real -`BasicJsonType` value, so it **does** carry a `JSON_DIAGNOSTICS` path if `BasicJsonType` was built with it enabled. -That path is relative to the root of the view's subtree, because `materialize()` builds only the subtree (with parent -pointers): it is empty if the failing value is the subtree's root itself, and it never is the path within the whole -document. +The exceptions thrown directly by this function or by a `from_json()` that reads the view (the first four bullets +above) carry the context of those of `BasicJsonType`: with [`JSON_DIAGNOSTICS`](../macros/json_diagnostics.md) the +JSON Pointer path of the value within the document, and with +[`JSON_DIAGNOSTIC_POSITIONS`](../macros/json_diagnostic_positions.md) its bytes in the source text. An exception +thrown while converting through `materialize()` (the last bullet) is thrown by the materialized `BasicJsonType` value +instead: its path is relative to the root of the view's subtree, because `materialize()` builds only the subtree +(with parent pointers), so it is empty if the failing value is the subtree's root itself. ## Complexity diff --git a/docs/mkdocs/docs/api/basic_json_view/get_string.md b/docs/mkdocs/docs/api/basic_json_view/get_string.md index 7539c540d..039dff6bc 100644 --- a/docs/mkdocs/docs/api/basic_json_view/get_string.md +++ b/docs/mkdocs/docs/api/basic_json_view/get_string.md @@ -22,9 +22,9 @@ Strong guarantee: there are no changes to the view or the document it refers to. Throws [`type_error.302`](../../home/exceptions.md#jsonexceptiontype_error302) if the value is not a string; example: `"type must be string, but is array"`. -This exception does not carry a [`JSON_DIAGNOSTICS`](../macros/json_diagnostics.md) path: the view has no -`BasicJsonType` value to point at, so the exception is created without one, even if `BasicJsonType` was built with -`JSON_DIAGNOSTICS` enabled. +As for `BasicJsonType`, with [`JSON_DIAGNOSTICS`](../macros/json_diagnostics.md) the exception names the JSON Pointer +path of the value within the document, and with [`JSON_DIAGNOSTIC_POSITIONS`](../macros/json_diagnostic_positions.md) +its bytes in the source text (where [`materialize()`](materialize.md) sets them). ## Complexity diff --git a/docs/mkdocs/docs/api/basic_json_view/number_token.md b/docs/mkdocs/docs/api/basic_json_view/number_token.md index 97d7a2ed2..3fa5b982c 100644 --- a/docs/mkdocs/docs/api/basic_json_view/number_token.md +++ b/docs/mkdocs/docs/api/basic_json_view/number_token.md @@ -22,9 +22,9 @@ Strong guarantee: there are no changes to the view or the document it refers to. Throws [`type_error.302`](../../home/exceptions.md#jsonexceptiontype_error302) if the value is not a number; example: `"type must be number, but is string"`. -This exception does not carry a [`JSON_DIAGNOSTICS`](../macros/json_diagnostics.md) path: the view has no -`BasicJsonType` value to point at, so the exception is created without one, even if `BasicJsonType` was built with -`JSON_DIAGNOSTICS` enabled. +As for `BasicJsonType`, with [`JSON_DIAGNOSTICS`](../macros/json_diagnostics.md) the exception names the JSON Pointer +path of the value within the document, and with [`JSON_DIAGNOSTIC_POSITIONS`](../macros/json_diagnostic_positions.md) +its bytes in the source text (where [`materialize()`](materialize.md) sets them). ## Complexity diff --git a/docs/mkdocs/docs/api/basic_json_view/operator[].md b/docs/mkdocs/docs/api/basic_json_view/operator[].md index 9c09a807b..78972277b 100644 --- a/docs/mkdocs/docs/api/basic_json_view/operator[].md +++ b/docs/mkdocs/docs/api/basic_json_view/operator[].md @@ -73,9 +73,10 @@ Strong guarantee: there are no changes to the view or the document it refers to. - [`parse_error.109`](../../home/exceptions.md#jsonexceptionparse_error109) if an array index in `ptr` is not a number. -None of these exceptions carry a [`JSON_DIAGNOSTICS`](../macros/json_diagnostics.md) path: the view has no -`BasicJsonType` value to point at, so the exception is created without one, even if `BasicJsonType` was built with -`JSON_DIAGNOSTICS` enabled. +As for `BasicJsonType`, with [`JSON_DIAGNOSTICS`](../macros/json_diagnostics.md) these exceptions name the JSON +Pointer path of the value within the document, and with +[`JSON_DIAGNOSTIC_POSITIONS`](../macros/json_diagnostic_positions.md) its bytes in the source text (where +[`materialize()`](materialize.md) sets them). ## Complexity diff --git a/docs/mkdocs/docs/api/basic_json_view/value.md b/docs/mkdocs/docs/api/basic_json_view/value.md index 7159325b2..a987b34c9 100644 --- a/docs/mkdocs/docs/api/basic_json_view/value.md +++ b/docs/mkdocs/docs/api/basic_json_view/value.md @@ -61,9 +61,10 @@ Strong guarantee: there are no changes to the view or the document it refers to. `default_value` instead of throwing, exactly as [`BasicJsonType::value`](../basic_json/value.md) catches `out_of_range` and returns `default_value`. -None of these exceptions carry a [`JSON_DIAGNOSTICS`](../macros/json_diagnostics.md) path: the view has no -`BasicJsonType` value to point at, so the exception is created without one, even if `BasicJsonType` was built with -`JSON_DIAGNOSTICS` enabled. +As for `BasicJsonType`, with [`JSON_DIAGNOSTICS`](../macros/json_diagnostics.md) these exceptions name the JSON +Pointer path of the value within the document, and with +[`JSON_DIAGNOSTIC_POSITIONS`](../macros/json_diagnostic_positions.md) its bytes in the source text (where +[`materialize()`](materialize.md) sets them). ## Complexity diff --git a/docs/mkdocs/docs/features/json_view.md b/docs/mkdocs/docs/features/json_view.md index 09779dba2..3074e4b0a 100644 --- a/docs/mkdocs/docs/features/json_view.md +++ b/docs/mkdocs/docs/features/json_view.md @@ -134,9 +134,12 @@ document: `#!cpp auto v = json_document::parse(text).root();` does not compile. - **Macros.** [`JSON_STRICT_NUL_HANDLING`](../api/macros/json_strict_nul_handling.md) and [`JSON_NOEXCEPTION`](../api/macros/json_noexception.md)/[`JSON_THROW_USER`](../api/macros/json_throw_user.md) behave the same way they do for ``. These are the macros that change what the parser accepts or - how it reports errors. The others either do not apply ([`JSON_DIAGNOSTICS`](../api/macros/json_diagnostics.md) - needs parent pointers in `basic_json` values) or only affect the values `materialize()` produces, through - `BasicJsonType`. + how it reports errors. With [`JSON_DIAGNOSTICS`](../api/macros/json_diagnostics.md) and + [`JSON_DIAGNOSTIC_POSITIONS`](../api/macros/json_diagnostic_positions.md), the exceptions of views name the path + and the bytes of the value as those of `#!cpp basic_json` do; as a view has neither parent pointers nor stored + positions, both are found from the document's index when the exception is thrown, which takes time linear in the + size of the document (only for an exception). The other macros only affect the values `materialize()` produces, + through `BasicJsonType`. ## What is different @@ -167,10 +170,6 @@ document: `#!cpp auto v = json_document::parse(text).root();` does not compile. [`at`](../api/basic_json_view/at.md) throws for every missing value. - **Duplicate keys: lookups find the first member, `parse()` keeps the last.** See [Duplicate keys](#duplicate-keys) below. -- **No [`JSON_DIAGNOSTICS`](../api/macros/json_diagnostics.md) path.** Exceptions thrown by `basic_json_view`'s own - element access and lookup functions never carry the JSON Pointer path `JSON_DIAGNOSTICS` would otherwise add: the - view has no `basic_json` value to point at, so the exception is created without one, regardless of how - `BasicJsonType` was built. - **Ordering comparisons are not provided** by `basic_json_view` — there is no `#!cpp operator<`. [`operator==`](../api/basic_json_view/operator_eq.md) and [`operator!=`](../api/basic_json_view/operator_ne.md) are provided, though: two views, or a view and a `BasicJsonType` value, compare equal exactly when @@ -329,14 +328,10 @@ Its body may only use what views and `basic_json` have in common: the type check a view does not have; such a type is converted through `materialize()`. A `from_json()` for `json` only is converted through `materialize()` as well. -Two things differ when a `from_json()` reads the view: - -- **Duplicate keys.** Members looked up by name (`at()`, `value()`, ... and thus the macros) find the **first** member - with a repeated key, see [Duplicate keys](#duplicate-keys); `#!cpp materialize().get()` would see the last one. - Containers built from an object, such as `#!cpp std::map`, keep the **last** value either way, as - [`parse()`](../api/basic_json/parse.md) does. -- **[`JSON_DIAGNOSTICS`](../api/macros/json_diagnostics.md).** Exceptions thrown by the view carry no path to the - value. +One thing differs when a `from_json()` reads the view: members looked up by name (`at()`, `value()`, ... and thus +the macros) find the **first** member with a repeated key, see [Duplicate keys](#duplicate-keys); +`#!cpp materialize().get()` would see the last one. Containers built from an object, such as `#!cpp std::map`, +keep the **last** value either way, as [`parse()`](../api/basic_json/parse.md) does. `to_json()` is unaffected: it writes `basic_json` values, never views. diff --git a/include/nlohmann/detail/exceptions.hpp b/include/nlohmann/detail/exceptions.hpp index ee2c0feec..144bb770c 100644 --- a/include/nlohmann/detail/exceptions.hpp +++ b/include/nlohmann/detail/exceptions.hpp @@ -431,6 +431,28 @@ std::nullptr_t exception_context(const View& /*unused*/) noexcept return nullptr; } +namespace view +{ +// the path and bytes of the value of a view (defined in ) +template +std::string diagnostics_of(const View& v); +} // namespace view + +// what goes in front of the message of an exception about j: nothing for a +// basic_json value (exception_context() passes the value itself), the context +// basic_json would add (JSON_DIAGNOSTICS, JSON_DIAGNOSTIC_POSITIONS) for a view +template::value, int> = 0> +const char* exception_prefix(const BasicJsonType& /*unused*/) noexcept +{ + return ""; +} + +template::value, int> = 0> +std::string exception_prefix(const View& v) +{ + return view::diagnostics_of(v); +} + /*! @brief throws because @a j does not have the type a conversion expects @param[in] expected the expected type(s), e.g. "array" or "binary or array" @@ -442,7 +464,7 @@ JSON_HEDLEY_NO_RETURN JSON_HEDLEY_NEVER_INLINE void throw_type_must_be(const cha { static_cast(expected); // unused when JSON_NOEXCEPTION is defined static_cast(j); - JSON_THROW(type_error::create(exception_id::type_mismatch, concat("type must be ", expected, ", but is ", j.type_name()), exception_context(j))); + JSON_THROW(type_error::create(exception_id::type_mismatch, concat(exception_prefix(j), "type must be ", expected, ", but is ", j.type_name()), exception_context(j))); } /*! diff --git a/include/nlohmann/detail/macro_scope.hpp b/include/nlohmann/detail/macro_scope.hpp index 65394b504..208c1c3ce 100644 --- a/include/nlohmann/detail/macro_scope.hpp +++ b/include/nlohmann/detail/macro_scope.hpp @@ -347,7 +347,7 @@ return ej_pair.second == j; \ }); \ if (it != std::end(m)) e = it->first; \ - else ::nlohmann::detail::templated_json_throw(nlohmann::detail::out_of_range::create(nlohmann::detail::exception_id::value_out_of_range, nlohmann::detail::concat("enum value out of range for " #ENUM_TYPE ": ", nlohmann::detail::from_json_dump(j)), nlohmann::detail::exception_context(j))); \ + else ::nlohmann::detail::templated_json_throw(nlohmann::detail::out_of_range::create(nlohmann::detail::exception_id::value_out_of_range, nlohmann::detail::concat(nlohmann::detail::exception_prefix(j), "enum value out of range for " #ENUM_TYPE ": ", nlohmann::detail::from_json_dump(j)), nlohmann::detail::exception_context(j))); \ } // Ugly macros to avoid uglier copy-paste when specializing basic_json. They diff --git a/include/nlohmann/detail/view/edit.hpp b/include/nlohmann/detail/view/edit.hpp index c542c1141..b572b4848 100644 --- a/include/nlohmann/detail/view/edit.hpp +++ b/include/nlohmann/detail/view/edit.hpp @@ -157,7 +157,7 @@ class editor node* const o = own(object); if (o->kind != node_kind::object && o->kind != node_kind::null) { - throw_type_error(exception_id::subscript_wrong_type, "cannot use operator[] with a string argument with ", object.type_name()); + throw_type_error(site_of(object), exception_id::subscript_wrong_type, "cannot use operator[] with a string argument with ", object.type_name()); } check_utf8(key.data(), key.size()); const encoded e = encode(std::forward(value)); @@ -208,9 +208,9 @@ class editor node* const a = own(array); if (a->kind != node_kind::array) { - throw_type_error(exception_id::subscript_wrong_type, "cannot use operator[] with a numeric argument with ", array.type_name()); + throw_type_error(site_of(array), exception_id::subscript_wrong_type, "cannot use operator[] with a numeric argument with ", array.type_name()); } - check_index(idx, a->len); + check_index(a, idx, a->len); const encoded e = encode(std::forward(value)); node* const slot = const_cast(nav::value(element_at(m_doc, a, idx))); // NOLINT(cppcoreguidelines-pro-type-const-cast) assign(slot, e, a, true); @@ -225,7 +225,7 @@ class editor node* const a = own(array); if (a->kind != node_kind::array && a->kind != node_kind::null) { - throw_type_error(exception_id::push_back_wrong_type, "cannot use push_back() with ", array.type_name()); + throw_type_error(site_of(array), exception_id::push_back_wrong_type, "cannot use push_back() with ", array.type_name()); } const encoded e = encode(std::forward(value)); if (a->kind == node_kind::null) @@ -249,9 +249,9 @@ class editor node* const a = own(array); if (a->kind != node_kind::array) { - throw_type_error(exception_id::insert_wrong_type, "cannot use insert() with ", array.type_name()); + throw_type_error(site_of(array), exception_id::insert_wrong_type, "cannot use insert() with ", array.type_name()); } - check_index(idx, a->len + 1); + check_index(a, idx, a->len + 1); const encoded e = encode(std::forward(value)); node* const slot = new_slot(e); node* const h = block_of(m_doc, a, 1); @@ -269,7 +269,7 @@ class editor node* const o = own(object); if (o->kind != node_kind::object) { - throw_type_error(exception_id::erase_wrong_type, "cannot use erase() with ", object.type_name()); + throw_type_error(site_of(object), exception_id::erase_wrong_type, "cannot use erase() with ", object.type_name()); } for (const node* k = nav::first(m_doc, o), *end = nav::end(m_doc, o); k != end; k = document_data::after(k + 1)) { @@ -287,9 +287,9 @@ class editor node* const a = own(array); if (a->kind != node_kind::array) { - throw_type_error(exception_id::erase_wrong_type, "cannot use erase() with ", array.type_name()); + throw_type_error(site_of(array), exception_id::erase_wrong_type, "cannot use erase() with ", array.type_name()); } - check_index(idx, a->len); + check_index(a, idx, a->len); node* const h = block_of(m_doc, a, 0); std::memmove(h + 1 + idx, h + 2 + idx, (h->next - 2 - idx) * sizeof(node)); --h->next; @@ -316,11 +316,11 @@ class editor return const_cast(v.m_node); // NOLINT(cppcoreguidelines-pro-type-const-cast): the nodes belong to this document } - static void check_index(std::size_t idx, std::size_t limit) + void check_index(const node* array, std::size_t idx, std::size_t limit) const { if (idx >= limit) { - throw_array_index_out_of_range(idx); + throw_array_index_out_of_range(error_site(&m_doc, array), idx); } } diff --git a/include/nlohmann/detail/view/errors.hpp b/include/nlohmann/detail/view/errors.hpp index 590bb2edb..e651fdb06 100644 --- a/include/nlohmann/detail/view/errors.hpp +++ b/include/nlohmann/detail/view/errors.hpp @@ -10,11 +10,15 @@ #include // min #include // size_t -#include // string +#include // string, to_string +#include // vector #include #include +#include #include +#include +#include NLOHMANN_JSON_NAMESPACE_BEGIN namespace detail @@ -22,8 +26,277 @@ namespace detail namespace view { +/*! +@brief the value an exception is about + +basic_json passes the value to the exception, which then names its path +(JSON_DIAGNOSTICS) and its bytes in the source (JSON_DIAGNOSTIC_POSITIONS). A +view's value has no parent pointer and no stored positions: both are found +from the document and the node, when the exception is thrown (so only the +error path pays for them). Without the two macros, a site is empty. +*/ +class error_site +{ + public: + error_site() noexcept = default; + + error_site(const document_data* d, const node* n) noexcept +#if JSON_DIAGNOSTICS || JSON_DIAGNOSTIC_POSITIONS + : m_doc(d), m_node(n) +#endif + { + static_cast(d); + static_cast(n); + } + +#if JSON_DIAGNOSTICS || JSON_DIAGNOSTIC_POSITIONS + const document_data* doc() const noexcept + { + return m_doc; + } + const node* value() const noexcept + { + return m_node; + } + + private: + const document_data* m_doc = nullptr; + const node* m_node = nullptr; +#endif +}; + +/// the site of a view's value (the views make this a friend) +struct view_access +{ + template + static error_site site(const View& v) noexcept + { + return error_site(v.m_doc, v.m_node); + } +}; + +template +error_site site_of(const View& v) noexcept +{ + return view_access::site(v); +} + +#if JSON_DIAGNOSTICS +/*! +@brief the JSON pointer tokens from the root of document @a d to node @a target + +Nodes have no parent index, so the path is found by descending from the root: +before the first edit, only into the subtree that contains the target (a +container spans the next `next` nodes); after edits, elements can live in +other sequences, so every container is searched. Iterative (the nesting depth +is limited by memory only). False if the target is not found (then tokens is +unspecified). +*/ +template +bool value_path(const document_data& d, const node* target, std::vector& tokens) +{ + using nav = navigation; + struct frame + { + const node* pos; + const node* end; + std::size_t index; + bool object; + }; + const node* const root = d.nodes; + if (target == root) + { + return true; + } + if (!is_container(*root)) + { + return false; + } + std::vector stack; + stack.push_back(frame{nav::first(d, root), nav::end(d, root), 0, root->kind == node_kind::object}); + // (tokens[i] leads from stack[i] to stack[i + 1], or to the target) + while (!stack.empty()) + { + frame& f = stack.back(); + if (f.pos == f.end) + { + stack.pop_back(); + if (!tokens.empty()) + { + tokens.pop_back(); + } + continue; + } + const node* entry = f.pos; + std::string token; + if (f.object) + { + // (as basic_json: the key up to a null byte) + token.assign(d.str(*entry), entry->len); + const std::size_t nul = token.find('\0'); + if (nul != std::string::npos) + { + token.resize(nul); + } + ++entry; + } + else + { + token = std::to_string(f.index); + } + const node* const child = nav::value(entry); + f.pos = document_data::after(entry); + ++f.index; + if (child == target) + { + tokens.push_back(token); + return true; + } + if (is_container(*child) && (nav::editable() || (target > child && target < child + child->next))) + { + tokens.push_back(token); + stack.push_back(frame{nav::first(d, child), nav::end(d, child), 0, child->kind == node_kind::object}); + } + } + return false; +} +#endif + +#if JSON_DIAGNOSTIC_POSITIONS +/*! +@brief the bytes [start, end) of node @a n in the source, as materialize() sets them + +Scalars as position_source::set_scalar; an array or object from its opening +bracket to its closing bracket, which is found by following the chain of last +elements to a scalar (or an empty container) and scanning from there to the +closing brackets. None (npos) for values written by edits, and for containers +whose last element has none, as with materialize(). +*/ +template +void value_range(const document_data& d, const node* n, std::size_t& start, std::size_t& end) +{ + using nav = navigation; + position_source p; + start = std::string::npos; + end = std::string::npos; + if (!is_container(*n)) + { + p.set_scalar(d, *n); + start = p.start; + end = p.end; + return; + } + if ((n->flags & node_flags::is_new) != 0 || n->off >= d.size) + { + return; + } + // the open brackets: n, then the last element as long as it is a non-empty container + std::size_t closing = 0; + std::size_t pos = std::string::npos; + for (const node* c = n;;) + { + if (is_container(*c)) + { + if ((c->flags & node_flags::is_new) != 0 || c->off >= d.size) + { + return; + } + ++closing; + const node* const first = nav::first(d, c); + const node* const last_end = nav::end(d, c); + if (first == last_end) + { + pos = std::size_t{c->off} + 1; + break; + } + const std::size_t value_offset = c->kind == node_kind::object ? 1 : 0; + const node* last = first; + for (const node* e = document_data::after(first + value_offset); e != last_end; e = document_data::after(e + value_offset)) + { + last = e; + } + c = nav::value(last + value_offset); + continue; + } + p.set_scalar(d, *c); + pos = p.end; + break; + } + for (; closing != 0 && pos != std::string::npos; --closing) + { + pos = position_source::container_end(d.base[0], d.size, pos); + } + if (pos != std::string::npos) + { + start = n->off; + end = pos; + } +} +#endif + +/// the context basic_json puts in front of the message of an exception about +/// a value: "(/path) " with JSON_DIAGNOSTICS (none for the root), then +/// "(bytes a-b) " with JSON_DIAGNOSTIC_POSITIONS (with JSON_DIAGNOSTICS, only +/// if there is a path) +inline std::string diagnostics(const error_site site) +{ +#if JSON_DIAGNOSTICS || JSON_DIAGNOSTIC_POSITIONS + if (site.doc() == nullptr || site.value() == nullptr) + { + return ""; + } + const document_data& d = *site.doc(); + const bool edited = d.edits != nullptr; + std::string bytes; +#if JSON_DIAGNOSTIC_POSITIONS + std::size_t start = 0; + std::size_t end = 0; + if (edited) + { + value_range(d, site.value(), start, end); + } + else + { + value_range(d, site.value(), start, end); + } + if (start != std::string::npos && end != std::string::npos) + { + bytes = concat("(bytes ", std::to_string(start), "-", std::to_string(end), ") "); + } +#endif +#if JSON_DIAGNOSTICS + std::vector tokens; + const bool found = edited ? value_path(d, site.value(), tokens) : value_path(d, site.value(), tokens); + if (!found || tokens.empty()) + { + return ""; + } + std::string path; + for (const std::string& token : tokens) + { + path.push_back('/'); + path += detail::escape(token); + } + return concat('(', path, ") ", bytes); +#else + return bytes; +#endif +#else + static_cast(site); + return ""; +#endif +} + +/// the context of an exception about view @a v (see exception_prefix()) +template +std::string diagnostics_of(const View& v) +{ + return diagnostics(site_of(v)); +} + // Exceptions are thrown out of line, so that the accessors that may throw stay -// small enough to be inlined. +// small enough to be inlined. The overloads with an error_site put its context +// in front of the message. [[noreturn]] NLOHMANN_VIEW_NOINLINE inline void throw_type_error(exception_id id, const char* prefix, const char* type) { @@ -45,6 +318,21 @@ namespace view NLOHMANN_VIEW_THROW(invalid_iterator::create(id, msg, nullptr)); } +[[noreturn]] NLOHMANN_VIEW_NOINLINE inline void throw_type_error(const error_site site, exception_id id, const char* prefix, const char* type) +{ + NLOHMANN_VIEW_THROW(type_error::create(id, concat(diagnostics(site), prefix, type), nullptr)); +} + +[[noreturn]] NLOHMANN_VIEW_NOINLINE inline void throw_out_of_range(const error_site site, exception_id id, const std::string& msg) +{ + NLOHMANN_VIEW_THROW(out_of_range::create(id, concat(diagnostics(site), msg), nullptr)); +} + +[[noreturn]] NLOHMANN_VIEW_NOINLINE inline void throw_invalid_iterator(const error_site site, exception_id id, const char* msg) +{ + NLOHMANN_VIEW_THROW(invalid_iterator::create(id, concat(diagnostics(site), msg), nullptr)); +} + /// a parse error without a position (as those of json_pointer) [[noreturn]] NLOHMANN_VIEW_NOINLINE inline void throw_parse_error(exception_id id, const std::string& msg) { @@ -53,22 +341,22 @@ namespace view /// out_of_range.401: the array index @a idx is not below the size template -[[noreturn]] NLOHMANN_VIEW_NOINLINE void throw_array_index_out_of_range(const Index idx) +[[noreturn]] NLOHMANN_VIEW_NOINLINE void throw_array_index_out_of_range(const error_site site, const Index idx) { - throw_out_of_range(exception_id::array_index_out_of_range, concat("array index ", std::to_string(idx), " is out of range")); + throw_out_of_range(site, exception_id::array_index_out_of_range, concat("array index ", std::to_string(idx), " is out of range")); } /// out_of_range.402: the JSON Pointer token '-' of an array of @a size elements -[[noreturn]] NLOHMANN_VIEW_NOINLINE inline void throw_array_index_past_end(std::size_t size) +[[noreturn]] NLOHMANN_VIEW_NOINLINE inline void throw_array_index_past_end(const error_site site, std::size_t size) { - throw_out_of_range(exception_id::pointer_past_the_end_index, concat("array index '-' (", std::to_string(size), ") is out of range")); + throw_out_of_range(site, exception_id::pointer_past_the_end_index, concat("array index '-' (", std::to_string(size), ") is out of range")); } /// out_of_range.403: the object has no member @a key template -[[noreturn]] NLOHMANN_VIEW_NOINLINE void throw_key_not_found(const Key& key) +[[noreturn]] NLOHMANN_VIEW_NOINLINE void throw_key_not_found(const error_site site, const Key& key) { - throw_out_of_range(exception_id::key_not_found, concat("key '", key, "' not found")); + throw_out_of_range(site, exception_id::key_not_found, concat("key '", key, "' not found")); } /// SAX handler of the error replay: accepts every event without storing it, diff --git a/include/nlohmann/detail/view/iterator.hpp b/include/nlohmann/detail/view/iterator.hpp index 9d8246210..e53946912 100644 --- a/include/nlohmann/detail/view/iterator.hpp +++ b/include/nlohmann/detail/view/iterator.hpp @@ -67,9 +67,17 @@ class view_iterator /// @param[in] pos the element, or the key of the member /// @param[in] object whether pos is a key (its value is the next node) - view_iterator(const document_data* d, const node* pos, bool object) noexcept + /// @param[in] container the value iterated over (the context of the + /// exceptions, kept only with JSON_DIAGNOSTICS or + /// JSON_DIAGNOSTIC_POSITIONS) + view_iterator(const document_data* d, const node* pos, bool object, const node* container) noexcept : m_doc(d), m_pos(pos), m_value_offset(object ? 1 : 0) - {} +#if JSON_DIAGNOSTICS || JSON_DIAGNOSTIC_POSITIONS + , m_container(container) +#endif + { + static_cast(container); + } NLOHMANN_VIEW_ALWAYS_INLINE View operator*() const noexcept { @@ -110,7 +118,7 @@ class view_iterator { if (NLOHMANN_VIEW_UNLIKELY(m_value_offset == 0)) { - throw_invalid_iterator(exception_id::iterator_key_not_object, "cannot use key() for non-object iterators"); + throw_invalid_iterator(site(), exception_id::iterator_key_not_object, "cannot use key() for non-object iterators"); } return string_view_t(m_doc->str(*m_pos), m_pos->len); } @@ -127,9 +135,22 @@ class view_iterator } private: + /// the value iterated over, as the context of an exception + error_site site() const noexcept + { +#if JSON_DIAGNOSTICS || JSON_DIAGNOSTIC_POSITIONS + return error_site(m_doc, m_container); +#else + return error_site(); +#endif + } + const document_data* m_doc = nullptr; const node* m_pos = nullptr; std::size_t m_value_offset = 0; ///< 1 for objects: the value follows its key +#if JSON_DIAGNOSTICS || JSON_DIAGNOSTIC_POSITIONS + const node* m_container = nullptr; +#endif }; /*! diff --git a/include/nlohmann/detail/view/pointer.hpp b/include/nlohmann/detail/view/pointer.hpp index be5aa14de..b7df4224d 100644 --- a/include/nlohmann/detail/view/pointer.hpp +++ b/include/nlohmann/detail/view/pointer.hpp @@ -45,6 +45,7 @@ index_status array_index(const StringType& s, std::size_t& idx) noexcept /// throws the exception json_pointer::array_index throws for this status template +/// (without a context, as json_pointer::array_index) [[noreturn]] NLOHMANN_VIEW_NOINLINE void throw_array_index_error(index_status status, const StringType& s) { switch (status) @@ -83,7 +84,7 @@ View resolve_pointer(View cur, const Tokens& tokens, pointer_mode mode) { if (mode == pointer_mode::checked) { - throw_key_not_found(token); + throw_key_not_found(site_of(cur), token); } return View(); } @@ -95,7 +96,7 @@ View resolve_pointer(View cur, const Tokens& tokens, pointer_mode mode) { if (throwing) { - throw_array_index_past_end(cur.size()); + throw_array_index_past_end(site_of(cur), cur.size()); } return View(); } @@ -114,7 +115,7 @@ View resolve_pointer(View cur, const Tokens& tokens, pointer_mode mode) { if (mode == pointer_mode::checked) { - throw_array_index_out_of_range(idx); + throw_array_index_out_of_range(site_of(cur), idx); } return View(); } @@ -124,7 +125,7 @@ View resolve_pointer(View cur, const Tokens& tokens, pointer_mode mode) { if (throwing) { - throw_out_of_range(exception_id::pointer_unresolved, concat("unresolved reference token '", token, "'")); + throw_out_of_range(site_of(cur), exception_id::pointer_unresolved, concat("unresolved reference token '", token, "'")); } return View(); } diff --git a/include/nlohmann/detail/view/value.hpp b/include/nlohmann/detail/view/value.hpp index 4b54a7785..ffdcc3605 100644 --- a/include/nlohmann/detail/view/value.hpp +++ b/include/nlohmann/detail/view/value.hpp @@ -60,7 +60,7 @@ NLOHMANN_VIEW_ALWAYS_INLINE T arithmetic_value(const document_data& d, const nod case value_t::binary: case value_t::discarded: default: - throw_type_error(exception_id::type_mismatch, "type must be number, but is ", value_type_name(static_cast(n.kind))); + throw_type_error(error_site(&d, &n), exception_id::type_mismatch, "type must be number, but is ", value_type_name(static_cast(n.kind))); } } @@ -70,7 +70,7 @@ std::vector vector_value(const View& v) { if (NLOHMANN_VIEW_UNLIKELY(!v.is_array())) { - throw_type_error(exception_id::type_mismatch, "type must be array, but is ", v.type_name()); + throw_type_error(site_of(v), exception_id::type_mismatch, "type must be array, but is ", v.type_name()); } std::vector r; r.reserve(v.size()); @@ -88,7 +88,7 @@ Map map_value(const View& v) { if (NLOHMANN_VIEW_UNLIKELY(!v.is_object())) { - throw_type_error(exception_id::type_mismatch, "type must be object, but is ", v.type_name()); + throw_type_error(site_of(v), exception_id::type_mismatch, "type must be object, but is ", v.type_name()); } Map r; for (auto it = v.begin(); it != v.end(); ++it) diff --git a/include/nlohmann/json_view.hpp b/include/nlohmann/json_view.hpp index 9d26d12ea..880610ca5 100644 --- a/include/nlohmann/json_view.hpp +++ b/include/nlohmann/json_view.hpp @@ -285,7 +285,7 @@ class basic_json_view { return {}; } - detail::view::throw_type_error(detail::exception_id::subscript_wrong_type, "cannot use operator[] with a string argument with ", type_name()); + detail::view::throw_type_error(site(), detail::exception_id::subscript_wrong_type, "cannot use operator[] with a string argument with ", type_name()); } return lookup(key); } @@ -311,7 +311,7 @@ class basic_json_view { return {}; } - detail::view::throw_type_error(detail::exception_id::subscript_wrong_type, "cannot use operator[] with a numeric argument with ", type_name()); + detail::view::throw_type_error(site(), detail::exception_id::subscript_wrong_type, "cannot use operator[] with a numeric argument with ", type_name()); } return idx < m_node->len ? element(idx) : basic_json_view(); } @@ -344,12 +344,12 @@ class basic_json_view { if (NLOHMANN_VIEW_UNLIKELY(!is_object())) { - detail::view::throw_type_error(detail::exception_id::at_wrong_type, "cannot use at() with ", type_name()); + detail::view::throw_type_error(site(), detail::exception_id::at_wrong_type, "cannot use at() with ", type_name()); } const basic_json_view r = lookup(key); if (NLOHMANN_VIEW_UNLIKELY(r.is_discarded())) { - detail::view::throw_key_not_found(std::string(key.data(), key.size())); + detail::view::throw_key_not_found(site(), std::string(key.data(), key.size())); } return r; } @@ -370,11 +370,11 @@ class basic_json_view { if (NLOHMANN_VIEW_UNLIKELY(!is_array())) { - detail::view::throw_type_error(detail::exception_id::at_wrong_type, "cannot use at() with ", type_name()); + detail::view::throw_type_error(site(), detail::exception_id::at_wrong_type, "cannot use at() with ", type_name()); } if (NLOHMANN_VIEW_UNLIKELY(idx >= m_node->len)) { - detail::view::throw_array_index_out_of_range(idx); + detail::view::throw_array_index_out_of_range(site(), idx); } return element(idx); } @@ -402,7 +402,7 @@ class basic_json_view { if (NLOHMANN_VIEW_UNLIKELY(!is_object())) { - detail::view::throw_type_error(detail::exception_id::value_wrong_type, "cannot use value() with ", type_name()); + detail::view::throw_type_error(site(), detail::exception_id::value_wrong_type, "cannot use value() with ", type_name()); } const basic_json_view r = lookup(key); return r.is_discarded() ? default_value : r.template get(); @@ -412,7 +412,7 @@ class basic_json_view { if (NLOHMANN_VIEW_UNLIKELY(!is_object())) { - detail::view::throw_type_error(detail::exception_id::value_wrong_type, "cannot use value() with ", type_name()); + detail::view::throw_type_error(site(), detail::exception_id::value_wrong_type, "cannot use value() with ", type_name()); } // (the default is made only if there is no such member) const basic_json_view r = lookup(key); @@ -427,7 +427,7 @@ class basic_json_view { if (NLOHMANN_VIEW_UNLIKELY(!is_structured())) { - detail::view::throw_type_error(detail::exception_id::value_wrong_type, "cannot use value() with ", type_name()); + detail::view::throw_type_error(site(), detail::exception_id::value_wrong_type, "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.is_discarded() ? default_value : r.template get(); @@ -437,7 +437,7 @@ class basic_json_view { if (NLOHMANN_VIEW_UNLIKELY(!is_structured())) { - detail::view::throw_type_error(detail::exception_id::value_wrong_type, "cannot use value() with ", type_name()); + detail::view::throw_type_error(site(), detail::exception_id::value_wrong_type, "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.is_discarded() ? string_t(default_value) : r.template get(); @@ -450,7 +450,7 @@ class basic_json_view const iterator it = begin(); if (NLOHMANN_VIEW_UNLIKELY(it == end())) { - detail::view::throw_invalid_iterator(detail::exception_id::iterator_value_unavailable, "cannot get value"); + detail::view::throw_invalid_iterator(site(), detail::exception_id::iterator_value_unavailable, "cannot get value"); } return *it; } @@ -481,7 +481,7 @@ class basic_json_view return end(); } const node* const k = find_key(key); - return k != nullptr ? iterator(m_doc, k, true) : end(); + return k != nullptr ? iterator(m_doc, k, true, m_node) : end(); } iterator find(const char* key) const @@ -544,18 +544,18 @@ class basic_json_view { if (NLOHMANN_VIEW_LIKELY(is_structured())) { - return iterator(m_doc, navigation::first(*m_doc, m_node), is_object()); + return iterator(m_doc, navigation::first(*m_doc, m_node), is_object(), m_node); } - return iterator(m_doc, m_node, false); + return iterator(m_doc, m_node, false, m_node); } NLOHMANN_VIEW_ALWAYS_INLINE iterator end() const noexcept { if (NLOHMANN_VIEW_LIKELY(is_structured())) { - return iterator(m_doc, navigation::end(*m_doc, m_node), is_object()); + return iterator(m_doc, navigation::end(*m_doc, m_node), is_object(), m_node); } - return iterator(m_doc, (is_null() || is_discarded()) ? m_node : m_node + 1, false); + return iterator(m_doc, (is_null() || is_discarded()) ? m_node : m_node + 1, false, m_node); } iterator cbegin() const noexcept @@ -606,7 +606,7 @@ class basic_json_view { if (NLOHMANN_VIEW_UNLIKELY(!is_string())) { - detail::view::throw_type_error(detail::exception_id::type_mismatch, "type must be string, but is ", type_name()); + detail::view::throw_type_error(site(), detail::exception_id::type_mismatch, "type must be string, but is ", type_name()); } return {m_doc->str(*m_node), m_node->len}; } @@ -618,7 +618,7 @@ class basic_json_view { if (NLOHMANN_VIEW_UNLIKELY(!is_number())) { - detail::view::throw_type_error(detail::exception_id::type_mismatch, "type must be number, but is ", type_name()); + detail::view::throw_type_error(site(), detail::exception_id::type_mismatch, "type must be number, but is ", type_name()); } return {m_doc->str(*m_node), detail::view::number_length(*m_node)}; } @@ -764,12 +764,19 @@ class basic_json_view template friend class basic_json_view; template friend class detail::view::editor; template friend class detail::view::view_side; + friend struct detail::view::view_access; friend iterator; basic_json_view(const document_data* d, const node* n) noexcept : m_doc(d), m_node(n) {} + /// this value as the context of an exception + detail::view::error_site site() const noexcept + { + return detail::view::error_site(m_doc, m_node); + } + using json_side_t = detail::view::json_side; static detail::view::view_side side(const basic_json_view& v) noexcept @@ -876,7 +883,7 @@ class basic_json_view { if (NLOHMANN_VIEW_UNLIKELY(!is_boolean())) { - detail::view::throw_type_error(detail::exception_id::type_mismatch, "type must be boolean, but is ", type_name()); + detail::view::throw_type_error(site(), detail::exception_id::type_mismatch, "type must be boolean, but is ", type_name()); } return (m_node->flags & detail::view::node_flags::is_true) != 0; } @@ -886,7 +893,7 @@ class basic_json_view { if (NLOHMANN_VIEW_UNLIKELY(m_node == nullptr)) { - detail::view::throw_type_error(detail::exception_id::type_mismatch, "type must be number, but is ", type_name()); + detail::view::throw_type_error(site(), detail::exception_id::type_mismatch, "type must be number, but is ", type_name()); } return detail::view::arithmetic_value(*m_doc, *m_node); } @@ -895,7 +902,7 @@ class basic_json_view { if (NLOHMANN_VIEW_UNLIKELY(!is_null())) { - detail::view::throw_type_error(detail::exception_id::type_mismatch, "type must be null, but is ", type_name()); + detail::view::throw_type_error(site(), detail::exception_id::type_mismatch, "type must be null, but is ", type_name()); } return nullptr; } @@ -1309,7 +1316,7 @@ class basic_json_document template::value, int>::type = 0> view_type set(view_type array, I idx, V && value) { - return editor().set(array, index(idx), std::forward(value)); + return editor().set(array, index(array, idx), std::forward(value)); } /// set the value at a JSON pointer: its parent must exist; an object @@ -1346,7 +1353,7 @@ class basic_json_document const std::size_t idx = pointer_index(token); if (idx >= 0xFFFFFFFFu) { - detail::view::throw_array_index_out_of_range(idx); + detail::view::throw_array_index_out_of_range(detail::view::site_of(parent), idx); } for (std::size_t i = 0; i < idx; ++i) { @@ -1380,7 +1387,7 @@ class basic_json_document template::value, int>::type = 0> view_type insert(view_type array, I idx, V && value) { - return editor().insert(array, index(idx), std::forward(value)); + return editor().insert(array, index(array, idx), std::forward(value)); } /// remove all members with this key; returns their number @@ -1393,7 +1400,7 @@ class basic_json_document template::value, int>::type = 0> void erase(view_type array, I idx) { - editor().erase(array, index(idx)); + editor().erase(array, index(array, idx)); } /// remove the value at a JSON pointer; returns the number of removed @@ -1427,25 +1434,25 @@ class basic_json_document return detail::view::editor(*m_data); } - /// an index (out_of_range.401 if negative) + /// an index into @a array (out_of_range.401 if negative) template - static std::size_t index(I idx) + static std::size_t index(const view_type& array, I idx) { - return index(idx, std::is_signed {}); + return index(array, idx, std::is_signed {}); } template - static std::size_t index(I idx, std::true_type /*signed*/) + static std::size_t index(const view_type& array, I idx, std::true_type /*signed*/) { if (idx < 0) { - detail::view::throw_array_index_out_of_range(idx); + detail::view::throw_array_index_out_of_range(detail::view::site_of(array), idx); } return static_cast(idx); } template - static std::size_t index(I idx, std::false_type /*unsigned*/) + static std::size_t index(const view_type& /*array*/, I idx, std::false_type /*unsigned*/) { return static_cast(idx); } diff --git a/single_include/nlohmann/json.hpp b/single_include/nlohmann/json.hpp index 10b42dac1..0604cdfdf 100644 --- a/single_include/nlohmann/json.hpp +++ b/single_include/nlohmann/json.hpp @@ -2720,7 +2720,7 @@ JSON_HEDLEY_DIAGNOSTIC_POP return ej_pair.second == j; \ }); \ if (it != std::end(m)) e = it->first; \ - else ::nlohmann::detail::templated_json_throw(nlohmann::detail::out_of_range::create(nlohmann::detail::exception_id::value_out_of_range, nlohmann::detail::concat("enum value out of range for " #ENUM_TYPE ": ", nlohmann::detail::from_json_dump(j)), nlohmann::detail::exception_context(j))); \ + else ::nlohmann::detail::templated_json_throw(nlohmann::detail::out_of_range::create(nlohmann::detail::exception_id::value_out_of_range, nlohmann::detail::concat(nlohmann::detail::exception_prefix(j), "enum value out of range for " #ENUM_TYPE ": ", nlohmann::detail::from_json_dump(j)), nlohmann::detail::exception_context(j))); \ } // Ugly macros to avoid uglier copy-paste when specializing basic_json. They @@ -5928,6 +5928,28 @@ std::nullptr_t exception_context(const View& /*unused*/) noexcept return nullptr; } +namespace view +{ +// the path and bytes of the value of a view (defined in ) +template +std::string diagnostics_of(const View& v); +} // namespace view + +// what goes in front of the message of an exception about j: nothing for a +// basic_json value (exception_context() passes the value itself), the context +// basic_json would add (JSON_DIAGNOSTICS, JSON_DIAGNOSTIC_POSITIONS) for a view +template::value, int> = 0> +const char* exception_prefix(const BasicJsonType& /*unused*/) noexcept +{ + return ""; +} + +template::value, int> = 0> +std::string exception_prefix(const View& v) +{ + return view::diagnostics_of(v); +} + /*! @brief throws because @a j does not have the type a conversion expects @param[in] expected the expected type(s), e.g. "array" or "binary or array" @@ -5939,7 +5961,7 @@ JSON_HEDLEY_NO_RETURN JSON_HEDLEY_NEVER_INLINE void throw_type_must_be(const cha { static_cast(expected); // unused when JSON_NOEXCEPTION is defined static_cast(j); - JSON_THROW(type_error::create(exception_id::type_mismatch, concat("type must be ", expected, ", but is ", j.type_name()), exception_context(j))); + JSON_THROW(type_error::create(exception_id::type_mismatch, concat(exception_prefix(j), "type must be ", expected, ", but is ", j.type_name()), exception_context(j))); } /*! diff --git a/single_include/nlohmann/json_view.hpp b/single_include/nlohmann/json_view.hpp index 8ff21c77f..beaf95b01 100644 --- a/single_include/nlohmann/json_view.hpp +++ b/single_include/nlohmann/json_view.hpp @@ -4059,13 +4059,44 @@ NLOHMANN_JSON_NAMESPACE_END #include // min #include // size_t -#include // string +#include // string, to_string +#include // vector // #include // #include +// #include + // #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 // array +#include // size_t +#include // int64_t, uint8_t +#include // string +#include // vector + +// #include +// #include + +// #include + +// #include + +// #include + +// #include + NLOHMANN_JSON_NAMESPACE_BEGIN namespace detail @@ -4073,8 +4104,705 @@ namespace detail namespace view { +#if JSON_DIAGNOSTIC_POSITIONS +/*! +@brief the source positions of the values materialize() replays + +Stands in for the lexer of json_sax_dom_parser, which reads the positions of +the values it stores from these three functions (diagnostic_positions). npos +is a value without a source position. One position_source serves a whole +materialize() call: before each value is handed to the handler, the range of +that value is set (or cleared), overwriting that of the value before. +*/ +struct position_source +{ + /// the token, of which diagnostic_positions only reads the length + struct token + { + std::size_t length; + std::size_t size() const noexcept + { + return length; + } + }; + + std::size_t start = std::string::npos; ///< the first byte of the next value + std::size_t end = std::string::npos; ///< one past the next value (start_object/start_array: one past the bracket) + + std::size_t get_chars_read_total() const noexcept + { + return end; + } + std::size_t get_token_start_position() const noexcept + { + return start; + } + token get_string() const noexcept + { + return token{end - start}; + } + + /// the range [first, last), or none if either is npos + void set(std::size_t first, std::size_t last) noexcept + { + if (first == std::string::npos || last == std::string::npos) + { + clear(); + return; + } + start = first; + end = last; + } + + /// no range: the next value has no source position + void clear() noexcept + { + start = std::string::npos; + end = std::string::npos; + } + + /// one past the closing quote of the string whose opening quote is at + /// src[quote], or npos (the scan is bounded by size, as images can be + /// loaded without a check) + static std::size_t string_end(const char* src, std::size_t size, std::size_t quote) noexcept + { + for (std::size_t i = quote + 1; i < size; ++i) + { + if (src[i] == '\\') + { + ++i; + } + else if (src[i] == '"') + { + return i + 1; + } + } + return std::string::npos; + } + + /// one past the closing bracket of the array or object whose last element + /// ends at src[pos] (or whose opening bracket is at src[pos - 1]), or npos; + /// in between, there can only be whitespace, comments, and a trailing comma + /// (the source was validated when it was parsed) + static std::size_t container_end(const char* src, std::size_t size, std::size_t pos) noexcept + { + const auto* const b = reinterpret_cast(src); // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast): the text as bytes, as the builder reads it + const unsigned char* const e = b + size; + while (pos < size) + { + switch (src[pos]) + { + case ' ': + case '\t': + case '\n': + case '\r': + case ',': + ++pos; + break; + case ']': + case '}': + return pos + 1; + case '/': + if (pos + 1 < size && src[pos + 1] == '/') + { + pos = static_cast(skip_line_comment(b + pos + 2, e) - b); + break; + } + if (pos + 1 < size && src[pos + 1] == '*') + { + // (unterminated: pos is size, which ends the loop) + pos = static_cast(find_block_comment_end(b + pos + 2, e) - b) + 2; + break; + } + return std::string::npos; + default: + return std::string::npos; + } + } + return std::string::npos; + } + + /// the range of the scalar n of document d + void set_scalar(const document_data& d, const node& n) noexcept + { + if ((n.flags & node_flags::is_new) != 0) + { + clear(); + return; + } + std::size_t first = std::string::npos; + std::size_t last = std::string::npos; + switch (n.kind) + { + case node_kind::string: + if ((n.flags & node_flags::storage) == 0) + { + // off is the first byte after the opening quote + if (n.off != 0) + { + first = n.off - 1; + last = n.off + std::size_t{n.len} + 1; + } + } + else if ((n.flags & node_flags::storage) == node_flags::escaped && n.next != 0) + { + // next is the offset of the opening quote plus one + first = n.next - 1; + last = string_end(d.base[0], d.size, first); + } + break; + case node_kind::number_integer: + case node_kind::number_unsigned: + case node_kind::number_float: + if ((n.flags & node_flags::storage) == 0) + { + first = n.off; + last = n.off + number_length(n); + } + break; + case node_kind::boolean: + first = n.off; + last = n.off + ((n.flags & node_flags::is_true) != 0 ? 4u : 5u); + break; + case node_kind::null: + first = n.off; + last = n.off + 4u; + break; + case node_kind::object: + case node_kind::array: + case node_kind::link: + default: + break; + } + // (a crafted image can point past the text) + if (last == std::string::npos || last > d.size) + { + clear(); + } + else + { + set(first, last); + } + } +}; +#endif + +/*! +@brief the basic_json value of the subtree at n + +The subtree is replayed into the SAX handler that parse() uses to build its +values, so the result is the value parse() would produce: duplicate keys keep +the last value, and with JSON_DIAGNOSTICS the parent pointers are set. It is +iterative, so the nesting depth is limited by memory only, as for parse(). + +With JSON_DIAGNOSTIC_POSITIONS, the values get their source positions as with +parse(): position_source stands in for the lexer. Values written by edits have +none (npos), and neither has an array or object whose elements changed, or +whose last element has none (the end of its closing bracket is found by +scanning from the end of the last element). +*/ +template +BasicJsonType materialize(const document_data& d, const node* n) +{ + using string_t = typename BasicJsonType::string_t; + using input_t = iterator_input_adapter; +#if JSON_DIAGNOSTIC_POSITIONS + using lexer_t = position_source; +#else + using lexer_t = lexer; // (the default: unused, as no lexer is passed) +#endif + using sax_t = json_sax_dom_parser; + using nav = navigation; + + // the open containers: their next element (or key of the next member), + // their end, and whether they are objects; for the first 16 levels on the + // stack, else sized once from the document's deepest nesting (for a + // subtree an upper bound) + struct frame + { + const node* pos; + const node* end; + bool object; +#if JSON_DIAGNOSTIC_POSITIONS + std::size_t start; ///< the opening bracket (npos: unknown) + std::size_t last_end; ///< the end of the last element read (npos: unknown) +#endif + }; + +#if !JSON_DIAGNOSTIC_POSITIONS + // a scalar needs neither the handler nor the stack of open containers + // (with positions, it goes through the handler, which sets them) + switch (static_cast(n->kind)) + { + case value_t::string: + return BasicJsonType(string_t(d.str(*n), n->len)); + case value_t::number_integer: + return BasicJsonType(static_cast(static_cast(integer_bits(*n)))); + case value_t::number_unsigned: + return BasicJsonType(static_cast(integer_bits(*n))); + case value_t::number_float: + return BasicJsonType(float_value(d, *n)); + case value_t::boolean: + return BasicJsonType((n->flags & node_flags::is_true) != 0); + case value_t::null: + return BasicJsonType(nullptr); + case value_t::object: + case value_t::array: + case value_t::binary: + case value_t::discarded: + default: + break; + } +#endif + + BasicJsonType result; +#if JSON_DIAGNOSTIC_POSITIONS + position_source positions; + sax_t sax(result, true, &positions); +#else + sax_t sax(result, true); +#endif + const string_t no_token {}; + // (shallow subtrees on the stack: a call for a small value then allocates only for the value) + std::array shallow; // NOLINT(cppcoreguidelines-pro-type-member-init,hicpp-member-init): filled as containers open + std::vector deep{}; + frame* open = shallow.data(); + std::size_t capacity = shallow.size(); + if (d.max_depth > capacity) + { + deep.resize(d.max_depth); + open = deep.data(); + capacity = deep.size(); + } + std::size_t depth = 0; + for (;;) + { +#if JSON_DIAGNOSTIC_POSITIONS + if (!is_container(*n)) + { + positions.set_scalar(d, *n); + if (depth != 0) + { + open[depth - 1].last_end = positions.end; + } + } +#endif + // false positive: n comes from nav::value(), which never returns null for a valid index + // @infer-ignore NULLPTR_DEREFERENCE + switch (static_cast(n->kind)) + { + case value_t::object: + case value_t::array: + { + const bool object = n->kind == node_kind::object; +#if JSON_DIAGNOSTIC_POSITIONS + // (moved containers are also new) + if ((n->flags & node_flags::is_new) != 0 || n->off >= d.size) + { + positions.clear(); + } + else + { + positions.set(std::size_t{n->off}, std::size_t{n->off} + 1); + } +#endif + if (object) + { + sax.start_object(n->len); + } + else + { + sax.start_array(n->len); + } + if (NLOHMANN_VIEW_UNLIKELY(depth == capacity)) + { + // (deeper than the recorded nesting, e.g. after edits) + deep.assign(open, open + depth); + deep.resize(depth * 2); + open = deep.data(); + capacity = deep.size(); + } + // (with positions: until an element is read, the container's + // own bracket is the last thing read) + open[depth++] = frame{nav::first(d, n), nav::end(d, n), object +#if JSON_DIAGNOSTIC_POSITIONS + , positions.start, positions.end +#endif + }; + break; + } + case value_t::string: + { + string_t s(d.str(*n), n->len); + sax.string(s); + break; + } + case value_t::number_integer: + sax.number_integer(static_cast(static_cast(integer_bits(*n)))); + break; + case value_t::number_unsigned: + sax.number_unsigned(static_cast(integer_bits(*n))); + break; + case value_t::number_float: + sax.number_float(float_value(d, *n), no_token); + break; + case value_t::boolean: + sax.boolean((n->flags & node_flags::is_true) != 0); + break; + case value_t::null: + case value_t::binary: + case value_t::discarded: + default: + // false positive: the parser's frame stack points into its own storage (never null) + // @infer-ignore NULLPTR_DEREFERENCE + sax.null(); + break; + } + + // the next value: close finished containers, then read the key + for (;;) + { + if (depth == 0) + { + return result; + } + frame& f = open[depth - 1]; + if (f.pos == f.end) + { +#if JSON_DIAGNOSTIC_POSITIONS + if (f.start == std::string::npos || f.last_end == std::string::npos) + { + positions.clear(); + } + else + { + const std::size_t end = position_source::container_end(d.base[0], d.size, f.last_end); + if (end == std::string::npos) + { + positions.clear(); + } + else + { + // the closing bracket, as the lexer reports it (end_object() + // and end_array() only read its end; the start of the + // container was set when it was opened) + positions.set(end - 1, end); + } + } + if (depth > 1) + { + open[depth - 2].last_end = positions.end; + } +#endif + if (f.object) + { + sax.end_object(); + } + else + { + sax.end_array(); + } + --depth; + continue; + } + const node* entry = f.pos; + if (f.object) + { + string_t key(d.str(*entry), entry->len); + sax.key(key); + ++entry; + } + n = nav::value(entry); + f.pos = document_data::after(entry); + break; + } + } +} + +} // namespace view +} // namespace detail +NLOHMANN_JSON_NAMESPACE_END + +// #include + + +NLOHMANN_JSON_NAMESPACE_BEGIN +namespace detail +{ +namespace view +{ + +/*! +@brief the value an exception is about + +basic_json passes the value to the exception, which then names its path +(JSON_DIAGNOSTICS) and its bytes in the source (JSON_DIAGNOSTIC_POSITIONS). A +view's value has no parent pointer and no stored positions: both are found +from the document and the node, when the exception is thrown (so only the +error path pays for them). Without the two macros, a site is empty. +*/ +class error_site +{ + public: + error_site() noexcept = default; + + error_site(const document_data* d, const node* n) noexcept +#if JSON_DIAGNOSTICS || JSON_DIAGNOSTIC_POSITIONS + : m_doc(d), m_node(n) +#endif + { + static_cast(d); + static_cast(n); + } + +#if JSON_DIAGNOSTICS || JSON_DIAGNOSTIC_POSITIONS + const document_data* doc() const noexcept + { + return m_doc; + } + const node* value() const noexcept + { + return m_node; + } + + private: + const document_data* m_doc = nullptr; + const node* m_node = nullptr; +#endif +}; + +/// the site of a view's value (the views make this a friend) +struct view_access +{ + template + static error_site site(const View& v) noexcept + { + return error_site(v.m_doc, v.m_node); + } +}; + +template +error_site site_of(const View& v) noexcept +{ + return view_access::site(v); +} + +#if JSON_DIAGNOSTICS +/*! +@brief the JSON pointer tokens from the root of document @a d to node @a target + +Nodes have no parent index, so the path is found by descending from the root: +before the first edit, only into the subtree that contains the target (a +container spans the next `next` nodes); after edits, elements can live in +other sequences, so every container is searched. Iterative (the nesting depth +is limited by memory only). False if the target is not found (then tokens is +unspecified). +*/ +template +bool value_path(const document_data& d, const node* target, std::vector& tokens) +{ + using nav = navigation; + struct frame + { + const node* pos; + const node* end; + std::size_t index; + bool object; + }; + const node* const root = d.nodes; + if (target == root) + { + return true; + } + if (!is_container(*root)) + { + return false; + } + std::vector stack; + stack.push_back(frame{nav::first(d, root), nav::end(d, root), 0, root->kind == node_kind::object}); + // (tokens[i] leads from stack[i] to stack[i + 1], or to the target) + while (!stack.empty()) + { + frame& f = stack.back(); + if (f.pos == f.end) + { + stack.pop_back(); + if (!tokens.empty()) + { + tokens.pop_back(); + } + continue; + } + const node* entry = f.pos; + std::string token; + if (f.object) + { + // (as basic_json: the key up to a null byte) + token.assign(d.str(*entry), entry->len); + const std::size_t nul = token.find('\0'); + if (nul != std::string::npos) + { + token.resize(nul); + } + ++entry; + } + else + { + token = std::to_string(f.index); + } + const node* const child = nav::value(entry); + f.pos = document_data::after(entry); + ++f.index; + if (child == target) + { + tokens.push_back(token); + return true; + } + if (is_container(*child) && (nav::editable() || (target > child && target < child + child->next))) + { + tokens.push_back(token); + stack.push_back(frame{nav::first(d, child), nav::end(d, child), 0, child->kind == node_kind::object}); + } + } + return false; +} +#endif + +#if JSON_DIAGNOSTIC_POSITIONS +/*! +@brief the bytes [start, end) of node @a n in the source, as materialize() sets them + +Scalars as position_source::set_scalar; an array or object from its opening +bracket to its closing bracket, which is found by following the chain of last +elements to a scalar (or an empty container) and scanning from there to the +closing brackets. None (npos) for values written by edits, and for containers +whose last element has none, as with materialize(). +*/ +template +void value_range(const document_data& d, const node* n, std::size_t& start, std::size_t& end) +{ + using nav = navigation; + position_source p; + start = std::string::npos; + end = std::string::npos; + if (!is_container(*n)) + { + p.set_scalar(d, *n); + start = p.start; + end = p.end; + return; + } + if ((n->flags & node_flags::is_new) != 0 || n->off >= d.size) + { + return; + } + // the open brackets: n, then the last element as long as it is a non-empty container + std::size_t closing = 0; + std::size_t pos = std::string::npos; + for (const node* c = n;;) + { + if (is_container(*c)) + { + if ((c->flags & node_flags::is_new) != 0 || c->off >= d.size) + { + return; + } + ++closing; + const node* const first = nav::first(d, c); + const node* const last_end = nav::end(d, c); + if (first == last_end) + { + pos = std::size_t{c->off} + 1; + break; + } + const std::size_t value_offset = c->kind == node_kind::object ? 1 : 0; + const node* last = first; + for (const node* e = document_data::after(first + value_offset); e != last_end; e = document_data::after(e + value_offset)) + { + last = e; + } + c = nav::value(last + value_offset); + continue; + } + p.set_scalar(d, *c); + pos = p.end; + break; + } + for (; closing != 0 && pos != std::string::npos; --closing) + { + pos = position_source::container_end(d.base[0], d.size, pos); + } + if (pos != std::string::npos) + { + start = n->off; + end = pos; + } +} +#endif + +/// the context basic_json puts in front of the message of an exception about +/// a value: "(/path) " with JSON_DIAGNOSTICS (none for the root), then +/// "(bytes a-b) " with JSON_DIAGNOSTIC_POSITIONS (with JSON_DIAGNOSTICS, only +/// if there is a path) +inline std::string diagnostics(const error_site site) +{ +#if JSON_DIAGNOSTICS || JSON_DIAGNOSTIC_POSITIONS + if (site.doc() == nullptr || site.value() == nullptr) + { + return ""; + } + const document_data& d = *site.doc(); + const bool edited = d.edits != nullptr; + std::string bytes; +#if JSON_DIAGNOSTIC_POSITIONS + std::size_t start = 0; + std::size_t end = 0; + if (edited) + { + value_range(d, site.value(), start, end); + } + else + { + value_range(d, site.value(), start, end); + } + if (start != std::string::npos && end != std::string::npos) + { + bytes = concat("(bytes ", std::to_string(start), "-", std::to_string(end), ") "); + } +#endif +#if JSON_DIAGNOSTICS + std::vector tokens; + const bool found = edited ? value_path(d, site.value(), tokens) : value_path(d, site.value(), tokens); + if (!found || tokens.empty()) + { + return ""; + } + std::string path; + for (const std::string& token : tokens) + { + path.push_back('/'); + path += detail::escape(token); + } + return concat('(', path, ") ", bytes); +#else + return bytes; +#endif +#else + static_cast(site); + return ""; +#endif +} + +/// the context of an exception about view @a v (see exception_prefix()) +template +std::string diagnostics_of(const View& v) +{ + return diagnostics(site_of(v)); +} + // Exceptions are thrown out of line, so that the accessors that may throw stay -// small enough to be inlined. +// small enough to be inlined. The overloads with an error_site put its context +// in front of the message. [[noreturn]] NLOHMANN_VIEW_NOINLINE inline void throw_type_error(exception_id id, const char* prefix, const char* type) { @@ -4096,6 +4824,21 @@ namespace view NLOHMANN_VIEW_THROW(invalid_iterator::create(id, msg, nullptr)); } +[[noreturn]] NLOHMANN_VIEW_NOINLINE inline void throw_type_error(const error_site site, exception_id id, const char* prefix, const char* type) +{ + NLOHMANN_VIEW_THROW(type_error::create(id, concat(diagnostics(site), prefix, type), nullptr)); +} + +[[noreturn]] NLOHMANN_VIEW_NOINLINE inline void throw_out_of_range(const error_site site, exception_id id, const std::string& msg) +{ + NLOHMANN_VIEW_THROW(out_of_range::create(id, concat(diagnostics(site), msg), nullptr)); +} + +[[noreturn]] NLOHMANN_VIEW_NOINLINE inline void throw_invalid_iterator(const error_site site, exception_id id, const char* msg) +{ + NLOHMANN_VIEW_THROW(invalid_iterator::create(id, concat(diagnostics(site), msg), nullptr)); +} + /// a parse error without a position (as those of json_pointer) [[noreturn]] NLOHMANN_VIEW_NOINLINE inline void throw_parse_error(exception_id id, const std::string& msg) { @@ -4104,22 +4847,22 @@ namespace view /// out_of_range.401: the array index @a idx is not below the size template -[[noreturn]] NLOHMANN_VIEW_NOINLINE void throw_array_index_out_of_range(const Index idx) +[[noreturn]] NLOHMANN_VIEW_NOINLINE void throw_array_index_out_of_range(const error_site site, const Index idx) { - throw_out_of_range(exception_id::array_index_out_of_range, concat("array index ", std::to_string(idx), " is out of range")); + throw_out_of_range(site, exception_id::array_index_out_of_range, concat("array index ", std::to_string(idx), " is out of range")); } /// out_of_range.402: the JSON Pointer token '-' of an array of @a size elements -[[noreturn]] NLOHMANN_VIEW_NOINLINE inline void throw_array_index_past_end(std::size_t size) +[[noreturn]] NLOHMANN_VIEW_NOINLINE inline void throw_array_index_past_end(const error_site site, std::size_t size) { - throw_out_of_range(exception_id::pointer_past_the_end_index, concat("array index '-' (", std::to_string(size), ") is out of range")); + throw_out_of_range(site, exception_id::pointer_past_the_end_index, concat("array index '-' (", std::to_string(size), ") is out of range")); } /// out_of_range.403: the object has no member @a key template -[[noreturn]] NLOHMANN_VIEW_NOINLINE void throw_key_not_found(const Key& key) +[[noreturn]] NLOHMANN_VIEW_NOINLINE void throw_key_not_found(const error_site site, const Key& key) { - throw_out_of_range(exception_id::key_not_found, concat("key '", key, "' not found")); + throw_out_of_range(site, exception_id::key_not_found, concat("key '", key, "' not found")); } /// SAX handler of the error replay: accepts every event without storing it, @@ -5156,7 +5899,7 @@ class editor node* const o = own(object); if (o->kind != node_kind::object && o->kind != node_kind::null) { - throw_type_error(exception_id::subscript_wrong_type, "cannot use operator[] with a string argument with ", object.type_name()); + throw_type_error(site_of(object), exception_id::subscript_wrong_type, "cannot use operator[] with a string argument with ", object.type_name()); } check_utf8(key.data(), key.size()); const encoded e = encode(std::forward(value)); @@ -5207,9 +5950,9 @@ class editor node* const a = own(array); if (a->kind != node_kind::array) { - throw_type_error(exception_id::subscript_wrong_type, "cannot use operator[] with a numeric argument with ", array.type_name()); + throw_type_error(site_of(array), exception_id::subscript_wrong_type, "cannot use operator[] with a numeric argument with ", array.type_name()); } - check_index(idx, a->len); + check_index(a, idx, a->len); const encoded e = encode(std::forward(value)); node* const slot = const_cast(nav::value(element_at(m_doc, a, idx))); // NOLINT(cppcoreguidelines-pro-type-const-cast) assign(slot, e, a, true); @@ -5224,7 +5967,7 @@ class editor node* const a = own(array); if (a->kind != node_kind::array && a->kind != node_kind::null) { - throw_type_error(exception_id::push_back_wrong_type, "cannot use push_back() with ", array.type_name()); + throw_type_error(site_of(array), exception_id::push_back_wrong_type, "cannot use push_back() with ", array.type_name()); } const encoded e = encode(std::forward(value)); if (a->kind == node_kind::null) @@ -5248,9 +5991,9 @@ class editor node* const a = own(array); if (a->kind != node_kind::array) { - throw_type_error(exception_id::insert_wrong_type, "cannot use insert() with ", array.type_name()); + throw_type_error(site_of(array), exception_id::insert_wrong_type, "cannot use insert() with ", array.type_name()); } - check_index(idx, a->len + 1); + check_index(a, idx, a->len + 1); const encoded e = encode(std::forward(value)); node* const slot = new_slot(e); node* const h = block_of(m_doc, a, 1); @@ -5268,7 +6011,7 @@ class editor node* const o = own(object); if (o->kind != node_kind::object) { - throw_type_error(exception_id::erase_wrong_type, "cannot use erase() with ", object.type_name()); + throw_type_error(site_of(object), exception_id::erase_wrong_type, "cannot use erase() with ", object.type_name()); } for (const node* k = nav::first(m_doc, o), *end = nav::end(m_doc, o); k != end; k = document_data::after(k + 1)) { @@ -5286,9 +6029,9 @@ class editor node* const a = own(array); if (a->kind != node_kind::array) { - throw_type_error(exception_id::erase_wrong_type, "cannot use erase() with ", array.type_name()); + throw_type_error(site_of(array), exception_id::erase_wrong_type, "cannot use erase() with ", array.type_name()); } - check_index(idx, a->len); + check_index(a, idx, a->len); node* const h = block_of(m_doc, a, 0); std::memmove(h + 1 + idx, h + 2 + idx, (h->next - 2 - idx) * sizeof(node)); --h->next; @@ -5315,11 +6058,11 @@ class editor return const_cast(v.m_node); // NOLINT(cppcoreguidelines-pro-type-const-cast): the nodes belong to this document } - static void check_index(std::size_t idx, std::size_t limit) + void check_index(const node* array, std::size_t idx, std::size_t limit) const { if (idx >= limit) { - throw_array_index_out_of_range(idx); + throw_array_index_out_of_range(error_site(&m_doc, array), idx); } } @@ -7005,9 +7748,17 @@ class view_iterator /// @param[in] pos the element, or the key of the member /// @param[in] object whether pos is a key (its value is the next node) - view_iterator(const document_data* d, const node* pos, bool object) noexcept + /// @param[in] container the value iterated over (the context of the + /// exceptions, kept only with JSON_DIAGNOSTICS or + /// JSON_DIAGNOSTIC_POSITIONS) + view_iterator(const document_data* d, const node* pos, bool object, const node* container) noexcept : m_doc(d), m_pos(pos), m_value_offset(object ? 1 : 0) - {} +#if JSON_DIAGNOSTICS || JSON_DIAGNOSTIC_POSITIONS + , m_container(container) +#endif + { + static_cast(container); + } NLOHMANN_VIEW_ALWAYS_INLINE View operator*() const noexcept { @@ -7048,7 +7799,7 @@ class view_iterator { if (NLOHMANN_VIEW_UNLIKELY(m_value_offset == 0)) { - throw_invalid_iterator(exception_id::iterator_key_not_object, "cannot use key() for non-object iterators"); + throw_invalid_iterator(site(), exception_id::iterator_key_not_object, "cannot use key() for non-object iterators"); } return string_view_t(m_doc->str(*m_pos), m_pos->len); } @@ -7065,9 +7816,22 @@ class view_iterator } private: + /// the value iterated over, as the context of an exception + error_site site() const noexcept + { +#if JSON_DIAGNOSTICS || JSON_DIAGNOSTIC_POSITIONS + return error_site(m_doc, m_container); +#else + return error_site(); +#endif + } + const document_data* m_doc = nullptr; const node* m_pos = nullptr; std::size_t m_value_offset = 0; ///< 1 for objects: the value follows its key +#if JSON_DIAGNOSTICS || JSON_DIAGNOSTIC_POSITIONS + const node* m_container = nullptr; +#endif }; /*! @@ -7207,458 +7971,6 @@ 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 // array -#include // size_t -#include // int64_t, uint8_t -#include // string -#include // vector - -// #include -// #include - -// #include - -// #include - -// #include - -// #include - - -NLOHMANN_JSON_NAMESPACE_BEGIN -namespace detail -{ -namespace view -{ - -#if JSON_DIAGNOSTIC_POSITIONS -/*! -@brief the source positions of the values materialize() replays - -Stands in for the lexer of json_sax_dom_parser, which reads the positions of -the values it stores from these three functions (diagnostic_positions). npos -is a value without a source position. One position_source serves a whole -materialize() call: before each value is handed to the handler, the range of -that value is set (or cleared), overwriting that of the value before. -*/ -struct position_source -{ - /// the token, of which diagnostic_positions only reads the length - struct token - { - std::size_t length; - std::size_t size() const noexcept - { - return length; - } - }; - - std::size_t start = std::string::npos; ///< the first byte of the next value - std::size_t end = std::string::npos; ///< one past the next value (start_object/start_array: one past the bracket) - - std::size_t get_chars_read_total() const noexcept - { - return end; - } - std::size_t get_token_start_position() const noexcept - { - return start; - } - token get_string() const noexcept - { - return token{end - start}; - } - - /// the range [first, last), or none if either is npos - void set(std::size_t first, std::size_t last) noexcept - { - if (first == std::string::npos || last == std::string::npos) - { - clear(); - return; - } - start = first; - end = last; - } - - /// no range: the next value has no source position - void clear() noexcept - { - start = std::string::npos; - end = std::string::npos; - } - - /// one past the closing quote of the string whose opening quote is at - /// src[quote], or npos (the scan is bounded by size, as images can be - /// loaded without a check) - static std::size_t string_end(const char* src, std::size_t size, std::size_t quote) noexcept - { - for (std::size_t i = quote + 1; i < size; ++i) - { - if (src[i] == '\\') - { - ++i; - } - else if (src[i] == '"') - { - return i + 1; - } - } - return std::string::npos; - } - - /// one past the closing bracket of the array or object whose last element - /// ends at src[pos] (or whose opening bracket is at src[pos - 1]), or npos; - /// in between, there can only be whitespace, comments, and a trailing comma - /// (the source was validated when it was parsed) - static std::size_t container_end(const char* src, std::size_t size, std::size_t pos) noexcept - { - const auto* const b = reinterpret_cast(src); // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast): the text as bytes, as the builder reads it - const unsigned char* const e = b + size; - while (pos < size) - { - switch (src[pos]) - { - case ' ': - case '\t': - case '\n': - case '\r': - case ',': - ++pos; - break; - case ']': - case '}': - return pos + 1; - case '/': - if (pos + 1 < size && src[pos + 1] == '/') - { - pos = static_cast(skip_line_comment(b + pos + 2, e) - b); - break; - } - if (pos + 1 < size && src[pos + 1] == '*') - { - // (unterminated: pos is size, which ends the loop) - pos = static_cast(find_block_comment_end(b + pos + 2, e) - b) + 2; - break; - } - return std::string::npos; - default: - return std::string::npos; - } - } - return std::string::npos; - } - - /// the range of the scalar n of document d - void set_scalar(const document_data& d, const node& n) noexcept - { - if ((n.flags & node_flags::is_new) != 0) - { - clear(); - return; - } - std::size_t first = std::string::npos; - std::size_t last = std::string::npos; - switch (n.kind) - { - case node_kind::string: - if ((n.flags & node_flags::storage) == 0) - { - // off is the first byte after the opening quote - if (n.off != 0) - { - first = n.off - 1; - last = n.off + std::size_t{n.len} + 1; - } - } - else if ((n.flags & node_flags::storage) == node_flags::escaped && n.next != 0) - { - // next is the offset of the opening quote plus one - first = n.next - 1; - last = string_end(d.base[0], d.size, first); - } - break; - case node_kind::number_integer: - case node_kind::number_unsigned: - case node_kind::number_float: - if ((n.flags & node_flags::storage) == 0) - { - first = n.off; - last = n.off + number_length(n); - } - break; - case node_kind::boolean: - first = n.off; - last = n.off + ((n.flags & node_flags::is_true) != 0 ? 4u : 5u); - break; - case node_kind::null: - first = n.off; - last = n.off + 4u; - break; - case node_kind::object: - case node_kind::array: - case node_kind::link: - default: - break; - } - // (a crafted image can point past the text) - if (last == std::string::npos || last > d.size) - { - clear(); - } - else - { - set(first, last); - } - } -}; -#endif - -/*! -@brief the basic_json value of the subtree at n - -The subtree is replayed into the SAX handler that parse() uses to build its -values, so the result is the value parse() would produce: duplicate keys keep -the last value, and with JSON_DIAGNOSTICS the parent pointers are set. It is -iterative, so the nesting depth is limited by memory only, as for parse(). - -With JSON_DIAGNOSTIC_POSITIONS, the values get their source positions as with -parse(): position_source stands in for the lexer. Values written by edits have -none (npos), and neither has an array or object whose elements changed, or -whose last element has none (the end of its closing bracket is found by -scanning from the end of the last element). -*/ -template -BasicJsonType materialize(const document_data& d, const node* n) -{ - using string_t = typename BasicJsonType::string_t; - using input_t = iterator_input_adapter; -#if JSON_DIAGNOSTIC_POSITIONS - using lexer_t = position_source; -#else - using lexer_t = lexer; // (the default: unused, as no lexer is passed) -#endif - using sax_t = json_sax_dom_parser; - using nav = navigation; - - // the open containers: their next element (or key of the next member), - // their end, and whether they are objects; for the first 16 levels on the - // stack, else sized once from the document's deepest nesting (for a - // subtree an upper bound) - struct frame - { - const node* pos; - const node* end; - bool object; -#if JSON_DIAGNOSTIC_POSITIONS - std::size_t start; ///< the opening bracket (npos: unknown) - std::size_t last_end; ///< the end of the last element read (npos: unknown) -#endif - }; - -#if !JSON_DIAGNOSTIC_POSITIONS - // a scalar needs neither the handler nor the stack of open containers - // (with positions, it goes through the handler, which sets them) - switch (static_cast(n->kind)) - { - case value_t::string: - return BasicJsonType(string_t(d.str(*n), n->len)); - case value_t::number_integer: - return BasicJsonType(static_cast(static_cast(integer_bits(*n)))); - case value_t::number_unsigned: - return BasicJsonType(static_cast(integer_bits(*n))); - case value_t::number_float: - return BasicJsonType(float_value(d, *n)); - case value_t::boolean: - return BasicJsonType((n->flags & node_flags::is_true) != 0); - case value_t::null: - return BasicJsonType(nullptr); - case value_t::object: - case value_t::array: - case value_t::binary: - case value_t::discarded: - default: - break; - } -#endif - - BasicJsonType result; -#if JSON_DIAGNOSTIC_POSITIONS - position_source positions; - sax_t sax(result, true, &positions); -#else - sax_t sax(result, true); -#endif - const string_t no_token {}; - // (shallow subtrees on the stack: a call for a small value then allocates only for the value) - std::array shallow; // NOLINT(cppcoreguidelines-pro-type-member-init,hicpp-member-init): filled as containers open - std::vector deep{}; - frame* open = shallow.data(); - std::size_t capacity = shallow.size(); - if (d.max_depth > capacity) - { - deep.resize(d.max_depth); - open = deep.data(); - capacity = deep.size(); - } - std::size_t depth = 0; - for (;;) - { -#if JSON_DIAGNOSTIC_POSITIONS - if (!is_container(*n)) - { - positions.set_scalar(d, *n); - if (depth != 0) - { - open[depth - 1].last_end = positions.end; - } - } -#endif - // false positive: n comes from nav::value(), which never returns null for a valid index - // @infer-ignore NULLPTR_DEREFERENCE - switch (static_cast(n->kind)) - { - case value_t::object: - case value_t::array: - { - const bool object = n->kind == node_kind::object; -#if JSON_DIAGNOSTIC_POSITIONS - // (moved containers are also new) - if ((n->flags & node_flags::is_new) != 0 || n->off >= d.size) - { - positions.clear(); - } - else - { - positions.set(std::size_t{n->off}, std::size_t{n->off} + 1); - } -#endif - if (object) - { - sax.start_object(n->len); - } - else - { - sax.start_array(n->len); - } - if (NLOHMANN_VIEW_UNLIKELY(depth == capacity)) - { - // (deeper than the recorded nesting, e.g. after edits) - deep.assign(open, open + depth); - deep.resize(depth * 2); - open = deep.data(); - capacity = deep.size(); - } - // (with positions: until an element is read, the container's - // own bracket is the last thing read) - open[depth++] = frame{nav::first(d, n), nav::end(d, n), object -#if JSON_DIAGNOSTIC_POSITIONS - , positions.start, positions.end -#endif - }; - break; - } - case value_t::string: - { - string_t s(d.str(*n), n->len); - sax.string(s); - break; - } - case value_t::number_integer: - sax.number_integer(static_cast(static_cast(integer_bits(*n)))); - break; - case value_t::number_unsigned: - sax.number_unsigned(static_cast(integer_bits(*n))); - break; - case value_t::number_float: - sax.number_float(float_value(d, *n), no_token); - break; - case value_t::boolean: - sax.boolean((n->flags & node_flags::is_true) != 0); - break; - case value_t::null: - case value_t::binary: - case value_t::discarded: - default: - // false positive: the parser's frame stack points into its own storage (never null) - // @infer-ignore NULLPTR_DEREFERENCE - sax.null(); - break; - } - - // the next value: close finished containers, then read the key - for (;;) - { - if (depth == 0) - { - return result; - } - frame& f = open[depth - 1]; - if (f.pos == f.end) - { -#if JSON_DIAGNOSTIC_POSITIONS - if (f.start == std::string::npos || f.last_end == std::string::npos) - { - positions.clear(); - } - else - { - const std::size_t end = position_source::container_end(d.base[0], d.size, f.last_end); - if (end == std::string::npos) - { - positions.clear(); - } - else - { - // the closing bracket, as the lexer reports it (end_object() - // and end_array() only read its end; the start of the - // container was set when it was opened) - positions.set(end - 1, end); - } - } - if (depth > 1) - { - open[depth - 2].last_end = positions.end; - } -#endif - if (f.object) - { - sax.end_object(); - } - else - { - sax.end_array(); - } - --depth; - continue; - } - const node* entry = f.pos; - if (f.object) - { - string_t key(d.str(*entry), entry->len); - sax.key(key); - ++entry; - } - n = nav::value(entry); - f.pos = document_data::after(entry); - break; - } - } -} - -} // namespace view -} // namespace detail -NLOHMANN_JSON_NAMESPACE_END // #include @@ -7714,6 +8026,7 @@ index_status array_index(const StringType& s, std::size_t& idx) noexcept /// throws the exception json_pointer::array_index throws for this status template +/// (without a context, as json_pointer::array_index) [[noreturn]] NLOHMANN_VIEW_NOINLINE void throw_array_index_error(index_status status, const StringType& s) { switch (status) @@ -7752,7 +8065,7 @@ View resolve_pointer(View cur, const Tokens& tokens, pointer_mode mode) { if (mode == pointer_mode::checked) { - throw_key_not_found(token); + throw_key_not_found(site_of(cur), token); } return View(); } @@ -7764,7 +8077,7 @@ View resolve_pointer(View cur, const Tokens& tokens, pointer_mode mode) { if (throwing) { - throw_array_index_past_end(cur.size()); + throw_array_index_past_end(site_of(cur), cur.size()); } return View(); } @@ -7783,7 +8096,7 @@ View resolve_pointer(View cur, const Tokens& tokens, pointer_mode mode) { if (mode == pointer_mode::checked) { - throw_array_index_out_of_range(idx); + throw_array_index_out_of_range(site_of(cur), idx); } return View(); } @@ -7793,7 +8106,7 @@ View resolve_pointer(View cur, const Tokens& tokens, pointer_mode mode) { if (throwing) { - throw_out_of_range(exception_id::pointer_unresolved, concat("unresolved reference token '", token, "'")); + throw_out_of_range(site_of(cur), exception_id::pointer_unresolved, concat("unresolved reference token '", token, "'")); } return View(); } @@ -9041,7 +9354,7 @@ NLOHMANN_VIEW_ALWAYS_INLINE T arithmetic_value(const document_data& d, const nod case value_t::binary: case value_t::discarded: default: - throw_type_error(exception_id::type_mismatch, "type must be number, but is ", value_type_name(static_cast(n.kind))); + throw_type_error(error_site(&d, &n), exception_id::type_mismatch, "type must be number, but is ", value_type_name(static_cast(n.kind))); } } @@ -9051,7 +9364,7 @@ std::vector vector_value(const View& v) { if (NLOHMANN_VIEW_UNLIKELY(!v.is_array())) { - throw_type_error(exception_id::type_mismatch, "type must be array, but is ", v.type_name()); + throw_type_error(site_of(v), exception_id::type_mismatch, "type must be array, but is ", v.type_name()); } std::vector r; r.reserve(v.size()); @@ -9069,7 +9382,7 @@ Map map_value(const View& v) { if (NLOHMANN_VIEW_UNLIKELY(!v.is_object())) { - throw_type_error(exception_id::type_mismatch, "type must be object, but is ", v.type_name()); + throw_type_error(site_of(v), exception_id::type_mismatch, "type must be object, but is ", v.type_name()); } Map r; for (auto it = v.begin(); it != v.end(); ++it) @@ -9326,7 +9639,7 @@ class basic_json_view { return {}; } - detail::view::throw_type_error(detail::exception_id::subscript_wrong_type, "cannot use operator[] with a string argument with ", type_name()); + detail::view::throw_type_error(site(), detail::exception_id::subscript_wrong_type, "cannot use operator[] with a string argument with ", type_name()); } return lookup(key); } @@ -9352,7 +9665,7 @@ class basic_json_view { return {}; } - detail::view::throw_type_error(detail::exception_id::subscript_wrong_type, "cannot use operator[] with a numeric argument with ", type_name()); + detail::view::throw_type_error(site(), detail::exception_id::subscript_wrong_type, "cannot use operator[] with a numeric argument with ", type_name()); } return idx < m_node->len ? element(idx) : basic_json_view(); } @@ -9385,12 +9698,12 @@ class basic_json_view { if (NLOHMANN_VIEW_UNLIKELY(!is_object())) { - detail::view::throw_type_error(detail::exception_id::at_wrong_type, "cannot use at() with ", type_name()); + detail::view::throw_type_error(site(), detail::exception_id::at_wrong_type, "cannot use at() with ", type_name()); } const basic_json_view r = lookup(key); if (NLOHMANN_VIEW_UNLIKELY(r.is_discarded())) { - detail::view::throw_key_not_found(std::string(key.data(), key.size())); + detail::view::throw_key_not_found(site(), std::string(key.data(), key.size())); } return r; } @@ -9411,11 +9724,11 @@ class basic_json_view { if (NLOHMANN_VIEW_UNLIKELY(!is_array())) { - detail::view::throw_type_error(detail::exception_id::at_wrong_type, "cannot use at() with ", type_name()); + detail::view::throw_type_error(site(), detail::exception_id::at_wrong_type, "cannot use at() with ", type_name()); } if (NLOHMANN_VIEW_UNLIKELY(idx >= m_node->len)) { - detail::view::throw_array_index_out_of_range(idx); + detail::view::throw_array_index_out_of_range(site(), idx); } return element(idx); } @@ -9443,7 +9756,7 @@ class basic_json_view { if (NLOHMANN_VIEW_UNLIKELY(!is_object())) { - detail::view::throw_type_error(detail::exception_id::value_wrong_type, "cannot use value() with ", type_name()); + detail::view::throw_type_error(site(), detail::exception_id::value_wrong_type, "cannot use value() with ", type_name()); } const basic_json_view r = lookup(key); return r.is_discarded() ? default_value : r.template get(); @@ -9453,7 +9766,7 @@ class basic_json_view { if (NLOHMANN_VIEW_UNLIKELY(!is_object())) { - detail::view::throw_type_error(detail::exception_id::value_wrong_type, "cannot use value() with ", type_name()); + detail::view::throw_type_error(site(), detail::exception_id::value_wrong_type, "cannot use value() with ", type_name()); } // (the default is made only if there is no such member) const basic_json_view r = lookup(key); @@ -9468,7 +9781,7 @@ class basic_json_view { if (NLOHMANN_VIEW_UNLIKELY(!is_structured())) { - detail::view::throw_type_error(detail::exception_id::value_wrong_type, "cannot use value() with ", type_name()); + detail::view::throw_type_error(site(), detail::exception_id::value_wrong_type, "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.is_discarded() ? default_value : r.template get(); @@ -9478,7 +9791,7 @@ class basic_json_view { if (NLOHMANN_VIEW_UNLIKELY(!is_structured())) { - detail::view::throw_type_error(detail::exception_id::value_wrong_type, "cannot use value() with ", type_name()); + detail::view::throw_type_error(site(), detail::exception_id::value_wrong_type, "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.is_discarded() ? string_t(default_value) : r.template get(); @@ -9491,7 +9804,7 @@ class basic_json_view const iterator it = begin(); if (NLOHMANN_VIEW_UNLIKELY(it == end())) { - detail::view::throw_invalid_iterator(detail::exception_id::iterator_value_unavailable, "cannot get value"); + detail::view::throw_invalid_iterator(site(), detail::exception_id::iterator_value_unavailable, "cannot get value"); } return *it; } @@ -9522,7 +9835,7 @@ class basic_json_view return end(); } const node* const k = find_key(key); - return k != nullptr ? iterator(m_doc, k, true) : end(); + return k != nullptr ? iterator(m_doc, k, true, m_node) : end(); } iterator find(const char* key) const @@ -9585,18 +9898,18 @@ class basic_json_view { if (NLOHMANN_VIEW_LIKELY(is_structured())) { - return iterator(m_doc, navigation::first(*m_doc, m_node), is_object()); + return iterator(m_doc, navigation::first(*m_doc, m_node), is_object(), m_node); } - return iterator(m_doc, m_node, false); + return iterator(m_doc, m_node, false, m_node); } NLOHMANN_VIEW_ALWAYS_INLINE iterator end() const noexcept { if (NLOHMANN_VIEW_LIKELY(is_structured())) { - return iterator(m_doc, navigation::end(*m_doc, m_node), is_object()); + return iterator(m_doc, navigation::end(*m_doc, m_node), is_object(), m_node); } - return iterator(m_doc, (is_null() || is_discarded()) ? m_node : m_node + 1, false); + return iterator(m_doc, (is_null() || is_discarded()) ? m_node : m_node + 1, false, m_node); } iterator cbegin() const noexcept @@ -9647,7 +9960,7 @@ class basic_json_view { if (NLOHMANN_VIEW_UNLIKELY(!is_string())) { - detail::view::throw_type_error(detail::exception_id::type_mismatch, "type must be string, but is ", type_name()); + detail::view::throw_type_error(site(), detail::exception_id::type_mismatch, "type must be string, but is ", type_name()); } return {m_doc->str(*m_node), m_node->len}; } @@ -9659,7 +9972,7 @@ class basic_json_view { if (NLOHMANN_VIEW_UNLIKELY(!is_number())) { - detail::view::throw_type_error(detail::exception_id::type_mismatch, "type must be number, but is ", type_name()); + detail::view::throw_type_error(site(), detail::exception_id::type_mismatch, "type must be number, but is ", type_name()); } return {m_doc->str(*m_node), detail::view::number_length(*m_node)}; } @@ -9805,12 +10118,19 @@ class basic_json_view template friend class basic_json_view; template friend class detail::view::editor; template friend class detail::view::view_side; + friend struct detail::view::view_access; friend iterator; basic_json_view(const document_data* d, const node* n) noexcept : m_doc(d), m_node(n) {} + /// this value as the context of an exception + detail::view::error_site site() const noexcept + { + return detail::view::error_site(m_doc, m_node); + } + using json_side_t = detail::view::json_side; static detail::view::view_side side(const basic_json_view& v) noexcept @@ -9917,7 +10237,7 @@ class basic_json_view { if (NLOHMANN_VIEW_UNLIKELY(!is_boolean())) { - detail::view::throw_type_error(detail::exception_id::type_mismatch, "type must be boolean, but is ", type_name()); + detail::view::throw_type_error(site(), detail::exception_id::type_mismatch, "type must be boolean, but is ", type_name()); } return (m_node->flags & detail::view::node_flags::is_true) != 0; } @@ -9927,7 +10247,7 @@ class basic_json_view { if (NLOHMANN_VIEW_UNLIKELY(m_node == nullptr)) { - detail::view::throw_type_error(detail::exception_id::type_mismatch, "type must be number, but is ", type_name()); + detail::view::throw_type_error(site(), detail::exception_id::type_mismatch, "type must be number, but is ", type_name()); } return detail::view::arithmetic_value(*m_doc, *m_node); } @@ -9936,7 +10256,7 @@ class basic_json_view { if (NLOHMANN_VIEW_UNLIKELY(!is_null())) { - detail::view::throw_type_error(detail::exception_id::type_mismatch, "type must be null, but is ", type_name()); + detail::view::throw_type_error(site(), detail::exception_id::type_mismatch, "type must be null, but is ", type_name()); } return nullptr; } @@ -10350,7 +10670,7 @@ class basic_json_document template::value, int>::type = 0> view_type set(view_type array, I idx, V && value) { - return editor().set(array, index(idx), std::forward(value)); + return editor().set(array, index(array, idx), std::forward(value)); } /// set the value at a JSON pointer: its parent must exist; an object @@ -10387,7 +10707,7 @@ class basic_json_document const std::size_t idx = pointer_index(token); if (idx >= 0xFFFFFFFFu) { - detail::view::throw_array_index_out_of_range(idx); + detail::view::throw_array_index_out_of_range(detail::view::site_of(parent), idx); } for (std::size_t i = 0; i < idx; ++i) { @@ -10421,7 +10741,7 @@ class basic_json_document template::value, int>::type = 0> view_type insert(view_type array, I idx, V && value) { - return editor().insert(array, index(idx), std::forward(value)); + return editor().insert(array, index(array, idx), std::forward(value)); } /// remove all members with this key; returns their number @@ -10434,7 +10754,7 @@ class basic_json_document template::value, int>::type = 0> void erase(view_type array, I idx) { - editor().erase(array, index(idx)); + editor().erase(array, index(array, idx)); } /// remove the value at a JSON pointer; returns the number of removed @@ -10468,25 +10788,25 @@ class basic_json_document return detail::view::editor(*m_data); } - /// an index (out_of_range.401 if negative) + /// an index into @a array (out_of_range.401 if negative) template - static std::size_t index(I idx) + static std::size_t index(const view_type& array, I idx) { - return index(idx, std::is_signed {}); + return index(array, idx, std::is_signed {}); } template - static std::size_t index(I idx, std::true_type /*signed*/) + static std::size_t index(const view_type& array, I idx, std::true_type /*signed*/) { if (idx < 0) { - detail::view::throw_array_index_out_of_range(idx); + detail::view::throw_array_index_out_of_range(detail::view::site_of(array), idx); } return static_cast(idx); } template - static std::size_t index(I idx, std::false_type /*unsigned*/) + static std::size_t index(const view_type& /*array*/, I idx, std::false_type /*unsigned*/) { return static_cast(idx); } diff --git a/tests/src/json_view_test_helpers.hpp b/tests/src/json_view_test_helpers.hpp index b5af61798..4f84a93ef 100644 --- a/tests/src/json_view_test_helpers.hpp +++ b/tests/src/json_view_test_helpers.hpp @@ -15,6 +15,7 @@ #include #include // any_of +#include // size_t #include // function #include // mt19937 #include // string @@ -88,6 +89,26 @@ inline bool has_duplicate_keys(const nlohmann::ordered_json_view& v) }); } +// the message of an exception about the value at path (a JSON pointer, "" for +// the root) whose source bytes are [start, end): the context basic_json adds +// with JSON_DIAGNOSTICS and JSON_DIAGNOSTIC_POSITIONS, if enabled, then what +inline std::string message(const std::string& id, const std::string& path, std::size_t start, std::size_t end, const std::string& what) +{ + std::string context; +#if JSON_DIAGNOSTIC_POSITIONS + context = "(bytes " + std::to_string(start) + "-" + std::to_string(end) + ") "; +#else + static_cast(start); + static_cast(end); +#endif +#if JSON_DIAGNOSTICS + context = path.empty() ? "" : "(" + path + ") " + context; +#else + static_cast(path); +#endif + return "[json.exception." + id + "] " + context + what; +} + #if !defined(JSON_NOEXCEPTION) // the exception a call throws, or "" if it throws none inline std::string exception_of_call(const std::function& f) diff --git a/tests/src/unit-json_view.cpp b/tests/src/unit-json_view.cpp index 77f7aa4fa..db7e1b6d5 100644 --- a/tests/src/unit-json_view.cpp +++ b/tests/src/unit-json_view.cpp @@ -225,7 +225,7 @@ TEST_CASE("json_view") #if JSON_DIAGNOSTICS // the parents are set, so errors name the path const json m = materialized(R"({"a":{"b":[1]}})"); - CHECK_THROWS_WITH_AS(m.at("a").at("b").at(0).at("x"), "[json.exception.type_error.304] (/a/b/0) cannot use at() with number", json::type_error&); + CHECK_THROWS_WITH_AS(m.at("a").at("b").at(0).at("x"), json_view_test::message("type_error.304", "/a/b/0", 11, 12, "cannot use at() with number").c_str(), json::type_error&); #endif } @@ -750,19 +750,6 @@ std::string exception_of(F f) return ""; } -// the exception a call on a basic_json value throws, without the byte range of -// the value (JSON_DIAGNOSTIC_POSITIONS), which the exceptions of views lack -template -std::string json_exception_of(F f) -{ - std::string what = exception_of(f); - const std::size_t at = what.find("] (bytes "); - if (at != std::string::npos) - { - what.erase(at + 2, what.find(") ", at) - at); - } - return what; -} #endif // compares a view with the ordered_json value materialize() gives for it: @@ -971,24 +958,24 @@ TEST_CASE("json_view element access and iteration") #if !defined(JSON_NOEXCEPTION) if (!j.is_object()) { - CHECK(exception_of([&] { static_cast(v["a"]); }) == json_exception_of([&] { static_cast(j["a"]); })); + CHECK(exception_of([&] { static_cast(v["a"]); }) == exception_of([&] { static_cast(j["a"]); })); } if (!j.is_array()) { - CHECK(exception_of([&] { static_cast(v[0]); }) == json_exception_of([&] { static_cast(j[0]); })); + CHECK(exception_of([&] { static_cast(v[0]); }) == exception_of([&] { static_cast(j[0]); })); } - CHECK(exception_of([&] { static_cast(v.at("a")); }) == json_exception_of([&] { static_cast(j.at("a")); })); - CHECK(exception_of([&] { static_cast(v.at("missing")); }) == json_exception_of([&] { static_cast(j.at("missing")); })); - CHECK(exception_of([&] { static_cast(v.at(0)); }) == json_exception_of([&] { static_cast(j.at(0)); })); - CHECK(exception_of([&] { static_cast(v.at(5)); }) == json_exception_of([&] { static_cast(j.at(5)); })); + CHECK(exception_of([&] { static_cast(v.at("a")); }) == exception_of([&] { static_cast(j.at("a")); })); + CHECK(exception_of([&] { static_cast(v.at("missing")); }) == exception_of([&] { static_cast(j.at("missing")); })); + CHECK(exception_of([&] { static_cast(v.at(0)); }) == exception_of([&] { static_cast(j.at(0)); })); + CHECK(exception_of([&] { static_cast(v.at(5)); }) == exception_of([&] { static_cast(j.at(5)); })); if (!(j.is_object() && j.empty())) // (key() of an end iterator) { - CHECK(exception_of([&] { static_cast(v.begin().key()); }) == json_exception_of([&] { static_cast(j.begin().key()); })); + CHECK(exception_of([&] { static_cast(v.begin().key()); }) == exception_of([&] { static_cast(j.begin().key()); })); } if (!j.empty() || j.is_null()) { - CHECK(exception_of([&] { static_cast(v.front()); }) == json_exception_of([&] { static_cast(j.front()); })); - CHECK(exception_of([&] { static_cast(v.back()); }) == json_exception_of([&] { static_cast(j.back()); })); + CHECK(exception_of([&] { static_cast(v.front()); }) == exception_of([&] { static_cast(j.front()); })); + CHECK(exception_of([&] { static_cast(v.back()); }) == exception_of([&] { static_cast(j.back()); })); } #endif CHECK(v.contains("a") == j.contains("a")); @@ -1000,8 +987,8 @@ TEST_CASE("json_view element access and iteration") const json_document d = json_document::parse(R"({"a":[]})"); CHECK(d.root()["b"].is_discarded()); CHECK(d.root()["a"][0].is_discarded()); - CHECK_THROWS_WITH_AS(d.root()["a"].front(), "[json.exception.invalid_iterator.214] cannot get value", json::invalid_iterator&); - CHECK_THROWS_WITH_AS(d.root()["a"].back(), "[json.exception.invalid_iterator.214] cannot get value", json::invalid_iterator&); + CHECK_THROWS_WITH_AS(d.root()["a"].front(), json_view_test::message("invalid_iterator.214", "/a", 5, 7, "cannot get value").c_str(), json::invalid_iterator&); + CHECK_THROWS_WITH_AS(d.root()["a"].back(), json_view_test::message("invalid_iterator.214", "/a", 5, 7, "cannot get value").c_str(), json::invalid_iterator&); const json_view invalid{}; CHECK(invalid.begin() == invalid.end()); CHECK(std::string(invalid.type_name()) == "discarded"); @@ -1032,10 +1019,10 @@ TEST_CASE("json_view element access and iteration") CHECK(v[json_view::json_pointer("/a/b/5")][0].is_discarded()); #if !defined(JSON_NOEXCEPTION) // type errors on values that are not discarded stay - CHECK_THROWS_WITH_AS(v[0], "[json.exception.type_error.305] cannot use operator[] with a numeric argument with object", json::type_error&); - CHECK_THROWS_WITH_AS(v["a"]["b"]["c"], "[json.exception.type_error.305] cannot use operator[] with a string argument with array", json::type_error&); - CHECK_THROWS_WITH_AS(v["s"]["c"], "[json.exception.type_error.305] cannot use operator[] with a string argument with string", json::type_error&); - CHECK_THROWS_WITH_AS(v["s"][0], "[json.exception.type_error.305] cannot use operator[] with a numeric argument with string", json::type_error&); + CHECK_THROWS_WITH_AS(v[0], json_view_test::message("type_error.305", "", 0, 29, "cannot use operator[] with a numeric argument with object").c_str(), json::type_error&); + CHECK_THROWS_WITH_AS(v["a"]["b"]["c"], json_view_test::message("type_error.305", "/a/b", 10, 17, "cannot use operator[] with a string argument with array").c_str(), json::type_error&); + CHECK_THROWS_WITH_AS(v["s"]["c"], json_view_test::message("type_error.305", "/s", 23, 28, "cannot use operator[] with a string argument with string").c_str(), json::type_error&); + CHECK_THROWS_WITH_AS(v["s"][0], json_view_test::message("type_error.305", "/s", 23, 28, "cannot use operator[] with a numeric argument with string").c_str(), json::type_error&); CHECK_THROWS_AS(v["a"]["b"][0]["c"], json::type_error&); CHECK_THROWS_AS(v["s"][json_view::json_pointer("/x")], json::out_of_range&); // at() keeps throwing on a discarded view @@ -1110,9 +1097,9 @@ TEST_CASE("json_view element access and iteration") CHECK(v[(std::numeric_limits::max)()].is_discarded()); CHECK(v[std::numeric_limits::max()].is_discarded()); #if !defined(JSON_NOEXCEPTION) - CHECK_THROWS_WITH_AS(v.at(3), "[json.exception.out_of_range.401] array index 3 is out of range", json::out_of_range&); - CHECK_THROWS_WITH_AS(v.at(3u), "[json.exception.out_of_range.401] array index 3 is out of range", json::out_of_range&); - CHECK_THROWS_WITH_AS(v.at(std::int64_t(3)), "[json.exception.out_of_range.401] array index 3 is out of range", json::out_of_range&); + CHECK_THROWS_WITH_AS(v.at(3), json_view_test::message("out_of_range.401", "", 0, 10, "array index 3 is out of range").c_str(), json::out_of_range&); + CHECK_THROWS_WITH_AS(v.at(3u), json_view_test::message("out_of_range.401", "", 0, 10, "array index 3 is out of range").c_str(), json::out_of_range&); + CHECK_THROWS_WITH_AS(v.at(std::int64_t(3)), json_view_test::message("out_of_range.401", "", 0, 10, "array index 3 is out of range").c_str(), json::out_of_range&); CHECK_THROWS_AS(v.at(-1), json::out_of_range&); CHECK_THROWS_AS(v.at(-1L), json::out_of_range&); CHECK_THROWS_AS(v.at(std::int64_t(-1)), json::out_of_range&); @@ -1246,26 +1233,6 @@ TEST_CASE("json_view element access and iteration") namespace { -#if !defined(JSON_NOEXCEPTION) -// 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; -} -#endif - // the bits of a float, to compare values bit for bit std::uint64_t bits(double x) { @@ -1336,18 +1303,18 @@ void check_values(const ordered_json_view& v, const ordered_json& j, const std:: // 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()); }) == 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()); }))); + CHECK(exception_of([&] { static_cast(v.get()); }) == exception_of([&] { static_cast(j.get()); })); + CHECK(exception_of([&] { static_cast(v.get()); }) == exception_of([&] { static_cast(j.get()); })); + CHECK(exception_of([&] { static_cast(v.get()); }) == 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>()); }))); + CHECK(exception_of([&] { static_cast(v.get>()); }) == 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>()); }))); + CHECK(exception_of([&] { static_cast(v.get>()); }) == exception_of([&] { static_cast(j.get>()); })); } #endif @@ -1490,8 +1457,8 @@ TEST_CASE("json_view values") 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&); + CHECK_THROWS_WITH_AS(v[6].number_token(), json_view_test::message("type_error.302", "/6", 56, 59, "type must be number, but is string").c_str(), json::type_error&); + CHECK_THROWS_WITH_AS(v.get_string(), json_view_test::message("type_error.302", "", 0, 60, "type must be string, but is array").c_str(), json::type_error&); } SECTION("conversions") @@ -1565,15 +1532,15 @@ TEST_CASE("json_view values") 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)); #if !defined(JSON_NOEXCEPTION) - 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)); }))); + CHECK(exception_of([&] { static_cast(v["o"]["x"].value("k", 0)); }) == exception_of([&] { static_cast(j["o"]["x"].value("k", 0)); })); + CHECK(exception_of([&] { static_cast(v.value("s", 0)); }) == exception_of([&] { static_cast(j.value("s", 0)); })); + CHECK(exception_of([&] { static_cast(v["n"].value("x", 0)); }) == exception_of([&] { static_cast(j["n"].value("x", 0)); })); + CHECK(exception_of([&] { static_cast(v["n"].value(json::json_pointer("/x"), 0)); }) == exception_of([&] { static_cast(j["n"].value(json::json_pointer("/x"), 0)); })); // string defaults: the member is not a string, or the value is neither an object nor an array - CHECK(exception_of([&] { static_cast(v.value("n", "default")); }) == without_path(exception_of([&] { static_cast(j.value("n", "default")); }))); - CHECK(exception_of([&] { static_cast(v["n"].value("x", "default")); }) == without_path(exception_of([&] { static_cast(j["n"].value("x", "default")); }))); - CHECK(exception_of([&] { static_cast(v.value(json::json_pointer("/n"), "default")); }) == without_path(exception_of([&] { static_cast(j.value(json::json_pointer("/n"), "default")); }))); - CHECK(exception_of([&] { static_cast(v["s"].value(json::json_pointer("/x"), "default")); }) == without_path(exception_of([&] { static_cast(j["s"].value(json::json_pointer("/x"), "default")); }))); + CHECK(exception_of([&] { static_cast(v.value("n", "default")); }) == exception_of([&] { static_cast(j.value("n", "default")); })); + CHECK(exception_of([&] { static_cast(v["n"].value("x", "default")); }) == exception_of([&] { static_cast(j["n"].value("x", "default")); })); + CHECK(exception_of([&] { static_cast(v.value(json::json_pointer("/n"), "default")); }) == exception_of([&] { static_cast(j.value(json::json_pointer("/n"), "default")); })); + CHECK(exception_of([&] { static_cast(v["s"].value(json::json_pointer("/x"), "default")); }) == exception_of([&] { static_cast(j["s"].value(json::json_pointer("/x"), "default")); })); #endif // (defaults and members longer than the short string optimization) CHECK(v.value("s", "a default longer than fifteen characters") == "text"); @@ -1632,7 +1599,7 @@ TEST_CASE("json_view JSON pointers") { CAPTURE(pointer) const json::json_pointer p(pointer); - const std::string at_error = without_path(exception_of([&] { static_cast(j.at(p)); })); + const std::string at_error = exception_of([&] { static_cast(j.at(p)); }); CHECK(exception_of([&] { static_cast(v.at(p)); }) == at_error); if (at_error.empty()) { @@ -1646,7 +1613,7 @@ TEST_CASE("json_view JSON pointers") } else { - CHECK(exception_of([&] { static_cast(v[p]); }) == without_path(exception_of([&] { static_cast(j[p]); }))); + CHECK(exception_of([&] { static_cast(v[p]); }) == 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 @@ -1657,7 +1624,7 @@ TEST_CASE("json_view JSON pointers") static_cast(found); }); 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)); }))); + CHECK(exception_of([&] { static_cast(v.value(p, 5)); }) == exception_of([&] { static_cast(j.value(p, 5)); })); } } #endif diff --git a/tests/src/unit-json_view_dump.cpp b/tests/src/unit-json_view_dump.cpp index 6470ab680..47b12654e 100644 --- a/tests/src/unit-json_view_dump.cpp +++ b/tests/src/unit-json_view_dump.cpp @@ -462,7 +462,7 @@ TEST_CASE("json_view large objects") CHECK(v[""].get_string() == "empty key"); CHECK(v["k1"].get() == 1); // the first of duplicate keys, as for small objects CHECK(!v.contains("missing")); - CHECK_THROWS_WITH_AS(v.at("missing"), "[json.exception.out_of_range.403] key 'missing' not found", json::out_of_range&); + CHECK_THROWS_WITH_AS(v.at("missing"), json_view_test::message("out_of_range.403", "", 0, text.size(), "key 'missing' not found").c_str(), json::out_of_range&); CHECK(v == j); CHECK(v.materialize() == j); } diff --git a/tests/src/unit-json_view_edit.cpp b/tests/src/unit-json_view_edit.cpp index 1b1a24ec8..2cc3d4701 100644 --- a/tests/src/unit-json_view_edit.cpp +++ b/tests/src/unit-json_view_edit.cpp @@ -532,18 +532,18 @@ TEST_CASE("json_view edits: errors") CHECK_THROWS_WITH_AS(d.set(other_editable.root(), 1), "[json.exception.invalid_iterator.202] view does not belong to this document", json::invalid_iterator&); CHECK_THROWS_WITH_AS(d.set(json_editable_view(), 1), "[json.exception.invalid_iterator.202] view does not belong to this document", json::invalid_iterator&); - CHECK_THROWS_WITH_AS(d.set(root["a"], "k", 1), "[json.exception.type_error.305] cannot use operator[] with a string argument with array", json::type_error&); - CHECK_THROWS_WITH_AS(d.set(root["o"], 0, 1), "[json.exception.type_error.305] cannot use operator[] with a numeric argument with object", json::type_error&); - CHECK_THROWS_WITH_AS(d.set(root["a"], 2, 1), "[json.exception.out_of_range.401] array index 2 is out of range", json::out_of_range&); - CHECK_THROWS_WITH_AS(d.set(root["a"], -1, 1), "[json.exception.out_of_range.401] array index -1 is out of range", json::out_of_range&); - CHECK_THROWS_WITH_AS(d.push_back(root["o"], 1), "[json.exception.type_error.308] cannot use push_back() with object", json::type_error&); - CHECK_THROWS_WITH_AS(d.insert(root["n"], 0, 1), "[json.exception.type_error.309] cannot use insert() with number", json::type_error&); - CHECK_THROWS_WITH_AS(d.insert(root["a"], 3, 1), "[json.exception.out_of_range.401] array index 3 is out of range", json::out_of_range&); - CHECK_THROWS_WITH_AS(d.erase(root["n"], "k"), "[json.exception.type_error.307] cannot use erase() with number", json::type_error&); - CHECK_THROWS_WITH_AS(d.erase(root["o"], 0), "[json.exception.type_error.307] cannot use erase() with object", json::type_error&); - CHECK_THROWS_WITH_AS(d.erase(root["a"], 2), "[json.exception.out_of_range.401] array index 2 is out of range", json::out_of_range&); + CHECK_THROWS_WITH_AS(d.set(root["a"], "k", 1), json_view_test::message("type_error.305", "/a", 21, 27, "cannot use operator[] with a string argument with array").c_str(), json::type_error&); + CHECK_THROWS_WITH_AS(d.set(root["o"], 0, 1), json_view_test::message("type_error.305", "/o", 6, 14, "cannot use operator[] with a numeric argument with object").c_str(), json::type_error&); + CHECK_THROWS_WITH_AS(d.set(root["a"], 2, 1), json_view_test::message("out_of_range.401", "/a", 21, 27, "array index 2 is out of range").c_str(), json::out_of_range&); + CHECK_THROWS_WITH_AS(d.set(root["a"], -1, 1), json_view_test::message("out_of_range.401", "/a", 21, 27, "array index -1 is out of range").c_str(), json::out_of_range&); + CHECK_THROWS_WITH_AS(d.push_back(root["o"], 1), json_view_test::message("type_error.308", "/o", 6, 14, "cannot use push_back() with object").c_str(), json::type_error&); + CHECK_THROWS_WITH_AS(d.insert(root["n"], 0, 1), json_view_test::message("type_error.309", "/n", 34, 35, "cannot use insert() with number").c_str(), json::type_error&); + CHECK_THROWS_WITH_AS(d.insert(root["a"], 3, 1), json_view_test::message("out_of_range.401", "/a", 21, 27, "array index 3 is out of range").c_str(), json::out_of_range&); + CHECK_THROWS_WITH_AS(d.erase(root["n"], "k"), json_view_test::message("type_error.307", "/n", 34, 35, "cannot use erase() with number").c_str(), json::type_error&); + CHECK_THROWS_WITH_AS(d.erase(root["o"], 0), json_view_test::message("type_error.307", "/o", 6, 14, "cannot use erase() with object").c_str(), json::type_error&); + CHECK_THROWS_WITH_AS(d.erase(root["a"], 2), json_view_test::message("out_of_range.401", "/a", 21, 27, "array index 2 is out of range").c_str(), json::out_of_range&); CHECK_THROWS_WITH_AS(d.erase(json::json_pointer("")), "[json.exception.out_of_range.405] JSON pointer has no parent", json::out_of_range&); - CHECK_THROWS_WITH_AS(d.erase(json::json_pointer("/missing/x")), "[json.exception.out_of_range.403] key 'missing' not found", json::out_of_range&); + CHECK_THROWS_WITH_AS(d.erase(json::json_pointer("/missing/x")), json_view_test::message("out_of_range.403", "", 0, 47, "key 'missing' not found").c_str(), json::out_of_range&); CHECK_THROWS_WITH_AS(d.set(json::json_pointer("/a/01"), 1), "[json.exception.parse_error.106] parse error: array index '01' must not begin with '0'", json::parse_error&); CHECK_THROWS_WITH_AS(d.set(root, json_editable_view()), "[json.exception.type_error.302] type must be a value, but is discarded", json::type_error&); CHECK_THROWS_WITH_AS(d.set(root, json::binary({1, 2})), "[json.exception.type_error.319] cannot store a binary value in a json_document", json::type_error&); diff --git a/tests/src/unit-json_view_from_json.cpp b/tests/src/unit-json_view_from_json.cpp index 7e84a9081..bd4d3891f 100644 --- a/tests/src/unit-json_view_from_json.cpp +++ b/tests/src/unit-json_view_from_json.cpp @@ -337,8 +337,7 @@ TEST_CASE("json_view from_json") CHECK(d.root().get>().at("a") == 3); } - // (the exceptions of views have neither the path nor the byte range of the value) -#if !defined(JSON_NOEXCEPTION) && !JSON_DIAGNOSTICS && !JSON_DIAGNOSTIC_POSITIONS +#if !defined(JSON_NOEXCEPTION) SECTION("std types: the same errors as from the materialized value") { const json_document d = json_document::parse(R"([[1, "x"], {"a": 1}, "text", [1], 5, null])"); @@ -403,10 +402,10 @@ TEST_CASE("json_view from_json") CHECK(d.root()[4].get() == strict_color::red); #if !defined(JSON_NOEXCEPTION) const json j = d.root().materialize(); - CHECK(exception_of_call([&] { static_cast(d.root()[2].get()); }) == "[json.exception.out_of_range.410] enum value out of range for strict_color: \"purple\""); #if !JSON_DIAGNOSTICS && !JSON_DIAGNOSTIC_POSITIONS - CHECK(exception_of_call([&] { static_cast(d.root()[2].get()); }) == exception_of_call([&] { static_cast(j[2].get()); })); + CHECK(exception_of_call([&] { static_cast(d.root()[2].get()); }) == "[json.exception.out_of_range.410] enum value out of range for strict_color: \"purple\""); #endif + CHECK(exception_of_call([&] { static_cast(d.root()[2].get()); }) == exception_of_call([&] { static_cast(j[2].get()); })); #endif } @@ -452,11 +451,9 @@ TEST_CASE("json_view from_json") CHECK(o.root().get().center.y == 2); } -#if !defined(JSON_NOEXCEPTION) && !JSON_DIAGNOSTICS && !JSON_DIAGNOSTIC_POSITIONS +#if !defined(JSON_NOEXCEPTION) SECTION("errors are those of the same conversion of a json value") { - // (with JSON_DIAGNOSTICS, those of json name the path of the value, - // and with JSON_DIAGNOSTIC_POSITIONS its bytes) const json_document d = json_document::parse(R"([{"x": 1}, {"x": "1", "y": 2}, [1, 2], {"id": "7", "c": "red"}])"); const json j = d.root().materialize(); for (std::size_t i = 0; i < 3; ++i) diff --git a/tests/src/unit-json_view_positions.cpp b/tests/src/unit-json_view_positions.cpp index 08ea47a08..b187365b3 100644 --- a/tests/src/unit-json_view_positions.cpp +++ b/tests/src/unit-json_view_positions.cpp @@ -81,6 +81,111 @@ std::string range(const json& j) { return j.start_pos() == npos && j.end_pos() == npos ? "none" : std::to_string(j.start_pos()) + "-" + std::to_string(j.end_pos()); } + +#if !defined(JSON_NOEXCEPTION) +using json_view_test::exception_of_call; + +// the exceptions about every value of v are those about the value at the same +// place in j, a value of the whole document from parse() or materialize() (so +// that its parents and positions are set); members whose key repeats are +// skipped (the view finds the first one, j holds the last) +template +void check_exceptions(const View& v, const json& j) +{ + CAPTURE(j.dump()) + if (!v.is_number()) + { + CHECK(exception_of_call([&] { static_cast(v.template get()); }) == exception_of_call([&] { static_cast(j.get()); })); + } + if (!v.is_string()) + { + CHECK(exception_of_call([&] { static_cast(v.template get()); }) == exception_of_call([&] { static_cast(j.get()); })); + } + if (!v.is_array()) + { + CHECK(exception_of_call([&] { static_cast(v.template get>()); }) == exception_of_call([&] { static_cast(j.get>()); })); + } + CHECK(exception_of_call([&] { static_cast(v.at("missing")); }) == exception_of_call([&] { static_cast(j.at("missing")); })); + CHECK(exception_of_call([&] { static_cast(v.at(1000)); }) == exception_of_call([&] { static_cast(j.at(1000)); })); + CHECK(exception_of_call([&] { static_cast(v.at(json::json_pointer("/missing"))); }) == exception_of_call([&] { static_cast(j.at(json::json_pointer("/missing"))); })); + CHECK(exception_of_call([&] { static_cast(v.at(json::json_pointer("/-"))); }) == exception_of_call([&] { static_cast(j.at(json::json_pointer("/-"))); })); + CHECK(exception_of_call([&] { static_cast(v.value("k", 0)); }) == exception_of_call([&] { static_cast(j.value("k", 0)); })); + if (!v.is_object()) + { + CHECK(exception_of_call([&] { static_cast(v["k"]); }) == exception_of_call([&] { static_cast(j["k"]); })); + CHECK(exception_of_call([&] { static_cast(v.begin().key()); }) == exception_of_call([&] { static_cast(j.begin().key()); })); + } + if (!v.is_array()) + { + CHECK(exception_of_call([&] { static_cast(v[0]); }) == exception_of_call([&] { static_cast(j[0]); })); + } + if (v.is_null()) + { + CHECK(exception_of_call([&] { static_cast(v.front()); }) == exception_of_call([&] { static_cast(j.front()); })); + } + 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.count(key) == 1) + { + check_exceptions(it.value(), j.at(key)); + } + } + } + else if (v.is_array()) + { + for (std::size_t i = 0; i < v.size(); ++i) + { + check_exceptions(v[i], j[i]); + } + } +} + +// as check_exceptions, for edits that fail (they change neither d nor j) +void check_edit_exceptions(json_editable_document& d, const json_editable_document::view_type& v, json& j) +{ + CAPTURE(j.dump()) + if (!v.is_object() && !v.is_null()) + { + CHECK(exception_of_call([&] { d.set(v, "k", 1); }) == exception_of_call([&] { j["k"] = 1; })); + CHECK(exception_of_call([&] { d.erase(v, "k"); }) == exception_of_call([&] { j.erase("k"); })); + } + if (!v.is_array() && !v.is_null()) + { + CHECK(exception_of_call([&] { d.set(v, 0, 1); }) == exception_of_call([&] { j[0] = 1; })); + CHECK(exception_of_call([&] { d.push_back(v, 1); }) == exception_of_call([&] { j.push_back(1); })); + } + if (!v.is_array()) + { + CHECK(exception_of_call([&] { d.insert(v, 0, 1); }) == exception_of_call([&] { j.insert(j.cbegin(), 1); })); + CHECK(exception_of_call([&] { d.erase(v, 0); }) == exception_of_call([&] { j.erase(0); })); + } + else + { + CHECK(exception_of_call([&] { d.erase(v, 1000); }) == exception_of_call([&] { j.erase(1000); })); + } + 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.count(key) == 1) + { + check_edit_exceptions(d, it.value(), j.at(key)); + } + } + } + else if (v.is_array()) + { + for (std::size_t i = 0; i < v.size(); ++i) + { + check_edit_exceptions(d, v[i], j[i]); + } + } +} +#endif } // namespace TEST_CASE("json_view materialize() source positions") @@ -345,3 +450,96 @@ TEST_CASE("json_view materialize() source positions") } #endif } + +#if !defined(JSON_NOEXCEPTION) +TEST_CASE("json_view exceptions name the path and the bytes of the value") +{ + SECTION("as those of basic_json") + { + const std::vector texts = + { + "null", "true", "42", "\"s\"", "[]", "{}", "[1, [2, [3, []]]]", R"({"a": {"b": [1, {"c": null}]}, "d": "e"})", + R"({"a/b": {"~": [1]}, "\u0041\n": {"x": "\t"}, "": [{}]})", + R"([1 /* ] */, {"a": [] // } +, "b": {} } , ])", + "\n\t[\r\n 1 ,\n {\n \"a\" : [ ]\n }\n]\n", + }; + for (const auto& text : texts) + { + CAPTURE(text) + const json j = json::parse(text, nullptr, true, true, true); + const json_document d = json_document::parse(text, true, true, true); + check_exceptions(d.root(), j); + json_editable_document e = json_editable_document::parse(text, true, true, true); + check_exceptions(e.root(), j); + json m = e.root().materialize(); + check_edit_exceptions(e, e.root(), m); + } + } + + SECTION("generated documents") + { + json_view_test::generator gen; + for (int i = 0; i < 100; ++i) + { + std::string text; + gen.value(text, 0); + CAPTURE(text) + const json_document d = json_document::parse(text); + check_exceptions(d.root(), json::parse(text)); + } + } + + SECTION("edited documents: as those of materialize()") + { + const std::string text = R"({"a": [1, 2], "b": {"c": "\n", "d": true}, "e": [3, [4]], "f": "x"})"; + json_editable_document d = json_editable_document::parse(text); + d.set(d.root()["f"], 5); + d.push_back(d.root()["a"], json::parse(R"({"n": [null]})")); + d.erase(d.root()["e"], 0); + d.set(d.root()["b"], "c2", d.root()["e"]); + d.insert(d.root()["e"], 0, "new"); + json m = d.root().materialize(); + check_exceptions(d.root(), m); + check_edit_exceptions(d, d.root(), m); + // (a value inside a new container: a path, but no bytes) + CHECK(exception_of_call([&] { static_cast(d.root()["a"][2]["n"][0].get()); }) == "[json.exception.type_error.302] " +#if JSON_DIAGNOSTICS + "(/a/2/n/0) " +#endif + "type must be number, but is null"); + } + + SECTION("examples") + { + const std::string text = R"({"people": [{"name": "a", "age": 1}, {"name": "c", "age": "x"}], "o": {"k": 1}})"; + const json_document d = json_document::parse(text); + CHECK(exception_of_call([&] { static_cast(d.root()["people"][1]["age"].get()); }) == "[json.exception.type_error.302] " +#if JSON_DIAGNOSTICS + "(/people/1/age) " +#endif + "(bytes 58-61) type must be number, but is string"); + CHECK(exception_of_call([&] { static_cast(d.root().at(json::json_pointer("/people/0/zz"))); }) == "[json.exception.out_of_range.403] " +#if JSON_DIAGNOSTICS + "(/people/0) " +#endif + "(bytes 12-35) key 'zz' not found"); + // the root has no path, and with JSON_DIAGNOSTICS then no bytes either (as basic_json) + CHECK(exception_of_call([&] { static_cast(d.root().at("missing")); }) == "[json.exception.out_of_range.403] " +#if !JSON_DIAGNOSTICS + "(bytes 0-79) " +#endif + "key 'missing' not found"); + } + + SECTION("duplicate keys: the value the view finds") + { + const json_document d = json_document::parse(R"({"x": "1", "x": "22"})"); + CHECK(exception_of_call([&] { static_cast(d.root()["x"].get()); }) == "[json.exception.type_error.302] " +#if JSON_DIAGNOSTICS + "(/x) " +#endif + "(bytes 6-9) type must be number, but is string"); + } +} +#endif