diff --git a/.github/labeler.yml b/.github/labeler.yml index 3e8914a14..014d9f9bb 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -67,8 +67,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 04b72f8cc..4c218fa57 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/mkdocs/docs/api/basic_json_document/erase.md b/docs/mkdocs/docs/api/basic_json_document/erase.md new file mode 100644 index 000000000..4cbc50d19 --- /dev/null +++ b/docs/mkdocs/docs/api/basic_json_document/erase.md @@ -0,0 +1,129 @@ +# 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). `ptr` must not be empty + ([`root()`](root.md) itself cannot be erased), and the *parent* of the target must already exist. The target is + removed as in 1. if the parent is an object (returns the number of removed members, `#!cpp 0` or more) and as in 2. + if it is an array (always returns `#!cpp 1`). + +## 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. + +Overloads 1. and 2. also throw [`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. Overload 3. never throws `invalid_iterator.202`: it takes no view, and it +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. What does change is the container's sequence of +*links* to its elements: removing an element of `array` (2.) shifts the links to the elements after it by one, as +[`insert`](insert.md) does in the other direction. *Every* iterator already taken over `array`/`object` is invalidated +by an erase, not only those at or after the removed element, since the container may now use a different sequence. + +## 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 b64b96e15..66e83b50e 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; ``` @@ -20,6 +20,11 @@ is move-only: a copy would duplicate the whole index, the decoded-string buffer, enough in every use we know of. The index, the decoded strings, and the owned text live in one heap block that the document holds through a `#!cpp std::unique_ptr`; this is also why views survive a move of the document. +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` @@ -27,14 +32,22 @@ document holds through a `#!cpp std::unique_ptr`; this is also why views survive [`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 @@ -51,6 +64,41 @@ document holds through a `#!cpp std::unique_ptr`; this is also why views survive - [**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..ae26ed516 --- /dev/null +++ b/docs/mkdocs/docs/api/basic_json_document/set.md @@ -0,0 +1,203 @@ +# 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. A [null](../basic_json_view/is_null.md) parent becomes what + [`basic_json::operator[]`](../basic_json/operator%5B%5D.md) with a JSON pointer makes of it: an array if the last + reference token is `#!cpp "-"` or consists of digits only (for an index beyond 0, the array is first filled with + null values up to that index), an object otherwise. 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. Likewise, a failure of `value` in 4. leaves a null parent that is set with an +index beyond 0 already filled with the null values before the index. + +## 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); the same errors are + thrown for a null parent and a token of digits (the parent is not changed then), and + [`out_of_range.401`](../../home/exceptions.md#jsonexceptionout_of_range401) if the index is 4294967295 or more. + 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 size of the document, the *first* time (the parent of `target` is looked up from + [`root()`](root.md), and then switches to links) -- see [Notes](#notes). Once the parent has links, `target` is + replaced in constant time: setting every element of a large array one after the other is linear overall. To avoid + the lookup altogether, use 3. (or 2. for an object), which know the parent. +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; for a null parent and an index, linear in the index. + +## 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 909be0751..4d6d7dd05 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; ``` @@ -36,16 +36,30 @@ Moving the document itself does not invalidate its views: the index is heap-allo [`json_pointer`](../json_pointer/index.md). Unlike `basic_json`, there are no implicit conversion operators (no `#!cpp operator ValueType()`), and no `#!cpp get_ptr()` or `#!cpp get_ref()`; see [`get`](get.md). +`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 f3a63022e..83b53de57 100644 --- a/docs/mkdocs/docs/features/json_view.md +++ b/docs/mkdocs/docs/features/json_view.md @@ -249,14 +249,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 37eccd04a..5ee7fcc5d 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). There is no node for a discarded value: a [discarded](../api/basic_json_view/is_discarded.md) view has none | +| 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; bit 3: moved, bit 4: new, bit 5: linked (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 key 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 | @@ -245,6 +245,23 @@ 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. +- A value that an entry of such a sequence links to gets the flag *linked* (bit 5): its extent in the parsed layout + (`next`) no longer matters, so a scalar can replace an array or object there in place. +- 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 6a57c1b0a..4fa8eb062 100644 --- a/docs/mkdocs/docs/home/exceptions.md +++ b/docs/mkdocs/docs/home/exceptions.md @@ -812,6 +812,25 @@ 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), +[`basic_json_document::push_back`](../api/basic_json_document/push_back.md), and +[`basic_json_document::insert`](../api/basic_json_document/insert.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 @@ -1066,13 +1085,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 4294967280 bytes (4 GiB minus 16 bytes) or more. +so they do not support an input of 4294967280 bytes (4 GiB minus 16 bytes) 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 4294967280 bytes 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 aa6d5d3b5..301250f2d 100644 --- a/docs/mkdocs/mkdocs.yml +++ b/docs/mkdocs/mkdocs.yml @@ -239,14 +239,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: @@ -308,6 +312,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 @@ -349,6 +355,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 31c4bb175..e31400a1e 100644 --- a/include/nlohmann/detail/view/document_data.hpp +++ b/include/nlohmann/detail/view/document_data.hpp @@ -13,6 +13,8 @@ #include // uint32_t #include // memcmp, memcpy #include // numeric_limits +#include // map +#include // unique_ptr #include // bad_alloc, operator new, placement new #include // string #include // vector @@ -52,9 +54,32 @@ struct document_data std::vector indexes; std::vector index_slots; std::vector large_objects; ///< positions of the objects to index (noted while parsing) - 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, unused (indexed by flags & node_flags::storage) bool discarded = true; + /// The storage of edits (editable documents only; see edit_storage.hpp). + /// No edit moves or resizes the parsed index, and the nodes of values + /// written by edits never move either, so a view stays valid whenever it + /// was taken, before the first edit or between later ones: 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 + /// One allocation for the header and room for `count` nodes, so that a /// small document needs one allocation instead of two. Large documents get /// a separate node array instead (so it can be trimmed). When a document @@ -97,6 +122,7 @@ struct document_data , indexes() // NOLINT(readability-redundant-member-init) , index_slots() // NOLINT(readability-redundant-member-init) , large_objects() // NOLINT(readability-redundant-member-init) + , edits() // NOLINT(readability-redundant-member-init) {} document_data(const document_data&) = delete; document_data(document_data&&) = delete; @@ -179,6 +205,90 @@ 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 +{ + /// whether the walk follows edits (the template argument as a runtime + /// condition: `Editable && ...` is a constant condition for MSVC, C4127) + static NLOHMANN_VIEW_ALWAYS_INLINE bool editable() noexcept + { + return false; + } + + 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 bool editable() noexcept + { + return true; + } + + 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..ec18793c9 --- /dev/null +++ b/include/nlohmann/detail/view/edit.hpp @@ -0,0 +1,883 @@ +// __ _____ _____ _____ +// __| | __| | | | 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 // 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 // vector + +#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 m_doc.key_equals(k, key.data(), key.size()); + } + + /// 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 (const 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 + { + const std::uint8_t linked = n->flags & node_flags::linked; + *n = node{}; + n->kind = static_cast(k); + n->flags = static_cast(node_flags::is_new | linked); + 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) + { + // an entry of a moved sequence links to the slot: it can take any extent + const std::uint8_t linked = slot->flags & node_flags::linked; + if (e.region == nullptr) + { + if (is_container(*slot) && slot->next > 1 && slot != m_doc.nodes && linked == 0) + { + // 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). Looking + // for the container is linear in the size of the document, so + // links (which are marked in the slot) avoid it. + 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; + slot->flags = static_cast(slot->flags | linked); + 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; + // Everything that can throw happens before the slot changes: a slot + // that is a container without the moved flag would show its old + // elements. reserve_moved() makes the set_moved() below, which sets + // the flag, safe; the entry of `regions` exists already (encode() + // added it), so that the assignment at the end does not allocate. + if (!was_moved) + { + reserve_moved(m_doc); + } + slot->kind = r->kind; + slot->extra = 0; + slot->len = r->len; + slot->next = extent; + // (set_moved() adds the moved flag to a slot that does not have it yet) + slot->flags = static_cast((was_moved ? node_flags::moved | node_flags::is_new : 0) | linked); + 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(); // NOLINT(misc-const-correctness): digits are written through p + 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: + // (an editable document only holds valid UTF-8, whatever the check of the other document was) + check_utf8(from.str(n), n.len); + 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"); + } + } + + // The subtrees are walked with an explicit stack (as materialize() does): + // the nesting depth is limited by memory only, not by the call stack. + + /// number of nodes of a subtree (containers, keys, scalars) + template + static std::size_t count_nodes(const document_data& d, const node* n) + { + using walk = navigation; + struct frame + { + const node* pos; ///< next element, or key of the next member + const node* end; + bool object; + }; + std::vector open; + std::size_t r = 0; + for (;;) + { + ++r; + if (is_container(*n)) + { + open.push_back(frame{walk::first(d, n), walk::end(d, n), n->kind == static_cast(value_t::object)}); + } + // the next value: close finished containers, then step over the key + for (;;) + { + if (open.empty()) + { + return r; + } + frame& f = open.back(); + if (f.pos == f.end) + { + open.pop_back(); + continue; + } + const node* v = f.pos; + if (f.object) + { + ++r; // the key + ++v; + } + f.pos = document_data::after(v); + n = walk::value(v); + break; + } + } + } + + /// copy a subtree (of any document) as a contiguous sequence of + /// count_nodes() nodes + template + void fill_nodes(const document_data& d, const node* n, node* out) + { + using walk = navigation; + struct frame + { + const node* pos; ///< next element, or key of the next member + const node* end; + bool object; + node* self; ///< the container in the copy + }; + std::vector open; + for (;;) + { + if (is_container(*n)) + { + node* const self = out++; + *self = plain_node(static_cast(n->kind)); + self->len = n->len; + open.push_back(frame{walk::first(d, n), walk::end(d, n), n->kind == static_cast(value_t::object), self}); + } + else + { + *out++ = copy_scalar(d, *n); + } + // the next value: close finished containers, then copy the key + for (;;) + { + if (open.empty()) + { + return; + } + frame& f = open.back(); + if (f.pos == f.end) + { + f.self->next = static_cast(out - f.self); + open.pop_back(); + continue; + } + const node* v = f.pos; + if (f.object) + { + *out++ = copy_scalar(d, *v); + ++v; + } + f.pos = document_data::after(v); + n = walk::value(v); + break; + } + } + } + + static std::size_t count_nodes(const BasicJsonType& j) + { + using iterator = typename BasicJsonType::const_iterator; + struct frame + { + iterator pos; + iterator end; + bool object; + }; + std::vector open; + const BasicJsonType* n = &j; + std::size_t r = 0; + for (;;) + { + ++r; + if (n->is_structured()) + { + open.push_back(frame{n->cbegin(), n->cend(), n->is_object()}); + } + for (;;) + { + if (open.empty()) + { + return r; + } + frame& f = open.back(); + if (f.pos == f.end) + { + open.pop_back(); + continue; + } + r += f.object ? 1 : 0; // the key + n = &*f.pos; + ++f.pos; + break; + } + } + } + + void fill_nodes(const BasicJsonType& j, node* out) + { + using iterator = typename BasicJsonType::const_iterator; + struct frame + { + iterator pos; + iterator end; + bool object; + node* self; ///< the container in the copy + }; + std::vector open; + const BasicJsonType* n = &j; + for (;;) + { + if (n->is_structured()) + { + node* const self = out++; + *self = plain_node(n->type()); + self->len = static_cast(n->size()); + open.push_back(frame{n->cbegin(), n->cend(), n->is_object(), self}); + } + else + { + *out++ = json_scalar(*n); + } + for (;;) + { + if (open.empty()) + { + return; + } + frame& f = open.back(); + if (f.pos == f.end) + { + f.self->next = static_cast(out - f.self); + open.pop_back(); + continue; + } + if (f.object) + { + const auto& key = f.pos.key(); + check_utf8(key.data(), key.size()); + *out++ = string_node(key.data(), key.size()); + } + n = &*f.pos; + ++f.pos; + break; + } + } + } + + 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..614a0fb0d --- /dev/null +++ b/include/nlohmann/detail/view/edit_storage.hpp @@ -0,0 +1,281 @@ +// __ _____ _____ _____ +// __| | __| | | | 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,modernize-make-unique): owned by the unique_ptr; std::make_unique is C++14 + } + 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)) + { + if (k >= e.chunk_next / 2) + { + // a large request (half of the next chunk or more) gets a chunk of + // its own, so that the rest of the current chunk stays available + // for smaller ones + std::unique_ptr own(new node[k]()); // NOLINT(cppcoreguidelines-avoid-c-arrays,hicpp-avoid-c-arrays,modernize-avoid-c-arrays) + e.chunks.push_back(std::move(own)); + e.bytes += k * sizeof(node); + return e.chunks.back().get(); + } + // a smaller request leaves the rest of the current chunk unused + // (fewer than k nodes, and k is less than half of the new chunk) + const std::size_t count = 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; +} + +/// The capacity of the edit arena after it grows by n bytes (`used` of `cap` +/// are taken): doubled, or what is needed plus some room, but never more than +/// the 4 GiB - 1 bytes that the 32-bit offsets of nodes can address. An error +/// if n more bytes do not fit even then. +inline std::size_t text_capacity(std::size_t cap, std::size_t used, std::size_t n) +{ + constexpr std::size_t limit = 0xFFFFFFFFu; + if (NLOHMANN_VIEW_UNLIKELY(used > limit || n > limit - used)) + { + throw_out_of_range(416, "edits of 4 GiB or more are not supported by json_document"); + } + const std::size_t needed = used + n; + const std::size_t wanted = needed + (std::min)(limit - needed, std::size_t{256}); + const std::size_t doubled = cap > limit / 2 ? limit : cap * 2; + return (std::max)(doubled, wanted); +} + +/// 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; n is +/// 0 for an empty string (written by an edit, or the key of a new member) +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 = text_capacity(e.text_cap, e.text_used, n); + std::unique_ptr fresh(new char[cap]); // NOLINT(cppcoreguidelines-avoid-c-arrays,hicpp-avoid-c-arrays,modernize-avoid-c-arrays) + if (NLOHMANN_VIEW_LIKELY(e.text_used != 0)) // (0 only for the first buffer, and then texts is empty) + { + 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]; +} + +/// Make room for one more moved container. This is the part of set_moved() +/// that can throw: a caller that changes a node before it calls set_moved() +/// calls this first, so that a failure leaves the node as it was. +inline void reserve_moved(document_data& d) +{ + document_data::edit_state& e = edit_state_of(d); + 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 + // (both conditions are needed: reserve() may allocate more than asked, + // so the two capacities can differ) + e.moved.reserve((e.moved.size() * 2) + 16); + e.moved_cap.reserve((e.moved.size() * 2) + 16); + } +} + +/// let container n take its elements from `seq` (header node first); cannot +/// throw if n is moved already or reserve_moved() was called +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; + } + reserve_moved(d); + 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; + } + reserve_moved(d); // (so that set_moved() below cannot throw: the links are marked before) + 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, const_cast(document_data::deref(c))); // NOLINT(cppcoreguidelines-pro-type-const-cast): the nodes belong to the document + ++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.nodes; + const node* hi = d.nodes + d.node_count; + const node* c = d.nodes; + 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 4275045da..ca790a13d 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 1e9c373c1..10f7ab400 100644 --- a/include/nlohmann/detail/view/lookup.hpp +++ b/include/nlohmann/detail/view/lookup.hpp @@ -88,18 +88,20 @@ class short_key /// nullptr (the search stops at the first match; materialize() and parse() /// keep the last value of a duplicate key instead); 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) && (!nav::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) { @@ -108,7 +110,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 (d.key_equals(*m, key, n)) { @@ -119,11 +121,13 @@ inline const node* find_member(const document_data& d, const node* object, const } /// the number of members of an object with this key (duplicates included) -inline std::size_t count_members(const document_data& d, const node* object, const char* key, std::size_t n) noexcept +template +std::size_t count_members(const document_data& d, const node* object, const char* key, std::size_t n) noexcept { + using nav = navigation; std::size_t result = 0; - const node* const end = document_data::child_end(object); - for (const node* m = document_data::first_child(object); m != end; m = document_data::after(m + 1)) + const node* const end = nav::end(d, object); + for (const node* m = nav::first(d, object); m != end; m = document_data::after(m + 1)) { if (d.key_equals(*m, key, n)) { @@ -151,10 +155,17 @@ SizeType to_index(IntegerType idx) noexcept return (idx < zero || static_cast(result) != idx) ? (std::numeric_limits::max)() : result; } -/// 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); + using nav = navigation; + const node* e = nav::first(d, array); + if (nav::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); @@ -162,13 +173,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 8eb4df8a2..b15bec551 100644 --- a/include/nlohmann/detail/view/materialize.hpp +++ b/include/nlohmann/detail/view/materialize.hpp @@ -36,26 +36,32 @@ 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; + + // the open containers: their next element (or key of the next member), + // their end, and whether they are objects; sized once from the + // document's deepest nesting (for a subtree an upper bound) + struct frame + { + const node* pos; + 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; sized - // once from the document's deepest nesting (for a subtree an upper bound) - struct open_container - { - const node* end; - bool is_object; - }; - std::vector open((std::max)(d.max_depth, std::size_t{1})); + std::vector open((std::max)(d.max_depth, std::size_t{1})); std::size_t depth = 0; 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: @@ -72,69 +78,68 @@ BasicJsonType materialize(const document_data& d, const node* n) } if (NLOHMANN_VIEW_UNLIKELY(depth == open.size())) { - open.resize(depth * 2); // (deeper than the document's recorded nesting) + open.resize(depth * 2); // (deeper than the recorded nesting, e.g. after edits) } - open[depth++] = open_container{document_data::child_end(n), object}; - n = document_data::first_child(n); + open[depth++] = 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 (depth == 0) { return result; } - if (n != open[depth - 1].end) + frame& f = open[depth - 1]; + if (f.pos == f.end) { - break; + if (f.object) + { + sax.end_object(); + } + else + { + sax.end_array(); + } + --depth; + continue; } - if (open[depth - 1].is_object) + 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(); - } - --depth; - } - if (open[depth - 1].is_object) - { - // 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 d4bfed7ea..7a05c8e0a 100644 --- a/include/nlohmann/detail/view/node.hpp +++ b/include/nlohmann/detail/view/node.hpp @@ -33,20 +33,28 @@ constexpr std::size_t max_input_size() noexcept struct node_flags { static constexpr std::uint8_t escaped = 1; ///< string payload lives in the decode arena, not the source - static constexpr std::uint8_t storage = 3; ///< mask: where a string or number token lives (index into document_data::base) + static constexpr std::uint8_t edited = 2; ///< string or number token lives in the edit arena (editable documents) + static constexpr std::uint8_t storage = escaped | edited; ///< mask: where a string or number token lives (index into document_data::base) static constexpr std::uint8_t is_true = 4; ///< boolean value + static constexpr std::uint8_t moved = 8; ///< array/object: the elements live in a separate sequence (editable documents) + static constexpr std::uint8_t is_new = 16; ///< written by an edit: no source position + static constexpr std::uint8_t linked = 32; ///< an entry of a moved sequence links to this value (editable documents): its extent in the parsed layout no longer matters }; +/// kind of an entry of an edited sequence that stands for a value stored +/// elsewhere (the address of the value's node is kept in the len/next bytes) +constexpr std::uint8_t kind_link = 10; + /// One entry of the flat index, in document order. An object's members are /// stored as key node followed by the value's subtree. Integers keep their /// converted 64-bit value in the len/next bytes (the node after a scalar is /// always the next one, and the token length follows from `extra`). struct node { - std::uint8_t kind; ///< value_t + std::uint8_t kind; ///< value_t, or kind_link std::uint8_t flags; ///< node_flags std::uint16_t extra; ///< numbers: integer digits (low byte) and fraction digits (high byte), 255 = "many"; objects: number of the object's key hash index (1-based, 0 = none); otherwise 0 - std::uint32_t off; ///< source offset (string content, number token, literal, bracket); arena offset if node_flags::escaped + std::uint32_t off; ///< source offset (string content, number token, literal, bracket); arena offset if escaped/edited; number of the element sequence if moved std::uint32_t len; ///< string: decoded bytes; float: token bytes; array/object: element count std::uint32_t next; ///< array/object: number of nodes of the subtree (its extent in the enclosing sequence) }; @@ -65,6 +73,23 @@ NLOHMANN_VIEW_ALWAYS_INLINE bool is_container(const node& n) noexcept return 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; +} + +/// let the entry n stand for the value at target (and mark the value) +inline void make_link(node& n, node* target) noexcept +{ + target->flags = static_cast(target->flags | node_flags::linked); + 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: len is its low half, next its high /// half (on little-endian targets the two words are the value in memory) 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 a800a7953..5409b477b 100644 --- a/include/nlohmann/detail/view/serializer.hpp +++ b/include/nlohmann/detail/view/serializer.hpp @@ -123,9 +123,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; @@ -158,7 +159,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 @@ -200,13 +201,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; } } @@ -258,9 +260,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 { @@ -309,9 +311,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/detail/view/value.hpp b/include/nlohmann/detail/view/value.hpp index 6014017cf..1f1eafde4 100644 --- a/include/nlohmann/detail/view/value.hpp +++ b/include/nlohmann/detail/view/value.hpp @@ -114,6 +114,10 @@ struct is_non_bool_arithmetic : conjunction, negation struct is_non_c_string_default : negation::type, const char*>> {}; +/// the integral types accepted as an array index by the edits of a document +template +struct is_non_bool_integral : conjunction, negation>> {}; + /// whether View::get() converts to T without materialize(); get_to() /// assigns these, and passes the others to from_json template diff --git a/include/nlohmann/json_view.hpp b/include/nlohmann/json_view.hpp index 137320bff..da18cd698 100644 --- a/include/nlohmann/json_view.hpp +++ b/include/nlohmann/json_view.hpp @@ -7,14 +7,16 @@ // SPDX-License-Identifier: MIT /****************************************************************************\ - * Zero-copy, read-only view of a parsed JSON text. * + * Zero-copy view of a parsed JSON text. * * * * json_document::parse() builds a flat index of the values of a JSON text * * (16 bytes per value) instead of a tree of basic_json values. Strings and * * numbers stay in the source text; only strings with escapes are decoded, * * into one buffer. json_view is a handle to one value of the document, with * * the read-only part of the basic_json interface; materialize() turns a * - * subtree into the basic_json value that parse() would produce. * + * subtree into the basic_json value that parse() would produce. An editable * + * document (json_editable_document) also has set(), push_back(), insert(), * + * and erase(): edits never write to the source text, and views stay valid. * * * * The source text must outlive a document that borrows it (lvalue byte * * containers, C strings); rvalue strings, streams, and other inputs are * @@ -24,7 +26,7 @@ #ifndef INCLUDE_NLOHMANN_JSON_VIEW_HPP_ #define INCLUDE_NLOHMANN_JSON_VIEW_HPP_ -#include // min +#include // all_of, min #include // array #include // nullptr_t, size_t // IWYU pragma: keep #include // uint32_t @@ -67,6 +69,7 @@ #include #include #include +#include #include #include #include @@ -88,11 +91,13 @@ NLOHMANN_JSON_NAMESPACE_BEGIN 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; @@ -289,7 +294,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(); } /// any other integer type (int, unsigned, long, std::int64_t, ...; a @@ -352,7 +357,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))); } /// any other integer type, see operator[]; negative values are out of @@ -427,7 +432,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(); } @@ -444,7 +449,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(); } @@ -461,7 +466,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 @@ -485,7 +490,7 @@ class basic_json_view /// more than once); 0 if this is not an object size_type count(string_view_t key) const { - return is_object() ? detail::view::count_members(*m_doc, m_node, key.data(), key.size()) : 0; + return is_object() ? detail::view::count_members(*m_doc, m_node, key.data(), key.size()) : 0; } size_type count(const char* key) const @@ -508,7 +513,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); } @@ -517,7 +522,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); } @@ -619,7 +624,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; } @@ -653,6 +658,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) { @@ -686,7 +704,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 @@ -694,12 +712,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 @@ -713,10 +733,25 @@ class basic_json_view return detail::view::view_side(v); } + /// the template argument as a runtime condition: testing it directly in + /// `Editable && ...` makes the condition constant, which MSVC reports as + /// C4127 + static bool editable() noexcept + { + return Editable; + } + /// the number of source bytes of this value (estimated for values with /// decoded strings); the estimate sizes the output buffer of dump() std::size_t source_extent() const noexcept { + if (editable() && m_doc->edits != nullptr) + { + // positions of moved and new values are not source offsets; the root + // (the first node) spans the source text and every edit + const bool is_root = m_node == m_doc->nodes; + return is_root ? m_doc->size + m_doc->edits->text_used : 64; + } const node* const end = m_doc->nodes + m_doc->node_count; if ((m_node->flags & detail::view::node_flags::storage) != 0) { @@ -747,8 +782,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 --- @@ -854,7 +889,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; @@ -863,7 +898,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 @@ -1013,7 +1048,8 @@ class basic_json_document + (m_data->nodes != m_data->inline_nodes ? m_data->node_capacity * 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 @@ -1032,7 +1068,8 @@ class basic_json_document // unchanged const bool shrink_arena = d.arena.capacity() > d.arena.size(); std::vector arena(shrink_arena ? d.arena : std::vector()); - const bool shrink_nodes = d.nodes != d.inline_nodes && d.node_count != d.node_capacity; + // (edits link to the nodes of the index, which then stays in place) + const bool shrink_nodes = d.nodes != d.inline_nodes && d.node_count != d.node_capacity && d.edits == nullptr; const bool into_header = d.node_count <= d.inline_capacity; std::vector indexes(d.indexes.capacity() > d.indexes.size() ? d.indexes : std::vector()); std::vector index_slots(d.index_slots.capacity() > d.index_slots.size() ? d.index_slots : std::vector()); @@ -1063,9 +1100,190 @@ 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::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. A null parent becomes what + /// basic_json's operator[](json_pointer) makes of it: an array for "-" + /// and for digits (padded with nulls up to the index), an object otherwise. + 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_null()) + { + if (token == "-") + { + return push_back(parent, std::forward(value)); + } + const bool digits = std::all_of(token.begin(), token.end(), [](const char c) + { + return c >= '0' && c <= '9'; + }); + if (digits) + { + // (an invalid index is an error before the parent changes) + const std::size_t idx = pointer_index(token); + if (idx >= 0xFFFFFFFFu) + { + detail::view::throw_out_of_range(401, detail::concat("array index ", std::to_string(idx), " is out of range")); + } + for (std::size_t i = 0; i < idx; ++i) + { + push_back(parent, nullptr); + } + return push_back(parent, std::forward(value)); + } + } + 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::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::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) { @@ -1119,6 +1337,8 @@ class basic_json_document d.src = src; d.size = size; d.node_count = 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(); @@ -1288,6 +1508,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 21b023440..7453e82d3 100644 --- a/single_include/nlohmann/json_view.hpp +++ b/single_include/nlohmann/json_view.hpp @@ -7,14 +7,16 @@ // SPDX-License-Identifier: MIT /****************************************************************************\ - * Zero-copy, read-only view of a parsed JSON text. * + * Zero-copy view of a parsed JSON text. * * * * json_document::parse() builds a flat index of the values of a JSON text * * (16 bytes per value) instead of a tree of basic_json values. Strings and * * numbers stay in the source text; only strings with escapes are decoded, * * into one buffer. json_view is a handle to one value of the document, with * * the read-only part of the basic_json interface; materialize() turns a * - * subtree into the basic_json value that parse() would produce. * + * subtree into the basic_json value that parse() would produce. An editable * + * document (json_editable_document) also has set(), push_back(), insert(), * + * and erase(): edits never write to the source text, and views stay valid. * * * * The source text must outlive a document that borrows it (lvalue byte * * containers, C strings); rvalue strings, streams, and other inputs are * @@ -24,7 +26,7 @@ #ifndef INCLUDE_NLOHMANN_JSON_VIEW_HPP_ #define INCLUDE_NLOHMANN_JSON_VIEW_HPP_ -#include // min +#include // all_of, min #include // array #include // nullptr_t, size_t // IWYU pragma: keep #include // uint32_t @@ -102,6 +104,8 @@ #include // uint32_t #include // memcmp, memcpy #include // numeric_limits +#include // map +#include // unique_ptr #include // bad_alloc, operator new, placement new #include // string #include // vector @@ -226,20 +230,28 @@ constexpr std::size_t max_input_size() noexcept struct node_flags { static constexpr std::uint8_t escaped = 1; ///< string payload lives in the decode arena, not the source - static constexpr std::uint8_t storage = 3; ///< mask: where a string or number token lives (index into document_data::base) + static constexpr std::uint8_t edited = 2; ///< string or number token lives in the edit arena (editable documents) + static constexpr std::uint8_t storage = escaped | edited; ///< mask: where a string or number token lives (index into document_data::base) static constexpr std::uint8_t is_true = 4; ///< boolean value + static constexpr std::uint8_t moved = 8; ///< array/object: the elements live in a separate sequence (editable documents) + static constexpr std::uint8_t is_new = 16; ///< written by an edit: no source position + static constexpr std::uint8_t linked = 32; ///< an entry of a moved sequence links to this value (editable documents): its extent in the parsed layout no longer matters }; +/// kind of an entry of an edited sequence that stands for a value stored +/// elsewhere (the address of the value's node is kept in the len/next bytes) +constexpr std::uint8_t kind_link = 10; + /// One entry of the flat index, in document order. An object's members are /// stored as key node followed by the value's subtree. Integers keep their /// converted 64-bit value in the len/next bytes (the node after a scalar is /// always the next one, and the token length follows from `extra`). struct node { - std::uint8_t kind; ///< value_t + std::uint8_t kind; ///< value_t, or kind_link std::uint8_t flags; ///< node_flags std::uint16_t extra; ///< numbers: integer digits (low byte) and fraction digits (high byte), 255 = "many"; objects: number of the object's key hash index (1-based, 0 = none); otherwise 0 - std::uint32_t off; ///< source offset (string content, number token, literal, bracket); arena offset if node_flags::escaped + std::uint32_t off; ///< source offset (string content, number token, literal, bracket); arena offset if escaped/edited; number of the element sequence if moved std::uint32_t len; ///< string: decoded bytes; float: token bytes; array/object: element count std::uint32_t next; ///< array/object: number of nodes of the subtree (its extent in the enclosing sequence) }; @@ -258,6 +270,23 @@ NLOHMANN_VIEW_ALWAYS_INLINE bool is_container(const node& n) noexcept return 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; +} + +/// let the entry n stand for the value at target (and mark the value) +inline void make_link(node& n, node* target) noexcept +{ + target->flags = static_cast(target->flags | node_flags::linked); + 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: len is its low half, next its high /// half (on little-endian targets the two words are the value in memory) NLOHMANN_VIEW_ALWAYS_INLINE std::uint64_t integer_bits(const node& n) noexcept @@ -339,9 +368,32 @@ struct document_data std::vector indexes; std::vector index_slots; std::vector large_objects; ///< positions of the objects to index (noted while parsing) - 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, unused (indexed by flags & node_flags::storage) bool discarded = true; + /// The storage of edits (editable documents only; see edit_storage.hpp). + /// No edit moves or resizes the parsed index, and the nodes of values + /// written by edits never move either, so a view stays valid whenever it + /// was taken, before the first edit or between later ones: 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 + /// One allocation for the header and room for `count` nodes, so that a /// small document needs one allocation instead of two. Large documents get /// a separate node array instead (so it can be trimmed). When a document @@ -384,6 +436,7 @@ struct document_data , indexes() // NOLINT(readability-redundant-member-init) , index_slots() // NOLINT(readability-redundant-member-init) , large_objects() // NOLINT(readability-redundant-member-init) + , edits() // NOLINT(readability-redundant-member-init) {} document_data(const document_data&) = delete; document_data(document_data&&) = delete; @@ -466,6 +519,90 @@ 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 +{ + /// whether the walk follows edits (the template argument as a runtime + /// condition: `Editable && ...` is a constant condition for MSVC, C4127) + static NLOHMANN_VIEW_ALWAYS_INLINE bool editable() noexcept + { + return false; + } + + 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 bool editable() noexcept + { + return true; + } + + 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 @@ -2477,6 +2614,53 @@ 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 // 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 // vector + +// #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++ @@ -2512,6 +2696,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)); @@ -2654,437 +2843,271 @@ 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 // max -#include // size_t -#include // istream, streambuf -#include // basic_string, char_traits, string -#include // decay, integral_constant, is_array, is_const, is_integral, is_lvalue_reference, is_pointer, is_same, remove_cv, remove_extent, remove_pointer, 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, ///< non-const rvalue std::string: owned without a copy - c_string, ///< pointer to single-byte characters (NUL-terminated): borrowed - char_array, ///< array of single-byte characters (e.g. a string literal): borrowed - borrow_range, ///< lvalue contiguous byte container, or std::string_view: borrowed - copy_range, ///< rvalue contiguous byte container (a const rvalue std::string too): copied - wide_c_string, ///< pointer to wide characters (NUL-terminated): converted to UTF-8 into a buffer - adapter, ///< streams, FILE*, wide strings and arrays, and the rest of what the library's input adapter reads: read into a buffer -}; - -/// a single-byte character type (char, signed/unsigned char, char8_t, std::uint8_t, ...) -template -struct is_byte_char : conjunction, std::integral_constant> {}; // NOLINT(bugprone-sizeof-expression): T is any pointee or element type - -/// a character type the library's input adapter converts to UTF-8 -template -struct is_wide_char : disjunction, std::is_same, std::is_same> {}; - -template -struct classify_input -{ - using R = typename std::remove_reference::type; - using D = typename std::decay::type; - using element = typename std::remove_cv::type>::type; - using pointee = typename std::remove_cv::type>::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 - // (arrays and pointers of other types, such as wide-character arrays and - // FILE*, go to the library's input adapter) - static constexpr input_kind value = - (std::is_array::value && is_byte_char::value) ? input_kind::char_array - : std::is_array::value ? input_kind::adapter - : (std::is_pointer::value && is_byte_char::value) ? input_kind::c_string - : (std::is_pointer::value && is_wide_char::value) ? input_kind::wide_c_string - : std::is_pointer::value ? input_kind::adapter - : (is_rvalue && !std::is_const::value && 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) -}; - -/// an integer type other than bool: a length passed where a flag is expected -template -struct is_integer_not_bool : std::is_integral {}; - -template<> -struct is_integer_not_bool : std::false_type {}; - -/// 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 {}; - -/// append what a json input adapter yields (UTF-16/32 inputs arrive as UTF-8) -template -void append_adapter(std::string& buf, Adapter ia) -{ - for (;;) - { - const auto ch = ia.get_character(); - if (ch == std::char_traits::eof()) - { - break; - } - buf.push_back(static_cast(ch)); - } -} - -/// an input adapter made by the document for an input that the library's -/// input_adapter() does not take as one argument (an iterator range, a wide C -/// string), to be appended with append_adapter() -template -struct adapter_input -{ - Adapter ia; -}; - -template -struct is_adapter_input : std::false_type {}; - -template -struct is_adapter_input> : std::true_type {}; - -template -adapter_input::type> make_adapter_input(Adapter&& ia) -{ - return {std::forward(ia)}; -} - -/// append the rest of a stream: its characters need no conversion, so they are -/// read in blocks from the stream buffer, straight into buf (reusing its -/// capacity). As the library's stream adapter, this reads the stream buffer -/// directly and sets eofbit at the end. -inline void append_stream(std::string& buf, std::istream& is) -{ - std::streambuf* const sb = is.rdbuf(); - if (sb == nullptr) - { - return; - } - for (;;) - { - const std::size_t used = buf.size(); - const std::size_t room = (std::max)(buf.capacity() - used, std::size_t{4096}); - buf.resize(used + room); - const std::streamsize got = sb->sgetn(&buf[used], static_cast(room)); - buf.resize(used + static_cast((std::max)(got, std::streamsize{0}))); - if (got <= 0) - { - break; - } - } - is.clear(is.rdstate() | std::ios::eofbit); -} - -} // 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 +// 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 { -/// the result of view_iterator::operator->: keeps the view alive for the -/// duration of the member access -template -class arrow_proxy +inline document_data::edit_state& edit_state_of(document_data& d) { - public: - explicit arrow_proxy(const View& v) noexcept - : m_view(v) - {} - - const View* operator->() const noexcept + if (!d.edits) { - return &m_view; + d.edits.reset(new document_data::edit_state()); // NOLINT(cppcoreguidelines-owning-memory,modernize-make-unique): owned by the unique_ptr; std::make_unique is C++14 } + return *d.edits; +} - 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 +/// k consecutive nodes that never move (new values and blocks) +inline node* alloc_nodes(document_data& d, std::size_t k) { - 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 + document_data::edit_state& e = edit_state_of(d); + if (NLOHMANN_VIEW_UNLIKELY(static_cast(e.chunk_end - e.chunk_cur) < k)) { - 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)) + if (k >= e.chunk_next / 2) { - throw_invalid_iterator(207, "cannot use key() for non-object iterators"); + // a large request (half of the next chunk or more) gets a chunk of + // its own, so that the rest of the current chunk stays available + // for smaller ones + std::unique_ptr own(new node[k]()); // NOLINT(cppcoreguidelines-avoid-c-arrays,hicpp-avoid-c-arrays,modernize-avoid-c-arrays) + e.chunks.push_back(std::move(own)); + e.bytes += k * sizeof(node); + return e.chunks.back().get(); } - return string_view_t(m_doc->str(*m_pos), m_pos->len); + // a smaller request leaves the rest of the current chunk unused + // (fewer than k nodes, and k is less than half of the new chunk) + const std::size_t count = 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; +} - 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 +/// The capacity of the edit arena after it grows by n bytes (`used` of `cap` +/// are taken): doubled, or what is needed plus some room, but never more than +/// the 4 GiB - 1 bytes that the 32-bit offsets of nodes can address. An error +/// if n more bytes do not fit even then. +inline std::size_t text_capacity(std::size_t cap, std::size_t used, std::size_t n) { - 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) + constexpr std::size_t limit = 0xFFFFFFFFu; + if (NLOHMANN_VIEW_UNLIKELY(used > limit || n > limit - used)) { - if (!it.is_object_iterator()) - { - m_index = std::to_string(index); - } + throw_out_of_range(416, "edits of 4 GiB or more are not supported by json_document"); } + const std::size_t needed = used + n; + const std::size_t wanted = needed + (std::min)(limit - needed, std::size_t{256}); + const std::size_t doubled = cap > limit / 2 ? limit : cap * 2; + return (std::max)(doubled, wanted); +} - /// 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 +/// 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; n is +/// 0 for an empty string (written by an edit, or the key of a new member) +inline std::uint32_t append_text(document_data& d, const char* s, std::size_t n) { - public: - using item = view_item; - - class iterator + document_data::edit_state& e = edit_state_of(d); + if (NLOHMANN_VIEW_UNLIKELY(e.text_cap - e.text_used < n)) { - 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 + const std::size_t cap = text_capacity(e.text_cap, e.text_used, n); + std::unique_ptr fresh(new char[cap]); // NOLINT(cppcoreguidelines-avoid-c-arrays,hicpp-avoid-c-arrays,modernize-avoid-c-arrays) + if (NLOHMANN_VIEW_LIKELY(e.text_used != 0)) // (0 only for the first buffer, and then texts is empty) { - return item(m_it, m_index); + std::memcpy(fresh.get(), e.texts.back().get(), e.text_used); } - - 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()); + e.texts.push_back(std::move(fresh)); + e.text_cap = cap; + e.bytes += cap; + d.base[2] = e.texts.back().get(); } - - iterator end() const noexcept + const auto off = static_cast(e.text_used); + if (n != 0) { - return iterator(m_view.end()); + std::memcpy(e.texts.back().get() + e.text_used, s, n); } + e.text_used += n; + return off; +} - private: - View m_view; -}; +/// 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]; +} + +/// Make room for one more moved container. This is the part of set_moved() +/// that can throw: a caller that changes a node before it calls set_moved() +/// calls this first, so that a failure leaves the node as it was. +inline void reserve_moved(document_data& d) +{ + document_data::edit_state& e = edit_state_of(d); + 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 + // (both conditions are needed: reserve() may allocate more than asked, + // so the two capacities can differ) + e.moved.reserve((e.moved.size() * 2) + 16); + e.moved_cap.reserve((e.moved.size() * 2) + 16); + } +} + +/// let container n take its elements from `seq` (header node first); cannot +/// throw if n is moved already or reserve_moved() was called +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; + } + reserve_moved(d); + 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; + } + reserve_moved(d); // (so that set_moved() below cannot throw: the links are marked before) + 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, const_cast(document_data::deref(c))); // NOLINT(cppcoreguidelines-pro-type-const-cast): the nodes belong to the document + ++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.nodes; + const node* hi = d.nodes + d.node_count; + const node* c = d.nodes; + 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 +// #include + // #include // __ _____ _____ _____ // __| | __| | | | JSON for Modern C++ @@ -3337,18 +3360,20 @@ class short_key /// nullptr (the search stops at the first match; materialize() and parse() /// keep the last value of a duplicate key instead); 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) && (!nav::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) { @@ -3357,7 +3382,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 (d.key_equals(*m, key, n)) { @@ -3368,11 +3393,13 @@ inline const node* find_member(const document_data& d, const node* object, const } /// the number of members of an object with this key (duplicates included) -inline std::size_t count_members(const document_data& d, const node* object, const char* key, std::size_t n) noexcept +template +std::size_t count_members(const document_data& d, const node* object, const char* key, std::size_t n) noexcept { + using nav = navigation; std::size_t result = 0; - const node* const end = document_data::child_end(object); - for (const node* m = document_data::first_child(object); m != end; m = document_data::after(m + 1)) + const node* const end = nav::end(d, object); + for (const node* m = nav::first(d, object); m != end; m = document_data::after(m + 1)) { if (d.key_equals(*m, key, n)) { @@ -3400,10 +3427,17 @@ SizeType to_index(IntegerType idx) noexcept return (idx < zero || static_cast(result) != idx) ? (std::numeric_limits::max)() : result; } -/// 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); + using nav = navigation; + const node* e = nav::first(d, array); + if (nav::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); @@ -3411,13 +3445,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; @@ -3431,6 +3466,1301 @@ 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 m_doc.key_equals(k, key.data(), key.size()); + } + + /// 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 (const 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 + { + const std::uint8_t linked = n->flags & node_flags::linked; + *n = node{}; + n->kind = static_cast(k); + n->flags = static_cast(node_flags::is_new | linked); + 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) + { + // an entry of a moved sequence links to the slot: it can take any extent + const std::uint8_t linked = slot->flags & node_flags::linked; + if (e.region == nullptr) + { + if (is_container(*slot) && slot->next > 1 && slot != m_doc.nodes && linked == 0) + { + // 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). Looking + // for the container is linear in the size of the document, so + // links (which are marked in the slot) avoid it. + 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; + slot->flags = static_cast(slot->flags | linked); + 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; + // Everything that can throw happens before the slot changes: a slot + // that is a container without the moved flag would show its old + // elements. reserve_moved() makes the set_moved() below, which sets + // the flag, safe; the entry of `regions` exists already (encode() + // added it), so that the assignment at the end does not allocate. + if (!was_moved) + { + reserve_moved(m_doc); + } + slot->kind = r->kind; + slot->extra = 0; + slot->len = r->len; + slot->next = extent; + // (set_moved() adds the moved flag to a slot that does not have it yet) + slot->flags = static_cast((was_moved ? node_flags::moved | node_flags::is_new : 0) | linked); + 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(); // NOLINT(misc-const-correctness): digits are written through p + 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: + // (an editable document only holds valid UTF-8, whatever the check of the other document was) + check_utf8(from.str(n), n.len); + 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"); + } + } + + // The subtrees are walked with an explicit stack (as materialize() does): + // the nesting depth is limited by memory only, not by the call stack. + + /// number of nodes of a subtree (containers, keys, scalars) + template + static std::size_t count_nodes(const document_data& d, const node* n) + { + using walk = navigation; + struct frame + { + const node* pos; ///< next element, or key of the next member + const node* end; + bool object; + }; + std::vector open; + std::size_t r = 0; + for (;;) + { + ++r; + if (is_container(*n)) + { + open.push_back(frame{walk::first(d, n), walk::end(d, n), n->kind == static_cast(value_t::object)}); + } + // the next value: close finished containers, then step over the key + for (;;) + { + if (open.empty()) + { + return r; + } + frame& f = open.back(); + if (f.pos == f.end) + { + open.pop_back(); + continue; + } + const node* v = f.pos; + if (f.object) + { + ++r; // the key + ++v; + } + f.pos = document_data::after(v); + n = walk::value(v); + break; + } + } + } + + /// copy a subtree (of any document) as a contiguous sequence of + /// count_nodes() nodes + template + void fill_nodes(const document_data& d, const node* n, node* out) + { + using walk = navigation; + struct frame + { + const node* pos; ///< next element, or key of the next member + const node* end; + bool object; + node* self; ///< the container in the copy + }; + std::vector open; + for (;;) + { + if (is_container(*n)) + { + node* const self = out++; + *self = plain_node(static_cast(n->kind)); + self->len = n->len; + open.push_back(frame{walk::first(d, n), walk::end(d, n), n->kind == static_cast(value_t::object), self}); + } + else + { + *out++ = copy_scalar(d, *n); + } + // the next value: close finished containers, then copy the key + for (;;) + { + if (open.empty()) + { + return; + } + frame& f = open.back(); + if (f.pos == f.end) + { + f.self->next = static_cast(out - f.self); + open.pop_back(); + continue; + } + const node* v = f.pos; + if (f.object) + { + *out++ = copy_scalar(d, *v); + ++v; + } + f.pos = document_data::after(v); + n = walk::value(v); + break; + } + } + } + + static std::size_t count_nodes(const BasicJsonType& j) + { + using iterator = typename BasicJsonType::const_iterator; + struct frame + { + iterator pos; + iterator end; + bool object; + }; + std::vector open; + const BasicJsonType* n = &j; + std::size_t r = 0; + for (;;) + { + ++r; + if (n->is_structured()) + { + open.push_back(frame{n->cbegin(), n->cend(), n->is_object()}); + } + for (;;) + { + if (open.empty()) + { + return r; + } + frame& f = open.back(); + if (f.pos == f.end) + { + open.pop_back(); + continue; + } + r += f.object ? 1 : 0; // the key + n = &*f.pos; + ++f.pos; + break; + } + } + } + + void fill_nodes(const BasicJsonType& j, node* out) + { + using iterator = typename BasicJsonType::const_iterator; + struct frame + { + iterator pos; + iterator end; + bool object; + node* self; ///< the container in the copy + }; + std::vector open; + const BasicJsonType* n = &j; + for (;;) + { + if (n->is_structured()) + { + node* const self = out++; + *self = plain_node(n->type()); + self->len = static_cast(n->size()); + open.push_back(frame{n->cbegin(), n->cend(), n->is_object(), self}); + } + else + { + *out++ = json_scalar(*n); + } + for (;;) + { + if (open.empty()) + { + return; + } + frame& f = open.back(); + if (f.pos == f.end) + { + f.self->next = static_cast(out - f.self); + open.pop_back(); + continue; + } + if (f.object) + { + const auto& key = f.pos.key(); + check_utf8(key.data(), key.size()); + *out++ = string_node(key.data(), key.size()); + } + n = &*f.pos; + ++f.pos; + break; + } + } + } + + document_data& m_doc; +}; + +} // namespace view +} // namespace detail +NLOHMANN_JSON_NAMESPACE_END + +// #include + +// #include +// __ _____ _____ _____ +// __| | __| | | | JSON for Modern C++ +// | | |__ | | | | | | version 3.12.0 +// |_____|_____|_____|_|___| https://github.com/nlohmann/json +// +// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann +// SPDX-License-Identifier: MIT + + + +#include // max +#include // size_t +#include // istream, streambuf +#include // basic_string, char_traits, string +#include // decay, integral_constant, is_array, is_const, is_integral, is_lvalue_reference, is_pointer, is_same, remove_cv, remove_extent, remove_pointer, 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, ///< non-const rvalue std::string: owned without a copy + c_string, ///< pointer to single-byte characters (NUL-terminated): borrowed + char_array, ///< array of single-byte characters (e.g. a string literal): borrowed + borrow_range, ///< lvalue contiguous byte container, or std::string_view: borrowed + copy_range, ///< rvalue contiguous byte container (a const rvalue std::string too): copied + wide_c_string, ///< pointer to wide characters (NUL-terminated): converted to UTF-8 into a buffer + adapter, ///< streams, FILE*, wide strings and arrays, and the rest of what the library's input adapter reads: read into a buffer +}; + +/// a single-byte character type (char, signed/unsigned char, char8_t, std::uint8_t, ...) +template +struct is_byte_char : conjunction, std::integral_constant> {}; // NOLINT(bugprone-sizeof-expression): T is any pointee or element type + +/// a character type the library's input adapter converts to UTF-8 +template +struct is_wide_char : disjunction, std::is_same, std::is_same> {}; + +template +struct classify_input +{ + using R = typename std::remove_reference::type; + using D = typename std::decay::type; + using element = typename std::remove_cv::type>::type; + using pointee = typename std::remove_cv::type>::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 + // (arrays and pointers of other types, such as wide-character arrays and + // FILE*, go to the library's input adapter) + static constexpr input_kind value = + (std::is_array::value && is_byte_char::value) ? input_kind::char_array + : std::is_array::value ? input_kind::adapter + : (std::is_pointer::value && is_byte_char::value) ? input_kind::c_string + : (std::is_pointer::value && is_wide_char::value) ? input_kind::wide_c_string + : std::is_pointer::value ? input_kind::adapter + : (is_rvalue && !std::is_const::value && 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) +}; + +/// an integer type other than bool: a length passed where a flag is expected +template +struct is_integer_not_bool : std::is_integral {}; + +template<> +struct is_integer_not_bool : std::false_type {}; + +/// 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 {}; + +/// append what a json input adapter yields (UTF-16/32 inputs arrive as UTF-8) +template +void append_adapter(std::string& buf, Adapter ia) +{ + for (;;) + { + const auto ch = ia.get_character(); + if (ch == std::char_traits::eof()) + { + break; + } + buf.push_back(static_cast(ch)); + } +} + +/// an input adapter made by the document for an input that the library's +/// input_adapter() does not take as one argument (an iterator range, a wide C +/// string), to be appended with append_adapter() +template +struct adapter_input +{ + Adapter ia; +}; + +template +struct is_adapter_input : std::false_type {}; + +template +struct is_adapter_input> : std::true_type {}; + +template +adapter_input::type> make_adapter_input(Adapter&& ia) +{ + return {std::forward(ia)}; +} + +/// append the rest of a stream: its characters need no conversion, so they are +/// read in blocks from the stream buffer, straight into buf (reusing its +/// capacity). As the library's stream adapter, this reads the stream buffer +/// directly and sets eofbit at the end. +inline void append_stream(std::string& buf, std::istream& is) +{ + std::streambuf* const sb = is.rdbuf(); + if (sb == nullptr) + { + return; + } + for (;;) + { + const std::size_t used = buf.size(); + const std::size_t room = (std::max)(buf.capacity() - used, std::size_t{4096}); + buf.resize(used + room); + const std::streamsize got = sb->sgetn(&buf[used], static_cast(room)); + buf.resize(used + static_cast((std::max)(got, std::streamsize{0}))); + if (got <= 0) + { + break; + } + } + is.clear(is.rdstate() | std::ios::eofbit); +} + +} // 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++ @@ -3468,6 +4798,7 @@ NLOHMANN_JSON_NAMESPACE_END #include // size_t #include // int64_t, uint64_t +#include // numeric_limits #include // string #include // integral_constant @@ -3590,11 +4921,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> {}); } @@ -3639,26 +4990,32 @@ 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; + + // the open containers: their next element (or key of the next member), + // their end, and whether they are objects; sized once from the + // document's deepest nesting (for a subtree an upper bound) + struct frame + { + const node* pos; + 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; sized - // once from the document's deepest nesting (for a subtree an upper bound) - struct open_container - { - const node* end; - bool is_object; - }; - std::vector open((std::max)(d.max_depth, std::size_t{1})); + std::vector open((std::max)(d.max_depth, std::size_t{1})); std::size_t depth = 0; 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: @@ -3675,69 +5032,68 @@ BasicJsonType materialize(const document_data& d, const node* n) } if (NLOHMANN_VIEW_UNLIKELY(depth == open.size())) { - open.resize(depth * 2); // (deeper than the document's recorded nesting) + open.resize(depth * 2); // (deeper than the recorded nesting, e.g. after edits) } - open[depth++] = open_container{document_data::child_end(n), object}; - n = document_data::first_child(n); + open[depth++] = 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 (depth == 0) { return result; } - if (n != open[depth - 1].end) + frame& f = open[depth - 1]; + if (f.pos == f.end) { - break; + if (f.object) + { + sax.end_object(); + } + else + { + sax.end_array(); + } + --depth; + continue; } - if (open[depth - 1].is_object) + 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(); - } - --depth; - } - if (open[depth - 1].is_object) - { - // 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; } } } @@ -4057,9 +5413,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; @@ -4092,7 +5449,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 @@ -4134,13 +5491,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; } } @@ -4192,9 +5550,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 { @@ -4243,9 +5601,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) @@ -4600,6 +5958,10 @@ struct is_non_bool_arithmetic : conjunction, negation struct is_non_c_string_default : negation::type, const char*>> {}; +/// the integral types accepted as an array index by the edits of a document +template +struct is_non_bool_integral : conjunction, negation>> {}; + /// whether View::get() converts to T without materialize(); get_to() /// assigns these, and passes the others to from_json template @@ -4634,11 +5996,13 @@ NLOHMANN_JSON_NAMESPACE_BEGIN 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; @@ -4835,7 +6199,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(); } /// any other integer type (int, unsigned, long, std::int64_t, ...; a @@ -4898,7 +6262,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))); } /// any other integer type, see operator[]; negative values are out of @@ -4973,7 +6337,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(); } @@ -4990,7 +6354,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(); } @@ -5007,7 +6371,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 @@ -5031,7 +6395,7 @@ class basic_json_view /// more than once); 0 if this is not an object size_type count(string_view_t key) const { - return is_object() ? detail::view::count_members(*m_doc, m_node, key.data(), key.size()) : 0; + return is_object() ? detail::view::count_members(*m_doc, m_node, key.data(), key.size()) : 0; } size_type count(const char* key) const @@ -5054,7 +6418,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); } @@ -5063,7 +6427,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); } @@ -5165,7 +6529,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; } @@ -5199,6 +6563,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) { @@ -5232,7 +6609,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 @@ -5240,12 +6617,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 @@ -5259,10 +6638,25 @@ class basic_json_view return detail::view::view_side(v); } + /// the template argument as a runtime condition: testing it directly in + /// `Editable && ...` makes the condition constant, which MSVC reports as + /// C4127 + static bool editable() noexcept + { + return Editable; + } + /// the number of source bytes of this value (estimated for values with /// decoded strings); the estimate sizes the output buffer of dump() std::size_t source_extent() const noexcept { + if (editable() && m_doc->edits != nullptr) + { + // positions of moved and new values are not source offsets; the root + // (the first node) spans the source text and every edit + const bool is_root = m_node == m_doc->nodes; + return is_root ? m_doc->size + m_doc->edits->text_used : 64; + } const node* const end = m_doc->nodes + m_doc->node_count; if ((m_node->flags & detail::view::node_flags::storage) != 0) { @@ -5293,8 +6687,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 --- @@ -5400,7 +6794,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; @@ -5409,7 +6803,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 @@ -5559,7 +6953,8 @@ class basic_json_document + (m_data->nodes != m_data->inline_nodes ? m_data->node_capacity * 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 @@ -5578,7 +6973,8 @@ class basic_json_document // unchanged const bool shrink_arena = d.arena.capacity() > d.arena.size(); std::vector arena(shrink_arena ? d.arena : std::vector()); - const bool shrink_nodes = d.nodes != d.inline_nodes && d.node_count != d.node_capacity; + // (edits link to the nodes of the index, which then stays in place) + const bool shrink_nodes = d.nodes != d.inline_nodes && d.node_count != d.node_capacity && d.edits == nullptr; const bool into_header = d.node_count <= d.inline_capacity; std::vector indexes(d.indexes.capacity() > d.indexes.size() ? d.indexes : std::vector()); std::vector index_slots(d.index_slots.capacity() > d.index_slots.size() ? d.index_slots : std::vector()); @@ -5609,9 +7005,190 @@ 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::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. A null parent becomes what + /// basic_json's operator[](json_pointer) makes of it: an array for "-" + /// and for digits (padded with nulls up to the index), an object otherwise. + 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_null()) + { + if (token == "-") + { + return push_back(parent, std::forward(value)); + } + const bool digits = std::all_of(token.begin(), token.end(), [](const char c) + { + return c >= '0' && c <= '9'; + }); + if (digits) + { + // (an invalid index is an error before the parent changes) + const std::size_t idx = pointer_index(token); + if (idx >= 0xFFFFFFFFu) + { + detail::view::throw_out_of_range(401, detail::concat("array index ", std::to_string(idx), " is out of range")); + } + for (std::size_t i = 0; i < idx; ++i) + { + push_back(parent, nullptr); + } + return push_back(parent, std::forward(value)); + } + } + 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::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::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) { @@ -5665,6 +7242,8 @@ class basic_json_document d.src = src; d.size = size; d.node_count = 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(); @@ -5834,6 +7413,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_builder.cpp b/tests/src/unit-json_view_builder.cpp index 2151d544c..b43af739d 100644 --- a/tests/src/unit-json_view_builder.cpp +++ b/tests/src/unit-json_view_builder.cpp @@ -377,7 +377,7 @@ TEST_CASE("json_view builder") const built shallow = build(nested, false, false, false); REQUIRE(shallow.ok); shallow.data->max_depth = 1; - CHECK(nlohmann::detail::view::materialize(*shallow.data, shallow.data->nodes) == json::parse(nested)); + CHECK(nlohmann::detail::view::materialize(*shallow.data, shallow.data->nodes) == json::parse(nested)); // the open containers beyond 64 levels live on the heap for (const std::size_t depth : diff --git a/tests/src/unit-json_view_edit.cpp b/tests/src/unit-json_view_edit.cpp new file mode 100644 index 000000000..eaca359c6 --- /dev/null +++ b/tests/src/unit-json_view_edit.cpp @@ -0,0 +1,1157 @@ +// __ _____ _____ _____ +// __| | __| | | | 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 +#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()); + } +} + +// the text of j, in which some objects repeat a key of theirs after their +// members: lookups of the edited document find the first member of a key, so +// j (the first members) is what they must agree with, while parse() would +// keep the last value +std::string text_with_duplicates(const ordered_json& j) +{ + if (j.is_object()) + { + std::vector keys; + for (const auto& kv : j.items()) + { + keys.push_back(kv.key()); + } + const auto repeated = [&keys]() + { + return ordered_json(keys[static_cast(r(static_cast(keys.size())))]).dump() + ":" + random_value(2).dump(); + }; + std::string text = "{"; + bool first = true; + for (const auto& kv : j.items()) + { + text += (first ? "" : ",") + ordered_json(kv.key()).dump() + ":" + text_with_duplicates(kv.value()); + first = false; + } + for (int i = keys.empty() ? 0 : r(3); i > 0; --i) + { + text += "," + repeated(); + } + return text + "}"; + } + if (j.is_array()) + { + std::string text = "["; + for (std::size_t i = 0; i < j.size(); ++i) + { + text += (i != 0 ? "," : "") + text_with_duplicates(j[i]); + } + return text + "]"; + } + return j.dump(); +} + +// the number of members of an object view with this key, by iteration +std::size_t occurrences(const ordered_json_editable_view& v, const std::string& key) +{ + std::size_t result = 0; + for (const auto& item : v.items()) + { + result += std::string(item.key().data(), item.key().size()) == key ? 1u : 0u; + } + return result; +} + +// every lookup of the edited view finds the first member of its key, which +// the iteration of the view shows +void check_lookups(const ordered_json_editable_view& v) +{ + if (v.is_object()) + { + std::set seen; + for (const auto item : v.items()) + { + const std::string key(item.key().data(), item.key().size()); + if (!seen.insert(key).second) + { + continue; // (not the first member of the key) + } + const ptr_t ptr = ptr_t() / key; + CAPTURE(key) + const ordered_json expected = item.value().materialize(); + const ordered_json_editable_view m = v[key]; + REQUIRE(!m.is_discarded()); + CHECK(m.materialize() == expected); + CHECK(v.at(key).materialize() == expected); + CHECK(v[ptr].materialize() == expected); + CHECK(v.at(ptr).materialize() == expected); + CHECK(v.contains(key)); + CHECK(v.contains(ptr)); + CHECK(v.count(key) == occurrences(v, key)); + const auto it = v.find(key); + REQUIRE(it != v.end()); + CHECK((*it).materialize() == expected); + CHECK(v.value(ptr, ordered_json(nullptr)) == expected); + if (expected.is_string()) + { + CHECK(v.value(key, std::string("-")) == expected.get()); + } + check_lookups(m); + } + CHECK(v["missing#key"].is_discarded()); + CHECK(!v.contains("missing#key")); + CHECK(v.find("missing#key") == v.end()); + } + else if (v.is_array()) + { + for (std::size_t i = 0; i < v.size(); ++i) // NOLINT(modernize-loop-convert): both v[i] and v.at(i) are tested + { + check_lookups(v[i]); + check_lookups(v.at(i)); + } + } +} + +// every lookup of the edited view finds what the first members of j hold +void check_first_members(const ordered_json_editable_view& v, const ordered_json& j) +{ + REQUIRE(v.type() == j.type()); + if (j.is_object()) + { + for (const auto& kv : j.items()) + { + const std::string& key = kv.key(); + const ptr_t ptr = ptr_t() / key; + CAPTURE(key) + REQUIRE(v.contains(key)); + CHECK(v.contains(ptr)); + CHECK(v.count(key) == occurrences(v, key)); + CHECK(v.find(key) != v.end()); + CHECK(v.at(ptr).type() == kv.value().type()); + check_first_members(v[key], kv.value()); + check_first_members(v.at(key), kv.value()); + check_first_members(v[ptr], kv.value()); + } + CHECK(!v.contains("missing#key")); + } + else if (j.is_array()) + { + REQUIRE(v.size() == j.size()); + for (std::size_t i = 0; i < j.size(); ++i) + { + check_first_members(v[i], j[i]); + } + } + else + { + CHECK(v.materialize() == j); + } +} + +// (for documents whose objects repeat keys: j holds the first members, the +// lookups must find them; dump(), size(), and iteration list all members, but +// materialize() keeps the last value of a key, as parse() does of the dump) +void check_duplicates(const ordered_json_editable_document& d, const ordered_json& j) +{ + check_first_members(d.root(), j); + check_lookups(d.root()); + CHECK(d.root().materialize() == ordered_json::parse(d.root().dump())); +} + +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 < 250; ++n) + { + // the last 100 documents repeat keys in some of their objects; j then + // holds the first member of each key, which the edits address + const bool duplicates = n >= 150; + ordered_json j = random_value(0); + if (r(4) == 0) + { + j = ordered_json::object({{"a", random_value(1)}, {"b", random_value(1)}}); + } + if (duplicates) + { + j = ordered_json::parse(j.dump()); // (normalized as the document reads it) + } + const std::string text = duplicates ? text_with_duplicates(j) : j.dump(r(2) == 0 ? -1 : 2); + CAPTURE(text) + ordered_json_editable_document d = ordered_json_editable_document::parse(text); + if (!duplicates) + { + 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) + if (duplicates) + { + check_duplicates(d, j); + } + else + { + 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"]); + // (a raw string with a backslash must not be a macro argument: MSVC C2017) + const std::string expected = R"({"copy":[1,"two",{"three":3.5}],"text":"a\nb","edited":{"inner":[1,2]},"self":[1,"two",{"three":3.5}]})"; + CHECK(d.root().dump() == expected); + 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]"); + } +} + +TEST_CASE("json_view edits: deeply nested values") +{ + // copying a value into a document must not recurse per nesting level + const std::size_t depth = 100000; + const std::string brackets = std::string(depth, '[') + std::string(depth, ']'); + std::string braces; + for (std::size_t i = 0; i < depth; ++i) + { + braces += "{\"a\":"; + } + braces += '1'; + braces += std::string(depth, '}'); + + SECTION("a view of a read-only document") + { + const json_document source = json_document::parse(brackets); + json_editable_document d = json_editable_document::parse("[]"); + d.push_back(d.root(), source.root()); + CHECK(d.root().dump() == "[" + brackets + "]"); + } + + SECTION("a view of an editable document") + { + const json_editable_document source = json_editable_document::parse(braces); + json_editable_document d = json_editable_document::parse("{}"); + d.set(d.root(), "deep", source.root()); + CHECK(d.root().dump() == "{\"deep\":" + braces + "}"); + } + + SECTION("a view of an edited document (values behind links)") + { + const json_document source = json_document::parse(brackets); + json_editable_document edited = json_editable_document::parse("[[]]"); + edited.push_back(edited.root()[0], source.root()); + edited.push_back(edited.root(), source.root()); + json_editable_document d = json_editable_document::parse("null"); + d.set(d.root(), edited.root()); + CHECK(d.root().dump() == "[[" + brackets + "]," + brackets + "]"); + } + + SECTION("a basic_json value") + { + json deep = json::array(); + json* inner = &deep; + for (std::size_t i = 1; i < depth; ++i) + { + inner->push_back(json::array()); + inner = &inner->back(); + } + json_editable_document d = json_editable_document::parse("[]"); + d.push_back(d.root(), deep); + CHECK(d.root().dump() == "[" + brackets + "]"); + } + + SECTION("a basic_json value with objects") + { + json deep = 1; + for (std::size_t i = 0; i < depth; ++i) + { + json outer = json::object(); + outer["a"] = std::move(deep); + deep = std::move(outer); + } + json_editable_document d = json_editable_document::parse("{}"); + d.set(d.root(), "deep", deep); + CHECK(d.root().dump() == "{\"deep\":" + braces + "}"); + } +} + +#if !defined(JSON_NOEXCEPTION) +TEST_CASE("json_view edits: pointers below a null value") +{ + // a null value on the way becomes what basic_json makes of it: an array + // for "-" and for digits, an object otherwise + struct test_case + { + const char* document; + const char* pointer; + }; + const std::array cases = + { + { + {R"({"a":null})", "/a/0"}, + {R"({"a":null})", "/a/-"}, + {R"({"a":null})", "/a/3"}, + {R"({"a":null})", "/a/x"}, + {R"({"a":null})", "/a/+1"}, + {R"({"a":null})", "/a/01"}, + {R"({"a":null})", "/a/"}, + {R"({"a":{"b":null}})", "/a/b/1"}, + {R"({"a":{"b":null}})", "/a/b/-"}, + {R"({"a":[null]})", "/a/0/0"}, + {R"({"a":[null,null]})", "/a/1/k"}, + {R"([null])", "/0"}, + {"null", "/0"}, + {"null", "/-"}, + {"null", "/k"}, + {"null", ""}, + } + }; + for (const test_case& c : cases) + { + CAPTURE(c.document) + CAPTURE(c.pointer) + json expected = json::parse(c.document); + const std::string error = exception_of_call([&] + { + expected[json::json_pointer(c.pointer)] = 1; + }); + json_editable_document d = json_editable_document::parse(c.document); + if (error.empty()) + { + d.set(json::json_pointer(c.pointer), 1); + CHECK(d.root().dump() == expected.dump()); + CHECK(d.root().materialize() == expected); + } + else + { + // the same error, and the document is not changed + CHECK(exception_of_call([&] { d.set(json::json_pointer(c.pointer), 1); }) == error); + CHECK(d.root().dump() == json::parse(c.document).dump()); + } + } +} + +TEST_CASE("json_view edits: strings of other documents are checked") +{ + // A document borrows the text it was parsed from, and sees later changes + // of the text: a way to get ill-formed UTF-8 into a view. Copying it into + // an editable document is an error, as for any other string. + std::string text = R"({"key":"abc","list":["abc"]})"; + const json_document source = json_document::parse(text); + const auto message_of = [](const std::string & bad) + { + return exception_of_call([&] + { + const std::string dumped = json(bad).dump(); + static_cast(dumped); + }); + }; + json_editable_document d = json_editable_document::parse("[1]"); + d.push_back(d.root(), source.root()); + CHECK(d.root().dump() == R"([1,{"key":"abc","list":["abc"]}])"); + + text[text.find("abc") + 1] = '\xC3'; // "a\xC3c" + text[text.rfind("abc") + 1] = '\xC3'; + const std::string bad_value = message_of(std::string("a\xC3" "c")); + CHECK(!bad_value.empty()); + CHECK(exception_of_call([&] { d.push_back(d.root(), source.root()["key"]); }) == bad_value); + CHECK(exception_of_call([&] { d.set(d.root()[0], source.root()["list"][0]); }) == bad_value); + CHECK(exception_of_call([&] { d.set(d.root(), 0, source.root()["list"]); }) == bad_value); + CHECK(exception_of_call([&] { d.set(d.root(), 0, source.root()); }) == bad_value); + + text[text.find("key") + 1] = '\xC3'; // a key is checked as well + const std::string bad_key = message_of(std::string("k\xC3" "y")); + CHECK(exception_of_call([&] { d.set(d.root(), 0, source.root()); }) == bad_key); + CHECK(exception_of_call([&] { d.push_back(d.root(), source.root()); }) == bad_key); + + // nothing of the failed edits is visible + CHECK(d.root().dump() == R"([1,{"key":"abc","list":["abc"]}])"); +} + +TEST_CASE("json_view edits: the size of a text arena") +{ + using nlohmann::detail::view::text_capacity; + constexpr std::size_t limit = 0xFFFFFFFFu; + // grows by doubling, or to what is needed (plus some room) + CHECK(text_capacity(0, 0, 10) == 266); + CHECK(text_capacity(1000, 990, 20) == 2000); + CHECK(text_capacity(100, 100, 5000) == 5356); + // an arena beyond 2 GiB: doubling is clamped to 4 GiB - 1 + CHECK(text_capacity(0x90000000u, 0x8FFFFFFFu, 2) == limit); + CHECK(text_capacity(limit, limit - 10, 10) == limit); + // exactly what fits is accepted, without room to spare + CHECK(text_capacity(100, 90, limit - 90) == limit); + CHECK(text_capacity(limit - 100, limit - 100, 100) == limit); + // what does not fit is an error + CHECK_THROWS_WITH_AS(text_capacity(100, 90, limit - 89), "[json.exception.out_of_range.416] edits of 4 GiB or more are not supported by json_document", json::out_of_range&); + CHECK_THROWS_WITH_AS(text_capacity(limit, limit, 1), "[json.exception.out_of_range.416] edits of 4 GiB or more are not supported by json_document", json::out_of_range&); +} +#endif + +TEST_CASE("json_view edits: the chunks of new nodes") +{ + using nlohmann::detail::view::alloc_nodes; + using nlohmann::detail::view::document_data; + const std::unique_ptr d(document_data::create(0)); + // the first chunk has room for 64 nodes, the next one for 128 + const nlohmann::detail::view::node* const first = alloc_nodes(*d, 10); + CHECK(d->edits->chunks.size() == 1); + // a request of half of the next chunk (128 nodes) or more gets a chunk of + // its own + const nlohmann::detail::view::node* const large = alloc_nodes(*d, 100); + CHECK(d->edits->chunks.size() == 2); + CHECK(large == d->edits->chunks.back().get()); + // and the rest of the first chunk is still used + CHECK(alloc_nodes(*d, 50) == first + 10); + CHECK(d->edits->chunks.size() == 2); + // a smaller request that does not fit starts a new (doubled) chunk; the + // call is a statement of its own, as the operands of == are unsequenced + // and MSVC reads chunks.back() before the call adds the chunk + const nlohmann::detail::view::node* const doubled = alloc_nodes(*d, 10); + CHECK(doubled == d->edits->chunks.back().get()); + CHECK(d->edits->chunks.size() == 3); + CHECK(d->edits->chunk_end - d->edits->chunk_cur == 128 - 10); + CHECK(d->edits->bytes == (64 + 100 + 128) * sizeof(nlohmann::detail::view::node)); +} + +TEST_CASE("json_view edits: replacing arrays and objects by scalars") +{ + // the first assignment switches the parent to links; the later ones do + // not need to look for the parent again + std::string text = "["; + json expected = json::array(); + for (int i = 0; i < 300; ++i) + { + text += (i != 0 ? ",[" : "[") + std::to_string(i) + ",{\"k\":" + std::to_string(i) + "}]"; + expected.push_back(json::array({i, json{{"k", i}}})); + } + text += ']'; + json_editable_document d = json_editable_document::parse(text); + CHECK(d.root().dump() == expected.dump()); + for (int i = 0; i < 300; i += 2) + { + d.set(d.root()[static_cast(i)], i); + expected[static_cast(i)] = i; + } + CHECK(d.root().dump() == expected.dump()); + for (int i = 1; i < 300; i += 2) // (elements that are still in the parsed layout of their parent) + { + d.set(d.root()[static_cast(i)][1], "x"); // replaces an object + expected[static_cast(i)][1] = "x"; + } + CHECK(d.root().dump() == expected.dump()); + + // new values, and values in new values + d.push_back(d.root(), json::parse(R"([[1,2],{"a":[3]}])")); + expected.push_back(json::parse(R"([[1,2],{"a":[3]}])")); + d.set(d.root()[300][0], 7); + expected[300][0] = 7; + d.insert(d.root(), 0, json::array({1, 2})); + expected.insert(expected.begin(), json::array({1, 2})); + d.set(d.root()[0], nullptr); + expected[0] = nullptr; + d.set(d.root()[301], 5); + expected[301] = 5; + CHECK(d.root().dump() == expected.dump()); + CHECK(d.root().materialize() == expected); +} + +namespace +{ +// reads of "a" in an object that repeats it: the first member wins, however the +// object is stored +template +void check_first_wins(const View& o, int expected) +{ + CAPTURE(expected) + CHECK(o["a"].template get() == expected); + CHECK(o.at("a").template get() == expected); + CHECK(o.find("a")->template get() == expected); + CHECK(o.value("a", -1) == expected); + CHECK(o[json::json_pointer("/a")].template get() == expected); + CHECK(o.at(json::json_pointer("/a")).template get() == expected); + CHECK(o.value(json::json_pointer("/a"), -1) == expected); + CHECK(o.contains("a")); + CHECK(o.contains(json::json_pointer("/a"))); + std::size_t members_a = 0; + for (const auto& item : o.items()) + { + members_a += std::string(item.key().data(), item.key().size()) == "a" ? 1u : 0u; + } + CHECK(o.count("a") == members_a); // every member, unlike the lookups +} +} // namespace + +TEST_CASE("json_view edits: duplicate keys") +{ + SECTION("an object in its parsed layout, then moved by edits") + { + json_editable_document d = json_editable_document::parse(R"({"a": 1, "b": 2, "a": 3})"); + check_first_wins(d.root(), 1); + CHECK(d.root().size() == 3); + CHECK(d.root().dump() == R"({"a":1,"b":2,"a":3})"); + CHECK(d.root().materialize()["a"] == 3); // (parse() keeps the last value) + + // an edit of a value does not move the object + d.set(d.root()["b"], 5); + check_first_wins(d.root(), 1); + d.set(d.root()["a"], 4); // the member that lookups find + check_first_wins(d.root(), 4); + CHECK(d.root().dump() == R"({"a":4,"b":5,"a":3})"); + + // an appended member moves it + d.set(d.root(), "c", true); + check_first_wins(d.root(), 4); + CHECK(d.root().size() == 4); + CHECK(d.root().dump() == R"({"a":4,"b":5,"a":3,"c":true})"); + d.set(d.root()["a"], 6); + check_first_wins(d.root(), 6); + CHECK(d.root().dump() == R"({"a":6,"b":5,"a":3,"c":true})"); + d.set(json::json_pointer("/a"), 7); + check_first_wins(d.root(), 7); + } + + SECTION("set assigns the member that lookups find") + { + // the key stays where its first occurrence is, the later members are dropped + ordered_json_editable_document d = ordered_json_editable_document::parse(R"({"a": 1, "b": 2, "a": 3, "c": 4, "a": 5})"); + const ordered_json_editable_view held = d.root()["a"]; + CHECK(held.get() == 1); + const ordered_json_editable_view assigned = d.set(d.root(), "a", "x"); + CHECK(held.get() == "x"); + CHECK(assigned.get() == "x"); + CHECK(d.root().dump() == R"({"a":"x","b":2,"c":4})"); + CHECK(d.root().materialize() == ordered_json::parse(R"({"a": "x", "b": 2, "c": 4})")); + CHECK(d.root().size() == 3); + + // as for the object that parse() makes of the text + ordered_json j = ordered_json::parse(R"({"a": 1, "b": 2, "a": 3, "c": 4, "a": 5})"); + j["a"] = "x"; + CHECK(d.root().materialize().dump() == j.dump()); + + // through a pointer, below a duplicate + d = ordered_json_editable_document::parse(R"({"a": {"x": 1}, "b": 2, "a": {"x": 3}})"); + d.set(ptr_t("/a/x"), 4); + CHECK(d.root().dump() == R"({"a":{"x":4},"b":2,"a":{"x":3}})"); + d.set(ptr_t("/a"), 0); + CHECK(d.root().dump() == R"({"a":0,"b":2})"); + } + + SECTION("erase removes every member") + { + json_editable_document d = json_editable_document::parse(R"({"a": 1, "b": 2, "a": 3})"); + d.set(d.root(), "c", 4); + check_first_wins(d.root(), 1); + CHECK(d.erase(d.root(), "a") == 2); + CHECK(d.root().dump() == R"({"b":2,"c":4})"); + CHECK(!d.root().contains("a")); + CHECK(d.root()["a"].is_discarded()); + CHECK(d.erase(d.root(), "a") == 0); + d = json_editable_document::parse(R"({"a": 1, "b": 2, "a": 3})"); + CHECK(d.erase(json::json_pointer("/a")) == 2); + CHECK(d.root().dump() == R"({"b":2})"); + } + + SECTION("a large object with an index") + { + std::string text = R"({"a":0,"k7":"first")"; + for (int i = 0; i < 200; ++i) + { + text += ",\"k" + std::to_string(i) + "\":" + std::to_string(i); + } + text += R"(,"a":1,"k7":"last","a":2})"; + json_editable_document d = json_editable_document::parse(text); + check_first_wins(d.root(), 0); + CHECK(d.root()["k7"].get_string() == "first"); + CHECK(d.root()["k199"].get() == 199); + + // a value assigned in place: the index stays in use + d.set(d.root()["a"], 3); + check_first_wins(d.root(), 3); + d.set(d.root()["k7"], "changed"); + CHECK(d.root()["k7"].get_string() == "changed"); + CHECK(d.root()["k199"].get() == 199); + + // an appended member moves the members: the lookup scans them + d.set(d.root(), "new", 1); + check_first_wins(d.root(), 3); + CHECK(d.root()["k7"].get_string() == "changed"); + CHECK(d.root()["k7"].get_string() == d.root().at("k7").get_string()); + CHECK(d.root()["k199"].get() == 199); + CHECK(d.root()["new"].get() == 1); + + // a value replaced by a container, in an object that was not moved + d = json_editable_document::parse(text); + d.set(d.root()["a"], json{{"x", 1}}); + CHECK(d.root()["a"]["x"].get() == 1); + CHECK(d.root()["k7"].get_string() == "first"); + CHECK(d.erase(d.root(), "k7") == 3); // (the key occurs three times) + CHECK(d.root()["k7"].is_discarded()); + CHECK(d.root()["a"]["x"].get() == 1); + } + + SECTION("objects in moved arrays and in new values") + { + json_editable_document d = json_editable_document::parse(R"([{"a": 1, "a": 2}, {"a": 3, "b": 0, "a": 4}])"); + check_first_wins(d.root()[0], 1); + check_first_wins(d.root()[1], 3); + d.insert(d.root(), 0, json::parse(R"({"a": 0})")); + d.push_back(d.root(), json::parse(R"({"a": 5, "a": 6})")); // (a basic_json value has one member) + check_first_wins(d.root()[1], 1); + check_first_wins(d.root()[2], 3); + CHECK(d.root()[3]["a"].get() == 6); + d.set(d.root()[1], "a", 8); + check_first_wins(d.root()[1], 8); + CHECK(d.root()[1].dump() == R"({"a":8})"); + d.erase(d.root(), 0); + check_first_wins(d.root()[1], 3); + CHECK(d.root().dump() == R"([{"a":8},{"a":3,"b":0,"a":4},{"a":6}])"); + } + + SECTION("values of other documents") + { + const json_document source = json_document::parse(R"({"list": [{"a": 1, "a": 2}], "o": {"b": {"a": 3, "a": 4}, "a": 5, "a": 6}})"); + json_editable_document d = json_editable_document::parse("{}"); + d.set(d.root(), "copy", source.root()["list"]); + d.set(d.root(), "o", source.root()["o"]); + // (the copies keep the repeated members) + CHECK(d.root().dump() == R"({"copy":[{"a":1,"a":2}],"o":{"b":{"a":3,"a":4},"a":5,"a":6}})"); + check_first_wins(d.root()["copy"][0], 1); + check_first_wins(d.root()["o"]["b"], 3); + check_first_wins(d.root()["o"], 5); + d.set(d.root()["o"]["b"], "c", 0); + check_first_wins(d.root()["o"]["b"], 3); + d.set(d.root()["o"], "d", 0); + check_first_wins(d.root()["o"], 5); + CHECK(d.root()["o"]["b"]["a"].get() == 3); + CHECK(d.root()["o"]["d"].get() == 0); + } +}