diff --git a/CMakeLists.txt b/CMakeLists.txt index 5c809d7..c4cc218 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 da95fed..45f4d35 100644 --- a/app/readme.cpp +++ b/app/readme.cpp @@ -1,5 +1,5 @@ -#include #include +#include #include @@ -12,16 +12,25 @@ 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 + "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", @@ -34,7 +43,8 @@ static json person_schema = R"( // The people are defined with brace initialization static json bad_person = {{"age", 42}}; -static json good_person = {{"name", "Albert"}, {"age", 42}}; +static json good_person = {{"name", "Albert"}, {"age", 42}, {"address", {{"street", "Main Street"}}}}; +static json good_defaulted_person = {{"name", "Knut"}, {"age", 69}, {"address", {}}}; int main() { @@ -51,12 +61,13 @@ int main() /* json-parse the people - API of 1.0.0, default throwing error handler */ - for (auto &person : {bad_person, good_person}) { + for (auto &person : {bad_person, good_person, good_defaulted_person}) { std::cout << "About to validate this person:\n" << std::setw(2) << person << std::endl; try { - validator.validate(person); // validate the document - uses the default throwing error-handler + auto defaultPatch = validator.validate(person); // validate the document - uses the default throwing error-handler std::cout << "Validation succeeded\n"; + std::cout << "Patch with defaults: " << defaultPatch.dump(2) << std::endl; } catch (const std::exception &e) { std::cerr << "Validation failed, here is why: " << e.what() << "\n"; } @@ -72,7 +83,6 @@ int main() } }; - for (auto &person : {bad_person, good_person}) { std::cout << "About to validate this person:\n" << std::setw(2) << person << std::endl; diff --git a/src/json-patch.cpp b/src/json-patch.cpp new file mode 100644 index 0000000..3f24d80 --- /dev/null +++ b/src/json-patch.cpp @@ -0,0 +1,75 @@ +#include "json-patch.hpp" + +namespace nlohmann +{ + +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..4f204be --- /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() = default; + 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 diff --git a/src/json-validator.cpp b/src/json-validator.cpp index 3a09957..c684578 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; @@ -30,6 +33,8 @@ using namespace nlohmann::json_schema; namespace { +static const json EmptyDefault{}; + class schema { protected: @@ -41,7 +46,12 @@ public: schema(root_schema *root) : root_(root) {} - virtual void validate(const json::json_pointer &ptr, const json &instance, 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 + { + return EmptyDefault; + } static std::shared_ptr make(json &schema, root_schema *root, @@ -54,14 +64,24 @@ class schema_ref : public schema const std::string id_; std::shared_ptr target_; - void validate(const json::json_pointer &ptr, const json &instance, 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, e); + target_->validate(ptr, instance, patch, e); else e.error(ptr, instance, "unresolved schema-reference " + id_); } + const json &defaultValue(const json::json_pointer &ptr, const json &instance, error_handler &e) const override + { + if (target_) + target_->defaultValue(ptr, instance, e); + else + e.error(ptr, instance, "unresolved schema-reference " + id_); + + return EmptyDefault; + } + public: schema_ref(const std::string &id, root_schema *root) : schema(root), id_(id) {} @@ -210,13 +230,23 @@ public: } while (1); } - void validate(const json::json_pointer &ptr, const json &instance, 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, e); + root_->validate(ptr, instance, patch, e); else e.error(ptr, "", "no root schema has yet been set for validating an instance"); } + + const json &defaultValue(const json::json_pointer &ptr, const json &instance, error_handler &e) const override + { + if (root_) + root_->defaultValue(ptr, instance, e); + else + e.error(ptr, "", "no root schema has yet been set for validating an instance"); + + return EmptyDefault; + } }; } // namespace json_schema @@ -250,15 +280,20 @@ class logical_not : public schema { std::shared_ptr subschema_; - void validate(const json::json_pointer &ptr, const json &instance, 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, esub); + subschema_->validate(ptr, instance, patch, esub); if (!esub) e.error(ptr, instance, "the subschema has succeeded, but it is required to not validate"); } + const json &defaultValue(const json::json_pointer &ptr, const json &instance, error_handler &e) const override + { + return subschema_->defaultValue(ptr, instance, e); + } + public: logical_not(json &sch, root_schema *root, @@ -280,13 +315,13 @@ class logical_combination : public schema { std::vector> subschemata_; - void validate(const json::json_pointer &ptr, const json &instance, 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; for (auto &s : subschemata_) { first_error_handler esub; - s->validate(ptr, instance, esub); + s->validate(ptr, instance, patch, esub); if (!esub) count++; @@ -350,6 +385,7 @@ bool logical_combination::is_validate_complete(const json &instance, cons class type_schema : public schema { + json defaultValue_{}; std::vector> type_; std::pair enum_, const_; std::vector> logic_; @@ -362,13 +398,18 @@ class type_schema : public schema std::shared_ptr if_, then_, else_; - void validate(const json::json_pointer &ptr, const json &instance, error_handler &e) const override final + const json &defaultValue(const json::json_pointer &, const json &, error_handler &) const override + { + return defaultValue_; + } + + 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()]; if (type) - type->validate(ptr, instance, e); + type->validate(ptr, instance, patch, e); else e.error(ptr, instance, "unexpected instance type"); @@ -389,18 +430,18 @@ class type_schema : public schema e.error(ptr, instance, "instance not const"); for (auto l : logic_) - l->validate(ptr, instance, e); + l->validate(ptr, instance, patch, e); if (if_) { first_error_handler err; - if_->validate(ptr, instance, err); + if_->validate(ptr, instance, patch, err); if (!err) { if (then_) - then_->validate(ptr, instance, e); + then_->validate(ptr, instance, patch, e); } else { if (else_) - else_->validate(ptr, instance, e); + else_->validate(ptr, instance, patch, e); } } } @@ -452,6 +493,11 @@ public: sch.erase(attr); } + const auto defaultAttr = sch.find("default"); + if (defaultAttr != sch.end()) { + defaultValue_ = defaultAttr.value(); + } + for (auto &key : known_keywords) sch.erase(key); @@ -544,7 +590,7 @@ class string : public schema return len; } - void validate(const json::json_pointer &ptr, const json &instance, 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) { @@ -634,7 +680,7 @@ class numeric : public schema return std::fabs(res) > std::fabs(eps); } - void validate(const json::json_pointer &ptr, const json &instance, 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 @@ -693,7 +739,7 @@ public: class null : public schema { - void validate(const json::json_pointer &ptr, const json &instance, 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"); @@ -706,7 +752,7 @@ public: class boolean_type : public schema { - void validate(const json::json_pointer &, const 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) @@ -716,7 +762,7 @@ public: class boolean : public schema { bool true_; - void validate(const json::json_pointer &ptr, const json &instance, 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 @@ -740,7 +786,7 @@ class required : public schema { const std::vector required_; - void validate(const json::json_pointer &ptr, const json &instance, 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()) @@ -768,7 +814,7 @@ class object : public schema std::shared_ptr propertyNames_; - void validate(const json::json_pointer &ptr, const json &instance, 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"); @@ -783,14 +829,14 @@ class object : public schema // for each property in instance for (auto &p : instance.items()) { if (propertyNames_) - propertyNames_->validate(ptr, p.key(), e); + propertyNames_->validate(ptr, p.key(), patch, e); bool a_prop_or_pattern_matched = false; auto schema_p = properties_.find(p.key()); // check if it is in "properties" if (schema_p != properties_.end()) { a_prop_or_pattern_matched = true; - schema_p->second->validate(ptr / p.key(), p.value(), e); + schema_p->second->validate(ptr / p.key(), p.value(), patch, e); } #ifndef NO_STD_REGEX @@ -798,23 +844,34 @@ class object : public schema for (auto &schema_pp : patternProperties_) if (REGEX_NAMESPACE::regex_search(p.key(), schema_pp.first)) { a_prop_or_pattern_matched = true; - schema_pp.second->validate(ptr / p.key(), p.value(), e); + schema_pp.second->validate(ptr / p.key(), p.value(), patch, e); } #endif // check additionalProperties as a last resort if (!a_prop_or_pattern_matched && additionalProperties_) { first_error_handler additional_prop_err; - additionalProperties_->validate(ptr / p.key(), p.value(), additional_prop_err); + additionalProperties_->validate(ptr / p.key(), p.value(), patch, additional_prop_err); if (additional_prop_err) e.error(ptr, instance, "validation failed for additional property '" + p.key() + "': " + additional_prop_err.message_); } } + // reverse search + for (auto const &prop : properties_) { + const auto finding = instance.find(prop.first); + 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.add((ptr / prop.first), defaultValue); + } + } + } + for (auto &dep : dependencies_) { auto prop = instance.find(dep.first); - if (prop != instance.end()) // if dependency-property is present in instance - dep.second->validate(ptr / dep.first, instance, e); // validate + if (prop != instance.end()) // if dependency-property is present in instance + dep.second->validate(ptr / dep.first, instance, patch, e); // validate } } @@ -909,7 +966,7 @@ class array : public schema std::shared_ptr contains_; - void validate(const json::json_pointer &ptr, const json &instance, 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"); @@ -928,7 +985,7 @@ class array : public schema size_t index = 0; if (items_schema_) for (auto &i : instance) { - items_schema_->validate(ptr / index, i, e); + items_schema_->validate(ptr / index, i, patch, e); index++; } else { @@ -945,7 +1002,7 @@ class array : public schema if (!item_validator) break; - item_validator->validate(ptr / index, i, e); + item_validator->validate(ptr / index, i, patch, e); } } @@ -953,7 +1010,7 @@ class array : public schema bool contained = false; for (auto &item : instance) { first_error_handler local_e; - contains_->validate(ptr, item, local_e); + contains_->validate(ptr, item, patch, local_e); if (!local_e) { contained = true; break; @@ -1161,16 +1218,18 @@ void json_validator::set_root_schema(json &&schema) root_->set_root_schema(std::move(schema)); } -void json_validator::validate(const json &instance) const +json json_validator::validate(const json &instance) const { throwing_error_handler err; - validate(instance, err); + return validate(instance, err); } -void json_validator::validate(const json &instance, error_handler &err) const +json json_validator::validate(const json &instance, error_handler &err) const { json::json_pointer ptr; - root_->validate(ptr, instance, err); + json_patch patch{}; + root_->validate(ptr, instance, patch, err); + return patch; } } // namespace json_schema diff --git a/src/nlohmann/json-schema.hpp b/src/nlohmann/json-schema.hpp index 2d18411..baa9719 100644 --- a/src/nlohmann/json-schema.hpp +++ b/src/nlohmann/json-schema.hpp @@ -186,10 +186,10 @@ public: void set_root_schema(json &&); // validate a json-document based on the root-schema - void validate(const json &) const; + json validate(const json &) const; // validate a json-document based on the root-schema with a custom error-handler - void validate(const json &, error_handler &) const; + json validate(const json &, error_handler &) const; }; } // namespace json_schema diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 9ca65d7..3371cb9 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -35,6 +35,10 @@ add_executable(issue-70-root-schema-constructor issue-70-root-schema-constructor target_link_libraries(issue-70-root-schema-constructor nlohmann_json_schema_validator) add_test(NAME issue-70-root-schema-constructor COMMAND issue-70-root-schema-constructor) +add_executable(issue-25-default-values issue-25-default-values.cpp) +target_link_libraries(issue-25-default-values nlohmann_json_schema_validator) +add_test(NAME issue-25-default-values COMMAND issue-25-default-values) + # 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/") diff --git a/test/issue-25-default-values.cpp b/test/issue-25-default-values.cpp new file mode 100644 index 0000000..f775281 --- /dev/null +++ b/test/issue-25-default-values.cpp @@ -0,0 +1,100 @@ +#include +#include + +using nlohmann::json; +using nlohmann::json_schema::json_validator; + +static const 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" + } + } + } + }, + "required": [ + "name", + "age" + ], + "additionalProperties": false, + "type": "object" +})"_json; + +int main(void) +{ + json_validator validator{}; + + // add address which is optional that should generate a diff containing a default street + json person_missing_address = R"({ + "name": "Hans", + "age": 69, + "address": {} +})"_json; + + validator.set_root_schema(person_schema); + + const auto default_patch = validator.validate(person_missing_address); + + if (!default_patch.is_array()) { + std::cerr << "Patch with defaults is expected to be an array" << std::endl; + return 1; + } + + if (default_patch.size() != 1) { + std::cerr << "Patch with defaults is expected to contain one opperation" << std::endl; + return 1; + } + + const auto &single_op = default_patch[0]; + + if (!single_op.contains("op")) { + std::cerr << "Patch with defaults is expected to contain opperation entry" << std::endl; + return 1; + } + + if (single_op["op"].get() != "add") { + std::cerr << "Patch with defaults is expected to contain add opperation" << std::endl; + return 1; + } + + if (!single_op.contains("path")) { + std::cerr << "Patch with defaults is expected to contain a path" << std::endl; + return 1; + } + + const auto &readPath = single_op["path"].get(); + if (readPath != "/address/street") { + std::cerr << "Patch with defaults contains wrong path. It is " << readPath << " and should be " + << "/address/street" << std::endl; + return 1; + } + + if (!single_op.contains("value")) { + std::cerr << "Patch with defaults is expected to contain a value" << std::endl; + return 1; + } + + if (single_op["value"].get() != "Abbey Road") { + std::cerr << "Patch with defaults contains wrong value" << std::endl; + return 1; + } + + return 0; +}