From 1e50a93626ae331ad066f3e8c1558bc1ceff9942 Mon Sep 17 00:00:00 2001 From: Patrick Boettcher Date: Sun, 13 Jan 2019 18:05:44 +0100 Subject: [PATCH] Fix #44: format-checker-callback was not used, is now --- src/json-validator.cpp | 366 +++++++++--------- test/JSON-Schema-Test-Suite/CMakeLists.txt | 25 +- .../json-schema-test.cpp | 2 + 3 files changed, 214 insertions(+), 179 deletions(-) diff --git a/src/json-validator.cpp b/src/json-validator.cpp index e902052..91b7e22 100644 --- a/src/json-validator.cpp +++ b/src/json-validator.cpp @@ -46,6 +46,185 @@ public: std::vector uris); }; +class schema_ref : public schema +{ + const std::string id_; + std::shared_ptr target_; + + void validate(const json &instance, basic_error_handler &e) const final + { + if (target_) + target_->validate(instance, e); + else + e.error("", instance, "unresolved schema-reference " + id_); + } + +public: + schema_ref(const std::string &id, root_schema *root) + : schema(root), id_(id) {} + + const std::string &id() const { return id_; } + void set_target(std::shared_ptr target) { target_ = target; } +}; + +} // namespace + +namespace nlohmann +{ +namespace json_schema +{ + +class root_schema : public schema +{ + std::function loader_; + std::function format_check_; + + std::shared_ptr root_; + + struct schema_file { + std::map> schemas; + std::map> unresolved; // contains all unresolved references from any other file seen during parsing + json unknown_keywords; + }; + + // location as key + std::map files_; + + schema_file &get_or_create_file(const std::string &loc) + { + auto file = files_.lower_bound(loc); + if (file != files_.end() && !(files_.key_comp()(loc, file->first))) + return file->second; + else + return files_.insert(file, {loc, {}})->second; + } + +public: + root_schema(std::function loader, + std::function format) + : schema(this), loader_(loader), format_check_(format) {} + + std::function &format_check() { return format_check_; } + + void insert(const json_uri &uri, const std::shared_ptr &s) + { + // std::cout << "adding schema '" << uri << "' '" << uri.location() << "'\n"; + + auto &file = get_or_create_file(uri.location()); + auto schema = file.schemas.lower_bound(uri.pointer()); + if (schema != file.schemas.end() && !(file.schemas.key_comp()(uri.pointer(), schema->first))) { + throw std::invalid_argument("schema with " + uri.to_string() + " already inserted\n"); + return; + } + + file.schemas.insert({uri.pointer(), s}); + + // was someone referencing this newly inserted schema? + auto unresolved = file.unresolved.find(uri.pointer()); + // std::cout << "resolving schemas looking for '" << uri.pointer() << "' in " << uri.location() << "\n"; + if (unresolved != file.unresolved.end()) { + // std::cout << " --> resolved!!\n"; + unresolved->second->set_target(s); + file.unresolved.erase(unresolved); + } + } + + void insert_unknown_keyword(const json_uri &uri, const std::string &key, json &value) + { + auto &file = get_or_create_file(uri.location()); + auto new_uri = uri.append(key); + auto pointer = new_uri.pointer(); + + // std::cout << "inserting unknown " << new_uri << " '" << pointer << "'\n"; + + // is there a reference looking for this unknown-keyword, which is thus no longer a unknown keyword but a schema + auto unresolved = file.unresolved.find(pointer); + if (unresolved != file.unresolved.end()) + schema::make(value, this, {}, {{new_uri}}); + else // no, nothing ref'd it + file.unknown_keywords[pointer] = value; + } + + std::shared_ptr get_or_create_ref(const json_uri &uri) + { + auto &file = get_or_create_file(uri.location()); + + // existing schema + auto schema = file.schemas.find(uri.pointer()); + if (schema != file.schemas.end()) + return schema->second; + + // referencing an unknown keyword, turn it into schema + try { + auto &subschema = file.unknown_keywords.at(uri.pointer()); + auto s = schema::make(subschema, this, {}, {{uri}}); + file.unknown_keywords.erase(uri.pointer()); + return s; + } catch (...) { + } + + // get or create a schema_ref + // std::cout << "using or creating a reference to " << uri << "\n"; + auto r = file.unresolved.lower_bound(uri.pointer()); + if (r != file.unresolved.end() && !(file.unresolved.key_comp()(uri.pointer(), r->first))) { + // std::cout << " --> using existing ref\n"; + return r->second; + } else { + // std::cout << " --> creating a new ref\n"; + return file.unresolved.insert(r, + {uri.pointer(), std::make_shared(uri.to_string(), this)}) + ->second; + } + } + + void set_root_schema(json schema) + { + root_ = schema::make(schema, this, {}, {{"#"}}); + + // load all files which have not yet been loaded + do { + bool new_schema_loaded = false; + + // files_ is modified during parsing, iterators are invalidated + std::vector locations; + for (auto &file : files_) + locations.push_back(file.first); + + for (auto &loc : locations) { + if (files_[loc].schemas.size() == 0) { // nothing has been loaded for this file + if (loader_) { + json sch; + + loader_(loc, sch); + + schema::make(sch, this, {}, {{loc}}); + new_schema_loaded = true; + } else { + throw std::invalid_argument("external schema reference '" + loc + "' needs loading, but no loader callback given\n"); + } + } + } + + if (!new_schema_loaded) // if no new schema loaded, no need to try again + break; + } while (1); + } + + void validate(const json &instance, basic_error_handler &e) const final + { + if (root_) + root_->validate(instance, e); + else + e.error("", "", "no root schema has yet been set for validating an instance."); + } +}; + +} // namespace json_schema +} // namespace nlohmann + +namespace +{ + class logical_not : public schema { std::shared_ptr subschema_; @@ -319,7 +498,6 @@ class string : public schema #endif std::pair format_; - std::function format_check_ = nullptr; std::size_t utf8_length(const std::string &s) const { @@ -355,10 +533,15 @@ class string : public schema #endif if (format_.first) { - if (format_check_ == nullptr) + 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); - else - format_check_(format_.second, instance); + else { + try { + root_->format_check()(format_.second, instance); + } catch (const std::exception &ex) { + e.error("", instance, std::string("Format-checking failed: ") + ex.what()); + } + } } } @@ -786,27 +969,6 @@ public: } }; -class schema_ref : public schema -{ - const std::string id_; - std::shared_ptr target_; - - void validate(const json &instance, basic_error_handler &e) const final - { - if (target_) - target_->validate(instance, e); - else - e.error("", instance, "unresolved schema-reference " + id_); - } - -public: - schema_ref(const std::string &id, root_schema *root) - : schema(root), id_(id) {} - - const std::string &id() const { return id_; } - void set_target(std::shared_ptr target) { target_ = target; } -}; - std::shared_ptr type_schema::make(json &schema, json::value_t type, root_schema *root, @@ -837,158 +999,6 @@ std::shared_ptr type_schema::make(json &schema, return nullptr; } } // namespace - -namespace nlohmann -{ -namespace json_schema -{ - -class root_schema : public schema -{ - std::function loader_; - std::function format_; - - std::shared_ptr root_; - - struct schema_file { - std::map> schemas; - std::map> unresolved; // contains all unresolved references from any other file seen during parsing - json unknown_keywords; - }; - - // location as key - std::map files_; - - schema_file &get_or_create_file(const std::string &loc) - { - auto file = files_.lower_bound(loc); - if (file != files_.end() && !(files_.key_comp()(loc, file->first))) - return file->second; - else - return files_.insert(file, {loc, {}})->second; - } - -public: - root_schema(std::function loader, - std::function format) - : schema(this), loader_(loader), format_(format) {} - - void insert(const json_uri &uri, const std::shared_ptr &s) - { - // std::cout << "adding schema '" << uri << "' '" << uri.location() << "'\n"; - - auto &file = get_or_create_file(uri.location()); - auto schema = file.schemas.lower_bound(uri.pointer()); - if (schema != file.schemas.end() && !(file.schemas.key_comp()(uri.pointer(), schema->first))) { - throw std::invalid_argument("schema with " + uri.to_string() + " already inserted\n"); - return; - } - - file.schemas.insert({uri.pointer(), s}); - - // was someone referencing this newly inserted schema? - auto unresolved = file.unresolved.find(uri.pointer()); - // std::cout << "resolving schemas looking for '" << uri.pointer() << "' in " << uri.location() << "\n"; - if (unresolved != file.unresolved.end()) { - // std::cout << " --> resolved!!\n"; - unresolved->second->set_target(s); - file.unresolved.erase(unresolved); - } - } - - void insert_unknown_keyword(const json_uri &uri, const std::string &key, json &value) - { - auto &file = get_or_create_file(uri.location()); - auto new_uri = uri.append(key); - auto pointer = new_uri.pointer(); - - // std::cout << "inserting unknown " << new_uri << " '" << pointer << "'\n"; - - // is there a reference looking for this unknown-keyword, which is thus no longer a unknown keyword but a schema - auto unresolved = file.unresolved.find(pointer); - if (unresolved != file.unresolved.end()) - schema::make(value, this, {}, {{new_uri}}); - else // no, nothing ref'd it - file.unknown_keywords[pointer] = value; - } - - std::shared_ptr get_or_create_ref(const json_uri &uri) - { - auto &file = get_or_create_file(uri.location()); - - // existing schema - auto schema = file.schemas.find(uri.pointer()); - if (schema != file.schemas.end()) - return schema->second; - - // referencing an unknown keyword, turn it into schema - try { - auto &subschema = file.unknown_keywords.at(uri.pointer()); - auto s = schema::make(subschema, this, {}, {{uri}}); - file.unknown_keywords.erase(uri.pointer()); - return s; - } catch (...) { - } - - // get or create a schema_ref - // std::cout << "using or creating a reference to " << uri << "\n"; - auto r = file.unresolved.lower_bound(uri.pointer()); - if (r != file.unresolved.end() && !(file.unresolved.key_comp()(uri.pointer(), r->first))) { - // std::cout << " --> using existing ref\n"; - return r->second; - } else { - // std::cout << " --> creating a new ref\n"; - return file.unresolved.insert(r, - {uri.pointer(), std::make_shared(uri.to_string(), this)}) - ->second; - } - } - - void set_root_schema(json schema) - { - root_ = schema::make(schema, this, {}, {{"#"}}); - - // load all files which have not yet been loaded - do { - bool new_schema_loaded = false; - - // files_ is modified during parsing, iterators are invalidated - std::vector locations; - for (auto &file : files_) - locations.push_back(file.first); - - for (auto &loc : locations) { - if (files_[loc].schemas.size() == 0) { // nothing has been loaded for this file - if (loader_) { - json sch; - - loader_(loc, sch); - - schema::make(sch, this, {}, {{loc}}); - new_schema_loaded = true; - } else { - throw std::invalid_argument("external schema reference '" + loc + "' needs loading, but no loader callback given\n"); - } - } - } - - if (!new_schema_loaded) // if no new schema loaded, no need to try again - break; - } while (1); - } - - void validate(const json &instance, basic_error_handler &e) const final - { - if (root_) - root_->validate(instance, e); - else - e.error("", "", "no root schema has yet been set for validating an instance."); - } -}; - -} // namespace json_schema -} // namespace nlohmann - namespace { @@ -1045,7 +1055,7 @@ std::shared_ptr schema::make(json &schema, return nullptr; // TODO error/throw? when schema is invalid } - for (auto &uri : uris) { // for all URI reference this schema + for (auto &uri : uris) { // for all URI references this schema root->insert(uri, sch); if (schema.type() == json::value_t::object) diff --git a/test/JSON-Schema-Test-Suite/CMakeLists.txt b/test/JSON-Schema-Test-Suite/CMakeLists.txt index 56e2bc1..721f7e7 100644 --- a/test/JSON-Schema-Test-Suite/CMakeLists.txt +++ b/test/JSON-Schema-Test-Suite/CMakeLists.txt @@ -33,6 +33,7 @@ if(JSON_SCHEMA_TEST_SUITE_PATH) if (JSON_SCHEMA_ENABLE_OPTIONAL_TESTS) file(GLOB OPT_TEST_FILES ${JSON_SCHEMA_TEST_SUITE_PATH}/tests/${DRAFT}/optional/*.json) + file(GLOB FORMAT_TEST_FILES ${JSON_SCHEMA_TEST_SUITE_PATH}/tests/${DRAFT}/optional/format/*.json) foreach(TEST_FILE ${OPT_TEST_FILES}) get_filename_component(TEST_NAME ${TEST_FILE} NAME_WE) @@ -40,11 +41,33 @@ if(JSON_SCHEMA_TEST_SUITE_PATH) COMMAND ${PIPE_IN_TEST_SCRIPT} $ ${TEST_FILE}) endforeach() - # some optional tests will fail as well. + foreach(TEST_FILE ${FORMAT_TEST_FILES}) + get_filename_component(TEST_NAME ${TEST_FILE} NAME_WE) + add_test(NAME "${JSON_SCHEMA_TEST_PREFIX}::Optional::Format::${TEST_NAME}" + COMMAND ${PIPE_IN_TEST_SCRIPT} $ ${TEST_FILE}) + endforeach() + + # some optional tests will fail set_tests_properties( JSON-Suite::Optional::bignum JSON-Suite::Optional::content JSON-Suite::Optional::zeroTerminatedFloats + JSON-Suite::Optional::ecmascript-regex + + JSON-Suite::Optional::Format::date-time + JSON-Suite::Optional::Format::date + JSON-Suite::Optional::Format::email + JSON-Suite::Optional::Format::idn-email + JSON-Suite::Optional::Format::idn-hostname + JSON-Suite::Optional::Format::iri-reference + JSON-Suite::Optional::Format::iri + JSON-Suite::Optional::Format::json-pointer + JSON-Suite::Optional::Format::relative-json-pointer + JSON-Suite::Optional::Format::time + JSON-Suite::Optional::Format::uri-reference + JSON-Suite::Optional::Format::uri-template + JSON-Suite::Optional::Format::uri + PROPERTIES WILL_FAIL ON) endif() diff --git a/test/JSON-Schema-Test-Suite/json-schema-test.cpp b/test/JSON-Schema-Test-Suite/json-schema-test.cpp index 67564c0..8c10c02 100644 --- a/test/JSON-Schema-Test-Suite/json-schema-test.cpp +++ b/test/JSON-Schema-Test-Suite/json-schema-test.cpp @@ -23,6 +23,7 @@ static void format_check(const std::string &format, const std::string &value) std::regex re(R"(^([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])(\.([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]{0,61}[a-zA-Z0-9]))*$)"); if (!std::regex_match(value, re)) throw std::invalid_argument(value + " is not a valid hostname."); + } else if (format == "ipv4") { std::regex re(R"(^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$)"); if (!std::regex_match(value, re)) @@ -32,6 +33,7 @@ static void format_check(const std::string &format, const std::string &value) std::regex re(R"((([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])))"); if (!std::regex_match(value, re)) throw std::invalid_argument(value + " is not a IPv6-address."); + } else if (format == "regex") { try { std::regex re(value, std::regex::ECMAScript);