mirror of
https://github.com/nlohmann/json.git
synced 2026-10-12 09:27:13 +00:00
An editable document reads the last member of a repeated key (as the read-only document does), but set(object, key, value) assigned the first one: a view taken from object[key] before the call did not show the new value. It now assigns the last member's value, keeps the key at the position of its first occurrence (where materialize() puts it), and drops the other members. The seeded differential test now also edits documents whose objects repeat keys and compares every lookup (operator[], at, find, value, contains, count, and JSON pointers) with the parsed basic_json value. A targeted test covers duplicates before and after edits that move the object, in large objects with an index, in moved arrays, and in values copied from other documents. Signed-off-by: Niels Lohmann <mail@nlohmann.me>
1063 lines
41 KiB
C++
1063 lines
41 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 <array>
|
|
#include <cmath>
|
|
#include <cstring>
|
|
#include <cstdint>
|
|
#include <functional>
|
|
#include <iterator>
|
|
#include <limits>
|
|
#include <random>
|
|
#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: before their
|
|
// members (the real member is then the last), or after (the repeat is)
|
|
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 = "{";
|
|
if (!keys.empty() && r(4) == 0)
|
|
{
|
|
text += repeated() + ",";
|
|
}
|
|
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();
|
|
}
|
|
|
|
// every lookup of the edited view finds what j holds, although the view may
|
|
// have several members for a key (j has the last value, at the position of the
|
|
// first member: what parse() and materialize() make of it)
|
|
void check_lookups(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)
|
|
const ordered_json_editable_view m = v[key];
|
|
REQUIRE(!m.is_discarded());
|
|
CHECK(m.materialize() == kv.value());
|
|
CHECK(v.at(key).materialize() == kv.value());
|
|
CHECK(v[ptr].materialize() == kv.value());
|
|
CHECK(v.at(ptr).materialize() == kv.value());
|
|
CHECK(v.contains(key));
|
|
CHECK(v.contains(ptr));
|
|
CHECK(v.count(key) == 1);
|
|
const auto it = v.find(key);
|
|
REQUIRE(it != v.end());
|
|
CHECK((*it).materialize() == kv.value());
|
|
CHECK(v.value(ptr, ordered_json(nullptr)) == kv.value());
|
|
if (kv.value().is_string())
|
|
{
|
|
CHECK(v.value(key, std::string("-")) == kv.value().get<std::string>());
|
|
}
|
|
check_lookups(m, kv.value());
|
|
}
|
|
CHECK(v["missing#key"].is_discarded());
|
|
CHECK(!v.contains("missing#key"));
|
|
CHECK(v.find("missing#key") == v.end());
|
|
}
|
|
else if (j.is_array())
|
|
{
|
|
for (std::size_t i = 0; i < j.size(); ++i)
|
|
{
|
|
check_lookups(v[i], j[i]);
|
|
check_lookups(v.at(i), j[i]);
|
|
}
|
|
}
|
|
}
|
|
|
|
// (for documents whose objects repeat keys: dump(), size(), and iteration
|
|
// list all members, so only the lookups and materialize() are compared)
|
|
void check_duplicates(const ordered_json_editable_document& d, const ordered_json& j)
|
|
{
|
|
CHECK(d.root().materialize() == j);
|
|
check_lookups(d.root(), j);
|
|
}
|
|
|
|
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
|
|
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)}});
|
|
}
|
|
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);
|
|
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);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
namespace
|
|
{
|
|
#if !defined(JSON_NOEXCEPTION)
|
|
// the exception a call throws, or "" if it throws none
|
|
std::string exception_of_call(const std::function<void()>& f)
|
|
{
|
|
try
|
|
{
|
|
f();
|
|
}
|
|
catch (const json::exception& e)
|
|
{
|
|
return e.what();
|
|
}
|
|
return "";
|
|
}
|
|
#endif
|
|
} // namespace
|
|
|
|
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);
|
|
}
|
|
|
|
TEST_CASE("json_view edits: views and values")
|
|
{
|
|
SECTION("a value that is no longer part of the document")
|
|
{
|
|
json_editable_document d = json_editable_document::parse("[[[1,2]]]");
|
|
const json_editable_view inner = d.root()[0][0];
|
|
d.set(d.root()[0], json::array({7}));
|
|
d.set(inner, 5);
|
|
CHECK(d.root().dump() == "[[7]]");
|
|
CHECK(inner.get<int>() == 5);
|
|
}
|
|
|
|
SECTION("views keep referring to their value")
|
|
{
|
|
json_editable_document d = json_editable_document::parse(R"({"a": [10, 20, 30], "b": {"c": "text"}})");
|
|
const json_editable_view a = d.root()["a"];
|
|
const json_editable_view twenty = a[1];
|
|
const json_editable_view c = d.root()["b"]["c"];
|
|
d.insert(a, 0, 5);
|
|
d.push_back(a, 40);
|
|
CHECK(twenty.get<int>() == 20);
|
|
CHECK(a[2].get<int>() == 20);
|
|
d.erase(a, 2);
|
|
CHECK(twenty.get<int>() == 20); // an erased value keeps its last value
|
|
d.set(c, 7);
|
|
CHECK(c.get<int>() == 7); // a held view sees an assignment
|
|
d.set(d.root()["b"], json::array({1, 2}));
|
|
CHECK(d.root()["b"].dump() == "[1,2]");
|
|
CHECK(d.root().dump() == R"({"a":[5,10,30,40],"b":[1,2]})");
|
|
CHECK(d.root()["a"][0].source_offset() == static_cast<std::size_t>(-1)); // a new value
|
|
CHECK(d.root()["a"][1].source_offset() != static_cast<std::size_t>(-1));
|
|
}
|
|
|
|
SECTION("strings stay valid while more edits come")
|
|
{
|
|
json_editable_document d = json_editable_document::parse("[]");
|
|
const auto first = d.push_back(d.root(), std::string(100, 'x')).get_string();
|
|
for (int i = 0; i < 1000; ++i)
|
|
{
|
|
d.push_back(d.root(), std::string(static_cast<std::size_t>(i % 50), 'y'));
|
|
}
|
|
CHECK(std::string(first.data(), first.size()) == std::string(100, 'x'));
|
|
CHECK(d.root().size() == 1001);
|
|
}
|
|
|
|
SECTION("numbers")
|
|
{
|
|
json_editable_document d = json_editable_document::parse(R"([1.50, 1E2, 3])");
|
|
d.set(d.root(), 2, 0.1);
|
|
d.push_back(d.root(), std::numeric_limits<double>::quiet_NaN());
|
|
d.push_back(d.root(), -std::numeric_limits<double>::infinity());
|
|
d.push_back(d.root(), (std::numeric_limits<std::uint64_t>::max)());
|
|
d.push_back(d.root(), (std::numeric_limits<std::int64_t>::min)());
|
|
CHECK(d.root().dump() == "[1.5,100.0,0.1,null,null,18446744073709551615,-9223372036854775808]");
|
|
CHECK(d.root().dump(-1, ' ', false, json_editable_view::number_format::source) == "[1.50,1E2,0.1,null,null,18446744073709551615,-9223372036854775808]");
|
|
CHECK(std::isnan(d.root()[3].get<double>()));
|
|
CHECK(std::isinf(d.root()[4].get<double>()));
|
|
CHECK(d.root()[2].number_token() == "0.1");
|
|
CHECK(d.root()[5].get<std::uint64_t>() == 18446744073709551615u);
|
|
CHECK(d.root()[6].number_token() == "-9223372036854775808");
|
|
CHECK(d.root().materialize().dump() == json::parse(R"([1.5, 100.0, 0.1, null, null, 18446744073709551615, -9223372036854775808])").dump());
|
|
}
|
|
|
|
SECTION("nulls become containers, and the root can be replaced")
|
|
{
|
|
json_editable_document d = json_editable_document::parse("[null, null]");
|
|
d.set(d.root()[0], "k", 1);
|
|
d.push_back(d.root()[1], true);
|
|
CHECK(d.root().dump() == R"([{"k":1},[true]])");
|
|
d.set(d.root(), "scalar");
|
|
CHECK(d.root().dump() == R"("scalar")");
|
|
d.set(d.root(), json{{"x", {1, 2}}});
|
|
d.set(json::json_pointer("/x/-"), 3);
|
|
d.set(json::json_pointer("/x/3"), 4); // the size of the array appends too
|
|
d.set(json::json_pointer("/y"), false);
|
|
CHECK(d.root().dump() == R"({"x":[1,2,3,4],"y":false})");
|
|
CHECK(d.erase(json::json_pointer("/x/0")) == 1);
|
|
CHECK(d.erase(json::json_pointer("/y")) == 1);
|
|
CHECK(d.erase(json::json_pointer("/nothing")) == 0);
|
|
CHECK(d.root().dump() == R"({"x":[2,3,4]})");
|
|
}
|
|
|
|
SECTION("duplicate keys")
|
|
{
|
|
json_editable_document d = json_editable_document::parse(R"({"a": 1, "b": 2, "a": 3})");
|
|
d.set(d.root(), "a", 4); // the last member is assigned (at the position of the first), the others dropped
|
|
CHECK(d.root().dump() == R"({"a":4,"b":2})");
|
|
d = json_editable_document::parse(R"({"a": 1, "b": 2, "a": 3})");
|
|
CHECK(d.erase(d.root(), "a") == 2);
|
|
CHECK(d.root().dump() == R"({"b":2})");
|
|
}
|
|
|
|
SECTION("values from other documents")
|
|
{
|
|
const json_document source = json_document::parse(R"({"list": [1, "two", {"three": 3.5}], "text": "a\nb"})");
|
|
json_editable_document edited = json_editable_document::parse("[0]");
|
|
edited.set(edited.root(), 0, json{{"inner", {1, 2}}});
|
|
json_editable_document d = json_editable_document::parse("{}");
|
|
d.set(d.root(), "copy", source.root()["list"]);
|
|
d.set(d.root(), "text", source.root()["text"]);
|
|
d.set(d.root(), "edited", edited.root()[0]);
|
|
d.set(d.root(), "self", d.root()["copy"]);
|
|
CHECK(d.root().dump() == R"({"copy":[1,"two",{"three":3.5}],"text":"a\nb","edited":{"inner":[1,2]},"self":[1,"two",{"three":3.5}]})");
|
|
CHECK(d.root()["copy"] == source.root()["list"]);
|
|
CHECK(source.root()["list"] == d.root()["self"]);
|
|
CHECK(d.root() != source.root());
|
|
}
|
|
|
|
SECTION("large objects")
|
|
{
|
|
std::string text = "{";
|
|
for (int i = 0; i < 200; ++i)
|
|
{
|
|
text += (i != 0 ? ",\"k" : "\"k") + std::to_string(i) + "\":" + std::to_string(i);
|
|
}
|
|
text += '}';
|
|
json_editable_document d = json_editable_document::parse(text);
|
|
d.set(d.root(), "k7", "seven"); // assigned in place: the index stays in use
|
|
CHECK(d.root()["k7"].get_string() == "seven");
|
|
d.set(d.root(), "new", 1); // appended: the members move, the lookup is linear
|
|
CHECK(d.root()["new"].get<int>() == 1);
|
|
CHECK(d.root()["k199"].get<int>() == 199);
|
|
d.erase(d.root(), "k0");
|
|
CHECK(!d.root().contains("k0"));
|
|
CHECK(d.root().size() == 200);
|
|
}
|
|
|
|
SECTION("reuse and memory")
|
|
{
|
|
json_editable_document d = json_editable_document::parse("[1, 2, 3]");
|
|
const std::size_t before = d.memory_usage();
|
|
for (int i = 0; i < 100; ++i)
|
|
{
|
|
d.push_back(d.root(), "some text");
|
|
}
|
|
CHECK(d.memory_usage() > before);
|
|
const json_editable_view first = d.root()[0];
|
|
d.shrink_to_fit(); // (with edits, the index stays in place)
|
|
CHECK(first.get<int>() == 1);
|
|
d.read(std::string("[true]"));
|
|
CHECK(d.root().dump() == "[true]");
|
|
d.push_back(d.root(), false);
|
|
CHECK(d.root().dump() == "[true,false]");
|
|
}
|
|
}
|
|
|
|
TEST_CASE("json_view edits: deeply nested values")
|
|
{
|
|
// copying a value into a document must not recurse per nesting level
|
|
const std::size_t depth = 100000;
|
|
const std::string brackets = std::string(depth, '[') + std::string(depth, ']');
|
|
std::string braces;
|
|
for (std::size_t i = 0; i < depth; ++i)
|
|
{
|
|
braces += "{\"a\":";
|
|
}
|
|
braces += '1';
|
|
braces += std::string(depth, '}');
|
|
|
|
SECTION("a view of a read-only document")
|
|
{
|
|
const json_document source = json_document::parse(brackets);
|
|
json_editable_document d = json_editable_document::parse("[]");
|
|
d.push_back(d.root(), source.root());
|
|
CHECK(d.root().dump() == "[" + brackets + "]");
|
|
}
|
|
|
|
SECTION("a view of an editable document")
|
|
{
|
|
const json_editable_document source = json_editable_document::parse(braces);
|
|
json_editable_document d = json_editable_document::parse("{}");
|
|
d.set(d.root(), "deep", source.root());
|
|
CHECK(d.root().dump() == "{\"deep\":" + braces + "}");
|
|
}
|
|
|
|
SECTION("a view of an edited document (values behind links)")
|
|
{
|
|
const json_document source = json_document::parse(brackets);
|
|
json_editable_document edited = json_editable_document::parse("[[]]");
|
|
edited.push_back(edited.root()[0], source.root());
|
|
edited.push_back(edited.root(), source.root());
|
|
json_editable_document d = json_editable_document::parse("null");
|
|
d.set(d.root(), edited.root());
|
|
CHECK(d.root().dump() == "[[" + brackets + "]," + brackets + "]");
|
|
}
|
|
|
|
SECTION("a basic_json value")
|
|
{
|
|
json deep = json::array();
|
|
json* inner = &deep;
|
|
for (std::size_t i = 1; i < depth; ++i)
|
|
{
|
|
inner->push_back(json::array());
|
|
inner = &inner->back();
|
|
}
|
|
json_editable_document d = json_editable_document::parse("[]");
|
|
d.push_back(d.root(), deep);
|
|
CHECK(d.root().dump() == "[" + brackets + "]");
|
|
}
|
|
|
|
SECTION("a basic_json value with objects")
|
|
{
|
|
json deep = 1;
|
|
for (std::size_t i = 0; i < depth; ++i)
|
|
{
|
|
json outer = json::object();
|
|
outer["a"] = std::move(deep);
|
|
deep = std::move(outer);
|
|
}
|
|
json_editable_document d = json_editable_document::parse("{}");
|
|
d.set(d.root(), "deep", deep);
|
|
CHECK(d.root().dump() == "{\"deep\":" + braces + "}");
|
|
}
|
|
}
|
|
|
|
#if !defined(JSON_NOEXCEPTION)
|
|
TEST_CASE("json_view edits: pointers below a null value")
|
|
{
|
|
// a null value on the way becomes what basic_json makes of it: an array
|
|
// for "-" and for digits, an object otherwise
|
|
struct test_case
|
|
{
|
|
const char* document;
|
|
const char* pointer;
|
|
};
|
|
const std::array<test_case, 16> cases =
|
|
{
|
|
{
|
|
{R"({"a":null})", "/a/0"},
|
|
{R"({"a":null})", "/a/-"},
|
|
{R"({"a":null})", "/a/3"},
|
|
{R"({"a":null})", "/a/x"},
|
|
{R"({"a":null})", "/a/+1"},
|
|
{R"({"a":null})", "/a/01"},
|
|
{R"({"a":null})", "/a/"},
|
|
{R"({"a":{"b":null}})", "/a/b/1"},
|
|
{R"({"a":{"b":null}})", "/a/b/-"},
|
|
{R"({"a":[null]})", "/a/0/0"},
|
|
{R"({"a":[null,null]})", "/a/1/k"},
|
|
{R"([null])", "/0"},
|
|
{"null", "/0"},
|
|
{"null", "/-"},
|
|
{"null", "/k"},
|
|
{"null", ""},
|
|
}
|
|
};
|
|
for (const test_case& c : cases)
|
|
{
|
|
CAPTURE(c.document)
|
|
CAPTURE(c.pointer)
|
|
json expected = json::parse(c.document);
|
|
const std::string error = exception_of_call([&]
|
|
{
|
|
expected[json::json_pointer(c.pointer)] = 1;
|
|
});
|
|
json_editable_document d = json_editable_document::parse(c.document);
|
|
if (error.empty())
|
|
{
|
|
d.set(json::json_pointer(c.pointer), 1);
|
|
CHECK(d.root().dump() == expected.dump());
|
|
CHECK(d.root().materialize() == expected);
|
|
}
|
|
else
|
|
{
|
|
// the same error, and the document is not changed
|
|
CHECK(exception_of_call([&] { d.set(json::json_pointer(c.pointer), 1); }) == error);
|
|
CHECK(d.root().dump() == json::parse(c.document).dump());
|
|
}
|
|
}
|
|
}
|
|
|
|
TEST_CASE("json_view edits: strings of other documents are checked")
|
|
{
|
|
// A document borrows the text it was parsed from, and sees later changes
|
|
// of the text: a way to get ill-formed UTF-8 into a view. Copying it into
|
|
// an editable document is an error, as for any other string.
|
|
std::string text = R"({"key":"abc","list":["abc"]})";
|
|
const json_document source = json_document::parse(text);
|
|
const auto message_of = [](const std::string & bad)
|
|
{
|
|
return exception_of_call([&]
|
|
{
|
|
const std::string dumped = json(bad).dump();
|
|
static_cast<void>(dumped);
|
|
});
|
|
};
|
|
json_editable_document d = json_editable_document::parse("[1]");
|
|
d.push_back(d.root(), source.root());
|
|
CHECK(d.root().dump() == R"([1,{"key":"abc","list":["abc"]}])");
|
|
|
|
text[text.find("abc") + 1] = '\xC3'; // "a\xC3c"
|
|
text[text.rfind("abc") + 1] = '\xC3';
|
|
const std::string bad_value = message_of(std::string("a\xC3" "c"));
|
|
CHECK(!bad_value.empty());
|
|
CHECK(exception_of_call([&] { d.push_back(d.root(), source.root()["key"]); }) == bad_value);
|
|
CHECK(exception_of_call([&] { d.set(d.root()[0], source.root()["list"][0]); }) == bad_value);
|
|
CHECK(exception_of_call([&] { d.set(d.root(), 0, source.root()["list"]); }) == bad_value);
|
|
CHECK(exception_of_call([&] { d.set(d.root(), 0, source.root()); }) == bad_value);
|
|
|
|
text[text.find("key") + 1] = '\xC3'; // a key is checked as well
|
|
const std::string bad_key = message_of(std::string("k\xC3" "y"));
|
|
CHECK(exception_of_call([&] { d.set(d.root(), 0, source.root()); }) == bad_key);
|
|
CHECK(exception_of_call([&] { d.push_back(d.root(), source.root()); }) == bad_key);
|
|
|
|
// nothing of the failed edits is visible
|
|
CHECK(d.root().dump() == R"([1,{"key":"abc","list":["abc"]}])");
|
|
}
|
|
|
|
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: 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);
|
|
}
|
|
|
|
namespace
|
|
{
|
|
// reads of "a" in an object that repeats it: the last member wins, however the
|
|
// object is stored
|
|
template<typename View>
|
|
void check_last_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")));
|
|
CHECK(o.count("a") == 1);
|
|
}
|
|
} // 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_last_wins(d.root(), 3);
|
|
CHECK(d.root().size() == 3);
|
|
CHECK(d.root().dump() == R"({"a":1,"b":2,"a":3})");
|
|
|
|
// an edit of a value does not move the object
|
|
d.set(d.root()["b"], 5);
|
|
check_last_wins(d.root(), 3);
|
|
d.set(d.root()["a"], 4); // the member that reads find
|
|
check_last_wins(d.root(), 4);
|
|
CHECK(d.root().dump() == R"({"a":1,"b":5,"a":4})");
|
|
|
|
// an appended member moves it
|
|
d.set(d.root(), "c", true);
|
|
check_last_wins(d.root(), 4);
|
|
CHECK(d.root().size() == 4);
|
|
CHECK(d.root().dump() == R"({"a":1,"b":5,"a":4,"c":true})");
|
|
d.set(d.root()["a"], 6);
|
|
check_last_wins(d.root(), 6);
|
|
CHECK(d.root().dump() == R"({"a":1,"b":5,"a":6,"c":true})");
|
|
d.set(json::json_pointer("/a"), 7);
|
|
check_last_wins(d.root(), 7);
|
|
}
|
|
|
|
SECTION("set assigns the member that reads find")
|
|
{
|
|
// the key keeps the position of its first occurrence (as in materialize()), 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>() == 5);
|
|
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":1},"b":2,"a":{"x":4}})");
|
|
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_last_wins(d.root(), 3);
|
|
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_last_wins(d.root(), 2);
|
|
CHECK(d.root()["k7"].get_string() == "last");
|
|
CHECK(d.root()["k199"].get<int>() == 199);
|
|
|
|
// a value assigned in place: the index stays in use
|
|
d.set(d.root()["a"], 3);
|
|
check_last_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_last_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() == "last");
|
|
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_last_wins(d.root()[0], 2);
|
|
check_last_wins(d.root()[1], 4);
|
|
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_last_wins(d.root()[1], 2);
|
|
check_last_wins(d.root()[2], 4);
|
|
CHECK(d.root()[3]["a"].get<int>() == 6);
|
|
d.set(d.root()[1], "a", 8);
|
|
check_last_wins(d.root()[1], 8);
|
|
CHECK(d.root()[1].dump() == R"({"a":8})");
|
|
d.erase(d.root(), 0);
|
|
check_last_wins(d.root()[1], 4);
|
|
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_last_wins(d.root()["copy"][0], 2);
|
|
check_last_wins(d.root()["o"]["b"], 4);
|
|
check_last_wins(d.root()["o"], 6);
|
|
d.set(d.root()["o"]["b"], "c", 0);
|
|
check_last_wins(d.root()["o"]["b"], 4);
|
|
d.set(d.root()["o"], "d", 0);
|
|
check_last_wins(d.root()["o"], 6);
|
|
CHECK(d.root()["o"]["b"]["a"].get<int>() == 4);
|
|
CHECK(d.root()["o"]["d"].get<int>() == 0);
|
|
}
|
|
}
|