Merge branch 'json-view/14-view-compare' into json-view/14b-view-float-layout

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
This commit is contained in:
Niels Lohmann
2026-09-29 22:47:28 +02:00
11 changed files with 85 additions and 28 deletions

View File

@@ -15,7 +15,7 @@ int main()
const auto values = measurements.root();
const int sum = std::accumulate(values.cbegin(), values.cend(), 0,
[](int total, const json_view & v)
[](int total, const json_view & v)
{
return total + v.materialize().get<int>();
});

View File

@@ -15,7 +15,7 @@ int main()
const auto records = batch.root();
const bool all_objects = std::all_of(records.cbegin(), records.cend(),
[](const json_view & v)
[](const json_view & v)
{
return v.is_object();
});

View File

@@ -19,7 +19,7 @@ int main()
// the source text; a json value's object_t is std::map, so
// materialize().dump() of the very same view sorts the keys instead
const json_document config = json_document::parse(
R"({"name": "cache", "host": "db1", "port": 6379, "timeout": 30})");
R"({"name": "cache", "host": "db1", "port": 6379, "timeout": 30})");
std::cout << config.root().dump(2) << "\n\n";
std::cout << config.root().materialize().dump(2) << '\n';
}

View File

@@ -10,9 +10,9 @@ int main()
// directly as views, without ever building a nlohmann::json value for
// either one
const json_document previous = json_document::parse(
R"({"name": "cache", "port": 6379, "timeout": 30})");
R"({"name": "cache", "port": 6379, "timeout": 30})");
const json_document current = json_document::parse(
R"({"port": 6379.0, "timeout": 30, "name": "cache"})");
R"({"port": 6379.0, "timeout": 30, "name": "cache"})");
// same members, reordered, and 6379 written as a float -- operator==
// treats them the same way BasicJsonType::operator== would
@@ -20,7 +20,7 @@ int main()
// an actually changed value is detected the same way
const json_document changed = json_document::parse(
R"({"name": "cache", "port": 6380, "timeout": 30})");
R"({"name": "cache", "port": 6380, "timeout": 30})");
std::cout << (previous.root() == changed.root()) << '\n';
// comparing a view directly against an expected json value -- handy in a

View File

@@ -11,7 +11,7 @@ int main()
// unwanted shape -- without ever materializing it into a json value just
// to compare
const json_document received = json_document::parse(
R"({"status": "ok", "code": 200})");
R"({"status": "ok", "code": 200})");
const json unwanted = {{"status", "error"}, {"code", 500}};
std::cout << std::boolalpha << (received.root() != unwanted) << '\n';

View File

@@ -13,7 +13,10 @@ int main()
json_document bad = json_document::parse("[1, 2, 3]");
json_document failed = json_document::parse("not json", /* allow_exceptions */ false);
for (const json_view v : { good.root(), bad.root(), failed.root() })
for (const json_view v :
{
good.root(), bad.root(), failed.root()
})
{
if (v.is_object())
{

View File

@@ -418,6 +418,14 @@ class builder
return w;
}
/// a compile-time option as a runtime condition: testing the template
/// argument directly makes a condition like `TrailingCommas && c == ']'`
/// constant when the option is off, which MSVC reports as C4127
static NLOHMANN_VIEW_ALWAYS_INLINE bool enabled(bool option) noexcept
{
return option;
}
/// The parse state and the parser proper. The cursor is a local object of
/// run() whose address never escapes (everything it calls out of line is a
/// member of the builder and gets the positions it needs), so that the
@@ -547,7 +555,7 @@ arr_next:
{
return false;
}
if (TrailingCommas && cur() == ']')
if (enabled(TrailingCommas) && cur() == ']')
{
++p;
goto close_container;
@@ -614,7 +622,7 @@ obj_next:
{
return false;
}
if (TrailingCommas && cur() == '}')
if (enabled(TrailingCommas) && cur() == '}')
{
++p;
goto close_container;
@@ -756,7 +764,7 @@ indent_done:
{
++p;
}
if (Comments && cur() == '/')
if (enabled(Comments) && cur() == '/')
{
const unsigned char* const q = cold.comment(p);
if (q == nullptr)

View File

@@ -962,6 +962,14 @@ class builder
return w;
}
/// a compile-time option as a runtime condition: testing the template
/// argument directly makes a condition like `TrailingCommas && c == ']'`
/// constant when the option is off, which MSVC reports as C4127
static NLOHMANN_VIEW_ALWAYS_INLINE bool enabled(bool option) noexcept
{
return option;
}
/// The parse state and the parser proper. The cursor is a local object of
/// run() whose address never escapes (everything it calls out of line is a
/// member of the builder and gets the positions it needs), so that the
@@ -1091,7 +1099,7 @@ arr_next:
{
return false;
}
if (TrailingCommas && cur() == ']')
if (enabled(TrailingCommas) && cur() == ']')
{
++p;
goto close_container;
@@ -1158,7 +1166,7 @@ obj_next:
{
return false;
}
if (TrailingCommas && cur() == '}')
if (enabled(TrailingCommas) && cur() == '}')
{
++p;
goto close_container;
@@ -1300,7 +1308,7 @@ indent_done:
{
++p;
}
if (Comments && cur() == '/')
if (enabled(Comments) && cur() == '/')
{
const unsigned char* const q = cold.comment(p);
if (q == nullptr)

View File

@@ -1465,7 +1465,8 @@ TEST_CASE("Eisel-Lemire float conversion")
// than the distance to the rounding boundary, so it must not change
std::string longer = token;
const std::size_t e = longer.find('e');
const std::string extra = longer.find('.') == std::string::npos ? ".000000000000000000001" : "000000000000000000001";
const std::size_t dot = longer.find('.');
const std::string extra = dot == std::string::npos ? ".000000000000000000001" : "000000000000000000001";
longer.insert(e == std::string::npos ? longer.size() : e, extra);
CAPTURE(longer);
if (eisel_lemire(longer, out))
@@ -1488,7 +1489,8 @@ TEST_CASE("Eisel-Lemire float conversion")
CHECK(bits_of(json::parse("-65.613616999999977").get<double>()) == bits_of(-65.613616999999977));
CHECK(bits_of(json::parse("2.2250738585072011e-308").get<double>()) == 0x000FFFFFFFFFFFFFu);
CHECK(bits_of(json::parse("4.9406564584124654e-324").get<double>()) == 1u);
CHECK_THROWS_WITH_AS(json::parse("1.7976931348623159e308"),
json _;
CHECK_THROWS_WITH_AS(_ = json::parse("1.7976931348623159e308"),
"[json.exception.out_of_range.406] number overflow parsing '1.7976931348623159e308'", json::out_of_range&);
}
}
@@ -1555,16 +1557,23 @@ TEST_CASE("string scanning kernels")
state ^= state << 17u;
return state;
};
// the upper half as a 32-bit value: converts to std::size_t implicitly on
// every platform (a cast of std::uint64_t is useless where both are the
// same type, and required where std::size_t is 32 bits wide)
const auto next_small = [&next]()
{
return static_cast<std::uint32_t>(next() >> 32u);
};
for (int round = 0; round < 100000; ++round)
{
// mostly ordinary text, so that runs span several words
std::string text(static_cast<std::size_t>(next() % 8), '.');
const auto count = static_cast<std::size_t>(next() % 12);
std::string text(next_small() % 8u, '.');
const std::size_t count = next_small() % 12u;
for (std::size_t k = 0; k < count; ++k)
{
const std::size_t p = (next() % 4 == 0) ? static_cast<std::size_t>(next() % pieces.size()) : 0;
const std::size_t p = (next() % 4 == 0) ? next_small() % pieces.size() : 0;
text += pieces[p];
text += std::string(static_cast<std::size_t>(next() % 10), 'x');
text += std::string(next_small() % 10u, 'x');
}
const auto* data = reinterpret_cast<const unsigned char*>(text.data()); // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast)
for (std::size_t offset = 0; offset < 3 && offset <= text.size(); ++offset)

View File

@@ -39,6 +39,7 @@ using nlohmann::ordered_json_view;
namespace
{
#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)
{
@@ -67,6 +68,7 @@ std::string view_exception(const std::string& text, bool comments = false, bool
}
return "";
}
#endif
// a small deterministic generator of documents
struct generator
@@ -156,7 +158,7 @@ TEST_CASE("json_view")
CHECK(v.materialize() == j);
}
const json_view invalid;
const json_view invalid{};
CHECK(invalid.is_discarded());
CHECK(!static_cast<bool>(invalid));
CHECK(invalid.type() == json::value_t::discarded);
@@ -201,9 +203,11 @@ TEST_CASE("json_view")
})
{
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());
@@ -211,8 +215,9 @@ TEST_CASE("json_view")
CHECK(d.node_count() == 0);
}
// the exception types
CHECK_THROWS_AS(json_document::parse("[1,"), json::parse_error&);
CHECK_THROWS_AS(json_document::parse("1e400"), json::out_of_range&);
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")
@@ -227,7 +232,9 @@ TEST_CASE("json_view")
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
}
}
}
@@ -239,7 +246,9 @@ TEST_CASE("json_view")
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(json_document::parse("\xEF\xBB\xBF[1]").root().materialize() == json::parse("\xEF\xBB\xBF[1]"));
#if !defined(JSON_NOEXCEPTION)
CHECK(view_exception("\xEF\xBB") == parse_exception("\xEF\xBB"));
#endif
}
SECTION("inputs")
@@ -385,6 +394,7 @@ TEST_CASE("json_view")
namespace
{
#if !defined(JSON_NOEXCEPTION)
// the exception a call throws, or "" if it throws none
template<typename F>
std::string exception_of(F f)
@@ -399,6 +409,7 @@ std::string exception_of(F f)
}
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
@@ -567,6 +578,7 @@ TEST_CASE("json_view element access and iteration")
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"]); }));
@@ -588,6 +600,7 @@ TEST_CASE("json_view element access and iteration")
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
@@ -599,7 +612,7 @@ TEST_CASE("json_view element access and iteration")
CHECK(!d.root()["a"][0]);
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;
const json_view invalid{};
CHECK(invalid.begin() == invalid.end());
CHECK(std::string(invalid.type_name()) == "discarded");
CHECK_THROWS_WITH_AS(invalid["a"], "[json.exception.type_error.305] cannot use operator[] with a string argument with discarded", json::type_error&);
@@ -659,6 +672,7 @@ TEST_CASE("json_view element access and iteration")
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
@@ -676,6 +690,7 @@ std::string without_path(std::string msg)
}
return msg;
}
#endif
// the bits of a float, to compare values bit for bit
std::uint64_t bits(double x)
@@ -755,6 +770,7 @@ void check_values(const ordered_json_view& v, const ordered_json& j, const std::
break;
}
#if !defined(JSON_NOEXCEPTION)
// conversions to the wrong type throw what basic_json throws
if (!j.is_number())
{
@@ -771,6 +787,7 @@ void check_values(const ordered_json_view& v, const ordered_json& j, const std::
{
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())
{
@@ -931,7 +948,7 @@ TEST_CASE("json_view values")
// a duplicate key: the last value, as parse()
CHECK((json_document::parse(R"({"a":1,"a":2})").root().get<std::map<std::string, int>>() == std::map<std::string, int> {{"a", 2}}));
const json_view invalid;
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());
}
@@ -952,10 +969,12 @@ TEST_CASE("json_view values")
// 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)); })));
#endif
}
}
@@ -994,6 +1013,7 @@ TEST_CASE("json_view JSON pointers")
}
}
#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})";
@@ -1026,11 +1046,16 @@ TEST_CASE("json_view JSON pointers")
// (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([&] { static_cast<void>(j.contains(p)); });
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
}
TEST_CASE("json_view dump")

View File

@@ -9,8 +9,12 @@
#include "doctest_compatibility.h"
#include <nlohmann/json.hpp>
#include <nlohmann/detail/view/builder.hpp>
#include <nlohmann/detail/view/string_ref.hpp>
#if JSON_TEST_USING_MULTIPLE_HEADERS
#include <nlohmann/detail/view/builder.hpp>
#include <nlohmann/detail/view/string_ref.hpp>
#else
#include <nlohmann/json_view.hpp> // the single header contains the internal headers
#endif
using nlohmann::json;
#include <cstdint>