clean up and added tests

This commit is contained in:
Niels 2016-04-24 19:03:33 +02:00
parent 09e9f6dcd4
commit fb54e212b6
3 changed files with 310 additions and 122 deletions

View file

@ -9518,42 +9518,44 @@ basic_json_parser_63:
*/ */
basic_json apply_patch(const basic_json& patch) const basic_json apply_patch(const basic_json& patch) const
{ {
// make a working copy to apply the patch to
basic_json result = *this; basic_json result = *this;
if (not patch.is_array()) // wrapper for "add" operation; add value at ptr
{ const auto operation_add = [&result](json_pointer & ptr, basic_json value)
// 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)
{ {
// get reference to parent of JSON pointer ptr
const auto last_path = ptr.pop_back(); const auto last_path = ptr.pop_back();
basic_json& parent = result.at(ptr); basic_json& parent = result.at(ptr);
if (parent.is_object()) if (parent.is_object())
{ {
// use operator[] to add value
parent[last_path] = value; parent[last_path] = value;
} }
else if (parent.is_array()) else if (parent.is_array())
{ {
if (last_path == "-") if (last_path == "-")
{ {
// special case: append to back
parent.push_back(value); parent.push_back(value);
} }
else else
{ {
parent.insert(parent.begin() + std::stoi(last_path), // default case: insert add offset
value); 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) const auto operation_remove = [&result](json_pointer & ptr)
{ {
// get reference to parent of JSON pointer ptr
const auto last_path = ptr.pop_back(); const auto last_path = ptr.pop_back();
basic_json& parent = result.at(ptr); basic_json& parent = result.at(ptr);
// remove child
if (parent.is_object()) if (parent.is_object())
{ {
parent.erase(parent.find(last_path)); 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) 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()) if (not val.is_object())
{ {
throw std::domain_error("JSON patch must be an array of objects"); throw std::domain_error("JSON patch must be an array of objects");
} }
// collect members // collect mandatory members
const auto it_op = val.m_value.object->find("op"); const std::string op = get_value("op", "op", true);
const auto it_path = val.m_value.object->find("path"); const std::string path = get_value(op, "path", true);
const auto it_value = val.m_value.object->find("value"); json_pointer ptr(get_value(op, "path", true));
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);
if (op == "add") if (op == "add")
{ {
if (it_value == val.m_value.object->end()) operation_add(ptr, get_value("add", "value"));
{
throw std::domain_error("'add' operation must have member 'value'");
}
operation_add(ptr, it_value->second);
} }
else if (op == "remove") else if (op == "remove")
{ {
@ -9606,21 +9624,11 @@ basic_json_parser_63:
} }
else if (op == "replace") else if (op == "replace")
{ {
if (it_value == val.m_value.object->end()) result.at(ptr) = get_value("replace", "value");
{
throw std::domain_error("'replace' operation must have member 'value'");
}
result.at(ptr) = it_value->second;
} }
else if (op == "move") else if (op == "move")
{ {
if (it_from == val.m_value.object->end()) const std::string from_path = get_value("move", "from", true);
{
throw std::domain_error("'move' operation must have member 'from'");
}
const std::string from_path = it_from->second;
json_pointer from_ptr(from_path); json_pointer from_ptr(from_path);
basic_json v = result[from_ptr]; basic_json v = result[from_ptr];
@ -9629,32 +9637,22 @@ basic_json_parser_63:
} }
else if (op == "copy") else if (op == "copy")
{ {
if (it_from == val.m_value.object->end()) const std::string from_path = get_value("copy", "from", true);;
{
throw std::domain_error("'copy' operation must have member 'from'");
}
const std::string from_path = it_from->second;
const json_pointer from_ptr(from_path); const json_pointer from_ptr(from_path);
result[ptr] = result.at(from_ptr); result[ptr] = result.at(from_ptr);
} }
else if (op == "test") else if (op == "test")
{ {
if (it_value == val.m_value.object->end()) if (result.at(ptr) != get_value("test", "value"))
{
throw std::domain_error("'test' operation must have member 'value'");
}
if (result.at(ptr) != it_value->second)
{ {
throw std::domain_error("unsuccessful: " + val.dump()); throw std::domain_error("unsuccessful: " + val.dump());
} }
} }
else else
{ {
// op must be "add", "remove", "replace", "move", // op must be "add", "remove", "replace", "move", "copy", or
// "copy", or "test" // "test"
throw std::domain_error("operation value '" + op + "' is invalid"); throw std::domain_error("operation value '" + op + "' is invalid");
} }
} }

View file

@ -8828,42 +8828,44 @@ class basic_json
*/ */
basic_json apply_patch(const basic_json& patch) const basic_json apply_patch(const basic_json& patch) const
{ {
// make a working copy to apply the patch to
basic_json result = *this; basic_json result = *this;
if (not patch.is_array()) // wrapper for "add" operation; add value at ptr
{ const auto operation_add = [&result](json_pointer & ptr, basic_json value)
// 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)
{ {
// get reference to parent of JSON pointer ptr
const auto last_path = ptr.pop_back(); const auto last_path = ptr.pop_back();
basic_json& parent = result.at(ptr); basic_json& parent = result.at(ptr);
if (parent.is_object()) if (parent.is_object())
{ {
// use operator[] to add value
parent[last_path] = value; parent[last_path] = value;
} }
else if (parent.is_array()) else if (parent.is_array())
{ {
if (last_path == "-") if (last_path == "-")
{ {
// special case: append to back
parent.push_back(value); parent.push_back(value);
} }
else else
{ {
parent.insert(parent.begin() + std::stoi(last_path), // default case: insert add offset
value); 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) const auto operation_remove = [&result](json_pointer & ptr)
{ {
// get reference to parent of JSON pointer ptr
const auto last_path = ptr.pop_back(); const auto last_path = ptr.pop_back();
basic_json& parent = result.at(ptr); basic_json& parent = result.at(ptr);
// remove child
if (parent.is_object()) if (parent.is_object())
{ {
parent.erase(parent.find(last_path)); 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) 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()) if (not val.is_object())
{ {
throw std::domain_error("JSON patch must be an array of objects"); throw std::domain_error("JSON patch must be an array of objects");
} }
// collect members // collect mandatory members
const auto it_op = val.m_value.object->find("op"); const std::string op = get_value("op", "op", true);
const auto it_path = val.m_value.object->find("path"); const std::string path = get_value(op, "path", true);
const auto it_value = val.m_value.object->find("value"); json_pointer ptr(get_value(op, "path", true));
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);
if (op == "add") if (op == "add")
{ {
if (it_value == val.m_value.object->end()) operation_add(ptr, get_value("add", "value"));
{
throw std::domain_error("'add' operation must have member 'value'");
}
operation_add(ptr, it_value->second);
} }
else if (op == "remove") else if (op == "remove")
{ {
@ -8916,21 +8934,11 @@ class basic_json
} }
else if (op == "replace") else if (op == "replace")
{ {
if (it_value == val.m_value.object->end()) result.at(ptr) = get_value("replace", "value");
{
throw std::domain_error("'replace' operation must have member 'value'");
}
result.at(ptr) = it_value->second;
} }
else if (op == "move") else if (op == "move")
{ {
if (it_from == val.m_value.object->end()) const std::string from_path = get_value("move", "from", true);
{
throw std::domain_error("'move' operation must have member 'from'");
}
const std::string from_path = it_from->second;
json_pointer from_ptr(from_path); json_pointer from_ptr(from_path);
basic_json v = result[from_ptr]; basic_json v = result[from_ptr];
@ -8939,32 +8947,22 @@ class basic_json
} }
else if (op == "copy") else if (op == "copy")
{ {
if (it_from == val.m_value.object->end()) const std::string from_path = get_value("copy", "from", true);;
{
throw std::domain_error("'copy' operation must have member 'from'");
}
const std::string from_path = it_from->second;
const json_pointer from_ptr(from_path); const json_pointer from_ptr(from_path);
result[ptr] = result.at(from_ptr); result[ptr] = result.at(from_ptr);
} }
else if (op == "test") else if (op == "test")
{ {
if (it_value == val.m_value.object->end()) if (result.at(ptr) != get_value("test", "value"))
{
throw std::domain_error("'test' operation must have member 'value'");
}
if (result.at(ptr) != it_value->second)
{ {
throw std::domain_error("unsuccessful: " + val.dump()); throw std::domain_error("unsuccessful: " + val.dump());
} }
} }
else else
{ {
// op must be "add", "remove", "replace", "move", // op must be "add", "remove", "replace", "move", "copy", or
// "copy", or "test" // "test"
throw std::domain_error("operation value '" + op + "' is invalid"); throw std::domain_error("operation value '" + op + "' is invalid");
} }
} }

View file

@ -12822,6 +12822,198 @@ TEST_CASE("JSON patch")
CHECK(doc.apply_patch(patch) == expected); 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") TEST_CASE("regression tests")