diff --git a/include/nlohmann/json_view.hpp b/include/nlohmann/json_view.hpp index 13c9db53a..31e37d9c0 100644 --- a/include/nlohmann/json_view.hpp +++ b/include/nlohmann/json_view.hpp @@ -295,7 +295,7 @@ class basic_json_view } detail::view::throw_type_error(305, "cannot use operator[] with a numeric argument with ", type_name()); } - return idx < m_node->len ? basic_json_view(m_doc, navigation::value(detail::view::element_at(*m_doc, m_node, idx))) : basic_json_view(); + return idx < m_node->len ? element(idx) : basic_json_view(); } /// any other integer type (int, unsigned, long, std::int64_t, ...; a @@ -358,7 +358,7 @@ class basic_json_view { detail::view::throw_out_of_range(401, detail::concat("array index ", std::to_string(idx), " is out of range")); } - return basic_json_view(m_doc, navigation::value(detail::view::element_at(*m_doc, m_node, idx))); + return element(idx); } /// any other integer type, see operator[]; negative values are out of @@ -444,7 +444,8 @@ class basic_json_view { if (is_structured() && m_node->len != 0) { - return basic_json_view(m_doc, navigation::value(detail::view::last_child(*m_doc, m_node) + (is_object() ? 1 : 0))); + return unedited() ? basic_json_view(m_doc, detail::view::last_child(*m_doc, m_node) + (is_object() ? 1 : 0)) + : basic_json_view(m_doc, navigation::value(detail::view::last_child(*m_doc, m_node) + (is_object() ? 1 : 0))); } return front(); } @@ -461,7 +462,8 @@ class basic_json_view { return end(); } - const node* const k = detail::view::find_member(*m_doc, m_node, key.data(), key.size()); + const node* const k = unedited() ? detail::view::find_member(*m_doc, m_node, key.data(), key.size()) + : detail::view::find_member(*m_doc, m_node, key.data(), key.size()); return k != nullptr ? iterator(m_doc, k, true) : end(); } @@ -478,7 +480,8 @@ class basic_json_view /// whether this is an object with a member with this key bool contains(string_view_t key) const { - return is_object() && detail::view::find_member(*m_doc, m_node, key.data(), key.size()) != nullptr; + return is_object() && (unedited() ? detail::view::find_member(*m_doc, m_node, key.data(), key.size()) != nullptr + : detail::view::find_member(*m_doc, m_node, key.data(), key.size()) != nullptr); } bool contains(const char* key) const @@ -638,7 +641,14 @@ class basic_json_view // the compact writer keeps 64 bytes of slack, so that it does not grow // the buffer just before the end const std::size_t estimate = source_extent() + (style.pretty ? source_extent() / 2 : 0) + 160; - detail::view::view_serializer(*m_doc, out, estimate, style).dump(m_node); + if (unedited()) + { + detail::view::view_serializer(*m_doc, out, estimate, style).dump(m_node); + } + else + { + detail::view::view_serializer(*m_doc, out, estimate, style).dump(m_node); + } return out; } @@ -718,7 +728,7 @@ class basic_json_view { return BasicJsonType(value_t::discarded); } - return detail::view::materialize(*m_doc, m_node); + return unedited() ? detail::view::materialize(*m_doc, m_node) : detail::view::materialize(*m_doc, m_node); } /// byte offset of this value in the source text (for strings: of the @@ -796,10 +806,32 @@ class basic_json_view /// (object required) NLOHMANN_VIEW_ALWAYS_INLINE basic_json_view lookup(string_view_t key) const noexcept { + if (unedited()) + { + const node* const k = detail::view::find_member(*m_doc, m_node, key.data(), key.size()); + return k != nullptr ? basic_json_view(m_doc, k + 1) : basic_json_view(); + } const node* const k = detail::view::find_member(*m_doc, m_node, key.data(), key.size()); return k != nullptr ? basic_json_view(m_doc, navigation::value(k + 1)) : basic_json_view(); } + /// whether the document has not been edited yet: until the first edit, its index is walked like that of a + /// read-only document (nothing is moved or linked), which is cheaper + NLOHMANN_VIEW_ALWAYS_INLINE bool unedited() const noexcept + { + return !navigation::editable() || m_doc->edits == nullptr; + } + + /// the element of an array at an index below its size + NLOHMANN_VIEW_ALWAYS_INLINE basic_json_view element(size_type idx) const + { + if (unedited()) + { + return basic_json_view(m_doc, detail::view::element_at(*m_doc, m_node, idx)); + } + return basic_json_view(m_doc, navigation::value(detail::view::element_at(*m_doc, m_node, idx))); + } + // --- get() dispatch --- bool get_impl(detail::view::value_tag /*unused*/, detail::priority_tag<2> /*unused*/) const diff --git a/single_include/nlohmann/json_view.hpp b/single_include/nlohmann/json_view.hpp index 960d204d7..3ca6f7fad 100644 --- a/single_include/nlohmann/json_view.hpp +++ b/single_include/nlohmann/json_view.hpp @@ -7875,7 +7875,7 @@ class basic_json_view } detail::view::throw_type_error(305, "cannot use operator[] with a numeric argument with ", type_name()); } - return idx < m_node->len ? basic_json_view(m_doc, navigation::value(detail::view::element_at(*m_doc, m_node, idx))) : basic_json_view(); + return idx < m_node->len ? element(idx) : basic_json_view(); } /// any other integer type (int, unsigned, long, std::int64_t, ...; a @@ -7938,7 +7938,7 @@ class basic_json_view { detail::view::throw_out_of_range(401, detail::concat("array index ", std::to_string(idx), " is out of range")); } - return basic_json_view(m_doc, navigation::value(detail::view::element_at(*m_doc, m_node, idx))); + return element(idx); } /// any other integer type, see operator[]; negative values are out of @@ -8024,7 +8024,8 @@ class basic_json_view { if (is_structured() && m_node->len != 0) { - return basic_json_view(m_doc, navigation::value(detail::view::last_child(*m_doc, m_node) + (is_object() ? 1 : 0))); + return unedited() ? basic_json_view(m_doc, detail::view::last_child(*m_doc, m_node) + (is_object() ? 1 : 0)) + : basic_json_view(m_doc, navigation::value(detail::view::last_child(*m_doc, m_node) + (is_object() ? 1 : 0))); } return front(); } @@ -8041,7 +8042,8 @@ class basic_json_view { return end(); } - const node* const k = detail::view::find_member(*m_doc, m_node, key.data(), key.size()); + const node* const k = unedited() ? detail::view::find_member(*m_doc, m_node, key.data(), key.size()) + : detail::view::find_member(*m_doc, m_node, key.data(), key.size()); return k != nullptr ? iterator(m_doc, k, true) : end(); } @@ -8058,7 +8060,8 @@ class basic_json_view /// whether this is an object with a member with this key bool contains(string_view_t key) const { - return is_object() && detail::view::find_member(*m_doc, m_node, key.data(), key.size()) != nullptr; + return is_object() && (unedited() ? detail::view::find_member(*m_doc, m_node, key.data(), key.size()) != nullptr + : detail::view::find_member(*m_doc, m_node, key.data(), key.size()) != nullptr); } bool contains(const char* key) const @@ -8218,7 +8221,14 @@ class basic_json_view // the compact writer keeps 64 bytes of slack, so that it does not grow // the buffer just before the end const std::size_t estimate = source_extent() + (style.pretty ? source_extent() / 2 : 0) + 160; - detail::view::view_serializer(*m_doc, out, estimate, style).dump(m_node); + if (unedited()) + { + detail::view::view_serializer(*m_doc, out, estimate, style).dump(m_node); + } + else + { + detail::view::view_serializer(*m_doc, out, estimate, style).dump(m_node); + } return out; } @@ -8298,7 +8308,7 @@ class basic_json_view { return BasicJsonType(value_t::discarded); } - return detail::view::materialize(*m_doc, m_node); + return unedited() ? detail::view::materialize(*m_doc, m_node) : detail::view::materialize(*m_doc, m_node); } /// byte offset of this value in the source text (for strings: of the @@ -8376,10 +8386,32 @@ class basic_json_view /// (object required) NLOHMANN_VIEW_ALWAYS_INLINE basic_json_view lookup(string_view_t key) const noexcept { + if (unedited()) + { + const node* const k = detail::view::find_member(*m_doc, m_node, key.data(), key.size()); + return k != nullptr ? basic_json_view(m_doc, k + 1) : basic_json_view(); + } const node* const k = detail::view::find_member(*m_doc, m_node, key.data(), key.size()); return k != nullptr ? basic_json_view(m_doc, navigation::value(k + 1)) : basic_json_view(); } + /// whether the document has not been edited yet: until the first edit, its index is walked like that of a + /// read-only document (nothing is moved or linked), which is cheaper + NLOHMANN_VIEW_ALWAYS_INLINE bool unedited() const noexcept + { + return !navigation::editable() || m_doc->edits == nullptr; + } + + /// the element of an array at an index below its size + NLOHMANN_VIEW_ALWAYS_INLINE basic_json_view element(size_type idx) const + { + if (unedited()) + { + return basic_json_view(m_doc, detail::view::element_at(*m_doc, m_node, idx)); + } + return basic_json_view(m_doc, navigation::value(detail::view::element_at(*m_doc, m_node, idx))); + } + // --- get() dispatch --- bool get_impl(detail::view::value_tag /*unused*/, detail::priority_tag<2> /*unused*/) const diff --git a/tests/src/unit-json_view_edit.cpp b/tests/src/unit-json_view_edit.cpp index 4546ad05b..4a6fbe69d 100644 --- a/tests/src/unit-json_view_edit.cpp +++ b/tests/src/unit-json_view_edit.cpp @@ -724,6 +724,52 @@ TEST_CASE("json_view edits: materializing values nested deeper than the document CHECK(d.root().get() == expected); } +TEST_CASE("json_view edits: an editable document before and after its first edit") +{ + // until the first edit, an editable document is read like a read-only one; views made before the edit + // keep working after it + const std::string text = R"({"a":[1,2,{"x":"y"},4],"b":{"c":true,"d":null},"e":"text","f":[]})"; + const json_document read_only = json_document::parse(text); + json_editable_document d = json_editable_document::parse(text); + const auto root = d.root(); + const auto same_as_read_only = [&] + { + CHECK(root.dump() == read_only.root().dump()); + CHECK(root.dump(2) == read_only.root().dump(2)); + CHECK(root.materialize() == read_only.root().materialize()); + CHECK(root["a"][2]["x"].get_string() == "y"); + CHECK(root["a"].at(3).get() == 4); + CHECK(root["a"].back().get() == 4); + CHECK(root.contains("b")); + CHECK(!root.contains("zz")); + CHECK(root.find("e")->get_string() == "text"); + CHECK(root.find("zz") == root.end()); + CHECK(root["b"]["c"].get()); + CHECK(root.value("e", "none") == "text"); + std::size_t count = 0; + for (const auto m : root["a"]) + { + CHECK(m.materialize() == read_only.root()["a"][count].materialize()); + ++count; + } + CHECK(count == 4); + }; + same_as_read_only(); + // an edit that changes a scalar in place, then one that moves an array + d.set(root["e"], "other"); + CHECK(root["e"].get_string() == "other"); + CHECK(root["a"][2]["x"].get_string() == "y"); + CHECK(root["a"].back().get() == 4); + d.push_back(root["f"], 7); + CHECK(root["f"][0].get() == 7); + CHECK(root["f"].back().get() == 7); + CHECK(root.dump() == R"({"a":[1,2,{"x":"y"},4],"b":{"c":true,"d":null},"e":"other","f":[7]})"); + d.set(root["a"], 2, json::array({1, 2})); + CHECK(root["a"][2][1].get() == 2); + CHECK(root["a"].dump() == "[1,2,[1,2],4]"); + CHECK(root.materialize() == json::parse(R"({"a":[1,2,[1,2],4],"b":{"c":true,"d":null},"e":"other","f":[7]})")); +} + namespace { // reads of "a" in an object that repeats it: the first member wins, however the