From a0b98325cfd0c0e69e157dd9831300de013140eb Mon Sep 17 00:00:00 2001 From: Niels Lohmann Date: Tue, 6 Oct 2026 10:48:07 +0200 Subject: [PATCH] Add editable json_documents: set, push_back, insert, and erase basic_json_document gets a second template parameter, Editable (false by default), plus the aliases json_editable_document, json_editable_view, ordered_json_editable_document and ordered_json_editable_view. Editable documents can change values and structure without rewriting the source text: set()/push_back() on values, keys, array indices and JSON pointers; insert() before an array element; erase() of an object key, array index or JSON pointer. New values and element sequences go into edit storage that the document owns and never moves, so views keep referring to their value across edits and a parsed node never moves. Read-only documents walk the plain node array and are unaffected. Strings are checked for UTF-8 on entry, so dump() of an editable document never throws type_error.316. Binary values cannot be stored (type_error.319). A seeded differential test applies random edits to an editable document and to the equivalent ordered_json and compares both after every step. Signed-off-by: Niels Lohmann --- .github/labeler.yml | 4 +- BUILD.bazel | 2 + docs/docset/docSet.sql | 8 + .../docs/api/basic_json_document/erase.md | 128 + .../docs/api/basic_json_document/index.md | 56 +- .../docs/api/basic_json_document/insert.md | 114 + .../docs/api/basic_json_document/push_back.md | 102 + .../docs/api/basic_json_document/set.md | 194 ++ docs/mkdocs/docs/api/basic_json_view/index.md | 16 +- .../mkdocs/docs/api/json_editable_document.md | 43 + docs/mkdocs/docs/api/json_editable_view.md | 41 + .../api/ordered_json_editable_document.md | 47 + .../docs/api/ordered_json_editable_view.md | 40 + .../examples/basic_json_document__erase.cpp | 38 + .../basic_json_document__erase.output | 18 + .../examples/basic_json_document__insert.cpp | 38 + .../basic_json_document__insert.output | 23 + .../basic_json_document__push_back.cpp | 24 + .../basic_json_document__push_back.output | 23 + .../examples/basic_json_document__set.cpp | 41 + .../examples/basic_json_document__set.output | 25 + .../docs/examples/json_editable_document.cpp | 30 + .../examples/json_editable_document.output | 4 + .../ordered_json_editable_document.cpp | 15 + .../ordered_json_editable_document.output | 1 + docs/mkdocs/docs/features/index.md | 1 + docs/mkdocs/docs/features/json_view.md | 75 +- docs/mkdocs/docs/home/architecture.md | 19 +- docs/mkdocs/docs/home/exceptions.md | 28 +- docs/mkdocs/mkdocs.yml | 8 + .../nlohmann/detail/view/document_data.hpp | 98 +- include/nlohmann/detail/view/edit.hpp | 767 ++++++ include/nlohmann/detail/view/edit_storage.hpp | 242 ++ include/nlohmann/detail/view/errors.hpp | 5 + include/nlohmann/detail/view/iterator.hpp | 2 +- include/nlohmann/detail/view/lookup.hpp | 37 +- include/nlohmann/detail/view/materialize.hpp | 62 +- include/nlohmann/detail/view/node.hpp | 28 +- include/nlohmann/detail/view/number.hpp | 21 + include/nlohmann/detail/view/serializer.hpp | 20 +- include/nlohmann/json_view.hpp | 228 +- single_include/nlohmann/json_view.hpp | 2184 ++++++++++++++--- tests/src/unit-json_view_edit.cpp | 565 +++++ 43 files changed, 4969 insertions(+), 496 deletions(-) create mode 100644 docs/mkdocs/docs/api/basic_json_document/erase.md create mode 100644 docs/mkdocs/docs/api/basic_json_document/insert.md create mode 100644 docs/mkdocs/docs/api/basic_json_document/push_back.md create mode 100644 docs/mkdocs/docs/api/basic_json_document/set.md create mode 100644 docs/mkdocs/docs/api/json_editable_document.md create mode 100644 docs/mkdocs/docs/api/json_editable_view.md create mode 100644 docs/mkdocs/docs/api/ordered_json_editable_document.md create mode 100644 docs/mkdocs/docs/api/ordered_json_editable_view.md create mode 100644 docs/mkdocs/docs/examples/basic_json_document__erase.cpp create mode 100644 docs/mkdocs/docs/examples/basic_json_document__erase.output create mode 100644 docs/mkdocs/docs/examples/basic_json_document__insert.cpp create mode 100644 docs/mkdocs/docs/examples/basic_json_document__insert.output create mode 100644 docs/mkdocs/docs/examples/basic_json_document__push_back.cpp create mode 100644 docs/mkdocs/docs/examples/basic_json_document__push_back.output create mode 100644 docs/mkdocs/docs/examples/basic_json_document__set.cpp create mode 100644 docs/mkdocs/docs/examples/basic_json_document__set.output create mode 100644 docs/mkdocs/docs/examples/json_editable_document.cpp create mode 100644 docs/mkdocs/docs/examples/json_editable_document.output create mode 100644 docs/mkdocs/docs/examples/ordered_json_editable_document.cpp create mode 100644 docs/mkdocs/docs/examples/ordered_json_editable_document.output 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/.github/labeler.yml b/.github/labeler.yml index 7884e69fb..564f1981c 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -55,8 +55,8 @@ labels: - "tools/amalgamate/config_json_view\\.json" - "docs/mkdocs/docs/features/json_view\\.md" - "docs/mkdocs/docs/api/basic_json_(document|view)/.*" - - "docs/mkdocs/docs/api/(ordered_)?json_(document|view)\\.md" - - "docs/mkdocs/docs/examples/(basic_json_(document|view)__|(ordered_)?json_(document|view)).*" + - "docs/mkdocs/docs/api/(ordered_)?json_(editable_)?(document|view)\\.md" + - "docs/mkdocs/docs/examples/(basic_json_(document|view)__|(ordered_)?json_(editable_)?(document|view)).*" - "tests/benchmarks/src/benchmarks_view\\.cpp" - label: "aspect: json_view" diff --git a/BUILD.bazel b/BUILD.bazel index d186ffbbd..bf7b0d72d 100644 --- a/BUILD.bazel +++ b/BUILD.bazel @@ -70,6 +70,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/docs/docset/docSet.sql b/docs/docset/docSet.sql index 15c24e53d..437c4067b 100644 --- a/docs/docset/docSet.sql +++ b/docs/docset/docSet.sql @@ -135,14 +135,18 @@ INSERT INTO searchIndex(name, type, path) VALUES ('basic_json::~basic_json', 'Me INSERT INTO searchIndex(name, type, path) VALUES ('basic_json_document', 'Class', 'api/basic_json_document/index.html'); INSERT INTO searchIndex(name, type, path) VALUES ('basic_json_document::basic_json_document', 'Constructor', 'api/basic_json_document/basic_json_document/index.html'); INSERT INTO searchIndex(name, type, path) VALUES ('basic_json_document::accept', 'Function', 'api/basic_json_document/accept/index.html'); +INSERT INTO searchIndex(name, type, path) VALUES ('basic_json_document::erase', 'Method', 'api/basic_json_document/erase/index.html'); +INSERT INTO searchIndex(name, type, path) VALUES ('basic_json_document::insert', 'Method', 'api/basic_json_document/insert/index.html'); INSERT INTO searchIndex(name, type, path) VALUES ('basic_json_document::is_discarded', 'Method', 'api/basic_json_document/is_discarded/index.html'); INSERT INTO searchIndex(name, type, path) VALUES ('basic_json_document::memory_usage', 'Method', 'api/basic_json_document/memory_usage/index.html'); INSERT INTO searchIndex(name, type, path) VALUES ('basic_json_document::node_count', 'Method', 'api/basic_json_document/node_count/index.html'); INSERT INTO searchIndex(name, type, path) VALUES ('basic_json_document::owns_source', 'Method', 'api/basic_json_document/owns_source/index.html'); INSERT INTO searchIndex(name, type, path) VALUES ('basic_json_document::parse', 'Function', 'api/basic_json_document/parse/index.html'); INSERT INTO searchIndex(name, type, path) VALUES ('basic_json_document::parse_copy', 'Function', 'api/basic_json_document/parse_copy/index.html'); +INSERT INTO searchIndex(name, type, path) VALUES ('basic_json_document::push_back', 'Method', 'api/basic_json_document/push_back/index.html'); INSERT INTO searchIndex(name, type, path) VALUES ('basic_json_document::read', 'Method', 'api/basic_json_document/read/index.html'); INSERT INTO searchIndex(name, type, path) VALUES ('basic_json_document::root', 'Method', 'api/basic_json_document/root/index.html'); +INSERT INTO searchIndex(name, type, path) VALUES ('basic_json_document::set', 'Method', 'api/basic_json_document/set/index.html'); INSERT INTO searchIndex(name, type, path) VALUES ('basic_json_document::shrink_to_fit', 'Method', 'api/basic_json_document/shrink_to_fit/index.html'); INSERT INTO searchIndex(name, type, path) VALUES ('basic_json_document::source', 'Method', 'api/basic_json_document/source/index.html'); INSERT INTO searchIndex(name, type, path) VALUES ('basic_json_view', 'Class', 'api/basic_json_view/index.html'); @@ -191,6 +195,8 @@ INSERT INTO searchIndex(name, type, path) VALUES ('basic_json_view::type_name', INSERT INTO searchIndex(name, type, path) VALUES ('basic_json_view::value', 'Method', 'api/basic_json_view/value/index.html'); INSERT INTO searchIndex(name, type, path) VALUES ('json', 'Class', 'api/json/index.html'); INSERT INTO searchIndex(name, type, path) VALUES ('json_document', 'Class', 'api/json_document/index.html'); +INSERT INTO searchIndex(name, type, path) VALUES ('json_editable_document', 'Class', 'api/json_editable_document/index.html'); +INSERT INTO searchIndex(name, type, path) VALUES ('json_editable_view', 'Class', 'api/json_editable_view/index.html'); INSERT INTO searchIndex(name, type, path) VALUES ('json_view', 'Class', 'api/json_view/index.html'); INSERT INTO searchIndex(name, type, path) VALUES ('json_pointer', 'Class', 'api/json_pointer/index.html'); INSERT INTO searchIndex(name, type, path) VALUES ('json_pointer::back', 'Method', 'api/json_pointer/back/index.html'); @@ -230,6 +236,8 @@ INSERT INTO searchIndex(name, type, path) VALUES ('operator<<', 'Operator', 'api INSERT INTO searchIndex(name, type, path) VALUES ('operator>>', 'Operator', 'api/operator_gtgt/index.html'); INSERT INTO searchIndex(name, type, path) VALUES ('ordered_json', 'Class', 'api/ordered_json/index.html'); INSERT INTO searchIndex(name, type, path) VALUES ('ordered_json_document', 'Class', 'api/ordered_json_document/index.html'); +INSERT INTO searchIndex(name, type, path) VALUES ('ordered_json_editable_document', 'Class', 'api/ordered_json_editable_document/index.html'); +INSERT INTO searchIndex(name, type, path) VALUES ('ordered_json_editable_view', 'Class', 'api/ordered_json_editable_view/index.html'); INSERT INTO searchIndex(name, type, path) VALUES ('ordered_json_view', 'Class', 'api/ordered_json_view/index.html'); INSERT INTO searchIndex(name, type, path) VALUES ('ordered_map', 'Class', 'api/ordered_map/index.html'); INSERT INTO searchIndex(name, type, path) VALUES ('std::formatter', 'Class', 'api/basic_json/std_formatter/index.html'); diff --git a/docs/mkdocs/docs/api/basic_json_document/erase.md b/docs/mkdocs/docs/api/basic_json_document/erase.md new file mode 100644 index 000000000..cbdc79765 --- /dev/null +++ b/docs/mkdocs/docs/api/basic_json_document/erase.md @@ -0,0 +1,128 @@ +# nlohmann::basic_json_document::erase + +```cpp +// (1) +std::size_t erase(view_type object, string_view_t key); + +// (2) +template +void erase(view_type array, I idx); + +// (3) +std::size_t erase(const json_pointer& ptr); +``` + +Only an **editable** document (`#!cpp Editable == true`, e.g. [`json_editable_document`](../json_editable_document.md)) +has `erase`; calling it on a read-only `basic_json_document` fails to compile (`#!cpp static_assert`). + +1. Removes every member of `object` whose key is `key` (see [Notes](#notes) on duplicate keys) and returns how many + were removed; `#!cpp 0` if `object` has no member with this key. +2. Removes the element at index `idx` of `array`, which must already exist (`#!cpp idx < array.size()`). +3. Removes the value the JSON pointer `ptr` refers to, relative to [`root()`](root.md), and returns how many values + were removed: the *parent* of the target must already exist, and the target itself is removed as in 1. (an object + member; `#!cpp 0` or more) or 2. (an array element; always `#!cpp 1`). `ptr` must not be empty -- [`root()`](root.md) + itself cannot be erased. + +## Template parameters + +`I` +: an integral type other than `#!cpp bool`, deduced (overloads taking a `#!cpp bool` or a non-integral type for + `idx` do not participate in overload resolution). + +## Parameters + +`object` (in) +: the object to remove a member of + +`array` (in) +: the array to remove an element of + +`key` (in) +: the key of the member(s) to remove + +`idx` (in) +: the index of the element to remove; a negative value throws (see [Exceptions](#exceptions)) + +`ptr` (in) +: a JSON pointer to the value to remove, relative to `root()` + +## Return value + +1. the number of removed members (`#!cpp 0` if `object` had none with this `key`) +2. (nothing) +3. the number of removed values (`#!cpp 0` or more for an object member, always `#!cpp 1` for an array element) + +## Exceptions + +1. Throws [`type_error.307`](../../home/exceptions.md#jsonexceptiontype_error307) if `object` is not an object -- the + same message [`BasicJsonType::erase`](../basic_json/erase.md) throws for the same type. +2. Throws `type_error.307` if `array` is not an array. Throws + [`out_of_range.401`](../../home/exceptions.md#jsonexceptionout_of_range401) if `idx` is negative, or if + `#!cpp idx >= array.size()`. +3. Throws [`out_of_range.405`](../../home/exceptions.md#jsonexceptionout_of_range405) ("JSON pointer has no parent") + if `ptr` is empty. Throws what [`at`](../basic_json_view/at.md) throws (overload 3) for resolving `ptr`'s parent. + For the last reference token itself: if the parent is an array, throws what 2. throws for an index that is out of + range, or, for a token that is not a valid array index, + [`parse_error.106`](../../home/exceptions.md#jsonexceptionparse_error106) (a leading `#!cpp '0'`), + [`parse_error.109`](../../home/exceptions.md#jsonexceptionparse_error109) (not a number), + [`out_of_range.410`](../../home/exceptions.md#jsonexceptionout_of_range410) (too large for `size_type`), or + [`out_of_range.404`](../../home/exceptions.md#jsonexceptionout_of_range404) (an empty token); otherwise (an + object, or a primitive value the pointer's parent resolves to) throws what 1. throws. + +Every overload also throws [`invalid_iterator.202`](../../home/exceptions.md#jsonexceptioninvalid_iterator202) ("view +does not belong to this document") if `object`/`array` is a [discarded](../basic_json_view/is_discarded.md) view or a +view of a *different* document (overloads 1-2 only; overload 3 always starts from this document's own +[`root()`](root.md)). + +## Complexity + +1. Linear in the number of members of `object`. +2. Linear in the number of elements of `array` at or after `idx` (they move one slot over). +3. Linear in the number of reference tokens of `ptr` and, for each token, in the number of members of the object at + that level or the index into the array (as [`at`](../basic_json_view/at.md)), plus the complexity of 1. or 2. for + the last token. + +## Notes + +!!! info "Duplicate keys" + + Overload 1. removes *every* member with `key`, not just the first -- unlike [`set`](set.md), which assigns the + first occurrence and drops the rest. This is why it returns a count rather than a single view: there may be + more than one member removed, or none. + +Like [`set`](set.md) and [`push_back`](push_back.md), `erase` never moves an element's *value*: a view still +referring to a removed member or element keeps showing what it last held (see [Edits](index.md#edits)) -- it just no +longer appears when `array`/`object` is read, dumped, or iterated. Removing an element of `array` (2.) does shift the +*links* to the elements after it, the same way `insert`, `set`, or `push_back` on the same array would; any iterator +already taken over `array`/`object` is invalidated by an erase, since it was walking the old layout. + +## Examples + +??? example + + The example below drops a deprecated field and a decommissioned entry from a configuration document -- using all + three overloads -- and shows what stays intact that would not with a plain `json`/`ordered_json` value: the order + of the fields around the ones removed, and the exact spelling of a number that was never touched. + + ```cpp + --8<-- "examples/basic_json_document__erase.cpp" + ``` + + Output: + + ```json + --8<-- "examples/basic_json_document__erase.output" + ``` + +## See also + +- [insert](insert.md) - insert an element into an array +- [set](set.md) - replace a value, or set an object member, an array element, or the value a JSON pointer refers to +- [push_back](push_back.md) - append to an array +- [root](root.md) - the view of the root value, the starting point of overload 3 +- [`BasicJsonType::erase`](../basic_json/erase.md) - the corresponding function of `basic_json` +- [Edits](index.md#edits) - what an edit guarantees, for every overload + +## Version history + +- Added in version 3.13.0. diff --git a/docs/mkdocs/docs/api/basic_json_document/index.md b/docs/mkdocs/docs/api/basic_json_document/index.md index 89564369a..534b63755 100644 --- a/docs/mkdocs/docs/api/basic_json_document/index.md +++ b/docs/mkdocs/docs/api/basic_json_document/index.md @@ -3,7 +3,7 @@ Defined in header `` ```cpp -template +template class basic_json_document; ``` @@ -19,6 +19,11 @@ it (a copy, or an rvalue `#!cpp std::string` that was moved in); see [`owns_sour is move-only: copying a document would either duplicate a potentially large index and text, or leave two documents claiming to borrow the same buffer, so it is disabled. +With `#!cpp Editable == true`, the document also offers [`set`](set.md), [`push_back`](push_back.md), +[`insert`](insert.md), and [`erase`](erase.md) to change values in place, see [Edits](#edits) below. The source text +itself is never written; a read-only document (`#!cpp Editable == false`, the default) does not carry any of the +bookkeeping edits need, and calling any of them on one fails to compile (`#!cpp static_assert`). + ## Template parameters `BasicJsonType` @@ -26,14 +31,22 @@ claiming to borrow the same buffer, so it is disabled. [`ordered_json`](../ordered_json.md). Only 64-bit `number_integer_t`/`number_unsigned_t` types are supported; this is checked with a `static_assert`. +`Editable` +: whether the document supports [`set`](set.md), [`push_back`](push_back.md), [`insert`](insert.md), and + [`erase`](erase.md) (optional, `#!cpp false` by default). See [Edits](#edits) below. + ## Specializations -- [**json_document**](../json_document.md) - documents of the default specialization [`json`](../json.md) -- [**ordered_json_document**](../ordered_json_document.md) - documents of [`ordered_json`](../ordered_json.md) +- [**json_document**](../json_document.md) - read-only documents of the default specialization [`json`](../json.md) +- [**ordered_json_document**](../ordered_json_document.md) - read-only documents of + [`ordered_json`](../ordered_json.md) +- [**json_editable_document**](../json_editable_document.md) - editable documents of [`json`](../json.md) +- [**ordered_json_editable_document**](../ordered_json_editable_document.md) - editable documents of + [`ordered_json`](../ordered_json.md) ## Member types -- **view_type** - the type of view returned by [`root()`](root.md) (`#!cpp basic_json_view`) +- **view_type** - the type of view returned by [`root()`](root.md) (`#!cpp basic_json_view`) - **value_t** - the JSON type enumeration, see [`basic_json::value_t`](../basic_json/value_t.md) ## Member functions @@ -50,6 +63,41 @@ claiming to borrow the same buffer, so it is disabled. - [**node_count**](node_count.md) - the number of index entries (values plus object keys) - [**memory_usage**](memory_usage.md) - the number of bytes held by the document - [**shrink_to_fit**](shrink_to_fit.md) - release unused index capacity +- [**set**](set.md) - replace a value, or set an object member, an array element, or the value a JSON pointer refers + to (`#!cpp Editable` documents only) +- [**push_back**](push_back.md) - append to an array (`#!cpp Editable` documents only) +- [**insert**](insert.md) - insert an element into an array before a given position (`#!cpp Editable` documents only) +- [**erase**](erase.md) - remove an object member, an array element, or the value a JSON pointer refers to + (`#!cpp Editable` documents only) + +## Edits + +An editable document (`#!cpp Editable == true`) can be changed after parsing, with [`set`](set.md), +[`push_back`](push_back.md), [`insert`](insert.md), and [`erase`](erase.md); +[`json_editable_document`](../json_editable_document.md) and +[`ordered_json_editable_document`](../ordered_json_editable_document.md) are the corresponding specializations. A few +points apply to every edit: + +- **The source text is never written**, and the parsed index never moves: every value keeps the node it was parsed + into, so [views](../basic_json_view/index.md) taken before an edit stay valid, including + [`root()`](root.md). New values (and the element sequences of an edited array/object) go to storage owned by the + document, allocated on demand. +- **A view keeps referring to the same value.** After [`set`](set.md) replaces the value a view refers to, that view + sees the new value; a view of a value that a later edit replaces or drops keeps showing what it last held. An edit + of an array or object, however, **invalidates the iterators taken over it** (its members may now live in a + different sequence), and a string obtained with [`get_string()`](../basic_json_view/get_string.md) stays valid even + as further edits happen (earlier buffers of edited text are kept alive, not overwritten). +- **Values are accepted three ways:** a [`basic_json_view`](../basic_json_view/index.md) of *any* document + (read-only or editable; it is copied, nothing is shared with the source document), a `BasicJsonType` value, or + anything `BasicJsonType` can be constructed from (numbers, strings, `#!cpp bool`, `#!cpp nullptr`, containers, ...). +- [`dump()`](../basic_json_view/dump.md) writes an edited document with members in document order, new members at + the end, and, with [`number_format::source`](../basic_json_view/number_format.md), keeps the spelling of every + number that was not itself edited -- see [Editing a document](../../features/json_view.md#editing-a-document) for + why this matters. +- [`read()`](read.md) discards all edits, [`shrink_to_fit()`](shrink_to_fit.md) does not move the node index once + there are edits, and [`memory_usage()`](memory_usage.md) includes the memory edits use. + [`source_offset()`](../basic_json_view/source_offset.md) of a value introduced by an edit is + `#!cpp static_cast(-1)`, the same value it reports for a decoded string. ## Version history diff --git a/docs/mkdocs/docs/api/basic_json_document/insert.md b/docs/mkdocs/docs/api/basic_json_document/insert.md new file mode 100644 index 000000000..b513dfc0d --- /dev/null +++ b/docs/mkdocs/docs/api/basic_json_document/insert.md @@ -0,0 +1,114 @@ +# nlohmann::basic_json_document::insert + +```cpp +template +view_type insert(view_type array, I idx, V&& value); +``` + +Only an **editable** document (`#!cpp Editable == true`, e.g. [`json_editable_document`](../json_editable_document.md)) +has `insert`; calling it on a read-only `basic_json_document` fails to compile (`#!cpp static_assert`). + +Inserts `value` into `array` as a new element before position `idx`, which must not be past the end +(`#!cpp idx <= array.size()`; `#!cpp idx == array.size()` appends, like [`push_back`](push_back.md)). Unlike +[`push_back`](push_back.md), a [null](../basic_json_view/is_null.md) `array` does *not* first become an empty array: +`array` must already be an array. + +`value` is accepted three ways: a [`basic_json_view`](../basic_json_view/index.md) of *any* document -- read-only or +editable, and it does not have to be `array`'s own document -- which is copied so that nothing is shared with the +source document afterward; a `BasicJsonType` value; or anything `BasicJsonType` can be constructed from (numbers, +strings, `#!cpp bool`, `#!cpp nullptr`, containers, ...). + +## Template parameters + +`I` +: an integral type other than `#!cpp bool`, deduced (overloads taking a `#!cpp bool` or a non-integral type for + `idx` do not participate in overload resolution). + +`V` +: the type of `value`, deduced; see above for what is accepted. + +## Parameters + +`array` (in) +: the array to insert into + +`idx` (in) +: the position to insert `value` before; a negative value throws (see [Exceptions](#exceptions)) + +`value` (in) +: the value to insert + +## Return value + +a view of the new element, now holding `value` + +## Exception safety + +Basic exception safety: `value` is fully encoded -- including the checks below -- into storage owned by the document +before anything already reachable from [`root()`](root.md) is touched, so a failure while encoding `value` (an +invalid argument, or `#!cpp std::bad_alloc`) leaves the document completely unchanged, other than memory allocated +for the encoding that is not reclaimed. A failure of a later allocation -- while `array` switches from its parsed +layout to a growable block, or while that block grows, see [Notes](#notes) -- can still leave `array` already +switched to that layout even though `value` itself was not inserted. + +## Exceptions + +Throws [`type_error.309`](../../home/exceptions.md#jsonexceptiontype_error309) if `array` is not an array -- the same +message [`BasicJsonType::insert`](../basic_json/insert.md) throws for the same type; a null `array` throws this too +(see above). Throws [`out_of_range.401`](../../home/exceptions.md#jsonexceptionout_of_range401) if `idx` is negative, +or if `#!cpp idx > array.size()`. Throws +[`invalid_iterator.202`](../../home/exceptions.md#jsonexceptioninvalid_iterator202) ("view does not belong to this +document") if `array` is a [discarded](../basic_json_view/is_discarded.md) view or a view of a *different* document. +Throws [`type_error.302`](../../home/exceptions.md#jsonexceptiontype_error302) if `value` is a +[discarded](../basic_json_view/is_discarded.md) view or a [discarded](../basic_json/is_discarded.md) `BasicJsonType` +value, and [`type_error.319`](../../home/exceptions.md#jsonexceptiontype_error319) if `value` is (or contains) a +binary value -- `BasicJsonType` can hold one, but a `json_document` cannot. Throws +[`type_error.316`](../../home/exceptions.md#jsonexceptiontype_error316) if `value` is (or contains) a string that is +not valid UTF-8, with the same message [`BasicJsonType::dump()`](../basic_json/dump.md) gives for that string. + +## Complexity + +Linear in the number of elements of `array` at or after `idx` (they move one slot over), plus time linear in the +size of `value` to encode it into the document's storage (constant for a scalar, linear in the number of nested +values for an array or object): like [`push_back`](push_back.md), the elements of `array` move to a growable block +of links the first time it is inserted into (or [`set`](set.md)/[`push_back`](push_back.md) on), and that block +grows in amortized constant time; inserting before the end within that block still shifts every later element. + +## Notes + +Like [`set`](set.md) on a member or an element, `insert` never moves an existing *element's value* -- only where +`array`'s *links* to its elements live -- so a view of an existing element of `array` stays valid across an +`insert`, and keeps referring to the same element even though its index shifts. Any iterator already taken over +`array` is invalidated, since it was walking the old layout. See [Edits](index.md#edits) for what stays valid across +an edit in general. + +## Examples + +??? example + + The example below inserts a step into the middle of a deployment plan, without touching the steps that come + after it, and shows that a view taken before the insert keeps referring to the same element even though its + index shifts -- something a plain `json`/`ordered_json` array, or its `std::vector`-based storage, has no + equivalent for. + + ```cpp + --8<-- "examples/basic_json_document__insert.cpp" + ``` + + Output: + + ```json + --8<-- "examples/basic_json_document__insert.output" + ``` + +## See also + +- [push_back](push_back.md) - append to an array +- [erase](erase.md) - remove an object member, an array element, or the value a JSON pointer refers to +- [set](set.md) - replace a value, or set an object member, an array element, or the value a JSON pointer refers to +- [`BasicJsonType::insert`](../basic_json/insert.md) - the corresponding function of `basic_json` +- [Edits](index.md#edits) - what an edit guarantees, for every overload + +## Version history + +- Added in version 3.13.0. diff --git a/docs/mkdocs/docs/api/basic_json_document/push_back.md b/docs/mkdocs/docs/api/basic_json_document/push_back.md new file mode 100644 index 000000000..37ce10bc5 --- /dev/null +++ b/docs/mkdocs/docs/api/basic_json_document/push_back.md @@ -0,0 +1,102 @@ +# nlohmann::basic_json_document::push_back + +```cpp +template +view_type push_back(view_type array, V&& value); +``` + +Appends `value` as a new last element of `array`. A [null](../basic_json_view/is_null.md) `array` first becomes an +empty array, the same way [`set`](set.md) turns a null `object` into an empty object. + +`value` is accepted three ways: a [`basic_json_view`](../basic_json_view/index.md) of *any* document -- read-only or +editable, and it does not have to be `array`'s own document -- which is copied so that nothing is shared with the +source document afterward; a `BasicJsonType` value; or anything `BasicJsonType` can be constructed from (numbers, +strings, `#!cpp bool`, `#!cpp nullptr`, containers, ...). + +Only an **editable** document (`#!cpp Editable == true`, e.g. [`json_editable_document`](../json_editable_document.md)) +has `push_back`; calling it on a read-only `basic_json_document` fails to compile (`#!cpp static_assert`). + +## Template parameters + +`V` +: the type of `value`, deduced; see above for what is accepted. + +## Parameters + +`array` (in) +: the array (or null value) to append to + +`value` (in) +: the value to append + +## Return value + +a view of the new last element of `array`, now holding `value` + +## Exception safety + +Basic exception safety: `value` is fully encoded -- including the checks below -- into storage owned by the document +before anything already reachable from [`root()`](root.md) is touched, so a failure while encoding `value` (an +invalid argument, or `#!cpp std::bad_alloc`) leaves the document completely unchanged, other than memory allocated +for the encoding that is not reclaimed. A failure of a later allocation -- while `array` switches from its parsed +layout to a growable block, or while that block grows, see [Notes](#notes) -- can still leave a partial effect, such +as a null `array` argument already turned into an empty array even though `value` itself was not appended. + +## Exceptions + +Throws [`type_error.308`](../../home/exceptions.md#jsonexceptiontype_error308) if `array` is neither an array nor +null -- the same message [`BasicJsonType::push_back`](../basic_json/push_back.md) throws for the same type. Throws +[`invalid_iterator.202`](../../home/exceptions.md#jsonexceptioninvalid_iterator202) ("view does not belong to this +document") if `array` is a [discarded](../basic_json_view/is_discarded.md) view or a view of a *different* document. +Throws [`type_error.302`](../../home/exceptions.md#jsonexceptiontype_error302) if `value` is a +[discarded](../basic_json_view/is_discarded.md) view or a [discarded](../basic_json/is_discarded.md) `BasicJsonType` +value, and [`type_error.319`](../../home/exceptions.md#jsonexceptiontype_error319) if `value` is (or contains) a +binary value -- `BasicJsonType` can hold one, but a `json_document` cannot. Throws +[`type_error.316`](../../home/exceptions.md#jsonexceptiontype_error316) if `value` is (or contains) a string that is +not valid UTF-8, with the same message [`BasicJsonType::dump()`](../basic_json/dump.md) gives for that string. + +## Complexity + +Amortized constant, plus time linear in the size of `value` to encode it into the document's storage (constant for +a scalar, linear in the number of nested values for an array or object): the elements of `array` move to a growable +block of links the first time it is appended to (or [`set`](set.md) on), and that block itself grows -- doubling its +capacity, so the cost of growing it amortizes to constant per element -- only once it runs out of room. See +[Notes](#notes). + +## Notes + +Like [`set`](set.md) on a member or an element, `push_back` never moves an existing element itself -- only where +`array`'s *links* to its elements live -- so a view of an existing element of `array` stays valid across a +`push_back`, but any iterator already taken over `array` is invalidated, since it was walking the old layout. See +[Edits](index.md#edits) for what stays valid across an edit in general. + +## Examples + +??? example + + The example below appends records to an array one at a time, as they might arrive from a stream of events, + without ever building a `BasicJsonType` value for the array or for the records already in it, and shows that a + view taken from an earlier `push_back` still refers to the same element once later ones have run. + + ```cpp + --8<-- "examples/basic_json_document__push_back.cpp" + ``` + + Output: + + ```json + --8<-- "examples/basic_json_document__push_back.output" + ``` + +## See also + +- [set](set.md) - replace a value, or set an object member, an array element, or the value a JSON pointer refers to +- [insert](insert.md) - insert an element into an array before a given position +- [erase](erase.md) - remove an object member, an array element, or the value a JSON pointer refers to +- [root](root.md) - the view of the root value +- [`BasicJsonType::push_back`](../basic_json/push_back.md) - the corresponding function of `basic_json` +- [Edits](index.md#edits) - what an edit guarantees, for every overload + +## Version history + +- Added in version 3.13.0. diff --git a/docs/mkdocs/docs/api/basic_json_document/set.md b/docs/mkdocs/docs/api/basic_json_document/set.md new file mode 100644 index 000000000..f9d73b94c --- /dev/null +++ b/docs/mkdocs/docs/api/basic_json_document/set.md @@ -0,0 +1,194 @@ +# nlohmann::basic_json_document::set + +```cpp +// (1) +template +view_type set(view_type target, V&& value); + +// (2) +template +view_type set(view_type object, string_view_t key, V&& value); + +// (3) +template +view_type set(view_type array, I idx, V&& value); + +// (4) +template +view_type set(const json_pointer& ptr, V&& value); +``` + +Only an **editable** document (`#!cpp Editable == true`, e.g. [`json_editable_document`](../json_editable_document.md)) +has `set`; calling it on a read-only `basic_json_document` fails to compile (`#!cpp static_assert`). + +1. Replaces the value `target` refers to with `value`. +2. Sets the member `key` of the object `object` to `value`: assigns it if `object` already has a member with this + key -- the first one, should the key occur more than once, and the later duplicates are then dropped (see the + [Notes](#notes) below) -- or appends a new member at the end otherwise. A [null](../basic_json_view/is_null.md) + `object` first becomes an empty object. +3. Assigns `value` to the element at index `idx` of the array `array`, which must already exist (`#!cpp idx < + array.size()`). +4. Sets the value the JSON pointer `ptr` refers to, relative to [`root()`](root.md), to `value`. The *parent* of the + target must already exist: an object member is set as in 2. (added if it does not exist yet), an array element is + assigned as in 3., and a last reference token of `#!cpp "-"`, or equal to the size of the array, appends `value` + instead, exactly as [`push_back`](push_back.md) would. An empty `ptr` sets [`root()`](root.md) itself, as in 1. + +In every overload, `value` is accepted three ways: a [`basic_json_view`](../basic_json_view/index.md) of *any* +document -- read-only or editable, and it does not have to be `target`'s/`object`'s/`array`'s own document -- which +is copied so that nothing is shared with the source document afterward; a `BasicJsonType` value; or anything +`BasicJsonType` can be constructed from (numbers, strings, `#!cpp bool`, `#!cpp nullptr`, containers, ...). + +## Template parameters + +`V` +: the type of `value`, deduced; see above for what is accepted. + +`I` +: an integral type other than `#!cpp bool`, deduced (overloads taking a `#!cpp bool` or a non-integral type for + `idx` do not participate in overload resolution). + +## Parameters + +`target` (in) +: the value to replace + +`object` (in) +: the object (or null value) whose member to set + +`array` (in) +: the array whose element to assign + +`key` (in) +: the key of the member to set + +`idx` (in) +: the index of the element to assign; a negative value throws (see [Exceptions](#exceptions)) + +`ptr` (in) +: a JSON pointer to the value to set, relative to `root()` + +`value` (in) +: the new value + +## Return value + +1. a view of `target`, now holding `value` +2. a view of the member `key` of `object`, now holding `value` +3. a view of the element `idx` of `array`, now holding `value` +4. a view of the value `ptr` refers to, now holding `value` + +## Exception safety + +Basic exception safety: `value` is fully encoded -- including the checks below -- into storage owned by the document +before anything already reachable from [`root()`](root.md) is touched, so a failure while encoding `value` (an +invalid argument, or `#!cpp std::bad_alloc`) leaves the document completely unchanged, other than memory allocated +for the encoding that is not reclaimed. A failure of a later allocation -- while an edited array or object switches +from its parsed layout to a growable block, see [Notes](#notes) -- can still leave a partial effect, such as a +[null](../basic_json_view/is_null.md) `object`/`array` argument already turned into an empty object/array even +though `value` itself was not linked in. + +## Exceptions + +1. Throws [`type_error.302`](../../home/exceptions.md#jsonexceptiontype_error302) if `value` is a + [discarded](../basic_json_view/is_discarded.md) view, or a [discarded](../basic_json/is_discarded.md) + `BasicJsonType` value (e.g. `#!cpp BasicJsonType(value_t::discarded)`) -- an object or array `value`, of either + kind, is fine and is encoded as a whole subtree. +2. Throws [`type_error.305`](../../home/exceptions.md#jsonexceptiontype_error305) if `object` is neither an object + nor null -- the same message [`operator[]`](../basic_json_view/operator%5B%5D.md) throws for a string argument on + such a value. Throws [`type_error.316`](../../home/exceptions.md#jsonexceptiontype_error316) if `key` is not + valid UTF-8, with the same message [`BasicJsonType::dump()`](../basic_json/dump.md) gives for that string. + Also throws what 1. throws for `value`. +3. Throws `type_error.305` if `array` is not an array -- the same message `operator[]` throws for a numeric argument + on such a value. Throws [`out_of_range.401`](../../home/exceptions.md#jsonexceptionout_of_range401) if `idx` is + negative, or if `#!cpp idx >= array.size()`. Also throws what 1. throws for `value`. +4. Throws what [`at`](../basic_json_view/at.md) throws (overload 3) for resolving `ptr`'s parent, except that a + missing object member or an array index equal to the array's size at the very last reference token is not an + error there (it becomes a new member or an appended element) instead of + [`out_of_range.403`](../../home/exceptions.md#jsonexceptionout_of_range403)/[`out_of_range.402`](../../home/exceptions.md#jsonexceptionout_of_range402). + For the last reference token itself: if the parent is an object (or a primitive value, where it throws + `type_error.305`), throws what 2. throws; if the parent is an array, throws what 3. throws for an index that is + out of range, or, for a token that is not a valid array index, + [`parse_error.106`](../../home/exceptions.md#jsonexceptionparse_error106) (a leading `#!cpp '0'`), + [`parse_error.109`](../../home/exceptions.md#jsonexceptionparse_error109) (not a number), + [`out_of_range.410`](../../home/exceptions.md#jsonexceptionout_of_range410) (too large for `size_type`), or + [`out_of_range.404`](../../home/exceptions.md#jsonexceptionout_of_range404) (an empty token). Also throws what 1. + throws for `value`. + +Every overload also throws [`type_error.319`](../../home/exceptions.md#jsonexceptiontype_error319) if `value` is (or +contains) a binary value -- `BasicJsonType` can hold one, but a `json_document` cannot -- and +[`invalid_iterator.202`](../../home/exceptions.md#jsonexceptioninvalid_iterator202) ("view does not belong to this +document") if `target`/`object`/`array` is a [discarded](../basic_json_view/is_discarded.md) view or a view of a +*different* document (overloads 1-3 only; overload 4 always starts from this document's own [`root()`](root.md)). + +## Complexity + +1. Linear in the size of `value` (encoding it into the document's storage): constant for a scalar, linear in the + number of nested values for an array or object. If `target` is itself an array or object that spans more than one + node in its parent's original, unedited layout, and `value` is a scalar, replacing it additionally costs time + linear in the number of elements of that parent, the *first* time -- see [Notes](#notes). +2. Linear in the number of members of `object`, to find an existing member with `key`, plus the complexity of 1. for + `value`. +3. Constant, plus the complexity of 1. for `value`. +4. Linear in the number of reference tokens of `ptr` and, for each token, in the number of members of the object at + that level or the index into the array (as [`at`](../basic_json_view/at.md)), plus the complexity of 2. or 3. for + the last token. + +## Notes + +!!! info "Duplicate keys" + + If `object` already has more than one member with `key` (2.), the *first* one is assigned `value` and every + later member with the same key is removed -- so that a lookup, an iteration, and + [`materialize()`](../basic_json_view/materialize.md) of `object` afterward all agree on a single value for + `key`, the same way [`operator[]`](../basic_json_view/operator%5B%5D.md) already picks the first occurrence of a + duplicate key for reading. See the [Notes on duplicate keys](../basic_json_view/operator%5B%5D.md#notes) of + `operator[]`. + +Setting a member (2.) or an element (3., through 4.) of an array or object whose elements have not been edited +before switches it from its parsed layout to a growable block holding links to its elements; a later +[`push_back`](push_back.md) or `set` on the same container reuses that block, growing it (amortized constant time) +only once it runs out of room. This never moves an element itself -- only where the container's *links* to its +elements live -- so a view of an element stays valid, but any iterator already taken over the container is +invalidated, since it was walking the old layout. See [Edits](index.md#edits) for what stays valid across an edit in +general. + +The same switch happens, for the same reason, when overload 1. replaces a multi-node array/object value with a +scalar: the *parent's* element sequence is what has to switch to links, not `target` itself, because the parent +originally stepped over `target`'s whole subtree by its node count, which no longer applies once `target` is a +one-node scalar. + +## Examples + +??? example "Example: (1)/(2)/(3)/(4) replace a value, set a member, assign an element, set via a JSON pointer" + + The example below edits a small configuration document -- replacing a value, adding an object member, assigning + an array element, and reaching a field through a JSON pointer -- and shows what + [`dump()`](../basic_json_view/dump.md) preserves that is lost once the same edits are made on a `BasicJsonType` + value instead: the order object members were written in, and the exact spelling of a number that was never + touched. + + ```cpp + --8<-- "examples/basic_json_document__set.cpp" + ``` + + Output: + + ```json + --8<-- "examples/basic_json_document__set.output" + ``` + +## See also + +- [push_back](push_back.md) - append to an array +- [insert](insert.md) - insert an element into an array +- [erase](erase.md) - remove an object member, an array element, or the value a JSON pointer refers to +- [root](root.md) - the view of the root value, the starting point of overload 4 +- [`basic_json_view::dump`](../basic_json_view/dump.md) - serialize the document, keeping an untouched number's + spelling with `#!cpp number_format::source` +- [Edits](index.md#edits) - what an edit guarantees, for every overload +- [Editing a document](../../features/json_view.md#editing-a-document) - why editable documents keep the source + text's order and number spelling + +## Version history + +- Added in version 3.13.0. diff --git a/docs/mkdocs/docs/api/basic_json_view/index.md b/docs/mkdocs/docs/api/basic_json_view/index.md index 56760db2f..98f8ce9eb 100644 --- a/docs/mkdocs/docs/api/basic_json_view/index.md +++ b/docs/mkdocs/docs/api/basic_json_view/index.md @@ -3,7 +3,7 @@ Defined in header `` ```cpp -template +template class basic_json_view; ``` @@ -27,16 +27,30 @@ subtree on demand. [`operator[]`](operator%5B%5D.md), [`at`](at.md), [`contains` [`operator!=`](operator_ne.md) compare two views, or a view and a `BasicJsonType` value, without ever building a `BasicJsonType` value for a view; no ordering comparison (`#!cpp operator<`) is provided. +`basic_json_view` itself is always read-only -- it never has a `set` or `push_back` of its own. A view of an +**editable** document (`#!cpp Editable == true`) sees every edit made through +[`basic_json_document::set`](../basic_json_document/set.md) and +[`basic_json_document::push_back`](../basic_json_document/push_back.md): once a value is changed, every view that +still refers to it -- including ones taken before the change -- reads the new value. See +[Edits](../basic_json_document/index.md#edits). + ## Template parameters `BasicJsonType` : a specialization of [`basic_json`](../basic_json/index.md), matching the [`basic_json_document`](../basic_json_document/index.md) the view was taken from. +`Editable` +: whether the view is of an editable document, matching the [`basic_json_document`](../basic_json_document/index.md) + it was taken from (optional, `#!cpp false` by default). See [Edits](../basic_json_document/index.md#edits). + ## Specializations - [**json_view**](../json_view.md) - views of a [`json_document`](../json_document.md) - [**ordered_json_view**](../ordered_json_view.md) - views of an [`ordered_json_document`](../ordered_json_document.md) +- [**json_editable_view**](../json_editable_view.md) - views of a [`json_editable_document`](../json_editable_document.md) +- [**ordered_json_editable_view**](../ordered_json_editable_view.md) - views of an + [`ordered_json_editable_document`](../ordered_json_editable_document.md) ## Member types diff --git a/docs/mkdocs/docs/api/json_editable_document.md b/docs/mkdocs/docs/api/json_editable_document.md new file mode 100644 index 000000000..ab3fc3868 --- /dev/null +++ b/docs/mkdocs/docs/api/json_editable_document.md @@ -0,0 +1,43 @@ +# nlohmann::json_editable_document + +Defined in header `` + +```cpp +using json_editable_document = basic_json_document; +``` + +This type is an **editable** [`basic_json_document`](basic_json_document/index.md) of the default +[`json`](json.md) specialization: in addition to everything [`json_document`](json_document.md) offers, +[`set`](basic_json_document/set.md) and [`push_back`](basic_json_document/push_back.md) change values after +parsing, without ever rewriting the source text -- see [Edits](basic_json_document/index.md#edits) and +[Editing a document](../features/json_view.md#editing-a-document). + +## Examples + +??? example + + The example below patches two fields of a small configuration document -- changing one and adding another -- + and dumps it back out with the member order and the spelling of the untouched number preserved, something a + plain [`json`](json.md) value cannot do. + + ```cpp + --8<-- "examples/json_editable_document.cpp" + ``` + + Output: + + ```json + --8<-- "examples/json_editable_document.output" + ``` + +## See also + +- [json_editable_view](json_editable_view.md) - a view of a value of a `json_editable_document` +- [json_document](json_document.md) - the read-only document this type adds edits to +- [ordered_json_editable_document](ordered_json_editable_document.md) - the corresponding editable document for + `ordered_json` +- [Edits](basic_json_document/index.md#edits) - what an edit guarantees + +## Version history + +Since version 3.13.0. diff --git a/docs/mkdocs/docs/api/json_editable_view.md b/docs/mkdocs/docs/api/json_editable_view.md new file mode 100644 index 000000000..f5fa59e57 --- /dev/null +++ b/docs/mkdocs/docs/api/json_editable_view.md @@ -0,0 +1,41 @@ +# nlohmann::json_editable_view + +Defined in header `` + +```cpp +using json_editable_view = basic_json_view; +``` + +This type is a [`basic_json_view`](basic_json_view/index.md) of a value of a +[`json_editable_document`](json_editable_document.md). It offers the same read-only interface as +[`json_view`](json_view.md); what is different is what it can be a view *of* -- a value that +[`set`](basic_json_document/set.md) and [`push_back`](basic_json_document/push_back.md) can change, with every view +still referring to it seeing the change, see [Edits](basic_json_document/index.md#edits). + +## Examples + +??? example + + The example below is the same as [`json_editable_document`'s](json_editable_document.md): every view read back + out of the document -- `#!cpp doc.root()` and the views nested under it -- sees the edits made through `set`. + + ```cpp + --8<-- "examples/json_editable_document.cpp" + ``` + + Output: + + ```json + --8<-- "examples/json_editable_document.output" + ``` + +## See also + +- [json_editable_document](json_editable_document.md) - the document type this view refers into +- [json_view](json_view.md) - the corresponding read-only view +- [ordered_json_editable_view](ordered_json_editable_view.md) - the corresponding view for + `ordered_json_editable_document` + +## Version history + +Since version 3.13.0. diff --git a/docs/mkdocs/docs/api/ordered_json_editable_document.md b/docs/mkdocs/docs/api/ordered_json_editable_document.md new file mode 100644 index 000000000..3127eea70 --- /dev/null +++ b/docs/mkdocs/docs/api/ordered_json_editable_document.md @@ -0,0 +1,47 @@ +# nlohmann::ordered_json_editable_document + +Defined in header `` + +```cpp +using ordered_json_editable_document = basic_json_document; +``` + +This type is an **editable** [`basic_json_document`](basic_json_document/index.md) of the +[`ordered_json`](ordered_json.md) specialization: [`set`](basic_json_document/set.md) and +[`push_back`](basic_json_document/push_back.md) change values after parsing, as for +[`json_editable_document`](json_editable_document.md), and +[`materialize()`](basic_json_view/materialize.md) preserves the document order of object members -- including +members [`set`](basic_json_document/set.md) added -- instead of sorting them like +[`json_editable_document`](json_editable_document.md) does. + +## Examples + +??? example + + The example below edits a document with `set`, then shows that `materialize()` keeps the member order of the + source text (with the new member at the end) for `ordered_json_editable_document`, where it would sort the + members alphabetically for [`json_editable_document`](json_editable_document.md). + + ```cpp + --8<-- "examples/ordered_json_editable_document.cpp" + ``` + + Output: + + ```json + --8<-- "examples/ordered_json_editable_document.output" + ``` + +## See also + +- [ordered_json_editable_view](ordered_json_editable_view.md) - a view of a value of an + `ordered_json_editable_document` +- [ordered_json_document](ordered_json_document.md) - the read-only document this type adds edits to +- [json_editable_document](json_editable_document.md) - the corresponding editable document for the default `json` + specialization +- [Object Order](../features/object_order.md) +- [Edits](basic_json_document/index.md#edits) - what an edit guarantees + +## Version history + +Since version 3.13.0. diff --git a/docs/mkdocs/docs/api/ordered_json_editable_view.md b/docs/mkdocs/docs/api/ordered_json_editable_view.md new file mode 100644 index 000000000..78351121e --- /dev/null +++ b/docs/mkdocs/docs/api/ordered_json_editable_view.md @@ -0,0 +1,40 @@ +# nlohmann::ordered_json_editable_view + +Defined in header `` + +```cpp +using ordered_json_editable_view = basic_json_view; +``` + +This type is a [`basic_json_view`](basic_json_view/index.md) of a value of an +[`ordered_json_editable_document`](ordered_json_editable_document.md), the corresponding view for +[`ordered_json_view`](ordered_json_view.md) the way [`json_editable_view`](json_editable_view.md) is for +[`json_view`](json_view.md). + +## Examples + +??? example + + The example below is the same as [`ordered_json_editable_document`'s](ordered_json_editable_document.md): the + views `set` returns see the document's member order preserved on `materialize()`, unlike for a + [`json_editable_document`](json_editable_document.md). + + ```cpp + --8<-- "examples/ordered_json_editable_document.cpp" + ``` + + Output: + + ```json + --8<-- "examples/ordered_json_editable_document.output" + ``` + +## See also + +- [ordered_json_editable_document](ordered_json_editable_document.md) - the document type this view refers into +- [ordered_json_view](ordered_json_view.md) - the corresponding read-only view +- [json_editable_view](json_editable_view.md) - the corresponding view for `json_editable_document` + +## Version history + +Since version 3.13.0. diff --git a/docs/mkdocs/docs/examples/basic_json_document__erase.cpp b/docs/mkdocs/docs/examples/basic_json_document__erase.cpp new file mode 100644 index 000000000..4b3197698 --- /dev/null +++ b/docs/mkdocs/docs/examples/basic_json_document__erase.cpp @@ -0,0 +1,38 @@ +#include +#include + +using json = nlohmann::json; +using json_editable_document = nlohmann::json_editable_document; +using json_editable_view = nlohmann::json_editable_view; + +int main() +{ + // a deprecated field is dropped from a configuration file, and a + // decommissioned replica is removed from the list -- "price" keeps its + // trailing zero, and the fields around the removed ones keep their order + const std::string text = R"({ + "name": "cache", + "legacy_host": "db0", + "host": "db1", + "price": 19.90, + "replicas": ["db2", "db3", "db4"] +})"; + + json_editable_document doc = json_editable_document::parse(text); + + doc.erase(doc.root(), "legacy_host"); // (1) an object member + doc.erase(doc.root()["replicas"], 1); // (2) an array element ("db3") + const std::size_t removed = doc.erase(json::json_pointer("/replicas/0")); // (3) via a JSON pointer + + std::cout << removed << '\n'; + std::cout << doc.root().dump(2, ' ', false, json_editable_view::number_format::source) << "\n\n"; + + // the same edits on a plain json value: object_t is a std::map, so + // parsing already sorted the keys, and dump() rewrites every number to + // its shortest form, even "price", which was never touched + json plain = json::parse(text); + plain.erase("legacy_host"); + plain["replicas"].erase(1); + plain["replicas"].erase(0); + std::cout << plain.dump(2) << '\n'; +} diff --git a/docs/mkdocs/docs/examples/basic_json_document__erase.output b/docs/mkdocs/docs/examples/basic_json_document__erase.output new file mode 100644 index 000000000..de764877a --- /dev/null +++ b/docs/mkdocs/docs/examples/basic_json_document__erase.output @@ -0,0 +1,18 @@ +1 +{ + "name": "cache", + "host": "db1", + "price": 19.90, + "replicas": [ + "db4" + ] +} + +{ + "host": "db1", + "name": "cache", + "price": 19.9, + "replicas": [ + "db4" + ] +} diff --git a/docs/mkdocs/docs/examples/basic_json_document__insert.cpp b/docs/mkdocs/docs/examples/basic_json_document__insert.cpp new file mode 100644 index 000000000..1c04bd6cc --- /dev/null +++ b/docs/mkdocs/docs/examples/basic_json_document__insert.cpp @@ -0,0 +1,38 @@ +#include +#include + +using json = nlohmann::json; +using json_editable_document = nlohmann::json_editable_document; +using json_editable_view = nlohmann::json_editable_view; + +int main() +{ + // a deployment plan -- "budget" is written with a trailing zero that has + // no effect on its value + const std::string text = R"({ + "release": "2026.09", + "steps": ["build", "test", "deploy"], + "budget": 19.90 +})"; + + json_editable_document doc = json_editable_document::parse(text); + + const std::size_t deploy_index = 2; + const auto deploy = doc.root()["steps"][deploy_index]; // held across the insert + + doc.insert(doc.root()["steps"], deploy_index, "smoke-test"); // insert before "deploy" + + // the held view still refers to "deploy", even though its index moved + // from 2 to 3, and nothing else in the document was touched + std::cout << deploy.dump() << '\n'; + std::cout << doc.root().dump(2, ' ', false, json_editable_view::number_format::source) << "\n\n"; + + // the same edit on a plain json value: an index held from before the + // insert now refers to whatever moved into that slot, and dump() + // rewrites "budget" to its shortest form even though it was never + // touched + json plain = json::parse(text); + plain["steps"].insert(plain["steps"].begin() + static_cast(deploy_index), "smoke-test"); + std::cout << plain["steps"][deploy_index].dump() << '\n'; + std::cout << plain.dump(2) << '\n'; +} diff --git a/docs/mkdocs/docs/examples/basic_json_document__insert.output b/docs/mkdocs/docs/examples/basic_json_document__insert.output new file mode 100644 index 000000000..1b2480df1 --- /dev/null +++ b/docs/mkdocs/docs/examples/basic_json_document__insert.output @@ -0,0 +1,23 @@ +"deploy" +{ + "release": "2026.09", + "steps": [ + "build", + "test", + "smoke-test", + "deploy" + ], + "budget": 19.90 +} + +"smoke-test" +{ + "budget": 19.9, + "release": "2026.09", + "steps": [ + "build", + "test", + "smoke-test", + "deploy" + ] +} diff --git a/docs/mkdocs/docs/examples/basic_json_document__push_back.cpp b/docs/mkdocs/docs/examples/basic_json_document__push_back.cpp new file mode 100644 index 000000000..16385ea99 --- /dev/null +++ b/docs/mkdocs/docs/examples/basic_json_document__push_back.cpp @@ -0,0 +1,24 @@ +#include +#include + +using json = nlohmann::json; +using json_editable_document = nlohmann::json_editable_document; + +int main() +{ + // "events" starts out null -- the first push_back() turns it into an + // array, exactly like set() turns a null object member into an object + json_editable_document doc = json_editable_document::parse(R"({"source": "sensor-1", "events": null})"); + + const auto first = doc.push_back(doc.root()["events"], json{{"type", "start"}, {"t", 0}}); + for (int t = 1; t <= 3; ++t) + { + doc.push_back(doc.root()["events"], json{{"type", "tick"}, {"t", t}}); + } + + // push_back() never moves an existing element: a view taken from an + // earlier call still refers to the same element after later ones + std::cout << first.dump() << '\n'; + std::cout << doc.root()["events"].size() << '\n'; + std::cout << doc.root().dump(2) << '\n'; +} diff --git a/docs/mkdocs/docs/examples/basic_json_document__push_back.output b/docs/mkdocs/docs/examples/basic_json_document__push_back.output new file mode 100644 index 000000000..5a7307aa4 --- /dev/null +++ b/docs/mkdocs/docs/examples/basic_json_document__push_back.output @@ -0,0 +1,23 @@ +{"t":0,"type":"start"} +4 +{ + "source": "sensor-1", + "events": [ + { + "t": 0, + "type": "start" + }, + { + "t": 1, + "type": "tick" + }, + { + "t": 2, + "type": "tick" + }, + { + "t": 3, + "type": "tick" + } + ] +} diff --git a/docs/mkdocs/docs/examples/basic_json_document__set.cpp b/docs/mkdocs/docs/examples/basic_json_document__set.cpp new file mode 100644 index 000000000..7631a7b1b --- /dev/null +++ b/docs/mkdocs/docs/examples/basic_json_document__set.cpp @@ -0,0 +1,41 @@ +#include +#include + +using json = nlohmann::json; +using json_editable_document = nlohmann::json_editable_document; +using json_editable_view = nlohmann::json_editable_view; + +int main() +{ + // a configuration file, as it might be read from disk -- "price" is + // written with a trailing zero that has no effect on its value + const std::string text = R"({ + "name": "cache", + "host": "db1", + "port": 6379, + "price": 19.90, + "replicas": ["db2", "db3"], + "timeout": 30 +})"; + + json_editable_document doc = json_editable_document::parse(text); + + doc.set(doc.root()["port"], 6380); // (1) replace a value + doc.set(doc.root(), "region", "us-east"); // (2) add a member + doc.set(doc.root()["replicas"], 0, "db4"); // (3) assign an element + doc.set(json::json_pointer("/timeout"), 45); // (4) via a JSON pointer + + // members stay in document order (the new one at the end), and a number + // that was not itself edited keeps its exact spelling + std::cout << doc.root().dump(2, ' ', false, json_editable_view::number_format::source) << "\n\n"; + + // the same edits on a plain json value: object_t is a std::map, so + // parsing already sorted the keys, and dump() rewrites every number to + // its shortest form, even "price", which was never touched + json plain = json::parse(text); + plain["port"] = 6380; + plain["region"] = "us-east"; + plain["replicas"][0] = "db4"; + plain[json::json_pointer("/timeout")] = 45; + std::cout << plain.dump(2) << '\n'; +} diff --git a/docs/mkdocs/docs/examples/basic_json_document__set.output b/docs/mkdocs/docs/examples/basic_json_document__set.output new file mode 100644 index 000000000..74392692e --- /dev/null +++ b/docs/mkdocs/docs/examples/basic_json_document__set.output @@ -0,0 +1,25 @@ +{ + "name": "cache", + "host": "db1", + "port": 6380, + "price": 19.90, + "replicas": [ + "db4", + "db3" + ], + "timeout": 45, + "region": "us-east" +} + +{ + "host": "db1", + "name": "cache", + "port": 6380, + "price": 19.9, + "region": "us-east", + "replicas": [ + "db4", + "db3" + ], + "timeout": 45 +} diff --git a/docs/mkdocs/docs/examples/json_editable_document.cpp b/docs/mkdocs/docs/examples/json_editable_document.cpp new file mode 100644 index 000000000..bf08c0d9b --- /dev/null +++ b/docs/mkdocs/docs/examples/json_editable_document.cpp @@ -0,0 +1,30 @@ +#include +#include + +using json = nlohmann::json; +using json_editable_document = nlohmann::json_editable_document; +using json_editable_view = nlohmann::json_editable_view; + +int main() +{ + // a configuration file, as it might be read from disk + const std::string text = R"({"name": "cache", "host": "db1", "port": 6379, "price": 19.90})"; + std::cout << text << "\n\n"; + + // patch two fields -- "price" is never touched + json_editable_document doc = json_editable_document::parse(text); + doc.set(doc.root(), "host", "db2"); + doc.set(doc.root(), "retries", 3); + + // member order (the new member at the end) and the untouched number's + // exact spelling survive + std::cout << doc.root().dump(-1, ' ', false, json_editable_view::number_format::source) << '\n'; + + // the same patch on a plain json value: keys are sorted (object_t is a + // std::map), and "price" is rewritten even though the patch never + // touched it + json plain = json::parse(text); + plain["host"] = "db2"; + plain["retries"] = 3; + std::cout << plain.dump() << '\n'; +} diff --git a/docs/mkdocs/docs/examples/json_editable_document.output b/docs/mkdocs/docs/examples/json_editable_document.output new file mode 100644 index 000000000..c949e3828 --- /dev/null +++ b/docs/mkdocs/docs/examples/json_editable_document.output @@ -0,0 +1,4 @@ +{"name": "cache", "host": "db1", "port": 6379, "price": 19.90} + +{"name":"cache","host":"db2","port":6379,"price":19.90,"retries":3} +{"host":"db2","name":"cache","port":6379,"price":19.9,"retries":3} diff --git a/docs/mkdocs/docs/examples/ordered_json_editable_document.cpp b/docs/mkdocs/docs/examples/ordered_json_editable_document.cpp new file mode 100644 index 000000000..c6e7fa08e --- /dev/null +++ b/docs/mkdocs/docs/examples/ordered_json_editable_document.cpp @@ -0,0 +1,15 @@ +#include +#include + +using ordered_json_editable_document = nlohmann::ordered_json_editable_document; + +int main() +{ + // ordered_json_editable_document is basic_json_document + ordered_json_editable_document doc = ordered_json_editable_document::parse(R"({"z": 1, "a": 2, "m": 3})"); + doc.set(doc.root(), "b", 4); // set() always appends a new member at the end + + // materialize() preserves the document order (with "b" at the end), + // instead of sorting the keys the way json_editable_document does + std::cout << doc.root().materialize().dump() << '\n'; +} diff --git a/docs/mkdocs/docs/examples/ordered_json_editable_document.output b/docs/mkdocs/docs/examples/ordered_json_editable_document.output new file mode 100644 index 000000000..a845dbaff --- /dev/null +++ b/docs/mkdocs/docs/examples/ordered_json_editable_document.output @@ -0,0 +1 @@ +{"z":1,"a":2,"m":3,"b":4} diff --git a/docs/mkdocs/docs/features/index.md b/docs/mkdocs/docs/features/index.md index d995c97d8..b1173b662 100644 --- a/docs/mkdocs/docs/features/index.md +++ b/docs/mkdocs/docs/features/index.md @@ -14,6 +14,7 @@ C++ types, and finally serialize it again. [parsing untrusted input](parsing/untrusted_input.md). - [Zero-copy JSON views](json_view.md) — read a JSON text through a flat index instead of building a `json` tree; strings and numbers stay in the input and are only decoded when needed. + [Editable documents](json_view.md#editing-a-document) can also be modified. - [Comments](comments.md) and [trailing commas](trailing_commas.md) — opt-in relaxations of the JSON grammar. ## Accessing and modifying values diff --git a/docs/mkdocs/docs/features/json_view.md b/docs/mkdocs/docs/features/json_view.md index 8030fe115..8a275db4a 100644 --- a/docs/mkdocs/docs/features/json_view.md +++ b/docs/mkdocs/docs/features/json_view.md @@ -187,14 +187,77 @@ a number to its parsed `#!cpp double`/`#!cpp int64_t` value. [`operator<<`](../api/basic_json_view/operator_ltlt.md) writes a view to a stream the way `basic_json`'s does, using the stream's `width`/`fill` for indentation. +## Editing a document + +Everything above is read-only: a `json_document`/`json_view` lets you look at a parsed text without copying it, but +not change it. [`basic_json_document`](../api/basic_json_document/index.md) -- more conveniently +spelled [`json_editable_document`](../api/json_editable_document.md) or +[`ordered_json_editable_document`](../api/ordered_json_editable_document.md) -- also lets you +[`set`](../api/basic_json_document/set.md) a value, [`push_back`](../api/basic_json_document/push_back.md) onto or +[`insert`](../api/basic_json_document/insert.md) into an array, and [`erase`](../api/basic_json_document/erase.md) +an object member or an array element, still without ever building a `basic_json` tree for parts you do not touch. + +`#!cpp Editable` defaults to `#!cpp false`, so `json_document`/`ordered_json_document` are unaffected -- they carry +none of the bookkeeping edits need, and calling `set`/`push_back`/`insert`/`erase` on one is a compile error, not a +runtime one. + +### Why: editing without reformatting + +The `#!cpp 19.90` price from [above](#writing-a-view-back) is exactly the kind of value that makes editing a `json` +or `ordered_json` value in place lossy. Say you parse a configuration file, patch one field, and write it back: + +- **`json`** re-sorts every key on the way in (`object_t` is a `#!cpp std::map`) and rewrites every number to its + shortest round-trip form on the way out -- a one-field patch turns into a diff that reorders the whole file and + rewrites `#!cpp 19.90` to `#!cpp 19.9`. +- **`ordered_json`** keeps the key order, but still rewrites every number the same way: parsing has already reduced + it to a `#!cpp double`/`#!cpp int64_t`, and there is no way back to how it was spelled in the source text. + +An editable document keeps both. [`dump()`](../api/basic_json_view/dump.md) of an edited document writes members in +document order -- a member [`set`](../api/basic_json_document/set.md) added goes at the end, exactly where it was +inserted, and an [`erase`](../api/basic_json_document/erase.md)d member simply leaves a gap: everything around it +keeps its place -- and [`number_format::source`](../api/basic_json_view/number_format.md) keeps the exact spelling +of every number an edit did not itself touch; a number an edit *did* touch is written the way +[`BasicJsonType::dump()`](../api/basic_json/dump.md) would write it, since there is no source spelling for a brand +new value. + +??? example "Example: patch a configuration, keeping member order and number spellings" + + ```cpp + --8<-- "examples/json_editable_document.cpp" + ``` + + Output: + + ```json + --8<-- "examples/json_editable_document.output" + ``` + +### What stays valid, and what an edit costs + +The source text itself is **never written**, and the parsed index never moves -- a value keeps the node it was +parsed into for as long as it is not itself replaced. So every [view](../api/basic_json_view/index.md) taken before +an edit, including a previously obtained [`root()`](../api/basic_json_document/root.md), stays valid and, if it +still refers to the edited value, sees the edit; a view of a value a later edit drops or replaces just keeps showing +what it last held. New values go to storage the document allocates and owns on demand. The one thing an edit does +invalidate is the **iterators** taken over an edited array or object: the first time one of its elements is set, +appended to, inserted into, or erased, its elements move from the parsed, fixed layout to a growable block of links +so that [`push_back`](../api/basic_json_document/push_back.md) can later grow it in amortized constant time -- +existing elements are not touched, but an iterator that was walking the old layout no longer matches. A string +obtained with +[`get_string()`](../api/basic_json_view/get_string.md) is unaffected either way and stays valid across further +edits. See [`basic_json_document`'s Edits](../api/basic_json_document/index.md#edits) for the details, and +[`set`'s Exception safety](../api/basic_json_document/set.md#exception-safety) for what an edit guarantees if it +throws (the *basic* guarantee, not the strong one `dump()` and the read-only functions provide). How edits are kept in +the index is described in the [architecture overview](../home/architecture.md#node-index-of-json-views). + ## Choosing between `json`, `ordered_json`, the SAX interface, and `json_view` -| | [`json`](../api/json.md) / [`ordered_json`](../api/ordered_json.md) | [SAX interface](parsing/sax_interface.md) | [`json_document`](../api/json_document.md) / [`json_view`](../api/json_view.md) | -|---|---|---|---| -| **Ownership** | owns every value | owns nothing; you decide what to keep, in your handler | borrows or owns the *text*; the index is always owned by the document | -| **Mutability** | freely mutable | not applicable (a one-shot event stream) | read-only | -| **What you get** | a full tree you can read, write, and keep as long as you like | a sequence of callbacks; whatever your handler builds from them | a flat index plus, on demand, [`materialize()`](../api/basic_json_view/materialize.md)d `json`/`ordered_json` values for the parts you actually use | -| **Typical use** | general-purpose JSON handling: config, request/response bodies you build or modify, anything you hold onto | validating or projecting a text into your own data structure without ever holding the whole thing as JSON | large or high-volume input where you only need part of it, or need it repeatedly, and can keep the source text (or a copy) alive for as long as the document lives | +| | [`json`](../api/json.md) / [`ordered_json`](../api/ordered_json.md) | [SAX interface](parsing/sax_interface.md) | [`json_document`](../api/json_document.md) / [`json_view`](../api/json_view.md) | [`json_editable_document`](../api/json_editable_document.md) / [`json_editable_view`](../api/json_editable_view.md) | +|---|---|---|---|---| +| **Ownership** | owns every value | owns nothing; you decide what to keep, in your handler | borrows or owns the *text*; the index is always owned by the document | same as `json_document`; edits go to storage the document owns | +| **Mutability** | freely mutable | not applicable (a one-shot event stream) | read-only | [`set`](../api/basic_json_document/set.md)/[`push_back`](../api/basic_json_document/push_back.md)/[`insert`](../api/basic_json_document/insert.md)/[`erase`](../api/basic_json_document/erase.md) edit in place; the source text is never rewritten | +| **What you get** | a full tree you can read, write, and keep as long as you like | a sequence of callbacks; whatever your handler builds from them | a flat index plus, on demand, [`materialize()`](../api/basic_json_view/materialize.md)d `json`/`ordered_json` values for the parts you actually use | the same, plus [`dump()`](../api/basic_json_view/dump.md) of an edited document that keeps the member order and, with [`number_format::source`](../api/basic_json_view/number_format.md), the spelling of every untouched number | +| **Typical use** | general-purpose JSON handling: config, request/response bodies you build or modify, anything you hold onto | validating or projecting a text into your own data structure without ever holding the whole thing as JSON | large or high-volume input where you only need part of it, or need it repeatedly, and can keep the source text (or a copy) alive for as long as the document lives | a document you read, patch a few fields of, and write back -- a configuration file, for instance -- where the rest of it should come back exactly as it was | ## Version history diff --git a/docs/mkdocs/docs/home/architecture.md b/docs/mkdocs/docs/home/architecture.md index df1f0e06d..3329998f5 100644 --- a/docs/mkdocs/docs/home/architecture.md +++ b/docs/mkdocs/docs/home/architecture.md @@ -194,8 +194,8 @@ packet-beta | Bytes | Field | Type | Contents | |-------|---------|------------|-------------------------------------------------------------------------------------------------------------------------------| -| 0 | `kind` | `uint8_t` | the type, numbered as [`value_t`](../api/basic_json/value_t.md): 0 null, 1 object, 2 array, 3 string, 4 boolean, 5 signed integer, 6 unsigned integer, 7 float | -| 1 | `flags` | `uint8_t` | bits 0-1: where a string's bytes are (0: the source text, 1: the buffer of decoded strings, for strings with escapes); bit 2: the value of a boolean | +| 0 | `kind` | `uint8_t` | the type, numbered as [`value_t`](../api/basic_json/value_t.md): 0 null, 1 object, 2 array, 3 string, 4 boolean, 5 signed integer, 6 unsigned integer, 7 float; 10 for a link (see below) | +| 1 | `flags` | `uint8_t` | bits 0-1: where a string's bytes (or a number's token) are (0: the source text, 1: the buffer of decoded strings, for strings with escapes, 2: the edit buffer); bit 2: the value of a boolean; bits 3 and 4: moved and new (see below) | | 2-3 | `extra` | `uint16_t` | numbers: the number of integer digits (low byte) and fraction digits (high byte), 255 for more; objects: the number of their hash index (1-based), or 0; otherwise 0 | | 4-7 | `off` | `uint32_t` | where the value starts: the first byte after a string's opening quote (or its position in the buffer of decoded strings), the first byte of a number or literal, the bracket of an array or object | | 8-11 | `len` | `uint32_t` | strings: the length after decoding; floats and literals: the length of the token; arrays and objects: the number of elements | @@ -244,6 +244,21 @@ flowchart LR All `flags` are 0. The integer's bytes 8-15 hold its value, 1; its `extra` says it has one digit. The float's `extra` says it has one integer and one fraction digit, and its `len` is that of the token `2.5`. +Editable documents ([`json_editable_document`](../api/json_editable_document.md), +[`detail/view/edit.hpp`](https://github.com/nlohmann/json/blob/develop/include/nlohmann/detail/view/edit.hpp) and +[`detail/view/edit_storage.hpp`](https://github.com/nlohmann/json/blob/develop/include/nlohmann/detail/view/edit_storage.hpp)) +never write the source text and never move or resize the parsed index, so views stay valid while the document is +edited: + +- A new scalar is written over its node. Its text (a string, or the token of a number as `dump()` writes it) goes to + the edit buffer, which `flags` bits 0-1 then name. +- An array or object whose elements change gets the flag *moved* (bit 3): its elements then live in a separate + sequence (a header node, then the entries), whose number is in `off`. The entries are links (`kind` 10), whose bytes + 8-15 hold the address of the value's node, so values never move. +- A node written by an edit gets the flag *new* (bit 4): it has no position in the source text. +- Views of read-only documents compile without any of this: how views walk the index is a template parameter + (`navigation`). + ## Input adapters Input is read via **input adapters** that abstract a source. Every input adapter provides this interface: diff --git a/docs/mkdocs/docs/home/exceptions.md b/docs/mkdocs/docs/home/exceptions.md index 15673d5c7..8b7d254dd 100644 --- a/docs/mkdocs/docs/home/exceptions.md +++ b/docs/mkdocs/docs/home/exceptions.md @@ -804,6 +804,24 @@ does not list an enumerator and it is therefore converted like the first listed [json.exception.type_error.318] duplicate object key 'red' ``` +### json.exception.type_error.319 + +[`basic_json_document::set`](../api/basic_json_document/set.md) and +[`basic_json_document::push_back`](../api/basic_json_document/push_back.md) can store any `basic_json` value except +a binary one: a `json_document` has no representation for [binary values](../features/binary_values.md), which only +ever arise from parsing a binary format or from an explicit [`json::binary`](../api/basic_json/binary.md) value, not +from JSON text. + +!!! failure "Example message" + + ``` + [json.exception.type_error.319] cannot store a binary value in a json_document + ``` + +!!! note + + This exception was added in version 3.13.0, together with editable [`json_document`s](../features/json_view.md). + ### json.exception.type_error.321 A discarded value (one created by [`parse()`](../api/basic_json/parse.md) with a callback that returns `false` for the @@ -1047,13 +1065,19 @@ MessagePack's ext type and BSON's binary subtype are each stored in a single byt [`basic_json_document::parse()`](../api/basic_json_document/parse.md) and the other parsing functions of [`basic_json_document`](../api/basic_json_document/index.md) index a value's position in the source text in 32 bits, -so they do not support an input of 4 GiB or more. +so they do not support an input of 4 GiB or more. The same 32-bit limit applies to an **editable** document's own +storage: [`set`](../api/basic_json_document/set.md) and [`push_back`](../api/basic_json_document/push_back.md) throw +this exception once the strings and number tokens written by edits reach 4 GiB in total, or once more than +4294967295 arrays/objects have had an element set or appended to them. -!!! failure "Example message" +!!! failure "Example messages" ``` [json.exception.out_of_range.416] input of 4 GiB or more is not supported by json_document ``` + ``` + [json.exception.out_of_range.416] edits of 4 GiB or more are not supported by json_document + ``` !!! note diff --git a/docs/mkdocs/mkdocs.yml b/docs/mkdocs/mkdocs.yml index 0eec15fe3..43ffcbdd1 100644 --- a/docs/mkdocs/mkdocs.yml +++ b/docs/mkdocs/mkdocs.yml @@ -237,14 +237,18 @@ nav: - 'Overview': api/basic_json_document/index.md - '(Constructor)': api/basic_json_document/basic_json_document.md - 'accept': api/basic_json_document/accept.md + - 'erase': api/basic_json_document/erase.md + - 'insert': api/basic_json_document/insert.md - 'is_discarded': api/basic_json_document/is_discarded.md - 'memory_usage': api/basic_json_document/memory_usage.md - 'node_count': api/basic_json_document/node_count.md - 'owns_source': api/basic_json_document/owns_source.md - 'parse': api/basic_json_document/parse.md - 'parse_copy': api/basic_json_document/parse_copy.md + - 'push_back': api/basic_json_document/push_back.md - 'read': api/basic_json_document/read.md - 'root': api/basic_json_document/root.md + - 'set': api/basic_json_document/set.md - 'shrink_to_fit': api/basic_json_document/shrink_to_fit.md - 'source': api/basic_json_document/source.md - basic_json_view: @@ -307,6 +311,8 @@ nav: - 'to_json': api/adl_serializer/to_json.md - 'json': api/json.md - 'json_document': api/json_document.md + - 'json_editable_document': api/json_editable_document.md + - 'json_editable_view': api/json_editable_view.md - json_pointer: - 'Overview': api/json_pointer/index.md - '(Constructor)': api/json_pointer/json_pointer.md @@ -348,6 +354,8 @@ nav: - 'operator""_json_pointer': api/operator_literal_json_pointer.md - 'ordered_json': api/ordered_json.md - 'ordered_json_document': api/ordered_json_document.md + - 'ordered_json_editable_document': api/ordered_json_editable_document.md + - 'ordered_json_editable_view': api/ordered_json_editable_view.md - 'ordered_json_view': api/ordered_json_view.md - 'ordered_map': api/ordered_map.md - macros: diff --git a/include/nlohmann/detail/view/document_data.hpp b/include/nlohmann/detail/view/document_data.hpp index fd72ee66e..9b3842e2c 100644 --- a/include/nlohmann/detail/view/document_data.hpp +++ b/include/nlohmann/detail/view/document_data.hpp @@ -12,6 +12,9 @@ #include // size_t #include // uint32_t #include // memcpy +#include // less +#include // map +#include // unique_ptr #include // operator new, placement new #include // string #include // vector @@ -50,9 +53,30 @@ struct document_data std::vector indexes{}; // NOLINT(readability-redundant-member-init) std::vector index_slots{}; // NOLINT(readability-redundant-member-init) std::vector large_objects{}; ///< positions of the objects to index (noted while parsing) // NOLINT(readability-redundant-member-init) - std::array base = {{nullptr, nullptr, nullptr, nullptr}}; ///< string bases: source, arena (indexed by flags & node_flags::storage) + std::array base = {{nullptr, nullptr, nullptr, nullptr}}; ///< string bases: source, arena, edit arena (indexed by flags & node_flags::storage) bool discarded = true; + /// The storage of edits (editable documents only; see edit_storage.hpp). + /// Edits never move or resize the parsed index, so views stay valid: an + /// array/object whose elements change gets node_flags::moved, and its + /// elements then live in a separate sequence (a header node, then the + /// entries), whose entries link to the values. + struct edit_state + { + std::vector moved{}; ///< element sequences of moved arrays/objects (header node first) // NOLINT(readability-redundant-member-init) + std::vector moved_cap{}; ///< capacity in nodes of a growable block; 0: a fixed sequence (a new value) // NOLINT(readability-redundant-member-init) + std::vector> chunks{}; ///< storage of new values and blocks; never moved // NOLINT(readability-redundant-member-init,cppcoreguidelines-avoid-c-arrays,hicpp-avoid-c-arrays,modernize-avoid-c-arrays) + std::map> regions{}; ///< new arrays/objects: root -> container that uses it as its element sequence (nullptr: linked from a block) // NOLINT(readability-redundant-member-init) + node* chunk_cur = nullptr; + node* chunk_end = nullptr; + std::size_t chunk_next = 64; + std::vector> texts{}; ///< edit arena, the current buffer last; earlier ones stay alive for string views // NOLINT(readability-redundant-member-init,cppcoreguidelines-avoid-c-arrays,hicpp-avoid-c-arrays,modernize-avoid-c-arrays) + std::size_t text_used = 0; + std::size_t text_cap = 0; + std::size_t bytes = 0; ///< memory held by edits + }; + std::unique_ptr edits{}; ///< created by the first edit // NOLINT(readability-redundant-member-init) + /// one allocation for the header and room for `nodes` nodes; large /// documents get a separate node array instead (so it can be trimmed) static document_data* create(std::size_t nodes) @@ -136,6 +160,78 @@ struct document_data { return n + n->next; } + + /// (editable documents) first element or key, also of a moved container + NLOHMANN_VIEW_ALWAYS_INLINE const node* first_child_edited(const node* n) const noexcept + { + return NLOHMANN_VIEW_LIKELY((n->flags & node_flags::moved) == 0) ? n + 1 : edits->moved[n->off] + 1; + } + + /// (editable documents) end of the elements, also of a moved container + NLOHMANN_VIEW_ALWAYS_INLINE const node* child_end_edited(const node* n) const noexcept + { + if (NLOHMANN_VIEW_LIKELY((n->flags & node_flags::moved) == 0)) + { + return n + n->next; + } + const node* const h = edits->moved[n->off]; + return h + h->next; + } + + /// (editable documents) the value at an element position: entries of + /// moved sequences are links. The link case is out of line, so that this + /// compiles to a predicted branch rather than a select that delays the + /// following loads. + static NLOHMANN_VIEW_ALWAYS_INLINE const node* deref(const node* n) noexcept + { + return NLOHMANN_VIEW_LIKELY(n->kind != kind_link) ? n : follow_link(n); + } + + static NLOHMANN_VIEW_NOINLINE const node* follow_link(const node* n) noexcept + { + return link_target(*n); + } +}; + +/// How the index is walked: views of read-only documents follow the node +/// array alone and compile without any of the edit handling; views of +/// editable documents also follow moved element sequences and links. +template +struct navigation +{ + static NLOHMANN_VIEW_ALWAYS_INLINE const node* first(const document_data& /*d*/, const node* n) noexcept + { + return n + 1; + } + + static NLOHMANN_VIEW_ALWAYS_INLINE const node* end(const document_data& /*d*/, const node* n) noexcept + { + return n + n->next; + } + + static NLOHMANN_VIEW_ALWAYS_INLINE const node* value(const node* n) noexcept + { + return n; + } +}; + +template<> +struct navigation +{ + static NLOHMANN_VIEW_ALWAYS_INLINE const node* first(const document_data& d, const node* n) noexcept + { + return d.first_child_edited(n); + } + + static NLOHMANN_VIEW_ALWAYS_INLINE const node* end(const document_data& d, const node* n) noexcept + { + return d.child_end_edited(n); + } + + static NLOHMANN_VIEW_ALWAYS_INLINE const node* value(const node* n) noexcept + { + return document_data::deref(n); + } }; } // namespace view diff --git a/include/nlohmann/detail/view/edit.hpp b/include/nlohmann/detail/view/edit.hpp new file mode 100644 index 000000000..b83697618 --- /dev/null +++ b/include/nlohmann/detail/view/edit.hpp @@ -0,0 +1,767 @@ +// __ _____ _____ _____ +// __| | __| | | | 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 +{ + +/// the index of the first true condition (the number of conditions if none is) +template +struct first_true : std::integral_constant {}; + +template +struct first_true : std::integral_constant < int, 1 + first_true::value > {}; + +/// 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); + } + + /// insert into an array before position idx (idx <= size()); returns a + /// view of the new element + template + View insert(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(309, "cannot use insert() with ", array.type_name()); + } + check_index(idx, a->len + 1); + const encoded e = encode(std::forward(value)); + node* const slot = new_slot(e); + node* const h = block_of(m_doc, a, 1); + std::memmove(h + 2 + idx, h + 1 + idx, (h->next - 1 - idx) * sizeof(node)); + make_link(h[1 + idx], slot); + ++h->next; + ++h->len; + ++a->len; + return View(&m_doc, slot); + } + + /// remove all members with this key; returns their number + std::size_t erase(const View& object, string_view_t key) + { + node* const o = own(object); + if (o->kind != static_cast(value_t::object)) + { + throw_type_error(307, "cannot use erase() with ", object.type_name()); + } + for (const node* k = nav::first(m_doc, o), *end = nav::end(m_doc, o); k != end; k = document_data::after(k + 1)) + { + if (key_equals(*k, key)) + { + return erase_members(o, key, false); + } + } + return 0; + } + + /// remove an array element + void erase(const View& array, std::size_t idx) + { + node* const a = own(array); + if (a->kind != static_cast(value_t::array)) + { + throw_type_error(307, "cannot use erase() with ", array.type_name()); + } + check_index(idx, a->len); + node* const h = block_of(m_doc, a, 0); + std::memmove(h + 1 + idx, h + 2 + idx, (h->next - 2 - idx) * sizeof(node)); + --h->next; + --h->len; + --a->len; + } + + 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::value, + std::is_same::value, + std::is_same::value, + std::is_same::value, + std::is_arithmetic::value, + std::is_convertible::value>::value> {}); + } + + /// 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::value, std::is_signed::value>::value> {}); + 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..11bff9faa --- /dev/null +++ b/include/nlohmann/detail/view/edit_storage.hpp @@ -0,0 +1,242 @@ +// __ _____ _____ _____ +// __| | __| | | | 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()); // NOLINT(cppcoreguidelines-owning-memory): owned by the unique_ptr + } + 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]()); // NOLINT(cppcoreguidelines-avoid-c-arrays,hicpp-avoid-c-arrays,modernize-avoid-c-arrays) + 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"); // LCOV_EXCL_LINE (4 GiB) + } + std::unique_ptr fresh(new char[cap]); // NOLINT(cppcoreguidelines-avoid-c-arrays,hicpp-avoid-c-arrays,modernize-avoid-c-arrays) + 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 + } + make_link(*o, document_data::deref(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; // LCOV_EXCL_LINE (an array/object with elements is in the index or a new value) + } + --it; + lo = it->first; + hi = lo + lo->next; + if (!lt(target, hi)) + { + return nullptr; // LCOV_EXCL_LINE (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; // LCOV_EXCL_LINE (the value is inside c) + } + 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/detail/view/iterator.hpp b/include/nlohmann/detail/view/iterator.hpp index dfeda4a34..12906e9c1 100644 --- a/include/nlohmann/detail/view/iterator.hpp +++ b/include/nlohmann/detail/view/iterator.hpp @@ -73,7 +73,7 @@ class view_iterator NLOHMANN_VIEW_ALWAYS_INLINE View operator*() const noexcept { - return View(m_doc, m_pos + m_value_offset); + return View(m_doc, View::navigation::value(m_pos + m_value_offset)); } pointer operator->() const noexcept diff --git a/include/nlohmann/detail/view/lookup.hpp b/include/nlohmann/detail/view/lookup.hpp index 5d50c0667..718acc077 100644 --- a/include/nlohmann/detail/view/lookup.hpp +++ b/include/nlohmann/detail/view/lookup.hpp @@ -84,18 +84,20 @@ class short_key /// 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 -inline const node* find_member(const document_data& d, const node* object, const char* key, std::size_t n) noexcept +template +const node* find_member(const document_data& d, const node* object, const char* key, std::size_t n) noexcept { - if (NLOHMANN_VIEW_UNLIKELY(object->extra != 0)) + 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 + return find_indexed(d, object, key, n); // a large object (whose members have not been edited) } - const node* const end = document_data::child_end(object); + 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 = document_data::first_child(object); m != end; m = document_data::after(m + 1)) + 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) { @@ -104,7 +106,7 @@ inline const node* find_member(const document_data& d, const node* object, const } return nullptr; } - for (const node* m = document_data::first_child(object); m != end; m = document_data::after(m + 1)) + 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) { @@ -114,10 +116,16 @@ inline const node* find_member(const document_data& d, const node* object, const return nullptr; } -/// the element of an array at an index below its size -inline const node* element_at(const node* array, std::size_t idx) noexcept +/// 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 = document_data::first_child(array); + 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); @@ -125,13 +133,14 @@ inline const node* element_at(const node* array, std::size_t idx) noexcept return e; } -/// the last element of a non-empty array, or the key of the last member of a -/// non-empty object -inline const node* last_child(const node* container) noexcept +/// 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 = document_data::child_end(container); - const node* last = document_data::first_child(container); + 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; diff --git a/include/nlohmann/detail/view/materialize.hpp b/include/nlohmann/detail/view/materialize.hpp index 79fafee1b..008b30ba8 100644 --- a/include/nlohmann/detail/view/materialize.hpp +++ b/include/nlohmann/detail/view/materialize.hpp @@ -34,19 +34,28 @@ iterative, so the nesting depth is limited by memory only, as for parse(). Without a lexer the handler records no source positions (JSON_DIAGNOSTIC_POSITIONS). */ -template +template BasicJsonType materialize(const document_data& d, const node* n) { using string_t = typename BasicJsonType::string_t; using sax_t = json_sax_dom_parser>; + using nav = navigation; + + struct frame + { + const node* pos; ///< next element, or key of the next member + const node* end; + bool object; + }; BasicJsonType result; sax_t sax(result, true); const string_t no_token{}; - // the ends of the open containers, and whether they are objects - std::vector> open; + std::vector open; for (;;) { + // false positive: n comes from nav::value(), which never returns null for a valid index + // @infer-ignore NULLPTR_DEREFERENCE switch (static_cast(n->kind)) { case value_t::object: @@ -61,67 +70,66 @@ BasicJsonType materialize(const document_data& d, const node* n) { sax.start_array(n->len); } - open.emplace_back(document_data::child_end(n), object); - n = document_data::first_child(n); + open.push_back(frame{nav::first(d, n), nav::end(d, n), object}); break; } case value_t::string: { string_t s(d.str(*n), n->len); sax.string(s); - ++n; break; } case value_t::number_integer: sax.number_integer(static_cast(static_cast(integer_bits(*n)))); - ++n; break; case value_t::number_unsigned: sax.number_unsigned(static_cast(integer_bits(*n))); - ++n; break; case value_t::number_float: sax.number_float(float_value(d, *n), no_token); - ++n; break; case value_t::boolean: sax.boolean((n->flags & node_flags::is_true) != 0); - ++n; break; case value_t::null: case value_t::binary: case value_t::discarded: default: sax.null(); - ++n; break; } + + // the next value: close finished containers, then read the key for (;;) { if (open.empty()) { return result; } - if (n != open.back().first) + frame& f = open.back(); + if (f.pos == f.end) { - break; + if (f.object) + { + sax.end_object(); + } + else + { + sax.end_array(); + } + open.pop_back(); + continue; } - if (open.back().second) + const node* entry = f.pos; + if (f.object) { - sax.end_object(); + string_t key(d.str(*entry), entry->len); + sax.key(key); + ++entry; } - else - { - sax.end_array(); - } - open.pop_back(); - } - if (open.back().second) - { - // the key of the next member - string_t key(d.str(*n), n->len); - sax.key(key); - ++n; + n = nav::value(entry); + f.pos = document_data::after(entry); + break; } } } diff --git a/include/nlohmann/detail/view/node.hpp b/include/nlohmann/detail/view/node.hpp index 471c1f844..82774dac8 100644 --- a/include/nlohmann/detail/view/node.hpp +++ b/include/nlohmann/detail/view/node.hpp @@ -33,20 +33,27 @@ static_assert(static_cast(value_t::null) == 0 && static_cast(n.kind) - 1u <= 1u; } +/// the value a link node stands for +NLOHMANN_VIEW_ALWAYS_INLINE const node* link_target(const node& n) noexcept +{ + const node* t = nullptr; + std::memcpy(static_cast(&t), reinterpret_cast(&n) + 8, sizeof(const node*)); // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast) + return t; +} + +inline void make_link(node& n, const node* target) noexcept +{ + n = node{}; + n.kind = kind_link; + std::memcpy(reinterpret_cast(&n) + 8, static_cast(&target), sizeof(const node*)); // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast) +} + /// the converted value of an integer node (stored in len/next) NLOHMANN_VIEW_ALWAYS_INLINE std::uint64_t integer_bits(const node& n) noexcept { diff --git a/include/nlohmann/detail/view/number.hpp b/include/nlohmann/detail/view/number.hpp index 24a87a7b8..fce98e9ba 100644 --- a/include/nlohmann/detail/view/number.hpp +++ b/include/nlohmann/detail/view/number.hpp @@ -10,6 +10,7 @@ #include // size_t #include // int64_t, uint64_t +#include // numeric_limits #include // string #include // integral_constant @@ -128,11 +129,31 @@ NLOHMANN_VIEW_ALWAYS_INLINE FloatType layout_float(const unsigned char* p, const return decimal_to_float(layout_decimal(p, e, int_digits, frac_digits, limit)); } +/// the value of a float set by an edit: its token (the shortest round-trip +/// text, or "nan", "inf", "-inf") in the edit arena +template +NLOHMANN_VIEW_NOINLINE FloatType edited_float(const char* token, const node& n) +{ + if (token[0] == 'n') + { + return std::numeric_limits::quiet_NaN(); + } + if (token[0] == 'i' || (token[0] == '-' && token[1] == 'i')) + { + return token[0] == 'i' ? std::numeric_limits::infinity() : -std::numeric_limits::infinity(); + } + return float_value(token, n); +} + /// the value of the float token of a node, as parse() converts it; floats and /// doubles with at most 19 digits are converted from the digit layout template FloatType float_value(const document_data& d, const node& n) { + if (NLOHMANN_VIEW_UNLIKELY((n.flags & node_flags::storage) == node_flags::edited)) + { + return edited_float(d.str(n), n); + } return float_value(d, n, std::integral_constant::value> {}); } diff --git a/include/nlohmann/detail/view/serializer.hpp b/include/nlohmann/detail/view/serializer.hpp index 26bfb3225..3c5addf11 100644 --- a/include/nlohmann/detail/view/serializer.hpp +++ b/include/nlohmann/detail/view/serializer.hpp @@ -115,9 +115,10 @@ the library's conversion; integers are copied from the source, where they are canonical (except "-0", which parse() reads as 0). The walk is iterative, so the nesting depth is limited by memory only. */ -template +template class view_serializer { + using nav = navigation; using string_t = typename BasicJsonType::string_t; using number_float_t = typename BasicJsonType::number_float_t; @@ -150,7 +151,7 @@ class view_serializer else { m_out.put(object ? '{' : '['); - stack.push_back(frame{document_data::first_child(n), document_data::child_end(n), object, true}); + stack.push_back(frame{nav::first(m_doc, n), nav::end(m_doc, n), object, true}); } } else @@ -192,13 +193,14 @@ class view_serializer { m_out.put(':'); } - n = f.pos + 1; + n = nav::value(f.pos + 1); + f.pos = document_data::after(f.pos + 1); } else { - n = f.pos; + n = nav::value(f.pos); + f.pos = document_data::after(f.pos); } - f.pos = document_data::after(n); break; } } @@ -250,9 +252,9 @@ class view_serializer break; } case value_t::number_float: - if (m_style.source_numbers) + if (m_style.source_numbers && (n.flags & node_flags::storage) != node_flags::edited) { - m_out.put(m_doc.str(n), n.len); + m_out.put(m_doc.str(n), n.len); // (a float set by an edit is written as with shortest) } else { @@ -299,9 +301,9 @@ class view_serializer { const char* const s = m_doc.str(n); m_out.put('"'); - if ((n.flags & node_flags::escaped) == 0 && !m_style.ensure_ascii) + if ((n.flags & node_flags::storage) == 0 && !m_style.ensure_ascii) { - // a string without escape sequences has nothing to escape + // a string of the source without escape sequences has nothing to escape m_out.put(s, n.len); } else if (m_style.ensure_ascii) diff --git a/include/nlohmann/json_view.hpp b/include/nlohmann/json_view.hpp index d70bde43f..0b65ed96b 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 @@ -65,7 +67,7 @@ NLOHMANN_JSON_NAMESPACE_BEGIN -template +template class basic_json_document; /*! @@ -74,11 +76,13 @@ class basic_json_document; Trivially copyable (two pointers). Valid as long as the document is alive and has not been re-parsed, and as long as a borrowed source text is alive. */ -template +template class basic_json_view { using node = detail::view::node; using document_data = detail::view::document_data; + /// how the index is walked (with edits only for editable documents) + using navigation = detail::view::navigation; public: using value_t = detail::value_t; @@ -271,7 +275,7 @@ class basic_json_view { detail::view::throw_type_error(305, "cannot use operator[] with a numeric argument with ", type_name()); } - return idx < m_node->len ? basic_json_view(m_doc, detail::view::element_at(m_node, idx)) : basic_json_view(); + return idx < m_node->len ? basic_json_view(m_doc, navigation::value(detail::view::element_at(*m_doc, m_node, idx))) : basic_json_view(); } /// (an int argument would be ambiguous between size_type and const char*) @@ -327,7 +331,7 @@ class basic_json_view { detail::view::throw_out_of_range(401, detail::concat("array index ", std::to_string(idx), " is out of range")); } - return basic_json_view(m_doc, detail::view::element_at(m_node, idx)); + return basic_json_view(m_doc, navigation::value(detail::view::element_at(*m_doc, m_node, idx))); } basic_json_view at(int idx) const @@ -399,7 +403,7 @@ class basic_json_view { if (is_structured() && m_node->len != 0) { - return basic_json_view(m_doc, detail::view::last_child(m_node) + (is_object() ? 1 : 0)); + return basic_json_view(m_doc, navigation::value(detail::view::last_child(*m_doc, m_node) + (is_object() ? 1 : 0))); } return front(); } @@ -416,7 +420,7 @@ class basic_json_view { return end(); } - const node* const k = detail::view::find_member(*m_doc, m_node, key.data(), key.size()); + const node* const k = detail::view::find_member(*m_doc, m_node, key.data(), key.size()); return k != nullptr ? iterator(m_doc, k, true) : end(); } @@ -433,7 +437,7 @@ class basic_json_view /// whether this is an object with a member with this key bool contains(string_view_t key) const { - return is_object() && detail::view::find_member(*m_doc, m_node, key.data(), key.size()) != nullptr; + return is_object() && detail::view::find_member(*m_doc, m_node, key.data(), key.size()) != nullptr; } bool contains(const char* key) const @@ -480,7 +484,7 @@ class basic_json_view { if (NLOHMANN_VIEW_LIKELY(is_structured())) { - return iterator(m_doc, document_data::first_child(m_node), is_object()); + return iterator(m_doc, navigation::first(*m_doc, m_node), is_object()); } return iterator(m_doc, m_node, false); } @@ -489,7 +493,7 @@ class basic_json_view { if (NLOHMANN_VIEW_LIKELY(is_structured())) { - return iterator(m_doc, document_data::child_end(m_node), is_object()); + return iterator(m_doc, navigation::end(*m_doc, m_node), is_object()); } return iterator(m_doc, (is_null() || is_discarded()) ? m_node : m_node + 1, false); } @@ -589,7 +593,7 @@ class basic_json_view style.source_numbers = numbers == number_format::source; // the compact text is about as long as the source text of the value const std::size_t estimate = source_extent() + (style.pretty ? source_extent() / 2 : 0) + 64; - detail::view::view_serializer(*m_doc, out, estimate, style).dump(m_node); + detail::view::view_serializer(*m_doc, out, estimate, style).dump(m_node); return out; } @@ -623,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) { @@ -656,7 +673,7 @@ class basic_json_view { return BasicJsonType(value_t::discarded); } - return detail::view::materialize(*m_doc, m_node); + return detail::view::materialize(*m_doc, m_node); } /// byte offset of this value in the source text (for strings: of the @@ -664,12 +681,14 @@ class basic_json_view /// a discarded view and for strings with escapes, which are decoded std::size_t source_offset() const noexcept { - return m_node != nullptr && (m_node->flags & detail::view::node_flags::storage) == 0 + return m_node != nullptr && (m_node->flags & (detail::view::node_flags::storage | detail::view::node_flags::moved | detail::view::node_flags::is_new)) == 0 ? m_node->off : static_cast(-1); } private: - template friend class basic_json_document; + 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 @@ -687,6 +706,11 @@ class basic_json_view /// decoded strings) std::size_t source_extent() const noexcept { + if (Editable && m_doc->edits != nullptr) + { + // positions of moved and new values are not source offsets + return m_node == m_doc->tape ? m_doc->size + m_doc->edits->text_used : 64; + } const node* const next = document_data::after(m_node); const bool in_source = (m_node->flags & detail::view::node_flags::storage) == 0; if (!in_source) @@ -704,8 +728,8 @@ class basic_json_view /// (object required) NLOHMANN_VIEW_ALWAYS_INLINE basic_json_view lookup(string_view_t key) const noexcept { - const node* const k = detail::view::find_member(*m_doc, m_node, key.data(), key.size()); - return k != nullptr ? basic_json_view(m_doc, k + 1) : basic_json_view(); + const node* const k = detail::view::find_member(*m_doc, m_node, key.data(), key.size()); + return k != nullptr ? basic_json_view(m_doc, navigation::value(k + 1)) : basic_json_view(); } // --- get() dispatch --- @@ -796,7 +820,7 @@ Borrowed parses keep a pointer to the caller's text, which must outlive the document. Owned parses (parse_copy, rvalue std::string, streams, and inputs that are not contiguous byte ranges) keep their own copy. */ -template +template class basic_json_document { using document_data = detail::view::document_data; @@ -805,7 +829,7 @@ class basic_json_document "json_view supports 64-bit integer types only"); public: - using view_type = basic_json_view; + using view_type = basic_json_view; using value_t = detail::value_t; /// an empty (discarded) document @@ -930,7 +954,8 @@ class basic_json_document + (m_data->tape != m_data->inline_tape ? m_data->tape_cap * sizeof(detail::view::node) : 0) + m_data->arena.capacity() + m_data->owned.capacity() + (m_data->indexes.capacity() * sizeof(document_data::object_index)) + (m_data->index_slots.capacity() * sizeof(std::uint32_t)) - + (m_data->large_objects.capacity() * sizeof(std::uint32_t)); + + (m_data->large_objects.capacity() * sizeof(std::uint32_t)) + + (m_data->edits != nullptr ? m_data->edits->bytes : 0); } /// release unused capacity of the index and the decoded strings; like @@ -949,7 +974,8 @@ class basic_json_document // unchanged const bool shrink_arena = d.arena.capacity() > d.arena.size(); std::string arena(shrink_arena ? d.arena : std::string()); - const bool shrink_tape = d.tape != d.inline_tape && d.tape_size != d.tape_cap; + // (edits link to the nodes of the index, which then stays in place) + const bool shrink_tape = d.tape != d.inline_tape && d.tape_size != d.tape_cap && d.edits == nullptr; const bool into_header = d.tape_size <= d.inline_cap; node* fresh = (shrink_tape && !into_header) ? static_cast(::operator new (d.tape_size * sizeof(node))) : d.inline_tape; @@ -967,9 +993,163 @@ 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)); + } + + /// insert into an array before position idx (idx <= size()); returns a + /// view of the new element + template < typename I, typename V, typename std::enable_if < std::is_integral::value && !std::is_same::value, int >::type = 0 > + view_type insert(view_type array, I idx, V && value) + { + return editor().insert(array, index(idx), std::forward(value)); + } + + /// remove all members with this key; returns their number + std::size_t erase(view_type object, string_view_t key) + { + return editor().erase(object, key); + } + + /// remove an array element + template < typename I, typename std::enable_if < std::is_integral::value && !std::is_same::value, int >::type = 0 > + void erase(view_type array, I idx) + { + editor().erase(array, index(idx)); + } + + /// remove the value at a JSON pointer; returns the number of removed + /// values + std::size_t erase(const json_pointer& ptr) + { + if (ptr.empty()) + { + detail::view::throw_out_of_range(405, "JSON pointer has no parent"); + } + const view_type parent = root().at(ptr.parent_pointer()); + const auto& token = ptr.back(); + if (parent.is_array()) + { + erase(parent, pointer_index(token)); + return 1; + } + return erase(parent, string_view_t(token.data(), token.size())); + } + 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) { @@ -999,6 +1179,8 @@ class basic_json_document d.src = src; d.size = size; d.tape_size = 0; + d.edits.reset(); // (views of the previous text end here anyway) + d.base[2] = nullptr; d.arena.clear(); d.indexes.clear(); d.index_slots.clear(); @@ -1136,6 +1318,14 @@ 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 diff --git a/single_include/nlohmann/json_view.hpp b/single_include/nlohmann/json_view.hpp index 5e69bd9bd..236186820 100644 --- a/single_include/nlohmann/json_view.hpp +++ b/single_include/nlohmann/json_view.hpp @@ -84,6 +84,9 @@ #include // size_t #include // uint32_t #include // memcpy +#include // less +#include // map +#include // unique_ptr #include // operator new, placement new #include // string #include // vector @@ -199,20 +202,27 @@ static_assert(static_cast(value_t::null) == 0 && static_cast(n.kind) - 1u <= 1u; } +/// the value a link node stands for +NLOHMANN_VIEW_ALWAYS_INLINE const node* link_target(const node& n) noexcept +{ + const node* t = nullptr; + std::memcpy(static_cast(&t), reinterpret_cast(&n) + 8, sizeof(const node*)); // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast) + return t; +} + +inline void make_link(node& n, const node* target) noexcept +{ + n = node{}; + n.kind = kind_link; + std::memcpy(reinterpret_cast(&n) + 8, static_cast(&target), sizeof(const node*)); // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast) +} + /// the converted value of an integer node (stored in len/next) NLOHMANN_VIEW_ALWAYS_INLINE std::uint64_t integer_bits(const node& n) noexcept { @@ -293,9 +318,30 @@ struct document_data std::vector indexes{}; // NOLINT(readability-redundant-member-init) std::vector index_slots{}; // NOLINT(readability-redundant-member-init) std::vector large_objects{}; ///< positions of the objects to index (noted while parsing) // NOLINT(readability-redundant-member-init) - std::array base = {{nullptr, nullptr, nullptr, nullptr}}; ///< string bases: source, arena (indexed by flags & node_flags::storage) + std::array base = {{nullptr, nullptr, nullptr, nullptr}}; ///< string bases: source, arena, edit arena (indexed by flags & node_flags::storage) bool discarded = true; + /// The storage of edits (editable documents only; see edit_storage.hpp). + /// Edits never move or resize the parsed index, so views stay valid: an + /// array/object whose elements change gets node_flags::moved, and its + /// elements then live in a separate sequence (a header node, then the + /// entries), whose entries link to the values. + struct edit_state + { + std::vector moved{}; ///< element sequences of moved arrays/objects (header node first) // NOLINT(readability-redundant-member-init) + std::vector moved_cap{}; ///< capacity in nodes of a growable block; 0: a fixed sequence (a new value) // NOLINT(readability-redundant-member-init) + std::vector> chunks{}; ///< storage of new values and blocks; never moved // NOLINT(readability-redundant-member-init,cppcoreguidelines-avoid-c-arrays,hicpp-avoid-c-arrays,modernize-avoid-c-arrays) + std::map> regions{}; ///< new arrays/objects: root -> container that uses it as its element sequence (nullptr: linked from a block) // NOLINT(readability-redundant-member-init) + node* chunk_cur = nullptr; + node* chunk_end = nullptr; + std::size_t chunk_next = 64; + std::vector> texts{}; ///< edit arena, the current buffer last; earlier ones stay alive for string views // NOLINT(readability-redundant-member-init,cppcoreguidelines-avoid-c-arrays,hicpp-avoid-c-arrays,modernize-avoid-c-arrays) + std::size_t text_used = 0; + std::size_t text_cap = 0; + std::size_t bytes = 0; ///< memory held by edits + }; + std::unique_ptr edits{}; ///< created by the first edit // NOLINT(readability-redundant-member-init) + /// one allocation for the header and room for `nodes` nodes; large /// documents get a separate node array instead (so it can be trimmed) static document_data* create(std::size_t nodes) @@ -379,6 +425,78 @@ struct document_data { return n + n->next; } + + /// (editable documents) first element or key, also of a moved container + NLOHMANN_VIEW_ALWAYS_INLINE const node* first_child_edited(const node* n) const noexcept + { + return NLOHMANN_VIEW_LIKELY((n->flags & node_flags::moved) == 0) ? n + 1 : edits->moved[n->off] + 1; + } + + /// (editable documents) end of the elements, also of a moved container + NLOHMANN_VIEW_ALWAYS_INLINE const node* child_end_edited(const node* n) const noexcept + { + if (NLOHMANN_VIEW_LIKELY((n->flags & node_flags::moved) == 0)) + { + return n + n->next; + } + const node* const h = edits->moved[n->off]; + return h + h->next; + } + + /// (editable documents) the value at an element position: entries of + /// moved sequences are links. The link case is out of line, so that this + /// compiles to a predicted branch rather than a select that delays the + /// following loads. + static NLOHMANN_VIEW_ALWAYS_INLINE const node* deref(const node* n) noexcept + { + return NLOHMANN_VIEW_LIKELY(n->kind != kind_link) ? n : follow_link(n); + } + + static NLOHMANN_VIEW_NOINLINE const node* follow_link(const node* n) noexcept + { + return link_target(*n); + } +}; + +/// How the index is walked: views of read-only documents follow the node +/// array alone and compile without any of the edit handling; views of +/// editable documents also follow moved element sequences and links. +template +struct navigation +{ + static NLOHMANN_VIEW_ALWAYS_INLINE const node* first(const document_data& /*d*/, const node* n) noexcept + { + return n + 1; + } + + static NLOHMANN_VIEW_ALWAYS_INLINE const node* end(const document_data& /*d*/, const node* n) noexcept + { + return n + n->next; + } + + static NLOHMANN_VIEW_ALWAYS_INLINE const node* value(const node* n) noexcept + { + return n; + } +}; + +template<> +struct navigation +{ + static NLOHMANN_VIEW_ALWAYS_INLINE const node* first(const document_data& d, const node* n) noexcept + { + return d.first_child_edited(n); + } + + static NLOHMANN_VIEW_ALWAYS_INLINE const node* end(const document_data& d, const node* n) noexcept + { + return d.child_end_edited(n); + } + + static NLOHMANN_VIEW_ALWAYS_INLINE const node* value(const node* n) noexcept + { + return document_data::deref(n); + } }; } // namespace view @@ -2313,6 +2431,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++ @@ -2348,6 +2512,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)); @@ -2408,28 +2577,17 @@ template } // namespace detail NLOHMANN_JSON_NAMESPACE_END -// #include -// __ _____ _____ _____ -// __| | __| | | | JSON for Modern C++ -// | | |__ | | | | | | version 3.12.0 -// |_____|_____|_____|_|___| https://github.com/nlohmann/json -// -// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann -// SPDX-License-Identifier: MIT - - - -#include // basic_string, char_traits, string -#include // decay, integral_constant, is_array, is_lvalue_reference, is_pointer, is_same, remove_reference -#include // forward - -// #include // #include +// #include -#if NLOHMANN_VIEW_HAS_CPP_17 - #include // string_view -#endif + +// 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 @@ -2437,337 +2595,214 @@ namespace detail namespace view { -/// how a document takes its input -enum class input_kind +inline document_data::edit_state& edit_state_of(document_data& d) { - move_string, ///< rvalue std::string: owned without a copy - c_string, ///< const char* (NUL-terminated): borrowed - char_array, ///< char array (e.g. a string literal): borrowed - borrow_range, ///< lvalue contiguous byte container, or std::string_view: borrowed - copy_range, ///< rvalue contiguous byte container: copied - adapter, ///< anything else parse() accepts (streams, wide strings, ...): read into a buffer -}; + if (!d.edits) + { + d.edits.reset(new document_data::edit_state()); // NOLINT(cppcoreguidelines-owning-memory): owned by the unique_ptr + } + return *d.edits; +} -template -struct classify_input +/// k consecutive nodes that never move (new values and blocks) +inline node* alloc_nodes(document_data& d, std::size_t k) { - using R = typename std::remove_reference::type; - using D = typename std::decay::type; - static constexpr bool is_rvalue = !std::is_lvalue_reference::value; - static constexpr bool is_bytes = is_contiguous_byte_container::value; -#if NLOHMANN_VIEW_HAS_CPP_17 - static constexpr bool is_string_view = std::is_same::value; -#else - static constexpr bool is_string_view = false; -#endif - // NOLINTBEGIN(readability-avoid-nested-conditional-operator): a constant expression of C++11 - static constexpr input_kind value = - std::is_array::value ? input_kind::char_array - : std::is_pointer::value ? input_kind::c_string - : (is_rvalue && std::is_same::value) ? input_kind::move_string - : (is_bytes && (!is_rvalue || is_string_view)) ? input_kind::borrow_range - : is_bytes ? input_kind::copy_range - : input_kind::adapter; - // NOLINTEND(readability-avoid-nested-conditional-operator) -}; + 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]()); // NOLINT(cppcoreguidelines-avoid-c-arrays,hicpp-avoid-c-arrays,modernize-avoid-c-arrays) + 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; +} -/// std::basic_string guarantees a NUL at data()[size()] (the parser's sentinel) -template -struct is_std_string : std::false_type {}; - -template -struct is_std_string> : std::true_type {}; - -/// drain a json input adapter (UTF-16/32 inputs arrive as UTF-8) -template -std::string collect_adapter(Adapter ia) +/// 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) { - std::string buf; + 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"); // LCOV_EXCL_LINE (4 GiB) + } + std::unique_ptr fresh(new char[cap]); // NOLINT(cppcoreguidelines-avoid-c-arrays,hicpp-avoid-c-arrays,modernize-avoid-c-arrays) + 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 + } + make_link(*o, document_data::deref(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; // LCOV_EXCL_LINE (an array/object with elements is in the index or a new value) + } + --it; + lo = it->first; + hi = lo + lo->next; + if (!lt(target, hi)) + { + return nullptr; // LCOV_EXCL_LINE (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 (;;) { - const auto ch = ia.get_character(); - if (ch == std::char_traits::eof()) + if (!is_container(*c)) { - break; + return nullptr; // LCOV_EXCL_LINE (the value is inside c) } - buf.push_back(static_cast(ch)); + 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; } - return buf; } } // namespace view } // namespace detail NLOHMANN_JSON_NAMESPACE_END -// #include -// __ _____ _____ _____ -// __| | __| | | | JSON for Modern C++ -// | | |__ | | | | | | version 3.12.0 -// |_____|_____|_____|_|___| https://github.com/nlohmann/json -// -// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann -// SPDX-License-Identifier: MIT - - - -#include // ptrdiff_t, size_t -#include // forward_iterator_tag -#include // string, to_string -#include // enable_if - -// #include -// #include - // #include -// #include - -// #include - - -NLOHMANN_JSON_NAMESPACE_BEGIN -namespace detail -{ -namespace view -{ - -/// the result of view_iterator::operator->: keeps the view alive for the -/// duration of the member access -template -class arrow_proxy -{ - public: - explicit arrow_proxy(const View& v) noexcept - : m_view(v) - {} - - const View* operator->() const noexcept - { - return &m_view; - } - - private: - View m_view; -}; - -/*! -@brief forward iterator over the elements of a basic_json_view - -Iterates over the elements of an array or the member values of an object, in -document order; key() gives the key of an object member. As for basic_json, a -primitive value iterates as a range of one element (itself), and null as an -empty range. -*/ -template -class view_iterator -{ - public: - using iterator_category = std::forward_iterator_tag; - using value_type = View; - using difference_type = std::ptrdiff_t; - using pointer = arrow_proxy; - using reference = View; - using string_view_t = typename View::string_view_t; - - view_iterator() noexcept = default; - - /// @param[in] pos the element, or the key of the member - /// @param[in] object whether pos is a key (its value is the next node) - view_iterator(const document_data* d, const node* pos, bool object) noexcept - : m_doc(d), m_pos(pos), m_value_offset(object ? 1 : 0) - {} - - NLOHMANN_VIEW_ALWAYS_INLINE View operator*() const noexcept - { - return View(m_doc, m_pos + m_value_offset); - } - - pointer operator->() const noexcept - { - return pointer(**this); - } - - NLOHMANN_VIEW_ALWAYS_INLINE view_iterator& operator++() noexcept - { - m_pos = document_data::after(m_pos + m_value_offset); - return *this; - } - - view_iterator operator++(int) noexcept - { - const view_iterator r = *this; - ++*this; - return r; - } - - friend bool operator==(const view_iterator& a, const view_iterator& b) noexcept - { - return a.m_pos == b.m_pos; - } - - friend bool operator!=(const view_iterator& a, const view_iterator& b) noexcept - { - return a.m_pos != b.m_pos; - } - - /// the key of the current object member; throws invalid_iterator.207 for - /// other iterators, like basic_json's iterators - string_view_t key() const - { - if (NLOHMANN_VIEW_UNLIKELY(m_value_offset == 0)) - { - throw_invalid_iterator(207, "cannot use key() for non-object iterators"); - } - return string_view_t(m_doc->str(*m_pos), m_pos->len); - } - - View value() const noexcept - { - return **this; - } - - /// whether the iterator runs over the members of an object - bool is_object_iterator() const noexcept - { - return m_value_offset != 0; - } - - private: - const document_data* m_doc = nullptr; - const node* m_pos = nullptr; - std::size_t m_value_offset = 0; ///< 1 for objects: the value follows its key -}; - -/*! -@brief a (key, value) item of basic_json_view::items() - -The key of an array element is its index, as for basic_json::items(). -Supports structured bindings: for (const auto [key, value] : view.items()) -*/ -template -class view_item -{ - public: - using string_view_t = typename View::string_view_t; - using iterator = view_iterator; - - view_item(const iterator& it, std::size_t index) - : m_it(it) - { - if (!it.is_object_iterator()) - { - m_index = std::to_string(index); - } - } - - /// the member key, or the element index for arrays - string_view_t key() const - { - if (m_it.is_object_iterator()) - { - return m_it.key(); - } - return string_view_t(m_index.data(), m_index.size()); - } - - View value() const noexcept - { - return *m_it; - } - - template::type = 0> - string_view_t get() const - { - return key(); - } - - template::type = 0> - View get() const noexcept - { - return value(); - } - - private: - iterator m_it; - std::string m_index{}; // NOLINT(readability-redundant-member-init) -}; - -/// the range returned by basic_json_view::items() -template -class view_items -{ - public: - using item = view_item; - - class iterator - { - public: - using iterator_category = std::forward_iterator_tag; - using value_type = item; - using difference_type = std::ptrdiff_t; - using pointer = void; - using reference = item; - - explicit iterator(const view_iterator& it) noexcept - : m_it(it) - {} - - item operator*() const - { - return item(m_it, m_index); - } - - iterator& operator++() noexcept - { - ++m_it; - ++m_index; - return *this; - } - - iterator operator++(int) noexcept - { - const iterator r = *this; - ++*this; - return r; - } - - friend bool operator==(const iterator& a, const iterator& b) noexcept - { - return a.m_it == b.m_it; - } - - friend bool operator!=(const iterator& a, const iterator& b) noexcept - { - return a.m_it != b.m_it; - } - - private: - view_iterator m_it; - std::size_t m_index = 0; - }; - - explicit view_items(const View& v) noexcept - : m_view(v) - {} - - iterator begin() const noexcept - { - return iterator(m_view.begin()); - } - - iterator end() const noexcept - { - return iterator(m_view.end()); - } - - private: - View m_view; -}; - -} // namespace view -} // namespace detail -NLOHMANN_JSON_NAMESPACE_END - // #include // __ _____ _____ _____ // __| | __| | | | JSON for Modern C++ @@ -2993,18 +3028,20 @@ class short_key /// 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 -inline const node* find_member(const document_data& d, const node* object, const char* key, std::size_t n) noexcept +template +const node* find_member(const document_data& d, const node* object, const char* key, std::size_t n) noexcept { - if (NLOHMANN_VIEW_UNLIKELY(object->extra != 0)) + 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 + return find_indexed(d, object, key, n); // a large object (whose members have not been edited) } - const node* const end = document_data::child_end(object); + 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 = document_data::first_child(object); m != end; m = document_data::after(m + 1)) + 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) { @@ -3013,7 +3050,7 @@ inline const node* find_member(const document_data& d, const node* object, const } return nullptr; } - for (const node* m = document_data::first_child(object); m != end; m = document_data::after(m + 1)) + 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) { @@ -3023,10 +3060,16 @@ inline const node* find_member(const document_data& d, const node* object, const return nullptr; } -/// the element of an array at an index below its size -inline const node* element_at(const node* array, std::size_t idx) noexcept +/// 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 = document_data::first_child(array); + 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); @@ -3034,13 +3077,14 @@ inline const node* element_at(const node* array, std::size_t idx) noexcept return e; } -/// the last element of a non-empty array, or the key of the last member of a -/// non-empty object -inline const node* last_child(const node* container) noexcept +/// 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 = document_data::child_end(container); - const node* last = document_data::first_child(container); + 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; @@ -3054,6 +3098,1117 @@ NLOHMANN_JSON_NAMESPACE_END // #include +// #include + + +NLOHMANN_JSON_NAMESPACE_BEGIN + +template +class basic_json_view; + +namespace detail +{ +namespace view +{ + +/// the index of the first true condition (the number of conditions if none is) +template +struct first_true : std::integral_constant {}; + +template +struct first_true : std::integral_constant < int, 1 + first_true::value > {}; + +/// 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); + } + + /// insert into an array before position idx (idx <= size()); returns a + /// view of the new element + template + View insert(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(309, "cannot use insert() with ", array.type_name()); + } + check_index(idx, a->len + 1); + const encoded e = encode(std::forward(value)); + node* const slot = new_slot(e); + node* const h = block_of(m_doc, a, 1); + std::memmove(h + 2 + idx, h + 1 + idx, (h->next - 1 - idx) * sizeof(node)); + make_link(h[1 + idx], slot); + ++h->next; + ++h->len; + ++a->len; + return View(&m_doc, slot); + } + + /// remove all members with this key; returns their number + std::size_t erase(const View& object, string_view_t key) + { + node* const o = own(object); + if (o->kind != static_cast(value_t::object)) + { + throw_type_error(307, "cannot use erase() with ", object.type_name()); + } + for (const node* k = nav::first(m_doc, o), *end = nav::end(m_doc, o); k != end; k = document_data::after(k + 1)) + { + if (key_equals(*k, key)) + { + return erase_members(o, key, false); + } + } + return 0; + } + + /// remove an array element + void erase(const View& array, std::size_t idx) + { + node* const a = own(array); + if (a->kind != static_cast(value_t::array)) + { + throw_type_error(307, "cannot use erase() with ", array.type_name()); + } + check_index(idx, a->len); + node* const h = block_of(m_doc, a, 0); + std::memmove(h + 1 + idx, h + 2 + idx, (h->next - 2 - idx) * sizeof(node)); + --h->next; + --h->len; + --a->len; + } + + 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::value, + std::is_same::value, + std::is_same::value, + std::is_same::value, + std::is_arithmetic::value, + std::is_convertible::value>::value> {}); + } + + /// 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::value, std::is_signed::value>::value> {}); + 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++ +// | | |__ | | | | | | version 3.12.0 +// |_____|_____|_____|_|___| https://github.com/nlohmann/json +// +// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann +// SPDX-License-Identifier: MIT + + + +#include // basic_string, char_traits, string +#include // decay, integral_constant, is_array, is_lvalue_reference, is_pointer, is_same, remove_reference +#include // forward + +// #include +// #include + + +#if NLOHMANN_VIEW_HAS_CPP_17 + #include // string_view +#endif + +NLOHMANN_JSON_NAMESPACE_BEGIN +namespace detail +{ +namespace view +{ + +/// how a document takes its input +enum class input_kind +{ + move_string, ///< rvalue std::string: owned without a copy + c_string, ///< const char* (NUL-terminated): borrowed + char_array, ///< char array (e.g. a string literal): borrowed + borrow_range, ///< lvalue contiguous byte container, or std::string_view: borrowed + copy_range, ///< rvalue contiguous byte container: copied + adapter, ///< anything else parse() accepts (streams, wide strings, ...): read into a buffer +}; + +template +struct classify_input +{ + using R = typename std::remove_reference::type; + using D = typename std::decay::type; + static constexpr bool is_rvalue = !std::is_lvalue_reference::value; + static constexpr bool is_bytes = is_contiguous_byte_container::value; +#if NLOHMANN_VIEW_HAS_CPP_17 + static constexpr bool is_string_view = std::is_same::value; +#else + static constexpr bool is_string_view = false; +#endif + // NOLINTBEGIN(readability-avoid-nested-conditional-operator): a constant expression of C++11 + static constexpr input_kind value = + std::is_array::value ? input_kind::char_array + : std::is_pointer::value ? input_kind::c_string + : (is_rvalue && std::is_same::value) ? input_kind::move_string + : (is_bytes && (!is_rvalue || is_string_view)) ? input_kind::borrow_range + : is_bytes ? input_kind::copy_range + : input_kind::adapter; + // NOLINTEND(readability-avoid-nested-conditional-operator) +}; + +/// std::basic_string guarantees a NUL at data()[size()] (the parser's sentinel) +template +struct is_std_string : std::false_type {}; + +template +struct is_std_string> : std::true_type {}; + +/// drain a json input adapter (UTF-16/32 inputs arrive as UTF-8) +template +std::string collect_adapter(Adapter ia) +{ + std::string buf; + for (;;) + { + const auto ch = ia.get_character(); + if (ch == std::char_traits::eof()) + { + break; + } + buf.push_back(static_cast(ch)); + } + return buf; +} + +} // namespace view +} // namespace detail +NLOHMANN_JSON_NAMESPACE_END + +// #include +// __ _____ _____ _____ +// __| | __| | | | JSON for Modern C++ +// | | |__ | | | | | | version 3.12.0 +// |_____|_____|_____|_|___| https://github.com/nlohmann/json +// +// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann +// SPDX-License-Identifier: MIT + + + +#include // ptrdiff_t, size_t +#include // forward_iterator_tag +#include // string, to_string +#include // enable_if + +// #include +// #include + +// #include + +// #include + +// #include + + +NLOHMANN_JSON_NAMESPACE_BEGIN +namespace detail +{ +namespace view +{ + +/// the result of view_iterator::operator->: keeps the view alive for the +/// duration of the member access +template +class arrow_proxy +{ + public: + explicit arrow_proxy(const View& v) noexcept + : m_view(v) + {} + + const View* operator->() const noexcept + { + return &m_view; + } + + private: + View m_view; +}; + +/*! +@brief forward iterator over the elements of a basic_json_view + +Iterates over the elements of an array or the member values of an object, in +document order; key() gives the key of an object member. As for basic_json, a +primitive value iterates as a range of one element (itself), and null as an +empty range. +*/ +template +class view_iterator +{ + public: + using iterator_category = std::forward_iterator_tag; + using value_type = View; + using difference_type = std::ptrdiff_t; + using pointer = arrow_proxy; + using reference = View; + using string_view_t = typename View::string_view_t; + + view_iterator() noexcept = default; + + /// @param[in] pos the element, or the key of the member + /// @param[in] object whether pos is a key (its value is the next node) + view_iterator(const document_data* d, const node* pos, bool object) noexcept + : m_doc(d), m_pos(pos), m_value_offset(object ? 1 : 0) + {} + + NLOHMANN_VIEW_ALWAYS_INLINE View operator*() const noexcept + { + return View(m_doc, View::navigation::value(m_pos + m_value_offset)); + } + + pointer operator->() const noexcept + { + return pointer(**this); + } + + NLOHMANN_VIEW_ALWAYS_INLINE view_iterator& operator++() noexcept + { + m_pos = document_data::after(m_pos + m_value_offset); + return *this; + } + + view_iterator operator++(int) noexcept + { + const view_iterator r = *this; + ++*this; + return r; + } + + friend bool operator==(const view_iterator& a, const view_iterator& b) noexcept + { + return a.m_pos == b.m_pos; + } + + friend bool operator!=(const view_iterator& a, const view_iterator& b) noexcept + { + return a.m_pos != b.m_pos; + } + + /// the key of the current object member; throws invalid_iterator.207 for + /// other iterators, like basic_json's iterators + string_view_t key() const + { + if (NLOHMANN_VIEW_UNLIKELY(m_value_offset == 0)) + { + throw_invalid_iterator(207, "cannot use key() for non-object iterators"); + } + return string_view_t(m_doc->str(*m_pos), m_pos->len); + } + + View value() const noexcept + { + return **this; + } + + /// whether the iterator runs over the members of an object + bool is_object_iterator() const noexcept + { + return m_value_offset != 0; + } + + private: + const document_data* m_doc = nullptr; + const node* m_pos = nullptr; + std::size_t m_value_offset = 0; ///< 1 for objects: the value follows its key +}; + +/*! +@brief a (key, value) item of basic_json_view::items() + +The key of an array element is its index, as for basic_json::items(). +Supports structured bindings: for (const auto [key, value] : view.items()) +*/ +template +class view_item +{ + public: + using string_view_t = typename View::string_view_t; + using iterator = view_iterator; + + view_item(const iterator& it, std::size_t index) + : m_it(it) + { + if (!it.is_object_iterator()) + { + m_index = std::to_string(index); + } + } + + /// the member key, or the element index for arrays + string_view_t key() const + { + if (m_it.is_object_iterator()) + { + return m_it.key(); + } + return string_view_t(m_index.data(), m_index.size()); + } + + View value() const noexcept + { + return *m_it; + } + + template::type = 0> + string_view_t get() const + { + return key(); + } + + template::type = 0> + View get() const noexcept + { + return value(); + } + + private: + iterator m_it; + std::string m_index{}; // NOLINT(readability-redundant-member-init) +}; + +/// the range returned by basic_json_view::items() +template +class view_items +{ + public: + using item = view_item; + + class iterator + { + public: + using iterator_category = std::forward_iterator_tag; + using value_type = item; + using difference_type = std::ptrdiff_t; + using pointer = void; + using reference = item; + + explicit iterator(const view_iterator& it) noexcept + : m_it(it) + {} + + item operator*() const + { + return item(m_it, m_index); + } + + iterator& operator++() noexcept + { + ++m_it; + ++m_index; + return *this; + } + + iterator operator++(int) noexcept + { + const iterator r = *this; + ++*this; + return r; + } + + friend bool operator==(const iterator& a, const iterator& b) noexcept + { + return a.m_it == b.m_it; + } + + friend bool operator!=(const iterator& a, const iterator& b) noexcept + { + return a.m_it != b.m_it; + } + + private: + view_iterator m_it; + std::size_t m_index = 0; + }; + + explicit view_items(const View& v) noexcept + : m_view(v) + {} + + iterator begin() const noexcept + { + return iterator(m_view.begin()); + } + + iterator end() const noexcept + { + return iterator(m_view.end()); + } + + private: + View m_view; +}; + +} // namespace view +} // namespace detail +NLOHMANN_JSON_NAMESPACE_END + +// #include + +// #include + // #include // __ _____ _____ _____ // __| | __| | | | JSON for Modern C++ @@ -3089,6 +4244,7 @@ NLOHMANN_JSON_NAMESPACE_END #include // size_t #include // int64_t, uint64_t +#include // numeric_limits #include // string #include // integral_constant @@ -3211,11 +4367,31 @@ NLOHMANN_VIEW_ALWAYS_INLINE FloatType layout_float(const unsigned char* p, const return decimal_to_float(layout_decimal(p, e, int_digits, frac_digits, limit)); } +/// the value of a float set by an edit: its token (the shortest round-trip +/// text, or "nan", "inf", "-inf") in the edit arena +template +NLOHMANN_VIEW_NOINLINE FloatType edited_float(const char* token, const node& n) +{ + if (token[0] == 'n') + { + return std::numeric_limits::quiet_NaN(); + } + if (token[0] == 'i' || (token[0] == '-' && token[1] == 'i')) + { + return token[0] == 'i' ? std::numeric_limits::infinity() : -std::numeric_limits::infinity(); + } + return float_value(token, n); +} + /// the value of the float token of a node, as parse() converts it; floats and /// doubles with at most 19 digits are converted from the digit layout template FloatType float_value(const document_data& d, const node& n) { + if (NLOHMANN_VIEW_UNLIKELY((n.flags & node_flags::storage) == node_flags::edited)) + { + return edited_float(d.str(n), n); + } return float_value(d, n, std::integral_constant::value> {}); } @@ -3260,19 +4436,28 @@ iterative, so the nesting depth is limited by memory only, as for parse(). Without a lexer the handler records no source positions (JSON_DIAGNOSTIC_POSITIONS). */ -template +template BasicJsonType materialize(const document_data& d, const node* n) { using string_t = typename BasicJsonType::string_t; using sax_t = json_sax_dom_parser>; + using nav = navigation; + + struct frame + { + const node* pos; ///< next element, or key of the next member + const node* end; + bool object; + }; BasicJsonType result; sax_t sax(result, true); const string_t no_token{}; - // the ends of the open containers, and whether they are objects - std::vector> open; + std::vector open; for (;;) { + // false positive: n comes from nav::value(), which never returns null for a valid index + // @infer-ignore NULLPTR_DEREFERENCE switch (static_cast(n->kind)) { case value_t::object: @@ -3287,67 +4472,66 @@ BasicJsonType materialize(const document_data& d, const node* n) { sax.start_array(n->len); } - open.emplace_back(document_data::child_end(n), object); - n = document_data::first_child(n); + open.push_back(frame{nav::first(d, n), nav::end(d, n), object}); break; } case value_t::string: { string_t s(d.str(*n), n->len); sax.string(s); - ++n; break; } case value_t::number_integer: sax.number_integer(static_cast(static_cast(integer_bits(*n)))); - ++n; break; case value_t::number_unsigned: sax.number_unsigned(static_cast(integer_bits(*n))); - ++n; break; case value_t::number_float: sax.number_float(float_value(d, *n), no_token); - ++n; break; case value_t::boolean: sax.boolean((n->flags & node_flags::is_true) != 0); - ++n; break; case value_t::null: case value_t::binary: case value_t::discarded: default: sax.null(); - ++n; break; } + + // the next value: close finished containers, then read the key for (;;) { if (open.empty()) { return result; } - if (n != open.back().first) + frame& f = open.back(); + if (f.pos == f.end) { - break; + if (f.object) + { + sax.end_object(); + } + else + { + sax.end_array(); + } + open.pop_back(); + continue; } - if (open.back().second) + const node* entry = f.pos; + if (f.object) { - sax.end_object(); + string_t key(d.str(*entry), entry->len); + sax.key(key); + ++entry; } - else - { - sax.end_array(); - } - open.pop_back(); - } - if (open.back().second) - { - // the key of the next member - string_t key(d.str(*n), n->len); - sax.key(key); - ++n; + n = nav::value(entry); + f.pos = document_data::after(entry); + break; } } } @@ -3657,9 +4841,10 @@ the library's conversion; integers are copied from the source, where they are canonical (except "-0", which parse() reads as 0). The walk is iterative, so the nesting depth is limited by memory only. */ -template +template class view_serializer { + using nav = navigation; using string_t = typename BasicJsonType::string_t; using number_float_t = typename BasicJsonType::number_float_t; @@ -3692,7 +4877,7 @@ class view_serializer else { m_out.put(object ? '{' : '['); - stack.push_back(frame{document_data::first_child(n), document_data::child_end(n), object, true}); + stack.push_back(frame{nav::first(m_doc, n), nav::end(m_doc, n), object, true}); } } else @@ -3734,13 +4919,14 @@ class view_serializer { m_out.put(':'); } - n = f.pos + 1; + n = nav::value(f.pos + 1); + f.pos = document_data::after(f.pos + 1); } else { - n = f.pos; + n = nav::value(f.pos); + f.pos = document_data::after(f.pos); } - f.pos = document_data::after(n); break; } } @@ -3792,9 +4978,9 @@ class view_serializer break; } case value_t::number_float: - if (m_style.source_numbers) + if (m_style.source_numbers && (n.flags & node_flags::storage) != node_flags::edited) { - m_out.put(m_doc.str(n), n.len); + m_out.put(m_doc.str(n), n.len); // (a float set by an edit is written as with shortest) } else { @@ -3841,9 +5027,9 @@ class view_serializer { const char* const s = m_doc.str(n); m_out.put('"'); - if ((n.flags & node_flags::escaped) == 0 && !m_style.ensure_ascii) + if ((n.flags & node_flags::storage) == 0 && !m_style.ensure_ascii) { - // a string without escape sequences has nothing to escape + // a string of the source without escape sequences has nothing to escape m_out.put(s, n.len); } else if (m_style.ensure_ascii) @@ -4194,7 +5380,7 @@ NLOHMANN_JSON_NAMESPACE_END NLOHMANN_JSON_NAMESPACE_BEGIN -template +template class basic_json_document; /*! @@ -4203,11 +5389,13 @@ class basic_json_document; Trivially copyable (two pointers). Valid as long as the document is alive and has not been re-parsed, and as long as a borrowed source text is alive. */ -template +template class basic_json_view { using node = detail::view::node; using document_data = detail::view::document_data; + /// how the index is walked (with edits only for editable documents) + using navigation = detail::view::navigation; public: using value_t = detail::value_t; @@ -4400,7 +5588,7 @@ class basic_json_view { detail::view::throw_type_error(305, "cannot use operator[] with a numeric argument with ", type_name()); } - return idx < m_node->len ? basic_json_view(m_doc, detail::view::element_at(m_node, idx)) : basic_json_view(); + return idx < m_node->len ? basic_json_view(m_doc, navigation::value(detail::view::element_at(*m_doc, m_node, idx))) : basic_json_view(); } /// (an int argument would be ambiguous between size_type and const char*) @@ -4456,7 +5644,7 @@ class basic_json_view { detail::view::throw_out_of_range(401, detail::concat("array index ", std::to_string(idx), " is out of range")); } - return basic_json_view(m_doc, detail::view::element_at(m_node, idx)); + return basic_json_view(m_doc, navigation::value(detail::view::element_at(*m_doc, m_node, idx))); } basic_json_view at(int idx) const @@ -4528,7 +5716,7 @@ class basic_json_view { if (is_structured() && m_node->len != 0) { - return basic_json_view(m_doc, detail::view::last_child(m_node) + (is_object() ? 1 : 0)); + return basic_json_view(m_doc, navigation::value(detail::view::last_child(*m_doc, m_node) + (is_object() ? 1 : 0))); } return front(); } @@ -4545,7 +5733,7 @@ class basic_json_view { return end(); } - const node* const k = detail::view::find_member(*m_doc, m_node, key.data(), key.size()); + const node* const k = detail::view::find_member(*m_doc, m_node, key.data(), key.size()); return k != nullptr ? iterator(m_doc, k, true) : end(); } @@ -4562,7 +5750,7 @@ class basic_json_view /// whether this is an object with a member with this key bool contains(string_view_t key) const { - return is_object() && detail::view::find_member(*m_doc, m_node, key.data(), key.size()) != nullptr; + return is_object() && detail::view::find_member(*m_doc, m_node, key.data(), key.size()) != nullptr; } bool contains(const char* key) const @@ -4609,7 +5797,7 @@ class basic_json_view { if (NLOHMANN_VIEW_LIKELY(is_structured())) { - return iterator(m_doc, document_data::first_child(m_node), is_object()); + return iterator(m_doc, navigation::first(*m_doc, m_node), is_object()); } return iterator(m_doc, m_node, false); } @@ -4618,7 +5806,7 @@ class basic_json_view { if (NLOHMANN_VIEW_LIKELY(is_structured())) { - return iterator(m_doc, document_data::child_end(m_node), is_object()); + return iterator(m_doc, navigation::end(*m_doc, m_node), is_object()); } return iterator(m_doc, (is_null() || is_discarded()) ? m_node : m_node + 1, false); } @@ -4718,7 +5906,7 @@ class basic_json_view style.source_numbers = numbers == number_format::source; // the compact text is about as long as the source text of the value const std::size_t estimate = source_extent() + (style.pretty ? source_extent() / 2 : 0) + 64; - detail::view::view_serializer(*m_doc, out, estimate, style).dump(m_node); + detail::view::view_serializer(*m_doc, out, estimate, style).dump(m_node); return out; } @@ -4752,6 +5940,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) { @@ -4785,7 +5986,7 @@ class basic_json_view { return BasicJsonType(value_t::discarded); } - return detail::view::materialize(*m_doc, m_node); + return detail::view::materialize(*m_doc, m_node); } /// byte offset of this value in the source text (for strings: of the @@ -4793,12 +5994,14 @@ class basic_json_view /// a discarded view and for strings with escapes, which are decoded std::size_t source_offset() const noexcept { - return m_node != nullptr && (m_node->flags & detail::view::node_flags::storage) == 0 + return m_node != nullptr && (m_node->flags & (detail::view::node_flags::storage | detail::view::node_flags::moved | detail::view::node_flags::is_new)) == 0 ? m_node->off : static_cast(-1); } private: - template friend class basic_json_document; + 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 @@ -4816,6 +6019,11 @@ class basic_json_view /// decoded strings) std::size_t source_extent() const noexcept { + if (Editable && m_doc->edits != nullptr) + { + // positions of moved and new values are not source offsets + return m_node == m_doc->tape ? m_doc->size + m_doc->edits->text_used : 64; + } const node* const next = document_data::after(m_node); const bool in_source = (m_node->flags & detail::view::node_flags::storage) == 0; if (!in_source) @@ -4833,8 +6041,8 @@ class basic_json_view /// (object required) NLOHMANN_VIEW_ALWAYS_INLINE basic_json_view lookup(string_view_t key) const noexcept { - const node* const k = detail::view::find_member(*m_doc, m_node, key.data(), key.size()); - return k != nullptr ? basic_json_view(m_doc, k + 1) : basic_json_view(); + const node* const k = detail::view::find_member(*m_doc, m_node, key.data(), key.size()); + return k != nullptr ? basic_json_view(m_doc, navigation::value(k + 1)) : basic_json_view(); } // --- get() dispatch --- @@ -4925,7 +6133,7 @@ Borrowed parses keep a pointer to the caller's text, which must outlive the document. Owned parses (parse_copy, rvalue std::string, streams, and inputs that are not contiguous byte ranges) keep their own copy. */ -template +template class basic_json_document { using document_data = detail::view::document_data; @@ -4934,7 +6142,7 @@ class basic_json_document "json_view supports 64-bit integer types only"); public: - using view_type = basic_json_view; + using view_type = basic_json_view; using value_t = detail::value_t; /// an empty (discarded) document @@ -5059,7 +6267,8 @@ class basic_json_document + (m_data->tape != m_data->inline_tape ? m_data->tape_cap * sizeof(detail::view::node) : 0) + m_data->arena.capacity() + m_data->owned.capacity() + (m_data->indexes.capacity() * sizeof(document_data::object_index)) + (m_data->index_slots.capacity() * sizeof(std::uint32_t)) - + (m_data->large_objects.capacity() * sizeof(std::uint32_t)); + + (m_data->large_objects.capacity() * sizeof(std::uint32_t)) + + (m_data->edits != nullptr ? m_data->edits->bytes : 0); } /// release unused capacity of the index and the decoded strings; like @@ -5078,7 +6287,8 @@ class basic_json_document // unchanged const bool shrink_arena = d.arena.capacity() > d.arena.size(); std::string arena(shrink_arena ? d.arena : std::string()); - const bool shrink_tape = d.tape != d.inline_tape && d.tape_size != d.tape_cap; + // (edits link to the nodes of the index, which then stays in place) + const bool shrink_tape = d.tape != d.inline_tape && d.tape_size != d.tape_cap && d.edits == nullptr; const bool into_header = d.tape_size <= d.inline_cap; node* fresh = (shrink_tape && !into_header) ? static_cast(::operator new (d.tape_size * sizeof(node))) : d.inline_tape; @@ -5096,9 +6306,163 @@ 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)); + } + + /// insert into an array before position idx (idx <= size()); returns a + /// view of the new element + template < typename I, typename V, typename std::enable_if < std::is_integral::value && !std::is_same::value, int >::type = 0 > + view_type insert(view_type array, I idx, V && value) + { + return editor().insert(array, index(idx), std::forward(value)); + } + + /// remove all members with this key; returns their number + std::size_t erase(view_type object, string_view_t key) + { + return editor().erase(object, key); + } + + /// remove an array element + template < typename I, typename std::enable_if < std::is_integral::value && !std::is_same::value, int >::type = 0 > + void erase(view_type array, I idx) + { + editor().erase(array, index(idx)); + } + + /// remove the value at a JSON pointer; returns the number of removed + /// values + std::size_t erase(const json_pointer& ptr) + { + if (ptr.empty()) + { + detail::view::throw_out_of_range(405, "JSON pointer has no parent"); + } + const view_type parent = root().at(ptr.parent_pointer()); + const auto& token = ptr.back(); + if (parent.is_array()) + { + erase(parent, pointer_index(token)); + return 1; + } + return erase(parent, string_view_t(token.data(), token.size())); + } + 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) { @@ -5128,6 +6492,8 @@ class basic_json_document d.src = src; d.size = size; d.tape_size = 0; + d.edits.reset(); // (views of the previous text end here anyway) + d.base[2] = nullptr; d.arena.clear(); d.indexes.clear(); d.index_slots.clear(); @@ -5265,6 +6631,14 @@ 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 diff --git a/tests/src/unit-json_view_edit.cpp b/tests/src/unit-json_view_edit.cpp new file mode 100644 index 000000000..5e0ece881 --- /dev/null +++ b/tests/src/unit-json_view_edit.cpp @@ -0,0 +1,565 @@ +// __ _____ _____ _____ +// __| | __| | | | 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 +#include + +namespace +{ +std::uint32_t rng() +{ + static std::mt19937 generator(5295); // NOLINT(cert-msc32-c,cert-msc51-cpp,bugprone-random-generator-seed): reproducible + // result_type is std::uint_fast32_t, which may be wider than 32 bits + const std::mt19937::result_type value = generator(); + return static_cast(value); +} + +int r(int n) +{ + return static_cast(rng() % static_cast(n)); +} + +int counter = 0; + +std::uint64_t bits(double x) +{ + std::uint64_t b = 0; + std::memcpy(&b, &x, sizeof(b)); + return b; +} + +std::string random_string() +{ + static const std::array 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[static_cast(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()) + { + CHECK(bits(v.get()) == bits(j.get())); + } + 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 == 6 && target.is_object() && !target.empty()) // erase a member + { + const std::string key = std::next(target.begin(), r(static_cast(target.size()))).key(); + if (r(2) == 0) + { + d.erase(tv, key); + } + else + { + d.erase(p / key); + } + j[p].erase(key); + } + 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 == 8 && target.is_array()) // insert + { + const auto i = static_cast(r(static_cast(target.size()) + 1)); + const ordered_json v = random_value(2); + d.insert(tv, i, v); + j[p].insert(j[p].begin() + static_cast(i), v); + } + else if (op == 9 && target.is_array() && !target.empty()) // erase an element + { + const auto i = static_cast(r(static_cast(target.size()))); + if (r(2) == 0) + { + d.erase(tv, i); + } + else + { + d.erase(p / i); + } + j[p].erase(i); + } + else if (op == 10 && target.is_array() && !target.empty()) // assign an element + { + const auto 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 +{ +#if !defined(JSON_NOEXCEPTION) +// the exception a call throws, or "" if it throws none +std::string exception_of_call(const std::function& f) +{ + try + { + f(); + } + catch (const json::exception& e) + { + return e.what(); + } + return ""; +} +#endif +} // namespace + +TEST_CASE("json_view edits: errors") +{ + SECTION("an empty document") + { + json_editable_document d; + CHECK_THROWS_WITH_AS(d.set(d.root(), 1), "[json.exception.invalid_iterator.202] view does not belong to this document", json::invalid_iterator&); + } + + 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.insert(root["n"], 0, 1), "[json.exception.type_error.309] cannot use insert() with number", json::type_error&); + CHECK_THROWS_WITH_AS(d.insert(root["a"], 3, 1), "[json.exception.out_of_range.401] array index 3 is out of range", json::out_of_range&); + CHECK_THROWS_WITH_AS(d.erase(root["n"], "k"), "[json.exception.type_error.307] cannot use erase() with number", json::type_error&); + CHECK_THROWS_WITH_AS(d.erase(root["o"], 0), "[json.exception.type_error.307] cannot use erase() with object", json::type_error&); + CHECK_THROWS_WITH_AS(d.erase(root["a"], 2), "[json.exception.out_of_range.401] array index 2 is out of range", json::out_of_range&); + CHECK_THROWS_WITH_AS(d.erase(json::json_pointer("")), "[json.exception.out_of_range.405] JSON pointer has no parent", json::out_of_range&); + CHECK_THROWS_WITH_AS(d.erase(json::json_pointer("/missing/x")), "[json.exception.out_of_range.403] key 'missing' not found", json::out_of_range&); + CHECK_THROWS_WITH_AS(d.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&); + +#if !defined(JSON_NOEXCEPTION) + // 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); + } +#endif + // 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("a value that is no longer part of the document") + { + json_editable_document d = json_editable_document::parse("[[[1,2]]]"); + const json_editable_view inner = d.root()[0][0]; + d.set(d.root()[0], json::array({7})); + d.set(inner, 5); + CHECK(d.root().dump() == "[[7]]"); + CHECK(inner.get() == 5); + } + + SECTION("views keep referring to their value") + { + json_editable_document d = json_editable_document::parse(R"({"a": [10, 20, 30], "b": {"c": "text"}})"); + const json_editable_view a = d.root()["a"]; + const json_editable_view twenty = a[1]; + const json_editable_view c = d.root()["b"]["c"]; + d.insert(a, 0, 5); + d.push_back(a, 40); + CHECK(twenty.get() == 20); + CHECK(a[2].get() == 20); + d.erase(a, 2); + CHECK(twenty.get() == 20); // an erased value keeps its last value + d.set(c, 7); + CHECK(c.get() == 7); // a held view sees an assignment + d.set(d.root()["b"], json::array({1, 2})); + CHECK(d.root()["b"].dump() == "[1,2]"); + CHECK(d.root().dump() == R"({"a":[5,10,30,40],"b":[1,2]})"); + CHECK(d.root()["a"][0].source_offset() == static_cast(-1)); // a new value + CHECK(d.root()["a"][1].source_offset() != static_cast(-1)); + } + + 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(), (std::numeric_limits::max)()); + d.push_back(d.root(), (std::numeric_limits::min)()); + 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})"); + CHECK(d.erase(json::json_pointer("/x/0")) == 1); + CHECK(d.erase(json::json_pointer("/y")) == 1); + CHECK(d.erase(json::json_pointer("/nothing")) == 0); + CHECK(d.root().dump() == R"({"x":[2,3,4]})"); + } + + 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})"); + d = json_editable_document::parse(R"({"a": 1, "b": 2, "a": 3})"); + CHECK(d.erase(d.root(), "a") == 2); + CHECK(d.root().dump() == R"({"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); + d.erase(d.root(), "k0"); + CHECK(!d.root().contains("k0")); + CHECK(d.root().size() == 200); + } + + 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]"); + } +}