Compare commits
11
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
37cd434784 | ||
|
|
3250b5ce46 | ||
|
|
52724f5ae5 | ||
|
|
d01e6382fe | ||
|
|
589c29d69b | ||
|
|
80fd9c0fb4 | ||
|
|
169f1fcb2d | ||
|
|
020a834128 | ||
|
|
7dcff6b8cd | ||
|
|
769c5cde57 | ||
|
|
993e37e7ec |
No files matched your search
+10
-3
@@ -108,9 +108,16 @@ The tests are located in [`tests/src/unit-*.cpp`](https://github.com/nlohmann/js
|
||||
are structured along the features of the library or the nature of the tests. Usually, it should be clear from the
|
||||
context which existing file needs to be extended, and only very few cases require creating new test files.
|
||||
|
||||
When fixing a bug, edit `unit-regression3.cpp` and add a section referencing the fixed issue.
|
||||
`unit-regression2.cpp` holds the older tests; the two files exist because a single one grew large enough for the
|
||||
MinGW linker to fail relocating it, so please keep adding to the smaller file rather than growing the larger one.
|
||||
When fixing a bug, edit `unit-regression3.cpp` and add a section referencing the fixed issue. Tests that need C++17 or
|
||||
C++20 go into `unit-regression3-cpp17.cpp` or `unit-regression3-cpp20.cpp` instead, so the large file is not rebuilt
|
||||
for every C++ standard. `unit-regression2.cpp` holds the older tests; the two files exist because a single one grew
|
||||
large enough for the MinGW linker to fail relocating it, so please keep adding to the smaller file rather than growing
|
||||
the larger one.
|
||||
|
||||
To keep compile times down, several test files are compiled together as one translation unit. This sets a few rules
|
||||
for test files, such as giving file-scope helpers file-specific names. See the README in the
|
||||
[`tests`](https://github.com/nlohmann/json/tree/develop/tests) folder for how the tests are built and what to keep in
|
||||
mind when adding them.
|
||||
|
||||
#### Exceptions
|
||||
|
||||
|
||||
@@ -18,8 +18,7 @@ environment:
|
||||
matrix:
|
||||
# The Visual Studio 2017 jobs compile everything with /std:c++17, so they
|
||||
# only build the C++17 variant of each test, split into two jobs each to
|
||||
# stay below AppVeyor's 60-minute limit per job. The Visual Studio 2019
|
||||
# job builds the tests for all standards and is split for the same reason.
|
||||
# stay below AppVeyor's 60-minute limit per job.
|
||||
- APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2015
|
||||
configuration: Debug
|
||||
platform: x86
|
||||
@@ -51,13 +50,7 @@ environment:
|
||||
configuration: Release
|
||||
platform: x86
|
||||
CXX_FLAGS: "/W4 /WX"
|
||||
CMAKE_OPTIONS: "-DJSON_ImplicitConversions=OFF -DJSON_TestShard=0/2"
|
||||
GENERATOR: Visual Studio 16 2019
|
||||
- APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2019
|
||||
configuration: Release
|
||||
platform: x86
|
||||
CXX_FLAGS: "/W4 /WX"
|
||||
CMAKE_OPTIONS: "-DJSON_ImplicitConversions=OFF -DJSON_TestShard=1/2"
|
||||
CMAKE_OPTIONS: "-DJSON_ImplicitConversions=OFF"
|
||||
GENERATOR: Visual Studio 16 2019
|
||||
|
||||
- APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2015
|
||||
|
||||
@@ -64,7 +64,6 @@ labels:
|
||||
- "tests/src/unit-json_view.*"
|
||||
- "tests/src/json_view_test_helpers\\.hpp"
|
||||
- "tests/src/fuzzer-(parse_json_view|json_view_image)\\.cpp"
|
||||
- "tests/benchmarks/json_view/.*"
|
||||
- "tools/amalgamate/config_json_view\\.json"
|
||||
- "docs/mkdocs/docs/features/json_view\\.md"
|
||||
- "docs/mkdocs/docs/api/basic_json_(document|view)/.*"
|
||||
|
||||
@@ -154,28 +154,6 @@ jobs:
|
||||
# PR content in a job without credentials.
|
||||
python3 tools/generate_natvis/generate_natvis.py $MAIN_DIR
|
||||
|
||||
- name: Check the nlohmann_json.natvis generator options
|
||||
run: |
|
||||
cd $MAIN_DIR
|
||||
# The options for configurations outside the checked-in file write
|
||||
# to a temporary file, so they do not affect the diff below.
|
||||
GENERATOR=tools/generate_natvis/generate_natvis.py
|
||||
python3 $GENERATOR --list-tags
|
||||
TAG_COUNT=$(python3 $GENERATOR --list-tags | wc -l)
|
||||
|
||||
python3 $GENERATOR --abi-tags diag,_dp,_vns --abi-tags _diag --output $RUNNER_TEMP/abi_tags.natvis
|
||||
grep -q '<!-- Namespace nlohmann::json_abi_diag_dp_vns -->' $RUNNER_TEMP/abi_tags.natvis
|
||||
grep -q '<!-- Namespace nlohmann::json_abi_diag_dp_vns_v[0-9_]* -->' $RUNNER_TEMP/abi_tags.natvis
|
||||
test $(grep -c '<!-- Namespace ' $RUNNER_TEMP/abi_tags.natvis) -eq $(( $(grep -c '<!-- Namespace ' nlohmann_json.natvis) + 2 ))
|
||||
|
||||
python3 $GENERATOR --all --output $RUNNER_TEMP/all.natvis
|
||||
test $(grep -c '<!-- Namespace ' $RUNNER_TEMP/all.natvis) -eq $(( (1 << TAG_COUNT) * 2 + 1 ))
|
||||
|
||||
if python3 $GENERATOR --abi-tags _unknown --output $RUNNER_TEMP/unknown.natvis; then
|
||||
echo "generate_natvis.py accepted an unknown ABI tag"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Build patch and check for differences
|
||||
id: diff
|
||||
run: |
|
||||
|
||||
@@ -1,84 +0,0 @@
|
||||
name: "json_view benchmarks"
|
||||
|
||||
# On demand only: runs the comparison of json_view with yyjson, simdjson, and
|
||||
# Boost.JSON (tests/benchmarks/json_view/compare.py) on GitHub-hosted runners,
|
||||
# for numbers from x86-64 and AArch64 Linux. It runs when started by hand, or
|
||||
# when a pull request gets the label "benchmark" (on both architectures, with
|
||||
# GCC and the default settings). Shared runners are noisy: the results show
|
||||
# where json_view stands, but published numbers need a quiet machine (see
|
||||
# tests/benchmarks/json_view/README.md).
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [labeled]
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
runner:
|
||||
description: "Runner image"
|
||||
type: choice
|
||||
options:
|
||||
- ubuntu-24.04
|
||||
- ubuntu-24.04-arm
|
||||
default: ubuntu-24.04
|
||||
compiler:
|
||||
description: "Compiler"
|
||||
type: choice
|
||||
options:
|
||||
- g++
|
||||
- clang++
|
||||
default: g++
|
||||
native:
|
||||
description: "Compile for the runner's CPU (-march=native)"
|
||||
type: boolean
|
||||
default: false
|
||||
rounds:
|
||||
description: "Rounds of bench_view"
|
||||
type: number
|
||||
default: 30
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
# (the label is part of the group: adding an unrelated label must not cancel a running benchmark)
|
||||
group: json-view-benchmarks-${{ github.workflow }}-${{ github.ref }}-${{ github.event.label.name }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
compare:
|
||||
if: github.event_name == 'workflow_dispatch' || github.event.label.name == 'benchmark'
|
||||
strategy:
|
||||
matrix:
|
||||
runner: ${{ fromJSON(github.event_name == 'workflow_dispatch' && format('["{0}"]', inputs.runner) || '["ubuntu-24.04", "ubuntu-24.04-arm"]') }}
|
||||
runs-on: ${{ matrix.runner }}
|
||||
timeout-minutes: 60
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@e14015d583714f6e62063499dc959a02595150a1 # v2.21.1
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Download test data
|
||||
run: |
|
||||
cmake -S . -B build -DJSON_BuildTests=On
|
||||
cmake --build build --target download_test_data
|
||||
|
||||
- name: Run the comparison
|
||||
env:
|
||||
CXX: ${{ inputs.compiler || 'g++' }}
|
||||
CC: ${{ inputs.compiler == 'clang++' && 'clang' || 'gcc' }}
|
||||
ROUNDS: ${{ inputs.rounds || 30 }}
|
||||
NATIVE: ${{ inputs.native && '--native' || '' }}
|
||||
run: python3 tests/benchmarks/json_view/compare.py --data build/test_files --download --rounds "$ROUNDS" $NATIVE
|
||||
|
||||
- name: Summary
|
||||
run: cat tests/benchmarks/json_view/results/*.md >> "$GITHUB_STEP_SUMMARY"
|
||||
|
||||
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: json_view-benchmarks-${{ matrix.runner }}-${{ inputs.compiler || 'g++' }}
|
||||
path: tests/benchmarks/json_view/results/
|
||||
@@ -37,7 +37,6 @@ cc_library(
|
||||
"include/nlohmann/detail/input/parser.hpp",
|
||||
"include/nlohmann/detail/input/position_t.hpp",
|
||||
"include/nlohmann/detail/input/pow5_table.hpp",
|
||||
"include/nlohmann/detail/input/simd_scan.hpp",
|
||||
"include/nlohmann/detail/input/string_scan.hpp",
|
||||
"include/nlohmann/detail/iterators/internal_iterator.hpp",
|
||||
"include/nlohmann/detail/iterators/iter_impl.hpp",
|
||||
|
||||
@@ -6,7 +6,6 @@ This file describes the source for supporting files; that is, files that are not
|
||||
- [GitHub](#github)
|
||||
- [REUSE](#reuse)
|
||||
- [Package Managers](#package-managers)
|
||||
- [Debugging](#debugging)
|
||||
|
||||
## Continuous Integration
|
||||
|
||||
@@ -290,29 +289,3 @@ The module definition for [Bazel](https://bazel.build)'s [Bzlmod](https://bazel.
|
||||
Further documentation:
|
||||
|
||||
- [Bazel modules](https://bazel.build/external/module)
|
||||
|
||||
## Debugging
|
||||
|
||||
### `nlohmann_json.natvis`
|
||||
|
||||
The [Natvis](https://learn.microsoft.com/en-us/visualstudio/debugger/create-custom-views-of-native-objects) file that
|
||||
gives `json` values a friendly view in the Visual Studio debugger. The file is generated by
|
||||
`tools/generate_natvis/generate_natvis.py` from the template `tools/generate_natvis/nlohmann_json.natvis.j2` and the ABI
|
||||
tags and version in `include/nlohmann/detail/abi_macros.hpp`; change the script or the template rather than editing
|
||||
the file by hand. The file can be updated by calling
|
||||
|
||||
```shell
|
||||
make natvis
|
||||
```
|
||||
|
||||
The "Check amalgamation" workflow fails if the file is out of date.
|
||||
|
||||
The file covers the namespace variants with up to two ABI tags. A Natvis file for other configurations can be
|
||||
generated with the `--abi-tags` or `--all` options, for example
|
||||
`make natvis NATVIS_ARGS="--abi-tags _diag,_dp,_vns --output my_nlohmann_json.natvis"`; see
|
||||
[`tools/generate_natvis`](tools/generate_natvis/README.md). With MSVC, CMake adds the file to the `nlohmann_json`
|
||||
target and installs it.
|
||||
|
||||
Further documentation:
|
||||
|
||||
- [Create custom views of C++ objects in the debugger using the Natvis framework](https://learn.microsoft.com/en-us/visualstudio/debugger/create-custom-views-of-native-objects)
|
||||
@@ -46,7 +46,7 @@ all:
|
||||
@echo "clean - remove built files"
|
||||
@echo "(fuzzers) - see tests/fuzzing.md, or run 'make -C tests fuzzers' to build the AFL fuzzers"
|
||||
@echo "macro_builder_check - check that macro_scope.hpp matches tools/macro_builder's output"
|
||||
@echo "natvis - regenerate nlohmann_json.natvis from the current ABI tags and version (pass generator options via NATVIS_ARGS, see tools/generate_natvis/README.md)"
|
||||
@echo "natvis - regenerate nlohmann_json.natvis from the current ABI tags and version"
|
||||
@echo "pretty - beautify code with Artistic Style"
|
||||
@echo "pvs_studio - run the PVS-Studio static analyzer and open the report"
|
||||
@echo "release - create the release files (include.zip, json.tar.xz, single headers, signatures, hashes)"
|
||||
@@ -120,15 +120,13 @@ $(AMALGAMATED_VIEW_FILE): $(SRCS)
|
||||
tools/amalgamate/amalgamate.py -c tools/amalgamate/config_json_view.json -s . --verbose=yes
|
||||
|
||||
NATVIS_VENV=tools/generate_natvis/venv
|
||||
# extra options for generate_natvis.py, e.g. NATVIS_ARGS="--abi-tags _diag,_dp,_vns --output my.natvis"
|
||||
NATVIS_ARGS=
|
||||
|
||||
# regenerate nlohmann_json.natvis from the ABI tags and version in include/nlohmann/detail/abi_macros.hpp
|
||||
# (the dependencies are installed into a virtual environment first)
|
||||
natvis:
|
||||
@test -d $(NATVIS_VENV) || python3 -mvenv $(NATVIS_VENV)
|
||||
@$(NATVIS_VENV)/bin/python3 -c 'import jinja2' 2>/dev/null || $(NATVIS_VENV)/bin/pip3 install --quiet -r tools/generate_natvis/requirements.txt
|
||||
$(NATVIS_VENV)/bin/python3 tools/generate_natvis/generate_natvis.py $(NATVIS_ARGS) .
|
||||
$(NATVIS_VENV)/bin/python3 tools/generate_natvis/generate_natvis.py .
|
||||
|
||||
# regenerate the two tools/macro_builder blocks of $(MACRO_SCOPE_HPP) (see its README.md) and diff against the
|
||||
# checked-in header; phony, because it never writes $(MACRO_SCOPE_HPP) itself
|
||||
|
||||
+3
-3
@@ -890,11 +890,11 @@ add_custom_target(ci_icpx
|
||||
# The following tests are excluded as they trigger known nvc++ 25.5 defects (not
|
||||
# library bugs); see https://github.com/nlohmann/json for tracking. Only the
|
||||
# affected language-standard variants are excluded so coverage is otherwise kept:
|
||||
# - test-comparison_cpp20, test-comparison_legacy_cpp20
|
||||
# - test-comparison-cpp20_cpp20, test-comparison_legacy-cpp20_cpp20
|
||||
# miscompiles cross-type/<=> comparison (e.g. `-17 <= null`)
|
||||
# - test-constructor1_cpp11
|
||||
# std::initializer_list lifetime bug -> SIGSEGV
|
||||
# - test-deserialization_cpp20
|
||||
# - test-deserialization-cpp20_cpp20
|
||||
# mangles the UTF-8 u8"" string literal in the char8_t (C++20) section
|
||||
add_custom_target(ci_nvhpc
|
||||
COMMAND ${CMAKE_COMMAND}
|
||||
@@ -906,7 +906,7 @@ add_custom_target(ci_nvhpc
|
||||
COMMAND ${CMAKE_COMMAND} --build ${PROJECT_BINARY_DIR}/build_nvhpc
|
||||
# the pipes are escaped so the surrounding shell passes them to ctest verbatim
|
||||
# instead of treating them as shell pipe operators
|
||||
COMMAND cd ${PROJECT_BINARY_DIR}/build_nvhpc && ${CMAKE_CTEST_COMMAND} --parallel ${N} --exclude-regex "test-comparison_cpp20\\|test-comparison_legacy_cpp20\\|test-constructor1_cpp11\\|test-deserialization_cpp20" --output-on-failure
|
||||
COMMAND cd ${PROJECT_BINARY_DIR}/build_nvhpc && ${CMAKE_CTEST_COMMAND} --parallel ${N} --exclude-regex "test-comparison-cpp20_cpp20\\|test-comparison_legacy-cpp20_cpp20\\|test-constructor1_cpp11\\|test-deserialization-cpp20_cpp20" --output-on-failure
|
||||
COMMENT "Compile and test with NVIDIA HPC SDK (nvc++)"
|
||||
)
|
||||
|
||||
|
||||
+366
-39
@@ -109,6 +109,55 @@ function(_json_test_apply_test_properties test_target properties_target)
|
||||
endif()
|
||||
endfunction()
|
||||
|
||||
# for internal use by _json_test_add_test() and _json_test_add_unity_batch():
|
||||
# registers the CTest test <test_name>_cpp<cxx_standard> (plus its Valgrind
|
||||
# variant), which runs the executable target <test_target> with the arguments
|
||||
# in ARGN, and applies the test properties of the test- and standard-specific
|
||||
# interface targets
|
||||
function(_json_test_register_test test_name test_target cxx_standard)
|
||||
set(ctest_name ${test_name}_cpp${cxx_standard})
|
||||
|
||||
if (JSON_FastTests)
|
||||
add_test(NAME ${ctest_name}
|
||||
COMMAND ${test_target} ${DOCTEST_TEST_FILTER} ${ARGN}
|
||||
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
|
||||
)
|
||||
else()
|
||||
add_test(NAME ${ctest_name}
|
||||
COMMAND ${test_target} ${DOCTEST_TEST_FILTER} ${ARGN} --no-skip
|
||||
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
|
||||
)
|
||||
endif()
|
||||
set_tests_properties(${ctest_name} PROPERTIES LABELS "all" FIXTURES_REQUIRED TEST_DATA)
|
||||
|
||||
# apply standard-specific test properties
|
||||
if(TARGET _json_test_interface__cpp_${cxx_standard})
|
||||
_json_test_apply_test_properties(${ctest_name} _json_test_interface__cpp_${cxx_standard})
|
||||
endif()
|
||||
|
||||
# apply test-specific test properties
|
||||
if(TARGET _json_test_interface_${test_name})
|
||||
_json_test_apply_test_properties(${ctest_name} _json_test_interface_${test_name})
|
||||
endif()
|
||||
|
||||
# apply test- and standard-specific test properties
|
||||
if(TARGET _json_test_interface_${test_name}_cpp_${cxx_standard})
|
||||
_json_test_apply_test_properties(${ctest_name}
|
||||
_json_test_interface_${test_name}_cpp_${cxx_standard}
|
||||
)
|
||||
endif()
|
||||
|
||||
if(JSON_Valgrind)
|
||||
add_test(NAME ${ctest_name}_valgrind
|
||||
COMMAND ${memcheck_command} $<TARGET_FILE:${test_target}> ${DOCTEST_TEST_FILTER} ${ARGN}
|
||||
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
|
||||
)
|
||||
set_tests_properties(${ctest_name}_valgrind PROPERTIES
|
||||
LABELS "valgrind" FIXTURES_REQUIRED TEST_DATA
|
||||
)
|
||||
endif()
|
||||
endfunction()
|
||||
|
||||
# for internal use by json_test_add_test_for()
|
||||
function(_json_test_add_test test_name file main cxx_standard)
|
||||
set(test_target ${test_name}_cpp${cxx_standard})
|
||||
@@ -143,45 +192,7 @@ function(_json_test_add_test test_name file main cxx_standard)
|
||||
)
|
||||
endif()
|
||||
|
||||
if (JSON_FastTests)
|
||||
add_test(NAME ${test_target}
|
||||
COMMAND ${test_target} ${DOCTEST_TEST_FILTER}
|
||||
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
|
||||
)
|
||||
else()
|
||||
add_test(NAME ${test_target}
|
||||
COMMAND ${test_target} ${DOCTEST_TEST_FILTER} --no-skip
|
||||
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
|
||||
)
|
||||
endif()
|
||||
set_tests_properties(${test_target} PROPERTIES LABELS "all" FIXTURES_REQUIRED TEST_DATA)
|
||||
|
||||
# apply standard-specific test properties
|
||||
if(TARGET _json_test_interface__cpp_${cxx_standard})
|
||||
_json_test_apply_test_properties(${test_target} _json_test_interface__cpp_${cxx_standard})
|
||||
endif()
|
||||
|
||||
# apply test-specific test properties
|
||||
if(TARGET _json_test_interface_${test_name})
|
||||
_json_test_apply_test_properties(${test_target} _json_test_interface_${test_name})
|
||||
endif()
|
||||
|
||||
# apply test- and standard-specific test properties
|
||||
if(TARGET _json_test_interface_${test_name}_cpp_${cxx_standard})
|
||||
_json_test_apply_test_properties(${test_target}
|
||||
_json_test_interface_${test_name}_cpp_${cxx_standard}
|
||||
)
|
||||
endif()
|
||||
|
||||
if(JSON_Valgrind)
|
||||
add_test(NAME ${test_target}_valgrind
|
||||
COMMAND ${memcheck_command} $<TARGET_FILE:${test_target}> ${DOCTEST_TEST_FILTER}
|
||||
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
|
||||
)
|
||||
set_tests_properties(${test_target}_valgrind PROPERTIES
|
||||
LABELS "valgrind" FIXTURES_REQUIRED TEST_DATA
|
||||
)
|
||||
endif()
|
||||
_json_test_register_test(${test_name} ${test_target} ${cxx_standard})
|
||||
endfunction()
|
||||
|
||||
#############################################################################
|
||||
@@ -200,6 +211,14 @@ endfunction()
|
||||
# Use NAME <name> to override the filename-derived test name.
|
||||
# Use FORCE to create the test regardless of the file containing
|
||||
# JSON_HAS_CPP_<version_number>.
|
||||
#
|
||||
# Tests that depend on the C++ standard (e.g., because they use the macros
|
||||
# JSON_HAS_FILESYSTEM, JSON_HAS_RANGES, or JSON_HAS_THREE_WAY_COMPARISON)
|
||||
# should not make the whole of a large unit-foo.cpp be rebuilt for every
|
||||
# standard. Put them into a separate file unit-foo-cpp<NN>.cpp (see, e.g.,
|
||||
# unit-items-cpp17.cpp) which wraps its content in #ifdef JSON_HAS_CPP_<NN>.
|
||||
# Then, unit-foo.cpp itself contains no JSON_HAS_CPP_<NN> and is only built for
|
||||
# C++11.
|
||||
# Test targets are linked against <main>.
|
||||
# CXX_STANDARDS defaults to "11".
|
||||
#############################################################################
|
||||
@@ -243,6 +262,314 @@ function(json_test_add_test_for file)
|
||||
endforeach()
|
||||
endfunction()
|
||||
|
||||
# for internal use by json_test_add_unity_tests(): sets <result> to whether the
|
||||
# (absolute) <file> is built for <cxx_standard>; same rule as in
|
||||
# json_test_add_test_for(): C++11 always, others only if the file contains
|
||||
# JSON_HAS_CPP_<cxx_standard> or <force> is set
|
||||
function(_json_test_unity_applies file cxx_standard force result)
|
||||
set(${result} TRUE PARENT_SCOPE)
|
||||
if(NOT ("${cxx_standard}" STREQUAL 11 OR force))
|
||||
file(READ ${file} file_content)
|
||||
string(FIND "${file_content}" JSON_HAS_CPP_${cxx_standard} has_cpp_found)
|
||||
if(${has_cpp_found} EQUAL -1)
|
||||
set(${result} FALSE PARENT_SCOPE)
|
||||
endif()
|
||||
endif()
|
||||
endfunction()
|
||||
|
||||
# for internal use by json_test_add_unity_tests(): creates the executable
|
||||
# test-unity-<batch_name>_cpp<cxx_standard> from the (absolute) source files
|
||||
# in ARGN, which are #include-d by a generated source file, and registers one
|
||||
# CTest test per source file that runs only the test cases of that file; if
|
||||
# <private> is true, the generated file defines JSON_TESTS_PRIVATE first
|
||||
function(_json_test_add_unity_batch batch_name cxx_standard main private)
|
||||
set(batch_target test-unity-${batch_name}_cpp${cxx_standard})
|
||||
set(batch_source ${PROJECT_BINARY_DIR}/tests/unity/${batch_target}.cpp)
|
||||
|
||||
set(batch_content "// generated by cmake/test.cmake; do not edit\n")
|
||||
if(private)
|
||||
string(APPEND batch_content "// at least one file of this batch needs access to private members of the library\n")
|
||||
string(APPEND batch_content "#define JSON_TESTS_PRIVATE\n")
|
||||
# the files define the macro again; mark this definition as used, as
|
||||
# -Wunused-macros reports an unused definition when it is redefined
|
||||
string(APPEND batch_content "#ifdef JSON_TESTS_PRIVATE\n#endif\n")
|
||||
endif()
|
||||
foreach(file ${ARGN})
|
||||
string(APPEND batch_content "#include \"${file}\" // NOLINT(bugprone-suspicious-include)\n")
|
||||
endforeach()
|
||||
|
||||
# only touch the generated file if it changed to keep incremental builds incremental
|
||||
set(old_content "")
|
||||
if(EXISTS ${batch_source})
|
||||
file(READ ${batch_source} old_content)
|
||||
endif()
|
||||
if(NOT "${old_content}" STREQUAL "${batch_content}")
|
||||
file(WRITE ${batch_source} "${batch_content}")
|
||||
endif()
|
||||
|
||||
add_executable(${batch_target} ${batch_source})
|
||||
target_link_libraries(${batch_target} PRIVATE ${main})
|
||||
set_target_properties(${batch_target} PROPERTIES
|
||||
CXX_STANDARD ${cxx_standard}
|
||||
CXX_STANDARD_REQUIRED ON
|
||||
)
|
||||
if(TARGET _json_test_interface__cpp_${cxx_standard})
|
||||
target_link_libraries(${batch_target} PRIVATE _json_test_interface__cpp_${cxx_standard})
|
||||
endif()
|
||||
|
||||
# rebuild the batch when one of its files changes, and show the files in IDEs;
|
||||
# files that are also built standalone (VARIANT_FILES) are left out, as
|
||||
# HEADER_FILE_ONLY is a per-file property and would also affect those targets
|
||||
set_source_files_properties(${batch_source} PROPERTIES OBJECT_DEPENDS "${ARGN}")
|
||||
foreach(file ${ARGN})
|
||||
if(NOT file IN_LIST _json_test_unity_variant_files)
|
||||
set_source_files_properties(${file} PROPERTIES HEADER_FILE_ONLY ON)
|
||||
target_sources(${batch_target} PRIVATE ${file})
|
||||
endif()
|
||||
endforeach()
|
||||
|
||||
foreach(file ${ARGN})
|
||||
get_filename_component(file_basename ${file} NAME_WE)
|
||||
string(REGEX REPLACE "unit-(.+)" "test-\\1" test_name ${file_basename})
|
||||
|
||||
# run only the test cases defined in this file (and in the shared
|
||||
# make_test_data_available.hpp if the file uses it)
|
||||
file(READ ${file} file_content)
|
||||
set(source_filter "--source-file=*${file_basename}.cpp")
|
||||
string(FIND "${file_content}" make_test_data_available.hpp uses_test_data)
|
||||
if(NOT ${uses_test_data} EQUAL -1)
|
||||
string(APPEND source_filter ",*make_test_data_available.hpp")
|
||||
endif()
|
||||
|
||||
_json_test_register_test(${test_name} ${batch_target} ${cxx_standard} "${source_filter}")
|
||||
endforeach()
|
||||
endfunction()
|
||||
|
||||
#############################################################################
|
||||
# json_test_add_unity_tests(
|
||||
# FILES <files>...
|
||||
# MAIN <main>
|
||||
# [CXX_STANDARDS <version_number>...] [FORCE]
|
||||
# [BATCH_SIZE <size>]
|
||||
# [GROUPS <group>...]
|
||||
# [VARIANT_FILES <files>...])
|
||||
#
|
||||
# Like calling json_test_add_test_for(<file> MAIN <main> ...) for each of the
|
||||
# <files>, but compiles several files together to speed up the build: for each
|
||||
# C++ standard, the files are split into batches of <size> files (default: 8)
|
||||
# and each batch is built as a single executable
|
||||
#
|
||||
# test-unity-<pool><index>_cpp<version_number>
|
||||
#
|
||||
# whose generated source file #include-s the files of the batch (so they share
|
||||
# the template instantiations of the library). The tests are still named
|
||||
# test-foo_cpp<version_number>, one per file, but run the batch executable with
|
||||
# a doctest filter that selects the test cases of that file only.
|
||||
#
|
||||
# Files are only batched with files that agree on the macros defined before
|
||||
# the library is included: files that define at most JSON_TESTS_PRIVATE (or
|
||||
# macros derived from global compile definitions) form the pools "plain" and
|
||||
# "private". All other files are added with json_test_add_test_for() as usual,
|
||||
# as are files with test-specific build settings (see
|
||||
# json_test_set_test_options()) and the files listed in the explicit
|
||||
# exclusion list below.
|
||||
# Each <group> names a list variable json_test_unity_group_<group> of test file
|
||||
# stems (file names without "unit-" and ".cpp"). The batchable files of a group
|
||||
# are compiled together (regardless of BATCH_SIZE) as one executable
|
||||
#
|
||||
# test-unity-<group>_cpp<version_number>
|
||||
#
|
||||
# so that related tests, which instantiate the same templates, share one
|
||||
# translation unit. A group may mix the pools "plain" and "private"; if any of
|
||||
# its files needs JSON_TESTS_PRIVATE, the whole group is built with it. Files
|
||||
# that cannot be batched stay standalone even if they are listed in a group.
|
||||
# Files in no group are batched by BATCH_SIZE as described above.
|
||||
# <files> in VARIANT_FILES are also built standalone with other settings, so
|
||||
# they are not marked as header-only sources of the batch.
|
||||
#############################################################################
|
||||
|
||||
function(json_test_add_unity_tests)
|
||||
cmake_parse_arguments(args "FORCE" "MAIN;BATCH_SIZE" "FILES;CXX_STANDARDS;VARIANT_FILES;GROUPS" ${ARGN})
|
||||
|
||||
if("${args_MAIN}" STREQUAL "")
|
||||
message(FATAL_ERROR "Required argument MAIN <main> missing.")
|
||||
endif()
|
||||
|
||||
if("${args_BATCH_SIZE}" STREQUAL "")
|
||||
set(args_BATCH_SIZE 8)
|
||||
endif()
|
||||
|
||||
if("${args_CXX_STANDARDS}" STREQUAL "")
|
||||
set(args_CXX_STANDARDS 11)
|
||||
endif()
|
||||
|
||||
if(args_FORCE)
|
||||
set(force FORCE)
|
||||
else()
|
||||
set(force "")
|
||||
endif()
|
||||
|
||||
set(_json_test_unity_variant_files "")
|
||||
foreach(file ${args_VARIANT_FILES})
|
||||
get_filename_component(file ${file} ABSOLUTE)
|
||||
list(APPEND _json_test_unity_variant_files ${file})
|
||||
endforeach()
|
||||
|
||||
# files that must not be merged into a batch: unit-32bit.cpp is only built
|
||||
# for 32bit targets, and unit-no-macro-leak.cpp checks that including the
|
||||
# library defines no unprefixed macro, which any other file would disturb
|
||||
set(standalone_files unit-32bit.cpp unit-no-macro-leak.cpp)
|
||||
|
||||
set(harmless_macros "^(DOCTEST_.*|SKIP_TESTS_FOR_.*|JSON_TEST_DEPRECATED_FUNCTIONS_DELETED|JSON_TEST_STRICT_NUL_HANDLING_ENABLED|JSON_TEST_STRINGIZE)$")
|
||||
|
||||
# classify the files
|
||||
set(plain_files "")
|
||||
set(private_files "")
|
||||
set(standalone_abs_files "")
|
||||
foreach(file ${args_FILES})
|
||||
get_filename_component(file_name ${file} NAME)
|
||||
get_filename_component(file_basename ${file} NAME_WE)
|
||||
string(REGEX REPLACE "unit-(.+)" "test-\\1" test_name ${file_basename})
|
||||
|
||||
set(batchable TRUE)
|
||||
if(file_name IN_LIST standalone_files)
|
||||
set(batchable FALSE)
|
||||
endif()
|
||||
if(TARGET _json_test_interface_${test_name})
|
||||
set(batchable FALSE)
|
||||
endif()
|
||||
foreach(cxx_standard ${args_CXX_STANDARDS})
|
||||
if(TARGET _json_test_interface_${test_name}_cpp_${cxx_standard})
|
||||
set(batchable FALSE)
|
||||
endif()
|
||||
endforeach()
|
||||
|
||||
# files that suppress GCC's -Wnoexcept around their include of the
|
||||
# library: the suppression has no effect once another file of the
|
||||
# batch included the library first
|
||||
if(batchable)
|
||||
file(READ ${file} file_content)
|
||||
string(FIND "${file_content}" "SUPPRESS_WARNING(\"-Wnoexcept\")" suppresses_noexcept)
|
||||
if(NOT ${suppresses_noexcept} EQUAL -1)
|
||||
set(batchable FALSE)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
set(pool plain)
|
||||
if(batchable)
|
||||
# collect the macros (un)defined before the library is included
|
||||
file(READ ${file} file_content)
|
||||
string(FIND "${file_content}" "#include <nlohmann/" include_position)
|
||||
if(NOT ${include_position} EQUAL -1)
|
||||
string(SUBSTRING "${file_content}" 0 ${include_position} file_content)
|
||||
endif()
|
||||
string(REGEX MATCHALL "(^|\n)[ \t]*#[ \t]*(define|undef)[ \t]+[A-Za-z_0-9]+" directives "${file_content}")
|
||||
foreach(directive ${directives})
|
||||
string(REGEX REPLACE "^.*[ \t]([A-Za-z_0-9]+)$" "\\1" macro "${directive}")
|
||||
if(macro MATCHES "${harmless_macros}")
|
||||
continue()
|
||||
elseif("${macro}" STREQUAL JSON_TESTS_PRIVATE)
|
||||
set(pool private)
|
||||
else()
|
||||
set(batchable FALSE)
|
||||
endif()
|
||||
endforeach()
|
||||
endif()
|
||||
|
||||
get_filename_component(file_abs ${file} ABSOLUTE)
|
||||
if(NOT batchable)
|
||||
list(APPEND standalone_abs_files ${file_abs})
|
||||
json_test_add_test_for(${file} MAIN ${args_MAIN} CXX_STANDARDS ${args_CXX_STANDARDS} ${force})
|
||||
continue()
|
||||
endif()
|
||||
get_filename_component(file ${file} ABSOLUTE)
|
||||
list(APPEND ${pool}_files ${file})
|
||||
endforeach()
|
||||
|
||||
# resolve the explicit groups: group_<name>_files are the (absolute) files
|
||||
# of the group, group_<name>_private tells whether one of them is private
|
||||
set(grouped_files "")
|
||||
foreach(group ${args_GROUPS})
|
||||
if(NOT DEFINED json_test_unity_group_${group})
|
||||
message(FATAL_ERROR "Unity test group '${group}' is not defined (json_test_unity_group_${group}).")
|
||||
endif()
|
||||
set(group_${group}_files "")
|
||||
set(group_${group}_private FALSE)
|
||||
foreach(stem ${json_test_unity_group_${group}})
|
||||
# check against the source directory, because FILES may be filtered (JSON_TestShard)
|
||||
get_filename_component(file ${CMAKE_CURRENT_SOURCE_DIR}/src/unit-${stem}.cpp ABSOLUTE)
|
||||
if(NOT EXISTS ${file})
|
||||
message(FATAL_ERROR "Unity test group '${group}' lists '${stem}', but ${file} does not exist.")
|
||||
endif()
|
||||
if(file IN_LIST grouped_files)
|
||||
message(FATAL_ERROR "Unity test file unit-${stem}.cpp is listed in more than one group (second: '${group}').")
|
||||
endif()
|
||||
list(APPEND grouped_files ${file})
|
||||
|
||||
if(file IN_LIST standalone_abs_files)
|
||||
message(STATUS "Unity test group '${group}': unit-${stem}.cpp cannot be batched and stays standalone")
|
||||
elseif(file IN_LIST plain_files)
|
||||
list(APPEND group_${group}_files ${file})
|
||||
list(REMOVE_ITEM plain_files ${file})
|
||||
elseif(file IN_LIST private_files)
|
||||
list(APPEND group_${group}_files ${file})
|
||||
list(REMOVE_ITEM private_files ${file})
|
||||
set(group_${group}_private TRUE)
|
||||
endif()
|
||||
endforeach()
|
||||
endforeach()
|
||||
|
||||
foreach(cxx_standard ${args_CXX_STANDARDS})
|
||||
if(NOT compiler_supports_cpp_${cxx_standard})
|
||||
continue()
|
||||
endif()
|
||||
|
||||
# explicit groups: one batch per group
|
||||
foreach(group ${args_GROUPS})
|
||||
set(batch_files "")
|
||||
foreach(file ${group_${group}_files})
|
||||
_json_test_unity_applies(${file} ${cxx_standard} "${force}" applies)
|
||||
if(applies)
|
||||
list(APPEND batch_files ${file})
|
||||
endif()
|
||||
endforeach()
|
||||
if(batch_files)
|
||||
_json_test_add_unity_batch(${group} ${cxx_standard} ${args_MAIN} ${group_${group}_private} ${batch_files})
|
||||
endif()
|
||||
endforeach()
|
||||
|
||||
# remaining files: batches of BATCH_SIZE files per pool
|
||||
foreach(pool plain private)
|
||||
set(is_private FALSE)
|
||||
if(pool STREQUAL private)
|
||||
set(is_private TRUE)
|
||||
endif()
|
||||
|
||||
set(batch_files "")
|
||||
set(batch_count 0)
|
||||
set(batch_index 0)
|
||||
foreach(file ${${pool}_files})
|
||||
_json_test_unity_applies(${file} ${cxx_standard} "${force}" applies)
|
||||
if(NOT applies)
|
||||
continue()
|
||||
endif()
|
||||
|
||||
list(APPEND batch_files ${file})
|
||||
math(EXPR batch_count "${batch_count} + 1")
|
||||
if(batch_count EQUAL args_BATCH_SIZE)
|
||||
_json_test_add_unity_batch(${pool}${batch_index} ${cxx_standard} ${args_MAIN} ${is_private} ${batch_files})
|
||||
set(batch_files "")
|
||||
set(batch_count 0)
|
||||
math(EXPR batch_index "${batch_index} + 1")
|
||||
endif()
|
||||
endforeach()
|
||||
if(batch_files)
|
||||
_json_test_add_unity_batch(${pool}${batch_index} ${cxx_standard} ${args_MAIN} ${is_private} ${batch_files})
|
||||
endif()
|
||||
endforeach()
|
||||
endforeach()
|
||||
endfunction()
|
||||
|
||||
#############################################################################
|
||||
# json_test_should_build_32bit_test(
|
||||
# <build_32bit_var> <build_32bit_only_var> <input>)
|
||||
|
||||
@@ -38,7 +38,6 @@ header. See also the [macro overview page](../../features/macros.md).
|
||||
- [**JSON_HAS_THREE_WAY_COMPARISON**](json_has_three_way_comparison.md) - control 3-way comparison support
|
||||
- [**JSON_NO_AUTOMATIC_UDLS**](json_no_automatic_udls.md) - do not include the user-defined string literals (UDLs) automatically
|
||||
- [**JSON_NO_IO**](json_no_io.md) - switch off functions relying on certain C++ I/O headers
|
||||
- [**JSON_NO_SIMD**](json_no_simd.md) - use only portable code instead of vector instructions (SSE2, SSSE3, NEON)
|
||||
- [**JSON_NO_THREAD_LOCAL**](json_no_thread_local.md) - switch off the use of `thread_local` storage
|
||||
- [**JSON_SKIP_UNSUPPORTED_COMPILER_CHECK**](json_skip_unsupported_compiler_check.md) - do not warn about unsupported compilers
|
||||
- [**JSON_USE_GLOBAL_UDLS**](json_use_global_udls.md) - place user-defined string literals (UDLs) into the global namespace
|
||||
|
||||
@@ -1,83 +0,0 @@
|
||||
# JSON_NO_SIMD
|
||||
|
||||
```cpp
|
||||
#define JSON_NO_SIMD
|
||||
```
|
||||
|
||||
When defined, the library uses only portable C++ code where it would otherwise use vector instructions: when scanning
|
||||
strings during parsing, and when writing floating-point numbers during serialization.
|
||||
|
||||
The macro is an escape hatch, for example for toolchains that cannot compile the vector code, for sanitizers or
|
||||
analyzers that do not understand it, or to compare the speed of both variants. It does not change any result: values,
|
||||
exceptions, and error positions are the same either way. Only the speed differs.
|
||||
|
||||
## Default definition
|
||||
|
||||
By default, `#!cpp JSON_NO_SIMD` is not defined, and vector instructions are used where the target has them.
|
||||
|
||||
```cpp
|
||||
#undef JSON_NO_SIMD
|
||||
```
|
||||
|
||||
## Notes
|
||||
|
||||
!!! note "Which instructions are used"
|
||||
|
||||
- **x86-64:** SSE2, which every x86-64 CPU has, finds the end of runs of plain string characters 16 bytes at a time
|
||||
and writes the digits of floating-point numbers. The UTF-8 check of non-ASCII text needs SSSE3, which is not part
|
||||
of x86-64: it is compiled for SSSE3 with a function attribute and used when the CPU has SSSE3 (every x86-64 CPU
|
||||
since about 2011), else the portable check. Code compiled for SSSE3 anyway (e.g., with `-mssse3` or `-march=native`)
|
||||
skips this check of the CPU. 32-bit x86 uses SSE2 for strings when the compiler targets it.
|
||||
- **AArch64:** NEON, which every 64-bit Arm CPU has, checks UTF-8 text and long runs of plain string characters 16
|
||||
bytes at a time, and writes the digits of floating-point numbers. With MSVC, only the floating-point digits use
|
||||
NEON.
|
||||
- **Other targets:** portable code only.
|
||||
|
||||
Strings are only scanned this way for contiguous input and for the characters a `#!cpp std::istream` holds in its
|
||||
stream buffer (see [`JSON_DISABLE_STREAM_BULK_SCAN`](json_disable_stream_bulk_scan.md)); other inputs are read one
|
||||
character at a time.
|
||||
|
||||
!!! note "Interaction with `JSON_USE_SIMDUTF`"
|
||||
|
||||
With [`JSON_USE_SIMDUTF`](json_use_simdutf.md), runs of string characters are first validated by simdutf; the code
|
||||
described here is used for what simdutf does not accept, so that errors are reported at the same position.
|
||||
`JSON_NO_SIMD` does not affect simdutf.
|
||||
|
||||
!!! note "ABI compatibility"
|
||||
|
||||
The macro selects between two definitions of the library's internal scanning and formatting functions. Whether it
|
||||
is defined is encoded in the [namespace](../../features/namespace.md) (tag `_ns`), resulting in distinct symbol
|
||||
names. Translation units compiled with and without it can therefore be linked into the same program without One
|
||||
Definition Rule (ODR) violations, but they cannot exchange instances of library types.
|
||||
|
||||
## Examples
|
||||
|
||||
??? example "Example: switching off vector instructions"
|
||||
|
||||
The program parses and serializes the same values with and without the macro:
|
||||
|
||||
```cpp
|
||||
#define JSON_NO_SIMD
|
||||
#include <iostream>
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
using json = nlohmann::json;
|
||||
|
||||
int main()
|
||||
{
|
||||
const json j = json::parse(R"({"name": "日本語 text", "pi": 3.141592653589793})");
|
||||
std::cout << j.dump() << '\n'; // {"name":"日本語 text","pi":3.141592653589793}
|
||||
}
|
||||
```
|
||||
|
||||
## See also
|
||||
|
||||
- [**parse**](../basic_json/parse.md) - deserialize from a compatible input
|
||||
- [**dump**](../basic_json/dump.md) - serialization
|
||||
- [**JSON_USE_SIMDUTF**](json_use_simdutf.md) - use the simdutf library to accelerate UTF-8 validation
|
||||
- [**JSON_DISABLE_STREAM_BULK_SCAN**](json_disable_stream_bulk_scan.md) - read a `std::istream` one character at a time
|
||||
- [Parsing: input sources and performance](../../features/parsing/index.md#input-sources-and-performance)
|
||||
|
||||
## Version history
|
||||
|
||||
- Added in version 3.13.0.
|
||||
@@ -6,9 +6,8 @@
|
||||
|
||||
When defined, the parser validates the UTF-8 content of JSON strings that come from a **contiguous byte input**
|
||||
(`std::string`, `std::vector<char>`/`<std::uint8_t>`, string literals, `const char*` ranges, …) using the
|
||||
[simdutf](https://github.com/simdutf/simdutf) library instead of the built-in validator (which uses SSSE3 or NEON where
|
||||
available, see [`JSON_NO_SIMD`](json_no_simd.md)). On text with many non-ASCII characters (e.g. CJK or emoji) this can
|
||||
validate faster, depending on the CPU.
|
||||
[simdutf](https://github.com/simdutf/simdutf) library instead of the built-in scalar validator. On text with many
|
||||
non-ASCII characters (e.g. CJK or emoji) this can validate several times faster.
|
||||
|
||||
This is an **opt-in external dependency**. The library itself remains header-only and its behavior is unchanged: the
|
||||
same input is accepted or rejected either way, and every parse error is reported at the same position with the same
|
||||
|
||||
@@ -39,6 +39,6 @@ the library.
|
||||
## Version history
|
||||
|
||||
- Added in version 3.11.0. Changed inline namespace name in version 3.11.2. Added the ABI tag `_dp` in
|
||||
version 3.12.0, and the ABI tags `_bics`, `_psp`, `_snul`, `_sbu8`, `_ekmo`, `_dsbs`, `_dufio`, `_ns`, `_vns`,
|
||||
and `_vssse3` in version 3.13.0; see
|
||||
version 3.12.0, and the ABI tags `_bics`, `_psp`, `_snul`, `_sbu8`, `_ekmo`, `_dsbs`, `_dufio`, `_vns`, and
|
||||
`_vssse3` in version 3.13.0; see
|
||||
[`nlohmann` Namespace](../../features/namespace.md#structure).
|
||||
@@ -174,13 +174,6 @@ security reasons (e.g., Intel Software Guard Extensions (SGX)).
|
||||
|
||||
See [full documentation of `JSON_NO_IO`](../api/macros/json_no_io.md).
|
||||
|
||||
## `JSON_NO_SIMD`
|
||||
|
||||
When defined, the library uses only portable code instead of vector instructions (SSE2, SSSE3, NEON) for scanning
|
||||
strings and writing floating-point numbers. Results are the same either way. It is not defined by default.
|
||||
|
||||
See [full documentation of `JSON_NO_SIMD`](../api/macros/json_no_simd.md).
|
||||
|
||||
## `JSON_NO_THREAD_LOCAL`
|
||||
|
||||
When defined, the library does not use `#!cpp thread_local` storage. Copying a value and comparing two values then
|
||||
|
||||
@@ -27,7 +27,6 @@ The complete default namespace name is derived as follows:
|
||||
`_dsbs`.
|
||||
- [`JSON_DISABLE_UNLOCKED_FILE_IO`](../api/macros/json_disable_unlocked_file_io.md) defined non-zero appends
|
||||
`_dufio`.
|
||||
- [`JSON_NO_SIMD`](../api/macros/json_no_simd.md) defined appends `_ns`.
|
||||
- [`JSON_VIEW_NO_SIMD`](../api/macros/json_view_no_simd.md) defined appends `_vns`.
|
||||
- [`JSON_VIEW_USE_SSSE3`](../api/macros/json_view_use_ssse3.md) defined appends `_vssse3`.
|
||||
- The inline namespace ends with the suffix `_v` followed by the 3 components of the version number separated by
|
||||
@@ -115,7 +114,6 @@ follows:
|
||||
([`JSON_STRICT_BINARY_UTF8`](../api/macros/json_strict_binary_utf8.md)), `_ekmo`
|
||||
([`JSON_USE_OBJECTS_FOR_ENUM_KEYED_MAPS`](../api/macros/json_use_objects_for_enum_keyed_maps.md)), `_dsbs`
|
||||
([`JSON_DISABLE_STREAM_BULK_SCAN`](../api/macros/json_disable_stream_bulk_scan.md)), `_dufio`
|
||||
([`JSON_DISABLE_UNLOCKED_FILE_IO`](../api/macros/json_disable_unlocked_file_io.md)), `_ns`
|
||||
([`JSON_NO_SIMD`](../api/macros/json_no_simd.md)), `_vns`
|
||||
([`JSON_DISABLE_UNLOCKED_FILE_IO`](../api/macros/json_disable_unlocked_file_io.md)), `_vns`
|
||||
([`JSON_VIEW_NO_SIMD`](../api/macros/json_view_no_simd.md)), and `_vssse3`
|
||||
([`JSON_VIEW_USE_SSSE3`](../api/macros/json_view_use_ssse3.md)) in version 3.13.0.
|
||||
@@ -79,10 +79,6 @@ follow the usual `std::streambuf` contract, and
|
||||
[`JSON_DISABLE_UNLOCKED_FILE_IO`](../../api/macros/json_disable_unlocked_file_io.md) for programs in which another
|
||||
thread must use the same `#!cpp FILE` object while it is parsed.
|
||||
|
||||
Where the target has them, strings are scanned with vector instructions: SSE2 on x86-64 (and SSSE3 for UTF-8 text, when
|
||||
the CPU has it) and NEON on AArch64. [`JSON_NO_SIMD`](../../api/macros/json_no_simd.md) selects portable code instead,
|
||||
again without changing any result.
|
||||
|
||||
## SAX vs. DOM parsing
|
||||
|
||||
The library offers two parsing models:
|
||||
|
||||
@@ -20,38 +20,31 @@ below apply on every supported standard. This adapter exposes two optimizations
|
||||
|
||||
- it can reconstruct already-consumed input on demand for error messages, instead of copying every character as it is
|
||||
read, and
|
||||
- the lexer can scan ordinary string characters, numbers, and indentation directly out of the buffer, several bytes at
|
||||
a time, rather than one character (and one function call) at a time.
|
||||
- the lexer can scan ordinary string characters directly out of the buffer, several bytes at a time, rather than one
|
||||
character (and one function call) at a time.
|
||||
|
||||
A `#!cpp std::istream` (including `#!cpp std::ifstream`) gets the same bulk scanning: `input_stream_adapter` scans the
|
||||
characters its stream buffer already holds (the *get area*) and asks the stream buffer for more only when they are used
|
||||
up. Nothing is read ahead, so the stream is left exactly where it would be after reading one character at a time, and
|
||||
[`operator>>`](../api/operator_gtgt.md) can still read several values from one stream. A stream buffer without a get
|
||||
area is read one character at a time. A `#!cpp FILE*` is still read one character at a time, but `file_input_adapter`
|
||||
locks the file once per parse instead of once per character. See
|
||||
[input sources and performance](parsing/index.md#input-sources-and-performance) for the full table. According to the
|
||||
description of [#5828](https://github.com/nlohmann/json/pull/5828), parsing from a stream is within about 10 to 20 % of
|
||||
parsing an equivalent `#!cpp std::string`.
|
||||
A `#!cpp std::istream` (including `#!cpp std::ifstream`) or `#!cpp FILE*`, by contrast, is read through
|
||||
`input_stream_adapter` or `file_input_adapter`, which read one character (or one block, for binary formats) at a time
|
||||
and expose neither optimization -- the lexer falls back to the same byte-at-a-time path it uses for any
|
||||
non-contiguous, general-purpose iterator range. An iterator pair over non-contiguous but random-access storage (e.g.
|
||||
`#!cpp std::deque<char>::iterator`) gets the first optimization but not the second, since the byte-scanning fast path
|
||||
additionally requires contiguous storage.
|
||||
|
||||
Inputs that are still read one character at a time: iterator ranges that are not contiguous (e.g.
|
||||
`#!cpp std::list<char>` or `#!cpp std::istreambuf_iterator<char>`), wide strings, and user-defined adapters. An
|
||||
iterator pair over non-contiguous but random-access storage (e.g. `#!cpp std::deque<char>::iterator`) gets the first
|
||||
optimization but not the second, since the byte-scanning fast path requires contiguous storage.
|
||||
|
||||
Practically: a file can be passed directly to `parse`, which also avoids the memory for a copy of the file. Reading it
|
||||
into a string first is no longer necessary for speed:
|
||||
Practically: if the JSON text is already in memory, or small enough to read into memory, prefer passing a
|
||||
`#!cpp std::string`, a `#!cpp std::vector<char>`, or a pointer range to `parse` over a `#!cpp std::istream`. For a
|
||||
file, that means reading it into a string first and then parsing the string, rather than passing a
|
||||
`#!cpp std::ifstream` directly to `parse` -- the latter never benefits from either optimization:
|
||||
|
||||
```cpp
|
||||
// fast: the stream buffer's get area is scanned in bulk
|
||||
// gets the contiguous fast paths
|
||||
std::ifstream f("example.json");
|
||||
json j = json::parse(f);
|
||||
```
|
||||
std::string contents((std::istreambuf_iterator<char>(f)), std::istreambuf_iterator<char>());
|
||||
json j = json::parse(contents);
|
||||
|
||||
Two macros switch the stream optimizations off without changing any result:
|
||||
[`JSON_DISABLE_STREAM_BULK_SCAN`](../api/macros/json_disable_stream_bulk_scan.md) for stream buffers that do not follow
|
||||
the usual `#!cpp std::streambuf` contract, and
|
||||
[`JSON_DISABLE_UNLOCKED_FILE_IO`](../api/macros/json_disable_unlocked_file_io.md) for programs in which another thread
|
||||
must use the same `#!cpp FILE` object while it is parsed.
|
||||
// does not: input_stream_adapter has no fast path
|
||||
std::ifstream f2("example.json");
|
||||
json j2 = json::parse(f2);
|
||||
```
|
||||
|
||||
For contiguous input with many non-ASCII characters, [`JSON_USE_SIMDUTF`](../api/macros/json_use_simdutf.md) can
|
||||
additionally speed up UTF-8 validation by using the [simdutf](https://github.com/simdutf/simdutf) library instead of
|
||||
|
||||
@@ -64,7 +64,7 @@ below are relative to `include/nlohmann`.
|
||||
|-----------|----------|
|
||||
| Value type enumeration | [`detail/value_t.hpp`](https://github.com/nlohmann/json/blob/develop/include/nlohmann/detail/value_t.hpp) |
|
||||
| Input adapters | [`detail/input/input_adapters.hpp`](https://github.com/nlohmann/json/blob/develop/include/nlohmann/detail/input/input_adapters.hpp) |
|
||||
| Lexer | [`detail/input/lexer.hpp`](https://github.com/nlohmann/json/blob/develop/include/nlohmann/detail/input/lexer.hpp), [`detail/input/number_parse.hpp`](https://github.com/nlohmann/json/blob/develop/include/nlohmann/detail/input/number_parse.hpp), [`detail/input/string_scan.hpp`](https://github.com/nlohmann/json/blob/develop/include/nlohmann/detail/input/string_scan.hpp), [`detail/input/simd_scan.hpp`](https://github.com/nlohmann/json/blob/develop/include/nlohmann/detail/input/simd_scan.hpp) |
|
||||
| Lexer | [`detail/input/lexer.hpp`](https://github.com/nlohmann/json/blob/develop/include/nlohmann/detail/input/lexer.hpp), [`detail/input/number_parse.hpp`](https://github.com/nlohmann/json/blob/develop/include/nlohmann/detail/input/number_parse.hpp), [`detail/input/string_scan.hpp`](https://github.com/nlohmann/json/blob/develop/include/nlohmann/detail/input/string_scan.hpp) |
|
||||
| Parser | [`detail/input/parser.hpp`](https://github.com/nlohmann/json/blob/develop/include/nlohmann/detail/input/parser.hpp) |
|
||||
| SAX interface and DOM builders | [`detail/input/json_sax.hpp`](https://github.com/nlohmann/json/blob/develop/include/nlohmann/detail/input/json_sax.hpp) |
|
||||
| Binary format readers | [`detail/input/binary_reader.hpp`](https://github.com/nlohmann/json/blob/develop/include/nlohmann/detail/input/binary_reader.hpp) |
|
||||
|
||||
@@ -10,29 +10,6 @@ at its root, a [Natvis](https://learn.microsoft.com/en-us/visualstudio/debugger/
|
||||
file that gives `json`/`ordered_json` values a friendly, key/value debugger view instead of showing raw internal
|
||||
fields, when debugging with the MSVC debug engine (`cppvsdbg`) in Visual Studio or VS Code.
|
||||
|
||||
The checked-in visualizers cover namespace variants with zero, one, or two ABI tags. Configurations with three or more
|
||||
ABI tags are not covered and may show raw internal fields instead. To generate a Natvis file for such a configuration:
|
||||
|
||||
1. Check out the release tag of the library version you use.
|
||||
2. List the ABI tags and the macros that enable them:
|
||||
|
||||
```shell
|
||||
make natvis NATVIS_ARGS="--list-tags"
|
||||
```
|
||||
|
||||
3. Generate a Natvis file with the tags of your configuration, for example for `JSON_DIAGNOSTICS`,
|
||||
`JSON_DIAGNOSTIC_POSITIONS`, and `JSON_VIEW_NO_SIMD`:
|
||||
|
||||
```shell
|
||||
make natvis NATVIS_ARGS="--abi-tags _diag,_dp,_vns --output my_nlohmann_json.natvis"
|
||||
```
|
||||
|
||||
4. Use the generated file instead of `nlohmann_json.natvis`.
|
||||
|
||||
`--abi-tags` can be repeated for several configurations, and `--all` generates every combination (about 60 MB for 12
|
||||
ABI tags, doubling with each further tag). See
|
||||
[`tools/generate_natvis`](https://github.com/nlohmann/json/tree/develop/tools/generate_natvis) for all options.
|
||||
|
||||
Debug engines that wrap LLDB instead of the MSVC debug engine (for example, `codelldb` in VS Code) only have
|
||||
partial/experimental Natvis support, and commonly fall back to showing raw internal fields even with the
|
||||
`.natvis` file present. Switching to `cppvsdbg` where available, or checking your debug extension's own Natvis
|
||||
|
||||
@@ -235,6 +235,17 @@ Build the unit tests against the [simdutf](https://github.com/simdutf/simdutf) U
|
||||
its version is set by the cache variable `JSON_SIMDUTF_VERSION`. This option is `OFF` by default. Depends on
|
||||
`JSON_BuildTests`.
|
||||
|
||||
### `JSON_TestUnityBuild`
|
||||
|
||||
Build the unit tests in batches of several test files per executable to speed up compilation: the files of a batch are
|
||||
compiled as one translation unit, so the template instantiations of the library are shared. Every test file still gets
|
||||
its own CTest test (e.g., `test-foo_cpp11`), which runs only the test cases of that file from the shared executable.
|
||||
This option is `ON` by default (and `OFF` with MinGW). Set it to `OFF` to get one executable per test file, e.g., when
|
||||
debugging a single file. The number of files per executable is set by the cache variable `JSON_TestUnityBatchSize`
|
||||
(default: `8`). Related test files (e.g., all binary formats) are grouped explicitly in `tests/CMakeLists.txt`; each
|
||||
group is compiled as one executable per C++ standard regardless of the batch size, and only the remaining files are
|
||||
batched by size. Depends on `JSON_BuildTests`.
|
||||
|
||||
### `JSON_Valgrind`
|
||||
|
||||
Execute the test suite with [Valgrind](https://valgrind.org). This option is `OFF` by default. Depends on `JSON_BuildTests`.
|
||||
|
||||
@@ -383,7 +383,6 @@ nav:
|
||||
- 'JSON_NO_AUTOMATIC_UDLS': api/macros/json_no_automatic_udls.md
|
||||
- 'JSON_NOEXCEPTION': api/macros/json_noexception.md
|
||||
- 'JSON_NO_IO': api/macros/json_no_io.md
|
||||
- 'JSON_NO_SIMD': api/macros/json_no_simd.md
|
||||
- 'JSON_NO_THREAD_LOCAL': api/macros/json_no_thread_local.md
|
||||
- 'JSON_PRECISE_STREAM_POSITION': api/macros/json_precise_stream_position.md
|
||||
- 'JSON_SKIP_LIBRARY_VERSION_CHECK': api/macros/json_skip_library_version_check.md
|
||||
|
||||
@@ -122,12 +122,6 @@
|
||||
#define NLOHMANN_JSON_ABI_TAG_DISABLE_UNLOCKED_FILE_IO
|
||||
#endif
|
||||
|
||||
#ifdef JSON_NO_SIMD
|
||||
#define NLOHMANN_JSON_ABI_TAG_NO_SIMD _ns
|
||||
#else
|
||||
#define NLOHMANN_JSON_ABI_TAG_NO_SIMD
|
||||
#endif
|
||||
|
||||
#ifdef JSON_VIEW_NO_SIMD
|
||||
#define NLOHMANN_JSON_ABI_TAG_VIEW_NO_SIMD _vns
|
||||
#else
|
||||
@@ -145,9 +139,9 @@
|
||||
#endif
|
||||
|
||||
// Construct the namespace ABI tags component
|
||||
#define NLOHMANN_JSON_ABI_TAGS_CONCAT_EX(a, b, c, d, e, f, g, h, i, j, k, l, m) json_abi ## a ## b ## c ## d ## e ## f ## g ## h ## i ## j ## k ## l ## m
|
||||
#define NLOHMANN_JSON_ABI_TAGS_CONCAT(a, b, c, d, e, f, g, h, i, j, k, l, m) \
|
||||
NLOHMANN_JSON_ABI_TAGS_CONCAT_EX(a, b, c, d, e, f, g, h, i, j, k, l, m)
|
||||
#define NLOHMANN_JSON_ABI_TAGS_CONCAT_EX(a, b, c, d, e, f, g, h, i, j, k, l) json_abi ## a ## b ## c ## d ## e ## f ## g ## h ## i ## j ## k ## l
|
||||
#define NLOHMANN_JSON_ABI_TAGS_CONCAT(a, b, c, d, e, f, g, h, i, j, k, l) \
|
||||
NLOHMANN_JSON_ABI_TAGS_CONCAT_EX(a, b, c, d, e, f, g, h, i, j, k, l)
|
||||
|
||||
#define NLOHMANN_JSON_ABI_TAGS \
|
||||
NLOHMANN_JSON_ABI_TAGS_CONCAT( \
|
||||
@@ -161,7 +155,6 @@
|
||||
NLOHMANN_JSON_ABI_TAG_OBJECTS_FOR_ENUM_KEYED_MAPS, \
|
||||
NLOHMANN_JSON_ABI_TAG_DISABLE_STREAM_BULK_SCAN, \
|
||||
NLOHMANN_JSON_ABI_TAG_DISABLE_UNLOCKED_FILE_IO, \
|
||||
NLOHMANN_JSON_ABI_TAG_NO_SIMD, \
|
||||
NLOHMANN_JSON_ABI_TAG_VIEW_NO_SIMD, \
|
||||
NLOHMANN_JSON_ABI_TAG_VIEW_USE_SSSE3)
|
||||
|
||||
|
||||
@@ -14,24 +14,11 @@
|
||||
#include <intrin0.h> // __umulh, _umul128, _BitScanForward64, _BitScanReverse64
|
||||
#endif
|
||||
|
||||
#ifdef _MSC_VER
|
||||
#include <cstdlib> // _byteswap_ushort, _byteswap_ulong, _byteswap_uint64
|
||||
#endif
|
||||
#if defined(_MSC_VER) && !defined(__clang__) && (defined(_M_IX86) || defined(_M_ARM))
|
||||
#include <intrin.h> // _BitScanForward, _BitScanReverse
|
||||
#endif
|
||||
|
||||
#include <nlohmann/detail/macro_scope.hpp> // JSON_HEDLEY_ALWAYS_INLINE, NLOHMANN_JSON_NAMESPACE_BEGIN
|
||||
|
||||
#if defined(__cpp_lib_bit_cast) || defined(__cpp_lib_bitops) || defined(__cpp_lib_byteswap)
|
||||
#include <bit> // bit_cast, byteswap, countl_zero, countr_zero
|
||||
#endif
|
||||
|
||||
// Portable bit-level helpers for the number and string scanners. They use
|
||||
// compiler builtins or platform-specific intrinsics where available, the
|
||||
// C++20 <bit> functions next, and plain C++ otherwise, so they work regardless
|
||||
// of byte order. (The builtins and intrinsics come first: MSVC's
|
||||
// std::countl_zero checks for LZCNT at run time unless AVX2 is enabled.)
|
||||
// compiler builtins or platform-specific intrinsics where available and plain
|
||||
// C++ otherwise, so they work regardless of byte order.
|
||||
|
||||
NLOHMANN_JSON_NAMESPACE_BEGIN
|
||||
namespace detail
|
||||
@@ -46,16 +33,6 @@ inline int count_leading_zeros(std::uint64_t x) noexcept
|
||||
unsigned long index = 0; // NOLINT(runtime/int): the type _BitScan*64 takes
|
||||
_BitScanReverse64(&index, x);
|
||||
return 63 - static_cast<int>(index);
|
||||
#elif defined(_MSC_VER) && (defined(_M_IX86) || defined(_M_ARM))
|
||||
unsigned long index = 0; // NOLINT(runtime/int): the type _BitScan* takes
|
||||
if (_BitScanReverse(&index, static_cast<unsigned long>(x >> 32u))) // NOLINT(runtime/int)
|
||||
{
|
||||
return 31 - static_cast<int>(index);
|
||||
}
|
||||
_BitScanReverse(&index, static_cast<unsigned long>(x)); // NOLINT(runtime/int)
|
||||
return 63 - static_cast<int>(index);
|
||||
#elif defined(__cpp_lib_bitops)
|
||||
return std::countl_zero(x);
|
||||
#else
|
||||
int n = 0;
|
||||
for (int shift = 32; shift != 0; shift >>= 1)
|
||||
@@ -79,16 +56,6 @@ inline int count_trailing_zeros(std::uint64_t x) noexcept
|
||||
unsigned long index = 0; // NOLINT(runtime/int): the type _BitScan*64 takes
|
||||
_BitScanForward64(&index, x);
|
||||
return static_cast<int>(index);
|
||||
#elif defined(_MSC_VER) && (defined(_M_IX86) || defined(_M_ARM))
|
||||
unsigned long index = 0; // NOLINT(runtime/int): the type _BitScan* takes
|
||||
if (_BitScanForward(&index, static_cast<unsigned long>(x))) // NOLINT(runtime/int)
|
||||
{
|
||||
return static_cast<int>(index);
|
||||
}
|
||||
_BitScanForward(&index, static_cast<unsigned long>(x >> 32u)); // NOLINT(runtime/int)
|
||||
return 32 + static_cast<int>(index);
|
||||
#elif defined(__cpp_lib_bitops)
|
||||
return std::countr_zero(x);
|
||||
#else
|
||||
int n = 0;
|
||||
for (int shift = 32; shift != 0; shift >>= 1)
|
||||
@@ -103,67 +70,6 @@ inline int count_trailing_zeros(std::uint64_t x) noexcept
|
||||
#endif
|
||||
}
|
||||
|
||||
/// the object representation of @a from as a @a To (std::bit_cast in C++20)
|
||||
template<typename To, typename From>
|
||||
JSON_HEDLEY_ALWAYS_INLINE To bit_cast(const From& from) noexcept
|
||||
{
|
||||
static_assert(sizeof(To) == sizeof(From), "bit_cast needs types of the same size");
|
||||
#if defined(__cpp_lib_bit_cast)
|
||||
return std::bit_cast<To>(from);
|
||||
#else
|
||||
To to {};
|
||||
std::memcpy(&to, &from, sizeof(To));
|
||||
return to;
|
||||
#endif
|
||||
}
|
||||
|
||||
/// x with its bytes in reverse order (std::byteswap in C++23); compilers lower
|
||||
/// it to a single bswap/rev/movbe
|
||||
inline std::uint16_t byteswap(std::uint16_t x) noexcept
|
||||
{
|
||||
#if defined(__GNUC__) || defined(__clang__)
|
||||
return __builtin_bswap16(x);
|
||||
#elif defined(_MSC_VER)
|
||||
return _byteswap_ushort(x);
|
||||
#elif defined(__cpp_lib_byteswap)
|
||||
return std::byteswap(x);
|
||||
#else
|
||||
return static_cast<std::uint16_t>((x >> 8u) | (x << 8u));
|
||||
#endif
|
||||
}
|
||||
|
||||
/// x with its bytes in reverse order (std::byteswap in C++23)
|
||||
inline std::uint32_t byteswap(std::uint32_t x) noexcept
|
||||
{
|
||||
#if defined(__GNUC__) || defined(__clang__)
|
||||
return __builtin_bswap32(x);
|
||||
#elif defined(_MSC_VER)
|
||||
return _byteswap_ulong(x);
|
||||
#elif defined(__cpp_lib_byteswap)
|
||||
return std::byteswap(x);
|
||||
#else
|
||||
return ((x & 0x000000FFu) << 24u) | ((x & 0x0000FF00u) << 8u)
|
||||
| ((x & 0x00FF0000u) >> 8u) | ((x & 0xFF000000u) >> 24u);
|
||||
#endif
|
||||
}
|
||||
|
||||
/// x with its bytes in reverse order (std::byteswap in C++23)
|
||||
inline std::uint64_t byteswap(std::uint64_t x) noexcept
|
||||
{
|
||||
#if defined(__GNUC__) || defined(__clang__)
|
||||
return __builtin_bswap64(x);
|
||||
#elif defined(_MSC_VER)
|
||||
return _byteswap_uint64(x);
|
||||
#elif defined(__cpp_lib_byteswap)
|
||||
return std::byteswap(x);
|
||||
#else
|
||||
x = ((x & 0x00000000FFFFFFFFull) << 32u) | ((x & 0xFFFFFFFF00000000ull) >> 32u);
|
||||
x = ((x & 0x0000FFFF0000FFFFull) << 16u) | ((x & 0xFFFF0000FFFF0000ull) >> 16u);
|
||||
x = ((x & 0x00FF00FF00FF00FFull) << 8u) | ((x & 0xFF00FF00FF00FF00ull) >> 8u);
|
||||
return x;
|
||||
#endif
|
||||
}
|
||||
|
||||
/// the 128-bit product of two 64-bit numbers
|
||||
struct uint128_parts
|
||||
{
|
||||
@@ -201,8 +107,8 @@ inline uint128_parts full_multiplication(std::uint64_t a, std::uint64_t b) noexc
|
||||
/// targets; always inlined, as GCC otherwise calls it in the number loops)
|
||||
JSON_HEDLEY_ALWAYS_INLINE std::uint64_t read_eight_bytes(const unsigned char* b) noexcept
|
||||
{
|
||||
#if JSON_LITTLE_ENDIAN_TARGET
|
||||
// the byte order already matches
|
||||
#if defined(_MSC_VER) || defined(__x86_64__) || defined(__i386__) || (defined(__BYTE_ORDER__) && defined(__ORDER_LITTLE_ENDIAN__) && __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__)
|
||||
// the byte order already matches (all MSVC targets are little-endian)
|
||||
std::uint64_t result = 0;
|
||||
std::memcpy(&result, b, sizeof(result));
|
||||
return result;
|
||||
|
||||
@@ -18,14 +18,17 @@
|
||||
#include <limits> // numeric_limits
|
||||
#include <type_traits> // conditional
|
||||
|
||||
#ifdef _MSC_VER
|
||||
#include <cstdlib> // _byteswap_uint64
|
||||
#endif
|
||||
|
||||
// SSE2 (every x86-64 CPU) and NEON (every 64-bit Arm CPU) convert the 16
|
||||
// digits of a double at once (unless JSON_NO_SIMD selects the portable code)
|
||||
#if !defined(JSON_NO_SIMD) && (defined(__x86_64__) || (defined(_M_X64) && !defined(_M_ARM64EC)))
|
||||
// digits of a double at once
|
||||
#if defined(__x86_64__) || (defined(_M_X64) && !defined(_M_ARM64EC))
|
||||
#include <emmintrin.h>
|
||||
#define JSON_DTOA_SSE2 1
|
||||
#define JSON_DTOA_NEON 0
|
||||
#elif !defined(JSON_NO_SIMD) && (defined(__aarch64__) || defined(_M_ARM64)) && !defined(_M_ARM64EC) && !defined(__ARM_BIG_ENDIAN)
|
||||
#elif (defined(__aarch64__) || defined(_M_ARM64)) && !defined(_M_ARM64EC) && !defined(__ARM_BIG_ENDIAN)
|
||||
#include <arm_neon.h>
|
||||
#define JSON_DTOA_SSE2 0
|
||||
#define JSON_DTOA_NEON 1
|
||||
@@ -34,7 +37,6 @@
|
||||
#define JSON_DTOA_NEON 0
|
||||
#endif
|
||||
|
||||
#include <nlohmann/detail/bit_ops.hpp>
|
||||
#include <nlohmann/detail/conversions/zmij.hpp>
|
||||
#include <nlohmann/detail/macro_scope.hpp>
|
||||
|
||||
@@ -64,6 +66,16 @@ For a detailed description of the algorithm see:
|
||||
namespace dtoa_impl
|
||||
{
|
||||
|
||||
template<typename Target, typename Source>
|
||||
Target reinterpret_bits(const Source source)
|
||||
{
|
||||
static_assert(sizeof(Target) == sizeof(Source), "size mismatch");
|
||||
|
||||
Target target;
|
||||
std::memcpy(&target, &source, sizeof(Source));
|
||||
return target;
|
||||
}
|
||||
|
||||
struct diyfp // f * 2^e
|
||||
{
|
||||
static constexpr int kPrecision = 64; // = q
|
||||
@@ -218,7 +230,7 @@ boundaries compute_boundaries(FloatType value)
|
||||
|
||||
using bits_type = typename std::conditional<kPrecision == 24, std::uint32_t, std::uint64_t >::type;
|
||||
|
||||
const auto bits = static_cast<std::uint64_t>(bit_cast<bits_type>(value));
|
||||
const auto bits = static_cast<std::uint64_t>(reinterpret_bits<bits_type>(value));
|
||||
const std::uint64_t E = bits >> (kPrecision - 1);
|
||||
const std::uint64_t F = bits & (kHiddenBit - 1);
|
||||
|
||||
@@ -1072,10 +1084,13 @@ inline std::uint64_t eight_digit_bytes(std::uint64_t abcdefgh) noexcept
|
||||
/// the byte stores once this is inlined)
|
||||
inline void store_msb_first(char* p, std::uint64_t v) noexcept
|
||||
{
|
||||
#if JSON_LITTLE_ENDIAN_TARGET
|
||||
v = byteswap(v);
|
||||
#if defined(__BYTE_ORDER__) && defined(__ORDER_LITTLE_ENDIAN__) && __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__
|
||||
v = __builtin_bswap64(v);
|
||||
std::memcpy(p, &v, sizeof(v));
|
||||
#elif JSON_BIG_ENDIAN_TARGET
|
||||
#elif defined(__BYTE_ORDER__) && defined(__ORDER_BIG_ENDIAN__) && __BYTE_ORDER__ == __ORDER_BIG_ENDIAN__
|
||||
std::memcpy(p, &v, sizeof(v));
|
||||
#elif defined(_MSC_VER) // (little-endian on all its targets)
|
||||
v = _byteswap_uint64(v);
|
||||
std::memcpy(p, &v, sizeof(v));
|
||||
#else
|
||||
for (unsigned i = 0; i < 8; ++i)
|
||||
@@ -1447,7 +1462,8 @@ char* write_positive_zmij(char* first, const char* last, FloatType value)
|
||||
{
|
||||
static_assert(is_binary64<FloatType>::value,
|
||||
"internal error: the conversion of Zmij needs IEEE 754 binary64 numbers");
|
||||
const auto bits = bit_cast<std::uint64_t>(value);
|
||||
std::uint64_t bits = 0;
|
||||
std::memcpy(&bits, &value, sizeof(bits));
|
||||
const zmij::shortest_decimal d = zmij::to_shortest(bits);
|
||||
if (JSON_HEDLEY_LIKELY(last - first >= 41))
|
||||
{
|
||||
|
||||
@@ -418,9 +418,8 @@ class json_sax_dom_parser
|
||||
JSON_ASSERT(!ref_stack.empty());
|
||||
JSON_ASSERT(ref_stack.back()->is_object());
|
||||
|
||||
// add null at the given key and store the reference for later; the
|
||||
// key is moved, as the lexer starts the next token with reset()
|
||||
object_element = &(ref_stack.back()->m_data.m_value.object->operator[](std::move(val)));
|
||||
// add null at the given key and store the reference for later
|
||||
object_element = &(ref_stack.back()->m_data.m_value.object->operator[](val));
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -12,7 +12,6 @@
|
||||
#include <cstddef> // size_t
|
||||
#include <cstdint> // uint32_t
|
||||
#include <cstdio> // snprintf
|
||||
#include <cstring> // memcmp
|
||||
#include <initializer_list> // initializer_list
|
||||
#include <string> // char_traits, string
|
||||
#include <utility> // move
|
||||
@@ -418,43 +417,30 @@ class lexer : public lexer_base<BasicJsonType>
|
||||
}
|
||||
|
||||
/// contiguous input: bulk-append the run of ordinary characters and complete
|
||||
/// well-formed UTF-8 sequences starting at the current read position, then
|
||||
/// read the first byte that needs individual handling (the closing quote, an
|
||||
/// escape, a control character, or an ill-formed UTF-8 byte) like get().
|
||||
/// That byte is taken from the buffer that was just scanned. Through get(),
|
||||
/// Apple Clang merged this read with the other get() calls of the scan
|
||||
/// function, which load the read position again right after it was stored
|
||||
/// (3% of json::accept() on jeopardy.json on Apple M1).
|
||||
char_int_type get_after_string_run(std::true_type /*bulk*/)
|
||||
/// well-formed UTF-8 sequences starting at the current read position, leaving
|
||||
/// the first byte that needs individual handling (the closing quote, an
|
||||
/// escape, a control character, or an ill-formed UTF-8 byte) for get()
|
||||
void scan_string_bulk(std::true_type /*bulk*/)
|
||||
{
|
||||
// a pending unget must be consumed through the normal path first
|
||||
if (next_unget)
|
||||
{
|
||||
return get();
|
||||
return;
|
||||
}
|
||||
const std::size_t remaining = ia.bulk_remaining();
|
||||
if (remaining == 0)
|
||||
{
|
||||
return get();
|
||||
return;
|
||||
}
|
||||
const auto* const raw = ia.bulk_data();
|
||||
const auto* const data = reinterpret_cast<const unsigned char*>(raw);
|
||||
const auto* const data = reinterpret_cast<const unsigned char*>(ia.bulk_data());
|
||||
|
||||
const std::size_t pos = lexer_string_bulk_run(data, remaining);
|
||||
if (pos != 0)
|
||||
const std::size_t pos = string_bulk_run(data, remaining);
|
||||
if (pos == 0)
|
||||
{
|
||||
token_buffer.append(reinterpret_cast<const typename string_t::value_type*>(data), pos);
|
||||
bulk_consume(reinterpret_cast<const char*>(data), pos);
|
||||
return;
|
||||
}
|
||||
if (pos == remaining)
|
||||
{
|
||||
return get(); // end of the contiguous input
|
||||
}
|
||||
// what get() does, with the byte at hand
|
||||
advance_position();
|
||||
ia.bulk_skip(1);
|
||||
current = char_traits<char_type>::to_int_type(raw[pos]);
|
||||
return track_after_read();
|
||||
token_buffer.append(reinterpret_cast<const typename string_t::value_type*>(data), pos);
|
||||
bulk_consume(reinterpret_cast<const char*>(data), pos);
|
||||
}
|
||||
|
||||
/// consume @a n characters at @a data (the adapter's bulk_data()) and do
|
||||
@@ -487,10 +473,7 @@ class lexer : public lexer_base<BasicJsonType>
|
||||
}
|
||||
|
||||
/// streaming input: no bulk fast path
|
||||
char_int_type get_after_string_run(std::false_type /*bulk*/)
|
||||
{
|
||||
return get();
|
||||
}
|
||||
void scan_string_bulk(std::false_type /*bulk*/) const noexcept {}
|
||||
|
||||
/*!
|
||||
@brief scan a string literal
|
||||
@@ -518,8 +501,11 @@ class lexer : public lexer_base<BasicJsonType>
|
||||
while (true)
|
||||
{
|
||||
// bulk-consume ordinary characters from contiguous input, then
|
||||
// get the next character, the one that ends the run
|
||||
switch (get_after_string_run(std::integral_constant<bool, bulk_scan>()))
|
||||
// handle the next special byte through the switch below
|
||||
scan_string_bulk(std::integral_constant<bool, bulk_scan> {});
|
||||
|
||||
// get the next character
|
||||
switch (get())
|
||||
{
|
||||
// end of file while parsing the string
|
||||
case char_traits<char_type>::eof():
|
||||
@@ -1639,12 +1625,9 @@ scan_number_done:
|
||||
const std::size_t avail = rem + 1;
|
||||
|
||||
// validate + classify the number extent (mirrors scan_number()'s grammar)
|
||||
// and accumulate the mantissa digits on the way, so that tokens with up
|
||||
// to 19 digits need no second pass for their value
|
||||
std::size_t i = 0;
|
||||
std::size_t dot_index = std::string::npos;
|
||||
token_type number_type = token_type::value_unsigned;
|
||||
std::uint64_t w = 0;
|
||||
if (data[0] == '-')
|
||||
{
|
||||
number_type = token_type::value_integer;
|
||||
@@ -1654,21 +1637,22 @@ scan_number_done:
|
||||
return token_type::uninitialized;
|
||||
}
|
||||
}
|
||||
const std::size_t integer_begin = i;
|
||||
if (data[i] == '0')
|
||||
{
|
||||
++i;
|
||||
}
|
||||
else if (data[i] >= '1' && data[i] <= '9')
|
||||
{
|
||||
i = accumulate_digits(data, i, avail, w);
|
||||
++i;
|
||||
while (i < avail && data[i] >= '0' && data[i] <= '9')
|
||||
{
|
||||
++i;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
return token_type::uninitialized;
|
||||
}
|
||||
std::size_t digit_count = i - integer_begin;
|
||||
std::size_t fraction_digits = 0;
|
||||
if (i < avail && data[i] == '.')
|
||||
{
|
||||
number_type = token_type::value_float;
|
||||
@@ -1678,39 +1662,29 @@ scan_number_done:
|
||||
{
|
||||
return token_type::uninitialized;
|
||||
}
|
||||
const std::size_t fraction_begin = i;
|
||||
i = accumulate_digits(data, i, avail, w);
|
||||
fraction_digits = i - fraction_begin;
|
||||
digit_count += fraction_digits;
|
||||
while (i < avail && data[i] >= '0' && data[i] <= '9')
|
||||
{
|
||||
++i;
|
||||
}
|
||||
}
|
||||
// the mantissa ends here, whether or not an exponent part follows
|
||||
const std::size_t mantissa_end = i;
|
||||
std::int64_t exponent = 0;
|
||||
if (i < avail && (data[i] == 'e' || data[i] == 'E'))
|
||||
{
|
||||
number_type = token_type::value_float;
|
||||
++i;
|
||||
bool negative_exponent = false;
|
||||
if (i < avail && (data[i] == '+' || data[i] == '-'))
|
||||
{
|
||||
negative_exponent = data[i] == '-';
|
||||
++i;
|
||||
}
|
||||
if (i >= avail || !(data[i] >= '0' && data[i] <= '9'))
|
||||
{
|
||||
return token_type::uninitialized;
|
||||
}
|
||||
// saturated far beyond every range, see parse_float_exponent()
|
||||
constexpr std::int64_t saturation = 100000000000000000; // 10^17
|
||||
while (i < avail && data[i] >= '0' && data[i] <= '9')
|
||||
{
|
||||
if (exponent < saturation)
|
||||
{
|
||||
exponent = (exponent * 10) + (data[i] - '0');
|
||||
}
|
||||
++i;
|
||||
}
|
||||
exponent = negative_exponent ? -exponent : exponent;
|
||||
}
|
||||
const std::size_t len = i;
|
||||
if (number_may_continue(len == avail, std::integral_constant<bool, bulk_window> {}))
|
||||
@@ -1731,10 +1705,7 @@ scan_number_done:
|
||||
#if !JSON_DIAGNOSTIC_POSITIONS
|
||||
if (number_type != token_type::value_float)
|
||||
{
|
||||
// up to 18 digits always fit into std::uint64_t and std::int64_t
|
||||
const token_type integer_result = (digit_count <= 18)
|
||||
? store_integer(number_type, w)
|
||||
: convert_integer(number_type, data, data + len);
|
||||
const token_type integer_result = convert_integer(number_type, data, data + len);
|
||||
if (JSON_HEDLEY_LIKELY(integer_result != token_type::uninitialized))
|
||||
{
|
||||
bulk_consume(data + 1, len - 1);
|
||||
@@ -1748,42 +1719,15 @@ scan_number_done:
|
||||
}
|
||||
#endif
|
||||
|
||||
// materialize the token exactly as scan_number() would. reset() already
|
||||
// cleared token_buffer, so append() fills it (assign() is avoided
|
||||
// because custom string_t types need not provide it)
|
||||
token_buffer.append(reinterpret_cast<const typename string_t::value_type*>(data), len);
|
||||
decimal_point_position = dot_index;
|
||||
|
||||
bulk_consume(data + 1, len - 1);
|
||||
read_number_terminator(std::integral_constant<bool, bulk_window> {});
|
||||
|
||||
// a float whose digits all fit into w converts from the accumulated
|
||||
// value; longer ones (and other float formats) take the general path
|
||||
if (number_type == token_type::value_float && digit_count <= 19)
|
||||
{
|
||||
float_significand s;
|
||||
s.w = w;
|
||||
s.exponent = exponent - static_cast<std::int64_t>(fraction_digits);
|
||||
s.negative = data[0] == '-';
|
||||
if (convert_accumulated_float(s, std::integral_constant<bool, has_native_float_format<number_float_t>::value> {}))
|
||||
{
|
||||
// the token text is only copied into token_buffer when
|
||||
// get_string() asks for it; the library's own SAX handlers
|
||||
// ignore it. A window into a stream may be refilled by the
|
||||
// next read, so there the text is copied right away.
|
||||
if (bulk_window)
|
||||
{
|
||||
token_buffer.append(reinterpret_cast<const typename string_t::value_type*>(data), len);
|
||||
}
|
||||
else
|
||||
{
|
||||
pending_number_text = data;
|
||||
pending_number_length = len;
|
||||
}
|
||||
return token_type::value_float;
|
||||
}
|
||||
}
|
||||
|
||||
// materialize the token exactly as scan_number() would. reset() already
|
||||
// cleared token_buffer, so append() fills it (assign() is avoided
|
||||
// because custom string_t types need not provide it)
|
||||
token_buffer.append(reinterpret_cast<const typename string_t::value_type*>(data), len);
|
||||
return convert_number(number_type, mantissa_end);
|
||||
}
|
||||
|
||||
@@ -1812,35 +1756,6 @@ scan_number_done:
|
||||
/// the input is not handed back, so the terminator can stay unread
|
||||
void read_number_terminator(std::false_type /*window*/) const noexcept {}
|
||||
|
||||
/// binary32/binary64: the value of a float token from its accumulated
|
||||
/// significand (see scan_number_bulk_contiguous())
|
||||
bool convert_accumulated_float(const float_significand& s, std::true_type /*native*/) noexcept
|
||||
{
|
||||
value_float = decimal_to_float<number_float_t>(s);
|
||||
return true;
|
||||
}
|
||||
|
||||
/// other formats (e.g. x87 long double): not instantiated for them, as
|
||||
/// decimal_to_float() only supports binary32/binary64
|
||||
static bool convert_accumulated_float(const float_significand& /*s*/, std::false_type /*native*/) noexcept
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
/// the value of an integer token with at most 18 digits, accumulated in @a w
|
||||
token_type store_integer(token_type number_type, std::uint64_t w) noexcept
|
||||
{
|
||||
if (number_type == token_type::value_unsigned)
|
||||
{
|
||||
value_unsigned = static_cast<number_unsigned_t>(w);
|
||||
// reject values that do not round-trip into a narrower type
|
||||
return (static_cast<std::uint64_t>(value_unsigned) == w) ? token_type::value_unsigned : token_type::uninitialized;
|
||||
}
|
||||
const std::int64_t x = -static_cast<std::int64_t>(w);
|
||||
value_integer = static_cast<number_integer_t>(x);
|
||||
return (static_cast<std::int64_t>(value_integer) == x) ? token_type::value_integer : token_type::uninitialized;
|
||||
}
|
||||
|
||||
/// contiguous input: try the number fast path, else the byte-path scanner
|
||||
token_type scan_number_dispatch(std::true_type /*bulk*/)
|
||||
{
|
||||
@@ -1855,23 +1770,18 @@ scan_number_done:
|
||||
}
|
||||
|
||||
/*!
|
||||
@param[in] rest the literal text after its first character, which
|
||||
is current (its length is a template parameter, so
|
||||
that the comparison of the contiguous fast path is
|
||||
a fixed-size load, not a call of memcmp)
|
||||
@param[in] return_type the token type to return on success
|
||||
@param[in] literal_text the literal text to expect
|
||||
@param[in] length the length of the passed literal text
|
||||
@param[in] return_type the token type to return on success
|
||||
*/
|
||||
template<std::size_t N>
|
||||
JSON_HEDLEY_NEVER_INLINE token_type scan_literal(const char (&rest)[N], token_type return_type) // NOLINT(cppcoreguidelines-avoid-c-arrays,hicpp-avoid-c-arrays,modernize-avoid-c-arrays)
|
||||
JSON_HEDLEY_NON_NULL(2)
|
||||
token_type scan_literal(const char_type* literal_text, const std::size_t length,
|
||||
token_type return_type)
|
||||
{
|
||||
constexpr std::size_t length = N - 1;
|
||||
if (scan_literal_bulk(rest, length, std::integral_constant<bool, bulk_scan> {}))
|
||||
JSON_ASSERT(char_traits<char_type>::to_char_type(current) == literal_text[0]);
|
||||
for (std::size_t i = 1; i < length; ++i)
|
||||
{
|
||||
return return_type;
|
||||
}
|
||||
for (std::size_t i = 0; i < length; ++i)
|
||||
{
|
||||
if (JSON_HEDLEY_UNLIKELY(char_traits<char_type>::to_char_type(get()) != static_cast<char_type>(rest[i])))
|
||||
if (JSON_HEDLEY_UNLIKELY(char_traits<char_type>::to_char_type(get()) != literal_text[i]))
|
||||
{
|
||||
error_message = "invalid literal";
|
||||
return token_type::parse_error;
|
||||
@@ -1880,27 +1790,6 @@ scan_number_done:
|
||||
return return_type;
|
||||
}
|
||||
|
||||
/// contiguous input: compare the rest of a literal at once; false (and
|
||||
/// nothing consumed) if it does not match, so that the byte path reports
|
||||
/// the error at the right character
|
||||
JSON_HEDLEY_ALWAYS_INLINE bool scan_literal_bulk(const char* rest, const std::size_t length, std::true_type /*bulk*/)
|
||||
{
|
||||
if (next_unget || ia.bulk_remaining() < length
|
||||
|| std::memcmp(ia.bulk_data(), rest, length) != 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
bulk_consume(reinterpret_cast<const char*>(ia.bulk_data()), length);
|
||||
current = static_cast<char_int_type>(static_cast<unsigned char>(rest[length - 1]));
|
||||
return true;
|
||||
}
|
||||
|
||||
/// streaming input: no bulk fast path
|
||||
bool scan_literal_bulk(const char* /*rest*/, const std::size_t /*length*/, std::false_type /*bulk*/) const noexcept
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
/////////////////////
|
||||
// input management
|
||||
/////////////////////
|
||||
@@ -1909,7 +1798,6 @@ scan_number_done:
|
||||
void reset() noexcept
|
||||
{
|
||||
token_buffer.clear();
|
||||
pending_number_text = nullptr;
|
||||
decimal_point_position = std::string::npos;
|
||||
|
||||
#if JSON_DIAGNOSTIC_POSITIONS
|
||||
@@ -2169,30 +2057,10 @@ scan_number_done:
|
||||
/// return current string value
|
||||
string_t& get_string()
|
||||
{
|
||||
if (pending_number_text != nullptr)
|
||||
{
|
||||
token_buffer.append(reinterpret_cast<const typename string_t::value_type*>(pending_number_text), pending_number_length);
|
||||
pending_number_text = nullptr;
|
||||
}
|
||||
// a number token holds '.' regardless of the locale (#4084)
|
||||
return token_buffer;
|
||||
}
|
||||
|
||||
/// the value of a string token (key or string), which never has pending
|
||||
/// number text, so get_string()'s check is not needed
|
||||
string_t& get_string_token() noexcept
|
||||
{
|
||||
JSON_ASSERT(pending_number_text == nullptr);
|
||||
return token_buffer;
|
||||
}
|
||||
|
||||
/// the text of a float token for a SAX handler that ignores it (the
|
||||
/// library's own handlers): empty, without copying the token
|
||||
const string_t& get_ignored_number_text() const noexcept
|
||||
{
|
||||
return token_buffer;
|
||||
}
|
||||
|
||||
/////////////////////
|
||||
// diagnostics
|
||||
/////////////////////
|
||||
@@ -2526,14 +2394,21 @@ scan_number_done:
|
||||
return token_type::value_separator;
|
||||
|
||||
// literals
|
||||
// (the text after the first character; no local arrays, which
|
||||
// would give this function a stack canary on every token)
|
||||
case 't':
|
||||
return scan_literal("rue", token_type::literal_true);
|
||||
{
|
||||
std::array<char_type, 4> true_literal = {{static_cast<char_type>('t'), static_cast<char_type>('r'), static_cast<char_type>('u'), static_cast<char_type>('e')}};
|
||||
return scan_literal(true_literal.data(), true_literal.size(), token_type::literal_true);
|
||||
}
|
||||
case 'f':
|
||||
return scan_literal("alse", token_type::literal_false);
|
||||
{
|
||||
std::array<char_type, 5> false_literal = {{static_cast<char_type>('f'), static_cast<char_type>('a'), static_cast<char_type>('l'), static_cast<char_type>('s'), static_cast<char_type>('e')}};
|
||||
return scan_literal(false_literal.data(), false_literal.size(), token_type::literal_false);
|
||||
}
|
||||
case 'n':
|
||||
return scan_literal("ull", token_type::literal_null);
|
||||
{
|
||||
std::array<char_type, 4> null_literal = {{static_cast<char_type>('n'), static_cast<char_type>('u'), static_cast<char_type>('l'), static_cast<char_type>('l')}};
|
||||
return scan_literal(null_literal.data(), null_literal.size(), token_type::literal_null);
|
||||
}
|
||||
|
||||
// string
|
||||
case '\"':
|
||||
@@ -2581,11 +2456,6 @@ scan_number_done:
|
||||
/// whether the next get() call should just return current
|
||||
bool next_unget = false;
|
||||
|
||||
/// the text of the current float token in the input, not yet copied into
|
||||
/// token_buffer (see get_string()), or nullptr
|
||||
const char* pending_number_text = nullptr;
|
||||
std::size_t pending_number_length = 0;
|
||||
|
||||
/// the start position of the current token
|
||||
position_t position {};
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
#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 <type_traits> // conditional, integral_constant, is_signed, true_type, false_type
|
||||
@@ -274,77 +275,13 @@ JSON_HEDLEY_ALWAYS_INLINE std::uint32_t parse_eight_digits(std::uint64_t v) noex
|
||||
return static_cast<std::uint32_t>(((v & 0x0000FFFF0000FFFFu) * 42949672960001u) >> 32u);
|
||||
}
|
||||
|
||||
/// whether the eight bytes in @a v (see read_eight_bytes()) are all ASCII
|
||||
/// digits (after fast_float)
|
||||
JSON_HEDLEY_ALWAYS_INLINE bool is_eight_digits(std::uint64_t v) noexcept
|
||||
/// whether [first, last) contains a digit other than '0'
|
||||
inline bool has_nonzero_digit(const char* first, const char* last) noexcept
|
||||
{
|
||||
return ((v & 0xF0F0F0F0F0F0F0F0u) | (((v + 0x0606060606060606u) & 0xF0F0F0F0F0F0F0F0u) >> 4u)) == 0x3333333333333333u;
|
||||
}
|
||||
|
||||
/*!
|
||||
@brief skip the ASCII digits starting at data[i] and accumulate their value
|
||||
|
||||
Eight digits are taken at a time while at least eight bytes remain. @a w wraps
|
||||
around after 19 digits; the caller only uses it for shorter digit runs.
|
||||
|
||||
@return the index of the first byte at or after @a i that is not a digit
|
||||
*/
|
||||
JSON_HEDLEY_ALWAYS_INLINE std::size_t accumulate_digits(const char* data, std::size_t i, std::size_t avail, std::uint64_t& w) noexcept
|
||||
{
|
||||
while (avail - i >= 8)
|
||||
return std::any_of(first, last, [](char c) noexcept
|
||||
{
|
||||
const std::uint64_t v = read_eight_bytes(data + i);
|
||||
if (!is_eight_digits(v))
|
||||
{
|
||||
break;
|
||||
}
|
||||
w = (w * 100000000u) + parse_eight_digits(v);
|
||||
i += 8;
|
||||
}
|
||||
while (i < avail && data[i] >= '0' && data[i] <= '9')
|
||||
{
|
||||
w = (w * 10u) + static_cast<std::uint64_t>(data[i] - '0');
|
||||
++i;
|
||||
}
|
||||
return i;
|
||||
}
|
||||
|
||||
/// whether [first, last) contains a digit other than '0'; a plain loop, as
|
||||
/// GCC calls std::any_of out of line even for the usual empty range
|
||||
JSON_HEDLEY_ALWAYS_INLINE bool has_nonzero_digit(const char* first, const char* last) noexcept
|
||||
{
|
||||
for (; first != last; ++first)
|
||||
{
|
||||
if (*first != '0')
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/*!
|
||||
@brief accumulate the validated digits [p, last) into @a w, as long as
|
||||
@a remaining digits still fit into it
|
||||
|
||||
Eight digits are taken at a time while at least eight remain, and @a remaining
|
||||
is decreased by the number of digits taken.
|
||||
|
||||
@return a pointer past the last digit taken
|
||||
*/
|
||||
JSON_HEDLEY_ALWAYS_INLINE const char* accumulate_significant_digits(const char* p, const char* last, int& remaining, std::uint64_t& w) noexcept
|
||||
{
|
||||
while (remaining >= 8 && last - p >= 8)
|
||||
{
|
||||
w = (w * 100000000u) + parse_eight_digits(read_eight_bytes(p));
|
||||
p += 8;
|
||||
remaining -= 8;
|
||||
}
|
||||
for (; remaining > 0 && p != last; ++p, --remaining)
|
||||
{
|
||||
w = (w * 10u) + static_cast<std::uint64_t>(*p - '0');
|
||||
}
|
||||
return p;
|
||||
return c != '0';
|
||||
});
|
||||
}
|
||||
|
||||
/// the value of the validated exponent digits [+-]?[0-9]+ in [first, last),
|
||||
@@ -390,7 +327,7 @@ truncated is set.
|
||||
std::string::npos if there is none
|
||||
@param[in] mantissa_end index of the 'e'/'E', or the token length
|
||||
*/
|
||||
JSON_HEDLEY_ALWAYS_INLINE float_significand parse_float_significand(const char* first, const char* last,
|
||||
inline float_significand parse_float_significand(const char* first, const char* last,
|
||||
std::size_t decimal_point_position, std::size_t mantissa_end) noexcept
|
||||
{
|
||||
float_significand s;
|
||||
@@ -405,7 +342,16 @@ JSON_HEDLEY_ALWAYS_INLINE float_significand parse_float_significand(const char*
|
||||
int remaining = 19; // digits that still fit into w
|
||||
if (*p != '0') // the integer part is "0" or [1-9][0-9]*
|
||||
{
|
||||
p = accumulate_significant_digits(p, integer_last, remaining, w);
|
||||
while (remaining >= 8 && integer_last - p >= 8)
|
||||
{
|
||||
w = (w * 100000000u) + parse_eight_digits(read_eight_bytes(p));
|
||||
p += 8;
|
||||
remaining -= 8;
|
||||
}
|
||||
for (; remaining > 0 && p != integer_last; ++p, --remaining)
|
||||
{
|
||||
w = (w * 10u) + static_cast<std::uint64_t>(*p - '0');
|
||||
}
|
||||
s.exponent = integer_last - p;
|
||||
s.truncated = has_nonzero_digit(p, integer_last);
|
||||
}
|
||||
@@ -424,7 +370,16 @@ JSON_HEDLEY_ALWAYS_INLINE float_significand parse_float_significand(const char*
|
||||
s.exponent -= p - zeros;
|
||||
}
|
||||
const char* const digits = p;
|
||||
p = accumulate_significant_digits(p, mantissa_last, remaining, w);
|
||||
while (remaining >= 8 && mantissa_last - p >= 8)
|
||||
{
|
||||
w = (w * 100000000u) + parse_eight_digits(read_eight_bytes(p));
|
||||
p += 8;
|
||||
remaining -= 8;
|
||||
}
|
||||
for (; remaining > 0 && p != mantissa_last; ++p, --remaining)
|
||||
{
|
||||
w = (w * 10u) + static_cast<std::uint64_t>(*p - '0');
|
||||
}
|
||||
s.exponent -= p - digits;
|
||||
s.truncated = s.truncated || has_nonzero_digit(p, mantissa_last);
|
||||
}
|
||||
@@ -757,14 +712,15 @@ std::uint64_t digit_comparison(const char* first, const char* last, std::uint64_
|
||||
inline void float_from_bits(std::uint64_t bits, double& value) noexcept
|
||||
{
|
||||
static_assert(sizeof(double) == sizeof(std::uint64_t), "double must have 64 bits");
|
||||
value = bit_cast<double>(bits);
|
||||
std::memcpy(&value, &bits, sizeof(value));
|
||||
}
|
||||
|
||||
/// the float with the IEEE-754 bits @a bits (the lower 32)
|
||||
inline void float_from_bits(std::uint64_t bits, float& value) noexcept
|
||||
{
|
||||
static_assert(sizeof(float) == sizeof(std::uint32_t), "float must have 32 bits");
|
||||
value = bit_cast<float>(static_cast<std::uint32_t>(bits));
|
||||
const auto bits32 = static_cast<std::uint32_t>(bits);
|
||||
std::memcpy(&value, &bits32, sizeof(bits32));
|
||||
}
|
||||
|
||||
/// the powers of ten that are exact in binary64 (up to 10^22)
|
||||
|
||||
@@ -51,20 +51,6 @@ template<typename BasicJsonType>
|
||||
using parser_callback_t =
|
||||
std::function<bool(int /*depth*/, parse_event_t /*event*/, BasicJsonType& /*parsed*/)>;
|
||||
|
||||
/// whether a SAX handler ignores the text of float tokens (the library's own
|
||||
/// handlers do), so that the parser need not materialize it
|
||||
template<typename SAX>
|
||||
struct sax_ignores_number_text : std::false_type {};
|
||||
|
||||
template<typename BasicJsonType, typename InputAdapterType>
|
||||
struct sax_ignores_number_text<json_sax_dom_parser<BasicJsonType, InputAdapterType>> : std::true_type {};
|
||||
|
||||
template<typename BasicJsonType, typename InputAdapterType>
|
||||
struct sax_ignores_number_text<json_sax_dom_callback_parser<BasicJsonType, InputAdapterType>> : std::true_type {};
|
||||
|
||||
template<typename BasicJsonType>
|
||||
struct sax_ignores_number_text<json_sax_acceptor<BasicJsonType>> : std::true_type {};
|
||||
|
||||
/*!
|
||||
@brief syntax analysis
|
||||
|
||||
@@ -185,23 +171,6 @@ class parser
|
||||
}
|
||||
|
||||
private:
|
||||
/// the text of the current float token for @a sax (see sax_ignores_number_text)
|
||||
template<typename SAX>
|
||||
const string_t& number_float_text(SAX* /*sax*/)
|
||||
{
|
||||
return number_float_text(std::integral_constant<bool, sax_ignores_number_text<SAX>::value> {});
|
||||
}
|
||||
|
||||
const string_t& number_float_text(std::true_type /*ignored*/) const noexcept
|
||||
{
|
||||
return m_lexer.get_ignored_number_text();
|
||||
}
|
||||
|
||||
string_t& number_float_text(std::false_type /*ignored*/)
|
||||
{
|
||||
return m_lexer.get_string();
|
||||
}
|
||||
|
||||
/*!
|
||||
@brief run a DOM SAX parser to completion and position the lexer
|
||||
|
||||
@@ -244,9 +213,8 @@ class parser
|
||||
bool sax_parse_internal(SAX* sax)
|
||||
{
|
||||
// stack to remember the hierarchy of structured values we are parsing
|
||||
// true = array; false = object (bytes, not std::vector<bool>, whose
|
||||
// push_back() is a call with GCC)
|
||||
std::vector<std::uint8_t> states;
|
||||
// true = array; false = object
|
||||
std::vector<bool> states;
|
||||
// value to avoid a goto (see comment where set to true)
|
||||
bool skip_to_state_evaluation = false;
|
||||
|
||||
@@ -279,7 +247,7 @@ class parser
|
||||
{
|
||||
return syntax_error(*sax, exception_message(token_type::value_string, "object key"));
|
||||
}
|
||||
if (JSON_HEDLEY_UNLIKELY(!sax->key(m_lexer.get_string_token())))
|
||||
if (JSON_HEDLEY_UNLIKELY(!sax->key(m_lexer.get_string())))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
@@ -291,7 +259,7 @@ class parser
|
||||
}
|
||||
|
||||
// remember we are now inside an object
|
||||
states.push_back(0);
|
||||
states.push_back(false);
|
||||
|
||||
// parse values
|
||||
get_token();
|
||||
@@ -316,7 +284,7 @@ class parser
|
||||
}
|
||||
|
||||
// remember we are now inside an array
|
||||
states.push_back(1);
|
||||
states.push_back(true);
|
||||
|
||||
// parse values (no need to call get_token)
|
||||
continue;
|
||||
@@ -333,7 +301,7 @@ class parser
|
||||
out_of_range::create(exception_id::number_overflow, concat("number overflow parsing '", m_lexer.get_token_string(), '\''), nullptr));
|
||||
}
|
||||
|
||||
if (JSON_HEDLEY_UNLIKELY(!sax->number_float(res, number_float_text(sax))))
|
||||
if (JSON_HEDLEY_UNLIKELY(!sax->number_float(res, m_lexer.get_string())))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
@@ -379,7 +347,7 @@ class parser
|
||||
|
||||
case token_type::value_string:
|
||||
{
|
||||
if (JSON_HEDLEY_UNLIKELY(!sax->string(m_lexer.get_string_token())))
|
||||
if (JSON_HEDLEY_UNLIKELY(!sax->string(m_lexer.get_string())))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
@@ -433,7 +401,7 @@ class parser
|
||||
return true;
|
||||
}
|
||||
|
||||
if (states.back() != 0) // array
|
||||
if (states.back()) // array
|
||||
{
|
||||
// comma -> next value
|
||||
// or end of array (ignore_trailing_commas = true)
|
||||
@@ -488,7 +456,7 @@ class parser
|
||||
return syntax_error(*sax, exception_message(token_type::value_string, "object key"));
|
||||
}
|
||||
|
||||
if (JSON_HEDLEY_UNLIKELY(!sax->key(m_lexer.get_string_token())))
|
||||
if (JSON_HEDLEY_UNLIKELY(!sax->key(m_lexer.get_string())))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -1,321 +0,0 @@
|
||||
// __ _____ _____ _____
|
||||
// __| | __| | | | JSON for Modern C++
|
||||
// | | |__ | | | | | | version 3.12.0
|
||||
// |_____|_____|_____|_|___| https://github.com/nlohmann/json
|
||||
//
|
||||
// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann <https://nlohmann.me>
|
||||
// SPDX-FileCopyrightText: 2018-2025 The simdjson authors <https://github.com/simdjson/simdjson>
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <array> // array
|
||||
#include <atomic> // atomic
|
||||
#include <cstddef> // size_t
|
||||
#include <cstdint> // uint8_t, uint64_t
|
||||
|
||||
#include <nlohmann/detail/bit_ops.hpp>
|
||||
#include <nlohmann/detail/macro_scope.hpp>
|
||||
|
||||
// Vector code for runs of string bytes, used by the lexer for contiguous input
|
||||
// (see string_bulk_run()). NEON (AArch64) and SSE2 (x86-64) belong to the
|
||||
// baseline instruction sets and are used by default. The vector UTF-8 check
|
||||
// needs NEON or SSSE3. SSSE3 is not part of x86-64, and the code must not
|
||||
// depend on the flags of a translation unit (two translation units with
|
||||
// different flags would have different definitions of the same inline
|
||||
// functions): the check is compiled for SSSE3 with a function attribute and
|
||||
// used where the CPU has SSSE3 (all x86-64 CPUs since about 2011), else the
|
||||
// portable check. Code compiled for SSSE3 anyway (-mssse3 or more) skips the
|
||||
// CPU check. JSON_NO_SIMD selects the portable code everywhere.
|
||||
// NLOHMANN_JSON_NO_TARGET_ATTRIBUTE (for compilers that ignore the target
|
||||
// attribute and then reject the SSSE3 intrinsics) keeps the check portable
|
||||
// without disabling the SSE2 code.
|
||||
#if !defined(JSON_NO_SIMD) && defined(__aarch64__) && (defined(__GNUC__) || defined(__clang__)) && defined(__BYTE_ORDER__) && defined(__ORDER_LITTLE_ENDIAN__) && __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__
|
||||
#include <arm_neon.h>
|
||||
#define JSON_SCAN_NEON 1
|
||||
#else
|
||||
#define JSON_SCAN_NEON 0
|
||||
#endif
|
||||
// (x86 only: other targets can define __SSE2__ as well, e.g., WebAssembly with -msse2, but have no <cpuid.h>)
|
||||
#if !defined(JSON_NO_SIMD) && !JSON_SCAN_NEON && (defined(__x86_64__) || defined(__i386__) || defined(_M_X64) || defined(_M_IX86)) && !defined(_M_ARM64EC) && (defined(__SSE2__) || defined(_M_X64) || (defined(_M_IX86_FP) && _M_IX86_FP >= 2))
|
||||
#include <emmintrin.h>
|
||||
#define JSON_SCAN_SSE2 1
|
||||
#else
|
||||
#define JSON_SCAN_SSE2 0
|
||||
#endif
|
||||
#if JSON_SCAN_SSE2 && defined(__SSSE3__)
|
||||
#include <tmmintrin.h>
|
||||
#define JSON_SCAN_SSSE3 1 // NOLINT(cppcoreguidelines-macro-to-enum,modernize-macro-to-enum)
|
||||
#else
|
||||
#define JSON_SCAN_SSSE3 0 // NOLINT(cppcoreguidelines-macro-to-enum,modernize-macro-to-enum)
|
||||
#endif
|
||||
#if JSON_SCAN_SSE2 && !JSON_SCAN_SSSE3 && !defined(NLOHMANN_JSON_NO_TARGET_ATTRIBUTE) && ((defined(__clang__) && __clang_major__ >= 4) || (defined(__GNUC__) && !defined(__clang__) && (__GNUC__ > 4 || (__GNUC__ == 4 && __GNUC_MINOR__ >= 9)) && !defined(__cpp_modules)))
|
||||
// (GCC before 4.9 has no SSSE3 intrinsics without -mssse3; GCC with C++20
|
||||
// modules drops the target attribute from functions it reads from a module
|
||||
// interface and then rejects the intrinsics: "needs isa option -mssse3")
|
||||
// <cpuid.h> of GCC before 11 has no include guard, and json.hpp and
|
||||
// json_view.hpp both include it
|
||||
#ifndef NLOHMANN_JSON_CPUID_INCLUDED
|
||||
#define NLOHMANN_JSON_CPUID_INCLUDED
|
||||
#include <cpuid.h>
|
||||
#endif
|
||||
#include <tmmintrin.h>
|
||||
#define JSON_SCAN_SSSE3_DISPATCH 1 // NOLINT(cppcoreguidelines-macro-to-enum,modernize-macro-to-enum)
|
||||
#define JSON_SCAN_SSSE3_TARGET __attribute__((target("ssse3")))
|
||||
#elif JSON_SCAN_SSE2 && !JSON_SCAN_SSSE3 && defined(_MSC_VER)
|
||||
// (MSVC compiles intrinsics of any instruction set)
|
||||
#include <intrin.h>
|
||||
#include <tmmintrin.h>
|
||||
#define JSON_SCAN_SSSE3_DISPATCH 1 // NOLINT(cppcoreguidelines-macro-to-enum,modernize-macro-to-enum)
|
||||
#define JSON_SCAN_SSSE3_TARGET
|
||||
#else
|
||||
#define JSON_SCAN_SSSE3_DISPATCH 0 // NOLINT(cppcoreguidelines-macro-to-enum,modernize-macro-to-enum)
|
||||
#define JSON_SCAN_SSSE3_TARGET
|
||||
#endif
|
||||
#define JSON_SCAN_VECTOR (JSON_SCAN_NEON || JSON_SCAN_SSE2)
|
||||
#define JSON_SCAN_VECTOR_UTF8 (JSON_SCAN_NEON || JSON_SCAN_SSSE3 || JSON_SCAN_SSSE3_DISPATCH)
|
||||
|
||||
NLOHMANN_JSON_NAMESPACE_BEGIN
|
||||
namespace detail
|
||||
{
|
||||
|
||||
#if JSON_SCAN_VECTOR
|
||||
/*!
|
||||
@brief the first byte of a string run that is a quote, a backslash, a control
|
||||
character, or not ASCII, 16 bytes per step
|
||||
|
||||
Stops at such a byte, or where fewer than 16 bytes are left (the caller tells
|
||||
the two apart). A signed compare with 0x20 finds control characters and
|
||||
non-ASCII bytes at once.
|
||||
*/
|
||||
JSON_HEDLEY_ALWAYS_INLINE const unsigned char* vector_plain_run(const unsigned char* p, const unsigned char* e) noexcept
|
||||
{
|
||||
while (e - p >= 16)
|
||||
{
|
||||
#if JSON_SCAN_NEON
|
||||
const uint8x16_t in = vld1q_u8(p);
|
||||
const uint8x16_t special = vorrq_u8(vorrq_u8(vceqq_u8(in, vdupq_n_u8('"')), vceqq_u8(in, vdupq_n_u8('\\'))),
|
||||
vcltq_s8(vreinterpretq_s8_u8(in), vdupq_n_s8(0x20)));
|
||||
// one nibble per byte (the usual NEON replacement of x86's movemask, see
|
||||
// D. Kutenin, "Porting x86 vector bitmask optimizations to Arm NEON", 2022)
|
||||
const std::uint64_t bits = vget_lane_u64(vreinterpret_u64_u8(vshrn_n_u16(vreinterpretq_u16_u8(special), 4)), 0);
|
||||
if (bits != 0)
|
||||
{
|
||||
return p + (count_trailing_zeros(bits) >> 2u);
|
||||
}
|
||||
#else
|
||||
const __m128i in = _mm_loadu_si128(static_cast<const __m128i*>(static_cast<const void*>(p))); // NOLINT(bugprone-casting-through-void)
|
||||
const __m128i special = _mm_or_si128(_mm_or_si128(_mm_cmpeq_epi8(in, _mm_set1_epi8('"')), _mm_cmpeq_epi8(in, _mm_set1_epi8('\\'))),
|
||||
_mm_cmplt_epi8(in, _mm_set1_epi8(0x20)));
|
||||
const auto bits = static_cast<std::uint64_t>(static_cast<unsigned>(_mm_movemask_epi8(special)));
|
||||
if (bits != 0)
|
||||
{
|
||||
return p + count_trailing_zeros(bits);
|
||||
}
|
||||
#endif
|
||||
p += 16;
|
||||
}
|
||||
return p;
|
||||
}
|
||||
#endif
|
||||
|
||||
#if JSON_SCAN_VECTOR
|
||||
/// vector_plain_run() for runs that are already longer than a few words:
|
||||
/// inline with SSE2, out of line with NEON, where a larger string loop slows
|
||||
/// down the rest of the lexer
|
||||
#if JSON_SCAN_NEON
|
||||
JSON_HEDLEY_NEVER_INLINE inline
|
||||
#else
|
||||
JSON_HEDLEY_ALWAYS_INLINE
|
||||
#endif
|
||||
const unsigned char* vector_plain_run_long(const unsigned char* p, const unsigned char* e) noexcept
|
||||
{
|
||||
return vector_plain_run(p, e);
|
||||
}
|
||||
#endif
|
||||
|
||||
#if JSON_SCAN_SSSE3_DISPATCH
|
||||
/// whether the CPU has SSSE3 (CPUID leaf 1, ECX bit 9)
|
||||
inline bool cpu_ssse3() noexcept
|
||||
{
|
||||
#if defined(_MSC_VER) && !defined(__clang__)
|
||||
std::array<int, 4> regs {{}};
|
||||
__cpuid(regs.data(), 1);
|
||||
return (static_cast<unsigned>(regs[2]) & (1u << 9u)) != 0;
|
||||
#else
|
||||
unsigned eax = 0;
|
||||
unsigned ebx = 0;
|
||||
unsigned ecx = 0;
|
||||
unsigned edx = 0;
|
||||
return __get_cpuid(1, &eax, &ebx, &ecx, &edx) != 0 && (ecx & (1u << 9u)) != 0;
|
||||
#endif
|
||||
}
|
||||
|
||||
/// whether the CPU has SSSE3, asked once: the answer is kept in an atomic
|
||||
/// that is initialized at compile time, so that neither a guard of a local
|
||||
/// static nor a global constructor is needed (threads that ask at the same
|
||||
/// time all store the same answer)
|
||||
JSON_HEDLEY_ALWAYS_INLINE bool cpu_has_ssse3() noexcept
|
||||
{
|
||||
static std::atomic<int> known{0}; // 0: not asked yet, 1: no, 2: yes
|
||||
int state = known.load(std::memory_order_relaxed);
|
||||
if (JSON_HEDLEY_UNLIKELY(state == 0))
|
||||
{
|
||||
state = cpu_ssse3() ? 2 : 1;
|
||||
known.store(state, std::memory_order_relaxed);
|
||||
}
|
||||
return state == 2;
|
||||
}
|
||||
#endif
|
||||
|
||||
#if JSON_SCAN_VECTOR_UTF8
|
||||
/// Tables of the UTF-8 check of J. Keiser and D. Lemire, "Validating UTF-8 In
|
||||
/// Less Than One Instruction Per Byte" (2021), as in simdjson ("lookup4"): each
|
||||
/// maps a nibble (high and low nibble of the previous byte, high nibble of the
|
||||
/// current byte) to the errors it allows; a byte pair is ill-formed if all
|
||||
/// three have an error bit in common.
|
||||
template<typename Dummy = void>
|
||||
struct utf8_lookup4
|
||||
{
|
||||
static constexpr std::uint8_t too_short = 1u << 0u, too_long = 1u << 1u, overlong_3 = 1u << 2u, too_large = 1u << 3u;
|
||||
static constexpr std::uint8_t surrogate = 1u << 4u, overlong_2 = 1u << 5u, too_large_1000 = 1u << 6u, overlong_4 = 1u << 6u;
|
||||
static constexpr std::uint8_t two_conts = 1u << 7u, carry = too_short | too_long | two_conts;
|
||||
static const std::array<std::uint8_t, 16> byte_1_high;
|
||||
static const std::array<std::uint8_t, 16> byte_1_low;
|
||||
static const std::array<std::uint8_t, 16> byte_2_high;
|
||||
};
|
||||
|
||||
template<typename Dummy>
|
||||
const std::array<std::uint8_t, 16> utf8_lookup4<Dummy>::byte_1_high =
|
||||
{
|
||||
{
|
||||
too_long, too_long, too_long, too_long, too_long, too_long, too_long, too_long,
|
||||
two_conts, two_conts, two_conts, two_conts,
|
||||
too_short | overlong_2, too_short, too_short | overlong_3 | surrogate, too_short | too_large | too_large_1000 | overlong_4
|
||||
}
|
||||
};
|
||||
|
||||
template<typename Dummy>
|
||||
const std::array<std::uint8_t, 16> utf8_lookup4<Dummy>::byte_1_low =
|
||||
{
|
||||
{
|
||||
carry | overlong_3 | overlong_2 | overlong_4, carry | overlong_2, carry, carry,
|
||||
carry | too_large, carry | too_large | too_large_1000, carry | too_large | too_large_1000, carry | too_large | too_large_1000,
|
||||
carry | too_large | too_large_1000, carry | too_large | too_large_1000, carry | too_large | too_large_1000, carry | too_large | too_large_1000,
|
||||
carry | too_large | too_large_1000, carry | too_large | too_large_1000 | surrogate, carry | too_large | too_large_1000, carry | too_large | too_large_1000
|
||||
}
|
||||
};
|
||||
|
||||
template<typename Dummy>
|
||||
const std::array<std::uint8_t, 16> utf8_lookup4<Dummy>::byte_2_high =
|
||||
{
|
||||
{
|
||||
too_short, too_short, too_short, too_short, too_short, too_short, too_short, too_short,
|
||||
static_cast<std::uint8_t>(too_long | overlong_2 | two_conts | overlong_3 | too_large_1000 | overlong_4),
|
||||
static_cast<std::uint8_t>(too_long | overlong_2 | two_conts | overlong_3 | too_large),
|
||||
static_cast<std::uint8_t>(too_long | overlong_2 | two_conts | surrogate | too_large),
|
||||
static_cast<std::uint8_t>(too_long | overlong_2 | two_conts | surrogate | too_large),
|
||||
too_short, too_short, too_short, too_short
|
||||
}
|
||||
};
|
||||
|
||||
/*!
|
||||
@brief the run of plain string bytes and well-formed UTF-8 at p (a character
|
||||
boundary), 16 bytes per step
|
||||
|
||||
The first quote, backslash, or control character is found with vector
|
||||
compares, and the UTF-8 check of J. Keiser and D. Lemire covers the bytes up
|
||||
to it. Returns true with @a block at that byte; returns false with @a block at
|
||||
the start of the 16 bytes where the check found ill-formed UTF-8 or where
|
||||
fewer than 16 bytes are left, for the caller to finish one sequence at a time.
|
||||
Out of line, so that no constants of the check occupy registers in the
|
||||
caller's loop. On x86-64, it is compiled for SSSE3 (see cpu_has_ssse3()).
|
||||
*/
|
||||
JSON_SCAN_SSSE3_TARGET JSON_HEDLEY_NEVER_INLINE inline bool vector_utf8_run(const unsigned char* p, const unsigned char* e, const unsigned char*& block_out) noexcept
|
||||
{
|
||||
using lookup = utf8_lookup4<>;
|
||||
const unsigned char* block = p;
|
||||
#if JSON_SCAN_NEON
|
||||
const uint8x16_t t1h = vld1q_u8(lookup::byte_1_high.data());
|
||||
const uint8x16_t t1l = vld1q_u8(lookup::byte_1_low.data());
|
||||
const uint8x16_t t2h = vld1q_u8(lookup::byte_2_high.data());
|
||||
uint8x16_t prev = vdupq_n_u8(0);
|
||||
while (e - block >= 16)
|
||||
{
|
||||
const uint8x16_t in = vld1q_u8(block);
|
||||
const uint8x16_t special = vorrq_u8(vorrq_u8(vceqq_u8(in, vdupq_n_u8('"')), vceqq_u8(in, vdupq_n_u8('\\'))), vcltq_u8(in, vdupq_n_u8(0x20)));
|
||||
const uint8x16_t prev1 = vextq_u8(prev, in, 15);
|
||||
const uint8x16_t sc = vandq_u8(vandq_u8(vqtbl1q_u8(t1h, vshrq_n_u8(prev1, 4)), vqtbl1q_u8(t1l, vandq_u8(prev1, vdupq_n_u8(0x0F)))), vqtbl1q_u8(t2h, vshrq_n_u8(in, 4)));
|
||||
const uint8x16_t must23 = vorrq_u8(vqsubq_u8(vextq_u8(prev, in, 14), vdupq_n_u8(0xE0 - 0x80)), vqsubq_u8(vextq_u8(prev, in, 13), vdupq_n_u8(0xF0 - 0x80)));
|
||||
const uint8x16_t err = veorq_u8(vandq_u8(must23, vdupq_n_u8(0x80)), sc);
|
||||
const std::uint64_t special_bits = vget_lane_u64(vreinterpret_u64_u8(vshrn_n_u16(vreinterpretq_u16_u8(special), 4)), 0);
|
||||
const std::uint64_t err_bits = vget_lane_u64(vreinterpret_u64_u8(vshrn_n_u16(vreinterpretq_u16_u8(vtstq_u8(err, err)), 4)), 0);
|
||||
if (special_bits != 0)
|
||||
{
|
||||
// errors up to the special byte count (an incomplete sequence
|
||||
// before a quote shows at the quote); the bytes after it do not
|
||||
const unsigned k = static_cast<unsigned>(count_trailing_zeros(special_bits)) >> 2u;
|
||||
const std::uint64_t upto = k == 15 ? ~std::uint64_t{0} :
|
||||
(std::uint64_t{1} << (4u * (k + 1u))) - 1u;
|
||||
if ((err_bits & upto) == 0)
|
||||
{
|
||||
block_out = block + k;
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
if (err_bits != 0)
|
||||
{
|
||||
break;
|
||||
}
|
||||
prev = in;
|
||||
block += 16;
|
||||
}
|
||||
#else
|
||||
// the same with SSSE3 (pshufb for the table lookups; nibbles from 16-bit
|
||||
// shifts, as there are no byte shifts)
|
||||
const __m128i t1h = _mm_loadu_si128(static_cast<const __m128i*>(static_cast<const void*>(lookup::byte_1_high.data()))); // NOLINT(bugprone-casting-through-void)
|
||||
const __m128i t1l = _mm_loadu_si128(static_cast<const __m128i*>(static_cast<const void*>(lookup::byte_1_low.data()))); // NOLINT(bugprone-casting-through-void)
|
||||
const __m128i t2h = _mm_loadu_si128(static_cast<const __m128i*>(static_cast<const void*>(lookup::byte_2_high.data()))); // NOLINT(bugprone-casting-through-void)
|
||||
const __m128i nibble = _mm_set1_epi8(0x0F);
|
||||
const __m128i zero = _mm_setzero_si128();
|
||||
__m128i prev = zero;
|
||||
while (e - block >= 16)
|
||||
{
|
||||
const __m128i in = _mm_loadu_si128(static_cast<const __m128i*>(static_cast<const void*>(block))); // NOLINT(bugprone-casting-through-void)
|
||||
const __m128i special = _mm_or_si128(_mm_or_si128(_mm_cmpeq_epi8(in, _mm_set1_epi8('"')), _mm_cmpeq_epi8(in, _mm_set1_epi8('\\'))),
|
||||
_mm_cmpeq_epi8(_mm_subs_epu8(in, _mm_set1_epi8(0x1F)), zero)); // in < 0x20
|
||||
const __m128i prev1 = _mm_alignr_epi8(in, prev, 15);
|
||||
const __m128i sc = _mm_and_si128(_mm_and_si128(_mm_shuffle_epi8(t1h, _mm_and_si128(_mm_srli_epi16(prev1, 4), nibble)),
|
||||
_mm_shuffle_epi8(t1l, _mm_and_si128(prev1, nibble))),
|
||||
_mm_shuffle_epi8(t2h, _mm_and_si128(_mm_srli_epi16(in, 4), nibble)));
|
||||
const __m128i must23 = _mm_or_si128(_mm_subs_epu8(_mm_alignr_epi8(in, prev, 14), _mm_set1_epi8(0xE0 - 0x80)),
|
||||
_mm_subs_epu8(_mm_alignr_epi8(in, prev, 13), _mm_set1_epi8(0xF0 - 0x80)));
|
||||
const __m128i err = _mm_xor_si128(_mm_and_si128(must23, _mm_set1_epi8(static_cast<char>(-128))), sc);
|
||||
const auto special_bits = static_cast<unsigned>(_mm_movemask_epi8(special));
|
||||
const auto err_bits = ~static_cast<unsigned>(_mm_movemask_epi8(_mm_cmpeq_epi8(err, zero))) & 0xFFFFu;
|
||||
if (special_bits != 0)
|
||||
{
|
||||
const auto k = static_cast<unsigned>(count_trailing_zeros(static_cast<std::uint64_t>(special_bits)));
|
||||
if ((err_bits & ((2u << k) - 1u)) == 0)
|
||||
{
|
||||
block_out = block + k;
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
if (err_bits != 0)
|
||||
{
|
||||
break;
|
||||
}
|
||||
prev = in;
|
||||
block += 16;
|
||||
}
|
||||
#endif
|
||||
block_out = block;
|
||||
return false;
|
||||
}
|
||||
#endif
|
||||
|
||||
} // namespace detail
|
||||
NLOHMANN_JSON_NAMESPACE_END
|
||||
@@ -14,7 +14,6 @@
|
||||
#include <cstring> // memcpy
|
||||
|
||||
#include <nlohmann/detail/bit_ops.hpp>
|
||||
#include <nlohmann/detail/input/simd_scan.hpp>
|
||||
#include <nlohmann/detail/macro_scope.hpp>
|
||||
|
||||
// Optional SIMD backend for bulk UTF-8 validation. This is an opt-in external
|
||||
@@ -35,7 +34,7 @@
|
||||
// This file contains the byte-level string-scanning helpers used by the lexer's
|
||||
// contiguous fast path. They operate purely on raw bytes (no dependency on the
|
||||
// lexer's template parameters) so they are free functions, keeping the lexer
|
||||
// itself focused on the state machine; see lexer::get_after_string_run().
|
||||
// itself focused on the state machine; see lexer::scan_string_bulk().
|
||||
|
||||
NLOHMANN_JSON_NAMESPACE_BEGIN
|
||||
namespace detail
|
||||
@@ -67,7 +66,7 @@ inline std::uint64_t swar_string_special(std::uint64_t v) noexcept
|
||||
|
||||
// return the index of the first is_string_special() byte in [data, data+n), or
|
||||
// n if every byte is ordinary; scans 8 bytes at a time
|
||||
JSON_HEDLEY_ALWAYS_INLINE std::size_t find_string_special(const unsigned char* data, std::size_t n) noexcept
|
||||
inline std::size_t find_string_special(const unsigned char* data, std::size_t n) noexcept
|
||||
{
|
||||
std::size_t i = 0;
|
||||
for (; i + 8 <= n; i += 8)
|
||||
@@ -236,143 +235,35 @@ inline std::size_t valid_utf8_prefix(const unsigned char* data, std::size_t n) n
|
||||
return pos;
|
||||
}
|
||||
|
||||
#if JSON_SCAN_VECTOR_UTF8
|
||||
/// the end of scan_string_vector from block, where the vector loop stopped
|
||||
/// (ill-formed UTF-8, or fewer than 16 bytes left): one byte or sequence at a
|
||||
/// time, from the start of a sequence that crosses into the block
|
||||
inline const unsigned char* scan_string_finish(const unsigned char* p, const unsigned char* block, const unsigned char* e) noexcept
|
||||
{
|
||||
for (int i = 1; i <= 3 && block - i >= p; ++i)
|
||||
{
|
||||
const unsigned char c = block[-i];
|
||||
if (c < 0x80)
|
||||
{
|
||||
break;
|
||||
}
|
||||
if (c >= 0xC0)
|
||||
{
|
||||
const int len = 2 + static_cast<int>(c >= 0xE0) + static_cast<int>(c >= 0xF0);
|
||||
if (len > i)
|
||||
{
|
||||
block -= i;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
for (p = block; p != e;)
|
||||
{
|
||||
if (*p < 0x80)
|
||||
{
|
||||
if (is_string_special(*p))
|
||||
{
|
||||
return p;
|
||||
}
|
||||
++p;
|
||||
continue;
|
||||
}
|
||||
const std::size_t n = validate_one_utf8(p, static_cast<std::size_t>(e - p));
|
||||
if (n == 0)
|
||||
{
|
||||
return p;
|
||||
}
|
||||
p += n;
|
||||
}
|
||||
return p;
|
||||
}
|
||||
|
||||
/// the rest of a string from p (a character boundary), with the vector UTF-8
|
||||
/// check and the scalar check for the end of the block where it stopped
|
||||
inline const unsigned char* scan_string_vector(const unsigned char* p, const unsigned char* e) noexcept
|
||||
{
|
||||
const unsigned char* block = p;
|
||||
if (vector_utf8_run(p, e, block))
|
||||
{
|
||||
return block;
|
||||
}
|
||||
return scan_string_finish(p, block, e);
|
||||
}
|
||||
#endif
|
||||
|
||||
#if JSON_SCAN_VECTOR
|
||||
/// the end of the run of plain string characters at @a p: the first quote,
|
||||
/// backslash, control character, or non-ASCII byte, or @a e. The first words
|
||||
/// are checked with SWAR (one; four with NEON, where the vector mask costs
|
||||
/// more), as most keys and short values end there; longer runs continue 16
|
||||
/// bytes at a time.
|
||||
JSON_HEDLEY_ALWAYS_INLINE const unsigned char* plain_run_end(const unsigned char* p, const unsigned char* e) noexcept
|
||||
{
|
||||
constexpr int prefix_words = JSON_SCAN_NEON ? 4 : 1;
|
||||
for (int words = prefix_words; words > 0 && e - p >= 8; --words)
|
||||
{
|
||||
const std::uint64_t special = swar_string_special(read_eight_bytes(p));
|
||||
if (special != 0)
|
||||
{
|
||||
return p + (static_cast<std::size_t>(count_trailing_zeros(special)) / 8);
|
||||
}
|
||||
p += 8;
|
||||
}
|
||||
// the vector loop only for 16 bytes or more: the serializer passes a
|
||||
// string without its closing quote, so most of its runs end at e, and a
|
||||
// call of the out-of-line NEON loop for a shorter rest made dump() up to
|
||||
// 10% slower
|
||||
if (e - p >= 16)
|
||||
{
|
||||
p = vector_plain_run_long(p, e);
|
||||
if (e - p >= 16)
|
||||
{
|
||||
return p; // the vector loop stopped at a special byte
|
||||
}
|
||||
}
|
||||
return p + find_string_special(p, static_cast<std::size_t>(e - p));
|
||||
}
|
||||
#endif
|
||||
|
||||
// Computation of the bulk run length: the number of leading bytes in
|
||||
// [data, data+n) that are ordinary ASCII or complete well-formed UTF-8
|
||||
// Scalar (C++11) computation of the bulk run length: the number of leading
|
||||
// bytes in [data, data+n) that are ordinary ASCII or complete well-formed UTF-8
|
||||
// sequences, stopping before the first byte that needs individual handling (the
|
||||
// closing quote, an escape, a control character, or an ill-formed/truncated
|
||||
// sequence). ASCII is skipped 16 bytes at a time with SSE2 and 8 bytes at a
|
||||
// time otherwise; runs of non-ASCII text use the vector UTF-8 check where
|
||||
// available.
|
||||
JSON_HEDLEY_ALWAYS_INLINE std::size_t scalar_string_bulk_run(const unsigned char* data, std::size_t n) noexcept
|
||||
// sequence). ASCII is skipped 8 bytes at a time.
|
||||
inline std::size_t scalar_string_bulk_run(const unsigned char* data, std::size_t n) noexcept
|
||||
{
|
||||
const unsigned char* const e = data + n;
|
||||
const unsigned char* p = data;
|
||||
while (p != e)
|
||||
std::size_t pos = 0;
|
||||
while (pos < n)
|
||||
{
|
||||
#if JSON_SCAN_VECTOR
|
||||
p = plain_run_end(p, e);
|
||||
#else
|
||||
p += find_string_special(p, static_cast<std::size_t>(e - p));
|
||||
#endif
|
||||
if (p == e || *p < 0x80u)
|
||||
pos += find_string_special(data + pos, n - pos);
|
||||
if (pos >= n || data[pos] < 0x80u)
|
||||
{
|
||||
break; // end of buffer, or a quote/escape/control byte
|
||||
}
|
||||
#if JSON_SCAN_VECTOR_UTF8
|
||||
#if JSON_SCAN_SSSE3_DISPATCH
|
||||
if (JSON_HEDLEY_LIKELY(cpu_has_ssse3()))
|
||||
#endif
|
||||
{
|
||||
return static_cast<std::size_t>(scan_string_vector(p, e) - data);
|
||||
}
|
||||
#endif
|
||||
#if !JSON_SCAN_VECTOR_UTF8 || JSON_SCAN_SSSE3_DISPATCH
|
||||
// a run of multi-byte sequences (e.g. CJK text) is validated sequence
|
||||
// by sequence without searching for the next special byte in between
|
||||
do
|
||||
{
|
||||
const std::size_t seq = validate_one_utf8(p, static_cast<std::size_t>(e - p));
|
||||
const std::size_t seq = validate_one_utf8(data + pos, n - pos);
|
||||
if (seq == 0)
|
||||
{
|
||||
return static_cast<std::size_t>(p - data); // ill-formed or truncated: let the byte path diagnose it
|
||||
return pos; // ill-formed or truncated: let the byte path diagnose it
|
||||
}
|
||||
p += seq;
|
||||
pos += seq;
|
||||
}
|
||||
while (p != e && *p >= 0x80u);
|
||||
#endif
|
||||
while (pos < n && data[pos] >= 0x80u);
|
||||
}
|
||||
return static_cast<std::size_t>(p - data);
|
||||
return pos;
|
||||
}
|
||||
|
||||
#if defined(JSON_USE_SIMDUTF) && defined(JSON_HAS_CPP_17)
|
||||
@@ -413,7 +304,7 @@ inline std::size_t find_string_delimiter(const unsigned char* data, std::size_t
|
||||
// next delimiter is validated in one shot by simdutf; on the rare failure the
|
||||
// scalar helper recomputes the exact valid prefix so the byte path still
|
||||
// produces the precise diagnostic. Without it, the pure scalar path is used.
|
||||
JSON_HEDLEY_ALWAYS_INLINE std::size_t string_bulk_run(const unsigned char* data, std::size_t n) noexcept
|
||||
inline std::size_t string_bulk_run(const unsigned char* data, std::size_t n) noexcept
|
||||
{
|
||||
#if defined(JSON_USE_SIMDUTF) && defined(JSON_HAS_CPP_17)
|
||||
const std::size_t run = find_string_delimiter(data, n);
|
||||
@@ -425,42 +316,6 @@ JSON_HEDLEY_ALWAYS_INLINE std::size_t string_bulk_run(const unsigned char* data,
|
||||
return scalar_string_bulk_run(data, n);
|
||||
}
|
||||
|
||||
#if JSON_SCAN_NEON && !(defined(JSON_USE_SIMDUTF) && defined(JSON_HAS_CPP_17))
|
||||
/// scalar_string_bulk_run() from @a start on, out of line (see
|
||||
/// lexer_string_bulk_run())
|
||||
JSON_HEDLEY_NEVER_INLINE inline std::size_t string_bulk_run_from(const unsigned char* data, std::size_t start, std::size_t n) noexcept
|
||||
{
|
||||
return start + scalar_string_bulk_run(data + start, n - start);
|
||||
}
|
||||
#endif
|
||||
|
||||
// string_bulk_run() for the lexer. With NEON, only the first four words are
|
||||
// checked inline (most keys and short values end there), and longer runs and
|
||||
// non-ASCII text continue out of line. The lexer's scan function runs once per
|
||||
// token, and the vector code inlined into it needs more registers, which every
|
||||
// token pays for: 5% of json::accept() on jeopardy.json with Apple Clang on
|
||||
// Apple M1, even though no string of that file reaches the vector code. The
|
||||
// serializer keeps string_bulk_run() inline, which is faster there.
|
||||
JSON_HEDLEY_ALWAYS_INLINE std::size_t lexer_string_bulk_run(const unsigned char* data, std::size_t n) noexcept
|
||||
{
|
||||
#if JSON_SCAN_NEON && !(defined(JSON_USE_SIMDUTF) && defined(JSON_HAS_CPP_17))
|
||||
std::size_t i = 0;
|
||||
for (int words = 4; words > 0 && n - i >= 8; --words)
|
||||
{
|
||||
const std::uint64_t special = swar_string_special(read_eight_bytes(data + i));
|
||||
if (special != 0)
|
||||
{
|
||||
const std::size_t pos = i + (static_cast<std::size_t>(count_trailing_zeros(special)) / 8);
|
||||
return data[pos] < 0x80u ? pos : string_bulk_run_from(data, pos, n);
|
||||
}
|
||||
i += 8;
|
||||
}
|
||||
return string_bulk_run_from(data, i, n);
|
||||
#else
|
||||
return string_bulk_run(data, n);
|
||||
#endif
|
||||
}
|
||||
|
||||
// Decode the 4 hex digits at [data, data+4) - the digits following a `\u`
|
||||
// escape - into a codepoint 0x0000..0xFFFF via one table lookup per byte
|
||||
// (after yyjson's read_hex_u16), or return -1 if any of the 4 bytes is not a
|
||||
|
||||
@@ -9,13 +9,10 @@
|
||||
#pragma once
|
||||
|
||||
#include <algorithm> // all_of
|
||||
#include <array> // array
|
||||
#include <cctype> // isdigit
|
||||
#include <cerrno> // errno, ERANGE
|
||||
#include <cstddef> // size_t
|
||||
#include <cstdint> // uint64_t
|
||||
#include <cstdlib> // strtoull
|
||||
#include <cstring> // memcmp, memcpy
|
||||
#ifndef JSON_NO_IO
|
||||
#include <iosfwd> // ostream
|
||||
#endif // JSON_NO_IO
|
||||
@@ -27,7 +24,6 @@
|
||||
#include <nlohmann/detail/exceptions.hpp>
|
||||
#include <nlohmann/detail/macro_scope.hpp>
|
||||
#include <nlohmann/detail/meta/cpp_future.hpp>
|
||||
#include <nlohmann/detail/meta/type_traits.hpp>
|
||||
#include <nlohmann/detail/string_concat.hpp>
|
||||
#include <nlohmann/detail/string_escape.hpp>
|
||||
#include <nlohmann/detail/string_utils.hpp>
|
||||
@@ -503,111 +499,50 @@ class json_pointer
|
||||
|
||||
private:
|
||||
/*!
|
||||
@brief the pointer prefixes of a flattened object that denote arrays
|
||||
@brief the pointer prefixes of a flattened object, and which of them denote arrays
|
||||
|
||||
@ref unflatten creates arrays exactly below the prefixes that have a
|
||||
reference token 0 among their children, and objects everywhere else.
|
||||
Deciding this up front keeps the result independent of the order in which
|
||||
the flattened object is iterated, which is unspecified for some object
|
||||
types.
|
||||
@ref unflatten collects the pointer prefixes that have a reference token 0
|
||||
among their children; @ref get_and_create creates arrays exactly below
|
||||
those prefixes and objects everywhere else. Deciding this up front keeps
|
||||
the result independent of the order in which the flattened object is
|
||||
iterated, which is unspecified for some object types.
|
||||
|
||||
A prefix is identified by the text of the key it is the beginning of:
|
||||
escaping is unambiguous, so equal prefixes of valid keys are equal texts.
|
||||
The set refers to the keys, which must outlive it.
|
||||
The prefixes form a tree and are numbered (number 0 is the empty prefix),
|
||||
so each of them is stored only once (as a node) rather than as a copy of
|
||||
all of its reference tokens. The tree does not own the reference tokens:
|
||||
it refers to the tokens of the parsed keys, which must outlive it.
|
||||
*/
|
||||
class array_prefix_set
|
||||
struct prefix_tree
|
||||
{
|
||||
public:
|
||||
void insert(const char* data, std::size_t size)
|
||||
{
|
||||
// flattened objects are usually sorted, so most keys that have a
|
||||
// 0 yield the prefix of the previous one
|
||||
if (last_data != nullptr && last_size == size && std::memcmp(last_data, data, size) == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
last_data = data;
|
||||
last_size = size;
|
||||
|
||||
if (table.empty())
|
||||
{
|
||||
table.resize(16);
|
||||
}
|
||||
const std::size_t hash = hash_of(data, size);
|
||||
const std::size_t mask = table.size() - 1;
|
||||
std::size_t i = hash & mask;
|
||||
while (table[i].data != nullptr)
|
||||
{
|
||||
if (table[i].hash == hash && table[i].size == size && std::memcmp(table[i].data, data, size) == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
i = (i + 1) & mask;
|
||||
}
|
||||
table[i] = slot(hash, data, size);
|
||||
|
||||
// keep the load factor below 1/2
|
||||
if (++used * 2 > table.size())
|
||||
{
|
||||
grow();
|
||||
}
|
||||
}
|
||||
|
||||
bool empty() const noexcept
|
||||
{
|
||||
return used == 0;
|
||||
}
|
||||
|
||||
bool contains(const char* data, std::size_t size) const noexcept
|
||||
{
|
||||
if (used == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
const std::size_t hash = hash_of(data, size);
|
||||
const std::size_t mask = table.size() - 1;
|
||||
for (std::size_t i = hash & mask; table[i].data != nullptr; i = (i + 1) & mask)
|
||||
{
|
||||
if (table[i].hash == hash && table[i].size == size && std::memcmp(table[i].data, data, size) == 0)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private:
|
||||
// open-addressing hash table from (parent, last reference token) to
|
||||
// the number of the node; empty slots have token == nullptr
|
||||
struct slot
|
||||
{
|
||||
slot() noexcept = default;
|
||||
std::size_t hash;
|
||||
std::size_t parent;
|
||||
const string_t* token;
|
||||
std::size_t id;
|
||||
|
||||
slot(std::size_t h, const char* d, std::size_t s) noexcept
|
||||
: hash(h), data(d), size(s)
|
||||
slot() noexcept
|
||||
: hash(0), parent(0), token(nullptr), id(0)
|
||||
{}
|
||||
|
||||
std::size_t hash = 0;
|
||||
const char* data = nullptr; // nullptr: the slot is empty
|
||||
std::size_t size = 0;
|
||||
slot(std::size_t h, std::size_t p, const string_t* t, std::size_t i) noexcept
|
||||
: hash(h), parent(p), token(t), id(i)
|
||||
{}
|
||||
};
|
||||
|
||||
static std::size_t hash_of(const char* data, std::size_t size) noexcept
|
||||
std::vector<slot> table;
|
||||
std::size_t used = 0;
|
||||
|
||||
static std::size_t hash_of(std::size_t parent, const string_t& token) noexcept
|
||||
{
|
||||
std::uint64_t h = 0x9E3779B97F4A7C15ULL ^ size;
|
||||
std::size_t i = 0;
|
||||
for (; i + 8 <= size; i += 8)
|
||||
std::size_t h = parent * static_cast<std::size_t>(0x9E3779B97F4A7C15ULL & (std::numeric_limits<std::size_t>::max)());
|
||||
for (std::size_t i = 0; i < token.size(); ++i)
|
||||
{
|
||||
std::uint64_t w = 0;
|
||||
std::memcpy(&w, data + i, 8);
|
||||
h = (h ^ w) * 0x100000001B3ULL;
|
||||
h ^= h >> 32u;
|
||||
h = (h ^ static_cast<unsigned char>(token[i])) * static_cast<std::size_t>(0x100000001B3ULL & (std::numeric_limits<std::size_t>::max)());
|
||||
}
|
||||
std::uint64_t w = 0;
|
||||
if (i < size)
|
||||
{
|
||||
std::memcpy(&w, data + i, size - i);
|
||||
}
|
||||
h = (h ^ w) * 0x9E3779B97F4A7C15ULL;
|
||||
return detail::conditional_static_cast<std::size_t>(h ^ (h >> 29u));
|
||||
return h ^ (h >> 29u);
|
||||
}
|
||||
|
||||
void grow()
|
||||
@@ -617,10 +552,10 @@ class json_pointer
|
||||
const std::size_t mask = table.size() - 1;
|
||||
for (const auto& s : old)
|
||||
{
|
||||
if (s.data != nullptr)
|
||||
if (s.token != nullptr)
|
||||
{
|
||||
std::size_t i = s.hash & mask;
|
||||
while (table[i].data != nullptr)
|
||||
while (table[i].token != nullptr)
|
||||
{
|
||||
i = (i + 1) & mask;
|
||||
}
|
||||
@@ -629,160 +564,117 @@ class json_pointer
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<slot> table{};
|
||||
std::size_t used = 0;
|
||||
const char* last_data = nullptr;
|
||||
std::size_t last_size = 0;
|
||||
// parent[id] is the number of the prefix without its last token
|
||||
std::vector<std::size_t> parent;
|
||||
// is_array[id] is true iff some flattened key has the reference token
|
||||
// 0 directly below the prefix with number id
|
||||
std::vector<bool> is_array;
|
||||
|
||||
// start with the empty prefix only
|
||||
prefix_tree()
|
||||
: table(16)
|
||||
, parent(1, 0)
|
||||
, is_array(1, false)
|
||||
{}
|
||||
|
||||
// return the number of the prefix with number id extended by
|
||||
// reference_token, adding it if it is new
|
||||
std::size_t add_child(std::size_t id, const string_t& reference_token)
|
||||
{
|
||||
const std::size_t hash = hash_of(id, reference_token);
|
||||
const std::size_t mask = table.size() - 1;
|
||||
std::size_t i = hash & mask;
|
||||
while (table[i].token != nullptr)
|
||||
{
|
||||
if (table[i].hash == hash && table[i].parent == id && *table[i].token == reference_token)
|
||||
{
|
||||
return table[i].id;
|
||||
}
|
||||
i = (i + 1) & mask;
|
||||
}
|
||||
|
||||
// new prefix
|
||||
const std::size_t child = parent.size();
|
||||
table[i] = slot(hash, id, &reference_token, child);
|
||||
if (reference_token == "0")
|
||||
{
|
||||
is_array[id] = true;
|
||||
}
|
||||
parent.push_back(id);
|
||||
is_array.push_back(false);
|
||||
|
||||
// keep the load factor below 1/2
|
||||
if (++used * 2 > table.size())
|
||||
{
|
||||
grow();
|
||||
}
|
||||
return child;
|
||||
}
|
||||
};
|
||||
|
||||
/// @brief the length of the longest common prefix of the first @a limit bytes of @a a and @a b
|
||||
static std::size_t common_prefix_length(const char* a, const char* b, const std::size_t limit) noexcept
|
||||
{
|
||||
std::size_t i = 0;
|
||||
for (; i + 8 <= limit; i += 8)
|
||||
{
|
||||
std::uint64_t x = 0;
|
||||
std::uint64_t y = 0;
|
||||
std::memcpy(&x, a + i, 8);
|
||||
std::memcpy(&y, b + i, 8);
|
||||
if (x != y)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
while (i < limit && a[i] == b[i])
|
||||
{
|
||||
++i;
|
||||
}
|
||||
return i;
|
||||
}
|
||||
|
||||
/*!
|
||||
@brief check that @a key is a valid JSON pointer, and add the prefixes of
|
||||
it that denote arrays to @a arrays
|
||||
@brief create and return a reference to the pointed to value
|
||||
|
||||
@throw parse_error.107 if the key is not empty or begins with '/'
|
||||
@throw parse_error.108 if character '~' is not followed by '0' or '1'
|
||||
@param[in] j the value to create the pointed to value in
|
||||
@param[in] tokens the reference tokens of the pointer
|
||||
@param[in] prefixes prefixes[i] is the number in @a tree of the prefix
|
||||
consisting of the first i reference tokens
|
||||
|
||||
The same checks as @ref split, which throws the same exceptions, but
|
||||
without building the reference tokens.
|
||||
*/
|
||||
static void scan_key(const string_t& key, const string_t* previous, array_prefix_set& arrays)
|
||||
{
|
||||
const std::size_t size = key.size();
|
||||
if (size == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
Complexity: Linear in the number of reference tokens.
|
||||
|
||||
const char* const data = key.data();
|
||||
if (JSON_HEDLEY_UNLIKELY(data[0] != '/'))
|
||||
{
|
||||
JSON_THROW(detail::parse_error::create(detail::exception_id::pointer_missing_slash, 1, detail::concat("JSON pointer must be empty or begin with '/' - was: '", key, "'"), nullptr));
|
||||
}
|
||||
|
||||
// The beginning of the key that it has in common with the previous
|
||||
// one, which was checked already, is skipped: the scan starts with
|
||||
// the token after the last slash in it.
|
||||
std::size_t begin = 1;
|
||||
if (previous != nullptr)
|
||||
{
|
||||
std::size_t same = common_prefix_length(data, previous->data(), (std::min)(size, previous->size()));
|
||||
while (same > 1 && data[same - 1] != '/')
|
||||
{
|
||||
--same;
|
||||
}
|
||||
if (same > 1)
|
||||
{
|
||||
begin = same;
|
||||
}
|
||||
}
|
||||
|
||||
for (std::size_t i = begin; i <= size; ++i)
|
||||
{
|
||||
if (i == size || data[i] == '/')
|
||||
{
|
||||
// a token 0 makes the pointer before it an array
|
||||
if (i - begin == 1 && data[begin] == '0')
|
||||
{
|
||||
arrays.insert(data, begin - 1);
|
||||
}
|
||||
begin = i + 1;
|
||||
}
|
||||
else if (data[i] == '~')
|
||||
{
|
||||
// ~ must be followed by 0 or 1
|
||||
if (JSON_HEDLEY_UNLIKELY(i + 1 == size || (data[i + 1] != '0' && data[i + 1] != '1')))
|
||||
{
|
||||
JSON_THROW(detail::parse_error::create(detail::exception_id::pointer_invalid_escape, 0, "escape character '~' must be followed with '0' or '1'", nullptr));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*!
|
||||
@brief resolve a plain JSON pointer without building its reference tokens
|
||||
|
||||
@param[in] ptr the value the pointer starts at
|
||||
@param[in] path a JSON pointer string
|
||||
@param[in] length the number of leading bytes of @a path to resolve; 0 or
|
||||
the position of a slash
|
||||
@param[in,out] token a string to hold the reference tokens, to reuse its storage
|
||||
|
||||
@return the pointed to value, or `nullptr` if the pointer does not denote
|
||||
an existing value, or does so in a way that is not plain: a
|
||||
reference token with an escape, an array index that is malformed or
|
||||
out of range, or a "-". No exception is thrown for these; callers
|
||||
use @ref json_pointer instead, which handles them.
|
||||
@throw parse_error.106 if an array index begins with '0'
|
||||
@throw parse_error.109 if array index is not a number
|
||||
@throw type_error.313 if value cannot be unflattened
|
||||
*/
|
||||
template<typename BasicJsonType>
|
||||
static BasicJsonType* resolve_plain(BasicJsonType* ptr, const string_t& path, const std::size_t length, string_t& token)
|
||||
static BasicJsonType& get_and_create(BasicJsonType& j, const std::vector<string_t>& tokens,
|
||||
const std::vector<std::size_t>& prefixes, const prefix_tree& tree)
|
||||
{
|
||||
for (std::size_t pos = 0; pos < length;)
|
||||
auto* result = &j;
|
||||
|
||||
// in case no reference tokens exist, return a reference to the JSON value
|
||||
// j which will be overwritten by a primitive value
|
||||
for (std::size_t i = 0; i < tokens.size(); ++i)
|
||||
{
|
||||
if (JSON_HEDLEY_UNLIKELY(path[pos] != '/'))
|
||||
const auto& reference_token = tokens[i];
|
||||
switch (result->type())
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
case detail::value_t::null:
|
||||
{
|
||||
if (tree.is_array[prefixes[i]])
|
||||
{
|
||||
// some reference token below this position is 0, so the
|
||||
// value is an array
|
||||
result = &result->operator[](array_index<BasicJsonType>(reference_token));
|
||||
}
|
||||
else
|
||||
{
|
||||
// start a new object otherwise
|
||||
result = &result->operator[](reference_token);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// the reference token between the slash at pos and the next one
|
||||
std::size_t end = path.find_first_of('/', pos + 1);
|
||||
if (end == string_t::npos || end > length)
|
||||
{
|
||||
end = length;
|
||||
}
|
||||
token.clear();
|
||||
token.append(path.data() + pos + 1, end - pos - 1);
|
||||
if (JSON_HEDLEY_UNLIKELY(token.find_first_of('~') != string_t::npos))
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
switch (ptr->type())
|
||||
{
|
||||
case detail::value_t::object:
|
||||
{
|
||||
const auto it = ptr->m_data.m_value.object->find(token);
|
||||
if (JSON_HEDLEY_UNLIKELY(it == ptr->m_data.m_value.object->end()))
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
ptr = &it->second;
|
||||
// create an entry in the object
|
||||
result = &object_member(*result, reference_token);
|
||||
break;
|
||||
}
|
||||
|
||||
case detail::value_t::array:
|
||||
{
|
||||
typename BasicJsonType::size_type idx{};
|
||||
if (JSON_HEDLEY_UNLIKELY(parse_array_index<BasicJsonType>(token, idx) != array_index_status::ok || idx >= ptr->m_data.m_value.array->size()))
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
ptr = &(*ptr->m_data.m_value.array)[idx];
|
||||
// create an entry in the array
|
||||
result = &result->operator[](array_index<BasicJsonType>(reference_token));
|
||||
break;
|
||||
}
|
||||
|
||||
case detail::value_t::null:
|
||||
/*
|
||||
The following code is only reached if there exists a reference
|
||||
token _and_ the current value is primitive. In this case, we have
|
||||
an error situation, because primitive values may only occur as
|
||||
a single value; that is, with an empty list of reference tokens.
|
||||
*/
|
||||
case detail::value_t::string:
|
||||
case detail::value_t::boolean:
|
||||
case detail::value_t::number_integer:
|
||||
@@ -791,12 +683,11 @@ class json_pointer
|
||||
case detail::value_t::binary:
|
||||
case detail::value_t::discarded:
|
||||
default:
|
||||
return nullptr;
|
||||
JSON_THROW(detail::type_error::create(detail::exception_id::unflatten_invalid_value, "invalid value to unflatten", &j));
|
||||
}
|
||||
pos = end;
|
||||
}
|
||||
|
||||
return ptr;
|
||||
return *result;
|
||||
}
|
||||
|
||||
/*!
|
||||
@@ -1251,45 +1142,20 @@ class json_pointer
|
||||
// Add the entry (key, v) to the object `obj`. Flattened paths are pairwise
|
||||
// distinct, so objects that can append a key known to be absent
|
||||
// (ordered_map) skip the lookup, which is linear and would make flatten
|
||||
// quadratic. Objects with a positional insert (std::map) insert next to
|
||||
// the entry added last: paths come out in nearly sorted order, so the
|
||||
// position is usually right and no search from the root is needed.
|
||||
// All other object types use operator[].
|
||||
// quadratic; all other object types use operator[].
|
||||
template<typename ObjectType, typename ValueType>
|
||||
static auto add_flattened(ObjectType& obj, typename ObjectType::iterator& /*hint*/, const string_t& key, ValueType&& v, detail::priority_tag<2> /*unused*/)
|
||||
static auto add_flattened(ObjectType& obj, const string_t& key, ValueType&& v, detail::priority_tag<1> /*unused*/)
|
||||
-> decltype(obj.emplace_new_key(key, std::forward<ValueType>(v)), void())
|
||||
{
|
||||
obj.emplace_new_key(key, std::forward<ValueType>(v));
|
||||
}
|
||||
|
||||
template<typename ObjectType, typename ValueType>
|
||||
static auto add_flattened(ObjectType& obj, typename ObjectType::iterator& hint, const string_t& key, ValueType&& v, detail::priority_tag<1> /*unused*/)
|
||||
-> decltype(hint = obj.emplace_hint(hint, key, std::forward<ValueType>(v)), void())
|
||||
{
|
||||
hint = obj.emplace_hint(hint, key, std::forward<ValueType>(v));
|
||||
++hint;
|
||||
}
|
||||
|
||||
template<typename ObjectType, typename ValueType>
|
||||
static void add_flattened(ObjectType& obj, typename ObjectType::iterator& /*hint*/, const string_t& key, ValueType&& v, detail::priority_tag<0> /*unused*/)
|
||||
static void add_flattened(ObjectType& obj, const string_t& key, ValueType&& v, detail::priority_tag<0> /*unused*/)
|
||||
{
|
||||
obj[key] = std::forward<ValueType>(v);
|
||||
}
|
||||
|
||||
/// append the decimal representation of @a i to @a out
|
||||
static void append_index(string_t& out, std::size_t i)
|
||||
{
|
||||
std::array<char, 24> buf{};
|
||||
std::size_t pos = buf.size();
|
||||
do
|
||||
{
|
||||
buf[--pos] = static_cast<char>('0' + (i % 10));
|
||||
i /= 10;
|
||||
}
|
||||
while (i != 0);
|
||||
out.append(buf.data() + pos, buf.size() - pos);
|
||||
}
|
||||
|
||||
/*!
|
||||
@param[in] reference_string the reference string to the current value
|
||||
@param[in] value the value to consider
|
||||
@@ -1335,8 +1201,7 @@ class json_pointer
|
||||
// flatten `v`, whose path is `path`: primitives and empty containers
|
||||
// are added to the result right away; other containers get a frame
|
||||
auto& object = *result.m_data.m_value.object;
|
||||
auto hint = object.end();
|
||||
const auto enter = [&stack, &path, &object, &hint](const BasicJsonType & v)
|
||||
const auto enter = [&stack, &path, &object](const BasicJsonType & v)
|
||||
{
|
||||
switch (v.type())
|
||||
{
|
||||
@@ -1345,7 +1210,7 @@ class json_pointer
|
||||
if (v.m_data.m_value.array->empty())
|
||||
{
|
||||
// flatten empty array as null
|
||||
add_flattened(object, hint, path, nullptr, detail::priority_tag<2> {});
|
||||
add_flattened(object, path, nullptr, detail::priority_tag<1> {});
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -1359,7 +1224,7 @@ class json_pointer
|
||||
if (v.m_data.m_value.object->empty())
|
||||
{
|
||||
// flatten empty object as null
|
||||
add_flattened(object, hint, path, nullptr, detail::priority_tag<2> {});
|
||||
add_flattened(object, path, nullptr, detail::priority_tag<1> {});
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -1379,7 +1244,7 @@ class json_pointer
|
||||
default:
|
||||
{
|
||||
// add a primitive value with its reference string
|
||||
add_flattened(object, hint, path, v, detail::priority_tag<2> {});
|
||||
add_flattened(object, path, v, detail::priority_tag<1> {});
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -1407,8 +1272,7 @@ class json_pointer
|
||||
|
||||
// iterate array and use index as a reference string
|
||||
++stack.back().index;
|
||||
path.push_back('/');
|
||||
append_index(path, i);
|
||||
detail::concat_into(path, '/', detail::to_string<string_t>(i));
|
||||
enter(array[i]);
|
||||
}
|
||||
else
|
||||
@@ -1422,8 +1286,7 @@ class json_pointer
|
||||
|
||||
// iterate object and use keys as reference string
|
||||
++stack.back().member;
|
||||
path.push_back('/');
|
||||
detail::escape_into(path, it->first);
|
||||
detail::concat_into(path, '/', detail::escape(it->first));
|
||||
enter(it->second);
|
||||
}
|
||||
}
|
||||
@@ -1454,30 +1317,53 @@ class json_pointer
|
||||
|
||||
BasicJsonType result;
|
||||
|
||||
// check every key, and collect the prefixes that have a reference
|
||||
// token 0 among their children; the values below them are arrays,
|
||||
// all others are objects (see array_prefix_set)
|
||||
array_prefix_set arrays;
|
||||
const string_t* previous_key = nullptr;
|
||||
// parse every key once; the reference tokens are kept for the second
|
||||
// pass (the vectors are never resized, so the tree may refer to them)
|
||||
std::vector<std::vector<string_t>> keys;
|
||||
keys.reserve(value.m_data.m_value.object->size());
|
||||
for (const auto& element : *value.m_data.m_value.object)
|
||||
{
|
||||
scan_key(element.first, previous_key, arrays);
|
||||
previous_key = &element.first;
|
||||
json_pointer ptr(element.first);
|
||||
keys.push_back(std::move(ptr.reference_tokens));
|
||||
}
|
||||
|
||||
// values[i] is the value after the first i reference tokens of the
|
||||
// previous key, ends[i] the length of that prefix of the key. Keys are
|
||||
// often sorted, so most of the path to the value of a key is the
|
||||
// same as for the previous key and need not be looked up again. Only
|
||||
// the values below the common prefix are dropped, and only they can
|
||||
// move when a value is added: the ones above are not inside the
|
||||
// container that grows.
|
||||
std::vector<BasicJsonType*> values(1, &result);
|
||||
std::vector<std::size_t> ends(1, 0);
|
||||
const string_t* previous = nullptr;
|
||||
string_t token;
|
||||
// collect the pointer prefixes that have a reference token 0 among
|
||||
// their children; the values below them are arrays, all others are
|
||||
// objects (see prefix_tree)
|
||||
prefix_tree tree;
|
||||
// the number of the prefix with i + 1 tokens of the previous key; keys
|
||||
// are often sorted, so most of the prefix is the same as for the
|
||||
// previous key and need not be looked up again
|
||||
std::vector<std::size_t> path;
|
||||
const std::vector<string_t>* previous = nullptr;
|
||||
// the number of the complete prefix (i.e., the key) of each key
|
||||
std::vector<std::size_t> leaves;
|
||||
leaves.reserve(keys.size());
|
||||
for (const auto& tokens : keys)
|
||||
{
|
||||
std::size_t common = 0;
|
||||
if (previous != nullptr)
|
||||
{
|
||||
const std::size_t limit = (std::min)(tokens.size(), previous->size());
|
||||
while (common < limit && tokens[common] == (*previous)[common])
|
||||
{
|
||||
++common;
|
||||
}
|
||||
}
|
||||
path.resize(common);
|
||||
std::size_t id = common == 0 ? 0 : path[common - 1];
|
||||
for (std::size_t i = common; i < tokens.size(); ++i)
|
||||
{
|
||||
id = tree.add_child(id, tokens[i]);
|
||||
path.push_back(id);
|
||||
}
|
||||
leaves.push_back(id);
|
||||
previous = &tokens;
|
||||
}
|
||||
|
||||
// iterate the JSON object values
|
||||
std::vector<std::size_t> prefixes;
|
||||
std::size_t key_index = 0;
|
||||
for (const auto& element : *value.m_data.m_value.object)
|
||||
{
|
||||
if (JSON_HEDLEY_UNLIKELY(!element.second.is_primitive()))
|
||||
@@ -1485,100 +1371,21 @@ class json_pointer
|
||||
JSON_THROW(detail::type_error::create(detail::exception_id::unflatten_value_not_primitive, "values in object must be primitive", &element.second));
|
||||
}
|
||||
|
||||
const string_t& key = element.first;
|
||||
const char* const data = key.data();
|
||||
|
||||
// the number of reference tokens the key has in common with the
|
||||
// previous one
|
||||
std::size_t depth = 0;
|
||||
if (previous != nullptr)
|
||||
const auto& tokens = keys[key_index];
|
||||
// number the prefixes of this key from its last one to the first
|
||||
prefixes.resize(tokens.size());
|
||||
for (std::size_t i = tokens.size(), id = leaves[key_index]; i > 0; --i)
|
||||
{
|
||||
const std::size_t same = common_prefix_length(data, previous->data(), (std::min)(key.size(), previous->size()));
|
||||
depth = ends.size() - 1;
|
||||
while (depth > 0 && (ends[depth] > same || (ends[depth] == same && same < key.size() && data[same] != '/')))
|
||||
{
|
||||
--depth;
|
||||
}
|
||||
id = tree.parent[id];
|
||||
prefixes[i - 1] = id;
|
||||
}
|
||||
values.resize(depth + 1);
|
||||
ends.resize(depth + 1);
|
||||
previous = &key;
|
||||
++key_index;
|
||||
|
||||
// Assign the value to the reference pointed to by JSON pointer. Note
|
||||
// that if the JSON pointer is "" (i.e., points to the whole value),
|
||||
// the value is the result itself. An assignment will then create a
|
||||
// primitive value.
|
||||
BasicJsonType* current = values.back();
|
||||
for (std::size_t pos = ends.back(); pos < key.size();)
|
||||
{
|
||||
// the reference token between the slash at pos and the next
|
||||
std::size_t end = key.find_first_of('/', pos + 1);
|
||||
if (end == string_t::npos)
|
||||
{
|
||||
end = key.size();
|
||||
}
|
||||
token.clear();
|
||||
token.append(data + pos + 1, end - pos - 1);
|
||||
if (token.find_first_of('~') != string_t::npos)
|
||||
{
|
||||
detail::unescape(token);
|
||||
}
|
||||
|
||||
switch (current->type())
|
||||
{
|
||||
case detail::value_t::null:
|
||||
{
|
||||
if (arrays.contains(data, pos))
|
||||
{
|
||||
// some reference token below this position is 0, so the
|
||||
// value is an array
|
||||
current = ¤t->operator[](array_index<BasicJsonType>(token));
|
||||
}
|
||||
else
|
||||
{
|
||||
// start a new object otherwise
|
||||
current = ¤t->operator[](token);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case detail::value_t::object:
|
||||
{
|
||||
// create an entry in the object
|
||||
current = &object_member(*current, token);
|
||||
break;
|
||||
}
|
||||
|
||||
case detail::value_t::array:
|
||||
{
|
||||
// create an entry in the array
|
||||
current = ¤t->operator[](array_index<BasicJsonType>(token));
|
||||
break;
|
||||
}
|
||||
|
||||
/*
|
||||
The following code is only reached if there exists a reference
|
||||
token _and_ the current value is primitive. In this case, we have
|
||||
an error situation, because primitive values may only occur as
|
||||
a single value; that is, with an empty list of reference tokens.
|
||||
*/
|
||||
case detail::value_t::string:
|
||||
case detail::value_t::boolean:
|
||||
case detail::value_t::number_integer:
|
||||
case detail::value_t::number_unsigned:
|
||||
case detail::value_t::number_float:
|
||||
case detail::value_t::binary:
|
||||
case detail::value_t::discarded:
|
||||
default:
|
||||
JSON_THROW(detail::type_error::create(detail::exception_id::unflatten_invalid_value, "invalid value to unflatten", &result));
|
||||
}
|
||||
|
||||
values.push_back(current);
|
||||
ends.push_back(end);
|
||||
pos = end;
|
||||
}
|
||||
|
||||
*current = element.second;
|
||||
// function get_and_create returns a reference to the result itself.
|
||||
// An assignment will then create a primitive value.
|
||||
get_and_create(result, tokens, prefixes, tree) = element.second;
|
||||
}
|
||||
|
||||
return result;
|
||||
|
||||
@@ -187,22 +187,6 @@
|
||||
#endif
|
||||
#endif
|
||||
|
||||
// the byte order of the target where the compiler tells it (std::endian is
|
||||
// C++20 and cannot select code in #if): JSON_LITTLE_ENDIAN_TARGET or
|
||||
// JSON_BIG_ENDIAN_TARGET is 1, or both are 0 where it is unknown (code then
|
||||
// handles the bytes one at a time); MSVC and x86 targets are little-endian
|
||||
#if defined(_MSC_VER) || defined(__x86_64__) || defined(__i386__) \
|
||||
|| (defined(__BYTE_ORDER__) && defined(__ORDER_LITTLE_ENDIAN__) && __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__)
|
||||
#define JSON_LITTLE_ENDIAN_TARGET 1 // NOLINT(cppcoreguidelines-macro-to-enum,modernize-macro-to-enum)
|
||||
#define JSON_BIG_ENDIAN_TARGET 0 // NOLINT(cppcoreguidelines-macro-to-enum,modernize-macro-to-enum)
|
||||
#elif defined(__BYTE_ORDER__) && defined(__ORDER_BIG_ENDIAN__) && __BYTE_ORDER__ == __ORDER_BIG_ENDIAN__
|
||||
#define JSON_LITTLE_ENDIAN_TARGET 0 // NOLINT(cppcoreguidelines-macro-to-enum,modernize-macro-to-enum)
|
||||
#define JSON_BIG_ENDIAN_TARGET 1 // NOLINT(cppcoreguidelines-macro-to-enum,modernize-macro-to-enum)
|
||||
#else
|
||||
#define JSON_LITTLE_ENDIAN_TARGET 0 // NOLINT(cppcoreguidelines-macro-to-enum,modernize-macro-to-enum)
|
||||
#define JSON_BIG_ENDIAN_TARGET 0 // NOLINT(cppcoreguidelines-macro-to-enum,modernize-macro-to-enum)
|
||||
#endif
|
||||
|
||||
#ifdef JSON_HAS_CPP_17
|
||||
#define JSON_INLINE_VARIABLE inline
|
||||
#else
|
||||
|
||||
@@ -19,20 +19,9 @@
|
||||
#undef NLOHMANN_CAN_CALL_STD_FUNC_IMPL
|
||||
#undef JSON_INLINE_VARIABLE
|
||||
#undef JSON_NO_UNIQUE_ADDRESS
|
||||
#undef JSON_DISABLE_ENUM_SERIALIZATION
|
||||
#undef JSON_DISABLE_TUPLE_REFERENCE_CONVERSION
|
||||
#undef JSON_UNLOCKED_FILE_IO
|
||||
#undef JSON_DTOA_SSE2
|
||||
#undef JSON_DTOA_NEON
|
||||
#undef JSON_LITTLE_ENDIAN_TARGET
|
||||
#undef JSON_BIG_ENDIAN_TARGET
|
||||
#undef JSON_SCAN_NEON
|
||||
#undef JSON_SCAN_SSE2
|
||||
#undef JSON_SCAN_SSSE3
|
||||
#undef JSON_SCAN_SSSE3_DISPATCH
|
||||
#undef JSON_SCAN_SSSE3_TARGET
|
||||
#undef JSON_SCAN_VECTOR
|
||||
#undef JSON_SCAN_VECTOR_UTF8
|
||||
|
||||
#ifndef JSON_TEST_KEEP_MACROS
|
||||
#undef JSON_CATCH
|
||||
@@ -57,6 +46,8 @@
|
||||
#undef JSON_STRICT_BINARY_UTF8
|
||||
#undef JSON_DELETE_DEPRECATED_FUNCTIONS
|
||||
#undef JSON_USE_OBJECTS_FOR_ENUM_KEYED_MAPS
|
||||
#undef JSON_DISABLE_ENUM_SERIALIZATION
|
||||
#undef JSON_DISABLE_TUPLE_REFERENCE_CONVERSION
|
||||
#undef JSON_DISABLE_STREAM_BULK_SCAN
|
||||
#undef JSON_DISABLE_UNLOCKED_FILE_IO
|
||||
#endif
|
||||
|
||||
@@ -10,26 +10,17 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstddef> // size_t
|
||||
#include <type_traits> // conditional, enable_if, false_type, integral_constant, is_arithmetic, is_constructible, is_integral, is_same, remove_cv, remove_reference, true_type
|
||||
#include <type_traits> // conditional, enable_if, false_type, integral_constant, is_constructible, is_integral, is_same, remove_cv, remove_reference, true_type
|
||||
#include <utility> // index_sequence, make_index_sequence, index_sequence_for
|
||||
|
||||
#include <nlohmann/detail/macro_scope.hpp>
|
||||
|
||||
#ifdef JSON_HAS_CPP_17
|
||||
#include <algorithm> // clamp
|
||||
#endif
|
||||
|
||||
NLOHMANN_JSON_NAMESPACE_BEGIN
|
||||
namespace detail
|
||||
{
|
||||
|
||||
#ifdef __cpp_lib_remove_cvref
|
||||
template<typename T>
|
||||
using uncvref_t = std::remove_cvref_t<T>;
|
||||
#else
|
||||
template<typename T>
|
||||
using uncvref_t = typename std::remove_cv<typename std::remove_reference<T>::type>::type;
|
||||
#endif
|
||||
template<typename T>
|
||||
using uncvref_t = typename std::remove_cv<typename std::remove_reference<T>::type>::type;
|
||||
|
||||
#ifdef JSON_HAS_CPP_14
|
||||
|
||||
@@ -169,19 +160,5 @@ struct static_const
|
||||
constexpr T static_const<T>::value;
|
||||
#endif
|
||||
|
||||
/// @a v bounded to [@a lo, @a hi] (requires !(hi < lo)); like std::clamp,
|
||||
/// but by value, so that static constexpr members can be passed without an
|
||||
/// out-of-line definition before C++17
|
||||
template<typename T, enable_if_t<std::is_arithmetic<T>::value, int> = 0>
|
||||
constexpr T clamp(T v, T lo, T hi) noexcept
|
||||
{
|
||||
#ifdef __cpp_lib_clamp
|
||||
return std::clamp(v, lo, hi);
|
||||
#else
|
||||
// NOLINTNEXTLINE(readability-avoid-nested-conditional-operator): a constant expression of C++11
|
||||
return v < lo ? lo : (hi < v ? hi : v);
|
||||
#endif
|
||||
}
|
||||
|
||||
} // namespace detail
|
||||
NLOHMANN_JSON_NAMESPACE_END
|
||||
@@ -313,14 +313,6 @@ struct char_traits<std::byte> : std::char_traits<char>
|
||||
// is_ functions //
|
||||
///////////////////
|
||||
|
||||
#ifdef __cpp_lib_logical_traits
|
||||
|
||||
using std::conjunction;
|
||||
using std::disjunction;
|
||||
using std::negation;
|
||||
|
||||
#else
|
||||
|
||||
// https://en.cppreference.com/w/cpp/types/conjunction
|
||||
template<class...> struct conjunction : std::true_type { };
|
||||
template<class B> struct conjunction<B> : B { };
|
||||
@@ -338,8 +330,6 @@ struct disjunction<B, Bn...>
|
||||
// https://en.cppreference.com/w/cpp/types/negation
|
||||
template<class B> struct negation : std::integral_constant < bool, !B::value > { };
|
||||
|
||||
#endif
|
||||
|
||||
// Reimplementation of is_constructible and is_default_constructible, due to them being broken for
|
||||
// std::pair and std::tuple until LWG 2367 fix (see https://cplusplus.github.io/LWG/lwg-defects.html#2367).
|
||||
// This causes compile errors in e.g., Clang 3.5 or GCC 4.9.
|
||||
@@ -1070,12 +1060,8 @@ constexpr bool value_in_range_of(T val)
|
||||
return value_in_range_of_impl1<OfType, T>::test(val);
|
||||
}
|
||||
|
||||
#ifdef __cpp_lib_bool_constant
|
||||
using std::bool_constant;
|
||||
#else
|
||||
template<bool Value>
|
||||
using bool_constant = std::integral_constant<bool, Value>;
|
||||
#endif
|
||||
template<bool Value>
|
||||
using bool_constant = std::integral_constant<bool, Value>;
|
||||
|
||||
// Stands in for number_unsigned_t in overload sets if it is the same type as
|
||||
// number_integer_t, so that the unsigned overloads do not collide with the
|
||||
|
||||
@@ -19,6 +19,10 @@
|
||||
#include <utility> // move
|
||||
#include <vector> // vector
|
||||
|
||||
#ifdef _MSC_VER
|
||||
#include <cstdlib> // _byteswap_ushort, _byteswap_ulong, _byteswap_uint64
|
||||
#endif
|
||||
|
||||
#include <nlohmann/detail/bit_ops.hpp>
|
||||
#include <nlohmann/detail/input/binary_reader.hpp>
|
||||
#include <nlohmann/detail/input/string_scan.hpp>
|
||||
@@ -2069,7 +2073,7 @@ class binary_writer
|
||||
{
|
||||
out[0] = to_char_type(first_byte);
|
||||
const unsigned unused_bits = 64u - ((8u << size_class) >> 1u);
|
||||
const std::uint64_t tail = is_little_endian ? byteswap(value) >> unused_bits : value << unused_bits;
|
||||
const std::uint64_t tail = is_little_endian ? byte_swap(value) >> unused_bits : value << unused_bits;
|
||||
std::memcpy(out + 1, &tail, sizeof(tail));
|
||||
return ((std::size_t{1} << size_class) >> 1u) + 1;
|
||||
}
|
||||
@@ -3513,14 +3517,53 @@ class binary_writer
|
||||
// Utility functions //
|
||||
///////////////////////
|
||||
|
||||
// single-instruction byte swaps (compilers lower these to bswap/rev/movbe);
|
||||
// used to emit big-endian numbers without a per-byte std::reverse loop
|
||||
static std::uint16_t byte_swap(std::uint16_t x) noexcept
|
||||
{
|
||||
#if defined(__GNUC__) || defined(__clang__)
|
||||
return __builtin_bswap16(x);
|
||||
#elif defined(_MSC_VER)
|
||||
return _byteswap_ushort(x);
|
||||
#else
|
||||
return static_cast<std::uint16_t>((x >> 8) | (x << 8));
|
||||
#endif
|
||||
}
|
||||
|
||||
static std::uint32_t byte_swap(std::uint32_t x) noexcept
|
||||
{
|
||||
#if defined(__GNUC__) || defined(__clang__)
|
||||
return __builtin_bswap32(x);
|
||||
#elif defined(_MSC_VER)
|
||||
return _byteswap_ulong(x);
|
||||
#else
|
||||
return ((x & 0x000000FFu) << 24) | ((x & 0x0000FF00u) << 8)
|
||||
| ((x & 0x00FF0000u) >> 8) | ((x & 0xFF000000u) >> 24);
|
||||
#endif
|
||||
}
|
||||
|
||||
static std::uint64_t byte_swap(std::uint64_t x) noexcept
|
||||
{
|
||||
#if defined(__GNUC__) || defined(__clang__)
|
||||
return __builtin_bswap64(x);
|
||||
#elif defined(_MSC_VER)
|
||||
return _byteswap_uint64(x);
|
||||
#else
|
||||
x = ((x & 0x00000000FFFFFFFFull) << 32) | ((x & 0xFFFFFFFF00000000ull) >> 32);
|
||||
x = ((x & 0x0000FFFF0000FFFFull) << 16) | ((x & 0xFFFF0000FFFF0000ull) >> 16);
|
||||
x = ((x & 0x00FF00FF00FF00FFull) << 8) | ((x & 0xFF00FF00FF00FF00ull) >> 8);
|
||||
return x;
|
||||
#endif
|
||||
}
|
||||
|
||||
/*!
|
||||
@brief reverse the bytes of a buffer by byte-swapping it as UIntType
|
||||
|
||||
Loading the buffer into an unsigned integer of the same width and swapping
|
||||
that is what lets the compiler emit a single bswap/rev/movbe; reversing the
|
||||
buffer element by element does not reliably get there (clang keeps a scalar
|
||||
shuffle). (The two memcpy calls, rather than bit_cast, keep GCC from
|
||||
inlining differently in C++20, which made writing slower.)
|
||||
shuffle). The two memcpy calls are the only portable way to reinterpret the
|
||||
bytes and are folded away by every optimizer.
|
||||
*/
|
||||
template<typename UIntType, std::size_t N>
|
||||
static void byte_swap_buffer(std::array<CharType, N>& a) noexcept
|
||||
@@ -3528,11 +3571,11 @@ class binary_writer
|
||||
static_assert(sizeof(UIntType) == N, "swap width must match the buffer size");
|
||||
UIntType v{};
|
||||
std::memcpy(&v, a.data(), sizeof(v));
|
||||
v = byteswap(v);
|
||||
v = byte_swap(v);
|
||||
std::memcpy(a.data(), &v, sizeof(v));
|
||||
}
|
||||
|
||||
// reverse the bytes of a fixed-size buffer; a single byteswap() for the
|
||||
// reverse the bytes of a fixed-size buffer; a single byte_swap() for the
|
||||
// common 2/4/8-byte number payloads, std::reverse for any other size
|
||||
static void reverse_bytes(std::array<CharType, 2>& a) noexcept
|
||||
{
|
||||
@@ -3657,7 +3700,9 @@ class binary_writer
|
||||
#endif
|
||||
|
||||
static_assert(sizeof(std::uint8_t) == sizeof(CharType), "size of CharType must be equal to std::uint8_t");
|
||||
return bit_cast<CharType>(x);
|
||||
CharType result;
|
||||
std::memcpy(&result, &x, sizeof(x));
|
||||
return result;
|
||||
}
|
||||
|
||||
template<typename C = CharType,
|
||||
|
||||
@@ -693,33 +693,6 @@ class serializer
|
||||
*/
|
||||
void dump_escaped(const string_t& s)
|
||||
{
|
||||
// Short strings of printable ASCII - most object keys and many values -
|
||||
// are checked byte by byte against a table and then copied in one go.
|
||||
// For them, this is cheaper than starting the vector scanner below,
|
||||
// whose setup costs more than such a string takes to scan. The same
|
||||
// check serves both values of ensure_ascii.
|
||||
if (s.size() <= 32)
|
||||
{
|
||||
const auto& copyable = ascii_copyable();
|
||||
const auto* const data = reinterpret_cast<const unsigned char*>(s.data());
|
||||
const std::size_t n = s.size();
|
||||
std::size_t k = 0;
|
||||
while (k + 4 <= n && copyable[data[k]] != 0 && copyable[data[k + 1]] != 0
|
||||
&& copyable[data[k + 2]] != 0 && copyable[data[k + 3]] != 0)
|
||||
{
|
||||
k += 4;
|
||||
}
|
||||
while (k < n && copyable[data[k]] != 0)
|
||||
{
|
||||
++k;
|
||||
}
|
||||
if (k == n)
|
||||
{
|
||||
put_string(s, 0, n);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// dispatch once here rather than test the flag inside the loop: it does
|
||||
// not change while a string is written, and folding it lets each of the
|
||||
// two scanners be inlined into a loop of its own
|
||||
@@ -1100,12 +1073,8 @@ class serializer
|
||||
the indentation character once and flushing that same content repeatedly:
|
||||
flushing does not disturb what the buffer holds, so re-filling it between
|
||||
flushes would be redundant work.
|
||||
|
||||
@note inlined on purpose, as is @ref put_literal: GCC otherwise stops
|
||||
inlining both into the pretty-printing loops once @ref dump_escaped
|
||||
has grown, which made indented output up to 13% slower
|
||||
*/
|
||||
JSON_HEDLEY_ALWAYS_INLINE void put_indent(std::size_t indent)
|
||||
void put_indent(std::size_t indent)
|
||||
{
|
||||
// closing braces at the outermost level ask for no indentation at all
|
||||
if (indent == 0)
|
||||
@@ -1155,7 +1124,7 @@ class serializer
|
||||
write-through path for oversized runs.
|
||||
*/
|
||||
template<std::size_t N>
|
||||
JSON_HEDLEY_ALWAYS_INLINE void put_literal(const char (&s)[N]) // NOLINT(cppcoreguidelines-avoid-c-arrays,hicpp-avoid-c-arrays,modernize-avoid-c-arrays)
|
||||
void put_literal(const char (&s)[N]) // NOLINT(cppcoreguidelines-avoid-c-arrays,hicpp-avoid-c-arrays,modernize-avoid-c-arrays)
|
||||
{
|
||||
static_assert(N >= 2, "put_literal expects a non-empty string literal");
|
||||
// the array bound counts the terminating NUL, which is not written
|
||||
@@ -1264,39 +1233,6 @@ class serializer
|
||||
}
|
||||
}
|
||||
|
||||
/*!
|
||||
@brief whether each byte can be written verbatim under either value of ensure_ascii
|
||||
|
||||
Entry c is 1 for printable ASCII other than '"' and '\\' (see
|
||||
is_ascii_copyable()) and 0 otherwise. A table lookup per byte is the
|
||||
cheapest test for the short strings handled first in dump_escaped().
|
||||
*/
|
||||
static const std::array<std::uint8_t, 256>& ascii_copyable() noexcept
|
||||
{
|
||||
static const std::array<std::uint8_t, 256> copyable =
|
||||
{
|
||||
{
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 0x00
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 0x10
|
||||
1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 0x20
|
||||
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 0x30
|
||||
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 0x40
|
||||
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, // 0x50
|
||||
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 0x60
|
||||
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, // 0x70
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 0x80
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 0x90
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 0xA0
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 0xB0
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 0xC0
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 0xD0
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 0xE0
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 // 0xF0
|
||||
}
|
||||
};
|
||||
return copyable;
|
||||
}
|
||||
|
||||
/*!
|
||||
@brief the JSON form of each character below 0x60
|
||||
|
||||
|
||||
@@ -23,7 +23,6 @@
|
||||
#include <nlohmann/detail/view/macro_scope.hpp>
|
||||
#include <nlohmann/detail/view/node.hpp>
|
||||
#include <nlohmann/detail/view/number.hpp>
|
||||
#include <nlohmann/detail/view/simd.hpp>
|
||||
|
||||
NLOHMANN_JSON_NAMESPACE_BEGIN
|
||||
namespace detail
|
||||
@@ -81,6 +80,13 @@ class output_buffer
|
||||
put(s, N - 1);
|
||||
}
|
||||
|
||||
void put_repeated(char c, std::size_t n)
|
||||
{
|
||||
reserve(n);
|
||||
std::memset(m_pos, c, n);
|
||||
m_pos += n;
|
||||
}
|
||||
|
||||
/// the write position and the end of the writable space, for a writer
|
||||
/// that keeps the position in a local variable (set_cursor() hands it back)
|
||||
char* cursor() const noexcept
|
||||
@@ -99,13 +105,17 @@ class output_buffer
|
||||
}
|
||||
|
||||
private:
|
||||
/// the size of a growth step
|
||||
static constexpr std::size_t step = 0x10000;
|
||||
/// the size of a growth step (a function: std::min() takes a reference,
|
||||
/// which a static constexpr member does not have before C++17)
|
||||
static constexpr std::size_t step() noexcept
|
||||
{
|
||||
return 0x10000;
|
||||
}
|
||||
|
||||
static StringType& sized(StringType& out, std::size_t estimate)
|
||||
{
|
||||
out.reserve(estimate);
|
||||
out.resize(detail::clamp(estimate, std::size_t{64}, step));
|
||||
out.resize((std::min)((std::max)(estimate, static_cast<std::size_t>(64)), step()));
|
||||
return out;
|
||||
}
|
||||
|
||||
@@ -113,9 +123,8 @@ class output_buffer
|
||||
{
|
||||
const auto used = static_cast<std::size_t>(m_pos - m_out.data());
|
||||
// (a step does not go beyond the reserved estimate, so that a good
|
||||
// estimate is never copied to a larger allocation; not a clamp: the
|
||||
// lower bound used + n + 256 can exceed capacity(), and then it must win)
|
||||
const std::size_t size = (std::max)((std::min)(m_out.size() + step, m_out.capacity()), used + n + 256);
|
||||
// estimate is never copied to a larger allocation)
|
||||
const std::size_t size = (std::max)((std::min)(m_out.size() + step(), m_out.capacity()), used + n + 256);
|
||||
if (size > m_out.capacity())
|
||||
{
|
||||
m_out.reserve((std::max)(m_out.capacity() * 2, size));
|
||||
@@ -137,25 +146,9 @@ class output_buffer
|
||||
/// image_check::bounds can have others, which are then written unchanged).
|
||||
inline std::size_t plain_output_run(const unsigned char* s, std::size_t n) noexcept
|
||||
{
|
||||
std::size_t i = 0;
|
||||
#if NLOHMANN_VIEW_SSE2
|
||||
// (16 bytes per step; with NEON, the mask of a step costs more than it saves on the
|
||||
// short runs that are typical here)
|
||||
for (; i + 16 <= n; i += 16)
|
||||
{
|
||||
const __m128i in = _mm_loadu_si128(static_cast<const __m128i*>(static_cast<const void*>(s + i))); // NOLINT(bugprone-casting-through-void)
|
||||
// (a byte is below 0x20 if it is its own minimum with 0x1F)
|
||||
const __m128i special = _mm_or_si128(_mm_or_si128(_mm_cmpeq_epi8(in, _mm_set1_epi8('"')), _mm_cmpeq_epi8(in, _mm_set1_epi8('\\'))),
|
||||
_mm_cmpeq_epi8(_mm_min_epu8(in, _mm_set1_epi8(0x1F)), in));
|
||||
const auto mask = static_cast<std::uint64_t>(static_cast<unsigned>(_mm_movemask_epi8(special)));
|
||||
if (mask != 0)
|
||||
{
|
||||
return i + static_cast<std::size_t>(count_trailing_zeros(mask));
|
||||
}
|
||||
}
|
||||
#endif
|
||||
constexpr std::uint64_t ones = 0x0101010101010101ull;
|
||||
constexpr std::uint64_t high = 0x8080808080808080ull;
|
||||
std::size_t i = 0;
|
||||
for (; i + 8 <= n; i += 8)
|
||||
{
|
||||
const std::uint64_t v = read_eight_bytes(s + i);
|
||||
@@ -178,98 +171,6 @@ inline std::size_t plain_output_run(const unsigned char* s, std::size_t n) noexc
|
||||
return n;
|
||||
}
|
||||
|
||||
/// Whether dump() with ensure_ascii writes the string at s unchanged, for a
|
||||
/// string of the source without escape sequences (which has no quote,
|
||||
/// backslash, or control character): no byte is DEL (0x7F) or above. Adding 1
|
||||
/// to each byte sets its high bit exactly for 0x7F, and a carry into the next
|
||||
/// byte only comes from 0xFF, whose own high bit is set anyway.
|
||||
inline bool is_ascii_without_delete(const char* s, std::size_t n) noexcept
|
||||
{
|
||||
constexpr std::uint64_t ones = 0x0101010101010101ull;
|
||||
constexpr std::uint64_t high = 0x8080808080808080ull;
|
||||
std::size_t i = 0;
|
||||
for (; i + 8 <= n; i += 8)
|
||||
{
|
||||
const std::uint64_t v = read_eight_bytes(s + i);
|
||||
if (((v | (v + ones)) & high) != 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
for (; i < n; ++i)
|
||||
{
|
||||
if (static_cast<unsigned char>(s[i]) >= 0x7F)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
#if NLOHMANN_VIEW_VECTOR
|
||||
/// the bytes of the 16 at p that ensure_ascii cannot copy (a quote, a
|
||||
/// backslash, a control character, DEL, or not ASCII; a signed compare with
|
||||
/// 0x20 finds control characters and non-ASCII bytes at once), one bit per
|
||||
/// byte (x86) or one nibble per byte (NEON)
|
||||
NLOHMANN_VIEW_ALWAYS_INLINE std::uint64_t ascii_stop_mask(const unsigned char* p) noexcept
|
||||
{
|
||||
#if NLOHMANN_VIEW_NEON
|
||||
const uint8x16_t in = vld1q_u8(p);
|
||||
const uint8x16_t special = vorrq_u8(vorrq_u8(vceqq_u8(in, vdupq_n_u8('"')), vceqq_u8(in, vdupq_n_u8('\\'))),
|
||||
vorrq_u8(vceqq_u8(in, vdupq_n_u8(0x7F)), vcltq_s8(vreinterpretq_s8_u8(in), vdupq_n_s8(0x20))));
|
||||
return vget_lane_u64(vreinterpret_u64_u8(vshrn_n_u16(vreinterpretq_u16_u8(special), 4)), 0);
|
||||
#else
|
||||
const __m128i in = _mm_loadu_si128(static_cast<const __m128i*>(static_cast<const void*>(p))); // NOLINT(bugprone-casting-through-void)
|
||||
const __m128i special = _mm_or_si128(_mm_or_si128(_mm_cmpeq_epi8(in, _mm_set1_epi8('"')), _mm_cmpeq_epi8(in, _mm_set1_epi8('\\'))),
|
||||
_mm_or_si128(_mm_cmpeq_epi8(in, _mm_set1_epi8(0x7F)), _mm_cmplt_epi8(in, _mm_set1_epi8(0x20))));
|
||||
return static_cast<std::uint64_t>(static_cast<unsigned>(_mm_movemask_epi8(special)));
|
||||
#endif
|
||||
}
|
||||
|
||||
/// the width in bits of a byte in the mask of ascii_stop_mask()
|
||||
constexpr unsigned ascii_stop_bits() noexcept
|
||||
{
|
||||
return NLOHMANN_VIEW_NEON ? 4u : 1u;
|
||||
}
|
||||
#endif
|
||||
|
||||
/// As find_ascii_copyable_run(), 16 bytes per step. end is the end of the
|
||||
/// buffer that holds the n bytes at p (a short run is read as 16 bytes if
|
||||
/// they are in the buffer).
|
||||
inline std::size_t ascii_copyable_run(const unsigned char* p, std::size_t n, const unsigned char* end) noexcept
|
||||
{
|
||||
#if NLOHMANN_VIEW_VECTOR
|
||||
if (n >= 16)
|
||||
{
|
||||
std::size_t i = 0;
|
||||
for (; i + 16 <= n; i += 16)
|
||||
{
|
||||
const std::uint64_t mask = ascii_stop_mask(p + i);
|
||||
if (mask != 0)
|
||||
{
|
||||
return i + (static_cast<std::size_t>(count_trailing_zeros(mask)) / ascii_stop_bits());
|
||||
}
|
||||
}
|
||||
if (i == n)
|
||||
{
|
||||
return n;
|
||||
}
|
||||
// the last 16 bytes: those before i are known to be copyable
|
||||
const std::uint64_t mask = ascii_stop_mask(p + n - 16);
|
||||
return mask != 0 ? n - 16 + (static_cast<std::size_t>(count_trailing_zeros(mask)) / ascii_stop_bits()) : n;
|
||||
}
|
||||
if (end - p >= 16)
|
||||
{
|
||||
// (the bytes after the run are masked out)
|
||||
const std::uint64_t mask = ascii_stop_mask(p) & ((std::uint64_t{1} << (n * ascii_stop_bits())) - 1u);
|
||||
return mask != 0 ? static_cast<std::size_t>(count_trailing_zeros(mask)) / ascii_stop_bits() : n;
|
||||
}
|
||||
#else
|
||||
static_cast<void>(end);
|
||||
#endif
|
||||
return find_ascii_copyable_run(p, n);
|
||||
}
|
||||
|
||||
/// A stack that starts in a buffer of the caller (a local array) and moves to
|
||||
/// the heap (a vector of the caller) only when that is full, so that dumps of
|
||||
/// shallow documents need no allocation. The top is a pointer, as in
|
||||
@@ -349,39 +250,6 @@ NLOHMANN_VIEW_ALWAYS_INLINE char* put_literal(char* w, const char (&s)[N]) noexc
|
||||
return w + N - 1;
|
||||
}
|
||||
|
||||
/// the depth of the innermost open container, counted only for indented output
|
||||
template<bool Counted>
|
||||
struct depth_counter
|
||||
{
|
||||
std::size_t value = 0;
|
||||
void enter() noexcept
|
||||
{
|
||||
++value;
|
||||
}
|
||||
std::size_t leave() noexcept
|
||||
{
|
||||
return --value;
|
||||
}
|
||||
std::size_t get() const noexcept
|
||||
{
|
||||
return value;
|
||||
}
|
||||
};
|
||||
|
||||
template<>
|
||||
struct depth_counter<false>
|
||||
{
|
||||
void enter() noexcept {}
|
||||
std::size_t leave() noexcept
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
std::size_t get() const noexcept
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
};
|
||||
|
||||
/// how the view's dump() writes a value
|
||||
struct dump_style
|
||||
{
|
||||
@@ -418,257 +286,121 @@ class view_serializer
|
||||
|
||||
void dump(const node* root)
|
||||
{
|
||||
if (m_style.ensure_ascii)
|
||||
if (!m_style.pretty && !m_style.ensure_ascii)
|
||||
{
|
||||
dump_ascii(root);
|
||||
if (m_style.source_numbers)
|
||||
{
|
||||
dump_compact<true>(root);
|
||||
}
|
||||
else
|
||||
{
|
||||
dump_compact<false>(root);
|
||||
}
|
||||
return;
|
||||
}
|
||||
else if (m_style.pretty)
|
||||
struct frame
|
||||
{
|
||||
dump_pretty(root);
|
||||
}
|
||||
else if (m_style.source_numbers)
|
||||
const node* pos; ///< next element, or key of the next member
|
||||
const node* end;
|
||||
bool object;
|
||||
bool first; ///< nothing written yet
|
||||
};
|
||||
std::array<frame, 32> buffer; // NOLINT(cppcoreguidelines-pro-type-member-init,hicpp-member-init): written before read
|
||||
std::vector<frame> heap;
|
||||
small_stack<frame> stack(buffer.data(), buffer.size(), heap);
|
||||
const node* n = root;
|
||||
for (;;)
|
||||
{
|
||||
dump_text<true, false, false>(root);
|
||||
}
|
||||
else
|
||||
{
|
||||
dump_text<false, false, false>(root);
|
||||
// write the value at n
|
||||
if (is_container(*n))
|
||||
{
|
||||
const bool object = n->kind == static_cast<std::uint8_t>(value_t::object);
|
||||
if (n->len == 0)
|
||||
{
|
||||
m_out.put(object ? "{}" : "[]", 2);
|
||||
}
|
||||
else
|
||||
{
|
||||
m_out.put(object ? '{' : '[');
|
||||
stack.push_back(frame{nav::first(m_doc, n), nav::end(m_doc, n), object, true});
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
write_scalar(*n);
|
||||
}
|
||||
|
||||
// go to the next value: close finished containers, then separate
|
||||
for (;;)
|
||||
{
|
||||
if (stack.empty())
|
||||
{
|
||||
m_out.finish();
|
||||
return;
|
||||
}
|
||||
if (stack.back().pos == stack.back().end)
|
||||
{
|
||||
const bool object = stack.back().object;
|
||||
stack.pop_back();
|
||||
newline(stack.size());
|
||||
m_out.put(object ? '}' : ']');
|
||||
continue;
|
||||
}
|
||||
frame& f = stack.back();
|
||||
if (!f.first)
|
||||
{
|
||||
m_out.put(',');
|
||||
}
|
||||
f.first = false;
|
||||
newline(stack.size());
|
||||
if (f.object)
|
||||
{
|
||||
write_string(*f.pos);
|
||||
if (m_style.pretty)
|
||||
{
|
||||
m_out.put(": ");
|
||||
}
|
||||
else
|
||||
{
|
||||
m_out.put(':');
|
||||
}
|
||||
n = nav::value(f.pos + 1);
|
||||
f.pos = document_data::after(f.pos + 1);
|
||||
}
|
||||
else
|
||||
{
|
||||
n = nav::value(f.pos);
|
||||
f.pos = document_data::after(f.pos);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
/// a string that is not a plain string of the source (decoded, or written
|
||||
/// by an edit), without ensure_ascii: runs without characters to escape
|
||||
/// are copied. If validate is set, the string must be valid UTF-8: the
|
||||
/// runs are found by the lexer's scan, which validates the sequences, and
|
||||
/// invalid UTF-8 throws what basic_json::dump() throws. (The compact output
|
||||
/// writes the bytes of a damaged image unchanged, and scans without
|
||||
/// validating them.)
|
||||
NLOHMANN_VIEW_NOINLINE void write_decoded(const node& n, bool validate)
|
||||
/// are copied
|
||||
NLOHMANN_VIEW_NOINLINE void write_decoded(const node& n)
|
||||
{
|
||||
const auto* const s = reinterpret_cast<const unsigned char*>(m_doc.str(n)); // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast)
|
||||
m_out.put('"');
|
||||
for (std::size_t i = 0; i < n.len;)
|
||||
{
|
||||
const std::size_t run = validate ? string_bulk_run(s + i, n.len - i) : plain_output_run(s + i, n.len - i);
|
||||
const std::size_t run = plain_output_run(s + i, n.len - i);
|
||||
if (run != 0)
|
||||
{
|
||||
m_out.put(reinterpret_cast<const char*>(s + i), run); // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast)
|
||||
i += run;
|
||||
continue;
|
||||
}
|
||||
if (validate && s[i] >= 0x80)
|
||||
{
|
||||
invalid_utf8(s, n.len); // (the scan stopped at ill-formed UTF-8)
|
||||
}
|
||||
write_codepoint<false>(s[i], s + i, 1); // a quote, a backslash, or a control character
|
||||
++i;
|
||||
}
|
||||
m_out.put('"');
|
||||
}
|
||||
|
||||
/// dump_text() with indentation, out of line: the compact walk is faster
|
||||
/// when it is not merged with the indented ones into dump()
|
||||
NLOHMANN_VIEW_NOINLINE void dump_pretty(const node* root)
|
||||
{
|
||||
if (m_style.source_numbers)
|
||||
{
|
||||
dump_text<true, true, false>(root);
|
||||
}
|
||||
else
|
||||
{
|
||||
dump_text<false, true, false>(root);
|
||||
}
|
||||
}
|
||||
|
||||
/// dump_text() with ensure_ascii, out of line: the walk without it is
|
||||
/// faster when it is not merged with this one into dump()
|
||||
NLOHMANN_VIEW_NOINLINE void dump_ascii(const node* root)
|
||||
{
|
||||
if (m_style.pretty)
|
||||
{
|
||||
if (m_style.source_numbers)
|
||||
{
|
||||
dump_text<true, true, true>(root);
|
||||
}
|
||||
else
|
||||
{
|
||||
dump_text<false, true, true>(root);
|
||||
}
|
||||
}
|
||||
else if (m_style.source_numbers)
|
||||
{
|
||||
dump_text<true, false, true>(root);
|
||||
}
|
||||
else
|
||||
{
|
||||
dump_text<false, false, true>(root);
|
||||
}
|
||||
}
|
||||
|
||||
/// whether dump_text() copies a string or key from the source: one of
|
||||
/// the source, and with ensure_ascii one without bytes >= 0x7F (an overload
|
||||
/// each, so that the walk without ensure_ascii is compiled exactly as
|
||||
/// before: with GCC, a condition that only folds away later changed its
|
||||
/// block layout and made it 4-6% slower)
|
||||
static bool copied(const node& n, const char* /*src*/, std::false_type /*ensure_ascii*/) noexcept
|
||||
{
|
||||
return (n.flags & node_flags::storage) == 0;
|
||||
}
|
||||
|
||||
static bool copied(const node& n, const char* src, std::true_type /*ensure_ascii*/) noexcept
|
||||
{
|
||||
return (n.flags & node_flags::storage) == 0 && is_ascii_without_delete(src + n.off, n.len);
|
||||
}
|
||||
|
||||
/*!
|
||||
@brief a string with ensure_ascii: the characters that are not printable ASCII as \\uXXXX
|
||||
(a character beyond U+FFFF as a surrogate pair); strings of a document
|
||||
are valid UTF-8, except in a damaged image loaded with image_check::bounds,
|
||||
for which this throws what basic_json::dump() throws for the string
|
||||
*/
|
||||
NLOHMANN_VIEW_NOINLINE void write_ascii(const node& n)
|
||||
{
|
||||
const auto* const s = reinterpret_cast<const unsigned char*>(m_doc.str(n)); // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast)
|
||||
const std::size_t size = n.len;
|
||||
char* w = m_out.cursor();
|
||||
char* lim = m_out.limit();
|
||||
// room for k bytes
|
||||
const auto room = [&](std::size_t k)
|
||||
{
|
||||
if (NLOHMANN_VIEW_UNLIKELY(static_cast<std::size_t>(lim - w) < k))
|
||||
{
|
||||
m_out.set_cursor(w);
|
||||
m_out.reserve(k);
|
||||
w = m_out.cursor();
|
||||
lim = m_out.limit();
|
||||
}
|
||||
};
|
||||
static constexpr const char* hex = "0123456789abcdef";
|
||||
// \\uXXXX
|
||||
const auto u_escape = [&](std::uint32_t u)
|
||||
{
|
||||
w[0] = '\\';
|
||||
w[1] = 'u';
|
||||
w[2] = hex[(u >> 12u) & 0xFu];
|
||||
w[3] = hex[(u >> 8u) & 0xFu];
|
||||
w[4] = hex[(u >> 4u) & 0xFu];
|
||||
w[5] = hex[u & 0xFu];
|
||||
w += 6;
|
||||
};
|
||||
room(1);
|
||||
*w++ = '"';
|
||||
std::size_t i = 0;
|
||||
// (a string of the source starts with a run that has nothing to escape, 16 bytes per step)
|
||||
const std::size_t prefix = (n.flags & node_flags::storage) == 0 ? ascii_copyable_run(s, size, reinterpret_cast<const unsigned char*>(m_doc.src + m_doc.size)) : 0; // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast)
|
||||
if (prefix != 0)
|
||||
{
|
||||
room(prefix);
|
||||
std::memcpy(w, s, prefix);
|
||||
w += prefix;
|
||||
i = prefix;
|
||||
}
|
||||
while (i < size)
|
||||
{
|
||||
// (a character that needs an escape sequence is not worth a call)
|
||||
const std::size_t run = is_ascii_copyable(s[i]) ? find_ascii_copyable_run(s + i, size - i) : 0;
|
||||
if (run != 0)
|
||||
{
|
||||
room(run);
|
||||
std::memcpy(w, s + i, run);
|
||||
w += run;
|
||||
i += run;
|
||||
continue;
|
||||
}
|
||||
room(12);
|
||||
if (s[i] >= 0x80)
|
||||
{
|
||||
const unsigned char lead = s[i];
|
||||
// (the two-byte sequences of Latin text and the three-byte sequences of most other
|
||||
// text, with continuation bytes of any value: the ranges that are restricted (E0,
|
||||
// ED, and the sequences of four bytes) go through validate_one_utf8())
|
||||
if (lead >= 0xE1 && lead <= 0xEF && lead != 0xED && size - i >= 3 && (s[i + 1] & 0xC0u) == 0x80 && (s[i + 2] & 0xC0u) == 0x80)
|
||||
{
|
||||
u_escape(((lead & 0x0Fu) << 12u) | ((s[i + 1] & 0x3Fu) << 6u) | (s[i + 2] & 0x3Fu));
|
||||
i += 3;
|
||||
continue;
|
||||
}
|
||||
if (lead >= 0xC2 && lead <= 0xDF && size - i >= 2 && (s[i + 1] & 0xC0u) == 0x80)
|
||||
{
|
||||
u_escape(((lead & 0x1Fu) << 6u) | (s[i + 1] & 0x3Fu));
|
||||
i += 2;
|
||||
continue;
|
||||
}
|
||||
const std::size_t len = validate_one_utf8(s + i, size - i);
|
||||
if (NLOHMANN_VIEW_UNLIKELY(len == 0))
|
||||
{
|
||||
m_out.set_cursor(w);
|
||||
invalid_utf8(s, size);
|
||||
return; // LCOV_EXCL_LINE (invalid_utf8 throws)
|
||||
}
|
||||
std::uint32_t codepoint = s[i] & (0xFFu >> (len + 1));
|
||||
for (std::size_t k = 1; k < len; ++k)
|
||||
{
|
||||
codepoint = (codepoint << 6u) | (s[i + k] & 0x3Fu);
|
||||
}
|
||||
i += len;
|
||||
if (codepoint <= 0xFFFF)
|
||||
{
|
||||
u_escape(codepoint);
|
||||
}
|
||||
else
|
||||
{
|
||||
u_escape(0xD7C0u + (codepoint >> 10u));
|
||||
u_escape(0xDC00u + (codepoint & 0x3FFu));
|
||||
}
|
||||
continue;
|
||||
}
|
||||
// a quote, a backslash, or a control character (or DEL)
|
||||
const unsigned char c = s[i++];
|
||||
char escape = 0;
|
||||
switch (c)
|
||||
{
|
||||
case 0x08:
|
||||
escape = 'b';
|
||||
break;
|
||||
case 0x09:
|
||||
escape = 't';
|
||||
break;
|
||||
case 0x0A:
|
||||
escape = 'n';
|
||||
break;
|
||||
case 0x0C:
|
||||
escape = 'f';
|
||||
break;
|
||||
case 0x0D:
|
||||
escape = 'r';
|
||||
break;
|
||||
case 0x22:
|
||||
escape = '"';
|
||||
break;
|
||||
case 0x5C:
|
||||
escape = '\\';
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
if (escape != 0)
|
||||
{
|
||||
w[0] = '\\';
|
||||
w[1] = escape;
|
||||
w += 2;
|
||||
}
|
||||
else
|
||||
{
|
||||
u_escape(c);
|
||||
}
|
||||
}
|
||||
room(1);
|
||||
*w++ = '"';
|
||||
m_out.set_cursor(w);
|
||||
}
|
||||
|
||||
/// the copies of dump_text() that are not fixed-size moves (long
|
||||
/// the copies of dump_compact() that are not fixed-size moves (long
|
||||
/// strings, or near the end of the source); out of line, so that the
|
||||
/// compiler does not merge the fixed-size moves into this call
|
||||
NLOHMANN_VIEW_NOINLINE static void copy_long(char* to, const char* from, std::size_t n) noexcept
|
||||
@@ -685,19 +417,17 @@ class view_serializer
|
||||
}
|
||||
|
||||
/*!
|
||||
@brief the output, compact (the default dump()) or indented, with or without ensure_ascii
|
||||
@brief the compact output without ensure_ascii (the default dump())
|
||||
|
||||
The same walk as dump(), with the write position in a local variable
|
||||
(stores through char pointers would otherwise force a reload of the
|
||||
buffer's members after each one), and with strings and number tokens of
|
||||
the source copied by fixed-size moves of 32 bytes where the source has
|
||||
that many bytes left, instead of a library call per token. The buffer
|
||||
keeps 64 bytes of slack for the overshoot. With ensure_ascii (Ascii), a
|
||||
string of the source is copied only if it has nothing to escape (see
|
||||
copied()); any other string is written by write_ascii().
|
||||
keeps 64 bytes of slack for the overshoot.
|
||||
*/
|
||||
template<bool SourceNumbers, bool Pretty, bool Ascii>
|
||||
void dump_text(const node* root)
|
||||
template<bool SourceNumbers>
|
||||
void dump_compact(const node* root)
|
||||
{
|
||||
struct frame
|
||||
{
|
||||
@@ -712,16 +442,6 @@ class view_serializer
|
||||
const char* const src_end = src + m_doc.size;
|
||||
char* w = m_out.cursor();
|
||||
char* lim = m_out.limit();
|
||||
// (indented output) a line break and 63 indentation characters, from
|
||||
// which the line break of a level of up to 63 characters is a copy
|
||||
std::array<char, 64> ruler; // NOLINT(cppcoreguidelines-pro-type-member-init,hicpp-member-init): filled below if used
|
||||
if (Pretty)
|
||||
{
|
||||
ruler.fill(m_style.indent_char);
|
||||
ruler[0] = '\n';
|
||||
}
|
||||
const std::size_t indent = m_style.indent;
|
||||
depth_counter<Pretty> level; // depth of the innermost open container (indented output)
|
||||
// room for n bytes and the slack
|
||||
const auto room = [&](std::size_t n)
|
||||
{
|
||||
@@ -753,35 +473,12 @@ class view_serializer
|
||||
}
|
||||
w += n;
|
||||
};
|
||||
// (indented output) the line break and the indentation of a level
|
||||
const auto newline = [&](std::size_t depth)
|
||||
{
|
||||
const std::size_t count = 1 + (depth * indent);
|
||||
room(count);
|
||||
if (count <= ruler.size())
|
||||
{
|
||||
std::memcpy(w, ruler.data(), ruler.size());
|
||||
}
|
||||
else
|
||||
{
|
||||
*w = '\n';
|
||||
std::memset(w + 1, m_style.indent_char, count - 1);
|
||||
}
|
||||
w += count;
|
||||
};
|
||||
// a string that is not a plain string of the source (out of line, so
|
||||
// that the cursor stays in a register here)
|
||||
const auto escaped = [&](const node & n)
|
||||
{
|
||||
m_out.set_cursor(w);
|
||||
if (enabled(Ascii))
|
||||
{
|
||||
write_ascii(n);
|
||||
}
|
||||
else
|
||||
{
|
||||
write_decoded(n, Pretty);
|
||||
}
|
||||
write_decoded(n);
|
||||
w = m_out.cursor();
|
||||
lim = m_out.limit();
|
||||
};
|
||||
@@ -804,7 +501,7 @@ class view_serializer
|
||||
switch (static_cast<value_t>(n->kind))
|
||||
{
|
||||
case value_t::string:
|
||||
if (copied(*n, src, std::integral_constant<bool, Ascii>()))
|
||||
if ((n->flags & node_flags::storage) == 0)
|
||||
{
|
||||
room(n->len + 2);
|
||||
*w++ = '"';
|
||||
@@ -881,7 +578,6 @@ class view_serializer
|
||||
{
|
||||
*w++ = object ? '{' : '[';
|
||||
stack.push_back(cur);
|
||||
level.enter();
|
||||
if (Editable)
|
||||
{
|
||||
cur = frame{nav::first(m_doc, n), nav::end(m_doc, n), object};
|
||||
@@ -924,10 +620,6 @@ class view_serializer
|
||||
{
|
||||
break;
|
||||
}
|
||||
if (Pretty)
|
||||
{
|
||||
newline(level.leave());
|
||||
}
|
||||
room(1);
|
||||
*w++ = cur.object ? '}' : ']';
|
||||
cur = stack.back();
|
||||
@@ -936,17 +628,13 @@ class view_serializer
|
||||
room(1);
|
||||
*w++ = ',';
|
||||
}
|
||||
if (Pretty)
|
||||
{
|
||||
newline(level.get());
|
||||
}
|
||||
const node* const at = Editable ? cur.pos : n;
|
||||
if (cur.object)
|
||||
{
|
||||
const node& key = *at;
|
||||
if (copied(key, src, std::integral_constant<bool, Ascii>()))
|
||||
if ((key.flags & node_flags::storage) == 0)
|
||||
{
|
||||
room(key.len + (Pretty ? 4 : 3));
|
||||
room(key.len + 3);
|
||||
*w++ = '"';
|
||||
copy(src + key.off, key.len);
|
||||
w[0] = '"';
|
||||
@@ -956,13 +644,9 @@ class view_serializer
|
||||
else
|
||||
{
|
||||
escaped(key);
|
||||
room(Pretty ? 2 : 1);
|
||||
room(1);
|
||||
*w++ = ':';
|
||||
}
|
||||
if (Pretty)
|
||||
{
|
||||
*w++ = ' ';
|
||||
}
|
||||
if (Editable)
|
||||
{
|
||||
n = nav::value(at + 1);
|
||||
@@ -981,6 +665,69 @@ class view_serializer
|
||||
}
|
||||
}
|
||||
|
||||
void newline(std::size_t level)
|
||||
{
|
||||
if (m_style.pretty)
|
||||
{
|
||||
m_out.put('\n');
|
||||
m_out.put_repeated(m_style.indent_char, level * m_style.indent);
|
||||
}
|
||||
}
|
||||
|
||||
void write_scalar(const node& n)
|
||||
{
|
||||
switch (static_cast<value_t>(n.kind))
|
||||
{
|
||||
case value_t::null:
|
||||
m_out.put("null");
|
||||
break;
|
||||
case value_t::boolean:
|
||||
if ((n.flags & node_flags::is_true) != 0)
|
||||
{
|
||||
m_out.put("true");
|
||||
}
|
||||
else
|
||||
{
|
||||
m_out.put("false");
|
||||
}
|
||||
break;
|
||||
case value_t::string:
|
||||
write_string(n);
|
||||
break;
|
||||
case value_t::number_integer:
|
||||
case value_t::number_unsigned:
|
||||
{
|
||||
const char* const token = m_doc.str(n);
|
||||
const std::uint32_t len = number_length(n);
|
||||
if (!m_style.source_numbers && len == 2 && token[0] == '-' && token[1] == '0')
|
||||
{
|
||||
m_out.put('0'); // parse() reads -0 as the integer 0
|
||||
}
|
||||
else
|
||||
{
|
||||
m_out.put(token, len);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case value_t::number_float:
|
||||
if (m_style.source_numbers && (n.flags & node_flags::storage) != node_flags::edited)
|
||||
{
|
||||
m_out.put(m_doc.str(n), n.len); // (a float set by an edit is written as with shortest)
|
||||
}
|
||||
else
|
||||
{
|
||||
write_float_node(n);
|
||||
}
|
||||
break;
|
||||
case value_t::object: // LCOV_EXCL_LINE (containers are written by dump())
|
||||
case value_t::array: // LCOV_EXCL_LINE
|
||||
case value_t::binary: // LCOV_EXCL_LINE (not in a document)
|
||||
case value_t::discarded: // LCOV_EXCL_LINE
|
||||
default: // LCOV_EXCL_LINE
|
||||
break; // LCOV_EXCL_LINE
|
||||
}
|
||||
}
|
||||
|
||||
/// a float node as dump() writes it
|
||||
void write_float_node(const node& n)
|
||||
{
|
||||
@@ -1047,7 +794,8 @@ class view_serializer
|
||||
static char* write_double_value_at(char* w, double x)
|
||||
{
|
||||
// (from the bits: without the checks of to_chars())
|
||||
auto bits = bit_cast<std::uint64_t>(x);
|
||||
std::uint64_t bits = 0;
|
||||
std::memcpy(&bits, &x, sizeof(bits));
|
||||
if (NLOHMANN_VIEW_UNLIKELY((bits & 0x7FF0000000000000u) == 0x7FF0000000000000u))
|
||||
{
|
||||
return write_text_at(w, "null", 4);
|
||||
@@ -1091,6 +839,71 @@ class view_serializer
|
||||
m_out.put(s.data(), s.size());
|
||||
}
|
||||
|
||||
void write_string(const node& n)
|
||||
{
|
||||
const char* const s = m_doc.str(n);
|
||||
m_out.put('"');
|
||||
if ((n.flags & node_flags::storage) == 0 && !m_style.ensure_ascii)
|
||||
{
|
||||
// a string of the source without escape sequences has nothing to escape
|
||||
m_out.put(s, n.len);
|
||||
}
|
||||
else if (m_style.ensure_ascii)
|
||||
{
|
||||
write_escaped<true>(reinterpret_cast<const unsigned char*>(s), n.len); // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast)
|
||||
}
|
||||
else
|
||||
{
|
||||
write_escaped<false>(reinterpret_cast<const unsigned char*>(s), n.len); // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast)
|
||||
}
|
||||
m_out.put('"');
|
||||
}
|
||||
|
||||
/// as serializer::dump_escaped(); strings of a document are valid UTF-8,
|
||||
/// except in a damaged image loaded with image_check::bounds, for which
|
||||
/// this throws what basic_json::dump() throws for the string
|
||||
template<bool EnsureAscii>
|
||||
void write_escaped(const unsigned char* s, std::size_t n)
|
||||
{
|
||||
std::size_t i = 0;
|
||||
while (i < n)
|
||||
{
|
||||
std::size_t run = 0;
|
||||
if (!EnsureAscii)
|
||||
{
|
||||
run = string_bulk_run(s + i, n - i);
|
||||
}
|
||||
else if (is_ascii_copyable(s[i]))
|
||||
{
|
||||
run = find_ascii_copyable_run(s + i, n - i);
|
||||
}
|
||||
if (run != 0)
|
||||
{
|
||||
m_out.put(reinterpret_cast<const char*>(s + i), run); // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast)
|
||||
i += run;
|
||||
continue;
|
||||
}
|
||||
std::uint32_t codepoint = s[i];
|
||||
std::size_t len = 1;
|
||||
if (codepoint >= 0x80)
|
||||
{
|
||||
len = validate_one_utf8(s + i, n - i);
|
||||
if (NLOHMANN_VIEW_UNLIKELY(len == 0))
|
||||
{
|
||||
invalid_utf8(s, n);
|
||||
return;
|
||||
}
|
||||
codepoint &= 0xFFu >> (len + 1);
|
||||
for (std::size_t k = 1; k < len; ++k)
|
||||
{
|
||||
codepoint = (codepoint << 6u) | (s[i + k] & 0x3Fu);
|
||||
}
|
||||
}
|
||||
write_codepoint<EnsureAscii>(codepoint, s + i, len);
|
||||
i += len;
|
||||
}
|
||||
}
|
||||
|
||||
/// throw what basic_json::dump() throws for a string that is not valid UTF-8
|
||||
NLOHMANN_VIEW_NOINLINE static void invalid_utf8(const unsigned char* s, std::size_t n)
|
||||
{
|
||||
|
||||
@@ -50,12 +50,7 @@
|
||||
#endif
|
||||
#if NLOHMANN_VIEW_SSE2 && !NLOHMANN_VIEW_SSSE3 && !defined(NLOHMANN_VIEW_NO_TARGET_ATTRIBUTE) && ((defined(__clang__) && __clang_major__ >= 4) || (defined(__GNUC__) && !defined(__clang__) && (__GNUC__ > 4 || (__GNUC__ == 4 && __GNUC_MINOR__ >= 9))))
|
||||
// (GCC before 4.9 has no SSSE3 intrinsics without -mssse3)
|
||||
// <cpuid.h> of GCC before 11 has no include guard, and json.hpp and
|
||||
// json_view.hpp both include it
|
||||
#ifndef NLOHMANN_JSON_CPUID_INCLUDED
|
||||
#define NLOHMANN_JSON_CPUID_INCLUDED
|
||||
#include <cpuid.h>
|
||||
#endif
|
||||
#include <cpuid.h>
|
||||
#include <tmmintrin.h>
|
||||
#define NLOHMANN_VIEW_SSSE3_DISPATCH 1 // NOLINT(cppcoreguidelines-macro-to-enum,modernize-macro-to-enum)
|
||||
#define NLOHMANN_VIEW_SSSE3_TARGET __attribute__((target("ssse3")))
|
||||
|
||||
+30
-241
@@ -7045,125 +7045,6 @@ public:
|
||||
detail::throw_patch_not_an_array(json_patch);
|
||||
}
|
||||
|
||||
// the usual patch operations "add", "remove", "replace" and "test" on
|
||||
// a pointer without escapes that denotes existing array elements and
|
||||
// object members (as "add" does for the parent of the new value), as
|
||||
// a diff creates them: apply the operation and return true; return
|
||||
// false, with nothing changed, for all others
|
||||
string_t token;
|
||||
const string_t value_key = "value";
|
||||
const auto try_plain = [&result, &token, &value_key](patch_operations operation, const string_t& path, const basic_json & val)
|
||||
{
|
||||
if (path.empty() || path.find_first_of('~') != string_t::npos)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// a missing "value" is reported below
|
||||
const auto value_it = val.m_data.m_value.object->find(value_key);
|
||||
const basic_json& value = value_it != val.m_data.m_value.object->end() ? value_it->second : val;
|
||||
if (operation != patch_operations::remove && value_it == val.m_data.m_value.object->end())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
switch (operation)
|
||||
{
|
||||
case patch_operations::replace:
|
||||
{
|
||||
basic_json* const target = json_pointer::resolve_plain(&result, path, path.size(), token);
|
||||
if (target == nullptr)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
*target = value;
|
||||
return true;
|
||||
}
|
||||
|
||||
case patch_operations::test:
|
||||
{
|
||||
const basic_json* const target = json_pointer::resolve_plain(&result, path, path.size(), token);
|
||||
// a failing test is reported below
|
||||
return target != nullptr && *target == value;
|
||||
}
|
||||
|
||||
case patch_operations::add:
|
||||
case patch_operations::remove:
|
||||
{
|
||||
// the parent of the pointer and its last reference token
|
||||
std::size_t slash = path.size();
|
||||
while (slash > 0 && path[slash - 1] != '/')
|
||||
{
|
||||
--slash;
|
||||
}
|
||||
if (slash == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
--slash;
|
||||
|
||||
basic_json* const parent = json_pointer::resolve_plain(&result, path, slash, token);
|
||||
if (parent == nullptr)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
token.clear();
|
||||
token.append(path.data() + slash + 1, path.size() - slash - 1);
|
||||
|
||||
if (operation == patch_operations::add)
|
||||
{
|
||||
if (parent->is_object())
|
||||
{
|
||||
(*parent)[token] = value;
|
||||
return true;
|
||||
}
|
||||
if (parent->is_array())
|
||||
{
|
||||
typename basic_json::size_type idx{};
|
||||
if (token == "-")
|
||||
{
|
||||
parent->push_back(value);
|
||||
return true;
|
||||
}
|
||||
if (json_pointer::template parse_array_index<basic_json_t>(token, idx) == json_pointer::array_index_status::ok && idx <= parent->size())
|
||||
{
|
||||
parent->insert(parent->begin() + static_cast<difference_type>(idx), value);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
if (parent->is_object())
|
||||
{
|
||||
const auto it = parent->find(token);
|
||||
if (it == parent->end())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
parent->erase(it);
|
||||
return true;
|
||||
}
|
||||
if (parent->is_array())
|
||||
{
|
||||
typename basic_json::size_type idx{};
|
||||
if (json_pointer::template parse_array_index<basic_json_t>(token, idx) == json_pointer::array_index_status::ok && idx < parent->size())
|
||||
{
|
||||
parent->erase(idx);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
case patch_operations::move:
|
||||
case patch_operations::copy:
|
||||
case patch_operations::invalid:
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
// iterate and apply the operations
|
||||
for (const auto& val : json_patch)
|
||||
{
|
||||
@@ -7200,19 +7081,9 @@ public:
|
||||
// collect mandatory members
|
||||
const string_t& op = *get_value("op", "op", true).m_data.m_value.string;
|
||||
const string_t& path = *get_value(op, "path", true).m_data.m_value.string;
|
||||
const patch_operations operation = get_op(op);
|
||||
|
||||
// the usual operations on a plain pointer are applied without
|
||||
// building the pointer's reference tokens; anything else (and
|
||||
// every error) is handled below
|
||||
if (try_plain(operation, path, val))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
json_pointer ptr(path);
|
||||
|
||||
switch (operation)
|
||||
switch (get_op(op))
|
||||
{
|
||||
case patch_operations::add:
|
||||
{
|
||||
@@ -7320,8 +7191,7 @@ public:
|
||||
{
|
||||
// the patch
|
||||
basic_json result(value_t::array);
|
||||
string_t buffer = path;
|
||||
diff_recursively(result, source, target, buffer, 0);
|
||||
diff_recursively(result, source, target, path, 0);
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -7365,69 +7235,31 @@ public:
|
||||
// function it appears in. In diff_recursively, which is on the call stack
|
||||
// once per nesting level, that made every level cost kilobytes of stack.
|
||||
|
||||
/// @brief append an operation @a op for @a path to @a result, with @a value if it is not null
|
||||
static void diff_operation(basic_json& result, const char* op, const string_t& path, const basic_json* value)
|
||||
{
|
||||
basic_json entry(value_t::object);
|
||||
entry["op"] = op;
|
||||
entry["path"] = path;
|
||||
if (value != nullptr)
|
||||
{
|
||||
entry["value"] = *value;
|
||||
}
|
||||
result.push_back(std::move(entry));
|
||||
}
|
||||
|
||||
/// @brief append a "replace" operation for @a path with @a value to @a result
|
||||
static void diff_replace(basic_json& result, const string_t& path, const basic_json& value)
|
||||
{
|
||||
diff_operation(result, "replace", path, &value);
|
||||
result.push_back(
|
||||
{
|
||||
{"op", "replace"}, {"path", path}, {"value", value}
|
||||
});
|
||||
}
|
||||
|
||||
/// @brief append a "remove" operation for @a path to @a result
|
||||
static void diff_remove(basic_json& result, const string_t& path)
|
||||
{
|
||||
diff_operation(result, "remove", path, nullptr);
|
||||
result.push_back(object(
|
||||
{
|
||||
{"op", "remove"}, {"path", path}
|
||||
}));
|
||||
}
|
||||
|
||||
/// @brief append an "add" operation for @a path with @a value to @a result
|
||||
static void diff_add(basic_json& result, const string_t& path, const basic_json& value)
|
||||
{
|
||||
diff_operation(result, "add", path, &value);
|
||||
}
|
||||
|
||||
/// @brief append "/" and the decimal representation of @a index to @a path
|
||||
static void diff_append_index(string_t& path, std::size_t index)
|
||||
{
|
||||
path.push_back('/');
|
||||
json_pointer::append_index(path, index);
|
||||
}
|
||||
|
||||
/// @brief append "/" and the escaped @a key to @a path
|
||||
static void diff_append_key(string_t& path, const string_t& key)
|
||||
{
|
||||
path.push_back('/');
|
||||
detail::escape_into(path, key);
|
||||
}
|
||||
|
||||
/// @brief the path of the member @a key of the value at @a path
|
||||
static string_t diff_key_path(const string_t& path, const string_t& key)
|
||||
{
|
||||
string_t result;
|
||||
result.reserve(path.size() + key.size() + 1);
|
||||
result.append(path.data(), path.size());
|
||||
diff_append_key(result, key);
|
||||
return result;
|
||||
}
|
||||
|
||||
/// @brief the path of the element @a index of the value at @a path
|
||||
static string_t diff_index_path(const string_t& path, const std::size_t index)
|
||||
{
|
||||
string_t result;
|
||||
result.reserve(path.size() + 8);
|
||||
result.append(path.data(), path.size());
|
||||
diff_append_index(result, index);
|
||||
return result;
|
||||
result.push_back(
|
||||
{
|
||||
{"op", "add"}, {"path", path}, {"value", value}
|
||||
});
|
||||
}
|
||||
|
||||
/// @brief append the "remove" operations for the elements of array
|
||||
@@ -7440,18 +7272,13 @@ public:
|
||||
// in that order avoids the quadratic reinsertion done before
|
||||
for (std::size_t j = source.size(); j > index; --j)
|
||||
{
|
||||
diff_remove(result, diff_index_path(path, j - 1));
|
||||
diff_remove(result, detail::concat<string_t>(path, '/', detail::to_string<string_t>(j - 1)));
|
||||
}
|
||||
|
||||
// add other remaining elements
|
||||
if (source.size() < target.size())
|
||||
for (std::size_t i = source.size(); i < target.size(); ++i)
|
||||
{
|
||||
string_t tail_path = path;
|
||||
tail_path.append("/-", 2);
|
||||
for (std::size_t i = source.size(); i < target.size(); ++i)
|
||||
{
|
||||
diff_add(result, tail_path, target[i]);
|
||||
}
|
||||
diff_add(result, detail::concat<string_t>(path, "/-"), target[i]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7460,7 +7287,7 @@ public:
|
||||
{
|
||||
for (const auto& it : added)
|
||||
{
|
||||
diff_add(result, diff_key_path(path, it.key()), it.value());
|
||||
diff_add(result, detail::concat<string_t>(path, '/', detail::escape(it.key())), it.value());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7481,25 +7308,10 @@ public:
|
||||
std::vector<const_iterator>& added)
|
||||
{
|
||||
// first pass: one lookup per source key
|
||||
// Objects of the same size usually have the same keys, in the same
|
||||
// order: walk them together as long as that holds, which takes no lookups.
|
||||
target_values.reserve(source.size());
|
||||
std::size_t common = 0;
|
||||
bool together = source.size() == target.size();
|
||||
for (auto it = source.cbegin(), partner = target.cbegin(); it != source.cend(); ++it)
|
||||
for (auto it = source.cbegin(); it != source.cend(); ++it)
|
||||
{
|
||||
if (together)
|
||||
{
|
||||
if (it.key() == partner.key())
|
||||
{
|
||||
target_values.push_back(&partner.value());
|
||||
++common;
|
||||
++partner;
|
||||
continue;
|
||||
}
|
||||
together = false;
|
||||
}
|
||||
|
||||
const auto found = target.find(it.key());
|
||||
if (found != target.cend())
|
||||
{
|
||||
@@ -7512,12 +7324,6 @@ public:
|
||||
}
|
||||
}
|
||||
|
||||
// the same keys in the same order
|
||||
if (together)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// second pass: find the keys only target has. An object type that
|
||||
// keeps its members in insertion order, such as the one backing
|
||||
// ordered_json, also needs the common keys to be in the same order
|
||||
@@ -7583,13 +7389,6 @@ public:
|
||||
return true;
|
||||
}
|
||||
|
||||
// equal objects, whose members are in a different order, need no
|
||||
// operations
|
||||
if (source == target)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// slow path: the common keys are in a different relative
|
||||
// order in source and target (only possible for a
|
||||
// reorderable object_t like ordered_map). Building a
|
||||
@@ -7606,7 +7405,7 @@ public:
|
||||
// it moves it to the end, fixing its position.
|
||||
for (auto it = source.cbegin(); it != source.cend(); ++it)
|
||||
{
|
||||
diff_remove(result, diff_key_path(path, it.key()));
|
||||
diff_remove(result, detail::concat<string_t>(path, '/', detail::escape(it.key())));
|
||||
}
|
||||
|
||||
// add every key that is either common (just removed
|
||||
@@ -7615,7 +7414,7 @@ public:
|
||||
// target exactly
|
||||
for (auto it = target.cbegin(); it != target.cend(); ++it)
|
||||
{
|
||||
diff_add(result, diff_key_path(path, it.key()), it.value());
|
||||
diff_add(result, detail::concat<string_t>(path, '/', detail::escape(it.key())), it.value());
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@@ -7624,9 +7423,6 @@ public:
|
||||
@brief @ref diff, for values at nesting level @a depth, appending the
|
||||
operations to @a result
|
||||
|
||||
@a path is the path of the values; it is a buffer that is extended and
|
||||
shortened again below, and is the same when the function returns.
|
||||
|
||||
Diffing two arrays or objects calls this function again, once per nesting
|
||||
level, so values nested deeply enough used to exhaust the call stack and
|
||||
terminate the process. The descent is bounded here: once @ref
|
||||
@@ -7634,7 +7430,7 @@ public:
|
||||
diff_iteratively diffs what is left without the call stack.
|
||||
*/
|
||||
static void diff_recursively(basic_json& result, const basic_json& source, const basic_json& target,
|
||||
string_t& path, const std::size_t depth)
|
||||
const string_t& path, const std::size_t depth)
|
||||
{
|
||||
// if the values are the same, there is nothing to do
|
||||
if (source == target)
|
||||
@@ -7646,9 +7442,9 @@ public:
|
||||
}
|
||||
|
||||
/// @brief @ref diff_recursively for values known to be unequal; callers
|
||||
/// compare first so they extend the path only for unequal ones
|
||||
/// compare first so they build no path for equal ones
|
||||
static void diff_unequal(basic_json& result, const basic_json& source, const basic_json& target,
|
||||
string_t& path, const std::size_t depth)
|
||||
const string_t& path, const std::size_t depth)
|
||||
{
|
||||
if (JSON_HEDLEY_UNLIKELY(depth >= detail::recursion_depth_limit()))
|
||||
{
|
||||
@@ -7667,8 +7463,6 @@ public:
|
||||
{
|
||||
case value_t::array:
|
||||
{
|
||||
const std::size_t path_length = path.size();
|
||||
|
||||
// first pass: traverse common elements
|
||||
std::size_t i = 0;
|
||||
while (i < source.size() && i < target.size())
|
||||
@@ -7676,9 +7470,7 @@ public:
|
||||
// recursive call to compare array values at index i
|
||||
if (!(source[i] == target[i]))
|
||||
{
|
||||
diff_append_index(path, i);
|
||||
diff_unequal(result, source[i], target[i], path, depth + 1);
|
||||
path.resize(path_length);
|
||||
diff_unequal(result, source[i], target[i], detail::concat<string_t>(path, '/', detail::to_string<string_t>(i)), depth + 1);
|
||||
}
|
||||
++i;
|
||||
}
|
||||
@@ -7700,7 +7492,6 @@ public:
|
||||
// lookup is needed. Deleted keys are interleaved here
|
||||
// too, in source's original order, to match the
|
||||
// historical (pre-reordering-aware) output order.
|
||||
const std::size_t path_length = path.size();
|
||||
std::size_t i = 0;
|
||||
for (auto it = source.cbegin(); it != source.cend(); ++it, ++i)
|
||||
{
|
||||
@@ -7708,15 +7499,13 @@ public:
|
||||
{
|
||||
if (!(it.value() == *target_values[i]))
|
||||
{
|
||||
diff_append_key(path, it.key());
|
||||
diff_unequal(result, it.value(), *target_values[i], path, depth + 1);
|
||||
path.resize(path_length);
|
||||
diff_unequal(result, it.value(), *target_values[i], detail::concat<string_t>(path, '/', detail::escape(it.key())), depth + 1);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// found a key that is not in target -> remove it
|
||||
diff_remove(result, diff_key_path(path, it.key()));
|
||||
diff_remove(result, detail::concat<string_t>(path, '/', detail::escape(it.key())));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7847,7 +7636,7 @@ public:
|
||||
if (i < source_array.size() && i < target_array.size())
|
||||
{
|
||||
++stack.back().index;
|
||||
diff_append_index(current_path, i);
|
||||
detail::concat_into(current_path, '/', detail::to_string<string_t>(i));
|
||||
enter(source_array[i], target_array[i]);
|
||||
if (stack.size() == depth)
|
||||
{
|
||||
@@ -7870,7 +7659,7 @@ public:
|
||||
if (target_value_ptr != nullptr)
|
||||
{
|
||||
const basic_json& target_value = *target_value_ptr;
|
||||
diff_append_key(current_path, it.key());
|
||||
detail::concat_into(current_path, '/', detail::escape(it.key()));
|
||||
enter(it.value(), target_value);
|
||||
if (stack.size() == depth)
|
||||
{
|
||||
@@ -7880,7 +7669,7 @@ public:
|
||||
else
|
||||
{
|
||||
// found a key that is not in target -> remove it
|
||||
diff_remove(result, diff_key_path(current_path, it.key()));
|
||||
diff_remove(result, detail::concat<string_t>(current_path, '/', detail::escape(it.key())));
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
Generated
+594516
-1924
File diff suppressed because it is too large.
Load diff
Generated
+415
-1663
File diff suppressed because it is too large.
Load diff
Generated
+3
-10
@@ -139,12 +139,6 @@
|
||||
#define NLOHMANN_JSON_ABI_TAG_DISABLE_UNLOCKED_FILE_IO
|
||||
#endif
|
||||
|
||||
#ifdef JSON_NO_SIMD
|
||||
#define NLOHMANN_JSON_ABI_TAG_NO_SIMD _ns
|
||||
#else
|
||||
#define NLOHMANN_JSON_ABI_TAG_NO_SIMD
|
||||
#endif
|
||||
|
||||
#ifdef JSON_VIEW_NO_SIMD
|
||||
#define NLOHMANN_JSON_ABI_TAG_VIEW_NO_SIMD _vns
|
||||
#else
|
||||
@@ -162,9 +156,9 @@
|
||||
#endif
|
||||
|
||||
// Construct the namespace ABI tags component
|
||||
#define NLOHMANN_JSON_ABI_TAGS_CONCAT_EX(a, b, c, d, e, f, g, h, i, j, k, l, m) json_abi ## a ## b ## c ## d ## e ## f ## g ## h ## i ## j ## k ## l ## m
|
||||
#define NLOHMANN_JSON_ABI_TAGS_CONCAT(a, b, c, d, e, f, g, h, i, j, k, l, m) \
|
||||
NLOHMANN_JSON_ABI_TAGS_CONCAT_EX(a, b, c, d, e, f, g, h, i, j, k, l, m)
|
||||
#define NLOHMANN_JSON_ABI_TAGS_CONCAT_EX(a, b, c, d, e, f, g, h, i, j, k, l) json_abi ## a ## b ## c ## d ## e ## f ## g ## h ## i ## j ## k ## l
|
||||
#define NLOHMANN_JSON_ABI_TAGS_CONCAT(a, b, c, d, e, f, g, h, i, j, k, l) \
|
||||
NLOHMANN_JSON_ABI_TAGS_CONCAT_EX(a, b, c, d, e, f, g, h, i, j, k, l)
|
||||
|
||||
#define NLOHMANN_JSON_ABI_TAGS \
|
||||
NLOHMANN_JSON_ABI_TAGS_CONCAT( \
|
||||
@@ -178,7 +172,6 @@
|
||||
NLOHMANN_JSON_ABI_TAG_OBJECTS_FOR_ENUM_KEYED_MAPS, \
|
||||
NLOHMANN_JSON_ABI_TAG_DISABLE_STREAM_BULK_SCAN, \
|
||||
NLOHMANN_JSON_ABI_TAG_DISABLE_UNLOCKED_FILE_IO, \
|
||||
NLOHMANN_JSON_ABI_TAG_NO_SIMD, \
|
||||
NLOHMANN_JSON_ABI_TAG_VIEW_NO_SIMD, \
|
||||
NLOHMANN_JSON_ABI_TAG_VIEW_USE_SSSE3)
|
||||
|
||||
|
||||
Generated
+245
-438
@@ -690,12 +690,7 @@ NLOHMANN_JSON_NAMESPACE_END
|
||||
#endif
|
||||
#if NLOHMANN_VIEW_SSE2 && !NLOHMANN_VIEW_SSSE3 && !defined(NLOHMANN_VIEW_NO_TARGET_ATTRIBUTE) && ((defined(__clang__) && __clang_major__ >= 4) || (defined(__GNUC__) && !defined(__clang__) && (__GNUC__ > 4 || (__GNUC__ == 4 && __GNUC_MINOR__ >= 9))))
|
||||
// (GCC before 4.9 has no SSSE3 intrinsics without -mssse3)
|
||||
// <cpuid.h> of GCC before 11 has no include guard, and json.hpp and
|
||||
// json_view.hpp both include it
|
||||
#ifndef NLOHMANN_JSON_CPUID_INCLUDED
|
||||
#define NLOHMANN_JSON_CPUID_INCLUDED
|
||||
#include <cpuid.h>
|
||||
#endif
|
||||
#include <cpuid.h>
|
||||
#include <tmmintrin.h>
|
||||
#define NLOHMANN_VIEW_SSSE3_DISPATCH 1 // NOLINT(cppcoreguidelines-macro-to-enum,modernize-macro-to-enum)
|
||||
#define NLOHMANN_VIEW_SSSE3_TARGET __attribute__((target("ssse3")))
|
||||
@@ -6089,8 +6084,6 @@ NLOHMANN_JSON_NAMESPACE_END
|
||||
|
||||
// #include <nlohmann/detail/view/number.hpp>
|
||||
|
||||
// #include <nlohmann/detail/view/simd.hpp>
|
||||
|
||||
|
||||
NLOHMANN_JSON_NAMESPACE_BEGIN
|
||||
namespace detail
|
||||
@@ -6148,6 +6141,13 @@ class output_buffer
|
||||
put(s, N - 1);
|
||||
}
|
||||
|
||||
void put_repeated(char c, std::size_t n)
|
||||
{
|
||||
reserve(n);
|
||||
std::memset(m_pos, c, n);
|
||||
m_pos += n;
|
||||
}
|
||||
|
||||
/// the write position and the end of the writable space, for a writer
|
||||
/// that keeps the position in a local variable (set_cursor() hands it back)
|
||||
char* cursor() const noexcept
|
||||
@@ -6166,13 +6166,17 @@ class output_buffer
|
||||
}
|
||||
|
||||
private:
|
||||
/// the size of a growth step
|
||||
static constexpr std::size_t step = 0x10000;
|
||||
/// the size of a growth step (a function: std::min() takes a reference,
|
||||
/// which a static constexpr member does not have before C++17)
|
||||
static constexpr std::size_t step() noexcept
|
||||
{
|
||||
return 0x10000;
|
||||
}
|
||||
|
||||
static StringType& sized(StringType& out, std::size_t estimate)
|
||||
{
|
||||
out.reserve(estimate);
|
||||
out.resize(detail::clamp(estimate, std::size_t{64}, step));
|
||||
out.resize((std::min)((std::max)(estimate, static_cast<std::size_t>(64)), step()));
|
||||
return out;
|
||||
}
|
||||
|
||||
@@ -6180,9 +6184,8 @@ class output_buffer
|
||||
{
|
||||
const auto used = static_cast<std::size_t>(m_pos - m_out.data());
|
||||
// (a step does not go beyond the reserved estimate, so that a good
|
||||
// estimate is never copied to a larger allocation; not a clamp: the
|
||||
// lower bound used + n + 256 can exceed capacity(), and then it must win)
|
||||
const std::size_t size = (std::max)((std::min)(m_out.size() + step, m_out.capacity()), used + n + 256);
|
||||
// estimate is never copied to a larger allocation)
|
||||
const std::size_t size = (std::max)((std::min)(m_out.size() + step(), m_out.capacity()), used + n + 256);
|
||||
if (size > m_out.capacity())
|
||||
{
|
||||
m_out.reserve((std::max)(m_out.capacity() * 2, size));
|
||||
@@ -6204,25 +6207,9 @@ class output_buffer
|
||||
/// image_check::bounds can have others, which are then written unchanged).
|
||||
inline std::size_t plain_output_run(const unsigned char* s, std::size_t n) noexcept
|
||||
{
|
||||
std::size_t i = 0;
|
||||
#if NLOHMANN_VIEW_SSE2
|
||||
// (16 bytes per step; with NEON, the mask of a step costs more than it saves on the
|
||||
// short runs that are typical here)
|
||||
for (; i + 16 <= n; i += 16)
|
||||
{
|
||||
const __m128i in = _mm_loadu_si128(static_cast<const __m128i*>(static_cast<const void*>(s + i))); // NOLINT(bugprone-casting-through-void)
|
||||
// (a byte is below 0x20 if it is its own minimum with 0x1F)
|
||||
const __m128i special = _mm_or_si128(_mm_or_si128(_mm_cmpeq_epi8(in, _mm_set1_epi8('"')), _mm_cmpeq_epi8(in, _mm_set1_epi8('\\'))),
|
||||
_mm_cmpeq_epi8(_mm_min_epu8(in, _mm_set1_epi8(0x1F)), in));
|
||||
const auto mask = static_cast<std::uint64_t>(static_cast<unsigned>(_mm_movemask_epi8(special)));
|
||||
if (mask != 0)
|
||||
{
|
||||
return i + static_cast<std::size_t>(count_trailing_zeros(mask));
|
||||
}
|
||||
}
|
||||
#endif
|
||||
constexpr std::uint64_t ones = 0x0101010101010101ull;
|
||||
constexpr std::uint64_t high = 0x8080808080808080ull;
|
||||
std::size_t i = 0;
|
||||
for (; i + 8 <= n; i += 8)
|
||||
{
|
||||
const std::uint64_t v = read_eight_bytes(s + i);
|
||||
@@ -6245,98 +6232,6 @@ inline std::size_t plain_output_run(const unsigned char* s, std::size_t n) noexc
|
||||
return n;
|
||||
}
|
||||
|
||||
/// Whether dump() with ensure_ascii writes the string at s unchanged, for a
|
||||
/// string of the source without escape sequences (which has no quote,
|
||||
/// backslash, or control character): no byte is DEL (0x7F) or above. Adding 1
|
||||
/// to each byte sets its high bit exactly for 0x7F, and a carry into the next
|
||||
/// byte only comes from 0xFF, whose own high bit is set anyway.
|
||||
inline bool is_ascii_without_delete(const char* s, std::size_t n) noexcept
|
||||
{
|
||||
constexpr std::uint64_t ones = 0x0101010101010101ull;
|
||||
constexpr std::uint64_t high = 0x8080808080808080ull;
|
||||
std::size_t i = 0;
|
||||
for (; i + 8 <= n; i += 8)
|
||||
{
|
||||
const std::uint64_t v = read_eight_bytes(s + i);
|
||||
if (((v | (v + ones)) & high) != 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
for (; i < n; ++i)
|
||||
{
|
||||
if (static_cast<unsigned char>(s[i]) >= 0x7F)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
#if NLOHMANN_VIEW_VECTOR
|
||||
/// the bytes of the 16 at p that ensure_ascii cannot copy (a quote, a
|
||||
/// backslash, a control character, DEL, or not ASCII; a signed compare with
|
||||
/// 0x20 finds control characters and non-ASCII bytes at once), one bit per
|
||||
/// byte (x86) or one nibble per byte (NEON)
|
||||
NLOHMANN_VIEW_ALWAYS_INLINE std::uint64_t ascii_stop_mask(const unsigned char* p) noexcept
|
||||
{
|
||||
#if NLOHMANN_VIEW_NEON
|
||||
const uint8x16_t in = vld1q_u8(p);
|
||||
const uint8x16_t special = vorrq_u8(vorrq_u8(vceqq_u8(in, vdupq_n_u8('"')), vceqq_u8(in, vdupq_n_u8('\\'))),
|
||||
vorrq_u8(vceqq_u8(in, vdupq_n_u8(0x7F)), vcltq_s8(vreinterpretq_s8_u8(in), vdupq_n_s8(0x20))));
|
||||
return vget_lane_u64(vreinterpret_u64_u8(vshrn_n_u16(vreinterpretq_u16_u8(special), 4)), 0);
|
||||
#else
|
||||
const __m128i in = _mm_loadu_si128(static_cast<const __m128i*>(static_cast<const void*>(p))); // NOLINT(bugprone-casting-through-void)
|
||||
const __m128i special = _mm_or_si128(_mm_or_si128(_mm_cmpeq_epi8(in, _mm_set1_epi8('"')), _mm_cmpeq_epi8(in, _mm_set1_epi8('\\'))),
|
||||
_mm_or_si128(_mm_cmpeq_epi8(in, _mm_set1_epi8(0x7F)), _mm_cmplt_epi8(in, _mm_set1_epi8(0x20))));
|
||||
return static_cast<std::uint64_t>(static_cast<unsigned>(_mm_movemask_epi8(special)));
|
||||
#endif
|
||||
}
|
||||
|
||||
/// the width in bits of a byte in the mask of ascii_stop_mask()
|
||||
constexpr unsigned ascii_stop_bits() noexcept
|
||||
{
|
||||
return NLOHMANN_VIEW_NEON ? 4u : 1u;
|
||||
}
|
||||
#endif
|
||||
|
||||
/// As find_ascii_copyable_run(), 16 bytes per step. end is the end of the
|
||||
/// buffer that holds the n bytes at p (a short run is read as 16 bytes if
|
||||
/// they are in the buffer).
|
||||
inline std::size_t ascii_copyable_run(const unsigned char* p, std::size_t n, const unsigned char* end) noexcept
|
||||
{
|
||||
#if NLOHMANN_VIEW_VECTOR
|
||||
if (n >= 16)
|
||||
{
|
||||
std::size_t i = 0;
|
||||
for (; i + 16 <= n; i += 16)
|
||||
{
|
||||
const std::uint64_t mask = ascii_stop_mask(p + i);
|
||||
if (mask != 0)
|
||||
{
|
||||
return i + (static_cast<std::size_t>(count_trailing_zeros(mask)) / ascii_stop_bits());
|
||||
}
|
||||
}
|
||||
if (i == n)
|
||||
{
|
||||
return n;
|
||||
}
|
||||
// the last 16 bytes: those before i are known to be copyable
|
||||
const std::uint64_t mask = ascii_stop_mask(p + n - 16);
|
||||
return mask != 0 ? n - 16 + (static_cast<std::size_t>(count_trailing_zeros(mask)) / ascii_stop_bits()) : n;
|
||||
}
|
||||
if (end - p >= 16)
|
||||
{
|
||||
// (the bytes after the run are masked out)
|
||||
const std::uint64_t mask = ascii_stop_mask(p) & ((std::uint64_t{1} << (n * ascii_stop_bits())) - 1u);
|
||||
return mask != 0 ? static_cast<std::size_t>(count_trailing_zeros(mask)) / ascii_stop_bits() : n;
|
||||
}
|
||||
#else
|
||||
static_cast<void>(end);
|
||||
#endif
|
||||
return find_ascii_copyable_run(p, n);
|
||||
}
|
||||
|
||||
/// A stack that starts in a buffer of the caller (a local array) and moves to
|
||||
/// the heap (a vector of the caller) only when that is full, so that dumps of
|
||||
/// shallow documents need no allocation. The top is a pointer, as in
|
||||
@@ -6416,39 +6311,6 @@ NLOHMANN_VIEW_ALWAYS_INLINE char* put_literal(char* w, const char (&s)[N]) noexc
|
||||
return w + N - 1;
|
||||
}
|
||||
|
||||
/// the depth of the innermost open container, counted only for indented output
|
||||
template<bool Counted>
|
||||
struct depth_counter
|
||||
{
|
||||
std::size_t value = 0;
|
||||
void enter() noexcept
|
||||
{
|
||||
++value;
|
||||
}
|
||||
std::size_t leave() noexcept
|
||||
{
|
||||
return --value;
|
||||
}
|
||||
std::size_t get() const noexcept
|
||||
{
|
||||
return value;
|
||||
}
|
||||
};
|
||||
|
||||
template<>
|
||||
struct depth_counter<false>
|
||||
{
|
||||
void enter() noexcept {}
|
||||
std::size_t leave() noexcept
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
std::size_t get() const noexcept
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
};
|
||||
|
||||
/// how the view's dump() writes a value
|
||||
struct dump_style
|
||||
{
|
||||
@@ -6485,257 +6347,121 @@ class view_serializer
|
||||
|
||||
void dump(const node* root)
|
||||
{
|
||||
if (m_style.ensure_ascii)
|
||||
if (!m_style.pretty && !m_style.ensure_ascii)
|
||||
{
|
||||
dump_ascii(root);
|
||||
if (m_style.source_numbers)
|
||||
{
|
||||
dump_compact<true>(root);
|
||||
}
|
||||
else
|
||||
{
|
||||
dump_compact<false>(root);
|
||||
}
|
||||
return;
|
||||
}
|
||||
else if (m_style.pretty)
|
||||
struct frame
|
||||
{
|
||||
dump_pretty(root);
|
||||
}
|
||||
else if (m_style.source_numbers)
|
||||
const node* pos; ///< next element, or key of the next member
|
||||
const node* end;
|
||||
bool object;
|
||||
bool first; ///< nothing written yet
|
||||
};
|
||||
std::array<frame, 32> buffer; // NOLINT(cppcoreguidelines-pro-type-member-init,hicpp-member-init): written before read
|
||||
std::vector<frame> heap;
|
||||
small_stack<frame> stack(buffer.data(), buffer.size(), heap);
|
||||
const node* n = root;
|
||||
for (;;)
|
||||
{
|
||||
dump_text<true, false, false>(root);
|
||||
}
|
||||
else
|
||||
{
|
||||
dump_text<false, false, false>(root);
|
||||
// write the value at n
|
||||
if (is_container(*n))
|
||||
{
|
||||
const bool object = n->kind == static_cast<std::uint8_t>(value_t::object);
|
||||
if (n->len == 0)
|
||||
{
|
||||
m_out.put(object ? "{}" : "[]", 2);
|
||||
}
|
||||
else
|
||||
{
|
||||
m_out.put(object ? '{' : '[');
|
||||
stack.push_back(frame{nav::first(m_doc, n), nav::end(m_doc, n), object, true});
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
write_scalar(*n);
|
||||
}
|
||||
|
||||
// go to the next value: close finished containers, then separate
|
||||
for (;;)
|
||||
{
|
||||
if (stack.empty())
|
||||
{
|
||||
m_out.finish();
|
||||
return;
|
||||
}
|
||||
if (stack.back().pos == stack.back().end)
|
||||
{
|
||||
const bool object = stack.back().object;
|
||||
stack.pop_back();
|
||||
newline(stack.size());
|
||||
m_out.put(object ? '}' : ']');
|
||||
continue;
|
||||
}
|
||||
frame& f = stack.back();
|
||||
if (!f.first)
|
||||
{
|
||||
m_out.put(',');
|
||||
}
|
||||
f.first = false;
|
||||
newline(stack.size());
|
||||
if (f.object)
|
||||
{
|
||||
write_string(*f.pos);
|
||||
if (m_style.pretty)
|
||||
{
|
||||
m_out.put(": ");
|
||||
}
|
||||
else
|
||||
{
|
||||
m_out.put(':');
|
||||
}
|
||||
n = nav::value(f.pos + 1);
|
||||
f.pos = document_data::after(f.pos + 1);
|
||||
}
|
||||
else
|
||||
{
|
||||
n = nav::value(f.pos);
|
||||
f.pos = document_data::after(f.pos);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
/// a string that is not a plain string of the source (decoded, or written
|
||||
/// by an edit), without ensure_ascii: runs without characters to escape
|
||||
/// are copied. If validate is set, the string must be valid UTF-8: the
|
||||
/// runs are found by the lexer's scan, which validates the sequences, and
|
||||
/// invalid UTF-8 throws what basic_json::dump() throws. (The compact output
|
||||
/// writes the bytes of a damaged image unchanged, and scans without
|
||||
/// validating them.)
|
||||
NLOHMANN_VIEW_NOINLINE void write_decoded(const node& n, bool validate)
|
||||
/// are copied
|
||||
NLOHMANN_VIEW_NOINLINE void write_decoded(const node& n)
|
||||
{
|
||||
const auto* const s = reinterpret_cast<const unsigned char*>(m_doc.str(n)); // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast)
|
||||
m_out.put('"');
|
||||
for (std::size_t i = 0; i < n.len;)
|
||||
{
|
||||
const std::size_t run = validate ? string_bulk_run(s + i, n.len - i) : plain_output_run(s + i, n.len - i);
|
||||
const std::size_t run = plain_output_run(s + i, n.len - i);
|
||||
if (run != 0)
|
||||
{
|
||||
m_out.put(reinterpret_cast<const char*>(s + i), run); // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast)
|
||||
i += run;
|
||||
continue;
|
||||
}
|
||||
if (validate && s[i] >= 0x80)
|
||||
{
|
||||
invalid_utf8(s, n.len); // (the scan stopped at ill-formed UTF-8)
|
||||
}
|
||||
write_codepoint<false>(s[i], s + i, 1); // a quote, a backslash, or a control character
|
||||
++i;
|
||||
}
|
||||
m_out.put('"');
|
||||
}
|
||||
|
||||
/// dump_text() with indentation, out of line: the compact walk is faster
|
||||
/// when it is not merged with the indented ones into dump()
|
||||
NLOHMANN_VIEW_NOINLINE void dump_pretty(const node* root)
|
||||
{
|
||||
if (m_style.source_numbers)
|
||||
{
|
||||
dump_text<true, true, false>(root);
|
||||
}
|
||||
else
|
||||
{
|
||||
dump_text<false, true, false>(root);
|
||||
}
|
||||
}
|
||||
|
||||
/// dump_text() with ensure_ascii, out of line: the walk without it is
|
||||
/// faster when it is not merged with this one into dump()
|
||||
NLOHMANN_VIEW_NOINLINE void dump_ascii(const node* root)
|
||||
{
|
||||
if (m_style.pretty)
|
||||
{
|
||||
if (m_style.source_numbers)
|
||||
{
|
||||
dump_text<true, true, true>(root);
|
||||
}
|
||||
else
|
||||
{
|
||||
dump_text<false, true, true>(root);
|
||||
}
|
||||
}
|
||||
else if (m_style.source_numbers)
|
||||
{
|
||||
dump_text<true, false, true>(root);
|
||||
}
|
||||
else
|
||||
{
|
||||
dump_text<false, false, true>(root);
|
||||
}
|
||||
}
|
||||
|
||||
/// whether dump_text() copies a string or key from the source: one of
|
||||
/// the source, and with ensure_ascii one without bytes >= 0x7F (an overload
|
||||
/// each, so that the walk without ensure_ascii is compiled exactly as
|
||||
/// before: with GCC, a condition that only folds away later changed its
|
||||
/// block layout and made it 4-6% slower)
|
||||
static bool copied(const node& n, const char* /*src*/, std::false_type /*ensure_ascii*/) noexcept
|
||||
{
|
||||
return (n.flags & node_flags::storage) == 0;
|
||||
}
|
||||
|
||||
static bool copied(const node& n, const char* src, std::true_type /*ensure_ascii*/) noexcept
|
||||
{
|
||||
return (n.flags & node_flags::storage) == 0 && is_ascii_without_delete(src + n.off, n.len);
|
||||
}
|
||||
|
||||
/*!
|
||||
@brief a string with ensure_ascii: the characters that are not printable ASCII as \\uXXXX
|
||||
(a character beyond U+FFFF as a surrogate pair); strings of a document
|
||||
are valid UTF-8, except in a damaged image loaded with image_check::bounds,
|
||||
for which this throws what basic_json::dump() throws for the string
|
||||
*/
|
||||
NLOHMANN_VIEW_NOINLINE void write_ascii(const node& n)
|
||||
{
|
||||
const auto* const s = reinterpret_cast<const unsigned char*>(m_doc.str(n)); // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast)
|
||||
const std::size_t size = n.len;
|
||||
char* w = m_out.cursor();
|
||||
char* lim = m_out.limit();
|
||||
// room for k bytes
|
||||
const auto room = [&](std::size_t k)
|
||||
{
|
||||
if (NLOHMANN_VIEW_UNLIKELY(static_cast<std::size_t>(lim - w) < k))
|
||||
{
|
||||
m_out.set_cursor(w);
|
||||
m_out.reserve(k);
|
||||
w = m_out.cursor();
|
||||
lim = m_out.limit();
|
||||
}
|
||||
};
|
||||
static constexpr const char* hex = "0123456789abcdef";
|
||||
// \\uXXXX
|
||||
const auto u_escape = [&](std::uint32_t u)
|
||||
{
|
||||
w[0] = '\\';
|
||||
w[1] = 'u';
|
||||
w[2] = hex[(u >> 12u) & 0xFu];
|
||||
w[3] = hex[(u >> 8u) & 0xFu];
|
||||
w[4] = hex[(u >> 4u) & 0xFu];
|
||||
w[5] = hex[u & 0xFu];
|
||||
w += 6;
|
||||
};
|
||||
room(1);
|
||||
*w++ = '"';
|
||||
std::size_t i = 0;
|
||||
// (a string of the source starts with a run that has nothing to escape, 16 bytes per step)
|
||||
const std::size_t prefix = (n.flags & node_flags::storage) == 0 ? ascii_copyable_run(s, size, reinterpret_cast<const unsigned char*>(m_doc.src + m_doc.size)) : 0; // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast)
|
||||
if (prefix != 0)
|
||||
{
|
||||
room(prefix);
|
||||
std::memcpy(w, s, prefix);
|
||||
w += prefix;
|
||||
i = prefix;
|
||||
}
|
||||
while (i < size)
|
||||
{
|
||||
// (a character that needs an escape sequence is not worth a call)
|
||||
const std::size_t run = is_ascii_copyable(s[i]) ? find_ascii_copyable_run(s + i, size - i) : 0;
|
||||
if (run != 0)
|
||||
{
|
||||
room(run);
|
||||
std::memcpy(w, s + i, run);
|
||||
w += run;
|
||||
i += run;
|
||||
continue;
|
||||
}
|
||||
room(12);
|
||||
if (s[i] >= 0x80)
|
||||
{
|
||||
const unsigned char lead = s[i];
|
||||
// (the two-byte sequences of Latin text and the three-byte sequences of most other
|
||||
// text, with continuation bytes of any value: the ranges that are restricted (E0,
|
||||
// ED, and the sequences of four bytes) go through validate_one_utf8())
|
||||
if (lead >= 0xE1 && lead <= 0xEF && lead != 0xED && size - i >= 3 && (s[i + 1] & 0xC0u) == 0x80 && (s[i + 2] & 0xC0u) == 0x80)
|
||||
{
|
||||
u_escape(((lead & 0x0Fu) << 12u) | ((s[i + 1] & 0x3Fu) << 6u) | (s[i + 2] & 0x3Fu));
|
||||
i += 3;
|
||||
continue;
|
||||
}
|
||||
if (lead >= 0xC2 && lead <= 0xDF && size - i >= 2 && (s[i + 1] & 0xC0u) == 0x80)
|
||||
{
|
||||
u_escape(((lead & 0x1Fu) << 6u) | (s[i + 1] & 0x3Fu));
|
||||
i += 2;
|
||||
continue;
|
||||
}
|
||||
const std::size_t len = validate_one_utf8(s + i, size - i);
|
||||
if (NLOHMANN_VIEW_UNLIKELY(len == 0))
|
||||
{
|
||||
m_out.set_cursor(w);
|
||||
invalid_utf8(s, size);
|
||||
return; // LCOV_EXCL_LINE (invalid_utf8 throws)
|
||||
}
|
||||
std::uint32_t codepoint = s[i] & (0xFFu >> (len + 1));
|
||||
for (std::size_t k = 1; k < len; ++k)
|
||||
{
|
||||
codepoint = (codepoint << 6u) | (s[i + k] & 0x3Fu);
|
||||
}
|
||||
i += len;
|
||||
if (codepoint <= 0xFFFF)
|
||||
{
|
||||
u_escape(codepoint);
|
||||
}
|
||||
else
|
||||
{
|
||||
u_escape(0xD7C0u + (codepoint >> 10u));
|
||||
u_escape(0xDC00u + (codepoint & 0x3FFu));
|
||||
}
|
||||
continue;
|
||||
}
|
||||
// a quote, a backslash, or a control character (or DEL)
|
||||
const unsigned char c = s[i++];
|
||||
char escape = 0;
|
||||
switch (c)
|
||||
{
|
||||
case 0x08:
|
||||
escape = 'b';
|
||||
break;
|
||||
case 0x09:
|
||||
escape = 't';
|
||||
break;
|
||||
case 0x0A:
|
||||
escape = 'n';
|
||||
break;
|
||||
case 0x0C:
|
||||
escape = 'f';
|
||||
break;
|
||||
case 0x0D:
|
||||
escape = 'r';
|
||||
break;
|
||||
case 0x22:
|
||||
escape = '"';
|
||||
break;
|
||||
case 0x5C:
|
||||
escape = '\\';
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
if (escape != 0)
|
||||
{
|
||||
w[0] = '\\';
|
||||
w[1] = escape;
|
||||
w += 2;
|
||||
}
|
||||
else
|
||||
{
|
||||
u_escape(c);
|
||||
}
|
||||
}
|
||||
room(1);
|
||||
*w++ = '"';
|
||||
m_out.set_cursor(w);
|
||||
}
|
||||
|
||||
/// the copies of dump_text() that are not fixed-size moves (long
|
||||
/// the copies of dump_compact() that are not fixed-size moves (long
|
||||
/// strings, or near the end of the source); out of line, so that the
|
||||
/// compiler does not merge the fixed-size moves into this call
|
||||
NLOHMANN_VIEW_NOINLINE static void copy_long(char* to, const char* from, std::size_t n) noexcept
|
||||
@@ -6752,19 +6478,17 @@ class view_serializer
|
||||
}
|
||||
|
||||
/*!
|
||||
@brief the output, compact (the default dump()) or indented, with or without ensure_ascii
|
||||
@brief the compact output without ensure_ascii (the default dump())
|
||||
|
||||
The same walk as dump(), with the write position in a local variable
|
||||
(stores through char pointers would otherwise force a reload of the
|
||||
buffer's members after each one), and with strings and number tokens of
|
||||
the source copied by fixed-size moves of 32 bytes where the source has
|
||||
that many bytes left, instead of a library call per token. The buffer
|
||||
keeps 64 bytes of slack for the overshoot. With ensure_ascii (Ascii), a
|
||||
string of the source is copied only if it has nothing to escape (see
|
||||
copied()); any other string is written by write_ascii().
|
||||
keeps 64 bytes of slack for the overshoot.
|
||||
*/
|
||||
template<bool SourceNumbers, bool Pretty, bool Ascii>
|
||||
void dump_text(const node* root)
|
||||
template<bool SourceNumbers>
|
||||
void dump_compact(const node* root)
|
||||
{
|
||||
struct frame
|
||||
{
|
||||
@@ -6779,16 +6503,6 @@ class view_serializer
|
||||
const char* const src_end = src + m_doc.size;
|
||||
char* w = m_out.cursor();
|
||||
char* lim = m_out.limit();
|
||||
// (indented output) a line break and 63 indentation characters, from
|
||||
// which the line break of a level of up to 63 characters is a copy
|
||||
std::array<char, 64> ruler; // NOLINT(cppcoreguidelines-pro-type-member-init,hicpp-member-init): filled below if used
|
||||
if (Pretty)
|
||||
{
|
||||
ruler.fill(m_style.indent_char);
|
||||
ruler[0] = '\n';
|
||||
}
|
||||
const std::size_t indent = m_style.indent;
|
||||
depth_counter<Pretty> level; // depth of the innermost open container (indented output)
|
||||
// room for n bytes and the slack
|
||||
const auto room = [&](std::size_t n)
|
||||
{
|
||||
@@ -6820,35 +6534,12 @@ class view_serializer
|
||||
}
|
||||
w += n;
|
||||
};
|
||||
// (indented output) the line break and the indentation of a level
|
||||
const auto newline = [&](std::size_t depth)
|
||||
{
|
||||
const std::size_t count = 1 + (depth * indent);
|
||||
room(count);
|
||||
if (count <= ruler.size())
|
||||
{
|
||||
std::memcpy(w, ruler.data(), ruler.size());
|
||||
}
|
||||
else
|
||||
{
|
||||
*w = '\n';
|
||||
std::memset(w + 1, m_style.indent_char, count - 1);
|
||||
}
|
||||
w += count;
|
||||
};
|
||||
// a string that is not a plain string of the source (out of line, so
|
||||
// that the cursor stays in a register here)
|
||||
const auto escaped = [&](const node & n)
|
||||
{
|
||||
m_out.set_cursor(w);
|
||||
if (enabled(Ascii))
|
||||
{
|
||||
write_ascii(n);
|
||||
}
|
||||
else
|
||||
{
|
||||
write_decoded(n, Pretty);
|
||||
}
|
||||
write_decoded(n);
|
||||
w = m_out.cursor();
|
||||
lim = m_out.limit();
|
||||
};
|
||||
@@ -6871,7 +6562,7 @@ class view_serializer
|
||||
switch (static_cast<value_t>(n->kind))
|
||||
{
|
||||
case value_t::string:
|
||||
if (copied(*n, src, std::integral_constant<bool, Ascii>()))
|
||||
if ((n->flags & node_flags::storage) == 0)
|
||||
{
|
||||
room(n->len + 2);
|
||||
*w++ = '"';
|
||||
@@ -6948,7 +6639,6 @@ class view_serializer
|
||||
{
|
||||
*w++ = object ? '{' : '[';
|
||||
stack.push_back(cur);
|
||||
level.enter();
|
||||
if (Editable)
|
||||
{
|
||||
cur = frame{nav::first(m_doc, n), nav::end(m_doc, n), object};
|
||||
@@ -6991,10 +6681,6 @@ class view_serializer
|
||||
{
|
||||
break;
|
||||
}
|
||||
if (Pretty)
|
||||
{
|
||||
newline(level.leave());
|
||||
}
|
||||
room(1);
|
||||
*w++ = cur.object ? '}' : ']';
|
||||
cur = stack.back();
|
||||
@@ -7003,17 +6689,13 @@ class view_serializer
|
||||
room(1);
|
||||
*w++ = ',';
|
||||
}
|
||||
if (Pretty)
|
||||
{
|
||||
newline(level.get());
|
||||
}
|
||||
const node* const at = Editable ? cur.pos : n;
|
||||
if (cur.object)
|
||||
{
|
||||
const node& key = *at;
|
||||
if (copied(key, src, std::integral_constant<bool, Ascii>()))
|
||||
if ((key.flags & node_flags::storage) == 0)
|
||||
{
|
||||
room(key.len + (Pretty ? 4 : 3));
|
||||
room(key.len + 3);
|
||||
*w++ = '"';
|
||||
copy(src + key.off, key.len);
|
||||
w[0] = '"';
|
||||
@@ -7023,13 +6705,9 @@ class view_serializer
|
||||
else
|
||||
{
|
||||
escaped(key);
|
||||
room(Pretty ? 2 : 1);
|
||||
room(1);
|
||||
*w++ = ':';
|
||||
}
|
||||
if (Pretty)
|
||||
{
|
||||
*w++ = ' ';
|
||||
}
|
||||
if (Editable)
|
||||
{
|
||||
n = nav::value(at + 1);
|
||||
@@ -7048,6 +6726,69 @@ class view_serializer
|
||||
}
|
||||
}
|
||||
|
||||
void newline(std::size_t level)
|
||||
{
|
||||
if (m_style.pretty)
|
||||
{
|
||||
m_out.put('\n');
|
||||
m_out.put_repeated(m_style.indent_char, level * m_style.indent);
|
||||
}
|
||||
}
|
||||
|
||||
void write_scalar(const node& n)
|
||||
{
|
||||
switch (static_cast<value_t>(n.kind))
|
||||
{
|
||||
case value_t::null:
|
||||
m_out.put("null");
|
||||
break;
|
||||
case value_t::boolean:
|
||||
if ((n.flags & node_flags::is_true) != 0)
|
||||
{
|
||||
m_out.put("true");
|
||||
}
|
||||
else
|
||||
{
|
||||
m_out.put("false");
|
||||
}
|
||||
break;
|
||||
case value_t::string:
|
||||
write_string(n);
|
||||
break;
|
||||
case value_t::number_integer:
|
||||
case value_t::number_unsigned:
|
||||
{
|
||||
const char* const token = m_doc.str(n);
|
||||
const std::uint32_t len = number_length(n);
|
||||
if (!m_style.source_numbers && len == 2 && token[0] == '-' && token[1] == '0')
|
||||
{
|
||||
m_out.put('0'); // parse() reads -0 as the integer 0
|
||||
}
|
||||
else
|
||||
{
|
||||
m_out.put(token, len);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case value_t::number_float:
|
||||
if (m_style.source_numbers && (n.flags & node_flags::storage) != node_flags::edited)
|
||||
{
|
||||
m_out.put(m_doc.str(n), n.len); // (a float set by an edit is written as with shortest)
|
||||
}
|
||||
else
|
||||
{
|
||||
write_float_node(n);
|
||||
}
|
||||
break;
|
||||
case value_t::object: // LCOV_EXCL_LINE (containers are written by dump())
|
||||
case value_t::array: // LCOV_EXCL_LINE
|
||||
case value_t::binary: // LCOV_EXCL_LINE (not in a document)
|
||||
case value_t::discarded: // LCOV_EXCL_LINE
|
||||
default: // LCOV_EXCL_LINE
|
||||
break; // LCOV_EXCL_LINE
|
||||
}
|
||||
}
|
||||
|
||||
/// a float node as dump() writes it
|
||||
void write_float_node(const node& n)
|
||||
{
|
||||
@@ -7114,7 +6855,8 @@ class view_serializer
|
||||
static char* write_double_value_at(char* w, double x)
|
||||
{
|
||||
// (from the bits: without the checks of to_chars())
|
||||
auto bits = bit_cast<std::uint64_t>(x);
|
||||
std::uint64_t bits = 0;
|
||||
std::memcpy(&bits, &x, sizeof(bits));
|
||||
if (NLOHMANN_VIEW_UNLIKELY((bits & 0x7FF0000000000000u) == 0x7FF0000000000000u))
|
||||
{
|
||||
return write_text_at(w, "null", 4);
|
||||
@@ -7158,6 +6900,71 @@ class view_serializer
|
||||
m_out.put(s.data(), s.size());
|
||||
}
|
||||
|
||||
void write_string(const node& n)
|
||||
{
|
||||
const char* const s = m_doc.str(n);
|
||||
m_out.put('"');
|
||||
if ((n.flags & node_flags::storage) == 0 && !m_style.ensure_ascii)
|
||||
{
|
||||
// a string of the source without escape sequences has nothing to escape
|
||||
m_out.put(s, n.len);
|
||||
}
|
||||
else if (m_style.ensure_ascii)
|
||||
{
|
||||
write_escaped<true>(reinterpret_cast<const unsigned char*>(s), n.len); // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast)
|
||||
}
|
||||
else
|
||||
{
|
||||
write_escaped<false>(reinterpret_cast<const unsigned char*>(s), n.len); // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast)
|
||||
}
|
||||
m_out.put('"');
|
||||
}
|
||||
|
||||
/// as serializer::dump_escaped(); strings of a document are valid UTF-8,
|
||||
/// except in a damaged image loaded with image_check::bounds, for which
|
||||
/// this throws what basic_json::dump() throws for the string
|
||||
template<bool EnsureAscii>
|
||||
void write_escaped(const unsigned char* s, std::size_t n)
|
||||
{
|
||||
std::size_t i = 0;
|
||||
while (i < n)
|
||||
{
|
||||
std::size_t run = 0;
|
||||
if (!EnsureAscii)
|
||||
{
|
||||
run = string_bulk_run(s + i, n - i);
|
||||
}
|
||||
else if (is_ascii_copyable(s[i]))
|
||||
{
|
||||
run = find_ascii_copyable_run(s + i, n - i);
|
||||
}
|
||||
if (run != 0)
|
||||
{
|
||||
m_out.put(reinterpret_cast<const char*>(s + i), run); // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast)
|
||||
i += run;
|
||||
continue;
|
||||
}
|
||||
std::uint32_t codepoint = s[i];
|
||||
std::size_t len = 1;
|
||||
if (codepoint >= 0x80)
|
||||
{
|
||||
len = validate_one_utf8(s + i, n - i);
|
||||
if (NLOHMANN_VIEW_UNLIKELY(len == 0))
|
||||
{
|
||||
invalid_utf8(s, n);
|
||||
return;
|
||||
}
|
||||
codepoint &= 0xFFu >> (len + 1);
|
||||
for (std::size_t k = 1; k < len; ++k)
|
||||
{
|
||||
codepoint = (codepoint << 6u) | (s[i + k] & 0x3Fu);
|
||||
}
|
||||
}
|
||||
write_codepoint<EnsureAscii>(codepoint, s + i, len);
|
||||
i += len;
|
||||
}
|
||||
}
|
||||
|
||||
/// throw what basic_json::dump() throws for a string that is not valid UTF-8
|
||||
NLOHMANN_VIEW_NOINLINE static void invalid_utf8(const unsigned char* s, std::size_t n)
|
||||
{
|
||||
|
||||
+36
-22
@@ -2,12 +2,14 @@ cmake_minimum_required(VERSION 3.13...4.0)
|
||||
|
||||
option(JSON_Valgrind "Execute test suite with Valgrind." OFF)
|
||||
option(JSON_FastTests "Skip expensive/slow tests." OFF)
|
||||
option(JSON_TestUnityBuild "Build the unit tests in batches of several files per executable to speed up compilation." ON)
|
||||
option(JSON_TestSimdutf "Build the unit tests against the simdutf UTF-8 validation backend." OFF)
|
||||
|
||||
set(JSON_SIMDUTF_VERSION 9.1.0 CACHE STRING "The simdutf version used by JSON_TestSimdutf.")
|
||||
|
||||
set(JSON_32bitTest AUTO CACHE STRING "Enable the 32bit unit test (ON/OFF/AUTO/ONLY).")
|
||||
set(JSON_TestStandards "" CACHE STRING "The list of standards to test explicitly.")
|
||||
set(JSON_TestUnityBatchSize 8 CACHE STRING "The number of unit test files compiled into one executable with JSON_TestUnityBuild.")
|
||||
set(JSON_TestShard "" CACHE STRING "Build only a part of the unit tests, given as <index>/<count> (e.g. 0/2), to split them across CI jobs with a time limit.")
|
||||
|
||||
# using an env var, since this will also affect targets executing cmake (such as "ci_test_compiler_default")
|
||||
@@ -20,6 +22,11 @@ endif()
|
||||
|
||||
include(test)
|
||||
|
||||
# MinGW's GNU ld hits section and relocation limits with the large merged translation units
|
||||
if(MINGW)
|
||||
set(JSON_TestUnityBuild OFF)
|
||||
endif()
|
||||
|
||||
#############################################################################
|
||||
# override standard support
|
||||
#############################################################################
|
||||
@@ -326,9 +333,28 @@ if(NOT "${JSON_TestShard}" STREQUAL "")
|
||||
message(STATUS "Test shard ${JSON_TestShard}: ${shard_file_count} of ${test_file_count} unit test files")
|
||||
endif()
|
||||
|
||||
foreach(file ${files})
|
||||
json_test_add_test_for(${file} MAIN test_main CXX_STANDARDS ${test_cxx_standards} ${test_force})
|
||||
endforeach()
|
||||
# Explicit groups of test files (file names without "unit-" and ".cpp") for
|
||||
# JSON_TestUnityBuild: all files of a group are compiled into one translation
|
||||
# unit per C++ standard, so that related tests share their template
|
||||
# instantiations (all binary formats instantiate the same readers and
|
||||
# writers). Only add a group where this was measured to pay off: grouping
|
||||
# files that share little just creates one slow translation unit. Files in no
|
||||
# group are batched alphabetically by JSON_TestUnityBatchSize.
|
||||
set(json_test_unity_groups binary)
|
||||
set(json_test_unity_group_binary binary_formats binary_utf8_error_handler binary_writer_sinks bjdata bon8 bon8-cpp17 bson byte_container_with_subtype cbor msgpack msgpack-cpp17 ubjson)
|
||||
|
||||
if(JSON_TestUnityBuild)
|
||||
message(STATUS "Building the unit tests in batches of ${JSON_TestUnityBatchSize} files (JSON_TestUnityBuild)")
|
||||
json_test_add_unity_tests(FILES ${files} MAIN test_main CXX_STANDARDS ${test_cxx_standards} ${test_force}
|
||||
BATCH_SIZE ${JSON_TestUnityBatchSize}
|
||||
GROUPS ${json_test_unity_groups}
|
||||
VARIANT_FILES src/unit-comparison.cpp src/unit-comparison-cpp20.cpp src/unit-class_parser.cpp src/unit-diagnostic-positions.cpp
|
||||
)
|
||||
else()
|
||||
foreach(file ${files})
|
||||
json_test_add_test_for(${file} MAIN test_main CXX_STANDARDS ${test_cxx_standards} ${test_force})
|
||||
endforeach()
|
||||
endif()
|
||||
|
||||
# tests/src/unit-no-macro-leak.cpp #include-s the generated leak-check file,
|
||||
# so its test targets must be built after generate_hedley_undef_checks.
|
||||
@@ -354,6 +380,13 @@ json_test_add_test_for(src/unit-comparison.cpp
|
||||
NAME test-comparison_legacy
|
||||
MAIN test_main CXX_STANDARDS ${test_cxx_standards} ${test_force}
|
||||
)
|
||||
json_test_set_test_options(test-comparison_legacy-cpp20
|
||||
COMPILE_DEFINITIONS JSON_USE_LEGACY_DISCARDED_VALUE_COMPARISON=1
|
||||
)
|
||||
json_test_add_test_for(src/unit-comparison-cpp20.cpp
|
||||
NAME test-comparison_legacy-cpp20
|
||||
MAIN test_main CXX_STANDARDS ${test_cxx_standards} ${test_force}
|
||||
)
|
||||
|
||||
# test the parser again with JSON_DIAGNOSTIC_POSITIONS enabled
|
||||
json_test_set_test_options(test-class_parser_diagnostic_positions
|
||||
@@ -373,25 +406,6 @@ json_test_add_test_for(src/unit-diagnostic-positions.cpp
|
||||
MAIN test_main CXX_STANDARDS ${test_cxx_standards} ${test_force}
|
||||
)
|
||||
|
||||
# the lexer again with the portable string scanning instead of SSE2/NEON, and on
|
||||
# x86-64 with SSSE3 compiled in (no check of the CPU)
|
||||
json_test_set_test_options(test-class_lexer_no_simd
|
||||
COMPILE_DEFINITIONS JSON_NO_SIMD
|
||||
)
|
||||
json_test_add_test_for(src/unit-class_lexer.cpp
|
||||
NAME test-class_lexer_no_simd
|
||||
MAIN test_main CXX_STANDARDS ${test_cxx_standards} ${test_force}
|
||||
)
|
||||
if(CMAKE_SYSTEM_PROCESSOR MATCHES "^(x86_64|AMD64|amd64)$" AND NOT MSVC)
|
||||
json_test_set_test_options(test-class_lexer_ssse3
|
||||
COMPILE_OPTIONS -mssse3
|
||||
)
|
||||
json_test_add_test_for(src/unit-class_lexer.cpp
|
||||
NAME test-class_lexer_ssse3
|
||||
MAIN test_main CXX_STANDARDS ${test_cxx_standards} ${test_force}
|
||||
)
|
||||
endif()
|
||||
|
||||
# test reading from a std::istream again without scanning the get area in bulk
|
||||
json_test_set_test_options(test-stream-bulk-scan_disabled
|
||||
COMPILE_DEFINITIONS JSON_DISABLE_STREAM_BULK_SCAN=1
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
# Unit tests
|
||||
|
||||
The unit tests are in [`src/unit-*.cpp`](src) and use [doctest](https://github.com/doctest/doctest). Each file becomes
|
||||
one CTest test per C++ standard it is built for, named after the file: `src/unit-foo.cpp` becomes `test-foo_cpp11`.
|
||||
|
||||
## Build and run
|
||||
|
||||
```sh
|
||||
cmake -S . -B build -DJSON_BuildTests=ON
|
||||
cmake --build build -j 10
|
||||
ctest --test-dir build -j 10
|
||||
```
|
||||
|
||||
The [CMake options](../docs/mkdocs/docs/integration/cmake.md) starting with `JSON_Test` (and `JSON_FastTests`,
|
||||
`JSON_Valgrind`) control the test build. The most relevant ones:
|
||||
|
||||
- `JSON_TestStandards`: build every test file for the given standards, e.g., `-DJSON_TestStandards=17`. By default,
|
||||
a file is built for C++11 and for each standard whose `JSON_HAS_CPP_<N>` macro it mentions (see below).
|
||||
- `JSON_TestUnityBuild`: compile several test files together (see below). Set it to `OFF` to get one executable per
|
||||
test file, e.g., to debug a single test in a debugger.
|
||||
- `JSON_TestShard`: build only a part of the test files, e.g., `-DJSON_TestShard=0/2`.
|
||||
|
||||
To run the tests of one file only, run its CTest test, e.g., `ctest --test-dir build -R test-foo_cpp11`.
|
||||
|
||||
## How test files are built
|
||||
|
||||
Two mechanisms keep the compile time down. Both are automatic, but they set a few rules for test files.
|
||||
|
||||
### C++ standards
|
||||
|
||||
A test file is built for a C++ standard beyond C++11 if its text contains `JSON_HAS_CPP_<N>`, for instance in
|
||||
`#ifdef JSON_HAS_CPP_17`, or in a comment `// JSON_HAS_CPP_17` next to code that needs a macro only defined for that
|
||||
standard, such as `JSON_HAS_FILESYSTEM`. Then the *whole file* is compiled again for that standard.
|
||||
|
||||
So that this does not happen for the large files, tests that need C++14, C++17, or C++20 go into a separate file
|
||||
`src/unit-<name>-cpp<N>.cpp` (e.g., [`src/unit-regression3-cpp17.cpp`](src/unit-regression3-cpp17.cpp)), whose body
|
||||
is wrapped in `#ifdef JSON_HAS_CPP_<N>`. The main file `src/unit-<name>.cpp` must not mention `JSON_HAS_CPP_<N>` at
|
||||
all, not even in a comment, so it is built for C++11 only. The CI jobs `ci_test_*_cxx<N>` still build every test file
|
||||
for every standard.
|
||||
|
||||
### Unity build
|
||||
|
||||
With `JSON_TestUnityBuild` (`ON` by default, `OFF` with MinGW), CMake compiles several test files as one translation
|
||||
unit, so they share the template instantiations of the library. CMake generates `build/tests/unity/test-unity-*.cpp`,
|
||||
which `#include` the test files of a batch. Every test file still gets its own CTest test, which runs the batch
|
||||
executable with `--source-file=*unit-foo.cpp`, so only the test cases of that file run.
|
||||
|
||||
CMake decides from the macros a file defines *before* including `<nlohmann/json.hpp>`:
|
||||
|
||||
| Macros defined before the include | Built as |
|
||||
|-------------------------------------------------------------------------------------------|-----------------------------------|
|
||||
| none (or only `SKIP_TESTS_FOR_*` and similar macros derived from global definitions) | batch with the other plain files |
|
||||
| only `JSON_TESTS_PRIVATE` | batch with the other such files |
|
||||
| any library configuration macro (`JSON_DIAGNOSTICS`, `JSON_NO_IO`, `JSON_ASSERT`, ...) | its own executable |
|
||||
|
||||
Files with test-specific build options (`json_test_set_test_options(test-foo ...)` in
|
||||
[`CMakeLists.txt`](CMakeLists.txt)) also get their own executable. The batchable files are split alphabetically into
|
||||
batches of `JSON_TestUnityBatchSize` files, except for explicit groups of related files: `json_test_unity_group_binary`
|
||||
compiles all binary-format tests together, because they share the binary readers and writers. Only add a group if it
|
||||
measurably pays off; files that share little just make one slow translation unit.
|
||||
|
||||
## Rules for test files
|
||||
|
||||
Because the files of a batch share one translation unit, a test file must not affect the files that follow it:
|
||||
|
||||
1. **Give file-scope helpers file-specific names.** An anonymous namespace does not help, as the batch is one
|
||||
translation unit. Use a name such as `my_allocator_2982`, or wrap the helpers in a namespace named after the file
|
||||
(e.g., `namespace unit_comparison_detail`). Clashes show up as compile errors.
|
||||
2. **Do not `#undef` library macros**, such as `JSON_HAS_CPP_17`, at the end of a file: this silently disables the tests
|
||||
that depend on them in the later files of the batch. `#undef` macros you define yourself after including the library.
|
||||
3. **Put version-dependent tests into `unit-<name>-cpp<N>.cpp`** (see [C++ standards](#c-standards)).
|
||||
4. **Define test cases in the `.cpp` file itself.** The batch executable selects the tests of a file by its name, so a
|
||||
`TEST_CASE` in a shared header is not run. The only exception is the test case in
|
||||
[`src/make_test_data_available.hpp`](src/make_test_data_available.hpp), which CMake handles explicitly.
|
||||
|
||||
To check a new file for clashes with all others at once, build with `-DJSON_TestUnityBatchSize=1000`, which puts all
|
||||
batchable files of a kind into one translation unit.
|
||||
|
||||
## Where to add tests
|
||||
|
||||
Tests are structured along the features of the library. Usually, an existing file is the right place:
|
||||
|
||||
- For a bug fix, add a section referencing the issue to [`src/unit-regression3.cpp`](src/unit-regression3.cpp), or to
|
||||
[`src/unit-regression3-cpp17.cpp`](src/unit-regression3-cpp17.cpp) /
|
||||
[`src/unit-regression3-cpp20.cpp`](src/unit-regression3-cpp20.cpp) if the test needs C++17 / C++20.
|
||||
[`src/unit-regression2.cpp`](src/unit-regression2.cpp) holds older tests and should not grow further.
|
||||
- When testing exceptions, use `CHECK_THROWS_WITH_AS`, which also checks the `what()` message.
|
||||
|
||||
A new file `src/unit-<name>.cpp` is picked up automatically when CMake runs again; no change to `CMakeLists.txt` is
|
||||
needed.
|
||||
|
||||
See also [`fuzzing.md`](fuzzing.md) for fuzz testing.
|
||||
@@ -24,7 +24,6 @@ foreach(test default noversion)
|
||||
JSON_USE_LEGACY_DISCARDED_VALUE_COMPARISON=1
|
||||
JSON_DISABLE_STREAM_BULK_SCAN=1
|
||||
JSON_DISABLE_UNLOCKED_FILE_IO=1
|
||||
JSON_NO_SIMD
|
||||
JSON_VIEW_NO_SIMD
|
||||
JSON_VIEW_USE_SSSE3)
|
||||
target_link_libraries(abi_config_${test}_all_tags PRIVATE abi_compat_main)
|
||||
|
||||
@@ -60,10 +60,6 @@ TEST_CASE("default namespace")
|
||||
expected += "_dufio";
|
||||
#endif
|
||||
|
||||
#ifdef JSON_NO_SIMD
|
||||
expected += "_ns";
|
||||
#endif
|
||||
|
||||
#ifdef JSON_VIEW_NO_SIMD
|
||||
expected += "_vns";
|
||||
#endif
|
||||
|
||||
@@ -61,10 +61,6 @@ TEST_CASE("default namespace without version component")
|
||||
expected += "_dufio";
|
||||
#endif
|
||||
|
||||
#ifdef JSON_NO_SIMD
|
||||
expected += "_ns";
|
||||
#endif
|
||||
|
||||
#ifdef JSON_VIEW_NO_SIMD
|
||||
expected += "_vns";
|
||||
#endif
|
||||
|
||||
@@ -22,15 +22,6 @@ Micro-benchmarks for parsing, serialization and the binary formats, written with
|
||||
| `ViewAccept` | validate with `json_document::accept`; compare with `Accept` |
|
||||
| `ViewMaterialize` | convert a parsed `json_document` into a `json` value |
|
||||
| `ViewDump` | serialize a parsed `json_document`; compare with `Dump` |
|
||||
| `SaxParse` | parse with a SAX handler that only counts events, so no values are built |
|
||||
| `Copy`, `CopyAssign`, `CopyRing` | copy a parsed document and destroy a copy, both timed: a temporary copy, an assignment over an earlier copy, and replacing the oldest of eight live copies |
|
||||
| `CopyOnly`, `DestroyOnly` | only the copy or only the destruction, the other half untimed; a fast destructor can leave work to the next allocations, so these tell the halves apart but do not compare versions |
|
||||
| `Equal`, `Iterate` | compare a parsed document with a copy, or visit every value with range-based `for` loops |
|
||||
| `Flatten`, `FlattenOrdered`, `Unflatten` | `flatten()` for `json` and `ordered_json`, and `unflatten()` |
|
||||
| `Diff`, `Patch` | `diff()` against the document with every eighth scalar changed, and applying that patch |
|
||||
| `JsonPointerParse`, `JsonPointerToString`, `JsonPointerAt`, `JsonPointerContains` | the JSON pointers of every scalar in a document: parse them from strings, turn them back into strings, look them up with `at()` and `contains()` |
|
||||
| `GetVectorInt`, `GetVectorDouble`, `GetMap` | `get<std::vector<std::int64_t>>()`, `get<std::vector<double>>()` and `get<std::map<std::string, json>>()` on large arrays and a large flat object |
|
||||
| `ToJsonStrings` | create a `json` array from a `std::vector` of 100,000 strings, copied or moved, and destroy it again |
|
||||
|
||||
The input files are those of [nativejson-benchmark](https://github.com/miloyip/nativejson-benchmark) (`canada`,
|
||||
`citm_catalog`, `twitter`), a large `jeopardy` file, and number-heavy files (`floats`, `signed_ints`, ...).
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
build/
|
||||
@@ -1,90 +0,0 @@
|
||||
# json_view compared with other libraries
|
||||
|
||||
The in-tree benchmarks in [`tests/benchmarks`](../README.md) measure `json_document` against `json::parse` only. The
|
||||
programs here compare it with [yyjson](https://github.com/ibireme/yyjson),
|
||||
[simdjson](https://github.com/simdjson/simdjson), and [Boost.JSON](https://github.com/boostorg/json): the question
|
||||
users ask when they pick a library. They are not built by CMake or run by CI.
|
||||
|
||||
## Reproducing the numbers
|
||||
|
||||
`compare.py` builds the programs against `include/` of this checkout, runs them, and writes the results together with
|
||||
everything needed to reproduce them to `results/<date>-<host>.md` (and `.csv`): the date, the commit, the CPU, the
|
||||
OS, the compiler, the flags, and the versions of all libraries.
|
||||
|
||||
```sh
|
||||
python3 tests/benchmarks/json_view/compare.py --data <json_test_data directory> [--native] [--rounds 30]
|
||||
```
|
||||
|
||||
- `--data` is the downloaded [test data](https://github.com/nlohmann/json_test_data), e.g. the `test_files` directory
|
||||
of a CMake build directory. It needs `nativejson-benchmark/{twitter,citm_catalog,canada}.json` and
|
||||
`jeopardy/jeopardy.json`.
|
||||
- The other libraries come from the system: pkg-config, or Homebrew (`brew install yyjson simdjson boost`). With
|
||||
`--download`, pinned releases are downloaded instead and checked against their SHA-256. Without Boost headers (or
|
||||
with `--no-boost`), the Boost.JSON columns are skipped, and the results say so.
|
||||
- `--corpus file...` adds files to the corpus benchmark, e.g. those of
|
||||
[simdjson-data](https://github.com/simdjson/simdjson-data) or the
|
||||
[yyjson benchmark](https://github.com/ibireme/yyjson_benchmark).
|
||||
- Only the Python 3 standard library is used; a C++17 compiler is needed (`CXX` and `CC` are honored).
|
||||
|
||||
For numbers worth publishing, use a quiet machine (see [Getting stable numbers](../README.md#getting-stable-numbers)),
|
||||
the default 30 rounds or more, and `--native` only if the other libraries were built for the same CPU.
|
||||
|
||||
### On GitHub-hosted runners
|
||||
|
||||
The workflow [json_view benchmarks](../../../.github/workflows/json_view_benchmarks.yml) runs `compare.py --download`
|
||||
on demand: by hand (Actions → "json_view benchmarks" → "Run workflow"), on an x86-64 or AArch64 Ubuntu runner with GCC
|
||||
or Clang, or when a pull request gets the label `benchmark`, on both architectures with GCC. The results appear as the
|
||||
job summary and as an artifact. Shared runners are noisy, so these numbers show
|
||||
where `json_view` stands on another architecture; they are not meant for publication.
|
||||
|
||||
## What is measured
|
||||
|
||||
`bench_view.cpp` runs four workloads on twitter, citm_catalog, canada, jeopardy, a single tweet (`status`), and a
|
||||
JSON-RPC request (`rpc`):
|
||||
|
||||
| workload | what it does |
|
||||
|---|---|
|
||||
| parse | build and free a document |
|
||||
| traverse | parse, then visit every value, convert every number, touch every string and key |
|
||||
| select | parse, then read a few fields per record (e.g. id, user name, and retweet count of each tweet) |
|
||||
| dump | serialize a parsed document (compact) |
|
||||
|
||||
`bench_corpus.cpp` runs parse, traverse, and dump on any list of files, so that no library is tuned to a handful of
|
||||
documents. Its dump also writes the numbers as they are in the input: `json_view` with `number_format::source`, and
|
||||
yyjson with numbers read as raw text (`YYJSON_READ_NUMBER_AS_RAW`), without converting them.
|
||||
|
||||
`bench_edit.cpp` measures read-modify-write: parse, apply the same logical edits with each library's own API, and
|
||||
serialize (compact). Workloads: `patch` (a handful of edits at fixed places) and `update` (edits in every record).
|
||||
An editable `json_document` edits in place; yyjson copies its immutable document into a mutable one first
|
||||
(`yyjson_doc_mut_copy`); Boost.JSON and `json::parse` build mutable DOMs; simdjson cannot edit a document. All
|
||||
outputs are checked to describe the same value.
|
||||
|
||||
Before anything is timed, all engines must accept each document and agree on the traversal: the number of values, the
|
||||
bytes of all strings and keys, and the sum of all numbers. All engines run interleaved in every round, and the best
|
||||
round is reported, as time and as a factor of the `json_view` time (below 1 means faster than `json_view`). Each timed
|
||||
call follows an untimed call of the same engine: otherwise the engine after `json::parse` pays for the allocator
|
||||
cleaning up the tens of thousands of nodes `json::parse` just freed (with glibc, this made `json_view` look 1.7 times
|
||||
slower on citm_catalog traverse).
|
||||
|
||||
The engines do not all offer the same features, which the numbers should be read with:
|
||||
|
||||
| engine | document | random access | editable | notes |
|
||||
|---|---|---|---|---|
|
||||
| `json_view` | immutable index into the text | yes | no | a fresh document per parse; "reused" parses into the same document |
|
||||
| yyjson | immutable (`yyjson_read`) | yes | via a mutable copy | |
|
||||
| simdjson DOM | immutable, parser reused | yes | no | "fresh" uses a new parser per parse |
|
||||
| simdjson On-Demand | none: forward-only, lazy | no | no | only traverse and select |
|
||||
| Boost.JSON | owning, mutable DOM | yes | yes | monotonic resource |
|
||||
| `json::parse` | owning, mutable DOM | yes | yes | |
|
||||
|
||||
Reusing memory matters as much as the parser. simdjson DOM reuses its parser, so it writes into memory it already
|
||||
touched; a fresh `json_view` document or yyjson document gets new memory for every parse. On Linux, glibc returns large
|
||||
blocks to the system when they are freed, so every fresh parse of a large document pays a page fault per 4 KiB page:
|
||||
on x86-64 Linux, a fresh `json_view` parse of jeopardy took about twice as long as a reused one. On macOS on Apple
|
||||
silicon, with 16 KiB pages, the difference is much smaller. Compare "json_view (reused)" with "simdjson DOM", and the
|
||||
fresh `json_view` with "simdjson DOM (fresh)" and yyjson.
|
||||
|
||||
## Published results
|
||||
|
||||
Results are only published with the file `compare.py` wrote, which names the machine and the versions; see
|
||||
`results/`. Numbers from one machine and compiler do not carry over to another: rerun the script.
|
||||
@@ -1,348 +0,0 @@
|
||||
// __ _____ _____ _____
|
||||
// __| | __| | | | JSON for Modern C++ (supporting code)
|
||||
// | | |__ | | | | | | version 3.12.0
|
||||
// |_____|_____|_____|_|___| https://github.com/nlohmann/json
|
||||
//
|
||||
// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann <https://nlohmann.me>
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
// Corpus benchmark: the read-only workloads of bench_view.cpp on any list of
|
||||
// JSON files (for example the benchmark sets of simdjson and yyjson).
|
||||
//
|
||||
// ./bench_corpus [--rounds N] file...
|
||||
//
|
||||
// For every file, all engines must accept it and agree on a traversal (value
|
||||
// count, string bytes, sum of numbers) before anything is timed. Workloads:
|
||||
// parse (build and free a document), traverse (visit every value, convert
|
||||
// every number), dump (compact), and for json_view also dump with the source
|
||||
// number text, compared with yyjson writing numbers read as raw text
|
||||
// (YYJSON_READ_NUMBER_AS_RAW). Results go to bench_corpus.csv.
|
||||
#include <nlohmann/json_view.hpp>
|
||||
|
||||
#if JSON_VIEW_BENCH_BOOST
|
||||
#include <boost/json.hpp>
|
||||
#include <boost/json/src.hpp>
|
||||
#endif
|
||||
#include <simdjson.h>
|
||||
#include <yyjson.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
#include <cmath>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <fstream>
|
||||
#include <functional>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
using nlohmann::json;
|
||||
using nlohmann::json_document;
|
||||
using nlohmann::json_view;
|
||||
|
||||
static volatile double g_sink;
|
||||
|
||||
struct stats
|
||||
{
|
||||
double num = 0;
|
||||
std::size_t str = 0, nodes = 0;
|
||||
};
|
||||
|
||||
static void walk(json_view v, stats& st)
|
||||
{
|
||||
++st.nodes;
|
||||
switch (v.type())
|
||||
{
|
||||
case json::value_t::object:
|
||||
for (auto it = v.begin(); it != v.end(); ++it)
|
||||
{
|
||||
st.str += it.key().size();
|
||||
walk(*it, st);
|
||||
}
|
||||
break;
|
||||
case json::value_t::array:
|
||||
for (const json_view e : v)
|
||||
{
|
||||
walk(e, st);
|
||||
}
|
||||
break;
|
||||
case json::value_t::string:
|
||||
st.str += v.get_string().size();
|
||||
break;
|
||||
case json::value_t::number_integer:
|
||||
st.num += static_cast<double>(v.get<std::int64_t>());
|
||||
break;
|
||||
case json::value_t::number_unsigned:
|
||||
st.num += static_cast<double>(v.get<std::uint64_t>());
|
||||
break;
|
||||
case json::value_t::number_float:
|
||||
st.num += v.get<double>();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
static void walk(yyjson_val* v, stats& st)
|
||||
{
|
||||
++st.nodes;
|
||||
switch (yyjson_get_type(v))
|
||||
{
|
||||
case YYJSON_TYPE_OBJ:
|
||||
{
|
||||
std::size_t idx, max;
|
||||
yyjson_val* k, * val;
|
||||
yyjson_obj_foreach(v, idx, max, k, val)
|
||||
{
|
||||
st.str += yyjson_get_len(k);
|
||||
walk(val, st);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case YYJSON_TYPE_ARR:
|
||||
{
|
||||
std::size_t idx, max;
|
||||
yyjson_val* val;
|
||||
yyjson_arr_foreach(v, idx, max, val)
|
||||
{
|
||||
walk(val, st);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case YYJSON_TYPE_STR:
|
||||
st.str += yyjson_get_len(v);
|
||||
break;
|
||||
case YYJSON_TYPE_NUM:
|
||||
st.num += yyjson_is_sint(v) ? static_cast<double>(yyjson_get_sint(v)) : yyjson_is_uint(v) ? static_cast<double>(yyjson_get_uint(v)) : yyjson_get_real(v);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
static void walk(simdjson::dom::element e, stats& st)
|
||||
{
|
||||
++st.nodes;
|
||||
switch (e.type())
|
||||
{
|
||||
case simdjson::dom::element_type::OBJECT:
|
||||
for (auto f : simdjson::dom::object(e))
|
||||
{
|
||||
st.str += f.key.size();
|
||||
walk(f.value, st);
|
||||
}
|
||||
break;
|
||||
case simdjson::dom::element_type::ARRAY:
|
||||
for (auto c : simdjson::dom::array(e))
|
||||
{
|
||||
walk(c, st);
|
||||
}
|
||||
break;
|
||||
case simdjson::dom::element_type::STRING:
|
||||
st.str += std::string_view(e).size();
|
||||
break;
|
||||
case simdjson::dom::element_type::INT64:
|
||||
st.num += static_cast<double>(int64_t(e));
|
||||
break;
|
||||
case simdjson::dom::element_type::UINT64:
|
||||
st.num += static_cast<double>(uint64_t(e));
|
||||
break;
|
||||
case simdjson::dom::element_type::DOUBLE:
|
||||
st.num += double(e);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
#if JSON_VIEW_BENCH_BOOST
|
||||
static void walk(const boost::json::value& v, stats& st)
|
||||
{
|
||||
++st.nodes;
|
||||
switch (v.kind())
|
||||
{
|
||||
case boost::json::kind::object:
|
||||
for (const auto& kv : v.get_object())
|
||||
{
|
||||
st.str += kv.key().size();
|
||||
walk(kv.value(), st);
|
||||
}
|
||||
break;
|
||||
case boost::json::kind::array:
|
||||
for (const auto& c : v.get_array())
|
||||
{
|
||||
walk(c, st);
|
||||
}
|
||||
break;
|
||||
case boost::json::kind::string:
|
||||
st.str += v.get_string().size();
|
||||
break;
|
||||
case boost::json::kind::int64:
|
||||
st.num += static_cast<double>(v.get_int64());
|
||||
break;
|
||||
case boost::json::kind::uint64:
|
||||
st.num += static_cast<double>(v.get_uint64());
|
||||
break;
|
||||
case boost::json::kind::double_:
|
||||
st.num += v.get_double();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
static std::string slurp(const std::string& p)
|
||||
{
|
||||
std::ifstream f(p, std::ios::binary);
|
||||
if (!f)
|
||||
{
|
||||
std::fprintf(stderr, "cannot open %s\n", p.c_str());
|
||||
std::exit(1);
|
||||
}
|
||||
std::stringstream ss;
|
||||
ss << f.rdbuf();
|
||||
return ss.str();
|
||||
}
|
||||
|
||||
static bool same(const stats& a, const stats& b)
|
||||
{
|
||||
return a.nodes == b.nodes && a.str == b.str && (a.num == b.num || std::fabs(a.num - b.num) <= 1e-9 * std::fabs(a.num));
|
||||
}
|
||||
|
||||
int main(int argc, char** argv)
|
||||
{
|
||||
int rounds = 0; // 0: by size
|
||||
std::vector<std::string> files;
|
||||
for (int i = 1; i < argc; ++i)
|
||||
{
|
||||
if (std::strcmp(argv[i], "--rounds") == 0 && i + 1 < argc)
|
||||
{
|
||||
// (clamped: 0 rounds by size, at most a million)
|
||||
rounds = static_cast<int>((std::min)((std::max)(std::strtol(argv[++i], nullptr, 10), 0L), 1000000L));
|
||||
}
|
||||
else
|
||||
{
|
||||
files.push_back(argv[i]);
|
||||
}
|
||||
}
|
||||
std::FILE* csv = std::fopen("bench_corpus.csv", "w");
|
||||
if (csv == nullptr)
|
||||
{
|
||||
std::perror("bench_corpus.csv");
|
||||
return 1;
|
||||
}
|
||||
std::fprintf(csv, "file,bytes,workload,engine,ns\n");
|
||||
json_document reused;
|
||||
simdjson::dom::parser sj;
|
||||
for (const auto& path : files)
|
||||
{
|
||||
const std::string s = slurp(path);
|
||||
const std::string name = path.substr(path.rfind('/') + 1);
|
||||
const simdjson::padded_string ps(s);
|
||||
|
||||
// all engines must agree before timing
|
||||
stats a, b, c, d;
|
||||
const json_document doc = json_document::parse(s);
|
||||
walk(doc.root(), a);
|
||||
yyjson_doc* y = yyjson_read(s.data(), s.size(), 0);
|
||||
auto sjr = sj.parse(ps);
|
||||
#if JSON_VIEW_BENCH_BOOST
|
||||
boost::json::parse_options opt;
|
||||
opt.numbers = boost::json::number_precision::precise;
|
||||
boost::json::monotonic_resource mr0;
|
||||
const boost::json::value bv = boost::json::parse(s, &mr0, opt);
|
||||
#endif
|
||||
if (y == nullptr || sjr.error())
|
||||
{
|
||||
std::printf("%-34s skipped (an engine rejects it)\n", name.c_str());
|
||||
yyjson_doc_free(y);
|
||||
continue;
|
||||
}
|
||||
walk(yyjson_doc_get_root(y), b);
|
||||
walk(sjr.value_unsafe(), c);
|
||||
#if JSON_VIEW_BENCH_BOOST
|
||||
walk(bv, d);
|
||||
#else
|
||||
d = a;
|
||||
#endif
|
||||
yyjson_doc_free(y);
|
||||
const bool ok = same(a, b) && same(a, c) && same(a, d);
|
||||
|
||||
const int r = rounds > 0 ? rounds : static_cast<int>(std::max<std::size_t>(3, std::min<std::size_t>(60, 400000000 / (s.size() + 1))));
|
||||
struct engine
|
||||
{
|
||||
std::string name;
|
||||
std::function<void()> fn;
|
||||
};
|
||||
json_document vd = json_document::parse(s);
|
||||
yyjson_doc* yd = yyjson_read(s.data(), s.size(), 0);
|
||||
yyjson_doc* yd_raw = yyjson_read(s.data(), s.size(), YYJSON_READ_NUMBER_AS_RAW);
|
||||
simdjson::dom::parser sjd;
|
||||
const simdjson::dom::element se = sjd.parse(ps).value_unsafe();
|
||||
const std::vector<std::pair<std::string, std::vector<engine>>> workloads =
|
||||
{
|
||||
{
|
||||
"parse", {
|
||||
{"json_view", [&] { auto x = json_document::parse(s); g_sink = static_cast<double>(x.node_count()); }},
|
||||
{"json_view (reused)", [&] { reused.read(s); g_sink = static_cast<double>(reused.node_count()); }},
|
||||
{"yyjson", [&] { yyjson_doc* x = yyjson_read(s.data(), s.size(), 0); g_sink = static_cast<double>(yyjson_doc_get_val_count(x)); yyjson_doc_free(x); }},
|
||||
{"simdjson DOM", [&] { auto e = sj.parse(ps).value_unsafe(); g_sink = e.is_object(); }},
|
||||
{"simdjson DOM (fresh)", [&] { simdjson::dom::parser p; auto e = p.parse(ps).value_unsafe(); g_sink = e.is_object(); }},
|
||||
#if JSON_VIEW_BENCH_BOOST
|
||||
{"Boost.JSON", [&] { boost::json::monotonic_resource mr; auto v = boost::json::parse(s, &mr); g_sink = v.is_object(); }},
|
||||
#endif
|
||||
}
|
||||
},
|
||||
{
|
||||
"traverse", {
|
||||
{"json_view", [&] { auto x = json_document::parse(s); stats st; walk(x.root(), st); g_sink = st.num; }},
|
||||
{"yyjson", [&] { yyjson_doc* x = yyjson_read(s.data(), s.size(), 0); stats st; walk(yyjson_doc_get_root(x), st); g_sink = st.num; yyjson_doc_free(x); }},
|
||||
{"simdjson DOM", [&] { stats st; walk(sj.parse(ps).value_unsafe(), st); g_sink = st.num; }},
|
||||
#if JSON_VIEW_BENCH_BOOST
|
||||
{"Boost.JSON", [&] { boost::json::monotonic_resource mr; auto v = boost::json::parse(s, &mr); stats st; walk(v, st); g_sink = st.num; }},
|
||||
#endif
|
||||
}
|
||||
},
|
||||
{
|
||||
"dump", {
|
||||
{"json_view", [&] { std::string o = vd.root().dump(); g_sink = static_cast<double>(o.size()); }},
|
||||
{"yyjson", [&] { std::size_t n = 0; char* o = yyjson_write(yd, 0, &n); g_sink = static_cast<double>(n); std::free(o); }},
|
||||
{"simdjson DOM", [&] { std::string o = simdjson::to_string(se); g_sink = static_cast<double>(o.size()); }},
|
||||
{"json_view (source numbers)", [&] { std::string o = vd.root().dump(-1, ' ', false, json_view::number_format::source); g_sink = static_cast<double>(o.size()); }},
|
||||
{"yyjson (raw numbers)", [&] { std::size_t n = 0; char* o = yyjson_write(yd_raw, 0, &n); g_sink = static_cast<double>(n); std::free(o); }},
|
||||
}
|
||||
},
|
||||
};
|
||||
std::printf("%-34s %9zu B%s\n", name.c_str(), s.size(), ok ? "" : " [ENGINES DISAGREE]");
|
||||
for (const auto& wl : workloads)
|
||||
{
|
||||
std::vector<double> best(wl.second.size(), 1e300);
|
||||
for (int i = 0; i < r; ++i)
|
||||
{
|
||||
for (std::size_t k = 0; k < wl.second.size(); ++k)
|
||||
{
|
||||
// an untimed call first: whatever the previous engine left to the allocator
|
||||
// (e.g. thousands of freed json nodes) is cleaned up here, not in the timing
|
||||
wl.second[k].fn();
|
||||
const auto t0 = std::chrono::steady_clock::now();
|
||||
wl.second[k].fn();
|
||||
best[k] = std::min(best[k], std::chrono::duration<double, std::nano>(std::chrono::steady_clock::now() - t0).count());
|
||||
}
|
||||
}
|
||||
std::printf(" %-9s", wl.first.c_str());
|
||||
for (std::size_t k = 0; k < wl.second.size(); ++k)
|
||||
{
|
||||
std::printf(" %s %.2f GB/s (%.2fx)", wl.second[k].name.c_str(), static_cast<double>(s.size()) / best[k], best[k] / best[0]);
|
||||
std::fprintf(csv, "%s,%zu,%s,%s,%.1f\n", name.c_str(), s.size(), wl.first.c_str(), wl.second[k].name.c_str(), best[k]);
|
||||
}
|
||||
std::printf("\n");
|
||||
std::fflush(stdout);
|
||||
}
|
||||
yyjson_doc_free(yd);
|
||||
yyjson_doc_free(yd_raw);
|
||||
}
|
||||
std::fclose(csv);
|
||||
}
|
||||
@@ -1,589 +0,0 @@
|
||||
// __ _____ _____ _____
|
||||
// __| | __| | | | JSON for Modern C++ (supporting code)
|
||||
// | | |__ | | | | | | version 3.12.0
|
||||
// |_____|_____|_____|_|___| https://github.com/nlohmann/json
|
||||
//
|
||||
// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann <https://nlohmann.me>
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
// Read-modify-write benchmark: parse a document, apply the same logical edits
|
||||
// with each library's own API, and serialize it (compact).
|
||||
//
|
||||
// json_view json_editable_document: edits in place, unchanged values stay in the index
|
||||
// yyjson yyjson_read + yyjson_doc_mut_copy (the way to edit a parsed document)
|
||||
// Boost.JSON parse into a mutable DOM (monotonic resource, precise numbers), serialize
|
||||
// json::parse nlohmann::json today
|
||||
// simdjson has no mutable document and is not part of this comparison.
|
||||
//
|
||||
// Workloads:
|
||||
// patch a handful of edits at fixed places (scalars, a new member, a new array element)
|
||||
// update edits in every record (twitter: 100 statuses, citm: 243 performances,
|
||||
// canada: 480 rings, jeopardy: 216,930 questions): set scalars, erase a
|
||||
// member, add a member (canada: replace the first point of every ring)
|
||||
//
|
||||
// Build: see README.md (same flags as bench_view.cpp).
|
||||
#include <nlohmann/json_view.hpp>
|
||||
|
||||
#if JSON_VIEW_BENCH_BOOST
|
||||
#include <boost/json.hpp>
|
||||
#include <boost/json/src.hpp>
|
||||
#endif
|
||||
#include <yyjson.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <fstream>
|
||||
#include <functional>
|
||||
#include <sstream>
|
||||
|
||||
using nlohmann::json;
|
||||
using nlohmann::json_editable_document;
|
||||
using nlohmann::json_editable_view;
|
||||
#if JSON_VIEW_BENCH_BOOST
|
||||
namespace bj = boost::json;
|
||||
#endif
|
||||
|
||||
static volatile std::size_t g_sink;
|
||||
|
||||
// ---------------- json_view ----------------
|
||||
|
||||
static std::string edit_view(const std::string& name, const std::string& s, bool update)
|
||||
{
|
||||
json_editable_document d = json_editable_document::parse(s);
|
||||
const json_editable_view r = d.root();
|
||||
if (name == "twitter")
|
||||
{
|
||||
if (update)
|
||||
{
|
||||
std::int64_t i = 0;
|
||||
for (const json_editable_view st : r["statuses"])
|
||||
{
|
||||
d.set(st, "retweet_count", i++);
|
||||
d.set(st, "favorited", true);
|
||||
d.set(st, "text", "redacted");
|
||||
d.erase(st, "entities");
|
||||
d.set(st, "edited", true);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
d.set(r["search_metadata"], "count", 200);
|
||||
d.set(r["statuses"][0], "text", "patched");
|
||||
d.set(r["statuses"][0]["user"], "followers_count", 1);
|
||||
d.set(r["statuses"][99], "favorited", true);
|
||||
d.set(r, "patched", true);
|
||||
}
|
||||
}
|
||||
else if (name == "citm_catalog")
|
||||
{
|
||||
if (update)
|
||||
{
|
||||
for (const json_editable_view p : r["performances"])
|
||||
{
|
||||
d.set(p, "name", "performance");
|
||||
d.set(p, "start", p["start"].get<std::int64_t>() + 1);
|
||||
d.erase(p, "seatMapImage");
|
||||
d.set(p, "edited", true);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
d.set(r["events"]["138586341"], "name", "patched");
|
||||
d.set(r["performances"][0], "start", 0);
|
||||
d.set(r["venueNames"], "PLEYEL_PLEYEL", "Salle");
|
||||
d.set(r, "patched", true);
|
||||
}
|
||||
}
|
||||
else if (name == "canada")
|
||||
{
|
||||
const json_editable_view coords = r["features"][0]["geometry"]["coordinates"];
|
||||
if (update)
|
||||
{
|
||||
for (const json_editable_view ring : coords)
|
||||
{
|
||||
d.set(ring, 0, json::array({0.5, 0.5}));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
d.set(r["features"][0]["properties"], "name", "patched");
|
||||
d.set(r, "type", "FeatureCollection2");
|
||||
d.set(coords[0], 0, json::array({0.0, 0.0}));
|
||||
}
|
||||
}
|
||||
else if (name == "jeopardy")
|
||||
{
|
||||
if (update)
|
||||
{
|
||||
for (const json_editable_view q : r)
|
||||
{
|
||||
d.set(q, "value", "$1");
|
||||
d.erase(q, "air_date");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
d.set(r[0], "value", "$0");
|
||||
d.set(r[100000], "answer", "patched");
|
||||
d.set(r[216929], "round", "x");
|
||||
d.push_back(r, json::object({{"category", "NEW"}, {"value", "$5"}}));
|
||||
}
|
||||
}
|
||||
else if (name == "status")
|
||||
{
|
||||
d.set(r, "retweet_count", 1);
|
||||
d.set(r["user"], "name", "x");
|
||||
}
|
||||
else if (name == "rpc")
|
||||
{
|
||||
d.set(r, "id", 4);
|
||||
d.set(r["params"], "subtrahend", 24);
|
||||
}
|
||||
return r.dump();
|
||||
}
|
||||
|
||||
// ---------------- nlohmann::json ----------------
|
||||
|
||||
static std::string edit_json(const std::string& name, const std::string& s, bool update)
|
||||
{
|
||||
json r = json::parse(s);
|
||||
if (name == "twitter")
|
||||
{
|
||||
if (update)
|
||||
{
|
||||
std::int64_t i = 0;
|
||||
for (auto& st : r["statuses"])
|
||||
{
|
||||
st["retweet_count"] = i++;
|
||||
st["favorited"] = true;
|
||||
st["text"] = "redacted";
|
||||
st.erase("entities");
|
||||
st["edited"] = true;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
r["search_metadata"]["count"] = 200;
|
||||
r["statuses"][0]["text"] = "patched";
|
||||
r["statuses"][0]["user"]["followers_count"] = 1;
|
||||
r["statuses"][99]["favorited"] = true;
|
||||
r["patched"] = true;
|
||||
}
|
||||
}
|
||||
else if (name == "citm_catalog")
|
||||
{
|
||||
if (update)
|
||||
{
|
||||
for (auto& p : r["performances"])
|
||||
{
|
||||
p["name"] = "performance";
|
||||
p["start"] = p["start"].get<std::int64_t>() + 1;
|
||||
p.erase("seatMapImage");
|
||||
p["edited"] = true;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
r["events"]["138586341"]["name"] = "patched";
|
||||
r["performances"][0]["start"] = 0;
|
||||
r["venueNames"]["PLEYEL_PLEYEL"] = "Salle";
|
||||
r["patched"] = true;
|
||||
}
|
||||
}
|
||||
else if (name == "canada")
|
||||
{
|
||||
json& coords = r["features"][0]["geometry"]["coordinates"];
|
||||
if (update)
|
||||
{
|
||||
for (auto& ring : coords)
|
||||
{
|
||||
ring[0] = json::array({0.5, 0.5});
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
r["features"][0]["properties"]["name"] = "patched";
|
||||
r["type"] = "FeatureCollection2";
|
||||
coords[0][0] = json::array({0.0, 0.0});
|
||||
}
|
||||
}
|
||||
else if (name == "jeopardy")
|
||||
{
|
||||
if (update)
|
||||
{
|
||||
for (auto& q : r)
|
||||
{
|
||||
q["value"] = "$1";
|
||||
q.erase("air_date");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
r[0]["value"] = "$0";
|
||||
r[100000]["answer"] = "patched";
|
||||
r[216929]["round"] = "x";
|
||||
r.push_back(json::object({{"category", "NEW"}, {"value", "$5"}}));
|
||||
}
|
||||
}
|
||||
else if (name == "status")
|
||||
{
|
||||
r["retweet_count"] = 1;
|
||||
r["user"]["name"] = "x";
|
||||
}
|
||||
else if (name == "rpc")
|
||||
{
|
||||
r["id"] = 4;
|
||||
r["params"]["subtrahend"] = 24;
|
||||
}
|
||||
return r.dump();
|
||||
}
|
||||
|
||||
// ---------------- yyjson ----------------
|
||||
|
||||
static std::string edit_yyjson(const std::string& name, const std::string& s, bool update)
|
||||
{
|
||||
yyjson_doc* idoc = yyjson_read(s.data(), s.size(), 0);
|
||||
yyjson_mut_doc* d = yyjson_doc_mut_copy(idoc, nullptr);
|
||||
yyjson_doc_free(idoc);
|
||||
yyjson_mut_val* r = yyjson_mut_doc_get_root(d);
|
||||
auto get = [](yyjson_mut_val * o, const char* k)
|
||||
{
|
||||
return yyjson_mut_obj_get(o, k);
|
||||
};
|
||||
if (name == "twitter")
|
||||
{
|
||||
yyjson_mut_val* sts = get(r, "statuses");
|
||||
if (update)
|
||||
{
|
||||
std::size_t idx, max;
|
||||
yyjson_mut_val* st;
|
||||
std::int64_t i = 0;
|
||||
yyjson_mut_arr_foreach(sts, idx, max, st)
|
||||
{
|
||||
yyjson_mut_set_sint(get(st, "retweet_count"), i++);
|
||||
yyjson_mut_set_bool(get(st, "favorited"), true);
|
||||
yyjson_mut_set_str(get(st, "text"), "redacted");
|
||||
yyjson_mut_obj_remove_key(st, "entities");
|
||||
yyjson_mut_obj_add_bool(d, st, "edited", true);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
yyjson_mut_set_sint(get(get(r, "search_metadata"), "count"), 200);
|
||||
yyjson_mut_val* s0 = yyjson_mut_arr_get(sts, 0);
|
||||
yyjson_mut_set_str(get(s0, "text"), "patched");
|
||||
yyjson_mut_set_sint(get(get(s0, "user"), "followers_count"), 1);
|
||||
yyjson_mut_set_bool(get(yyjson_mut_arr_get(sts, 99), "favorited"), true);
|
||||
yyjson_mut_obj_add_bool(d, r, "patched", true);
|
||||
}
|
||||
}
|
||||
else if (name == "citm_catalog")
|
||||
{
|
||||
if (update)
|
||||
{
|
||||
std::size_t idx, max;
|
||||
yyjson_mut_val* p;
|
||||
yyjson_mut_arr_foreach(get(r, "performances"), idx, max, p)
|
||||
{
|
||||
yyjson_mut_set_str(get(p, "name"), "performance");
|
||||
yyjson_mut_val* start = get(p, "start");
|
||||
yyjson_mut_set_sint(start, yyjson_mut_get_sint(start) + 1);
|
||||
yyjson_mut_obj_remove_key(p, "seatMapImage");
|
||||
yyjson_mut_obj_add_bool(d, p, "edited", true);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
yyjson_mut_set_str(get(get(get(r, "events"), "138586341"), "name"), "patched");
|
||||
yyjson_mut_set_sint(get(yyjson_mut_arr_get(get(r, "performances"), 0), "start"), 0);
|
||||
yyjson_mut_set_str(get(get(r, "venueNames"), "PLEYEL_PLEYEL"), "Salle");
|
||||
yyjson_mut_obj_add_bool(d, r, "patched", true);
|
||||
}
|
||||
}
|
||||
else if (name == "canada")
|
||||
{
|
||||
yyjson_mut_val* f0 = yyjson_mut_arr_get(get(r, "features"), 0);
|
||||
yyjson_mut_val* coords = get(get(f0, "geometry"), "coordinates");
|
||||
if (update)
|
||||
{
|
||||
static const double half[2] = {0.5, 0.5};
|
||||
std::size_t idx, max;
|
||||
yyjson_mut_val* ring;
|
||||
yyjson_mut_arr_foreach(coords, idx, max, ring)
|
||||
{
|
||||
yyjson_mut_arr_replace(ring, 0, yyjson_mut_arr_with_real(d, half, 2));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
static const double zero[2] = {0.0, 0.0};
|
||||
yyjson_mut_set_str(get(get(f0, "properties"), "name"), "patched");
|
||||
yyjson_mut_set_str(get(r, "type"), "FeatureCollection2");
|
||||
yyjson_mut_arr_replace(yyjson_mut_arr_get(coords, 0), 0, yyjson_mut_arr_with_real(d, zero, 2));
|
||||
}
|
||||
}
|
||||
else if (name == "jeopardy")
|
||||
{
|
||||
if (update)
|
||||
{
|
||||
std::size_t idx, max;
|
||||
yyjson_mut_val* q;
|
||||
yyjson_mut_arr_foreach(r, idx, max, q)
|
||||
{
|
||||
yyjson_mut_set_str(get(q, "value"), "$1");
|
||||
yyjson_mut_obj_remove_key(q, "air_date");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
yyjson_mut_set_str(get(yyjson_mut_arr_get(r, 0), "value"), "$0");
|
||||
yyjson_mut_set_str(get(yyjson_mut_arr_get(r, 100000), "answer"), "patched");
|
||||
yyjson_mut_set_str(get(yyjson_mut_arr_get(r, 216929), "round"), "x");
|
||||
yyjson_mut_val* o = yyjson_mut_obj(d);
|
||||
yyjson_mut_obj_add_str(d, o, "category", "NEW");
|
||||
yyjson_mut_obj_add_str(d, o, "value", "$5");
|
||||
yyjson_mut_arr_append(r, o);
|
||||
}
|
||||
}
|
||||
else if (name == "status")
|
||||
{
|
||||
yyjson_mut_set_sint(get(r, "retweet_count"), 1);
|
||||
yyjson_mut_set_str(get(get(r, "user"), "name"), "x");
|
||||
}
|
||||
else if (name == "rpc")
|
||||
{
|
||||
yyjson_mut_set_sint(get(r, "id"), 4);
|
||||
yyjson_mut_set_sint(get(get(r, "params"), "subtrahend"), 24);
|
||||
}
|
||||
std::size_t n = 0;
|
||||
char* out = yyjson_mut_write(d, 0, &n);
|
||||
std::string result(out, n);
|
||||
std::free(out);
|
||||
yyjson_mut_doc_free(d);
|
||||
return result;
|
||||
}
|
||||
|
||||
#if JSON_VIEW_BENCH_BOOST
|
||||
// ---------------- Boost.JSON ----------------
|
||||
|
||||
static std::string edit_boost(const std::string& name, const std::string& s, bool update)
|
||||
{
|
||||
bj::monotonic_resource mr;
|
||||
bj::parse_options opt;
|
||||
opt.numbers = bj::number_precision::precise; // correctly rounded, like the others
|
||||
bj::value v = bj::parse(s, &mr, opt);
|
||||
bj::object* const obj = v.if_object(); // nullptr for jeopardy (an array)
|
||||
if (name == "twitter")
|
||||
{
|
||||
bj::array& sts = (*obj)["statuses"].as_array();
|
||||
if (update)
|
||||
{
|
||||
std::int64_t i = 0;
|
||||
for (auto& e : sts)
|
||||
{
|
||||
bj::object& st = e.as_object();
|
||||
st["retweet_count"] = i++;
|
||||
st["favorited"] = true;
|
||||
st["text"] = "redacted";
|
||||
st.erase("entities");
|
||||
st["edited"] = true;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
(*obj)["search_metadata"].as_object()["count"] = 200;
|
||||
bj::object& s0 = sts[0].as_object();
|
||||
s0["text"] = "patched";
|
||||
s0["user"].as_object()["followers_count"] = 1;
|
||||
sts[99].as_object()["favorited"] = true;
|
||||
(*obj)["patched"] = true;
|
||||
}
|
||||
}
|
||||
else if (name == "citm_catalog")
|
||||
{
|
||||
if (update)
|
||||
{
|
||||
for (auto& e : (*obj)["performances"].as_array())
|
||||
{
|
||||
bj::object& p = e.as_object();
|
||||
p["name"] = "performance";
|
||||
p["start"] = p["start"].as_int64() + 1;
|
||||
p.erase("seatMapImage");
|
||||
p["edited"] = true;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
(*obj)["events"].as_object()["138586341"].as_object()["name"] = "patched";
|
||||
(*obj)["performances"].as_array()[0].as_object()["start"] = 0;
|
||||
(*obj)["venueNames"].as_object()["PLEYEL_PLEYEL"] = "Salle";
|
||||
(*obj)["patched"] = true;
|
||||
}
|
||||
}
|
||||
else if (name == "canada")
|
||||
{
|
||||
bj::object& f0 = (*obj)["features"].as_array()[0].as_object();
|
||||
bj::array& coords = f0["geometry"].as_object()["coordinates"].as_array();
|
||||
if (update)
|
||||
{
|
||||
for (auto& ring : coords)
|
||||
{
|
||||
ring.as_array()[0] = bj::array({0.5, 0.5});
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
f0["properties"].as_object()["name"] = "patched";
|
||||
(*obj)["type"] = "FeatureCollection2";
|
||||
coords[0].as_array()[0] = bj::array({0.0, 0.0});
|
||||
}
|
||||
}
|
||||
else if (name == "jeopardy")
|
||||
{
|
||||
bj::array& a = v.as_array();
|
||||
if (update)
|
||||
{
|
||||
for (auto& e : a)
|
||||
{
|
||||
bj::object& q = e.as_object();
|
||||
q["value"] = "$1";
|
||||
q.erase("air_date");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
a[0].as_object()["value"] = "$0";
|
||||
a[100000].as_object()["answer"] = "patched";
|
||||
a[216929].as_object()["round"] = "x";
|
||||
a.push_back(bj::object({{"category", "NEW"}, {"value", "$5"}}));
|
||||
}
|
||||
}
|
||||
else if (name == "status")
|
||||
{
|
||||
(*obj)["retweet_count"] = 1;
|
||||
(*obj)["user"].as_object()["name"] = "x";
|
||||
}
|
||||
else if (name == "rpc")
|
||||
{
|
||||
(*obj)["id"] = 4;
|
||||
(*obj)["params"].as_object()["subtrahend"] = 24;
|
||||
}
|
||||
return bj::serialize(v);
|
||||
}
|
||||
#endif
|
||||
|
||||
// ---------------- harness ----------------
|
||||
|
||||
static std::string slurp(const std::string& p)
|
||||
{
|
||||
std::ifstream f(p, std::ios::binary);
|
||||
if (!f)
|
||||
{
|
||||
std::fprintf(stderr, "cannot open %s\n", p.c_str());
|
||||
std::exit(1);
|
||||
}
|
||||
std::stringstream ss;
|
||||
ss << f.rdbuf();
|
||||
return ss.str();
|
||||
}
|
||||
|
||||
int main(int argc, char** argv)
|
||||
{
|
||||
if (argc < 2)
|
||||
{
|
||||
std::fprintf(stderr, "usage: %s <json_test_data directory> [rounds] [document]\n", argv[0]);
|
||||
return 1;
|
||||
}
|
||||
const std::string T = std::string(argv[1]) + "/";
|
||||
// (clamped to 1 to a million)
|
||||
const int rounds = argc > 2 ? static_cast<int>((std::min)((std::max)(std::strtol(argv[2], nullptr, 10), 1L), 1000000L)) : 20;
|
||||
const std::string only = argc > 3 ? argv[3] : "";
|
||||
struct doc
|
||||
{
|
||||
std::string name, text;
|
||||
int batch;
|
||||
};
|
||||
std::vector<doc> docs;
|
||||
for (const char* f :
|
||||
{"nativejson-benchmark/twitter.json", "nativejson-benchmark/citm_catalog.json", "nativejson-benchmark/canada.json", "jeopardy/jeopardy.json"
|
||||
})
|
||||
{
|
||||
std::string n = std::string(f).substr(std::string(f).find('/') + 1);
|
||||
docs.push_back({n.substr(0, n.size() - 5), slurp(T + f), 1});
|
||||
}
|
||||
docs.push_back({"status", json::parse(docs[0].text)["statuses"][0].dump(), 200});
|
||||
docs.push_back({"rpc", R"({"jsonrpc": "2.0", "method": "subtract", "params": {"minuend": 42, "subtrahend": 23}, "id": 3})", 5000});
|
||||
|
||||
using fn = std::string (*)(const std::string&, const std::string&, bool);
|
||||
const std::vector<std::pair<std::string, fn>> engines =
|
||||
{
|
||||
{"json_view", edit_view}, {"yyjson", edit_yyjson},
|
||||
#if JSON_VIEW_BENCH_BOOST
|
||||
{"Boost.JSON", edit_boost},
|
||||
#endif
|
||||
{"json::parse", edit_json}
|
||||
};
|
||||
|
||||
std::FILE* csv = std::fopen("bench_edit.csv", "w");
|
||||
if (csv == nullptr)
|
||||
{
|
||||
std::perror("bench_edit.csv");
|
||||
return 1;
|
||||
}
|
||||
std::fprintf(csv, "doc,bytes,workload,engine,ns\n");
|
||||
for (const auto& dc : docs)
|
||||
{
|
||||
if (!only.empty() && dc.name != only)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
for (const bool update :
|
||||
{
|
||||
false, true
|
||||
})
|
||||
{
|
||||
if (update && (dc.name == "status" || dc.name == "rpc"))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
// all engines must produce the same value
|
||||
const json expected = json::parse(edit_json(dc.name, dc.text, update));
|
||||
bool ok = true;
|
||||
for (const auto& e : engines)
|
||||
{
|
||||
ok = ok && json::parse(e.second(dc.name, dc.text, update)) == expected;
|
||||
}
|
||||
std::vector<double> best(engines.size(), 1e300);
|
||||
const int r = dc.text.size() > 10000000 ? std::max(3, rounds / 4) : rounds;
|
||||
for (int i = 0; i < r; ++i)
|
||||
{
|
||||
for (std::size_t k = 0; k < engines.size(); ++k)
|
||||
{
|
||||
// an untimed call first: whatever the previous engine left to the allocator
|
||||
// (e.g. thousands of freed json nodes) is cleaned up here, not in the timing
|
||||
g_sink = engines[k].second(dc.name, dc.text, update).size();
|
||||
const auto t0 = std::chrono::steady_clock::now();
|
||||
for (int b = 0; b < dc.batch; ++b)
|
||||
{
|
||||
g_sink = engines[k].second(dc.name, dc.text, update).size();
|
||||
}
|
||||
const double ns = std::chrono::duration<double, std::nano>(std::chrono::steady_clock::now() - t0).count() / dc.batch;
|
||||
best[k] = std::min(best[k], ns);
|
||||
}
|
||||
}
|
||||
const char* wl = update ? "update" : "patch";
|
||||
std::printf("%-13s %-7s %s", dc.name.c_str(), wl, ok ? "" : "[OUTPUT MISMATCH] ");
|
||||
for (std::size_t k = 0; k < engines.size(); ++k)
|
||||
{
|
||||
const double us = best[k] / 1e3;
|
||||
std::printf(" %s %.*fus (%.2fx)", engines[k].first.c_str(), us < 10 ? 3 : (us < 1000 ? 1 : 0), us, best[k] / best[0]);
|
||||
std::fprintf(csv, "%s,%zu,%s,%s,%.1f\n", dc.name.c_str(), dc.text.size(), wl, engines[k].first.c_str(), best[k]);
|
||||
}
|
||||
std::printf("\n");
|
||||
std::fflush(stdout);
|
||||
}
|
||||
}
|
||||
std::fclose(csv);
|
||||
}
|
||||
@@ -1,755 +0,0 @@
|
||||
// __ _____ _____ _____
|
||||
// __| | __| | | | JSON for Modern C++ (supporting code)
|
||||
// | | |__ | | | | | | version 3.12.0
|
||||
// |_____|_____|_____|_|___| https://github.com/nlohmann/json
|
||||
//
|
||||
// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann <https://nlohmann.me>
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
// Same-feature-set benchmark: read-only JSON documents with random access.
|
||||
//
|
||||
// json_view nlohmann/json_view.hpp (fresh document per parse / reused)
|
||||
// yyjson yyjson_read(): immutable document, random access
|
||||
// simdjson DOM dom::parser (reused, as recommended; "fresh": a new parser
|
||||
// per parse): immutable, random access
|
||||
// references (different feature sets):
|
||||
// simdjson OD On-Demand: forward-only, lazy
|
||||
// Boost.JSON owning, mutable DOM (monotonic resource)
|
||||
// json::parse owning, mutable DOM (nlohmann today)
|
||||
//
|
||||
// Workloads: parse (build + free), traverse (visit everything, convert every
|
||||
// number, touch every string and key), select (a few fields per document),
|
||||
// dump (compact serialization of the parsed document).
|
||||
// All engines run interleaved in every round, each timed call after an untimed
|
||||
// one of the same engine; the best round is reported.
|
||||
#include <nlohmann/json_view.hpp>
|
||||
|
||||
#if JSON_VIEW_BENCH_BOOST
|
||||
#include <boost/json.hpp>
|
||||
#include <boost/json/src.hpp>
|
||||
#endif
|
||||
#include <simdjson.h>
|
||||
#include <yyjson.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
#include <cmath>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <fstream>
|
||||
#include <functional>
|
||||
#include <map>
|
||||
#include <sstream>
|
||||
|
||||
using nlohmann::json;
|
||||
using nlohmann::json_document;
|
||||
using nlohmann::json_view;
|
||||
|
||||
static volatile double g_sink;
|
||||
|
||||
struct stats
|
||||
{
|
||||
double num = 0;
|
||||
std::size_t str = 0, nodes = 0;
|
||||
};
|
||||
|
||||
// ---------------- traversal ----------------
|
||||
|
||||
static void walk(json_view v, stats& st)
|
||||
{
|
||||
++st.nodes;
|
||||
switch (v.type())
|
||||
{
|
||||
case json::value_t::object:
|
||||
for (auto it = v.begin(); it != v.end(); ++it)
|
||||
{
|
||||
st.str += it.key().size();
|
||||
walk(*it, st);
|
||||
}
|
||||
break;
|
||||
case json::value_t::array:
|
||||
for (const json_view e : v)
|
||||
{
|
||||
walk(e, st);
|
||||
}
|
||||
break;
|
||||
case json::value_t::string:
|
||||
st.str += v.get_string().size();
|
||||
break;
|
||||
case json::value_t::number_integer:
|
||||
st.num += static_cast<double>(v.get<std::int64_t>());
|
||||
break;
|
||||
case json::value_t::number_unsigned:
|
||||
st.num += static_cast<double>(v.get<std::uint64_t>());
|
||||
break;
|
||||
case json::value_t::number_float:
|
||||
st.num += v.get<double>();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
static void walk(const json& j, stats& st)
|
||||
{
|
||||
++st.nodes;
|
||||
switch (j.type())
|
||||
{
|
||||
case json::value_t::object:
|
||||
for (const auto& kv : j.get_ref<const json::object_t&>())
|
||||
{
|
||||
st.str += kv.first.size();
|
||||
walk(kv.second, st);
|
||||
}
|
||||
break;
|
||||
case json::value_t::array:
|
||||
for (const auto& e : j.get_ref<const json::array_t&>())
|
||||
{
|
||||
walk(e, st);
|
||||
}
|
||||
break;
|
||||
case json::value_t::string:
|
||||
st.str += j.get_ref<const std::string&>().size();
|
||||
break;
|
||||
case json::value_t::number_integer:
|
||||
st.num += static_cast<double>(*j.get_ptr<const json::number_integer_t*>());
|
||||
break;
|
||||
case json::value_t::number_unsigned:
|
||||
st.num += static_cast<double>(*j.get_ptr<const json::number_unsigned_t*>());
|
||||
break;
|
||||
case json::value_t::number_float:
|
||||
st.num += *j.get_ptr<const json::number_float_t*>();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
static void walk(yyjson_val* v, stats& st)
|
||||
{
|
||||
++st.nodes;
|
||||
switch (yyjson_get_type(v))
|
||||
{
|
||||
case YYJSON_TYPE_OBJ:
|
||||
{
|
||||
std::size_t idx, max;
|
||||
yyjson_val* k, * val;
|
||||
yyjson_obj_foreach(v, idx, max, k, val)
|
||||
{
|
||||
st.str += yyjson_get_len(k);
|
||||
walk(val, st);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case YYJSON_TYPE_ARR:
|
||||
{
|
||||
std::size_t idx, max;
|
||||
yyjson_val* val;
|
||||
yyjson_arr_foreach(v, idx, max, val)
|
||||
{
|
||||
walk(val, st);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case YYJSON_TYPE_STR:
|
||||
st.str += yyjson_get_len(v);
|
||||
break;
|
||||
case YYJSON_TYPE_NUM:
|
||||
if (yyjson_is_sint(v))
|
||||
{
|
||||
st.num += static_cast<double>(yyjson_get_sint(v));
|
||||
}
|
||||
else if (yyjson_is_uint(v))
|
||||
{
|
||||
st.num += static_cast<double>(yyjson_get_uint(v));
|
||||
}
|
||||
else
|
||||
{
|
||||
st.num += yyjson_get_real(v);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
static void walk(simdjson::dom::element e, stats& st)
|
||||
{
|
||||
++st.nodes;
|
||||
switch (e.type())
|
||||
{
|
||||
case simdjson::dom::element_type::OBJECT:
|
||||
for (auto f : simdjson::dom::object(e))
|
||||
{
|
||||
st.str += f.key.size();
|
||||
walk(f.value, st);
|
||||
}
|
||||
break;
|
||||
case simdjson::dom::element_type::ARRAY:
|
||||
for (auto c : simdjson::dom::array(e))
|
||||
{
|
||||
walk(c, st);
|
||||
}
|
||||
break;
|
||||
case simdjson::dom::element_type::STRING:
|
||||
st.str += std::string_view(e).size();
|
||||
break;
|
||||
case simdjson::dom::element_type::INT64:
|
||||
st.num += static_cast<double>(int64_t(e));
|
||||
break;
|
||||
case simdjson::dom::element_type::UINT64:
|
||||
st.num += static_cast<double>(uint64_t(e));
|
||||
break;
|
||||
case simdjson::dom::element_type::DOUBLE:
|
||||
st.num += double(e);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
static void walk_od(simdjson::ondemand::value v, stats& st)
|
||||
{
|
||||
++st.nodes;
|
||||
switch (v.type())
|
||||
{
|
||||
case simdjson::ondemand::json_type::object:
|
||||
for (auto f : v.get_object())
|
||||
{
|
||||
st.str += std::string_view(f.unescaped_key()).size();
|
||||
walk_od(f.value(), st);
|
||||
}
|
||||
break;
|
||||
case simdjson::ondemand::json_type::array:
|
||||
for (auto c : v.get_array())
|
||||
{
|
||||
walk_od(c.value(), st);
|
||||
}
|
||||
break;
|
||||
case simdjson::ondemand::json_type::string:
|
||||
st.str += std::string_view(v.get_string()).size();
|
||||
break;
|
||||
case simdjson::ondemand::json_type::number:
|
||||
{
|
||||
simdjson::ondemand::number n = v.get_number();
|
||||
switch (n.get_number_type())
|
||||
{
|
||||
case simdjson::ondemand::number_type::signed_integer:
|
||||
st.num += static_cast<double>(n.get_int64());
|
||||
break;
|
||||
case simdjson::ondemand::number_type::unsigned_integer:
|
||||
st.num += static_cast<double>(n.get_uint64());
|
||||
break;
|
||||
default:
|
||||
st.num += n.get_double();
|
||||
break;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case simdjson::ondemand::json_type::boolean:
|
||||
(void)bool(v.get_bool());
|
||||
break;
|
||||
default:
|
||||
(void)v.is_null();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
#if JSON_VIEW_BENCH_BOOST
|
||||
static void walk(const boost::json::value& v, stats& st)
|
||||
{
|
||||
++st.nodes;
|
||||
switch (v.kind())
|
||||
{
|
||||
case boost::json::kind::object:
|
||||
for (const auto& kv : v.get_object())
|
||||
{
|
||||
st.str += kv.key().size();
|
||||
walk(kv.value(), st);
|
||||
}
|
||||
break;
|
||||
case boost::json::kind::array:
|
||||
for (const auto& c : v.get_array())
|
||||
{
|
||||
walk(c, st);
|
||||
}
|
||||
break;
|
||||
case boost::json::kind::string:
|
||||
st.str += v.get_string().size();
|
||||
break;
|
||||
case boost::json::kind::int64:
|
||||
st.num += static_cast<double>(v.get_int64());
|
||||
break;
|
||||
case boost::json::kind::uint64:
|
||||
st.num += static_cast<double>(v.get_uint64());
|
||||
break;
|
||||
case boost::json::kind::double_:
|
||||
st.num += v.get_double();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
// ---------------- selective access ----------------
|
||||
// twitter: per status id, user.screen_name, retweet_count
|
||||
// citm: per performance id, eventId, #seatCategories; #events
|
||||
// canada: type, features[0].geometry.type, #coordinates
|
||||
// jeopardy: per question: round == "Final Jeopardy!", len(category)
|
||||
// status (one tweet): id, user.screen_name, retweet_count
|
||||
// rpc: method, params.minuend, id
|
||||
|
||||
static double pick(const std::string& name, json_view r)
|
||||
{
|
||||
double acc = 0;
|
||||
if (name == "twitter" || name == "status")
|
||||
{
|
||||
auto one = [&](json_view s)
|
||||
{
|
||||
acc += static_cast<double>(s["id"].get<std::uint64_t>());
|
||||
acc += static_cast<double>(s["user"]["screen_name"].get_string().size());
|
||||
acc += static_cast<double>(s["retweet_count"].get<std::int64_t>());
|
||||
};
|
||||
if (name == "twitter")
|
||||
{
|
||||
for (const json_view s : r["statuses"])
|
||||
{
|
||||
one(s);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
one(r);
|
||||
}
|
||||
}
|
||||
else if (name == "citm_catalog")
|
||||
{
|
||||
for (const json_view p : r["performances"])
|
||||
{
|
||||
acc += static_cast<double>(p["id"].get<std::uint64_t>() + p["eventId"].get<std::uint64_t>() + p["seatCategories"].size());
|
||||
}
|
||||
acc += static_cast<double>(r["events"].size());
|
||||
}
|
||||
else if (name == "canada")
|
||||
{
|
||||
const json_view g = r["features"][0]["geometry"];
|
||||
acc += static_cast<double>(r["type"].get_string().size() + g["type"].get_string().size() + g["coordinates"].size());
|
||||
}
|
||||
else if (name == "jeopardy")
|
||||
{
|
||||
for (const json_view q : r)
|
||||
{
|
||||
acc += q["round"].get_string() == "Final Jeopardy!" ? 1 : 0;
|
||||
acc += static_cast<double>(q["category"].get_string().size());
|
||||
}
|
||||
}
|
||||
else if (name == "rpc")
|
||||
{
|
||||
acc += static_cast<double>(r["method"].get_string().size());
|
||||
acc += static_cast<double>(r["params"]["minuend"].get<std::int64_t>() + r["id"].get<std::int64_t>());
|
||||
}
|
||||
return acc;
|
||||
}
|
||||
|
||||
static double pick(const std::string& name, const json& r)
|
||||
{
|
||||
double acc = 0;
|
||||
if (name == "twitter" || name == "status")
|
||||
{
|
||||
auto one = [&](const json & s)
|
||||
{
|
||||
acc += static_cast<double>(s["id"].get<std::uint64_t>());
|
||||
acc += static_cast<double>(s["user"]["screen_name"].get_ref<const std::string&>().size());
|
||||
acc += static_cast<double>(s["retweet_count"].get<std::int64_t>());
|
||||
};
|
||||
if (name == "twitter")
|
||||
{
|
||||
for (const auto& s : r["statuses"])
|
||||
{
|
||||
one(s);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
one(r);
|
||||
}
|
||||
}
|
||||
else if (name == "citm_catalog")
|
||||
{
|
||||
for (const auto& p : r["performances"])
|
||||
{
|
||||
acc += static_cast<double>(p["id"].get<std::uint64_t>() + p["eventId"].get<std::uint64_t>() + p["seatCategories"].size());
|
||||
}
|
||||
acc += static_cast<double>(r["events"].size());
|
||||
}
|
||||
else if (name == "canada")
|
||||
{
|
||||
const json& g = r["features"][0]["geometry"];
|
||||
acc += static_cast<double>(r["type"].get_ref<const std::string&>().size() + g["type"].get_ref<const std::string&>().size() + g["coordinates"].size());
|
||||
}
|
||||
else if (name == "jeopardy")
|
||||
{
|
||||
for (const auto& q : r)
|
||||
{
|
||||
acc += q["round"].get_ref<const std::string&>() == "Final Jeopardy!" ? 1 : 0;
|
||||
acc += static_cast<double>(q["category"].get_ref<const std::string&>().size());
|
||||
}
|
||||
}
|
||||
else if (name == "rpc")
|
||||
{
|
||||
acc += static_cast<double>(r["method"].get_ref<const std::string&>().size());
|
||||
acc += static_cast<double>(r["params"]["minuend"].get<std::int64_t>() + r["id"].get<std::int64_t>());
|
||||
}
|
||||
return acc;
|
||||
}
|
||||
|
||||
static double pick(const std::string& name, yyjson_val* r)
|
||||
{
|
||||
double acc = 0;
|
||||
auto get = [](yyjson_val * o, const char* k)
|
||||
{
|
||||
return yyjson_obj_get(o, k);
|
||||
};
|
||||
if (name == "twitter" || name == "status")
|
||||
{
|
||||
auto one = [&](yyjson_val * s)
|
||||
{
|
||||
acc += static_cast<double>(yyjson_get_uint(get(s, "id")));
|
||||
acc += static_cast<double>(yyjson_get_len(get(get(s, "user"), "screen_name")));
|
||||
acc += static_cast<double>(yyjson_get_sint(get(s, "retweet_count")));
|
||||
};
|
||||
if (name == "twitter")
|
||||
{
|
||||
std::size_t idx, max;
|
||||
yyjson_val* s;
|
||||
yyjson_arr_foreach(get(r, "statuses"), idx, max, s)
|
||||
{
|
||||
one(s);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
one(r);
|
||||
}
|
||||
}
|
||||
else if (name == "citm_catalog")
|
||||
{
|
||||
std::size_t idx, max;
|
||||
yyjson_val* p;
|
||||
yyjson_arr_foreach(get(r, "performances"), idx, max, p)
|
||||
{
|
||||
acc += static_cast<double>(yyjson_get_uint(get(p, "id")) + yyjson_get_uint(get(p, "eventId")) + yyjson_arr_size(get(p, "seatCategories")));
|
||||
}
|
||||
acc += static_cast<double>(yyjson_obj_size(get(r, "events")));
|
||||
}
|
||||
else if (name == "canada")
|
||||
{
|
||||
yyjson_val* g = get(yyjson_arr_get(get(r, "features"), 0), "geometry");
|
||||
acc += static_cast<double>(yyjson_get_len(get(r, "type")) + yyjson_get_len(get(g, "type")) + yyjson_arr_size(get(g, "coordinates")));
|
||||
}
|
||||
else if (name == "jeopardy")
|
||||
{
|
||||
std::size_t idx, max;
|
||||
yyjson_val* q;
|
||||
yyjson_arr_foreach(r, idx, max, q)
|
||||
{
|
||||
acc += yyjson_equals_str(get(q, "round"), "Final Jeopardy!") ? 1 : 0;
|
||||
acc += static_cast<double>(yyjson_get_len(get(q, "category")));
|
||||
}
|
||||
}
|
||||
else if (name == "rpc")
|
||||
{
|
||||
acc += static_cast<double>(yyjson_get_len(get(r, "method")));
|
||||
acc += static_cast<double>(yyjson_get_sint(get(get(r, "params"), "minuend")) + yyjson_get_sint(get(r, "id")));
|
||||
}
|
||||
return acc;
|
||||
}
|
||||
|
||||
static double pick(const std::string& name, simdjson::dom::element r)
|
||||
{
|
||||
double acc = 0;
|
||||
if (name == "twitter" || name == "status")
|
||||
{
|
||||
auto one = [&](simdjson::dom::element s)
|
||||
{
|
||||
acc += static_cast<double>(uint64_t(s["id"]));
|
||||
acc += static_cast<double>(std::string_view(s["user"]["screen_name"]).size());
|
||||
acc += static_cast<double>(int64_t(s["retweet_count"]));
|
||||
};
|
||||
if (name == "twitter")
|
||||
{
|
||||
for (auto s : simdjson::dom::array(r["statuses"]))
|
||||
{
|
||||
one(s);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
one(r);
|
||||
}
|
||||
}
|
||||
else if (name == "citm_catalog")
|
||||
{
|
||||
for (auto p : simdjson::dom::array(r["performances"]))
|
||||
{
|
||||
acc += static_cast<double>(uint64_t(p["id"]) + uint64_t(p["eventId"]) + simdjson::dom::array(p["seatCategories"]).size());
|
||||
}
|
||||
acc += static_cast<double>(simdjson::dom::object(r["events"]).size());
|
||||
}
|
||||
else if (name == "canada")
|
||||
{
|
||||
auto g = r["features"].at(0)["geometry"];
|
||||
acc += static_cast<double>(std::string_view(r["type"]).size() + std::string_view(g["type"]).size() + simdjson::dom::array(g["coordinates"]).size());
|
||||
}
|
||||
else if (name == "jeopardy")
|
||||
{
|
||||
for (auto q : simdjson::dom::array(r))
|
||||
{
|
||||
acc += std::string_view(q["round"]) == "Final Jeopardy!" ? 1 : 0;
|
||||
acc += static_cast<double>(std::string_view(q["category"]).size());
|
||||
}
|
||||
}
|
||||
else if (name == "rpc")
|
||||
{
|
||||
acc += static_cast<double>(std::string_view(r["method"]).size());
|
||||
acc += static_cast<double>(int64_t(r["params"]["minuend"]) + int64_t(r["id"]));
|
||||
}
|
||||
return acc;
|
||||
}
|
||||
|
||||
static double pick_od(const std::string& name, simdjson::ondemand::document& d)
|
||||
{
|
||||
double acc = 0;
|
||||
if (name == "twitter" || name == "status")
|
||||
{
|
||||
auto one = [&](simdjson::ondemand::object s)
|
||||
{
|
||||
acc += static_cast<double>(uint64_t(s["id"]));
|
||||
acc += static_cast<double>(std::string_view(s["user"]["screen_name"]).size());
|
||||
acc += static_cast<double>(int64_t(s["retweet_count"]));
|
||||
};
|
||||
if (name == "twitter")
|
||||
{
|
||||
for (auto s : d["statuses"])
|
||||
{
|
||||
one(s.get_object());
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
one(d.get_object());
|
||||
}
|
||||
}
|
||||
else if (name == "citm_catalog")
|
||||
{
|
||||
simdjson::ondemand::object ev = d["events"].get_object();
|
||||
acc += static_cast<double>(ev.count_fields());
|
||||
for (auto p : d["performances"])
|
||||
{
|
||||
simdjson::ondemand::object o = p.get_object();
|
||||
const auto a = uint64_t(o["eventId"]) + uint64_t(o["id"]);
|
||||
simdjson::ondemand::array sc = o["seatCategories"].get_array();
|
||||
acc += static_cast<double>(a + sc.count_elements());
|
||||
}
|
||||
}
|
||||
else if (name == "canada")
|
||||
{
|
||||
acc += static_cast<double>(std::string_view(d["type"]).size());
|
||||
auto g = d["features"].at(0)["geometry"];
|
||||
acc += static_cast<double>(std::string_view(g["type"]).size());
|
||||
simdjson::ondemand::array co = g["coordinates"].get_array();
|
||||
acc += static_cast<double>(co.count_elements());
|
||||
}
|
||||
else if (name == "jeopardy")
|
||||
{
|
||||
for (auto q : d)
|
||||
{
|
||||
simdjson::ondemand::object o = q.get_object();
|
||||
acc += static_cast<double>(std::string_view(o["category"]).size());
|
||||
acc += std::string_view(o["round"]) == "Final Jeopardy!" ? 1 : 0;
|
||||
}
|
||||
}
|
||||
else if (name == "rpc")
|
||||
{
|
||||
acc += static_cast<double>(std::string_view(d["method"]).size());
|
||||
acc += static_cast<double>(int64_t(d["params"]["minuend"]));
|
||||
acc += static_cast<double>(int64_t(d["id"]));
|
||||
}
|
||||
return acc;
|
||||
}
|
||||
|
||||
// ---------------- harness ----------------
|
||||
|
||||
static std::string slurp(const std::string& p)
|
||||
{
|
||||
std::ifstream f(p, std::ios::binary);
|
||||
if (!f)
|
||||
{
|
||||
std::fprintf(stderr, "cannot open %s\n", p.c_str());
|
||||
std::exit(1);
|
||||
}
|
||||
std::stringstream ss;
|
||||
ss << f.rdbuf();
|
||||
return ss.str();
|
||||
}
|
||||
|
||||
struct engine
|
||||
{
|
||||
std::string name;
|
||||
std::function<void()> fn;
|
||||
};
|
||||
|
||||
int main(int argc, char** argv)
|
||||
{
|
||||
if (argc < 2)
|
||||
{
|
||||
std::fprintf(stderr, "usage: %s <json_test_data directory> [rounds] [document]\n", argv[0]);
|
||||
return 1;
|
||||
}
|
||||
const std::string T = std::string(argv[1]) + "/";
|
||||
// (clamped to 1 to a million)
|
||||
const int rounds = argc > 2 ? static_cast<int>((std::min)((std::max)(std::strtol(argv[2], nullptr, 10), 1L), 1000000L)) : 30;
|
||||
const std::string only = argc > 3 ? argv[3] : "";
|
||||
struct doc
|
||||
{
|
||||
std::string name, text;
|
||||
int batch;
|
||||
};
|
||||
std::vector<doc> docs;
|
||||
for (const char* f :
|
||||
{"nativejson-benchmark/twitter.json", "nativejson-benchmark/citm_catalog.json", "nativejson-benchmark/canada.json", "jeopardy/jeopardy.json"
|
||||
})
|
||||
{
|
||||
std::string n = std::string(f).substr(std::string(f).find('/') + 1);
|
||||
docs.push_back({n.substr(0, n.size() - 5), slurp(T + f), 1});
|
||||
}
|
||||
docs.push_back({"status", json::parse(docs[0].text)["statuses"][0].dump(), 200});
|
||||
docs.push_back({"rpc", R"({"jsonrpc": "2.0", "method": "subtract", "params": {"minuend": 42, "subtrahend": 23}, "id": 3})", 5000});
|
||||
|
||||
// correctness cross-check of the workloads
|
||||
for (const auto& dc : docs)
|
||||
{
|
||||
stats a, b, c, dd;
|
||||
walk(json::parse(dc.text), a);
|
||||
auto d = json_document::parse(dc.text);
|
||||
walk(d.root(), b);
|
||||
yyjson_doc* y = yyjson_read(dc.text.data(), dc.text.size(), 0);
|
||||
walk(yyjson_doc_get_root(y), c);
|
||||
simdjson::dom::parser p;
|
||||
walk(p.parse(dc.text).value(), dd);
|
||||
const bool ok = a.nodes == b.nodes && a.nodes == c.nodes && a.nodes == dd.nodes && a.str == b.str && a.str == c.str && a.str == dd.str
|
||||
&& std::fabs(a.num - b.num) <= 1e-9 * std::fabs(a.num) && std::fabs(a.num - c.num) <= 1e-9 * std::fabs(a.num);
|
||||
const double pa = pick(dc.name, json::parse(dc.text)), pb = pick(dc.name, d.root()), pc = pick(dc.name, yyjson_doc_get_root(y)), pd = pick(dc.name, p.parse(dc.text).value());
|
||||
std::printf("check %-13s traverse %s select %s\n", dc.name.c_str(), ok ? "OK" : "MISMATCH", (pa == pb && pa == pc && pa == pd) ? "OK" : "MISMATCH");
|
||||
yyjson_doc_free(y);
|
||||
}
|
||||
|
||||
std::FILE* csv = std::fopen("bench_view.csv", "w");
|
||||
if (csv == nullptr)
|
||||
{
|
||||
std::perror("bench_view.csv");
|
||||
return 1;
|
||||
}
|
||||
std::fprintf(csv, "doc,bytes,workload,engine,ns\n");
|
||||
json_document reused;
|
||||
simdjson::dom::parser sj;
|
||||
simdjson::ondemand::parser od;
|
||||
for (const auto& dc : docs)
|
||||
{
|
||||
if (!only.empty() && dc.name != only)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
const std::string& s = dc.text;
|
||||
const simdjson::padded_string ps(s);
|
||||
const std::string name = dc.name;
|
||||
std::vector<std::pair<std::string, std::vector<engine>>> workloads;
|
||||
|
||||
workloads.push_back({"parse", {
|
||||
{"json_view", [&] { auto d = json_document::parse(s); g_sink = static_cast<double>(d.node_count()); }},
|
||||
{"json_view (reused)", [&] { reused.read(s); g_sink = static_cast<double>(reused.node_count()); }},
|
||||
{"yyjson", [&] { yyjson_doc* d = yyjson_read(s.data(), s.size(), 0); g_sink = static_cast<double>(yyjson_doc_get_val_count(d)); yyjson_doc_free(d); }},
|
||||
{"simdjson DOM", [&] { auto e = sj.parse(ps).value_unsafe(); g_sink = e.is_object(); }},
|
||||
{"simdjson DOM (fresh)", [&] { simdjson::dom::parser p; auto e = p.parse(ps).value_unsafe(); g_sink = e.is_object(); }},
|
||||
#if JSON_VIEW_BENCH_BOOST
|
||||
{"Boost.JSON", [&] { boost::json::monotonic_resource mr; auto v = boost::json::parse(s, &mr); g_sink = v.is_object(); }},
|
||||
#endif
|
||||
{"json::parse", [&] { json j = json::parse(s); g_sink = static_cast<double>(j.size()); }},
|
||||
}});
|
||||
workloads.push_back({"traverse", {
|
||||
{"json_view", [&] { auto d = json_document::parse(s); stats st; walk(d.root(), st); g_sink = st.num; }},
|
||||
{"json_view (reused)", [&] { reused.read(s); stats st; walk(reused.root(), st); g_sink = st.num; }},
|
||||
{"yyjson", [&] { yyjson_doc* d = yyjson_read(s.data(), s.size(), 0); stats st; walk(yyjson_doc_get_root(d), st); g_sink = st.num; yyjson_doc_free(d); }},
|
||||
{"simdjson DOM", [&] { stats st; walk(sj.parse(ps).value_unsafe(), st); g_sink = st.num; }},
|
||||
{"simdjson OD", [&] { auto d = od.iterate(ps).value_unsafe(); stats st; walk_od(d.get_value().value_unsafe(), st); g_sink = st.num; }},
|
||||
#if JSON_VIEW_BENCH_BOOST
|
||||
{"Boost.JSON", [&] { boost::json::monotonic_resource mr; auto v = boost::json::parse(s, &mr); stats st; walk(v, st); g_sink = st.num; }},
|
||||
#endif
|
||||
{"json::parse", [&] { json j = json::parse(s); stats st; walk(j, st); g_sink = st.num; }},
|
||||
}});
|
||||
workloads.push_back({"select", {
|
||||
{"json_view", [&] { auto d = json_document::parse(s); g_sink = pick(name, d.root()); }},
|
||||
{"json_view (reused)", [&] { reused.read(s); g_sink = pick(name, reused.root()); }},
|
||||
{"yyjson", [&] { yyjson_doc* d = yyjson_read(s.data(), s.size(), 0); g_sink = pick(name, yyjson_doc_get_root(d)); yyjson_doc_free(d); }},
|
||||
{"simdjson DOM", [&] { g_sink = pick(name, sj.parse(ps).value_unsafe()); }},
|
||||
{"simdjson OD", [&] { auto d = od.iterate(ps).value_unsafe(); g_sink = pick_od(name, d); }},
|
||||
{"json::parse", [&] { json j = json::parse(s); g_sink = pick(name, j); }},
|
||||
}});
|
||||
{
|
||||
// serialization of an already parsed document
|
||||
static json_document vd;
|
||||
vd.read(s);
|
||||
static yyjson_doc* yd = nullptr;
|
||||
if (yd)
|
||||
{
|
||||
yyjson_doc_free(yd);
|
||||
}
|
||||
yd = yyjson_read(s.data(), s.size(), 0);
|
||||
static simdjson::dom::parser sjd;
|
||||
static simdjson::dom::element se;
|
||||
se = sjd.parse(ps).value_unsafe();
|
||||
static json jd;
|
||||
jd = json::parse(s);
|
||||
workloads.push_back({"dump", {
|
||||
{"json_view", [&] { std::string o = vd.root().dump(); g_sink = static_cast<double>(o.size()); }},
|
||||
{"yyjson", [&] { std::size_t n = 0; char* o = yyjson_write(yd, 0, &n); g_sink = static_cast<double>(n); std::free(o); }},
|
||||
{"simdjson DOM", [&] { std::string o = simdjson::to_string(se); g_sink = static_cast<double>(o.size()); }},
|
||||
{"json::parse", [&] { std::string o = jd.dump(); g_sink = static_cast<double>(o.size()); }},
|
||||
}});
|
||||
}
|
||||
|
||||
for (auto& wl : workloads)
|
||||
{
|
||||
std::vector<double> best(wl.second.size(), 1e300);
|
||||
const int r = s.size() > 10000000 ? std::max(3, rounds / 5) : rounds;
|
||||
for (int i = 0; i < r; ++i)
|
||||
{
|
||||
for (std::size_t k = 0; k < wl.second.size(); ++k)
|
||||
{
|
||||
// an untimed call first: whatever the previous engine left to the allocator
|
||||
// (e.g. thousands of freed json nodes) is cleaned up here, not in the timing
|
||||
wl.second[k].fn();
|
||||
const auto t0 = std::chrono::steady_clock::now();
|
||||
for (int b = 0; b < dc.batch; ++b)
|
||||
{
|
||||
wl.second[k].fn();
|
||||
}
|
||||
const double ns = std::chrono::duration<double, std::nano>(std::chrono::steady_clock::now() - t0).count() / dc.batch;
|
||||
best[k] = std::min(best[k], ns);
|
||||
}
|
||||
}
|
||||
const double ref = best[0];
|
||||
std::printf("%-13s %-9s", dc.name.c_str(), wl.first.c_str());
|
||||
for (std::size_t k = 0; k < wl.second.size(); ++k)
|
||||
{
|
||||
const double us = best[k] / 1e3;
|
||||
std::printf(" %s %s%s (%.2fx)", wl.second[k].name.c_str(), us >= 100 ? "" : "", (us >= 1000 ? std::to_string(static_cast<long>(us)) + "us" : (std::to_string(us).substr(0, 5) + "us")).c_str(), best[k] / ref);
|
||||
std::fprintf(csv, "%s,%zu,%s,%s,%.1f\n", dc.name.c_str(), s.size(), wl.first.c_str(), wl.second[k].name.c_str(), best[k]);
|
||||
}
|
||||
std::printf("\n");
|
||||
std::fflush(stdout);
|
||||
}
|
||||
}
|
||||
std::fclose(csv);
|
||||
}
|
||||
@@ -1,342 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
# __ _____ _____ _____
|
||||
# __| | __| | | | JSON for Modern C++ (supporting code)
|
||||
# | | |__ | | | | | | version 3.12.0
|
||||
# |_____|_____|_____|_|___| https://github.com/nlohmann/json
|
||||
#
|
||||
# SPDX-FileCopyrightText: 2013-2026 Niels Lohmann <https://nlohmann.me>
|
||||
# SPDX-License-Identifier: MIT
|
||||
|
||||
"""Compare json_view with yyjson, simdjson, Boost.JSON, and json::parse.
|
||||
|
||||
Builds bench_view.cpp, bench_corpus.cpp, and bench_edit.cpp against the include/ directory of
|
||||
this checkout, runs them, and writes the results with everything needed to
|
||||
reproduce them (date, commit, CPU, OS, compiler, library versions, flags) to
|
||||
results/<date>-<host>.md and .csv next to this script.
|
||||
|
||||
The other libraries come from the system (--system, the default: pkg-config
|
||||
or Homebrew) or are downloaded as pinned releases and checked against their
|
||||
SHA-256 (--download). Boost.JSON is optional: without Boost headers, its
|
||||
columns are skipped, and the results say so.
|
||||
|
||||
Only the Python 3 standard library is used; a C++17 compiler is needed.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import datetime
|
||||
import hashlib
|
||||
import os
|
||||
import platform
|
||||
import re
|
||||
import shlex
|
||||
import shutil
|
||||
# runs only the compilers and benchmark binaries this script builds
|
||||
import subprocess # nosec B404
|
||||
import sys
|
||||
import tarfile
|
||||
import tempfile
|
||||
import urllib.request
|
||||
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
REPO = os.path.abspath(os.path.join(HERE, '..', '..', '..'))
|
||||
|
||||
# seconds without data after which a download fails
|
||||
DOWNLOAD_TIMEOUT = 60
|
||||
|
||||
# pinned releases for --download; the hashes are those of the archives
|
||||
#
|
||||
# The yyjson and simdjson archives are the tag archives GitHub generates
|
||||
# (archive/refs/tags/...), whose bytes GitHub does not guarantee to be stable. A SHA-256
|
||||
# mismatch for one of them means that GitHub regenerated the archive: check the upstream
|
||||
# tag, then pin the new hash.
|
||||
PINNED = {
|
||||
'yyjson': {
|
||||
'version': '0.13.0',
|
||||
'url': 'https://github.com/ibireme/yyjson/archive/refs/tags/0.13.0.tar.gz',
|
||||
'sha256': '34e0f62a2bc11ab20d601e8ca1cc2b2079503aa45119a19133d89d19b94a0fae',
|
||||
'dir': 'yyjson-0.13.0',
|
||||
},
|
||||
'simdjson': {
|
||||
'version': '4.6.11',
|
||||
'url': 'https://github.com/simdjson/simdjson/archive/refs/tags/v4.6.11.tar.gz',
|
||||
'sha256': '61d948fc24f0d793829ad658058e7597d064988a89b4607ea02e401a82df98ff',
|
||||
'dir': 'simdjson-4.6.11',
|
||||
},
|
||||
'boost': {
|
||||
'version': '1.92.0',
|
||||
'url': 'https://archives.boost.io/release/1.92.0/source/boost_1_92_0.tar.gz',
|
||||
'sha256': 'c4a3b310ddd2472416e091067166b0713be97c63f38c212c484ada022fd296ce',
|
||||
'dir': 'boost_1_92_0',
|
||||
},
|
||||
}
|
||||
|
||||
# the documents of bench_view.cpp, relative to the json_test_data directory
|
||||
DEFAULT_CORPUS = [
|
||||
'nativejson-benchmark/twitter.json',
|
||||
'nativejson-benchmark/citm_catalog.json',
|
||||
'nativejson-benchmark/canada.json',
|
||||
'jeopardy/jeopardy.json',
|
||||
]
|
||||
|
||||
|
||||
def run(cmd, **kwargs):
|
||||
print('+ ' + ' '.join(shlex.quote(c) for c in cmd), flush=True)
|
||||
# cmd is an argument list built by this script, never a shell string
|
||||
return subprocess.run(cmd, check=True, **kwargs) # nosec B603
|
||||
|
||||
|
||||
def output(cmd):
|
||||
try:
|
||||
# cmd is an argument list built by this script, never a shell string
|
||||
return subprocess.run(cmd, check=True, capture_output=True, text=True).stdout.strip() # nosec B603
|
||||
except (OSError, subprocess.CalledProcessError):
|
||||
return ''
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# libraries
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class Library:
|
||||
"""include directories, sources to compile, and linker flags of a library"""
|
||||
|
||||
def __init__(self, name, include=None, sources=None, link=None, version=''):
|
||||
self.name = name
|
||||
self.include = include or []
|
||||
self.sources = sources or []
|
||||
self.link = link or []
|
||||
self.version = version
|
||||
|
||||
|
||||
def header_version(path, pattern):
|
||||
try:
|
||||
with open(path, encoding='utf-8', errors='replace') as f:
|
||||
m = re.search(pattern, f.read())
|
||||
return m.group(1) if m else ''
|
||||
except OSError:
|
||||
return ''
|
||||
|
||||
|
||||
def library_version(name, include_dirs):
|
||||
patterns = {
|
||||
'yyjson': ('yyjson.h', r'#define\s+YYJSON_VERSION_STRING\s+"([^"]+)"'),
|
||||
'simdjson': ('simdjson.h', r'#define\s+SIMDJSON_VERSION\s+"?([0-9.]+)"?'),
|
||||
'boost': (os.path.join('boost', 'version.hpp'), r'#define\s+BOOST_LIB_VERSION\s+"([^"]+)"'),
|
||||
}
|
||||
header, pattern = patterns[name]
|
||||
for d in include_dirs:
|
||||
v = header_version(os.path.join(d, header), pattern)
|
||||
if v:
|
||||
return v.replace('_', '.')
|
||||
return ''
|
||||
|
||||
|
||||
def system_library(name):
|
||||
"""a library found with pkg-config or Homebrew, or None"""
|
||||
flags = output(['pkg-config', '--cflags', '--libs', name]).split()
|
||||
if flags:
|
||||
include = [f[2:] for f in flags if f.startswith('-I')]
|
||||
link = [f for f in flags if f.startswith('-L') or f.startswith('-l')]
|
||||
libdirs = [f[2:] for f in link if f.startswith('-L')]
|
||||
link += ['-Wl,-rpath,' + d for d in libdirs]
|
||||
return Library(name, include, [], link, library_version(name, include))
|
||||
prefix = output(['brew', '--prefix', name]) if shutil.which('brew') else ''
|
||||
if prefix and os.path.isdir(os.path.join(prefix, 'include')):
|
||||
include = [os.path.join(prefix, 'include')]
|
||||
link = []
|
||||
if name != 'boost':
|
||||
lib = os.path.join(prefix, 'lib')
|
||||
link = ['-L' + lib, '-l' + name, '-Wl,-rpath,' + lib]
|
||||
return Library(name, include, [], link, library_version(name, include))
|
||||
if name == 'boost':
|
||||
for d in ['/usr/include', '/usr/local/include']:
|
||||
if os.path.isfile(os.path.join(d, 'boost', 'json.hpp')):
|
||||
return Library(name, [d], [], [], library_version(name, [d]))
|
||||
return None
|
||||
|
||||
|
||||
def sha256_file(path):
|
||||
"""SHA-256 of a file, read in chunks (the Boost archive is more than 100 MB)"""
|
||||
h = hashlib.sha256()
|
||||
with open(path, 'rb') as f:
|
||||
for chunk in iter(lambda: f.read(1 << 20), b''):
|
||||
h.update(chunk)
|
||||
return h.hexdigest()
|
||||
|
||||
|
||||
def download_library(name, work):
|
||||
"""a pinned release, downloaded and checked, or an error"""
|
||||
pin = PINNED[name]
|
||||
archive = os.path.join(work, 'download', os.path.basename(pin['url']))
|
||||
os.makedirs(os.path.dirname(archive), exist_ok=True)
|
||||
if not os.path.isfile(archive):
|
||||
print(f'downloading {pin["url"]}', flush=True)
|
||||
# the URLs are the https constants in PINNED, and the SHA-256 is checked below
|
||||
# (into a .part file first, so that an interrupted download is not kept)
|
||||
with urllib.request.urlopen(pin['url'], timeout=DOWNLOAD_TIMEOUT) as response: # nosec B310
|
||||
with open(archive + '.part', 'wb') as f:
|
||||
shutil.copyfileobj(response, f)
|
||||
os.replace(archive + '.part', archive)
|
||||
digest = sha256_file(archive)
|
||||
if digest != pin['sha256']:
|
||||
os.remove(archive) # downloaded again by the next run
|
||||
sys.exit(f'error: SHA-256 of {archive} is {digest}, expected {pin["sha256"]} (removed)')
|
||||
src = os.path.join(work, 'download', pin['dir'])
|
||||
if not os.path.isdir(src):
|
||||
# extracted into a temporary directory first, so that an interrupted extraction
|
||||
# is not mistaken for a complete one
|
||||
tmp = tempfile.mkdtemp(prefix=pin['dir'] + '.part-', dir=os.path.join(work, 'download'))
|
||||
try:
|
||||
with tarfile.open(archive) as t:
|
||||
# (the 'data' filter rejects links and paths outside the target where Python has it)
|
||||
kwargs = {'filter': 'data'} if hasattr(tarfile, 'data_filter') else {}
|
||||
t.extractall(tmp, **kwargs) # noqa: S202 (checked archive) # nosec B202
|
||||
os.rename(os.path.join(tmp, pin['dir']), src)
|
||||
finally:
|
||||
shutil.rmtree(tmp, ignore_errors=True)
|
||||
if name == 'yyjson':
|
||||
return Library(name, [os.path.join(src, 'src')], [os.path.join(src, 'src', 'yyjson.c')], [], pin['version'])
|
||||
if name == 'simdjson':
|
||||
single = os.path.join(src, 'singleheader')
|
||||
return Library(name, [single], [os.path.join(single, 'simdjson.cpp')], [], pin['version'])
|
||||
return Library(name, [src], [], [], pin['version'])
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# machine description
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def cpu_model():
|
||||
if sys.platform == 'darwin':
|
||||
return output(['sysctl', '-n', 'machdep.cpu.brand_string'])
|
||||
try:
|
||||
with open('/proc/cpuinfo', encoding='utf-8') as f:
|
||||
for line in f:
|
||||
if line.startswith('model name') or line.startswith('Model'):
|
||||
return line.split(':', 1)[1].strip()
|
||||
except OSError:
|
||||
pass
|
||||
# (AArch64 Linux: /proc/cpuinfo has no model name, lscpu knows it)
|
||||
for line in output(['lscpu']).splitlines():
|
||||
if line.startswith('Model name:'):
|
||||
return line.split(':', 1)[1].strip()
|
||||
return platform.processor() or platform.machine()
|
||||
|
||||
|
||||
def git_commit():
|
||||
commit = output(['git', '-C', REPO, 'rev-parse', '--short=12', 'HEAD'])
|
||||
dirty = output(['git', '-C', REPO, 'status', '--porcelain', '--untracked-files=no'])
|
||||
return commit + (' (with local changes)' if dirty else '')
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# main
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
||||
ap.add_argument('--data', required=True, help='json_test_data directory (with nativejson-benchmark/ and jeopardy/)')
|
||||
ap.add_argument('--download', action='store_true', help='use pinned downloads instead of system libraries')
|
||||
ap.add_argument('--no-boost', action='store_true', help='skip Boost.JSON')
|
||||
ap.add_argument('--native', action='store_true', help='compile for this CPU (-march=native / -mcpu=native)')
|
||||
ap.add_argument('--rounds', type=int, default=30, help='rounds of bench_view (default: 30)')
|
||||
ap.add_argument('--corpus', nargs='*', default=[], help='more files for bench_corpus')
|
||||
ap.add_argument('--build-dir', default=os.path.join(HERE, 'build'), help='where to build (default: build/ next to this script)')
|
||||
args = ap.parse_args()
|
||||
# the benchmarks run in the build directory: make the paths absolute
|
||||
args.data = os.path.abspath(args.data)
|
||||
args.corpus = [os.path.abspath(f) for f in args.corpus]
|
||||
args.build_dir = os.path.abspath(args.build_dir)
|
||||
|
||||
# (CXX and CC may contain arguments, e.g. 'ccache g++')
|
||||
cxx = shlex.split(os.environ.get('CXX', 'c++'))
|
||||
cc = shlex.split(os.environ.get('CC', 'cc'))
|
||||
os.makedirs(args.build_dir, exist_ok=True)
|
||||
|
||||
libs = {}
|
||||
for name in ['yyjson', 'simdjson', 'boost']:
|
||||
if name == 'boost' and args.no_boost:
|
||||
continue
|
||||
lib = download_library(name, args.build_dir) if args.download else system_library(name)
|
||||
if lib is None and name != 'boost':
|
||||
sys.exit(f'error: {name} not found; install it, or use --download')
|
||||
if lib is not None:
|
||||
libs[name] = lib
|
||||
with_boost = 'boost' in libs
|
||||
if not with_boost:
|
||||
print('Boost.JSON not found: its columns are skipped', flush=True)
|
||||
|
||||
flags = ['-std=c++17', '-O3', '-DNDEBUG', f'-DJSON_VIEW_BENCH_BOOST={1 if with_boost else 0}']
|
||||
if args.native:
|
||||
flags.append('-mcpu=native' if platform.machine().lower() in ('arm64', 'aarch64') else '-march=native')
|
||||
include = ['-I' + os.path.join(REPO, 'include')] + ['-I' + d for lib in libs.values() for d in lib.include]
|
||||
link = [f for lib in libs.values() for f in lib.link]
|
||||
|
||||
# C sources of downloaded libraries are compiled once
|
||||
objects = []
|
||||
for lib in libs.values():
|
||||
for src in lib.sources:
|
||||
obj = os.path.join(args.build_dir, os.path.basename(src) + '.o')
|
||||
is_c = src.endswith('.c')
|
||||
run((cc if is_c else cxx) + ([] if is_c else ['-std=c++17']) + ['-O3', '-DNDEBUG', '-c', src, '-o', obj]
|
||||
+ ['-I' + d for d in lib.include])
|
||||
objects.append(obj)
|
||||
|
||||
binaries = {}
|
||||
for bench in ['bench_view', 'bench_corpus', 'bench_edit']:
|
||||
exe = os.path.join(args.build_dir, bench)
|
||||
run(cxx + flags + include + [os.path.join(HERE, bench + '.cpp')] + objects + link + ['-o', exe])
|
||||
binaries[bench] = exe
|
||||
|
||||
# run: bench_view on its documents, bench_corpus on those and the given files
|
||||
corpus = [os.path.join(args.data, f) for f in DEFAULT_CORPUS] + args.corpus
|
||||
outputs = {}
|
||||
outputs['bench_view'] = run([binaries['bench_view'], args.data, str(args.rounds)], cwd=args.build_dir,
|
||||
capture_output=True, text=True).stdout
|
||||
outputs['bench_corpus'] = run([binaries['bench_corpus']] + corpus, cwd=args.build_dir,
|
||||
capture_output=True, text=True).stdout
|
||||
outputs['bench_edit'] = run([binaries['bench_edit'], args.data, str(max(1, args.rounds // 2))], cwd=args.build_dir,
|
||||
capture_output=True, text=True).stdout
|
||||
for name, text in outputs.items():
|
||||
print(text)
|
||||
|
||||
# results with their metadata
|
||||
now = datetime.datetime.now()
|
||||
host = re.sub(r'[^A-Za-z0-9-]+', '-', platform.node().split('.')[0]) or 'host'
|
||||
stem = os.path.join(HERE, 'results', f'{now:%Y-%m-%d}-{host}')
|
||||
os.makedirs(os.path.dirname(stem), exist_ok=True)
|
||||
cxx_version = output(cxx + ['--version'])
|
||||
compiler_version = cxx_version.splitlines()[0] if cxx_version else ' '.join(cxx)
|
||||
meta = [
|
||||
('date', f'{now:%Y-%m-%d %H:%M}'),
|
||||
('commit', git_commit()),
|
||||
('CPU', cpu_model()),
|
||||
('OS', f'{platform.system()} {platform.release()} ({platform.machine()})'),
|
||||
('compiler', compiler_version),
|
||||
('flags', ' '.join(flags)),
|
||||
('yyjson', libs['yyjson'].version),
|
||||
('simdjson', libs['simdjson'].version),
|
||||
('Boost.JSON', libs['boost'].version if with_boost else 'skipped (not found)'),
|
||||
('libraries from', 'pinned downloads' if args.download else 'the system'),
|
||||
('rounds', str(args.rounds)),
|
||||
]
|
||||
with open(stem + '.md', 'w', encoding='utf-8') as f:
|
||||
f.write(f'# json_view comparison, {now:%Y-%m-%d}\n\n')
|
||||
f.write('Generated by `tests/benchmarks/json_view/compare.py`; best of the interleaved rounds.\n\n')
|
||||
f.write('| | |\n|---|---|\n')
|
||||
for key, value in meta:
|
||||
f.write(f'| {key} | {value} |\n')
|
||||
for name, text in outputs.items():
|
||||
f.write(f'\n## {name}\n\n```\n{text.rstrip()}\n```\n')
|
||||
with open(stem + '.csv', 'w', encoding='utf-8') as out:
|
||||
out.write(''.join(f'# {key}: {value}\n' for key, value in meta))
|
||||
for name in ['bench_view', 'bench_corpus', 'bench_edit']:
|
||||
path = os.path.join(args.build_dir, name + '.csv')
|
||||
if os.path.isfile(path):
|
||||
with open(path, encoding='utf-8') as f:
|
||||
out.write(f'# {name}\n' + f.read())
|
||||
print(f'results: {stem}.md, {stem}.csv')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -9,15 +9,12 @@
|
||||
#include <benchmark/benchmark.h>
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <deque>
|
||||
#include <fstream>
|
||||
#include <map>
|
||||
#include <numeric>
|
||||
#include <set>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <test_data.hpp>
|
||||
|
||||
@@ -713,584 +710,6 @@ static void FromCborChunkedString(benchmark::State& state, const std::size_t chu
|
||||
|
||||
BENCHMARK_CAPTURE(FromCborChunkedString, 10000 chunks, 10000);
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
// SAX-parse JSON from string
|
||||
//
|
||||
// sax_parse() with a handler that does next to nothing runs the lexer and
|
||||
// parser without building values, so the gap to ParseString above is the cost
|
||||
// of building the DOM.
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
static std::string read_file(const char* filename)
|
||||
{
|
||||
std::ifstream f(filename);
|
||||
return std::string((std::istreambuf_iterator<char>(f)), std::istreambuf_iterator<char>());
|
||||
}
|
||||
|
||||
static json load_json(const char* filename)
|
||||
{
|
||||
return json::parse(read_file(filename));
|
||||
}
|
||||
|
||||
/// SAX handler that only counts the events, so that the handler costs next to nothing
|
||||
class counting_sax : public nlohmann::json_sax<json>
|
||||
{
|
||||
public:
|
||||
std::size_t events = 0;
|
||||
|
||||
bool null() override
|
||||
{
|
||||
return count();
|
||||
}
|
||||
bool boolean(bool /*val*/) override
|
||||
{
|
||||
return count();
|
||||
}
|
||||
bool number_integer(number_integer_t /*val*/) override
|
||||
{
|
||||
return count();
|
||||
}
|
||||
bool number_unsigned(number_unsigned_t /*val*/) override
|
||||
{
|
||||
return count();
|
||||
}
|
||||
bool number_float(number_float_t /*val*/, const string_t& /*s*/) override
|
||||
{
|
||||
return count();
|
||||
}
|
||||
bool string(string_t& /*val*/) override
|
||||
{
|
||||
return count();
|
||||
}
|
||||
bool binary(binary_t& /*val*/) override
|
||||
{
|
||||
return count();
|
||||
}
|
||||
bool start_object(std::size_t /*elements*/) override
|
||||
{
|
||||
return count();
|
||||
}
|
||||
bool key(string_t& /*val*/) override
|
||||
{
|
||||
return count();
|
||||
}
|
||||
bool end_object() override
|
||||
{
|
||||
return count();
|
||||
}
|
||||
bool start_array(std::size_t /*elements*/) override
|
||||
{
|
||||
return count();
|
||||
}
|
||||
bool end_array() override
|
||||
{
|
||||
return count();
|
||||
}
|
||||
bool parse_error(std::size_t /*position*/, const std::string& /*last_token*/, const json::exception& /*ex*/) override
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
private:
|
||||
bool count()
|
||||
{
|
||||
++events;
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
static void SaxParse(benchmark::State& state, const char* filename)
|
||||
{
|
||||
const std::string str = read_file(filename);
|
||||
|
||||
for (auto _ : state)
|
||||
{
|
||||
counting_sax sax;
|
||||
bool result = json::sax_parse(str, &sax);
|
||||
benchmark::DoNotOptimize(result);
|
||||
benchmark::DoNotOptimize(sax.events);
|
||||
}
|
||||
|
||||
state.SetBytesProcessed(state.iterations() * str.size());
|
||||
}
|
||||
BENCHMARK_CAPTURE(SaxParse, canada, TEST_DATA_DIRECTORY "/nativejson-benchmark/canada.json");
|
||||
BENCHMARK_CAPTURE(SaxParse, citm_catalog, TEST_DATA_DIRECTORY "/nativejson-benchmark/citm_catalog.json");
|
||||
BENCHMARK_CAPTURE(SaxParse, twitter, TEST_DATA_DIRECTORY "/nativejson-benchmark/twitter.json");
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
// work with values: copy, destroy, compare, iterate
|
||||
//
|
||||
// The parsed test files stand in for a document an application keeps around.
|
||||
// Each benchmark times one whole-document operation; setup and cleanup run
|
||||
// outside the timed section.
|
||||
//
|
||||
// A copy is always destroyed again, and how a document is freed shapes the
|
||||
// heap the next copy allocates from: a destructor that frees quickly can leave
|
||||
// work to the allocator's next calls. Copy, CopyAssign and CopyRing therefore
|
||||
// time the copy together with the destruction of a copy; CopyOnly and
|
||||
// DestroyOnly time one half each and are meant for telling the halves apart,
|
||||
// not for comparing versions.
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
/// copy a document and destroy the copy again, as a temporary copy is used
|
||||
static void Copy(benchmark::State& state, const char* filename)
|
||||
{
|
||||
const json j = load_json(filename);
|
||||
|
||||
for (auto _ : state)
|
||||
{
|
||||
json copy = j;
|
||||
benchmark::DoNotOptimize(copy);
|
||||
}
|
||||
}
|
||||
BENCHMARK_CAPTURE(Copy, canada, TEST_DATA_DIRECTORY "/nativejson-benchmark/canada.json");
|
||||
BENCHMARK_CAPTURE(Copy, citm_catalog, TEST_DATA_DIRECTORY "/nativejson-benchmark/citm_catalog.json");
|
||||
BENCHMARK_CAPTURE(Copy, twitter, TEST_DATA_DIRECTORY "/nativejson-benchmark/twitter.json");
|
||||
|
||||
/// copy-assign a document over an earlier copy of it, which is freed in the process
|
||||
static void CopyAssign(benchmark::State& state, const char* filename)
|
||||
{
|
||||
const json j = load_json(filename);
|
||||
json target = j;
|
||||
|
||||
for (auto _ : state)
|
||||
{
|
||||
target = j;
|
||||
benchmark::DoNotOptimize(target);
|
||||
}
|
||||
}
|
||||
BENCHMARK_CAPTURE(CopyAssign, canada, TEST_DATA_DIRECTORY "/nativejson-benchmark/canada.json");
|
||||
BENCHMARK_CAPTURE(CopyAssign, citm_catalog, TEST_DATA_DIRECTORY "/nativejson-benchmark/citm_catalog.json");
|
||||
BENCHMARK_CAPTURE(CopyAssign, twitter, TEST_DATA_DIRECTORY "/nativejson-benchmark/twitter.json");
|
||||
|
||||
/// keep eight copies alive and replace the oldest one, as a cache or an undo
|
||||
/// history does, so that the copy is not made from memory just freed by an
|
||||
/// identical document
|
||||
static void CopyRing(benchmark::State& state, const char* filename)
|
||||
{
|
||||
const json j = load_json(filename);
|
||||
std::vector<json> ring(8, j);
|
||||
std::size_t oldest = 0;
|
||||
|
||||
for (auto _ : state)
|
||||
{
|
||||
ring[oldest] = j;
|
||||
benchmark::DoNotOptimize(ring[oldest]);
|
||||
oldest = (oldest + 1) % ring.size();
|
||||
}
|
||||
}
|
||||
BENCHMARK_CAPTURE(CopyRing, canada, TEST_DATA_DIRECTORY "/nativejson-benchmark/canada.json");
|
||||
BENCHMARK_CAPTURE(CopyRing, citm_catalog, TEST_DATA_DIRECTORY "/nativejson-benchmark/citm_catalog.json");
|
||||
BENCHMARK_CAPTURE(CopyRing, twitter, TEST_DATA_DIRECTORY "/nativejson-benchmark/twitter.json");
|
||||
|
||||
/// only the copy; the copy is destroyed with the timer paused (diagnostic)
|
||||
static void CopyOnly(benchmark::State& state, const char* filename)
|
||||
{
|
||||
const json j = load_json(filename);
|
||||
|
||||
for (auto _ : state)
|
||||
{
|
||||
auto* copy = new json(j);
|
||||
|
||||
state.PauseTiming();
|
||||
delete copy;
|
||||
state.ResumeTiming();
|
||||
}
|
||||
}
|
||||
BENCHMARK_CAPTURE(CopyOnly, canada, TEST_DATA_DIRECTORY "/nativejson-benchmark/canada.json");
|
||||
BENCHMARK_CAPTURE(CopyOnly, citm_catalog, TEST_DATA_DIRECTORY "/nativejson-benchmark/citm_catalog.json");
|
||||
BENCHMARK_CAPTURE(CopyOnly, twitter, TEST_DATA_DIRECTORY "/nativejson-benchmark/twitter.json");
|
||||
|
||||
/// only the destruction; the copy is made with the timer paused (diagnostic)
|
||||
static void DestroyOnly(benchmark::State& state, const char* filename)
|
||||
{
|
||||
const json j = load_json(filename);
|
||||
|
||||
for (auto _ : state)
|
||||
{
|
||||
state.PauseTiming();
|
||||
auto* copy = new json(j);
|
||||
state.ResumeTiming();
|
||||
|
||||
delete copy;
|
||||
}
|
||||
}
|
||||
BENCHMARK_CAPTURE(DestroyOnly, canada, TEST_DATA_DIRECTORY "/nativejson-benchmark/canada.json");
|
||||
BENCHMARK_CAPTURE(DestroyOnly, citm_catalog, TEST_DATA_DIRECTORY "/nativejson-benchmark/citm_catalog.json");
|
||||
BENCHMARK_CAPTURE(DestroyOnly, twitter, TEST_DATA_DIRECTORY "/nativejson-benchmark/twitter.json");
|
||||
|
||||
static void Equal(benchmark::State& state, const char* filename)
|
||||
{
|
||||
// two separate values, so that the comparison cannot stop at identical addresses
|
||||
const json j = load_json(filename);
|
||||
const json copy = j;
|
||||
|
||||
for (auto _ : state)
|
||||
{
|
||||
bool result = (j == copy);
|
||||
benchmark::DoNotOptimize(result);
|
||||
}
|
||||
}
|
||||
BENCHMARK_CAPTURE(Equal, canada, TEST_DATA_DIRECTORY "/nativejson-benchmark/canada.json");
|
||||
BENCHMARK_CAPTURE(Equal, citm_catalog, TEST_DATA_DIRECTORY "/nativejson-benchmark/citm_catalog.json");
|
||||
BENCHMARK_CAPTURE(Equal, twitter, TEST_DATA_DIRECTORY "/nativejson-benchmark/twitter.json");
|
||||
|
||||
/// visit every value with range-based for loops, as client code walks a document
|
||||
static std::size_t count_values(const json& j)
|
||||
{
|
||||
std::size_t result = 1;
|
||||
if (j.is_structured())
|
||||
{
|
||||
for (const auto& element : j)
|
||||
{
|
||||
result += count_values(element);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
static void Iterate(benchmark::State& state, const char* filename)
|
||||
{
|
||||
const json j = load_json(filename);
|
||||
|
||||
for (auto _ : state)
|
||||
{
|
||||
std::size_t result = count_values(j);
|
||||
benchmark::DoNotOptimize(result);
|
||||
}
|
||||
|
||||
state.SetItemsProcessed(state.iterations() * static_cast<std::int64_t>(count_values(j)));
|
||||
}
|
||||
BENCHMARK_CAPTURE(Iterate, canada, TEST_DATA_DIRECTORY "/nativejson-benchmark/canada.json");
|
||||
BENCHMARK_CAPTURE(Iterate, citm_catalog, TEST_DATA_DIRECTORY "/nativejson-benchmark/citm_catalog.json");
|
||||
BENCHMARK_CAPTURE(Iterate, twitter, TEST_DATA_DIRECTORY "/nativejson-benchmark/twitter.json");
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
// flatten and unflatten
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
static void Flatten(benchmark::State& state, const char* filename)
|
||||
{
|
||||
const json j = load_json(filename);
|
||||
|
||||
for (auto _ : state)
|
||||
{
|
||||
json flat = j.flatten();
|
||||
benchmark::DoNotOptimize(flat);
|
||||
}
|
||||
}
|
||||
BENCHMARK_CAPTURE(Flatten, canada, TEST_DATA_DIRECTORY "/nativejson-benchmark/canada.json");
|
||||
BENCHMARK_CAPTURE(Flatten, citm_catalog, TEST_DATA_DIRECTORY "/nativejson-benchmark/citm_catalog.json");
|
||||
BENCHMARK_CAPTURE(Flatten, twitter, TEST_DATA_DIRECTORY "/nativejson-benchmark/twitter.json");
|
||||
|
||||
// ordered_json objects look keys up linearly, which flatten() used to do for
|
||||
// every value; canada (330,000 values in one flat object) is left out, as it
|
||||
// would take minutes with such versions
|
||||
static void FlattenOrdered(benchmark::State& state, const char* filename)
|
||||
{
|
||||
const nlohmann::ordered_json j = nlohmann::ordered_json::parse(read_file(filename));
|
||||
|
||||
for (auto _ : state)
|
||||
{
|
||||
nlohmann::ordered_json flat = j.flatten();
|
||||
benchmark::DoNotOptimize(flat);
|
||||
}
|
||||
}
|
||||
BENCHMARK_CAPTURE(FlattenOrdered, citm_catalog, TEST_DATA_DIRECTORY "/nativejson-benchmark/citm_catalog.json");
|
||||
BENCHMARK_CAPTURE(FlattenOrdered, twitter, TEST_DATA_DIRECTORY "/nativejson-benchmark/twitter.json");
|
||||
|
||||
static void Unflatten(benchmark::State& state, const char* filename)
|
||||
{
|
||||
const json flat = load_json(filename).flatten();
|
||||
|
||||
for (auto _ : state)
|
||||
{
|
||||
json j = flat.unflatten();
|
||||
benchmark::DoNotOptimize(j);
|
||||
}
|
||||
}
|
||||
BENCHMARK_CAPTURE(Unflatten, canada, TEST_DATA_DIRECTORY "/nativejson-benchmark/canada.json");
|
||||
BENCHMARK_CAPTURE(Unflatten, citm_catalog, TEST_DATA_DIRECTORY "/nativejson-benchmark/citm_catalog.json");
|
||||
BENCHMARK_CAPTURE(Unflatten, twitter, TEST_DATA_DIRECTORY "/nativejson-benchmark/twitter.json");
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
// diff and patch
|
||||
//
|
||||
// The target is the document with every eighth scalar changed, so the patch
|
||||
// has one "replace" operation per changed value and the structure stays the
|
||||
// same.
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
static void change_every_eighth_scalar(json& j, std::size_t& counter)
|
||||
{
|
||||
if (j.is_structured())
|
||||
{
|
||||
for (auto& element : j)
|
||||
{
|
||||
change_every_eighth_scalar(element, counter);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (++counter % 8 != 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
switch (j.type())
|
||||
{
|
||||
case json::value_t::number_integer:
|
||||
j = j.get<json::number_integer_t>() + 1;
|
||||
break;
|
||||
case json::value_t::number_unsigned:
|
||||
j = j.get<json::number_unsigned_t>() + 1;
|
||||
break;
|
||||
case json::value_t::number_float:
|
||||
j = j.get<json::number_float_t>() + 0.5;
|
||||
break;
|
||||
case json::value_t::string:
|
||||
j = j.get<std::string>() + "!";
|
||||
break;
|
||||
case json::value_t::boolean:
|
||||
j = !j.get<bool>();
|
||||
break;
|
||||
case json::value_t::null:
|
||||
case json::value_t::object:
|
||||
case json::value_t::array:
|
||||
case json::value_t::binary:
|
||||
case json::value_t::discarded:
|
||||
default:
|
||||
j = 0;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
static json changed_copy(const json& j)
|
||||
{
|
||||
json result = j;
|
||||
std::size_t counter = 0;
|
||||
change_every_eighth_scalar(result, counter);
|
||||
return result;
|
||||
}
|
||||
|
||||
static void Diff(benchmark::State& state, const char* filename)
|
||||
{
|
||||
const json source = load_json(filename);
|
||||
const json target = changed_copy(source);
|
||||
|
||||
for (auto _ : state)
|
||||
{
|
||||
json patch = json::diff(source, target);
|
||||
benchmark::DoNotOptimize(patch);
|
||||
}
|
||||
|
||||
state.SetItemsProcessed(state.iterations() * static_cast<std::int64_t>(json::diff(source, target).size()));
|
||||
}
|
||||
BENCHMARK_CAPTURE(Diff, canada, TEST_DATA_DIRECTORY "/nativejson-benchmark/canada.json");
|
||||
BENCHMARK_CAPTURE(Diff, citm_catalog, TEST_DATA_DIRECTORY "/nativejson-benchmark/citm_catalog.json");
|
||||
BENCHMARK_CAPTURE(Diff, twitter, TEST_DATA_DIRECTORY "/nativejson-benchmark/twitter.json");
|
||||
|
||||
static void Patch(benchmark::State& state, const char* filename)
|
||||
{
|
||||
const json source = load_json(filename);
|
||||
const json patch = json::diff(source, changed_copy(source));
|
||||
|
||||
for (auto _ : state)
|
||||
{
|
||||
json result = source.patch(patch);
|
||||
benchmark::DoNotOptimize(result);
|
||||
}
|
||||
|
||||
state.SetItemsProcessed(state.iterations() * static_cast<std::int64_t>(patch.size()));
|
||||
}
|
||||
BENCHMARK_CAPTURE(Patch, canada, TEST_DATA_DIRECTORY "/nativejson-benchmark/canada.json");
|
||||
BENCHMARK_CAPTURE(Patch, citm_catalog, TEST_DATA_DIRECTORY "/nativejson-benchmark/citm_catalog.json");
|
||||
BENCHMARK_CAPTURE(Patch, twitter, TEST_DATA_DIRECTORY "/nativejson-benchmark/twitter.json");
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
// JSON Pointer
|
||||
//
|
||||
// The pointers are those of every scalar in the document, i.e., the keys of
|
||||
// its flattened form.
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
static std::vector<std::string> pointer_strings(const json& j)
|
||||
{
|
||||
std::vector<std::string> result;
|
||||
const json flat = j.flatten();
|
||||
result.reserve(flat.size());
|
||||
for (auto it = flat.begin(); it != flat.end(); ++it)
|
||||
{
|
||||
result.push_back(it.key());
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
static std::vector<json::json_pointer> pointers(const json& j)
|
||||
{
|
||||
const std::vector<std::string> strings = pointer_strings(j);
|
||||
std::vector<json::json_pointer> result;
|
||||
result.reserve(strings.size());
|
||||
for (const auto& s : strings)
|
||||
{
|
||||
result.emplace_back(s);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
static void JsonPointerParse(benchmark::State& state, const char* filename)
|
||||
{
|
||||
const std::vector<std::string> strings = pointer_strings(load_json(filename));
|
||||
|
||||
for (auto _ : state)
|
||||
{
|
||||
for (const auto& s : strings)
|
||||
{
|
||||
json::json_pointer ptr(s);
|
||||
benchmark::DoNotOptimize(ptr);
|
||||
}
|
||||
}
|
||||
|
||||
state.SetItemsProcessed(state.iterations() * static_cast<std::int64_t>(strings.size()));
|
||||
}
|
||||
BENCHMARK_CAPTURE(JsonPointerParse, citm_catalog, TEST_DATA_DIRECTORY "/nativejson-benchmark/citm_catalog.json");
|
||||
BENCHMARK_CAPTURE(JsonPointerParse, twitter, TEST_DATA_DIRECTORY "/nativejson-benchmark/twitter.json");
|
||||
|
||||
static void JsonPointerToString(benchmark::State& state, const char* filename)
|
||||
{
|
||||
const std::vector<json::json_pointer> ptrs = pointers(load_json(filename));
|
||||
|
||||
for (auto _ : state)
|
||||
{
|
||||
for (const auto& ptr : ptrs)
|
||||
{
|
||||
std::string s = ptr.to_string();
|
||||
benchmark::DoNotOptimize(s);
|
||||
}
|
||||
}
|
||||
|
||||
state.SetItemsProcessed(state.iterations() * static_cast<std::int64_t>(ptrs.size()));
|
||||
}
|
||||
BENCHMARK_CAPTURE(JsonPointerToString, citm_catalog, TEST_DATA_DIRECTORY "/nativejson-benchmark/citm_catalog.json");
|
||||
BENCHMARK_CAPTURE(JsonPointerToString, twitter, TEST_DATA_DIRECTORY "/nativejson-benchmark/twitter.json");
|
||||
|
||||
static void JsonPointerAt(benchmark::State& state, const char* filename)
|
||||
{
|
||||
const json j = load_json(filename);
|
||||
const std::vector<json::json_pointer> ptrs = pointers(j);
|
||||
|
||||
for (auto _ : state)
|
||||
{
|
||||
for (const auto& ptr : ptrs)
|
||||
{
|
||||
const json& value = j.at(ptr);
|
||||
benchmark::DoNotOptimize(&value);
|
||||
}
|
||||
}
|
||||
|
||||
state.SetItemsProcessed(state.iterations() * static_cast<std::int64_t>(ptrs.size()));
|
||||
}
|
||||
BENCHMARK_CAPTURE(JsonPointerAt, citm_catalog, TEST_DATA_DIRECTORY "/nativejson-benchmark/citm_catalog.json");
|
||||
BENCHMARK_CAPTURE(JsonPointerAt, twitter, TEST_DATA_DIRECTORY "/nativejson-benchmark/twitter.json");
|
||||
|
||||
static void JsonPointerContains(benchmark::State& state, const char* filename)
|
||||
{
|
||||
const json j = load_json(filename);
|
||||
const std::vector<json::json_pointer> ptrs = pointers(j);
|
||||
|
||||
for (auto _ : state)
|
||||
{
|
||||
for (const auto& ptr : ptrs)
|
||||
{
|
||||
bool result = j.contains(ptr);
|
||||
benchmark::DoNotOptimize(result);
|
||||
}
|
||||
}
|
||||
|
||||
state.SetItemsProcessed(state.iterations() * static_cast<std::int64_t>(ptrs.size()));
|
||||
}
|
||||
BENCHMARK_CAPTURE(JsonPointerContains, citm_catalog, TEST_DATA_DIRECTORY "/nativejson-benchmark/citm_catalog.json");
|
||||
BENCHMARK_CAPTURE(JsonPointerContains, twitter, TEST_DATA_DIRECTORY "/nativejson-benchmark/twitter.json");
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
// conversions from and to C++ containers
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
static void GetVectorInt(benchmark::State& state, const char* filename)
|
||||
{
|
||||
const json j = load_json(filename);
|
||||
|
||||
for (auto _ : state)
|
||||
{
|
||||
auto v = j.get<std::vector<std::int64_t>>();
|
||||
benchmark::DoNotOptimize(v);
|
||||
}
|
||||
|
||||
state.SetItemsProcessed(state.iterations() * static_cast<std::int64_t>(j.size()));
|
||||
}
|
||||
BENCHMARK_CAPTURE(GetVectorInt, signed_ints, TEST_DATA_DIRECTORY "/regression/signed_ints.json");
|
||||
|
||||
static void GetVectorDouble(benchmark::State& state, const char* filename)
|
||||
{
|
||||
const json j = load_json(filename);
|
||||
|
||||
for (auto _ : state)
|
||||
{
|
||||
auto v = j.get<std::vector<double>>();
|
||||
benchmark::DoNotOptimize(v);
|
||||
}
|
||||
|
||||
state.SetItemsProcessed(state.iterations() * static_cast<std::int64_t>(j.size()));
|
||||
}
|
||||
BENCHMARK_CAPTURE(GetVectorDouble, floats, TEST_DATA_DIRECTORY "/regression/floats.json");
|
||||
|
||||
// a large flat object: the flattened document, one member per scalar
|
||||
static void GetMap(benchmark::State& state, const char* filename)
|
||||
{
|
||||
const json j = load_json(filename).flatten();
|
||||
|
||||
for (auto _ : state)
|
||||
{
|
||||
auto m = j.get<std::map<std::string, json>>();
|
||||
benchmark::DoNotOptimize(m);
|
||||
}
|
||||
|
||||
state.SetItemsProcessed(state.iterations() * static_cast<std::int64_t>(j.size()));
|
||||
}
|
||||
BENCHMARK_CAPTURE(GetMap, citm_catalog, TEST_DATA_DIRECTORY "/nativejson-benchmark/citm_catalog.json");
|
||||
|
||||
/// 100,000 strings, each too long for the small string optimization
|
||||
static std::vector<std::string> make_strings()
|
||||
{
|
||||
std::vector<std::string> result;
|
||||
result.reserve(100000);
|
||||
for (std::size_t i = 0; i < 100000; ++i)
|
||||
{
|
||||
result.push_back("a string that does not fit in place: " + std::to_string(i));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
static void ToJsonStrings(benchmark::State& state, const bool move)
|
||||
{
|
||||
const std::vector<std::string> strings = make_strings();
|
||||
|
||||
for (auto _ : state)
|
||||
{
|
||||
state.PauseTiming();
|
||||
std::vector<std::string> input = strings;
|
||||
state.ResumeTiming();
|
||||
|
||||
// destroyed inside the timed section, like the copies above
|
||||
auto* j = move ? new json(std::move(input)) : new json(input);
|
||||
delete j;
|
||||
}
|
||||
|
||||
state.SetItemsProcessed(state.iterations() * static_cast<std::int64_t>(strings.size()));
|
||||
}
|
||||
BENCHMARK_CAPTURE(ToJsonStrings, copy, false);
|
||||
BENCHMARK_CAPTURE(ToJsonStrings, move, true);
|
||||
|
||||
// conversion of a JSON array into standard containers
|
||||
template<typename Container>
|
||||
static void GetArray(benchmark::State& state)
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
// __ _____ _____ _____
|
||||
// __| | __| | | | JSON for Modern C++ (supporting code)
|
||||
// | | |__ | | | | | | version 3.12.0
|
||||
// |_____|_____|_____|_|___| https://github.com/nlohmann/json
|
||||
//
|
||||
// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann <https://nlohmann.me>
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
// This file contains the C++17-only part of unit-allocator.cpp (std::string_view key
|
||||
// lookup with a failing allocator). It is kept in a separate translation unit so the (much
|
||||
// larger) unit-allocator.cpp is built for C++11 only and not rebuilt for every C++
|
||||
// standard.
|
||||
|
||||
#include "doctest_compatibility.h"
|
||||
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
#ifdef JSON_HAS_CPP_17
|
||||
#include <memory>
|
||||
#include <new>
|
||||
#include <string_view>
|
||||
#include <utility>
|
||||
|
||||
namespace
|
||||
{
|
||||
bool next_construct_fails_cpp17 = false;
|
||||
bool next_destroy_fails_cpp17 = false;
|
||||
bool next_deallocate_fails_cpp17 = false;
|
||||
|
||||
template<class T>
|
||||
struct my_allocator_cpp17 : std::allocator<T>
|
||||
{
|
||||
using std::allocator<T>::allocator;
|
||||
|
||||
template<class... Args>
|
||||
void construct(T* p, Args&& ... args)
|
||||
{
|
||||
if (next_construct_fails_cpp17)
|
||||
{
|
||||
next_construct_fails_cpp17 = false;
|
||||
throw std::bad_alloc();
|
||||
}
|
||||
|
||||
::new (reinterpret_cast<void*>(p)) T(std::forward<Args>(args)...);
|
||||
}
|
||||
|
||||
void deallocate(T* p, std::size_t n)
|
||||
{
|
||||
if (next_deallocate_fails_cpp17)
|
||||
{
|
||||
next_deallocate_fails_cpp17 = false;
|
||||
throw std::bad_alloc();
|
||||
}
|
||||
|
||||
std::allocator<T>::deallocate(p, n);
|
||||
}
|
||||
|
||||
void destroy(T* p)
|
||||
{
|
||||
if (next_destroy_fails_cpp17)
|
||||
{
|
||||
next_destroy_fails_cpp17 = false;
|
||||
throw std::bad_alloc();
|
||||
}
|
||||
|
||||
static_cast<void>(p); // fix MSVC's C4100 warning
|
||||
p->~T();
|
||||
}
|
||||
|
||||
template <class U>
|
||||
struct rebind
|
||||
{
|
||||
using other = my_allocator_cpp17<U>;
|
||||
};
|
||||
};
|
||||
} // namespace
|
||||
|
||||
// the no-exceptions CI job skips every CHECK_THROWS_AS, which would leave
|
||||
// next_construct_fails_cpp17 set for the next allocation outside a check
|
||||
#if !defined(JSON_NOEXCEPTION)
|
||||
TEST_CASE("a failed allocation leaves the value unchanged (C++17)")
|
||||
{
|
||||
// create JSON type using the throwing allocator
|
||||
using my_json = nlohmann::json::with_allocator_t<my_allocator_cpp17>;
|
||||
|
||||
SECTION("turning a null value into an array or object")
|
||||
{
|
||||
my_json j;
|
||||
|
||||
next_construct_fails_cpp17 = true;
|
||||
CHECK_THROWS_AS(j[std::string_view("key")], std::bad_alloc&);
|
||||
CHECK(j.is_null());
|
||||
|
||||
next_construct_fails_cpp17 = false;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
@@ -0,0 +1,115 @@
|
||||
// __ _____ _____ _____
|
||||
// __| | __| | | | JSON for Modern C++ (supporting code)
|
||||
// | | |__ | | | | | | version 3.12.0
|
||||
// |_____|_____|_____|_|___| https://github.com/nlohmann/json
|
||||
//
|
||||
// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann <https://nlohmann.me>
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
// This file contains the C++20-only part of unit-allocator.cpp (creating a value from a
|
||||
// std::ranges view with a failing allocator). It is kept in a separate translation unit so
|
||||
// the (much larger) unit-allocator.cpp is built for C++11 only and not rebuilt for every
|
||||
// C++ standard.
|
||||
|
||||
#include "doctest_compatibility.h"
|
||||
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
#ifdef JSON_HAS_CPP_20
|
||||
#if JSON_HAS_RANGES
|
||||
#include <ranges>
|
||||
#endif
|
||||
|
||||
#include <memory>
|
||||
#include <new>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace
|
||||
{
|
||||
bool next_construct_fails_cpp20 = false;
|
||||
bool next_destroy_fails_cpp20 = false;
|
||||
bool next_deallocate_fails_cpp20 = false;
|
||||
|
||||
template<class T>
|
||||
struct my_allocator_cpp20 : std::allocator<T>
|
||||
{
|
||||
using std::allocator<T>::allocator;
|
||||
|
||||
template<class... Args>
|
||||
void construct(T* p, Args&& ... args)
|
||||
{
|
||||
if (next_construct_fails_cpp20)
|
||||
{
|
||||
next_construct_fails_cpp20 = false;
|
||||
throw std::bad_alloc();
|
||||
}
|
||||
|
||||
::new (reinterpret_cast<void*>(p)) T(std::forward<Args>(args)...);
|
||||
}
|
||||
|
||||
void deallocate(T* p, std::size_t n)
|
||||
{
|
||||
if (next_deallocate_fails_cpp20)
|
||||
{
|
||||
next_deallocate_fails_cpp20 = false;
|
||||
throw std::bad_alloc();
|
||||
}
|
||||
|
||||
std::allocator<T>::deallocate(p, n);
|
||||
}
|
||||
|
||||
void destroy(T* p)
|
||||
{
|
||||
if (next_destroy_fails_cpp20)
|
||||
{
|
||||
next_destroy_fails_cpp20 = false;
|
||||
throw std::bad_alloc();
|
||||
}
|
||||
|
||||
static_cast<void>(p); // fix MSVC's C4100 warning
|
||||
p->~T();
|
||||
}
|
||||
|
||||
template <class U>
|
||||
struct rebind
|
||||
{
|
||||
using other = my_allocator_cpp20<U>;
|
||||
};
|
||||
};
|
||||
} // namespace
|
||||
|
||||
// the no-exceptions CI job skips every CHECK_THROWS_AS, which would leave
|
||||
// next_construct_fails_cpp20 set for the next allocation outside a check
|
||||
#if !defined(JSON_NOEXCEPTION)
|
||||
TEST_CASE("a failed allocation leaves the value unchanged (C++20)")
|
||||
{
|
||||
// create JSON type using the throwing allocator
|
||||
using my_json = nlohmann::json::with_allocator_t<my_allocator_cpp20>;
|
||||
|
||||
// With iterator debugging, VS 2015's containers construct a proxy with the
|
||||
// allocator in constructors that cannot report its failure, so a failing
|
||||
// allocator crashes this section there (SIGSEGV with VS 2015 Debug x86).
|
||||
#if !(defined(_MSC_VER) && _MSC_VER < 1910 && defined(_ITERATOR_DEBUG_LEVEL) && _ITERATOR_DEBUG_LEVEL > 0)
|
||||
SECTION("converting into an existing value")
|
||||
{
|
||||
// to_json replaces the value it is given; the old one must survive a
|
||||
// failed creation of the new one
|
||||
my_json j = "old";
|
||||
|
||||
#if JSON_HAS_RANGES && !defined(__MINGW32__)
|
||||
const std::vector<int> numbers = {1, 2};
|
||||
next_construct_fails_cpp20 = true;
|
||||
CHECK_THROWS_AS(nlohmann::to_json(j, numbers | std::views::filter([](int /*unused*/)
|
||||
{
|
||||
return true;
|
||||
})), std::bad_alloc&);
|
||||
CHECK(j == "old");
|
||||
#endif
|
||||
|
||||
next_construct_fails_cpp20 = false;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
@@ -13,9 +13,6 @@
|
||||
using nlohmann::json;
|
||||
|
||||
#include <valarray>
|
||||
#if JSON_HAS_RANGES
|
||||
#include <ranges>
|
||||
#endif
|
||||
|
||||
namespace
|
||||
{
|
||||
@@ -684,12 +681,6 @@ TEST_CASE("a failed allocation leaves the value unchanged")
|
||||
CHECK_THROWS_AS(j["key"], std::bad_alloc&);
|
||||
CHECK(j.is_null());
|
||||
|
||||
#ifdef JSON_HAS_CPP_17
|
||||
next_construct_fails = true;
|
||||
CHECK_THROWS_AS(j[std::string_view("key")], std::bad_alloc&);
|
||||
CHECK(j.is_null());
|
||||
#endif
|
||||
|
||||
next_construct_fails = true;
|
||||
CHECK_THROWS_AS(j.push_back(my_json(1)), std::bad_alloc&);
|
||||
CHECK(j.is_null());
|
||||
@@ -787,16 +778,6 @@ TEST_CASE("a failed allocation leaves the value unchanged")
|
||||
CHECK_THROWS_AS(nlohmann::to_json(j, std::valarray<int> {1, 2}), std::bad_alloc&);
|
||||
CHECK(j == "old");
|
||||
|
||||
#if JSON_HAS_RANGES && !defined(__MINGW32__)
|
||||
const std::vector<int> numbers = {1, 2};
|
||||
next_construct_fails = true;
|
||||
CHECK_THROWS_AS(nlohmann::to_json(j, numbers | std::views::filter([](int /*unused*/)
|
||||
{
|
||||
return true;
|
||||
})), std::bad_alloc&);
|
||||
CHECK(j == "old");
|
||||
#endif
|
||||
|
||||
next_construct_fails = false;
|
||||
nlohmann::to_json(j, std::vector<int> {1, 2});
|
||||
CHECK(j == my_json({1, 2}));
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
// __ _____ _____ _____
|
||||
// __| | __| | | | JSON for Modern C++ (supporting code)
|
||||
// | | |__ | | | | | | version 3.12.0
|
||||
// |_____|_____|_____|_|___| https://github.com/nlohmann/json
|
||||
//
|
||||
// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann <https://nlohmann.me>
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
// This file contains the C++17-only part of unit-bon8.cpp (BON8 with std::byte
|
||||
// containers). It is kept in a separate translation unit so the (much larger) unit-
|
||||
// bon8.cpp is built for C++11 only and not rebuilt for every C++ standard.
|
||||
|
||||
#include "doctest_compatibility.h"
|
||||
|
||||
#include <nlohmann/json.hpp>
|
||||
using nlohmann::json;
|
||||
|
||||
#ifdef JSON_HAS_CPP_17
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
|
||||
TEST_CASE("BON8 with std::byte")
|
||||
{
|
||||
SECTION("vector roundtrip")
|
||||
{
|
||||
const json original =
|
||||
{
|
||||
{"name", "test"},
|
||||
{"value", 42},
|
||||
{"array", {1, 2, 3}}
|
||||
};
|
||||
|
||||
const std::vector<uint8_t> temp = json::to_bon8(original);
|
||||
std::vector<std::byte> bon8_data(temp.size());
|
||||
for (size_t i = 0; i < temp.size(); ++i)
|
||||
{
|
||||
bon8_data[i] = std::byte(temp[i]);
|
||||
}
|
||||
|
||||
json from_bytes;
|
||||
CHECK_NOTHROW(from_bytes = json::from_bon8(bon8_data));
|
||||
CHECK(from_bytes == original);
|
||||
}
|
||||
|
||||
SECTION("empty vector")
|
||||
{
|
||||
const std::vector<std::byte> empty_data;
|
||||
CHECK_THROWS_WITH_AS([&]()
|
||||
{
|
||||
[[maybe_unused]] auto result = json::from_bon8(empty_data);
|
||||
return true;
|
||||
}
|
||||
(),
|
||||
"[json.exception.parse_error.110] parse error at byte 1: syntax error while parsing BON8 value: unexpected end of input",
|
||||
json::parse_error&);
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -1099,41 +1099,3 @@ TEST_CASE("issue #5648 - from_bon8(ptr, len) must read len bytes, not treat ptr
|
||||
#endif
|
||||
}
|
||||
|
||||
#ifdef JSON_HAS_CPP_17
|
||||
TEST_CASE("BON8 with std::byte")
|
||||
{
|
||||
SECTION("vector roundtrip")
|
||||
{
|
||||
const json original =
|
||||
{
|
||||
{"name", "test"},
|
||||
{"value", 42},
|
||||
{"array", {1, 2, 3}}
|
||||
};
|
||||
|
||||
const std::vector<uint8_t> temp = json::to_bon8(original);
|
||||
std::vector<std::byte> bon8_data(temp.size());
|
||||
for (size_t i = 0; i < temp.size(); ++i)
|
||||
{
|
||||
bon8_data[i] = std::byte(temp[i]);
|
||||
}
|
||||
|
||||
json from_bytes;
|
||||
CHECK_NOTHROW(from_bytes = json::from_bon8(bon8_data));
|
||||
CHECK(from_bytes == original);
|
||||
}
|
||||
|
||||
SECTION("empty vector")
|
||||
{
|
||||
const std::vector<std::byte> empty_data;
|
||||
CHECK_THROWS_WITH_AS([&]()
|
||||
{
|
||||
[[maybe_unused]] auto result = json::from_bon8(empty_data);
|
||||
return true;
|
||||
}
|
||||
(),
|
||||
"[json.exception.parse_error.110] parse error at byte 1: syntax error while parsing BON8 value: unexpected end of input",
|
||||
json::parse_error&);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -13,15 +13,11 @@
|
||||
using nlohmann::json;
|
||||
|
||||
#include <array> // array
|
||||
#include <cctype> // toupper
|
||||
#include <cstdint> // uint32_t, uint64_t
|
||||
#include <cstdio> // snprintf
|
||||
#include <cmath> // isfinite
|
||||
#include <cstdlib> // strtod
|
||||
#include <cstring> // memcpy
|
||||
#include <limits> // numeric_limits
|
||||
#include <initializer_list> // initializer_list
|
||||
#include <list> // list
|
||||
#include <map> // map
|
||||
#include <random> // mt19937
|
||||
#include <sstream> // stringstream
|
||||
@@ -336,112 +332,6 @@ TEST_CASE("lexer number fast path")
|
||||
}
|
||||
}
|
||||
|
||||
#if !defined(JSON_NOEXCEPTION)
|
||||
// this section reports parse errors, which abort when exceptions are off
|
||||
SECTION("digits accumulated while scanning")
|
||||
{
|
||||
// The fast path accumulates the digits while it validates them, eight
|
||||
// at a time where possible, and converts tokens with at most 19
|
||||
// digits (floats) or 18 digits (integers) from that value. Every digit
|
||||
// count from 1 to 25, in the integer and in the fraction part, with
|
||||
// and without sign and exponent, must give the byte path's result. A
|
||||
// std::list has no contiguous storage, so parsing it takes the byte
|
||||
// path.
|
||||
// type and value, or the error (values beyond the range of double
|
||||
// are rejected with out_of_range.406 on both paths)
|
||||
const auto outcome = [](const std::string & doc, bool byte_path) -> std::string
|
||||
{
|
||||
JSON_TRY
|
||||
{
|
||||
const std::list<char> chars(doc.begin(), doc.end());
|
||||
const json j = byte_path ? json::parse(chars.begin(), chars.end()) : json::parse(doc);
|
||||
return std::string(j[0].type_name()) + '|' + j.dump();
|
||||
}
|
||||
JSON_CATCH (const json::exception & e)
|
||||
{
|
||||
return e.what();
|
||||
}
|
||||
};
|
||||
|
||||
// the concatenation of @a parts
|
||||
const auto join = [](std::initializer_list<std::string> parts)
|
||||
{
|
||||
std::string result;
|
||||
for (const auto& part : parts)
|
||||
{
|
||||
result += part;
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
std::vector<std::string> numbers;
|
||||
for (std::size_t digits = 1; digits <= 25; ++digits)
|
||||
{
|
||||
std::string run;
|
||||
for (std::size_t i = 0; i < digits; ++i)
|
||||
{
|
||||
run += static_cast<char>('1' + static_cast<char>((i * 7) % 9));
|
||||
}
|
||||
const std::string nines(digits, '9');
|
||||
for (const char* sign_text :
|
||||
{"", "-"
|
||||
})
|
||||
{
|
||||
const std::string sign = sign_text;
|
||||
numbers.push_back(join({sign, run}));
|
||||
numbers.push_back(join({sign, nines}));
|
||||
numbers.push_back(join({sign, "1", std::string(digits - 1, '0')}));
|
||||
numbers.push_back(join({sign, "0.", run}));
|
||||
numbers.push_back(join({sign, "0.", std::string(digits, '0'), "1"}));
|
||||
numbers.push_back(join({sign, run, ".", run}));
|
||||
numbers.push_back(join({sign, run, "e-", std::to_string(digits)}));
|
||||
numbers.push_back(join({sign, "1.", run, "E+", std::to_string(digits * 13)}));
|
||||
}
|
||||
}
|
||||
// exponents beyond every range, and zeros
|
||||
numbers.insert(numbers.end(), {"1e400", "1e-400", "-1e99999999999999999999", "1e-99999999999999999999",
|
||||
"0e99999999999999999999", "0.0e-5", "-0.000", "123456789e-0000000000000000000000330"
|
||||
});
|
||||
|
||||
for (const auto& n : numbers)
|
||||
{
|
||||
CAPTURE(n)
|
||||
const std::string doc = "[" + n + "]";
|
||||
const std::string a = outcome(doc, false);
|
||||
CHECK(a == outcome(doc, true));
|
||||
const double expected = std::strtod(n.c_str(), nullptr);
|
||||
if (a.compare(0, 6, "number") == 0 && std::isfinite(expected) && n.find_first_of(".eE") != std::string::npos)
|
||||
{
|
||||
CHECK(json::parse(doc)[0].get<double>() == expected);
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
SECTION("integers that do not fit a narrower type become floats")
|
||||
{
|
||||
// up to 18 digits, the fast path stores the accumulated value without
|
||||
// an overflow check; a narrower number_integer_t/number_unsigned_t
|
||||
// must still see the value rejected and converted as a float
|
||||
using json32 = nlohmann::basic_json<std::map, std::vector, std::string, bool, std::int32_t, std::uint32_t, float>;
|
||||
for (const char* n :
|
||||
{"4294967295", "4294967296", "2147483647", "-2147483648", "-2147483649", "99999999999", "-99999999999"
|
||||
})
|
||||
{
|
||||
CAPTURE(n)
|
||||
const std::string doc = std::string("[") + n + "]";
|
||||
const std::list<char> chars(doc.begin(), doc.end());
|
||||
const json32 a = json32::parse(doc);
|
||||
const json32 b = json32::parse(chars.begin(), chars.end());
|
||||
CHECK(a[0].type() == b[0].type());
|
||||
CHECK(a == b);
|
||||
}
|
||||
CHECK(json32::parse("[4294967295]")[0].is_number_unsigned());
|
||||
CHECK(json32::parse("[4294967296]")[0].is_number_float());
|
||||
CHECK(json32::parse("[-2147483648]")[0].is_number_integer());
|
||||
CHECK(json32::parse("[-2147483649]")[0].is_number_float());
|
||||
}
|
||||
|
||||
SECTION("token type classification")
|
||||
{
|
||||
CHECK((scan_string("0") == json::lexer::token_type::value_unsigned));
|
||||
@@ -600,67 +490,6 @@ TEST_CASE("lexer number fast path")
|
||||
#endif
|
||||
}
|
||||
|
||||
#if !defined(JSON_NOEXCEPTION)
|
||||
// this test case compares the errors of both lexer paths, which abort when
|
||||
// exceptions are off
|
||||
TEST_CASE("lexer literal fast path")
|
||||
{
|
||||
// For contiguous input, the rest of a literal is compared at once, and a
|
||||
// mismatch is left to the byte path, which reports the error at the
|
||||
// character that differs. Every truncation and every wrong character of
|
||||
// each literal, at the end of the input or followed by more text, must
|
||||
// give the byte path's result (a std::list is read one character at a
|
||||
// time).
|
||||
const auto outcome = [](const std::string & doc, bool byte_path) -> std::string
|
||||
{
|
||||
JSON_TRY
|
||||
{
|
||||
const std::list<char> chars(doc.begin(), doc.end());
|
||||
return (byte_path ? json::parse(chars.begin(), chars.end()) : json::parse(doc)).dump();
|
||||
}
|
||||
JSON_CATCH (const json::exception & e)
|
||||
{
|
||||
return e.what();
|
||||
}
|
||||
};
|
||||
|
||||
std::vector<std::string> mismatches;
|
||||
for (const std::string literal :
|
||||
{"true", "false", "null"
|
||||
})
|
||||
{
|
||||
std::vector<std::string> variants;
|
||||
for (std::size_t length = 1; length <= literal.size(); ++length)
|
||||
{
|
||||
variants.push_back(literal.substr(0, length));
|
||||
}
|
||||
for (std::size_t i = 1; i < literal.size(); ++i)
|
||||
{
|
||||
std::string changed = literal;
|
||||
changed[i] = 'X';
|
||||
variants.push_back(changed);
|
||||
changed[i] = static_cast<char>(std::toupper(static_cast<unsigned char>(literal[i])));
|
||||
variants.push_back(changed);
|
||||
}
|
||||
for (const auto& variant : variants)
|
||||
{
|
||||
for (const std::string& doc :
|
||||
{
|
||||
variant, "[" + variant + "]", "[" + variant, variant + " ", variant + "1", "{\"k\":" + variant + "}"
|
||||
})
|
||||
{
|
||||
if (outcome(doc, false) != outcome(doc, true))
|
||||
{
|
||||
mismatches.push_back(doc);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
CAPTURE(mismatches)
|
||||
CHECK(mismatches.empty());
|
||||
}
|
||||
#endif
|
||||
|
||||
TEST_CASE("lexer string fast path")
|
||||
{
|
||||
// Build a byte string from explicit values: a hex escape in a string
|
||||
@@ -781,73 +610,6 @@ TEST_CASE("lexer string fast path")
|
||||
CAPTURE(mismatches)
|
||||
CHECK(mismatches.empty());
|
||||
}
|
||||
|
||||
SECTION("special bytes at every offset of the vector blocks")
|
||||
{
|
||||
// With SSE2 or NEON, runs are scanned 16 bytes at a time (after one
|
||||
// SWAR word, or four with NEON), and runs of non-ASCII text are
|
||||
// checked 16 bytes at a time as well. Place every kind of byte that
|
||||
// ends a run at each offset across several blocks, after ASCII and
|
||||
// after CJK text, so sequences also straddle the block boundaries.
|
||||
// The reference is a std::list, which has no contiguous storage and
|
||||
// is therefore read one character at a time.
|
||||
const auto byte_path = [](const std::string & doc) -> std::string
|
||||
{
|
||||
const std::list<char> chars(doc.begin(), doc.end());
|
||||
JSON_TRY
|
||||
{
|
||||
return json::parse(chars.begin(), chars.end()).dump();
|
||||
}
|
||||
JSON_CATCH (const json::exception & e)
|
||||
{
|
||||
return e.what();
|
||||
}
|
||||
};
|
||||
const std::vector<std::string> specials =
|
||||
{
|
||||
"\"", "\\", "\\u00e9", bytes({0x01}), bytes({0x1F}), bytes({0x7F}),
|
||||
bytes({0xC3, 0xA9}), bytes({0xE4, 0xB8, 0xAD}), bytes({0xF0, 0x9F, 0x98, 0x80}),
|
||||
bytes({0xFF}), bytes({0xC3}), bytes({0xE4, 0xB8}), bytes({0xED, 0xA0, 0x80}), bytes({0xC0, 0x80})
|
||||
};
|
||||
const std::string cjk = bytes({0xE4, 0xB8, 0xAD}); // U+4E2D
|
||||
|
||||
std::vector<std::string> mismatches;
|
||||
for (std::size_t offset = 0; offset <= 80; ++offset)
|
||||
{
|
||||
std::string text;
|
||||
while (text.size() < offset)
|
||||
{
|
||||
text += cjk;
|
||||
}
|
||||
for (const auto& prefix :
|
||||
{
|
||||
std::string(offset, 'a'), text
|
||||
})
|
||||
{
|
||||
for (const auto& special : specials)
|
||||
{
|
||||
// the closing quote right behind, or in a later block
|
||||
for (const int tail :
|
||||
{
|
||||
0, 1, 3, 20
|
||||
})
|
||||
{
|
||||
std::string doc = "[\"";
|
||||
doc += prefix;
|
||||
doc += special;
|
||||
doc.append(static_cast<std::size_t>(tail), 'b');
|
||||
doc += "\"]";
|
||||
if (outcome(doc, false) != byte_path(doc))
|
||||
{
|
||||
mismatches.push_back(doc);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
CAPTURE(mismatches)
|
||||
CHECK(mismatches.empty());
|
||||
}
|
||||
#endif
|
||||
|
||||
// json::accept() never throws, so the ranges stay covered without exceptions
|
||||
|
||||
@@ -30,9 +30,7 @@ using nlohmann::json;
|
||||
#include <valarray>
|
||||
#include <algorithm>
|
||||
#include <cstdio>
|
||||
#include <cstdint>
|
||||
#include <fstream>
|
||||
#include <iterator>
|
||||
#include <list>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
@@ -2452,109 +2450,6 @@ std::string parse_error_message_range(IteratorType first, IteratorType last)
|
||||
}
|
||||
} // namespace
|
||||
|
||||
TEST_CASE("object keys are moved into the value")
|
||||
{
|
||||
// json_sax_dom_parser::key() moves the key out of the lexer's buffer into
|
||||
// the object, and the lexer starts the next token with an empty buffer.
|
||||
// Keys of every length (inline and allocated strings), keys with escapes,
|
||||
// and duplicate keys must give the same objects as reading the input one
|
||||
// character at a time, and as ordered_json, whose map copies the key.
|
||||
const std::string long_key(100, 'k');
|
||||
const std::string doc = R"({"a": 1, ")" + long_key + R"(": {"b": [2, 3], "sixteen_chars_xx": 4}, )"
|
||||
R"("esc\"apedé": 5, "a": 6, ")" + long_key + R"(x": 7, "": 8, "dup": 9, "dup": 10})";
|
||||
|
||||
const json j = json::parse(doc);
|
||||
const std::list<char> chars(doc.begin(), doc.end());
|
||||
CHECK(j == json::parse(chars.begin(), chars.end()));
|
||||
|
||||
CHECK(j.size() == 6);
|
||||
CHECK(j["a"] == 6);
|
||||
CHECK(j[long_key]["b"] == json({2, 3}));
|
||||
CHECK(j[long_key]["sixteen_chars_xx"] == 4);
|
||||
CHECK(j["esc\"aped\xc3\xa9"] == 5);
|
||||
CHECK(j[long_key + "x"] == 7);
|
||||
CHECK(j[""] == 8);
|
||||
CHECK(j["dup"] == 10);
|
||||
|
||||
const auto o = nlohmann::ordered_json::parse(doc);
|
||||
CHECK(o.size() == 6);
|
||||
CHECK(o[long_key + "x"] == 7);
|
||||
CHECK(json(o) == j);
|
||||
}
|
||||
|
||||
TEST_CASE("text of float tokens")
|
||||
{
|
||||
// For contiguous input, the lexer copies the text of a float token into
|
||||
// its buffer only when a SAX handler asks for it (the library's own
|
||||
// handlers do not). A user handler must still see the exact text, for
|
||||
// every input adapter and in every position of a document.
|
||||
const std::string doc = R"([1.5, -0.0, 0.1, 1e5, -2.5E-4, 12345678901234567890.5, 0.12345678901234567890123,
|
||||
{"a": 3.25, "b": [6.02e23, 1]}, "x", true, 7e-1])";
|
||||
const std::vector<std::string> expected_floats =
|
||||
{
|
||||
"number_float(1.5)", "number_float(-0.0)", "number_float(0.1)", "number_float(1e5)", "number_float(-2.5E-4)",
|
||||
"number_float(12345678901234567890.5)", "number_float(0.12345678901234567890123)", "number_float(3.25)",
|
||||
"number_float(6.02e23)", "number_float(7e-1)"
|
||||
};
|
||||
|
||||
const auto floats = [](const std::vector<std::string>& events)
|
||||
{
|
||||
std::vector<std::string> result;
|
||||
std::copy_if(events.begin(), events.end(), std::back_inserter(result), [](const std::string & e)
|
||||
{
|
||||
return e.compare(0, 13, "number_float(") == 0;
|
||||
});
|
||||
return result;
|
||||
};
|
||||
|
||||
SECTION("contiguous input")
|
||||
{
|
||||
SaxEventLogger l;
|
||||
CHECK(json::sax_parse(doc, &l));
|
||||
CHECK(floats(l.events) == expected_floats);
|
||||
}
|
||||
|
||||
SECTION("byte input")
|
||||
{
|
||||
const std::vector<std::uint8_t> bytes(doc.begin(), doc.end());
|
||||
SaxEventLogger l;
|
||||
CHECK(json::sax_parse(bytes, &l));
|
||||
CHECK(floats(l.events) == expected_floats);
|
||||
}
|
||||
|
||||
SECTION("stream input")
|
||||
{
|
||||
std::istringstream is(doc);
|
||||
SaxEventLogger l;
|
||||
CHECK(json::sax_parse(is, &l));
|
||||
CHECK(floats(l.events) == expected_floats);
|
||||
}
|
||||
|
||||
SECTION("non-contiguous iterators")
|
||||
{
|
||||
const std::list<char> chars(doc.begin(), doc.end());
|
||||
SaxEventLogger l;
|
||||
CHECK(json::sax_parse(chars.begin(), chars.end(), &l));
|
||||
CHECK(floats(l.events) == expected_floats);
|
||||
}
|
||||
|
||||
SECTION("the library's own handlers do not need the text")
|
||||
{
|
||||
using lexer_adapter = nlohmann::detail::iterator_input_adapter<const char*>;
|
||||
CHECK(nlohmann::detail::sax_ignores_number_text<nlohmann::detail::json_sax_dom_parser<json, lexer_adapter>>::value);
|
||||
CHECK(nlohmann::detail::sax_ignores_number_text<nlohmann::detail::json_sax_dom_callback_parser<json, lexer_adapter>>::value);
|
||||
CHECK(nlohmann::detail::sax_ignores_number_text<nlohmann::detail::json_sax_acceptor<json>>::value);
|
||||
CHECK_FALSE(nlohmann::detail::sax_ignores_number_text<SaxEventLogger>::value);
|
||||
|
||||
// and the values are the same as when the text is read
|
||||
const json j = json::parse(doc);
|
||||
CHECK(j[0] == 1.5);
|
||||
CHECK(j[5].get<double>() == 12345678901234567890.5);
|
||||
CHECK(j[7]["b"][0] == 6.02e23);
|
||||
CHECK(json::accept(doc));
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("last-read diagnostics are identical across input adapters")
|
||||
{
|
||||
// The lexer reconstructs the "last read" token lazily for seekable adapters
|
||||
|
||||
@@ -0,0 +1,526 @@
|
||||
// __ _____ _____ _____
|
||||
// __| | __| | | | JSON for Modern C++ (supporting code)
|
||||
// | | |__ | | | | | | version 3.12.0
|
||||
// |_____|_____|_____|_|___| https://github.com/nlohmann/json
|
||||
//
|
||||
// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann <https://nlohmann.me>
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
// This file contains the C++20-only part of unit-comparison.cpp (tests of operator<=> and
|
||||
// other three-way comparison specific behavior). It is kept in a separate translation unit
|
||||
// so the (much larger) unit-comparison.cpp is built for C++11 only and not rebuilt for
|
||||
// every C++ standard.
|
||||
|
||||
#include "doctest_compatibility.h"
|
||||
|
||||
#include <nlohmann/json.hpp>
|
||||
using nlohmann::json;
|
||||
|
||||
#ifdef JSON_HAS_CPP_20
|
||||
#if JSON_HAS_THREE_WAY_COMPARISON
|
||||
#include <cmath>
|
||||
#include <compare>
|
||||
#include <cstddef>
|
||||
#include <limits>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
// this can be replaced with the doctest stl extension header in version 2.5
|
||||
namespace doctest
|
||||
{
|
||||
template<> struct StringMaker<std::partial_ordering>
|
||||
{
|
||||
static String convert(const std::partial_ordering& order)
|
||||
{
|
||||
if (order == std::partial_ordering::less)
|
||||
{
|
||||
return "std::partial_ordering::less";
|
||||
}
|
||||
if (order == std::partial_ordering::equivalent)
|
||||
{
|
||||
return "std::partial_ordering::equivalent";
|
||||
}
|
||||
if (order == std::partial_ordering::greater)
|
||||
{
|
||||
return "std::partial_ordering::greater";
|
||||
}
|
||||
if (order == std::partial_ordering::unordered)
|
||||
{
|
||||
return "std::partial_ordering::unordered";
|
||||
}
|
||||
return "{?}";
|
||||
}
|
||||
};
|
||||
} // namespace doctest
|
||||
|
||||
TEST_CASE("lexicographical comparison operators (C++20)")
|
||||
{
|
||||
constexpr auto f_ = false;
|
||||
constexpr auto _t = true;
|
||||
constexpr auto nan = std::numeric_limits<json::number_float_t>::quiet_NaN();
|
||||
constexpr auto lt = std::partial_ordering::less;
|
||||
constexpr auto gt = std::partial_ordering::greater;
|
||||
constexpr auto eq = std::partial_ordering::equivalent;
|
||||
constexpr auto un = std::partial_ordering::unordered;
|
||||
|
||||
INFO("using 3-way comparison");
|
||||
|
||||
#if JSON_USE_LEGACY_DISCARDED_VALUE_COMPARISON
|
||||
INFO("using legacy comparison");
|
||||
#endif
|
||||
|
||||
SECTION("types")
|
||||
{
|
||||
std::vector<json::value_t> j_types =
|
||||
{
|
||||
json::value_t::null,
|
||||
json::value_t::boolean,
|
||||
json::value_t::number_integer,
|
||||
json::value_t::number_unsigned,
|
||||
json::value_t::number_float,
|
||||
json::value_t::object,
|
||||
json::value_t::array,
|
||||
json::value_t::string,
|
||||
json::value_t::binary,
|
||||
json::value_t::discarded
|
||||
};
|
||||
|
||||
std::vector<std::vector<bool>> expected_lt =
|
||||
{
|
||||
//0 1 2 3 4 5 6 7 8 9
|
||||
{f_, _t, _t, _t, _t, _t, _t, _t, _t, f_}, // 0
|
||||
{f_, f_, _t, _t, _t, _t, _t, _t, _t, f_}, // 1
|
||||
{f_, f_, f_, f_, f_, _t, _t, _t, _t, f_}, // 2
|
||||
{f_, f_, f_, f_, f_, _t, _t, _t, _t, f_}, // 3
|
||||
{f_, f_, f_, f_, f_, _t, _t, _t, _t, f_}, // 4
|
||||
{f_, f_, f_, f_, f_, f_, _t, _t, _t, f_}, // 5
|
||||
{f_, f_, f_, f_, f_, f_, f_, _t, _t, f_}, // 6
|
||||
{f_, f_, f_, f_, f_, f_, f_, f_, _t, f_}, // 7
|
||||
{f_, f_, f_, f_, f_, f_, f_, f_, f_, f_}, // 8
|
||||
{f_, f_, f_, f_, f_, f_, f_, f_, f_, f_}, // 9
|
||||
};
|
||||
|
||||
SECTION("comparison: less")
|
||||
{
|
||||
for (size_t i = 0; i < j_types.size(); ++i)
|
||||
{
|
||||
for (size_t j = 0; j < j_types.size(); ++j)
|
||||
{
|
||||
CAPTURE(i)
|
||||
CAPTURE(j)
|
||||
// check precomputed values
|
||||
CHECK((j_types[i] < j_types[j]) == expected_lt[i][j]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SECTION("comparison: 3-way")
|
||||
{
|
||||
// doctest runs the REQUIRE in the test case body once per leaf section; keep it
|
||||
// here to run it as often as before the 3-way sections moved from unit-comparison.cpp
|
||||
REQUIRE(std::isnan(nan));
|
||||
|
||||
std::vector<std::vector<std::partial_ordering>> expected =
|
||||
{
|
||||
//0 1 2 3 4 5 6 7 8 9
|
||||
{eq, lt, lt, lt, lt, lt, lt, lt, lt, un}, // 0
|
||||
{gt, eq, lt, lt, lt, lt, lt, lt, lt, un}, // 1
|
||||
{gt, gt, eq, eq, eq, lt, lt, lt, lt, un}, // 2
|
||||
{gt, gt, eq, eq, eq, lt, lt, lt, lt, un}, // 3
|
||||
{gt, gt, eq, eq, eq, lt, lt, lt, lt, un}, // 4
|
||||
{gt, gt, gt, gt, gt, eq, lt, lt, lt, un}, // 5
|
||||
{gt, gt, gt, gt, gt, gt, eq, lt, lt, un}, // 6
|
||||
{gt, gt, gt, gt, gt, gt, gt, eq, lt, un}, // 7
|
||||
{gt, gt, gt, gt, gt, gt, gt, gt, eq, un}, // 8
|
||||
{un, un, un, un, un, un, un, un, un, un}, // 9
|
||||
};
|
||||
|
||||
// check expected partial_ordering against expected boolean
|
||||
REQUIRE(expected.size() == expected_lt.size());
|
||||
for (size_t i = 0; i < expected.size(); ++i)
|
||||
{
|
||||
REQUIRE(expected[i].size() == expected_lt[i].size());
|
||||
for (size_t j = 0; j < expected[i].size(); ++j)
|
||||
{
|
||||
CAPTURE(i)
|
||||
CAPTURE(j)
|
||||
CHECK(std::is_lt(expected[i][j]) == expected_lt[i][j]);
|
||||
}
|
||||
}
|
||||
|
||||
// check 3-way comparison against expected partial_ordering
|
||||
REQUIRE(expected.size() == j_types.size());
|
||||
for (size_t i = 0; i < j_types.size(); ++i)
|
||||
{
|
||||
REQUIRE(expected[i].size() == j_types.size());
|
||||
for (size_t j = 0; j < j_types.size(); ++j)
|
||||
{
|
||||
CAPTURE(i)
|
||||
CAPTURE(j)
|
||||
CHECK((j_types[i] <=> j_types[j]) == expected[i][j]); // *NOPAD*
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SECTION("values")
|
||||
{
|
||||
json j_values =
|
||||
{
|
||||
nullptr, nullptr, // 0 1
|
||||
-17, 42, // 2 3
|
||||
8u, 13u, // 4 5
|
||||
3.14159, 23.42, // 6 7
|
||||
nan, nan, // 8 9
|
||||
"foo", "bar", // 10 11
|
||||
true, false, // 12 13
|
||||
{1, 2, 3}, {"one", "two", "three"}, // 14 15
|
||||
{{"first", 1}, {"second", 2}}, {{"a", "A"}, {"b", {"B"}}}, // 16 17
|
||||
json::binary({1, 2, 3}), json::binary({1, 2, 4}), // 18 19
|
||||
json(json::value_t::discarded), json(json::value_t::discarded) // 20 21
|
||||
};
|
||||
|
||||
std::vector<std::vector<bool>> expected_eq =
|
||||
{
|
||||
//0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
|
||||
{_t, _t, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_}, // 0
|
||||
{_t, _t, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_}, // 1
|
||||
{f_, f_, _t, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_}, // 2
|
||||
{f_, f_, f_, _t, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_}, // 3
|
||||
{f_, f_, f_, f_, _t, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_}, // 4
|
||||
{f_, f_, f_, f_, f_, _t, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_}, // 5
|
||||
{f_, f_, f_, f_, f_, f_, _t, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_}, // 6
|
||||
{f_, f_, f_, f_, f_, f_, f_, _t, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_}, // 7
|
||||
{f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_}, // 8
|
||||
{f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_}, // 9
|
||||
{f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, _t, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_}, // 10
|
||||
{f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, _t, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_}, // 11
|
||||
{f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, _t, f_, f_, f_, f_, f_, f_, f_, f_, f_}, // 12
|
||||
{f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, _t, f_, f_, f_, f_, f_, f_, f_, f_}, // 13
|
||||
{f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, _t, f_, f_, f_, f_, f_, f_, f_}, // 14
|
||||
{f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, _t, f_, f_, f_, f_, f_, f_}, // 15
|
||||
{f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, _t, f_, f_, f_, f_, f_}, // 16
|
||||
{f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, _t, f_, f_, f_, f_}, // 17
|
||||
{f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, _t, f_, f_, f_}, // 18
|
||||
{f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, _t, f_, f_}, // 19
|
||||
{f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_}, // 20
|
||||
{f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_}, // 21
|
||||
};
|
||||
|
||||
std::vector<std::vector<bool>> expected_lt =
|
||||
{
|
||||
//0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
|
||||
{f_, f_, _t, _t, _t, _t, _t, _t, _t, _t, _t, _t, _t, _t, _t, _t, _t, _t, _t, _t, f_, f_}, // 0
|
||||
{f_, f_, _t, _t, _t, _t, _t, _t, _t, _t, _t, _t, _t, _t, _t, _t, _t, _t, _t, _t, f_, f_}, // 1
|
||||
{f_, f_, f_, _t, _t, _t, _t, _t, f_, f_, _t, _t, f_, f_, _t, _t, _t, _t, _t, _t, f_, f_}, // 2
|
||||
{f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, _t, _t, f_, f_, _t, _t, _t, _t, _t, _t, f_, f_}, // 3
|
||||
{f_, f_, f_, _t, f_, _t, f_, _t, f_, f_, _t, _t, f_, f_, _t, _t, _t, _t, _t, _t, f_, f_}, // 4
|
||||
{f_, f_, f_, _t, f_, f_, f_, _t, f_, f_, _t, _t, f_, f_, _t, _t, _t, _t, _t, _t, f_, f_}, // 5
|
||||
{f_, f_, f_, _t, _t, _t, f_, _t, f_, f_, _t, _t, f_, f_, _t, _t, _t, _t, _t, _t, f_, f_}, // 6
|
||||
{f_, f_, f_, _t, f_, f_, f_, f_, f_, f_, _t, _t, f_, f_, _t, _t, _t, _t, _t, _t, f_, f_}, // 7
|
||||
{f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, _t, _t, f_, f_, _t, _t, _t, _t, _t, _t, f_, f_}, // 8
|
||||
{f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, _t, _t, f_, f_, _t, _t, _t, _t, _t, _t, f_, f_}, // 9
|
||||
{f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, _t, _t, f_, f_}, // 10
|
||||
{f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, _t, f_, f_, f_, f_, f_, f_, f_, _t, _t, f_, f_}, // 11
|
||||
{f_, f_, _t, _t, _t, _t, _t, _t, _t, _t, _t, _t, f_, f_, _t, _t, _t, _t, _t, _t, f_, f_}, // 12
|
||||
{f_, f_, _t, _t, _t, _t, _t, _t, _t, _t, _t, _t, _t, f_, _t, _t, _t, _t, _t, _t, f_, f_}, // 13
|
||||
{f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, _t, _t, f_, f_, f_, _t, f_, f_, _t, _t, f_, f_}, // 14
|
||||
{f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, _t, _t, f_, f_, f_, f_, f_, f_, _t, _t, f_, f_}, // 15
|
||||
{f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, _t, _t, f_, f_, _t, _t, f_, f_, _t, _t, f_, f_}, // 16
|
||||
{f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, _t, _t, f_, f_, _t, _t, _t, f_, _t, _t, f_, f_}, // 17
|
||||
{f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, _t, f_, f_}, // 18
|
||||
{f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_}, // 19
|
||||
{f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_}, // 20
|
||||
{f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_, f_}, // 21
|
||||
};
|
||||
|
||||
SECTION("signed/unsigned mixed comparison above INT64_MAX")
|
||||
{
|
||||
const json above_int64_max = static_cast<std::uint64_t>((std::numeric_limits<std::int64_t>::max)()) + 1ULL;
|
||||
const json max_uint64 = (std::numeric_limits<std::uint64_t>::max)();
|
||||
const json negative_one = -1;
|
||||
const json one = 1;
|
||||
const json max_int64 = (std::numeric_limits<std::int64_t>::max)();
|
||||
|
||||
CHECK((negative_one <=> above_int64_max) == std::partial_ordering::less); // *NOPAD*
|
||||
CHECK((above_int64_max <=> negative_one) == std::partial_ordering::greater); // *NOPAD*
|
||||
CHECK((negative_one <=> max_uint64) == std::partial_ordering::less); // *NOPAD*
|
||||
CHECK((max_uint64 <=> negative_one) == std::partial_ordering::greater); // *NOPAD*
|
||||
CHECK((one <=> above_int64_max) == std::partial_ordering::less); // *NOPAD*
|
||||
CHECK((above_int64_max <=> one) == std::partial_ordering::greater); // *NOPAD*
|
||||
CHECK((max_int64 <=> above_int64_max) == std::partial_ordering::less); // *NOPAD*
|
||||
CHECK((above_int64_max <=> max_int64) == std::partial_ordering::greater); // *NOPAD*
|
||||
}
|
||||
|
||||
SECTION("integer/float mixed comparison is exact")
|
||||
{
|
||||
// Widening the integer to a double loses precision past the
|
||||
// mantissa, so 2^63-2 and 2^63-1 both used to compare equal to the
|
||||
// double 2^63 while differing from each other. That makes equality
|
||||
// intransitive and the ordering not a strict weak ordering.
|
||||
const json below_two_63 = static_cast<std::int64_t>(9223372036854775806LL);
|
||||
const json max_int64 = (std::numeric_limits<std::int64_t>::max)();
|
||||
const json two_63 = 9223372036854775808.0;
|
||||
|
||||
// the same past the unsigned range
|
||||
const json max_uint64 = (std::numeric_limits<std::uint64_t>::max)();
|
||||
const json two_64 = 18446744073709551616.0;
|
||||
|
||||
CHECK((max_int64 <=> two_63) == std::partial_ordering::less); // *NOPAD*
|
||||
CHECK((two_63 <=> max_int64) == std::partial_ordering::greater); // *NOPAD*
|
||||
CHECK((below_two_63 <=> max_int64) == std::partial_ordering::less); // *NOPAD*
|
||||
CHECK((max_uint64 <=> two_64) == std::partial_ordering::less); // *NOPAD*
|
||||
CHECK((json(1) <=> json(1.0)) == std::partial_ordering::equivalent); // *NOPAD*
|
||||
CHECK((json(1) <=> json(nan)) == std::partial_ordering::unordered); // *NOPAD*
|
||||
}
|
||||
|
||||
SECTION("comparison: 3-way")
|
||||
{
|
||||
// doctest runs the REQUIRE in the test case body once per leaf section; keep it
|
||||
// here to run it as often as before the 3-way sections moved from unit-comparison.cpp
|
||||
REQUIRE(std::isnan(nan));
|
||||
|
||||
std::vector<std::vector<std::partial_ordering>> expected =
|
||||
{
|
||||
//0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
|
||||
{eq, eq, lt, lt, lt, lt, lt, lt, lt, lt, lt, lt, lt, lt, lt, lt, lt, lt, lt, lt, un, un}, // 0
|
||||
{eq, eq, lt, lt, lt, lt, lt, lt, lt, lt, lt, lt, lt, lt, lt, lt, lt, lt, lt, lt, un, un}, // 1
|
||||
{gt, gt, eq, lt, lt, lt, lt, lt, un, un, lt, lt, gt, gt, lt, lt, lt, lt, lt, lt, un, un}, // 2
|
||||
{gt, gt, gt, eq, gt, gt, gt, gt, un, un, lt, lt, gt, gt, lt, lt, lt, lt, lt, lt, un, un}, // 3
|
||||
{gt, gt, gt, lt, eq, lt, gt, lt, un, un, lt, lt, gt, gt, lt, lt, lt, lt, lt, lt, un, un}, // 4
|
||||
{gt, gt, gt, lt, gt, eq, gt, lt, un, un, lt, lt, gt, gt, lt, lt, lt, lt, lt, lt, un, un}, // 5
|
||||
{gt, gt, gt, lt, lt, lt, eq, lt, un, un, lt, lt, gt, gt, lt, lt, lt, lt, lt, lt, un, un}, // 6
|
||||
{gt, gt, gt, lt, gt, gt, gt, eq, un, un, lt, lt, gt, gt, lt, lt, lt, lt, lt, lt, un, un}, // 7
|
||||
{gt, gt, un, un, un, un, un, un, un, un, lt, lt, gt, gt, lt, lt, lt, lt, lt, lt, un, un}, // 8
|
||||
{gt, gt, un, un, un, un, un, un, un, un, lt, lt, gt, gt, lt, lt, lt, lt, lt, lt, un, un}, // 9
|
||||
{gt, gt, gt, gt, gt, gt, gt, gt, gt, gt, eq, gt, gt, gt, gt, gt, gt, gt, lt, lt, un, un}, // 10
|
||||
{gt, gt, gt, gt, gt, gt, gt, gt, gt, gt, lt, eq, gt, gt, gt, gt, gt, gt, lt, lt, un, un}, // 11
|
||||
{gt, gt, lt, lt, lt, lt, lt, lt, lt, lt, lt, lt, eq, gt, lt, lt, lt, lt, lt, lt, un, un}, // 12
|
||||
{gt, gt, lt, lt, lt, lt, lt, lt, lt, lt, lt, lt, lt, eq, lt, lt, lt, lt, lt, lt, un, un}, // 13
|
||||
{gt, gt, gt, gt, gt, gt, gt, gt, gt, gt, lt, lt, gt, gt, eq, lt, gt, gt, lt, lt, un, un}, // 14
|
||||
{gt, gt, gt, gt, gt, gt, gt, gt, gt, gt, lt, lt, gt, gt, gt, eq, gt, gt, lt, lt, un, un}, // 15
|
||||
{gt, gt, gt, gt, gt, gt, gt, gt, gt, gt, lt, lt, gt, gt, lt, lt, eq, gt, lt, lt, un, un}, // 16
|
||||
{gt, gt, gt, gt, gt, gt, gt, gt, gt, gt, lt, lt, gt, gt, lt, lt, lt, eq, lt, lt, un, un}, // 17
|
||||
{gt, gt, gt, gt, gt, gt, gt, gt, gt, gt, gt, gt, gt, gt, gt, gt, gt, gt, eq, lt, un, un}, // 18
|
||||
{gt, gt, gt, gt, gt, gt, gt, gt, gt, gt, gt, gt, gt, gt, gt, gt, gt, gt, gt, eq, un, un}, // 19
|
||||
{un, un, un, un, un, un, un, un, un, un, un, un, un, un, un, un, un, un, un, un, un, un}, // 20
|
||||
{un, un, un, un, un, un, un, un, un, un, un, un, un, un, un, un, un, un, un, un, un, un}, // 21
|
||||
};
|
||||
|
||||
// check expected partial_ordering against expected booleans
|
||||
REQUIRE(expected.size() == expected_eq.size());
|
||||
REQUIRE(expected.size() == expected_lt.size());
|
||||
for (size_t i = 0; i < expected.size(); ++i)
|
||||
{
|
||||
REQUIRE(expected[i].size() == expected_eq[i].size());
|
||||
REQUIRE(expected[i].size() == expected_lt[i].size());
|
||||
for (size_t j = 0; j < expected[i].size(); ++j)
|
||||
{
|
||||
CAPTURE(i)
|
||||
CAPTURE(j)
|
||||
CHECK(std::is_eq(expected[i][j]) == expected_eq[i][j]);
|
||||
CHECK(std::is_lt(expected[i][j]) == expected_lt[i][j]);
|
||||
if (std::is_gt(expected[i][j]))
|
||||
{
|
||||
CHECK((!expected_eq[i][j] && !expected_lt[i][j]));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// check that two values compare according to their expected ordering
|
||||
REQUIRE(expected.size() == j_values.size());
|
||||
for (size_t i = 0; i < j_values.size(); ++i)
|
||||
{
|
||||
REQUIRE(expected[i].size() == j_values.size());
|
||||
for (size_t j = 0; j < j_values.size(); ++j)
|
||||
{
|
||||
CAPTURE(i)
|
||||
CAPTURE(j)
|
||||
CHECK((j_values[i] <=> j_values[j]) == expected[i][j]); // *NOPAD*
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
TEST_CASE("regression #3868 - heterogeneous comparisons compile under C++20 (P2468R2)")
|
||||
{
|
||||
// Issue #3868: operator!= was preventing compiler from synthesizing reversed
|
||||
// operator== candidates under C++20's P2468R2 rewritten candidate rules.
|
||||
// Verify that heterogeneous comparisons now work.
|
||||
|
||||
SECTION("string vs json")
|
||||
{
|
||||
std::string s = "string";
|
||||
json j = "string";
|
||||
CHECK(s == j);
|
||||
CHECK(j == s);
|
||||
CHECK_FALSE(s != j);
|
||||
CHECK_FALSE(j != s);
|
||||
}
|
||||
|
||||
SECTION("other heterogeneous types")
|
||||
{
|
||||
int i = 42;
|
||||
json j = 42;
|
||||
CHECK(i == j);
|
||||
CHECK(j == i);
|
||||
CHECK_FALSE(i != j);
|
||||
CHECK_FALSE(j != i);
|
||||
}
|
||||
}
|
||||
|
||||
#if JSON_USE_LEGACY_DISCARDED_VALUE_COMPARISON
|
||||
TEST_CASE("regression #5665 - scalar <= discarded and scalar >= discarded in C++20 legacy mode")
|
||||
{
|
||||
// Issue #5665: with a scalar on the left-hand side, <= and >= only had the
|
||||
// candidate rewritten from operator<=>, which does not emulate the legacy
|
||||
// discarded-value behavior. Check that scalar-on-the-left now matches the
|
||||
// other three operand orders.
|
||||
const json discarded(json::value_t::discarded);
|
||||
const json one = 1;
|
||||
|
||||
CHECK(discarded <= 1);
|
||||
CHECK(discarded >= 1);
|
||||
CHECK(one <= discarded);
|
||||
CHECK(one >= discarded);
|
||||
CHECK(1 <= discarded);
|
||||
CHECK(1 >= discarded);
|
||||
CHECK(1.5 <= discarded);
|
||||
CHECK(1.5 >= discarded);
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
TEST_CASE("containers are compared element by element (C++20)")
|
||||
{
|
||||
// Containers nested deeper than a bound are compared without the call
|
||||
// stack, by code of their own; every relation is checked both at the top
|
||||
// level and below that bound.
|
||||
const auto deep = [](const json & j, const std::size_t depth)
|
||||
{
|
||||
json result = j;
|
||||
for (std::size_t i = 0; i < depth; ++i)
|
||||
{
|
||||
result = json::array({std::move(result)});
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
for (const std::size_t depth : std::vector<std::size_t> {0, 200})
|
||||
{
|
||||
CAPTURE(depth)
|
||||
|
||||
// objects with different keys
|
||||
{
|
||||
const json a = deep({{"a", 1}}, depth);
|
||||
const json b = deep({{"b", 1}}, depth);
|
||||
CHECK((a <=> b) == std::partial_ordering::less); // *NOPAD*
|
||||
CHECK((b <=> a) == std::partial_ordering::greater); // *NOPAD*
|
||||
CHECK((a <=> a) == std::partial_ordering::equivalent); // *NOPAD*
|
||||
}
|
||||
|
||||
// a container that is a prefix of the other one
|
||||
{
|
||||
// the one that runs out of elements first is the smaller one
|
||||
const json shorter = deep({1}, depth);
|
||||
const json longer = deep({1, 2}, depth);
|
||||
|
||||
const json smaller_object = deep({{"a", 1}}, depth);
|
||||
const json larger_object = deep({{"a", 1}, {"b", 2}}, depth);
|
||||
CHECK((shorter <=> longer) == std::partial_ordering::less); // *NOPAD*
|
||||
CHECK((longer <=> shorter) == std::partial_ordering::greater); // *NOPAD*
|
||||
}
|
||||
|
||||
// elements that cannot be ordered
|
||||
{
|
||||
const double nan = std::numeric_limits<double>::quiet_NaN();
|
||||
const json lhs = deep({nan, 1}, depth);
|
||||
const json rhs = deep({nan, 2}, depth);
|
||||
// operator<=> stops there, as std::lexicographical_compare_three_way
|
||||
// does, and operator< is derived from it
|
||||
CHECK((lhs <=> rhs) == std::partial_ordering::unordered); // *NOPAD*
|
||||
CHECK_FALSE(lhs < rhs);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("operator<=> of binary values with a different subtype does not depend on nesting depth")
|
||||
{
|
||||
// #5654: std::vector<std::uint8_t>::operator<=>, which the binary type's
|
||||
// own operator<=> uses, ignores the subtype that operator== checks. So a
|
||||
// pair of binary values with the same bytes but a different subtype is
|
||||
// unequal, yet <=>-equivalent - the same inconsistency between == and <=>
|
||||
// that a NaN has. Within the nesting bound, an array compares itself
|
||||
// with std::vector's own operator<=>, which treats an equivalent pair as
|
||||
// undecided and lets the next element decide, same as
|
||||
// std::lexicographical_compare_three_way does. Past the bound,
|
||||
// compare_iteratively<true>() takes over and must classify the pair the
|
||||
// same way, or the result of operator<=> - and of <, which C++20 derives
|
||||
// from it - depends on how deeply the values are nested.
|
||||
const json a = json::array({json::binary({1}, 1), 1});
|
||||
const json b = json::array({json::binary({1}, 2), 2});
|
||||
|
||||
// the root inconsistency: unequal, yet <=>-equivalent
|
||||
CHECK_FALSE(a[0] == b[0]);
|
||||
CHECK((a[0] <=> b[0]) == std::partial_ordering::equivalent); // *NOPAD*
|
||||
|
||||
const auto deep = [](const json & j, const std::size_t depth)
|
||||
{
|
||||
json result = j;
|
||||
for (std::size_t i = 0; i < depth; ++i)
|
||||
{
|
||||
result = json::array({std::move(result)});
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
// 127 levels stay within nesting_depth_limit() (128); 128 and 200 do not,
|
||||
// and must still agree with the levels that do
|
||||
for (const std::size_t depth : std::vector<std::size_t> {0, 127, 128, 200})
|
||||
{
|
||||
CAPTURE(depth)
|
||||
const json x = deep(a, depth);
|
||||
const json y = deep(b, depth);
|
||||
CHECK((x <=> y) == std::partial_ordering::less); // *NOPAD*
|
||||
CHECK((y <=> x) == std::partial_ordering::greater); // *NOPAD*
|
||||
CHECK(x < y);
|
||||
CHECK(y > x);
|
||||
CHECK_FALSE(y < x);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("comparison of scalars (inline fast path): operator<=>")
|
||||
{
|
||||
// the three-way part of "comparison of scalars (inline fast path)" in
|
||||
// unit-comparison.cpp, which is built for C++11 only
|
||||
const json::number_integer_t neg = -1;
|
||||
|
||||
SECTION("NaN")
|
||||
{
|
||||
const json nan = std::numeric_limits<json::number_float_t>::quiet_NaN();
|
||||
const json one = 1.0;
|
||||
CHECK((nan <=> nan) == std::partial_ordering::unordered); // *NOPAD*
|
||||
CHECK((nan <=> one) == std::partial_ordering::unordered); // *NOPAD*
|
||||
CHECK((one <=> nan) == std::partial_ordering::unordered); // *NOPAD*
|
||||
CHECK((one <=> json(2.0)) == std::partial_ordering::less); // *NOPAD*
|
||||
CHECK((json(2.0) <=> one) == std::partial_ordering::greater); // *NOPAD*
|
||||
CHECK((one <=> one) == std::partial_ordering::equivalent); // *NOPAD*
|
||||
CHECK((json(neg) <=> json(1u)) == std::partial_ordering::less); // *NOPAD*
|
||||
CHECK((json(nullptr) <=> json(nullptr)) == std::partial_ordering::equivalent); // *NOPAD*
|
||||
CHECK((json(true) <=> json(false)) == std::partial_ordering::greater); // *NOPAD*
|
||||
}
|
||||
|
||||
SECTION("discarded")
|
||||
{
|
||||
const json d(json::value_t::discarded);
|
||||
CHECK((d <=> d) == std::partial_ordering::unordered); // *NOPAD*
|
||||
CHECK((d <=> json(1)) == std::partial_ordering::unordered); // *NOPAD*
|
||||
CHECK((json(1) <=> d) == std::partial_ordering::unordered); // *NOPAD*
|
||||
}
|
||||
}
|
||||
|
||||
#endif // JSON_HAS_THREE_WAY_COMPARISON
|
||||
#endif
|
||||
+59
-356
@@ -6,12 +6,11 @@
|
||||
// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann <https://nlohmann.me>
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
// cmake/test.cmake selects the C++ standard versions with which to build a
|
||||
// unit test based on the presence of JSON_HAS_CPP_<VERSION> macros.
|
||||
// When using macros that are only defined for particular versions of the standard
|
||||
// (e.g., JSON_HAS_FILESYSTEM for C++17 and up), please mention the corresponding
|
||||
// version macro in a comment close by, like this:
|
||||
// JSON_HAS_CPP_<VERSION> (do not remove; see note at top of file)
|
||||
// cmake/test.cmake builds a unit test with C++ standards beyond C++11 only if the
|
||||
// source file mentions the corresponding version macro. To avoid rebuilding this
|
||||
// large file for every standard, tests that depend on the standard version (e.g.,
|
||||
// those using JSON_HAS_FILESYSTEM, JSON_HAS_RANGES, or JSON_HAS_THREE_WAY_COMPARISON)
|
||||
// go into a separate file unit-comparison-cpp<NN>.cpp. This file stays C++11-only.
|
||||
|
||||
#include "doctest_compatibility.h"
|
||||
|
||||
@@ -29,37 +28,6 @@
|
||||
#include <nlohmann/json.hpp>
|
||||
using nlohmann::json;
|
||||
|
||||
#if JSON_HAS_THREE_WAY_COMPARISON
|
||||
// this can be replaced with the doctest stl extension header in version 2.5
|
||||
namespace doctest
|
||||
{
|
||||
template<> struct StringMaker<std::partial_ordering>
|
||||
{
|
||||
static String convert(const std::partial_ordering& order)
|
||||
{
|
||||
if (order == std::partial_ordering::less)
|
||||
{
|
||||
return "std::partial_ordering::less";
|
||||
}
|
||||
if (order == std::partial_ordering::equivalent)
|
||||
{
|
||||
return "std::partial_ordering::equivalent";
|
||||
}
|
||||
if (order == std::partial_ordering::greater)
|
||||
{
|
||||
return "std::partial_ordering::greater";
|
||||
}
|
||||
if (order == std::partial_ordering::unordered)
|
||||
{
|
||||
return "std::partial_ordering::unordered";
|
||||
}
|
||||
return "{?}";
|
||||
}
|
||||
};
|
||||
} // namespace doctest
|
||||
|
||||
#endif
|
||||
|
||||
namespace
|
||||
{
|
||||
// helper function to check std::less<json::value_t>
|
||||
@@ -76,16 +44,6 @@ TEST_CASE("lexicographical comparison operators")
|
||||
constexpr auto f_ = false;
|
||||
constexpr auto _t = true;
|
||||
constexpr auto nan = std::numeric_limits<json::number_float_t>::quiet_NaN();
|
||||
#if JSON_HAS_THREE_WAY_COMPARISON
|
||||
constexpr auto lt = std::partial_ordering::less;
|
||||
constexpr auto gt = std::partial_ordering::greater;
|
||||
constexpr auto eq = std::partial_ordering::equivalent;
|
||||
constexpr auto un = std::partial_ordering::unordered;
|
||||
#endif
|
||||
|
||||
#if JSON_HAS_THREE_WAY_COMPARISON
|
||||
INFO("using 3-way comparison");
|
||||
#endif
|
||||
|
||||
#if JSON_USE_LEGACY_DISCARDED_VALUE_COMPARISON
|
||||
INFO("using legacy comparison");
|
||||
@@ -136,62 +94,13 @@ TEST_CASE("lexicographical comparison operators")
|
||||
CAPTURE(i)
|
||||
CAPTURE(j)
|
||||
// check precomputed values
|
||||
#if JSON_HAS_THREE_WAY_COMPARISON
|
||||
// JSON_HAS_CPP_20 (do not remove; see note at top of file)
|
||||
CHECK((j_types[i] < j_types[j]) == expected_lt[i][j]);
|
||||
#else
|
||||
#if !JSON_HAS_THREE_WAY_COMPARISON
|
||||
CHECK(operator<(j_types[i], j_types[j]) == expected_lt[i][j]);
|
||||
#endif
|
||||
CHECK(f(j_types[i], j_types[j]) == expected_lt[i][j]);
|
||||
}
|
||||
}
|
||||
}
|
||||
#if JSON_HAS_THREE_WAY_COMPARISON
|
||||
// JSON_HAS_CPP_20 (do not remove; see note at top of file)
|
||||
SECTION("comparison: 3-way")
|
||||
{
|
||||
std::vector<std::vector<std::partial_ordering>> expected =
|
||||
{
|
||||
//0 1 2 3 4 5 6 7 8 9
|
||||
{eq, lt, lt, lt, lt, lt, lt, lt, lt, un}, // 0
|
||||
{gt, eq, lt, lt, lt, lt, lt, lt, lt, un}, // 1
|
||||
{gt, gt, eq, eq, eq, lt, lt, lt, lt, un}, // 2
|
||||
{gt, gt, eq, eq, eq, lt, lt, lt, lt, un}, // 3
|
||||
{gt, gt, eq, eq, eq, lt, lt, lt, lt, un}, // 4
|
||||
{gt, gt, gt, gt, gt, eq, lt, lt, lt, un}, // 5
|
||||
{gt, gt, gt, gt, gt, gt, eq, lt, lt, un}, // 6
|
||||
{gt, gt, gt, gt, gt, gt, gt, eq, lt, un}, // 7
|
||||
{gt, gt, gt, gt, gt, gt, gt, gt, eq, un}, // 8
|
||||
{un, un, un, un, un, un, un, un, un, un}, // 9
|
||||
};
|
||||
|
||||
// check expected partial_ordering against expected boolean
|
||||
REQUIRE(expected.size() == expected_lt.size());
|
||||
for (size_t i = 0; i < expected.size(); ++i)
|
||||
{
|
||||
REQUIRE(expected[i].size() == expected_lt[i].size());
|
||||
for (size_t j = 0; j < expected[i].size(); ++j)
|
||||
{
|
||||
CAPTURE(i)
|
||||
CAPTURE(j)
|
||||
CHECK(std::is_lt(expected[i][j]) == expected_lt[i][j]);
|
||||
}
|
||||
}
|
||||
|
||||
// check 3-way comparison against expected partial_ordering
|
||||
REQUIRE(expected.size() == j_types.size());
|
||||
for (size_t i = 0; i < j_types.size(); ++i)
|
||||
{
|
||||
REQUIRE(expected[i].size() == j_types.size());
|
||||
for (size_t j = 0; j < j_types.size(); ++j)
|
||||
{
|
||||
CAPTURE(i)
|
||||
CAPTURE(j)
|
||||
CHECK((j_types[i] <=> j_types[j]) == expected[i][j]); // *NOPAD*
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
SECTION("values")
|
||||
@@ -320,18 +229,6 @@ TEST_CASE("lexicographical comparison operators")
|
||||
CHECK_FALSE(above_int64_max <= max_int64);
|
||||
CHECK(above_int64_max > max_int64);
|
||||
CHECK(above_int64_max >= max_int64);
|
||||
|
||||
#if JSON_HAS_THREE_WAY_COMPARISON
|
||||
// JSON_HAS_CPP_20 (do not remove; see note at top of file)
|
||||
CHECK((negative_one <=> above_int64_max) == std::partial_ordering::less); // *NOPAD*
|
||||
CHECK((above_int64_max <=> negative_one) == std::partial_ordering::greater); // *NOPAD*
|
||||
CHECK((negative_one <=> max_uint64) == std::partial_ordering::less); // *NOPAD*
|
||||
CHECK((max_uint64 <=> negative_one) == std::partial_ordering::greater); // *NOPAD*
|
||||
CHECK((one <=> above_int64_max) == std::partial_ordering::less); // *NOPAD*
|
||||
CHECK((above_int64_max <=> one) == std::partial_ordering::greater); // *NOPAD*
|
||||
CHECK((max_int64 <=> above_int64_max) == std::partial_ordering::less); // *NOPAD*
|
||||
CHECK((above_int64_max <=> max_int64) == std::partial_ordering::greater); // *NOPAD*
|
||||
#endif
|
||||
}
|
||||
|
||||
SECTION("integer/float mixed comparison is exact")
|
||||
@@ -382,16 +279,6 @@ TEST_CASE("lexicographical comparison operators")
|
||||
CHECK_FALSE(json(1) < json(nan));
|
||||
CHECK_FALSE(json(nan) < json(1));
|
||||
CHECK_FALSE(json(1u) == json(nan));
|
||||
|
||||
#if JSON_HAS_THREE_WAY_COMPARISON
|
||||
// JSON_HAS_CPP_20 (do not remove; see note at top of file)
|
||||
CHECK((max_int64 <=> two_63) == std::partial_ordering::less); // *NOPAD*
|
||||
CHECK((two_63 <=> max_int64) == std::partial_ordering::greater); // *NOPAD*
|
||||
CHECK((below_two_63 <=> max_int64) == std::partial_ordering::less); // *NOPAD*
|
||||
CHECK((max_uint64 <=> two_64) == std::partial_ordering::less); // *NOPAD*
|
||||
CHECK((json(1) <=> json(1.0)) == std::partial_ordering::equivalent); // *NOPAD*
|
||||
CHECK((json(1) <=> json(nan)) == std::partial_ordering::unordered); // *NOPAD*
|
||||
#endif
|
||||
}
|
||||
|
||||
SECTION("compares unordered")
|
||||
@@ -618,72 +505,6 @@ TEST_CASE("lexicographical comparison operators")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#if JSON_HAS_THREE_WAY_COMPARISON
|
||||
// JSON_HAS_CPP_20 (do not remove; see note at top of file)
|
||||
SECTION("comparison: 3-way")
|
||||
{
|
||||
std::vector<std::vector<std::partial_ordering>> expected =
|
||||
{
|
||||
//0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
|
||||
{eq, eq, lt, lt, lt, lt, lt, lt, lt, lt, lt, lt, lt, lt, lt, lt, lt, lt, lt, lt, un, un}, // 0
|
||||
{eq, eq, lt, lt, lt, lt, lt, lt, lt, lt, lt, lt, lt, lt, lt, lt, lt, lt, lt, lt, un, un}, // 1
|
||||
{gt, gt, eq, lt, lt, lt, lt, lt, un, un, lt, lt, gt, gt, lt, lt, lt, lt, lt, lt, un, un}, // 2
|
||||
{gt, gt, gt, eq, gt, gt, gt, gt, un, un, lt, lt, gt, gt, lt, lt, lt, lt, lt, lt, un, un}, // 3
|
||||
{gt, gt, gt, lt, eq, lt, gt, lt, un, un, lt, lt, gt, gt, lt, lt, lt, lt, lt, lt, un, un}, // 4
|
||||
{gt, gt, gt, lt, gt, eq, gt, lt, un, un, lt, lt, gt, gt, lt, lt, lt, lt, lt, lt, un, un}, // 5
|
||||
{gt, gt, gt, lt, lt, lt, eq, lt, un, un, lt, lt, gt, gt, lt, lt, lt, lt, lt, lt, un, un}, // 6
|
||||
{gt, gt, gt, lt, gt, gt, gt, eq, un, un, lt, lt, gt, gt, lt, lt, lt, lt, lt, lt, un, un}, // 7
|
||||
{gt, gt, un, un, un, un, un, un, un, un, lt, lt, gt, gt, lt, lt, lt, lt, lt, lt, un, un}, // 8
|
||||
{gt, gt, un, un, un, un, un, un, un, un, lt, lt, gt, gt, lt, lt, lt, lt, lt, lt, un, un}, // 9
|
||||
{gt, gt, gt, gt, gt, gt, gt, gt, gt, gt, eq, gt, gt, gt, gt, gt, gt, gt, lt, lt, un, un}, // 10
|
||||
{gt, gt, gt, gt, gt, gt, gt, gt, gt, gt, lt, eq, gt, gt, gt, gt, gt, gt, lt, lt, un, un}, // 11
|
||||
{gt, gt, lt, lt, lt, lt, lt, lt, lt, lt, lt, lt, eq, gt, lt, lt, lt, lt, lt, lt, un, un}, // 12
|
||||
{gt, gt, lt, lt, lt, lt, lt, lt, lt, lt, lt, lt, lt, eq, lt, lt, lt, lt, lt, lt, un, un}, // 13
|
||||
{gt, gt, gt, gt, gt, gt, gt, gt, gt, gt, lt, lt, gt, gt, eq, lt, gt, gt, lt, lt, un, un}, // 14
|
||||
{gt, gt, gt, gt, gt, gt, gt, gt, gt, gt, lt, lt, gt, gt, gt, eq, gt, gt, lt, lt, un, un}, // 15
|
||||
{gt, gt, gt, gt, gt, gt, gt, gt, gt, gt, lt, lt, gt, gt, lt, lt, eq, gt, lt, lt, un, un}, // 16
|
||||
{gt, gt, gt, gt, gt, gt, gt, gt, gt, gt, lt, lt, gt, gt, lt, lt, lt, eq, lt, lt, un, un}, // 17
|
||||
{gt, gt, gt, gt, gt, gt, gt, gt, gt, gt, gt, gt, gt, gt, gt, gt, gt, gt, eq, lt, un, un}, // 18
|
||||
{gt, gt, gt, gt, gt, gt, gt, gt, gt, gt, gt, gt, gt, gt, gt, gt, gt, gt, gt, eq, un, un}, // 19
|
||||
{un, un, un, un, un, un, un, un, un, un, un, un, un, un, un, un, un, un, un, un, un, un}, // 20
|
||||
{un, un, un, un, un, un, un, un, un, un, un, un, un, un, un, un, un, un, un, un, un, un}, // 21
|
||||
};
|
||||
|
||||
// check expected partial_ordering against expected booleans
|
||||
REQUIRE(expected.size() == expected_eq.size());
|
||||
REQUIRE(expected.size() == expected_lt.size());
|
||||
for (size_t i = 0; i < expected.size(); ++i)
|
||||
{
|
||||
REQUIRE(expected[i].size() == expected_eq[i].size());
|
||||
REQUIRE(expected[i].size() == expected_lt[i].size());
|
||||
for (size_t j = 0; j < expected[i].size(); ++j)
|
||||
{
|
||||
CAPTURE(i)
|
||||
CAPTURE(j)
|
||||
CHECK(std::is_eq(expected[i][j]) == expected_eq[i][j]);
|
||||
CHECK(std::is_lt(expected[i][j]) == expected_lt[i][j]);
|
||||
if (std::is_gt(expected[i][j]))
|
||||
{
|
||||
CHECK((!expected_eq[i][j] && !expected_lt[i][j]));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// check that two values compare according to their expected ordering
|
||||
REQUIRE(expected.size() == j_values.size());
|
||||
for (size_t i = 0; i < j_values.size(); ++i)
|
||||
{
|
||||
REQUIRE(expected[i].size() == j_values.size());
|
||||
for (size_t j = 0; j < j_values.size(); ++j)
|
||||
{
|
||||
CAPTURE(i)
|
||||
CAPTURE(j)
|
||||
CHECK((j_values[i] <=> j_values[j]) == expected[i][j]); // *NOPAD*
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
#if JSON_USE_LEGACY_DISCARDED_VALUE_COMPARISON
|
||||
@@ -722,60 +543,7 @@ TEST_CASE("lexicographical comparison operators")
|
||||
#endif
|
||||
}
|
||||
|
||||
#if JSON_HAS_THREE_WAY_COMPARISON
|
||||
// JSON_HAS_CPP_20 (do not remove; see note at top of file)
|
||||
|
||||
TEST_CASE("regression #3868 - heterogeneous comparisons compile under C++20 (P2468R2)")
|
||||
{
|
||||
// Issue #3868: operator!= was preventing compiler from synthesizing reversed
|
||||
// operator== candidates under C++20's P2468R2 rewritten candidate rules.
|
||||
// Verify that heterogeneous comparisons now work.
|
||||
|
||||
SECTION("string vs json")
|
||||
{
|
||||
std::string s = "string";
|
||||
json j = "string";
|
||||
CHECK(s == j);
|
||||
CHECK(j == s);
|
||||
CHECK_FALSE(s != j);
|
||||
CHECK_FALSE(j != s);
|
||||
}
|
||||
|
||||
SECTION("other heterogeneous types")
|
||||
{
|
||||
int i = 42;
|
||||
json j = 42;
|
||||
CHECK(i == j);
|
||||
CHECK(j == i);
|
||||
CHECK_FALSE(i != j);
|
||||
CHECK_FALSE(j != i);
|
||||
}
|
||||
}
|
||||
|
||||
#if JSON_USE_LEGACY_DISCARDED_VALUE_COMPARISON
|
||||
TEST_CASE("regression #5665 - scalar <= discarded and scalar >= discarded in C++20 legacy mode")
|
||||
{
|
||||
// Issue #5665: with a scalar on the left-hand side, <= and >= only had the
|
||||
// candidate rewritten from operator<=>, which does not emulate the legacy
|
||||
// discarded-value behavior. Check that scalar-on-the-left now matches the
|
||||
// other three operand orders.
|
||||
const json discarded(json::value_t::discarded);
|
||||
const json one = 1;
|
||||
|
||||
CHECK(discarded <= 1);
|
||||
CHECK(discarded >= 1);
|
||||
CHECK(one <= discarded);
|
||||
CHECK(one >= discarded);
|
||||
CHECK(1 <= discarded);
|
||||
CHECK(1 >= discarded);
|
||||
CHECK(1.5 <= discarded);
|
||||
CHECK(1.5 >= discarded);
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif
|
||||
|
||||
namespace
|
||||
namespace unit_comparison_detail
|
||||
{
|
||||
// orders keys ascending or descending, as chosen when a map is created
|
||||
template<class Key>
|
||||
@@ -827,29 +595,6 @@ struct unordered_object_t : std::map<Key, Value, directed_less<Key>, Allocator>
|
||||
};
|
||||
using unordered_json = nlohmann::json::with_object_t<unordered_object_t>;
|
||||
|
||||
// the entries "0" to "9", enumerated in ascending or in descending order
|
||||
unordered_json make_unordered_object(const bool descending)
|
||||
{
|
||||
unordered_json j = unordered_json::object_t(directed_less<std::string>(descending));
|
||||
for (int i = 0; i < 10; ++i)
|
||||
{
|
||||
j[std::to_string(i)] = i;
|
||||
}
|
||||
return j;
|
||||
}
|
||||
|
||||
template<typename Json>
|
||||
Json nest(Json j, const std::size_t depth)
|
||||
{
|
||||
for (std::size_t i = 0; i < depth; ++i)
|
||||
{
|
||||
Json outer = Json::object();
|
||||
outer["x"] = std::move(j);
|
||||
j = std::move(outer);
|
||||
}
|
||||
return j;
|
||||
}
|
||||
|
||||
// a std::map comparator with state: case-insensitive, unless constructed
|
||||
// case-sensitive. Used to check that copying an object copies the original's
|
||||
// comparator rather than default-constructing a new one (see #5649).
|
||||
@@ -878,18 +623,6 @@ template<class Key, class Value, class /*Compare*/, class Allocator>
|
||||
using key_case_map = std::map<Key, Value, key_case_less, Allocator>;
|
||||
using key_case_json = nlohmann::json::with_object_t<key_case_map>;
|
||||
|
||||
// the innermost value of a chain of single-element arrays
|
||||
template<typename Json>
|
||||
const Json& innermost(const Json& j)
|
||||
{
|
||||
const Json* p = &j;
|
||||
while (p->is_array())
|
||||
{
|
||||
p = &(*p)[0];
|
||||
}
|
||||
return *p;
|
||||
}
|
||||
|
||||
// orders keys case-insensitively, so "key" and "KEY" compare equivalent
|
||||
// (neither less than the other) although they are not equal
|
||||
struct case_insensitive_less
|
||||
@@ -907,7 +640,58 @@ struct case_insensitive_less
|
||||
template<class Key, class Value, class /*Compare*/, class Allocator>
|
||||
using case_insensitive_map = std::map<Key, Value, case_insensitive_less, Allocator>;
|
||||
using ci_json = nlohmann::json::with_object_t<case_insensitive_map>;
|
||||
|
||||
// the types above keep external linkage: a basic_json specialized with a type
|
||||
// of an anonymous namespace has internal linkage, which GCC reports in a file
|
||||
// #include-d into a batch (-Wsubobject-linkage); the functions need none
|
||||
namespace
|
||||
{
|
||||
// the entries "0" to "9", enumerated in ascending or in descending order
|
||||
unordered_json make_unordered_object(const bool descending)
|
||||
{
|
||||
unordered_json j = unordered_json::object_t(directed_less<std::string>(descending));
|
||||
for (int i = 0; i < 10; ++i)
|
||||
{
|
||||
j[std::to_string(i)] = i;
|
||||
}
|
||||
return j;
|
||||
}
|
||||
|
||||
template<typename Json>
|
||||
Json nest(Json j, const std::size_t depth)
|
||||
{
|
||||
for (std::size_t i = 0; i < depth; ++i)
|
||||
{
|
||||
Json outer = Json::object();
|
||||
outer["x"] = std::move(j);
|
||||
j = std::move(outer);
|
||||
}
|
||||
return j;
|
||||
}
|
||||
|
||||
// the innermost value of a chain of single-element arrays
|
||||
template<typename Json>
|
||||
const Json& innermost(const Json& j)
|
||||
{
|
||||
const Json* p = &j;
|
||||
while (p->is_array())
|
||||
{
|
||||
p = &(*p)[0];
|
||||
}
|
||||
return *p;
|
||||
}
|
||||
} // namespace
|
||||
} // namespace unit_comparison_detail
|
||||
|
||||
// using-declarations rather than a using-directive, as this file may be
|
||||
// #include-d into a batch with other test files (JSON_TestUnityBuild)
|
||||
using unit_comparison_detail::ci_json;
|
||||
using unit_comparison_detail::innermost;
|
||||
using unit_comparison_detail::key_case_json;
|
||||
using unit_comparison_detail::key_case_less;
|
||||
using unit_comparison_detail::make_unordered_object;
|
||||
using unit_comparison_detail::nest;
|
||||
using unit_comparison_detail::unordered_json;
|
||||
|
||||
TEST_CASE("equality of objects whose entries have no fixed order")
|
||||
{
|
||||
@@ -1050,12 +834,6 @@ TEST_CASE("containers are compared element by element")
|
||||
CHECK(a < b);
|
||||
CHECK(b > a);
|
||||
CHECK_FALSE(b < a);
|
||||
#if JSON_HAS_THREE_WAY_COMPARISON
|
||||
// JSON_HAS_CPP_20 (do not remove; see note at top of file)
|
||||
CHECK((a <=> b) == std::partial_ordering::less); // *NOPAD*
|
||||
CHECK((b <=> a) == std::partial_ordering::greater); // *NOPAD*
|
||||
CHECK((a <=> a) == std::partial_ordering::equivalent); // *NOPAD*
|
||||
#endif
|
||||
}
|
||||
|
||||
// a container that is a prefix of the other one
|
||||
@@ -1073,11 +851,6 @@ TEST_CASE("containers are compared element by element")
|
||||
CHECK(smaller_object < larger_object);
|
||||
CHECK(larger_object > smaller_object);
|
||||
CHECK_FALSE(smaller_object == larger_object);
|
||||
#if JSON_HAS_THREE_WAY_COMPARISON
|
||||
// JSON_HAS_CPP_20 (do not remove; see note at top of file)
|
||||
CHECK((shorter <=> longer) == std::partial_ordering::less); // *NOPAD*
|
||||
CHECK((longer <=> shorter) == std::partial_ordering::greater); // *NOPAD*
|
||||
#endif
|
||||
}
|
||||
|
||||
// elements that cannot be ordered
|
||||
@@ -1088,13 +861,7 @@ TEST_CASE("containers are compared element by element")
|
||||
|
||||
CHECK_FALSE(lhs == lhs);
|
||||
CHECK_FALSE(rhs < lhs);
|
||||
#if JSON_HAS_THREE_WAY_COMPARISON
|
||||
// JSON_HAS_CPP_20 (do not remove; see note at top of file)
|
||||
// operator<=> stops there, as std::lexicographical_compare_three_way
|
||||
// does, and operator< is derived from it
|
||||
CHECK((lhs <=> rhs) == std::partial_ordering::unordered); // *NOPAD*
|
||||
CHECK_FALSE(lhs < rhs);
|
||||
#else
|
||||
#if !JSON_HAS_THREE_WAY_COMPARISON
|
||||
// operator< skips a pair of elements that cannot be ordered, as
|
||||
// std::lexicographical_compare does, and the next pair decides
|
||||
CHECK(lhs < rhs);
|
||||
@@ -1103,54 +870,6 @@ TEST_CASE("containers are compared element by element")
|
||||
}
|
||||
}
|
||||
|
||||
#if JSON_HAS_THREE_WAY_COMPARISON
|
||||
// JSON_HAS_CPP_20 (do not remove; see note at top of file)
|
||||
TEST_CASE("operator<=> of binary values with a different subtype does not depend on nesting depth")
|
||||
{
|
||||
// #5654: std::vector<std::uint8_t>::operator<=>, which the binary type's
|
||||
// own operator<=> uses, ignores the subtype that operator== checks. So a
|
||||
// pair of binary values with the same bytes but a different subtype is
|
||||
// unequal, yet <=>-equivalent - the same inconsistency between == and <=>
|
||||
// that a NaN has. Within the nesting bound, an array compares itself
|
||||
// with std::vector's own operator<=>, which treats an equivalent pair as
|
||||
// undecided and lets the next element decide, same as
|
||||
// std::lexicographical_compare_three_way does. Past the bound,
|
||||
// compare_iteratively<true>() takes over and must classify the pair the
|
||||
// same way, or the result of operator<=> - and of <, which C++20 derives
|
||||
// from it - depends on how deeply the values are nested.
|
||||
const json a = json::array({json::binary({1}, 1), 1});
|
||||
const json b = json::array({json::binary({1}, 2), 2});
|
||||
|
||||
// the root inconsistency: unequal, yet <=>-equivalent
|
||||
CHECK_FALSE(a[0] == b[0]);
|
||||
CHECK((a[0] <=> b[0]) == std::partial_ordering::equivalent); // *NOPAD*
|
||||
|
||||
const auto deep = [](const json & j, const std::size_t depth)
|
||||
{
|
||||
json result = j;
|
||||
for (std::size_t i = 0; i < depth; ++i)
|
||||
{
|
||||
result = json::array({std::move(result)});
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
// 127 levels stay within nesting_depth_limit() (128); 128 and 200 do not,
|
||||
// and must still agree with the levels that do
|
||||
for (const std::size_t depth : std::vector<std::size_t> {0, 127, 128, 200})
|
||||
{
|
||||
CAPTURE(depth)
|
||||
const json x = deep(a, depth);
|
||||
const json y = deep(b, depth);
|
||||
CHECK((x <=> y) == std::partial_ordering::less); // *NOPAD*
|
||||
CHECK((y <=> x) == std::partial_ordering::greater); // *NOPAD*
|
||||
CHECK(x < y);
|
||||
CHECK(y > x);
|
||||
CHECK_FALSE(y < x);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
TEST_CASE("comparison of scalars (inline fast path)")
|
||||
{
|
||||
const json::number_integer_t neg = -1;
|
||||
@@ -1249,17 +968,6 @@ TEST_CASE("comparison of scalars (inline fast path)")
|
||||
CHECK_FALSE(nan < json(1u));
|
||||
CHECK_FALSE(json(1u) < nan);
|
||||
|
||||
#if JSON_HAS_THREE_WAY_COMPARISON
|
||||
CHECK((nan <=> nan) == std::partial_ordering::unordered); // *NOPAD*
|
||||
CHECK((nan <=> one) == std::partial_ordering::unordered); // *NOPAD*
|
||||
CHECK((one <=> nan) == std::partial_ordering::unordered); // *NOPAD*
|
||||
CHECK((one <=> json(2.0)) == std::partial_ordering::less); // *NOPAD*
|
||||
CHECK((json(2.0) <=> one) == std::partial_ordering::greater); // *NOPAD*
|
||||
CHECK((one <=> one) == std::partial_ordering::equivalent); // *NOPAD*
|
||||
CHECK((json(neg) <=> json(1u)) == std::partial_ordering::less); // *NOPAD*
|
||||
CHECK((json(nullptr) <=> json(nullptr)) == std::partial_ordering::equivalent); // *NOPAD*
|
||||
CHECK((json(true) <=> json(false)) == std::partial_ordering::greater); // *NOPAD*
|
||||
#endif
|
||||
}
|
||||
|
||||
SECTION("discarded")
|
||||
@@ -1275,11 +983,6 @@ TEST_CASE("comparison of scalars (inline fast path)")
|
||||
CHECK_FALSE(d == json(nullptr));
|
||||
CHECK_FALSE(json(nullptr) == d);
|
||||
|
||||
#if JSON_HAS_THREE_WAY_COMPARISON
|
||||
CHECK((d <=> d) == std::partial_ordering::unordered); // *NOPAD*
|
||||
CHECK((d <=> json(1)) == std::partial_ordering::unordered); // *NOPAD*
|
||||
CHECK((json(1) <=> d) == std::partial_ordering::unordered); // *NOPAD*
|
||||
#endif
|
||||
}
|
||||
|
||||
SECTION("scalar overloads")
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
// __ _____ _____ _____
|
||||
// __| | __| | | | JSON for Modern C++ (supporting code)
|
||||
// | | |__ | | | | | | version 3.12.0
|
||||
// |_____|_____|_____|_|___| https://github.com/nlohmann/json
|
||||
//
|
||||
// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann <https://nlohmann.me>
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
// This file contains the C++17-only part of unit-conversions1.cpp (conversions to and from
|
||||
// std::string_view). It is kept in a separate translation unit so the (much larger) unit-
|
||||
// conversions1.cpp is built for C++11 only and not rebuilt for every C++ standard.
|
||||
|
||||
#include "doctest_compatibility.h"
|
||||
|
||||
#include <nlohmann/json.hpp>
|
||||
using nlohmann::json;
|
||||
|
||||
// workaround for MSVC, which does not set __cplusplus to the language version (#464)
|
||||
#if (defined(__cplusplus) && __cplusplus >= 201703L) || (defined(_HAS_CXX17) && _HAS_CXX17 == 1) // fix for issue #464
|
||||
#define JSON_HAS_CPP_17
|
||||
#endif
|
||||
|
||||
#ifdef JSON_HAS_CPP_17
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
#if __has_include(<optional>)
|
||||
#include <optional>
|
||||
#elif __has_include(<experimental/optional>)
|
||||
#include <experimental/optional>
|
||||
#endif
|
||||
|
||||
TEST_CASE("value conversion (C++17)")
|
||||
{
|
||||
SECTION("get a string (explicit)")
|
||||
{
|
||||
const json::string_t s_reference{"Hello world"};
|
||||
json j(s_reference);
|
||||
|
||||
SECTION("std::string_view")
|
||||
{
|
||||
std::string_view const s = j.get<std::string_view>();
|
||||
CHECK(json(s) == j);
|
||||
}
|
||||
|
||||
SECTION("exception in case of a non-string type using string_view")
|
||||
{
|
||||
CHECK_THROWS_WITH_AS(json(json::value_t::null).get<std::string_view>(),
|
||||
"[json.exception.type_error.302] type must be string, but is null", json::type_error&);
|
||||
CHECK_THROWS_WITH_AS(json(json::value_t::object).get<std::string_view>(),
|
||||
"[json.exception.type_error.302] type must be string, but is object", json::type_error&);
|
||||
CHECK_THROWS_WITH_AS(json(json::value_t::array).get<std::string_view>(),
|
||||
"[json.exception.type_error.302] type must be string, but is array", json::type_error&);
|
||||
CHECK_THROWS_WITH_AS(json(json::value_t::boolean).get<std::string_view>(),
|
||||
"[json.exception.type_error.302] type must be string, but is boolean", json::type_error&);
|
||||
CHECK_THROWS_WITH_AS(json(json::value_t::number_integer).get<std::string_view>(),
|
||||
"[json.exception.type_error.302] type must be string, but is number", json::type_error&);
|
||||
CHECK_THROWS_WITH_AS(json(json::value_t::number_unsigned).get<std::string_view>(),
|
||||
"[json.exception.type_error.302] type must be string, but is number", json::type_error&);
|
||||
CHECK_THROWS_WITH_AS(json(json::value_t::number_float).get<std::string_view>(),
|
||||
"[json.exception.type_error.302] type must be string, but is number", json::type_error&);
|
||||
}
|
||||
}
|
||||
|
||||
SECTION("get a string (explicit, get_to)")
|
||||
{
|
||||
const json::string_t s_reference{"Hello world"};
|
||||
json j(s_reference);
|
||||
|
||||
SECTION("std::string_view")
|
||||
{
|
||||
std::string const s = "previous value";
|
||||
std::string_view sv = s;
|
||||
j.get_to(sv);
|
||||
CHECK(json(sv) == j);
|
||||
}
|
||||
}
|
||||
|
||||
#if JSON_USE_IMPLICIT_CONVERSIONS
|
||||
SECTION("get a string (implicit)")
|
||||
{
|
||||
const json::string_t s_reference{"Hello world"};
|
||||
json j(s_reference);
|
||||
|
||||
SECTION("std::string_view")
|
||||
{
|
||||
std::string_view const s = j.get<std::string_view>();
|
||||
CHECK(json(s) == j);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -6,12 +6,11 @@
|
||||
// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann <https://nlohmann.me>
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
// cmake/test.cmake selects the C++ standard versions with which to build a
|
||||
// unit test based on the presence of JSON_HAS_CPP_<VERSION> macros.
|
||||
// When using macros that are only defined for particular versions of the standard
|
||||
// (e.g., JSON_HAS_FILESYSTEM for C++17 and up), please mention the corresponding
|
||||
// version macro in a comment close by, like this:
|
||||
// JSON_HAS_CPP_<VERSION> (do not remove; see note at top of file)
|
||||
// cmake/test.cmake builds a unit test with C++ standards beyond C++11 only if the
|
||||
// source file mentions the corresponding version macro. To avoid rebuilding this
|
||||
// large file for every standard, tests that depend on the standard version (e.g.,
|
||||
// those using JSON_HAS_FILESYSTEM, JSON_HAS_RANGES, or JSON_HAS_THREE_WAY_COMPARISON)
|
||||
// go into a separate file unit-conversions1-cpp<NN>.cpp. This file stays C++11-only.
|
||||
|
||||
#include "doctest_compatibility.h"
|
||||
|
||||
@@ -31,25 +30,6 @@ using nlohmann::json;
|
||||
DOCTEST_CLANG_SUPPRESS_WARNING_PUSH
|
||||
DOCTEST_CLANG_SUPPRESS_WARNING("-Wexit-time-destructors")
|
||||
|
||||
#if (defined(__cplusplus) && __cplusplus >= 201703L) || (defined(_HAS_CXX17) && _HAS_CXX17 == 1) // fix for issue #464
|
||||
#define JSON_HAS_CPP_17
|
||||
#define JSON_HAS_CPP_14
|
||||
#elif (defined(__cplusplus) && __cplusplus >= 201402L) || (defined(_HAS_CXX14) && _HAS_CXX14 == 1)
|
||||
#define JSON_HAS_CPP_14
|
||||
#endif
|
||||
|
||||
#ifdef JSON_HAS_CPP_17
|
||||
#if __has_include(<optional>)
|
||||
#include <optional>
|
||||
#elif __has_include(<experimental/optional>)
|
||||
#include <experimental/optional>
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#if defined(JSON_HAS_CPP_17)
|
||||
#include <string_view>
|
||||
#endif
|
||||
|
||||
TEST_CASE("value conversion")
|
||||
{
|
||||
SECTION("get an object (explicit)")
|
||||
@@ -519,13 +499,6 @@ TEST_CASE("value conversion")
|
||||
const std::string s = j.get<std::string>();
|
||||
CHECK(json(s) == j);
|
||||
}
|
||||
#if defined(JSON_HAS_CPP_17)
|
||||
SECTION("std::string_view")
|
||||
{
|
||||
std::string_view const s = j.get<std::string_view>();
|
||||
CHECK(json(s) == j);
|
||||
}
|
||||
#endif
|
||||
|
||||
SECTION("exception in case of a non-string type")
|
||||
{
|
||||
@@ -551,26 +524,6 @@ TEST_CASE("value conversion")
|
||||
json(json::value_t::number_float).get<json::string_t>(),
|
||||
"[json.exception.type_error.302] type must be string, but is number", json::type_error&);
|
||||
}
|
||||
|
||||
#if defined(JSON_HAS_CPP_17)
|
||||
SECTION("exception in case of a non-string type using string_view")
|
||||
{
|
||||
CHECK_THROWS_WITH_AS(json(json::value_t::null).get<std::string_view>(),
|
||||
"[json.exception.type_error.302] type must be string, but is null", json::type_error&);
|
||||
CHECK_THROWS_WITH_AS(json(json::value_t::object).get<std::string_view>(),
|
||||
"[json.exception.type_error.302] type must be string, but is object", json::type_error&);
|
||||
CHECK_THROWS_WITH_AS(json(json::value_t::array).get<std::string_view>(),
|
||||
"[json.exception.type_error.302] type must be string, but is array", json::type_error&);
|
||||
CHECK_THROWS_WITH_AS(json(json::value_t::boolean).get<std::string_view>(),
|
||||
"[json.exception.type_error.302] type must be string, but is boolean", json::type_error&);
|
||||
CHECK_THROWS_WITH_AS(json(json::value_t::number_integer).get<std::string_view>(),
|
||||
"[json.exception.type_error.302] type must be string, but is number", json::type_error&);
|
||||
CHECK_THROWS_WITH_AS(json(json::value_t::number_unsigned).get<std::string_view>(),
|
||||
"[json.exception.type_error.302] type must be string, but is number", json::type_error&);
|
||||
CHECK_THROWS_WITH_AS(json(json::value_t::number_float).get<std::string_view>(),
|
||||
"[json.exception.type_error.302] type must be string, but is number", json::type_error&);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
SECTION("get a string (explicit, get_to)")
|
||||
@@ -591,15 +544,6 @@ TEST_CASE("value conversion")
|
||||
j.get_to(s);
|
||||
CHECK(json(s) == j);
|
||||
}
|
||||
#if defined(JSON_HAS_CPP_17)
|
||||
SECTION("std::string_view")
|
||||
{
|
||||
std::string const s = "previous value";
|
||||
std::string_view sv = s;
|
||||
j.get_to(sv);
|
||||
CHECK(json(sv) == j);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
SECTION("get null (explicit)")
|
||||
@@ -638,14 +582,6 @@ TEST_CASE("value conversion")
|
||||
CHECK(json(s) == j);
|
||||
}
|
||||
|
||||
#if defined(JSON_HAS_CPP_17)
|
||||
SECTION("std::string_view")
|
||||
{
|
||||
std::string_view const s = j.get<std::string_view>();
|
||||
CHECK(json(s) == j);
|
||||
}
|
||||
#endif
|
||||
|
||||
SECTION("std::string")
|
||||
{
|
||||
const std::string s = j;
|
||||
@@ -1256,11 +1192,4 @@ TEST_CASE("value conversion")
|
||||
#endif
|
||||
}
|
||||
|
||||
#ifdef JSON_HAS_CPP_17
|
||||
#undef JSON_HAS_CPP_17
|
||||
#endif
|
||||
|
||||
#ifdef JSON_HAS_CPP_14
|
||||
#undef JSON_HAS_CPP_14
|
||||
#endif
|
||||
DOCTEST_CLANG_SUPPRESS_WARNING_POP
|
||||
@@ -0,0 +1,225 @@
|
||||
// __ _____ _____ _____
|
||||
// __| | __| | | | JSON for Modern C++ (supporting code)
|
||||
// | | |__ | | | | | | version 3.12.0
|
||||
// |_____|_____|_____|_|___| https://github.com/nlohmann/json
|
||||
//
|
||||
// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann <https://nlohmann.me>
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
// This file contains the C++17-only part of unit-conversions2.cpp (conversions of
|
||||
// std::filesystem::path, std::u8string and std::optional). It is kept in a separate
|
||||
// translation unit so the (much larger) unit-conversions2.cpp is built for C++11 only and
|
||||
// not rebuilt for every C++ standard.
|
||||
|
||||
#include "doctest_compatibility.h"
|
||||
|
||||
#include <nlohmann/json.hpp>
|
||||
using nlohmann::json;
|
||||
|
||||
// workaround for MSVC, which does not set __cplusplus to the language version (#464)
|
||||
#if (defined(__cplusplus) && __cplusplus >= 201703L) || (defined(_HAS_CXX17) && _HAS_CXX17 == 1) // fix for issue #464
|
||||
#define JSON_HAS_CPP_17
|
||||
#endif
|
||||
|
||||
// NLOHMANN_JSON_SERIALIZE_ENUM uses a static std::pair
|
||||
DOCTEST_CLANG_SUPPRESS_WARNING_PUSH
|
||||
DOCTEST_CLANG_SUPPRESS_WARNING("-Wexit-time-destructors")
|
||||
|
||||
#ifdef JSON_HAS_CPP_17
|
||||
#include <map>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <type_traits>
|
||||
#include <vector>
|
||||
|
||||
#if __has_include(<optional>)
|
||||
#include <optional>
|
||||
#elif __has_include(<experimental/optional>)
|
||||
#include <experimental/optional>
|
||||
#endif
|
||||
|
||||
#if JSON_HAS_FILESYSTEM || JSON_HAS_EXPERIMENTAL_FILESYSTEM
|
||||
TEST_CASE("std::filesystem::path")
|
||||
{
|
||||
SECTION("ascii")
|
||||
{
|
||||
json const j_string = "Path";
|
||||
auto p = j_string.template get<nlohmann::detail::std_fs::path>();
|
||||
json const j_path = p;
|
||||
|
||||
CHECK(j_path.template get<std::string>() ==
|
||||
j_string.template get<std::string>());
|
||||
}
|
||||
|
||||
SECTION("utf-8")
|
||||
{
|
||||
json const j_string = "P\xc4\x9b\xc5\xa1ina";
|
||||
auto p = j_string.template get<nlohmann::detail::std_fs::path>();
|
||||
json const j_path = p;
|
||||
|
||||
CHECK(j_path.template get<std::string>() ==
|
||||
j_string.template get<std::string>());
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
// the ADL to_json overload for std::u8string only exists under the same guard
|
||||
// as std::filesystem::path support (it is otherwise only reached indirectly,
|
||||
// via std::filesystem::path::u8string()) -- mirror both #if conditions from
|
||||
// include/nlohmann/detail/conversions/to_json.hpp exactly
|
||||
#if JSON_HAS_FILESYSTEM || JSON_HAS_EXPERIMENTAL_FILESYSTEM
|
||||
#if defined(__cpp_lib_char8_t)
|
||||
TEST_CASE("std::u8string")
|
||||
{
|
||||
SECTION("ascii")
|
||||
{
|
||||
const std::u8string s = u8"Path";
|
||||
json const j = s;
|
||||
|
||||
CHECK(j.template get<std::string>() == "Path");
|
||||
}
|
||||
|
||||
SECTION("utf-8")
|
||||
{
|
||||
// use \u universal-character-names (rather than raw \x byte escapes
|
||||
// or literal non-ASCII source bytes) to compose the multi-byte UTF-8
|
||||
// encoding -- MSVC treats \x escapes used that way inside a u8
|
||||
// literal as a nonstandard extension (warning C5321), which some of
|
||||
// our CI configs promote to an error; \u is portable and produces
|
||||
// the exact same encoded bytes without depending on the source
|
||||
// file's encoding
|
||||
const std::u8string s = u8"P\u011B\u0161ina";
|
||||
json const j = s;
|
||||
|
||||
CHECK(j.template get<std::string>() == "P\xc4\x9b\xc5\xa1ina");
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#if !defined(JSON_NOEXCEPTION)
|
||||
namespace
|
||||
{
|
||||
// a type whose to_json reports an error by throwing, used below to check that
|
||||
// converting a std::optional<T> to JSON propagates an exception thrown while
|
||||
// converting its contained value instead of calling std::terminate (#5642)
|
||||
struct throwing_to_json_type {};
|
||||
|
||||
[[noreturn]] void to_json(json& /*unused*/, const throwing_to_json_type& /*unused*/)
|
||||
{
|
||||
throw std::runtime_error("cannot serialize throwing_to_json_type");
|
||||
}
|
||||
} // namespace
|
||||
#endif
|
||||
|
||||
TEST_CASE("std::optional")
|
||||
{
|
||||
SECTION("null")
|
||||
{
|
||||
const json j_null;
|
||||
const std::optional<std::string> opt_null;
|
||||
|
||||
CHECK(json(opt_null) == j_null);
|
||||
CHECK(j_null.get<std::optional<std::string>>() == std::nullopt);
|
||||
|
||||
// Constructing std::optional<T> directly from JSON null throws because
|
||||
// std::optional's own converting constructor is chosen over basic_json's
|
||||
// operator T(). This is a language-level limitation (std::optional<T> is
|
||||
// constructible from T, and T is constructible from basic_json via the
|
||||
// operator); there is no SFINAE path that distinguishes "call from inside
|
||||
// std::optional's constructor" from "direct call". Use get<std::optional<T>>()
|
||||
// or get_to() instead for correct null handling. See #4864 and #5246.
|
||||
CHECK_THROWS_WITH_AS(std::optional<std::string>(j_null),
|
||||
"[json.exception.type_error.302] type must be string, but is null", json::type_error&);
|
||||
CHECK_THROWS_WITH_AS(std::optional<int>(j_null),
|
||||
"[json.exception.type_error.302] type must be number, but is null", json::type_error&);
|
||||
|
||||
// Assignment goes through the same overload resolution as direct
|
||||
// construction, so it throws for the same reason. This relies on
|
||||
// basic_json's implicit conversion operator, so it only applies
|
||||
// when JSON_USE_IMPLICIT_CONVERSIONS is enabled (the default).
|
||||
#if JSON_USE_IMPLICIT_CONVERSIONS
|
||||
std::optional<std::string> opt_assign;
|
||||
CHECK_THROWS_WITH_AS(opt_assign = j_null,
|
||||
"[json.exception.type_error.302] type must be string, but is null", json::type_error&);
|
||||
#endif
|
||||
|
||||
// get_to() is the correct way to obtain std::nullopt from a JSON null.
|
||||
std::optional<std::string> opt_get_to = "placeholder";
|
||||
j_null.get_to(opt_get_to);
|
||||
CHECK(opt_get_to == std::nullopt);
|
||||
}
|
||||
|
||||
SECTION("string")
|
||||
{
|
||||
json j_string = "string";
|
||||
std::optional<std::string> opt_string = "string";
|
||||
|
||||
CHECK(json(opt_string) == j_string);
|
||||
CHECK(std::optional<std::string>(j_string) == opt_string);
|
||||
// false positive: Infer attributes the destruction of the temporaries above to opt_string
|
||||
// @infer-ignore USE_AFTER_DELETE
|
||||
}
|
||||
|
||||
SECTION("bool")
|
||||
{
|
||||
json j_bool = true;
|
||||
std::optional<bool> opt_bool = true;
|
||||
|
||||
CHECK(json(opt_bool) == j_bool);
|
||||
CHECK(std::optional<bool>(j_bool) == opt_bool);
|
||||
}
|
||||
|
||||
SECTION("number")
|
||||
{
|
||||
json j_number = 1;
|
||||
std::optional<int> opt_int = 1;
|
||||
|
||||
CHECK(json(opt_int) == j_number);
|
||||
CHECK(j_number.get<std::optional<int>>() == opt_int);
|
||||
}
|
||||
|
||||
SECTION("array")
|
||||
{
|
||||
json j_array = {1, 2, nullptr};
|
||||
std::vector<std::optional<int>> opt_array = {{1, 2, std::nullopt}};
|
||||
|
||||
CHECK(json(opt_array) == j_array);
|
||||
CHECK(j_array.get<std::vector<std::optional<int>>>() == opt_array);
|
||||
}
|
||||
|
||||
SECTION("object")
|
||||
{
|
||||
json j_object = {{"one", 1}, {"two", 2}, {"zero", nullptr}};
|
||||
std::map<std::string, std::optional<int>> opt_object {{"one", 1}, {"two", 2}, {"zero", std::nullopt}};
|
||||
|
||||
CHECK(json(opt_object) == j_object);
|
||||
CHECK(std::map<std::string, std::optional<int>>(j_object) == opt_object);
|
||||
}
|
||||
|
||||
#if !defined(JSON_NOEXCEPTION)
|
||||
SECTION("exception from contained value's to_json propagates (#5642)")
|
||||
{
|
||||
// to_json(BasicJsonType&, const std::optional<T>&) must not be
|
||||
// noexcept: it calls T's to_json, which may throw (a user-defined
|
||||
// to_json that reports an error, or std::bad_alloc for T =
|
||||
// std::string/vector/json). Before the fix, this called
|
||||
// std::terminate() instead of letting the exception propagate.
|
||||
const std::optional<throwing_to_json_type> opt = throwing_to_json_type{};
|
||||
CHECK_THROWS_WITH_AS(json(opt), "cannot serialize throwing_to_json_type", std::runtime_error&);
|
||||
|
||||
// the conversion is noexcept exactly when converting the contained value is
|
||||
// (except with MSVC 2017, where it is never noexcept, see to_json.hpp)
|
||||
#if !defined(_MSC_VER) || defined(__clang__) || _MSC_VER >= 1920
|
||||
static_assert(!std::is_nothrow_constructible<json, const std::optional<throwing_to_json_type>&>::value);
|
||||
static_assert(std::is_nothrow_constructible<json, const std::optional<int>&>::value);
|
||||
#endif
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
DOCTEST_CLANG_SUPPRESS_WARNING_POP
|
||||
#endif
|
||||
@@ -6,12 +6,11 @@
|
||||
// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann <https://nlohmann.me>
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
// cmake/test.cmake selects the C++ standard versions with which to build a
|
||||
// unit test based on the presence of JSON_HAS_CPP_<VERSION> macros.
|
||||
// When using macros that are only defined for particular versions of the standard
|
||||
// (e.g., JSON_HAS_FILESYSTEM for C++17 and up), please mention the corresponding
|
||||
// version macro in a comment close by, like this:
|
||||
// JSON_HAS_CPP_<VERSION> (do not remove; see note at top of file)
|
||||
// cmake/test.cmake builds a unit test with C++ standards beyond C++11 only if the
|
||||
// source file mentions the corresponding version macro. To avoid rebuilding this
|
||||
// large file for every standard, tests that depend on the standard version (e.g.,
|
||||
// those using JSON_HAS_FILESYSTEM, JSON_HAS_RANGES, or JSON_HAS_THREE_WAY_COMPARISON)
|
||||
// go into a separate file unit-conversions2-cpp<NN>.cpp. This file stays C++11-only.
|
||||
|
||||
#include "doctest_compatibility.h"
|
||||
|
||||
@@ -36,25 +35,6 @@ using nlohmann::json;
|
||||
DOCTEST_CLANG_SUPPRESS_WARNING_PUSH
|
||||
DOCTEST_CLANG_SUPPRESS_WARNING("-Wexit-time-destructors")
|
||||
|
||||
#if (defined(__cplusplus) && __cplusplus >= 201703L) || (defined(_HAS_CXX17) && _HAS_CXX17 == 1) // fix for issue #464
|
||||
#define JSON_HAS_CPP_17
|
||||
#define JSON_HAS_CPP_14
|
||||
#elif (defined(__cplusplus) && __cplusplus >= 201402L) || (defined(_HAS_CXX14) && _HAS_CXX14 == 1)
|
||||
#define JSON_HAS_CPP_14
|
||||
#endif
|
||||
|
||||
#ifdef JSON_HAS_CPP_17
|
||||
#if __has_include(<optional>)
|
||||
#include <optional>
|
||||
#elif __has_include(<experimental/optional>)
|
||||
#include <experimental/optional>
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#if defined(JSON_HAS_CPP_17)
|
||||
#include <string_view>
|
||||
#endif
|
||||
|
||||
TEST_CASE("value conversion")
|
||||
{
|
||||
SECTION("get a binary value (explicit)")
|
||||
@@ -680,194 +660,4 @@ TEST_CASE("Strict JSON to enum mapping")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#ifdef JSON_HAS_CPP_17
|
||||
#if JSON_HAS_FILESYSTEM || JSON_HAS_EXPERIMENTAL_FILESYSTEM
|
||||
TEST_CASE("std::filesystem::path")
|
||||
{
|
||||
SECTION("ascii")
|
||||
{
|
||||
json const j_string = "Path";
|
||||
auto p = j_string.template get<nlohmann::detail::std_fs::path>();
|
||||
json const j_path = p;
|
||||
|
||||
CHECK(j_path.template get<std::string>() ==
|
||||
j_string.template get<std::string>());
|
||||
}
|
||||
|
||||
SECTION("utf-8")
|
||||
{
|
||||
json const j_string = "P\xc4\x9b\xc5\xa1ina";
|
||||
auto p = j_string.template get<nlohmann::detail::std_fs::path>();
|
||||
json const j_path = p;
|
||||
|
||||
CHECK(j_path.template get<std::string>() ==
|
||||
j_string.template get<std::string>());
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
// the ADL to_json overload for std::u8string only exists under the same guard
|
||||
// as std::filesystem::path support (it is otherwise only reached indirectly,
|
||||
// via std::filesystem::path::u8string()) -- mirror both #if conditions from
|
||||
// include/nlohmann/detail/conversions/to_json.hpp exactly
|
||||
#if JSON_HAS_FILESYSTEM || JSON_HAS_EXPERIMENTAL_FILESYSTEM
|
||||
#if defined(__cpp_lib_char8_t)
|
||||
TEST_CASE("std::u8string")
|
||||
{
|
||||
SECTION("ascii")
|
||||
{
|
||||
const std::u8string s = u8"Path";
|
||||
json const j = s;
|
||||
|
||||
CHECK(j.template get<std::string>() == "Path");
|
||||
}
|
||||
|
||||
SECTION("utf-8")
|
||||
{
|
||||
// use \u universal-character-names (rather than raw \x byte escapes
|
||||
// or literal non-ASCII source bytes) to compose the multi-byte UTF-8
|
||||
// encoding -- MSVC treats \x escapes used that way inside a u8
|
||||
// literal as a nonstandard extension (warning C5321), which some of
|
||||
// our CI configs promote to an error; \u is portable and produces
|
||||
// the exact same encoded bytes without depending on the source
|
||||
// file's encoding
|
||||
const std::u8string s = u8"P\u011B\u0161ina";
|
||||
json const j = s;
|
||||
|
||||
CHECK(j.template get<std::string>() == "P\xc4\x9b\xc5\xa1ina");
|
||||
}
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#if !defined(JSON_NOEXCEPTION)
|
||||
namespace
|
||||
{
|
||||
// a type whose to_json reports an error by throwing, used below to check that
|
||||
// converting a std::optional<T> to JSON propagates an exception thrown while
|
||||
// converting its contained value instead of calling std::terminate (#5642)
|
||||
struct throwing_to_json_type {};
|
||||
|
||||
[[noreturn]] void to_json(json& /*unused*/, const throwing_to_json_type& /*unused*/)
|
||||
{
|
||||
throw std::runtime_error("cannot serialize throwing_to_json_type");
|
||||
}
|
||||
} // namespace
|
||||
#endif
|
||||
|
||||
TEST_CASE("std::optional")
|
||||
{
|
||||
SECTION("null")
|
||||
{
|
||||
const json j_null;
|
||||
const std::optional<std::string> opt_null;
|
||||
|
||||
CHECK(json(opt_null) == j_null);
|
||||
CHECK(j_null.get<std::optional<std::string>>() == std::nullopt);
|
||||
|
||||
// Constructing std::optional<T> directly from JSON null throws because
|
||||
// std::optional's own converting constructor is chosen over basic_json's
|
||||
// operator T(). This is a language-level limitation (std::optional<T> is
|
||||
// constructible from T, and T is constructible from basic_json via the
|
||||
// operator); there is no SFINAE path that distinguishes "call from inside
|
||||
// std::optional's constructor" from "direct call". Use get<std::optional<T>>()
|
||||
// or get_to() instead for correct null handling. See #4864 and #5246.
|
||||
CHECK_THROWS_WITH_AS(std::optional<std::string>(j_null),
|
||||
"[json.exception.type_error.302] type must be string, but is null", json::type_error&);
|
||||
CHECK_THROWS_WITH_AS(std::optional<int>(j_null),
|
||||
"[json.exception.type_error.302] type must be number, but is null", json::type_error&);
|
||||
|
||||
// Assignment goes through the same overload resolution as direct
|
||||
// construction, so it throws for the same reason. This relies on
|
||||
// basic_json's implicit conversion operator, so it only applies
|
||||
// when JSON_USE_IMPLICIT_CONVERSIONS is enabled (the default).
|
||||
#if JSON_USE_IMPLICIT_CONVERSIONS
|
||||
std::optional<std::string> opt_assign;
|
||||
CHECK_THROWS_WITH_AS(opt_assign = j_null,
|
||||
"[json.exception.type_error.302] type must be string, but is null", json::type_error&);
|
||||
#endif
|
||||
|
||||
// get_to() is the correct way to obtain std::nullopt from a JSON null.
|
||||
std::optional<std::string> opt_get_to = "placeholder";
|
||||
j_null.get_to(opt_get_to);
|
||||
CHECK(opt_get_to == std::nullopt);
|
||||
}
|
||||
|
||||
SECTION("string")
|
||||
{
|
||||
json j_string = "string";
|
||||
std::optional<std::string> opt_string = "string";
|
||||
|
||||
CHECK(json(opt_string) == j_string);
|
||||
CHECK(std::optional<std::string>(j_string) == opt_string);
|
||||
// false positive: Infer attributes the destruction of the temporaries above to opt_string
|
||||
// @infer-ignore USE_AFTER_DELETE
|
||||
}
|
||||
|
||||
SECTION("bool")
|
||||
{
|
||||
json j_bool = true;
|
||||
std::optional<bool> opt_bool = true;
|
||||
|
||||
CHECK(json(opt_bool) == j_bool);
|
||||
CHECK(std::optional<bool>(j_bool) == opt_bool);
|
||||
}
|
||||
|
||||
SECTION("number")
|
||||
{
|
||||
json j_number = 1;
|
||||
std::optional<int> opt_int = 1;
|
||||
|
||||
CHECK(json(opt_int) == j_number);
|
||||
CHECK(j_number.get<std::optional<int>>() == opt_int);
|
||||
}
|
||||
|
||||
SECTION("array")
|
||||
{
|
||||
json j_array = {1, 2, nullptr};
|
||||
std::vector<std::optional<int>> opt_array = {{1, 2, std::nullopt}};
|
||||
|
||||
CHECK(json(opt_array) == j_array);
|
||||
CHECK(j_array.get<std::vector<std::optional<int>>>() == opt_array);
|
||||
}
|
||||
|
||||
SECTION("object")
|
||||
{
|
||||
json j_object = {{"one", 1}, {"two", 2}, {"zero", nullptr}};
|
||||
std::map<std::string, std::optional<int>> opt_object {{"one", 1}, {"two", 2}, {"zero", std::nullopt}};
|
||||
|
||||
CHECK(json(opt_object) == j_object);
|
||||
CHECK(std::map<std::string, std::optional<int>>(j_object) == opt_object);
|
||||
}
|
||||
|
||||
#if !defined(JSON_NOEXCEPTION)
|
||||
SECTION("exception from contained value's to_json propagates (#5642)")
|
||||
{
|
||||
// to_json(BasicJsonType&, const std::optional<T>&) must not be
|
||||
// noexcept: it calls T's to_json, which may throw (a user-defined
|
||||
// to_json that reports an error, or std::bad_alloc for T =
|
||||
// std::string/vector/json). Before the fix, this called
|
||||
// std::terminate() instead of letting the exception propagate.
|
||||
const std::optional<throwing_to_json_type> opt = throwing_to_json_type{};
|
||||
CHECK_THROWS_WITH_AS(json(opt), "cannot serialize throwing_to_json_type", std::runtime_error&);
|
||||
|
||||
// the conversion is noexcept exactly when converting the contained value is
|
||||
// (except with MSVC 2017, where it is never noexcept, see to_json.hpp)
|
||||
#if !defined(_MSC_VER) || defined(__clang__) || _MSC_VER >= 1920
|
||||
static_assert(!std::is_nothrow_constructible<json, const std::optional<throwing_to_json_type>&>::value);
|
||||
static_assert(std::is_nothrow_constructible<json, const std::optional<int>&>::value);
|
||||
#endif
|
||||
}
|
||||
#endif
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef JSON_HAS_CPP_17
|
||||
#undef JSON_HAS_CPP_17
|
||||
#endif
|
||||
|
||||
#ifdef JSON_HAS_CPP_14
|
||||
#undef JSON_HAS_CPP_14
|
||||
#endif
|
||||
DOCTEST_CLANG_SUPPRESS_WARNING_POP
|
||||
@@ -44,10 +44,10 @@ TEST_CASE("JSON Node Metadata")
|
||||
{
|
||||
SECTION("type int")
|
||||
{
|
||||
using json = json_with_metadata<int>;
|
||||
json null;
|
||||
auto obj = json::object();
|
||||
auto array = json::array();
|
||||
using json_t = json_with_metadata<int>;
|
||||
json_t null;
|
||||
auto obj = json_t::object();
|
||||
auto array = json_t::array();
|
||||
|
||||
null.metadata() = 1;
|
||||
obj.metadata() = 2;
|
||||
@@ -61,8 +61,8 @@ TEST_CASE("JSON Node Metadata")
|
||||
}
|
||||
SECTION("type vector<int>")
|
||||
{
|
||||
using json = json_with_metadata<std::vector<int>>;
|
||||
json value;
|
||||
using json_t = json_with_metadata<std::vector<int>>;
|
||||
json_t value;
|
||||
value.metadata().emplace_back(1);
|
||||
auto copy = value;
|
||||
value.metadata().emplace_back(2);
|
||||
@@ -75,12 +75,12 @@ TEST_CASE("JSON Node Metadata")
|
||||
}
|
||||
SECTION("copy ctor")
|
||||
{
|
||||
using json = json_with_metadata<std::vector<int>>;
|
||||
json value;
|
||||
using json_t = json_with_metadata<std::vector<int>>;
|
||||
json_t value;
|
||||
value.metadata().emplace_back(1);
|
||||
value.metadata().emplace_back(2);
|
||||
|
||||
json copy = value;
|
||||
json_t copy = value;
|
||||
|
||||
CHECK(copy.metadata().size() == 2);
|
||||
CHECK(copy.metadata().at(0) == 1);
|
||||
@@ -95,12 +95,12 @@ TEST_CASE("JSON Node Metadata")
|
||||
}
|
||||
SECTION("move ctor")
|
||||
{
|
||||
using json = json_with_metadata<std::vector<int>>;
|
||||
json value;
|
||||
using json_t = json_with_metadata<std::vector<int>>;
|
||||
json_t value;
|
||||
value.metadata().emplace_back(1);
|
||||
value.metadata().emplace_back(2);
|
||||
|
||||
const json moved = std::move(value);
|
||||
const json_t moved = std::move(value);
|
||||
|
||||
CHECK(moved.metadata().size() == 2);
|
||||
CHECK(moved.metadata().at(0) == 1);
|
||||
@@ -108,12 +108,12 @@ TEST_CASE("JSON Node Metadata")
|
||||
}
|
||||
SECTION("move assign")
|
||||
{
|
||||
using json = json_with_metadata<std::vector<int>>;
|
||||
json value;
|
||||
using json_t = json_with_metadata<std::vector<int>>;
|
||||
json_t value;
|
||||
value.metadata().emplace_back(1);
|
||||
value.metadata().emplace_back(2);
|
||||
|
||||
json moved;
|
||||
json_t moved;
|
||||
moved = std::move(value);
|
||||
|
||||
CHECK(moved.metadata().size() == 2);
|
||||
@@ -122,12 +122,12 @@ TEST_CASE("JSON Node Metadata")
|
||||
}
|
||||
SECTION("copy assign")
|
||||
{
|
||||
using json = json_with_metadata<std::vector<int>>;
|
||||
json value;
|
||||
using json_t = json_with_metadata<std::vector<int>>;
|
||||
json_t value;
|
||||
value.metadata().emplace_back(1);
|
||||
value.metadata().emplace_back(2);
|
||||
|
||||
json copy;
|
||||
json_t copy;
|
||||
copy = value;
|
||||
|
||||
CHECK(copy.metadata().size() == 2);
|
||||
@@ -143,8 +143,8 @@ TEST_CASE("JSON Node Metadata")
|
||||
}
|
||||
SECTION("type unique_ptr<int>")
|
||||
{
|
||||
using json = json_with_metadata<std::unique_ptr<int>>;
|
||||
json value;
|
||||
using json_t = json_with_metadata<std::unique_ptr<int>>;
|
||||
json_t value;
|
||||
value.metadata().reset(new int(42)); // NOLINT(cppcoreguidelines-owning-memory)
|
||||
auto moved = std::move(value);
|
||||
|
||||
@@ -153,12 +153,12 @@ TEST_CASE("JSON Node Metadata")
|
||||
}
|
||||
SECTION("type vector<int> in json array")
|
||||
{
|
||||
using json = json_with_metadata<std::vector<int>>;
|
||||
json value;
|
||||
using json_t = json_with_metadata<std::vector<int>>;
|
||||
json_t value;
|
||||
value.metadata().emplace_back(1);
|
||||
value.metadata().emplace_back(2);
|
||||
|
||||
json const array(10, value);
|
||||
json_t const array(10, value);
|
||||
|
||||
CHECK(value.metadata().size() == 2);
|
||||
CHECK(value.metadata().at(0) == 1);
|
||||
@@ -173,10 +173,10 @@ TEST_CASE("JSON Node Metadata")
|
||||
}
|
||||
SECTION("member swap")
|
||||
{
|
||||
using json = json_with_metadata<int>;
|
||||
json a = 1;
|
||||
using json_t = json_with_metadata<int>;
|
||||
json_t a = 1;
|
||||
a.metadata() = 100;
|
||||
json b = 2;
|
||||
json_t b = 2;
|
||||
b.metadata() = 200;
|
||||
|
||||
a.swap(b);
|
||||
@@ -188,10 +188,10 @@ TEST_CASE("JSON Node Metadata")
|
||||
}
|
||||
SECTION("nonmember swap")
|
||||
{
|
||||
using json = json_with_metadata<int>;
|
||||
json a = 1;
|
||||
using json_t = json_with_metadata<int>;
|
||||
json_t a = 1;
|
||||
a.metadata() = 100;
|
||||
json b = 2;
|
||||
json_t b = 2;
|
||||
b.metadata() = 200;
|
||||
|
||||
using std::swap;
|
||||
@@ -204,10 +204,10 @@ TEST_CASE("JSON Node Metadata")
|
||||
}
|
||||
SECTION("std::swap")
|
||||
{
|
||||
using json = json_with_metadata<int>;
|
||||
json a = 1;
|
||||
using json_t = json_with_metadata<int>;
|
||||
json_t a = 1;
|
||||
a.metadata() = 100;
|
||||
json b = 2;
|
||||
json_t b = 2;
|
||||
b.metadata() = 200;
|
||||
|
||||
std::swap(a, b);
|
||||
@@ -221,15 +221,15 @@ TEST_CASE("JSON Node Metadata")
|
||||
{
|
||||
// std::sort mixes swap() with moves; each value's metadata must
|
||||
// travel with it, just as it does for copy, move, and assignment
|
||||
using json = json_with_metadata<int>;
|
||||
std::vector<json> values;
|
||||
using json_t = json_with_metadata<int>;
|
||||
std::vector<json_t> values;
|
||||
for (const int v :
|
||||
{
|
||||
5, 3, 9, 1, 7, 2, 8, 4, 6, 0, 15, 13, 19, 11, 17, 12, 18, 14, 16, 10,
|
||||
25, 23, 29, 21, 27, 22, 28, 24, 26, 20, 35, 33
|
||||
})
|
||||
{
|
||||
json value = v;
|
||||
json_t value = v;
|
||||
value.metadata() = v;
|
||||
values.push_back(value);
|
||||
}
|
||||
@@ -267,19 +267,19 @@ template <class Ptr, class Fnc>
|
||||
void visitor_adaptor::do_visit(const Ptr& ptr, const Fnc& fnc) const
|
||||
{
|
||||
using value_t = nlohmann::detail::value_t;
|
||||
const json_with_visitor_t& json = *static_cast<const json_with_visitor_t*>(this); // NOLINT(cppcoreguidelines-pro-type-static-cast-downcast)
|
||||
switch (json.type())
|
||||
const json_with_visitor_t& self = *static_cast<const json_with_visitor_t*>(this); // NOLINT(cppcoreguidelines-pro-type-static-cast-downcast)
|
||||
switch (self.type())
|
||||
{
|
||||
case value_t::object:
|
||||
for (const auto& entry : json.items())
|
||||
for (const auto& entry : self.items())
|
||||
{
|
||||
entry.value().do_visit(ptr / entry.key(), fnc);
|
||||
}
|
||||
break;
|
||||
case value_t::array:
|
||||
for (std::size_t i = 0; i < json.size(); ++i)
|
||||
for (std::size_t i = 0; i < self.size(); ++i)
|
||||
{
|
||||
json.at(i).do_visit(ptr / std::to_string(i), fnc);
|
||||
self.at(i).do_visit(ptr / std::to_string(i), fnc);
|
||||
}
|
||||
break;
|
||||
case value_t::discarded:
|
||||
@@ -292,22 +292,22 @@ void visitor_adaptor::do_visit(const Ptr& ptr, const Fnc& fnc) const
|
||||
case value_t::number_float:
|
||||
case value_t::binary:
|
||||
default:
|
||||
fnc(ptr, json);
|
||||
fnc(ptr, self);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("JSON Visit Node")
|
||||
{
|
||||
json_with_visitor_t json;
|
||||
json["null"];
|
||||
json["int"] = -1;
|
||||
json["uint"] = 1U;
|
||||
json["float"] = 1.0;
|
||||
json["boolean"] = true;
|
||||
json["string"] = "string";
|
||||
json["array"].push_back(0);
|
||||
json["array"].push_back(1);
|
||||
json["array"].push_back(json);
|
||||
json_with_visitor_t root;
|
||||
root["null"];
|
||||
root["int"] = -1;
|
||||
root["uint"] = 1U;
|
||||
root["float"] = 1.0;
|
||||
root["boolean"] = true;
|
||||
root["string"] = "string";
|
||||
root["array"].push_back(0);
|
||||
root["array"].push_back(1);
|
||||
root["array"].push_back(root);
|
||||
|
||||
std::set<std::string> expected
|
||||
{
|
||||
@@ -330,7 +330,7 @@ TEST_CASE("JSON Visit Node")
|
||||
"/array/2/array/1 - number_integer - 1"
|
||||
};
|
||||
|
||||
json.visit(
|
||||
root.visit(
|
||||
[&](const json_with_visitor_t::json_pointer & p,
|
||||
const json_with_visitor_t& j)
|
||||
{
|
||||
@@ -374,7 +374,7 @@ TEST_CASE("JSON Visit Node")
|
||||
break;
|
||||
}
|
||||
str << " - " << j.dump();
|
||||
CHECK(json.at(p) == j);
|
||||
CHECK(root.at(p) == j);
|
||||
INFO(str.str());
|
||||
CHECK(expected.count(str.str()) == 1);
|
||||
expected.erase(str.str());
|
||||
@@ -404,22 +404,22 @@ using json_with_hidden_base_members = nlohmann::json::with_base_class_t<base_cla
|
||||
|
||||
TEST_CASE("JSON Node as_base_class")
|
||||
{
|
||||
using json = json_with_hidden_base_members;
|
||||
using json_t = json_with_hidden_base_members;
|
||||
|
||||
static_assert(std::is_same<decltype(std::declval<json&>().as_base_class()), json::json_base_class_t&>::value, "");
|
||||
static_assert(std::is_same<decltype(std::declval<const json&>().as_base_class()), const json::json_base_class_t&>::value, "");
|
||||
static_assert(noexcept(std::declval<json&>().as_base_class()), "");
|
||||
static_assert(noexcept(std::declval<const json&>().as_base_class()), "");
|
||||
static_assert(std::is_same<decltype(std::declval<json_t&>().as_base_class()), json_t::json_base_class_t&>::value, "");
|
||||
static_assert(std::is_same<decltype(std::declval<const json_t&>().as_base_class()), const json_t::json_base_class_t&>::value, "");
|
||||
static_assert(noexcept(std::declval<json_t&>().as_base_class()), "");
|
||||
static_assert(noexcept(std::declval<const json_t&>().as_base_class()), "");
|
||||
|
||||
SECTION("non-const")
|
||||
{
|
||||
json j = {1, 2, 3};
|
||||
json_t j = {1, 2, 3};
|
||||
|
||||
CHECK(std::string(j.type_name()) == "array");
|
||||
CHECK(j.size() == 3);
|
||||
CHECK(std::string(j.as_base_class().type_name()) == "custom type_name");
|
||||
CHECK(j.as_base_class().size() == 42);
|
||||
CHECK(&j.as_base_class() == &static_cast<json::json_base_class_t&>(j));
|
||||
CHECK(&j.as_base_class() == &static_cast<json_t::json_base_class_t&>(j));
|
||||
|
||||
j.as_base_class().m_size = 7;
|
||||
CHECK(j.as_base_class().size() == 7);
|
||||
@@ -428,13 +428,13 @@ TEST_CASE("JSON Node as_base_class")
|
||||
|
||||
SECTION("const")
|
||||
{
|
||||
const json j = {1, 2, 3};
|
||||
const json_t j = {1, 2, 3};
|
||||
|
||||
CHECK(std::string(j.type_name()) == "array");
|
||||
CHECK(j.size() == 3);
|
||||
CHECK(std::string(j.as_base_class().type_name()) == "custom type_name");
|
||||
CHECK(j.as_base_class().size() == 42);
|
||||
CHECK(&j.as_base_class() == &static_cast<const json::json_base_class_t&>(j));
|
||||
CHECK(&j.as_base_class() == &static_cast<const json_t::json_base_class_t&>(j));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
// __ _____ _____ _____
|
||||
// __| | __| | | | JSON for Modern C++ (supporting code)
|
||||
// | | |__ | | | | | | version 3.12.0
|
||||
// |_____|_____|_____|_|___| https://github.com/nlohmann/json
|
||||
//
|
||||
// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann <https://nlohmann.me>
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
// This file contains the C++17-only part of unit-custom-binary-type.cpp (binary types
|
||||
// whose value type is std::byte). It is kept in a separate translation unit so the (much
|
||||
// larger) unit-custom-binary-type.cpp is built for C++11 only and not rebuilt for every
|
||||
// C++ standard.
|
||||
|
||||
#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>
|
||||
|
||||
#ifdef JSON_HAS_CPP_17
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
// a BinaryType whose value type is not an integer type at all
|
||||
using byte_binary_json = nlohmann::json::with_binary_t<std::vector<std::byte>>;
|
||||
|
||||
TEST_CASE("binary type whose value type is not std::uint8_t (C++17)")
|
||||
{
|
||||
SECTION("dumping a value type that is not an integer")
|
||||
{
|
||||
const std::vector<std::byte> bytes{std::byte{0}, std::byte{1}, std::byte{0xFF}};
|
||||
CHECK(byte_binary_json::binary(bytes).dump() == R"({"bytes":[0,1,255],"subtype":null})");
|
||||
CHECK(byte_binary_json::binary(bytes, 42).dump() == R"({"bytes":[0,1,255],"subtype":42})");
|
||||
CHECK(byte_binary_json::binary({}).dump() == R"({"bytes":[],"subtype":null})");
|
||||
}
|
||||
|
||||
SECTION("hashing and the binary formats")
|
||||
{
|
||||
const std::vector<std::byte> bytes{std::byte{0}, std::byte{1}, std::byte{0xFF}};
|
||||
const auto j = byte_binary_json::binary(bytes);
|
||||
|
||||
CHECK(std::hash<byte_binary_json> {}(j) == std::hash<byte_binary_json> {}(j));
|
||||
CHECK(byte_binary_json::from_cbor(byte_binary_json::to_cbor(j)) == j);
|
||||
CHECK(byte_binary_json::from_msgpack(byte_binary_json::to_msgpack(j)) == j);
|
||||
|
||||
// UBJSON has no binary type, so binary values are written as an array
|
||||
CHECK(byte_binary_json::from_ubjson(byte_binary_json::to_ubjson(j)) == byte_binary_json({0, 1, 255}));
|
||||
// the same holds for BON8
|
||||
CHECK(byte_binary_json::from_bon8(byte_binary_json::to_bon8(j)) == byte_binary_json({0, 1, 255}));
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
@@ -8,13 +8,6 @@
|
||||
|
||||
#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 <cstdint>
|
||||
@@ -24,10 +17,6 @@
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#ifdef JSON_HAS_CPP_17
|
||||
#include <cstddef>
|
||||
#endif
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
@@ -35,11 +24,6 @@ namespace
|
||||
// processed as the numbers 0..255
|
||||
using char_binary_json = nlohmann::json::with_binary_t<std::vector<char>>;
|
||||
|
||||
#ifdef JSON_HAS_CPP_17
|
||||
// a BinaryType whose value type is not an integer type at all
|
||||
using byte_binary_json = nlohmann::json::with_binary_t<std::vector<std::byte>>;
|
||||
#endif
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST_CASE("binary type whose value type is not std::uint8_t")
|
||||
@@ -66,79 +50,4 @@ TEST_CASE("binary type whose value type is not std::uint8_t")
|
||||
{
|
||||
CHECK(nlohmann::json::binary({0, 1, 255}, 42).dump() == R"({"bytes":[0,1,255],"subtype":42})");
|
||||
}
|
||||
|
||||
#ifdef JSON_HAS_CPP_17
|
||||
SECTION("dumping a value type that is not an integer")
|
||||
{
|
||||
const std::vector<std::byte> bytes{std::byte{0}, std::byte{1}, std::byte{0xFF}};
|
||||
CHECK(byte_binary_json::binary(bytes).dump() == R"({"bytes":[0,1,255],"subtype":null})");
|
||||
CHECK(byte_binary_json::binary(bytes, 42).dump() == R"({"bytes":[0,1,255],"subtype":42})");
|
||||
CHECK(byte_binary_json::binary({}).dump() == R"({"bytes":[],"subtype":null})");
|
||||
}
|
||||
|
||||
SECTION("hashing and the binary formats")
|
||||
{
|
||||
const std::vector<std::byte> bytes{std::byte{0}, std::byte{1}, std::byte{0xFF}};
|
||||
const auto j = byte_binary_json::binary(bytes);
|
||||
|
||||
CHECK(std::hash<byte_binary_json> {}(j) == std::hash<byte_binary_json> {}(j));
|
||||
CHECK(byte_binary_json::from_cbor(byte_binary_json::to_cbor(j)) == j);
|
||||
CHECK(byte_binary_json::from_msgpack(byte_binary_json::to_msgpack(j)) == j);
|
||||
|
||||
// UBJSON has no binary type, so binary values are written as an array
|
||||
CHECK(byte_binary_json::from_ubjson(byte_binary_json::to_ubjson(j)) == byte_binary_json({0, 1, 255}));
|
||||
// the same holds for BON8
|
||||
CHECK(byte_binary_json::from_bon8(byte_binary_json::to_bon8(j)) == byte_binary_json({0, 1, 255}));
|
||||
}
|
||||
#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
|
||||
@@ -0,0 +1,177 @@
|
||||
// __ _____ _____ _____
|
||||
// __| | __| | | | JSON for Modern C++ (supporting code)
|
||||
// | | |__ | | | | | | version 3.12.0
|
||||
// |_____|_____|_____|_|___| https://github.com/nlohmann/json
|
||||
//
|
||||
// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann <https://nlohmann.me>
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
// This file contains the C++20-only part of unit-deserialization.cpp (char8_t support:
|
||||
// _json with char8_t literals and char8_t input). It is kept in a separate translation
|
||||
// unit so the (much larger) unit-deserialization.cpp is built for C++11 only and not
|
||||
// rebuilt for every C++ standard.
|
||||
|
||||
#include "doctest_compatibility.h"
|
||||
|
||||
#include <nlohmann/json.hpp>
|
||||
using nlohmann::json;
|
||||
#ifdef JSON_TEST_NO_GLOBAL_UDLS
|
||||
using namespace nlohmann::literals; // NOLINT(google-build-using-namespace)
|
||||
#endif
|
||||
|
||||
#ifdef JSON_HAS_CPP_20
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#if defined(__cpp_char8_t) && (__cpp_char8_t >= 201811L)
|
||||
namespace
|
||||
{
|
||||
// copy of SaxEventLogger from unit-deserialization.cpp (renamed to avoid clashes in unity builds)
|
||||
struct SaxEventLoggerChar8 : public nlohmann::json_sax<json>
|
||||
{
|
||||
bool null() override
|
||||
{
|
||||
events.emplace_back("null()");
|
||||
return true;
|
||||
}
|
||||
|
||||
bool boolean(bool val) override
|
||||
{
|
||||
events.emplace_back(val ? "boolean(true)" : "boolean(false)");
|
||||
return true;
|
||||
}
|
||||
|
||||
bool number_integer(json::number_integer_t val) override
|
||||
{
|
||||
events.push_back("number_integer(" + std::to_string(val) + ")");
|
||||
return true;
|
||||
}
|
||||
|
||||
bool number_unsigned(json::number_unsigned_t val) override
|
||||
{
|
||||
events.push_back("number_unsigned(" + std::to_string(val) + ")");
|
||||
return true;
|
||||
}
|
||||
|
||||
bool number_float(json::number_float_t /*val*/, const std::string& s) override
|
||||
{
|
||||
events.push_back("number_float(" + s + ")");
|
||||
return true;
|
||||
}
|
||||
|
||||
bool string(std::string& val) override
|
||||
{
|
||||
events.push_back("string(" + val + ")");
|
||||
return true;
|
||||
}
|
||||
|
||||
bool binary(json::binary_t& val) override
|
||||
{
|
||||
std::string binary_contents = "binary(";
|
||||
std::string comma_space;
|
||||
for (auto b : val)
|
||||
{
|
||||
binary_contents.append(comma_space);
|
||||
binary_contents.append(std::to_string(static_cast<int>(b)));
|
||||
comma_space = ", ";
|
||||
}
|
||||
binary_contents.append(")");
|
||||
events.push_back(binary_contents);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool start_object(std::size_t elements) override
|
||||
{
|
||||
if (elements == (std::numeric_limits<std::size_t>::max)())
|
||||
{
|
||||
events.emplace_back("start_object()");
|
||||
}
|
||||
else
|
||||
{
|
||||
events.push_back("start_object(" + std::to_string(elements) + ")");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool key(std::string& val) override
|
||||
{
|
||||
events.push_back("key(" + val + ")");
|
||||
return true;
|
||||
}
|
||||
|
||||
bool end_object() override
|
||||
{
|
||||
events.emplace_back("end_object()");
|
||||
return true;
|
||||
}
|
||||
|
||||
bool start_array(std::size_t elements) override
|
||||
{
|
||||
if (elements == (std::numeric_limits<std::size_t>::max)())
|
||||
{
|
||||
events.emplace_back("start_array()");
|
||||
}
|
||||
else
|
||||
{
|
||||
events.push_back("start_array(" + std::to_string(elements) + ")");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool end_array() override
|
||||
{
|
||||
events.emplace_back("end_array()");
|
||||
return true;
|
||||
}
|
||||
|
||||
bool parse_error(std::size_t position, const std::string& /*last_token*/, const json::exception& /*ex*/) override
|
||||
{
|
||||
events.push_back("parse_error(" + std::to_string(position) + ")");
|
||||
return false;
|
||||
}
|
||||
|
||||
std::vector<std::string> events {}; // NOLINT(readability-redundant-member-init)
|
||||
};
|
||||
} // namespace
|
||||
|
||||
TEST_CASE_TEMPLATE("deserialization of different character types (ASCII) (C++20)", T, char8_t) // NOLINT(readability-math-missing-parentheses, bugprone-throwing-static-initialization)
|
||||
{
|
||||
std::vector<T> const v = {'t', 'r', 'u', 'e'};
|
||||
CHECK(json::parse(v) == json(true));
|
||||
CHECK(json::accept(v));
|
||||
|
||||
SaxEventLoggerChar8 l;
|
||||
CHECK(json::sax_parse(v, &l));
|
||||
CHECK(l.events.size() == 1);
|
||||
CHECK(l.events == std::vector<std::string>({"boolean(true)"}));
|
||||
}
|
||||
#endif
|
||||
|
||||
TEST_CASE("deserialization (C++20)")
|
||||
{
|
||||
#if defined(__cpp_char8_t)
|
||||
SECTION("Using _json with char8_t literals #4945")
|
||||
{
|
||||
// Regular narrow string literal
|
||||
const auto j1 = R"({"key": "value", "num": 42})"_json;
|
||||
CHECK(j1["key"] == "value");
|
||||
CHECK(j1["num"] == 42);
|
||||
|
||||
// UTF-8 prefixed literal (C++20 and later); the emoji is written as a
|
||||
// \U escape rather than a raw multibyte character so this does not
|
||||
// depend on the compiler's source-file encoding (e.g., MSVC without
|
||||
// /utf-8, or classic ICC, which does not encode non-ASCII narrow
|
||||
// string literals as UTF-8 - compare against a \x-escaped expectation
|
||||
// for the same reason)
|
||||
const auto j2 = u8"{\"emoji\": \"\U0001F600\", \"msg\": \"hello\"}"_json;
|
||||
CHECK(j2["emoji"] == "\xF0\x9F\x98\x80");
|
||||
CHECK(j2["msg"] == "hello");
|
||||
|
||||
const auto j3 = u8R"({"key": "value", "num": 42})"_json;
|
||||
CHECK(j3["key"] == "value");
|
||||
CHECK(j3["num"] == 42);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -1167,44 +1167,9 @@ TEST_CASE("deserialization")
|
||||
CHECK(line == "foo");
|
||||
}
|
||||
}
|
||||
|
||||
// build with C++20
|
||||
// JSON_HAS_CPP_20
|
||||
#if defined(__cpp_char8_t)
|
||||
SECTION("Using _json with char8_t literals #4945")
|
||||
{
|
||||
// Regular narrow string literal
|
||||
const auto j1 = R"({"key": "value", "num": 42})"_json;
|
||||
CHECK(j1["key"] == "value");
|
||||
CHECK(j1["num"] == 42);
|
||||
|
||||
// UTF-8 prefixed literal (C++20 and later); the emoji is written as a
|
||||
// \U escape rather than a raw multibyte character so this does not
|
||||
// depend on the compiler's source-file encoding (e.g., MSVC without
|
||||
// /utf-8, or classic ICC, which does not encode non-ASCII narrow
|
||||
// string literals as UTF-8 - compare against a \x-escaped expectation
|
||||
// for the same reason)
|
||||
const auto j2 = u8"{\"emoji\": \"\U0001F600\", \"msg\": \"hello\"}"_json;
|
||||
CHECK(j2["emoji"] == "\xF0\x9F\x98\x80");
|
||||
CHECK(j2["msg"] == "hello");
|
||||
|
||||
const auto j3 = u8R"({"key": "value", "num": 42})"_json;
|
||||
CHECK(j3["key"] == "value");
|
||||
CHECK(j3["num"] == 42);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
// select the types to test - char8_t is only available since C++20 if and only
|
||||
// if __cpp_char8_t is defined.
|
||||
#define TYPE_LIST(...) __VA_ARGS__
|
||||
#if defined(__cpp_char8_t) && (__cpp_char8_t >= 201811L)
|
||||
#define ASCII_TYPES TYPE_LIST(char, wchar_t, char16_t, char32_t, char8_t)
|
||||
#else
|
||||
#define ASCII_TYPES TYPE_LIST(char, wchar_t, char16_t, char32_t)
|
||||
#endif
|
||||
|
||||
TEST_CASE_TEMPLATE("deserialization of different character types (ASCII)", T, ASCII_TYPES) // NOLINT(readability-math-missing-parentheses, bugprone-throwing-static-initialization)
|
||||
TEST_CASE_TEMPLATE("deserialization of different character types (ASCII)", T, char, wchar_t, char16_t, char32_t) // NOLINT(readability-math-missing-parentheses, bugprone-throwing-static-initialization)
|
||||
{
|
||||
std::vector<T> const v = {'t', 'r', 'u', 'e'};
|
||||
CHECK(json::parse(v) == json(true));
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
@@ -27,6 +27,9 @@ using nlohmann::json;
|
||||
DOCTEST_CLANG_SUPPRESS_WARNING_PUSH
|
||||
DOCTEST_CLANG_SUPPRESS_WARNING("-Wexit-time-destructors")
|
||||
|
||||
// the types live in a namespace, because the file may be compiled together with other test files
|
||||
namespace enum_keyed_maps_default
|
||||
{
|
||||
enum class cards {kreuz, pik, herz, karo};
|
||||
|
||||
// NOLINTNEXTLINE(misc-use-internal-linkage,misc-const-correctness,cppcoreguidelines-avoid-c-arrays,hicpp-avoid-c-arrays,modernize-avoid-c-arrays) - false positive
|
||||
@@ -65,6 +68,7 @@ NLOHMANN_JSON_SERIALIZE_ENUM_STRICT(strict_cards,
|
||||
{strict_cards::herz, "herz"},
|
||||
{strict_cards::karo, "karo"}
|
||||
})
|
||||
} // namespace enum_keyed_maps_default
|
||||
|
||||
namespace
|
||||
{
|
||||
@@ -82,6 +86,15 @@ struct enum_hash
|
||||
// see unit-enum_keyed_maps.cpp for JSON_USE_OBJECTS_FOR_ENUM_KEYED_MAPS=1
|
||||
TEST_CASE("maps with enum keys")
|
||||
{
|
||||
// block-scope using-declarations take precedence over same-named types of other files in the same translation unit
|
||||
using enum_keyed_maps_default::cards;
|
||||
using enum_keyed_maps_default::strict_cards;
|
||||
using enum_keyed_maps_default::TaskState;
|
||||
using enum_keyed_maps_default::TS_COMPLETED;
|
||||
using enum_keyed_maps_default::TS_INVALID;
|
||||
using enum_keyed_maps_default::TS_RUNNING;
|
||||
using enum_keyed_maps_default::TS_STOPPED;
|
||||
|
||||
using task_map = std::map<TaskState, std::string>;
|
||||
using task_umap = std::unordered_map<TaskState, std::string, enum_hash>;
|
||||
using task_gmap = std::map<TaskState, std::string, std::greater<TaskState>>;
|
||||
|
||||
@@ -6,11 +6,11 @@
|
||||
// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann <https://nlohmann.me>
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
// cmake/test.cmake selects the C++ standard versions with which to build a
|
||||
// unit test based on the presence of JSON_HAS_CPP_<VERSION> macros.
|
||||
// The regression below only showed on C++17, so build this file for every
|
||||
// standard like the other regression tests:
|
||||
// JSON_HAS_CPP_17 JSON_HAS_CPP_20 (do not remove; see note at top of file)
|
||||
// cmake/test.cmake builds a unit test with C++ standards beyond C++11 only if the
|
||||
// source file mentions the corresponding version macro. The regression below only
|
||||
// showed on C++17, and the whole file is just this explicit instantiation, so it
|
||||
// cannot be split: build this file for every standard like the other regression tests:
|
||||
// JSON_HAS_CPP_17 JSON_HAS_CPP_20 (do not remove)
|
||||
|
||||
#include "doctest_compatibility.h"
|
||||
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
// __ _____ _____ _____
|
||||
// __| | __| | | | JSON for Modern C++ (supporting code)
|
||||
// | | |__ | | | | | | version 3.12.0
|
||||
// |_____|_____|_____|_|___| https://github.com/nlohmann/json
|
||||
//
|
||||
// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann <https://nlohmann.me>
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
// This file contains the C++20-only part of unit-iterators2.cpp (iterators and
|
||||
// std::ranges). It is kept in a separate translation unit so the (much larger) unit-
|
||||
// iterators2.cpp is built for C++11 only and not rebuilt for every C++ standard.
|
||||
|
||||
#include "doctest_compatibility.h"
|
||||
|
||||
#include <nlohmann/json.hpp>
|
||||
using nlohmann::json;
|
||||
|
||||
#ifdef JSON_HAS_CPP_20
|
||||
#include <iterator>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <type_traits>
|
||||
#include <utility>
|
||||
|
||||
#if JSON_HAS_RANGES
|
||||
#include <algorithm>
|
||||
#include <ranges>
|
||||
#endif
|
||||
|
||||
TEST_CASE("iterators 2 (C++20)")
|
||||
{
|
||||
#if JSON_HAS_RANGES
|
||||
SECTION("ranges")
|
||||
{
|
||||
SECTION("concepts")
|
||||
{
|
||||
using nlohmann::detail::iteration_proxy_value;
|
||||
CHECK(std::bidirectional_iterator<json::iterator>);
|
||||
CHECK(std::input_iterator<iteration_proxy_value<json::iterator>>);
|
||||
|
||||
CHECK(std::is_same<json::iterator, std::ranges::iterator_t<json>>::value);
|
||||
CHECK(std::ranges::bidirectional_range<json>);
|
||||
|
||||
using nlohmann::detail::iteration_proxy;
|
||||
using items_type = decltype(std::declval<json&>().items());
|
||||
CHECK(std::is_same<items_type, iteration_proxy<json::iterator>>::value);
|
||||
CHECK(std::is_same<iteration_proxy_value<json::iterator>, std::ranges::iterator_t<items_type>>::value);
|
||||
CHECK(std::ranges::input_range<items_type>);
|
||||
}
|
||||
|
||||
SECTION("algorithms")
|
||||
{
|
||||
SECTION("copy")
|
||||
{
|
||||
json j{"foo", "bar"};
|
||||
auto j_copied = json::array();
|
||||
|
||||
std::ranges::copy(j, std::back_inserter(j_copied));
|
||||
|
||||
CHECK(j == j_copied);
|
||||
}
|
||||
|
||||
SECTION("find_if")
|
||||
{
|
||||
json j{1, 3, 2, 4};
|
||||
auto j_even = json::array();
|
||||
|
||||
#if JSON_USE_IMPLICIT_CONVERSIONS
|
||||
auto it = std::ranges::find_if(j, [](int v) noexcept
|
||||
{
|
||||
return (v % 2) == 0;
|
||||
});
|
||||
#else
|
||||
auto it = std::ranges::find_if(j, [](const json & j) noexcept
|
||||
{
|
||||
int v;
|
||||
j.get_to(v);
|
||||
return (v % 2) == 0;
|
||||
});
|
||||
#endif
|
||||
|
||||
CHECK(*it == 2);
|
||||
}
|
||||
}
|
||||
|
||||
SECTION("views")
|
||||
{
|
||||
SECTION("reverse")
|
||||
{
|
||||
json j{1, 2, 3, 4, 5};
|
||||
json j_expected{5, 4, 3, 2, 1};
|
||||
|
||||
auto reversed = j | std::views::reverse;
|
||||
CHECK(reversed == j_expected);
|
||||
}
|
||||
|
||||
SECTION("transform")
|
||||
{
|
||||
json j
|
||||
{
|
||||
{ "a_key", "a_value"},
|
||||
{ "b_key", "b_value"},
|
||||
{ "c_key", "c_value"},
|
||||
};
|
||||
json j_expected{"a_key", "b_key", "c_key"};
|
||||
|
||||
// NOLINTNEXTLINE(fuchsia-trailing-return)
|
||||
auto transformed = j.items() | std::views::transform([](const auto & item) -> std::string_view
|
||||
{
|
||||
return item.key();
|
||||
});
|
||||
auto j_transformed = json::array();
|
||||
std::ranges::copy(transformed, std::back_inserter(j_transformed));
|
||||
|
||||
CHECK(j_transformed == j_expected);
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -6,23 +6,17 @@
|
||||
// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann <https://nlohmann.me>
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
// cmake/test.cmake selects the C++ standard versions with which to build a
|
||||
// unit test based on the presence of JSON_HAS_CPP_<VERSION> macros.
|
||||
// When using macros that are only defined for particular versions of the standard
|
||||
// (e.g., JSON_HAS_FILESYSTEM for C++17 and up), please mention the corresponding
|
||||
// version macro in a comment close by, like this:
|
||||
// JSON_HAS_CPP_<VERSION> (do not remove; see note at top of file)
|
||||
// cmake/test.cmake builds a unit test with C++ standards beyond C++11 only if the
|
||||
// source file mentions the corresponding version macro. To avoid rebuilding this
|
||||
// large file for every standard, tests that depend on the standard version (e.g.,
|
||||
// those using JSON_HAS_FILESYSTEM, JSON_HAS_RANGES, or JSON_HAS_THREE_WAY_COMPARISON)
|
||||
// go into a separate file unit-iterators2-cpp<NN>.cpp. This file stays C++11-only.
|
||||
|
||||
#include "doctest_compatibility.h"
|
||||
|
||||
#include <nlohmann/json.hpp>
|
||||
using nlohmann::json;
|
||||
|
||||
#if JSON_HAS_RANGES
|
||||
#include <algorithm>
|
||||
#include <ranges>
|
||||
#endif
|
||||
|
||||
TEST_CASE("iterators 2")
|
||||
{
|
||||
SECTION("iterator comparisons")
|
||||
@@ -872,94 +866,4 @@ TEST_CASE("iterators 2")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#if JSON_HAS_RANGES
|
||||
// JSON_HAS_CPP_20 (do not remove; see note at top of file)
|
||||
SECTION("ranges")
|
||||
{
|
||||
SECTION("concepts")
|
||||
{
|
||||
using nlohmann::detail::iteration_proxy_value;
|
||||
CHECK(std::bidirectional_iterator<json::iterator>);
|
||||
CHECK(std::input_iterator<iteration_proxy_value<json::iterator>>);
|
||||
|
||||
CHECK(std::is_same<json::iterator, std::ranges::iterator_t<json>>::value);
|
||||
CHECK(std::ranges::bidirectional_range<json>);
|
||||
|
||||
using nlohmann::detail::iteration_proxy;
|
||||
using items_type = decltype(std::declval<json&>().items());
|
||||
CHECK(std::is_same<items_type, iteration_proxy<json::iterator>>::value);
|
||||
CHECK(std::is_same<iteration_proxy_value<json::iterator>, std::ranges::iterator_t<items_type>>::value);
|
||||
CHECK(std::ranges::input_range<items_type>);
|
||||
}
|
||||
|
||||
SECTION("algorithms")
|
||||
{
|
||||
SECTION("copy")
|
||||
{
|
||||
json j{"foo", "bar"};
|
||||
auto j_copied = json::array();
|
||||
|
||||
std::ranges::copy(j, std::back_inserter(j_copied));
|
||||
|
||||
CHECK(j == j_copied);
|
||||
}
|
||||
|
||||
SECTION("find_if")
|
||||
{
|
||||
json j{1, 3, 2, 4};
|
||||
auto j_even = json::array();
|
||||
|
||||
#if JSON_USE_IMPLICIT_CONVERSIONS
|
||||
auto it = std::ranges::find_if(j, [](int v) noexcept
|
||||
{
|
||||
return (v % 2) == 0;
|
||||
});
|
||||
#else
|
||||
auto it = std::ranges::find_if(j, [](const json & j) noexcept
|
||||
{
|
||||
int v;
|
||||
j.get_to(v);
|
||||
return (v % 2) == 0;
|
||||
});
|
||||
#endif
|
||||
|
||||
CHECK(*it == 2);
|
||||
}
|
||||
}
|
||||
|
||||
SECTION("views")
|
||||
{
|
||||
SECTION("reverse")
|
||||
{
|
||||
json j{1, 2, 3, 4, 5};
|
||||
json j_expected{5, 4, 3, 2, 1};
|
||||
|
||||
auto reversed = j | std::views::reverse;
|
||||
CHECK(reversed == j_expected);
|
||||
}
|
||||
|
||||
SECTION("transform")
|
||||
{
|
||||
json j
|
||||
{
|
||||
{ "a_key", "a_value"},
|
||||
{ "b_key", "b_value"},
|
||||
{ "c_key", "c_value"},
|
||||
};
|
||||
json j_expected{"a_key", "b_key", "c_key"};
|
||||
|
||||
// NOLINTNEXTLINE(fuchsia-trailing-return)
|
||||
auto transformed = j.items() | std::views::transform([](const auto & item) -> std::string_view
|
||||
{
|
||||
return item.key();
|
||||
});
|
||||
auto j_transformed = json::array();
|
||||
std::ranges::copy(transformed, std::back_inserter(j_transformed));
|
||||
|
||||
CHECK(j_transformed == j_expected);
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
// __ _____ _____ _____
|
||||
// __| | __| | | | JSON for Modern C++ (supporting code)
|
||||
// | | |__ | | | | | | version 3.12.0
|
||||
// |_____|_____|_____|_|___| https://github.com/nlohmann/json
|
||||
//
|
||||
// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann <https://nlohmann.me>
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
// This file contains the C++20-only part of unit-json_pointer.cpp (JSON pointer comparison
|
||||
// with C++20 rewritten candidates and operator<=>, and char8_t literals). It is kept in a
|
||||
// separate translation unit so the (much larger) unit-json_pointer.cpp is built for C++11
|
||||
// only and not rebuilt for every C++ standard.
|
||||
|
||||
#include "doctest_compatibility.h"
|
||||
|
||||
// capture whether JSON_DELETE_DEPRECATED_FUNCTIONS was enabled on the command
|
||||
// line *before* including json.hpp, since the library #undefs it once the header
|
||||
// has been fully processed (see include/nlohmann/detail/macro_unscope.hpp); the
|
||||
// tests of deprecated functions are skipped if these functions are deleted
|
||||
#if defined(JSON_DELETE_DEPRECATED_FUNCTIONS) && (JSON_DELETE_DEPRECATED_FUNCTIONS == 1)
|
||||
#define JSON_TEST_DEPRECATED_FUNCTIONS_DELETED
|
||||
#endif
|
||||
|
||||
#include <nlohmann/json.hpp>
|
||||
using nlohmann::json;
|
||||
#ifdef JSON_TEST_NO_GLOBAL_UDLS
|
||||
using namespace nlohmann::literals; // NOLINT(google-build-using-namespace)
|
||||
#endif
|
||||
|
||||
#ifdef JSON_HAS_CPP_20
|
||||
#if JSON_HAS_THREE_WAY_COMPARISON
|
||||
#include <compare>
|
||||
#endif
|
||||
#include <string>
|
||||
#include <type_traits>
|
||||
|
||||
TEST_CASE("JSON pointers (C++20)")
|
||||
{
|
||||
SECTION("equality comparison")
|
||||
{
|
||||
std::string ptr_string{"/foo/bar"};
|
||||
auto ptr1 = json::json_pointer(ptr_string);
|
||||
auto ptr2 = json::json_pointer(ptr_string);
|
||||
|
||||
CHECK(ptr1 == ptr2);
|
||||
|
||||
CHECK_FALSE(ptr1 != ptr2);
|
||||
|
||||
#ifndef JSON_TEST_DEPRECATED_FUNCTIONS_DELETED
|
||||
const char* ptr_cpstring = "/foo/bar";
|
||||
const char ptr_castring[] = "/foo/bar"; // NOLINT(misc-const-correctness,hicpp-avoid-c-arrays,modernize-avoid-c-arrays,cppcoreguidelines-avoid-c-arrays)
|
||||
|
||||
CHECK(ptr1 == "/foo/bar");
|
||||
CHECK(ptr1 == ptr_cpstring);
|
||||
CHECK(ptr1 == ptr_castring);
|
||||
CHECK(ptr1 == ptr_string);
|
||||
|
||||
CHECK("/foo/bar" == ptr1);
|
||||
CHECK(ptr_cpstring == ptr1);
|
||||
CHECK(ptr_castring == ptr1);
|
||||
CHECK(ptr_string == ptr1);
|
||||
|
||||
CHECK_FALSE(ptr1 != "/foo/bar");
|
||||
CHECK_FALSE(ptr1 != ptr_cpstring);
|
||||
CHECK_FALSE(ptr1 != ptr_castring);
|
||||
CHECK_FALSE(ptr1 != ptr_string);
|
||||
|
||||
CHECK_FALSE("/foo/bar" != ptr1);
|
||||
CHECK_FALSE(ptr_cpstring != ptr1);
|
||||
CHECK_FALSE(ptr_castring != ptr1);
|
||||
CHECK_FALSE(ptr_string != ptr1);
|
||||
|
||||
SECTION("exceptions")
|
||||
{
|
||||
CHECK_THROWS_WITH_AS(ptr1 == "foo",
|
||||
"[json.exception.parse_error.107] parse error at byte 1: JSON pointer must be empty or begin with '/' - was: 'foo'", json::parse_error&);
|
||||
CHECK_THROWS_WITH_AS("foo" == ptr1,
|
||||
"[json.exception.parse_error.107] parse error at byte 1: JSON pointer must be empty or begin with '/' - was: 'foo'", json::parse_error&);
|
||||
CHECK_THROWS_WITH_AS(ptr1 == "/~~",
|
||||
"[json.exception.parse_error.108] parse error: escape character '~' must be followed with '0' or '1'", json::parse_error&);
|
||||
CHECK_THROWS_WITH_AS("/~~" == ptr1,
|
||||
"[json.exception.parse_error.108] parse error: escape character '~' must be followed with '0' or '1'", json::parse_error&);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
SECTION("less-than comparison")
|
||||
{
|
||||
auto ptr1 = json::json_pointer("/foo/a");
|
||||
auto ptr2 = json::json_pointer("/foo/b");
|
||||
|
||||
#if JSON_HAS_THREE_WAY_COMPARISON
|
||||
CHECK((ptr1 <=> ptr2) == std::strong_ordering::less); // *NOPAD*
|
||||
CHECK(ptr2 > ptr1);
|
||||
#endif
|
||||
}
|
||||
|
||||
SECTION("backwards compatibility and mixing")
|
||||
{
|
||||
json j = R"(
|
||||
{
|
||||
"foo": ["bar", "baz"]
|
||||
}
|
||||
)"_json;
|
||||
|
||||
using nlohmann::ordered_json;
|
||||
using json_ptr_str = nlohmann::json_pointer<std::string>;
|
||||
using json_ptr_j = nlohmann::json_pointer<json>;
|
||||
using json_ptr_oj = nlohmann::json_pointer<ordered_json>;
|
||||
|
||||
std::string const ptr_string{"/foo/0"};
|
||||
json_ptr_str ptr{ptr_string};
|
||||
json_ptr_j ptr_j{ptr_string};
|
||||
json_ptr_oj ptr_oj{ptr_string};
|
||||
|
||||
SECTION("equality comparison")
|
||||
{
|
||||
CHECK(ptr == ptr_j);
|
||||
CHECK(ptr == ptr_oj);
|
||||
CHECK(ptr_j == ptr);
|
||||
CHECK(ptr_j == ptr_oj);
|
||||
CHECK(ptr_oj == ptr_j);
|
||||
CHECK(ptr_oj == ptr);
|
||||
|
||||
CHECK_FALSE(ptr != ptr_j);
|
||||
CHECK_FALSE(ptr != ptr_oj);
|
||||
CHECK_FALSE(ptr_j != ptr);
|
||||
CHECK_FALSE(ptr_j != ptr_oj);
|
||||
CHECK_FALSE(ptr_oj != ptr_j);
|
||||
CHECK_FALSE(ptr_oj != ptr);
|
||||
}
|
||||
}
|
||||
|
||||
#if defined(__cpp_char8_t)
|
||||
SECTION("Using _json_pointer with char8_t literals #4945")
|
||||
{
|
||||
const json j = R"({"a": {"b": {"c": 123}}})"_json;
|
||||
const auto p1 = "/a/b/c"_json_pointer;
|
||||
CHECK(j[p1] == 123);
|
||||
|
||||
const auto p2 = u8"/a/b/c"_json_pointer;
|
||||
CHECK(j[p2] == 123);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -779,14 +779,13 @@ TEST_CASE("JSON pointers")
|
||||
CHECK(ptr.to_string() == "/object/~1");
|
||||
}
|
||||
|
||||
#if !JSON_HAS_THREE_WAY_COMPARISON // with three-way comparison, see unit-json_pointer-cpp20.cpp
|
||||
SECTION("equality comparison")
|
||||
{
|
||||
std::string ptr_string{"/foo/bar"};
|
||||
auto ptr1 = json::json_pointer(ptr_string);
|
||||
auto ptr2 = json::json_pointer(ptr_string);
|
||||
|
||||
// build with C++20 to test rewritten candidates
|
||||
// JSON_HAS_CPP_20
|
||||
|
||||
CHECK(ptr1 == ptr2);
|
||||
|
||||
@@ -829,6 +828,7 @@ TEST_CASE("JSON pointers")
|
||||
}
|
||||
#endif
|
||||
}
|
||||
#endif
|
||||
|
||||
SECTION("less-than comparison")
|
||||
{
|
||||
@@ -838,12 +838,6 @@ TEST_CASE("JSON pointers")
|
||||
CHECK(ptr1 < ptr2);
|
||||
CHECK_FALSE(ptr2 < ptr1);
|
||||
|
||||
// build with C++20
|
||||
// JSON_HAS_CPP_20
|
||||
#if JSON_HAS_THREE_WAY_COMPARISON
|
||||
CHECK((ptr1 <=> ptr2) == std::strong_ordering::less); // *NOPAD*
|
||||
CHECK(ptr2 > ptr1);
|
||||
#endif
|
||||
}
|
||||
|
||||
SECTION("usable as map key")
|
||||
@@ -899,10 +893,9 @@ TEST_CASE("JSON pointers")
|
||||
CHECK_FALSE(ptr != ptr_j);
|
||||
CHECK_FALSE(ptr != ptr_oj);
|
||||
|
||||
#if !JSON_HAS_THREE_WAY_COMPARISON // with three-way comparison, see unit-json_pointer-cpp20.cpp
|
||||
SECTION("equality comparison")
|
||||
{
|
||||
// build with C++20 to test rewritten candidates
|
||||
// JSON_HAS_CPP_20
|
||||
|
||||
CHECK(ptr == ptr_j);
|
||||
CHECK(ptr == ptr_oj);
|
||||
@@ -918,6 +911,7 @@ TEST_CASE("JSON pointers")
|
||||
CHECK_FALSE(ptr_oj != ptr_j);
|
||||
CHECK_FALSE(ptr_oj != ptr);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
SECTION("value(json_pointer, default) with ordered_json #5664")
|
||||
@@ -939,19 +933,6 @@ TEST_CASE("JSON pointers")
|
||||
CHECK(j.value(ptr_missing, 42) == 42);
|
||||
}
|
||||
|
||||
// build with C++20
|
||||
// JSON_HAS_CPP_20
|
||||
#if defined(__cpp_char8_t)
|
||||
SECTION("Using _json_pointer with char8_t literals #4945")
|
||||
{
|
||||
const json j = R"({"a": {"b": {"c": 123}}})"_json;
|
||||
const auto p1 = "/a/b/c"_json_pointer;
|
||||
CHECK(j[p1] == 123);
|
||||
|
||||
const auto p2 = u8"/a/b/c"_json_pointer;
|
||||
CHECK(j[p2] == 123);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
TEST_CASE("unescaping keeps a '~' that does not start an escape sequence")
|
||||
|
||||
@@ -173,7 +173,7 @@ void check_string(const std::string& content)
|
||||
}
|
||||
|
||||
// a small deterministic generator of documents
|
||||
struct generator
|
||||
struct builder_generator
|
||||
{
|
||||
std::mt19937 rng{5295}; // NOLINT(cert-msc32-c,cert-msc51-cpp,bugprone-random-generator-seed)
|
||||
|
||||
@@ -407,7 +407,7 @@ TEST_CASE("json_view builder")
|
||||
|
||||
SECTION("generated documents and damaged copies")
|
||||
{
|
||||
generator g;
|
||||
builder_generator g;
|
||||
for (int i = 0; i < 3000; ++i)
|
||||
{
|
||||
std::string text;
|
||||
|
||||
@@ -12,7 +12,6 @@
|
||||
using nlohmann::json;
|
||||
using nlohmann::ordered_json;
|
||||
using nlohmann::json_document;
|
||||
using nlohmann::ordered_json_editable_document;
|
||||
using nlohmann::json_view;
|
||||
using nlohmann::ordered_json_document;
|
||||
using nlohmann::ordered_json_view;
|
||||
@@ -186,131 +185,6 @@ TEST_CASE("json_view dump")
|
||||
CHECK(deep_document.root().dump() == deep);
|
||||
}
|
||||
|
||||
SECTION("ensure_ascii: a character that needs an escape at each position of a string")
|
||||
{
|
||||
// string lengths around the width of the vector steps, in a string value, a key, and a value near the end of the text
|
||||
for (const char* const special :
|
||||
{
|
||||
"\xC3\xA9", "\xE4\xB8\xAD", "\xF0\x9F\x98\x80", "\x7F", "\\n", "\\u0001", "\\\"", "\\\\"
|
||||
})
|
||||
{
|
||||
for (std::size_t length = 0; length <= 40; ++length)
|
||||
{
|
||||
for (std::size_t position = 0; position <= length; ++position)
|
||||
{
|
||||
std::string content(length, 'a');
|
||||
content.insert(position, special);
|
||||
const std::string texts[] =
|
||||
{
|
||||
"\"" + content + "\"",
|
||||
"[\"" + content + "\"]",
|
||||
"{\"" + content + "\":1}",
|
||||
};
|
||||
for (const std::string& text : texts)
|
||||
{
|
||||
CAPTURE(text)
|
||||
const ordered_json_document d = ordered_json_document::parse(text);
|
||||
const ordered_json j = ordered_json::parse(text);
|
||||
CHECK(d.root().dump(-1, ' ', true) == j.dump(-1, ' ', true));
|
||||
CHECK(d.root().dump(2, ' ', true) == j.dump(2, ' ', true));
|
||||
CHECK(d.root().dump() == j.dump());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SECTION("ensure_ascii: every character of the Basic Multilingual Plane and some beyond")
|
||||
{
|
||||
// (the two- and three-byte sequences, including the ranges that need a check: E0, ED, EF BF BF)
|
||||
std::string text = "[";
|
||||
std::size_t in_chunk = 0;
|
||||
const auto append = [&](std::uint32_t cp)
|
||||
{
|
||||
if (cp < 0x80)
|
||||
{
|
||||
if (cp < 0x20 || cp == '"' || cp == '\\')
|
||||
{
|
||||
return;
|
||||
}
|
||||
text += static_cast<char>(cp);
|
||||
}
|
||||
else if (cp < 0x800)
|
||||
{
|
||||
text += static_cast<char>(0xC0 | (cp >> 6));
|
||||
text += static_cast<char>(0x80 | (cp & 0x3F));
|
||||
}
|
||||
else if (cp < 0x10000)
|
||||
{
|
||||
text += static_cast<char>(0xE0 | (cp >> 12));
|
||||
text += static_cast<char>(0x80 | ((cp >> 6) & 0x3F));
|
||||
text += static_cast<char>(0x80 | (cp & 0x3F));
|
||||
}
|
||||
else
|
||||
{
|
||||
text += static_cast<char>(0xF0 | (cp >> 18));
|
||||
text += static_cast<char>(0x80 | ((cp >> 12) & 0x3F));
|
||||
text += static_cast<char>(0x80 | ((cp >> 6) & 0x3F));
|
||||
text += static_cast<char>(0x80 | (cp & 0x3F));
|
||||
}
|
||||
};
|
||||
for (std::uint32_t cp = 0x20; cp < 0x10400; ++cp)
|
||||
{
|
||||
if (cp >= 0xD800 && cp < 0xE000)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (in_chunk == 0)
|
||||
{
|
||||
text += text.size() > 1 ? ",\"" : "\"";
|
||||
}
|
||||
append(cp);
|
||||
if (++in_chunk == 37)
|
||||
{
|
||||
text += "\"";
|
||||
in_chunk = 0;
|
||||
}
|
||||
}
|
||||
text += in_chunk != 0 ? "\"]" : "]";
|
||||
const ordered_json_document d = ordered_json_document::parse(text);
|
||||
const ordered_json j = ordered_json::parse(text);
|
||||
CHECK(d.root().dump(-1, ' ', true) == j.dump(-1, ' ', true));
|
||||
CHECK(d.root().dump(2, ' ', true) == j.dump(2, ' ', true));
|
||||
CHECK(d.root().dump() == j.dump());
|
||||
CHECK(d.root().dump(2) == j.dump(2));
|
||||
}
|
||||
|
||||
SECTION("deep nesting, indented")
|
||||
{
|
||||
// indentations of up to and beyond the width of the writer's fixed-size copy
|
||||
for (const int depth :
|
||||
{
|
||||
1, 5, 20, 70, 300
|
||||
})
|
||||
{
|
||||
std::string text;
|
||||
for (int i = 0; i < depth; ++i)
|
||||
{
|
||||
text += i % 2 == 0 ? "[1," : "{\"k\":2,\"\\u00e9\":";
|
||||
}
|
||||
text += "[]";
|
||||
for (int i = depth - 1; i >= 0; --i)
|
||||
{
|
||||
text += i % 2 == 0 ? "]" : "}";
|
||||
}
|
||||
const ordered_json_document d = ordered_json_document::parse(text);
|
||||
const ordered_json j = ordered_json::parse(text);
|
||||
for (const int indent :
|
||||
{
|
||||
0, 1, 2, 3, 8
|
||||
})
|
||||
{
|
||||
CHECK(d.root().dump(indent) == j.dump(indent));
|
||||
CHECK(d.root().dump(indent, '\t') == j.dump(indent, '\t'));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SECTION("the output buffer of a small value is small")
|
||||
{
|
||||
// an escaped key after the value: its node lies in the arena, so the
|
||||
@@ -345,76 +219,6 @@ TEST_CASE("json_view dump")
|
||||
CHECK(d.root()["a"].dump(-1, ' ', true).size() == 2 + (5000 * 6));
|
||||
}
|
||||
|
||||
SECTION("ensure_ascii with strings of the source, decoded strings, and edited strings")
|
||||
{
|
||||
// plain ASCII, non-ASCII, escaped (decoded), and mixed strings, as values and as keys;
|
||||
// the second text has a DEL, which ensure_ascii writes as an escape
|
||||
for (const char* const del :
|
||||
{"", "\x7F"
|
||||
})
|
||||
{
|
||||
const std::string text = std::string("{\"plain\":\"abc\",\"caf\xC3\xA9\":\"\xC3\xA9\xE2\x82\xAC\xF0\x9F\x98\x80\",")
|
||||
+ "\"esc\\n\\\"\":\"a\\tb\\u00e4\\ud83d\\ude00\",\"del\":\"a" + del + "b\",\"\":\"\"," // NOLINT(modernize-raw-string-literal)
|
||||
+ "\"list\":[\"x\",\"\xC3\xBC\",\"\\\\\",\"long ASCII string with more than thirty-two bytes in it\","
|
||||
+ "\"long string with more than thirty-two bytes and one non-ASCII byte \xC3\xB6\"],"
|
||||
+ "\"n\":[1,2.5,-0,true,null]}";
|
||||
const ordered_json j = ordered_json::parse(text);
|
||||
const ordered_json_document d = ordered_json_document::parse(text);
|
||||
CHECK(d.root().dump(-1, ' ', true) == j.dump(-1, ' ', true));
|
||||
CHECK(d.root().dump(4, ' ', true) == j.dump(4, ' ', true));
|
||||
CHECK(d.root().dump() == j.dump());
|
||||
CHECK(d.root()["list"].dump(-1, ' ', true) == j["list"].dump(-1, ' ', true));
|
||||
CHECK(d.root()["del"].dump(-1, ' ', true) == j["del"].dump(-1, ' ', true));
|
||||
|
||||
// strings that an edit wrote (the source strings that they replace were plain)
|
||||
ordered_json_editable_document e = ordered_json_editable_document::parse(text);
|
||||
ordered_json k = j;
|
||||
e.set(e.root()["plain"], std::string("\xC3\xA4gypten"));
|
||||
k["plain"] = "\xC3\xA4gypten";
|
||||
e.set(e.root()["list"][0], std::string("ascii after edit"));
|
||||
k["list"][0] = "ascii after edit";
|
||||
e.set(e.root()["list"][1], std::string("\xC3\xA4"));
|
||||
k["list"][1] = "\xC3\xA4";
|
||||
e.set(e.root()["del"], std::string("tab\there"));
|
||||
k["del"] = "tab\there";
|
||||
CHECK(e.root().dump(-1, ' ', true) == k.dump(-1, ' ', true));
|
||||
CHECK(e.root().dump(2, ' ', true) == k.dump(2, ' ', true));
|
||||
CHECK(e.root().dump() == k.dump());
|
||||
|
||||
// strings that were edited from or to ASCII text
|
||||
e.set(e.root()["list"][2], std::string("\xC3\xA4"));
|
||||
k["list"][2] = "\xC3\xA4";
|
||||
e.set(e.root()["list"][4], std::string("plain again"));
|
||||
k["list"][4] = "plain again";
|
||||
CHECK(e.root().dump(-1, ' ', true) == k.dump(-1, ' ', true));
|
||||
}
|
||||
}
|
||||
|
||||
SECTION("ensure_ascii with DEL or a non-ASCII character at every position")
|
||||
{
|
||||
// the check whether a string of the source has no byte >= 0x7F reads
|
||||
// eight bytes at a time and then the rest one by one
|
||||
for (std::size_t length = 1; length <= 20; ++length)
|
||||
{
|
||||
for (std::size_t pos = 0; pos < length; ++pos)
|
||||
{
|
||||
for (const char* const c :
|
||||
{
|
||||
"\x7F", "\x7E", "\xC3\xA4", "\xC3\xBF" // NOLINT(modernize-raw-string-literal)
|
||||
})
|
||||
{
|
||||
const std::string s = std::string(pos, 'a') + c + std::string(length - pos - 1, 'b');
|
||||
std::string text = "[\"";
|
||||
text.append(s).append("\",{\"").append(s).append("\":1}]");
|
||||
const json_document d = json_document::parse(text);
|
||||
const json j = json::parse(text);
|
||||
CHECK(d.root().dump(-1, ' ', true) == j.dump(-1, ' ', true));
|
||||
CHECK(d.root().dump(1, ' ', true) == j.dump(1, ' ', true));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SECTION("streams and discarded views")
|
||||
{
|
||||
const json_document d = json_document::parse(R"({"a": [1, 2]})");
|
||||
|
||||
@@ -39,7 +39,7 @@ using ptr_t = ordered_json::json_pointer;
|
||||
|
||||
namespace
|
||||
{
|
||||
std::uint32_t rng()
|
||||
std::uint32_t edit_rng()
|
||||
{
|
||||
static std::mt19937 generator(5295); // NOLINT(cert-msc32-c,cert-msc51-cpp,bugprone-random-generator-seed): reproducible
|
||||
// result_type is std::uint_fast32_t, which may be wider than 32 bits
|
||||
@@ -49,12 +49,12 @@ std::uint32_t rng()
|
||||
|
||||
int r(int n)
|
||||
{
|
||||
return static_cast<int>(rng() % static_cast<unsigned>(n));
|
||||
return static_cast<int>(edit_rng() % static_cast<unsigned>(n));
|
||||
}
|
||||
|
||||
int counter = 0;
|
||||
int edit_counter = 0;
|
||||
|
||||
std::uint64_t bits(double x)
|
||||
std::uint64_t edit_bits(double x)
|
||||
{
|
||||
std::uint64_t b = 0;
|
||||
std::memcpy(&b, &x, sizeof(b));
|
||||
@@ -81,11 +81,11 @@ ordered_json random_scalar()
|
||||
case 1:
|
||||
return r(2) == 0;
|
||||
case 2:
|
||||
return static_cast<std::int64_t>(rng()) - 2147483648LL;
|
||||
return static_cast<std::int64_t>(edit_rng()) - 2147483648LL;
|
||||
case 3:
|
||||
return (static_cast<std::uint64_t>(rng()) * 4294967296ULL) + rng();
|
||||
return (static_cast<std::uint64_t>(edit_rng()) * 4294967296ULL) + edit_rng();
|
||||
case 4:
|
||||
return static_cast<double>(static_cast<std::int32_t>(rng())) / (1 + r(1000));
|
||||
return static_cast<double>(static_cast<std::int32_t>(edit_rng())) / (1 + r(1000));
|
||||
case 5:
|
||||
return r(2) == 0 ? 1e300 * (r(2) == 0 ? 1 : -1) : 5e-324;
|
||||
case 6:
|
||||
@@ -103,7 +103,7 @@ ordered_json random_value(int depth)
|
||||
ordered_json o = ordered_json::object();
|
||||
for (int i = r(5); i > 0; --i)
|
||||
{
|
||||
o[random_string() + "#" + std::to_string(counter++)] = random_value(depth + 1);
|
||||
o[random_string() + "#" + std::to_string(edit_counter++)] = random_value(depth + 1);
|
||||
}
|
||||
return o;
|
||||
}
|
||||
@@ -178,7 +178,7 @@ void compare(const ordered_json_editable_view& v, const ordered_json& j)
|
||||
}
|
||||
else if (j.is_number_float())
|
||||
{
|
||||
CHECK(bits(v.get<double>()) == bits(j.get<double>()));
|
||||
CHECK(edit_bits(v.get<double>()) == edit_bits(j.get<double>()));
|
||||
}
|
||||
else if (j.is_number_integer())
|
||||
{
|
||||
@@ -435,7 +435,7 @@ TEST_CASE("json_view edits: differential")
|
||||
}
|
||||
else if ((op == 4 || op == 5) && target.is_object()) // set a member (new or existing)
|
||||
{
|
||||
std::string key = random_string() + "#" + std::to_string(counter++);
|
||||
std::string key = random_string() + "#" + std::to_string(edit_counter++);
|
||||
if (op == 5 && !target.empty())
|
||||
{
|
||||
key = std::next(target.begin(), r(static_cast<int>(target.size()))).key();
|
||||
|
||||
@@ -38,7 +38,7 @@ using nlohmann::detail::view::node;
|
||||
namespace
|
||||
{
|
||||
#if !defined(JSON_NOEXCEPTION)
|
||||
std::string exception_of(const std::function<void()>& f)
|
||||
std::string image_exception_of(const std::function<void()>& f)
|
||||
{
|
||||
try
|
||||
{
|
||||
@@ -119,7 +119,7 @@ void set_node(std::vector<std::uint8_t>& image, std::size_t i, const node& n)
|
||||
/// the result of loading an image with a check: "" or the exception message
|
||||
std::string load_result(const std::vector<std::uint8_t>& image, image_check check)
|
||||
{
|
||||
return exception_of([&]
|
||||
return image_exception_of([&]
|
||||
{
|
||||
const json_document d = json_document::load(image, check);
|
||||
static_cast<void>(d);
|
||||
@@ -160,7 +160,7 @@ void check_round_trip(const Document& d)
|
||||
CHECK(e.root().dump() == d.root().dump());
|
||||
}
|
||||
|
||||
std::uint32_t rng()
|
||||
std::uint32_t image_rng()
|
||||
{
|
||||
static std::mt19937 generator(5295); // NOLINT(cert-msc32-c,cert-msc51-cpp,bugprone-random-generator-seed): reproducible
|
||||
// result_type is std::uint_fast32_t, which may be wider than 32 bits
|
||||
@@ -682,26 +682,6 @@ TEST_CASE("json_view images: ownership")
|
||||
#if !defined(JSON_NOEXCEPTION)
|
||||
TEST_CASE("json_view images: errors")
|
||||
{
|
||||
SECTION("invalid UTF-8 of a decoded string: the compact dump writes it, the others throw")
|
||||
{
|
||||
const json_document d = json_document::parse(R"({"k\n":"\u00e9x"})");
|
||||
std::vector<std::uint8_t> image = d.save();
|
||||
const std::size_t arena = text_at(image) + header_size_field(image, 16) + 1;
|
||||
image[arena + 3] = 0x28; // the key is "k\n"; the value starts at 3: 0xC3 0x28 is invalid
|
||||
const json_document loaded = json_document::load(image, image_check::bounds);
|
||||
CHECK(loaded.root().dump().size() == 13);
|
||||
for (const int indent :
|
||||
{
|
||||
0, 2
|
||||
})
|
||||
{
|
||||
CHECK_THROWS_AS(loaded.root().dump(indent), json::type_error);
|
||||
CHECK_THROWS_AS(loaded.root().dump(indent, ' ', true), json::type_error);
|
||||
}
|
||||
CHECK_THROWS_AS(loaded.root().dump(-1, ' ', true), json::type_error);
|
||||
CHECK_THROWS_WITH_AS(loaded.root().dump(2), "[json.exception.type_error.316] invalid UTF-8 byte at index 1: 0x28", json::type_error&);
|
||||
}
|
||||
|
||||
SECTION("a literal as the root: dump() after loading")
|
||||
{
|
||||
for (const char* text :
|
||||
@@ -721,9 +701,9 @@ TEST_CASE("json_view images: errors")
|
||||
SECTION("saving a discarded document")
|
||||
{
|
||||
const json_document empty{};
|
||||
CHECK(exception_of([&] { static_cast<void>(empty.save()); }) == "[json.exception.type_error.320] cannot save a discarded json_document");
|
||||
CHECK(image_exception_of([&] { static_cast<void>(empty.save()); }) == "[json.exception.type_error.320] cannot save a discarded json_document");
|
||||
const json_document failed = json_document::parse("[1,", false);
|
||||
CHECK(exception_of([&] { static_cast<void>(failed.save()); }) == "[json.exception.type_error.320] cannot save a discarded json_document");
|
||||
CHECK(image_exception_of([&] { static_cast<void>(failed.save()); }) == "[json.exception.type_error.320] cannot save a discarded json_document");
|
||||
}
|
||||
|
||||
const std::vector<std::uint8_t> image = json_document::parse(R"({"a": [1, "\u00e9"]})").save();
|
||||
@@ -731,12 +711,12 @@ TEST_CASE("json_view images: errors")
|
||||
|
||||
SECTION("header and sizes")
|
||||
{
|
||||
CHECK(exception_of([]
|
||||
CHECK(image_exception_of([]
|
||||
{
|
||||
const json_document d = json_document::load(nullptr, 0);
|
||||
static_cast<void>(d);
|
||||
}) == prefix + "too short");
|
||||
CHECK(exception_of([&]
|
||||
CHECK(image_exception_of([&]
|
||||
{
|
||||
const json_document d = json_document::load(image.data(), 63);
|
||||
static_cast<void>(d);
|
||||
@@ -1223,12 +1203,12 @@ TEST_CASE("json_view images: damaged images")
|
||||
for (int round = 0; round < 3000; ++round)
|
||||
{
|
||||
std::vector<std::uint8_t> b = image;
|
||||
const std::uint32_t flips = 1 + (rng() % 3);
|
||||
const std::uint32_t flips = 1 + (image_rng() % 3);
|
||||
for (std::uint32_t k = 0; k < flips; ++k)
|
||||
{
|
||||
// mostly the nodes, where the damage matters most
|
||||
const std::size_t at = rng() % 4 != 0 ? header_size + (rng() % (b.size() - header_size)) : rng() % b.size();
|
||||
b[at] = static_cast<std::uint8_t>(rng() % 3 == 0 ? rng() : b[at] ^ (1u << (rng() % 8)));
|
||||
const std::size_t at = image_rng() % 4 != 0 ? header_size + (image_rng() % (b.size() - header_size)) : image_rng() % b.size();
|
||||
b[at] = static_cast<std::uint8_t>(image_rng() % 3 == 0 ? image_rng() : b[at] ^ (1u << (image_rng() % 8)));
|
||||
}
|
||||
for (const image_check check :
|
||||
{
|
||||
|
||||
@@ -46,7 +46,7 @@ void reference_merge_patch(json& target, const json& patch)
|
||||
|
||||
// objects nested `depth` levels deep under the key "a", with members that
|
||||
// differ by `variant` on the way down
|
||||
std::string nested_objects(const std::size_t depth, const int variant)
|
||||
std::string nested_objects_merge_patch(const std::size_t depth, const int variant)
|
||||
{
|
||||
std::string text;
|
||||
for (std::size_t i = 0; i < depth; ++i)
|
||||
@@ -309,9 +309,9 @@ TEST_CASE("JSON Merge Patch on deeply nested values")
|
||||
for (int variant = 0; variant < 3; ++variant)
|
||||
{
|
||||
CAPTURE(variant)
|
||||
const json patch = json::parse(nested_objects(depth, variant));
|
||||
const json patch = json::parse(nested_objects_merge_patch(depth, variant));
|
||||
|
||||
json result = json::parse(nested_objects(depth, (variant + 1) % 3));
|
||||
json result = json::parse(nested_objects_merge_patch(depth, (variant + 1) % 3));
|
||||
json expected = result;
|
||||
result.merge_patch(patch);
|
||||
reference_merge_patch(expected, patch);
|
||||
@@ -332,8 +332,8 @@ TEST_CASE("JSON Merge Patch on deeply nested values")
|
||||
// applying a patch used to recurse once per nesting level. The result
|
||||
// is only walked, never copied or compared, since those recurse too.
|
||||
const std::size_t depth = 100000;
|
||||
json target = json::parse(nested_objects(depth, 0));
|
||||
target.merge_patch(json::parse(nested_objects(depth, 1)));
|
||||
json target = json::parse(nested_objects_merge_patch(depth, 0));
|
||||
target.merge_patch(json::parse(nested_objects_merge_patch(depth, 1)));
|
||||
|
||||
const json* p = ⌖
|
||||
for (std::size_t i = 0; i < depth; ++i)
|
||||
@@ -404,7 +404,7 @@ TEST_CASE("merge_patch() with an argument that aliases *this (#5641)")
|
||||
})
|
||||
{
|
||||
CAPTURE(depth)
|
||||
json j = json::parse(nested_objects(depth, 0));
|
||||
json j = json::parse(nested_objects_merge_patch(depth, 0));
|
||||
const json expected = j;
|
||||
j.merge_patch(j);
|
||||
CHECK(j == expected);
|
||||
|
||||
@@ -35,7 +35,7 @@ void reference_update(json& target, const json& source)
|
||||
|
||||
// objects nested `depth` levels deep under the key "a", with members that
|
||||
// differ by `variant` on the way down
|
||||
std::string nested_objects(const std::size_t depth, const int variant)
|
||||
std::string nested_objects_update(const std::size_t depth, const int variant)
|
||||
{
|
||||
std::string text;
|
||||
for (std::size_t i = 0; i < depth; ++i)
|
||||
@@ -1131,8 +1131,8 @@ TEST_CASE("update() on deeply nested values")
|
||||
for (int variant = 0; variant < 3; ++variant)
|
||||
{
|
||||
CAPTURE(variant)
|
||||
const json source = json::parse(nested_objects(depth, variant));
|
||||
json result = json::parse(nested_objects(depth, (variant + 1) % 3));
|
||||
const json source = json::parse(nested_objects_update(depth, variant));
|
||||
json result = json::parse(nested_objects_update(depth, (variant + 1) % 3));
|
||||
json expected = result;
|
||||
result.update(source, true);
|
||||
reference_update(expected, source);
|
||||
@@ -1146,8 +1146,8 @@ TEST_CASE("update() on deeply nested values")
|
||||
// merging used to recurse once per nesting level. The result is only
|
||||
// walked, never copied or compared, since those recurse too.
|
||||
const std::size_t depth = 100000;
|
||||
json target = json::parse(nested_objects(depth, 0));
|
||||
target.update(json::parse(nested_objects(depth, 1)), true);
|
||||
json target = json::parse(nested_objects_update(depth, 0));
|
||||
target.update(json::parse(nested_objects_update(depth, 1)), true);
|
||||
|
||||
const json* p = ⌖
|
||||
for (std::size_t i = 0; i < depth; ++i)
|
||||
@@ -1223,7 +1223,7 @@ TEST_CASE("update() with an argument that aliases *this (#5641)")
|
||||
})
|
||||
{
|
||||
CAPTURE(depth)
|
||||
json j = json::parse(nested_objects(depth, 0));
|
||||
json j = json::parse(nested_objects_update(depth, 0));
|
||||
const json expected = j;
|
||||
j.update(j, true);
|
||||
CHECK(j == expected);
|
||||
|
||||
@@ -35,7 +35,7 @@ using nlohmann::json;
|
||||
|
||||
#if SIZE_MAX > UINT32_MAX
|
||||
template<typename T, typename A = std::allocator<T>>
|
||||
struct huge_array : std::vector<T, A>
|
||||
struct msgpack_huge_array : std::vector<T, A>
|
||||
{
|
||||
using base = std::vector<T, A>;
|
||||
using base::base;
|
||||
@@ -53,24 +53,24 @@ struct huge_array : std::vector<T, A>
|
||||
}
|
||||
};
|
||||
|
||||
using huge_array_json = nlohmann::json::with_array_t<huge_array>;
|
||||
using msgpack_huge_array_json = nlohmann::json::with_array_t<msgpack_huge_array>;
|
||||
|
||||
TEST_CASE("MessagePack Size above uint32 for array")
|
||||
{
|
||||
huge_array_json j = huge_array_json::array();
|
||||
msgpack_huge_array_json j = msgpack_huge_array_json::array();
|
||||
|
||||
j.push_back(1);
|
||||
j.push_back(2);
|
||||
j.push_back(3);
|
||||
|
||||
auto& array = j.get_ref<huge_array_json::array_t&>();
|
||||
auto& array = j.get_ref<msgpack_huge_array_json::array_t&>();
|
||||
array.fake_size = true;
|
||||
|
||||
// write into a caller-owned vector: to_msgpack(j) reserves space based on
|
||||
// the (faked) element count, which fails with bad_alloc on Windows
|
||||
std::vector<std::uint8_t> result;
|
||||
CHECK_THROWS_WITH_AS(
|
||||
huge_array_json::to_msgpack(j, result),
|
||||
msgpack_huge_array_json::to_msgpack(j, result),
|
||||
"[json.exception.out_of_range.412] MessagePack length 4294967296 exceeds maximum of 4294967295",
|
||||
json::out_of_range&);
|
||||
|
||||
@@ -80,7 +80,7 @@ TEST_CASE("MessagePack Size above uint32 for array")
|
||||
template<typename K, typename V,
|
||||
typename C = std::less<K>,
|
||||
typename A = std::allocator<std::pair<const K, V>>>
|
||||
struct huge_map : std::map<K, V, C, A>
|
||||
struct msgpack_huge_map : std::map<K, V, C, A>
|
||||
{
|
||||
using base = std::map<K, V, C, A>;
|
||||
using base::base;
|
||||
@@ -98,24 +98,24 @@ template<typename K, typename V,
|
||||
}
|
||||
};
|
||||
|
||||
using huge_object_json = nlohmann::json::with_object_t<huge_map>;
|
||||
using msgpack_huge_object_json = nlohmann::json::with_object_t<msgpack_huge_map>;
|
||||
|
||||
TEST_CASE("MessagePack Size above uint32 for object")
|
||||
{
|
||||
|
||||
huge_object_json j = huge_object_json::object();
|
||||
msgpack_huge_object_json j = msgpack_huge_object_json::object();
|
||||
|
||||
j["one"] = 1;
|
||||
j["two"] = 2;
|
||||
|
||||
auto& object = j.get_ref<huge_object_json::object_t&>();
|
||||
auto& object = j.get_ref<msgpack_huge_object_json::object_t&>();
|
||||
object.fake_size = true;
|
||||
|
||||
// write into a caller-owned vector: to_msgpack(j) reserves space based on
|
||||
// the (faked) element count, which fails with bad_alloc on Windows
|
||||
std::vector<std::uint8_t> result;
|
||||
CHECK_THROWS_WITH_AS(
|
||||
huge_object_json::to_msgpack(j, result),
|
||||
msgpack_huge_object_json::to_msgpack(j, result),
|
||||
"[json.exception.out_of_range.412] MessagePack length 4294967296 exceeds maximum of 4294967295",
|
||||
json::out_of_range&);
|
||||
|
||||
@@ -123,7 +123,7 @@ TEST_CASE("MessagePack Size above uint32 for object")
|
||||
}
|
||||
|
||||
#ifdef JSON_TEST_BEYOND_UINT32_STRING
|
||||
struct huge_string : std::string
|
||||
struct msgpack_huge_string : std::string
|
||||
{
|
||||
using std::string::string;
|
||||
|
||||
@@ -133,20 +133,20 @@ struct huge_string : std::string
|
||||
}
|
||||
};
|
||||
|
||||
using huge_string_json = nlohmann::json::with_string_t<huge_string>;
|
||||
using msgpack_huge_string_json = nlohmann::json::with_string_t<msgpack_huge_string>;
|
||||
|
||||
TEST_CASE("MessagePack Size above uint32 for string")
|
||||
{
|
||||
const huge_string_json j = "hello";
|
||||
const msgpack_huge_string_json j = "hello";
|
||||
|
||||
CHECK_THROWS_WITH_AS(
|
||||
huge_string_json::to_msgpack(j),
|
||||
msgpack_huge_string_json::to_msgpack(j),
|
||||
"[json.exception.out_of_range.412] MessagePack length 4294967296 exceeds maximum of 4294967295",
|
||||
json::out_of_range&);
|
||||
}
|
||||
#endif
|
||||
|
||||
struct huge_binary : std::vector<std::uint8_t>
|
||||
struct msgpack_huge_binary : std::vector<std::uint8_t>
|
||||
{
|
||||
using std::vector<std::uint8_t>::vector;
|
||||
|
||||
@@ -156,18 +156,18 @@ struct huge_binary : std::vector<std::uint8_t>
|
||||
}
|
||||
};
|
||||
|
||||
using huge_binary_json = nlohmann::json::with_binary_t<huge_binary>;
|
||||
using msgpack_huge_binary_json = nlohmann::json::with_binary_t<msgpack_huge_binary>;
|
||||
|
||||
TEST_CASE("MessagePack Size above uint32 for binary")
|
||||
{
|
||||
|
||||
huge_binary_json j = huge_binary_json::binary(huge_binary{});
|
||||
msgpack_huge_binary_json j = msgpack_huge_binary_json::binary(msgpack_huge_binary{});
|
||||
|
||||
j.get_binary().push_back(0x01);
|
||||
j.get_binary().push_back(0x02);
|
||||
|
||||
CHECK_THROWS_WITH_AS(
|
||||
huge_binary_json::to_msgpack(j),
|
||||
msgpack_huge_binary_json::to_msgpack(j),
|
||||
"[json.exception.out_of_range.412] MessagePack length 4294967296 exceeds maximum of 4294967295",
|
||||
json::out_of_range&);
|
||||
}
|
||||
|
||||
@@ -44,6 +44,8 @@ using nlohmann::ordered_json;
|
||||
// this type, which is presumably why the gap was never noticed.
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
namespace unit_ordered_json2_detail
|
||||
{
|
||||
class alt_string;
|
||||
bool operator<(const char* op1, const alt_string& op2) noexcept; // NOLINT(misc-use-internal-linkage)
|
||||
void int_to_string(alt_string& target, std::size_t value); // NOLINT(misc-use-internal-linkage)
|
||||
@@ -217,12 +219,15 @@ void int_to_string(alt_string& target, std::size_t value)
|
||||
target = std::to_string(value).c_str();
|
||||
}
|
||||
|
||||
using alt_json = nlohmann::json::with_string_t<alt_string>;
|
||||
using ordered_json2_alt_json = nlohmann::json::with_string_t<alt_string>;
|
||||
|
||||
bool operator<(const char* op1, const alt_string& op2) noexcept
|
||||
{
|
||||
return op1 < op2.str_impl;
|
||||
}
|
||||
} // namespace unit_ordered_json2_detail
|
||||
|
||||
using unit_ordered_json2_detail::ordered_json2_alt_json;
|
||||
|
||||
namespace
|
||||
{
|
||||
@@ -253,11 +258,11 @@ ordered_json make_rich_ordered_json()
|
||||
return j;
|
||||
}
|
||||
|
||||
alt_json make_rich_alt_json()
|
||||
ordered_json2_alt_json make_rich_alt_json()
|
||||
{
|
||||
alt_json j;
|
||||
ordered_json2_alt_json j;
|
||||
j["zebra"] = 1;
|
||||
j["apple"] = alt_json::array({1, 2, 3});
|
||||
j["apple"] = ordered_json2_alt_json::array({1, 2, 3});
|
||||
j["mango"]["z_nested"] = true;
|
||||
j["mango"]["a_nested"] = nullptr;
|
||||
j["banana"] = "some text";
|
||||
@@ -330,47 +335,47 @@ TEST_CASE("ordered_json across binary formats")
|
||||
|
||||
TEST_CASE("alt_json (custom string_t) across binary formats")
|
||||
{
|
||||
const alt_json original = make_rich_alt_json();
|
||||
const ordered_json2_alt_json original = make_rich_alt_json();
|
||||
|
||||
SECTION("CBOR")
|
||||
{
|
||||
const auto bytes = alt_json::to_cbor(original);
|
||||
const auto restored = alt_json::from_cbor(bytes);
|
||||
const auto bytes = ordered_json2_alt_json::to_cbor(original);
|
||||
const auto restored = ordered_json2_alt_json::from_cbor(bytes);
|
||||
CHECK(restored == original);
|
||||
}
|
||||
|
||||
SECTION("MessagePack")
|
||||
{
|
||||
const auto bytes = alt_json::to_msgpack(original);
|
||||
const auto restored = alt_json::from_msgpack(bytes);
|
||||
const auto bytes = ordered_json2_alt_json::to_msgpack(original);
|
||||
const auto restored = ordered_json2_alt_json::from_msgpack(bytes);
|
||||
CHECK(restored == original);
|
||||
}
|
||||
|
||||
SECTION("UBJSON")
|
||||
{
|
||||
const auto bytes = alt_json::to_ubjson(original);
|
||||
const auto restored = alt_json::from_ubjson(bytes);
|
||||
const auto bytes = ordered_json2_alt_json::to_ubjson(original);
|
||||
const auto restored = ordered_json2_alt_json::from_ubjson(bytes);
|
||||
CHECK(restored == original);
|
||||
}
|
||||
|
||||
SECTION("BON8")
|
||||
{
|
||||
const auto bytes = alt_json::to_bon8(original);
|
||||
const auto restored = alt_json::from_bon8(bytes);
|
||||
const auto bytes = ordered_json2_alt_json::to_bon8(original);
|
||||
const auto restored = ordered_json2_alt_json::from_bon8(bytes);
|
||||
CHECK(restored == original);
|
||||
}
|
||||
|
||||
SECTION("BSON")
|
||||
{
|
||||
const auto bytes = alt_json::to_bson(original);
|
||||
const auto restored = alt_json::from_bson(bytes);
|
||||
const auto bytes = ordered_json2_alt_json::to_bson(original);
|
||||
const auto restored = ordered_json2_alt_json::from_bson(bytes);
|
||||
CHECK(restored == original);
|
||||
}
|
||||
|
||||
SECTION("BJData")
|
||||
{
|
||||
const auto bytes = alt_json::to_bjdata(original);
|
||||
const auto restored = alt_json::from_bjdata(bytes);
|
||||
const auto bytes = ordered_json2_alt_json::to_bjdata(original);
|
||||
const auto restored = ordered_json2_alt_json::from_bjdata(bytes);
|
||||
CHECK(restored == original);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
// __ _____ _____ _____
|
||||
// __| | __| | | | JSON for Modern C++ (supporting code)
|
||||
// | | |__ | | | | | | version 3.12.0
|
||||
// |_____|_____|_____|_|___| https://github.com/nlohmann/json
|
||||
//
|
||||
// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann <https://nlohmann.me>
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
// This file contains the C++17-only part of unit-ordered_map.cpp (ordered_map::find with
|
||||
// std::string_view keys). It is kept in a separate translation unit so the (much larger)
|
||||
// unit-ordered_map.cpp is built for C++11 only and not rebuilt for every C++ standard.
|
||||
|
||||
#include "doctest_compatibility.h"
|
||||
|
||||
#include <nlohmann/json.hpp>
|
||||
using nlohmann::ordered_map;
|
||||
|
||||
#ifdef JSON_HAS_CPP_17
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
TEST_CASE("ordered_map (C++17)")
|
||||
{
|
||||
SECTION("find")
|
||||
{
|
||||
ordered_map<std::string, std::string> om;
|
||||
om["eins"] = "one";
|
||||
om["zwei"] = "two";
|
||||
om["drei"] = "three";
|
||||
const auto com = om;
|
||||
|
||||
const std::string eins("eins");
|
||||
const std::string vier("vier");
|
||||
|
||||
CHECK(om.find(std::string_view("eins")) == om.begin());
|
||||
CHECK(com.find(std::string_view("eins")) == com.begin());
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -360,11 +360,6 @@ TEST_CASE("ordered_map")
|
||||
CHECK(com.find("vier") == com.end());
|
||||
CHECK(com.find(std::string("vier")) == com.end());
|
||||
CHECK(com.find(vier) == com.end());
|
||||
|
||||
#ifdef JSON_HAS_CPP_17
|
||||
CHECK(om.find(std::string_view("eins")) == om.begin());
|
||||
CHECK(com.find(std::string_view("eins")) == com.begin());
|
||||
#endif
|
||||
}
|
||||
|
||||
SECTION("insert")
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
// __ _____ _____ _____
|
||||
// __| | __| | | | JSON for Modern C++ (supporting code)
|
||||
// | | |__ | | | | | | version 3.12.0
|
||||
// |_____|_____|_____|_|___| https://github.com/nlohmann/json
|
||||
//
|
||||
// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann <https://nlohmann.me>
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
// This file contains the C++17-only part of unit-regression2.cpp (std::variant, std::any
|
||||
// and std::optional regression tests). It is kept in a separate translation unit so the
|
||||
// (much larger) unit-regression2.cpp is built for C++11 only and not rebuilt for every C++
|
||||
// standard.
|
||||
|
||||
#include "doctest_compatibility.h"
|
||||
|
||||
#include <nlohmann/json.hpp>
|
||||
using json = nlohmann::json;
|
||||
#ifdef JSON_TEST_NO_GLOBAL_UDLS
|
||||
using namespace nlohmann::literals; // NOLINT(google-build-using-namespace)
|
||||
#endif
|
||||
|
||||
#ifdef JSON_HAS_CPP_17
|
||||
#include <string>
|
||||
#include <type_traits>
|
||||
#include <vector>
|
||||
|
||||
#include <any>
|
||||
#include <variant>
|
||||
|
||||
#if __has_include(<optional>)
|
||||
#include <optional>
|
||||
#elif __has_include(<experimental/optional>)
|
||||
#include <experimental/optional>
|
||||
#endif
|
||||
|
||||
/////////////////////////////////////////////////////////////////////
|
||||
// for #4804
|
||||
/////////////////////////////////////////////////////////////////////
|
||||
using json_4804 = nlohmann::json::with_binary_t<std::vector<std::byte>>;
|
||||
|
||||
TEST_CASE("regression tests 2 (C++17)")
|
||||
{
|
||||
SECTION("issue #1292 - Serializing std::variant causes stack overflow")
|
||||
{
|
||||
static_assert(!std::is_constructible<json, std::variant<int, float>>::value, "unexpected value");
|
||||
}
|
||||
|
||||
SECTION("issue #5066 - MSVC converts json to std::variant<json> via the conversion operator")
|
||||
{
|
||||
// std::variant<json> must not be retrievable via get<>(), because otherwise the
|
||||
// implicit conversion operator becomes a candidate that MSVC picks over the variant's
|
||||
// converting constructor, routing a number through the string from_json overload
|
||||
static_assert(!nlohmann::detail::is_detected<nlohmann::detail::get_template_function, const json&, std::variant<json>>::value,
|
||||
"std::variant<json> must not be retrievable via get<>()");
|
||||
|
||||
// clang before 7 cannot instantiate libstdc++'s std::variant<json>
|
||||
#if !(defined(__clang__) && __clang_major__ < 7)
|
||||
// push_back, not emplace_back: #5066 needs the implicit conversion
|
||||
// from json to the vector's value type
|
||||
std::vector<std::variant<json>> v;
|
||||
v.push_back(json(1)); // NOLINT(hicpp-use-emplace,modernize-use-emplace)
|
||||
CHECK(std::get<0>(v[0]) == 1);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,57 @@
|
||||
// __ _____ _____ _____
|
||||
// __| | __| | | | JSON for Modern C++ (supporting code)
|
||||
// | | |__ | | | | | | version 3.12.0
|
||||
// |_____|_____|_____|_|___| https://github.com/nlohmann/json
|
||||
//
|
||||
// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann <https://nlohmann.me>
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
// This file contains the C++20-only part of unit-regression2.cpp (std::span and ranges
|
||||
// regression tests). It is kept in a separate translation unit so the (much larger) unit-
|
||||
// regression2.cpp is built for C++11 only and not rebuilt for every C++ standard.
|
||||
|
||||
#include "doctest_compatibility.h"
|
||||
|
||||
#include <nlohmann/json.hpp>
|
||||
using json = nlohmann::json;
|
||||
#ifdef JSON_TEST_NO_GLOBAL_UDLS
|
||||
using namespace nlohmann::literals; // NOLINT(google-build-using-namespace)
|
||||
#endif
|
||||
|
||||
#ifdef JSON_HAS_CPP_20
|
||||
#include <string>
|
||||
|
||||
#if __has_include(<span>)
|
||||
#include <span>
|
||||
#endif
|
||||
|
||||
/////////////////////////////////////////////////////////////////////
|
||||
// for #4440
|
||||
/////////////////////////////////////////////////////////////////////
|
||||
#if JSON_HAS_RANGES == 1
|
||||
#include <ranges>
|
||||
#endif
|
||||
|
||||
TEST_CASE("regression tests 2 (C++20)")
|
||||
{
|
||||
#ifndef _LIBCPP_VERSION // see https://github.com/nlohmann/json/issues/4490
|
||||
// classic Intel ICC reports <span> as includable but cannot actually compile
|
||||
// std::span/std::as_bytes usage below
|
||||
#if __has_include(<span>) && !defined(__ICC) && !defined(__INTEL_COMPILER)
|
||||
SECTION("issue #2546 - parsing containers of std::byte")
|
||||
{
|
||||
const char DATA[] = R"("Hello, world!")"; // NOLINT(misc-const-correctness,cppcoreguidelines-avoid-c-arrays,hicpp-avoid-c-arrays,modernize-avoid-c-arrays)
|
||||
// exclude the trailing '\0' that string-literal initialization adds to
|
||||
// DATA: std::span(DATA) would span the full array extent (including
|
||||
// that NUL), which is only silently accepted as end-of-input by default
|
||||
// and would fail under JSON_STRICT_NUL_HANDLING
|
||||
const auto s = std::as_bytes(std::span(DATA, sizeof(DATA) - 1));
|
||||
const json j = json::parse(s);
|
||||
CHECK(j.dump() == "\"Hello, world!\"");
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -6,12 +6,11 @@
|
||||
// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann <https://nlohmann.me>
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
// cmake/test.cmake selects the C++ standard versions with which to build a
|
||||
// unit test based on the presence of JSON_HAS_CPP_<VERSION> macros.
|
||||
// When using macros that are only defined for particular versions of the standard
|
||||
// (e.g., JSON_HAS_FILESYSTEM for C++17 and up), please mention the corresponding
|
||||
// version macro in a comment close by, like this:
|
||||
// JSON_HAS_CPP_<VERSION> (do not remove; see note at top of file)
|
||||
// cmake/test.cmake builds a unit test with C++ standards beyond C++11 only if the
|
||||
// source file mentions the corresponding version macro. To avoid rebuilding this
|
||||
// large file for every standard, tests that depend on the standard version (e.g.,
|
||||
// those using JSON_HAS_FILESYSTEM, JSON_HAS_RANGES, or JSON_HAS_THREE_WAY_COMPARISON)
|
||||
// go into a separate file unit-regression2-cpp<NN>.cpp. This file stays C++11-only.
|
||||
|
||||
#include "doctest_compatibility.h"
|
||||
|
||||
@@ -49,30 +48,6 @@ using ordered_json = nlohmann::ordered_json;
|
||||
|
||||
#include "test_utils.hpp"
|
||||
|
||||
#ifdef JSON_HAS_CPP_17
|
||||
#include <any>
|
||||
#include <variant>
|
||||
#endif
|
||||
|
||||
#ifdef JSON_HAS_CPP_17
|
||||
#if __has_include(<optional>)
|
||||
#include <optional>
|
||||
#elif __has_include(<experimental/optional>)
|
||||
#include <experimental/optional>
|
||||
#endif
|
||||
|
||||
/////////////////////////////////////////////////////////////////////
|
||||
// for #4804
|
||||
/////////////////////////////////////////////////////////////////////
|
||||
using json_4804 = nlohmann::json::with_binary_t<std::vector<std::byte>>;
|
||||
#endif
|
||||
|
||||
#ifdef JSON_HAS_CPP_20
|
||||
#if __has_include(<span>)
|
||||
#include <span>
|
||||
#endif
|
||||
#endif
|
||||
|
||||
/////////////////////////////////////////////////////////////////////
|
||||
// for #4825 - explicitly instantiating basic_json must compile; this
|
||||
// forces instantiation of binary_writer::write_bjdata_ndarray, whose
|
||||
@@ -81,13 +56,6 @@ using ordered_json = nlohmann::ordered_json;
|
||||
/////////////////////////////////////////////////////////////////////
|
||||
template class nlohmann::basic_json<>;
|
||||
|
||||
/////////////////////////////////////////////////////////////////////
|
||||
// for #4440
|
||||
/////////////////////////////////////////////////////////////////////
|
||||
#if JSON_HAS_RANGES == 1
|
||||
#include <ranges>
|
||||
#endif
|
||||
|
||||
// NLOHMANN_JSON_SERIALIZE_ENUM uses a static std::pair
|
||||
DOCTEST_CLANG_SUPPRESS_WARNING_PUSH
|
||||
DOCTEST_CLANG_SUPPRESS_WARNING("-Wexit-time-destructors")
|
||||
@@ -285,18 +253,18 @@ struct adl_serializer<NonDefaultConstructible>
|
||||
/////////////////////////////////////////////////////////////////////
|
||||
|
||||
template<class T>
|
||||
class my_allocator : public std::allocator<T>
|
||||
class my_allocator_2982 : public std::allocator<T>
|
||||
{
|
||||
public:
|
||||
using std::allocator<T>::allocator;
|
||||
|
||||
my_allocator() = default;
|
||||
template<class U> my_allocator(const my_allocator<U>& /*unused*/) { }
|
||||
my_allocator_2982() = default;
|
||||
template<class U> my_allocator_2982(const my_allocator_2982<U>& /*unused*/) { }
|
||||
|
||||
template <class U>
|
||||
struct rebind
|
||||
{
|
||||
using other = my_allocator<U>;
|
||||
using other = my_allocator_2982<U>;
|
||||
};
|
||||
};
|
||||
|
||||
@@ -468,13 +436,6 @@ TEST_CASE("regression tests 2")
|
||||
CHECK(diffs.size() == 1); // Note the change here, was 2
|
||||
}
|
||||
|
||||
#ifdef JSON_HAS_CPP_17
|
||||
SECTION("issue #1292 - Serializing std::variant causes stack overflow")
|
||||
{
|
||||
static_assert(!std::is_constructible<json, std::variant<int, float>>::value, "unexpected value");
|
||||
}
|
||||
#endif
|
||||
|
||||
SECTION("issue #1299 - compile error in from_json converting to container "
|
||||
"with std::pair")
|
||||
{
|
||||
@@ -718,26 +679,6 @@ TEST_CASE("regression tests 2")
|
||||
CHECK(j.dump() == "{}");
|
||||
}
|
||||
|
||||
#ifdef JSON_HAS_CPP_20
|
||||
#ifndef _LIBCPP_VERSION // see https://github.com/nlohmann/json/issues/4490
|
||||
// classic Intel ICC reports <span> as includable but cannot actually compile
|
||||
// std::span/std::as_bytes usage below
|
||||
#if __has_include(<span>) && !defined(__ICC) && !defined(__INTEL_COMPILER)
|
||||
SECTION("issue #2546 - parsing containers of std::byte")
|
||||
{
|
||||
const char DATA[] = R"("Hello, world!")"; // NOLINT(misc-const-correctness,cppcoreguidelines-avoid-c-arrays,hicpp-avoid-c-arrays,modernize-avoid-c-arrays)
|
||||
// exclude the trailing '\0' that string-literal initialization adds to
|
||||
// DATA: std::span(DATA) would span the full array extent (including
|
||||
// that NUL), which is only silently accepted as end-of-input by default
|
||||
// and would fail under JSON_STRICT_NUL_HANDLING
|
||||
const auto s = std::as_bytes(std::span(DATA, sizeof(DATA) - 1));
|
||||
const json j = json::parse(s);
|
||||
CHECK(j.dump() == "\"Hello, world!\"");
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
#endif
|
||||
|
||||
SECTION("issue #2574 - Deserialization to std::array, std::pair, and std::tuple with non-default constructable types fails")
|
||||
{
|
||||
SECTION("std::array")
|
||||
@@ -868,7 +809,7 @@ TEST_CASE("regression tests 2")
|
||||
|
||||
SECTION("issue #2982 - to_{binary format} does not provide a mechanism for specifying a custom allocator for the returned type")
|
||||
{
|
||||
std::vector<std::uint8_t, my_allocator<std::uint8_t>> my_vector;
|
||||
std::vector<std::uint8_t, my_allocator_2982<std::uint8_t>> my_vector;
|
||||
const json j = {1, 2, 3, 4};
|
||||
json::to_cbor(j, my_vector);
|
||||
json k = json::from_cbor(my_vector);
|
||||
@@ -884,26 +825,6 @@ TEST_CASE("regression tests 2")
|
||||
CHECK(node.dump(-1, ' ', true, json::error_handler_t::keep) == "{\"test\":\"test\334\\u0005\"}");
|
||||
}
|
||||
|
||||
#ifdef JSON_HAS_CPP_17
|
||||
SECTION("issue #5066 - MSVC converts json to std::variant<json> via the conversion operator")
|
||||
{
|
||||
// std::variant<json> must not be retrievable via get<>(), because otherwise the
|
||||
// implicit conversion operator becomes a candidate that MSVC picks over the variant's
|
||||
// converting constructor, routing a number through the string from_json overload
|
||||
static_assert(!nlohmann::detail::is_detected<nlohmann::detail::get_template_function, const json&, std::variant<json>>::value,
|
||||
"std::variant<json> must not be retrievable via get<>()");
|
||||
|
||||
// clang before 7 cannot instantiate libstdc++'s std::variant<json>
|
||||
#if !(defined(__clang__) && __clang_major__ < 7)
|
||||
// push_back, not emplace_back: #5066 needs the implicit conversion
|
||||
// from json to the vector's value type
|
||||
std::vector<std::variant<json>> v;
|
||||
v.push_back(json(1)); // NOLINT(hicpp-use-emplace,modernize-use-emplace)
|
||||
CHECK(std::get<0>(v[0]) == 1);
|
||||
#endif
|
||||
}
|
||||
#endif
|
||||
|
||||
SECTION("issue #3669 - invalid use of incomplete type with optional member and to_json")
|
||||
{
|
||||
const Issue3669Holder h{};
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
// __ _____ _____ _____
|
||||
// __| | __| | | | JSON for Modern C++ (supporting code)
|
||||
// | | |__ | | | | | | version 3.12.0
|
||||
// |_____|_____|_____|_|___| https://github.com/nlohmann/json
|
||||
//
|
||||
// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann <https://nlohmann.me>
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
// This file contains the C++17-only part of unit-regression3.cpp (std::any, std::optional,
|
||||
// std::variant and std::filesystem regression tests). It is kept in a separate translation
|
||||
// unit so the (much larger) unit-regression3.cpp is built for C++11 only and not rebuilt
|
||||
// for every C++ standard.
|
||||
|
||||
#include "doctest_compatibility.h"
|
||||
|
||||
#include <nlohmann/json.hpp>
|
||||
using json = nlohmann::json;
|
||||
using ordered_json = nlohmann::ordered_json;
|
||||
#ifdef JSON_TEST_NO_GLOBAL_UDLS
|
||||
using namespace nlohmann::literals; // NOLINT(google-build-using-namespace)
|
||||
#endif
|
||||
|
||||
#ifdef JSON_HAS_CPP_17
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <typeinfo>
|
||||
#include <vector>
|
||||
|
||||
#include <any>
|
||||
#include <variant>
|
||||
|
||||
#if __has_include(<optional>)
|
||||
#include <optional>
|
||||
#elif __has_include(<experimental/optional>)
|
||||
#endif
|
||||
|
||||
/////////////////////////////////////////////////////////////////////
|
||||
// for #4740
|
||||
/////////////////////////////////////////////////////////////////////
|
||||
|
||||
struct Example_4740
|
||||
{
|
||||
std::optional<std::string> host = std::nullopt;
|
||||
std::optional<int> port = std::nullopt;
|
||||
NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(Example_4740, host, port)
|
||||
};
|
||||
|
||||
TEST_CASE("regression tests 3 (C++17)")
|
||||
{
|
||||
#if JSON_HAS_FILESYSTEM || JSON_HAS_EXPERIMENTAL_FILESYSTEM
|
||||
SECTION("issue #3070 - Version 3.10.3 breaks backward-compatibility with 3.10.2 ")
|
||||
{
|
||||
nlohmann::detail::std_fs::path text_path("/tmp/text.txt");
|
||||
const json j(text_path);
|
||||
|
||||
const auto j_path = j.get<nlohmann::detail::std_fs::path>();
|
||||
CHECK(j_path == text_path);
|
||||
|
||||
#if DOCTEST_CLANG || DOCTEST_GCC >= DOCTEST_COMPILER(8, 4, 0)
|
||||
// only known to work on Clang and GCC >=8.4
|
||||
CHECK_THROWS_WITH_AS(nlohmann::detail::std_fs::path(json(1)), "[json.exception.type_error.302] type must be string, but is number", json::type_error);
|
||||
#endif
|
||||
}
|
||||
#endif
|
||||
|
||||
#if JSON_USE_IMPLICIT_CONVERSIONS
|
||||
SECTION("issue #3428 - Error occurred when converting nlohmann::json to std::any")
|
||||
{
|
||||
const json j;
|
||||
const std::any a1 = j;
|
||||
std::any&& a2 = j;
|
||||
|
||||
CHECK(a1.type() == typeid(j));
|
||||
CHECK(a2.type() == typeid(j));
|
||||
}
|
||||
#endif
|
||||
|
||||
SECTION("issue #4740 - build issue with std::optional")
|
||||
{
|
||||
const auto t1 = Example_4740();
|
||||
const auto j1 = nlohmann::json(t1);
|
||||
CHECK(j1.dump() == "{\"host\":null,\"port\":null}");
|
||||
const auto t2 = j1.get<Example_4740>();
|
||||
CHECK(!t2.host.has_value());
|
||||
CHECK(!t2.port.has_value());
|
||||
|
||||
// improve coverage
|
||||
auto t3 = Example_4740();
|
||||
t3.port = 80;
|
||||
t3.host = "example.com";
|
||||
const auto j2 = nlohmann::json(t3);
|
||||
CHECK(j2.dump() == "{\"host\":\"example.com\",\"port\":80}");
|
||||
const auto t4 = j2.get<Example_4740>();
|
||||
CHECK(t4.host.has_value());
|
||||
CHECK(t4.port.has_value());
|
||||
}
|
||||
|
||||
SECTION("issue #5046 - implicit conversion of return json to std::optional no longer implicit")
|
||||
{
|
||||
const json jval{};
|
||||
auto GetValue = [](const json & valRoot) -> std::optional<json>
|
||||
{
|
||||
if (valRoot.contains("default"))
|
||||
{
|
||||
return valRoot.at("default");
|
||||
}
|
||||
return std::nullopt;
|
||||
};
|
||||
auto result = GetValue(jval);
|
||||
CHECK(!result.has_value());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,127 @@
|
||||
// __ _____ _____ _____
|
||||
// __| | __| | | | JSON for Modern C++ (supporting code)
|
||||
// | | |__ | | | | | | version 3.12.0
|
||||
// |_____|_____|_____|_|___| https://github.com/nlohmann/json
|
||||
//
|
||||
// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann <https://nlohmann.me>
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
// This file contains the C++20-only part of unit-regression3.cpp (C++20 regression tests
|
||||
// (operator<=> related aggregates, ranges)). It is kept in a separate translation unit so
|
||||
// the (much larger) unit-regression3.cpp is built for C++11 only and not rebuilt for every
|
||||
// C++ standard.
|
||||
|
||||
#include "doctest_compatibility.h"
|
||||
|
||||
// skip tests if JSON_DisableEnumSerialization=ON (#4384): std::byte is a
|
||||
// scoped enum, so get<std::byte>() (needed below to get<std::vector<std::byte>>()
|
||||
// from a plain JSON array, not just from an already-binary value) relies on
|
||||
// enum serialization being enabled
|
||||
#if defined(JSON_DISABLE_ENUM_SERIALIZATION) && (JSON_DISABLE_ENUM_SERIALIZATION == 1)
|
||||
#define SKIP_TESTS_FOR_ENUM_SERIALIZATION
|
||||
#endif
|
||||
|
||||
#include <nlohmann/json.hpp>
|
||||
using json = nlohmann::json;
|
||||
using ordered_json = nlohmann::ordered_json;
|
||||
#ifdef JSON_TEST_NO_GLOBAL_UDLS
|
||||
using namespace nlohmann::literals; // NOLINT(google-build-using-namespace)
|
||||
#endif
|
||||
|
||||
#ifdef JSON_HAS_CPP_20
|
||||
#include <string>
|
||||
|
||||
#if __has_include(<span>)
|
||||
#include <span>
|
||||
#endif
|
||||
|
||||
/////////////////////////////////////////////////////////////////////
|
||||
// for #4440
|
||||
/////////////////////////////////////////////////////////////////////
|
||||
#if JSON_HAS_RANGES == 1
|
||||
#include <ranges>
|
||||
#endif
|
||||
|
||||
/////////////////////////////////////////////////////////////////////
|
||||
// for #3312
|
||||
/////////////////////////////////////////////////////////////////////
|
||||
|
||||
struct for_3312
|
||||
{
|
||||
std::string name;
|
||||
};
|
||||
|
||||
inline void from_json(const json& j, for_3312& obj) // NOLINT(misc-use-internal-linkage)
|
||||
{
|
||||
j.at("name").get_to(obj.name);
|
||||
}
|
||||
|
||||
TEST_CASE("regression tests 3 (C++20)")
|
||||
{
|
||||
SECTION("issue #3312 - Parse to custom class from unordered_json breaks on G++11.2.0 with C++20")
|
||||
{
|
||||
// see test for #3171
|
||||
const ordered_json j = {{"name", "class"}};
|
||||
for_3312 obj{};
|
||||
|
||||
j.get_to(obj);
|
||||
|
||||
CHECK(obj.name == "class");
|
||||
}
|
||||
|
||||
#if JSON_HAS_RANGES == 1
|
||||
SECTION("issue #4440 - assert when using std::views::filter and GCC 10")
|
||||
{
|
||||
auto noOpFilter = std::views::filter([](auto&&) noexcept
|
||||
{
|
||||
return true;
|
||||
});
|
||||
json j = {1, 2, 3};
|
||||
auto filtered = j | noOpFilter;
|
||||
CHECK(*filtered.begin() == 1);
|
||||
}
|
||||
#endif
|
||||
|
||||
#if JSON_HAS_RANGE_VIEW_CONVERSION
|
||||
SECTION("issue #4916 - constructing array from C++20 ranges view does not work")
|
||||
{
|
||||
std::vector<int> nums{1, 2, 37, 42, 21};
|
||||
auto filteredNums = nums | std::views::filter([](int i)
|
||||
{
|
||||
return i > 10;
|
||||
});
|
||||
json const j(filteredNums);
|
||||
CHECK(j.type() == json::value_t::array);
|
||||
CHECK(j == json({37, 42, 21}));
|
||||
}
|
||||
#endif
|
||||
|
||||
// owning_view is not available in libstdc++ < 12
|
||||
#if JSON_HAS_RANGE_VIEW_CONVERSION && !(defined(__GLIBCXX__) && _GLIBCXX_RELEASE < 12)
|
||||
SECTION("issue #4916 - constructing array from prvalue C++20 ranges view (owning_view)")
|
||||
{
|
||||
json const j(std::vector<int> {1, 2, 37, 42, 21} | std::views::filter([](int i)
|
||||
{
|
||||
return i > 10;
|
||||
}));
|
||||
CHECK(j.type() == json::value_t::array);
|
||||
CHECK(j == json({37, 42, 21}));
|
||||
}
|
||||
#endif
|
||||
|
||||
#if JSON_HAS_RANGE_VIEW_CONVERSION
|
||||
SECTION("issue #4916 - constructing array from C++20 transform view (prvalue elements)")
|
||||
{
|
||||
std::vector<int> nums{1, 2, 3};
|
||||
auto t = nums | std::views::transform([](int i) noexcept
|
||||
{
|
||||
return i * 2;
|
||||
});
|
||||
json const j(t);
|
||||
CHECK(j.type() == json::value_t::array);
|
||||
CHECK(j == json({2, 4, 6}));
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -6,12 +6,11 @@
|
||||
// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann <https://nlohmann.me>
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
// cmake/test.cmake selects the C++ standard versions with which to build a
|
||||
// unit test based on the presence of JSON_HAS_CPP_<VERSION> macros.
|
||||
// When using macros that are only defined for particular versions of the standard
|
||||
// (e.g., JSON_HAS_FILESYSTEM for C++17 and up), please mention the corresponding
|
||||
// version macro in a comment close by, like this:
|
||||
// JSON_HAS_CPP_<VERSION> (do not remove; see note at top of file)
|
||||
// cmake/test.cmake builds a unit test with C++ standards beyond C++11 only if the
|
||||
// source file mentions the corresponding version macro. To avoid rebuilding this
|
||||
// large file for every standard, tests that depend on the standard version (e.g.,
|
||||
// those using JSON_HAS_FILESYSTEM, JSON_HAS_RANGES, or JSON_HAS_THREE_WAY_COMPARISON)
|
||||
// go into a separate file unit-regression3-cpp<NN>.cpp. This file stays C++11-only.
|
||||
|
||||
#include "doctest_compatibility.h"
|
||||
|
||||
@@ -47,33 +46,8 @@ using ordered_json = nlohmann::ordered_json;
|
||||
#include <type_traits>
|
||||
#include <utility>
|
||||
|
||||
#ifdef JSON_HAS_CPP_17
|
||||
#include <any>
|
||||
#include <variant>
|
||||
#endif
|
||||
|
||||
#ifdef JSON_HAS_CPP_17
|
||||
#if __has_include(<optional>)
|
||||
#include <optional>
|
||||
#elif __has_include(<experimental/optional>)
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#ifdef JSON_HAS_CPP_20
|
||||
#if __has_include(<span>)
|
||||
#include <span>
|
||||
#endif
|
||||
#endif
|
||||
|
||||
// the explicit instantiation for #4825 is in unit-explicit_instantiation.cpp
|
||||
|
||||
/////////////////////////////////////////////////////////////////////
|
||||
// for #4440
|
||||
/////////////////////////////////////////////////////////////////////
|
||||
#if JSON_HAS_RANGES == 1
|
||||
#include <ranges>
|
||||
#endif
|
||||
|
||||
// NLOHMANN_JSON_SERIALIZE_ENUM uses a static std::pair
|
||||
DOCTEST_CLANG_SUPPRESS_WARNING_PUSH
|
||||
DOCTEST_CLANG_SUPPRESS_WARNING("-Wexit-time-destructors")
|
||||
@@ -188,22 +162,6 @@ inline void from_json(const json& j, for_3171_base& tb) // NOLINT(misc-use-inter
|
||||
tb._from_json(j);
|
||||
}
|
||||
|
||||
/////////////////////////////////////////////////////////////////////
|
||||
// for #3312
|
||||
/////////////////////////////////////////////////////////////////////
|
||||
|
||||
#ifdef JSON_HAS_CPP_20
|
||||
struct for_3312
|
||||
{
|
||||
std::string name;
|
||||
};
|
||||
|
||||
inline void from_json(const json& j, for_3312& obj) // NOLINT(misc-use-internal-linkage)
|
||||
{
|
||||
j.at("name").get_to(obj.name);
|
||||
}
|
||||
#endif
|
||||
|
||||
/////////////////////////////////////////////////////////////////////
|
||||
// for #3204
|
||||
/////////////////////////////////////////////////////////////////////
|
||||
@@ -267,37 +225,8 @@ struct Example_3810
|
||||
|
||||
NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(Example_3810, bla) // NOLINT(misc-use-internal-linkage)
|
||||
|
||||
/////////////////////////////////////////////////////////////////////
|
||||
// for #4740
|
||||
/////////////////////////////////////////////////////////////////////
|
||||
|
||||
#ifdef JSON_HAS_CPP_17
|
||||
struct Example_4740
|
||||
{
|
||||
std::optional<std::string> host = std::nullopt;
|
||||
std::optional<int> port = std::nullopt;
|
||||
NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(Example_4740, host, port)
|
||||
};
|
||||
#endif
|
||||
|
||||
TEST_CASE("regression tests 3")
|
||||
{
|
||||
#if JSON_HAS_FILESYSTEM || JSON_HAS_EXPERIMENTAL_FILESYSTEM
|
||||
// JSON_HAS_CPP_17 (do not remove; see note at top of file)
|
||||
SECTION("issue #3070 - Version 3.10.3 breaks backward-compatibility with 3.10.2 ")
|
||||
{
|
||||
nlohmann::detail::std_fs::path text_path("/tmp/text.txt");
|
||||
const json j(text_path);
|
||||
|
||||
const auto j_path = j.get<nlohmann::detail::std_fs::path>();
|
||||
CHECK(j_path == text_path);
|
||||
|
||||
#if DOCTEST_CLANG || DOCTEST_GCC >= DOCTEST_COMPILER(8, 4, 0)
|
||||
// only known to work on Clang and GCC >=8.4
|
||||
CHECK_THROWS_WITH_AS(nlohmann::detail::std_fs::path(json(1)), "[json.exception.type_error.302] type must be string, but is number", json::type_error);
|
||||
#endif
|
||||
}
|
||||
#endif
|
||||
|
||||
SECTION("issue #3077 - explicit constructor with default does not compile")
|
||||
{
|
||||
@@ -348,31 +277,6 @@ TEST_CASE("regression tests 3")
|
||||
CHECK(td.str == "value");
|
||||
}
|
||||
|
||||
#ifdef JSON_HAS_CPP_20
|
||||
SECTION("issue #3312 - Parse to custom class from unordered_json breaks on G++11.2.0 with C++20")
|
||||
{
|
||||
// see test for #3171
|
||||
const ordered_json j = {{"name", "class"}};
|
||||
for_3312 obj{};
|
||||
|
||||
j.get_to(obj);
|
||||
|
||||
CHECK(obj.name == "class");
|
||||
}
|
||||
#endif
|
||||
|
||||
#if defined(JSON_HAS_CPP_17) && JSON_USE_IMPLICIT_CONVERSIONS
|
||||
SECTION("issue #3428 - Error occurred when converting nlohmann::json to std::any")
|
||||
{
|
||||
const json j;
|
||||
const std::any a1 = j;
|
||||
std::any&& a2 = j;
|
||||
|
||||
CHECK(a1.type() == typeid(j));
|
||||
CHECK(a2.type() == typeid(j));
|
||||
}
|
||||
#endif
|
||||
|
||||
SECTION("issue #3204 - ambiguous regression")
|
||||
{
|
||||
const for_3204_bar bar_from_foo([](for_3204_foo) noexcept {}); // NOLINT(performance-unnecessary-value-param)
|
||||
@@ -415,28 +319,6 @@ TEST_CASE("regression tests 3")
|
||||
CHECK(oj["test"].dump() == expected);
|
||||
}
|
||||
|
||||
#ifdef JSON_HAS_CPP_17
|
||||
SECTION("issue #4740 - build issue with std::optional")
|
||||
{
|
||||
const auto t1 = Example_4740();
|
||||
const auto j1 = nlohmann::json(t1);
|
||||
CHECK(j1.dump() == "{\"host\":null,\"port\":null}");
|
||||
const auto t2 = j1.get<Example_4740>();
|
||||
CHECK(!t2.host.has_value());
|
||||
CHECK(!t2.port.has_value());
|
||||
|
||||
// improve coverage
|
||||
auto t3 = Example_4740();
|
||||
t3.port = 80;
|
||||
t3.host = "example.com";
|
||||
const auto j2 = nlohmann::json(t3);
|
||||
CHECK(j2.dump() == "{\"host\":\"example.com\",\"port\":80}");
|
||||
const auto t4 = j2.get<Example_4740>();
|
||||
CHECK(t4.host.has_value());
|
||||
CHECK(t4.port.has_value());
|
||||
}
|
||||
#endif
|
||||
|
||||
#if !defined(_MSVC_LANG)
|
||||
// MSVC returns garbage on invalid enum values, so this test is excluded
|
||||
// there.
|
||||
@@ -451,76 +333,7 @@ TEST_CASE("regression tests 3")
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef JSON_HAS_CPP_17
|
||||
SECTION("issue #5046 - implicit conversion of return json to std::optional no longer implicit")
|
||||
{
|
||||
const json jval{};
|
||||
auto GetValue = [](const json & valRoot) -> std::optional<json>
|
||||
{
|
||||
if (valRoot.contains("default"))
|
||||
{
|
||||
return valRoot.at("default");
|
||||
}
|
||||
return std::nullopt;
|
||||
};
|
||||
auto result = GetValue(jval);
|
||||
CHECK(!result.has_value());
|
||||
}
|
||||
#endif
|
||||
|
||||
#if JSON_HAS_RANGES == 1
|
||||
SECTION("issue #4440 - assert when using std::views::filter and GCC 10")
|
||||
{
|
||||
auto noOpFilter = std::views::filter([](auto&&) noexcept
|
||||
{
|
||||
return true;
|
||||
});
|
||||
json j = {1, 2, 3};
|
||||
auto filtered = j | noOpFilter;
|
||||
CHECK(*filtered.begin() == 1);
|
||||
}
|
||||
#endif
|
||||
|
||||
#if JSON_HAS_RANGE_VIEW_CONVERSION
|
||||
SECTION("issue #4916 - constructing array from C++20 ranges view does not work")
|
||||
{
|
||||
std::vector<int> nums{1, 2, 37, 42, 21};
|
||||
auto filteredNums = nums | std::views::filter([](int i)
|
||||
{
|
||||
return i > 10;
|
||||
});
|
||||
json const j(filteredNums);
|
||||
CHECK(j.type() == json::value_t::array);
|
||||
CHECK(j == json({37, 42, 21}));
|
||||
}
|
||||
#endif
|
||||
|
||||
// owning_view is not available in libstdc++ < 12
|
||||
#if JSON_HAS_RANGE_VIEW_CONVERSION && !(defined(__GLIBCXX__) && _GLIBCXX_RELEASE < 12)
|
||||
SECTION("issue #4916 - constructing array from prvalue C++20 ranges view (owning_view)")
|
||||
{
|
||||
json const j(std::vector<int> {1, 2, 37, 42, 21} | std::views::filter([](int i)
|
||||
{
|
||||
return i > 10;
|
||||
}));
|
||||
CHECK(j.type() == json::value_t::array);
|
||||
CHECK(j == json({37, 42, 21}));
|
||||
}
|
||||
#endif
|
||||
|
||||
#if JSON_HAS_RANGE_VIEW_CONVERSION
|
||||
SECTION("issue #4916 - constructing array from C++20 transform view (prvalue elements)")
|
||||
{
|
||||
std::vector<int> nums{1, 2, 3};
|
||||
auto t = nums | std::views::transform([](int i) noexcept
|
||||
{
|
||||
return i * 2;
|
||||
});
|
||||
json const j(t);
|
||||
CHECK(j.type() == json::value_t::array);
|
||||
CHECK(j == json({2, 4, 6}));
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
TEST_CASE_TEMPLATE("issue #4798 - nlohmann::json::to_msgpack() encode float NaN as double", T, double, float) // NOLINT(readability-math-missing-parentheses, bugprone-throwing-static-initialization)
|
||||
|
||||
Loaded 100 of 109 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user