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]"); + } +}