* 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>
944 lines
36 KiB
C++
944 lines
36 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();
|
|
}
|
|
|
|
TEST_CASE("json_view edits: materializing values nested deeper than the document was parsed")
|
|
{
|
|
// the stack of open containers of materialize() is sized from the nesting of the parsed text;
|
|
// a value added by an edit can be deeper than that (and than the stack that is local to the call)
|
|
json_editable_document d = json_editable_document::parse("[1,[2]]");
|
|
json expected = json::array({1, json::array({2})});
|
|
json nested = 3;
|
|
for (int level = 0; level < 40; ++level)
|
|
{
|
|
nested = json::array({nested, json{{"k", level}}});
|
|
}
|
|
d.push_back(d.root(), nested);
|
|
expected.push_back(nested);
|
|
CHECK(d.root().materialize() == expected);
|
|
CHECK(d.root()[2].materialize() == nested);
|
|
d.set(d.root()[1][0], nested);
|
|
expected[1][0] = nested;
|
|
CHECK(d.root().materialize() == expected);
|
|
CHECK(d.root().get<json>() == expected);
|
|
}
|
|
|
|
TEST_CASE("json_view edits: an editable document before and after its first edit")
|
|
{
|
|
// until the first edit, an editable document is read like a read-only one; views made before the edit
|
|
// keep working after it
|
|
const std::string text = R"({"a":[1,2,{"x":"y"},4],"b":{"c":true,"d":null},"e":"text","f":[]})";
|
|
const json_document read_only = json_document::parse(text);
|
|
json_editable_document d = json_editable_document::parse(text);
|
|
const auto root = d.root();
|
|
const auto same_as_read_only = [&]
|
|
{
|
|
CHECK(root.dump() == read_only.root().dump());
|
|
CHECK(root.dump(2) == read_only.root().dump(2));
|
|
CHECK(root.materialize() == read_only.root().materialize());
|
|
CHECK(root["a"][2]["x"].get_string() == "y");
|
|
CHECK(root["a"].at(3).get<int>() == 4);
|
|
CHECK(root["a"].back().get<int>() == 4);
|
|
CHECK(root.contains("b"));
|
|
CHECK(!root.contains("zz"));
|
|
CHECK(root.find("e")->get_string() == "text");
|
|
CHECK(root.find("zz") == root.end());
|
|
CHECK(root["b"]["c"].get<bool>());
|
|
CHECK(root.value("e", "none") == "text");
|
|
std::size_t count = 0;
|
|
for (const auto m : root["a"])
|
|
{
|
|
CHECK(m.materialize() == read_only.root()["a"][count].materialize());
|
|
++count;
|
|
}
|
|
CHECK(count == 4);
|
|
};
|
|
same_as_read_only();
|
|
// an edit that changes a scalar in place, then one that moves an array
|
|
d.set(root["e"], "other");
|
|
CHECK(root["e"].get_string() == "other");
|
|
CHECK(root["a"][2]["x"].get_string() == "y");
|
|
CHECK(root["a"].back().get<int>() == 4);
|
|
d.push_back(root["f"], 7);
|
|
CHECK(root["f"][0].get<int>() == 7);
|
|
CHECK(root["f"].back().get<int>() == 7);
|
|
CHECK(root.dump() == R"({"a":[1,2,{"x":"y"},4],"b":{"c":true,"d":null},"e":"other","f":[7]})");
|
|
d.set(root["a"], 2, json::array({1, 2}));
|
|
CHECK(root["a"][2][1].get<int>() == 2);
|
|
CHECK(root["a"].dump() == "[1,2,[1,2],4]");
|
|
CHECK(root.materialize() == json::parse(R"({"a":[1,2,[1,2],4],"b":{"c":true,"d":null},"e":"other","f":[7]})"));
|
|
}
|
|
|
|
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);
|
|
}
|
|
}
|