mirror of
https://github.com/nlohmann/json.git
synced 2026-10-24 14:58:54 +00:00
* Read C streams into a json_document in blocks json_document::parse(FILE*) (and read(), accept(), parse_copy()) went through the library's file input adapter and appended the text character by character with fgetc. Read it with fread in blocks instead, like the std::istream overload already does. Parsing citm_catalog.json (1.7 MB) from a FILE*: Apple M1: 80.6 M -> 8.4 M instructions, 4.8 ms -> 0.55 ms (parse of the same text from a std::string: 8.7 M) Linux x86-64 Ir (whole run), GCC 13 / Clang 18: 47.0 M -> 13.7 M / 45.8 M -> 14.2 M The parsed documents and the error cases are identical (checked over the test corpus and 30 mutants of each file). A null FILE* is handled as before (by the library's adapter). Belongs to the PR "json_view: read-side speedups". Signed-off-by: Niels Lohmann <mail@nlohmann.me> * Index arrays without subtrees directly operator[](idx), at(idx) and back() skipped the elements of an array one after the other, whatever they were. In an array none of whose elements is an array or object (numbers, strings, booleans, null) the elements are adjacent nodes, which the array node's own sizes tell: its subtree has exactly one node more than it has elements. Such an element is now the node idx after the array node; any other array is walked as before. Reading r[i] for every i of an array of 3000 integers: Apple M1: 28.8 M instructions, 3.1 ms -> 0.1 M instructions, 4 us Linux x86-64 Ir (whole run), GCC 13 / Clang 18: 84.1 M -> 3.1 M / 75.0 M -> 3.0 M Arrays that contain arrays are unchanged. Belongs to the PR "json_view: read-side speedups". Signed-off-by: Niels Lohmann <mail@nlohmann.me> * Skip the bounds checks of number and string tokens in NUL-terminated input For a NUL-terminated input (std::string, C string, string literal), the parser reads the terminator instead of comparing p with the end, via cursor::cur(). Number and string scanning still tested `p != e` before each look at the next byte (after the integer digits for '.', for 'e', after the exponent sign and digits, and after a string for its closing quote), although the terminator can never be one of those characters. They now use cur(), which is the plain load for NUL-terminated input and the bounds check for ptr+len input, as before. Parsing, Apple M1 instructions per parse: twitter 3.126 M -> 3.089 M (-1.2 %), citm_catalog 8.70 M -> 8.58 M (-1.3 %), jeopardy 306.1 M -> 302.5 M (-1.2 %), signed integers 281 M -> 266 M (-5.4 %), canada and floats -0.1 % / -0.9 %. Linux x86-64 Ir, GCC 13 / Clang 18: twitter -0.5 % / -0.0 %, signed integers +0.2 % / -0.7 %. Belongs to the PR "json_view: read-side speedups". Signed-off-by: Niels Lohmann <mail@nlohmann.me> * Make the index text of items() on arrays lazy basic_json_view::items() on an array formatted every element's index with std::to_string when the item was made, whether or not the loop asks for the key. The item now keeps the position and makes the text on the first key() call. Objects are not affected. Iterating over the items of an array of 3000 numbers and reading only the values: Apple M1: 25.5 us -> 4.6 us (0.45 M -> ~0 net instructions) Linux x86-64 Ir (whole run), GCC 13 / Clang 18: 14.9 M -> 4.1 M / 15.3 M -> 4.6 M Reading key() costs what it did (24.5 us). Belongs to the PR "json_view: read-side speedups". Signed-off-by: Niels Lohmann <mail@nlohmann.me> * Make the default of value() with a C string only if the member is missing basic_json_view::value(key, "text") and value(pointer, "text") built a std::string from the default first and passed it on, so a call allocated for the default even when the member exists, and copied it again when it does not. They now look the member up and make the result directly: from the member if there is one, else from the C string. Behavior, including the exceptions, is the same. Two calls (member found, member missing) with a 40 character default, Apple M1: 1.6 K -> 0.7 K instructions Linux x86-64 Ir (500 rounds), GCC 13 / Clang 18: 2.46 M -> 2.22 M / 2.40 M -> 2.15 M Belongs to the PR "json_view: read-side speedups". Signed-off-by: Niels Lohmann <mail@nlohmann.me> * Materialize scalars directly and keep the stack of small subtrees local basic_json_view::materialize() (and get<BasicJsonType>()) always set up the SAX handler that parse() uses and a std::vector for the open containers, sized from the document's deepest nesting. For a scalar, which is most of what is materialized one value at a time, neither is needed: the value is made directly (the same constructors the handler ends up calling). For arrays and objects the stack of open containers is now a local array for the first 16 levels, and the vector is only used for deeper documents. Materializing five scalars of a small document, Apple M1: 3.6 K -> 0.9 K instructions Linux x86-64 Ir (500 rounds), GCC 13 / Clang 18: 2.96 M -> 2.37 M / 2.90 M -> 2.29 M Belongs to the PR "json_view: read-side speedups". Signed-off-by: Niels Lohmann <mail@nlohmann.me> * Copy the text between escapes while scanning it A string that contains an escape is decoded by builder::slow_string(). After each escape it scanned the next run of plain bytes (8 or 16 byte-by-byte steps, then a vector loop), and then copied that run to the decode arena in a second pass. The run is now copied 16 bytes at a time while it is scanned (one vector load, a store of the same 16 bytes, and the mask of the bytes that end the run), and the write position lives in a local variable, not in the builder, since a store of a character may alias the builder's members and made the compiler reload it after every escape. A run that ends at a quote needs no scan, and non-ASCII text goes through the UTF-8 check as before; the portable code is unchanged. Parsing, Apple M1 instructions: jeopardy.json 304.0 M -> 293.8 M (-3.4 %), 3000 strings with escapes 1.72 M -> 1.54 M (-10 %), twitter -0.6 %. Linux x86-64 Ir, GCC 13 / Clang 18: escaped strings -7.6 % / -11.4 %, twitter 0.0 % / 0.0 %. Documents and error messages are identical on 900 k random strings of escapes, UTF-8 and control characters (also under ASan), and on mutants of the test corpus. Belongs to the PR "json_view: read-side speedups". Signed-off-by: Niels Lohmann <mail@nlohmann.me> * Hash the last bytes of a key of a large object without a loop key_hash() mixed a key eight bytes at a time and then packed the remaining 0-7 bytes one by one, in a loop. The last word of a key of eight or more bytes is now read as the last eight bytes of the key (overlapping the word before), and a key of 2-7 bytes as two overlapping words of four or two bytes. A product only moves bits upwards, and the slot is taken from the low bits: the high bits of the last word are folded down before the multiplication, since keys that differ only in their last characters ("user_17", "user_18") would otherwise share slots. The hash is not stored, so its values may change. Looking up every key of an object of 1000 random keys, Apple M1 instructions per 1000 lookups: keys of 5 bytes 175 K -> 93 K, hex strings of 8 bytes 177 K -> 168 K, keys of 12 bytes 196 K -> 186 K Linux x86-64 Ir (whole run), GCC 13 / Clang 18: 5-byte keys -4.7 % / -8.8 %, 12-byte keys -3.3 % / -7.4 %. Building the tables while parsing such objects is cheaper as well (5-byte keys: 13.3 us -> 10.1 us). Belongs to the PR "json_view: read-side speedups". Signed-off-by: Niels Lohmann <mail@nlohmann.me> * Compare short keys of a large object without a call of memcmp find_indexed() compared the key of a member the table points to with memcmp, a library call for any length. Up to 16 bytes, the bytes are now compared as two overlapping words (the first and the last 8, 4, or 2 bytes), as find_member() already does for the objects without a table; longer keys still use memcmp. Looking up every key of an object of 1000 random keys, Apple M1 instructions per 1000 lookups (with the previous commit): keys of 5 bytes 93 K -> 72 K, hex strings of 8 bytes 168 K -> 148 K, keys of 12 bytes 186 K -> 170 K, keys of 24 bytes 198 K -> 193 K (unchanged, memcmp). Linux x86-64 Ir (whole run), GCC 13 / Clang 18: 5-byte keys -7.2 % / -7.3 %, 12-byte keys -5.9 % / -6.6 %. Belongs to the PR "json_view: read-side speedups". Signed-off-by: Niels Lohmann <mail@nlohmann.me> * Read an unedited editable document like a read-only one Views of a json_editable_document followed edits at every step: the lookup, the element access, back(), dump() and materialize() were built for nodes that can be moved or linked, though no edit changes the index until the first one (every edit creates the edit state before it touches a node). Until then these now take the read-only code: one check of the document's edit state per call, not per member or per node. After the first edit nothing changes. Views that were made earlier keep working, as the check is made when they are used. Unedited editable document, Apple M1: dump() of twitter.json 1.40 M -> 1.22 M instructions (-12.6 %); lookups unchanged. Linux x86-64 Ir, GCC 13 / Clang 18: dump -1.7 % / -1.0 %, lookups -0.1 % / 0.0 %. Walking with iterators is unchanged, and so is dump() after an edit. Belongs to the PR "json_view: read-side speedups". Signed-off-by: Niels Lohmann <mail@nlohmann.me> * Format the numbered test keys of json_view's index with a stream The keys were made with snprintf() and a format from a list, which Flawfinder reports as an error (CWE-134, a format string that is not a literal). A stream now formats them; the 30000 keys of the test are the same as before. Signed-off-by: Niels Lohmann <mail@nlohmann.me> * Put the key number of the json_view index test in parentheses astyle formatted i * step after << as a pointer declaration (i* step), which failed the amalgamation check. Signed-off-by: Niels Lohmann <mail@nlohmann.me> * Fix the compiler and lint findings of json_view's read side and tests - find() and contains() dispatch on Editable for the member lookup; for documents that cannot be edited both branches were the same call (GCC -Wduplicated-branches) - json_view.hpp includes <cstdio> for the FILE overload (cpplint) - the arena check macro of the builder is on one line (cpplint read it as an if body without braces) - tests: no nested conditional (also MSVC C4018), no useless cast, parentheses around mixed operators, raw string literals, += instead of temporary concatenations, and NOLINT for the fixed seeds and a find() != npos, as elsewhere in the tests Signed-off-by: Niels Lohmann <mail@nlohmann.me> * Grow the decode arena through a function and store its position back on every return slow_string() kept its write position in locals and grew the arena through the macro NLOHMANN_VIEW_ENSURE. The macro is now the always-inline member arena_ensure() (a lambda is not inlined at every call site by clang, which then keeps the position in memory: +11 % instructions on escape-heavy strings), and a guard stores the position back into m_arena_write on every return instead of by hand on the successful one. Instruction counts are unchanged (clang) or 1 % lower (GCC). Also word the complexity of back() as that of at(). Signed-off-by: Niels Lohmann <mail@nlohmann.me> --------- Signed-off-by: Niels Lohmann <mail@nlohmann.me>
1596 lines
72 KiB
C++
1596 lines
72 KiB
C++
// __ _____ _____ _____
|
|
// __| | __| | | | 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
|
|
|
|
#include "doctest_compatibility.h"
|
|
|
|
#include <nlohmann/json_view.hpp>
|
|
using nlohmann::json;
|
|
using nlohmann::ordered_json;
|
|
using nlohmann::json_document;
|
|
using nlohmann::json_view;
|
|
using nlohmann::ordered_json_document;
|
|
using nlohmann::ordered_json_view;
|
|
|
|
#include "json_view_test_helpers.hpp"
|
|
using json_view_test::generator;
|
|
using json_view_test::has_duplicate_keys;
|
|
|
|
#include <algorithm>
|
|
#include <array>
|
|
#include <cmath>
|
|
#include <cstddef>
|
|
#include <cstdint>
|
|
#include <cstdio>
|
|
#include <cstring>
|
|
#include <iomanip>
|
|
#include <ios>
|
|
#include <istream>
|
|
#include <iterator>
|
|
#include <limits>
|
|
#include <list>
|
|
#include <map>
|
|
#include <random>
|
|
#include <sstream>
|
|
#include <string>
|
|
#include <type_traits>
|
|
#include <unordered_map>
|
|
#include <utility>
|
|
#include <vector>
|
|
|
|
#ifdef JSON_HAS_CPP_17
|
|
#include <string_view>
|
|
#endif
|
|
|
|
namespace
|
|
{
|
|
// the value of a text, through a named document: the views of a temporary
|
|
// document would dangle (root() of an rvalue document does not compile)
|
|
template<typename Document, typename... Args>
|
|
auto materialized(Args&& ... args) -> decltype(std::declval<typename Document::view_type>().materialize())
|
|
{
|
|
const Document d = Document::parse(std::forward<Args>(args)...);
|
|
return d.root().materialize();
|
|
}
|
|
|
|
template<typename Document, typename Input>
|
|
auto materialized_copy(Input&& input) -> decltype(std::declval<typename Document::view_type>().materialize())
|
|
{
|
|
const Document d = Document::parse_copy(std::forward<Input>(input));
|
|
return d.root().materialize();
|
|
}
|
|
|
|
// a "byte container" that claims to hold `size` bytes, to reach the limit on
|
|
// the size of the input without allocating gigabytes; nothing past the first
|
|
// bytes is ever read, because the size is checked before the parse starts
|
|
struct oversized_input
|
|
{
|
|
using value_type = char;
|
|
std::size_t claimed;
|
|
|
|
const char* data() const // NOLINT(readability-convert-member-functions-to-static): container interface
|
|
{
|
|
return "[1]";
|
|
}
|
|
|
|
std::size_t size() const
|
|
{
|
|
return claimed;
|
|
}
|
|
};
|
|
|
|
// detection of calls that must not compile
|
|
template<typename... Args>
|
|
using parse_call_t = decltype(json_document::parse(std::declval<Args>()...));
|
|
template<typename... Args>
|
|
using parse_copy_call_t = decltype(json_document::parse_copy(std::declval<Args>()...));
|
|
template<typename... Args>
|
|
using accept_call_t = decltype(json_document::accept(std::declval<Args>()...));
|
|
template<typename... Args>
|
|
using read_call_t = decltype(std::declval<json_document&>().read(std::declval<Args>()...));
|
|
template<typename Document>
|
|
using root_call_t = decltype(std::declval<Document>().root());
|
|
template<typename View>
|
|
using bool_conversion_t = decltype(static_cast<bool>(std::declval<View>()));
|
|
|
|
#if !defined(JSON_NOEXCEPTION)
|
|
// the exception parse() throws for a text, or "" if it accepts it
|
|
std::string parse_exception(const std::string& text, bool comments = false, bool trailing_commas = false)
|
|
{
|
|
try
|
|
{
|
|
const json j = json::parse(text, nullptr, true, comments, trailing_commas);
|
|
static_cast<void>(j);
|
|
}
|
|
catch (const json::exception& e)
|
|
{
|
|
return e.what();
|
|
}
|
|
return "";
|
|
}
|
|
|
|
std::string view_exception(const std::string& text, bool comments = false, bool trailing_commas = false)
|
|
{
|
|
try
|
|
{
|
|
const json_document d = json_document::parse(text, true, comments, trailing_commas);
|
|
static_cast<void>(d);
|
|
}
|
|
catch (const json::exception& e)
|
|
{
|
|
return e.what();
|
|
}
|
|
return "";
|
|
}
|
|
#endif
|
|
} // namespace
|
|
|
|
TEST_CASE("json_view")
|
|
{
|
|
SECTION("types and capacity")
|
|
{
|
|
for (const char* text :
|
|
{"null", "true", "false", "0", "-1", "18446744073709551615", "-9223372036854775808", "18446744073709551616", "1.5",
|
|
"\"\"", "\"text\"", "[]", "[1,2,3]", "{}", "{\"a\":1,\"b\":2}" // NOLINT(modernize-raw-string-literal)
|
|
})
|
|
{
|
|
CAPTURE(text)
|
|
const json j = json::parse(text);
|
|
const json_document d = json_document::parse(text);
|
|
const json_view v = d.root();
|
|
CHECK(v.type() == j.type());
|
|
CHECK(v.is_null() == j.is_null());
|
|
CHECK(v.is_boolean() == j.is_boolean());
|
|
CHECK(v.is_number() == j.is_number());
|
|
CHECK(v.is_number_integer() == j.is_number_integer());
|
|
CHECK(v.is_number_unsigned() == j.is_number_unsigned());
|
|
CHECK(v.is_number_float() == j.is_number_float());
|
|
CHECK(v.is_string() == j.is_string());
|
|
CHECK(v.is_array() == j.is_array());
|
|
CHECK(v.is_object() == j.is_object());
|
|
CHECK(v.is_binary() == j.is_binary());
|
|
CHECK(v.is_primitive() == j.is_primitive());
|
|
CHECK(v.is_structured() == j.is_structured());
|
|
CHECK(!v.is_discarded());
|
|
CHECK(v.size() == j.size());
|
|
CHECK(v.empty() == j.empty());
|
|
CHECK(v.materialize() == j);
|
|
}
|
|
|
|
const json_view invalid{};
|
|
CHECK(invalid.is_discarded());
|
|
CHECK(invalid.type() == json::value_t::discarded);
|
|
CHECK(invalid.size() == 0);
|
|
CHECK(invalid.empty());
|
|
CHECK(invalid.materialize().is_discarded());
|
|
CHECK(invalid.source_offset() == static_cast<std::size_t>(-1));
|
|
}
|
|
|
|
SECTION("materialize")
|
|
{
|
|
generator g;
|
|
for (int i = 0; i < 2000; ++i)
|
|
{
|
|
std::string text;
|
|
g.value(text, 0);
|
|
CAPTURE(text)
|
|
CHECK(materialized<json_document>(text) == json::parse(text));
|
|
// member order as ordered_json::parse keeps it
|
|
CHECK(materialized<ordered_json_document>(text).dump() == ordered_json::parse(text).dump());
|
|
}
|
|
// duplicate keys: the last value, at the position of the first key
|
|
CHECK(materialized<json_document>(R"({"a":1,"b":2,"a":3})") == json::parse(R"({"a":1,"b":2,"a":3})"));
|
|
CHECK(materialized<ordered_json_document>(R"({"a":1,"b":2,"a":3})").dump() == R"({"a":3,"b":2})");
|
|
// scalars (made without the handler), and nesting around the depth up to which the stack of open containers is local
|
|
for (const char* scalar :
|
|
{"null", "true", "false", "0", "-7", "18446744073709551615", "1.5e300", "-0.0", "\"\"", "\"short\"", "\"a string that is longer than fifteen bytes\"", R"("\u00e4\n")"
|
|
})
|
|
{
|
|
CAPTURE(scalar)
|
|
CHECK(materialized<json_document>(scalar) == json::parse(scalar));
|
|
const std::string in_array = std::string("[1,") + scalar + "]";
|
|
const json_document d = json_document::parse(in_array);
|
|
CHECK(d.root()[1].materialize() == json::parse(scalar));
|
|
CHECK(d.root()[1].get<json>() == json::parse(scalar));
|
|
}
|
|
for (const std::size_t levels :
|
|
{
|
|
std::size_t{1}, std::size_t{15}, std::size_t{16}, std::size_t{17}, std::size_t{32}, std::size_t{33}
|
|
})
|
|
{
|
|
CAPTURE(levels)
|
|
const std::string nested = std::string(levels, '[') + R"({"k":[1,{"j":2}]})" + std::string(levels, ']');
|
|
CHECK(materialized<json_document>(nested) == json::parse(nested));
|
|
std::string wide = R"({"a":)";
|
|
wide += nested;
|
|
wide += R"(,"b":)";
|
|
wide += nested;
|
|
wide += "}";
|
|
CHECK(materialized<json_document>(wide) == json::parse(wide));
|
|
}
|
|
// very deep nesting (iterative, as parse())
|
|
const std::string deep = std::string(100000, '[') + std::string(100000, ']');
|
|
// (one check per step, so that a failure shows which step it is)
|
|
const json_document deep_document = json_document::parse(deep);
|
|
CHECK(deep_document.root().is_array());
|
|
const json deep_value = deep_document.root().materialize();
|
|
CHECK(deep_value.is_array());
|
|
const json deep_parsed = json::parse(deep);
|
|
CHECK(deep_parsed.is_array());
|
|
CHECK(deep_value == deep_parsed);
|
|
#if JSON_DIAGNOSTICS
|
|
// the parents are set, so errors name the path
|
|
const json m = materialized<json_document>(R"({"a":{"b":[1]}})");
|
|
CHECK_THROWS_WITH_AS(m.at("a").at("b").at(0).at("x"), "[json.exception.type_error.304] (/a/b/0) cannot use at() with number", json::type_error&);
|
|
#endif
|
|
}
|
|
|
|
SECTION("parse errors are those of parse()")
|
|
{
|
|
for (const char* text :
|
|
{
|
|
"", " ", "[", "]", "{", "[1,]", "{\"a\":1,}", "[1 2]", "{\"a\" 1}", "{1:2}", "tru", "nul", "fals", "truex", "-", "01", "1.", ".5", "1e",
|
|
"\"", "\"abc", "\"\\x\"", "\"\\u12\"", "\"\\ud800\"", "\"\\udc00\"", "\"\x01\"", "\"\xff\"", "\"\xc3\"", "[1]x", "/", "/*", "[\n 1,\n x\n]", // NOLINT(modernize-raw-string-literal)
|
|
"1e400", "-1e400", "[1.7976931348623159e308]", "{\"a\":\n{\"b\": [1, 2,\n 3 x]}}"
|
|
})
|
|
{
|
|
CAPTURE(text)
|
|
#if !defined(JSON_NOEXCEPTION)
|
|
const std::string expected = parse_exception(text);
|
|
REQUIRE(!expected.empty());
|
|
CHECK(view_exception(text) == expected);
|
|
#endif
|
|
CHECK(!json_document::accept(text));
|
|
const json_document d = json_document::parse(text, false);
|
|
CHECK(d.is_discarded());
|
|
CHECK(d.root().is_discarded());
|
|
CHECK(d.node_count() == 0);
|
|
}
|
|
// the exception types
|
|
json_document _;
|
|
CHECK_THROWS_AS(_ = json_document::parse("[1,"), json::parse_error&);
|
|
CHECK_THROWS_AS(_ = json_document::parse("1e400"), json::out_of_range&);
|
|
}
|
|
|
|
SECTION("parse options")
|
|
{
|
|
for (const char* text :
|
|
{"// c\n[1]", "[1, /* c */ 2]", "[1,]", "{\"a\":1,}", "[1,/* c */]", "/", "/* ", "[1,,]"
|
|
})
|
|
{
|
|
CAPTURE(text)
|
|
for (int options = 0; options < 4; ++options)
|
|
{
|
|
const bool comments = (options & 1) != 0;
|
|
const bool trailing_commas = (options & 2) != 0;
|
|
CHECK(json_document::accept(text, comments, trailing_commas) == json::accept(text, comments, trailing_commas));
|
|
#if !defined(JSON_NOEXCEPTION)
|
|
CHECK(view_exception(text, comments, trailing_commas) == parse_exception(text, comments, trailing_commas));
|
|
#endif
|
|
}
|
|
}
|
|
}
|
|
|
|
SECTION("overflow of the floating-point type")
|
|
{
|
|
// the overflow is that of the document's number_float_t: 1e39
|
|
// overflows a float, the largest float 3.4028235e38 does not, but
|
|
// 3.4028236e38 rounds beyond it; a double document is not affected
|
|
using json_float = nlohmann::basic_json<std::map, std::vector, std::string, bool, std::int64_t, std::uint64_t, float>;
|
|
using float_document = nlohmann::basic_json_document<json_float>;
|
|
CHECK_FALSE(json_float::accept("1e39"));
|
|
CHECK(json_float::accept("3.4028235e38"));
|
|
CHECK_FALSE(json_float::accept("3.4028236e38"));
|
|
for (const char* text :
|
|
{
|
|
"1e39", "-1e39", "[3.4028235e38]", "[3.4028236e38]", "{\"a\": [1e38, -3.4028235e38]}", "{\"a\": [1e-50, 1e39]}"
|
|
})
|
|
{
|
|
CAPTURE(text)
|
|
const bool accepted = json_float::accept(text);
|
|
CHECK(float_document::accept(text) == accepted);
|
|
CHECK(float_document::parse(text, false).is_discarded() == !accepted);
|
|
CHECK(json_document::accept(text));
|
|
if (accepted)
|
|
{
|
|
CHECK(materialized<float_document>(text) == json_float::parse(text));
|
|
}
|
|
}
|
|
float_document f;
|
|
CHECK_THROWS_WITH_AS(f = float_document::parse("1e39"), "[json.exception.out_of_range.406] number overflow parsing '1e39'", json::out_of_range&);
|
|
CHECK_THROWS_WITH_AS(f = float_document::parse("[3.4028236e38]"), "[json.exception.out_of_range.406] number overflow parsing '3.4028236e38'", json::out_of_range&);
|
|
}
|
|
|
|
SECTION("NUL and BOM")
|
|
{
|
|
const std::string with_nul("[1]\0garbage", 11);
|
|
CHECK(json_document::accept(with_nul) == json::accept(with_nul));
|
|
const std::string nul_in_comment("[1, // c\0\n2]", 12);
|
|
CHECK(json_document::accept(nul_in_comment, true) == json::accept(nul_in_comment, true));
|
|
CHECK(materialized<json_document>("\xEF\xBB\xBF[1]") == json::parse("\xEF\xBB\xBF[1]"));
|
|
#if !defined(JSON_NOEXCEPTION)
|
|
CHECK(view_exception("\xEF\xBB") == parse_exception("\xEF\xBB"));
|
|
#endif
|
|
}
|
|
|
|
SECTION("inputs")
|
|
{
|
|
const std::string text = R"([1, "two", {"three": 3.5}])";
|
|
const json expected = json::parse(text);
|
|
|
|
// borrowed: the text must outlive the document
|
|
const json_document borrowed = json_document::parse(text);
|
|
CHECK(!borrowed.owns_source());
|
|
CHECK(borrowed.source().data() == text.data());
|
|
CHECK(borrowed.root().materialize() == expected);
|
|
CHECK(materialized<json_document>(text.c_str()) == expected);
|
|
CHECK(materialized<json_document>(R"([1, "two", {"three": 3.5}])") == expected);
|
|
CHECK(materialized<json_document>(text.data(), text.data() + text.size()) == expected);
|
|
const std::vector<char> chars(text.begin(), text.end());
|
|
CHECK(!json_document::parse(chars).owns_source());
|
|
CHECK(materialized<json_document>(chars) == expected);
|
|
const std::vector<std::uint8_t> bytes(text.begin(), text.end());
|
|
CHECK(materialized<json_document>(bytes) == expected);
|
|
#ifdef JSON_HAS_CPP_17
|
|
const std::string_view sv = text;
|
|
CHECK(!json_document::parse(sv).owns_source());
|
|
CHECK(materialized<json_document>(sv) == expected);
|
|
#endif
|
|
|
|
// owned
|
|
std::string moved = text;
|
|
const json_document from_rvalue = json_document::parse(std::move(moved));
|
|
CHECK(from_rvalue.owns_source());
|
|
CHECK(from_rvalue.root().materialize() == expected);
|
|
CHECK(json_document::parse(std::vector<char>(text.begin(), text.end())).owns_source());
|
|
// a const rvalue cannot be moved from, and is not borrowed (it may be a
|
|
// temporary): it is copied, as is a const rvalue of any container
|
|
const std::string const_text = text; // NOLINT(performance-unnecessary-copy-initialization)
|
|
const json_document from_const_rvalue = json_document::parse(std::move(const_text)); // NOLINT(performance-move-const-arg,hicpp-move-const-arg)
|
|
CHECK(from_const_rvalue.owns_source());
|
|
CHECK(from_const_rvalue.source().data() != const_text.data()); // NOLINT(bugprone-use-after-move,hicpp-invalid-access-moved): const, not moved from
|
|
CHECK(from_const_rvalue.root().materialize() == expected);
|
|
json_document read_const_rvalue;
|
|
read_const_rvalue.read(std::move(const_text)); // NOLINT(performance-move-const-arg,hicpp-move-const-arg,bugprone-use-after-move,hicpp-invalid-access-moved)
|
|
CHECK(read_const_rvalue.owns_source());
|
|
CHECK(read_const_rvalue.root().materialize() == expected);
|
|
const std::vector<char> const_chars(text.begin(), text.end());
|
|
CHECK(json_document::parse(std::move(const_chars)).owns_source()); // NOLINT(performance-move-const-arg,hicpp-move-const-arg)
|
|
CHECK(json_document::parse_copy(text).owns_source());
|
|
CHECK(materialized_copy<json_document>(text) == expected);
|
|
std::istringstream stream(text);
|
|
const json_document from_stream = json_document::parse(stream);
|
|
CHECK(from_stream.owns_source());
|
|
CHECK(from_stream.root().materialize() == expected);
|
|
const std::list<char> list(text.begin(), text.end());
|
|
CHECK(json_document::parse(list.begin(), list.end()).owns_source());
|
|
CHECK(materialized<json_document>(list.begin(), list.end()) == expected);
|
|
|
|
// iterator pairs: pointers are borrowed, and so are contiguous library
|
|
// iterators where the input adapter detects them (C++20)
|
|
CHECK(!json_document::parse(text.data(), text.data() + text.size()).owns_source());
|
|
const bool contiguous = nlohmann::detail::iterator_input_adapter<std::vector<char>::const_iterator>::supports_bulk_scan;
|
|
const json_document from_iterators = json_document::parse(chars.cbegin(), chars.cend());
|
|
CHECK(from_iterators.owns_source() != contiguous);
|
|
CHECK((from_iterators.source().data() == chars.data()) == contiguous);
|
|
CHECK(from_iterators.root().materialize() == expected);
|
|
const std::string padded = "x" + text + "x";
|
|
CHECK(materialized<json_document>(padded.begin() + 1, padded.end() - 1) == expected);
|
|
CHECK(json_document::parse(chars.cbegin(), chars.cbegin(), false).is_discarded());
|
|
const std::wstring wide = L"[\"\u00e4\u20ac\", 1]";
|
|
CHECK(materialized<json_document>(wide) == json::parse(wide));
|
|
CHECK(json_document::parse(static_cast<const char*>(nullptr), false).is_discarded());
|
|
CHECK(json_document::parse("", false).is_discarded());
|
|
|
|
// wide-character arrays and pointers are converted to UTF-8, as by
|
|
// parse(); the pointers are NUL-terminated
|
|
CHECK(materialized<json_document>(L"[\"ä€\", 1]") == json::parse(L"[\"ä€\", 1]"));
|
|
CHECK(materialized<json_document>(u"[\"ä€\", 1]") == json::parse(u"[\"ä€\", 1]"));
|
|
CHECK(materialized<json_document>(U"[\"ä€\", 1]") == json::parse(U"[\"ä€\", 1]"));
|
|
const wchar_t* const wide_pointer = wide.c_str();
|
|
const json_document from_wide_pointer = json_document::parse(wide_pointer);
|
|
CHECK(from_wide_pointer.owns_source());
|
|
CHECK(from_wide_pointer.root().materialize() == json::parse(wide));
|
|
const std::u16string wide16 = u"{\"\U0001F600\": [true]}";
|
|
CHECK(materialized<json_document>(wide16.c_str()) == json::parse(wide16));
|
|
const std::u32string wide32 = U"{\"\U0001F600\": [true]}";
|
|
CHECK(materialized<json_document>(wide32.c_str()) == json::parse(wide32));
|
|
CHECK(json_document::parse(static_cast<const wchar_t*>(nullptr), false).is_discarded());
|
|
|
|
// FILE*: read to its end
|
|
std::FILE* file = std::tmpfile(); // NOLINT(cppcoreguidelines-owning-memory)
|
|
if (file != nullptr)
|
|
{
|
|
CHECK(std::fputs(text.c_str(), file) >= 0);
|
|
CHECK(std::fseek(file, 0, SEEK_SET) == 0);
|
|
const json_document from_file = json_document::parse(file);
|
|
CHECK(from_file.owns_source());
|
|
CHECK(from_file.root().materialize() == expected);
|
|
CHECK(std::fclose(file) == 0); // NOLINT(cppcoreguidelines-owning-memory)
|
|
}
|
|
}
|
|
|
|
SECTION("streams")
|
|
{
|
|
// longer than one block of the bulk read
|
|
std::string text = "[";
|
|
for (int i = 0; i < 5000; ++i)
|
|
{
|
|
text += (i != 0 ? ",\"" : "\"") + std::to_string(i) + "\"";
|
|
}
|
|
text += ']';
|
|
std::istringstream stream(text);
|
|
const json_document from_stream = json_document::parse(stream);
|
|
CHECK(from_stream.source() == text);
|
|
CHECK(from_stream.root().materialize() == json::parse(text));
|
|
// the stream is read to its end, and eofbit set, as by parse()
|
|
CHECK(stream.eof());
|
|
CHECK(!stream.bad());
|
|
|
|
std::istringstream empty_stream;
|
|
CHECK(json_document::parse(empty_stream, false).is_discarded());
|
|
CHECK(empty_stream.eof());
|
|
std::istringstream rvalue_source("[1]");
|
|
CHECK(json_document::parse_copy(std::move(rvalue_source)).owns_source());
|
|
|
|
// a C stream is read in blocks as well (more than one block, and a
|
|
// text whose length is a multiple of the block size)
|
|
for (const std::size_t extra :
|
|
{
|
|
std::size_t{0}, std::size_t{1}
|
|
})
|
|
{
|
|
std::FILE* long_file = std::tmpfile(); // NOLINT(cppcoreguidelines-owning-memory)
|
|
if (long_file != nullptr)
|
|
{
|
|
std::string padded = text;
|
|
padded.append(((4096 - (padded.size() % 4096)) % 4096) + extra, ' ');
|
|
CHECK(std::fwrite(padded.data(), 1, padded.size(), long_file) == padded.size());
|
|
CHECK(std::fseek(long_file, 0, SEEK_SET) == 0);
|
|
const json_document from_long_file = json_document::parse(long_file);
|
|
CHECK(from_long_file.source() == padded);
|
|
CHECK(from_long_file.root().materialize() == json::parse(text));
|
|
CHECK(std::fclose(long_file) == 0); // NOLINT(cppcoreguidelines-owning-memory)
|
|
}
|
|
}
|
|
{
|
|
json_document from_null;
|
|
CHECK_THROWS_AS(from_null = json_document::parse(static_cast<std::FILE*>(nullptr)), json::parse_error&);
|
|
}
|
|
|
|
// a stream without a buffer is an empty input
|
|
std::istream no_buffer(nullptr);
|
|
CHECK(json_document::parse(no_buffer, false).is_discarded());
|
|
|
|
#if !defined(JSON_NOEXCEPTION)
|
|
// an exception while reading the input leaves a discarded document
|
|
json_document d = json_document::parse(std::vector<char>(text.begin(), text.end()));
|
|
std::istringstream throwing("[1]");
|
|
throwing.exceptions(std::ios::eofbit);
|
|
CHECK_THROWS_AS(d.read(throwing), std::ios_base::failure&);
|
|
CHECK(d.is_discarded());
|
|
CHECK(d.root().is_discarded());
|
|
d.read("[2]");
|
|
CHECK(d.root().materialize() == json::parse("[2]"));
|
|
#endif
|
|
}
|
|
|
|
SECTION("read() reuses the owned copy")
|
|
{
|
|
std::string big = "[";
|
|
for (int i = 0; i < 1000; ++i)
|
|
{
|
|
big += (i != 0 ? ",\"" : "\"") + std::to_string(i) + "\"";
|
|
}
|
|
big += ']';
|
|
const std::string small = "{\"a\": [1, 2]}";
|
|
|
|
// copied inputs are written into the existing buffer
|
|
json_document d = json_document::parse(std::vector<char>(big.begin(), big.end()));
|
|
REQUIRE(d.owns_source());
|
|
const char* const buffer = d.source().data();
|
|
d.read(std::vector<char>(small.begin(), small.end()));
|
|
CHECK(d.owns_source());
|
|
CHECK(d.source().data() == buffer);
|
|
CHECK(d.root().materialize() == json::parse(small));
|
|
std::istringstream stream(small);
|
|
d.read(stream);
|
|
CHECK(d.source().data() == buffer);
|
|
CHECK(d.root().materialize() == json::parse(small));
|
|
d.read(std::wstring(L"[3]"));
|
|
CHECK(d.source().data() == buffer);
|
|
CHECK(d.root().materialize() == json::parse("[3]"));
|
|
|
|
// a moved-in string replaces it (long enough to be on the heap)
|
|
std::string moved = big;
|
|
const char* const moved_buffer = moved.data();
|
|
d.read(std::move(moved));
|
|
CHECK(d.source().data() == moved_buffer);
|
|
CHECK(d.root().materialize() == json::parse(big));
|
|
|
|
// a borrowed input leaves nothing owned
|
|
d.read(big);
|
|
CHECK(!d.owns_source());
|
|
CHECK(d.source().data() == big.data());
|
|
}
|
|
|
|
SECTION("integer arguments do not compile")
|
|
{
|
|
using nlohmann::detail::is_detected;
|
|
|
|
static_assert(is_detected<parse_call_t, const char*, bool>::value, "parse(ptr, bool) is valid");
|
|
static_assert(is_detected<parse_call_t, const char*, bool, bool, bool>::value, "parse(ptr, bool, bool, bool) is valid");
|
|
static_assert(is_detected<parse_call_t, const char*, const char*>::value, "parse(first, last) is valid");
|
|
static_assert(is_detected<parse_call_t, const char*, const char*, bool>::value, "parse(first, last, bool) is valid");
|
|
static_assert(is_detected<parse_copy_call_t, const char*, bool>::value, "parse_copy(ptr, bool) is valid");
|
|
static_assert(is_detected<accept_call_t, const char*, bool>::value, "accept(ptr, bool) is valid");
|
|
static_assert(is_detected<read_call_t, const char*, bool>::value, "read(ptr, bool) is valid");
|
|
|
|
// a length is not a flag: parse(ptr, len) would convert len to
|
|
// allow_exceptions and read ptr as a C string, which need not end
|
|
//
|
|
// MSVC 2015 does not treat selecting a deleted function in decltype as
|
|
// a substitution failure, so it detects the deleted overloads as
|
|
// callable; calling them still fails to compile
|
|
#if !(defined(_MSC_VER) && _MSC_VER < 1910)
|
|
static_assert(!is_detected<parse_call_t, const char*, std::size_t>::value, "parse(ptr, len) must not compile");
|
|
static_assert(!is_detected<parse_call_t, const char*, int>::value, "parse(ptr, int) must not compile");
|
|
static_assert(!is_detected<parse_call_t, const char*, char>::value, "parse(ptr, char) must not compile");
|
|
static_assert(!is_detected<parse_call_t, const char*, std::size_t, bool>::value, "parse(ptr, len, bool) must not compile");
|
|
static_assert(!is_detected<parse_call_t, const std::string&, std::size_t>::value, "parse(string, len) must not compile");
|
|
static_assert(!is_detected<parse_call_t, const std::vector<char>&, std::size_t>::value, "parse(vector, len) must not compile");
|
|
static_assert(!is_detected<parse_copy_call_t, const char*, std::size_t>::value, "parse_copy(ptr, len) must not compile");
|
|
static_assert(!is_detected<accept_call_t, const char*, std::size_t>::value, "accept(ptr, len) must not compile");
|
|
static_assert(!is_detected<read_call_t, const char*, std::size_t>::value, "read(ptr, len) must not compile");
|
|
#endif
|
|
|
|
// json_view has no conversion to bool: unlike basic_json's, it would
|
|
// mean "exists", not "is not null"; use is_discarded()
|
|
static_assert(!is_detected<bool_conversion_t, json_view>::value, "json_view must not convert to bool");
|
|
|
|
// the valid calls still work
|
|
const char* const text = "[1]";
|
|
CHECK(materialized<json_document>(text, true) == json::parse(text));
|
|
CHECK(json_document::accept(text, true, true));
|
|
}
|
|
|
|
SECTION("root of a temporary document does not compile")
|
|
{
|
|
using nlohmann::detail::is_detected;
|
|
|
|
// the view would dangle: auto v = json_document::parse(text).root();
|
|
static_assert(is_detected<root_call_t, json_document&>::value, "root() of an lvalue is valid");
|
|
static_assert(is_detected<root_call_t, const json_document&>::value, "root() of a const lvalue is valid");
|
|
static_assert(!is_detected<root_call_t, json_document>::value, "root() of an rvalue must not compile");
|
|
static_assert(!is_detected < root_call_t, json_document && >::value, "root() of an rvalue must not compile");
|
|
static_assert(!is_detected < root_call_t, const json_document && >::value, "root() of a const rvalue must not compile");
|
|
static_assert(!is_detected<root_call_t, ordered_json_document>::value, "root() of an rvalue must not compile");
|
|
|
|
// a named document is fine, also after a move
|
|
json_document d = json_document::parse("[1]");
|
|
CHECK(d.root().size() == 1);
|
|
const json_document moved = std::move(d);
|
|
CHECK(moved.root().size() == 1);
|
|
}
|
|
|
|
SECTION("input size limit")
|
|
{
|
|
// 32-bit offsets: the limit is 4 GiB minus 16 bytes (a margin below 2^32),
|
|
// which is what the exception message and the documentation say
|
|
const std::size_t limit = nlohmann::detail::view::max_input_size();
|
|
CHECK(limit == std::size_t{4294967279u});
|
|
|
|
const oversized_input input{limit + 1};
|
|
CHECK(!json_document::accept(input));
|
|
CHECK(json_document::parse(input, false).is_discarded());
|
|
#if !defined(JSON_NOEXCEPTION)
|
|
json_document d;
|
|
CHECK_THROWS_WITH_AS(d = json_document::parse(input), "[json.exception.out_of_range.416] input of 4294967280 bytes or more is not supported by json_document", json::out_of_range&);
|
|
#endif
|
|
}
|
|
|
|
SECTION("document lifetime and reuse")
|
|
{
|
|
json_document d;
|
|
CHECK(d.is_discarded());
|
|
CHECK(d.root().is_discarded());
|
|
CHECK(d.node_count() == 0);
|
|
CHECK(d.memory_usage() == 0);
|
|
CHECK(d.source().empty());
|
|
|
|
const std::string a = "[1,2,3]";
|
|
const std::string b = "{\"x\":[true]}";
|
|
d.read(a);
|
|
CHECK(d.node_count() == 4);
|
|
CHECK(d.root().materialize() == json::parse(a));
|
|
d.read(b);
|
|
CHECK(d.node_count() == 4);
|
|
CHECK(d.root().materialize() == json::parse(b));
|
|
d.read("[", false);
|
|
CHECK(d.is_discarded());
|
|
|
|
// views stay valid when the document moves
|
|
json_document first = json_document::parse(a);
|
|
const json_view root = first.root();
|
|
const json_document second = std::move(first);
|
|
CHECK(root.materialize() == json::parse(a));
|
|
CHECK(second.root().materialize() == json::parse(a));
|
|
}
|
|
|
|
SECTION("memory")
|
|
{
|
|
std::string big = "[";
|
|
for (int i = 0; i < 10000; ++i)
|
|
{
|
|
big += (i != 0 ? ",\"" : "\"") + std::to_string(i) + "\"";
|
|
}
|
|
big += ']';
|
|
json_document d = json_document::parse(big);
|
|
CHECK(d.node_count() == 10001);
|
|
const std::size_t before = d.memory_usage();
|
|
d.shrink_to_fit(); // (invalidates views, like std::vector::shrink_to_fit)
|
|
CHECK(d.memory_usage() <= before);
|
|
CHECK(d.node_count() == 10001);
|
|
CHECK(d.root().materialize() == json::parse(big));
|
|
CHECK(d.root().size() == 10000);
|
|
d.shrink_to_fit(); // nothing left to release
|
|
|
|
// after reading a smaller text, both the index and the decoded strings
|
|
// shrink, and the strings are found in their new place
|
|
std::string escaped = "[";
|
|
for (int i = 0; i < 1000; ++i)
|
|
{
|
|
escaped += (i != 0 ? ",\"a\\n" : "\"a\\n") + std::to_string(i) + "\"";
|
|
}
|
|
escaped += ']';
|
|
json_document reused = json_document::parse(escaped);
|
|
const std::string smaller = "[\"x\\ty\", [true, \"\\u00e4\"]]"; // NOLINT(modernize-raw-string-literal)
|
|
reused.read(smaller);
|
|
const std::size_t grown = reused.memory_usage();
|
|
reused.shrink_to_fit();
|
|
CHECK(reused.memory_usage() < grown);
|
|
CHECK(reused.root().materialize() == json::parse(smaller));
|
|
|
|
// an empty document has nothing to release
|
|
json_document empty;
|
|
empty.shrink_to_fit();
|
|
CHECK(empty.memory_usage() == 0);
|
|
|
|
// a small document stays in the storage block of the header
|
|
json_document small = json_document::parse("[1,[2,3],{\"a\":\"b\\n\"}]"); // NOLINT(modernize-raw-string-literal)
|
|
small.shrink_to_fit();
|
|
CHECK(small.root().materialize() == json::parse("[1,[2,3],{\"a\":\"b\\n\"}]"));
|
|
}
|
|
|
|
SECTION("source offsets")
|
|
{
|
|
const std::string text = R"( {"key": "value", "escaped": "a\nb", "n": 42})";
|
|
const json_document d = json_document::parse(text);
|
|
CHECK(d.root().source_offset() == 2);
|
|
CHECK(d.root()["key"].source_offset() == text.find("value"));
|
|
CHECK(d.root()["escaped"].source_offset() == static_cast<std::size_t>(-1));
|
|
CHECK(d.root()["n"].source_offset() == text.find("42"));
|
|
}
|
|
}
|
|
|
|
namespace
|
|
{
|
|
#if !defined(JSON_NOEXCEPTION)
|
|
// the exception a call throws, or "" if it throws none
|
|
template<typename F>
|
|
std::string exception_of(F f)
|
|
{
|
|
try
|
|
{
|
|
f();
|
|
}
|
|
catch (const json::exception& e)
|
|
{
|
|
return e.what();
|
|
}
|
|
return "";
|
|
}
|
|
#endif
|
|
|
|
// compares a view with the ordered_json value materialize() gives for it:
|
|
// types, sizes, elements and members (by index, key, and iteration), in
|
|
// document order; duplicate keys are found as their first occurrence
|
|
void check_access(const ordered_json_view& v, const ordered_json& j)
|
|
{
|
|
REQUIRE(v.type() == j.type());
|
|
CHECK(std::string(v.type_name()) == j.type_name());
|
|
if (v.is_array())
|
|
{
|
|
REQUIRE(v.size() == j.size());
|
|
std::size_t i = 0;
|
|
for (const ordered_json_view e : v)
|
|
{
|
|
CHECK(v[i].materialize() == e.materialize());
|
|
CHECK(v.at(i).materialize() == e.materialize());
|
|
check_access(e, j[i]);
|
|
++i;
|
|
}
|
|
CHECK(i == v.size());
|
|
CHECK(v[v.size()].is_discarded());
|
|
std::size_t index = 0;
|
|
for (const auto& item : v.items())
|
|
{
|
|
CHECK(item.key() == std::to_string(index));
|
|
CHECK(item.value().materialize() == j[index]);
|
|
++index;
|
|
}
|
|
if (!v.empty())
|
|
{
|
|
CHECK(v.front().materialize() == j.front());
|
|
CHECK(v.back().materialize() == j.back());
|
|
}
|
|
}
|
|
else if (v.is_object())
|
|
{
|
|
std::vector<std::string> keys; // first occurrences, in order
|
|
std::size_t members = 0;
|
|
for (auto it = v.begin(); it != v.end(); ++it)
|
|
{
|
|
++members;
|
|
const std::string key(it.key().data(), it.key().size());
|
|
CHECK(v.contains(key));
|
|
std::size_t occurrences = 0;
|
|
for (auto other = v.begin(); other != v.end(); ++other)
|
|
{
|
|
occurrences += other.key() == it.key() ? 1u : 0u;
|
|
}
|
|
CHECK(v.count(key) == occurrences);
|
|
// lookups find the first member with the key, which is this one if
|
|
// there is no earlier one
|
|
if (std::find(keys.begin(), keys.end(), key) != keys.end())
|
|
{
|
|
continue;
|
|
}
|
|
keys.push_back(key);
|
|
CHECK(v.find(key) == it);
|
|
CHECK(v[key].materialize() == it->materialize());
|
|
CHECK(v.at(key).materialize() == it.value().materialize());
|
|
CHECK(v[key.c_str()].materialize() == (*it).materialize());
|
|
}
|
|
CHECK(members == v.size());
|
|
REQUIRE(keys.size() == j.size());
|
|
std::size_t k = 0;
|
|
for (const auto& member : j.items())
|
|
{
|
|
CHECK(keys[k++] == member.key());
|
|
}
|
|
if (keys.size() == members)
|
|
{
|
|
// no duplicates: the values are those of the object
|
|
for (const auto& key : keys)
|
|
{
|
|
check_access(v[key], j[key]);
|
|
}
|
|
if (!v.empty())
|
|
{
|
|
CHECK(v.front().materialize() == j.front());
|
|
CHECK(v.back().materialize() == j.back());
|
|
}
|
|
}
|
|
CHECK(v["not a key in the generated documents"].is_discarded());
|
|
CHECK(v.find("not a key in the generated documents") == v.end());
|
|
}
|
|
else
|
|
{
|
|
// a primitive is a range of one element; null is empty
|
|
CHECK(static_cast<std::size_t>(std::distance(v.begin(), v.end())) == (v.is_null() ? 0u : 1u));
|
|
if (!v.is_null())
|
|
{
|
|
CHECK((*v.begin()).materialize() == j);
|
|
CHECK(v.front().materialize() == j);
|
|
CHECK(v.back().materialize() == j);
|
|
}
|
|
}
|
|
}
|
|
} // namespace
|
|
|
|
TEST_CASE("json_view element access and iteration")
|
|
{
|
|
SECTION("generated documents")
|
|
{
|
|
generator g;
|
|
for (int i = 0; i < 2000; ++i)
|
|
{
|
|
std::string text;
|
|
g.value(text, 0);
|
|
CAPTURE(text)
|
|
const ordered_json_document d = ordered_json_document::parse(text);
|
|
check_access(d.root(), ordered_json::parse(text));
|
|
}
|
|
}
|
|
|
|
SECTION("keys")
|
|
{
|
|
// keys of every length around the 2/4/8/16-byte loads, with escapes
|
|
std::string text = "{";
|
|
std::vector<std::string> keys = {"", "x"};
|
|
for (std::size_t n = 1; n <= 40; ++n)
|
|
{
|
|
keys.emplace_back(n, 'k');
|
|
keys.push_back(std::string(n, 'k') + "x");
|
|
keys.push_back("x" + std::string(n, 'k'));
|
|
}
|
|
for (std::size_t i = 0; i < keys.size(); ++i)
|
|
{
|
|
text += (i != 0 ? ",\"" : "\"") + keys[i] + "\":" + std::to_string(i);
|
|
}
|
|
text += ",\"esc\\u0061ped\":\"escaped key\"}"; // NOLINT(modernize-raw-string-literal)
|
|
const json_document d = json_document::parse(text);
|
|
const json_view root = d.root();
|
|
for (std::size_t i = 0; i < keys.size(); ++i)
|
|
{
|
|
CAPTURE(keys[i])
|
|
CHECK(root[keys[i]].materialize() == i);
|
|
CHECK(root.at(keys[i]).materialize() == i);
|
|
CHECK(root.find(keys[i]).key() == keys[i]);
|
|
CHECK(!root.contains(keys[i] + "y"));
|
|
}
|
|
CHECK(root["escaped"].materialize() == "escaped key");
|
|
CHECK(!root.contains("esc\\u0061ped"));
|
|
#ifdef JSON_HAS_CPP_17
|
|
CHECK(root[std::string_view("kkk")].materialize() == root["kkk"].materialize());
|
|
#endif
|
|
}
|
|
|
|
SECTION("duplicate keys: lookups find the first member, iteration all")
|
|
{
|
|
const json_document d = json_document::parse(R"({"a":1,"b":2,"a":3})");
|
|
const json_view v = d.root();
|
|
CHECK(v.size() == 3);
|
|
CHECK(v["a"].materialize() == 1);
|
|
CHECK(v.at("a").materialize() == 1);
|
|
CHECK(v.find("a") == v.begin());
|
|
CHECK(v.find("a").value().materialize() == 1);
|
|
CHECK(v.find("b") == std::next(v.begin()));
|
|
CHECK(v.count("a") == 2); // every member, unlike materialize()
|
|
CHECK(v.count("b") == 1);
|
|
CHECK(v.contains("a"));
|
|
CHECK(v.value("a", 0) == 1);
|
|
CHECK(v.materialize()["a"] == 3); // materialize() keeps the last value, unlike the lookups
|
|
// JSON pointers resolve to the first member at every level
|
|
const json_document dp = json_document::parse(R"({"a":{"b":1},"a":{"b":2,"b":3}})");
|
|
CHECK(dp.root()[json::json_pointer("/a/b")].materialize() == 1);
|
|
CHECK(dp.root().at(json::json_pointer("/a/b")).materialize() == 1);
|
|
CHECK(dp.root().value(json::json_pointer("/a/b"), 0) == 1);
|
|
CHECK(dp.root().materialize()[json::json_pointer("/a/b")] == 3);
|
|
// get<map> keeps the last value, as materialize()
|
|
CHECK((v.get<std::map<std::string, int>>() == std::map<std::string, int> {{"a", 3}, {"b", 2}}));
|
|
// keys of every length class (the 16-byte short compare and memcmp)
|
|
for (const std::size_t n :
|
|
{
|
|
0u, 1u, 3u, 7u, 8u, 15u, 16u, 17u, 40u
|
|
})
|
|
{
|
|
const std::string key(n, 'k');
|
|
const json_document dk = json_document::parse("{\"" + key + "\":1,\"" + key + "x\":2,\"" + key + "\":3,\"" + key + "\":4}"); // NOLINT(performance-inefficient-string-concatenation)
|
|
CAPTURE(n)
|
|
CHECK(dk.root()[key].materialize() == 1);
|
|
CHECK(dk.root().at(key).materialize() == 1);
|
|
CHECK(dk.root().find(key) == dk.root().begin());
|
|
CHECK(dk.root().value(key, 0) == 1);
|
|
CHECK(dk.root()[key + "x"].materialize() == 2);
|
|
}
|
|
std::string order;
|
|
for (auto it = v.begin(); it != v.end(); ++it)
|
|
{
|
|
order += std::string(it.key().data(), it.key().size()) + it->materialize().dump();
|
|
}
|
|
CHECK(order == "a1b2a3");
|
|
CHECK(v.back().materialize() == 3);
|
|
CHECK(v.materialize() == json::parse(R"({"a":1,"b":2,"a":3})")); // the last value, as parse()
|
|
}
|
|
|
|
SECTION("errors are those of const basic_json")
|
|
{
|
|
for (const char* text :
|
|
{"null", "true", "42", "-1", "1.5", "\"s\"", "[]", "[1,2]", "{}", "{\"a\":1}"
|
|
})
|
|
{
|
|
CAPTURE(text)
|
|
const json_document d = json_document::parse(text);
|
|
const json_view v = d.root();
|
|
const json j = v.materialize();
|
|
#if !defined(JSON_NOEXCEPTION)
|
|
if (!j.is_object())
|
|
{
|
|
CHECK(exception_of([&] { static_cast<void>(v["a"]); }) == exception_of([&] { static_cast<void>(j["a"]); }));
|
|
}
|
|
if (!j.is_array())
|
|
{
|
|
CHECK(exception_of([&] { static_cast<void>(v[0]); }) == exception_of([&] { static_cast<void>(j[0]); }));
|
|
}
|
|
CHECK(exception_of([&] { static_cast<void>(v.at("a")); }) == exception_of([&] { static_cast<void>(j.at("a")); }));
|
|
CHECK(exception_of([&] { static_cast<void>(v.at("missing")); }) == exception_of([&] { static_cast<void>(j.at("missing")); }));
|
|
CHECK(exception_of([&] { static_cast<void>(v.at(0)); }) == exception_of([&] { static_cast<void>(j.at(0)); }));
|
|
CHECK(exception_of([&] { static_cast<void>(v.at(5)); }) == exception_of([&] { static_cast<void>(j.at(5)); }));
|
|
if (!(j.is_object() && j.empty())) // (key() of an end iterator)
|
|
{
|
|
CHECK(exception_of([&] { static_cast<void>(v.begin().key()); }) == exception_of([&] { static_cast<void>(j.begin().key()); }));
|
|
}
|
|
if (!j.empty() || j.is_null())
|
|
{
|
|
CHECK(exception_of([&] { static_cast<void>(v.front()); }) == exception_of([&] { static_cast<void>(j.front()); }));
|
|
CHECK(exception_of([&] { static_cast<void>(v.back()); }) == exception_of([&] { static_cast<void>(j.back()); }));
|
|
}
|
|
#endif
|
|
CHECK(v.contains("a") == j.contains("a"));
|
|
CHECK(v.count("a") == j.count("a"));
|
|
CHECK((v.find("a") == v.end()) == (j.find("a") == j.end())); // NOLINT(readability-container-contains): find() is what is tested
|
|
}
|
|
|
|
// where basic_json has undefined behavior, the view answers safely
|
|
const json_document d = json_document::parse(R"({"a":[]})");
|
|
CHECK(d.root()["b"].is_discarded());
|
|
CHECK(d.root()["a"][0].is_discarded());
|
|
CHECK_THROWS_WITH_AS(d.root()["a"].front(), "[json.exception.invalid_iterator.214] cannot get value", json::invalid_iterator&);
|
|
CHECK_THROWS_WITH_AS(d.root()["a"].back(), "[json.exception.invalid_iterator.214] cannot get value", json::invalid_iterator&);
|
|
const json_view invalid{};
|
|
CHECK(invalid.begin() == invalid.end());
|
|
CHECK(std::string(invalid.type_name()) == "discarded");
|
|
}
|
|
|
|
SECTION("chained access is safe: operator[] of a discarded view is discarded")
|
|
{
|
|
const json_document d = json_document::parse(R"({"a":{"b":[10,20]},"s":"str"})");
|
|
const json_view v = d.root();
|
|
// missing keys and indexes
|
|
CHECK(v["x"].is_discarded());
|
|
CHECK(v["x"]["y"].is_discarded());
|
|
CHECK(v["x"]["y"]["z"].is_discarded());
|
|
CHECK(v["x"][0].is_discarded());
|
|
CHECK(v["x"][0u][1L].is_discarded());
|
|
CHECK(v["a"]["b"][2].is_discarded());
|
|
CHECK(v["a"]["b"][2]["c"].is_discarded());
|
|
CHECK(v["a"]["b"][2][json_view::json_pointer("/c")].is_discarded());
|
|
CHECK(v["x"][json_view::json_pointer("/a/b")].is_discarded());
|
|
CHECK(v["x"][json_view::json_pointer("")].is_discarded());
|
|
CHECK(v["x"]["y"].is_discarded());
|
|
CHECK(v["x"][std::string("y")].is_discarded());
|
|
// a resolvable path still resolves
|
|
CHECK(v["a"]["b"][1].materialize() == 20);
|
|
CHECK(v[json_view::json_pointer("/a/b/1")].materialize() == 20);
|
|
// the discarded view of an unresolved pointer is discarded too
|
|
CHECK(v[json_view::json_pointer("/x/y")]["z"].is_discarded());
|
|
CHECK(v[json_view::json_pointer("/a/b/5")][0].is_discarded());
|
|
#if !defined(JSON_NOEXCEPTION)
|
|
// type errors on values that are not discarded stay
|
|
CHECK_THROWS_WITH_AS(v[0], "[json.exception.type_error.305] cannot use operator[] with a numeric argument with object", json::type_error&);
|
|
CHECK_THROWS_WITH_AS(v["a"]["b"]["c"], "[json.exception.type_error.305] cannot use operator[] with a string argument with array", json::type_error&);
|
|
CHECK_THROWS_WITH_AS(v["s"]["c"], "[json.exception.type_error.305] cannot use operator[] with a string argument with string", json::type_error&);
|
|
CHECK_THROWS_WITH_AS(v["s"][0], "[json.exception.type_error.305] cannot use operator[] with a numeric argument with string", json::type_error&);
|
|
CHECK_THROWS_AS(v["a"]["b"][0]["c"], json::type_error&);
|
|
CHECK_THROWS_AS(v["s"][json_view::json_pointer("/x")], json::out_of_range&);
|
|
// at() keeps throwing on a discarded view
|
|
const json_view invalid{};
|
|
CHECK(invalid["a"].is_discarded());
|
|
CHECK(invalid[0].is_discarded());
|
|
CHECK(invalid[json_view::json_pointer("/a")].is_discarded());
|
|
CHECK_THROWS_WITH_AS(invalid.at("a"), "[json.exception.type_error.304] cannot use at() with discarded", json::type_error&);
|
|
CHECK_THROWS_WITH_AS(invalid.at(0), "[json.exception.type_error.304] cannot use at() with discarded", json::type_error&);
|
|
CHECK_THROWS_AS(v.at("x").at("y"), json::out_of_range&);
|
|
CHECK_THROWS_AS(v["x"].at("y"), json::type_error&);
|
|
CHECK_THROWS_AS(v.at(json_view::json_pointer("/x/y")), json::out_of_range&);
|
|
CHECK_THROWS_AS(v["x"].at(json_view::json_pointer("/y")), json::out_of_range&);
|
|
#endif
|
|
}
|
|
|
|
SECTION("integer types as array indexes")
|
|
{
|
|
const json_document d = json_document::parse("[10,20,30]");
|
|
const json_view v = d.root();
|
|
const json j = v.materialize();
|
|
// (a variable: a cast of a constant to int32_t is a useless cast to GCC)
|
|
const std::int32_t int32_index = 1;
|
|
// (compile-time: no overload is ambiguous)
|
|
CHECK(v[0].materialize() == 10);
|
|
CHECK(v[1].materialize() == 20);
|
|
CHECK(v[0u].materialize() == 10);
|
|
CHECK(v[1u].materialize() == 20);
|
|
CHECK(v[1L].materialize() == 20);
|
|
CHECK(v[2UL].materialize() == 30);
|
|
CHECK(v[1LL].materialize() == 20);
|
|
CHECK(v[2ULL].materialize() == 30);
|
|
CHECK(v[static_cast<short>(1)].materialize() == 20);
|
|
CHECK(v[static_cast<unsigned short>(2)].materialize() == 30);
|
|
CHECK(v[static_cast<signed char>(1)].materialize() == 20);
|
|
CHECK(v[static_cast<unsigned char>(2)].materialize() == 30);
|
|
CHECK(v[std::int8_t(1)].materialize() == 20);
|
|
CHECK(v[std::int16_t(2)].materialize() == 30);
|
|
CHECK(v[int32_index].materialize() == 20);
|
|
CHECK(v[std::int64_t(2)].materialize() == 30);
|
|
CHECK(v[std::uint32_t(0)].materialize() == 10);
|
|
CHECK(v[std::uint64_t(1)].materialize() == 20);
|
|
CHECK(v[std::size_t(2)].materialize() == 30);
|
|
CHECK(v[std::ptrdiff_t(1)].materialize() == 20);
|
|
CHECK(j[0u] == 10); // as basic_json
|
|
|
|
CHECK(v.at(0).materialize() == 10);
|
|
CHECK(v.at(1u).materialize() == 20);
|
|
CHECK(v.at(1L).materialize() == 20);
|
|
CHECK(v.at(2LL).materialize() == 30);
|
|
CHECK(v.at(2ULL).materialize() == 30);
|
|
CHECK(v.at(static_cast<short>(1)).materialize() == 20);
|
|
CHECK(v.at(static_cast<unsigned short>(2)).materialize() == 30);
|
|
CHECK(v.at(int32_index - 1).materialize() == 10);
|
|
CHECK(v.at(std::uint32_t(0)).materialize() == 10);
|
|
CHECK(v.at(std::int64_t(0)).materialize() == 10);
|
|
CHECK(v.at(std::uint64_t(1)).materialize() == 20);
|
|
CHECK(v.at(std::size_t(2)).materialize() == 30);
|
|
CHECK(j.at(std::uint32_t(0)) == 10); // as basic_json
|
|
|
|
// out of range, including negative values (no wrap-around)
|
|
CHECK(v[3].is_discarded());
|
|
CHECK(v[3u].is_discarded());
|
|
CHECK(v[3L].is_discarded());
|
|
CHECK(v[-1].is_discarded());
|
|
CHECK(v[-1L].is_discarded());
|
|
CHECK(v[-1LL].is_discarded());
|
|
CHECK(v[static_cast<short>(-1)].is_discarded());
|
|
CHECK(v[std::int64_t(-3)].is_discarded());
|
|
CHECK(v[(std::numeric_limits<std::int64_t>::min)()].is_discarded());
|
|
CHECK(v[(std::numeric_limits<std::uint64_t>::max)()].is_discarded());
|
|
CHECK(v[(std::numeric_limits<std::size_t>::max)()].is_discarded());
|
|
CHECK(v[std::numeric_limits<int>::max()].is_discarded());
|
|
#if !defined(JSON_NOEXCEPTION)
|
|
CHECK_THROWS_WITH_AS(v.at(3), "[json.exception.out_of_range.401] array index 3 is out of range", json::out_of_range&);
|
|
CHECK_THROWS_WITH_AS(v.at(3u), "[json.exception.out_of_range.401] array index 3 is out of range", json::out_of_range&);
|
|
CHECK_THROWS_WITH_AS(v.at(std::int64_t(3)), "[json.exception.out_of_range.401] array index 3 is out of range", json::out_of_range&);
|
|
CHECK_THROWS_AS(v.at(-1), json::out_of_range&);
|
|
CHECK_THROWS_AS(v.at(-1L), json::out_of_range&);
|
|
CHECK_THROWS_AS(v.at(std::int64_t(-1)), json::out_of_range&);
|
|
CHECK_THROWS_AS(v.at((std::numeric_limits<std::int64_t>::min)()), json::out_of_range&);
|
|
CHECK_THROWS_AS(v.at((std::numeric_limits<std::uint64_t>::max)()), json::out_of_range&);
|
|
// not an array
|
|
const json_document o = json_document::parse("{}");
|
|
CHECK_THROWS_AS(o.root()[0u], json::type_error&);
|
|
CHECK_THROWS_AS(o.root()[1L], json::type_error&);
|
|
CHECK_THROWS_AS(o.root().at(std::uint32_t(0)), json::type_error&);
|
|
CHECK_THROWS_AS(o.root().at(std::int64_t(0)), json::type_error&);
|
|
#endif
|
|
}
|
|
|
|
SECTION("arrays whose elements have no subtrees: index and back()")
|
|
{
|
|
// (the elements are adjacent in the index; an array, an object, or an
|
|
// empty container anywhere makes the lookup skip element by element)
|
|
const std::vector<std::string> texts =
|
|
{
|
|
"[]", "[1]", "[1,2,3]", "[1,\"two\",3.5,true,null,-7,18446744073709551615]", "[[]]", "[{}]", "[1,[2],3]",
|
|
"[1,2,{\"a\":3}]", "[[1,2],3,4]", "[1,2,3,[4,5,6,7,8],9,10,11,12]", "[{\"a\":[1]},2]", R"(["a\u00e4","b"])"
|
|
};
|
|
for (const std::string& text : texts)
|
|
{
|
|
CAPTURE(text)
|
|
const json_document d = json_document::parse(text);
|
|
const json_view v = d.root();
|
|
const json expected = json::parse(text);
|
|
for (std::size_t i = 0; i < expected.size(); ++i)
|
|
{
|
|
CHECK(v[i].materialize() == expected[i]);
|
|
CHECK(v.at(i).materialize() == expected[i]);
|
|
}
|
|
CHECK(v[expected.size()].is_discarded());
|
|
if (!expected.empty())
|
|
{
|
|
CHECK(v.back().materialize() == expected.back());
|
|
}
|
|
}
|
|
// a long array (the direct lookup is not limited to short ones), also with a container at its end
|
|
std::string text = "[0";
|
|
for (int i = 1; i < 1000; ++i)
|
|
{
|
|
text += "," + std::to_string(i);
|
|
}
|
|
const json_document scalars = json_document::parse(text + "]");
|
|
CHECK(scalars.root()[999].get<int>() == 999);
|
|
CHECK(scalars.root()[500].get<int>() == 500);
|
|
CHECK(scalars.root().at(1).get<int>() == 1);
|
|
CHECK(scalars.root().back().get<int>() == 999);
|
|
const json_document container_last = json_document::parse(text + ",[1000]]");
|
|
CHECK(container_last.root()[999].get<int>() == 999);
|
|
CHECK(container_last.root()[1000][0].get<int>() == 1000);
|
|
CHECK(container_last.root().back()[0].get<int>() == 1000);
|
|
}
|
|
|
|
SECTION("iterators")
|
|
{
|
|
const json_document d = json_document::parse(R"({"x":[1,{"y":2}],"z":null})");
|
|
const json_view v = d.root();
|
|
json_view::iterator it = v.begin();
|
|
CHECK(it.is_object_iterator());
|
|
CHECK(it->is_array());
|
|
CHECK(it->size() == 2);
|
|
const json_view::iterator previous = it++;
|
|
CHECK(previous.key() == "x");
|
|
CHECK(it.key() == "z");
|
|
CHECK(it.value().is_null());
|
|
CHECK(++it == v.end());
|
|
CHECK(v.cbegin() == v.begin());
|
|
CHECK(v.cend() == v.end());
|
|
CHECK(!v["x"].begin().is_object_iterator());
|
|
CHECK(json_view::iterator() == json_view::iterator());
|
|
// standard algorithms
|
|
CHECK(std::count_if(v["x"].begin(), v["x"].end(), [](const json_view & e)
|
|
{
|
|
return e.is_object();
|
|
}) == 1);
|
|
}
|
|
|
|
SECTION("items")
|
|
{
|
|
const json_document d = json_document::parse(R"({"a":1,"b":[true,false]})");
|
|
std::string keys;
|
|
for (const auto& item : d.root().items())
|
|
{
|
|
keys += std::string(item.key().data(), item.key().size());
|
|
CHECK(item.value().materialize() == d.root()[item.key()].materialize());
|
|
}
|
|
CHECK(keys == "ab");
|
|
auto items = d.root()["b"].items();
|
|
auto first = items.begin();
|
|
CHECK((*first++).key() == "0");
|
|
CHECK((*first).key() == "1");
|
|
CHECK(++first == items.end());
|
|
|
|
// the keys of array elements are their indexes, whether or not they are asked for
|
|
std::string numbers = "[0";
|
|
for (int i = 1; i < 1234; ++i)
|
|
{
|
|
numbers += "," + std::to_string(i);
|
|
}
|
|
const json_document array = json_document::parse(numbers + "]");
|
|
std::size_t position = 0;
|
|
for (const auto& item : array.root().items())
|
|
{
|
|
CHECK(item.value().get<std::size_t>() == position);
|
|
if (position % 3 != 0) // (not for every item: values alone do not make the key)
|
|
{
|
|
CHECK(item.key() == std::to_string(position));
|
|
CHECK(item.key().data() == item.key().data()); // (the same text every time)
|
|
const json_view::item copy = item;
|
|
CHECK(copy.key() == std::to_string(position));
|
|
}
|
|
++position;
|
|
}
|
|
CHECK(position == 1234);
|
|
#ifdef JSON_HAS_CPP_17
|
|
std::string pairs;
|
|
for (const auto [key, value] : d.root().items())
|
|
{
|
|
pairs += std::string(key) + "=" + value.materialize().dump() + ";";
|
|
}
|
|
CHECK(pairs == "a=1;b=[true,false];");
|
|
static_assert(std::tuple_size<json_view::item>::value == 2, "tuple_size of an item is 2");
|
|
static_assert(std::is_same<std::tuple_element<1, json_view::item>::type, json_view>::value, "the second element of an item is a view");
|
|
#endif
|
|
}
|
|
}
|
|
|
|
namespace
|
|
{
|
|
#if !defined(JSON_NOEXCEPTION)
|
|
// an exception message without the context that basic_json adds with
|
|
// JSON_DIAGNOSTICS ("(/path) ") and JSON_DIAGNOSTIC_POSITIONS ("(bytes 1-2) ");
|
|
// the view's exceptions have no such context
|
|
std::string without_path(std::string msg)
|
|
{
|
|
for (const char* prefix :
|
|
{"] (/", "] (bytes "
|
|
})
|
|
{
|
|
const std::size_t open = msg.find(prefix);
|
|
if (open != std::string::npos)
|
|
{
|
|
msg.erase(open + 2, msg.find(") ", open) + 2 - (open + 2));
|
|
}
|
|
}
|
|
return msg;
|
|
}
|
|
#endif
|
|
|
|
// the bits of a float, to compare values bit for bit
|
|
std::uint64_t bits(double x)
|
|
{
|
|
std::uint64_t r = 0;
|
|
std::memcpy(&r, &x, sizeof(r));
|
|
return r;
|
|
}
|
|
|
|
std::uint32_t bits(float x)
|
|
{
|
|
std::uint32_t r = 0;
|
|
std::memcpy(&r, &x, sizeof(r));
|
|
return r;
|
|
}
|
|
|
|
// compares the conversions of a view with those of ordered_json
|
|
void check_values(const ordered_json_view& v, const ordered_json& j, const std::string& text)
|
|
{
|
|
CHECK(v.get<ordered_json>() == j);
|
|
switch (j.type())
|
|
{
|
|
case json::value_t::number_integer:
|
|
case json::value_t::number_unsigned:
|
|
case json::value_t::number_float:
|
|
{
|
|
// (converting a float out of range of the target type is undefined)
|
|
if (j.is_number_unsigned())
|
|
{
|
|
CHECK(v.get<std::uint64_t>() == j.get<std::uint64_t>());
|
|
}
|
|
else if (j.is_number_integer())
|
|
{
|
|
CHECK(v.get<std::int64_t>() == j.get<std::int64_t>());
|
|
}
|
|
CHECK(bits(v.get<double>()) == bits(j.get<double>()));
|
|
if (std::abs(j.get<double>()) < 1e9)
|
|
{
|
|
CHECK(v.get<int>() == j.get<int>());
|
|
}
|
|
const auto token = v.number_token();
|
|
CHECK(text.compare(v.source_offset(), token.size(), token.data(), token.size()) == 0);
|
|
break;
|
|
}
|
|
case json::value_t::string:
|
|
CHECK(v.get<std::string>() == j.get<std::string>());
|
|
CHECK(std::string(v.get_string().data(), v.get_string().size()) == j.get<std::string>());
|
|
break;
|
|
case json::value_t::boolean:
|
|
CHECK(v.get<bool>() == j.get<bool>());
|
|
CHECK(v.get<int>() == j.get<int>());
|
|
break;
|
|
case json::value_t::null:
|
|
CHECK(v.get<std::nullptr_t>() == nullptr);
|
|
break;
|
|
case json::value_t::array:
|
|
CHECK(v.get<std::vector<ordered_json>>() == j.get<std::vector<ordered_json>>());
|
|
break;
|
|
case json::value_t::object:
|
|
CHECK((v.get<std::map<std::string, ordered_json>>() == j.get<std::map<std::string, ordered_json>>()));
|
|
break;
|
|
case json::value_t::binary:
|
|
case json::value_t::discarded:
|
|
default:
|
|
break;
|
|
}
|
|
|
|
#if !defined(JSON_NOEXCEPTION)
|
|
// conversions to the wrong type throw what basic_json throws
|
|
if (!j.is_number())
|
|
{
|
|
CHECK(exception_of([&] { static_cast<void>(v.get<int>()); }) == without_path(exception_of([&] { static_cast<void>(j.get<int>()); })));
|
|
}
|
|
CHECK(exception_of([&] { static_cast<void>(v.get<bool>()); }) == without_path(exception_of([&] { static_cast<void>(j.get<bool>()); })));
|
|
CHECK(exception_of([&] { static_cast<void>(v.get<std::string>()); }) == without_path(exception_of([&] { static_cast<void>(j.get<std::string>()); })));
|
|
CHECK(exception_of([&] { static_cast<void>(v.get<std::nullptr_t>()); }) == without_path(exception_of([&] { static_cast<void>(j.get<std::nullptr_t>()); })));
|
|
if (!j.is_array())
|
|
{
|
|
CHECK(exception_of([&] { static_cast<void>(v.get<std::vector<int>>()); }) == without_path(exception_of([&] { static_cast<void>(j.get<std::vector<int>>()); })));
|
|
}
|
|
if (!j.is_object())
|
|
{
|
|
CHECK(exception_of([&] { static_cast<void>(v.get<std::map<std::string, int>>()); }) == without_path(exception_of([&] { static_cast<void>(j.get<std::map<std::string, int>>()); })));
|
|
}
|
|
#endif
|
|
|
|
if (v.is_array())
|
|
{
|
|
std::size_t i = 0;
|
|
for (const ordered_json_view e : v)
|
|
{
|
|
check_values(e, j[i++], text);
|
|
}
|
|
}
|
|
else if (v.is_object())
|
|
{
|
|
for (auto it = v.begin(); it != v.end(); ++it)
|
|
{
|
|
const std::string key(it.key().data(), it.key().size());
|
|
if (v.size() == j.size()) // (no duplicate keys)
|
|
{
|
|
check_values(it.value(), j[key], text);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
struct record
|
|
{
|
|
std::string name{}; // NOLINT(readability-redundant-member-init)
|
|
int count = 0;
|
|
};
|
|
|
|
void from_json(const json& j, record& r)
|
|
{
|
|
j.at("name").get_to(r.name);
|
|
j.at("count").get_to(r.count);
|
|
}
|
|
|
|
struct defaults
|
|
{
|
|
int a = 1;
|
|
int b = 2;
|
|
};
|
|
|
|
// sets only the members the JSON value has
|
|
void from_json(const json& j, defaults& d)
|
|
{
|
|
if (j.contains("a"))
|
|
{
|
|
j.at("a").get_to(d.a);
|
|
}
|
|
if (j.contains("b"))
|
|
{
|
|
j.at("b").get_to(d.b);
|
|
}
|
|
}
|
|
} // namespace
|
|
|
|
TEST_CASE("json_view values")
|
|
{
|
|
SECTION("generated documents")
|
|
{
|
|
generator g;
|
|
for (int i = 0; i < 2000; ++i)
|
|
{
|
|
std::string text;
|
|
g.value(text, 0);
|
|
CAPTURE(text)
|
|
const ordered_json_document d = ordered_json_document::parse(text);
|
|
check_values(d.root(), ordered_json::parse(text), text);
|
|
}
|
|
}
|
|
|
|
SECTION("floats are converted as parse() converts them")
|
|
{
|
|
std::mt19937_64 rng(5295); // NOLINT(cert-msc32-c,cert-msc51-cpp,bugprone-random-generator-seed)
|
|
std::vector<std::string> tokens = {"0.1", "-0.0", "1e308", "1.7976931348623157e308", "2.2250738585072011e-308", "4.9e-324", "5e-324",
|
|
"0.1000000000000000055511151231257827021181583404541015625", "123456789012345678901234567890",
|
|
"9007199254740993", "1.00000000000000011102230246251565404236316680908203125", "7.2057594037927933e16",
|
|
// around the limits of the conversion from the digit layout: 19 and 20
|
|
// digits, and those of Clinger's fast path (2^53, 10^22)
|
|
"1234567890.123456789", "1234567890.1234567891", "0.0000000000000000001", "123456789012345678.9",
|
|
"9007199254740992.0", "9007199254740993.0", "9007199254740994.0", "1.5e22", "1.5e23", "15e-22", "15e-23",
|
|
"1e-400", "0.0e0", "-0.0e-5", "12E+3", "12e-0",
|
|
// and of float: 2^24 + 1 and 2^24 + 3 (ties), the subnormal and normal limits
|
|
"16777217", "16777219", "1.4e-45", "7.006492321624085e-46", "7.006492321624086e-46",
|
|
"1.17549435e-38", "0.30000001192092896", "3.4028234e37"
|
|
};
|
|
for (int i = 0; i < 20000; ++i)
|
|
{
|
|
const std::uint64_t bits = rng();
|
|
double d = 0;
|
|
std::memcpy(&d, &bits, sizeof(d));
|
|
if (!std::isfinite(d))
|
|
{
|
|
continue;
|
|
}
|
|
std::array<char, 400> buf{};
|
|
switch (i % 5) // NOLINT(hicpp-multiway-paths-covered)
|
|
{
|
|
case 0:
|
|
static_cast<void>(std::snprintf(buf.data(), buf.size(), "%.17g", d)); // NOLINT(cppcoreguidelines-pro-type-vararg,hicpp-vararg)
|
|
break;
|
|
case 1:
|
|
static_cast<void>(std::snprintf(buf.data(), buf.size(), "%.15g", d)); // NOLINT(cppcoreguidelines-pro-type-vararg,hicpp-vararg)
|
|
break;
|
|
case 2:
|
|
static_cast<void>(std::snprintf(buf.data(), buf.size(), "%.3e", d)); // NOLINT(cppcoreguidelines-pro-type-vararg,hicpp-vararg)
|
|
break;
|
|
case 3:
|
|
static_cast<void>(std::snprintf(buf.data(), buf.size(), "%.25g", d)); // NOLINT(cppcoreguidelines-pro-type-vararg,hicpp-vararg)
|
|
break;
|
|
default:
|
|
static_cast<void>(std::snprintf(buf.data(), buf.size(), "%.0f", d)); // NOLINT(cppcoreguidelines-pro-type-vararg,hicpp-vararg)
|
|
break;
|
|
}
|
|
tokens.emplace_back(buf.data());
|
|
}
|
|
using json_float = nlohmann::basic_json<std::map, std::vector, std::string, bool, std::int64_t, std::uint64_t, float>;
|
|
for (const auto& token : tokens)
|
|
{
|
|
CAPTURE(token)
|
|
const std::string text = "[" + token + "]";
|
|
const double b = json::parse(text)[0].get<double>();
|
|
const json_document dd = json_document::parse(text);
|
|
CHECK(bits(dd.root()[0].get<double>()) == bits(b));
|
|
if (std::abs(b) < 1e38)
|
|
{
|
|
const nlohmann::basic_json_document<json_float> df = nlohmann::basic_json_document<json_float>::parse(text);
|
|
CHECK(bits(df.root()[0].get<float>()) == bits(json_float::parse(text)[0].get<float>()));
|
|
}
|
|
}
|
|
}
|
|
|
|
SECTION("number tokens")
|
|
{
|
|
const json_document d = json_document::parse(R"([1.50, 1E2, -0, 123456789012345678901234567890, -12, 7, "x"])");
|
|
const json_view v = d.root();
|
|
CHECK(v[0].number_token() == "1.50");
|
|
CHECK(v[1].number_token() == "1E2");
|
|
CHECK(v[2].number_token() == "-0");
|
|
CHECK(v[3].number_token() == "123456789012345678901234567890");
|
|
CHECK(v[4].number_token() == "-12");
|
|
CHECK(v[5].number_token() == "7");
|
|
CHECK_THROWS_WITH_AS(v[6].number_token(), "[json.exception.type_error.302] type must be number, but is string", json::type_error&);
|
|
CHECK_THROWS_WITH_AS(v.get_string(), "[json.exception.type_error.302] type must be string, but is array", json::type_error&);
|
|
}
|
|
|
|
SECTION("conversions")
|
|
{
|
|
const std::string text = R"({"name": "widget", "count": 3, "tags": ["a", "b\n"], "sizes": {"s": 1, "m": 2}, "pair": [1, "x"]})";
|
|
const json_document d = json_document::parse(text);
|
|
const json_view v = d.root();
|
|
const json j = json::parse(text);
|
|
|
|
// user types with from_json, and other types, through basic_json
|
|
const auto r = v.get<record>();
|
|
CHECK(r.name == "widget");
|
|
CHECK(r.count == 3);
|
|
CHECK((v["pair"].get<std::pair<int, std::string>>() == j["pair"].get<std::pair<int, std::string>>()));
|
|
CHECK(v["tags"].get<std::list<std::string>>() == j["tags"].get<std::list<std::string>>());
|
|
CHECK((v["sizes"].get<std::unordered_map<std::string, int>>() == j["sizes"].get<std::unordered_map<std::string, int>>()));
|
|
CHECK(v["tags"].get<std::vector<std::string>>() == std::vector<std::string> {"a", "b\n"});
|
|
|
|
// views of the elements
|
|
const auto views = v["tags"].get<std::vector<json_view>>();
|
|
CHECK(views.size() == 2);
|
|
CHECK(views[1].get_string() == "b\n");
|
|
const auto members = v.get<std::map<std::string, json_view>>();
|
|
CHECK(members.at("count").get<int>() == 3);
|
|
CHECK(v.get<json_view>()["name"].get_string() == "widget");
|
|
|
|
// strings without a copy point into the source text
|
|
CHECK(v["name"].get_string().data() == text.data() + text.find("widget"));
|
|
#ifdef JSON_HAS_CPP_17
|
|
CHECK(v["name"].get<std::string_view>() == "widget");
|
|
#endif
|
|
|
|
std::string name;
|
|
int count = 0;
|
|
CHECK(&v["name"].get_to(name) == &name);
|
|
v["count"].get_to(count);
|
|
CHECK(name == "widget");
|
|
CHECK(count == 3);
|
|
|
|
// other types go to from_json with the existing value, as basic_json::get_to()
|
|
defaults def;
|
|
def.b = 5;
|
|
const json_document partial = json_document::parse(R"({"a":4})");
|
|
partial.root().get_to(def);
|
|
CHECK(def.a == 4);
|
|
CHECK(def.b == 5);
|
|
|
|
// a duplicate key: the last value, as parse()
|
|
const json_document dup = json_document::parse(R"({"a":1,"a":2})");
|
|
CHECK((dup.root().get<std::map<std::string, int>>() == std::map<std::string, int> {{"a", 2}}));
|
|
|
|
const json_view invalid{};
|
|
CHECK_THROWS_WITH_AS(invalid.get<int>(), "[json.exception.type_error.302] type must be number, but is discarded", json::type_error&);
|
|
CHECK(invalid.get<json>().is_discarded());
|
|
}
|
|
|
|
SECTION("value")
|
|
{
|
|
const json_document d = json_document::parse(R"({"n": 1, "s": "text", "o": {"x": [10, 20]}})");
|
|
const json_view v = d.root();
|
|
const json j = v.materialize();
|
|
CHECK(v.value("n", 0) == j.value("n", 0));
|
|
CHECK(v.value("missing", 42) == j.value("missing", 42));
|
|
CHECK(v.value("s", "default") == j.value("s", "default"));
|
|
CHECK(v.value("missing", "default") == j.value("missing", "default"));
|
|
CHECK(v.value(std::string("n"), 2.5) == j.value(std::string("n"), 2.5));
|
|
CHECK(v.value(json::json_pointer("/o/x/1"), 0) == j.value(json::json_pointer("/o/x/1"), 0));
|
|
CHECK(v.value(json::json_pointer("/o/x/5"), 0) == j.value(json::json_pointer("/o/x/5"), 0));
|
|
CHECK(v.value(json::json_pointer("/o/y"), "none") == j.value(json::json_pointer("/o/y"), "none"));
|
|
// with a JSON pointer, arrays can be asked as well
|
|
CHECK(v["o"]["x"].value(json::json_pointer("/1"), 0) == j["o"]["x"].value(json::json_pointer("/1"), 0));
|
|
CHECK(v["o"]["x"].value(json::json_pointer("/7"), 3) == j["o"]["x"].value(json::json_pointer("/7"), 3));
|
|
#if !defined(JSON_NOEXCEPTION)
|
|
CHECK(exception_of([&] { static_cast<void>(v["o"]["x"].value("k", 0)); }) == without_path(exception_of([&] { static_cast<void>(j["o"]["x"].value("k", 0)); })));
|
|
CHECK(exception_of([&] { static_cast<void>(v.value("s", 0)); }) == without_path(exception_of([&] { static_cast<void>(j.value("s", 0)); })));
|
|
CHECK(exception_of([&] { static_cast<void>(v["n"].value("x", 0)); }) == without_path(exception_of([&] { static_cast<void>(j["n"].value("x", 0)); })));
|
|
CHECK(exception_of([&] { static_cast<void>(v["n"].value(json::json_pointer("/x"), 0)); }) == without_path(exception_of([&] { static_cast<void>(j["n"].value(json::json_pointer("/x"), 0)); })));
|
|
// string defaults: the member is not a string, or the value is neither an object nor an array
|
|
CHECK(exception_of([&] { static_cast<void>(v.value("n", "default")); }) == without_path(exception_of([&] { static_cast<void>(j.value("n", "default")); })));
|
|
CHECK(exception_of([&] { static_cast<void>(v["n"].value("x", "default")); }) == without_path(exception_of([&] { static_cast<void>(j["n"].value("x", "default")); })));
|
|
CHECK(exception_of([&] { static_cast<void>(v.value(json::json_pointer("/n"), "default")); }) == without_path(exception_of([&] { static_cast<void>(j.value(json::json_pointer("/n"), "default")); })));
|
|
CHECK(exception_of([&] { static_cast<void>(v["s"].value(json::json_pointer("/x"), "default")); }) == without_path(exception_of([&] { static_cast<void>(j["s"].value(json::json_pointer("/x"), "default")); })));
|
|
#endif
|
|
// (defaults and members longer than the short string optimization)
|
|
CHECK(v.value("s", "a default longer than fifteen characters") == "text");
|
|
CHECK(v.value("missing", "a default longer than fifteen characters") == "a default longer than fifteen characters");
|
|
CHECK(v.value(json::json_pointer("/s"), "a default longer than fifteen characters") == "text");
|
|
CHECK(v.value(json::json_pointer("/zz"), "a default longer than fifteen characters") == "a default longer than fifteen characters");
|
|
}
|
|
}
|
|
|
|
TEST_CASE("json_view JSON pointers")
|
|
{
|
|
SECTION("every value of generated documents")
|
|
{
|
|
generator g;
|
|
for (int i = 0; i < 1000; ++i)
|
|
{
|
|
std::string text;
|
|
g.value(text, 0);
|
|
const ordered_json_document d = ordered_json_document::parse(text);
|
|
if (has_duplicate_keys(d.root()))
|
|
{
|
|
continue;
|
|
}
|
|
CAPTURE(text)
|
|
const ordered_json j = ordered_json::parse(text);
|
|
const ordered_json flat = j.flatten();
|
|
for (const auto& leaf : flat.items())
|
|
{
|
|
// the leaf and each of its parents
|
|
for (ordered_json::json_pointer p(leaf.key());; p = p.parent_pointer())
|
|
{
|
|
CAPTURE(p.to_string())
|
|
CHECK(d.root()[p].materialize() == j[p]);
|
|
CHECK(d.root().at(p).materialize() == j.at(p));
|
|
CHECK(d.root().contains(p));
|
|
if (p.empty())
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#if !defined(JSON_NOEXCEPTION)
|
|
SECTION("errors are those of basic_json")
|
|
{
|
|
const std::string text = R"({"a": [1, {"b": null}], "c": "s", "": {"": 0}, "a~b": 1, "c/d": 2})";
|
|
const json_document d = json_document::parse(text);
|
|
const json_view v = d.root();
|
|
const json j = v.materialize();
|
|
for (const char* pointer :
|
|
{"", "/", "//", "/a", "/a/0", "/a/1/b", "/a/-", "/a/01", "/a/00", "/a/1a", "/a/a", "/a/", "/a/2", "/a/99", "/a/99999999999999999999",
|
|
"/a/18446744073709551615", "/a/-1", "/a/+1", "/a/ 1", "/x", "/c/x", "/a/0/x", "/a/1/b/c", "/a~0b", "/c~1d", "/c~1d/x", "/a/1/-"
|
|
})
|
|
{
|
|
CAPTURE(pointer)
|
|
const json::json_pointer p(pointer);
|
|
const std::string at_error = without_path(exception_of([&] { static_cast<void>(j.at(p)); }));
|
|
CHECK(exception_of([&] { static_cast<void>(v.at(p)); }) == at_error);
|
|
if (at_error.empty())
|
|
{
|
|
CHECK(v.at(p).materialize() == j.at(p));
|
|
CHECK(v[p].materialize() == j[p]);
|
|
}
|
|
else if (at_error.find("out_of_range.401") != std::string::npos || at_error.find("out_of_range.403") != std::string::npos) // NOLINT(abseil-string-find-str-contains)
|
|
{
|
|
// undefined behavior for const basic_json::operator[]
|
|
CHECK(v[p].is_discarded());
|
|
}
|
|
else
|
|
{
|
|
CHECK(exception_of([&] { static_cast<void>(v[p]); }) == without_path(exception_of([&] { static_cast<void>(j[p]); })));
|
|
}
|
|
// (basic_json::contains() throws out_of_range.404 for an empty
|
|
// array index token, although it is not meant to throw; the view
|
|
// answers false)
|
|
const std::string contains_error = exception_of([&]
|
|
{
|
|
const bool found = j.contains(p);
|
|
static_cast<void>(found);
|
|
});
|
|
CHECK(v.contains(p) == (contains_error.empty() && j.contains(p)));
|
|
CHECK(exception_of([&] { static_cast<void>(v.value(p, 5)); }) == without_path(exception_of([&] { static_cast<void>(j.value(p, 5)); })));
|
|
}
|
|
}
|
|
#endif
|
|
}
|