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);