diff --git a/CMakeLists.txt b/CMakeLists.txt index 5c809d7..974e23a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -13,6 +13,7 @@ add_library(nlohmann_json_schema_validator src/json-schema-draft7.json.cpp src/json-uri.cpp src/json-validator.cpp + src/json_patch.cpp src/string-format-check.cpp) target_include_directories(nlohmann_json_schema_validator diff --git a/app/readme.cpp b/app/readme.cpp index 863cd07..9e9f5d4 100644 --- a/app/readme.cpp +++ b/app/readme.cpp @@ -10,41 +10,65 @@ using nlohmann::json_schema::json_validator; static json person_schema = R"( { "$schema": "http://json-schema.org/draft-07/schema#", - "title": "A person", - "properties": { - "name": { - "description": "Name", - "type": "string" - }, - "age": { - "description": "Age of the person", - "type": "number", - "minimum": 2, - "maximum": 200 - }, - "address":{ - "type": "object", - "properties":{ - "street":{ - "type": "string", - "default": "Abbey Road" + "title": "Dict", + "type": "array", + "items": { + "title": "A person", + "properties": { + "name": { + "description": "Name", + "type": "string" + }, + "age": { + "description": "Age of the person", + "type": "number", + "minimum": 2, + "maximum": 200 + }, + "address":{ + "type": "object", + "properties":{ + "street":{ + "type": "string", + "default": "Abbey Road" + } } } - } - }, - "required": [ - "name", - "age" - ], - "type": "object" -} - + }, + "required": [ + "name", + "age" + ], + "type": "object" + } + } )"_json; // The people are defined with brace initialization -static json bad_person = {{"age", 42}}; -static json good_person = {{"name", "Albert"}, {"age", 42}, {"address", {{"street", "Main Street"}}}}; -static json good_defaulted_person = {{"name", "Knut"}, {"age", 69}, {"address", {}}}; +static json bad_person = R"([ + {"age": 42} + ])"_json; + +static json good_person = R"([ + { + "name": "Albert", + "age": 42, + "address": {"street": "Main Street"} + } +])"_json; + +static json good_defaulted_person = R"([ + { + "name": "Albert", + "age": 42, + "address": {"street": "Main Street"} + }, + { + "name": "Knut", + "age": 69, + "address": {} + } +])"_json; int main() { diff --git a/src/json-validator.cpp b/src/json-validator.cpp index 9a17640..bb4d6f5 100644 --- a/src/json-validator.cpp +++ b/src/json-validator.cpp @@ -8,11 +8,14 @@ */ #include +#include "json_patch.hpp" + #include #include #include using nlohmann::json; +using nlohmann::json_patch; using nlohmann::json_uri; using nlohmann::json_schema::root_schema; using namespace nlohmann::json_schema; @@ -43,7 +46,7 @@ public: schema(root_schema *root) : root_(root) {} - virtual void validate(const json::json_pointer &ptr, const json &instance, json &patch, error_handler &e) const = 0; + virtual void validate(const json::json_pointer &ptr, const json &instance, json_patch &patch, error_handler &e) const = 0; virtual const json &defaultValue(const json::json_pointer &, const json &, error_handler &) const { @@ -61,7 +64,7 @@ class schema_ref : public schema const std::string id_; std::shared_ptr target_; - void validate(const json::json_pointer &ptr, const json &instance, json &patch, error_handler &e) const final + void validate(const json::json_pointer &ptr, const json &instance, json_patch &patch, error_handler &e) const final { if (target_) target_->validate(ptr, instance, patch, e); @@ -227,7 +230,7 @@ public: } while (1); } - void validate(const json::json_pointer &ptr, const json &instance, json &patch, error_handler &e) const final + void validate(const json::json_pointer &ptr, const json &instance, json_patch &patch, error_handler &e) const final { if (root_) root_->validate(ptr, instance, patch, e); @@ -277,7 +280,7 @@ class logical_not : public schema { std::shared_ptr subschema_; - void validate(const json::json_pointer &ptr, const json &instance, json &patch, error_handler &e) const final + void validate(const json::json_pointer &ptr, const json &instance, json_patch &patch, error_handler &e) const final { first_error_handler esub; subschema_->validate(ptr, instance, patch, esub); @@ -312,7 +315,7 @@ class logical_combination : public schema { std::vector> subschemata_; - void validate(const json::json_pointer &ptr, const json &instance, json &patch, error_handler &e) const final + void validate(const json::json_pointer &ptr, const json &instance, json_patch &patch, error_handler &e) const final { size_t count = 0; @@ -400,7 +403,7 @@ class type_schema : public schema return defaultValue_; } - void validate(const json::json_pointer &ptr, const json &instance, json &patch, error_handler &e) const override final + void validate(const json::json_pointer &ptr, const json &instance, json_patch &patch, error_handler &e) const override final { // depending on the type of instance run the type specific validator - if present auto type = type_[(uint8_t) instance.type()]; @@ -587,7 +590,7 @@ class string : public schema return len; } - void validate(const json::json_pointer &ptr, const json &instance, json &, error_handler &e) const override + void validate(const json::json_pointer &ptr, const json &instance, json_patch &, error_handler &e) const override { if (minLength_.first) { if (utf8_length(instance) < minLength_.second) { @@ -677,7 +680,7 @@ class numeric : public schema return std::fabs(res) > std::fabs(eps); } - void validate(const json::json_pointer &ptr, const json &instance, json &, error_handler &e) const override + void validate(const json::json_pointer &ptr, const json &instance, json_patch &, error_handler &e) const override { T value = instance; // conversion of json to value_type @@ -736,7 +739,7 @@ public: class null : public schema { - void validate(const json::json_pointer &ptr, const json &instance, json &, error_handler &e) const override + void validate(const json::json_pointer &ptr, const json &instance, json_patch &, error_handler &e) const override { if (!instance.is_null()) e.error(ptr, instance, "expected to be null"); @@ -749,7 +752,7 @@ public: class boolean_type : public schema { - void validate(const json::json_pointer &, const json &, json &, error_handler &) const override {} + void validate(const json::json_pointer &, const json &, json_patch &, error_handler &) const override {} public: boolean_type(json &, root_schema *root) @@ -759,7 +762,7 @@ public: class boolean : public schema { bool true_; - void validate(const json::json_pointer &ptr, const json &instance, json &, error_handler &e) const override + void validate(const json::json_pointer &ptr, const json &instance, json_patch &, error_handler &e) const override { if (!true_) { // false schema // empty array @@ -783,7 +786,7 @@ class required : public schema { const std::vector required_; - void validate(const json::json_pointer &ptr, const json &instance, json &, error_handler &e) const override final + void validate(const json::json_pointer &ptr, const json &instance, json_patch &, error_handler &e) const override final { for (auto &r : required_) if (instance.find(r) == instance.end()) @@ -811,7 +814,7 @@ class object : public schema std::shared_ptr propertyNames_; - void validate(const json::json_pointer &ptr, const json &instance, json &patch, error_handler &e) const override + void validate(const json::json_pointer &ptr, const json &instance, json_patch &patch, error_handler &e) const override { if (maxProperties_.first && instance.size() > maxProperties_.second) e.error(ptr, instance, "too many properties"); @@ -860,7 +863,7 @@ class object : public schema if (instance.end() == finding) { // if the prop is not in the instance const auto &defaultValue = prop.second->defaultValue(ptr, instance, e); if (!defaultValue.empty()) { // if default value is available - patch[ptr / prop.first] = defaultValue; + patch.add((ptr / prop.first), defaultValue); } } } @@ -963,7 +966,7 @@ class array : public schema std::shared_ptr contains_; - void validate(const json::json_pointer &ptr, const json &instance, json &patch, error_handler &e) const override + void validate(const json::json_pointer &ptr, const json &instance, json_patch &patch, error_handler &e) const override { if (maxItems_.first && instance.size() > maxItems_.second) e.error(ptr, instance, "array has too many items"); @@ -1224,9 +1227,9 @@ json json_validator::validate(const json &instance) const json json_validator::validate(const json &instance, error_handler &err) const { json::json_pointer ptr; - json mergePatch{}; - root_->validate(ptr, instance, mergePatch, err); - return mergePatch; + json_patch patch{}; + root_->validate(ptr, instance, patch, err); + return patch; } } // namespace json_schema diff --git a/src/json_patch.cpp b/src/json_patch.cpp new file mode 100644 index 0000000..3387697 --- /dev/null +++ b/src/json_patch.cpp @@ -0,0 +1,78 @@ +#include "json_patch.hpp" + +namespace nlohmann +{ + +json_patch::json_patch() + : j_{R"([])"_json} {} + +json_patch::json_patch(json &&patch) + : j_{std::move(patch)} +{ + validateJsonPatch(j_); +} + +json_patch::json_patch(const json &patch) + : j_{std::move(patch)} +{ + validateJsonPatch(j_); +} + +json_patch &json_patch::add(std::string path, json value) +{ + j_.push_back(json{{"op", "add"}, {"path", std::move(path)}, {"value", std::move(value)}}); + return *this; +} + +json_patch &json_patch::replace(std::string path, json value) +{ + j_.push_back(json{{"op", "replace"}, {"path", std::move(path)}, {"value", std::move(value)}}); + return *this; +} + +json_patch &json_patch::remove(std::string path) +{ + j_.push_back(json{{"op", "remove"}, {"path", std::move(path)}}); + return *this; +} + +void json_patch::validateJsonPatch(json const &patch) +{ + if (!patch.is_array()) { + throw JsonPatchFormatException{"Json is not an array"}; + } + + for (auto const &op : patch) { + if (!op.is_object()) { + throw JsonPatchFormatException{"Each json patch entry needs to be an op object"}; + } + + 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"}; + } + } + } +} + +} // namespace nlohmann diff --git a/src/json_patch.hpp b/src/json_patch.hpp new file mode 100644 index 0000000..6453297 --- /dev/null +++ b/src/json_patch.hpp @@ -0,0 +1,38 @@ +#pragma once + +#include +#include + +namespace nlohmann +{ +class JsonPatchFormatException : public std::exception +{ +public: + explicit JsonPatchFormatException(std::string msg) + : ex_{std::move(msg)} {} + + inline const char *what() const noexcept override final { return ex_.c_str(); } + +private: + std::string ex_; +}; + +class json_patch +{ +public: + json_patch(); + 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); + + operator json() const { return j_; } + +private: + json j_; + + static void validateJsonPatch(json const &patch); +}; +} // namespace nlohmann