From 6c482e10355c650b5840fecdb30f05b98d8441c9 Mon Sep 17 00:00:00 2001 From: Patrick Boettcher Date: Thu, 24 Jan 2019 16:52:00 +0100 Subject: [PATCH] error-handler now receives a json_pointer as path Indicating where in the instance the error occurred. The pointer is relative to the root of the instance. --- app/json-schema-validate.cpp | 13 +--- app/readme.cpp | 6 +- src/json-schema.hpp | 21 ++--- src/json-uri.cpp | 2 +- src/json-validator.cpp | 147 ++++++++++++++++++----------------- test/CMakeLists.txt | 3 + test/errors.cpp | 117 ++++++++++++++++++++++++++++ 7 files changed, 211 insertions(+), 98 deletions(-) create mode 100644 test/errors.cpp diff --git a/app/json-schema-validate.cpp b/app/json-schema-validate.cpp index b6699e3..e0061d2 100644 --- a/app/json-schema-validate.cpp +++ b/app/json-schema-validate.cpp @@ -21,13 +21,6 @@ static void usage(const char *name) exit(EXIT_FAILURE); } -#if 0 - resolver r(nlohmann::json_schema_draft4::root_schema, - nlohmann::json_schema_draft4::root_schema["id"]); - schema_refs_.insert(r.schema_refs.begin(), r.schema_refs.end()); - assert(r.undefined_refs.size() == 0); -#endif - static void loader(const json_uri &uri, json &schema) { std::string filename = "./" + uri.path(); @@ -44,10 +37,10 @@ static void loader(const json_uri &uri, json &schema) class custom_error_handler : public nlohmann::json_schema::basic_error_handler { - void error(const std::string &path, const json &instance, const std::string &message) override + void error(const nlohmann::json::json_pointer &ptr, const json &instance, const std::string &message) override { - nlohmann::json_schema::basic_error_handler::error(path, instance, message); - std::cerr << "ERROR: '" << path << "' - '" << instance << "': " << message << "\n"; + nlohmann::json_schema::basic_error_handler::error(ptr, instance, message); + std::cerr << "ERROR: '" << ptr << "' - '" << instance << "': " << message << "\n"; } }; diff --git a/app/readme.cpp b/app/readme.cpp index a25d652..9122f48 100644 --- a/app/readme.cpp +++ b/app/readme.cpp @@ -65,10 +65,10 @@ int main() /* json-parse the people - with custom error handler */ class custom_error_handler : public nlohmann::json_schema::basic_error_handler { - void error(const std::string &path, const json &instance, const std::string &message) override + void error(const nlohmann::json::json_pointer &ptr, const json &instance, const std::string &message) override { - nlohmann::json_schema::basic_error_handler::error(path, instance, message); - std::cerr << "ERROR: '" << path << "' - '" << instance << "': " << message << "\n"; + nlohmann::json_schema::basic_error_handler::error(ptr, instance, message); + std::cerr << "ERROR: '" << ptr << "' - '" << instance << "': " << message << "\n"; } }; diff --git a/src/json-schema.hpp b/src/json-schema.hpp index 22c15f1..3f2fff7 100644 --- a/src/json-schema.hpp +++ b/src/json-schema.hpp @@ -24,11 +24,11 @@ #include #ifdef NLOHMANN_JSON_VERSION_MAJOR -# if NLOHMANN_JSON_VERSION_MAJOR < 3 || NLOHMANN_JSON_VERSION_MINOR < 5 || NLOHMANN_JSON_VERSION_PATCH < 0 -# error "Please use this library with NLohmann's JSON version 3.5.0 or higher" +# if (NLOHMANN_JSON_VERSION_MAJOR * 10000 + NLOHMANN_JSON_VERSION_MINOR * 100 + NLOHMANN_JSON_VERSION_PATCH) < 30600 +# error "Please use this library with NLohmann's JSON version 3.6.0 or higher" # endif #else -# error "expected existing NLOHMANN_JSON_VERSION_MAJOR preproc variable, please update to NLohmann's JSON 3.5.0" +# error "expected existing NLOHMANN_JSON_VERSION_MAJOR preproc variable, please update to NLohmann's JSON 3.6.0" #endif // make yourself a home - welcome to nlohmann's namespace @@ -51,7 +51,7 @@ class JSON_SCHEMA_VALIDATOR_API json_uri std::string proto_; std::string hostname_; std::string path_; - nlohmann::json::json_pointer pointer_; + json::json_pointer pointer_; protected: // decodes a JSON uri and replaces all or part of the currently stored values @@ -72,7 +72,7 @@ public: const std::string hostname() const { return hostname_; } const std::string path() const { return path_; } - const nlohmann::json::json_pointer pointer() const { return pointer_; } + const json::json_pointer pointer() const { return pointer_; } const std::string url() const { return location(); } const std::string location() const; @@ -92,12 +92,7 @@ public: json_uri append(const std::string &field) const { json_uri u = *this; -#if NLOHMANN_JSON_VERSION_MAJOR >= 3 && NLOHMANN_JSON_VERSION_MINOR >= 5 && NLOHMANN_JSON_VERSION_PATCH >= 1 - u.pointer_.push_back(field); -#else - u.pointer_ = nlohmann::json::json_pointer(u.pointer_.to_string() + '/' + escape(field)); -#endif - + u.pointer_ /= field; return u; } @@ -126,12 +121,12 @@ class basic_error_handler bool error_{false}; public: - virtual void error(const std::string & /*path*/, const json & /* instance */, const std::string & /*message*/) + virtual void error(const json::json_pointer & /*path*/, const json & /* instance */, const std::string & /*message*/) { error_ = true; } - void reset() { error_ = false; } + virtual void reset() { error_ = false; } operator bool() const { return error_; } }; diff --git a/src/json-uri.cpp b/src/json-uri.cpp index f8d6cbe..c0e37cb 100644 --- a/src/json-uri.cpp +++ b/src/json-uri.cpp @@ -91,7 +91,7 @@ void json_uri::update(const std::string &uri) } } - pointer_ = nlohmann::json::json_pointer(pointer); + pointer_ = json::json_pointer(pointer); } const std::string json_uri::location() const diff --git a/src/json-validator.cpp b/src/json-validator.cpp index 4ca00d8..03e98fd 100644 --- a/src/json-validator.cpp +++ b/src/json-validator.cpp @@ -39,7 +39,7 @@ public: schema(root_schema *root) : root_(root) {} - virtual void validate(const json &instance, basic_error_handler &e) const = 0; + virtual void validate(const json::json_pointer &ptr, const json &instance, basic_error_handler &e) const = 0; static std::shared_ptr make(json &schema, root_schema *root, @@ -52,12 +52,12 @@ class schema_ref : public schema const std::string id_; std::shared_ptr target_; - void validate(const json &instance, basic_error_handler &e) const final + void validate(const json::json_pointer &ptr, const json &instance, basic_error_handler &e) const final { if (target_) - target_->validate(instance, e); + target_->validate(ptr, instance, e); else - e.error("", instance, "unresolved schema-reference " + id_); + e.error(ptr, instance, "unresolved schema-reference " + id_); } public: @@ -83,8 +83,8 @@ class root_schema : public schema std::shared_ptr root_; struct schema_file { - std::map> schemas; - std::map> unresolved; // contains all unresolved references from any other file seen during parsing + std::map> schemas; + std::map> unresolved; // contains all unresolved references from any other file seen during parsing json unknown_keywords; }; @@ -211,12 +211,12 @@ public: } while (1); } - void validate(const json &instance, basic_error_handler &e) const final + void validate(const json::json_pointer &ptr, const json &instance, basic_error_handler &e) const final { if (root_) - root_->validate(instance, e); + root_->validate(ptr, instance, e); else - e.error("", "", "no root schema has yet been set for validating an instance."); + e.error(ptr, "", "no root schema has yet been set for validating an instance."); } }; @@ -230,13 +230,13 @@ class logical_not : public schema { std::shared_ptr subschema_; - void validate(const json &instance, basic_error_handler &e) const final + void validate(const json::json_pointer &ptr, const json &instance, basic_error_handler &e) const final { basic_error_handler err; - subschema_->validate(instance, err); + subschema_->validate(ptr, instance, err); if (!err) - e.error("", instance, "instance is valid, whereas it should NOT be as required by schema"); + e.error(ptr, instance, "instance is valid, whereas it should NOT be as required by schema"); } public: @@ -260,27 +260,28 @@ class logical_combination : public schema { std::vector> subschemata_; - void validate(const json &instance, basic_error_handler &e) const final + void validate(const json::json_pointer &ptr, const json &instance, basic_error_handler &e) const final { size_t count = 0; for (auto &s : subschemata_) { basic_error_handler err; - s->validate(instance, err); + s->validate(ptr, instance, err); + if (!err) count++; - if (is_validate_complete(instance, e, err, count)) + if (is_validate_complete(instance, ptr, e, err, count)) return; } if (count == 0) - e.error("", instance, "no validation has succeeded but ANYOF/ONEOF them is required to validate."); + e.error(ptr, instance, "no validation has succeeded but ANYOF/ONEOF them is required to validate."); } // specialized for each of the logical_combination_types static const std::string key; - static bool is_validate_complete(const json &instance, basic_error_handler &e, bool err, size_t count); + static bool is_validate_complete(const json &, const json::json_pointer &, basic_error_handler &, bool, size_t); public: logical_combination(json &sch, @@ -305,24 +306,24 @@ template <> const std::string logical_combination::key = "oneOf"; template <> -bool logical_combination::is_validate_complete(const json &instance, basic_error_handler &e, bool err, size_t) +bool logical_combination::is_validate_complete(const json &instance, const json::json_pointer &ptr, basic_error_handler &e, bool err, size_t) { if (err) - e.error("", instance, "at least one schema has failed, but ALLOF them are required to validate."); + e.error(ptr, instance, "at least one schema has failed, but ALLOF them are required to validate."); return err; } template <> -bool logical_combination::is_validate_complete(const json &, basic_error_handler &, bool, size_t count) +bool logical_combination::is_validate_complete(const json &, const json::json_pointer &, basic_error_handler &, bool, size_t count) { return count == 1; } template <> -bool logical_combination::is_validate_complete(const json &instance, basic_error_handler &e, bool, size_t count) +bool logical_combination::is_validate_complete(const json &instance, const json::json_pointer &ptr, basic_error_handler &e, bool, size_t count) { if (count > 1) - e.error("", instance, "more than one schema has succeeded, but only ONEOF them is required to validate."); + e.error(ptr, instance, "more than one schema has succeeded, but only ONEOF them is required to validate."); return count > 1; } @@ -340,15 +341,15 @@ class type_schema : public schema std::shared_ptr if_, then_, else_; - void validate(const json &instance, basic_error_handler &e) const override final + void validate(const json::json_pointer &ptr, const json &instance, basic_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(instance, e); + type->validate(ptr, instance, e); else - e.error("", instance, "unexpected instance type"); + e.error(ptr, instance, "unexpected instance type"); if (enum_.first) { bool seen_in_enum = false; @@ -359,26 +360,26 @@ class type_schema : public schema } if (!seen_in_enum) - e.error("", instance, "instance not found in required enum"); + e.error(ptr, instance, "instance not found in required enum"); } if (const_.first && const_.second != instance) - e.error("", instance, "instance not const"); + e.error(ptr, instance, "instance not const"); for (auto l : logic_) - l->validate(instance, e); + l->validate(ptr, instance, e); if (if_) { basic_error_handler err; - if_->validate(instance, err); + if_->validate(ptr, instance, err); if (!err) { if (then_) - then_->validate(instance, e); + then_->validate(ptr, instance, e); } else { if (else_) - else_->validate(instance, e); + else_->validate(ptr, instance, e); } } } @@ -521,13 +522,13 @@ class string : public schema return len; } - void validate(const json &instance, basic_error_handler &e) const override + void validate(const json::json_pointer &ptr, const json &instance, basic_error_handler &e) const override { if (minLength_.first) { if (utf8_length(instance) < minLength_.second) { std::ostringstream s; s << "'" << instance << "' is too short as per minLength (" << minLength_.second << ")"; - e.error("", instance, s.str()); + e.error(ptr, instance, s.str()); } } @@ -535,24 +536,24 @@ class string : public schema if (utf8_length(instance) > maxLength_.second) { std::ostringstream s; s << "'" << instance << "' is too long as per maxLength (" << maxLength_.second << ")"; - e.error("", instance, s.str()); + e.error(ptr, instance, s.str()); } } #ifndef NO_STD_REGEX if (pattern_.first && !REGEX_NAMESPACE::regex_search(instance.get(), pattern_.second)) - e.error("", instance, instance.get() + " does not match regex pattern: " + patternString_); + e.error(ptr, instance, instance.get() + " does not match regex pattern: " + patternString_); #endif if (format_.first) { if (root_->format_check() == nullptr) - e.error("", instance, std::string("A format checker was not provided but a format-attribute for this string is present. ") + " cannot be validated for " + format_.second); + e.error(ptr, instance, std::string("A format checker was not provided but a format-attribute for this string is present. ") + " cannot be validated for " + format_.second); else { try { root_->format_check()(format_.second, instance); } catch (const std::exception &ex) { - e.error("", instance, std::string("Format-checking failed: ") + ex.what()); + e.error(ptr, instance, std::string("Format-checking failed: ") + ex.what()); } } } @@ -611,23 +612,23 @@ class numeric : public schema return std::fabs(res) > std::fabs(eps); } - void validate(const json &instance, basic_error_handler &e) const override + void validate(const json::json_pointer &ptr, const json &instance, basic_error_handler &e) const override { T value = instance; // conversion of json to value_type if (multipleOf_.first && value != 0) // zero is multiple of everything if (violates_multiple_of(value)) - e.error("", instance, "is not a multiple of " + std::to_string(multipleOf_.second)); + e.error(ptr, instance, "is not a multiple of " + std::to_string(multipleOf_.second)); if (maximum_.first) if ((exclusiveMaximum_ && value >= maximum_.second) || value > maximum_.second) - e.error("", instance, "exceeds maximum of " + std::to_string(maximum_.second)); + e.error(ptr, instance, "exceeds maximum of " + std::to_string(maximum_.second)); if (minimum_.first) if ((exclusiveMinimum_ && value <= minimum_.second) || value < minimum_.second) - e.error("", instance, "is below minimum of " + std::to_string(minimum_.second)); + e.error(ptr, instance, "is below minimum of " + std::to_string(minimum_.second)); } public: @@ -670,10 +671,10 @@ public: class null : public schema { - void validate(const json &instance, basic_error_handler &e) const override + void validate(const json::json_pointer &ptr, const json &instance, basic_error_handler &e) const override { if (!instance.is_null()) - e.error("", instance, "expected to be null"); + e.error(ptr, instance, "expected to be null"); } public: @@ -683,7 +684,7 @@ public: class boolean_type : public schema { - void validate(const json &, basic_error_handler &) const override {} + void validate(const json::json_pointer &, const json &, basic_error_handler &) const override {} public: boolean_type(json &, root_schema *root) @@ -693,18 +694,18 @@ public: class boolean : public schema { bool true_; - void validate(const json &instance, basic_error_handler &e) const override + void validate(const json::json_pointer &ptr, const json &instance, basic_error_handler &e) const override { if (!true_) { // false schema // empty array //switch (instance.type()) { //case json::value_t::array: // if (instance.size() != 0) // valid false-schema - // e.error("", instance, "false-schema required empty array"); + // e.error(ptr, instance, "false-schema required empty array"); // return; //} - e.error("", instance, "instance invalid as par false-schema"); + e.error(ptr, instance, "instance invalid as par false-schema"); } } @@ -717,11 +718,11 @@ class required : public schema { const std::vector required_; - void validate(const json &instance, basic_error_handler &e) const override final + void validate(const json::json_pointer &ptr, const json &instance, basic_error_handler &e) const override final { for (auto &r : required_) if (instance.find(r) == instance.end()) - e.error("", instance, "required property '" + r + "' not found in object as a dependency"); + e.error(ptr, instance, "required property '" + r + "' not found in object as a dependency"); } public: @@ -745,46 +746,46 @@ class object : public schema std::shared_ptr propertyNames_; - void validate(const json &instance, basic_error_handler &e) const override + void validate(const json::json_pointer &ptr, const json &instance, basic_error_handler &e) const override { if (maxProperties_.first && instance.size() > maxProperties_.second) - e.error("", instance, "too many properties."); + e.error(ptr, instance, "too many properties."); if (minProperties_.first && instance.size() < minProperties_.second) - e.error("", instance, "too few properties."); + e.error(ptr, instance, "too few properties."); for (auto &r : required_) if (instance.find(r) == instance.end()) - e.error("", instance, "required property '" + r + "' not found in object '"); + e.error(ptr, instance, "required property '" + r + "' not found in object"); // for each property in instance for (auto &p : instance.items()) { if (propertyNames_) - propertyNames_->validate(p.key(), e); + propertyNames_->validate(ptr, p.key(), 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(p.value(), e); + schema_p->second->validate(ptr / p.key(), p.value(), e); } // check all matching patternProperties 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(p.value(), e); + schema_pp.second->validate(ptr / p.key(), p.value(), e); } // check additionalProperties as a last resort if (!a_prop_or_pattern_matched && additionalProperties_) - additionalProperties_->validate(p.value(), e); + additionalProperties_->validate(ptr / p.key(), p.value(), e); } for (auto &dep : dependencies_) { auto prop = instance.find(dep.first); - if (prop != instance.end()) // if dependency-property is present in instance - dep.second->validate(instance, e); // validate + if (prop != instance.end()) // if dependency-property is present in instance + dep.second->validate(ptr / dep.first, instance, e); // validate } } @@ -879,25 +880,28 @@ class array : public schema std::shared_ptr contains_; - void validate(const json &instance, basic_error_handler &e) const override + void validate(const json::json_pointer &ptr, const json &instance, basic_error_handler &e) const override { if (maxItems_.first && instance.size() > maxItems_.second) - e.error("", instance, "has too many items."); + e.error(ptr, instance, "has too many items."); if (minItems_.first && instance.size() < minItems_.second) - e.error("", instance, "has too few items."); + e.error(ptr, instance, "has too few items."); if (uniqueItems_) { for (auto it = instance.cbegin(); it != instance.cend(); ++it) { auto v = std::find(it + 1, instance.end(), *it); if (v != instance.end()) - e.error("", instance, "items have to be unique for this array."); + e.error(ptr, instance, "items have to be unique for this array."); } } + size_t index = 0; if (items_schema_) - for (auto &i : instance) - items_schema_->validate(i, e); + for (auto &i : instance) { + items_schema_->validate(ptr / index, i, e); + index++; + } else { auto item = items_.cbegin(); for (auto &i : instance) { @@ -912,7 +916,7 @@ class array : public schema if (!item_validator) break; - item_validator->validate(i, e); + item_validator->validate(ptr / index, i, e); } } @@ -920,14 +924,14 @@ class array : public schema bool contained = false; for (auto &item : instance) { basic_error_handler local_e; - contains_->validate(item, local_e); + contains_->validate(ptr, item, local_e); if (!local_e) { contained = true; break; } } if (!contained) - e.error("", instance, "array does not contain required element as per 'contains'"); + e.error(ptr, instance, "array does not contain required element as per 'contains'"); } } @@ -1080,9 +1084,9 @@ std::shared_ptr schema::make(json &schema, class throwing_error_handler : public basic_error_handler { - void error(const std::string &path, const json &instance, const std::string &message) + void error(const json::json_pointer &ptr, const json &instance, const std::string &message) { - throw std::invalid_argument(std::string("At ") + path + " of " + instance.dump() + " - " + message + "\n"); + throw std::invalid_argument(std::string("At ") + ptr.to_string() + " of " + instance.dump() + " - " + message + "\n"); } }; @@ -1118,7 +1122,8 @@ void json_validator::validate(const json &instance) void json_validator::validate(const json &instance, basic_error_handler &err) { - root_->validate(instance, err); + json::json_pointer ptr; + root_->validate(ptr, instance, err); } } // namespace json_schema diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index b1b1c01..5d08e41 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -22,4 +22,7 @@ endforeach() add_executable(uri uri.cpp) target_link_libraries(uri json-schema-validator) +add_executable(errors errors.cpp) +target_link_libraries(errors json-schema-validator) + add_test(NAME uri COMMAND uri) diff --git a/test/errors.cpp b/test/errors.cpp new file mode 100644 index 0000000..dfa6b88 --- /dev/null +++ b/test/errors.cpp @@ -0,0 +1,117 @@ +#include + +#include + +static int error_count; + +#define EXPECT_EQ(a, b) \ + do { \ + if (a != b) { \ + std::cerr << "Failed: '" << a << "' != '" << b << "'\n"; \ + error_count++; \ + } \ + } while (0) + +using nlohmann::json; +using nlohmann::json_schema::json_validator; + +namespace +{ + +// The schema is defined based upon a string literal +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 + }, + "phones": { + "type": "array", + "items": { + "type": "number" + } + } + }, + "required": [ + "name", + "age" + ], + "type": "object" +})"_json; + +class store_ptr_err_handler : public nlohmann::json_schema::basic_error_handler +{ + void error(const nlohmann::json::json_pointer &ptr, const json &instance, const std::string &message) override + { + nlohmann::json_schema::basic_error_handler::error(ptr, instance, message); + std::cerr << "ERROR: '" << ptr << "' - '" << instance << "': " << message << "\n"; + failed_pointers.push_back(ptr); + } + +public: + std::vector failed_pointers; + + void reset() override + { + nlohmann::json_schema::basic_error_handler::reset(); + failed_pointers.clear(); + } +}; + +} // namespace + +int main(void) +{ + json_validator validator; + + try { + validator.set_root_schema(person_schema); // insert root-schema + } catch (const std::exception &e) { + std::cerr << "Validation of schema failed, here is why: " << e.what() << "\n"; + return EXIT_FAILURE; + } + + store_ptr_err_handler err; + + validator.validate({{"age", 42}, {"name", "John"}}, err); // OK + EXPECT_EQ(err.failed_pointers.size(), 0); + err.reset(); + + validator.validate({{"age", 42}}, err); // no name + + EXPECT_EQ(err.failed_pointers[0].to_string(), ""); + EXPECT_EQ(err.failed_pointers.size(), 1); + err.reset(); + + validator.validate({{"street", "Boulevard"}}, err); // no name and no age + EXPECT_EQ(err.failed_pointers[0].to_string(), ""); + EXPECT_EQ(err.failed_pointers[1].to_string(), ""); + EXPECT_EQ(err.failed_pointers.size(), 2); + err.reset(); + + validator.validate({{"age", 42}, {"name", 12}}, err); // name must be a string + EXPECT_EQ(err.failed_pointers[0].to_string(), "/name"); + EXPECT_EQ(err.failed_pointers.size(), 1); + err.reset(); + + validator.validate({ + {"age", 42}, + {"name", "John"}, + {"phones", {1234, "223"}}, + }, + err); // name must be a string + EXPECT_EQ(err.failed_pointers[0].to_string(), "/phones/1"); + EXPECT_EQ(err.failed_pointers.size(), 1); + err.reset(); + + return error_count; +}