Compare commits

...
Author SHA1 Message Date
Niels Lohmann da4ce8b8a3 Skip the indentation after a newline 8 bytes at a time
Pretty-printed JSON indents every line with a run of spaces, which
skip_whitespace() read one character at a time. For contiguous input
(bulk_scan) the lexer now skips the spaces after a newline 8 bytes at a
time directly in the input buffer, including the leading spaces of the
last, partial word. This bypasses get(), which is safe because
bulk_scan implies the lazy position from the previous commit: nothing
is counted per character.

Compared to lazy positions alone, pretty-printed input (twitter,
citm_catalog with indent 2 and 4) parses up to 13% faster and accept()
gets up to 30% faster (GCC 13 and clang 18 on x86-64, Apple clang 21 on
arm64). Minified input is unchanged within noise.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-10-13 11:24:24 +02:00
Niels Lohmann 1d4dfc3af0 Report the parse errors of the position test via SAX
The test caught json::parse_error, so it aborted in the
ci_test_noexceptions build, where JSON_NOEXCEPTION turns the throw into
std::abort(). sax_parse() hands the error position and the exception
object to parse_error() without throwing, so the test now records them
there and runs in every configuration.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-10-13 11:22:48 +02:00
Niels Lohmann db3d98de07 Move the std::vector<std::byte> binary type tests out of unit-regression3
The #4804/#4209 tests instantiated a second basic_json specialization
(with_binary_t<std::vector<std::byte>>), including its parser and CBOR
reader. With clang 18, the C++20 object of unit-regression3.cpp had
32656 COFF sections on develop; the lazy position helpers pushed it to
32975, past what the MinGW linker of the Windows clang jobs handles
("relocation truncated to fit: IMAGE_REL_AMD64_REL32 against .rdata").
unit-custom-binary-type.cpp already instantiates the same type, so the
tests move there; unit-regression3.cpp drops to 27616 sections.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-10-13 09:40:36 +02:00
Niels Lohmann 3dbed2a0cb Derive the lexer position lazily for seekable input
The lexer counted the read position on every character: two increments
and a newline check per get(), all on members kept in memory. For
seekable input (pointers, strings, other random-access byte ranges) the
position can be derived instead: chars_read_total is the adapter's read
count (including reads at EOF) minus a pending unget, and line and
column are recomputed from the consumed input only when get_position()
is called, i.e. on parse errors. JSON_DIAGNOSTIC_POSITIONS uses the new
O(1) get_chars_read_total(). Stream, file, wide-string and user-defined
adapters keep the eager counters.

Parsing pretty-printed input gets 8-12% faster and accept() 12-27%
(canada, citm_catalog, twitter; clang 21 arm64 and GCC 13 x86-64).
Error messages, byte offsets, SAX positions and diagnostic positions are
unchanged (differential comparison against develop on 548k inputs).

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-10-13 08:52:49 +02:00
satelliteprogrammer 40a62947d3 support JSON Merge Patch (RFC 7396) diff creation (#4965)
* support JSON Merge Patch (RFC 7396) diff creation

The JSON merge patch document format describes the set of modifications
to a resource's content, that more closely mimics the syntax of the
resource being modified.

However, and in contrast to JSON Patch (RFC 6902), a JSON Merge Patch
cannot express certain modifications, e.g., changing an array element
at a specific index, or setting a specific object value to null.
The null value in a JSON Merge Patch is used to remove the key from the
object.

The diff algorithm is not part of the RFC 7396, but it was tested
against all examples provided, plus additional cases on how null values
are handled.

Signed-off-by: Luís Murta <luis@murta.dev>

* improve algorithm efficiency

Signed-off-by: Luís Murta <luis@murta.dev>

* fix inconsistent nulls and empty arrays

Also add CHECKs for the patch, documentation and run `make amalgamate`.

Signed-off-by: Luís Murta <luis@murta.dev>

* documentation cleanups

Signed-off-by: Luís Murta <luis@murta.dev>

* add an iterative fallback to merge_diff

Bound the recursion depth like diff() and merge_patch(), and test values
nested past the bound.

Signed-off-by: Luís Murta <luis@murta.dev>

* address the remaining review comments

Signed-off-by: Luís Murta <luis@murta.dev>

* move multi-line raw strings out of CHECK macros

GCC 4.8/5, ICPC and NVHPC mis-tokenize multi-line raw string literals
inside macro arguments, breaking the build or the parsed JSON.

Signed-off-by: Luís Murta <luis@murta.dev>

---------

Signed-off-by: Luís Murta <luis@murta.dev>
2026-10-12 09:33:43 +02:00
22 changed files with 2148 additions and 132 deletions

No files matched your search

+1
View File
@@ -59,6 +59,7 @@ Currently, only `remove`, `add`, and `replace` operations are generated.
- [patch](patch.md) applies a JSON Patch - [patch](patch.md) applies a JSON Patch
- [patch_inplace](patch_inplace.md) applies a JSON Patch in place - [patch_inplace](patch_inplace.md) applies a JSON Patch in place
- [merge_patch](merge_patch.md) applies a JSON Merge Patch - [merge_patch](merge_patch.md) applies a JSON Merge Patch
- [merge_diff](merge_diff.md) creates a diff as a JSON Merge Patch
## Version history ## Version history
+1
View File
@@ -285,6 +285,7 @@ Access to the JSON value
### JSON Merge Patch functions ### JSON Merge Patch functions
- [**merge_patch**](merge_patch.md) - applies a JSON Merge Patch - [**merge_patch**](merge_patch.md) - applies a JSON Merge Patch
- [**merge_diff**](merge_diff.md) (_static_) - creates a diff as a JSON Merge Patch
## Static functions ## Static functions
@@ -0,0 +1,79 @@
# <small>nlohmann::basic_json::</small>merge_diff
```cpp
static basic_json merge_diff(const basic_json& source,
const basic_json& target);
```
Creates a [JSON Merge Patch](https://tools.ietf.org/html/rfc7396) so that value `source` can be changed into the value
`target` by calling the [`merge_patch`](merge_patch.md) function. The patch contains only what differs: changed and
added members with their new values, and removed members with the value `#!json null`. Nested objects are compared
member by member; any other value that differs, including an array, is replaced as a whole.
For two JSON values `source` and `target`, where `target` contains no object member whose value is `#!json null`, the
following code always yields `#!cpp true`:
```cpp
basic_json patched = source;
patched.merge_patch(merge_diff(source, target));
patched == target;
```
## Parameters
`source` (in)
: JSON value to compare from
`target` (in)
: JSON value to compare against
## Return value
a JSON Merge Patch to convert the `source` to `target`
## Exception safety
Strong guarantee: `source` and `target` are never modified.
## Complexity
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, or adding one to the patch): 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)²).
## Notes
- A merge patch uses `#!json null` to remove a member, so it cannot set a member to `#!json null`. As in RFC 7396, a
`#!json null` member of `target` is treated as absent: the patch removes the member from `source` instead of setting
it to `#!json null`. A member that is `#!json null` in both `source` and `target` is unchanged and left out of the
patch, so `source` keeps it.
- A merge patch cannot reorder object members, and [`merge_patch`](merge_patch.md) appends added members at the end.
For [`ordered_json`](../ordered_json.md), the patched value therefore has the same members as `target`, but not
necessarily in the same order, and may compare unequal to it. The patch lists changed and removed members in the
order of `source`, followed by added members in the order of `target`.
## Examples
??? example
The following code shows how a JSON Merge Patch is created as a diff for two JSON values.
```cpp
--8<-- "examples/merge_diff.cpp"
```
Output:
```json
--8<-- "examples/merge_diff.output"
```
## See also
- [RFC 7396 (JSON Merge Patch)](https://tools.ietf.org/html/rfc7396)
- [merge_patch](merge_patch.md) applies a JSON Merge Patch
- [diff](diff.md) creates a diff as a JSON Patch
## Version history
- Added in version 3.13.0.
@@ -67,6 +67,7 @@ returned by `#!cpp (*this)[key]`); it is read as it was when `merge_patch()` was
- [RFC 7396 (JSON Merge Patch)](https://tools.ietf.org/html/rfc7396) - [RFC 7396 (JSON Merge Patch)](https://tools.ietf.org/html/rfc7396)
- [patch](patch.md) apply a JSON patch - [patch](patch.md) apply a JSON patch
- [merge_diff](merge_diff.md) creates a diff as a JSON Merge Patch
## Version history ## Version history
+51
View File
@@ -0,0 +1,51 @@
#include <iostream>
#include <iomanip>
#include <nlohmann/json.hpp>
using json = nlohmann::json;
using namespace nlohmann::literals;
int main()
{
// the source document
json source = R"(
{
"title": "Goodbye!",
"author": {
"givenName": "John",
"familyName": "Doe"
},
"tags": [
"example",
"sample"
],
"content": "This will be unchanged"
}
)"_json;
// the target document
json target = R"(
{
"title": "Hello!",
"author": {
"givenName": "John"
},
"tags": [
"example"
],
"content": "This will be unchanged",
"phoneNumber": "+01-123-456-7890"
}
)"_json;
// create the patch
json patch = json::merge_diff(source, target);
// roundtrip
json patched_source = source;
patched_source.merge_patch(patch);
// output patch and roundtrip result
std::cout << std::setw(4) << patch << "\n\n"
<< std::setw(4) << patched_source << std::endl;
}
@@ -0,0 +1,22 @@
{
"author": {
"familyName": null
},
"phoneNumber": "+01-123-456-7890",
"tags": [
"example"
],
"title": "Hello!"
}
{
"author": {
"givenName": "John"
},
"content": "This will be unchanged",
"phoneNumber": "+01-123-456-7890",
"tags": [
"example"
],
"title": "Hello!"
}
+4
View File
@@ -9,6 +9,9 @@ syntax that closely mimics the document being modified. Unlike [JSON Patch](json
express every kind of change (e.g., it cannot reorder array elements or remove a specific array element), but it is express every kind of change (e.g., it cannot reorder array elements or remove a specific array element), but it is
easier to read and write for object-shaped documents. easier to read and write for object-shaped documents.
The function [`merge_diff`](../api/basic_json/merge_diff.md) creates a merge patch that changes one JSON value into
another.
!!! tip "Not a general deep merge" !!! tip "Not a general deep merge"
A merge patch is not a general deep merge: a `#!json null` value in the patch deletes the key from the target. A merge patch is not a general deep merge: a `#!json null` value in the patch deletes the key from the target.
@@ -35,4 +38,5 @@ easier to read and write for object-shaped documents.
- [JSON Patch and Diff](json_patch.md) - a more expressive alternative that describes a sequence of operations - [JSON Patch and Diff](json_patch.md) - a more expressive alternative that describes a sequence of operations
- [JSON Pointer](json_pointer.md) - the addressing scheme used by JSON Patch - [JSON Pointer](json_pointer.md) - the addressing scheme used by JSON Patch
- Function [`merge_patch`](../api/basic_json/merge_patch.md) - Function [`merge_patch`](../api/basic_json/merge_patch.md)
- Function [`merge_diff`](../api/basic_json/merge_diff.md)
- Function [`update`](../api/basic_json/update.md) - merge objects, optionally recursively - Function [`update`](../api/basic_json/update.md) - merge objects, optionally recursively
+4 -2
View File
@@ -185,9 +185,11 @@ The lexer detects two optional extensions at compile time. Only `iterator_input_
random-access input of single-byte characters: random-access input of single-byte characters:
- `supports_seek`, `get_consumed_count()`, and `copy_consumed_range()` let the lexer reconstruct already consumed input - `supports_seek`, `get_consumed_count()`, and `copy_consumed_range()` let the lexer reconstruct already consumed input
for error messages instead of copying every character it reads. for error messages instead of copying every character it reads. With `get_read_count()` and
`count_consumed_lines()`, the lexer also derives the read position from the adapter and counts lines only when an
error needs them, instead of counting characters and lines on every read.
- `supports_bulk_scan`, `bulk_data()`, `bulk_remaining()`, and `bulk_skip()` let the lexer scan strings directly in - `supports_bulk_scan`, `bulk_data()`, `bulk_remaining()`, and `bulk_skip()` let the lexer scan strings directly in
contiguous memory, several bytes at a time. contiguous memory, several bytes at a time, and skip the spaces that indent a line in 8-byte steps.
The function `input_adapter` picks the right adapter for the argument passed to `parse`, `accept`, `sax_parse`, or the The function `input_adapter` picks the right adapter for the argument passed to `parse`, `accept`, `sax_parse`, or the
`from_*` functions: `from_*` functions:
+1
View File
@@ -182,6 +182,7 @@ nav:
- 'json_serializer': api/basic_json/json_serializer.md - 'json_serializer': api/basic_json/json_serializer.md
- 'max_size': api/basic_json/max_size.md - 'max_size': api/basic_json/max_size.md
- 'meta': api/basic_json/meta.md - 'meta': api/basic_json/meta.md
- 'merge_diff': api/basic_json/merge_diff.md
- 'merge_patch': api/basic_json/merge_patch.md - 'merge_patch': api/basic_json/merge_patch.md
- 'number_float_t': api/basic_json/number_float_t.md - 'number_float_t': api/basic_json/number_float_t.md
- 'number_integer_t': api/basic_json/number_integer_t.md - 'number_integer_t': api/basic_json/number_integer_t.md
+19
View File
@@ -42,6 +42,25 @@ inline int count_leading_zeros(std::uint64_t x) noexcept
#endif #endif
} }
/// number of trailing zero bits of x (x != 0)
inline int count_trailing_zeros(std::uint64_t x) noexcept
{
#if defined(__GNUC__) || defined(__clang__)
return __builtin_ctzll(x);
#else
int n = 0;
for (int shift = 32; shift != 0; shift >>= 1)
{
if ((x << (64 - shift)) == 0)
{
n += shift;
x >>= shift;
}
}
return n;
#endif
}
/// the 128-bit product of two 64-bit numbers /// the 128-bit product of two 64-bit numbers
struct uint128_parts struct uint128_parts
{ {
@@ -8,12 +8,12 @@
#pragma once #pragma once
#include <algorithm> // min #include <algorithm> // count, find, min
#include <array> // array #include <array> // array
#include <cstddef> // size_t #include <cstddef> // size_t
#include <cstdint> // uint32_t #include <cstdint> // uint32_t
#include <cstring> // strlen #include <cstring> // strlen
#include <iterator> // begin, end, iterator_traits, random_access_iterator_tag, distance, next #include <iterator> // begin, end, iterator_traits, random_access_iterator_tag, distance, next, reverse_iterator
#include <streambuf> // streambuf #include <streambuf> // streambuf
#include <string> // string, char_traits #include <string> // string, char_traits
#include <type_traits> // enable_if, is_base_of, is_pointer, is_integral, remove_pointer #include <type_traits> // enable_if, is_base_of, is_pointer, is_integral, remove_pointer
@@ -297,6 +297,7 @@ class iterator_input_adapter
return result; return result;
} }
++eof_reads;
return char_traits<char_type>::eof(); return char_traits<char_type>::eof();
} }
@@ -306,6 +307,26 @@ class iterator_input_adapter
return static_cast<std::size_t>(std::distance(begin, current)); return static_cast<std::size_t>(std::distance(begin, current));
} }
// number of get_character() calls so far, including those that returned
// EOF; the lexer derives its position from it (see lexer::lazy_position)
std::size_t get_read_count() const
{
return get_consumed_count() + eof_reads;
}
// number of newlines in the consumed range [0, last_index); @a line_start
// is set to the index after the last of them (0 if there is none); only
// valid when supports_seek
std::size_t count_consumed_lines(std::size_t last_index, std::size_t& line_start) const
{
const auto to = std::next(begin, static_cast<typename std::iterator_traits<IteratorType>::difference_type>(last_index));
const auto newline = char_traits<char_type>::to_char_type('\n');
using reverse_it = std::reverse_iterator<IteratorType>;
const auto last_newline = std::find(reverse_it(to), reverse_it(begin), newline);
line_start = static_cast<std::size_t>(std::distance(begin, last_newline.base()));
return static_cast<std::size_t>(std::count(begin, last_newline.base(), newline));
}
// append the already-consumed characters in the half-open range // append the already-consumed characters in the half-open range
// [first_index, last_index) to @a out; only valid when supports_seek // [first_index, last_index) to @a out; only valid when supports_seek
template<typename ContainerType> template<typename ContainerType>
@@ -422,6 +443,8 @@ class iterator_input_adapter
IteratorType begin; IteratorType begin;
IteratorType current; IteratorType current;
SentinelType end; SentinelType end;
/// number of get_character() calls that returned EOF
std::size_t eof_reads = 0;
template<typename BaseInputAdapter, size_t T> template<typename BaseInputAdapter, size_t T>
friend struct wide_string_input_helper; friend struct wide_string_input_helper;
+3 -3
View File
@@ -217,7 +217,7 @@ struct diagnostic_positions
{ {
// Lexer has read the first character of the container, so // Lexer has read the first character of the container, so
// subtract 1 from the position to get the correct start position. // subtract 1 from the position to get the correct start position.
v.start_position = lexer->get_position() - 1; v.start_position = lexer->get_chars_read_total() - 1;
} }
} }
@@ -231,7 +231,7 @@ struct diagnostic_positions
if (lexer) if (lexer)
{ {
// Lexer's position is past the closing brace or bracket, so set that as the end position. // Lexer's position is past the closing brace or bracket, so set that as the end position.
v.end_position = lexer->get_position(); v.end_position = lexer->get_chars_read_total();
} }
} }
@@ -247,7 +247,7 @@ struct diagnostic_positions
// Lexer has read past the current field value, so set the end position to the current position. // Lexer has read past the current field value, so set the end position to the current position.
// The start position will be set below based on the length of the string representation // The start position will be set below based on the length of the string representation
// of the value. // of the value.
v.end_position = lexer->get_position(); v.end_position = lexer->get_chars_read_total();
switch (v.type()) switch (v.type())
{ {
+138 -20
View File
@@ -17,6 +17,7 @@
#include <utility> // move #include <utility> // move
#include <vector> // vector #include <vector> // vector
#include <nlohmann/detail/bit_ops.hpp>
#include <nlohmann/detail/input/input_adapters.hpp> #include <nlohmann/detail/input/input_adapters.hpp>
#include <nlohmann/detail/input/number_parse.hpp> #include <nlohmann/detail/input/number_parse.hpp>
#include <nlohmann/detail/input/position_t.hpp> #include <nlohmann/detail/input/position_t.hpp>
@@ -186,6 +187,15 @@ class lexer : public lexer_base<BasicJsonType>
static constexpr bool lazy_token_string = static constexpr bool lazy_token_string =
input_adapter_supports_seek<InputAdapterType>(is_detected<detect_supports_seek, InputAdapterType> {}); input_adapter_supports_seek<InputAdapterType>(is_detected<detect_supports_seek, InputAdapterType> {});
/// whether the read position is derived from the input adapter on demand
/// instead of being counted on every scanned character. Seekable adapters
/// know how much input they consumed and can revisit it, so
/// chars_read_total is the adapter's read count (minus a pending unget),
/// and line and column are recomputed from the consumed input only when
/// get_position() is called (errors). Counting per character kept two
/// increments and a newline check in the hot path of every get().
static constexpr bool lazy_position = lazy_token_string;
/// whether a simulated unget can be passed on to the input adapter, which /// whether a simulated unget can be passed on to the input adapter, which
/// then leaves the character in the input; see /// then leaves the character in the input; see
/// input_adapter_supports_lookahead /// input_adapter_supports_lookahead
@@ -195,7 +205,9 @@ class lexer : public lexer_base<BasicJsonType>
/// whether string scanning may bulk-consume runs of ordinary characters /// whether string scanning may bulk-consume runs of ordinary characters
/// directly from a contiguous input buffer (SWAR fast path). This requires /// directly from a contiguous input buffer (SWAR fast path). This requires
/// the token to be reconstructible lazily (lazy_token_string), so bypassing /// the token to be reconstructible lazily (lazy_token_string), so bypassing
/// the per-character capture in get() cannot lose error diagnostics. /// the per-character capture in get() cannot lose error diagnostics. It
/// also implies lazy_position, so the bulk paths need not advance the
/// position counters.
static constexpr bool bulk_scan = static constexpr bool bulk_scan =
lazy_token_string lazy_token_string
&& input_adapter_supports_bulk_scan<InputAdapterType>(is_detected<detect_supports_bulk_scan, InputAdapterType> {}); && input_adapter_supports_bulk_scan<InputAdapterType>(is_detected<detect_supports_bulk_scan, InputAdapterType> {});
@@ -331,10 +343,6 @@ class lexer : public lexer_base<BasicJsonType>
} }
token_buffer.append(reinterpret_cast<const typename string_t::value_type*>(data), pos); token_buffer.append(reinterpret_cast<const typename string_t::value_type*>(data), pos);
ia.bulk_skip(pos); ia.bulk_skip(pos);
// the run contains no newline (all bytes < 0x20 are treated as special),
// so only the flat character counters advance
position.chars_read_total += pos;
position.chars_read_current_line += pos;
} }
/// streaming input: no bulk fast path /// streaming input: no bulk fast path
@@ -1567,8 +1575,6 @@ scan_number_done:
if (JSON_HEDLEY_LIKELY(integer_result != token_type::uninitialized)) if (JSON_HEDLEY_LIKELY(integer_result != token_type::uninitialized))
{ {
ia.bulk_skip(len - 1); ia.bulk_skip(len - 1);
position.chars_read_total += (len - 1);
position.chars_read_current_line += (len - 1);
return integer_result; return integer_result;
} }
// The value does not fit an integer, so this token converts as a // The value does not fit an integer, so this token converts as a
@@ -1585,8 +1591,6 @@ scan_number_done:
decimal_point_position = dot_index; decimal_point_position = dot_index;
ia.bulk_skip(len - 1); ia.bulk_skip(len - 1);
position.chars_read_total += (len - 1);
position.chars_read_current_line += (len - 1);
return convert_number(number_type, mantissa_end); return convert_number(number_type, mantissa_end);
} }
@@ -1637,7 +1641,7 @@ scan_number_done:
#if JSON_DIAGNOSTIC_POSITIONS #if JSON_DIAGNOSTIC_POSITIONS
// the first character of the token has already been read, hence the -1 // the first character of the token has already been read, hence the -1
token_start_position = position.chars_read_total - 1; token_start_position = get_chars_read_total() - 1;
#endif #endif
note_token_start(std::integral_constant<bool, lazy_token_string> {}); note_token_start(std::integral_constant<bool, lazy_token_string> {});
@@ -1690,6 +1694,15 @@ scan_number_done:
/// per-character position counters (line-count-on-'\n' bookkeeping is /// per-character position counters (line-count-on-'\n' bookkeeping is
/// handled afterwards, in track_after_read(), once `current` is known) /// handled afterwards, in track_after_read(), once `current` is known)
void advance_position() noexcept void advance_position() noexcept
{
advance_position_impl(std::integral_constant<bool, lazy_position> {});
}
/// seekable adapter: the position is derived from the adapter on demand
void advance_position_impl(std::true_type /*lazy*/) const noexcept {}
/// streaming adapter: count the character
void advance_position_impl(std::false_type /*lazy*/) noexcept
{ {
++position.chars_read_total; ++position.chars_read_total;
++position.chars_read_current_line; ++position.chars_read_current_line;
@@ -1703,7 +1716,16 @@ scan_number_done:
// seekable adapters reconstruct the token lazily on error (see // seekable adapters reconstruct the token lazily on error (see
// get_token_string), so the eager per-character copy is skipped // get_token_string), so the eager per-character copy is skipped
capture_char(std::integral_constant<bool, lazy_token_string> {}); capture_char(std::integral_constant<bool, lazy_token_string> {});
track_newline(std::integral_constant<bool, lazy_position> {});
return current;
}
/// seekable adapter: lines are counted on demand in get_position()
void track_newline(std::true_type /*lazy*/) const noexcept {}
/// streaming adapter: start a new line after reading a newline
void track_newline(std::false_type /*lazy*/) noexcept
{
if (current == '\n') if (current == '\n')
{ {
++position.lines_read; ++position.lines_read;
@@ -1712,8 +1734,6 @@ scan_number_done:
chars_read_before_newline = position.chars_read_current_line; chars_read_before_newline = position.chars_read_current_line;
position.chars_read_current_line = 0; position.chars_read_current_line = 0;
} }
return current;
} }
/*! /*!
@@ -1757,12 +1777,22 @@ scan_number_done:
We implement unget by setting variable next_unget to true. The input is not We implement unget by setting variable next_unget to true. The input is not
changed - we just simulate ungetting by modifying chars_read_total, changed - we just simulate ungetting by modifying chars_read_total,
chars_read_current_line, and token_string. The next call to get() will chars_read_current_line, and token_string. The next call to get() will
behave as if the unget character is read again. behave as if the unget character is read again. With lazy_position, the
pending next_unget is all get_chars_read_total() needs.
*/ */
void unget() void unget()
{ {
next_unget = true; next_unget = true;
unget_position(std::integral_constant<bool, lazy_position> {});
uncapture_char(std::integral_constant<bool, lazy_token_string> {});
}
/// seekable adapter: get_chars_read_total() accounts for next_unget
void unget_position(std::true_type /*lazy*/) const noexcept {}
/// streaming adapter: step the counters back over the ungotten character
void unget_position(std::false_type /*lazy*/) noexcept
{
--position.chars_read_total; --position.chars_read_total;
// in case we "unget" a newline, we have to also decrement the lines_read // in case we "unget" a newline, we have to also decrement the lines_read
@@ -1785,8 +1815,6 @@ scan_number_done:
{ {
--position.chars_read_current_line; --position.chars_read_current_line;
} }
uncapture_char(std::integral_constant<bool, lazy_token_string> {});
} }
/// adapter without lookahead: nothing to do (see release_lookahead) /// adapter without lookahead: nothing to do (see release_lookahead)
@@ -1857,10 +1885,21 @@ scan_number_done:
// diagnostics // diagnostics
///////////////////// /////////////////////
/// return position of last read token /*!
constexpr position_t get_position() const noexcept @brief return the number of characters read so far
Unlike get_position(), this is O(1) for all adapters, so it may be called
for every value (JSON_DIAGNOSTIC_POSITIONS).
*/
std::size_t get_chars_read_total() const noexcept
{ {
return position; return get_chars_read_total_impl(std::integral_constant<bool, lazy_position> {});
}
/// return position of last read token
position_t get_position() const
{
return get_position_impl(std::integral_constant<bool, lazy_position> {});
} }
/*! /*!
@@ -1897,6 +1936,43 @@ scan_number_done:
} }
#endif #endif
private:
/// seekable adapter: every get() either read from the adapter (including
/// reads at EOF, which get() counts as characters as well) or replayed an
/// ungotten character, so the count is the adapter's read count minus a
/// still pending unget
std::size_t get_chars_read_total_impl(std::true_type /*lazy*/) const noexcept
{
return ia.get_read_count() - (next_unget ? 1u : 0u);
}
/// streaming adapter: the counter is maintained by get() and unget()
std::size_t get_chars_read_total_impl(std::false_type /*lazy*/) const noexcept
{
return position.chars_read_total;
}
/// seekable adapter: count the lines in the consumed input; O(input read
/// so far), but only called on errors
position_t get_position_impl(std::true_type /*lazy*/) const
{
position_t result;
result.chars_read_total = get_chars_read_total();
// reads at EOF count as characters but are not part of the input
const std::size_t consumed = (std::min)(result.chars_read_total, ia.get_consumed_count());
std::size_t line_start = 0;
result.lines_read = ia.count_consumed_lines(consumed, line_start);
result.chars_read_current_line = result.chars_read_total - line_start;
return result;
}
/// streaming adapter: the position is maintained by get() and unget()
position_t get_position_impl(std::false_type /*lazy*/) const noexcept
{
return position;
}
public:
/// seekable adapter: rebuild the last read token from the input on demand /// seekable adapter: rebuild the last read token from the input on demand
const std::vector<char_type>& collect_token_chars(std::vector<char_type>& out, std::true_type /*lazy*/) const const std::vector<char_type>& collect_token_chars(std::vector<char_type>& out, std::true_type /*lazy*/) const
{ {
@@ -2004,15 +2080,57 @@ scan_number_done:
// into one that reloads the pointer from memory every character // into one that reloads the pointer from memory every character
do do
{ {
if (current == '\n')
{
skip_indentation(std::integral_constant<bool, bulk_scan> {});
}
get_ignoring_pending_unget(); get_ignoring_pending_unget();
} }
while (current_is_whitespace()); while (current_is_whitespace());
} }
/*!
@brief contiguous input: skip the space indentation that follows a newline
8 bytes at a time
Pretty-printed JSON indents every line with a run of spaces, which the loop
in skip_whitespace() would otherwise read one character at a time. The
spaces are skipped 8 bytes at a time; the last 7 bytes of the input and any
other whitespace are left to that loop. Skipping bypasses get(), which is
fine because bulk_scan implies lazy_position: the position is derived from
the adapter, not counted per character.
*/
void skip_indentation(std::true_type /*bulk*/)
{
JSON_ASSERT(!next_unget);
const std::size_t remaining = ia.bulk_remaining();
if (remaining < 8)
{
return;
}
const char* const data = reinterpret_cast<const char*>(ia.bulk_data());
std::size_t pos = 0;
for (; pos + 8 <= remaining; pos += 8)
{
// little-endian, so the first byte is the lowest
const std::uint64_t non_space = read_eight_bytes(data + pos) ^ 0x2020202020202020ull;
if (non_space != 0)
{
// skip the spaces before the first other byte of this word
pos += static_cast<std::size_t>(count_trailing_zeros(non_space)) / 8;
break;
}
}
ia.bulk_skip(pos);
}
/// streaming input: no bulk fast path
void skip_indentation(std::false_type /*bulk*/) const noexcept {}
token_type scan() token_type scan()
{ {
// initially, skip the BOM // initially, skip the BOM
if (position.chars_read_total == 0 && !skip_bom()) if (get_chars_read_total() == 0 && !skip_bom())
{ {
error_message = "invalid BOM; must be 0xEF 0xBB 0xBF if given"; error_message = "invalid BOM; must be 0xEF 0xBB 0xBF if given";
return token_type::parse_error; return token_type::parse_error;
@@ -2039,7 +2157,7 @@ scan_number_done:
token_type scan_expecting(token_type expected_type) token_type scan_expecting(token_type expected_type)
{ {
JSON_ASSERT(expected_type == token_type::name_separator || expected_type == token_type::value_separator); JSON_ASSERT(expected_type == token_type::name_separator || expected_type == token_type::value_separator);
JSON_ASSERT(position.chars_read_total > 0); JSON_ASSERT(get_chars_read_total() > 0);
const char_int_type expected_char = static_cast<unsigned char>((expected_type == token_type::name_separator) ? ':' : ','); const char_int_type expected_char = static_cast<unsigned char>((expected_type == token_type::name_separator) ? ':' : ',');
skip_whitespace(); skip_whitespace();
if (JSON_HEDLEY_LIKELY(current == expected_char)) if (JSON_HEDLEY_LIKELY(current == expected_char))
+5 -3
View File
@@ -370,7 +370,7 @@ class parser
} }
case token_type::end_of_input: case token_type::end_of_input:
{ {
if (JSON_HEDLEY_UNLIKELY(m_lexer.get_position().chars_read_total == 1)) if (JSON_HEDLEY_UNLIKELY(m_lexer.get_chars_read_total() == 1))
{ {
return syntax_error(*sax, "attempting to parse an empty input; check that your input string or stream contains the expected JSON"); return syntax_error(*sax, "attempting to parse an empty input; check that your input string or stream contains the expected JSON");
} }
@@ -520,8 +520,10 @@ class parser
{ {
// MSVC 2015 reports C4100 (unreferenced parameter) when SAX::parse_error is static // MSVC 2015 reports C4100 (unreferenced parameter) when SAX::parse_error is static
static_cast<void>(sax); static_cast<void>(sax);
return sax.parse_error(m_lexer.get_position(), m_lexer.get_token_string(), // computing the position is O(input read so far) for seekable input
parse_error::create(exception_id::syntax_error, m_lexer.get_position(), message, nullptr)); const position_t position = m_lexer.get_position();
return sax.parse_error(position, m_lexer.get_token_string(),
parse_error::create(exception_id::syntax_error, position, message, nullptr));
} }
std::string exception_message(const token_type expected, const std::string& context) std::string exception_message(const token_type expected, const std::string& context)
+170
View File
@@ -7449,6 +7449,176 @@ public:
} }
public: public:
/// @brief creates a diff as a JSON Merge Patch
/// @sa https://json.nlohmann.me/api/basic_json/merge_diff/
JSON_HEDLEY_WARN_UNUSED_RESULT
static basic_json merge_diff(const basic_json& source, const basic_json& target)
{
// unless both are objects, the patch is the target itself
if (!source.is_object() || !target.is_object())
{
return target;
}
return merge_diff_recursively(source, target, 0);
}
private:
/// @brief two objects @ref merge_diff_iteratively is diffing
struct merge_diff_frame
{
merge_diff_frame(const basic_json* source_, const basic_json* target_)
: source(source_), target(target_), member(source_->cbegin()), patch(value_t::object)
{}
// declared for GCC's -Weffc++, as in diff_frame
merge_diff_frame(const merge_diff_frame&) = default;
merge_diff_frame(merge_diff_frame&&) = default;
merge_diff_frame& operator=(const merge_diff_frame&) = default;
merge_diff_frame& operator=(merge_diff_frame&&) = default;
~merge_diff_frame() = default;
/// the objects being diffed
const basic_json* source;
const basic_json* target;
/// the member of source being diffed; advanced once it is done
const_iterator member;
/// the patch for the members of source diffed so far
basic_json patch;
};
/// @brief add the members of object @a target that @a source does not
/// have to the merge patch @a result
static void merge_diff_added_members(basic_json& result, const basic_json& source, const basic_json& target)
{
for (auto it = target.begin(); it != target.end(); ++it)
{
auto itf = source.find(it.key());
if (itf == source.end())
{
result[it.key()] = it.value();
}
}
}
/*!
@brief @ref merge_diff of two objects at nesting level @a depth
Diffing two nested objects calls this function again, once per nesting
level, so values nested deeply enough would exhaust the call stack and
terminate the process. The descent is bounded here: once @ref
detail::recursion_depth_limit levels have been entered, @ref
merge_diff_iteratively diffs what is left without the call stack.
*/
static basic_json merge_diff_recursively(const basic_json& source, const basic_json& target, const std::size_t depth)
{
JSON_ASSERT(source.is_object() && target.is_object());
if (JSON_HEDLEY_UNLIKELY(depth >= detail::recursion_depth_limit()))
{
return merge_diff_iteratively(source, target);
}
basic_json result(value_t::object);
for (auto it = source.begin(); it != source.end(); ++it)
{
auto itf = target.find(it.key());
if (itf != target.end())
{
if (it.value().is_object() && itf.value().is_object())
{
auto diff = merge_diff_recursively(it.value(), itf.value(), depth + 1);
if (!diff.empty())
{
result[it.key()] = std::move(diff);
}
}
else if (it.value() != itf.value())
{
result[it.key()] = itf.value();
}
}
else
{
result[it.key()] = value_t::null;
}
}
merge_diff_added_members(result, source, target);
return result;
}
/*!
@brief @ref merge_diff of two objects without the call stack
Produces the same patch as @ref merge_diff_recursively, keeping the objects
being diffed on an explicit stack instead of descending into them. Two
nested objects are still diffed completely before the next member, so the
patch has its members in the same order as in the recursive version. Only
reached for objects nested more deeply than @ref
detail::recursion_depth_limit.
*/
static basic_json merge_diff_iteratively(const basic_json& source, const basic_json& target)
{
// the patch of source and target, set once their frame is popped
basic_json result;
std::vector<merge_diff_frame> stack;
stack.emplace_back(&source, &target);
while (!stack.empty())
{
// the frame's members are copied out and it is changed through
// stack.back(): pushing a frame for nested objects or popping
// this one would invalidate a reference to it
const basic_json* const s = stack.back().source;
const basic_json* const t = stack.back().target;
const const_iterator it = stack.back().member;
if (it != s->cend())
{
const const_iterator itf = t->find(it.key());
if (itf != t->cend())
{
if (it.value().is_object() && itf.value().is_object())
{
// member is advanced once their patch is under its key
stack.emplace_back(&it.value(), &itf.value());
continue;
}
if (it.value() != itf.value())
{
stack.back().patch[it.key()] = itf.value();
}
}
else
{
stack.back().patch[it.key()] = value_t::null;
}
++stack.back().member;
continue;
}
merge_diff_added_members(stack.back().patch, *s, *t);
// these objects are done: continue with the ones they are in
basic_json patch = std::move(stack.back().patch);
stack.pop_back();
if (stack.empty())
{
result = std::move(patch);
}
else
{
if (!patch.empty())
{
stack.back().patch[stack.back().member.key()] = std::move(patch);
}
++stack.back().member;
}
}
return result;
}
/// @} /// @}
}; };
+385 -52
View File
@@ -7963,12 +7963,12 @@ NLOHMANN_JSON_NAMESPACE_END
#include <algorithm> // min #include <algorithm> // count, find, min
#include <array> // array #include <array> // array
#include <cstddef> // size_t #include <cstddef> // size_t
#include <cstdint> // uint32_t #include <cstdint> // uint32_t
#include <cstring> // strlen #include <cstring> // strlen
#include <iterator> // begin, end, iterator_traits, random_access_iterator_tag, distance, next #include <iterator> // begin, end, iterator_traits, random_access_iterator_tag, distance, next, reverse_iterator
#include <streambuf> // streambuf #include <streambuf> // streambuf
#include <string> // string, char_traits #include <string> // string, char_traits
#include <type_traits> // enable_if, is_base_of, is_pointer, is_integral, remove_pointer #include <type_traits> // enable_if, is_base_of, is_pointer, is_integral, remove_pointer
@@ -8257,6 +8257,7 @@ class iterator_input_adapter
return result; return result;
} }
++eof_reads;
return char_traits<char_type>::eof(); return char_traits<char_type>::eof();
} }
@@ -8266,6 +8267,26 @@ class iterator_input_adapter
return static_cast<std::size_t>(std::distance(begin, current)); return static_cast<std::size_t>(std::distance(begin, current));
} }
// number of get_character() calls so far, including those that returned
// EOF; the lexer derives its position from it (see lexer::lazy_position)
std::size_t get_read_count() const
{
return get_consumed_count() + eof_reads;
}
// number of newlines in the consumed range [0, last_index); @a line_start
// is set to the index after the last of them (0 if there is none); only
// valid when supports_seek
std::size_t count_consumed_lines(std::size_t last_index, std::size_t& line_start) const
{
const auto to = std::next(begin, static_cast<typename std::iterator_traits<IteratorType>::difference_type>(last_index));
const auto newline = char_traits<char_type>::to_char_type('\n');
using reverse_it = std::reverse_iterator<IteratorType>;
const auto last_newline = std::find(reverse_it(to), reverse_it(begin), newline);
line_start = static_cast<std::size_t>(std::distance(begin, last_newline.base()));
return static_cast<std::size_t>(std::count(begin, last_newline.base(), newline));
}
// append the already-consumed characters in the half-open range // append the already-consumed characters in the half-open range
// [first_index, last_index) to @a out; only valid when supports_seek // [first_index, last_index) to @a out; only valid when supports_seek
template<typename ContainerType> template<typename ContainerType>
@@ -8382,6 +8403,8 @@ class iterator_input_adapter
IteratorType begin; IteratorType begin;
IteratorType current; IteratorType current;
SentinelType end; SentinelType end;
/// number of get_character() calls that returned EOF
std::size_t eof_reads = 0;
template<typename BaseInputAdapter, size_t T> template<typename BaseInputAdapter, size_t T>
friend struct wide_string_input_helper; friend struct wide_string_input_helper;
@@ -8902,30 +8925,6 @@ NLOHMANN_JSON_NAMESPACE_END
#include <utility> // move #include <utility> // move
#include <vector> // vector #include <vector> // vector
// #include <nlohmann/detail/input/input_adapters.hpp>
// #include <nlohmann/detail/input/number_parse.hpp>
// __ _____ _____ _____
// __| | __| | | | JSON for Modern C++
// | | |__ | | | | | | version 3.12.0
// |_____|_____|_____|_|___| https://github.com/nlohmann/json
//
// SPDX-FileCopyrightText: 2021 The fast_float authors <https://github.com/fastfloat/fast_float>
// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann <https://nlohmann.me>
// SPDX-License-Identifier: MIT
#include <array> // array
#include <cfloat> // FLT_EVAL_METHOD
#include <clocale> // localeconv
#include <cstddef> // size_t
#include <cstdint> // int64_t, uint64_t
#include <cstdlib> // strtof, strtod, strtold
#include <cstring> // memcpy
#include <limits> // numeric_limits
#include <string> // string
// #include <nlohmann/detail/bit_ops.hpp> // #include <nlohmann/detail/bit_ops.hpp>
// __ _____ _____ _____ // __ _____ _____ _____
// __| | __| | | | JSON for Modern C++ // __| | __| | | | JSON for Modern C++
@@ -8972,6 +8971,25 @@ inline int count_leading_zeros(std::uint64_t x) noexcept
#endif #endif
} }
/// number of trailing zero bits of x (x != 0)
inline int count_trailing_zeros(std::uint64_t x) noexcept
{
#if defined(__GNUC__) || defined(__clang__)
return __builtin_ctzll(x);
#else
int n = 0;
for (int shift = 32; shift != 0; shift >>= 1)
{
if ((x << (64 - shift)) == 0)
{
n += shift;
x >>= shift;
}
}
return n;
#endif
}
/// the 128-bit product of two 64-bit numbers /// the 128-bit product of two 64-bit numbers
struct uint128_parts struct uint128_parts
{ {
@@ -9019,6 +9037,32 @@ inline std::uint64_t read_eight_bytes(const char* p) noexcept
} // namespace detail } // namespace detail
NLOHMANN_JSON_NAMESPACE_END NLOHMANN_JSON_NAMESPACE_END
// #include <nlohmann/detail/input/input_adapters.hpp>
// #include <nlohmann/detail/input/number_parse.hpp>
// __ _____ _____ _____
// __| | __| | | | JSON for Modern C++
// | | |__ | | | | | | version 3.12.0
// |_____|_____|_____|_|___| https://github.com/nlohmann/json
//
// SPDX-FileCopyrightText: 2021 The fast_float authors <https://github.com/fastfloat/fast_float>
// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann <https://nlohmann.me>
// SPDX-License-Identifier: MIT
#include <array> // array
#include <cfloat> // FLT_EVAL_METHOD
#include <clocale> // localeconv
#include <cstddef> // size_t
#include <cstdint> // int64_t, uint64_t
#include <cstdlib> // strtof, strtod, strtold
#include <cstring> // memcpy
#include <limits> // numeric_limits
#include <string> // string
// #include <nlohmann/detail/bit_ops.hpp>
// #include <nlohmann/detail/input/pow5_table.hpp> // #include <nlohmann/detail/input/pow5_table.hpp>
// __ _____ _____ _____ // __ _____ _____ _____
// __| | __| | | | JSON for Modern C++ // __| | __| | | | JSON for Modern C++
@@ -10584,6 +10628,15 @@ class lexer : public lexer_base<BasicJsonType>
static constexpr bool lazy_token_string = static constexpr bool lazy_token_string =
input_adapter_supports_seek<InputAdapterType>(is_detected<detect_supports_seek, InputAdapterType> {}); input_adapter_supports_seek<InputAdapterType>(is_detected<detect_supports_seek, InputAdapterType> {});
/// whether the read position is derived from the input adapter on demand
/// instead of being counted on every scanned character. Seekable adapters
/// know how much input they consumed and can revisit it, so
/// chars_read_total is the adapter's read count (minus a pending unget),
/// and line and column are recomputed from the consumed input only when
/// get_position() is called (errors). Counting per character kept two
/// increments and a newline check in the hot path of every get().
static constexpr bool lazy_position = lazy_token_string;
/// whether a simulated unget can be passed on to the input adapter, which /// whether a simulated unget can be passed on to the input adapter, which
/// then leaves the character in the input; see /// then leaves the character in the input; see
/// input_adapter_supports_lookahead /// input_adapter_supports_lookahead
@@ -10593,7 +10646,9 @@ class lexer : public lexer_base<BasicJsonType>
/// whether string scanning may bulk-consume runs of ordinary characters /// whether string scanning may bulk-consume runs of ordinary characters
/// directly from a contiguous input buffer (SWAR fast path). This requires /// directly from a contiguous input buffer (SWAR fast path). This requires
/// the token to be reconstructible lazily (lazy_token_string), so bypassing /// the token to be reconstructible lazily (lazy_token_string), so bypassing
/// the per-character capture in get() cannot lose error diagnostics. /// the per-character capture in get() cannot lose error diagnostics. It
/// also implies lazy_position, so the bulk paths need not advance the
/// position counters.
static constexpr bool bulk_scan = static constexpr bool bulk_scan =
lazy_token_string lazy_token_string
&& input_adapter_supports_bulk_scan<InputAdapterType>(is_detected<detect_supports_bulk_scan, InputAdapterType> {}); && input_adapter_supports_bulk_scan<InputAdapterType>(is_detected<detect_supports_bulk_scan, InputAdapterType> {});
@@ -10729,10 +10784,6 @@ class lexer : public lexer_base<BasicJsonType>
} }
token_buffer.append(reinterpret_cast<const typename string_t::value_type*>(data), pos); token_buffer.append(reinterpret_cast<const typename string_t::value_type*>(data), pos);
ia.bulk_skip(pos); ia.bulk_skip(pos);
// the run contains no newline (all bytes < 0x20 are treated as special),
// so only the flat character counters advance
position.chars_read_total += pos;
position.chars_read_current_line += pos;
} }
/// streaming input: no bulk fast path /// streaming input: no bulk fast path
@@ -11965,8 +12016,6 @@ scan_number_done:
if (JSON_HEDLEY_LIKELY(integer_result != token_type::uninitialized)) if (JSON_HEDLEY_LIKELY(integer_result != token_type::uninitialized))
{ {
ia.bulk_skip(len - 1); ia.bulk_skip(len - 1);
position.chars_read_total += (len - 1);
position.chars_read_current_line += (len - 1);
return integer_result; return integer_result;
} }
// The value does not fit an integer, so this token converts as a // The value does not fit an integer, so this token converts as a
@@ -11983,8 +12032,6 @@ scan_number_done:
decimal_point_position = dot_index; decimal_point_position = dot_index;
ia.bulk_skip(len - 1); ia.bulk_skip(len - 1);
position.chars_read_total += (len - 1);
position.chars_read_current_line += (len - 1);
return convert_number(number_type, mantissa_end); return convert_number(number_type, mantissa_end);
} }
@@ -12035,7 +12082,7 @@ scan_number_done:
#if JSON_DIAGNOSTIC_POSITIONS #if JSON_DIAGNOSTIC_POSITIONS
// the first character of the token has already been read, hence the -1 // the first character of the token has already been read, hence the -1
token_start_position = position.chars_read_total - 1; token_start_position = get_chars_read_total() - 1;
#endif #endif
note_token_start(std::integral_constant<bool, lazy_token_string> {}); note_token_start(std::integral_constant<bool, lazy_token_string> {});
@@ -12088,6 +12135,15 @@ scan_number_done:
/// per-character position counters (line-count-on-'\n' bookkeeping is /// per-character position counters (line-count-on-'\n' bookkeeping is
/// handled afterwards, in track_after_read(), once `current` is known) /// handled afterwards, in track_after_read(), once `current` is known)
void advance_position() noexcept void advance_position() noexcept
{
advance_position_impl(std::integral_constant<bool, lazy_position> {});
}
/// seekable adapter: the position is derived from the adapter on demand
void advance_position_impl(std::true_type /*lazy*/) const noexcept {}
/// streaming adapter: count the character
void advance_position_impl(std::false_type /*lazy*/) noexcept
{ {
++position.chars_read_total; ++position.chars_read_total;
++position.chars_read_current_line; ++position.chars_read_current_line;
@@ -12101,7 +12157,16 @@ scan_number_done:
// seekable adapters reconstruct the token lazily on error (see // seekable adapters reconstruct the token lazily on error (see
// get_token_string), so the eager per-character copy is skipped // get_token_string), so the eager per-character copy is skipped
capture_char(std::integral_constant<bool, lazy_token_string> {}); capture_char(std::integral_constant<bool, lazy_token_string> {});
track_newline(std::integral_constant<bool, lazy_position> {});
return current;
}
/// seekable adapter: lines are counted on demand in get_position()
void track_newline(std::true_type /*lazy*/) const noexcept {}
/// streaming adapter: start a new line after reading a newline
void track_newline(std::false_type /*lazy*/) noexcept
{
if (current == '\n') if (current == '\n')
{ {
++position.lines_read; ++position.lines_read;
@@ -12110,8 +12175,6 @@ scan_number_done:
chars_read_before_newline = position.chars_read_current_line; chars_read_before_newline = position.chars_read_current_line;
position.chars_read_current_line = 0; position.chars_read_current_line = 0;
} }
return current;
} }
/*! /*!
@@ -12155,12 +12218,22 @@ scan_number_done:
We implement unget by setting variable next_unget to true. The input is not We implement unget by setting variable next_unget to true. The input is not
changed - we just simulate ungetting by modifying chars_read_total, changed - we just simulate ungetting by modifying chars_read_total,
chars_read_current_line, and token_string. The next call to get() will chars_read_current_line, and token_string. The next call to get() will
behave as if the unget character is read again. behave as if the unget character is read again. With lazy_position, the
pending next_unget is all get_chars_read_total() needs.
*/ */
void unget() void unget()
{ {
next_unget = true; next_unget = true;
unget_position(std::integral_constant<bool, lazy_position> {});
uncapture_char(std::integral_constant<bool, lazy_token_string> {});
}
/// seekable adapter: get_chars_read_total() accounts for next_unget
void unget_position(std::true_type /*lazy*/) const noexcept {}
/// streaming adapter: step the counters back over the ungotten character
void unget_position(std::false_type /*lazy*/) noexcept
{
--position.chars_read_total; --position.chars_read_total;
// in case we "unget" a newline, we have to also decrement the lines_read // in case we "unget" a newline, we have to also decrement the lines_read
@@ -12183,8 +12256,6 @@ scan_number_done:
{ {
--position.chars_read_current_line; --position.chars_read_current_line;
} }
uncapture_char(std::integral_constant<bool, lazy_token_string> {});
} }
/// adapter without lookahead: nothing to do (see release_lookahead) /// adapter without lookahead: nothing to do (see release_lookahead)
@@ -12255,10 +12326,21 @@ scan_number_done:
// diagnostics // diagnostics
///////////////////// /////////////////////
/// return position of last read token /*!
constexpr position_t get_position() const noexcept @brief return the number of characters read so far
Unlike get_position(), this is O(1) for all adapters, so it may be called
for every value (JSON_DIAGNOSTIC_POSITIONS).
*/
std::size_t get_chars_read_total() const noexcept
{ {
return position; return get_chars_read_total_impl(std::integral_constant<bool, lazy_position> {});
}
/// return position of last read token
position_t get_position() const
{
return get_position_impl(std::integral_constant<bool, lazy_position> {});
} }
/*! /*!
@@ -12295,6 +12377,43 @@ scan_number_done:
} }
#endif #endif
private:
/// seekable adapter: every get() either read from the adapter (including
/// reads at EOF, which get() counts as characters as well) or replayed an
/// ungotten character, so the count is the adapter's read count minus a
/// still pending unget
std::size_t get_chars_read_total_impl(std::true_type /*lazy*/) const noexcept
{
return ia.get_read_count() - (next_unget ? 1u : 0u);
}
/// streaming adapter: the counter is maintained by get() and unget()
std::size_t get_chars_read_total_impl(std::false_type /*lazy*/) const noexcept
{
return position.chars_read_total;
}
/// seekable adapter: count the lines in the consumed input; O(input read
/// so far), but only called on errors
position_t get_position_impl(std::true_type /*lazy*/) const
{
position_t result;
result.chars_read_total = get_chars_read_total();
// reads at EOF count as characters but are not part of the input
const std::size_t consumed = (std::min)(result.chars_read_total, ia.get_consumed_count());
std::size_t line_start = 0;
result.lines_read = ia.count_consumed_lines(consumed, line_start);
result.chars_read_current_line = result.chars_read_total - line_start;
return result;
}
/// streaming adapter: the position is maintained by get() and unget()
position_t get_position_impl(std::false_type /*lazy*/) const noexcept
{
return position;
}
public:
/// seekable adapter: rebuild the last read token from the input on demand /// seekable adapter: rebuild the last read token from the input on demand
const std::vector<char_type>& collect_token_chars(std::vector<char_type>& out, std::true_type /*lazy*/) const const std::vector<char_type>& collect_token_chars(std::vector<char_type>& out, std::true_type /*lazy*/) const
{ {
@@ -12402,15 +12521,57 @@ scan_number_done:
// into one that reloads the pointer from memory every character // into one that reloads the pointer from memory every character
do do
{ {
if (current == '\n')
{
skip_indentation(std::integral_constant<bool, bulk_scan> {});
}
get_ignoring_pending_unget(); get_ignoring_pending_unget();
} }
while (current_is_whitespace()); while (current_is_whitespace());
} }
/*!
@brief contiguous input: skip the space indentation that follows a newline
8 bytes at a time
Pretty-printed JSON indents every line with a run of spaces, which the loop
in skip_whitespace() would otherwise read one character at a time. The
spaces are skipped 8 bytes at a time; the last 7 bytes of the input and any
other whitespace are left to that loop. Skipping bypasses get(), which is
fine because bulk_scan implies lazy_position: the position is derived from
the adapter, not counted per character.
*/
void skip_indentation(std::true_type /*bulk*/)
{
JSON_ASSERT(!next_unget);
const std::size_t remaining = ia.bulk_remaining();
if (remaining < 8)
{
return;
}
const char* const data = reinterpret_cast<const char*>(ia.bulk_data());
std::size_t pos = 0;
for (; pos + 8 <= remaining; pos += 8)
{
// little-endian, so the first byte is the lowest
const std::uint64_t non_space = read_eight_bytes(data + pos) ^ 0x2020202020202020ull;
if (non_space != 0)
{
// skip the spaces before the first other byte of this word
pos += static_cast<std::size_t>(count_trailing_zeros(non_space)) / 8;
break;
}
}
ia.bulk_skip(pos);
}
/// streaming input: no bulk fast path
void skip_indentation(std::false_type /*bulk*/) const noexcept {}
token_type scan() token_type scan()
{ {
// initially, skip the BOM // initially, skip the BOM
if (position.chars_read_total == 0 && !skip_bom()) if (get_chars_read_total() == 0 && !skip_bom())
{ {
error_message = "invalid BOM; must be 0xEF 0xBB 0xBF if given"; error_message = "invalid BOM; must be 0xEF 0xBB 0xBF if given";
return token_type::parse_error; return token_type::parse_error;
@@ -12437,7 +12598,7 @@ scan_number_done:
token_type scan_expecting(token_type expected_type) token_type scan_expecting(token_type expected_type)
{ {
JSON_ASSERT(expected_type == token_type::name_separator || expected_type == token_type::value_separator); JSON_ASSERT(expected_type == token_type::name_separator || expected_type == token_type::value_separator);
JSON_ASSERT(position.chars_read_total > 0); JSON_ASSERT(get_chars_read_total() > 0);
const char_int_type expected_char = static_cast<unsigned char>((expected_type == token_type::name_separator) ? ':' : ','); const char_int_type expected_char = static_cast<unsigned char>((expected_type == token_type::name_separator) ? ':' : ',');
skip_whitespace(); skip_whitespace();
if (JSON_HEDLEY_LIKELY(current == expected_char)) if (JSON_HEDLEY_LIKELY(current == expected_char))
@@ -12791,7 +12952,7 @@ struct diagnostic_positions
{ {
// Lexer has read the first character of the container, so // Lexer has read the first character of the container, so
// subtract 1 from the position to get the correct start position. // subtract 1 from the position to get the correct start position.
v.start_position = lexer->get_position() - 1; v.start_position = lexer->get_chars_read_total() - 1;
} }
} }
@@ -12805,7 +12966,7 @@ struct diagnostic_positions
if (lexer) if (lexer)
{ {
// Lexer's position is past the closing brace or bracket, so set that as the end position. // Lexer's position is past the closing brace or bracket, so set that as the end position.
v.end_position = lexer->get_position(); v.end_position = lexer->get_chars_read_total();
} }
} }
@@ -12821,7 +12982,7 @@ struct diagnostic_positions
// Lexer has read past the current field value, so set the end position to the current position. // Lexer has read past the current field value, so set the end position to the current position.
// The start position will be set below based on the length of the string representation // The start position will be set below based on the length of the string representation
// of the value. // of the value.
v.end_position = lexer->get_position(); v.end_position = lexer->get_chars_read_total();
switch (v.type()) switch (v.type())
{ {
@@ -18664,7 +18825,7 @@ class parser
} }
case token_type::end_of_input: case token_type::end_of_input:
{ {
if (JSON_HEDLEY_UNLIKELY(m_lexer.get_position().chars_read_total == 1)) if (JSON_HEDLEY_UNLIKELY(m_lexer.get_chars_read_total() == 1))
{ {
return syntax_error(*sax, "attempting to parse an empty input; check that your input string or stream contains the expected JSON"); return syntax_error(*sax, "attempting to parse an empty input; check that your input string or stream contains the expected JSON");
} }
@@ -18814,8 +18975,10 @@ class parser
{ {
// MSVC 2015 reports C4100 (unreferenced parameter) when SAX::parse_error is static // MSVC 2015 reports C4100 (unreferenced parameter) when SAX::parse_error is static
static_cast<void>(sax); static_cast<void>(sax);
return sax.parse_error(m_lexer.get_position(), m_lexer.get_token_string(), // computing the position is O(input read so far) for seekable input
parse_error::create(exception_id::syntax_error, m_lexer.get_position(), message, nullptr)); const position_t position = m_lexer.get_position();
return sax.parse_error(position, m_lexer.get_token_string(),
parse_error::create(exception_id::syntax_error, position, message, nullptr));
} }
std::string exception_message(const token_type expected, const std::string& context) std::string exception_message(const token_type expected, const std::string& context)
@@ -35570,6 +35733,176 @@ public:
} }
public: public:
/// @brief creates a diff as a JSON Merge Patch
/// @sa https://json.nlohmann.me/api/basic_json/merge_diff/
JSON_HEDLEY_WARN_UNUSED_RESULT
static basic_json merge_diff(const basic_json& source, const basic_json& target)
{
// unless both are objects, the patch is the target itself
if (!source.is_object() || !target.is_object())
{
return target;
}
return merge_diff_recursively(source, target, 0);
}
private:
/// @brief two objects @ref merge_diff_iteratively is diffing
struct merge_diff_frame
{
merge_diff_frame(const basic_json* source_, const basic_json* target_)
: source(source_), target(target_), member(source_->cbegin()), patch(value_t::object)
{}
// declared for GCC's -Weffc++, as in diff_frame
merge_diff_frame(const merge_diff_frame&) = default;
merge_diff_frame(merge_diff_frame&&) = default;
merge_diff_frame& operator=(const merge_diff_frame&) = default;
merge_diff_frame& operator=(merge_diff_frame&&) = default;
~merge_diff_frame() = default;
/// the objects being diffed
const basic_json* source;
const basic_json* target;
/// the member of source being diffed; advanced once it is done
const_iterator member;
/// the patch for the members of source diffed so far
basic_json patch;
};
/// @brief add the members of object @a target that @a source does not
/// have to the merge patch @a result
static void merge_diff_added_members(basic_json& result, const basic_json& source, const basic_json& target)
{
for (auto it = target.begin(); it != target.end(); ++it)
{
auto itf = source.find(it.key());
if (itf == source.end())
{
result[it.key()] = it.value();
}
}
}
/*!
@brief @ref merge_diff of two objects at nesting level @a depth
Diffing two nested objects calls this function again, once per nesting
level, so values nested deeply enough would exhaust the call stack and
terminate the process. The descent is bounded here: once @ref
detail::recursion_depth_limit levels have been entered, @ref
merge_diff_iteratively diffs what is left without the call stack.
*/
static basic_json merge_diff_recursively(const basic_json& source, const basic_json& target, const std::size_t depth)
{
JSON_ASSERT(source.is_object() && target.is_object());
if (JSON_HEDLEY_UNLIKELY(depth >= detail::recursion_depth_limit()))
{
return merge_diff_iteratively(source, target);
}
basic_json result(value_t::object);
for (auto it = source.begin(); it != source.end(); ++it)
{
auto itf = target.find(it.key());
if (itf != target.end())
{
if (it.value().is_object() && itf.value().is_object())
{
auto diff = merge_diff_recursively(it.value(), itf.value(), depth + 1);
if (!diff.empty())
{
result[it.key()] = std::move(diff);
}
}
else if (it.value() != itf.value())
{
result[it.key()] = itf.value();
}
}
else
{
result[it.key()] = value_t::null;
}
}
merge_diff_added_members(result, source, target);
return result;
}
/*!
@brief @ref merge_diff of two objects without the call stack
Produces the same patch as @ref merge_diff_recursively, keeping the objects
being diffed on an explicit stack instead of descending into them. Two
nested objects are still diffed completely before the next member, so the
patch has its members in the same order as in the recursive version. Only
reached for objects nested more deeply than @ref
detail::recursion_depth_limit.
*/
static basic_json merge_diff_iteratively(const basic_json& source, const basic_json& target)
{
// the patch of source and target, set once their frame is popped
basic_json result;
std::vector<merge_diff_frame> stack;
stack.emplace_back(&source, &target);
while (!stack.empty())
{
// the frame's members are copied out and it is changed through
// stack.back(): pushing a frame for nested objects or popping
// this one would invalidate a reference to it
const basic_json* const s = stack.back().source;
const basic_json* const t = stack.back().target;
const const_iterator it = stack.back().member;
if (it != s->cend())
{
const const_iterator itf = t->find(it.key());
if (itf != t->cend())
{
if (it.value().is_object() && itf.value().is_object())
{
// member is advanced once their patch is under its key
stack.emplace_back(&it.value(), &itf.value());
continue;
}
if (it.value() != itf.value())
{
stack.back().patch[it.key()] = itf.value();
}
}
else
{
stack.back().patch[it.key()] = value_t::null;
}
++stack.back().member;
continue;
}
merge_diff_added_members(stack.back().patch, *s, *t);
// these objects are done: continue with the ones they are in
basic_json patch = std::move(stack.back().patch);
stack.pop_back();
if (stack.empty())
{
result = std::move(patch);
}
else
{
if (!patch.empty())
{
stack.back().patch[stack.back().member.key()] = std::move(patch);
}
++stack.back().member;
}
}
return result;
}
/// @} /// @}
}; };
+4
View File
@@ -938,6 +938,10 @@ TEST_CASE("Eisel-Lemire float conversion")
const int k = i % 64; const int k = i % 64;
const std::uint64_t x = (std::uint64_t{1} << k) | (a & ((std::uint64_t{1} << k) - 1)); const std::uint64_t x = (std::uint64_t{1} << k) | (a & ((std::uint64_t{1} << k) - 1));
CHECK(nlohmann::detail::count_leading_zeros(x) == 63 - k); CHECK(nlohmann::detail::count_leading_zeros(x) == 63 - k);
// the mirror image: bit k set, arbitrary bits above it
const std::uint64_t y = (std::uint64_t{1} << k) | (k == 63 ? 0 : (a << (k + 1)));
CHECK(nlohmann::detail::count_trailing_zeros(y) == k);
} }
} }
+57
View File
@@ -8,6 +8,13 @@
#include "doctest_compatibility.h" #include "doctest_compatibility.h"
// capture whether JSON_DISABLE_ENUM_SERIALIZATION was enabled on the command
// line before including json.hpp, which #undefs it; std::byte is an enum, so
// converting std::vector<std::byte> needs enum serialization
#if defined(JSON_DISABLE_ENUM_SERIALIZATION) && (JSON_DISABLE_ENUM_SERIALIZATION == 1)
#define SKIP_TESTS_FOR_ENUM_SERIALIZATION
#endif
#include <nlohmann/json.hpp> #include <nlohmann/json.hpp>
#include <cstdint> #include <cstdint>
@@ -85,3 +92,53 @@ TEST_CASE("binary type whose value type is not std::uint8_t")
} }
#endif #endif
} }
#ifdef JSON_HAS_CPP_17
// moved from unit-regression3.cpp, so that file does not instantiate a second
// basic_json specialization (MinGW section limit, see windows.yml)
TEST_CASE("std::vector<std::byte> as binary type")
{
SECTION("issue #4804: from_cbor incompatible with std::vector<std::byte> as binary_t")
{
const std::vector<std::uint8_t> data = {0x80};
const auto decoded = byte_binary_json::from_cbor(data);
CHECK((decoded == byte_binary_json::array()));
}
#ifndef SKIP_TESTS_FOR_ENUM_SERIALIZATION
SECTION("discussion #4209 - custom BinaryType direct assignment and round-tripping")
{
// Test that assigning a custom BinaryType directly creates a binary value, not an array
const std::vector<std::byte> original{std::byte{1}, std::byte{2}, std::byte{3}};
const byte_binary_json j = original;
CHECK(j.is_binary());
CHECK(!j.is_array());
// Test round-tripping: extracting the binary value back as the custom container type
const auto extracted = j.get<std::vector<std::byte>>();
CHECK(extracted == original);
// Test that the default json alias behavior is unchanged: std::vector<uint8_t> -> array
const nlohmann::json default_json = std::vector<std::uint8_t> {1, 2, 3};
CHECK(default_json.is_array());
CHECK(!default_json.is_binary());
}
SECTION("discussion #4209 - custom BinaryType extraction from parsed array")
{
// Test that extracting a custom BinaryType from a parsed JSON array still works
// (not just from a binary-typed node)
const auto j = byte_binary_json::parse("[1,2,3]");
CHECK(j.is_array());
CHECK(!j.is_binary());
// Extracting as custom BinaryType should work from arrays
const auto extracted = j.get<std::vector<std::byte>>();
CHECK(extracted.size() == 3);
CHECK(extracted[0] == std::byte{1});
CHECK(extracted[1] == std::byte{2});
CHECK(extracted[2] == std::byte{3});
}
#endif
}
#endif
+137
View File
@@ -35,6 +35,7 @@ using nlohmann::json;
#include <sstream> #include <sstream>
#include <string> #include <string>
#include <valarray> #include <valarray>
#include <vector>
#include "sax_event_loggers.hpp" #include "sax_event_loggers.hpp"
@@ -1250,3 +1251,139 @@ TEST_CASE_TEMPLATE("deserialization of different character types (UTF-32)", T, c
CHECK(json::sax_parse(v, &l)); CHECK(json::sax_parse(v, &l));
CHECK(l.events.size() == 1); CHECK(l.events.size() == 1);
} }
namespace
{
// records the position and message of a parse error; reporting the error via
// SAX does not throw, so the test also runs with JSON_NOEXCEPTION
struct SaxErrorRecorder : public SaxEventLogger
{
bool parse_error(std::size_t position, const std::string& /*last_token*/, const json::exception& ex) override
{
error = std::to_string(position) + " " + ex.what();
return false;
}
std::string error {}; // NOLINT(readability-redundant-member-init)
};
} // namespace
TEST_CASE("parse error positions are the same for contiguous and stream input")
{
// contiguous input derives the position from the input on error, stream
// input counts it while reading (see lexer::lazy_position); the cases
// cover newlines before the error, ungetting a newline after a number,
// newlines inside a string, a comment, and reads at the end of input
struct test_case
{
std::string input;
std::size_t byte;
std::string message;
};
const std::vector<test_case> cases =
{
{"[\n 1,\n 2\n", 12, "[json.exception.parse_error.101] parse error at line 4, column 1: syntax error while parsing array - unexpected end of input; expected ']'"},
{"[1\n,2,\n]", 8, "[json.exception.parse_error.101] parse error at line 3, column 1: syntax error while parsing value - unexpected ']'; expected '[', '{', or a literal"},
{"[1\n x]", 5, "[json.exception.parse_error.101] parse error at line 2, column 2: syntax error while parsing array - invalid literal; last read: '1<U+000A> x'; expected ']'"},
{"1\n2", 3, "[json.exception.parse_error.101] parse error at line 2, column 1: syntax error while parsing value - unexpected number literal; expected end of input"},
{
"{\"a\":\n\"b\nc\"}", 9, R"([json.exception.parse_error.101] parse error at line 3, column 0: syntax error while parsing value - invalid string: control character U+000A (LF) must be escaped to \u000A or \n; last read: '"b<U+000A>')"
},
{"/* a\n b */\n x", 13, "[json.exception.parse_error.101] parse error at line 3, column 2: syntax error while parsing value - invalid literal; last read: '/* a<U+000A> b */<U+000A> x'"},
{"\n\n", 3, "[json.exception.parse_error.101] parse error at line 3, column 1: syntax error while parsing value - unexpected end of input; expected '[', '{', or a literal"},
{"[\"a\",\n 1e\n]", 10, "[json.exception.parse_error.101] parse error at line 3, column 0: syntax error while parsing value - invalid number; expected '+', '-', or digit after exponent; last read: '1e<U+000A>'"},
{"[tru\n]", 5, "[json.exception.parse_error.101] parse error at line 2, column 0: syntax error while parsing value - invalid literal; last read: '[tru<U+000A>'"},
};
for (const auto& c : cases)
{
CAPTURE(c.input)
const std::string expected = std::to_string(c.byte) + " " + c.message;
SaxErrorRecorder string_sax;
CHECK_FALSE(json::sax_parse(c.input, &string_sax, json::input_format_t::json, true, true));
CHECK(string_sax.error == expected);
SaxErrorRecorder pointer_sax;
CHECK_FALSE(json::sax_parse(c.input.data(), c.input.data() + c.input.size(), &pointer_sax, json::input_format_t::json, true, true));
CHECK(pointer_sax.error == expected);
std::istringstream is(c.input);
SaxErrorRecorder stream_sax;
CHECK_FALSE(json::sax_parse(is, &stream_sax, json::input_format_t::json, true, true));
CHECK(stream_sax.error == expected);
}
}
#if !defined(JSON_NOEXCEPTION)
TEST_CASE("indentation runs of every length around the 8-byte skip")
{
// contiguous input skips the spaces after a newline 8 bytes at a time (see
// lexer::skip_indentation), stream input reads them one by one; results,
// byte offsets, and messages must be the same for runs that end anywhere
// relative to a word boundary, including at the end of the input
const auto parse_result = [](const std::string & input, bool contiguous)
{
try
{
if (contiguous)
{
return json::parse(input.data(), input.data() + input.size(), nullptr, true, true).dump();
}
std::istringstream is(input);
return json::parse(is, nullptr, true, true).dump();
}
catch (const json::parse_error& e)
{
return std::to_string(e.byte).append(" ").append(e.what());
}
};
// replace every '~' in a pattern by an indentation run
const auto indented = [](const std::string & pattern, const std::string & indent)
{
std::string result;
for (const char c : pattern)
{
if (c == '~')
{
result += indent;
}
else
{
result += c;
}
}
return result;
};
const std::vector<std::string> patterns =
{
"[\n~1,\n~2\n]",
"[\n~1,\n~x]",
"[\n~1,\n~",
"1\n~",
"[\n~\t1,\n~\r\n~2]",
"[\n~/* c */ 1]",
"[\n~1\n~2]",
};
for (std::size_t n = 0; n <= 33; ++n)
{
const std::string indent(n, ' ');
for (const auto& pattern : patterns)
{
const std::string input = indented(pattern, indent);
CAPTURE(n)
CAPTURE(input)
const std::string expected = parse_result(input, false);
CHECK(parse_result(input, true) == expected);
CHECK(json::accept(input.data(), input.data() + input.size(), true) == json::accept(input, true));
}
}
// and against fixed expectations, in case both paths broke the same way
CHECK(parse_result(indented("[\n~x]", std::string(16, ' ')), true) == "19 [json.exception.parse_error.101] parse error at line 2, column 17: syntax error while parsing value - invalid literal; last read: '[<U+000A> x'");
CHECK(parse_result(indented("[\n~1]", std::string(17, ' ')), true) == "[1]");
}
#endif
+17 -2
View File
@@ -567,9 +567,9 @@ TEST_CASE("Regression tests for extended diagnostics")
} }
} }
TEST_CASE("Better diagnostics past the descent bound of update() and merge_patch()") TEST_CASE("Better diagnostics past the descent bound of update(), merge_patch() and merge_diff()")
{ {
// Both merge objects nested more than detail::recursion_depth_limit() // All three handle objects nested more than detail::recursion_depth_limit()
// (128) levels deep without recursing; the values they add or replace // (128) levels deep without recursing; the values they add or replace
// there must still know their parents. // there must still know their parents.
// The values are built rather than parsed, so that the expected messages // The values are built rather than parsed, so that the expected messages
@@ -615,4 +615,19 @@ TEST_CASE("Better diagnostics past the descent bound of update() and merge_patch
CHECK_THROWS_WITH_AS(p->at("x").at(0), expected_x.c_str(), json::type_error); CHECK_THROWS_WITH_AS(p->at("x").at(0), expected_x.c_str(), json::type_error);
CHECK_THROWS_WITH_AS(p->at("y").at(0), expected_y.c_str(), json::type_error); CHECK_THROWS_WITH_AS(p->at("y").at(0), expected_y.c_str(), json::type_error);
} }
SECTION("merge_diff()")
{
// target and patch differ only in their innermost members, so the
// diff of the two is nested as deeply: {"a": ... {"x": null, "y": 2}}
const json diff = json::merge_diff(target, patch); // NOLINT(readability-suspicious-call-argument)
const json* p = &diff;
for (std::size_t i = 0; i < depth; ++i)
{
p = &p->at("a");
}
CHECK(p->at("x").is_null());
CHECK_THROWS_WITH_AS(p->at("y").at(0), expected_y.c_str(), json::type_error);
}
} }
File diff suppressed because it is too large. Load diff
-48
View File
@@ -58,11 +58,6 @@ using ordered_json = nlohmann::ordered_json;
#include <optional> #include <optional>
#elif __has_include(<experimental/optional>) #elif __has_include(<experimental/optional>)
#endif #endif
/////////////////////////////////////////////////////////////////////
// for #4804
/////////////////////////////////////////////////////////////////////
using json_4804 = nlohmann::json::with_binary_t<std::vector<std::byte>>;
#endif #endif
#ifdef JSON_HAS_CPP_20 #ifdef JSON_HAS_CPP_20
@@ -458,49 +453,6 @@ TEST_CASE("regression tests 3")
#endif #endif
#ifdef JSON_HAS_CPP_17 #ifdef JSON_HAS_CPP_17
SECTION("issue #4804: from_cbor incompatible with std::vector<std::byte> as binary_t")
{
const std::vector<std::uint8_t> data = {0x80};
const auto decoded = json_4804::from_cbor(data);
CHECK((decoded == json_4804::array()));
}
#ifndef SKIP_TESTS_FOR_ENUM_SERIALIZATION
SECTION("discussion #4209 - custom BinaryType direct assignment and round-tripping")
{
// Test that assigning a custom BinaryType directly creates a binary value, not an array
const std::vector<std::byte> original{std::byte{1}, std::byte{2}, std::byte{3}};
const json_4804 j = original;
CHECK(j.is_binary());
CHECK(!j.is_array());
// Test round-tripping: extracting the binary value back as the custom container type
const auto extracted = j.get<std::vector<std::byte>>();
CHECK(extracted == original);
// Test that the default json alias behavior is unchanged: std::vector<uint8_t> -> array
const json default_json = std::vector<std::uint8_t> {1, 2, 3};
CHECK(default_json.is_array());
CHECK(!default_json.is_binary());
}
SECTION("discussion #4209 - custom BinaryType extraction from parsed array")
{
// Test that extracting a custom BinaryType from a parsed JSON array still works
// (not just from a binary-typed node)
const auto j = json_4804::parse("[1,2,3]");
CHECK(j.is_array());
CHECK(!j.is_binary());
// Extracting as custom BinaryType should work from arrays
const auto extracted = j.get<std::vector<std::byte>>();
CHECK(extracted.size() == 3);
CHECK(extracted[0] == std::byte{1});
CHECK(extracted[1] == std::byte{2});
CHECK(extracted[2] == std::byte{3});
}
#endif
SECTION("issue #5046 - implicit conversion of return json to std::optional no longer implicit") SECTION("issue #5046 - implicit conversion of return json to std::optional no longer implicit")
{ {
const json jval{}; const json jval{};