mirror of
https://github.com/nlohmann/json.git
synced 2026-10-22 22:08:55 +00:00
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>
877 lines
33 KiB
C++
877 lines
33 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_editable_document;
|
|
using nlohmann::json_editable_view;
|
|
using nlohmann::ordered_json_document;
|
|
using nlohmann::ordered_json_editable_document;
|
|
using nlohmann::ordered_json_editable_view;
|
|
using ptr_t = ordered_json::json_pointer;
|
|
|
|
#include "json_view_test_helpers.hpp"
|
|
#if !defined(JSON_NOEXCEPTION)
|
|
using json_view_test::exception_of_call;
|
|
#endif
|
|
|
|
#include <array>
|
|
#include <cmath>
|
|
#include <cstring>
|
|
#include <cstdint>
|
|
#include <iterator>
|
|
#include <limits>
|
|
#include <map>
|
|
#include <memory>
|
|
#include <random>
|
|
#include <set>
|
|
#include <string>
|
|
#include <vector>
|
|
|
|
namespace
|
|
{
|
|
std::uint32_t rng()
|
|
{
|
|
static std::mt19937 generator(5295); // NOLINT(cert-msc32-c,cert-msc51-cpp,bugprone-random-generator-seed): reproducible
|
|
// result_type is std::uint_fast32_t, which may be wider than 32 bits
|
|
const std::mt19937::result_type value = generator();
|
|
return static_cast<std::uint32_t>(value);
|
|
}
|
|
|
|
int r(int n)
|
|
{
|
|
return static_cast<int>(rng() % static_cast<unsigned>(n));
|
|
}
|
|
|
|
int counter = 0;
|
|
|
|
std::uint64_t bits(double x)
|
|
{
|
|
std::uint64_t b = 0;
|
|
std::memcpy(&b, &x, sizeof(b));
|
|
return b;
|
|
}
|
|
|
|
std::string random_string()
|
|
{
|
|
static const std::array<const char*, 15> pieces = {{"a", "Z", " ", "~", "\n", "\"", "\\", "/", "\xc3\xa9", "\xe3\x81\x82", "\xf0\x9f\x98\x80", "\x7f", "\x1f", "0", "key"}};
|
|
std::string s;
|
|
for (int i = r(3) == 0 ? r(30) : r(6); i > 0; --i)
|
|
{
|
|
s += pieces[static_cast<std::size_t>(r(15))];
|
|
}
|
|
return s;
|
|
}
|
|
|
|
ordered_json random_scalar()
|
|
{
|
|
switch (r(9))
|
|
{
|
|
case 0:
|
|
return nullptr;
|
|
case 1:
|
|
return r(2) == 0;
|
|
case 2:
|
|
return static_cast<std::int64_t>(rng()) - 2147483648LL;
|
|
case 3:
|
|
return (static_cast<std::uint64_t>(rng()) * 4294967296ULL) + rng();
|
|
case 4:
|
|
return static_cast<double>(static_cast<std::int32_t>(rng())) / (1 + r(1000));
|
|
case 5:
|
|
return r(2) == 0 ? 1e300 * (r(2) == 0 ? 1 : -1) : 5e-324;
|
|
case 6:
|
|
return -0.0;
|
|
default:
|
|
return random_string();
|
|
}
|
|
}
|
|
|
|
ordered_json random_value(int depth)
|
|
{
|
|
const int k = depth > 3 ? 4 : r(7);
|
|
if (k == 0)
|
|
{
|
|
ordered_json o = ordered_json::object();
|
|
for (int i = r(5); i > 0; --i)
|
|
{
|
|
o[random_string() + "#" + std::to_string(counter++)] = random_value(depth + 1);
|
|
}
|
|
return o;
|
|
}
|
|
if (k == 1)
|
|
{
|
|
ordered_json a = ordered_json::array();
|
|
for (int i = r(5); i > 0; --i)
|
|
{
|
|
a.push_back(random_value(depth + 1));
|
|
}
|
|
return a;
|
|
}
|
|
return random_scalar();
|
|
}
|
|
|
|
void collect(const ordered_json& j, const ptr_t& p, std::vector<ptr_t>& out)
|
|
{
|
|
out.push_back(p);
|
|
if (j.is_object())
|
|
{
|
|
for (const auto& kv : j.items())
|
|
{
|
|
collect(kv.value(), p / kv.key(), out);
|
|
}
|
|
}
|
|
else if (j.is_array())
|
|
{
|
|
for (std::size_t i = 0; i < j.size(); ++i)
|
|
{
|
|
collect(j[i], p / i, out);
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
// the edited view and the ordered_json value read the same through the whole
|
|
// read API
|
|
void compare(const ordered_json_editable_view& v, const ordered_json& j)
|
|
{
|
|
REQUIRE(v.type() == j.type());
|
|
REQUIRE(v.size() == j.size());
|
|
if (j.is_object())
|
|
{
|
|
auto jt = j.begin();
|
|
for (auto it = v.begin(); it != v.end(); ++it, ++jt)
|
|
{
|
|
CHECK(std::string(it.key().data(), it.key().size()) == jt.key());
|
|
CHECK(v[jt.key()].dump() == jt.value().dump());
|
|
CHECK(v.contains(jt.key()));
|
|
compare(*it, jt.value());
|
|
}
|
|
}
|
|
else if (j.is_array())
|
|
{
|
|
for (std::size_t i = 0; i < j.size(); ++i)
|
|
{
|
|
CHECK(v[i].dump() == j[i].dump());
|
|
}
|
|
std::size_t i = 0;
|
|
for (const auto e : v)
|
|
{
|
|
compare(e, j[i++]);
|
|
}
|
|
if (!j.empty())
|
|
{
|
|
CHECK(v.back().dump() == j.back().dump());
|
|
}
|
|
}
|
|
else if (j.is_string())
|
|
{
|
|
CHECK(v.get<std::string>() == j.get<std::string>());
|
|
}
|
|
else if (j.is_number_float())
|
|
{
|
|
CHECK(bits(v.get<double>()) == bits(j.get<double>()));
|
|
}
|
|
else if (j.is_number_integer())
|
|
{
|
|
CHECK(v.get<std::int64_t>() == j.get<std::int64_t>());
|
|
CHECK(v.get<std::uint64_t>() == j.get<std::uint64_t>());
|
|
}
|
|
}
|
|
|
|
// the text of j, in which some objects repeat a key of theirs after their
|
|
// members: lookups of the edited document find the first member of a key, so
|
|
// j (the first members) is what they must agree with, while parse() would
|
|
// keep the last value
|
|
std::string text_with_duplicates(const ordered_json& j)
|
|
{
|
|
if (j.is_object())
|
|
{
|
|
std::vector<std::string> keys;
|
|
for (const auto& kv : j.items())
|
|
{
|
|
keys.push_back(kv.key());
|
|
}
|
|
const auto repeated = [&keys]()
|
|
{
|
|
return ordered_json(keys[static_cast<std::size_t>(r(static_cast<int>(keys.size())))]).dump() + ":" + random_value(2).dump();
|
|
};
|
|
std::string text = "{";
|
|
bool first = true;
|
|
for (const auto& kv : j.items())
|
|
{
|
|
text += (first ? "" : ",") + ordered_json(kv.key()).dump() + ":" + text_with_duplicates(kv.value());
|
|
first = false;
|
|
}
|
|
for (int i = keys.empty() ? 0 : r(3); i > 0; --i)
|
|
{
|
|
text += "," + repeated();
|
|
}
|
|
return text + "}";
|
|
}
|
|
if (j.is_array())
|
|
{
|
|
std::string text = "[";
|
|
for (std::size_t i = 0; i < j.size(); ++i)
|
|
{
|
|
text += (i != 0 ? "," : "") + text_with_duplicates(j[i]);
|
|
}
|
|
return text + "]";
|
|
}
|
|
return j.dump();
|
|
}
|
|
|
|
// the number of members of an object view with this key, by iteration
|
|
std::size_t occurrences(const ordered_json_editable_view& v, const std::string& key)
|
|
{
|
|
std::size_t result = 0;
|
|
for (const auto& item : v.items())
|
|
{
|
|
result += std::string(item.key().data(), item.key().size()) == key ? 1u : 0u;
|
|
}
|
|
return result;
|
|
}
|
|
|
|
// every lookup of the edited view finds the first member of its key, which
|
|
// the iteration of the view shows
|
|
void check_lookups(const ordered_json_editable_view& v)
|
|
{
|
|
if (v.is_object())
|
|
{
|
|
std::set<std::string> seen;
|
|
for (const auto item : v.items())
|
|
{
|
|
const std::string key(item.key().data(), item.key().size());
|
|
if (!seen.insert(key).second)
|
|
{
|
|
continue; // (not the first member of the key)
|
|
}
|
|
const ptr_t ptr = ptr_t() / key;
|
|
CAPTURE(key)
|
|
const ordered_json expected = item.value().materialize();
|
|
const ordered_json_editable_view m = v[key];
|
|
REQUIRE(!m.is_discarded());
|
|
CHECK(m.materialize() == expected);
|
|
CHECK(v.at(key).materialize() == expected);
|
|
CHECK(v[ptr].materialize() == expected);
|
|
CHECK(v.at(ptr).materialize() == expected);
|
|
CHECK(v.contains(key));
|
|
CHECK(v.contains(ptr));
|
|
CHECK(v.count(key) == occurrences(v, key));
|
|
const auto it = v.find(key);
|
|
REQUIRE(it != v.end());
|
|
CHECK((*it).materialize() == expected);
|
|
CHECK(v.value(ptr, ordered_json(nullptr)) == expected);
|
|
if (expected.is_string())
|
|
{
|
|
CHECK(v.value(key, std::string("-")) == expected.get<std::string>());
|
|
}
|
|
check_lookups(m);
|
|
}
|
|
CHECK(v["missing#key"].is_discarded());
|
|
CHECK(!v.contains("missing#key"));
|
|
CHECK(v.find("missing#key") == v.end());
|
|
}
|
|
else if (v.is_array())
|
|
{
|
|
for (std::size_t i = 0; i < v.size(); ++i) // NOLINT(modernize-loop-convert): both v[i] and v.at(i) are tested
|
|
{
|
|
check_lookups(v[i]);
|
|
check_lookups(v.at(i));
|
|
}
|
|
}
|
|
}
|
|
|
|
// every lookup of the edited view finds what the first members of j hold
|
|
void check_first_members(const ordered_json_editable_view& v, const ordered_json& j)
|
|
{
|
|
REQUIRE(v.type() == j.type());
|
|
if (j.is_object())
|
|
{
|
|
for (const auto& kv : j.items())
|
|
{
|
|
const std::string& key = kv.key();
|
|
const ptr_t ptr = ptr_t() / key;
|
|
CAPTURE(key)
|
|
REQUIRE(v.contains(key));
|
|
CHECK(v.contains(ptr));
|
|
CHECK(v.count(key) == occurrences(v, key));
|
|
CHECK(v.find(key) != v.end());
|
|
CHECK(v.at(ptr).type() == kv.value().type());
|
|
check_first_members(v[key], kv.value());
|
|
check_first_members(v.at(key), kv.value());
|
|
check_first_members(v[ptr], kv.value());
|
|
}
|
|
CHECK(!v.contains("missing#key"));
|
|
}
|
|
else if (j.is_array())
|
|
{
|
|
REQUIRE(v.size() == j.size());
|
|
for (std::size_t i = 0; i < j.size(); ++i)
|
|
{
|
|
check_first_members(v[i], j[i]);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
CHECK(v.materialize() == j);
|
|
}
|
|
}
|
|
|
|
// (for documents whose objects repeat keys: j holds the first members, the
|
|
// lookups must find them; dump(), size(), and iteration list all members, but
|
|
// materialize() keeps the last value of a key, as parse() does of the dump)
|
|
void check_duplicates(const ordered_json_editable_document& d, const ordered_json& j)
|
|
{
|
|
check_first_members(d.root(), j);
|
|
check_lookups(d.root());
|
|
CHECK(d.root().materialize() == ordered_json::parse(d.root().dump()));
|
|
}
|
|
|
|
void check_all(const ordered_json_editable_document& d, const ordered_json& j, bool deep)
|
|
{
|
|
const std::string text = d.root().dump();
|
|
REQUIRE(text == j.dump());
|
|
CHECK(d.root().materialize() == j);
|
|
if (deep)
|
|
{
|
|
CHECK(d.root().dump(2) == j.dump(2));
|
|
CHECK(d.root().dump(-1, ' ', true) == j.dump(-1, ' ', true));
|
|
CHECK(d.root() == j);
|
|
compare(d.root(), j);
|
|
// a fresh document of the text reads the same
|
|
const ordered_json_document fresh = ordered_json_document::parse(text);
|
|
CHECK(fresh.root() == d.root());
|
|
}
|
|
}
|
|
} // namespace
|
|
|
|
TEST_CASE("json_view edits: differential")
|
|
{
|
|
// random edits are applied to an ordered_json_editable_document and to the
|
|
// ordered_json parse() produces; after every edit both must serialize,
|
|
// materialize, and read back the same
|
|
for (int n = 0; n < 250; ++n)
|
|
{
|
|
// the last 100 documents repeat keys in some of their objects; j then
|
|
// holds the first member of each key, which the edits address
|
|
const bool duplicates = n >= 150;
|
|
ordered_json j = random_value(0);
|
|
if (r(4) == 0)
|
|
{
|
|
j = ordered_json::object({{"a", random_value(1)}, {"b", random_value(1)}});
|
|
}
|
|
if (duplicates)
|
|
{
|
|
j = ordered_json::parse(j.dump()); // (normalized as the document reads it)
|
|
}
|
|
const std::string text = duplicates ? text_with_duplicates(j) : j.dump(r(2) == 0 ? -1 : 2);
|
|
CAPTURE(text)
|
|
ordered_json_editable_document d = ordered_json_editable_document::parse(text);
|
|
if (!duplicates)
|
|
{
|
|
j = ordered_json::parse(text);
|
|
}
|
|
const ordered_json_document other = ordered_json_document::parse(random_value(0).dump());
|
|
const int edits = 30;
|
|
for (int e = 0; e < edits; ++e)
|
|
{
|
|
std::vector<ptr_t> paths;
|
|
collect(j, ptr_t(), paths);
|
|
const ptr_t p = paths[static_cast<std::size_t>(r(static_cast<int>(paths.size())))];
|
|
const ordered_json& target = j[p];
|
|
const ordered_json_editable_view tv = d.root().at(p);
|
|
const int op = r(12);
|
|
{
|
|
if (op == 0) // assign a scalar
|
|
{
|
|
const ordered_json v = random_scalar();
|
|
if (v.is_string() && r(2) == 0)
|
|
{
|
|
d.set(tv, v.get<std::string>());
|
|
}
|
|
else if (v.is_number_unsigned() && r(2) == 0)
|
|
{
|
|
d.set(tv, v.get<std::uint64_t>());
|
|
}
|
|
else
|
|
{
|
|
d.set(tv, v);
|
|
}
|
|
j[p] = v;
|
|
}
|
|
else if (op == 1) // assign a new array/object (or anything), sometimes via the pointer API
|
|
{
|
|
const ordered_json v = random_value(2);
|
|
if (r(2) == 0)
|
|
{
|
|
d.set(p, v);
|
|
}
|
|
else
|
|
{
|
|
d.set(tv, v);
|
|
}
|
|
j[p] = v;
|
|
}
|
|
else if (op == 2) // copy a value of the same document
|
|
{
|
|
const ptr_t& q = paths[static_cast<std::size_t>(r(static_cast<int>(paths.size())))];
|
|
const ordered_json v = j[q];
|
|
d.set(tv, d.root().at(q));
|
|
j[p] = v;
|
|
}
|
|
else if (op == 3) // copy a value of another document
|
|
{
|
|
d.set(tv, other.root());
|
|
j[p] = other.root().materialize();
|
|
}
|
|
else if ((op == 4 || op == 5) && target.is_object()) // set a member (new or existing)
|
|
{
|
|
std::string key = random_string() + "#" + std::to_string(counter++);
|
|
if (op == 5 && !target.empty())
|
|
{
|
|
key = std::next(target.begin(), r(static_cast<int>(target.size()))).key();
|
|
}
|
|
const ordered_json v = random_value(2);
|
|
d.set(tv, key, v);
|
|
j[p][key] = v;
|
|
}
|
|
else if (op == 6 && target.is_object() && !target.empty()) // erase a member
|
|
{
|
|
const std::string key = std::next(target.begin(), r(static_cast<int>(target.size()))).key();
|
|
if (r(2) == 0)
|
|
{
|
|
d.erase(tv, key);
|
|
}
|
|
else
|
|
{
|
|
d.erase(p / key);
|
|
}
|
|
j[p].erase(key);
|
|
}
|
|
else if (op == 7 && (target.is_array() || target.is_null())) // push_back
|
|
{
|
|
const ordered_json v = random_value(2);
|
|
d.push_back(tv, v);
|
|
j[p].push_back(v);
|
|
}
|
|
else if (op == 8 && target.is_array()) // insert
|
|
{
|
|
const auto i = static_cast<std::size_t>(r(static_cast<int>(target.size()) + 1));
|
|
const ordered_json v = random_value(2);
|
|
d.insert(tv, i, v);
|
|
j[p].insert(j[p].begin() + static_cast<std::ptrdiff_t>(i), v);
|
|
}
|
|
else if (op == 9 && target.is_array() && !target.empty()) // erase an element
|
|
{
|
|
const auto i = static_cast<std::size_t>(r(static_cast<int>(target.size())));
|
|
if (r(2) == 0)
|
|
{
|
|
d.erase(tv, i);
|
|
}
|
|
else
|
|
{
|
|
d.erase(p / i);
|
|
}
|
|
j[p].erase(i);
|
|
}
|
|
else if (op == 10 && target.is_array() && !target.empty()) // assign an element
|
|
{
|
|
const auto i = static_cast<std::size_t>(r(static_cast<int>(target.size())));
|
|
const ordered_json v = random_value(2);
|
|
d.set(tv, i, v);
|
|
j[p][i] = v;
|
|
}
|
|
else if (op == 11) // a held view sees the assignment
|
|
{
|
|
const ordered_json v = random_value(2);
|
|
const ordered_json_editable_view held = d.root().at(p);
|
|
d.set(p, v);
|
|
j[p] = v;
|
|
CHECK(held.dump() == v.dump());
|
|
}
|
|
else
|
|
{
|
|
continue;
|
|
}
|
|
}
|
|
CAPTURE(p.to_string())
|
|
CAPTURE(op)
|
|
if (duplicates)
|
|
{
|
|
check_duplicates(d, j);
|
|
}
|
|
else
|
|
{
|
|
check_all(d, j, e % 8 == 7 || e == edits - 1);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
TEST_CASE("json_view edits: errors")
|
|
{
|
|
SECTION("an empty document")
|
|
{
|
|
json_editable_document d;
|
|
CHECK_THROWS_WITH_AS(d.set(d.root(), 1), "[json.exception.invalid_iterator.202] view does not belong to this document", json::invalid_iterator&);
|
|
}
|
|
|
|
json_editable_document d = json_editable_document::parse(R"({"o": {"a": 1}, "a": [1, 2], "n": 1, "z": null})");
|
|
const json_editable_view root = d.root();
|
|
const json_document other = json_document::parse("[1]");
|
|
json_editable_document other_editable = json_editable_document::parse("[1]");
|
|
|
|
CHECK_THROWS_WITH_AS(d.set(other_editable.root(), 1), "[json.exception.invalid_iterator.202] view does not belong to this document", json::invalid_iterator&);
|
|
CHECK_THROWS_WITH_AS(d.set(json_editable_view(), 1), "[json.exception.invalid_iterator.202] view does not belong to this document", json::invalid_iterator&);
|
|
CHECK_THROWS_WITH_AS(d.set(root["a"], "k", 1), "[json.exception.type_error.305] cannot use operator[] with a string argument with array", json::type_error&);
|
|
CHECK_THROWS_WITH_AS(d.set(root["o"], 0, 1), "[json.exception.type_error.305] cannot use operator[] with a numeric argument with object", json::type_error&);
|
|
CHECK_THROWS_WITH_AS(d.set(root["a"], 2, 1), "[json.exception.out_of_range.401] array index 2 is out of range", json::out_of_range&);
|
|
CHECK_THROWS_WITH_AS(d.set(root["a"], -1, 1), "[json.exception.out_of_range.401] array index -1 is out of range", json::out_of_range&);
|
|
CHECK_THROWS_WITH_AS(d.push_back(root["o"], 1), "[json.exception.type_error.308] cannot use push_back() with object", json::type_error&);
|
|
CHECK_THROWS_WITH_AS(d.insert(root["n"], 0, 1), "[json.exception.type_error.309] cannot use insert() with number", json::type_error&);
|
|
CHECK_THROWS_WITH_AS(d.insert(root["a"], 3, 1), "[json.exception.out_of_range.401] array index 3 is out of range", json::out_of_range&);
|
|
CHECK_THROWS_WITH_AS(d.erase(root["n"], "k"), "[json.exception.type_error.307] cannot use erase() with number", json::type_error&);
|
|
CHECK_THROWS_WITH_AS(d.erase(root["o"], 0), "[json.exception.type_error.307] cannot use erase() with object", json::type_error&);
|
|
CHECK_THROWS_WITH_AS(d.erase(root["a"], 2), "[json.exception.out_of_range.401] array index 2 is out of range", json::out_of_range&);
|
|
CHECK_THROWS_WITH_AS(d.erase(json::json_pointer("")), "[json.exception.out_of_range.405] JSON pointer has no parent", json::out_of_range&);
|
|
CHECK_THROWS_WITH_AS(d.erase(json::json_pointer("/missing/x")), "[json.exception.out_of_range.403] key 'missing' not found", json::out_of_range&);
|
|
CHECK_THROWS_WITH_AS(d.set(json::json_pointer("/a/01"), 1), "[json.exception.parse_error.106] parse error: array index '01' must not begin with '0'", json::parse_error&);
|
|
CHECK_THROWS_WITH_AS(d.set(root, json_editable_view()), "[json.exception.type_error.302] type must be a value, but is discarded", json::type_error&);
|
|
CHECK_THROWS_WITH_AS(d.set(root, json::binary({1, 2})), "[json.exception.type_error.319] cannot store a binary value in a json_document", json::type_error&);
|
|
|
|
#if !defined(JSON_NOEXCEPTION)
|
|
// invalid UTF-8 is rejected when it enters the document, with the error
|
|
// basic_json::dump() reports for the same string
|
|
for (const std::string bad :
|
|
{"\xC3\x28", "a\xE2\x28\xA1", "\xF0\x28\x8C\xBC", "\xE2\x82", "x\xF0\x9F\x98", "\xFF", "\xED\xA0\x80", "\xC0\xAF"
|
|
})
|
|
{
|
|
CAPTURE(bad)
|
|
const std::string expected = exception_of_call([&]
|
|
{
|
|
const std::string text = json(bad).dump();
|
|
static_cast<void>(text);
|
|
});
|
|
CHECK(!expected.empty());
|
|
CHECK(exception_of_call([&] { d.set(root["z"], bad); }) == expected);
|
|
CHECK(exception_of_call([&] { d.set(root["o"], bad, 1); }) == expected);
|
|
CHECK(exception_of_call([&] { d.set(root["z"], json{{"k", bad}}); }) == expected);
|
|
}
|
|
#endif
|
|
// nothing of the failed edits is visible
|
|
CHECK(root.dump() == R"({"o":{"a":1},"a":[1,2],"n":1,"z":null})");
|
|
static_cast<void>(other);
|
|
}
|
|
|
|
#if !defined(JSON_NOEXCEPTION)
|
|
TEST_CASE("json_view edits: the size of a text arena")
|
|
{
|
|
using nlohmann::detail::view::text_capacity;
|
|
constexpr std::size_t limit = 0xFFFFFFFFu;
|
|
// grows by doubling, or to what is needed (plus some room)
|
|
CHECK(text_capacity(0, 0, 10) == 266);
|
|
CHECK(text_capacity(1000, 990, 20) == 2000);
|
|
CHECK(text_capacity(100, 100, 5000) == 5356);
|
|
// an arena beyond 2 GiB: doubling is clamped to 4 GiB - 1
|
|
CHECK(text_capacity(0x90000000u, 0x8FFFFFFFu, 2) == limit);
|
|
CHECK(text_capacity(limit, limit - 10, 10) == limit);
|
|
// exactly what fits is accepted, without room to spare
|
|
CHECK(text_capacity(100, 90, limit - 90) == limit);
|
|
CHECK(text_capacity(limit - 100, limit - 100, 100) == limit);
|
|
// what does not fit is an error
|
|
CHECK_THROWS_WITH_AS(text_capacity(100, 90, limit - 89), "[json.exception.out_of_range.416] edits of 4 GiB or more are not supported by json_document", json::out_of_range&);
|
|
CHECK_THROWS_WITH_AS(text_capacity(limit, limit, 1), "[json.exception.out_of_range.416] edits of 4 GiB or more are not supported by json_document", json::out_of_range&);
|
|
}
|
|
#endif
|
|
|
|
TEST_CASE("json_view edits: the chunks of new nodes")
|
|
{
|
|
using nlohmann::detail::view::alloc_nodes;
|
|
using nlohmann::detail::view::document_data;
|
|
const std::unique_ptr<document_data, document_data::deleter> d(document_data::create(0));
|
|
// the first chunk has room for 64 nodes, the next one for 128
|
|
const nlohmann::detail::view::node* const first = alloc_nodes(*d, 10);
|
|
CHECK(d->edits->chunks.size() == 1);
|
|
// a request of half of the next chunk (128 nodes) or more gets a chunk of
|
|
// its own
|
|
const nlohmann::detail::view::node* const large = alloc_nodes(*d, 100);
|
|
CHECK(d->edits->chunks.size() == 2);
|
|
CHECK(large == d->edits->chunks.back().get());
|
|
// and the rest of the first chunk is still used
|
|
CHECK(alloc_nodes(*d, 50) == first + 10);
|
|
CHECK(d->edits->chunks.size() == 2);
|
|
// a smaller request that does not fit starts a new (doubled) chunk; the
|
|
// call is a statement of its own, as the operands of == are unsequenced
|
|
// and MSVC reads chunks.back() before the call adds the chunk
|
|
const nlohmann::detail::view::node* const doubled = alloc_nodes(*d, 10);
|
|
CHECK(doubled == d->edits->chunks.back().get());
|
|
CHECK(d->edits->chunks.size() == 3);
|
|
CHECK(d->edits->chunk_end - d->edits->chunk_cur == 128 - 10);
|
|
CHECK(d->edits->bytes == (64 + 100 + 128) * sizeof(nlohmann::detail::view::node));
|
|
}
|
|
|
|
TEST_CASE("json_view edits: replacing arrays and objects by scalars")
|
|
{
|
|
// the first assignment switches the parent to links; the later ones do
|
|
// not need to look for the parent again
|
|
std::string text = "[";
|
|
json expected = json::array();
|
|
for (int i = 0; i < 300; ++i)
|
|
{
|
|
text += (i != 0 ? ",[" : "[") + std::to_string(i) + ",{\"k\":" + std::to_string(i) + "}]";
|
|
expected.push_back(json::array({i, json{{"k", i}}}));
|
|
}
|
|
text += ']';
|
|
json_editable_document d = json_editable_document::parse(text);
|
|
CHECK(d.root().dump() == expected.dump());
|
|
for (int i = 0; i < 300; i += 2)
|
|
{
|
|
d.set(d.root()[static_cast<std::size_t>(i)], i);
|
|
expected[static_cast<std::size_t>(i)] = i;
|
|
}
|
|
CHECK(d.root().dump() == expected.dump());
|
|
for (int i = 1; i < 300; i += 2) // (elements that are still in the parsed layout of their parent)
|
|
{
|
|
d.set(d.root()[static_cast<std::size_t>(i)][1], "x"); // replaces an object
|
|
expected[static_cast<std::size_t>(i)][1] = "x";
|
|
}
|
|
CHECK(d.root().dump() == expected.dump());
|
|
|
|
// new values, and values in new values
|
|
d.push_back(d.root(), json::parse(R"([[1,2],{"a":[3]}])"));
|
|
expected.push_back(json::parse(R"([[1,2],{"a":[3]}])"));
|
|
d.set(d.root()[300][0], 7);
|
|
expected[300][0] = 7;
|
|
d.insert(d.root(), 0, json::array({1, 2}));
|
|
expected.insert(expected.begin(), json::array({1, 2}));
|
|
d.set(d.root()[0], nullptr);
|
|
expected[0] = nullptr;
|
|
d.set(d.root()[301], 5);
|
|
expected[301] = 5;
|
|
CHECK(d.root().dump() == expected.dump());
|
|
CHECK(d.root().materialize() == expected);
|
|
}
|
|
|
|
TEST_CASE("json_view edits: arrays whose elements have no subtrees")
|
|
{
|
|
// such an array is indexed directly while it is in its parsed layout; edits
|
|
// that replace elements by scalars keep it, others move it
|
|
json_editable_document d = json_editable_document::parse("[10,20,30,40]");
|
|
json expected = json::array({10, 20, 30, 40});
|
|
const auto check = [&]
|
|
{
|
|
CHECK(d.root().size() == expected.size());
|
|
for (std::size_t i = 0; i < expected.size(); ++i)
|
|
{
|
|
CHECK(d.root()[i].materialize() == expected[i]);
|
|
CHECK(d.root().at(i).materialize() == expected[i]);
|
|
}
|
|
CHECK(d.root()[expected.size()].is_discarded());
|
|
CHECK(d.root().back().materialize() == expected.back());
|
|
CHECK(d.root().dump() == expected.dump());
|
|
};
|
|
check();
|
|
d.set(d.root()[1], "x");
|
|
expected[1] = "x";
|
|
check();
|
|
d.set(d.root()[3], 2.5);
|
|
expected[3] = 2.5;
|
|
check();
|
|
d.set(d.root()[2], json::array({1, 2}));
|
|
expected[2] = json::array({1, 2});
|
|
check();
|
|
d.push_back(d.root(), 50);
|
|
expected.push_back(50);
|
|
check();
|
|
d.insert(d.root(), 0, true);
|
|
expected.insert(expected.begin(), true);
|
|
check();
|
|
d.erase(d.root(), 1);
|
|
expected.erase(expected.begin() + 1);
|
|
check();
|
|
d.set(d.root()[2], nullptr);
|
|
expected[2] = nullptr;
|
|
check();
|
|
}
|
|
|
|
namespace
|
|
{
|
|
// reads of "a" in an object that repeats it: the first member wins, however the
|
|
// object is stored
|
|
template<typename View>
|
|
void check_first_wins(const View& o, int expected)
|
|
{
|
|
CAPTURE(expected)
|
|
CHECK(o["a"].template get<int>() == expected);
|
|
CHECK(o.at("a").template get<int>() == expected);
|
|
CHECK(o.find("a")->template get<int>() == expected);
|
|
CHECK(o.value("a", -1) == expected);
|
|
CHECK(o[json::json_pointer("/a")].template get<int>() == expected);
|
|
CHECK(o.at(json::json_pointer("/a")).template get<int>() == expected);
|
|
CHECK(o.value(json::json_pointer("/a"), -1) == expected);
|
|
CHECK(o.contains("a"));
|
|
CHECK(o.contains(json::json_pointer("/a")));
|
|
std::size_t members_a = 0;
|
|
for (const auto& item : o.items())
|
|
{
|
|
members_a += std::string(item.key().data(), item.key().size()) == "a" ? 1u : 0u;
|
|
}
|
|
CHECK(o.count("a") == members_a); // every member, unlike the lookups
|
|
}
|
|
} // namespace
|
|
|
|
TEST_CASE("json_view edits: duplicate keys")
|
|
{
|
|
SECTION("an object in its parsed layout, then moved by edits")
|
|
{
|
|
json_editable_document d = json_editable_document::parse(R"({"a": 1, "b": 2, "a": 3})");
|
|
check_first_wins(d.root(), 1);
|
|
CHECK(d.root().size() == 3);
|
|
CHECK(d.root().dump() == R"({"a":1,"b":2,"a":3})");
|
|
CHECK(d.root().materialize()["a"] == 3); // (parse() keeps the last value)
|
|
|
|
// an edit of a value does not move the object
|
|
d.set(d.root()["b"], 5);
|
|
check_first_wins(d.root(), 1);
|
|
d.set(d.root()["a"], 4); // the member that lookups find
|
|
check_first_wins(d.root(), 4);
|
|
CHECK(d.root().dump() == R"({"a":4,"b":5,"a":3})");
|
|
|
|
// an appended member moves it
|
|
d.set(d.root(), "c", true);
|
|
check_first_wins(d.root(), 4);
|
|
CHECK(d.root().size() == 4);
|
|
CHECK(d.root().dump() == R"({"a":4,"b":5,"a":3,"c":true})");
|
|
d.set(d.root()["a"], 6);
|
|
check_first_wins(d.root(), 6);
|
|
CHECK(d.root().dump() == R"({"a":6,"b":5,"a":3,"c":true})");
|
|
d.set(json::json_pointer("/a"), 7);
|
|
check_first_wins(d.root(), 7);
|
|
}
|
|
|
|
SECTION("set assigns the member that lookups find")
|
|
{
|
|
// the key stays where its first occurrence is, the later members are dropped
|
|
ordered_json_editable_document d = ordered_json_editable_document::parse(R"({"a": 1, "b": 2, "a": 3, "c": 4, "a": 5})");
|
|
const ordered_json_editable_view held = d.root()["a"];
|
|
CHECK(held.get<int>() == 1);
|
|
const ordered_json_editable_view assigned = d.set(d.root(), "a", "x");
|
|
CHECK(held.get<std::string>() == "x");
|
|
CHECK(assigned.get<std::string>() == "x");
|
|
CHECK(d.root().dump() == R"({"a":"x","b":2,"c":4})");
|
|
CHECK(d.root().materialize() == ordered_json::parse(R"({"a": "x", "b": 2, "c": 4})"));
|
|
CHECK(d.root().size() == 3);
|
|
|
|
// as for the object that parse() makes of the text
|
|
ordered_json j = ordered_json::parse(R"({"a": 1, "b": 2, "a": 3, "c": 4, "a": 5})");
|
|
j["a"] = "x";
|
|
CHECK(d.root().materialize().dump() == j.dump());
|
|
|
|
// through a pointer, below a duplicate
|
|
d = ordered_json_editable_document::parse(R"({"a": {"x": 1}, "b": 2, "a": {"x": 3}})");
|
|
d.set(ptr_t("/a/x"), 4);
|
|
CHECK(d.root().dump() == R"({"a":{"x":4},"b":2,"a":{"x":3}})");
|
|
d.set(ptr_t("/a"), 0);
|
|
CHECK(d.root().dump() == R"({"a":0,"b":2})");
|
|
}
|
|
|
|
SECTION("erase removes every member")
|
|
{
|
|
json_editable_document d = json_editable_document::parse(R"({"a": 1, "b": 2, "a": 3})");
|
|
d.set(d.root(), "c", 4);
|
|
check_first_wins(d.root(), 1);
|
|
CHECK(d.erase(d.root(), "a") == 2);
|
|
CHECK(d.root().dump() == R"({"b":2,"c":4})");
|
|
CHECK(!d.root().contains("a"));
|
|
CHECK(d.root()["a"].is_discarded());
|
|
CHECK(d.erase(d.root(), "a") == 0);
|
|
d = json_editable_document::parse(R"({"a": 1, "b": 2, "a": 3})");
|
|
CHECK(d.erase(json::json_pointer("/a")) == 2);
|
|
CHECK(d.root().dump() == R"({"b":2})");
|
|
}
|
|
|
|
SECTION("a large object with an index")
|
|
{
|
|
std::string text = R"({"a":0,"k7":"first")";
|
|
for (int i = 0; i < 200; ++i)
|
|
{
|
|
text += ",\"k" + std::to_string(i) + "\":" + std::to_string(i);
|
|
}
|
|
text += R"(,"a":1,"k7":"last","a":2})";
|
|
json_editable_document d = json_editable_document::parse(text);
|
|
check_first_wins(d.root(), 0);
|
|
CHECK(d.root()["k7"].get_string() == "first");
|
|
CHECK(d.root()["k199"].get<int>() == 199);
|
|
|
|
// a value assigned in place: the index stays in use
|
|
d.set(d.root()["a"], 3);
|
|
check_first_wins(d.root(), 3);
|
|
d.set(d.root()["k7"], "changed");
|
|
CHECK(d.root()["k7"].get_string() == "changed");
|
|
CHECK(d.root()["k199"].get<int>() == 199);
|
|
|
|
// an appended member moves the members: the lookup scans them
|
|
d.set(d.root(), "new", 1);
|
|
check_first_wins(d.root(), 3);
|
|
CHECK(d.root()["k7"].get_string() == "changed");
|
|
CHECK(d.root()["k7"].get_string() == d.root().at("k7").get_string());
|
|
CHECK(d.root()["k199"].get<int>() == 199);
|
|
CHECK(d.root()["new"].get<int>() == 1);
|
|
|
|
// a value replaced by a container, in an object that was not moved
|
|
d = json_editable_document::parse(text);
|
|
d.set(d.root()["a"], json{{"x", 1}});
|
|
CHECK(d.root()["a"]["x"].get<int>() == 1);
|
|
CHECK(d.root()["k7"].get_string() == "first");
|
|
CHECK(d.erase(d.root(), "k7") == 3); // (the key occurs three times)
|
|
CHECK(d.root()["k7"].is_discarded());
|
|
CHECK(d.root()["a"]["x"].get<int>() == 1);
|
|
}
|
|
|
|
SECTION("objects in moved arrays and in new values")
|
|
{
|
|
json_editable_document d = json_editable_document::parse(R"([{"a": 1, "a": 2}, {"a": 3, "b": 0, "a": 4}])");
|
|
check_first_wins(d.root()[0], 1);
|
|
check_first_wins(d.root()[1], 3);
|
|
d.insert(d.root(), 0, json::parse(R"({"a": 0})"));
|
|
d.push_back(d.root(), json::parse(R"({"a": 5, "a": 6})")); // (a basic_json value has one member)
|
|
check_first_wins(d.root()[1], 1);
|
|
check_first_wins(d.root()[2], 3);
|
|
CHECK(d.root()[3]["a"].get<int>() == 6);
|
|
d.set(d.root()[1], "a", 8);
|
|
check_first_wins(d.root()[1], 8);
|
|
CHECK(d.root()[1].dump() == R"({"a":8})");
|
|
d.erase(d.root(), 0);
|
|
check_first_wins(d.root()[1], 3);
|
|
CHECK(d.root().dump() == R"([{"a":8},{"a":3,"b":0,"a":4},{"a":6}])");
|
|
}
|
|
|
|
SECTION("values of other documents")
|
|
{
|
|
const json_document source = json_document::parse(R"({"list": [{"a": 1, "a": 2}], "o": {"b": {"a": 3, "a": 4}, "a": 5, "a": 6}})");
|
|
json_editable_document d = json_editable_document::parse("{}");
|
|
d.set(d.root(), "copy", source.root()["list"]);
|
|
d.set(d.root(), "o", source.root()["o"]);
|
|
// (the copies keep the repeated members)
|
|
CHECK(d.root().dump() == R"({"copy":[{"a":1,"a":2}],"o":{"b":{"a":3,"a":4},"a":5,"a":6}})");
|
|
check_first_wins(d.root()["copy"][0], 1);
|
|
check_first_wins(d.root()["o"]["b"], 3);
|
|
check_first_wins(d.root()["o"], 5);
|
|
d.set(d.root()["o"]["b"], "c", 0);
|
|
check_first_wins(d.root()["o"]["b"], 3);
|
|
d.set(d.root()["o"], "d", 0);
|
|
check_first_wins(d.root()["o"], 5);
|
|
CHECK(d.root()["o"]["b"]["a"].get<int>() == 3);
|
|
CHECK(d.root()["o"]["d"].get<int>() == 0);
|
|
}
|
|
}
|