From 76897a9b9b1920f0818d9207b48f80c273659e1a Mon Sep 17 00:00:00 2001 From: Niels Lohmann Date: Tue, 29 Sep 2026 00:20:25 +0200 Subject: [PATCH] Add editable documents with set() and push_back() basic_json_document (json_editable_document, ordered_json_editable_document) can be edited: - set(view, value): replace a value - set(object, key, value): assign a member, or add it (a null becomes an object); with duplicate keys, the first is assigned and the others go - set(array, index, value): assign an element - set(json_pointer, value): the member, element, or ("-", or the size of the array) the end of an array a pointer names - push_back(array, value): append (a null becomes an array) Values are views (of any document, copied), BasicJsonType values, and everything BasicJsonType can be constructed from. The source text is never written, and the parsed index never moves: new values and element sequences go to storage owned by the document (edit_storage.hpp), so views stay valid, and a view keeps referring to its value (after an assignment, it sees the new one). Read-only documents are unchanged; editing one does not compile. Errors are those of basic_json where the operation corresponds (type_error.305/308, out_of_range.401/403/405, parse_error.106/109); a view of another document is invalid_iterator.202. Strings are checked for UTF-8 when they enter the document, with the type_error.316 that basic_json::dump() throws for the same string, so that a document only holds valid UTF-8. Binary values cannot be stored (the new type_error.319), and edits of 4 GiB or more end with out_of_range.416. Views of editable and read-only documents compare with each other. Tests (unit-json_view_edit.cpp): random assignments, member and element changes, copies within and between documents, and pushes, applied to an ordered_json_editable_document and to the ordered_json value; after every edit both must serialize (also indented and with ensure_ascii), materialize, compare, and read back the same. Further: the errors, strings that stay valid while the edit arena grows, numbers (NaN, infinities, extremes; number_format::source), nulls that become containers, the root replaced, duplicate keys, values of other documents, large objects, and documents reused with read(). Signed-off-by: Niels Lohmann --- BUILD.bazel | 2 + include/nlohmann/detail/view/edit.hpp | 704 +++++++ include/nlohmann/detail/view/edit_storage.hpp | 249 +++ include/nlohmann/detail/view/errors.hpp | 5 + include/nlohmann/json_view.hpp | 140 ++ single_include/nlohmann/json_view.hpp | 1688 ++++++++++++++--- tests/src/unit-json_view_edit.cpp | 466 +++++ 7 files changed, 2965 insertions(+), 289 deletions(-) create mode 100644 include/nlohmann/detail/view/edit.hpp create mode 100644 include/nlohmann/detail/view/edit_storage.hpp create mode 100644 tests/src/unit-json_view_edit.cpp diff --git a/BUILD.bazel b/BUILD.bazel index 021b8a11e..273d93762 100644 --- a/BUILD.bazel +++ b/BUILD.bazel @@ -69,6 +69,8 @@ cc_library( "include/nlohmann/detail/view/builder.hpp", "include/nlohmann/detail/view/compare.hpp", "include/nlohmann/detail/view/document_data.hpp", + "include/nlohmann/detail/view/edit.hpp", + "include/nlohmann/detail/view/edit_storage.hpp", "include/nlohmann/detail/view/errors.hpp", "include/nlohmann/detail/view/input.hpp", "include/nlohmann/detail/view/iterator.hpp", diff --git a/include/nlohmann/detail/view/edit.hpp b/include/nlohmann/detail/view/edit.hpp new file mode 100644 index 000000000..1c0e272e4 --- /dev/null +++ b/include/nlohmann/detail/view/edit.hpp @@ -0,0 +1,704 @@ +// __ _____ _____ _____ +// __| | __| | | | JSON for Modern C++ +// | | |__ | | | | | | version 3.12.0 +// |_____|_____|_____|_|___| https://github.com/nlohmann/json +// +// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann +// SPDX-License-Identifier: MIT + +#pragma once + +#include // array +#include // isinf, isnan +#include // size_t +#include // int64_t, uint8_t, uint32_t, uint64_t +#include // memcmp, memmove +#include // numeric_limits +#include // string, to_string +#include // decay, enable_if, integral_constant, is_arithmetic, is_convertible, is_floating_point, is_same, is_signed +#include // forward + +#include +#include +#include +#include +#include +#include +#include + +NLOHMANN_JSON_NAMESPACE_BEGIN + +template +class basic_json_view; + +namespace detail +{ +namespace view +{ + +/// Checks a string the way basic_json's serializer does when it writes it +/// (type_error.316 with the same message), so that an editable document +/// only holds valid UTF-8: the error is at the first byte that no +/// well-formed sequence can continue with (Unicode, Table 3-7). +inline void check_utf8(const char* s, std::size_t n) +{ + const auto* const p = reinterpret_cast(s); // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast) + const auto hex = [](unsigned char c) + { + constexpr const char* digits = "0123456789ABCDEF"; + return std::string{digits[c >> 4u], digits[c & 0xFu]}; + }; + for (std::size_t i = 0; i < n;) + { + const unsigned char c = p[i]; + if (c < 0x80) + { + ++i; + continue; + } + std::size_t len = 0; + unsigned char lo = 0x80; + unsigned char hi = 0xBF; + if (c >= 0xC2 && c <= 0xDF) + { + len = 2; + } + else if (c >= 0xE0 && c <= 0xEF) + { + len = 3; + lo = c == 0xE0 ? 0xA0 : 0x80; + hi = c == 0xED ? 0x9F : 0xBF; + } + else if (c >= 0xF0 && c <= 0xF4) + { + len = 4; + lo = c == 0xF0 ? 0x90 : 0x80; + hi = c == 0xF4 ? 0x8F : 0xBF; + } + else + { + throw_type_error(316, concat("invalid UTF-8 byte at index ", std::to_string(i), ": 0x", hex(c))); + } + for (std::size_t k = 1; k < len; ++k) + { + if (i + k == n) + { + throw_type_error(316, concat("incomplete UTF-8 string; last byte: 0x", hex(p[n - 1]))); + } + const unsigned char b = p[i + k]; + if (b < (k == 1 ? lo : 0x80) || b > (k == 1 ? hi : 0xBF)) + { + throw_type_error(316, concat("invalid UTF-8 byte at index ", std::to_string(i + k), ": 0x", hex(b))); + } + } + i += len; + } +} + +/*! +@brief the edits of an editable basic_json_document + +Values are accepted as views (of any document), BasicJsonType values, and +everything BasicJsonType can be constructed from. The source text is never +written: new values go to storage owned by the document (see +edit_storage.hpp). +*/ +template +class editor +{ + using number_integer_t = typename BasicJsonType::number_integer_t; + using number_unsigned_t = typename BasicJsonType::number_unsigned_t; + using number_float_t = typename BasicJsonType::number_float_t; + using string_t = typename BasicJsonType::string_t; + using string_view_t = typename View::string_view_t; + using nav = navigation; + + public: + explicit editor(document_data& d) noexcept + : m_doc(d) + {} + + /// replace a value; returns its view + template + View set(const View& target, V&& value) + { + node* const slot = own(target); + const encoded e = encode(std::forward(value)); + assign(slot, e, nullptr, false); + return View(&m_doc, slot); + } + + /// set a member (appended if missing; a null value becomes an object); + /// returns a view of the member value + template + View set(const View& object, string_view_t key, V&& value) + { + node* const o = own(object); + if (o->kind != static_cast(value_t::object) && o->kind != static_cast(value_t::null)) + { + throw_type_error(305, "cannot use operator[] with a string argument with ", object.type_name()); + } + check_utf8(key.data(), key.size()); + const encoded e = encode(std::forward(value)); + if (o->kind == static_cast(value_t::null)) + { + become_empty(o, value_t::object); + } + // an existing member: assign it (and drop later duplicates, so that + // lookups, iteration, and materialize() agree) + node* slot = nullptr; + bool duplicates = false; + for (const node* k = nav::first(m_doc, o), *end = nav::end(m_doc, o); k != end; k = document_data::after(k + 1)) + { + if (key_equals(*k, key)) + { + if (slot != nullptr) + { + duplicates = true; + break; + } + slot = const_cast(nav::value(k + 1)); // NOLINT(cppcoreguidelines-pro-type-const-cast): the nodes belong to this document + } + } + if (slot != nullptr) + { + if (duplicates) + { + erase_members(o, key, true); + } + assign(slot, e, o, true); + return View(&m_doc, slot); + } + const node k = string_node(key.data(), key.size()); + slot = new_slot(e); + node* const h = block_of(m_doc, o, 2); + h[h->next] = k; + make_link(h[h->next + 1], slot); + h->next += 2; + ++h->len; + ++o->len; + return View(&m_doc, slot); + } + + /// assign an existing array element; returns a view of it + template + View set(const View& array, std::size_t idx, V&& value) + { + node* const a = own(array); + if (a->kind != static_cast(value_t::array)) + { + throw_type_error(305, "cannot use operator[] with a numeric argument with ", array.type_name()); + } + check_index(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); + return View(&m_doc, slot); + } + + /// append to an array (a null value becomes an array); returns a view of + /// the new element + template + View push_back(const View& array, V&& value) + { + node* const a = own(array); + if (a->kind != static_cast(value_t::array) && a->kind != static_cast(value_t::null)) + { + throw_type_error(308, "cannot use push_back() with ", array.type_name()); + } + const encoded e = encode(std::forward(value)); + if (a->kind == static_cast(value_t::null)) + { + become_empty(a, value_t::array); + } + node* const slot = new_slot(e); + node* const h = block_of(m_doc, a, 1); + make_link(h[h->next], slot); + ++h->next; + ++h->len; + ++a->len; + return View(&m_doc, slot); + } + + private: + /// an encoded value: a scalar node, or the root of a new array/object + struct encoded + { + node scalar{}; + node* region = nullptr; + }; + + /// the node of a view of this document + node* own(const View& v) + { + if (NLOHMANN_VIEW_UNLIKELY(v.m_doc != &m_doc || v.m_node == nullptr)) + { + throw_invalid_iterator(202, "view does not belong to this document"); + } + edit_state_of(m_doc); + 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) + { + if (idx >= limit) + { + throw_out_of_range(401, concat("array index ", std::to_string(idx), " is out of range")); + } + } + + bool key_equals(const node& k, string_view_t key) const noexcept + { + return k.len == key.size() && (key.size() == 0 || std::memcmp(m_doc.str(k), key.data(), key.size()) == 0); + } + + /// remove the members with this key (all, or all but the first) from an object + std::size_t erase_members(node* o, string_view_t key, bool keep_first) + { + node* const h = block_of(m_doc, o, 0); + node* w = h + 1; + std::size_t erased = 0; + bool kept = false; + for (node* r = h + 1, *end = h + h->next; r != end; r += 2) + { + const bool match = key_equals(*r, key); + if (match && (kept || !keep_first)) + { + ++erased; + continue; + } + kept = kept || match; + if (w != r) + { + w[0] = r[0]; + w[1] = r[1]; + } + w += 2; + } + h->next = static_cast(w - h); + h->len -= static_cast(erased); + o->len -= static_cast(erased); + return erased; + } + + /// turn a null into an empty array/object in place + static void become_empty(node* n, value_t k) noexcept + { + *n = node{}; + n->kind = static_cast(k); + n->flags = node_flags::is_new; + n->next = 1; + } + + /// Replace the value at slot; `parent` is the container whose elements + /// include slot (if known). + void assign(node* slot, const encoded& e, node* parent, bool parent_known) + { + if (e.region == nullptr) + { + if (is_container(*slot) && slot->next > 1 && slot != m_doc.tape) + { + // The slot spans its old elements in the enclosing sequence, but + // a scalar is one node: the enclosing container first switches to + // links (then the extent of the slot no longer matters). + node* const p = parent_known ? parent : find_parent(m_doc, slot); + if (p != nullptr && ((p->flags & node_flags::moved) == 0 || moved_capacity(m_doc, p) == 0)) + { + block_of(m_doc, p, 0); + } + } + *slot = e.scalar; + return; + } + // an array/object: the slot keeps its extent (so that the enclosing + // sequence still steps over it), and the elements come from the new + // sequence + const node* const r = e.region; + const std::uint32_t extent = is_container(*slot) ? slot->next : 1; + const bool was_moved = (slot->flags & node_flags::moved) != 0; + slot->kind = r->kind; + slot->extra = 0; + slot->len = r->len; + slot->next = extent; + slot->flags = was_moved ? static_cast(node_flags::moved | node_flags::is_new) : std::uint8_t{0}; + set_moved(m_doc, slot, e.region, 0); + edit_state_of(m_doc).regions[e.region] = slot; + } + + /// a node for a new element (links point to it; it never moves) + node* new_slot(const encoded& e) + { + if (e.region != nullptr) + { + return e.region; + } + node* const s = alloc_nodes(m_doc, 1); + *s = e.scalar; + return s; + } + + ////////////// + // encoding // + ////////////// + + template + using encode_tag = std::integral_constant; + + template + struct is_view : std::false_type {}; + + template + struct is_view> : std::true_type {}; + + template + encoded encode(V&& v) + { + using D = typename std::decay::type; + return encode_impl(std::forward(v), encode_tag < is_view::value ? 0 + : std::is_same::value ? 1 + : std::is_same::value ? 2 + : std::is_same::value ? 3 + : std::is_arithmetic::value ? 4 + : std::is_convertible::value ? 5 : 6 > {}); + } + + /// a view of any document (copied; nothing is shared with it) + template + encoded encode_impl(const basic_json_view& v, encode_tag<0> /*view*/) + { + if (NLOHMANN_VIEW_UNLIKELY(v.m_node == nullptr)) + { + throw_type_error(302, "type must be a value, but is ", "discarded"); + } + encoded r; + if (!is_container(*v.m_node)) + { + r.scalar = copy_scalar(*v.m_doc, *v.m_node); + return r; + } + r.region = alloc_nodes(m_doc, count_nodes(*v.m_doc, v.m_node)); + fill_nodes(*v.m_doc, v.m_node, r.region); + edit_state_of(m_doc).regions.emplace(r.region, nullptr); + return r; + } + + encoded encode_impl(const BasicJsonType& j, encode_tag<1> /*json*/) + { + encoded r; + if (!j.is_structured()) + { + r.scalar = json_scalar(j); + return r; + } + r.region = alloc_nodes(m_doc, count_nodes(j)); + fill_nodes(j, r.region); + edit_state_of(m_doc).regions.emplace(r.region, nullptr); + return r; + } + + encoded encode_impl(std::nullptr_t /*unused*/, encode_tag<2> /*null*/) + { + encoded r; + r.scalar = plain_node(value_t::null); + return r; + } + + encoded encode_impl(bool b, encode_tag<3> /*boolean*/) + { + encoded r; + r.scalar = plain_node(value_t::boolean); + r.scalar.flags = static_cast(r.scalar.flags | (b ? node_flags::is_true : 0)); + return r; + } + + template + encoded encode_impl(T x, encode_tag<4> /*number*/) + { + encoded r; + r.scalar = number_node(x, std::integral_constant < int, std::is_floating_point::value ? 0 : (std::is_signed::value ? 1 : 2) > {}); + return r; + } + + template + encoded encode_impl(const T& s, encode_tag<5> /*string*/) + { + const string_view_t sv(s); + check_utf8(sv.data(), sv.size()); + encoded r; + r.scalar = string_node(sv.data(), sv.size()); + return r; + } + + template + encoded encode_impl(T&& x, encode_tag<6> /*other*/) + { + return encode_impl(BasicJsonType(std::forward(x)), encode_tag<1> {}); + } + + static node plain_node(value_t k) noexcept + { + node n{}; + n.kind = static_cast(k); + n.flags = node_flags::is_new; + return n; + } + + template + node number_node(T x, std::integral_constant /*floating-point*/) + { + return float_node(static_cast(x)); + } + + template + node number_node(T x, std::integral_constant /*signed*/) + { + return integer_node(static_cast(static_cast(x)), value_t::number_integer); + } + + template + node number_node(T x, std::integral_constant /*unsigned*/) + { + return integer_node(static_cast(x), value_t::number_unsigned); + } + + /// an integer with its canonical token in the edit arena + node integer_node(std::uint64_t bits, value_t k) + { + const bool negative = k == value_t::number_integer && static_cast(bits) < 0; + std::uint64_t magnitude = negative ? 0 - bits : bits; + std::array buf{}; + char* p = buf.data() + buf.size(); + do + { + *--p = static_cast('0' + (magnitude % 10)); + magnitude /= 10; + } + while (magnitude != 0); + if (negative) + { + *--p = '-'; + } + const auto len = static_cast(buf.data() + buf.size() - p); + node n = plain_node(k); + n.flags = static_cast(n.flags | node_flags::edited); + n.off = append_text(m_doc, p, len); + // number_length() adds one for the sign of number_integer nodes + n.extra = static_cast(k == value_t::number_integer ? len - 1 : len); + set_integer_bits(n, bits); + return n; + } + + /// a float with its shortest round-trip token (as basic_json::dump() + /// writes it), or nan, inf, -inf, in the edit arena + node float_node(number_float_t x) + { + string_t text; + if (std::isnan(x)) + { + text = "nan"; + } + else if (std::isinf(x)) + { + text = x > 0 ? "inf" : "-inf"; + } + else + { + text = BasicJsonType(x).dump(); + } + node n = plain_node(value_t::number_float); + n.flags = static_cast(n.flags | node_flags::edited); + n.extra = 0xFFFFu; // (the digit layout is not recorded) + n.off = append_text(m_doc, text.data(), text.size()); + n.len = static_cast(text.size()); + return n; + } + + /// a string (or key) in the edit arena + node string_node(const char* s, std::size_t len) + { + if (NLOHMANN_VIEW_UNLIKELY(len >= 0xFFFFFFFFu)) + { + throw_out_of_range(416, "strings of 4 GiB or more are not supported by json_document"); // LCOV_EXCL_LINE + } + node n = plain_node(value_t::string); + n.flags = static_cast(n.flags | node_flags::edited); + n.off = append_text(m_doc, s, len); + n.len = static_cast(len); + return n; + } + + /// a scalar of a view (of any document) as a node of this document + node copy_scalar(const document_data& from, const node& n) + { + if (&from == &m_doc) + { + return n; // the same storage + } + switch (static_cast(n.kind)) + { + case value_t::string: + return string_node(from.str(n), n.len); + case value_t::number_integer: + case value_t::number_unsigned: + return integer_node(integer_bits(n), static_cast(n.kind)); + case value_t::number_float: + { + node r = plain_node(value_t::number_float); + r.flags = static_cast(r.flags | node_flags::edited); + r.off = append_text(m_doc, from.str(n), n.len); + r.len = n.len; + r.extra = n.extra; + return r; + } + case value_t::boolean: + { + node r = plain_node(value_t::boolean); + r.flags = static_cast(r.flags | (n.flags & node_flags::is_true)); + return r; + } + case value_t::null: + case value_t::object: + case value_t::array: + case value_t::binary: + case value_t::discarded: + default: + return plain_node(value_t::null); + } + } + + node json_scalar(const BasicJsonType& j) + { + switch (j.type()) + { + case value_t::null: + return plain_node(value_t::null); + case value_t::boolean: + { + node r = plain_node(value_t::boolean); + r.flags = static_cast(r.flags | (j.template get() ? node_flags::is_true : 0)); + return r; + } + case value_t::number_integer: + return integer_node(static_cast(static_cast(j.template get())), value_t::number_integer); + case value_t::number_unsigned: + return integer_node(static_cast(j.template get()), value_t::number_unsigned); + case value_t::number_float: + return float_node(j.template get()); + case value_t::string: + { + const auto& s = j.template get_ref(); + check_utf8(s.data(), s.size()); + return string_node(s.data(), s.size()); + } + case value_t::binary: + throw_type_error(319, "cannot store a binary value in a json_document", ""); + case value_t::discarded: + case value_t::object: + case value_t::array: + default: + throw_type_error(302, "type must be a value, but is ", "discarded"); + } + } + + /// number of nodes of a subtree (containers, keys, scalars) + template + static std::size_t count_nodes(const document_data& d, const node* n) + { + if (!is_container(*n)) + { + return 1; + } + const bool object = n->kind == static_cast(value_t::object); + std::size_t r = 1; + for (const node* c = navigation::first(d, n), *end = navigation::end(d, n); c != end;) + { + const node* const v = object ? c + 1 : c; + r += (object ? 1 : 0) + count_nodes(d, navigation::value(v)); + c = document_data::after(v); + } + return r; + } + + /// copy a subtree (of any document) as a contiguous sequence; returns its end + template + node* fill_nodes(const document_data& d, const node* n, node* out) + { + if (!is_container(*n)) + { + *out = copy_scalar(d, *n); + return out + 1; + } + node* const self = out++; + *self = plain_node(static_cast(n->kind)); + self->len = n->len; + const bool object = n->kind == static_cast(value_t::object); + for (const node* c = navigation::first(d, n), *end = navigation::end(d, n); c != end;) + { + if (object) + { + *out++ = copy_scalar(d, *c); + ++c; + } + out = fill_nodes(d, navigation::value(c), out); + c = document_data::after(c); + } + self->next = static_cast(out - self); + return out; + } + + static std::size_t count_nodes(const BasicJsonType& j) + { + std::size_t r = 1; + if (j.is_object()) + { + for (const auto& member : j.items()) + { + r += 1 + count_nodes(member.value()); + } + } + else if (j.is_array()) + { + for (const auto& e : j) + { + r += count_nodes(e); + } + } + return r; + } + + node* fill_nodes(const BasicJsonType& j, node* out) + { + if (!j.is_structured()) + { + *out = json_scalar(j); + return out + 1; + } + node* const self = out++; + *self = plain_node(j.type()); + self->len = static_cast(j.size()); + if (j.is_object()) + { + for (const auto& member : j.items()) + { + check_utf8(member.key().data(), member.key().size()); + *out++ = string_node(member.key().data(), member.key().size()); + out = fill_nodes(member.value(), out); + } + } + else + { + for (const auto& e : j) + { + out = fill_nodes(e, out); + } + } + self->next = static_cast(out - self); + return out; + } + + document_data& m_doc; +}; + +} // namespace view +} // namespace detail +NLOHMANN_JSON_NAMESPACE_END diff --git a/include/nlohmann/detail/view/edit_storage.hpp b/include/nlohmann/detail/view/edit_storage.hpp new file mode 100644 index 000000000..f166980ee --- /dev/null +++ b/include/nlohmann/detail/view/edit_storage.hpp @@ -0,0 +1,249 @@ +// __ _____ _____ _____ +// __| | __| | | | JSON for Modern C++ +// | | |__ | | | | | | version 3.12.0 +// |_____|_____|_____|_|___| https://github.com/nlohmann/json +// +// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann +// SPDX-License-Identifier: MIT + +#pragma once + +#include // max, min +#include // size_t +#include // uint8_t, uint32_t +#include // memcpy +#include // less +#include // unique_ptr +#include // move + +#include +#include +#include +#include +#include + +// The storage of edits. Edits never move or resize the parsed index: every +// value keeps its node, so views stay valid. New values and element sequences +// live in chunks that never move; strings and number tokens written by edits +// live in the edit arena. An array/object whose elements change gets +// node_flags::moved: its elements then live in a separate sequence (a header +// node, then the entries), whose entries link to the values (kind_link). + +NLOHMANN_JSON_NAMESPACE_BEGIN +namespace detail +{ +namespace view +{ + +inline document_data::edit_state& edit_state_of(document_data& d) +{ + if (!d.edits) + { + d.edits.reset(new document_data::edit_state()); + } + return *d.edits; +} + +/// k consecutive nodes that never move (new values and blocks) +inline node* alloc_nodes(document_data& d, std::size_t k) +{ + document_data::edit_state& e = edit_state_of(d); + if (NLOHMANN_VIEW_UNLIKELY(static_cast(e.chunk_end - e.chunk_cur) < k)) + { + const std::size_t count = (std::max)(k, e.chunk_next); + std::unique_ptr fresh(new node[count]()); + e.chunks.push_back(std::move(fresh)); + e.chunk_cur = e.chunks.back().get(); + e.chunk_end = e.chunk_cur + count; + e.chunk_next = (std::min)(e.chunk_next * 2, std::size_t{65536}); + e.bytes += count * sizeof(node); + } + node* const r = e.chunk_cur; + e.chunk_cur += k; + return r; +} + +/// copy n bytes into the edit arena and return their offset; a new buffer +/// leaves the old one alive, so that string views into it remain valid +inline std::uint32_t append_text(document_data& d, const char* s, std::size_t n) +{ + document_data::edit_state& e = edit_state_of(d); + if (NLOHMANN_VIEW_UNLIKELY(e.text_cap - e.text_used < n)) + { + const std::size_t cap = (std::max)(e.text_cap * 2, e.text_used + n + 256); + if (cap > 0xFFFFFFFFu) + { + throw_out_of_range(416, "edits of 4 GiB or more are not supported by json_document"); + } + std::unique_ptr fresh(new char[cap]); + if (e.text_used != 0) + { + std::memcpy(fresh.get(), e.texts.back().get(), e.text_used); + } + e.texts.push_back(std::move(fresh)); + e.text_cap = cap; + e.bytes += cap; + d.base[2] = e.texts.back().get(); + } + const auto off = static_cast(e.text_used); + if (n != 0) + { + std::memcpy(e.texts.back().get() + e.text_used, s, n); + } + e.text_used += n; + return off; +} + +/// the capacity in nodes of the block of a moved container (0: a fixed +/// sequence, the elements of a new value) +inline std::size_t moved_capacity(const document_data& d, const node* n) noexcept +{ + return d.edits->moved_cap[n->off]; +} + +/// let container n take its elements from `seq` (header node first) +inline void set_moved(document_data& d, node* n, node* seq, std::size_t cap) +{ + document_data::edit_state& e = edit_state_of(d); + if ((n->flags & node_flags::moved) != 0) + { + e.moved[n->off] = seq; + e.moved_cap[n->off] = cap; + return; + } + if (e.moved.size() >= 0xFFFFFFFFu) + { + throw_out_of_range(416, "more than 4294967295 edited arrays and objects are not supported by json_document"); // LCOV_EXCL_LINE + } + if (e.moved.size() == e.moved.capacity() || e.moved_cap.size() == e.moved_cap.capacity()) + { + // both grow before either changes, so that the push_backs cannot throw + e.moved.reserve((2 * e.moved.size()) + 16); + e.moved_cap.reserve((2 * e.moved.size()) + 16); + } + e.moved.push_back(seq); + e.moved_cap.push_back(cap); + n->off = static_cast(e.moved.size() - 1); + n->flags = static_cast(n->flags | node_flags::moved | node_flags::is_new); +} + +/// Make the elements of container n a growable block with room for `extra` +/// more nodes, and return its header. The entries link to the existing +/// values, which stay where they are. A block that grows is copied (its old +/// space is not reused). +inline node* block_of(document_data& d, node* n, std::size_t extra) +{ + if ((n->flags & node_flags::moved) != 0 && moved_capacity(d, n) != 0) + { + node* const h = d.edits->moved[n->off]; + if (h->next + extra <= moved_capacity(d, n)) + { + return h; + } + const std::size_t cap = (std::max)(2 * moved_capacity(d, n), h->next + extra); + node* const nh = alloc_nodes(d, cap); + std::memcpy(nh, h, h->next * sizeof(node)); + set_moved(d, n, nh, cap); + return nh; + } + const bool object = n->kind == static_cast(value_t::object); + const std::size_t used = 1 + (static_cast(n->len) * (object ? 2 : 1)); + const std::size_t cap = used + extra; + node* const h = alloc_nodes(d, cap); + *h = node{}; + h->kind = n->kind; + h->len = n->len; + h->next = static_cast(used); + node* o = h + 1; + for (const node* c = d.first_child_edited(n), *e = d.child_end_edited(n); c != e;) + { + if (object) + { + *o++ = *c++; // the key + } + if (c->kind == kind_link) + { + *o = *c; + } + else + { + make_link(*o, c); + } + ++o; + c = document_data::after(c); + } + set_moved(d, n, h, cap); + return h; +} + +/// The container whose elements include `target`; nullptr for the root, for +/// a value that is no longer part of the document, and for a value that is +/// only reached through a link. Values never move between allocations, so +/// the path to `target` stays inside the allocation that holds it (the parsed +/// index, or one new value), where the extent of each container (`next`) +/// still covers its original subtree. +inline node* find_parent(const document_data& d, const node* target) +{ + const std::less lt; + const node* lo = d.tape; + const node* hi = d.tape + d.tape_size; + const node* c = d.tape; + if (lt(target, lo) || !lt(target, hi)) + { + if (!d.edits) + { + return nullptr; // LCOV_EXCL_LINE (nodes outside the index exist only after edits) + } + auto it = d.edits->regions.upper_bound(target); + if (it == d.edits->regions.begin()) + { + return nullptr; + } + --it; + lo = it->first; + hi = lo + lo->next; + if (!lt(target, hi)) + { + return nullptr; // a single-node value, reached through a link + } + // the root of a new value is the element sequence of its owner, or a linked value + c = it->second != nullptr ? it->second : lo; + } + if (target == lo) + { + return nullptr; + } + for (;;) + { + if (!is_container(*c)) + { + return nullptr; + } + const bool object = c->kind == static_cast(value_t::object); + const node* down = nullptr; + for (const node* p = d.first_child_edited(c), *e = d.child_end_edited(c); p != e;) + { + const node* const at = object ? p + 1 : p; + const node* const v = document_data::deref(at); + if (v == target) + { + return const_cast(c); // NOLINT(cppcoreguidelines-pro-type-const-cast): the nodes belong to the document + } + if (is_container(*v) && !lt(v, lo) && lt(v, target) && lt(target, v + v->next)) + { + down = v; + break; + } + p = document_data::after(at); + } + if (down == nullptr) + { + return nullptr; + } + c = down; + } +} + +} // namespace view +} // namespace detail +NLOHMANN_JSON_NAMESPACE_END diff --git a/include/nlohmann/detail/view/errors.hpp b/include/nlohmann/detail/view/errors.hpp index 44d8a6438..5f3dca477 100644 --- a/include/nlohmann/detail/view/errors.hpp +++ b/include/nlohmann/detail/view/errors.hpp @@ -30,6 +30,11 @@ namespace view NLOHMANN_VIEW_THROW(type_error::create(id, concat(prefix, type), nullptr)); } +[[noreturn]] NLOHMANN_VIEW_NOINLINE inline void throw_type_error(int id, const std::string& msg) +{ + NLOHMANN_VIEW_THROW(type_error::create(id, msg, nullptr)); +} + [[noreturn]] NLOHMANN_VIEW_NOINLINE inline void throw_out_of_range(int id, const std::string& msg) { NLOHMANN_VIEW_THROW(out_of_range::create(id, msg, nullptr)); diff --git a/include/nlohmann/json_view.hpp b/include/nlohmann/json_view.hpp index 4fc738ab9..9a2d9c20f 100644 --- a/include/nlohmann/json_view.hpp +++ b/include/nlohmann/json_view.hpp @@ -50,6 +50,8 @@ #include #include #include +#include +#include #include #include #include @@ -625,6 +627,19 @@ class basic_json_view return !(a == b); } + /// a view of an editable document compares with one of a read-only document + template < bool E, typename std::enable_if < E != Editable, int >::type = 0 > + friend bool operator==(const basic_json_view& a, const basic_json_view& b) + { + return detail::view::equal(side(a), detail::view::view_side>(b)); + } + + template < bool E, typename std::enable_if < E != Editable, int >::type = 0 > + friend bool operator!=(const basic_json_view& a, const basic_json_view& b) + { + return !(a == b); + } + /// whether the value parse() would produce for a view equals a value friend bool operator==(const basic_json_view& a, const BasicJsonType& j) { @@ -673,6 +688,7 @@ class basic_json_view private: template friend class basic_json_document; template friend class basic_json_view; + template friend class detail::view::editor; friend iterator; basic_json_view(const document_data* d, const node* n) noexcept @@ -976,9 +992,124 @@ class basic_json_document } } + /////////// + // edits // + /////////// + + // The source text is never written; new values go to storage owned by + // the document. A view keeps referring to the same value: after an + // assignment it sees the new value, and edits elsewhere do not affect it. + // A view of an erased value keeps its last value. An edit of an + // array/object invalidates the iterators over it. Values are accepted as + // views (of any document), BasicJsonType values, and everything + // BasicJsonType can be constructed from. + + using string_view_t = typename view_type::string_view_t; + using json_pointer = typename BasicJsonType::json_pointer; + + /// replace a value (a view of this document); returns a view of it + template + view_type set(view_type target, V&& value) + { + return editor().set(target, std::forward(value)); + } + + /// set a member (added if missing; a null value becomes an object); + /// returns a view of the member value + template + view_type set(view_type object, string_view_t key, V&& value) + { + return editor().set(object, key, std::forward(value)); + } + + /// assign an existing array element; returns a view of it + template < typename I, typename V, typename std::enable_if < std::is_integral::value && !std::is_same::value, int >::type = 0 > + view_type set(view_type array, I idx, V && value) + { + return editor().set(array, index(idx), std::forward(value)); + } + + /// set the value at a JSON pointer: its parent must exist; an object + /// member is set (added if missing), an array element assigned, and "-" + /// or the size of the array appends + template + view_type set(const json_pointer& ptr, V&& value) + { + if (ptr.empty()) + { + return set(root(), std::forward(value)); + } + const view_type parent = root().at(ptr.parent_pointer()); + const auto& token = ptr.back(); + if (parent.is_array()) + { + const std::size_t idx = token == "-" ? parent.size() : pointer_index(token); + if (idx == parent.size()) + { + return push_back(parent, std::forward(value)); + } + return set(parent, idx, std::forward(value)); + } + return set(parent, string_view_t(token.data(), token.size()), std::forward(value)); + } + + /// append to an array (a null value becomes an array); returns a view of + /// the new element + template + view_type push_back(view_type array, V&& value) + { + return editor().push_back(array, std::forward(value)); + } + private: using input_kind = detail::view::input_kind; + detail::view::editor editor() + { + static_assert(Editable, "only an editable document can be edited: use basic_json_document (json_editable_document)"); + if (NLOHMANN_VIEW_UNLIKELY(!m_data || m_data->discarded)) + { + detail::view::throw_invalid_iterator(202, "view does not belong to this document"); + } + return detail::view::editor(*m_data); + } + + /// an index (out_of_range.401 if negative) + template + static std::size_t index(I idx) + { + return index(idx, std::is_signed {}); + } + + template + static std::size_t index(I idx, std::true_type /*signed*/) + { + if (idx < 0) + { + detail::view::throw_out_of_range(401, detail::concat("array index ", std::to_string(idx), " is out of range")); + } + return static_cast(idx); + } + + template + static std::size_t index(I idx, std::false_type /*unsigned*/) + { + return static_cast(idx); + } + + /// the array index of a JSON pointer token (json_pointer's rules) + template + static std::size_t pointer_index(const StringType& token) + { + std::size_t idx = 0; + const detail::view::index_status status = detail::view::array_index(token, idx); + if (status != detail::view::index_status::ok) + { + detail::view::throw_array_index_error(status, token); + } + return idx; + } + /// create the storage (sized for the input) on first use void ensure_data(const char* src, std::size_t size) { @@ -1145,6 +1276,15 @@ using json_view = basic_json_view; using ordered_json_document = basic_json_document; /// a value of an ordered_json_document using ordered_json_view = basic_json_view; +/// an editable parsed JSON text for json +using json_editable_document = basic_json_document; +/// a value of a json_editable_document +using json_editable_view = basic_json_view; +/// an editable parsed JSON text for ordered_json +using ordered_json_editable_document = basic_json_document; +/// a value of an ordered_json_editable_document +using ordered_json_editable_view = basic_json_view; + NLOHMANN_JSON_NAMESPACE_END // tuple protocol for the items of basic_json_view::items() (structured bindings) diff --git a/single_include/nlohmann/json_view.hpp b/single_include/nlohmann/json_view.hpp index c8f447a97..d58356189 100644 --- a/single_include/nlohmann/json_view.hpp +++ b/single_include/nlohmann/json_view.hpp @@ -2344,6 +2344,52 @@ 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 // isinf, isnan +#include // size_t +#include // int64_t, uint8_t, uint32_t, uint64_t +#include // memcmp, memmove +#include // numeric_limits +#include // string, to_string +#include // decay, enable_if, integral_constant, is_arithmetic, is_convertible, is_floating_point, is_same, is_signed +#include // forward + +// #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 // max, min +#include // size_t +#include // uint8_t, uint32_t +#include // memcpy +#include // less +#include // unique_ptr +#include // move + +// #include +// #include + // #include // __ _____ _____ _____ // __| | __| | | | JSON for Modern C++ @@ -2379,6 +2425,11 @@ namespace view NLOHMANN_VIEW_THROW(type_error::create(id, concat(prefix, type), nullptr)); } +[[noreturn]] NLOHMANN_VIEW_NOINLINE inline void throw_type_error(int id, const std::string& msg) +{ + NLOHMANN_VIEW_THROW(type_error::create(id, msg, nullptr)); +} + [[noreturn]] NLOHMANN_VIEW_NOINLINE inline void throw_out_of_range(int id, const std::string& msg) { NLOHMANN_VIEW_THROW(out_of_range::create(id, msg, nullptr)); @@ -2439,6 +2490,1216 @@ template } // namespace detail NLOHMANN_JSON_NAMESPACE_END +// #include + +// #include + + +// The storage of edits. Edits never move or resize the parsed index: every +// value keeps its node, so views stay valid. New values and element sequences +// live in chunks that never move; strings and number tokens written by edits +// live in the edit arena. An array/object whose elements change gets +// node_flags::moved: its elements then live in a separate sequence (a header +// node, then the entries), whose entries link to the values (kind_link). + +NLOHMANN_JSON_NAMESPACE_BEGIN +namespace detail +{ +namespace view +{ + +inline document_data::edit_state& edit_state_of(document_data& d) +{ + if (!d.edits) + { + d.edits.reset(new document_data::edit_state()); + } + return *d.edits; +} + +/// k consecutive nodes that never move (new values and blocks) +inline node* alloc_nodes(document_data& d, std::size_t k) +{ + document_data::edit_state& e = edit_state_of(d); + if (NLOHMANN_VIEW_UNLIKELY(static_cast(e.chunk_end - e.chunk_cur) < k)) + { + const std::size_t count = (std::max)(k, e.chunk_next); + std::unique_ptr fresh(new node[count]()); + e.chunks.push_back(std::move(fresh)); + e.chunk_cur = e.chunks.back().get(); + e.chunk_end = e.chunk_cur + count; + e.chunk_next = (std::min)(e.chunk_next * 2, std::size_t{65536}); + e.bytes += count * sizeof(node); + } + node* const r = e.chunk_cur; + e.chunk_cur += k; + return r; +} + +/// copy n bytes into the edit arena and return their offset; a new buffer +/// leaves the old one alive, so that string views into it remain valid +inline std::uint32_t append_text(document_data& d, const char* s, std::size_t n) +{ + document_data::edit_state& e = edit_state_of(d); + if (NLOHMANN_VIEW_UNLIKELY(e.text_cap - e.text_used < n)) + { + const std::size_t cap = (std::max)(e.text_cap * 2, e.text_used + n + 256); + if (cap > 0xFFFFFFFFu) + { + throw_out_of_range(416, "edits of 4 GiB or more are not supported by json_document"); + } + std::unique_ptr fresh(new char[cap]); + if (e.text_used != 0) + { + std::memcpy(fresh.get(), e.texts.back().get(), e.text_used); + } + e.texts.push_back(std::move(fresh)); + e.text_cap = cap; + e.bytes += cap; + d.base[2] = e.texts.back().get(); + } + const auto off = static_cast(e.text_used); + if (n != 0) + { + std::memcpy(e.texts.back().get() + e.text_used, s, n); + } + e.text_used += n; + return off; +} + +/// the capacity in nodes of the block of a moved container (0: a fixed +/// sequence, the elements of a new value) +inline std::size_t moved_capacity(const document_data& d, const node* n) noexcept +{ + return d.edits->moved_cap[n->off]; +} + +/// let container n take its elements from `seq` (header node first) +inline void set_moved(document_data& d, node* n, node* seq, std::size_t cap) +{ + document_data::edit_state& e = edit_state_of(d); + if ((n->flags & node_flags::moved) != 0) + { + e.moved[n->off] = seq; + e.moved_cap[n->off] = cap; + return; + } + if (e.moved.size() >= 0xFFFFFFFFu) + { + throw_out_of_range(416, "more than 4294967295 edited arrays and objects are not supported by json_document"); // LCOV_EXCL_LINE + } + if (e.moved.size() == e.moved.capacity() || e.moved_cap.size() == e.moved_cap.capacity()) + { + // both grow before either changes, so that the push_backs cannot throw + e.moved.reserve((2 * e.moved.size()) + 16); + e.moved_cap.reserve((2 * e.moved.size()) + 16); + } + e.moved.push_back(seq); + e.moved_cap.push_back(cap); + n->off = static_cast(e.moved.size() - 1); + n->flags = static_cast(n->flags | node_flags::moved | node_flags::is_new); +} + +/// Make the elements of container n a growable block with room for `extra` +/// more nodes, and return its header. The entries link to the existing +/// values, which stay where they are. A block that grows is copied (its old +/// space is not reused). +inline node* block_of(document_data& d, node* n, std::size_t extra) +{ + if ((n->flags & node_flags::moved) != 0 && moved_capacity(d, n) != 0) + { + node* const h = d.edits->moved[n->off]; + if (h->next + extra <= moved_capacity(d, n)) + { + return h; + } + const std::size_t cap = (std::max)(2 * moved_capacity(d, n), h->next + extra); + node* const nh = alloc_nodes(d, cap); + std::memcpy(nh, h, h->next * sizeof(node)); + set_moved(d, n, nh, cap); + return nh; + } + const bool object = n->kind == static_cast(value_t::object); + const std::size_t used = 1 + (static_cast(n->len) * (object ? 2 : 1)); + const std::size_t cap = used + extra; + node* const h = alloc_nodes(d, cap); + *h = node{}; + h->kind = n->kind; + h->len = n->len; + h->next = static_cast(used); + node* o = h + 1; + for (const node* c = d.first_child_edited(n), *e = d.child_end_edited(n); c != e;) + { + if (object) + { + *o++ = *c++; // the key + } + if (c->kind == kind_link) + { + *o = *c; + } + else + { + make_link(*o, c); + } + ++o; + c = document_data::after(c); + } + set_moved(d, n, h, cap); + return h; +} + +/// The container whose elements include `target`; nullptr for the root, for +/// a value that is no longer part of the document, and for a value that is +/// only reached through a link. Values never move between allocations, so +/// the path to `target` stays inside the allocation that holds it (the parsed +/// index, or one new value), where the extent of each container (`next`) +/// still covers its original subtree. +inline node* find_parent(const document_data& d, const node* target) +{ + const std::less lt; + const node* lo = d.tape; + const node* hi = d.tape + d.tape_size; + const node* c = d.tape; + if (lt(target, lo) || !lt(target, hi)) + { + if (!d.edits) + { + return nullptr; // LCOV_EXCL_LINE (nodes outside the index exist only after edits) + } + auto it = d.edits->regions.upper_bound(target); + if (it == d.edits->regions.begin()) + { + return nullptr; + } + --it; + lo = it->first; + hi = lo + lo->next; + if (!lt(target, hi)) + { + return nullptr; // a single-node value, reached through a link + } + // the root of a new value is the element sequence of its owner, or a linked value + c = it->second != nullptr ? it->second : lo; + } + if (target == lo) + { + return nullptr; + } + for (;;) + { + if (!is_container(*c)) + { + return nullptr; + } + const bool object = c->kind == static_cast(value_t::object); + const node* down = nullptr; + for (const node* p = d.first_child_edited(c), *e = d.child_end_edited(c); p != e;) + { + const node* const at = object ? p + 1 : p; + const node* const v = document_data::deref(at); + if (v == target) + { + return const_cast(c); // NOLINT(cppcoreguidelines-pro-type-const-cast): the nodes belong to the document + } + if (is_container(*v) && !lt(v, lo) && lt(v, target) && lt(target, v + v->next)) + { + down = v; + break; + } + p = document_data::after(at); + } + if (down == nullptr) + { + return nullptr; + } + c = down; + } +} + +} // namespace view +} // namespace detail +NLOHMANN_JSON_NAMESPACE_END + +// #include + +// #include +// __ _____ _____ _____ +// __| | __| | | | JSON for Modern C++ +// | | |__ | | | | | | version 3.12.0 +// |_____|_____|_____|_|___| https://github.com/nlohmann/json +// +// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann +// SPDX-License-Identifier: MIT + + + +#include // size_t +#include // uint16_t, uint32_t, uint64_t +#include // memcmp, memcpy + +// #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 // size_t +#include // uint32_t, uint64_t +#include // memcmp + +// #include +// #include + +// #include + +// #include + + +// Hash indexes of large objects, so that a lookup does not compare thousands +// of keys (as Boost.JSON switches from a linear search to a hash table for +// large objects). An object with document_data::index_min_members members or +// more gets an open-addressing table after parsing; its node stores the +// number of the table (1-based) in `extra`. A slot holds the offset of a key +// node from its object node (0: empty). Of duplicate keys, the first is kept, +// as for the linear search. + +NLOHMANN_JSON_NAMESPACE_BEGIN +namespace detail +{ +namespace view +{ + +/// hash of a key: its bytes, eight at a time, in a fixed byte order +inline std::uint64_t key_hash(const char* s, std::size_t n) noexcept +{ + std::uint64_t h = 0x9E3779B97F4A7C15u * (n + 1); + const auto* p = reinterpret_cast(s); // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast) + while (n >= 8) + { + h = (h ^ read_eight_bytes(p)) * 0xBF58476D1CE4E5B9u; + h ^= h >> 29u; + p += 8; + n -= 8; + } + std::uint64_t w = 0; + for (std::size_t i = 0; i < n; ++i) + { + w |= static_cast(p[i]) << (8u * i); + } + h = (h ^ w) * 0x94D049BB133111EBu; + return h ^ (h >> 31u); +} + +/// build the table of a large object +inline void build_object_index(document_data& d, node* obj) +{ + if (d.indexes.size() >= 0xFFFFu) + { + return; // LCOV_EXCL_LINE (the number must fit `extra`; more large objects are searched linearly) + } + std::size_t cap = 16; + while (cap < 2 * static_cast(obj->len)) + { + cap *= 2; + } + const std::size_t start = d.index_slots.size(); + d.index_slots.resize(start + cap, 0); + std::uint32_t* const slots = d.index_slots.data() + start; + const std::size_t mask = cap - 1; + for (const node* k = document_data::first_child(obj), *end = document_data::child_end(obj); k != end; k = document_data::after(k + 1)) + { + const char* const key = d.str(*k); + std::size_t i = static_cast(key_hash(key, k->len)) & mask; + bool duplicate = false; + while (slots[i] != 0) + { + const node* const other = obj + slots[i]; + if (other->len == k->len && (k->len == 0 || std::memcmp(d.str(*other), key, k->len) == 0)) + { + duplicate = true; // keep the first + break; + } + i = (i + 1) & mask; + } + if (!duplicate) + { + slots[i] = static_cast(k - obj); + } + } + d.indexes.push_back(document_data::object_index{start, static_cast(mask)}); + obj->extra = static_cast(d.indexes.size()); +} + +/// build the tables of the large objects the parser noted +inline void build_object_indexes(document_data& d) +{ + for (const std::uint32_t i : d.large_objects) + { + build_object_index(d, d.tape + i); + } +} + +/// the key node of the first member with this key of an indexed object, or +/// nullptr +inline const node* find_indexed(const document_data& d, const node* obj, const char* key, std::size_t n) noexcept +{ + const document_data::object_index& ix = d.indexes[obj->extra - 1u]; + const std::uint32_t* const slots = d.index_slots.data() + ix.start; + std::size_t i = static_cast(key_hash(key, n)) & ix.mask; + for (;;) + { + const std::uint32_t s = slots[i]; + if (s == 0) + { + return nullptr; + } + const node* const k = obj + s; + if (k->len == n && (n == 0 || std::memcmp(d.str(*k), key, n) == 0)) + { + return k; + } + i = (i + 1) & ix.mask; + } +} + +} // namespace view +} // namespace detail +NLOHMANN_JSON_NAMESPACE_END + + +NLOHMANN_JSON_NAMESPACE_BEGIN +namespace detail +{ +namespace view +{ + +/// equality test for strings of one length n <= 16: two overlapping loads per +/// string (the first and the last 8, 4, or 2 bytes) replace a memcmp, and no +/// byte outside [s, s + n) is read +class short_key +{ + public: + short_key(const unsigned char* k, std::size_t n) noexcept + : m_n(n) + { + load(k, m_a, m_b); + } + + NLOHMANN_VIEW_ALWAYS_INLINE bool matches(const unsigned char* s) const noexcept + { + std::uint64_t a = 0; + std::uint64_t b = 0; + load(s, a, b); + return a == m_a && b == m_b; + } + + private: + template + static NLOHMANN_VIEW_ALWAYS_INLINE std::uint64_t load_word(const unsigned char* s) noexcept + { + T w = 0; + std::memcpy(&w, s, sizeof(T)); + return w; + } + + NLOHMANN_VIEW_ALWAYS_INLINE void load(const unsigned char* s, std::uint64_t& a, std::uint64_t& b) const noexcept + { + if (m_n >= 8) + { + a = load_word(s); + b = load_word(s + m_n - 8); + } + else if (m_n >= 4) + { + a = load_word(s); + b = load_word(s + m_n - 4); + } + else if (m_n >= 2) + { + a = load_word(s); + b = load_word(s + m_n - 2); + } + else + { + a = m_n == 1 ? s[0] : 0; + b = 0; + } + } + + std::size_t m_n; + std::uint64_t m_a = 0; + std::uint64_t m_b = 0; +}; + +/// the key node of the first member of an object with the given key, or +/// nullptr; most keys are rejected by their length, from the index alone +template +const node* find_member(const document_data& d, const node* object, const char* key, std::size_t n) noexcept +{ + using nav = navigation; + if (NLOHMANN_VIEW_UNLIKELY(object->extra != 0) && (!Editable || (object->flags & node_flags::moved) == 0)) + { + return find_indexed(d, object, key, n); // a large object (whose members have not been edited) + } + const node* const end = nav::end(d, object); + const auto* const k = reinterpret_cast(key); // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast) + if (NLOHMANN_VIEW_LIKELY(n <= 16)) + { + const short_key probe(k, n); + for (const node* m = nav::first(d, object); m != end; m = document_data::after(m + 1)) + { + if (m->len == n && probe.matches(reinterpret_cast(d.str(*m)))) // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast) + { + return m; + } + } + return nullptr; + } + for (const node* m = nav::first(d, object); m != end; m = document_data::after(m + 1)) + { + if (m->len == n && std::memcmp(d.str(*m), key, n) == 0) + { + return m; + } + } + return nullptr; +} + +/// the entry of the element of an array at an index below its size (a link +/// in the moved sequences of editable documents) +template +const node* element_at(const document_data& d, const node* array, std::size_t idx) noexcept +{ + const node* e = navigation::first(d, array); + if (Editable && (array->flags & node_flags::moved) != 0 && d.edits->moved_cap[array->off] != 0) + { + return e + idx; // a growable block: one link per element + } + for (std::size_t i = 0; i < idx; ++i) + { + e = document_data::after(e); + } + return e; +} + +/// the entry of the last element of a non-empty array, or the key of the +/// last member of a non-empty object +template +const node* last_child(const document_data& d, const node* container) noexcept +{ + const std::size_t value_offset = container->kind == static_cast(value_t::object) ? 1 : 0; + const node* const end = navigation::end(d, container); + const node* last = navigation::first(d, container); + for (const node* c = document_data::after(last + value_offset); c != end; c = document_data::after(c + value_offset)) + { + last = c; + } + return last; +} + +} // namespace view +} // namespace detail +NLOHMANN_JSON_NAMESPACE_END + +// #include + +// #include + + +NLOHMANN_JSON_NAMESPACE_BEGIN + +template +class basic_json_view; + +namespace detail +{ +namespace view +{ + +/// Checks a string the way basic_json's serializer does when it writes it +/// (type_error.316 with the same message), so that an editable document +/// only holds valid UTF-8: the error is at the first byte that no +/// well-formed sequence can continue with (Unicode, Table 3-7). +inline void check_utf8(const char* s, std::size_t n) +{ + const auto* const p = reinterpret_cast(s); // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast) + const auto hex = [](unsigned char c) + { + constexpr const char* digits = "0123456789ABCDEF"; + return std::string{digits[c >> 4u], digits[c & 0xFu]}; + }; + for (std::size_t i = 0; i < n;) + { + const unsigned char c = p[i]; + if (c < 0x80) + { + ++i; + continue; + } + std::size_t len = 0; + unsigned char lo = 0x80; + unsigned char hi = 0xBF; + if (c >= 0xC2 && c <= 0xDF) + { + len = 2; + } + else if (c >= 0xE0 && c <= 0xEF) + { + len = 3; + lo = c == 0xE0 ? 0xA0 : 0x80; + hi = c == 0xED ? 0x9F : 0xBF; + } + else if (c >= 0xF0 && c <= 0xF4) + { + len = 4; + lo = c == 0xF0 ? 0x90 : 0x80; + hi = c == 0xF4 ? 0x8F : 0xBF; + } + else + { + throw_type_error(316, concat("invalid UTF-8 byte at index ", std::to_string(i), ": 0x", hex(c))); + } + for (std::size_t k = 1; k < len; ++k) + { + if (i + k == n) + { + throw_type_error(316, concat("incomplete UTF-8 string; last byte: 0x", hex(p[n - 1]))); + } + const unsigned char b = p[i + k]; + if (b < (k == 1 ? lo : 0x80) || b > (k == 1 ? hi : 0xBF)) + { + throw_type_error(316, concat("invalid UTF-8 byte at index ", std::to_string(i + k), ": 0x", hex(b))); + } + } + i += len; + } +} + +/*! +@brief the edits of an editable basic_json_document + +Values are accepted as views (of any document), BasicJsonType values, and +everything BasicJsonType can be constructed from. The source text is never +written: new values go to storage owned by the document (see +edit_storage.hpp). +*/ +template +class editor +{ + using number_integer_t = typename BasicJsonType::number_integer_t; + using number_unsigned_t = typename BasicJsonType::number_unsigned_t; + using number_float_t = typename BasicJsonType::number_float_t; + using string_t = typename BasicJsonType::string_t; + using string_view_t = typename View::string_view_t; + using nav = navigation; + + public: + explicit editor(document_data& d) noexcept + : m_doc(d) + {} + + /// replace a value; returns its view + template + View set(const View& target, V&& value) + { + node* const slot = own(target); + const encoded e = encode(std::forward(value)); + assign(slot, e, nullptr, false); + return View(&m_doc, slot); + } + + /// set a member (appended if missing; a null value becomes an object); + /// returns a view of the member value + template + View set(const View& object, string_view_t key, V&& value) + { + node* const o = own(object); + if (o->kind != static_cast(value_t::object) && o->kind != static_cast(value_t::null)) + { + throw_type_error(305, "cannot use operator[] with a string argument with ", object.type_name()); + } + check_utf8(key.data(), key.size()); + const encoded e = encode(std::forward(value)); + if (o->kind == static_cast(value_t::null)) + { + become_empty(o, value_t::object); + } + // an existing member: assign it (and drop later duplicates, so that + // lookups, iteration, and materialize() agree) + node* slot = nullptr; + bool duplicates = false; + for (const node* k = nav::first(m_doc, o), *end = nav::end(m_doc, o); k != end; k = document_data::after(k + 1)) + { + if (key_equals(*k, key)) + { + if (slot != nullptr) + { + duplicates = true; + break; + } + slot = const_cast(nav::value(k + 1)); // NOLINT(cppcoreguidelines-pro-type-const-cast): the nodes belong to this document + } + } + if (slot != nullptr) + { + if (duplicates) + { + erase_members(o, key, true); + } + assign(slot, e, o, true); + return View(&m_doc, slot); + } + const node k = string_node(key.data(), key.size()); + slot = new_slot(e); + node* const h = block_of(m_doc, o, 2); + h[h->next] = k; + make_link(h[h->next + 1], slot); + h->next += 2; + ++h->len; + ++o->len; + return View(&m_doc, slot); + } + + /// assign an existing array element; returns a view of it + template + View set(const View& array, std::size_t idx, V&& value) + { + node* const a = own(array); + if (a->kind != static_cast(value_t::array)) + { + throw_type_error(305, "cannot use operator[] with a numeric argument with ", array.type_name()); + } + check_index(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); + return View(&m_doc, slot); + } + + /// append to an array (a null value becomes an array); returns a view of + /// the new element + template + View push_back(const View& array, V&& value) + { + node* const a = own(array); + if (a->kind != static_cast(value_t::array) && a->kind != static_cast(value_t::null)) + { + throw_type_error(308, "cannot use push_back() with ", array.type_name()); + } + const encoded e = encode(std::forward(value)); + if (a->kind == static_cast(value_t::null)) + { + become_empty(a, value_t::array); + } + node* const slot = new_slot(e); + node* const h = block_of(m_doc, a, 1); + make_link(h[h->next], slot); + ++h->next; + ++h->len; + ++a->len; + return View(&m_doc, slot); + } + + private: + /// an encoded value: a scalar node, or the root of a new array/object + struct encoded + { + node scalar{}; + node* region = nullptr; + }; + + /// the node of a view of this document + node* own(const View& v) + { + if (NLOHMANN_VIEW_UNLIKELY(v.m_doc != &m_doc || v.m_node == nullptr)) + { + throw_invalid_iterator(202, "view does not belong to this document"); + } + edit_state_of(m_doc); + 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) + { + if (idx >= limit) + { + throw_out_of_range(401, concat("array index ", std::to_string(idx), " is out of range")); + } + } + + bool key_equals(const node& k, string_view_t key) const noexcept + { + return k.len == key.size() && (key.size() == 0 || std::memcmp(m_doc.str(k), key.data(), key.size()) == 0); + } + + /// remove the members with this key (all, or all but the first) from an object + std::size_t erase_members(node* o, string_view_t key, bool keep_first) + { + node* const h = block_of(m_doc, o, 0); + node* w = h + 1; + std::size_t erased = 0; + bool kept = false; + for (node* r = h + 1, *end = h + h->next; r != end; r += 2) + { + const bool match = key_equals(*r, key); + if (match && (kept || !keep_first)) + { + ++erased; + continue; + } + kept = kept || match; + if (w != r) + { + w[0] = r[0]; + w[1] = r[1]; + } + w += 2; + } + h->next = static_cast(w - h); + h->len -= static_cast(erased); + o->len -= static_cast(erased); + return erased; + } + + /// turn a null into an empty array/object in place + static void become_empty(node* n, value_t k) noexcept + { + *n = node{}; + n->kind = static_cast(k); + n->flags = node_flags::is_new; + n->next = 1; + } + + /// Replace the value at slot; `parent` is the container whose elements + /// include slot (if known). + void assign(node* slot, const encoded& e, node* parent, bool parent_known) + { + if (e.region == nullptr) + { + if (is_container(*slot) && slot->next > 1 && slot != m_doc.tape) + { + // The slot spans its old elements in the enclosing sequence, but + // a scalar is one node: the enclosing container first switches to + // links (then the extent of the slot no longer matters). + node* const p = parent_known ? parent : find_parent(m_doc, slot); + if (p != nullptr && ((p->flags & node_flags::moved) == 0 || moved_capacity(m_doc, p) == 0)) + { + block_of(m_doc, p, 0); + } + } + *slot = e.scalar; + return; + } + // an array/object: the slot keeps its extent (so that the enclosing + // sequence still steps over it), and the elements come from the new + // sequence + const node* const r = e.region; + const std::uint32_t extent = is_container(*slot) ? slot->next : 1; + const bool was_moved = (slot->flags & node_flags::moved) != 0; + slot->kind = r->kind; + slot->extra = 0; + slot->len = r->len; + slot->next = extent; + slot->flags = was_moved ? static_cast(node_flags::moved | node_flags::is_new) : std::uint8_t{0}; + set_moved(m_doc, slot, e.region, 0); + edit_state_of(m_doc).regions[e.region] = slot; + } + + /// a node for a new element (links point to it; it never moves) + node* new_slot(const encoded& e) + { + if (e.region != nullptr) + { + return e.region; + } + node* const s = alloc_nodes(m_doc, 1); + *s = e.scalar; + return s; + } + + ////////////// + // encoding // + ////////////// + + template + using encode_tag = std::integral_constant; + + template + struct is_view : std::false_type {}; + + template + struct is_view> : std::true_type {}; + + template + encoded encode(V&& v) + { + using D = typename std::decay::type; + return encode_impl(std::forward(v), encode_tag < is_view::value ? 0 + : std::is_same::value ? 1 + : std::is_same::value ? 2 + : std::is_same::value ? 3 + : std::is_arithmetic::value ? 4 + : std::is_convertible::value ? 5 : 6 > {}); + } + + /// a view of any document (copied; nothing is shared with it) + template + encoded encode_impl(const basic_json_view& v, encode_tag<0> /*view*/) + { + if (NLOHMANN_VIEW_UNLIKELY(v.m_node == nullptr)) + { + throw_type_error(302, "type must be a value, but is ", "discarded"); + } + encoded r; + if (!is_container(*v.m_node)) + { + r.scalar = copy_scalar(*v.m_doc, *v.m_node); + return r; + } + r.region = alloc_nodes(m_doc, count_nodes(*v.m_doc, v.m_node)); + fill_nodes(*v.m_doc, v.m_node, r.region); + edit_state_of(m_doc).regions.emplace(r.region, nullptr); + return r; + } + + encoded encode_impl(const BasicJsonType& j, encode_tag<1> /*json*/) + { + encoded r; + if (!j.is_structured()) + { + r.scalar = json_scalar(j); + return r; + } + r.region = alloc_nodes(m_doc, count_nodes(j)); + fill_nodes(j, r.region); + edit_state_of(m_doc).regions.emplace(r.region, nullptr); + return r; + } + + encoded encode_impl(std::nullptr_t /*unused*/, encode_tag<2> /*null*/) + { + encoded r; + r.scalar = plain_node(value_t::null); + return r; + } + + encoded encode_impl(bool b, encode_tag<3> /*boolean*/) + { + encoded r; + r.scalar = plain_node(value_t::boolean); + r.scalar.flags = static_cast(r.scalar.flags | (b ? node_flags::is_true : 0)); + return r; + } + + template + encoded encode_impl(T x, encode_tag<4> /*number*/) + { + encoded r; + r.scalar = number_node(x, std::integral_constant < int, std::is_floating_point::value ? 0 : (std::is_signed::value ? 1 : 2) > {}); + return r; + } + + template + encoded encode_impl(const T& s, encode_tag<5> /*string*/) + { + const string_view_t sv(s); + check_utf8(sv.data(), sv.size()); + encoded r; + r.scalar = string_node(sv.data(), sv.size()); + return r; + } + + template + encoded encode_impl(T&& x, encode_tag<6> /*other*/) + { + return encode_impl(BasicJsonType(std::forward(x)), encode_tag<1> {}); + } + + static node plain_node(value_t k) noexcept + { + node n{}; + n.kind = static_cast(k); + n.flags = node_flags::is_new; + return n; + } + + template + node number_node(T x, std::integral_constant /*floating-point*/) + { + return float_node(static_cast(x)); + } + + template + node number_node(T x, std::integral_constant /*signed*/) + { + return integer_node(static_cast(static_cast(x)), value_t::number_integer); + } + + template + node number_node(T x, std::integral_constant /*unsigned*/) + { + return integer_node(static_cast(x), value_t::number_unsigned); + } + + /// an integer with its canonical token in the edit arena + node integer_node(std::uint64_t bits, value_t k) + { + const bool negative = k == value_t::number_integer && static_cast(bits) < 0; + std::uint64_t magnitude = negative ? 0 - bits : bits; + std::array buf{}; + char* p = buf.data() + buf.size(); + do + { + *--p = static_cast('0' + (magnitude % 10)); + magnitude /= 10; + } + while (magnitude != 0); + if (negative) + { + *--p = '-'; + } + const auto len = static_cast(buf.data() + buf.size() - p); + node n = plain_node(k); + n.flags = static_cast(n.flags | node_flags::edited); + n.off = append_text(m_doc, p, len); + // number_length() adds one for the sign of number_integer nodes + n.extra = static_cast(k == value_t::number_integer ? len - 1 : len); + set_integer_bits(n, bits); + return n; + } + + /// a float with its shortest round-trip token (as basic_json::dump() + /// writes it), or nan, inf, -inf, in the edit arena + node float_node(number_float_t x) + { + string_t text; + if (std::isnan(x)) + { + text = "nan"; + } + else if (std::isinf(x)) + { + text = x > 0 ? "inf" : "-inf"; + } + else + { + text = BasicJsonType(x).dump(); + } + node n = plain_node(value_t::number_float); + n.flags = static_cast(n.flags | node_flags::edited); + n.extra = 0xFFFFu; // (the digit layout is not recorded) + n.off = append_text(m_doc, text.data(), text.size()); + n.len = static_cast(text.size()); + return n; + } + + /// a string (or key) in the edit arena + node string_node(const char* s, std::size_t len) + { + if (NLOHMANN_VIEW_UNLIKELY(len >= 0xFFFFFFFFu)) + { + throw_out_of_range(416, "strings of 4 GiB or more are not supported by json_document"); // LCOV_EXCL_LINE + } + node n = plain_node(value_t::string); + n.flags = static_cast(n.flags | node_flags::edited); + n.off = append_text(m_doc, s, len); + n.len = static_cast(len); + return n; + } + + /// a scalar of a view (of any document) as a node of this document + node copy_scalar(const document_data& from, const node& n) + { + if (&from == &m_doc) + { + return n; // the same storage + } + switch (static_cast(n.kind)) + { + case value_t::string: + return string_node(from.str(n), n.len); + case value_t::number_integer: + case value_t::number_unsigned: + return integer_node(integer_bits(n), static_cast(n.kind)); + case value_t::number_float: + { + node r = plain_node(value_t::number_float); + r.flags = static_cast(r.flags | node_flags::edited); + r.off = append_text(m_doc, from.str(n), n.len); + r.len = n.len; + r.extra = n.extra; + return r; + } + case value_t::boolean: + { + node r = plain_node(value_t::boolean); + r.flags = static_cast(r.flags | (n.flags & node_flags::is_true)); + return r; + } + case value_t::null: + case value_t::object: + case value_t::array: + case value_t::binary: + case value_t::discarded: + default: + return plain_node(value_t::null); + } + } + + node json_scalar(const BasicJsonType& j) + { + switch (j.type()) + { + case value_t::null: + return plain_node(value_t::null); + case value_t::boolean: + { + node r = plain_node(value_t::boolean); + r.flags = static_cast(r.flags | (j.template get() ? node_flags::is_true : 0)); + return r; + } + case value_t::number_integer: + return integer_node(static_cast(static_cast(j.template get())), value_t::number_integer); + case value_t::number_unsigned: + return integer_node(static_cast(j.template get()), value_t::number_unsigned); + case value_t::number_float: + return float_node(j.template get()); + case value_t::string: + { + const auto& s = j.template get_ref(); + check_utf8(s.data(), s.size()); + return string_node(s.data(), s.size()); + } + case value_t::binary: + throw_type_error(319, "cannot store a binary value in a json_document", ""); + case value_t::discarded: + case value_t::object: + case value_t::array: + default: + throw_type_error(302, "type must be a value, but is ", "discarded"); + } + } + + /// number of nodes of a subtree (containers, keys, scalars) + template + static std::size_t count_nodes(const document_data& d, const node* n) + { + if (!is_container(*n)) + { + return 1; + } + const bool object = n->kind == static_cast(value_t::object); + std::size_t r = 1; + for (const node* c = navigation::first(d, n), *end = navigation::end(d, n); c != end;) + { + const node* const v = object ? c + 1 : c; + r += (object ? 1 : 0) + count_nodes(d, navigation::value(v)); + c = document_data::after(v); + } + return r; + } + + /// copy a subtree (of any document) as a contiguous sequence; returns its end + template + node* fill_nodes(const document_data& d, const node* n, node* out) + { + if (!is_container(*n)) + { + *out = copy_scalar(d, *n); + return out + 1; + } + node* const self = out++; + *self = plain_node(static_cast(n->kind)); + self->len = n->len; + const bool object = n->kind == static_cast(value_t::object); + for (const node* c = navigation::first(d, n), *end = navigation::end(d, n); c != end;) + { + if (object) + { + *out++ = copy_scalar(d, *c); + ++c; + } + out = fill_nodes(d, navigation::value(c), out); + c = document_data::after(c); + } + self->next = static_cast(out - self); + return out; + } + + static std::size_t count_nodes(const BasicJsonType& j) + { + std::size_t r = 1; + if (j.is_object()) + { + for (const auto& member : j.items()) + { + r += 1 + count_nodes(member.value()); + } + } + else if (j.is_array()) + { + for (const auto& e : j) + { + r += count_nodes(e); + } + } + return r; + } + + node* fill_nodes(const BasicJsonType& j, node* out) + { + if (!j.is_structured()) + { + *out = json_scalar(j); + return out + 1; + } + node* const self = out++; + *self = plain_node(j.type()); + self->len = static_cast(j.size()); + if (j.is_object()) + { + for (const auto& member : j.items()) + { + check_utf8(member.key().data(), member.key().size()); + *out++ = string_node(member.key().data(), member.key().size()); + out = fill_nodes(member.value(), out); + } + } + else + { + for (const auto& e : j) + { + out = fill_nodes(e, out); + } + } + self->next = static_cast(out - self); + return out; + } + + document_data& m_doc; +}; + +} // namespace view +} // namespace detail +NLOHMANN_JSON_NAMESPACE_END + +// #include + +// #include + // #include // __ _____ _____ _____ // __| | __| | | | JSON for Modern C++ @@ -2800,295 +4061,6 @@ class view_items NLOHMANN_JSON_NAMESPACE_END // #include -// __ _____ _____ _____ -// __| | __| | | | JSON for Modern C++ -// | | |__ | | | | | | version 3.12.0 -// |_____|_____|_____|_|___| https://github.com/nlohmann/json -// -// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann -// SPDX-License-Identifier: MIT - - - -#include // size_t -#include // uint16_t, uint32_t, uint64_t -#include // memcmp, memcpy - -// #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 // size_t -#include // uint32_t, uint64_t -#include // memcmp - -// #include -// #include - -// #include - -// #include - - -// Hash indexes of large objects, so that a lookup does not compare thousands -// of keys (as Boost.JSON switches from a linear search to a hash table for -// large objects). An object with document_data::index_min_members members or -// more gets an open-addressing table after parsing; its node stores the -// number of the table (1-based) in `extra`. A slot holds the offset of a key -// node from its object node (0: empty). Of duplicate keys, the first is kept, -// as for the linear search. - -NLOHMANN_JSON_NAMESPACE_BEGIN -namespace detail -{ -namespace view -{ - -/// hash of a key: its bytes, eight at a time, in a fixed byte order -inline std::uint64_t key_hash(const char* s, std::size_t n) noexcept -{ - std::uint64_t h = 0x9E3779B97F4A7C15u * (n + 1); - const auto* p = reinterpret_cast(s); // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast) - while (n >= 8) - { - h = (h ^ read_eight_bytes(p)) * 0xBF58476D1CE4E5B9u; - h ^= h >> 29u; - p += 8; - n -= 8; - } - std::uint64_t w = 0; - for (std::size_t i = 0; i < n; ++i) - { - w |= static_cast(p[i]) << (8u * i); - } - h = (h ^ w) * 0x94D049BB133111EBu; - return h ^ (h >> 31u); -} - -/// build the table of a large object -inline void build_object_index(document_data& d, node* obj) -{ - if (d.indexes.size() >= 0xFFFFu) - { - return; // LCOV_EXCL_LINE (the number must fit `extra`; more large objects are searched linearly) - } - std::size_t cap = 16; - while (cap < 2 * static_cast(obj->len)) - { - cap *= 2; - } - const std::size_t start = d.index_slots.size(); - d.index_slots.resize(start + cap, 0); - std::uint32_t* const slots = d.index_slots.data() + start; - const std::size_t mask = cap - 1; - for (const node* k = document_data::first_child(obj), *end = document_data::child_end(obj); k != end; k = document_data::after(k + 1)) - { - const char* const key = d.str(*k); - std::size_t i = static_cast(key_hash(key, k->len)) & mask; - bool duplicate = false; - while (slots[i] != 0) - { - const node* const other = obj + slots[i]; - if (other->len == k->len && (k->len == 0 || std::memcmp(d.str(*other), key, k->len) == 0)) - { - duplicate = true; // keep the first - break; - } - i = (i + 1) & mask; - } - if (!duplicate) - { - slots[i] = static_cast(k - obj); - } - } - d.indexes.push_back(document_data::object_index{start, static_cast(mask)}); - obj->extra = static_cast(d.indexes.size()); -} - -/// build the tables of the large objects the parser noted -inline void build_object_indexes(document_data& d) -{ - for (const std::uint32_t i : d.large_objects) - { - build_object_index(d, d.tape + i); - } -} - -/// the key node of the first member with this key of an indexed object, or -/// nullptr -inline const node* find_indexed(const document_data& d, const node* obj, const char* key, std::size_t n) noexcept -{ - const document_data::object_index& ix = d.indexes[obj->extra - 1u]; - const std::uint32_t* const slots = d.index_slots.data() + ix.start; - std::size_t i = static_cast(key_hash(key, n)) & ix.mask; - for (;;) - { - const std::uint32_t s = slots[i]; - if (s == 0) - { - return nullptr; - } - const node* const k = obj + s; - if (k->len == n && (n == 0 || std::memcmp(d.str(*k), key, n) == 0)) - { - return k; - } - i = (i + 1) & ix.mask; - } -} - -} // namespace view -} // namespace detail -NLOHMANN_JSON_NAMESPACE_END - - -NLOHMANN_JSON_NAMESPACE_BEGIN -namespace detail -{ -namespace view -{ - -/// equality test for strings of one length n <= 16: two overlapping loads per -/// string (the first and the last 8, 4, or 2 bytes) replace a memcmp, and no -/// byte outside [s, s + n) is read -class short_key -{ - public: - short_key(const unsigned char* k, std::size_t n) noexcept - : m_n(n) - { - load(k, m_a, m_b); - } - - NLOHMANN_VIEW_ALWAYS_INLINE bool matches(const unsigned char* s) const noexcept - { - std::uint64_t a = 0; - std::uint64_t b = 0; - load(s, a, b); - return a == m_a && b == m_b; - } - - private: - template - static NLOHMANN_VIEW_ALWAYS_INLINE std::uint64_t load_word(const unsigned char* s) noexcept - { - T w = 0; - std::memcpy(&w, s, sizeof(T)); - return w; - } - - NLOHMANN_VIEW_ALWAYS_INLINE void load(const unsigned char* s, std::uint64_t& a, std::uint64_t& b) const noexcept - { - if (m_n >= 8) - { - a = load_word(s); - b = load_word(s + m_n - 8); - } - else if (m_n >= 4) - { - a = load_word(s); - b = load_word(s + m_n - 4); - } - else if (m_n >= 2) - { - a = load_word(s); - b = load_word(s + m_n - 2); - } - else - { - a = m_n == 1 ? s[0] : 0; - b = 0; - } - } - - std::size_t m_n; - std::uint64_t m_a = 0; - std::uint64_t m_b = 0; -}; - -/// the key node of the first member of an object with the given key, or -/// nullptr; most keys are rejected by their length, from the index alone -template -const node* find_member(const document_data& d, const node* object, const char* key, std::size_t n) noexcept -{ - using nav = navigation; - if (NLOHMANN_VIEW_UNLIKELY(object->extra != 0) && (!Editable || (object->flags & node_flags::moved) == 0)) - { - return find_indexed(d, object, key, n); // a large object (whose members have not been edited) - } - const node* const end = nav::end(d, object); - const auto* const k = reinterpret_cast(key); // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast) - if (NLOHMANN_VIEW_LIKELY(n <= 16)) - { - const short_key probe(k, n); - for (const node* m = nav::first(d, object); m != end; m = document_data::after(m + 1)) - { - if (m->len == n && probe.matches(reinterpret_cast(d.str(*m)))) // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast) - { - return m; - } - } - return nullptr; - } - for (const node* m = nav::first(d, object); m != end; m = document_data::after(m + 1)) - { - if (m->len == n && std::memcmp(d.str(*m), key, n) == 0) - { - return m; - } - } - return nullptr; -} - -/// the entry of the element of an array at an index below its size (a link -/// in the moved sequences of editable documents) -template -const node* element_at(const document_data& d, const node* array, std::size_t idx) noexcept -{ - const node* e = navigation::first(d, array); - if (Editable && (array->flags & node_flags::moved) != 0 && d.edits->moved_cap[array->off] != 0) - { - return e + idx; // a growable block: one link per element - } - for (std::size_t i = 0; i < idx; ++i) - { - e = document_data::after(e); - } - return e; -} - -/// the entry of the last element of a non-empty array, or the key of the -/// last member of a non-empty object -template -const node* last_child(const document_data& d, const node* container) noexcept -{ - const std::size_t value_offset = container->kind == static_cast(value_t::object) ? 1 : 0; - const node* const end = navigation::end(d, container); - const node* last = navigation::first(d, container); - for (const node* c = document_data::after(last + value_offset); c != end; c = document_data::after(c + value_offset)) - { - last = c; - } - return last; -} - -} // namespace view -} // namespace detail -NLOHMANN_JSON_NAMESPACE_END // #include @@ -4828,6 +5800,19 @@ class basic_json_view return !(a == b); } + /// a view of an editable document compares with one of a read-only document + template < bool E, typename std::enable_if < E != Editable, int >::type = 0 > + friend bool operator==(const basic_json_view& a, const basic_json_view& b) + { + return detail::view::equal(side(a), detail::view::view_side>(b)); + } + + template < bool E, typename std::enable_if < E != Editable, int >::type = 0 > + friend bool operator!=(const basic_json_view& a, const basic_json_view& b) + { + return !(a == b); + } + /// whether the value parse() would produce for a view equals a value friend bool operator==(const basic_json_view& a, const BasicJsonType& j) { @@ -4876,6 +5861,7 @@ class basic_json_view private: template friend class basic_json_document; template friend class basic_json_view; + template friend class detail::view::editor; friend iterator; basic_json_view(const document_data* d, const node* n) noexcept @@ -5179,9 +6165,124 @@ class basic_json_document } } + /////////// + // edits // + /////////// + + // The source text is never written; new values go to storage owned by + // the document. A view keeps referring to the same value: after an + // assignment it sees the new value, and edits elsewhere do not affect it. + // A view of an erased value keeps its last value. An edit of an + // array/object invalidates the iterators over it. Values are accepted as + // views (of any document), BasicJsonType values, and everything + // BasicJsonType can be constructed from. + + using string_view_t = typename view_type::string_view_t; + using json_pointer = typename BasicJsonType::json_pointer; + + /// replace a value (a view of this document); returns a view of it + template + view_type set(view_type target, V&& value) + { + return editor().set(target, std::forward(value)); + } + + /// set a member (added if missing; a null value becomes an object); + /// returns a view of the member value + template + view_type set(view_type object, string_view_t key, V&& value) + { + return editor().set(object, key, std::forward(value)); + } + + /// assign an existing array element; returns a view of it + template < typename I, typename V, typename std::enable_if < std::is_integral::value && !std::is_same::value, int >::type = 0 > + view_type set(view_type array, I idx, V && value) + { + return editor().set(array, index(idx), std::forward(value)); + } + + /// set the value at a JSON pointer: its parent must exist; an object + /// member is set (added if missing), an array element assigned, and "-" + /// or the size of the array appends + template + view_type set(const json_pointer& ptr, V&& value) + { + if (ptr.empty()) + { + return set(root(), std::forward(value)); + } + const view_type parent = root().at(ptr.parent_pointer()); + const auto& token = ptr.back(); + if (parent.is_array()) + { + const std::size_t idx = token == "-" ? parent.size() : pointer_index(token); + if (idx == parent.size()) + { + return push_back(parent, std::forward(value)); + } + return set(parent, idx, std::forward(value)); + } + return set(parent, string_view_t(token.data(), token.size()), std::forward(value)); + } + + /// append to an array (a null value becomes an array); returns a view of + /// the new element + template + view_type push_back(view_type array, V&& value) + { + return editor().push_back(array, std::forward(value)); + } + private: using input_kind = detail::view::input_kind; + detail::view::editor editor() + { + static_assert(Editable, "only an editable document can be edited: use basic_json_document (json_editable_document)"); + if (NLOHMANN_VIEW_UNLIKELY(!m_data || m_data->discarded)) + { + detail::view::throw_invalid_iterator(202, "view does not belong to this document"); + } + return detail::view::editor(*m_data); + } + + /// an index (out_of_range.401 if negative) + template + static std::size_t index(I idx) + { + return index(idx, std::is_signed {}); + } + + template + static std::size_t index(I idx, std::true_type /*signed*/) + { + if (idx < 0) + { + detail::view::throw_out_of_range(401, detail::concat("array index ", std::to_string(idx), " is out of range")); + } + return static_cast(idx); + } + + template + static std::size_t index(I idx, std::false_type /*unsigned*/) + { + return static_cast(idx); + } + + /// the array index of a JSON pointer token (json_pointer's rules) + template + static std::size_t pointer_index(const StringType& token) + { + std::size_t idx = 0; + const detail::view::index_status status = detail::view::array_index(token, idx); + if (status != detail::view::index_status::ok) + { + detail::view::throw_array_index_error(status, token); + } + return idx; + } + /// create the storage (sized for the input) on first use void ensure_data(const char* src, std::size_t size) { @@ -5348,6 +6449,15 @@ using json_view = basic_json_view; using ordered_json_document = basic_json_document; /// a value of an ordered_json_document using ordered_json_view = basic_json_view; +/// an editable parsed JSON text for json +using json_editable_document = basic_json_document; +/// a value of a json_editable_document +using json_editable_view = basic_json_view; +/// an editable parsed JSON text for ordered_json +using ordered_json_editable_document = basic_json_document; +/// a value of an ordered_json_editable_document +using ordered_json_editable_view = basic_json_view; + NLOHMANN_JSON_NAMESPACE_END // tuple protocol for the items of basic_json_view::items() (structured bindings) diff --git a/tests/src/unit-json_view_edit.cpp b/tests/src/unit-json_view_edit.cpp new file mode 100644 index 000000000..3a59e3ec9 --- /dev/null +++ b/tests/src/unit-json_view_edit.cpp @@ -0,0 +1,466 @@ +// __ _____ _____ _____ +// __| | __| | | | JSON for Modern C++ (supporting code) +// | | |__ | | | | | | version 3.12.0 +// |_____|_____|_____|_|___| https://github.com/nlohmann/json +// +// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann +// SPDX-License-Identifier: MIT + +#include "doctest_compatibility.h" + +#include +using nlohmann::json; +using nlohmann::ordered_json; +using nlohmann::json_document; +using nlohmann::json_editable_document; +using nlohmann::json_editable_view; +using nlohmann::ordered_json_document; +using nlohmann::ordered_json_editable_document; +using nlohmann::ordered_json_editable_view; +using ptr_t = ordered_json::json_pointer; + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace +{ +std::uint32_t rng() +{ + static std::mt19937 generator(5295); + return generator(); +} + +int r(int n) +{ + return static_cast(rng() % static_cast(n)); +} + +int counter = 0; + +std::string random_string() +{ + static const char* const pieces[] = {"a", "Z", " ", "~", "\n", "\"", "\\", "/", "\xc3\xa9", "\xe3\x81\x82", "\xf0\x9f\x98\x80", "\x7f", "\x1f", "0", "key"}; + std::string s; + for (int i = r(3) == 0 ? r(30) : r(6); i > 0; --i) + { + s += pieces[r(15)]; + } + return s; +} + +ordered_json random_scalar() +{ + switch (r(9)) + { + case 0: + return nullptr; + case 1: + return r(2) == 0; + case 2: + return static_cast(rng()) - 2147483648LL; + case 3: + return static_cast(rng()) * 4294967296ULL + rng(); + case 4: + return static_cast(static_cast(rng())) / (1 + r(1000)); + case 5: + return r(2) == 0 ? 1e300 * (r(2) == 0 ? 1 : -1) : 5e-324; + case 6: + return -0.0; + default: + return random_string(); + } +} + +ordered_json random_value(int depth) +{ + const int k = depth > 3 ? 4 : r(7); + if (k == 0) + { + ordered_json o = ordered_json::object(); + for (int i = r(5); i > 0; --i) + { + o[random_string() + "#" + std::to_string(counter++)] = random_value(depth + 1); + } + return o; + } + if (k == 1) + { + ordered_json a = ordered_json::array(); + for (int i = r(5); i > 0; --i) + { + a.push_back(random_value(depth + 1)); + } + return a; + } + return random_scalar(); +} + +void collect(const ordered_json& j, const ptr_t& p, std::vector& out) +{ + out.push_back(p); + if (j.is_object()) + { + for (const auto& kv : j.items()) + { + collect(kv.value(), p / kv.key(), out); + } + } + else if (j.is_array()) + { + for (std::size_t i = 0; i < j.size(); ++i) + { + collect(j[i], p / i, out); + } + } +} + + +// the edited view and the ordered_json value read the same through the whole +// read API +void compare(const ordered_json_editable_view& v, const ordered_json& j) +{ + REQUIRE(v.type() == j.type()); + REQUIRE(v.size() == j.size()); + if (j.is_object()) + { + auto jt = j.begin(); + for (auto it = v.begin(); it != v.end(); ++it, ++jt) + { + CHECK(std::string(it.key().data(), it.key().size()) == jt.key()); + CHECK(v[jt.key()].dump() == jt.value().dump()); + CHECK(v.contains(jt.key())); + compare(*it, jt.value()); + } + } + else if (j.is_array()) + { + for (std::size_t i = 0; i < j.size(); ++i) + { + CHECK(v[i].dump() == j[i].dump()); + } + std::size_t i = 0; + for (const auto e : v) + { + compare(e, j[i++]); + } + if (!j.empty()) + { + CHECK(v.back().dump() == j.back().dump()); + } + } + else if (j.is_string()) + { + CHECK(v.get() == j.get()); + } + else if (j.is_number_float()) + { + const double a = v.get(); + const double b = j.get(); + CHECK(std::memcmp(&a, &b, sizeof(double)) == 0); + } + else if (j.is_number_integer()) + { + CHECK(v.get() == j.get()); + CHECK(v.get() == j.get()); + } +} + +void check_all(const ordered_json_editable_document& d, const ordered_json& j, bool deep) +{ + const std::string text = d.root().dump(); + REQUIRE(text == j.dump()); + CHECK(d.root().materialize() == j); + if (deep) + { + CHECK(d.root().dump(2) == j.dump(2)); + CHECK(d.root().dump(-1, ' ', true) == j.dump(-1, ' ', true)); + CHECK(d.root() == j); + compare(d.root(), j); + // a fresh document of the text reads the same + const ordered_json_document fresh = ordered_json_document::parse(text); + CHECK(fresh.root() == d.root()); + } +} +} // namespace + +TEST_CASE("json_view edits: differential") +{ + // random edits are applied to an ordered_json_editable_document and to the + // ordered_json parse() produces; after every edit both must serialize, + // materialize, and read back the same + for (int n = 0; n < 150; ++n) + { + ordered_json j = random_value(0); + if (r(4) == 0) + { + j = ordered_json::object({{"a", random_value(1)}, {"b", random_value(1)}}); + } + const std::string text = j.dump(r(2) == 0 ? -1 : 2); + CAPTURE(text); + ordered_json_editable_document d = ordered_json_editable_document::parse(text); + j = ordered_json::parse(text); + const ordered_json_document other = ordered_json_document::parse(random_value(0).dump()); + const int edits = 30; + for (int e = 0; e < edits; ++e) + { + std::vector paths; + collect(j, ptr_t(), paths); + const ptr_t p = paths[static_cast(r(static_cast(paths.size())))]; + const ordered_json& target = j[p]; + const ordered_json_editable_view tv = d.root().at(p); + const int op = r(12); + { + if (op == 0) // assign a scalar + { + const ordered_json v = random_scalar(); + if (v.is_string() && r(2) == 0) + { + d.set(tv, v.get()); + } + else if (v.is_number_unsigned() && r(2) == 0) + { + d.set(tv, v.get()); + } + else + { + d.set(tv, v); + } + j[p] = v; + } + else if (op == 1) // assign a new array/object (or anything), sometimes via the pointer API + { + const ordered_json v = random_value(2); + if (r(2) == 0) + { + d.set(p, v); + } + else + { + d.set(tv, v); + } + j[p] = v; + } + else if (op == 2) // copy a value of the same document + { + const ptr_t q = paths[static_cast(r(static_cast(paths.size())))]; + const ordered_json v = j[q]; + d.set(tv, d.root().at(q)); + j[p] = v; + } + else if (op == 3) // copy a value of another document + { + d.set(tv, other.root()); + j[p] = other.root().materialize(); + } + else if ((op == 4 || op == 5) && target.is_object()) // set a member (new or existing) + { + std::string key = random_string() + "#" + std::to_string(counter++); + if (op == 5 && !target.empty()) + { + key = std::next(target.begin(), r(static_cast(target.size()))).key(); + } + const ordered_json v = random_value(2); + d.set(tv, key, v); + j[p][key] = v; + } + else if (op == 7 && (target.is_array() || target.is_null())) // push_back + { + const ordered_json v = random_value(2); + d.push_back(tv, v); + j[p].push_back(v); + } + else if (op == 10 && target.is_array() && !target.empty()) // assign an element + { + const std::size_t i = static_cast(r(static_cast(target.size()))); + const ordered_json v = random_value(2); + d.set(tv, i, v); + j[p][i] = v; + } + else if (op == 11) // a held view sees the assignment + { + const ordered_json v = random_value(2); + const ordered_json_editable_view held = d.root().at(p); + d.set(p, v); + j[p] = v; + CHECK(held.dump() == v.dump()); + } + else + { + continue; + } + } + CAPTURE(p.to_string()); + CAPTURE(op); + check_all(d, j, e % 8 == 7 || e == edits - 1); + } + } +} + +namespace +{ +std::string exception_of_call(const std::function& f) +{ + try + { + f(); + } + catch (const json::exception& e) + { + return e.what(); + } + return ""; +} +} // namespace + +TEST_CASE("json_view edits: errors") +{ + json_editable_document d = json_editable_document::parse(R"({"o": {"a": 1}, "a": [1, 2], "n": 1, "z": null})"); + const json_editable_view root = d.root(); + const json_document other = json_document::parse("[1]"); + json_editable_document other_editable = json_editable_document::parse("[1]"); + + 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.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&); + + // invalid UTF-8 is rejected when it enters the document, with the error + // basic_json::dump() reports for the same string + for (const std::string bad : + {"\xC3\x28", "a\xE2\x28\xA1", "\xF0\x28\x8C\xBC", "\xE2\x82", "x\xF0\x9F\x98", "\xFF", "\xED\xA0\x80", "\xC0\xAF" + }) + { + CAPTURE(bad); + const std::string expected = exception_of_call([&] + { + const std::string text = json(bad).dump(); + static_cast(text); + }); + CHECK(!expected.empty()); + CHECK(exception_of_call([&] { d.set(root["z"], bad); }) == expected); + CHECK(exception_of_call([&] { d.set(root["o"], bad, 1); }) == expected); + CHECK(exception_of_call([&] { d.set(root["z"], json{{"k", bad}}); }) == expected); + } + // nothing of the failed edits is visible + CHECK(root.dump() == R"({"o":{"a":1},"a":[1,2],"n":1,"z":null})"); + static_cast(other); +} + +TEST_CASE("json_view edits: views and values") +{ + SECTION("strings stay valid while more edits come") + { + json_editable_document d = json_editable_document::parse("[]"); + const auto first = d.push_back(d.root(), std::string(100, 'x')).get_string(); + for (int i = 0; i < 1000; ++i) + { + d.push_back(d.root(), std::string(static_cast(i % 50), 'y')); + } + CHECK(std::string(first.data(), first.size()) == std::string(100, 'x')); + CHECK(d.root().size() == 1001); + } + + SECTION("numbers") + { + json_editable_document d = json_editable_document::parse(R"([1.50, 1E2, 3])"); + d.set(d.root(), 2, 0.1); + d.push_back(d.root(), std::numeric_limits::quiet_NaN()); + d.push_back(d.root(), -std::numeric_limits::infinity()); + d.push_back(d.root(), static_cast(18446744073709551615u)); + d.push_back(d.root(), static_cast(-9223372036854775807 - 1)); + CHECK(d.root().dump() == "[1.5,100.0,0.1,null,null,18446744073709551615,-9223372036854775808]"); + CHECK(d.root().dump(-1, ' ', false, json_editable_view::number_format::source) == "[1.50,1E2,0.1,null,null,18446744073709551615,-9223372036854775808]"); + CHECK(std::isnan(d.root()[3].get())); + CHECK(std::isinf(d.root()[4].get())); + CHECK(d.root()[2].number_token() == "0.1"); + CHECK(d.root()[5].get() == 18446744073709551615u); + CHECK(d.root()[6].number_token() == "-9223372036854775808"); + CHECK(d.root().materialize().dump() == json::parse(R"([1.5, 100.0, 0.1, null, null, 18446744073709551615, -9223372036854775808])").dump()); + } + + SECTION("nulls become containers, and the root can be replaced") + { + json_editable_document d = json_editable_document::parse("[null, null]"); + d.set(d.root()[0], "k", 1); + d.push_back(d.root()[1], true); + CHECK(d.root().dump() == R"([{"k":1},[true]])"); + d.set(d.root(), "scalar"); + CHECK(d.root().dump() == R"("scalar")"); + d.set(d.root(), json{{"x", {1, 2}}}); + d.set(json::json_pointer("/x/-"), 3); + d.set(json::json_pointer("/x/3"), 4); // the size of the array appends too + d.set(json::json_pointer("/y"), false); + CHECK(d.root().dump() == R"({"x":[1,2,3,4],"y":false})"); + } + + SECTION("duplicate keys") + { + json_editable_document d = json_editable_document::parse(R"({"a": 1, "b": 2, "a": 3})"); + d.set(d.root(), "a", 4); // the first member is assigned, the others dropped + CHECK(d.root().dump() == R"({"a":4,"b":2})"); + } + + SECTION("values from other documents") + { + const json_document source = json_document::parse(R"({"list": [1, "two", {"three": 3.5}], "text": "a\nb"})"); + json_editable_document edited = json_editable_document::parse("[0]"); + edited.set(edited.root(), 0, json{{"inner", {1, 2}}}); + json_editable_document d = json_editable_document::parse("{}"); + d.set(d.root(), "copy", source.root()["list"]); + d.set(d.root(), "text", source.root()["text"]); + d.set(d.root(), "edited", edited.root()[0]); + d.set(d.root(), "self", d.root()["copy"]); + CHECK(d.root().dump() == R"({"copy":[1,"two",{"three":3.5}],"text":"a\nb","edited":{"inner":[1,2]},"self":[1,"two",{"three":3.5}]})"); + CHECK(d.root()["copy"] == source.root()["list"]); + CHECK(source.root()["list"] == d.root()["self"]); + CHECK(d.root() != source.root()); + } + + SECTION("large objects") + { + std::string text = "{"; + for (int i = 0; i < 200; ++i) + { + text += (i != 0 ? ",\"k" : "\"k") + std::to_string(i) + "\":" + std::to_string(i); + } + text += "}"; + json_editable_document d = json_editable_document::parse(text); + d.set(d.root(), "k7", "seven"); // assigned in place: the index stays in use + CHECK(d.root()["k7"].get_string() == "seven"); + d.set(d.root(), "new", 1); // appended: the members move, the lookup is linear + CHECK(d.root()["new"].get() == 1); + CHECK(d.root()["k199"].get() == 199); + CHECK(d.root().size() == 201); + } + + SECTION("reuse and memory") + { + json_editable_document d = json_editable_document::parse("[1, 2, 3]"); + const std::size_t before = d.memory_usage(); + for (int i = 0; i < 100; ++i) + { + d.push_back(d.root(), "some text"); + } + CHECK(d.memory_usage() > before); + const json_editable_view first = d.root()[0]; + d.shrink_to_fit(); // (with edits, the index stays in place) + CHECK(first.get() == 1); + d.read(std::string("[true]")); + CHECK(d.root().dump() == "[true]"); + d.push_back(d.root(), false); + CHECK(d.root().dump() == "[true,false]"); + } +}