mirror of
https://github.com/nlohmann/json.git
synced 2026-10-27 00:08:55 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d2e05c8b45 | ||
|
|
d1566fe9e8 | ||
|
|
3351a8795b | ||
|
|
81d66291c5 | ||
|
|
c712df0637 | ||
|
|
7a10bf25d2 | ||
|
|
c20b6eddf4 | ||
|
|
8679c011db | ||
|
|
d6431a9104 |
No files matched your search
@@ -43,8 +43,8 @@ jobs:
|
|||||||
- name: flawfinder_scan
|
- name: flawfinder_scan
|
||||||
uses: david-a-wheeler/flawfinder@c4216b74cf2639ffa98503768bd6e4299b5440c9 # v2.0.20
|
uses: david-a-wheeler/flawfinder@c4216b74cf2639ffa98503768bd6e4299b5440c9 # v2.0.20
|
||||||
with:
|
with:
|
||||||
# single_include/ is omitted: it is generated from include/ and would report every finding twice
|
# single_include/ is generated from include/ and would report every finding twice
|
||||||
arguments: '--sarif ./cmake ./docs ./include ./src ./tests ./tools'
|
arguments: "--sarif --exclude '*/single_include/*' ./"
|
||||||
output: 'flawfinder_results.sarif'
|
output: 'flawfinder_results.sarif'
|
||||||
|
|
||||||
- name: Upload analysis results to GitHub Security tab
|
- name: Upload analysis results to GitHub Security tab
|
||||||
|
|||||||
@@ -90,7 +90,8 @@ Strong exception safety: if an exception occurs, the original value stays intact
|
|||||||
1. Constant.
|
1. Constant.
|
||||||
2. Logarithmic in the size of the container.
|
2. Logarithmic in the size of the container.
|
||||||
3. Logarithmic in the size of the container.
|
3. Logarithmic in the size of the container.
|
||||||
4. Logarithmic in the size of the container.
|
4. Linear in the number of reference tokens of `ptr`; for each of them, logarithmic in the size of the object, or
|
||||||
|
constant for an array.
|
||||||
|
|
||||||
## Notes
|
## Notes
|
||||||
|
|
||||||
|
|||||||
@@ -61,11 +61,18 @@ Strong exception safety: if an exception occurs, the original value stays intact
|
|||||||
|
|
||||||
## Complexity
|
## Complexity
|
||||||
|
|
||||||
Logarithmic in the size of the JSON object.
|
1. Logarithmic in the size of the JSON object.
|
||||||
|
2. Logarithmic in the size of the JSON object.
|
||||||
|
3. At most one lookup per reference token of `ptr`, each logarithmic in the size of the container at that level:
|
||||||
|
O(*n* log *m*) for *n* reference tokens and containers of size at most *m*.
|
||||||
|
4. Deleted; a call with an integral argument does not compile.
|
||||||
|
|
||||||
## Notes
|
## Notes
|
||||||
|
|
||||||
- This method always returns `#!cpp false` when executed on a JSON type that is not an object.
|
- Overloads (1) and (2) always return `#!cpp false` when executed on a JSON type that is not an object. Overload (3)
|
||||||
|
also works on arrays and on primitive values: the empty JSON pointer resolves in every JSON value (so
|
||||||
|
`#!cpp json(1).contains(""_json_pointer)` returns `#!cpp true`), and `#!cpp json({1, 2}).contains("/1"_json_pointer)`
|
||||||
|
returns `#!cpp true`.
|
||||||
- This method can be executed on any JSON value type.
|
- This method can be executed on any JSON value type.
|
||||||
- Calling this function with an integer argument (for example, `#!cpp contains(0)`) does not compile: such an argument
|
- Calling this function with an integer argument (for example, `#!cpp contains(0)`) does not compile: such an argument
|
||||||
would otherwise implicitly convert to a null `#!cpp const char*` and, from there, cause undefined behavior when
|
would otherwise implicitly convert to a null `#!cpp const char*` and, from there, cause undefined behavior when
|
||||||
|
|||||||
@@ -2,17 +2,27 @@
|
|||||||
|
|
||||||
```cpp
|
```cpp
|
||||||
static basic_json diff(const basic_json& source,
|
static basic_json diff(const basic_json& source,
|
||||||
const basic_json& target);
|
const basic_json& target,
|
||||||
|
const string_t& path = "");
|
||||||
```
|
```
|
||||||
|
|
||||||
Creates a [JSON Patch](http://jsonpatch.com) so that value `source` can be changed into the value `target` by calling
|
Creates a [JSON Patch](http://jsonpatch.com) so that value `source` can be changed into the value `target` by calling
|
||||||
[`patch`](patch.md) function.
|
[`patch`](patch.md) function.
|
||||||
|
|
||||||
For two JSON values `source` and `target`, the following code always yields `#!cpp true`:
|
For two JSON values `source` and `target`, the following code yields `#!cpp true`:
|
||||||
```cpp
|
```cpp
|
||||||
source.patch(diff(source, target)) == target;
|
source.patch(diff(source, target)) == target;
|
||||||
```
|
```
|
||||||
|
|
||||||
|
The equality holds up to [`operator==`](operator_eq.md), so it has the following exceptions:
|
||||||
|
|
||||||
|
- A value that is not equal to itself cannot round-trip. This applies to `NaN` (also nested in an array or object) and
|
||||||
|
to [discarded](is_discarded.md) values: `diff` emits a `replace` operation for them. A `NaN` in the patch is
|
||||||
|
serialized as `#!json null`, so a patch that was dumped and parsed again would set `#!json null`.
|
||||||
|
- Numbers that compare equal produce no operation, even if their types or signs differ: `diff(1, 1.0)`,
|
||||||
|
`diff(1u, 1)` (unsigned and signed integer) and `diff(0.0, -0.0)` all return `#!json []`. The patched value is then
|
||||||
|
the `source` and keeps its number type.
|
||||||
|
|
||||||
## Parameters
|
## Parameters
|
||||||
|
|
||||||
`source` (in)
|
`source` (in)
|
||||||
@@ -21,6 +31,12 @@ source.patch(diff(source, target)) == target;
|
|||||||
`target` (in)
|
`target` (in)
|
||||||
: JSON value to compare against
|
: JSON value to compare against
|
||||||
|
|
||||||
|
`path` (in)
|
||||||
|
: a prefix that is put verbatim in front of the `path` member of every operation (default: `""`, which addresses the
|
||||||
|
root of the value). The prefix is neither escaped nor validated: `#!cpp diff(1, 2, "abc")` yields an operation with
|
||||||
|
the `path` `abc`, which is not a valid JSON Pointer. Use it with a valid JSON Pointer such as `/root` and escape
|
||||||
|
`~` and `/` in its reference tokens (see [JSON Pointer](../../features/json_pointer.md)).
|
||||||
|
|
||||||
## Return value
|
## Return value
|
||||||
|
|
||||||
a JSON patch to convert the `source` to `target`
|
a JSON patch to convert the `source` to `target`
|
||||||
@@ -31,11 +47,32 @@ Strong guarantee: `source` and `target` are never modified.
|
|||||||
|
|
||||||
## Complexity
|
## Complexity
|
||||||
|
|
||||||
Linear in the lengths of `source` and `target`.
|
Linear in the total number of values (including nested ones) in `source` and `target`, times the cost of a key-based
|
||||||
|
object operation (looking up a key in an object): logarithmic for [`json`](../json.md), and linear for
|
||||||
|
[`ordered_json`](../ordered_json.md) (see [`ordered_map` complexity](../ordered_map.md#complexity)). For
|
||||||
|
`ordered_json`, diffing two objects with `n` and `m` members therefore takes O((n+m)²) when their keys do not line up
|
||||||
|
(e.g., a key was added or the keys are in a different order). The size of the patch is proportional to the number of
|
||||||
|
operations times the length of their paths, so a small change deep inside a value yields a long `path`.
|
||||||
|
|
||||||
## Notes
|
## Notes
|
||||||
|
|
||||||
Currently, only `remove`, `add`, and `replace` operations are generated.
|
- Currently, only `remove`, `add`, and `replace` operations are generated.
|
||||||
|
- The patch is correct, but not guaranteed to be minimal. For instance, `diff([1,2,3], [0,1,2,3])` yields three
|
||||||
|
`replace` operations and one `add` operation instead of a single insertion.
|
||||||
|
- Differences between two arrays are reported index by index. Elements that were added at the end of the array are added
|
||||||
|
with `add` operations on the path `/-` (appended in order), and elements that were removed are removed with `remove`
|
||||||
|
operations from the highest index down to the lowest, so that the remaining indices stay valid.
|
||||||
|
- For objects, the operations for the members of `source` come first (`replace`, `remove`, or the operations for nested
|
||||||
|
values, in the iteration order of `source`), followed by `add` operations for the members that only `target` has.
|
||||||
|
- For [`ordered_json`](../ordered_json.md), objects with the same keys in a different order are different. As a patch
|
||||||
|
cannot reorder members, `diff` removes all keys of the `source` object and adds them again in the order of the
|
||||||
|
`target`.
|
||||||
|
- If `source` and `target` differ as a whole (e.g., different types), the patch consists of a single `replace`
|
||||||
|
operation for the root of the value, i.e., with the `path` `""` (or the given `path` prefix).
|
||||||
|
- The `value` members of the patch hold copies of values of `target`. A [binary](../../features/binary_values.md) value
|
||||||
|
has no JSON representation: when a patch that contains one is serialized, it becomes an object such as
|
||||||
|
`#!json {"bytes":[1,3],"subtype":null}`, so a parsed patch no longer holds a binary value (and would set that object
|
||||||
|
instead).
|
||||||
|
|
||||||
## Examples
|
## Examples
|
||||||
|
|
||||||
|
|||||||
@@ -39,13 +39,33 @@ Basic guarantee: if an exception is thrown during the operation, the JSON value
|
|||||||
|
|
||||||
## Complexity
|
## Complexity
|
||||||
|
|
||||||
Linear in the lengths of `apply_patch`.
|
Linear in the total number of values (including nested ones) in `apply_patch`, times the cost of a key-based object
|
||||||
|
operation (looking up, adding, or erasing a member): logarithmic for [`json`](../json.md), and linear for
|
||||||
|
[`ordered_json`](../ordered_json.md) (see [`ordered_map` complexity](../ordered_map.md#complexity)). For
|
||||||
|
`ordered_json`, patching an object that has `n` members with a patch that has `m` members therefore takes O(n·m). The
|
||||||
|
cost also includes one copy of `apply_patch` (see the notes below).
|
||||||
|
|
||||||
## Notes
|
## Notes
|
||||||
|
|
||||||
`apply_patch` may be `#!cpp *this` itself or refer to a value contained in `#!cpp *this` (for example, a subobject
|
- `apply_patch` may be `#!cpp *this` itself or refer to a value contained in `#!cpp *this` (for example, a subobject
|
||||||
returned by `#!cpp (*this)[key]`); it is read as it was when `merge_patch()` was called, before any modification of
|
returned by `#!cpp (*this)[key]`); it is read as it was when `merge_patch()` was called, before any modification of
|
||||||
`#!cpp *this`.
|
`#!cpp *this`. To make this possible, the function copies `apply_patch` before it modifies `#!cpp *this`, so the cost
|
||||||
|
includes one copy of the patch.
|
||||||
|
- A merge patch uses `#!json null` to remove a member, so it cannot set a member to `#!json null`: applying
|
||||||
|
`#!json {"a":null}` to `#!json {"a":1}` gives `#!json {}`.
|
||||||
|
- Arrays are not merged: an array in the patch replaces the target value as a whole, and a `#!json null` inside an
|
||||||
|
array is kept as an element. `#!json null` members of the target that the patch does not mention are preserved.
|
||||||
|
- A `#!json null` in the patch only removes a member; it does not prevent the creation of the enclosing objects.
|
||||||
|
Applying `#!json {"a":{"b":null}}` to `#!json {}` gives `#!json {"a":{}}` (see example 15 in Appendix A of the RFC).
|
||||||
|
- If `apply_patch` is not an object, it replaces the target as a whole. In particular, a `#!json null` patch turns the
|
||||||
|
target into `#!json null`. If `apply_patch` is an object and the target is not, the target is first replaced by an
|
||||||
|
empty object, so applying `#!json {}` to `#!json 1` gives `#!json {}`.
|
||||||
|
- A merge patch cannot express every change; for example, it cannot reorder array elements or remove a specific array
|
||||||
|
element. Use [JSON Patch](../../features/json_patch.md) ([RFC 6902](https://tools.ietf.org/html/rfc6902)) for that.
|
||||||
|
[`merge_diff`](merge_diff.md) has a matching limitation, see its notes.
|
||||||
|
- A [discarded](is_discarded.md) value is treated like any other non-object value: a discarded
|
||||||
|
`apply_patch` turns the target into a discarded value, and an object patch replaces a discarded target by an object
|
||||||
|
as described above.
|
||||||
|
|
||||||
## Examples
|
## Examples
|
||||||
|
|
||||||
|
|||||||
@@ -92,12 +92,20 @@ Strong exception safety: if an exception occurs, the original value stays intact
|
|||||||
For the **const** version, an object key or array index in `ptr` that does not exist is not reported by an
|
For the **const** version, an object key or array index in `ptr` that does not exist is not reported by an
|
||||||
exception, but is undefined behavior (see the notes below). Use [`at`](at.md) for checked access.
|
exception, but is undefined behavior (see the notes below). Use [`at`](at.md) for checked access.
|
||||||
|
|
||||||
|
If the **non-const** version throws, the value is left unchanged: values it created for earlier reference tokens
|
||||||
|
of `ptr` are removed again. This also holds for exceptions such as `#!cpp std::bad_alloc` or
|
||||||
|
`#!cpp std::length_error` thrown while padding an array up to a very large index. With custom
|
||||||
|
[template parameters](../../features/types/template_parameters.md), this requires that `erase()` of the object and
|
||||||
|
array types and the move operations of the custom base class do not throw.
|
||||||
|
|
||||||
## Complexity
|
## Complexity
|
||||||
|
|
||||||
1. Constant if `idx` is in the range of the array. Otherwise, linear in `idx - size()`.
|
1. Constant if `idx` is in the range of the array. Otherwise, linear in `idx - size()`.
|
||||||
2. Logarithmic in the size of the container.
|
2. Logarithmic in the size of the container.
|
||||||
3. Logarithmic in the size of the container.
|
3. Logarithmic in the size of the container.
|
||||||
4. Logarithmic in the size of the container.
|
4. Linear in the number of reference tokens of `ptr`; for each of them, logarithmic in the size of the object, or
|
||||||
|
constant for an array. When the non-const version pads an array with `#!json null` values, it is additionally
|
||||||
|
linear in `idx - size()` for that array.
|
||||||
|
|
||||||
## Notes
|
## Notes
|
||||||
|
|
||||||
@@ -137,10 +145,21 @@ Strong exception safety: if an exception occurs, the original value stays intact
|
|||||||
|
|
||||||
When the JSON pointer traverses intermediate levels that don't exist at all yet (not just a missing
|
When the JSON pointer traverses intermediate levels that don't exist at all yet (not just a missing
|
||||||
leaf), each missing level is created as an array or an object depending on whether the corresponding
|
leaf), each missing level is created as an array or an object depending on whether the corresponding
|
||||||
pointer token parses as a non-negative integer: a numeric token creates an array, a non-numeric token
|
pointer token is a valid array index: the token `0`, a sequence of digits that does not begin with `0`,
|
||||||
creates an object. For example, on an initially `#!json null` value, `/foo/0/0/0` creates nested arrays,
|
or the token `-` creates an array, and every other token creates an object. For example, on an
|
||||||
while `/foo/one/one/one` creates nested objects. This is not specified by the JSON Pointer RFC; it is
|
initially `#!json null` value, `/foo/0/0/0` creates nested arrays, while `/foo/one/one/one` creates
|
||||||
this library's own, intentional disambiguation rule. See also [JSON Pointer](../../features/json_pointer.md).
|
nested objects. Tokens such as `01` or the empty token cannot be array indices (cf. RFC 6901, Sect. 4)
|
||||||
|
and therefore create objects, just as they would if the level already existed as an object. This is not
|
||||||
|
specified by the JSON Pointer RFC; it is this library's own, intentional disambiguation rule. See also
|
||||||
|
[JSON Pointer](../../features/json_pointer.md).
|
||||||
|
|
||||||
|
!!! warning "Large array indices"
|
||||||
|
|
||||||
|
As the array is filled up with `#!json null` values to the given index, the memory used grows with the index
|
||||||
|
and not with the size of the pointer: on a 64-bit platform, the index `10000000` allocates about 160 MB.
|
||||||
|
The library does not limit the index, so check the array indices of a JSON pointer from untrusted input
|
||||||
|
before using it with the non-const version. See
|
||||||
|
[Parsing untrusted input](../../features/parsing/untrusted_input.md#json-pointers-and-flattened-keys).
|
||||||
|
|
||||||
!!! warning "Deprecation"
|
!!! warning "Deprecation"
|
||||||
|
|
||||||
@@ -291,4 +310,6 @@ Strong exception safety: if an exception occurs, the original value stays intact
|
|||||||
4. Added in version 2.0.0. A missing array index in the const version is guarded by a runtime assertion since
|
4. Added in version 2.0.0. A missing array index in the const version is guarded by a runtime assertion since
|
||||||
version 3.13.0. Throws [`parse_error.109`](../../home/exceptions.md#jsonexceptionparse_error109) instead of
|
version 3.13.0. Throws [`parse_error.109`](../../home/exceptions.md#jsonexceptionparse_error109) instead of
|
||||||
[`out_of_range.404`](../../home/exceptions.md#jsonexceptionout_of_range404) for a one-character array index that is not
|
[`out_of_range.404`](../../home/exceptions.md#jsonexceptionout_of_range404) for a one-character array index that is not
|
||||||
a digit (e.g., `/x`) in version 3.13.0, as it already did for longer ones.
|
a digit (e.g., `/x`) in version 3.13.0, as it already did for longer ones. Fixed in version 3.13.0 to leave the value
|
||||||
|
unchanged if the non-const version throws (strong exception safety, as documented); before, values created for earlier
|
||||||
|
reference tokens of `ptr` were kept.
|
||||||
@@ -40,7 +40,7 @@ Strong guarantee: if an exception is thrown, there are no changes in the JSON va
|
|||||||
where an existing element is required (the "path" of "replace", the "from" of "move" and "copy"); example:
|
where an existing element is required (the "path" of "replace", the "from" of "move" and "copy"); example:
|
||||||
`"array index '-' (3) is out of range"`.
|
`"array index '-' (3) is out of range"`.
|
||||||
- Throws [`out_of_range.403`](../../home/exceptions.md#jsonexceptionout_of_range403) if a JSON pointer inside the patch
|
- Throws [`out_of_range.403`](../../home/exceptions.md#jsonexceptionout_of_range403) if a JSON pointer inside the patch
|
||||||
could not be resolved successfully in the current JSON value; example: `"key baz not found"`.
|
could not be resolved successfully in the current JSON value; example: `"key 'baz' not found"`.
|
||||||
- Throws [`out_of_range.404`](../../home/exceptions.md#jsonexceptionout_of_range404) if a reference token of a JSON
|
- Throws [`out_of_range.404`](../../home/exceptions.md#jsonexceptionout_of_range404) if a reference token of a JSON
|
||||||
pointer inside the patch cannot be resolved, e.g., `-` in a "remove" operation or `1a` for an array; example:
|
pointer inside the patch cannot be resolved, e.g., `-` in a "remove" operation or `1a` for an array; example:
|
||||||
`"unresolved reference token '-'"`.
|
`"unresolved reference token '-'"`.
|
||||||
@@ -53,7 +53,10 @@ Strong guarantee: if an exception is thrown, there are no changes in the JSON va
|
|||||||
- Throws [`out_of_range.414`](../../home/exceptions.md#jsonexceptionout_of_range414) if a "move" operation's "from"
|
- Throws [`out_of_range.414`](../../home/exceptions.md#jsonexceptionout_of_range414) if a "move" operation's "from"
|
||||||
location is a proper prefix of its "path" location.
|
location is a proper prefix of its "path" location.
|
||||||
- Throws [`other_error.501`](../../home/exceptions.md#jsonexceptionother_error501) if "test" operation was
|
- Throws [`other_error.501`](../../home/exceptions.md#jsonexceptionother_error501) if "test" operation was
|
||||||
unsuccessful.
|
unsuccessful. A "test" operation whose "path" does not exist is unsuccessful, too: the failed lookup is reported as a
|
||||||
|
failed test, even if the operation has no "value" member (which would otherwise be
|
||||||
|
[`parse_error.105`](../../home/exceptions.md#jsonexceptionparse_error105)). Which of the two errors is reported for
|
||||||
|
this combination depends on the order in which the operands of a comparison are evaluated, so do not rely on it.
|
||||||
|
|
||||||
## Complexity
|
## Complexity
|
||||||
|
|
||||||
@@ -65,6 +68,10 @@ the JSON value, the complexity can usually be neglected.
|
|||||||
The application of a patch is atomic: Either all operations succeed and the patched document is returned or an exception
|
The application of a patch is atomic: Either all operations succeed and the patched document is returned or an exception
|
||||||
is thrown. In any case, the original value is not changed: the patch is applied to a copy of the value.
|
is thrown. In any case, the original value is not changed: the patch is applied to a copy of the value.
|
||||||
|
|
||||||
|
Duplicate member names inside an operation object are resolved by the parser before the patch is applied (the last one
|
||||||
|
is kept, see [`object_t`](object_t.md#behavior)), so `#!json {"op": "add", "op": "remove", "path": "/a"}` is a
|
||||||
|
`remove` operation.
|
||||||
|
|
||||||
## Examples
|
## Examples
|
||||||
|
|
||||||
??? example "Example: apply a JSON patch"
|
??? example "Example: apply a JSON patch"
|
||||||
@@ -108,6 +115,7 @@ is thrown. In any case, the original value is not changed: the patch is applied
|
|||||||
- Added in version 2.0.0.
|
- Added in version 2.0.0.
|
||||||
- Added [`out_of_range.411`](../../home/exceptions.md#jsonexceptionout_of_range411) and stopped relying on an internal assertion when an "add" operation's
|
- Added [`out_of_range.411`](../../home/exceptions.md#jsonexceptionout_of_range411) and stopped relying on an internal assertion when an "add" operation's
|
||||||
target location has a non-object/non-array parent in version 3.13.0.
|
target location has a non-object/non-array parent in version 3.13.0.
|
||||||
|
This includes a `null` parent, which was previously silently converted to an object, in version 3.13.0.
|
||||||
- Added [`out_of_range.413`](../../home/exceptions.md#jsonexceptionout_of_range413) and stopped silently ignoring a "remove" operation whose target
|
- Added [`out_of_range.413`](../../home/exceptions.md#jsonexceptionout_of_range413) and stopped silently ignoring a "remove" operation whose target
|
||||||
location has a non-object/non-array parent in version 3.13.0.
|
location has a non-object/non-array parent in version 3.13.0.
|
||||||
- Added [`out_of_range.414`](../../home/exceptions.md#jsonexceptionout_of_range414) and rejected a "move" operation whose "from" location is a proper
|
- Added [`out_of_range.414`](../../home/exceptions.md#jsonexceptionout_of_range414) and rejected a "move" operation whose "from" location is a proper
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ No guarantees, value may be corrupted by an unsuccessful patch operation.
|
|||||||
where an existing element is required (the "path" of "replace", the "from" of "move" and "copy"); example:
|
where an existing element is required (the "path" of "replace", the "from" of "move" and "copy"); example:
|
||||||
`"array index '-' (3) is out of range"`.
|
`"array index '-' (3) is out of range"`.
|
||||||
- Throws [`out_of_range.403`](../../home/exceptions.md#jsonexceptionout_of_range403) if a JSON pointer inside the patch
|
- Throws [`out_of_range.403`](../../home/exceptions.md#jsonexceptionout_of_range403) if a JSON pointer inside the patch
|
||||||
could not be resolved successfully in the current JSON value; example: `"key baz not found"`.
|
could not be resolved successfully in the current JSON value; example: `"key 'baz' not found"`.
|
||||||
- Throws [`out_of_range.404`](../../home/exceptions.md#jsonexceptionout_of_range404) if a reference token of a JSON
|
- Throws [`out_of_range.404`](../../home/exceptions.md#jsonexceptionout_of_range404) if a reference token of a JSON
|
||||||
pointer inside the patch cannot be resolved, e.g., `-` in a "remove" operation or `1a` for an array; example:
|
pointer inside the patch cannot be resolved, e.g., `-` in a "remove" operation or `1a` for an array; example:
|
||||||
`"unresolved reference token '-'"`.
|
`"unresolved reference token '-'"`.
|
||||||
@@ -49,7 +49,10 @@ No guarantees, value may be corrupted by an unsuccessful patch operation.
|
|||||||
- Throws [`out_of_range.414`](../../home/exceptions.md#jsonexceptionout_of_range414) if a "move" operation's "from"
|
- Throws [`out_of_range.414`](../../home/exceptions.md#jsonexceptionout_of_range414) if a "move" operation's "from"
|
||||||
location is a proper prefix of its "path" location.
|
location is a proper prefix of its "path" location.
|
||||||
- Throws [`other_error.501`](../../home/exceptions.md#jsonexceptionother_error501) if "test" operation was
|
- Throws [`other_error.501`](../../home/exceptions.md#jsonexceptionother_error501) if "test" operation was
|
||||||
unsuccessful.
|
unsuccessful. A "test" operation whose "path" does not exist is unsuccessful, too: the failed lookup is reported as a
|
||||||
|
failed test, even if the operation has no "value" member (which would otherwise be
|
||||||
|
[`parse_error.105`](../../home/exceptions.md#jsonexceptionparse_error105)). Which of the two errors is reported for
|
||||||
|
this combination depends on the order in which the operands of a comparison are evaluated, so do not rely on it.
|
||||||
|
|
||||||
## Complexity
|
## Complexity
|
||||||
|
|
||||||
@@ -62,6 +65,10 @@ Unlike [`patch`](patch.md), `patch_inplace` applies the operation "in place" and
|
|||||||
That makes it faster for large documents by avoiding the copy. However, the JSON value might be corrupted if the
|
That makes it faster for large documents by avoiding the copy. However, the JSON value might be corrupted if the
|
||||||
function throws an exception.
|
function throws an exception.
|
||||||
|
|
||||||
|
Duplicate member names inside an operation object are resolved by the parser before the patch is applied (the last one
|
||||||
|
is kept, see [`object_t`](object_t.md#behavior)), so `#!json {"op": "add", "op": "remove", "path": "/a"}` is a
|
||||||
|
`remove` operation.
|
||||||
|
|
||||||
## Examples
|
## Examples
|
||||||
|
|
||||||
??? example "Example: apply a JSON patch in place"
|
??? example "Example: apply a JSON patch in place"
|
||||||
@@ -106,6 +113,7 @@ function throws an exception.
|
|||||||
- Added in version 3.11.0.
|
- Added in version 3.11.0.
|
||||||
- Added [`out_of_range.411`](../../home/exceptions.md#jsonexceptionout_of_range411) and stopped relying on an internal assertion when an "add" operation's
|
- Added [`out_of_range.411`](../../home/exceptions.md#jsonexceptionout_of_range411) and stopped relying on an internal assertion when an "add" operation's
|
||||||
target location has a non-object/non-array parent in version 3.13.0.
|
target location has a non-object/non-array parent in version 3.13.0.
|
||||||
|
This includes a `null` parent, which was previously silently converted to an object, in version 3.13.0.
|
||||||
- Added [`out_of_range.413`](../../home/exceptions.md#jsonexceptionout_of_range413) and stopped silently ignoring a "remove" operation whose target
|
- Added [`out_of_range.413`](../../home/exceptions.md#jsonexceptionout_of_range413) and stopped silently ignoring a "remove" operation whose target
|
||||||
location has a non-object/non-array parent in version 3.13.0.
|
location has a non-object/non-array parent in version 3.13.0.
|
||||||
- Added [`out_of_range.414`](../../home/exceptions.md#jsonexceptionout_of_range414) and rejected a "move" operation whose "from" location is a proper
|
- Added [`out_of_range.414`](../../home/exceptions.md#jsonexceptionout_of_range414) and rejected a "move" operation whose "from" location is a proper
|
||||||
|
|||||||
@@ -36,9 +36,9 @@ The function can throw the following exceptions:
|
|||||||
- Throws [`parse_error.109`](../../home/exceptions.md#jsonexceptionparse_error109) if an array index in a key is not a
|
- Throws [`parse_error.109`](../../home/exceptions.md#jsonexceptionparse_error109) if an array index in a key is not a
|
||||||
number; example: `"array index 'one' is not a number"`
|
number; example: `"array index 'one' is not a number"`
|
||||||
- Throws [`out_of_range.404`](../../home/exceptions.md#jsonexceptionout_of_range404) if a level becomes an array
|
- Throws [`out_of_range.404`](../../home/exceptions.md#jsonexceptionout_of_range404) if a level becomes an array
|
||||||
(because one of its keys is `0`) and another key at that level begins with a digit but is not a valid array index
|
(because one of its keys is `0`) and another key at that level is empty (as in `/a/`), is `-`, or begins with a digit
|
||||||
(such as `1a`), or is `-`; example:
|
but is not a valid array index (such as `1a`); examples: `"unresolved reference token '-'"`,
|
||||||
`"unresolved reference token '-'"`
|
`"unresolved reference token ''"`
|
||||||
|
|
||||||
## Complexity
|
## Complexity
|
||||||
|
|
||||||
@@ -46,7 +46,7 @@ Linear in the size of the JSON value.
|
|||||||
|
|
||||||
## Notes
|
## Notes
|
||||||
|
|
||||||
Empty objects and arrays are flattened by [`flatten()`](flatten.md) to `#!json null` values and cannot unflattened to
|
Empty objects and arrays are flattened by [`flatten()`](flatten.md) to `#!json null` values and cannot be unflattened to
|
||||||
their original type.
|
their original type.
|
||||||
|
|
||||||
A flattened array and a flattened object whose keys are array indices are indistinguishable, because both are
|
A flattened array and a flattened object whose keys are array indices are indistinguishable, because both are
|
||||||
@@ -54,7 +54,12 @@ described by the same JSON pointers. A value is therefore restored as an array i
|
|||||||
reference token `0`, and as an object otherwise: `#!json {"2": 1}` is restored unchanged, whereas `#!json {"0": 1}` is
|
reference token `0`, and as an object otherwise: `#!json {"2": 1}` is restored unchanged, whereas `#!json {"0": 1}` is
|
||||||
restored as `#!json [1]`. This decision does not depend on the order in which the flattened object is iterated.
|
restored as `#!json [1]`. This decision does not depend on the order in which the flattened object is iterated.
|
||||||
|
|
||||||
Apart from these two cases, for a JSON value `j`, the following is always true:
|
A third case is an object that has the key `0` next to a key that is not a valid array index: `#!json {"0": 1, "b": 2}`
|
||||||
|
is flattened to `#!json {"/0": 1, "/b": 2}`, and unflatten treats the level as an array and throws
|
||||||
|
[`parse_error.109`](../../home/exceptions.md#jsonexceptionparse_error109) because `b` is not a number. (For the keys
|
||||||
|
`-`, `1a`, or the empty key instead of `b`, it throws [`out_of_range.404`](../../home/exceptions.md#jsonexceptionout_of_range404).)
|
||||||
|
|
||||||
|
Apart from these three cases, for a JSON value `j`, the following is always true:
|
||||||
`#!cpp j == j.flatten().unflatten()`.
|
`#!cpp j == j.flatten().unflatten()`.
|
||||||
|
|
||||||
## Examples
|
## Examples
|
||||||
|
|||||||
@@ -45,6 +45,9 @@ This is equivalent to Python's `dict.get(key, default)`.
|
|||||||
return default_value;
|
return default_value;
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
with one exception: if the JSON value is neither an array nor an object, the function throws
|
||||||
|
[`type_error.306`](../../home/exceptions.md#jsonexceptiontype_error306) before looking at `ptr`, even for the empty
|
||||||
|
pointer, whereas `at` returns the value itself for the empty pointer.
|
||||||
|
|
||||||
!!! note "Differences to `at` and `operator[]`"
|
!!! note "Differences to `at` and `operator[]`"
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,8 @@ Parent of this JSON pointer; in case this JSON pointer is the root, the root its
|
|||||||
|
|
||||||
## Exception safety
|
## Exception safety
|
||||||
|
|
||||||
No-throw guarantee: this function never throws exceptions.
|
Strong exception safety: if an exception occurs, the original value stays intact. The function is not `#!cpp noexcept`
|
||||||
|
and allocates a new JSON pointer, so it can throw `#!cpp std::bad_alloc`; it throws no library exceptions.
|
||||||
|
|
||||||
## Complexity
|
## Complexity
|
||||||
|
|
||||||
|
|||||||
@@ -37,7 +37,8 @@ the stream `o`
|
|||||||
1. Throws [`type_error.316`](../home/exceptions.md#jsonexceptiontype_error316) if a string stored inside the JSON
|
1. Throws [`type_error.316`](../home/exceptions.md#jsonexceptiontype_error316) if a string stored inside the JSON
|
||||||
value is not UTF-8 encoded. Note that unlike the [`dump`](basic_json/dump.md) member functions, no `error_handler`
|
value is not UTF-8 encoded. Note that unlike the [`dump`](basic_json/dump.md) member functions, no `error_handler`
|
||||||
can be set.
|
can be set.
|
||||||
2. None.
|
2. Throws no library exceptions. As the string representation is built with [`to_string`](json_pointer/to_string.md),
|
||||||
|
`#!cpp std::bad_alloc` is possible.
|
||||||
|
|
||||||
## Complexity
|
## Complexity
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,12 @@ to the current JSON value by executing all operations from the patch, yielding t
|
|||||||
If the document is large and the original value is no longer needed,
|
If the document is large and the original value is no longer needed,
|
||||||
[`patch_inplace`](../api/basic_json/patch_inplace.md) applies the same operations in place instead.
|
[`patch_inplace`](../api/basic_json/patch_inplace.md) applies the same operations in place instead.
|
||||||
|
|
||||||
|
!!! note "Duplicate member names"
|
||||||
|
|
||||||
|
Duplicate member names inside an operation object are resolved by the parser before the patch is applied (the last
|
||||||
|
one is kept, see [`object_t`](../api/basic_json/object_t.md#behavior)), so
|
||||||
|
`#!json {"op": "add", "op": "remove", "path": "/a"}` is a `remove` operation.
|
||||||
|
|
||||||
??? example "Example: apply a JSON Patch"
|
??? example "Example: apply a JSON Patch"
|
||||||
|
|
||||||
The following code shows how a JSON patch is applied to a value.
|
The following code shows how a JSON patch is applied to a value.
|
||||||
@@ -41,12 +47,23 @@ flowchart LR
|
|||||||
|
|
||||||
!!! success "Invariant"
|
!!! success "Invariant"
|
||||||
|
|
||||||
For two JSON values *source* and *target*, the following code yields always true:
|
For two JSON values *source* and *target*, the following code yields true:
|
||||||
|
|
||||||
```cpp
|
```cpp
|
||||||
source.patch(diff(source, target)) == target;
|
source.patch(diff(source, target)) == target;
|
||||||
```
|
```
|
||||||
|
|
||||||
|
The equality holds up to [`operator==`](../api/basic_json/operator_eq.md), with these exceptions:
|
||||||
|
|
||||||
|
- `NaN` and [discarded](../api/basic_json/is_discarded.md) values are not equal to themselves, so the invariant
|
||||||
|
cannot hold for them. `diff` emits a `replace` operation for such a value, and a `NaN` in a patch is serialized
|
||||||
|
as `#!json null`.
|
||||||
|
- Numbers that compare equal produce no operation even if their types differ: `diff(1, 1.0)`, `diff(1u, 1)`, and
|
||||||
|
`diff(0.0, -0.0)` return `#!json []`, so the patched value keeps the number type of *source*.
|
||||||
|
- A [binary value](binary_values.md) in a patch is serialized as an object
|
||||||
|
(`#!json {"bytes":[...],"subtype":null}`), so a patch that was dumped and parsed again no longer holds a binary
|
||||||
|
value.
|
||||||
|
|
||||||
??? example "Example: create a JSON Patch from the difference of two values"
|
??? example "Example: create a JSON Patch from the difference of two values"
|
||||||
|
|
||||||
The following code shows how a JSON patch is created as a diff for two JSON values.
|
The following code shows how a JSON patch is created as a diff for two JSON values.
|
||||||
|
|||||||
@@ -37,6 +37,10 @@ Then every value inside the JSON document can be identified as follows:
|
|||||||
Note `/` does not identify the root (i.e., the whole document), but an object entry with empty key `""`. See
|
Note `/` does not identify the root (i.e., the whole document), but an object entry with empty key `""`. See
|
||||||
[RFC 6901](https://tools.ietf.org/html/rfc6901) for more information.
|
[RFC 6901](https://tools.ietf.org/html/rfc6901) for more information.
|
||||||
|
|
||||||
|
[RFC 6901, section 4](https://tools.ietf.org/html/rfc6901#section-4) states that evaluation fails if an object has
|
||||||
|
non-unique member names. A `basic_json` object holds one value per key (see [`object_t`](../api/basic_json/object_t.md)
|
||||||
|
for how duplicate keys are handled while parsing), so evaluating a JSON Pointer never encounters duplicates.
|
||||||
|
|
||||||
## JSON Pointer creation
|
## JSON Pointer creation
|
||||||
|
|
||||||
JSON Pointers can be created from a string:
|
JSON Pointers can be created from a string:
|
||||||
@@ -54,6 +58,13 @@ auto p = "/nested/one"_json_pointer;
|
|||||||
The escaping rules of [RFC 6901](https://tools.ietf.org/html/rfc6901) are implemented. See the
|
The escaping rules of [RFC 6901](https://tools.ietf.org/html/rfc6901) are implemented. See the
|
||||||
[constructor documentation](../api/json_pointer/json_pointer.md) for more information.
|
[constructor documentation](../api/json_pointer/json_pointer.md) for more information.
|
||||||
|
|
||||||
|
!!! note "URI fragment representation"
|
||||||
|
|
||||||
|
[RFC 6901, section 6](https://tools.ietf.org/html/rfc6901#section-6) also defines a URI fragment identifier
|
||||||
|
representation (`#/a%20b`). It is not supported: `#!cpp json::json_pointer("#/a")` throws
|
||||||
|
[`parse_error.107`](../home/exceptions.md#jsonexceptionparse_error107). Strip the leading `#` and
|
||||||
|
percent-decode the remainder before creating the JSON pointer.
|
||||||
|
|
||||||
## Value access
|
## Value access
|
||||||
|
|
||||||
JSON Pointers can be used in the [`at`](../api/basic_json/at.md), [`operator[]`](../api/basic_json/operator%5B%5D.md),
|
JSON Pointers can be used in the [`at`](../api/basic_json/at.md), [`operator[]`](../api/basic_json/operator%5B%5D.md),
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
The library supports JSON Merge Patch ([RFC 7396](https://tools.ietf.org/html/rfc7396)) as a patch format.
|
The library supports JSON Merge Patch ([RFC 7396](https://tools.ietf.org/html/rfc7396)) as a patch format.
|
||||||
The merge patch format is primarily intended for use with the HTTP PATCH method as a means of describing a set of
|
The merge patch format is primarily intended for use with the HTTP PATCH method as a means of describing a set of
|
||||||
modifications to a target resource's content. This function applies a merge patch to the current JSON value.
|
modifications to a target resource's content. Applying a merge patch changes the JSON value the patch is applied to.
|
||||||
|
|
||||||
Instead of using [JSON Pointer](json_pointer.md) to specify values to be manipulated, it describes the changes using a
|
Instead of using [JSON Pointer](json_pointer.md) to specify values to be manipulated, it describes the changes using a
|
||||||
syntax that closely mimics the document being modified. Unlike [JSON Patch](json_patch.md), a JSON Merge Patch cannot
|
syntax that closely mimics the document being modified. Unlike [JSON Patch](json_patch.md), a JSON Merge Patch cannot
|
||||||
@@ -19,6 +19,18 @@ another.
|
|||||||
[`update`](../api/basic_json/update.md) with `merge_objects` set to `#!cpp true`; see
|
[`update`](../api/basic_json/update.md) with `merge_objects` set to `#!cpp true`; see
|
||||||
[Merging objects](modifying_values.md#merging-objects).
|
[Merging objects](modifying_values.md#merging-objects).
|
||||||
|
|
||||||
|
!!! note "Limitations of RFC 7396"
|
||||||
|
|
||||||
|
- A patch cannot set a member to `#!json null`, because `#!json null` removes the member:
|
||||||
|
`#!json {"a":1}` patched with `#!json {"a":null}` gives `#!json {}`.
|
||||||
|
- Arrays are replaced as a whole, never merged, and a `#!json null` inside an array is kept.
|
||||||
|
- `#!json null` members of the target that the patch does not mention are preserved.
|
||||||
|
- A `#!json null` that would remove a member of a not yet existing object still creates that object:
|
||||||
|
`#!json {}` patched with `#!json {"a":{"b":null}}` gives `#!json {"a":{}}`.
|
||||||
|
- A patch that is not an object replaces the whole target (a `#!json null` patch makes the target
|
||||||
|
`#!json null`); an object patch applied to a non-object target starts from an empty object.
|
||||||
|
- [`merge_diff`](../api/basic_json/merge_diff.md#notes) has a matching limitation for `#!json null` members.
|
||||||
|
|
||||||
??? example
|
??? example
|
||||||
|
|
||||||
The following code shows how a JSON Merge Patch is applied to a JSON document.
|
The following code shows how a JSON Merge Patch is applied to a JSON document.
|
||||||
|
|||||||
@@ -130,6 +130,17 @@ parameters of [`parse()`](../../api/basic_json/parse.md) or [`accept()`](../../a
|
|||||||
enable either for input whose conformance you cannot otherwise control, since interoperability with strictly
|
enable either for input whose conformance you cannot otherwise control, since interoperability with strictly
|
||||||
conforming JSON consumers is exactly what the default rejects.
|
conforming JSON consumers is exactly what the default rejects.
|
||||||
|
|
||||||
|
## JSON Pointers and flattened keys
|
||||||
|
|
||||||
|
A [JSON Pointer](../json_pointer.md) can come from untrusted input as well, for instance as a path in a request or as a
|
||||||
|
key of a flattened object. The non-const [`operator[]`](../../api/basic_json/operator%5B%5D.md) with a JSON Pointer and
|
||||||
|
[`unflatten`](../../api/basic_json/unflatten.md) create missing values, and when an array index lies beyond the end of
|
||||||
|
an array, they fill the array with `#!json null` values up to that index. The memory used therefore grows with the
|
||||||
|
index, not with the size of the pointer: on a 64-bit platform, the index `10000000` allocates about 160 MB, the index
|
||||||
|
`100000000` about 1.6 GB. The library does not limit the index. For pointers from untrusted input, check the array
|
||||||
|
indices (or the pointer as a whole) before using them, or use [`at`](../../api/basic_json/at.md) or
|
||||||
|
[`contains`](../../api/basic_json/contains.md), which never add values.
|
||||||
|
|
||||||
## Checklist
|
## Checklist
|
||||||
|
|
||||||
- Wrap parsing in a `#!cpp try`/`#!cpp catch` block, or use `allow_exceptions=false`/`accept()` if your environment
|
- Wrap parsing in a `#!cpp try`/`#!cpp catch` block, or use `allow_exceptions=false`/`accept()` if your environment
|
||||||
@@ -145,6 +156,8 @@ conforming JSON consumers is exactly what the default rejects.
|
|||||||
`JSON_STRICT_NUL_HANDLING`.
|
`JSON_STRICT_NUL_HANDLING`.
|
||||||
- Decide whether duplicate object keys should be an error for your application, and add a callback if so.
|
- Decide whether duplicate object keys should be an error for your application, and add a callback if so.
|
||||||
- Do not enable `ignore_comments` or `ignore_trailing_commas` for input that must be strictly conforming JSON.
|
- Do not enable `ignore_comments` or `ignore_trailing_commas` for input that must be strictly conforming JSON.
|
||||||
|
- Check the array indices of untrusted JSON Pointers and flattened keys before passing them to the non-const
|
||||||
|
`operator[]` or `unflatten()`, which fill arrays with `#!json null` values up to the index.
|
||||||
|
|
||||||
For the broader design rationale and how it is tested (fuzzing, sanitizers, static analysis), see the
|
For the broader design rationale and how it is tested (fuzzing, sanitizers, static analysis), see the
|
||||||
[assurance case](../../community/assurance_case.md) and [quality assurance](../../community/quality_assurance.md). To
|
[assurance case](../../community/assurance_case.md) and [quality assurance](../../community/quality_assurance.md). To
|
||||||
|
|||||||
@@ -390,6 +390,7 @@ using array_t = ArrayType<basic_json, AllocatorType<basic_json>>;
|
|||||||
described in RFC 6901. Neither `find(const StringType&, size_type)`, nor `substr(pos, count)`, nor
|
described in RFC 6901. Neither `find(const StringType&, size_type)`, nor `substr(pos, count)`, nor
|
||||||
`replace(pos, count, const StringType&)` is required.
|
`replace(pos, count, const StringType&)` is required.
|
||||||
- `empty()`.
|
- `empty()`.
|
||||||
|
- `resize(n)` (one argument) -- used by `to_string` to size the result before the reference tokens are copied into it.
|
||||||
- `begin()` and `end()` -- used by
|
- `begin()` and `end()` -- used by
|
||||||
[`operator[](const json_pointer&)`](../../api/basic_json/operator%5B%5D.md) to decide whether a reference token
|
[`operator[](const json_pointer&)`](../../api/basic_json/operator%5B%5D.md) to decide whether a reference token
|
||||||
denotes an array index.
|
denotes an array index.
|
||||||
|
|||||||
@@ -1083,9 +1083,14 @@ a JSON pointer exceeds the range of `size_type` (e.g., on 32-bit platforms).
|
|||||||
|
|
||||||
```
|
```
|
||||||
enum value out of range
|
enum value out of range
|
||||||
array index 18446744073709551616 exceeds size_type
|
array index 18446744073709551615 exceeds size_type
|
||||||
```
|
```
|
||||||
|
|
||||||
|
The second message is thrown, for instance, by `#!cpp json::parse("[1, 2]").at("/18446744073709551615"_json_pointer)`
|
||||||
|
on a 64-bit platform, where `size_type` is `#!cpp std::size_t` and this index is `SIZE_MAX`. An index that does not
|
||||||
|
fit into 64 bits (such as `18446744073709551616`) is not reported as 410 but as
|
||||||
|
[`out_of_range.404`](#jsonexceptionout_of_range404).
|
||||||
|
|
||||||
### json.exception.out_of_range.411
|
### json.exception.out_of_range.411
|
||||||
|
|
||||||
A JSON Patch `add` operation cannot be applied because the target location's parent is neither an object nor an array. Per [RFC 6902](https://datatracker.ietf.org/doc/html/rfc6902), an `add` target must reference a member of an existing object or an element of an existing array; a primitive value (string, number, boolean, etc.) cannot receive a new member or element.
|
A JSON Patch `add` operation cannot be applied because the target location's parent is neither an object nor an array. Per [RFC 6902](https://datatracker.ietf.org/doc/html/rfc6902), an `add` target must reference a member of an existing object or an element of an existing array; a primitive value (string, number, boolean, etc.) cannot receive a new member or element.
|
||||||
@@ -1100,6 +1105,8 @@ A JSON Patch `add` operation cannot be applied because the target location's par
|
|||||||
|
|
||||||
This exception was added in version 3.13.0. Before that, this situation hit an internal assertion (aborting the program in debug builds) or was silently ignored when assertions were disabled.
|
This exception was added in version 3.13.0. Before that, this situation hit an internal assertion (aborting the program in debug builds) or was silently ignored when assertions were disabled.
|
||||||
|
|
||||||
|
A `null` parent is rejected as well. Before version 3.13.0, an `add` into a `null` parent silently converted it to an object (e.g., `add` at `/foo/bar` turned `{"foo":null}` into `{"foo":{"bar":1}}`).
|
||||||
|
|
||||||
### json.exception.out_of_range.412
|
### json.exception.out_of_range.412
|
||||||
|
|
||||||
BSON stores the length of documents, arrays, strings, and binary values in a signed 32-bit integer, and MessagePack
|
BSON stores the length of documents, arrays, strings, and binary values in a signed 32-bit integer, and MessagePack
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <algorithm> // all_of, copy
|
#include <algorithm> // copy
|
||||||
#include <array> // array
|
#include <array> // array
|
||||||
#include <cctype> // isdigit
|
#include <cctype> // isdigit
|
||||||
#include <cerrno> // errno, ERANGE
|
#include <cerrno> // errno, ERANGE
|
||||||
@@ -16,9 +16,11 @@
|
|||||||
#include <cstdint> // uint64_t
|
#include <cstdint> // uint64_t
|
||||||
#include <cstdlib> // strtoull
|
#include <cstdlib> // strtoull
|
||||||
#include <cstring> // memchr, memcmp, memcpy
|
#include <cstring> // memchr, memcmp, memcpy
|
||||||
|
#include <functional> // less
|
||||||
#ifndef JSON_NO_IO
|
#ifndef JSON_NO_IO
|
||||||
#include <iosfwd> // ostream
|
#include <iosfwd> // ostream
|
||||||
#endif // JSON_NO_IO
|
#endif // JSON_NO_IO
|
||||||
|
#include <iterator> // next
|
||||||
#include <limits> // max
|
#include <limits> // max
|
||||||
#include <string> // string
|
#include <string> // string
|
||||||
#include <utility> // move
|
#include <utility> // move
|
||||||
@@ -28,6 +30,7 @@
|
|||||||
#include <nlohmann/detail/macro_scope.hpp>
|
#include <nlohmann/detail/macro_scope.hpp>
|
||||||
#include <nlohmann/detail/meta/cpp_future.hpp>
|
#include <nlohmann/detail/meta/cpp_future.hpp>
|
||||||
#include <nlohmann/detail/meta/type_traits.hpp>
|
#include <nlohmann/detail/meta/type_traits.hpp>
|
||||||
|
#include <nlohmann/detail/meta/void_t.hpp>
|
||||||
#include <nlohmann/detail/string_concat.hpp>
|
#include <nlohmann/detail/string_concat.hpp>
|
||||||
#include <nlohmann/detail/string_escape.hpp>
|
#include <nlohmann/detail/string_escape.hpp>
|
||||||
#include <nlohmann/detail/string_utils.hpp>
|
#include <nlohmann/detail/string_utils.hpp>
|
||||||
@@ -134,6 +137,26 @@ JSON_HEDLEY_ALWAYS_INLINE array_index_status parse_array_index(const StringType&
|
|||||||
return array_index_status::ok;
|
return array_index_status::ok;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// whether two keys of type @a StringType are equivalent under @a Compare
|
||||||
|
/// exactly if they are equal: std::less<StringType> and std::less<>
|
||||||
|
template<typename Compare, typename StringType>
|
||||||
|
struct is_less_comparator : std::false_type {};
|
||||||
|
|
||||||
|
template<typename StringType>
|
||||||
|
struct is_less_comparator<std::less<StringType>, StringType> : std::true_type {};
|
||||||
|
|
||||||
|
template<typename StringType>
|
||||||
|
struct is_less_comparator<std::less<void>, StringType> : std::true_type {};
|
||||||
|
|
||||||
|
/// whether the objects of type @a ObjectType find a key of type @a StringType
|
||||||
|
/// exactly if it equals a stored key
|
||||||
|
template<typename ObjectType, typename StringType, typename = void>
|
||||||
|
struct finds_equal_keys : std::false_type {};
|
||||||
|
|
||||||
|
template<typename ObjectType, typename StringType>
|
||||||
|
struct finds_equal_keys<ObjectType, StringType, void_t<typename ObjectType::key_compare, typename ObjectType::key_type>>
|
||||||
|
: conjunction<is_less_comparator<typename ObjectType::key_compare, StringType>, std::is_same<typename ObjectType::key_type, StringType>> {};
|
||||||
|
|
||||||
} // namespace detail
|
} // namespace detail
|
||||||
|
|
||||||
/// @brief JSON Pointer defines a string syntax for identifying a specific value within a JSON document
|
/// @brief JSON Pointer defines a string syntax for identifying a specific value within a JSON document
|
||||||
@@ -589,6 +612,228 @@ class json_pointer
|
|||||||
return object_member_impl(object, key, detail::priority_tag<1> {});
|
return object_member_impl(object, key, detail::priority_tag<1> {});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// @brief append a null value to @a array, like operator[] with the size of @a array
|
||||||
|
template<typename BasicJsonType>
|
||||||
|
static BasicJsonType& array_append(BasicJsonType& array)
|
||||||
|
{
|
||||||
|
auto& elements = *array.m_data.m_value.array;
|
||||||
|
const auto old_capacity = array.container_capacity();
|
||||||
|
elements.emplace_back();
|
||||||
|
return array.set_parent_after_insert(elements.back(), old_capacity);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @brief add @a key, which is missing, to @a object
|
||||||
|
template<typename BasicJsonType>
|
||||||
|
static auto object_add_impl(BasicJsonType& object, const string_t& key, detail::priority_tag<1> /*unused*/)
|
||||||
|
-> decltype(object.m_data.m_value.object->emplace(key, nullptr), std::declval<BasicJsonType&>())
|
||||||
|
{
|
||||||
|
const auto old_capacity = object.container_capacity();
|
||||||
|
const auto it = object.m_data.m_value.object->emplace(key, nullptr).first;
|
||||||
|
return object.set_parent_after_insert(it->second, old_capacity);
|
||||||
|
}
|
||||||
|
|
||||||
|
template<typename BasicJsonType>
|
||||||
|
static BasicJsonType& object_add_impl(BasicJsonType& object, const string_t& key, detail::priority_tag<0> /*unused*/)
|
||||||
|
{
|
||||||
|
return object.operator[](key);
|
||||||
|
}
|
||||||
|
|
||||||
|
template<typename BasicJsonType>
|
||||||
|
static BasicJsonType& object_add(BasicJsonType& object, const string_t& key)
|
||||||
|
{
|
||||||
|
return object_add_impl(object, key, detail::priority_tag<1> {});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @brief search @a key in @a members: the result is where the key is or,
|
||||||
|
/// for objects that can insert at a position (std::map), where it belongs
|
||||||
|
template<typename ObjectType>
|
||||||
|
static auto object_search_impl(ObjectType& members, const string_t& key, detail::priority_tag<1> /*unused*/)
|
||||||
|
-> decltype(members.key_comp()(key, members.lower_bound(key)->first), members.lower_bound(key))
|
||||||
|
{
|
||||||
|
return members.lower_bound(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
template<typename ObjectType>
|
||||||
|
static auto object_search_impl(ObjectType& members, const string_t& key, detail::priority_tag<0> /*unused*/)
|
||||||
|
-> decltype(members.find(key))
|
||||||
|
{
|
||||||
|
return members.find(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
template<typename ObjectType>
|
||||||
|
static auto object_search(ObjectType& members, const string_t& key)
|
||||||
|
-> decltype(object_search_impl(members, key, detail::priority_tag<1> {}))
|
||||||
|
{
|
||||||
|
return object_search_impl(members, key, detail::priority_tag<1> {});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @brief whether @a it, the result of @ref object_search for @a key, points to @a key
|
||||||
|
///
|
||||||
|
/// With std::less, the key at the lower bound is equivalent to @a key
|
||||||
|
/// exactly if it is equal to it, and comparing for equality is cheaper.
|
||||||
|
template<typename ObjectType, typename Iterator,
|
||||||
|
detail::enable_if_t<detail::finds_equal_keys<ObjectType, string_t>::value, int> = 0>
|
||||||
|
static auto object_search_found_impl(ObjectType& members, const Iterator& it, const string_t& key, detail::priority_tag<2> /*unused*/)
|
||||||
|
-> decltype(members.lower_bound(key), it->first == key)
|
||||||
|
{
|
||||||
|
return it != members.end() && it->first == key;
|
||||||
|
}
|
||||||
|
|
||||||
|
template<typename ObjectType, typename Iterator>
|
||||||
|
static auto object_search_found_impl(ObjectType& members, const Iterator& it, const string_t& key, detail::priority_tag<1> /*unused*/)
|
||||||
|
-> decltype(members.key_comp()(key, it->first), members.lower_bound(key), bool())
|
||||||
|
{
|
||||||
|
return it != members.end() && !members.key_comp()(key, it->first);
|
||||||
|
}
|
||||||
|
|
||||||
|
template<typename ObjectType, typename Iterator>
|
||||||
|
static bool object_search_found_impl(ObjectType& members, const Iterator& it, const string_t& /*key*/, detail::priority_tag<0> /*unused*/)
|
||||||
|
{
|
||||||
|
return it != members.end();
|
||||||
|
}
|
||||||
|
|
||||||
|
template<typename ObjectType, typename Iterator>
|
||||||
|
static bool object_search_found(ObjectType& members, const Iterator& it, const string_t& key)
|
||||||
|
{
|
||||||
|
return object_search_found_impl(members, it, key, detail::priority_tag<2> {});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @brief insert @a key, which is missing, into @a object at @a it, the
|
||||||
|
/// result of @ref object_search for @a key
|
||||||
|
template<typename BasicJsonType, typename Iterator>
|
||||||
|
static auto object_insert_impl(BasicJsonType& object, const Iterator& it, const string_t& key, detail::priority_tag<1> /*unused*/)
|
||||||
|
-> decltype(object.m_data.m_value.object->emplace_hint(it, key, nullptr), std::declval<BasicJsonType&>())
|
||||||
|
{
|
||||||
|
const auto old_capacity = object.container_capacity();
|
||||||
|
const auto pos = object.m_data.m_value.object->emplace_hint(it, key, nullptr);
|
||||||
|
return object.set_parent_after_insert(pos->second, old_capacity);
|
||||||
|
}
|
||||||
|
|
||||||
|
template<typename BasicJsonType, typename Iterator>
|
||||||
|
static BasicJsonType& object_insert_impl(BasicJsonType& object, const Iterator& /*it*/, const string_t& key, detail::priority_tag<0> /*unused*/)
|
||||||
|
{
|
||||||
|
return object_add(object, key);
|
||||||
|
}
|
||||||
|
|
||||||
|
template<typename BasicJsonType, typename Iterator>
|
||||||
|
static BasicJsonType& object_insert(BasicJsonType& object, const Iterator& it, const string_t& key)
|
||||||
|
{
|
||||||
|
return object_insert_impl(object, it, key, detail::priority_tag<1> {});
|
||||||
|
}
|
||||||
|
|
||||||
|
/*!
|
||||||
|
@brief undoes the change @ref get_unchecked_create made to an existing value
|
||||||
|
|
||||||
|
@ref get_unchecked walks existing values without changing them. At the
|
||||||
|
first reference token that does not select an existing value, @ref
|
||||||
|
get_unchecked_create makes exactly one change to an existing value: it
|
||||||
|
converts a null value, inserts a key into an object, or grows an array.
|
||||||
|
Everything it creates afterwards lies below that change. If a later step
|
||||||
|
throws, the destructor of this guard reverts that one change, which
|
||||||
|
removes everything created after it, so the value is left as it was
|
||||||
|
(strong exception safety). The guard needs no try/catch and therefore also
|
||||||
|
compiles with JSON_NOEXCEPTION.
|
||||||
|
|
||||||
|
Reverting the change must not throw, as it happens in a destructor. It
|
||||||
|
erases the inserted member by its iterator (no key is converted or
|
||||||
|
compared, nothing is allocated), erases the added elements at the end of
|
||||||
|
the array, or swaps the null value back. This holds for std::map,
|
||||||
|
std::vector, ordered_map, and every object_t, array_t, and
|
||||||
|
json_base_class_t whose erase() and move operations do not throw.
|
||||||
|
Between recording and reverting, nothing is inserted into or erased from
|
||||||
|
the object or array, so the iterator stays valid.
|
||||||
|
*/
|
||||||
|
template<typename BasicJsonType>
|
||||||
|
class get_unchecked_rollback
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
using object_iterator = typename BasicJsonType::object_t::iterator;
|
||||||
|
|
||||||
|
/// record that @a member was inserted into the object @a value
|
||||||
|
get_unchecked_rollback(BasicJsonType* value, const object_iterator& member)
|
||||||
|
: m_value(value)
|
||||||
|
, m_member(member)
|
||||||
|
, m_change(change::key_inserted)
|
||||||
|
{}
|
||||||
|
|
||||||
|
/// record the change that the next reference token makes to the null value or array @a value
|
||||||
|
explicit get_unchecked_rollback(BasicJsonType* value)
|
||||||
|
: m_value(value)
|
||||||
|
{
|
||||||
|
if (value->is_null())
|
||||||
|
{
|
||||||
|
// keep the null value (with its base class and positions) and
|
||||||
|
// continue with a fresh null value in its place, which is what
|
||||||
|
// assigning an array or object to it would have left there
|
||||||
|
m_saved_null.swap(*value);
|
||||||
|
m_change = change::null_converted;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
JSON_ASSERT(value->is_array());
|
||||||
|
m_old_size = value->m_data.m_value.array->size();
|
||||||
|
m_change = change::array_grown;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get_unchecked_rollback(const get_unchecked_rollback&) = delete;
|
||||||
|
get_unchecked_rollback(get_unchecked_rollback&&) = delete;
|
||||||
|
get_unchecked_rollback& operator=(const get_unchecked_rollback&) = delete;
|
||||||
|
get_unchecked_rollback& operator=(get_unchecked_rollback&&) = delete;
|
||||||
|
|
||||||
|
~get_unchecked_rollback()
|
||||||
|
{
|
||||||
|
if (JSON_HEDLEY_UNLIKELY(m_change != change::none))
|
||||||
|
{
|
||||||
|
undo();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// keep the change
|
||||||
|
void commit() noexcept
|
||||||
|
{
|
||||||
|
m_change = change::none;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
JSON_HEDLEY_NEVER_INLINE void undo() noexcept
|
||||||
|
{
|
||||||
|
switch (m_change)
|
||||||
|
{
|
||||||
|
case change::null_converted:
|
||||||
|
// the values created since go away with m_saved_null
|
||||||
|
m_value->swap(m_saved_null);
|
||||||
|
break;
|
||||||
|
case change::key_inserted:
|
||||||
|
m_value->m_data.m_value.object->erase(m_member);
|
||||||
|
break;
|
||||||
|
case change::array_grown:
|
||||||
|
{
|
||||||
|
auto& elements = *m_value->m_data.m_value.array;
|
||||||
|
elements.erase(std::next(elements.begin(), static_cast<typename BasicJsonType::difference_type>(m_old_size)), elements.end());
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case change::none:
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class change
|
||||||
|
{
|
||||||
|
none,
|
||||||
|
null_converted,
|
||||||
|
key_inserted,
|
||||||
|
array_grown
|
||||||
|
};
|
||||||
|
|
||||||
|
BasicJsonType* m_value;
|
||||||
|
object_iterator m_member {};
|
||||||
|
typename BasicJsonType::size_type m_old_size = 0;
|
||||||
|
change m_change = change::none;
|
||||||
|
BasicJsonType m_saved_null {};
|
||||||
|
};
|
||||||
|
|
||||||
JSON_PRIVATE_UNLESS_TESTED:
|
JSON_PRIVATE_UNLESS_TESTED:
|
||||||
json_pointer top() const
|
json_pointer top() const
|
||||||
{
|
{
|
||||||
@@ -972,43 +1217,245 @@ class json_pointer
|
|||||||
|
|
||||||
Complexity: Linear in the length of the JSON pointer.
|
Complexity: Linear in the length of the JSON pointer.
|
||||||
|
|
||||||
@throw parse_error.106 if an array index begins with '0'
|
Exception safety: Strong guarantee. This function walks existing values
|
||||||
@throw parse_error.109 if an array index was not a number
|
until the first reference token that does not select one. If that is the
|
||||||
@throw out_of_range.404 if the JSON pointer can not be resolved
|
last reference token, inserting the key into the object or growing the
|
||||||
|
array is strongly exception safe by itself; any other change is made by
|
||||||
|
@ref get_unchecked_create, which undoes it if a later step throws.
|
||||||
|
|
||||||
|
@throw parse_error.106 if an array index of an existing array begins with '0'
|
||||||
|
@throw parse_error.109 if an array index of an existing array was not a number
|
||||||
|
@throw out_of_range.404 if the JSON pointer can not be resolved, or if an
|
||||||
|
array index is too large for strtoull
|
||||||
|
@throw out_of_range.410 if an array index exceeds size_type
|
||||||
|
@throw std::length_error if padding an array exceeds its maximal size
|
||||||
|
@throw std::bad_alloc if an allocation fails
|
||||||
*/
|
*/
|
||||||
template<typename BasicJsonType>
|
template<typename BasicJsonType>
|
||||||
BasicJsonType& get_unchecked(BasicJsonType* ptr) const
|
BasicJsonType& get_unchecked(BasicJsonType* ptr) const
|
||||||
{
|
{
|
||||||
for (const auto& reference_token : reference_tokens)
|
const auto* token = reference_tokens.data();
|
||||||
|
const auto* const end = token + reference_tokens.size();
|
||||||
|
if (token == end)
|
||||||
{
|
{
|
||||||
|
return *ptr;
|
||||||
|
}
|
||||||
|
if (ptr->is_null())
|
||||||
|
{
|
||||||
|
// the null value is converted to an array or an object
|
||||||
|
return get_unchecked_create(ptr, token, end);
|
||||||
|
}
|
||||||
|
|
||||||
|
// walk existing values; the walk itself inserts nothing, which keeps
|
||||||
|
// the loop fast (insertions are done below or in get_unchecked_create)
|
||||||
|
typename BasicJsonType::object_t::iterator it;
|
||||||
|
do
|
||||||
|
{
|
||||||
|
if (ptr->is_object())
|
||||||
|
{
|
||||||
|
auto& members = *ptr->m_data.m_value.object;
|
||||||
|
it = object_search(members, *token);
|
||||||
|
if (!object_search_found(members, it, *token))
|
||||||
|
{
|
||||||
|
// the key is inserted below or in get_unchecked_create
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
ptr = &it->second;
|
||||||
|
}
|
||||||
|
else if (ptr->is_array())
|
||||||
|
{
|
||||||
|
if (is_dash(*token))
|
||||||
|
{
|
||||||
|
if (token + 1 == end)
|
||||||
|
{
|
||||||
|
// explicitly treat "-" as index beyond the end
|
||||||
|
return array_append(*ptr);
|
||||||
|
}
|
||||||
|
// an element is appended in get_unchecked_create
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (token + 1 == end)
|
||||||
|
{
|
||||||
|
// convert array index to number; unchecked access
|
||||||
|
return ptr->operator[](array_index<BasicJsonType>(*token));
|
||||||
|
}
|
||||||
|
const auto idx = array_index<BasicJsonType>(*token);
|
||||||
|
if (idx >= ptr->m_data.m_value.array->size())
|
||||||
|
{
|
||||||
|
// the array is padded in get_unchecked_create
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
ptr = &(*ptr->m_data.m_value.array)[idx];
|
||||||
|
}
|
||||||
|
else if (ptr->is_null())
|
||||||
|
{
|
||||||
|
// the null value is converted in get_unchecked_create
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// primitive value with a reference token left
|
||||||
|
throw_unresolved(*token, ptr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
while (++token != end);
|
||||||
|
|
||||||
|
if (token == end)
|
||||||
|
{
|
||||||
|
return *ptr;
|
||||||
|
}
|
||||||
|
if (token + 1 == end && ptr->is_object())
|
||||||
|
{
|
||||||
|
// insert the missing key where the walk found it belongs
|
||||||
|
return object_insert(*ptr, it, *token);
|
||||||
|
}
|
||||||
|
return get_unchecked_create(ptr, token, end);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*!
|
||||||
|
@brief create the values for the reference tokens from @a first on
|
||||||
|
|
||||||
|
Continues @ref get_unchecked at the first reference token that does not
|
||||||
|
select an existing value. That token changes @a ptr, an existing value (see
|
||||||
|
@ref get_unchecked_rollback); all later tokens only change values created
|
||||||
|
here.
|
||||||
|
|
||||||
|
If the emplace() of the object type finds a member that the lookup in @ref
|
||||||
|
get_unchecked did not find (the two disagree, e.g. for fifo_map), nothing
|
||||||
|
is inserted, and the walk continues with that member as in @ref
|
||||||
|
get_unchecked, which is what basic_json::operator[] does.
|
||||||
|
|
||||||
|
@param[in] ptr the value the reference token @a first is applied to
|
||||||
|
@param[in] first the first reference token to resolve (an element of
|
||||||
|
@ref reference_tokens)
|
||||||
|
@param[in] end the end of @ref reference_tokens
|
||||||
|
|
||||||
|
@throw parse_error.106 if an array index of an existing array begins with
|
||||||
|
'0' (only after emplace() found a member, see above)
|
||||||
|
@throw parse_error.109 if an array index of an existing array was not a
|
||||||
|
number (only after emplace() found a member)
|
||||||
|
@throw out_of_range.404 if an array index is too large for strtoull, or (only
|
||||||
|
after emplace() found a member) if the JSON pointer
|
||||||
|
can not be resolved
|
||||||
|
@throw out_of_range.410 if an array index exceeds size_type
|
||||||
|
@throw std::length_error if padding an array exceeds its maximal size
|
||||||
|
@throw std::bad_alloc if an allocation fails
|
||||||
|
*/
|
||||||
|
template<typename BasicJsonType>
|
||||||
|
JSON_HEDLEY_NEVER_INLINE static BasicJsonType& get_unchecked_create(BasicJsonType* ptr, const string_t* first, const string_t* end)
|
||||||
|
{
|
||||||
|
const auto* token = first;
|
||||||
|
while (ptr->is_object())
|
||||||
|
{
|
||||||
|
const auto old_capacity = ptr->container_capacity();
|
||||||
|
const auto inserted = ptr->m_data.m_value.object->emplace(*token, nullptr);
|
||||||
|
if (JSON_HEDLEY_LIKELY(inserted.second))
|
||||||
|
{
|
||||||
|
get_unchecked_rollback<BasicJsonType> rollback(ptr, inserted.first);
|
||||||
|
BasicJsonType& result = create_values(&ptr->set_parent_after_insert(inserted.first->second, old_capacity), token + 1, end);
|
||||||
|
rollback.commit();
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// nothing was inserted: walk existing values from the member
|
||||||
|
// emplace() found on, like get_unchecked
|
||||||
|
ptr = &inserted.first->second;
|
||||||
|
while (++token != end)
|
||||||
|
{
|
||||||
|
if (ptr->is_object())
|
||||||
|
{
|
||||||
|
auto& members = *ptr->m_data.m_value.object;
|
||||||
|
const auto it = object_search(members, *token);
|
||||||
|
if (!object_search_found(members, it, *token))
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
ptr = &it->second;
|
||||||
|
}
|
||||||
|
else if (ptr->is_array())
|
||||||
|
{
|
||||||
|
if (is_dash(*token))
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
const auto idx = array_index<BasicJsonType>(*token);
|
||||||
|
if (idx >= ptr->m_data.m_value.array->size())
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
ptr = &(*ptr->m_data.m_value.array)[idx];
|
||||||
|
}
|
||||||
|
else if (ptr->is_null())
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// primitive value with a reference token left
|
||||||
|
throw_unresolved(*token, ptr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (token == end)
|
||||||
|
{
|
||||||
|
return *ptr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get_unchecked_rollback<BasicJsonType> rollback(ptr);
|
||||||
|
BasicJsonType& result = create_values(ptr, token, end);
|
||||||
|
rollback.commit();
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*!
|
||||||
|
@brief the loop of @ref get_unchecked_create
|
||||||
|
|
||||||
|
@a ptr is a null value or an array. Every value created here is a fresh
|
||||||
|
one, so every object a key is added to is empty.
|
||||||
|
|
||||||
|
@throw out_of_range.404 if an array index is too large for strtoull
|
||||||
|
@throw out_of_range.410 if an array index exceeds size_type
|
||||||
|
@throw std::length_error if padding an array exceeds its maximal size
|
||||||
|
@throw std::bad_alloc if an allocation fails
|
||||||
|
*/
|
||||||
|
template<typename BasicJsonType>
|
||||||
|
static BasicJsonType& create_values(BasicJsonType* ptr, const string_t* first, const string_t* end)
|
||||||
|
{
|
||||||
|
for (const auto* token = first; token != end; ++token)
|
||||||
|
{
|
||||||
|
const auto& reference_token = *token;
|
||||||
|
|
||||||
// convert null values to arrays or objects before continuing
|
// convert null values to arrays or objects before continuing
|
||||||
if (ptr->is_null())
|
if (ptr->is_null())
|
||||||
{
|
{
|
||||||
// check if the reference token is a number
|
// change value to an array for array indices or "-" or to an
|
||||||
const bool nums =
|
// object otherwise; every null value here is a fresh one (see
|
||||||
std::all_of(reference_token.begin(), reference_token.end(),
|
// get_unchecked_rollback), so converting it in place leaves
|
||||||
[](const unsigned char x)
|
// the same value as assigning an array or object to it
|
||||||
|
if (is_array_index_token(reference_token) || is_dash(reference_token))
|
||||||
{
|
{
|
||||||
return std::isdigit(x);
|
ptr->template convert_null_to<typename BasicJsonType::array_t>();
|
||||||
});
|
}
|
||||||
|
else
|
||||||
// change value to an array for numbers or "-" or to object otherwise
|
{
|
||||||
*ptr = (nums || reference_token == "-")
|
ptr->template convert_null_to<typename BasicJsonType::object_t>();
|
||||||
? detail::value_t::array
|
}
|
||||||
: detail::value_t::object;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ptr->is_object())
|
if (ptr->is_object())
|
||||||
{
|
{
|
||||||
// use unchecked object access
|
// the object is new and empty
|
||||||
ptr = &object_member(*ptr, reference_token);
|
ptr = &object_add(*ptr, reference_token);
|
||||||
}
|
}
|
||||||
else if (ptr->is_array())
|
else
|
||||||
{
|
{
|
||||||
if (reference_token == "-")
|
// only the first value is not created here, and it is a null
|
||||||
|
// value or an array (see get_unchecked_create)
|
||||||
|
JSON_ASSERT(ptr->is_array());
|
||||||
|
if (is_dash(reference_token))
|
||||||
{
|
{
|
||||||
// explicitly treat "-" as index beyond the end
|
// explicitly treat "-" as index beyond the end
|
||||||
ptr = &ptr->operator[](ptr->m_data.m_value.array->size());
|
ptr = &array_append(*ptr);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -1016,16 +1463,31 @@ class json_pointer
|
|||||||
ptr = &ptr->operator[](array_index<BasicJsonType>(reference_token));
|
ptr = &ptr->operator[](array_index<BasicJsonType>(reference_token));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
|
||||||
{
|
|
||||||
// primitive value with a reference token left
|
|
||||||
throw_unresolved(reference_token, ptr);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return *ptr;
|
return *ptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// whether @a s is a valid array index, that is a nonempty sequence of
|
||||||
|
/// digits without a leading '0' (cf. RFC 6901, Sect. 4); tokens that could
|
||||||
|
/// never be a valid array index (such as "01" or "") are object keys
|
||||||
|
static bool is_array_index_token(const string_t& s)
|
||||||
|
{
|
||||||
|
const std::size_t size = s.size();
|
||||||
|
if (size == 0 || (size > 1 && s[0] == '0'))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
for (std::size_t i = 0; i < size; ++i)
|
||||||
|
{
|
||||||
|
if (std::isdigit(static_cast<unsigned char>(s[i])) == 0)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
/*!
|
/*!
|
||||||
@throw parse_error.106 if an array index begins with '0'
|
@throw parse_error.106 if an array index begins with '0'
|
||||||
@throw parse_error.109 if an array index was not a number
|
@throw parse_error.109 if an array index was not a number
|
||||||
|
|||||||
@@ -7003,7 +7003,6 @@ public:
|
|||||||
|
|
||||||
switch (parent.m_data.m_type)
|
switch (parent.m_data.m_type)
|
||||||
{
|
{
|
||||||
case value_t::null:
|
|
||||||
case value_t::object:
|
case value_t::object:
|
||||||
{
|
{
|
||||||
// use operator[] to add value
|
// use operator[] to add value
|
||||||
@@ -7035,6 +7034,7 @@ public:
|
|||||||
|
|
||||||
// the parent of an "add" target must be an object or array
|
// the parent of an "add" target must be an object or array
|
||||||
// (see #4292)
|
// (see #4292)
|
||||||
|
case value_t::null:
|
||||||
case value_t::string:
|
case value_t::string:
|
||||||
case value_t::boolean:
|
case value_t::boolean:
|
||||||
case value_t::number_integer:
|
case value_t::number_integer:
|
||||||
|
|||||||
Generated
+490
-27
@@ -23585,7 +23585,7 @@ NLOHMANN_JSON_NAMESPACE_END
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
#include <algorithm> // all_of, copy
|
#include <algorithm> // copy
|
||||||
#include <array> // array
|
#include <array> // array
|
||||||
#include <cctype> // isdigit
|
#include <cctype> // isdigit
|
||||||
#include <cerrno> // errno, ERANGE
|
#include <cerrno> // errno, ERANGE
|
||||||
@@ -23593,9 +23593,11 @@ NLOHMANN_JSON_NAMESPACE_END
|
|||||||
#include <cstdint> // uint64_t
|
#include <cstdint> // uint64_t
|
||||||
#include <cstdlib> // strtoull
|
#include <cstdlib> // strtoull
|
||||||
#include <cstring> // memchr, memcmp, memcpy
|
#include <cstring> // memchr, memcmp, memcpy
|
||||||
|
#include <functional> // less
|
||||||
#ifndef JSON_NO_IO
|
#ifndef JSON_NO_IO
|
||||||
#include <iosfwd> // ostream
|
#include <iosfwd> // ostream
|
||||||
#endif // JSON_NO_IO
|
#endif // JSON_NO_IO
|
||||||
|
#include <iterator> // next
|
||||||
#include <limits> // max
|
#include <limits> // max
|
||||||
#include <string> // string
|
#include <string> // string
|
||||||
#include <utility> // move
|
#include <utility> // move
|
||||||
@@ -23609,6 +23611,8 @@ NLOHMANN_JSON_NAMESPACE_END
|
|||||||
|
|
||||||
// #include <nlohmann/detail/meta/type_traits.hpp>
|
// #include <nlohmann/detail/meta/type_traits.hpp>
|
||||||
|
|
||||||
|
// #include <nlohmann/detail/meta/void_t.hpp>
|
||||||
|
|
||||||
// #include <nlohmann/detail/string_concat.hpp>
|
// #include <nlohmann/detail/string_concat.hpp>
|
||||||
|
|
||||||
// #include <nlohmann/detail/string_escape.hpp>
|
// #include <nlohmann/detail/string_escape.hpp>
|
||||||
@@ -23719,6 +23723,26 @@ JSON_HEDLEY_ALWAYS_INLINE array_index_status parse_array_index(const StringType&
|
|||||||
return array_index_status::ok;
|
return array_index_status::ok;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// whether two keys of type @a StringType are equivalent under @a Compare
|
||||||
|
/// exactly if they are equal: std::less<StringType> and std::less<>
|
||||||
|
template<typename Compare, typename StringType>
|
||||||
|
struct is_less_comparator : std::false_type {};
|
||||||
|
|
||||||
|
template<typename StringType>
|
||||||
|
struct is_less_comparator<std::less<StringType>, StringType> : std::true_type {};
|
||||||
|
|
||||||
|
template<typename StringType>
|
||||||
|
struct is_less_comparator<std::less<void>, StringType> : std::true_type {};
|
||||||
|
|
||||||
|
/// whether the objects of type @a ObjectType find a key of type @a StringType
|
||||||
|
/// exactly if it equals a stored key
|
||||||
|
template<typename ObjectType, typename StringType, typename = void>
|
||||||
|
struct finds_equal_keys : std::false_type {};
|
||||||
|
|
||||||
|
template<typename ObjectType, typename StringType>
|
||||||
|
struct finds_equal_keys<ObjectType, StringType, void_t<typename ObjectType::key_compare, typename ObjectType::key_type>>
|
||||||
|
: conjunction<is_less_comparator<typename ObjectType::key_compare, StringType>, std::is_same<typename ObjectType::key_type, StringType>> {};
|
||||||
|
|
||||||
} // namespace detail
|
} // namespace detail
|
||||||
|
|
||||||
/// @brief JSON Pointer defines a string syntax for identifying a specific value within a JSON document
|
/// @brief JSON Pointer defines a string syntax for identifying a specific value within a JSON document
|
||||||
@@ -24174,6 +24198,228 @@ class json_pointer
|
|||||||
return object_member_impl(object, key, detail::priority_tag<1> {});
|
return object_member_impl(object, key, detail::priority_tag<1> {});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// @brief append a null value to @a array, like operator[] with the size of @a array
|
||||||
|
template<typename BasicJsonType>
|
||||||
|
static BasicJsonType& array_append(BasicJsonType& array)
|
||||||
|
{
|
||||||
|
auto& elements = *array.m_data.m_value.array;
|
||||||
|
const auto old_capacity = array.container_capacity();
|
||||||
|
elements.emplace_back();
|
||||||
|
return array.set_parent_after_insert(elements.back(), old_capacity);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @brief add @a key, which is missing, to @a object
|
||||||
|
template<typename BasicJsonType>
|
||||||
|
static auto object_add_impl(BasicJsonType& object, const string_t& key, detail::priority_tag<1> /*unused*/)
|
||||||
|
-> decltype(object.m_data.m_value.object->emplace(key, nullptr), std::declval<BasicJsonType&>())
|
||||||
|
{
|
||||||
|
const auto old_capacity = object.container_capacity();
|
||||||
|
const auto it = object.m_data.m_value.object->emplace(key, nullptr).first;
|
||||||
|
return object.set_parent_after_insert(it->second, old_capacity);
|
||||||
|
}
|
||||||
|
|
||||||
|
template<typename BasicJsonType>
|
||||||
|
static BasicJsonType& object_add_impl(BasicJsonType& object, const string_t& key, detail::priority_tag<0> /*unused*/)
|
||||||
|
{
|
||||||
|
return object.operator[](key);
|
||||||
|
}
|
||||||
|
|
||||||
|
template<typename BasicJsonType>
|
||||||
|
static BasicJsonType& object_add(BasicJsonType& object, const string_t& key)
|
||||||
|
{
|
||||||
|
return object_add_impl(object, key, detail::priority_tag<1> {});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @brief search @a key in @a members: the result is where the key is or,
|
||||||
|
/// for objects that can insert at a position (std::map), where it belongs
|
||||||
|
template<typename ObjectType>
|
||||||
|
static auto object_search_impl(ObjectType& members, const string_t& key, detail::priority_tag<1> /*unused*/)
|
||||||
|
-> decltype(members.key_comp()(key, members.lower_bound(key)->first), members.lower_bound(key))
|
||||||
|
{
|
||||||
|
return members.lower_bound(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
template<typename ObjectType>
|
||||||
|
static auto object_search_impl(ObjectType& members, const string_t& key, detail::priority_tag<0> /*unused*/)
|
||||||
|
-> decltype(members.find(key))
|
||||||
|
{
|
||||||
|
return members.find(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
template<typename ObjectType>
|
||||||
|
static auto object_search(ObjectType& members, const string_t& key)
|
||||||
|
-> decltype(object_search_impl(members, key, detail::priority_tag<1> {}))
|
||||||
|
{
|
||||||
|
return object_search_impl(members, key, detail::priority_tag<1> {});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @brief whether @a it, the result of @ref object_search for @a key, points to @a key
|
||||||
|
///
|
||||||
|
/// With std::less, the key at the lower bound is equivalent to @a key
|
||||||
|
/// exactly if it is equal to it, and comparing for equality is cheaper.
|
||||||
|
template<typename ObjectType, typename Iterator,
|
||||||
|
detail::enable_if_t<detail::finds_equal_keys<ObjectType, string_t>::value, int> = 0>
|
||||||
|
static auto object_search_found_impl(ObjectType& members, const Iterator& it, const string_t& key, detail::priority_tag<2> /*unused*/)
|
||||||
|
-> decltype(members.lower_bound(key), it->first == key)
|
||||||
|
{
|
||||||
|
return it != members.end() && it->first == key;
|
||||||
|
}
|
||||||
|
|
||||||
|
template<typename ObjectType, typename Iterator>
|
||||||
|
static auto object_search_found_impl(ObjectType& members, const Iterator& it, const string_t& key, detail::priority_tag<1> /*unused*/)
|
||||||
|
-> decltype(members.key_comp()(key, it->first), members.lower_bound(key), bool())
|
||||||
|
{
|
||||||
|
return it != members.end() && !members.key_comp()(key, it->first);
|
||||||
|
}
|
||||||
|
|
||||||
|
template<typename ObjectType, typename Iterator>
|
||||||
|
static bool object_search_found_impl(ObjectType& members, const Iterator& it, const string_t& /*key*/, detail::priority_tag<0> /*unused*/)
|
||||||
|
{
|
||||||
|
return it != members.end();
|
||||||
|
}
|
||||||
|
|
||||||
|
template<typename ObjectType, typename Iterator>
|
||||||
|
static bool object_search_found(ObjectType& members, const Iterator& it, const string_t& key)
|
||||||
|
{
|
||||||
|
return object_search_found_impl(members, it, key, detail::priority_tag<2> {});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @brief insert @a key, which is missing, into @a object at @a it, the
|
||||||
|
/// result of @ref object_search for @a key
|
||||||
|
template<typename BasicJsonType, typename Iterator>
|
||||||
|
static auto object_insert_impl(BasicJsonType& object, const Iterator& it, const string_t& key, detail::priority_tag<1> /*unused*/)
|
||||||
|
-> decltype(object.m_data.m_value.object->emplace_hint(it, key, nullptr), std::declval<BasicJsonType&>())
|
||||||
|
{
|
||||||
|
const auto old_capacity = object.container_capacity();
|
||||||
|
const auto pos = object.m_data.m_value.object->emplace_hint(it, key, nullptr);
|
||||||
|
return object.set_parent_after_insert(pos->second, old_capacity);
|
||||||
|
}
|
||||||
|
|
||||||
|
template<typename BasicJsonType, typename Iterator>
|
||||||
|
static BasicJsonType& object_insert_impl(BasicJsonType& object, const Iterator& /*it*/, const string_t& key, detail::priority_tag<0> /*unused*/)
|
||||||
|
{
|
||||||
|
return object_add(object, key);
|
||||||
|
}
|
||||||
|
|
||||||
|
template<typename BasicJsonType, typename Iterator>
|
||||||
|
static BasicJsonType& object_insert(BasicJsonType& object, const Iterator& it, const string_t& key)
|
||||||
|
{
|
||||||
|
return object_insert_impl(object, it, key, detail::priority_tag<1> {});
|
||||||
|
}
|
||||||
|
|
||||||
|
/*!
|
||||||
|
@brief undoes the change @ref get_unchecked_create made to an existing value
|
||||||
|
|
||||||
|
@ref get_unchecked walks existing values without changing them. At the
|
||||||
|
first reference token that does not select an existing value, @ref
|
||||||
|
get_unchecked_create makes exactly one change to an existing value: it
|
||||||
|
converts a null value, inserts a key into an object, or grows an array.
|
||||||
|
Everything it creates afterwards lies below that change. If a later step
|
||||||
|
throws, the destructor of this guard reverts that one change, which
|
||||||
|
removes everything created after it, so the value is left as it was
|
||||||
|
(strong exception safety). The guard needs no try/catch and therefore also
|
||||||
|
compiles with JSON_NOEXCEPTION.
|
||||||
|
|
||||||
|
Reverting the change must not throw, as it happens in a destructor. It
|
||||||
|
erases the inserted member by its iterator (no key is converted or
|
||||||
|
compared, nothing is allocated), erases the added elements at the end of
|
||||||
|
the array, or swaps the null value back. This holds for std::map,
|
||||||
|
std::vector, ordered_map, and every object_t, array_t, and
|
||||||
|
json_base_class_t whose erase() and move operations do not throw.
|
||||||
|
Between recording and reverting, nothing is inserted into or erased from
|
||||||
|
the object or array, so the iterator stays valid.
|
||||||
|
*/
|
||||||
|
template<typename BasicJsonType>
|
||||||
|
class get_unchecked_rollback
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
using object_iterator = typename BasicJsonType::object_t::iterator;
|
||||||
|
|
||||||
|
/// record that @a member was inserted into the object @a value
|
||||||
|
get_unchecked_rollback(BasicJsonType* value, const object_iterator& member)
|
||||||
|
: m_value(value)
|
||||||
|
, m_member(member)
|
||||||
|
, m_change(change::key_inserted)
|
||||||
|
{}
|
||||||
|
|
||||||
|
/// record the change that the next reference token makes to the null value or array @a value
|
||||||
|
explicit get_unchecked_rollback(BasicJsonType* value)
|
||||||
|
: m_value(value)
|
||||||
|
{
|
||||||
|
if (value->is_null())
|
||||||
|
{
|
||||||
|
// keep the null value (with its base class and positions) and
|
||||||
|
// continue with a fresh null value in its place, which is what
|
||||||
|
// assigning an array or object to it would have left there
|
||||||
|
m_saved_null.swap(*value);
|
||||||
|
m_change = change::null_converted;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
JSON_ASSERT(value->is_array());
|
||||||
|
m_old_size = value->m_data.m_value.array->size();
|
||||||
|
m_change = change::array_grown;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get_unchecked_rollback(const get_unchecked_rollback&) = delete;
|
||||||
|
get_unchecked_rollback(get_unchecked_rollback&&) = delete;
|
||||||
|
get_unchecked_rollback& operator=(const get_unchecked_rollback&) = delete;
|
||||||
|
get_unchecked_rollback& operator=(get_unchecked_rollback&&) = delete;
|
||||||
|
|
||||||
|
~get_unchecked_rollback()
|
||||||
|
{
|
||||||
|
if (JSON_HEDLEY_UNLIKELY(m_change != change::none))
|
||||||
|
{
|
||||||
|
undo();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// keep the change
|
||||||
|
void commit() noexcept
|
||||||
|
{
|
||||||
|
m_change = change::none;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
JSON_HEDLEY_NEVER_INLINE void undo() noexcept
|
||||||
|
{
|
||||||
|
switch (m_change)
|
||||||
|
{
|
||||||
|
case change::null_converted:
|
||||||
|
// the values created since go away with m_saved_null
|
||||||
|
m_value->swap(m_saved_null);
|
||||||
|
break;
|
||||||
|
case change::key_inserted:
|
||||||
|
m_value->m_data.m_value.object->erase(m_member);
|
||||||
|
break;
|
||||||
|
case change::array_grown:
|
||||||
|
{
|
||||||
|
auto& elements = *m_value->m_data.m_value.array;
|
||||||
|
elements.erase(std::next(elements.begin(), static_cast<typename BasicJsonType::difference_type>(m_old_size)), elements.end());
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case change::none:
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class change
|
||||||
|
{
|
||||||
|
none,
|
||||||
|
null_converted,
|
||||||
|
key_inserted,
|
||||||
|
array_grown
|
||||||
|
};
|
||||||
|
|
||||||
|
BasicJsonType* m_value;
|
||||||
|
object_iterator m_member {};
|
||||||
|
typename BasicJsonType::size_type m_old_size = 0;
|
||||||
|
change m_change = change::none;
|
||||||
|
BasicJsonType m_saved_null {};
|
||||||
|
};
|
||||||
|
|
||||||
JSON_PRIVATE_UNLESS_TESTED:
|
JSON_PRIVATE_UNLESS_TESTED:
|
||||||
json_pointer top() const
|
json_pointer top() const
|
||||||
{
|
{
|
||||||
@@ -24557,43 +24803,245 @@ class json_pointer
|
|||||||
|
|
||||||
Complexity: Linear in the length of the JSON pointer.
|
Complexity: Linear in the length of the JSON pointer.
|
||||||
|
|
||||||
@throw parse_error.106 if an array index begins with '0'
|
Exception safety: Strong guarantee. This function walks existing values
|
||||||
@throw parse_error.109 if an array index was not a number
|
until the first reference token that does not select one. If that is the
|
||||||
@throw out_of_range.404 if the JSON pointer can not be resolved
|
last reference token, inserting the key into the object or growing the
|
||||||
|
array is strongly exception safe by itself; any other change is made by
|
||||||
|
@ref get_unchecked_create, which undoes it if a later step throws.
|
||||||
|
|
||||||
|
@throw parse_error.106 if an array index of an existing array begins with '0'
|
||||||
|
@throw parse_error.109 if an array index of an existing array was not a number
|
||||||
|
@throw out_of_range.404 if the JSON pointer can not be resolved, or if an
|
||||||
|
array index is too large for strtoull
|
||||||
|
@throw out_of_range.410 if an array index exceeds size_type
|
||||||
|
@throw std::length_error if padding an array exceeds its maximal size
|
||||||
|
@throw std::bad_alloc if an allocation fails
|
||||||
*/
|
*/
|
||||||
template<typename BasicJsonType>
|
template<typename BasicJsonType>
|
||||||
BasicJsonType& get_unchecked(BasicJsonType* ptr) const
|
BasicJsonType& get_unchecked(BasicJsonType* ptr) const
|
||||||
{
|
{
|
||||||
for (const auto& reference_token : reference_tokens)
|
const auto* token = reference_tokens.data();
|
||||||
|
const auto* const end = token + reference_tokens.size();
|
||||||
|
if (token == end)
|
||||||
{
|
{
|
||||||
|
return *ptr;
|
||||||
|
}
|
||||||
|
if (ptr->is_null())
|
||||||
|
{
|
||||||
|
// the null value is converted to an array or an object
|
||||||
|
return get_unchecked_create(ptr, token, end);
|
||||||
|
}
|
||||||
|
|
||||||
|
// walk existing values; the walk itself inserts nothing, which keeps
|
||||||
|
// the loop fast (insertions are done below or in get_unchecked_create)
|
||||||
|
typename BasicJsonType::object_t::iterator it;
|
||||||
|
do
|
||||||
|
{
|
||||||
|
if (ptr->is_object())
|
||||||
|
{
|
||||||
|
auto& members = *ptr->m_data.m_value.object;
|
||||||
|
it = object_search(members, *token);
|
||||||
|
if (!object_search_found(members, it, *token))
|
||||||
|
{
|
||||||
|
// the key is inserted below or in get_unchecked_create
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
ptr = &it->second;
|
||||||
|
}
|
||||||
|
else if (ptr->is_array())
|
||||||
|
{
|
||||||
|
if (is_dash(*token))
|
||||||
|
{
|
||||||
|
if (token + 1 == end)
|
||||||
|
{
|
||||||
|
// explicitly treat "-" as index beyond the end
|
||||||
|
return array_append(*ptr);
|
||||||
|
}
|
||||||
|
// an element is appended in get_unchecked_create
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (token + 1 == end)
|
||||||
|
{
|
||||||
|
// convert array index to number; unchecked access
|
||||||
|
return ptr->operator[](array_index<BasicJsonType>(*token));
|
||||||
|
}
|
||||||
|
const auto idx = array_index<BasicJsonType>(*token);
|
||||||
|
if (idx >= ptr->m_data.m_value.array->size())
|
||||||
|
{
|
||||||
|
// the array is padded in get_unchecked_create
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
ptr = &(*ptr->m_data.m_value.array)[idx];
|
||||||
|
}
|
||||||
|
else if (ptr->is_null())
|
||||||
|
{
|
||||||
|
// the null value is converted in get_unchecked_create
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// primitive value with a reference token left
|
||||||
|
throw_unresolved(*token, ptr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
while (++token != end);
|
||||||
|
|
||||||
|
if (token == end)
|
||||||
|
{
|
||||||
|
return *ptr;
|
||||||
|
}
|
||||||
|
if (token + 1 == end && ptr->is_object())
|
||||||
|
{
|
||||||
|
// insert the missing key where the walk found it belongs
|
||||||
|
return object_insert(*ptr, it, *token);
|
||||||
|
}
|
||||||
|
return get_unchecked_create(ptr, token, end);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*!
|
||||||
|
@brief create the values for the reference tokens from @a first on
|
||||||
|
|
||||||
|
Continues @ref get_unchecked at the first reference token that does not
|
||||||
|
select an existing value. That token changes @a ptr, an existing value (see
|
||||||
|
@ref get_unchecked_rollback); all later tokens only change values created
|
||||||
|
here.
|
||||||
|
|
||||||
|
If the emplace() of the object type finds a member that the lookup in @ref
|
||||||
|
get_unchecked did not find (the two disagree, e.g. for fifo_map), nothing
|
||||||
|
is inserted, and the walk continues with that member as in @ref
|
||||||
|
get_unchecked, which is what basic_json::operator[] does.
|
||||||
|
|
||||||
|
@param[in] ptr the value the reference token @a first is applied to
|
||||||
|
@param[in] first the first reference token to resolve (an element of
|
||||||
|
@ref reference_tokens)
|
||||||
|
@param[in] end the end of @ref reference_tokens
|
||||||
|
|
||||||
|
@throw parse_error.106 if an array index of an existing array begins with
|
||||||
|
'0' (only after emplace() found a member, see above)
|
||||||
|
@throw parse_error.109 if an array index of an existing array was not a
|
||||||
|
number (only after emplace() found a member)
|
||||||
|
@throw out_of_range.404 if an array index is too large for strtoull, or (only
|
||||||
|
after emplace() found a member) if the JSON pointer
|
||||||
|
can not be resolved
|
||||||
|
@throw out_of_range.410 if an array index exceeds size_type
|
||||||
|
@throw std::length_error if padding an array exceeds its maximal size
|
||||||
|
@throw std::bad_alloc if an allocation fails
|
||||||
|
*/
|
||||||
|
template<typename BasicJsonType>
|
||||||
|
JSON_HEDLEY_NEVER_INLINE static BasicJsonType& get_unchecked_create(BasicJsonType* ptr, const string_t* first, const string_t* end)
|
||||||
|
{
|
||||||
|
const auto* token = first;
|
||||||
|
while (ptr->is_object())
|
||||||
|
{
|
||||||
|
const auto old_capacity = ptr->container_capacity();
|
||||||
|
const auto inserted = ptr->m_data.m_value.object->emplace(*token, nullptr);
|
||||||
|
if (JSON_HEDLEY_LIKELY(inserted.second))
|
||||||
|
{
|
||||||
|
get_unchecked_rollback<BasicJsonType> rollback(ptr, inserted.first);
|
||||||
|
BasicJsonType& result = create_values(&ptr->set_parent_after_insert(inserted.first->second, old_capacity), token + 1, end);
|
||||||
|
rollback.commit();
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// nothing was inserted: walk existing values from the member
|
||||||
|
// emplace() found on, like get_unchecked
|
||||||
|
ptr = &inserted.first->second;
|
||||||
|
while (++token != end)
|
||||||
|
{
|
||||||
|
if (ptr->is_object())
|
||||||
|
{
|
||||||
|
auto& members = *ptr->m_data.m_value.object;
|
||||||
|
const auto it = object_search(members, *token);
|
||||||
|
if (!object_search_found(members, it, *token))
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
ptr = &it->second;
|
||||||
|
}
|
||||||
|
else if (ptr->is_array())
|
||||||
|
{
|
||||||
|
if (is_dash(*token))
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
const auto idx = array_index<BasicJsonType>(*token);
|
||||||
|
if (idx >= ptr->m_data.m_value.array->size())
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
ptr = &(*ptr->m_data.m_value.array)[idx];
|
||||||
|
}
|
||||||
|
else if (ptr->is_null())
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// primitive value with a reference token left
|
||||||
|
throw_unresolved(*token, ptr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (token == end)
|
||||||
|
{
|
||||||
|
return *ptr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get_unchecked_rollback<BasicJsonType> rollback(ptr);
|
||||||
|
BasicJsonType& result = create_values(ptr, token, end);
|
||||||
|
rollback.commit();
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*!
|
||||||
|
@brief the loop of @ref get_unchecked_create
|
||||||
|
|
||||||
|
@a ptr is a null value or an array. Every value created here is a fresh
|
||||||
|
one, so every object a key is added to is empty.
|
||||||
|
|
||||||
|
@throw out_of_range.404 if an array index is too large for strtoull
|
||||||
|
@throw out_of_range.410 if an array index exceeds size_type
|
||||||
|
@throw std::length_error if padding an array exceeds its maximal size
|
||||||
|
@throw std::bad_alloc if an allocation fails
|
||||||
|
*/
|
||||||
|
template<typename BasicJsonType>
|
||||||
|
static BasicJsonType& create_values(BasicJsonType* ptr, const string_t* first, const string_t* end)
|
||||||
|
{
|
||||||
|
for (const auto* token = first; token != end; ++token)
|
||||||
|
{
|
||||||
|
const auto& reference_token = *token;
|
||||||
|
|
||||||
// convert null values to arrays or objects before continuing
|
// convert null values to arrays or objects before continuing
|
||||||
if (ptr->is_null())
|
if (ptr->is_null())
|
||||||
{
|
{
|
||||||
// check if the reference token is a number
|
// change value to an array for array indices or "-" or to an
|
||||||
const bool nums =
|
// object otherwise; every null value here is a fresh one (see
|
||||||
std::all_of(reference_token.begin(), reference_token.end(),
|
// get_unchecked_rollback), so converting it in place leaves
|
||||||
[](const unsigned char x)
|
// the same value as assigning an array or object to it
|
||||||
|
if (is_array_index_token(reference_token) || is_dash(reference_token))
|
||||||
{
|
{
|
||||||
return std::isdigit(x);
|
ptr->template convert_null_to<typename BasicJsonType::array_t>();
|
||||||
});
|
}
|
||||||
|
else
|
||||||
// change value to an array for numbers or "-" or to object otherwise
|
{
|
||||||
*ptr = (nums || reference_token == "-")
|
ptr->template convert_null_to<typename BasicJsonType::object_t>();
|
||||||
? detail::value_t::array
|
}
|
||||||
: detail::value_t::object;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ptr->is_object())
|
if (ptr->is_object())
|
||||||
{
|
{
|
||||||
// use unchecked object access
|
// the object is new and empty
|
||||||
ptr = &object_member(*ptr, reference_token);
|
ptr = &object_add(*ptr, reference_token);
|
||||||
}
|
}
|
||||||
else if (ptr->is_array())
|
else
|
||||||
{
|
{
|
||||||
if (reference_token == "-")
|
// only the first value is not created here, and it is a null
|
||||||
|
// value or an array (see get_unchecked_create)
|
||||||
|
JSON_ASSERT(ptr->is_array());
|
||||||
|
if (is_dash(reference_token))
|
||||||
{
|
{
|
||||||
// explicitly treat "-" as index beyond the end
|
// explicitly treat "-" as index beyond the end
|
||||||
ptr = &ptr->operator[](ptr->m_data.m_value.array->size());
|
ptr = &array_append(*ptr);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -24601,16 +25049,31 @@ class json_pointer
|
|||||||
ptr = &ptr->operator[](array_index<BasicJsonType>(reference_token));
|
ptr = &ptr->operator[](array_index<BasicJsonType>(reference_token));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
|
||||||
{
|
|
||||||
// primitive value with a reference token left
|
|
||||||
throw_unresolved(reference_token, ptr);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return *ptr;
|
return *ptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// whether @a s is a valid array index, that is a nonempty sequence of
|
||||||
|
/// digits without a leading '0' (cf. RFC 6901, Sect. 4); tokens that could
|
||||||
|
/// never be a valid array index (such as "01" or "") are object keys
|
||||||
|
static bool is_array_index_token(const string_t& s)
|
||||||
|
{
|
||||||
|
const std::size_t size = s.size();
|
||||||
|
if (size == 0 || (size > 1 && s[0] == '0'))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
for (std::size_t i = 0; i < size; ++i)
|
||||||
|
{
|
||||||
|
if (std::isdigit(static_cast<unsigned char>(s[i])) == 0)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
/*!
|
/*!
|
||||||
@throw parse_error.106 if an array index begins with '0'
|
@throw parse_error.106 if an array index begins with '0'
|
||||||
@throw parse_error.109 if an array index was not a number
|
@throw parse_error.109 if an array index was not a number
|
||||||
@@ -42793,7 +43256,6 @@ public:
|
|||||||
|
|
||||||
switch (parent.m_data.m_type)
|
switch (parent.m_data.m_type)
|
||||||
{
|
{
|
||||||
case value_t::null:
|
|
||||||
case value_t::object:
|
case value_t::object:
|
||||||
{
|
{
|
||||||
// use operator[] to add value
|
// use operator[] to add value
|
||||||
@@ -42825,6 +43287,7 @@ public:
|
|||||||
|
|
||||||
// the parent of an "add" target must be an object or array
|
// the parent of an "add" target must be an object or array
|
||||||
// (see #4292)
|
// (see #4292)
|
||||||
|
case value_t::null:
|
||||||
case value_t::string:
|
case value_t::string:
|
||||||
case value_t::boolean:
|
case value_t::boolean:
|
||||||
case value_t::number_integer:
|
case value_t::number_integer:
|
||||||
|
|||||||
@@ -373,6 +373,23 @@ TEST_CASE("alternative string type")
|
|||||||
CHECK_FALSE(j.contains(alt_json::json_pointer("/foo/bar")));
|
CHECK_FALSE(j.contains(alt_json::json_pointer("/foo/bar")));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SECTION("non-const operator[](json_pointer)")
|
||||||
|
{
|
||||||
|
// creating values must compile without string_t::begin()
|
||||||
|
alt_json j = alt_json::parse(R"({"a":{"b":[1]},"c":null})");
|
||||||
|
const alt_json before = j;
|
||||||
|
j[alt_json::json_pointer("/a/b/3/x/-/y")] = 1; // pad, append, create
|
||||||
|
j[alt_json::json_pointer("/a/z")] = 2; // insert a key
|
||||||
|
j[alt_json::json_pointer("/c/0/q")] = 3; // convert null
|
||||||
|
j[alt_json::json_pointer("/a/b/-")] = 4; // append
|
||||||
|
CHECK(j.dump() == R"({"a":{"b":[1,null,null,{"x":[{"y":1}]},4],"z":2},"c":[{"q":3}]})");
|
||||||
|
|
||||||
|
// a failing access leaves the value unchanged
|
||||||
|
alt_json k = before;
|
||||||
|
CHECK_THROWS_AS(k[alt_json::json_pointer("/a/n/99999999999999999999999")], alt_json::out_of_range&);
|
||||||
|
CHECK(k == before);
|
||||||
|
}
|
||||||
|
|
||||||
SECTION("operator/(std::size_t)")
|
SECTION("operator/(std::size_t)")
|
||||||
{
|
{
|
||||||
// json_pointer::operator/=(std::size_t) must compile without string_t
|
// json_pointer::operator/=(std::size_t) must compile without string_t
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ using nlohmann::json;
|
|||||||
#endif
|
#endif
|
||||||
|
|
||||||
#include <fstream>
|
#include <fstream>
|
||||||
|
#include <limits>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
#include "make_test_data_available.hpp"
|
#include "make_test_data_available.hpp"
|
||||||
@@ -490,7 +491,9 @@ TEST_CASE("JSON patch")
|
|||||||
}
|
}
|
||||||
|
|
||||||
// A.13. Invalid JSON Patch Document
|
// A.13. Invalid JSON Patch Document
|
||||||
// not applicable
|
// not applicable: the library cannot tell the duplicate "op" members
|
||||||
|
// apart, because the parser resolves them (the last one is kept); see
|
||||||
|
// "duplicate member names in an operation" below
|
||||||
|
|
||||||
SECTION("A.14. Escape Ordering")
|
SECTION("A.14. Escape Ordering")
|
||||||
{
|
{
|
||||||
@@ -1515,6 +1518,24 @@ TEST_CASE("JSON patch - add to a primitive parent (regression #4292)")
|
|||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SECTION("null parent")
|
||||||
|
{
|
||||||
|
json const doc = {{"foo", nullptr}};
|
||||||
|
json const patch = {{{"op", "add"}, {"path", "/foo/bar"}, {"value", 1}}};
|
||||||
|
#if JSON_DIAGNOSTICS
|
||||||
|
CHECK_THROWS_WITH_AS(doc.patch(patch), "[json.exception.out_of_range.411] (/foo) cannot add value: the JSON Patch 'add' target's parent is of type null, but must be an object or array", json::out_of_range&);
|
||||||
|
#else
|
||||||
|
CHECK_THROWS_WITH_AS(doc.patch(patch), "[json.exception.out_of_range.411] cannot add value: the JSON Patch 'add' target's parent is of type null, but must be an object or array", json::out_of_range&);
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
SECTION("null document")
|
||||||
|
{
|
||||||
|
json const doc = nullptr;
|
||||||
|
json const patch = {{{"op", "add"}, {"path", "/-"}, {"value", 1}}};
|
||||||
|
CHECK_THROWS_WITH_AS(doc.patch(patch), "[json.exception.out_of_range.411] cannot add value: the JSON Patch 'add' target's parent is of type null, but must be an object or array", json::out_of_range&);
|
||||||
|
}
|
||||||
|
|
||||||
SECTION("number parent")
|
SECTION("number parent")
|
||||||
{
|
{
|
||||||
json const doc = {{"foo", 1}};
|
json const doc = {{"foo", 1}};
|
||||||
@@ -2103,3 +2124,204 @@ TEST_CASE_TEMPLATE("JSON patch - diff round trip on hand-made documents", Json,
|
|||||||
roundtrip(Json(1), Json::parse("[1]"));
|
roundtrip(Json(1), Json::parse("[1]"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TEST_CASE("JSON patch - behavior documented in the API reference")
|
||||||
|
{
|
||||||
|
SECTION("duplicate member names in an operation")
|
||||||
|
{
|
||||||
|
// RFC 6902, A.13: the parser resolves the duplicates (the last one
|
||||||
|
// is kept), so this is a "remove" and not an "add"
|
||||||
|
const json patch = json::parse(R"([{"op": "add", "op": "remove", "path": "/a"}])");
|
||||||
|
CHECK(patch == json::parse(R"([{"op": "remove", "path": "/a"}])"));
|
||||||
|
CHECK(json({{"a", 1}, {"b", 2}}).patch(patch) == json({{"b", 2}}));
|
||||||
|
}
|
||||||
|
|
||||||
|
SECTION("add with a leading zero in an array index")
|
||||||
|
{
|
||||||
|
const json patch = R"([{"op": "add", "path": "/01", "value": 1}])"_json;
|
||||||
|
json doc = json::array();
|
||||||
|
#if !defined(JSON_NOEXCEPTION)
|
||||||
|
CHECK_THROWS_AS(doc.patch(patch), json::parse_error&);
|
||||||
|
CHECK_THROWS_AS(doc.patch_inplace(patch), json::parse_error&);
|
||||||
|
#endif
|
||||||
|
CHECK(doc == json::array());
|
||||||
|
}
|
||||||
|
|
||||||
|
SECTION("test compares numbers by value")
|
||||||
|
{
|
||||||
|
const json doc = R"({"a": 1, "b": 0, "c": 1.5})"_json;
|
||||||
|
CHECK(doc.patch(R"([{"op": "test", "path": "/a", "value": 1.0}])"_json) == doc);
|
||||||
|
CHECK(doc.patch(R"([{"op": "test", "path": "/b", "value": -0.0}])"_json) == doc);
|
||||||
|
CHECK(doc.patch(R"([{"op": "test", "path": "/c", "value": 1.5}])"_json) == doc);
|
||||||
|
CHECK_THROWS_AS(doc.patch(R"([{"op": "test", "path": "/a", "value": 2}])"_json), json::other_error&);
|
||||||
|
CHECK_THROWS_AS(doc.patch(R"([{"op": "test", "path": "/a", "value": "1"}])"_json), json::other_error&);
|
||||||
|
}
|
||||||
|
|
||||||
|
SECTION("test ignores the order of object members, but not the order of array elements")
|
||||||
|
{
|
||||||
|
const json doc = R"({"o": {"x": 1, "y": 2}, "a": [1, 2]})"_json;
|
||||||
|
CHECK(doc.patch(R"([{"op": "test", "path": "/o", "value": {"y": 2, "x": 1}}])"_json) == doc);
|
||||||
|
CHECK(doc.patch(R"([{"op": "test", "path": "/a", "value": [1, 2]}])"_json) == doc);
|
||||||
|
CHECK_THROWS_AS(doc.patch(R"([{"op": "test", "path": "/a", "value": [2, 1]}])"_json), json::other_error&);
|
||||||
|
}
|
||||||
|
|
||||||
|
SECTION("test on a path that does not exist")
|
||||||
|
{
|
||||||
|
const json doc = R"({"a": 1})"_json;
|
||||||
|
// with a "value" member, the failed lookup is a failed test
|
||||||
|
CHECK_THROWS_AS(doc.patch(R"([{"op": "test", "path": "/b", "value": 1}])"_json), json::other_error&);
|
||||||
|
CHECK_THROWS_AS(doc.patch(R"([{"op": "test", "path": "/b", "value": null}])"_json), json::other_error&);
|
||||||
|
// without it, an exception is thrown, too; which one depends on the
|
||||||
|
// order of evaluation of the operands of ==, so it is not checked
|
||||||
|
CHECK_THROWS(doc.patch(R"([{"op": "test", "path": "/b"}])"_json));
|
||||||
|
}
|
||||||
|
|
||||||
|
SECTION("patch() has the strong guarantee")
|
||||||
|
{
|
||||||
|
const json doc = R"({"a": [1, 2], "o": {"k": "v"}})"_json;
|
||||||
|
const json before = doc;
|
||||||
|
|
||||||
|
const std::vector<json> failing =
|
||||||
|
{
|
||||||
|
// remove of a member that does not exist
|
||||||
|
R"([{"op": "add", "path": "/n", "value": 1}, {"op": "remove", "path": "/missing"}])"_json,
|
||||||
|
// replace of a member that does not exist
|
||||||
|
R"([{"op": "remove", "path": "/a/0"}, {"op": "replace", "path": "/missing", "value": 1}])"_json,
|
||||||
|
// move from a member that does not exist
|
||||||
|
R"([{"op": "add", "path": "/n", "value": 1}, {"op": "move", "from": "/missing", "path": "/m"}])"_json,
|
||||||
|
// copy from a member that does not exist
|
||||||
|
R"([{"op": "add", "path": "/n", "value": 1}, {"op": "copy", "from": "/missing", "path": "/m"}])"_json,
|
||||||
|
// out-of-range array index
|
||||||
|
R"([{"op": "add", "path": "/n", "value": 1}, {"op": "add", "path": "/a/5", "value": 1}])"_json,
|
||||||
|
// failing test after a successful operation
|
||||||
|
R"([{"op": "remove", "path": "/o"}, {"op": "test", "path": "/a", "value": []}])"_json,
|
||||||
|
};
|
||||||
|
for (const auto& patch : failing)
|
||||||
|
{
|
||||||
|
CAPTURE(patch.dump())
|
||||||
|
CHECK_THROWS_AS(doc.patch(patch), json::exception&);
|
||||||
|
CHECK(doc == before);
|
||||||
|
|
||||||
|
json copy = doc;
|
||||||
|
CHECK_THROWS_AS(copy.patch(patch), json::exception&);
|
||||||
|
CHECK(copy == before);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("JSON patch - diff behavior documented in the API reference")
|
||||||
|
{
|
||||||
|
SECTION("path is a verbatim prefix")
|
||||||
|
{
|
||||||
|
// not escaped and not validated
|
||||||
|
CHECK(json::diff(1, 2) == R"([{"op": "replace", "path": "", "value": 2}])"_json);
|
||||||
|
CHECK(json::diff(1, 2, "abc") == R"([{"op": "replace", "path": "abc", "value": 2}])"_json);
|
||||||
|
CHECK(json::diff(1, 2, "/root") == R"([{"op": "replace", "path": "/root", "value": 2}])"_json);
|
||||||
|
// an escaped token in a non-root prefix is kept as given
|
||||||
|
CHECK(json::diff(R"({"a": 1})"_json, R"({"a": 2})"_json, "/x~1y/~0")
|
||||||
|
== R"([{"op": "replace", "path": "/x~1y/~0/a", "value": 2}])"_json);
|
||||||
|
CHECK(json::diff(R"({"a": 1})"_json, R"({"a": 2})"_json, "/p")
|
||||||
|
== R"([{"op": "replace", "path": "/p/a", "value": 2}])"_json);
|
||||||
|
}
|
||||||
|
|
||||||
|
SECTION("keys are escaped in the paths")
|
||||||
|
{
|
||||||
|
const json source = json::object();
|
||||||
|
const json target =
|
||||||
|
{
|
||||||
|
{"", 0}, {"/", 1}, {"~", 2}, {"~1", 3}, {"~01", 4}
|
||||||
|
};
|
||||||
|
const json patch = json::diff(source, target);
|
||||||
|
REQUIRE(patch.size() == 5);
|
||||||
|
|
||||||
|
// the order of the operations is not checked
|
||||||
|
json paths = json::object();
|
||||||
|
for (const auto& op : patch)
|
||||||
|
{
|
||||||
|
CHECK(op["op"] == "add");
|
||||||
|
paths[op["path"].get<std::string>()] = op["value"];
|
||||||
|
}
|
||||||
|
CHECK(paths == json({{"/", 0}, {"/~1", 1}, {"/~0", 2}, {"/~01", 3}, {"/~001", 4}}));
|
||||||
|
CHECK(source.patch(patch) == target);
|
||||||
|
}
|
||||||
|
|
||||||
|
SECTION("NaN is not equal to itself")
|
||||||
|
{
|
||||||
|
const json nan = std::numeric_limits<json::number_float_t>::quiet_NaN();
|
||||||
|
const json patch = json::diff(nan, nan);
|
||||||
|
CHECK(patch.size() == 1);
|
||||||
|
CHECK(patch[0]["op"] == "replace");
|
||||||
|
CHECK(patch[0]["path"] == "");
|
||||||
|
CHECK(patch[0]["value"].is_number_float());
|
||||||
|
CHECK(nan.patch(patch) != nan);
|
||||||
|
|
||||||
|
// a NaN in a patch is serialized as null
|
||||||
|
CHECK(json::parse(patch.dump())[0]["value"].is_null());
|
||||||
|
|
||||||
|
const json nested = {{"a", {1, nan}}};
|
||||||
|
const json nested_patch = json::diff(nested, nested);
|
||||||
|
CHECK(nested_patch.size() == 1);
|
||||||
|
CHECK(nested_patch[0]["path"] == "/a/1");
|
||||||
|
|
||||||
|
// infinity is equal to itself
|
||||||
|
const json inf = std::numeric_limits<json::number_float_t>::infinity();
|
||||||
|
CHECK(json::diff(inf, inf).empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
SECTION("discarded values are not equal to themselves")
|
||||||
|
{
|
||||||
|
const json discarded(json::value_t::discarded);
|
||||||
|
const json patch = json::diff(discarded, discarded);
|
||||||
|
CHECK(patch.size() == 1);
|
||||||
|
CHECK(patch[0]["op"] == "replace");
|
||||||
|
CHECK(patch[0]["path"] == "");
|
||||||
|
CHECK(patch[0]["value"].is_discarded());
|
||||||
|
CHECK(discarded.patch(patch) != discarded);
|
||||||
|
}
|
||||||
|
|
||||||
|
SECTION("numbers that compare equal produce no operation")
|
||||||
|
{
|
||||||
|
CHECK(json::diff(json(1), json(1.0)).empty());
|
||||||
|
CHECK(json::diff(json(1.0), json(1)).empty());
|
||||||
|
CHECK(json::diff(json(1U), json(1)).empty());
|
||||||
|
CHECK(json::diff(json(1), json(1U)).empty());
|
||||||
|
CHECK(json::diff(json(0.0), json(-0.0)).empty());
|
||||||
|
|
||||||
|
// the patched value keeps the number type of the source
|
||||||
|
const json source = 1;
|
||||||
|
const json patched = source.patch(json::diff(source, json(1.0)));
|
||||||
|
CHECK(patched.is_number_integer());
|
||||||
|
CHECK(patched == json(1.0));
|
||||||
|
|
||||||
|
// different values are replaced
|
||||||
|
CHECK(json::diff(json(1), json(2.0)) == R"([{"op": "replace", "path": "", "value": 2.0}])"_json);
|
||||||
|
CHECK(json::diff(json(1), json(-1)) == R"([{"op": "replace", "path": "", "value": -1}])"_json);
|
||||||
|
CHECK(json::diff(json(1.0), json(1.5)) == R"([{"op": "replace", "path": "", "value": 1.5}])"_json);
|
||||||
|
CHECK(json::diff(json(1), json("1")) == R"([{"op": "replace", "path": "", "value": "1"}])"_json);
|
||||||
|
}
|
||||||
|
|
||||||
|
SECTION("binary values")
|
||||||
|
{
|
||||||
|
const json a = json::binary({1, 2});
|
||||||
|
const json b = json::binary({1, 3});
|
||||||
|
CHECK(json::diff(a, a).empty());
|
||||||
|
|
||||||
|
const json patch = json::diff(a, b);
|
||||||
|
CHECK(patch.size() == 1);
|
||||||
|
CHECK(patch[0]["op"] == "replace");
|
||||||
|
CHECK(patch[0]["path"] == "");
|
||||||
|
CHECK(patch[0]["value"].is_binary());
|
||||||
|
CHECK(a.patch(patch) == b);
|
||||||
|
|
||||||
|
// serialized, the binary value is an object
|
||||||
|
const json parsed = json::parse(patch.dump());
|
||||||
|
CHECK(parsed[0]["value"].is_object());
|
||||||
|
CHECK(parsed[0]["value"] == R"({"bytes": [1, 3], "subtype": null})"_json);
|
||||||
|
}
|
||||||
|
|
||||||
|
SECTION("a difference as a whole is a replace of the root")
|
||||||
|
{
|
||||||
|
CHECK(json::diff(json::array({1}), json::object()) == R"([{"op": "replace", "path": "", "value": {}}])"_json);
|
||||||
|
CHECK(json::diff(json(nullptr), json::array()) == R"([{"op": "replace", "path": "", "value": []}])"_json);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -29,9 +29,64 @@ using nlohmann::json;
|
|||||||
#include <map>
|
#include <map>
|
||||||
#include <random>
|
#include <random>
|
||||||
#include <sstream>
|
#include <sstream>
|
||||||
|
#include <stdexcept>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
// the non-const operator[] must throw for pointer and leave the value parsed
|
||||||
|
// from init unchanged (strong exception safety)
|
||||||
|
template<typename Json>
|
||||||
|
void check_strong_guarantee(const char* init, const std::string& pointer, const char* message)
|
||||||
|
{
|
||||||
|
CAPTURE(init)
|
||||||
|
CAPTURE(pointer)
|
||||||
|
Json j = Json::parse(init);
|
||||||
|
const Json before = Json::parse(init);
|
||||||
|
const typename Json::json_pointer ptr(pointer);
|
||||||
|
CHECK_THROWS_WITH_AS(j[ptr], message, typename Json::exception&);
|
||||||
|
CHECK(j == before);
|
||||||
|
}
|
||||||
|
|
||||||
|
// as above, for an index that fits into size_type, but not into an array
|
||||||
|
template<typename Json>
|
||||||
|
void check_strong_guarantee_length_error(const char* init, const std::string& pointer)
|
||||||
|
{
|
||||||
|
CAPTURE(init)
|
||||||
|
CAPTURE(pointer)
|
||||||
|
Json j = Json::parse(init);
|
||||||
|
const Json before = Json::parse(init);
|
||||||
|
const typename Json::json_pointer ptr(pointer);
|
||||||
|
CHECK_THROWS_AS(j[ptr], std::length_error&);
|
||||||
|
CHECK(j == before);
|
||||||
|
}
|
||||||
|
|
||||||
|
template<typename Json>
|
||||||
|
void check_strong_guarantee_cases()
|
||||||
|
{
|
||||||
|
const char* const unresolved = "[json.exception.out_of_range.404] unresolved reference token '99999999999999999999999'";
|
||||||
|
|
||||||
|
// the first change converts a null value
|
||||||
|
check_strong_guarantee<Json>("null", "/99999999999999999999999", unresolved);
|
||||||
|
check_strong_guarantee<Json>("null", "/a/99999999999999999999999", unresolved);
|
||||||
|
check_strong_guarantee<Json>(R"({"a":null})", "/a/b/-/99999999999999999999999", unresolved);
|
||||||
|
// the first change inserts a key into an object
|
||||||
|
check_strong_guarantee<Json>("{}", "/a/99999999999999999999999", unresolved);
|
||||||
|
check_strong_guarantee<Json>(R"({"a":{"b":1}})", "/a/c/d/99999999999999999999999", unresolved);
|
||||||
|
// the first change pads or appends to an array
|
||||||
|
check_strong_guarantee<Json>(R"({"a":[1]})", "/a/5/99999999999999999999999", unresolved);
|
||||||
|
check_strong_guarantee<Json>(R"({"a":[1]})", "/a/-/x/1/99999999999999999999999", unresolved);
|
||||||
|
// an index that does not fit into size_type
|
||||||
|
check_strong_guarantee<Json>("null", "/x/18446744073709551615",
|
||||||
|
"[json.exception.out_of_range.410] array index 18446744073709551615 exceeds size_type");
|
||||||
|
// an index that fits into size_type, but not into an array
|
||||||
|
const auto too_large = std::to_string((std::numeric_limits<typename Json::size_type>::max)() - 1);
|
||||||
|
check_strong_guarantee_length_error<Json>("null", "/" + too_large);
|
||||||
|
check_strong_guarantee_length_error<Json>(R"({"a":[[1]]})", "/a/0/3/" + too_large);
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
TEST_CASE("JSON pointers")
|
TEST_CASE("JSON pointers")
|
||||||
{
|
{
|
||||||
SECTION("errors")
|
SECTION("errors")
|
||||||
@@ -493,6 +548,89 @@ TEST_CASE("JSON pointers")
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SECTION("creating intermediate levels")
|
||||||
|
{
|
||||||
|
SECTION("tokens that are valid array indices create arrays")
|
||||||
|
{
|
||||||
|
json j;
|
||||||
|
j["/0"_json_pointer] = 1;
|
||||||
|
CHECK(j == json({1}));
|
||||||
|
|
||||||
|
json j2;
|
||||||
|
j2["/2"_json_pointer] = 1;
|
||||||
|
CHECK(j2 == json({nullptr, nullptr, 1}));
|
||||||
|
|
||||||
|
json j3;
|
||||||
|
j3["/-"_json_pointer] = 1;
|
||||||
|
CHECK(j3 == json({1}));
|
||||||
|
|
||||||
|
json j4;
|
||||||
|
j4["/foo/0/0"_json_pointer] = 1;
|
||||||
|
CHECK(j4 == json({{"foo", {{1}}}}));
|
||||||
|
}
|
||||||
|
|
||||||
|
SECTION("tokens that are no valid array indices create objects")
|
||||||
|
{
|
||||||
|
json j;
|
||||||
|
j["/one"_json_pointer] = 1;
|
||||||
|
CHECK(j == json({{"one", 1}}));
|
||||||
|
|
||||||
|
// leading '0' can never be a valid array index (RFC 6901, Sect. 4)
|
||||||
|
json j2;
|
||||||
|
j2["/01"_json_pointer] = 1;
|
||||||
|
CHECK(j2 == json({{"01", 1}}));
|
||||||
|
|
||||||
|
// the empty token is a valid object key, but no valid array index
|
||||||
|
json j3;
|
||||||
|
j3["/"_json_pointer] = 1;
|
||||||
|
CHECK(j3 == json({{"", 1}}));
|
||||||
|
}
|
||||||
|
|
||||||
|
SECTION("creating a level yields the same result as reusing it (#5357)")
|
||||||
|
{
|
||||||
|
json j;
|
||||||
|
j["/a/b/01/d"_json_pointer] = "value";
|
||||||
|
|
||||||
|
json j_init = json::object();
|
||||||
|
j_init["/a/b"_json_pointer] = json::object();
|
||||||
|
j_init["/a/b/01/d"_json_pointer] = "value";
|
||||||
|
|
||||||
|
const json expected = json::parse(R"({"a":{"b":{"01":{"d":"value"}}}})");
|
||||||
|
CHECK(j == expected);
|
||||||
|
CHECK(j_init == expected);
|
||||||
|
|
||||||
|
// unflatten uses the same key
|
||||||
|
const json flat = {{"/a/b/01/d", "value"}};
|
||||||
|
CHECK(flat.unflatten() == expected);
|
||||||
|
}
|
||||||
|
|
||||||
|
SECTION("existing arrays still reject invalid indices")
|
||||||
|
{
|
||||||
|
json j = {1, 2, 3};
|
||||||
|
CHECK_THROWS_WITH_AS(j["/01"_json_pointer],
|
||||||
|
"[json.exception.parse_error.106] parse error: array index '01' must not begin with '0'", json::parse_error&);
|
||||||
|
CHECK_THROWS_WITH_AS(j.at("/01"_json_pointer),
|
||||||
|
"[json.exception.parse_error.106] parse error: array index '01' must not begin with '0'", json::parse_error&);
|
||||||
|
}
|
||||||
|
|
||||||
|
SECTION("a failing access leaves the value unchanged")
|
||||||
|
{
|
||||||
|
check_strong_guarantee_cases<json>();
|
||||||
|
check_strong_guarantee_cases<nlohmann::ordered_json>();
|
||||||
|
}
|
||||||
|
|
||||||
|
SECTION("a successful access still creates the same values")
|
||||||
|
{
|
||||||
|
json j = {{"a", {1}}};
|
||||||
|
j["/a/3/x/-/y"_json_pointer] = 1;
|
||||||
|
CHECK(j == json::parse(R"({"a":[1,null,null,{"x":[{"y":1}]}]})"));
|
||||||
|
j["/a/-"_json_pointer] = 2;
|
||||||
|
j["/a/0"_json_pointer] = 3;
|
||||||
|
j["/b"_json_pointer] = 4;
|
||||||
|
CHECK(j == json::parse(R"({"a":[3,null,null,{"x":[{"y":1}]},2],"b":4})"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
SECTION("flatten")
|
SECTION("flatten")
|
||||||
{
|
{
|
||||||
json j =
|
json j =
|
||||||
@@ -1044,6 +1182,56 @@ TEST_CASE("flatten of structured values")
|
|||||||
CHECK(flat_object.unflatten() == nlohmann::ordered_json::parse(R"({"b": {"1": 2}})"));
|
CHECK(flat_object.unflatten() == nlohmann::ordered_json::parse(R"({"b": {"1": 2}})"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#if !defined(JSON_NOEXCEPTION)
|
||||||
|
SECTION("an object with the key \"0\" next to a non-index key does not round-trip")
|
||||||
|
{
|
||||||
|
// the third documented case in which j != j.flatten().unflatten()
|
||||||
|
const json value = {{"a", {{"0", 1}, {"b", 2}}}};
|
||||||
|
const json flat = value.flatten();
|
||||||
|
CHECK(flat == json({{"/a/0", 1}, {"/a/b", 2}}));
|
||||||
|
CHECK_THROWS_WITH_AS(flat.unflatten(),
|
||||||
|
"[json.exception.parse_error.109] parse error: array index 'b' is not a number", json::parse_error&);
|
||||||
|
|
||||||
|
// the same for other keys that are not valid array indices
|
||||||
|
CHECK_THROWS_WITH_AS(json({{"/a/0", 1}, {"/a/1a", 2}}).unflatten(),
|
||||||
|
"[json.exception.out_of_range.404] unresolved reference token '1a'", json::out_of_range&);
|
||||||
|
CHECK_THROWS_WITH_AS(json({{"/a/0", 1}, {"/a/-", 2}}).unflatten(),
|
||||||
|
"[json.exception.out_of_range.404] unresolved reference token '-'", json::out_of_range&);
|
||||||
|
}
|
||||||
|
|
||||||
|
SECTION("unflatten with the empty token among array indices")
|
||||||
|
{
|
||||||
|
CHECK_THROWS_WITH_AS(json({{"/a/0", 1}, {"/a/", 2}}).unflatten(),
|
||||||
|
"[json.exception.out_of_range.404] unresolved reference token ''", json::out_of_range&);
|
||||||
|
CHECK_THROWS_WITH_AS(json({{"/a/0", 1}, {"/a/1", 2}, {"/a/", 3}}).unflatten(),
|
||||||
|
"[json.exception.out_of_range.404] unresolved reference token ''", json::out_of_range&);
|
||||||
|
|
||||||
|
// without an index "0", the empty token is an ordinary object key
|
||||||
|
CHECK(json({{"/a/", 2}}).unflatten() == json({{"a", {{"", 2}}}}));
|
||||||
|
}
|
||||||
|
|
||||||
|
SECTION("value() with the empty pointer on a primitive root")
|
||||||
|
{
|
||||||
|
// unlike at(), value() rejects primitive values before it looks at the pointer
|
||||||
|
const json number = 1;
|
||||||
|
CHECK(number.at(""_json_pointer) == 1);
|
||||||
|
CHECK_THROWS_WITH_AS(number.value(""_json_pointer, 7),
|
||||||
|
"[json.exception.type_error.306] cannot use value() with number", json::type_error&);
|
||||||
|
|
||||||
|
const json string = "s";
|
||||||
|
CHECK_THROWS_WITH_AS(string.value(""_json_pointer, 7),
|
||||||
|
"[json.exception.type_error.306] cannot use value() with string", json::type_error&);
|
||||||
|
|
||||||
|
// contains() accepts the empty pointer on every value
|
||||||
|
CHECK(number.contains(""_json_pointer));
|
||||||
|
CHECK_FALSE(number.contains("/a"_json_pointer));
|
||||||
|
|
||||||
|
// arrays and objects return the default for a missing location
|
||||||
|
CHECK(json::array().value(""_json_pointer, json(7)) == json::array());
|
||||||
|
CHECK(json::array().value("/0"_json_pointer, 7) == 7);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
SECTION("objects and arrays interleaved")
|
SECTION("objects and arrays interleaved")
|
||||||
{
|
{
|
||||||
const json value =
|
const json value =
|
||||||
|
|||||||
@@ -423,3 +423,86 @@ TEST_CASE("merge_patch() with an argument that aliases *this (#5641)")
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TEST_CASE("JSON Merge Patch on non-object, discarded, and binary values")
|
||||||
|
{
|
||||||
|
SECTION("an empty object patch turns a non-object target into an empty object")
|
||||||
|
{
|
||||||
|
json target = 1;
|
||||||
|
target.merge_patch(json::object());
|
||||||
|
CHECK(target == json::object());
|
||||||
|
|
||||||
|
json from_null;
|
||||||
|
from_null.merge_patch(json::object());
|
||||||
|
CHECK(from_null == json::object());
|
||||||
|
}
|
||||||
|
|
||||||
|
SECTION("a non-object patch replaces a deeply nested target")
|
||||||
|
{
|
||||||
|
// checked by type, since == recurses
|
||||||
|
SECTION("number")
|
||||||
|
{
|
||||||
|
json target = json::parse(nested_objects(1000, 0));
|
||||||
|
target.merge_patch(json(42));
|
||||||
|
CHECK(target.is_number_integer());
|
||||||
|
CHECK(target.get<int>() == 42);
|
||||||
|
}
|
||||||
|
|
||||||
|
SECTION("array")
|
||||||
|
{
|
||||||
|
json target = json::parse(nested_objects(1000, 0));
|
||||||
|
target.merge_patch(json({1, nullptr}));
|
||||||
|
CHECK(target.is_array());
|
||||||
|
CHECK(target.size() == 2);
|
||||||
|
CHECK(target[1].is_null());
|
||||||
|
}
|
||||||
|
|
||||||
|
SECTION("null")
|
||||||
|
{
|
||||||
|
json target = json::parse(nested_objects(1000, 0));
|
||||||
|
target.merge_patch(json(nullptr));
|
||||||
|
CHECK(target.is_null());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SECTION("discarded values")
|
||||||
|
{
|
||||||
|
const json discarded(json::value_t::discarded);
|
||||||
|
|
||||||
|
// a discarded patch is not an object and replaces the target
|
||||||
|
json target = {{"a", 1}};
|
||||||
|
target.merge_patch(discarded);
|
||||||
|
CHECK(target.is_discarded());
|
||||||
|
|
||||||
|
// a discarded target is replaced by an empty object first
|
||||||
|
json discarded_target(json::value_t::discarded);
|
||||||
|
discarded_target.merge_patch(json({{"a", 1}}));
|
||||||
|
CHECK(discarded_target == json({{"a", 1}}));
|
||||||
|
|
||||||
|
json both(json::value_t::discarded);
|
||||||
|
both.merge_patch(discarded);
|
||||||
|
CHECK(both.is_discarded());
|
||||||
|
}
|
||||||
|
|
||||||
|
SECTION("binary values")
|
||||||
|
{
|
||||||
|
const json bin = json::binary({1, 2, 3}, 7);
|
||||||
|
|
||||||
|
// a binary patch is not an object and replaces the target
|
||||||
|
json target = {{"a", 1}};
|
||||||
|
target.merge_patch(bin);
|
||||||
|
CHECK(target.is_binary());
|
||||||
|
CHECK(target.get_binary() == bin.get_binary());
|
||||||
|
|
||||||
|
// a binary target is replaced by an object
|
||||||
|
json binary_target = bin;
|
||||||
|
binary_target.merge_patch(json({{"a", 1}}));
|
||||||
|
CHECK(binary_target == json({{"a", 1}}));
|
||||||
|
|
||||||
|
// binary values nested in the patch are kept
|
||||||
|
json nested = json::object();
|
||||||
|
nested.merge_patch(json({{"a", bin}}));
|
||||||
|
CHECK(nested["a"].is_binary());
|
||||||
|
CHECK(nested["a"].get_binary() == bin.get_binary());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1499,6 +1499,23 @@ TEST_CASE("regression tests 1")
|
|||||||
CHECK(order.dump() == R"({"a":2,"m":{"y":1,"b":2},"new_key":3})");
|
CHECK(order.dump() == R"({"a":2,"m":{"y":1,"b":2},"new_key":3})");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SECTION("operator[](json_pointer) when emplace() finds a key the lookup does not")
|
||||||
|
{
|
||||||
|
// a copied fifo_map restarts its timestamps, so a new key "k" gets
|
||||||
|
// the timestamp of "x": the lookup does not find "k", but emplace()
|
||||||
|
// finds "x" and inserts nothing; the pointer then continues with the
|
||||||
|
// member of "x", as basic_json::operator[] does
|
||||||
|
const my_json orig = my_json::parse(R"({"x":1})");
|
||||||
|
const std::array<const char*, 3> pointers = {{"/k/0", "/k/a/b", "/k/-"}};
|
||||||
|
for (const char* pointer : pointers)
|
||||||
|
{
|
||||||
|
CAPTURE(pointer)
|
||||||
|
my_json j = orig;
|
||||||
|
CHECK_THROWS_AS(j[my_json::json_pointer(pointer)], my_json::out_of_range&);
|
||||||
|
CHECK(j.dump() == R"({"x":1})");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
SECTION("issue #977 - Assigning between different json types")
|
SECTION("issue #977 - Assigning between different json types")
|
||||||
{
|
{
|
||||||
foo_json lj = ns::foo{3};
|
foo_json lj = ns::foo{3};
|
||||||
|
|||||||
Reference in new issue
Block a user