From 8ac510b8d35bd6d0a9554e4d6a9cfe285d53773d Mon Sep 17 00:00:00 2001 From: Niels Lohmann Date: Fri, 30 Oct 2026 21:51:23 +0100 Subject: [PATCH] json_view: exceptions name the path and the bytes of the value With JSON_DIAGNOSTICS and JSON_DIAGNOSTIC_POSITIONS, the exceptions of views now carry the same context as those of basic_json: "(/path) " and "(bytes a-b) " in front of the message, in basic_json's format (with JSON_DIAGNOSTICS, no context for the root). Before, views threw without any context, and since get() reads the view directly, conversions had lost even the relative path they used to get through materialize(). The throw helpers of detail/view/errors.hpp take an error_site (document and node; empty without the two macros). Only on the error path, the path is found by descending from the root (pruned by subtree extent before the first edit, a full walk through edited sequences after), and the bytes as materialize() sets them (position_source; a container's end by following the chain of last elements and scanning to the closing brackets). from_json() for views and the strict enum macro put the same context in front through exception_prefix(). Iterators keep their container only under the two macros, for key(). Without the macros, the accessors compile to the same code as before. Tests compare every value's exceptions with those of the parsed or materialized basic_json value, including edited documents; the checks that were disabled under the two macros run again. Signed-off-by: Niels Lohmann --- docs/mkdocs/docs/api/basic_json_view/at.md | 7 +- docs/mkdocs/docs/api/basic_json_view/back.md | 6 +- docs/mkdocs/docs/api/basic_json_view/front.md | 6 +- docs/mkdocs/docs/api/basic_json_view/get.md | 15 +- .../docs/api/basic_json_view/get_string.md | 6 +- .../docs/api/basic_json_view/number_token.md | 6 +- .../docs/api/basic_json_view/operator[].md | 7 +- docs/mkdocs/docs/api/basic_json_view/value.md | 7 +- docs/mkdocs/docs/features/json_view.md | 25 +- include/nlohmann/detail/exceptions.hpp | 24 +- include/nlohmann/detail/macro_scope.hpp | 2 +- include/nlohmann/detail/view/edit.hpp | 22 +- include/nlohmann/detail/view/errors.hpp | 304 +++- include/nlohmann/detail/view/iterator.hpp | 27 +- include/nlohmann/detail/view/pointer.hpp | 9 +- include/nlohmann/detail/view/value.hpp | 6 +- include/nlohmann/json_view.hpp | 69 +- single_include/nlohmann/json.hpp | 26 +- single_include/nlohmann/json_view.hpp | 1344 ++++++++++------- tests/src/json_view_test_helpers.hpp | 21 + tests/src/unit-json_view.cpp | 109 +- tests/src/unit-json_view_dump.cpp | 2 +- tests/src/unit-json_view_edit.cpp | 22 +- tests/src/unit-json_view_from_json.cpp | 11 +- tests/src/unit-json_view_positions.cpp | 198 +++ 25 files changed, 1571 insertions(+), 710 deletions(-) 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