diff --git a/src/json-patch.cpp b/src/json-patch.cpp index 3f24d80..2276c92 100644 --- a/src/json-patch.cpp +++ b/src/json-patch.cpp @@ -1,75 +1,115 @@ #include "json-patch.hpp" +#include + +namespace +{ + +// originally from http://jsonpatch.com/, http://json.schemastore.org/json-patch +// with fixes +const nlohmann::json patch_schema = R"patch({ + "title": "JSON schema for JSONPatch files", + "$schema": "http://json-schema.org/draft-04/schema#", + "type": "array", + + "items": { + "oneOf": [ + { + "additionalProperties": false, + "required": [ "value", "op", "path"], + "properties": { + "path" : { "$ref": "#/definitions/path" }, + "op": { + "description": "The operation to perform.", + "type": "string", + "enum": [ "add", "replace", "test" ] + }, + "value": { + "description": "The value to add, replace or test." + } + } + }, + { + "additionalProperties": false, + "required": [ "op", "path"], + "properties": { + "path" : { "$ref": "#/definitions/path" }, + "op": { + "description": "The operation to perform.", + "type": "string", + "enum": [ "remove" ] + } + } + }, + { + "additionalProperties": false, + "required": [ "from", "op", "path" ], + "properties": { + "path" : { "$ref": "#/definitions/path" }, + "op": { + "description": "The operation to perform.", + "type": "string", + "enum": [ "move", "copy" ] + }, + "from": { + "$ref": "#/definitions/path", + "description": "A JSON Pointer path pointing to the location to move/copy from." + } + } + } + ] + }, + "definitions": { + "path": { + "description": "A JSON Pointer path.", + "type": "string" + } + } +})patch"_json; +}; // namespace + namespace nlohmann { json_patch::json_patch(json &&patch) - : j_{std::move(patch)} + : j_(std::move(patch)) { validateJsonPatch(j_); } json_patch::json_patch(const json &patch) - : j_{std::move(patch)} + : j_(std::move(patch)) { validateJsonPatch(j_); } -json_patch &json_patch::add(std::string path, json value) +json_patch &json_patch::add(const json::json_pointer &ptr, json value) { - j_.push_back(json{{"op", "add"}, {"path", std::move(path)}, {"value", std::move(value)}}); + j_.push_back(json{{"op", "add"}, {"path", ptr}, {"value", std::move(value)}}); return *this; } -json_patch &json_patch::replace(std::string path, json value) +json_patch &json_patch::replace(const json::json_pointer &ptr, json value) { - j_.push_back(json{{"op", "replace"}, {"path", std::move(path)}, {"value", std::move(value)}}); + j_.push_back(json{{"op", "replace"}, {"path", ptr}, {"value", std::move(value)}}); return *this; } -json_patch &json_patch::remove(std::string path) +json_patch &json_patch::remove(const json::json_pointer &ptr) { - j_.push_back(json{{"op", "remove"}, {"path", std::move(path)}}); + j_.push_back(json{{"op", "remove"}, {"path", ptr}}); return *this; } void json_patch::validateJsonPatch(json const &patch) { - if (!patch.is_array()) { - throw JsonPatchFormatException{"Json is not an array"}; - } + // static put here to have it created at the first usage of validateJsonPatch + static nlohmann::json_schema::json_validator patch_validator(patch_schema); - for (auto const &op : patch) { - if (!op.is_object()) { - throw JsonPatchFormatException{"Each json patch entry needs to be an op object"}; - } + patch_validator.validate(patch); - if (!op.contains("op")) { - throw JsonPatchFormatException{"Each json patch entry needs op element"}; - } - - const auto opType = op["op"].get(); - if ((opType != "add") && (opType != "remove") && (opType != "replace")) { - throw JsonPatchFormatException{std::string{"Operation "} + opType + std::string{"is invalid"}}; - } - - if (!op.contains("path")) { - throw JsonPatchFormatException{"Each json patch entry needs path element"}; - } - - try { - // try parse to path - [[maybe_unused]] const auto p = json::json_pointer{op["path"].get()}; - } catch (json::exception &e) { - throw JsonPatchFormatException{e.what()}; - } - - if (opType != "remove") { - if (!op.contains("value")) { - throw JsonPatchFormatException{"Remove and replace needs value element"}; - } - } - } + for (auto const &op : patch) + json::json_pointer(op["path"].get()); } } // namespace nlohmann diff --git a/src/json-patch.hpp b/src/json-patch.hpp index 4f204be..80fe1df 100644 --- a/src/json-patch.hpp +++ b/src/json-patch.hpp @@ -24,9 +24,9 @@ public: json_patch(json &&patch); json_patch(const json &patch); - json_patch &add(std::string path, json value); - json_patch &replace(std::string path, json value); - json_patch &remove(std::string path); + json_patch &add(const json::json_pointer &, json value); + json_patch &replace(const json::json_pointer &, json value); + json_patch &remove(const json::json_pointer &); operator json() const { return j_; } diff --git a/src/json-validator.cpp b/src/json-validator.cpp index 26831a1..62bac0a 100644 --- a/src/json-validator.cpp +++ b/src/json-validator.cpp @@ -1245,7 +1245,7 @@ json json_validator::validate(const json &instance) const json json_validator::validate(const json &instance, error_handler &err) const { json::json_pointer ptr; - json_patch patch{}; + json_patch patch; root_->validate(ptr, instance, patch, err); return patch; } diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 0de2a05..8d67e90 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -44,8 +44,14 @@ target_link_libraries(issue-98 nlohmann_json_schema_validator) add_test(NAME issue-98-erase-exception-unknown-keywords COMMAND issue-98) # Unit test for string format checks -add_executable("string-format-check-test" "string-format-check-test.cpp") -target_include_directories("string-format-check-test" PRIVATE "${PROJECT_SOURCE_DIR}/src/") -target_link_libraries("string-format-check-test" nlohmann_json_schema_validator) +add_executable(string-format-check-test string-format-check-test.cpp) +target_include_directories(string-format-check-test PRIVATE ${PROJECT_SOURCE_DIR}/src/) +target_link_libraries(string-format-check-test nlohmann_json_schema_validator) -add_test(NAME "string-format-check-test" COMMAND "string-format-check-test") +add_test(NAME string-format-check-test COMMAND string-format-check-test) + +# Unit test for json-patch +add_executable(json-patch json-patch.cpp) +target_include_directories(json-patch PRIVATE ${PROJECT_SOURCE_DIR}/src) +target_link_libraries(json-patch nlohmann_json_schema_validator) +add_test(NAME json-patch COMMAND json-patch) diff --git a/test/json-patch.cpp b/test/json-patch.cpp new file mode 100644 index 0000000..6e9da4d --- /dev/null +++ b/test/json-patch.cpp @@ -0,0 +1,48 @@ +#include "../src/json-patch.hpp" + +#include + +using nlohmann::json_patch; + +#define OK(code) \ + do { \ + try { \ + code; \ + } catch (const std::exception &e) { \ + std::cerr << "UNEXPECTED FAILED: " << e.what() << "\n"; \ + return 1; \ + } \ + } while (0) + +#define KO(code) \ + do { \ + try { \ + code; \ + std::cerr << "UNEXPECTED SUCCESS.\n"; \ + return 1; \ + } catch (const std::exception &e) { \ + std::cerr << "EXPECTED FAIL: " << e.what() << "\n"; \ + } \ + } while (0) + +int main(void) +{ + OK( json_patch p1( R"([{"op":"add","path":"/0/renderable/bg","value":"Black"}])"_json)); + OK( json_patch p1( R"([{"op":"replace","path":"/0/renderable/bg","value":"Black"}])"_json)); + OK( json_patch p1( R"([{"op":"remove","path":"/0/renderable/bg"}])"_json)); + + // value not needed + KO( json_patch p1( R"([{"op":"remove","path":"/0/renderable/bg", "value":"Black"}])"_json)); + // value missing + KO( json_patch p1( R"([{"op":"add","path":"/0/renderable/bg"}])"_json)); + // value missing + KO( json_patch p1( R"([{"op":"replace","path":"/0/renderable/bg"}])"_json)); + + // wrong op + KO( json_patch p1( R"([{"op":"ad","path":"/0/renderable/bg","value":"Black"}])"_json)); + + // invalid json-pointer + KO( json_patch p1( R"([{"op":"add","path":"0/renderable/bg","value":"Black"}])"_json)); + + return 0; +}