Add insert() and erase() to editable documents
- insert(array, index, value): insert before an element (index <= size) - erase(object, key): remove all members with the key; returns their number - erase(array, index): remove an element - erase(json_pointer): remove the member or element a pointer names The errors are those of basic_json (type_error.307/309, out_of_range.401/403/405). A view of an erased value keeps its last value, and views of other values keep referring to them when elements move. Tests: the differential test now also inserts and erases members and elements, directly and through JSON pointers; plus the errors, views across inserts and erasures, duplicate keys, and large objects. Signed-off-by: Niels Lohmann <mail@nlohmann.me>
This commit is contained in:
4 files changed
+261
-1
No files matched your search
@@ -227,6 +227,62 @@ class editor
|
|||||||
return View(&m_doc, slot);
|
return View(&m_doc, slot);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// insert into an array before position idx (idx <= size()); returns a
|
||||||
|
/// view of the new element
|
||||||
|
template<typename V>
|
||||||
|
View insert(const View& array, std::size_t idx, V&& value)
|
||||||
|
{
|
||||||
|
node* const a = own(array);
|
||||||
|
if (a->kind != static_cast<std::uint8_t>(value_t::array))
|
||||||
|
{
|
||||||
|
throw_type_error(309, "cannot use insert() with ", array.type_name());
|
||||||
|
}
|
||||||
|
check_index(idx, a->len + 1);
|
||||||
|
const encoded e = encode(std::forward<V>(value));
|
||||||
|
node* const slot = new_slot(e);
|
||||||
|
node* const h = block_of(m_doc, a, 1);
|
||||||
|
std::memmove(h + 2 + idx, h + 1 + idx, (h->next - 1 - idx) * sizeof(node));
|
||||||
|
make_link(h[1 + idx], slot);
|
||||||
|
++h->next;
|
||||||
|
++h->len;
|
||||||
|
++a->len;
|
||||||
|
return View(&m_doc, slot);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// remove all members with this key; returns their number
|
||||||
|
std::size_t erase(const View& object, string_view_t key)
|
||||||
|
{
|
||||||
|
node* const o = own(object);
|
||||||
|
if (o->kind != static_cast<std::uint8_t>(value_t::object))
|
||||||
|
{
|
||||||
|
throw_type_error(307, "cannot use erase() with ", object.type_name());
|
||||||
|
}
|
||||||
|
for (const node* k = nav::first(m_doc, o), *end = nav::end(m_doc, o); k != end; k = document_data::after(k + 1))
|
||||||
|
{
|
||||||
|
if (key_equals(*k, key))
|
||||||
|
{
|
||||||
|
return erase_members(o, key, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// remove an array element
|
||||||
|
void erase(const View& array, std::size_t idx)
|
||||||
|
{
|
||||||
|
node* const a = own(array);
|
||||||
|
if (a->kind != static_cast<std::uint8_t>(value_t::array))
|
||||||
|
{
|
||||||
|
throw_type_error(307, "cannot use erase() with ", array.type_name());
|
||||||
|
}
|
||||||
|
check_index(idx, a->len);
|
||||||
|
node* const h = block_of(m_doc, a, 0);
|
||||||
|
std::memmove(h + 1 + idx, h + 2 + idx, (h->next - 2 - idx) * sizeof(node));
|
||||||
|
--h->next;
|
||||||
|
--h->len;
|
||||||
|
--a->len;
|
||||||
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
/// an encoded value: a scalar node, or the root of a new array/object
|
/// an encoded value: a scalar node, or the root of a new array/object
|
||||||
struct encoded
|
struct encoded
|
||||||
|
|||||||
@@ -1061,6 +1061,45 @@ class basic_json_document
|
|||||||
return editor().push_back(array, std::forward<V>(value));
|
return editor().push_back(array, std::forward<V>(value));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// insert into an array before position idx (idx <= size()); returns a
|
||||||
|
/// view of the new element
|
||||||
|
template < typename I, typename V, typename std::enable_if < std::is_integral<I>::value && !std::is_same<I, bool>::value, int >::type = 0 >
|
||||||
|
view_type insert(view_type array, I idx, V && value)
|
||||||
|
{
|
||||||
|
return editor().insert(array, index(idx), std::forward<V>(value));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// remove all members with this key; returns their number
|
||||||
|
std::size_t erase(view_type object, string_view_t key)
|
||||||
|
{
|
||||||
|
return editor().erase(object, key);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// remove an array element
|
||||||
|
template < typename I, typename std::enable_if < std::is_integral<I>::value && !std::is_same<I, bool>::value, int >::type = 0 >
|
||||||
|
void erase(view_type array, I idx)
|
||||||
|
{
|
||||||
|
editor().erase(array, index(idx));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// remove the value at a JSON pointer; returns the number of removed
|
||||||
|
/// values
|
||||||
|
std::size_t erase(const json_pointer& ptr)
|
||||||
|
{
|
||||||
|
if (ptr.empty())
|
||||||
|
{
|
||||||
|
detail::view::throw_out_of_range(405, "JSON pointer has no parent");
|
||||||
|
}
|
||||||
|
const view_type parent = root().at(ptr.parent_pointer());
|
||||||
|
const auto& token = ptr.back();
|
||||||
|
if (parent.is_array())
|
||||||
|
{
|
||||||
|
erase(parent, pointer_index(token));
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
return erase(parent, string_view_t(token.data(), token.size()));
|
||||||
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
using input_kind = detail::view::input_kind;
|
using input_kind = detail::view::input_kind;
|
||||||
|
|
||||||
|
|||||||
@@ -3228,6 +3228,62 @@ class editor
|
|||||||
return View(&m_doc, slot);
|
return View(&m_doc, slot);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// insert into an array before position idx (idx <= size()); returns a
|
||||||
|
/// view of the new element
|
||||||
|
template<typename V>
|
||||||
|
View insert(const View& array, std::size_t idx, V&& value)
|
||||||
|
{
|
||||||
|
node* const a = own(array);
|
||||||
|
if (a->kind != static_cast<std::uint8_t>(value_t::array))
|
||||||
|
{
|
||||||
|
throw_type_error(309, "cannot use insert() with ", array.type_name());
|
||||||
|
}
|
||||||
|
check_index(idx, a->len + 1);
|
||||||
|
const encoded e = encode(std::forward<V>(value));
|
||||||
|
node* const slot = new_slot(e);
|
||||||
|
node* const h = block_of(m_doc, a, 1);
|
||||||
|
std::memmove(h + 2 + idx, h + 1 + idx, (h->next - 1 - idx) * sizeof(node));
|
||||||
|
make_link(h[1 + idx], slot);
|
||||||
|
++h->next;
|
||||||
|
++h->len;
|
||||||
|
++a->len;
|
||||||
|
return View(&m_doc, slot);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// remove all members with this key; returns their number
|
||||||
|
std::size_t erase(const View& object, string_view_t key)
|
||||||
|
{
|
||||||
|
node* const o = own(object);
|
||||||
|
if (o->kind != static_cast<std::uint8_t>(value_t::object))
|
||||||
|
{
|
||||||
|
throw_type_error(307, "cannot use erase() with ", object.type_name());
|
||||||
|
}
|
||||||
|
for (const node* k = nav::first(m_doc, o), *end = nav::end(m_doc, o); k != end; k = document_data::after(k + 1))
|
||||||
|
{
|
||||||
|
if (key_equals(*k, key))
|
||||||
|
{
|
||||||
|
return erase_members(o, key, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// remove an array element
|
||||||
|
void erase(const View& array, std::size_t idx)
|
||||||
|
{
|
||||||
|
node* const a = own(array);
|
||||||
|
if (a->kind != static_cast<std::uint8_t>(value_t::array))
|
||||||
|
{
|
||||||
|
throw_type_error(307, "cannot use erase() with ", array.type_name());
|
||||||
|
}
|
||||||
|
check_index(idx, a->len);
|
||||||
|
node* const h = block_of(m_doc, a, 0);
|
||||||
|
std::memmove(h + 1 + idx, h + 2 + idx, (h->next - 2 - idx) * sizeof(node));
|
||||||
|
--h->next;
|
||||||
|
--h->len;
|
||||||
|
--a->len;
|
||||||
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
/// an encoded value: a scalar node, or the root of a new array/object
|
/// an encoded value: a scalar node, or the root of a new array/object
|
||||||
struct encoded
|
struct encoded
|
||||||
@@ -6240,6 +6296,45 @@ class basic_json_document
|
|||||||
return editor().push_back(array, std::forward<V>(value));
|
return editor().push_back(array, std::forward<V>(value));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// insert into an array before position idx (idx <= size()); returns a
|
||||||
|
/// view of the new element
|
||||||
|
template < typename I, typename V, typename std::enable_if < std::is_integral<I>::value && !std::is_same<I, bool>::value, int >::type = 0 >
|
||||||
|
view_type insert(view_type array, I idx, V && value)
|
||||||
|
{
|
||||||
|
return editor().insert(array, index(idx), std::forward<V>(value));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// remove all members with this key; returns their number
|
||||||
|
std::size_t erase(view_type object, string_view_t key)
|
||||||
|
{
|
||||||
|
return editor().erase(object, key);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// remove an array element
|
||||||
|
template < typename I, typename std::enable_if < std::is_integral<I>::value && !std::is_same<I, bool>::value, int >::type = 0 >
|
||||||
|
void erase(view_type array, I idx)
|
||||||
|
{
|
||||||
|
editor().erase(array, index(idx));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// remove the value at a JSON pointer; returns the number of removed
|
||||||
|
/// values
|
||||||
|
std::size_t erase(const json_pointer& ptr)
|
||||||
|
{
|
||||||
|
if (ptr.empty())
|
||||||
|
{
|
||||||
|
detail::view::throw_out_of_range(405, "JSON pointer has no parent");
|
||||||
|
}
|
||||||
|
const view_type parent = root().at(ptr.parent_pointer());
|
||||||
|
const auto& token = ptr.back();
|
||||||
|
if (parent.is_array())
|
||||||
|
{
|
||||||
|
erase(parent, pointer_index(token));
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
return erase(parent, string_view_t(token.data(), token.size()));
|
||||||
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
using input_kind = detail::view::input_kind;
|
using input_kind = detail::view::input_kind;
|
||||||
|
|
||||||
|
|||||||
@@ -278,12 +278,45 @@ TEST_CASE("json_view edits: differential")
|
|||||||
d.set(tv, key, v);
|
d.set(tv, key, v);
|
||||||
j[p][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
|
else if (op == 7 && (target.is_array() || target.is_null())) // push_back
|
||||||
{
|
{
|
||||||
const ordered_json v = random_value(2);
|
const ordered_json v = random_value(2);
|
||||||
d.push_back(tv, v);
|
d.push_back(tv, v);
|
||||||
j[p].push_back(v);
|
j[p].push_back(v);
|
||||||
}
|
}
|
||||||
|
else if (op == 8 && target.is_array()) // insert
|
||||||
|
{
|
||||||
|
const std::size_t 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 std::size_t 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
|
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 auto i = static_cast<std::size_t>(r(static_cast<int>(target.size())));
|
||||||
@@ -350,6 +383,13 @@ TEST_CASE("json_view edits: errors")
|
|||||||
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"], 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.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.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(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_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&);
|
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&);
|
||||||
@@ -390,6 +430,27 @@ TEST_CASE("json_view edits: views and values")
|
|||||||
CHECK(inner.get<int>() == 5);
|
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")
|
SECTION("strings stay valid while more edits come")
|
||||||
{
|
{
|
||||||
json_editable_document d = json_editable_document::parse("[]");
|
json_editable_document d = json_editable_document::parse("[]");
|
||||||
@@ -433,6 +494,10 @@ TEST_CASE("json_view edits: views and values")
|
|||||||
d.set(json::json_pointer("/x/3"), 4); // the size of the array appends too
|
d.set(json::json_pointer("/x/3"), 4); // the size of the array appends too
|
||||||
d.set(json::json_pointer("/y"), false);
|
d.set(json::json_pointer("/y"), false);
|
||||||
CHECK(d.root().dump() == R"({"x":[1,2,3,4],"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")
|
SECTION("duplicate keys")
|
||||||
@@ -440,6 +505,9 @@ TEST_CASE("json_view edits: views and values")
|
|||||||
json_editable_document d = json_editable_document::parse(R"({"a": 1, "b": 2, "a": 3})");
|
json_editable_document d = json_editable_document::parse(R"({"a": 1, "b": 2, "a": 3})");
|
||||||
d.set(d.root(), "a", 4); // the first member is assigned, the others dropped
|
d.set(d.root(), "a", 4); // the first member is assigned, the others dropped
|
||||||
CHECK(d.root().dump() == R"({"a":4,"b":2})");
|
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")
|
SECTION("values from other documents")
|
||||||
@@ -472,7 +540,9 @@ TEST_CASE("json_view edits: views and values")
|
|||||||
d.set(d.root(), "new", 1); // appended: the members move, the lookup is linear
|
d.set(d.root(), "new", 1); // appended: the members move, the lookup is linear
|
||||||
CHECK(d.root()["new"].get<int>() == 1);
|
CHECK(d.root()["new"].get<int>() == 1);
|
||||||
CHECK(d.root()["k199"].get<int>() == 199);
|
CHECK(d.root()["k199"].get<int>() == 199);
|
||||||
CHECK(d.root().size() == 201);
|
d.erase(d.root(), "k0");
|
||||||
|
CHECK(!d.root().contains("k0"));
|
||||||
|
CHECK(d.root().size() == 200);
|
||||||
}
|
}
|
||||||
|
|
||||||
SECTION("reuse and memory")
|
SECTION("reuse and memory")
|
||||||
|
|||||||
Reference in new issue
Block a user