clean up and added tests
This commit is contained in:
parent
09e9f6dcd4
commit
fb54e212b6
3 changed files with 310 additions and 122 deletions
120
src/json.hpp
120
src/json.hpp
|
@ -9518,42 +9518,44 @@ basic_json_parser_63:
|
|||
*/
|
||||
basic_json apply_patch(const basic_json& patch) const
|
||||
{
|
||||
// make a working copy to apply the patch to
|
||||
basic_json result = *this;
|
||||
|
||||
if (not patch.is_array())
|
||||
{
|
||||
// a JSON patch must be an array of objects
|
||||
throw std::domain_error("JSON patch must be an array of objects");
|
||||
}
|
||||
|
||||
const auto operation_add = [&result](json_pointer & ptr,
|
||||
basic_json & value)
|
||||
// wrapper for "add" operation; add value at ptr
|
||||
const auto operation_add = [&result](json_pointer & ptr, basic_json value)
|
||||
{
|
||||
// get reference to parent of JSON pointer ptr
|
||||
const auto last_path = ptr.pop_back();
|
||||
basic_json& parent = result.at(ptr);
|
||||
|
||||
if (parent.is_object())
|
||||
{
|
||||
// use operator[] to add value
|
||||
parent[last_path] = value;
|
||||
}
|
||||
else if (parent.is_array())
|
||||
{
|
||||
if (last_path == "-")
|
||||
{
|
||||
// special case: append to back
|
||||
parent.push_back(value);
|
||||
}
|
||||
else
|
||||
{
|
||||
parent.insert(parent.begin() + std::stoi(last_path),
|
||||
value);
|
||||
// default case: insert add offset
|
||||
parent.insert(parent.begin() + std::stoi(last_path), value);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// wrapper for "remove" operation; remove value at ptr
|
||||
const auto operation_remove = [&result](json_pointer & ptr)
|
||||
{
|
||||
// get reference to parent of JSON pointer ptr
|
||||
const auto last_path = ptr.pop_back();
|
||||
basic_json& parent = result.at(ptr);
|
||||
|
||||
// remove child
|
||||
if (parent.is_object())
|
||||
{
|
||||
parent.erase(parent.find(last_path));
|
||||
|
@ -9564,41 +9566,57 @@ basic_json_parser_63:
|
|||
}
|
||||
};
|
||||
|
||||
// type check
|
||||
if (not patch.is_array())
|
||||
{
|
||||
// a JSON patch must be an array of objects
|
||||
throw std::domain_error("JSON patch must be an array of objects");
|
||||
}
|
||||
|
||||
// iterate and apply th eoperations
|
||||
for (const auto& val : patch)
|
||||
{
|
||||
// wrapper to get a value for an operation
|
||||
const auto get_value = [&val](const std::string & op,
|
||||
const std::string & member,
|
||||
bool string_type = false) -> basic_json&
|
||||
{
|
||||
// find value
|
||||
auto it = val.m_value.object->find(member);
|
||||
|
||||
// context-sensitive error message
|
||||
const auto error_msg = (op == "op") ? "operation" : "operation '" + op + "'";
|
||||
|
||||
// check if desired value is present
|
||||
if (it == val.m_value.object->end())
|
||||
{
|
||||
throw std::domain_error(error_msg + " must have member '" + member + "'");
|
||||
}
|
||||
|
||||
// check if result is of type string
|
||||
if (string_type and not it->second.is_string())
|
||||
{
|
||||
throw std::domain_error(error_msg + " must have string member '" + member + "'");
|
||||
}
|
||||
|
||||
// no error: return value
|
||||
return it->second;
|
||||
};
|
||||
|
||||
// type check
|
||||
if (not val.is_object())
|
||||
{
|
||||
throw std::domain_error("JSON patch must be an array of objects");
|
||||
}
|
||||
|
||||
// collect members
|
||||
const auto it_op = val.m_value.object->find("op");
|
||||
const auto it_path = val.m_value.object->find("path");
|
||||
const auto it_value = val.m_value.object->find("value");
|
||||
const auto it_from = val.m_value.object->find("from");
|
||||
|
||||
if (it_op == val.m_value.object->end() or not it_op->second.is_string())
|
||||
{
|
||||
throw std::domain_error("operation must have a string 'op' member");
|
||||
}
|
||||
|
||||
if (it_path == val.m_value.object->end() or not it_op->second.is_string())
|
||||
{
|
||||
throw std::domain_error("operation must have a string 'path' member");
|
||||
}
|
||||
|
||||
const std::string op = it_op->second;
|
||||
const std::string path = it_path->second;
|
||||
json_pointer ptr(path);
|
||||
// collect mandatory members
|
||||
const std::string op = get_value("op", "op", true);
|
||||
const std::string path = get_value(op, "path", true);
|
||||
json_pointer ptr(get_value(op, "path", true));
|
||||
|
||||
if (op == "add")
|
||||
{
|
||||
if (it_value == val.m_value.object->end())
|
||||
{
|
||||
throw std::domain_error("'add' operation must have member 'value'");
|
||||
}
|
||||
|
||||
operation_add(ptr, it_value->second);
|
||||
operation_add(ptr, get_value("add", "value"));
|
||||
}
|
||||
else if (op == "remove")
|
||||
{
|
||||
|
@ -9606,21 +9624,11 @@ basic_json_parser_63:
|
|||
}
|
||||
else if (op == "replace")
|
||||
{
|
||||
if (it_value == val.m_value.object->end())
|
||||
{
|
||||
throw std::domain_error("'replace' operation must have member 'value'");
|
||||
}
|
||||
|
||||
result.at(ptr) = it_value->second;
|
||||
result.at(ptr) = get_value("replace", "value");
|
||||
}
|
||||
else if (op == "move")
|
||||
{
|
||||
if (it_from == val.m_value.object->end())
|
||||
{
|
||||
throw std::domain_error("'move' operation must have member 'from'");
|
||||
}
|
||||
|
||||
const std::string from_path = it_from->second;
|
||||
const std::string from_path = get_value("move", "from", true);
|
||||
json_pointer from_ptr(from_path);
|
||||
basic_json v = result[from_ptr];
|
||||
|
||||
|
@ -9629,32 +9637,22 @@ basic_json_parser_63:
|
|||
}
|
||||
else if (op == "copy")
|
||||
{
|
||||
if (it_from == val.m_value.object->end())
|
||||
{
|
||||
throw std::domain_error("'copy' operation must have member 'from'");
|
||||
}
|
||||
|
||||
const std::string from_path = it_from->second;
|
||||
const std::string from_path = get_value("copy", "from", true);;
|
||||
const json_pointer from_ptr(from_path);
|
||||
|
||||
result[ptr] = result.at(from_ptr);
|
||||
}
|
||||
else if (op == "test")
|
||||
{
|
||||
if (it_value == val.m_value.object->end())
|
||||
{
|
||||
throw std::domain_error("'test' operation must have member 'value'");
|
||||
}
|
||||
|
||||
if (result.at(ptr) != it_value->second)
|
||||
if (result.at(ptr) != get_value("test", "value"))
|
||||
{
|
||||
throw std::domain_error("unsuccessful: " + val.dump());
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// op must be "add", "remove", "replace", "move",
|
||||
// "copy", or "test"
|
||||
// op must be "add", "remove", "replace", "move", "copy", or
|
||||
// "test"
|
||||
throw std::domain_error("operation value '" + op + "' is invalid");
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8828,42 +8828,44 @@ class basic_json
|
|||
*/
|
||||
basic_json apply_patch(const basic_json& patch) const
|
||||
{
|
||||
// make a working copy to apply the patch to
|
||||
basic_json result = *this;
|
||||
|
||||
if (not patch.is_array())
|
||||
{
|
||||
// a JSON patch must be an array of objects
|
||||
throw std::domain_error("JSON patch must be an array of objects");
|
||||
}
|
||||
|
||||
const auto operation_add = [&result](json_pointer & ptr,
|
||||
basic_json & value)
|
||||
// wrapper for "add" operation; add value at ptr
|
||||
const auto operation_add = [&result](json_pointer & ptr, basic_json value)
|
||||
{
|
||||
// get reference to parent of JSON pointer ptr
|
||||
const auto last_path = ptr.pop_back();
|
||||
basic_json& parent = result.at(ptr);
|
||||
|
||||
if (parent.is_object())
|
||||
{
|
||||
// use operator[] to add value
|
||||
parent[last_path] = value;
|
||||
}
|
||||
else if (parent.is_array())
|
||||
{
|
||||
if (last_path == "-")
|
||||
{
|
||||
// special case: append to back
|
||||
parent.push_back(value);
|
||||
}
|
||||
else
|
||||
{
|
||||
parent.insert(parent.begin() + std::stoi(last_path),
|
||||
value);
|
||||
// default case: insert add offset
|
||||
parent.insert(parent.begin() + std::stoi(last_path), value);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// wrapper for "remove" operation; remove value at ptr
|
||||
const auto operation_remove = [&result](json_pointer & ptr)
|
||||
{
|
||||
// get reference to parent of JSON pointer ptr
|
||||
const auto last_path = ptr.pop_back();
|
||||
basic_json& parent = result.at(ptr);
|
||||
|
||||
// remove child
|
||||
if (parent.is_object())
|
||||
{
|
||||
parent.erase(parent.find(last_path));
|
||||
|
@ -8874,41 +8876,57 @@ class basic_json
|
|||
}
|
||||
};
|
||||
|
||||
// type check
|
||||
if (not patch.is_array())
|
||||
{
|
||||
// a JSON patch must be an array of objects
|
||||
throw std::domain_error("JSON patch must be an array of objects");
|
||||
}
|
||||
|
||||
// iterate and apply th eoperations
|
||||
for (const auto& val : patch)
|
||||
{
|
||||
// wrapper to get a value for an operation
|
||||
const auto get_value = [&val](const std::string & op,
|
||||
const std::string & member,
|
||||
bool string_type = false) -> basic_json&
|
||||
{
|
||||
// find value
|
||||
auto it = val.m_value.object->find(member);
|
||||
|
||||
// context-sensitive error message
|
||||
const auto error_msg = (op == "op") ? "operation" : "operation '" + op + "'";
|
||||
|
||||
// check if desired value is present
|
||||
if (it == val.m_value.object->end())
|
||||
{
|
||||
throw std::domain_error(error_msg + " must have member '" + member + "'");
|
||||
}
|
||||
|
||||
// check if result is of type string
|
||||
if (string_type and not it->second.is_string())
|
||||
{
|
||||
throw std::domain_error(error_msg + " must have string member '" + member + "'");
|
||||
}
|
||||
|
||||
// no error: return value
|
||||
return it->second;
|
||||
};
|
||||
|
||||
// type check
|
||||
if (not val.is_object())
|
||||
{
|
||||
throw std::domain_error("JSON patch must be an array of objects");
|
||||
}
|
||||
|
||||
// collect members
|
||||
const auto it_op = val.m_value.object->find("op");
|
||||
const auto it_path = val.m_value.object->find("path");
|
||||
const auto it_value = val.m_value.object->find("value");
|
||||
const auto it_from = val.m_value.object->find("from");
|
||||
|
||||
if (it_op == val.m_value.object->end() or not it_op->second.is_string())
|
||||
{
|
||||
throw std::domain_error("operation must have a string 'op' member");
|
||||
}
|
||||
|
||||
if (it_path == val.m_value.object->end() or not it_op->second.is_string())
|
||||
{
|
||||
throw std::domain_error("operation must have a string 'path' member");
|
||||
}
|
||||
|
||||
const std::string op = it_op->second;
|
||||
const std::string path = it_path->second;
|
||||
json_pointer ptr(path);
|
||||
// collect mandatory members
|
||||
const std::string op = get_value("op", "op", true);
|
||||
const std::string path = get_value(op, "path", true);
|
||||
json_pointer ptr(get_value(op, "path", true));
|
||||
|
||||
if (op == "add")
|
||||
{
|
||||
if (it_value == val.m_value.object->end())
|
||||
{
|
||||
throw std::domain_error("'add' operation must have member 'value'");
|
||||
}
|
||||
|
||||
operation_add(ptr, it_value->second);
|
||||
operation_add(ptr, get_value("add", "value"));
|
||||
}
|
||||
else if (op == "remove")
|
||||
{
|
||||
|
@ -8916,21 +8934,11 @@ class basic_json
|
|||
}
|
||||
else if (op == "replace")
|
||||
{
|
||||
if (it_value == val.m_value.object->end())
|
||||
{
|
||||
throw std::domain_error("'replace' operation must have member 'value'");
|
||||
}
|
||||
|
||||
result.at(ptr) = it_value->second;
|
||||
result.at(ptr) = get_value("replace", "value");
|
||||
}
|
||||
else if (op == "move")
|
||||
{
|
||||
if (it_from == val.m_value.object->end())
|
||||
{
|
||||
throw std::domain_error("'move' operation must have member 'from'");
|
||||
}
|
||||
|
||||
const std::string from_path = it_from->second;
|
||||
const std::string from_path = get_value("move", "from", true);
|
||||
json_pointer from_ptr(from_path);
|
||||
basic_json v = result[from_ptr];
|
||||
|
||||
|
@ -8939,32 +8947,22 @@ class basic_json
|
|||
}
|
||||
else if (op == "copy")
|
||||
{
|
||||
if (it_from == val.m_value.object->end())
|
||||
{
|
||||
throw std::domain_error("'copy' operation must have member 'from'");
|
||||
}
|
||||
|
||||
const std::string from_path = it_from->second;
|
||||
const std::string from_path = get_value("copy", "from", true);;
|
||||
const json_pointer from_ptr(from_path);
|
||||
|
||||
result[ptr] = result.at(from_ptr);
|
||||
}
|
||||
else if (op == "test")
|
||||
{
|
||||
if (it_value == val.m_value.object->end())
|
||||
{
|
||||
throw std::domain_error("'test' operation must have member 'value'");
|
||||
}
|
||||
|
||||
if (result.at(ptr) != it_value->second)
|
||||
if (result.at(ptr) != get_value("test", "value"))
|
||||
{
|
||||
throw std::domain_error("unsuccessful: " + val.dump());
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// op must be "add", "remove", "replace", "move",
|
||||
// "copy", or "test"
|
||||
// op must be "add", "remove", "replace", "move", "copy", or
|
||||
// "test"
|
||||
throw std::domain_error("operation value '" + op + "' is invalid");
|
||||
}
|
||||
}
|
||||
|
|
192
test/unit.cpp
192
test/unit.cpp
|
@ -12822,6 +12822,198 @@ TEST_CASE("JSON patch")
|
|||
CHECK(doc.apply_patch(patch) == expected);
|
||||
}
|
||||
}
|
||||
|
||||
SECTION("errors")
|
||||
{
|
||||
SECTION("unknown operation")
|
||||
{
|
||||
SECTION("missing 'op'")
|
||||
{
|
||||
json j;
|
||||
json patch = {{{"foo", "bar"}}};
|
||||
CHECK_THROWS_AS(j.apply_patch(patch), std::domain_error);
|
||||
CHECK_THROWS_WITH(j.apply_patch(patch), "operation must have member 'op'");
|
||||
}
|
||||
|
||||
SECTION("non-string 'op'")
|
||||
{
|
||||
json j;
|
||||
json patch = {{{"op", 1}}};
|
||||
CHECK_THROWS_AS(j.apply_patch(patch), std::domain_error);
|
||||
CHECK_THROWS_WITH(j.apply_patch(patch), "operation must have string member 'op'");
|
||||
}
|
||||
}
|
||||
|
||||
SECTION("add")
|
||||
{
|
||||
SECTION("missing 'path'")
|
||||
{
|
||||
json j;
|
||||
json patch = {{{"op", "add"}}};
|
||||
CHECK_THROWS_AS(j.apply_patch(patch), std::domain_error);
|
||||
CHECK_THROWS_WITH(j.apply_patch(patch), "operation 'add' must have member 'path'");
|
||||
}
|
||||
|
||||
SECTION("non-string 'path'")
|
||||
{
|
||||
json j;
|
||||
json patch = {{{"op", "add"}, {"path", 1}}};
|
||||
CHECK_THROWS_AS(j.apply_patch(patch), std::domain_error);
|
||||
CHECK_THROWS_WITH(j.apply_patch(patch), "operation 'add' must have string member 'path'");
|
||||
}
|
||||
|
||||
SECTION("missing 'value'")
|
||||
{
|
||||
json j;
|
||||
json patch = {{{"op", "add"}, {"path", ""}}};
|
||||
CHECK_THROWS_AS(j.apply_patch(patch), std::domain_error);
|
||||
CHECK_THROWS_WITH(j.apply_patch(patch), "operation 'add' must have member 'value'");
|
||||
}
|
||||
}
|
||||
|
||||
SECTION("remove")
|
||||
{
|
||||
SECTION("missing 'path'")
|
||||
{
|
||||
json j;
|
||||
json patch = {{{"op", "remove"}}};
|
||||
CHECK_THROWS_AS(j.apply_patch(patch), std::domain_error);
|
||||
CHECK_THROWS_WITH(j.apply_patch(patch), "operation 'remove' must have member 'path'");
|
||||
}
|
||||
|
||||
SECTION("non-string 'path'")
|
||||
{
|
||||
json j;
|
||||
json patch = {{{"op", "remove"}, {"path", 1}}};
|
||||
CHECK_THROWS_AS(j.apply_patch(patch), std::domain_error);
|
||||
CHECK_THROWS_WITH(j.apply_patch(patch), "operation 'remove' must have string member 'path'");
|
||||
}
|
||||
}
|
||||
|
||||
SECTION("replace")
|
||||
{
|
||||
SECTION("missing 'path'")
|
||||
{
|
||||
json j;
|
||||
json patch = {{{"op", "replace"}}};
|
||||
CHECK_THROWS_AS(j.apply_patch(patch), std::domain_error);
|
||||
CHECK_THROWS_WITH(j.apply_patch(patch), "operation 'replace' must have member 'path'");
|
||||
}
|
||||
|
||||
SECTION("non-string 'path'")
|
||||
{
|
||||
json j;
|
||||
json patch = {{{"op", "replace"}, {"path", 1}}};
|
||||
CHECK_THROWS_AS(j.apply_patch(patch), std::domain_error);
|
||||
CHECK_THROWS_WITH(j.apply_patch(patch), "operation 'replace' must have string member 'path'");
|
||||
}
|
||||
|
||||
SECTION("missing 'value'")
|
||||
{
|
||||
json j;
|
||||
json patch = {{{"op", "replace"}, {"path", ""}}};
|
||||
CHECK_THROWS_AS(j.apply_patch(patch), std::domain_error);
|
||||
CHECK_THROWS_WITH(j.apply_patch(patch), "operation 'replace' must have member 'value'");
|
||||
}
|
||||
}
|
||||
|
||||
SECTION("move")
|
||||
{
|
||||
SECTION("missing 'path'")
|
||||
{
|
||||
json j;
|
||||
json patch = {{{"op", "move"}}};
|
||||
CHECK_THROWS_AS(j.apply_patch(patch), std::domain_error);
|
||||
CHECK_THROWS_WITH(j.apply_patch(patch), "operation 'move' must have member 'path'");
|
||||
}
|
||||
|
||||
SECTION("non-string 'path'")
|
||||
{
|
||||
json j;
|
||||
json patch = {{{"op", "move"}, {"path", 1}}};
|
||||
CHECK_THROWS_AS(j.apply_patch(patch), std::domain_error);
|
||||
CHECK_THROWS_WITH(j.apply_patch(patch), "operation 'move' must have string member 'path'");
|
||||
}
|
||||
|
||||
SECTION("missing 'from'")
|
||||
{
|
||||
json j;
|
||||
json patch = {{{"op", "move"}, {"path", ""}}};
|
||||
CHECK_THROWS_AS(j.apply_patch(patch), std::domain_error);
|
||||
CHECK_THROWS_WITH(j.apply_patch(patch), "operation 'move' must have member 'from'");
|
||||
}
|
||||
|
||||
SECTION("non-string 'from'")
|
||||
{
|
||||
json j;
|
||||
json patch = {{{"op", "move"}, {"path", ""}, {"from", 1}}};
|
||||
CHECK_THROWS_AS(j.apply_patch(patch), std::domain_error);
|
||||
CHECK_THROWS_WITH(j.apply_patch(patch), "operation 'move' must have string member 'from'");
|
||||
}
|
||||
}
|
||||
|
||||
SECTION("copy")
|
||||
{
|
||||
SECTION("missing 'path'")
|
||||
{
|
||||
json j;
|
||||
json patch = {{{"op", "copy"}}};
|
||||
CHECK_THROWS_AS(j.apply_patch(patch), std::domain_error);
|
||||
CHECK_THROWS_WITH(j.apply_patch(patch), "operation 'copy' must have member 'path'");
|
||||
}
|
||||
|
||||
SECTION("non-string 'path'")
|
||||
{
|
||||
json j;
|
||||
json patch = {{{"op", "copy"}, {"path", 1}}};
|
||||
CHECK_THROWS_AS(j.apply_patch(patch), std::domain_error);
|
||||
CHECK_THROWS_WITH(j.apply_patch(patch), "operation 'copy' must have string member 'path'");
|
||||
}
|
||||
|
||||
SECTION("missing 'from'")
|
||||
{
|
||||
json j;
|
||||
json patch = {{{"op", "copy"}, {"path", ""}}};
|
||||
CHECK_THROWS_AS(j.apply_patch(patch), std::domain_error);
|
||||
CHECK_THROWS_WITH(j.apply_patch(patch), "operation 'copy' must have member 'from'");
|
||||
}
|
||||
|
||||
SECTION("non-string 'from'")
|
||||
{
|
||||
json j;
|
||||
json patch = {{{"op", "copy"}, {"path", ""}, {"from", 1}}};
|
||||
CHECK_THROWS_AS(j.apply_patch(patch), std::domain_error);
|
||||
CHECK_THROWS_WITH(j.apply_patch(patch), "operation 'copy' must have string member 'from'");
|
||||
}
|
||||
}
|
||||
|
||||
SECTION("test")
|
||||
{
|
||||
SECTION("missing 'path'")
|
||||
{
|
||||
json j;
|
||||
json patch = {{{"op", "test"}}};
|
||||
CHECK_THROWS_AS(j.apply_patch(patch), std::domain_error);
|
||||
CHECK_THROWS_WITH(j.apply_patch(patch), "operation 'test' must have member 'path'");
|
||||
}
|
||||
|
||||
SECTION("non-string 'path'")
|
||||
{
|
||||
json j;
|
||||
json patch = {{{"op", "test"}, {"path", 1}}};
|
||||
CHECK_THROWS_AS(j.apply_patch(patch), std::domain_error);
|
||||
CHECK_THROWS_WITH(j.apply_patch(patch), "operation 'test' must have string member 'path'");
|
||||
}
|
||||
|
||||
SECTION("missing 'value'")
|
||||
{
|
||||
json j;
|
||||
json patch = {{{"op", "test"}, {"path", ""}}};
|
||||
CHECK_THROWS_AS(j.apply_patch(patch), std::domain_error);
|
||||
CHECK_THROWS_WITH(j.apply_patch(patch), "operation 'test' must have member 'value'");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("regression tests")
|
||||
|
|
Loading…
Reference in a new issue