aboutsummaryrefslogtreecommitdiff
path: root/include/jsoncons_ext/jsonschema/keyword_validator_factory.hpp
diff options
context:
space:
mode:
Diffstat (limited to 'include/jsoncons_ext/jsonschema/keyword_validator_factory.hpp')
-rw-r--r--include/jsoncons_ext/jsonschema/keyword_validator_factory.hpp556
1 files changed, 556 insertions, 0 deletions
diff --git a/include/jsoncons_ext/jsonschema/keyword_validator_factory.hpp b/include/jsoncons_ext/jsonschema/keyword_validator_factory.hpp
new file mode 100644
index 0000000..f538105
--- /dev/null
+++ b/include/jsoncons_ext/jsonschema/keyword_validator_factory.hpp
@@ -0,0 +1,556 @@
+// Copyright 2020 Daniel Parker
+// Distributed under the Boost license, Version 1.0.
+// (See accompanying file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
+
+// See https://github.com/danielaparker/jsoncons for latest version
+
+#ifndef JSONCONS_JSONSCHEMA_KEYWORD_VALIDATOR_FACTORY_HPP
+#define JSONCONS_JSONSCHEMA_KEYWORD_VALIDATOR_FACTORY_HPP
+
+#include <jsoncons/config/jsoncons_config.hpp>
+#include <jsoncons/uri.hpp>
+#include <jsoncons/json.hpp>
+#include <jsoncons_ext/jsonpointer/jsonpointer.hpp>
+#include <jsoncons_ext/jsonschema/subschema.hpp>
+#include <jsoncons_ext/jsonschema/keyword_validator.hpp>
+#include <jsoncons_ext/jsonschema/schema_draft7.hpp>
+#include <jsoncons_ext/jsonschema/schema_version.hpp>
+#include <cassert>
+#include <set>
+#include <sstream>
+#include <iostream>
+#include <cassert>
+#if defined(JSONCONS_HAS_STD_REGEX)
+#include <regex>
+#endif
+
+namespace jsoncons {
+namespace jsonschema {
+
+ template <class Json>
+ using uri_resolver = std::function<Json(const jsoncons::uri & /*id*/)>;
+
+ template <class Json>
+ class reference_schema : public keyword_validator<Json>
+ {
+ using validator_pointer = typename keyword_validator<Json>::self_pointer;
+
+ validator_pointer referred_schema_;
+
+ public:
+ reference_schema(const std::string& id)
+ : keyword_validator<Json>(id), referred_schema_(nullptr) {}
+
+ void set_referred_schema(validator_pointer target) { referred_schema_ = target; }
+
+ private:
+
+ void do_validate(const Json& instance,
+ const jsonpointer::json_pointer& instance_location,
+ error_reporter& reporter,
+ Json& patch) const override
+ {
+ if (!referred_schema_)
+ {
+ reporter.error(validation_output("",
+ this->absolute_keyword_location(),
+ instance_location.to_uri_fragment(),
+ "Unresolved schema reference " + this->absolute_keyword_location()));
+ return;
+ }
+
+ referred_schema_->validate(instance, instance_location, reporter, patch);
+ }
+
+ jsoncons::optional<Json> get_default_value(const jsonpointer::json_pointer& instance_location,
+ const Json& instance,
+ error_reporter& reporter) const override
+ {
+ if (!referred_schema_)
+ {
+ reporter.error(validation_output("",
+ this->absolute_keyword_location(),
+ instance_location.to_uri_fragment(),
+ "Unresolved schema reference " + this->absolute_keyword_location()));
+ return jsoncons::optional<Json>();
+ }
+
+ return referred_schema_->get_default_value(instance_location, instance, reporter);
+ }
+ };
+
+ template <class Json>
+ class keyword_validator_factory;
+
+ template <class Json>
+ class json_schema
+ {
+ using validator_pointer = typename keyword_validator<Json>::self_pointer;
+
+ friend class keyword_validator_factory<Json>;
+
+ std::vector<std::unique_ptr<keyword_validator<Json>>> subschemas_;
+ validator_pointer root_;
+ public:
+ json_schema(std::vector<std::unique_ptr<keyword_validator<Json>>>&& subschemas,
+ validator_pointer root)
+ : subschemas_(std::move(subschemas)), root_(root)
+ {
+ if (root_ == nullptr)
+ JSONCONS_THROW(schema_error("There is no root schema to validate an instance against"));
+ }
+
+ json_schema(const json_schema&) = delete;
+ json_schema(json_schema&&) = default;
+ json_schema& operator=(const json_schema&) = delete;
+ json_schema& operator=(json_schema&&) = default;
+
+ void validate(const Json& instance,
+ const jsonpointer::json_pointer& instance_location,
+ error_reporter& reporter,
+ Json& patch) const
+ {
+ JSONCONS_ASSERT(root_ != nullptr);
+ root_->validate(instance, instance_location, reporter, patch);
+ }
+ };
+
+ template <class Json>
+ struct default_uri_resolver
+ {
+ Json operator()(const jsoncons::uri& uri)
+ {
+ if (uri.path() == "/draft-07/schema")
+ {
+ return jsoncons::jsonschema::schema_draft7<Json>::get_schema();
+ }
+
+ JSONCONS_THROW(jsonschema::schema_error("Don't know how to load JSON Schema " + std::string(uri.base())));
+ }
+ };
+
+ template <class Json>
+ class keyword_validator_factory : public abstract_keyword_validator_factory<Json>
+ {
+ using validator_pointer = typename keyword_validator<Json>::self_pointer;
+
+ struct subschema_registry
+ {
+ std::map<std::string, validator_pointer> schemas; // schemas
+ std::map<std::string, reference_schema<Json>*> unresolved; // unresolved references
+ std::map<std::string, Json> unprocessed_keywords;
+ };
+
+ uri_resolver<Json> resolver_;
+ validator_pointer root_;
+
+ // Owns all schemas
+ std::vector<std::unique_ptr<keyword_validator<Json>>> subschemas_;
+
+ // Map location to subschema_registry
+ std::map<std::string, subschema_registry> subschema_registries_;
+
+ public:
+ keyword_validator_factory(uri_resolver<Json>&& resolver) noexcept
+
+ : resolver_(std::move(resolver))
+ {
+ }
+
+ keyword_validator_factory(const keyword_validator_factory&) = delete;
+ keyword_validator_factory& operator=(const keyword_validator_factory&) = delete;
+ keyword_validator_factory(keyword_validator_factory&&) = default;
+ keyword_validator_factory& operator=(keyword_validator_factory&&) = default;
+
+ std::shared_ptr<json_schema<Json>> get_schema()
+ {
+ return std::make_shared<json_schema<Json>>(std::move(subschemas_), root_);
+ }
+
+ validator_pointer make_required_validator(const std::vector<schema_location>& uris,
+ const std::vector<std::string>& r) override
+ {
+ auto sch_orig = jsoncons::make_unique<required_validator<Json>>(uris, r);
+ auto sch = sch_orig.get();
+ subschemas_.emplace_back(std::move(sch_orig));
+ return sch;
+ }
+
+ validator_pointer make_null_validator(const std::vector<schema_location>& uris) override
+ {
+ auto sch_orig = jsoncons::make_unique<null_validator<Json>>(uris);
+ auto sch = sch_orig.get();
+ subschemas_.emplace_back(std::move(sch_orig));
+ return sch;
+ }
+
+ validator_pointer make_true_validator(const std::vector<schema_location>& uris) override
+ {
+ auto sch_orig = jsoncons::make_unique<true_validator<Json>>(uris);
+ auto sch = sch_orig.get();
+ subschemas_.emplace_back(std::move(sch_orig));
+ return sch;
+ }
+
+ validator_pointer make_false_validator(const std::vector<schema_location>& uris) override
+ {
+ auto sch_orig = jsoncons::make_unique<false_validator<Json>>(uris);
+ auto sch = sch_orig.get();
+ subschemas_.emplace_back(std::move(sch_orig));
+ return sch;
+ }
+
+ validator_pointer make_object_validator(const Json& schema,
+ const std::vector<schema_location>& uris) override
+ {
+ auto sch_orig = jsoncons::make_unique<object_validator<Json>>(this, schema, uris);
+ auto sch = sch_orig.get();
+ subschemas_.emplace_back(std::move(sch_orig));
+ return sch;
+ }
+
+ validator_pointer make_array_validator(const Json& schema,
+ const std::vector<schema_location>& uris) override
+ {
+ auto sch_orig = jsoncons::make_unique<array_validator<Json>>(this, schema, uris);
+ auto sch = sch_orig.get();
+ subschemas_.emplace_back(std::move(sch_orig));
+ return sch;
+ }
+
+ validator_pointer make_string_validator(const Json& schema,
+ const std::vector<schema_location>& uris) override
+ {
+ auto sch_orig = jsoncons::make_unique<string_validator<Json>>(schema, uris);
+ auto sch = sch_orig.get();
+ subschemas_.emplace_back(std::move(sch_orig));
+ return sch;
+ }
+
+ validator_pointer make_boolean_validator(const std::vector<schema_location>& uris) override
+ {
+ auto sch_orig = jsoncons::make_unique<boolean_validator<Json>>(uris);
+ auto sch = sch_orig.get();
+ subschemas_.emplace_back(std::move(sch_orig));
+ return sch;
+ }
+
+ validator_pointer make_integer_validator(const Json& schema,
+ const std::vector<schema_location>& uris,
+ std::set<std::string>& keywords) override
+ {
+ auto sch_orig = jsoncons::make_unique<integer_validator<Json>>(schema, uris, keywords);
+ auto sch = sch_orig.get();
+ subschemas_.emplace_back(std::move(sch_orig));
+ return sch;
+ }
+
+ validator_pointer make_number_validator(const Json& schema,
+ const std::vector<schema_location>& uris,
+ std::set<std::string>& keywords) override
+ {
+ auto sch_orig = jsoncons::make_unique<number_validator<Json>>(schema, uris, keywords);
+ auto sch = sch_orig.get();
+ subschemas_.emplace_back(std::move(sch_orig));
+ return sch;
+ }
+
+ validator_pointer make_not_validator(const Json& schema,
+ const std::vector<schema_location>& uris) override
+ {
+ auto sch_orig = jsoncons::make_unique<not_validator<Json>>(this, schema, uris);
+ auto sch = sch_orig.get();
+ subschemas_.emplace_back(std::move(sch_orig));
+ return sch;
+ }
+
+ validator_pointer make_all_of_validator(const Json& schema,
+ const std::vector<schema_location>& uris) override
+ {
+ auto sch_orig = jsoncons::make_unique<combining_validator<Json,all_of_criterion<Json>>>(this, schema, uris);
+ auto sch = sch_orig.get();
+ subschemas_.emplace_back(std::move(sch_orig));
+ return sch;
+ }
+
+ validator_pointer make_any_of_validator(const Json& schema,
+ const std::vector<schema_location>& uris) override
+ {
+ auto sch_orig = jsoncons::make_unique<combining_validator<Json,any_of_criterion<Json>>>(this, schema, uris);
+ auto sch = sch_orig.get();
+ subschemas_.emplace_back(std::move(sch_orig));
+ return sch;
+ }
+
+ validator_pointer make_one_of_validator(const Json& schema,
+ const std::vector<schema_location>& uris) override
+ {
+ auto sch_orig = jsoncons::make_unique<combining_validator<Json,one_of_criterion<Json>>>(this, schema, uris);
+ auto sch = sch_orig.get();
+ subschemas_.emplace_back(std::move(sch_orig));
+ return sch;
+ }
+
+ validator_pointer make_type_validator(const Json& schema,
+ const std::vector<schema_location>& uris) override
+ {
+ auto sch_orig = jsoncons::make_unique<type_validator<Json>>(this, schema, uris);
+ auto sch = sch_orig.get();
+ subschemas_.emplace_back(std::move(sch_orig));
+ return sch;
+ }
+
+ validator_pointer make_keyword_validator(const Json& schema,
+ const std::vector<schema_location>& uris,
+ const std::vector<std::string>& keys) override
+ {
+ std::vector<schema_location> new_uris = update_uris(schema, uris, keys);
+
+ validator_pointer sch = nullptr;
+
+ switch (schema.type())
+ {
+ case json_type::bool_value:
+ if (schema.template as<bool>())
+ {
+ sch = make_true_validator(new_uris);
+ }
+ else
+ {
+ sch = make_false_validator(new_uris);
+ }
+ break;
+ case json_type::object_value:
+ {
+ auto it = schema.find("definitions");
+ if (it != schema.object_range().end())
+ {
+ for (const auto& def : it->value().object_range())
+ make_keyword_validator(def.value(), new_uris, {"definitions", def.key()});
+ }
+
+ it = schema.find("$ref");
+ if (it != schema.object_range().end()) // this schema is a reference
+ {
+ schema_location relative(it->value().template as<std::string>());
+ schema_location id = relative.resolve(new_uris.back());
+ sch = get_or_create_reference(id);
+ }
+ else
+ {
+ sch = make_type_validator(schema, new_uris);
+ }
+ break;
+ }
+ default:
+ JSONCONS_THROW(schema_error("invalid JSON-type for a schema for " + new_uris[0].string() + ", expected: boolean or object"));
+ break;
+ }
+
+ for (const auto& uri : new_uris)
+ {
+ insert(uri, sch);
+
+ if (schema.type() == json_type::object_value)
+ {
+ for (const auto& item : schema.object_range())
+ insert_unknown_keyword(uri, item.key(), item.value()); // save unknown keywords for later reference
+ }
+ }
+ return sch;
+ }
+
+ void load_root(const Json& sch)
+ {
+ if (sch.is_object())
+ {
+ auto it = sch.find("$schema");
+ if (it != sch.object_range().end())
+ {
+ auto sv = it->value().as_string_view();
+ if (!schema_version::contains(sv))
+ {
+ std::string message("Unsupported schema version ");
+ message.append(sv.data(), sv.size());
+ JSONCONS_THROW(schema_error(message));
+ }
+ }
+ }
+ load(sch);
+ }
+
+ void load(const Json& sch)
+ {
+ subschema_registries_.clear();
+ root_ = make_keyword_validator(sch, {{"#"}}, {});
+
+ // load all external schemas that have not already been loaded
+
+ std::size_t loaded_count = 0;
+ do
+ {
+ loaded_count = 0;
+
+ std::vector<std::string> locations;
+ for (const auto& item : subschema_registries_)
+ locations.push_back(item.first);
+
+ for (const auto& loc : locations)
+ {
+ if (subschema_registries_[loc].schemas.empty()) // registry for this file is empty
+ {
+ if (resolver_)
+ {
+ Json external_schema = resolver_(loc);
+ make_keyword_validator(external_schema, {{loc}}, {});
+ ++loaded_count;
+ }
+ else
+ {
+ JSONCONS_THROW(schema_error("External schema reference '" + loc + "' needs to be loaded, but no resolver provided"));
+ }
+ }
+ }
+ }
+ while (loaded_count > 0);
+
+ for (const auto &file : subschema_registries_)
+ {
+ if (!file.second.unresolved.empty())
+ {
+ JSONCONS_THROW(schema_error("after all files have been parsed, '" +
+ (file.first == "" ? "<root>" : file.first) +
+ "' has still undefined references."));
+ }
+ }
+ }
+
+ private:
+
+ void insert(const schema_location& uri, validator_pointer s)
+ {
+ auto& file = get_or_create_file(std::string(uri.base()));
+ auto schemas_it = file.schemas.find(std::string(uri.fragment()));
+ if (schemas_it != file.schemas.end())
+ {
+ JSONCONS_THROW(schema_error("schema with " + uri.string() + " already inserted"));
+ return;
+ }
+
+ file.schemas.insert({std::string(uri.fragment()), s});
+
+ // is there an unresolved reference to this newly inserted schema?
+ auto unresolved_it = file.unresolved.find(std::string(uri.fragment()));
+ if (unresolved_it != file.unresolved.end())
+ {
+ unresolved_it->second->set_referred_schema(s);
+ file.unresolved.erase(unresolved_it);
+
+ }
+ }
+
+ void insert_unknown_keyword(const schema_location& uri,
+ const std::string& key,
+ const Json& value)
+ {
+ auto &file = get_or_create_file(std::string(uri.base()));
+ auto new_u = uri.append(key);
+ schema_location new_uri(new_u);
+
+ if (new_uri.has_fragment() && !new_uri.has_identifier())
+ {
+ auto fragment = std::string(new_uri.fragment());
+ // 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(fragment);
+ if (unresolved != file.unresolved.end())
+ make_keyword_validator(value, {{new_uri}}, {});
+ else // no, nothing ref'd it, keep for later
+ file.unprocessed_keywords[fragment] = value;
+
+ // recursively add possible subschemas of unknown keywords
+ if (value.type() == json_type::object_value)
+ for (const auto& subsch : value.object_range())
+ {
+ insert_unknown_keyword(new_uri, subsch.key(), subsch.value());
+ }
+ }
+ }
+
+ validator_pointer get_or_create_reference(const schema_location& uri)
+ {
+ auto &file = get_or_create_file(std::string(uri.base()));
+
+ // a schema already exists
+ auto sch = file.schemas.find(std::string(uri.fragment()));
+ if (sch != file.schemas.end())
+ return sch->second;
+
+ // referencing an unknown keyword, turn it into schema
+ //
+ // an unknown keyword can only be referenced by a JSONPointer,
+ // not by a plain name identifier
+ if (uri.has_fragment() && !uri.has_identifier())
+ {
+ std::string fragment = std::string(uri.fragment());
+ auto unprocessed_keywords_it = file.unprocessed_keywords.find(fragment);
+ if (unprocessed_keywords_it != file.unprocessed_keywords.end())
+ {
+ auto &subsch = unprocessed_keywords_it->second;
+ auto s = make_keyword_validator(subsch, {{uri}}, {}); // A JSON Schema MUST be an object or a boolean.
+ file.unprocessed_keywords.erase(unprocessed_keywords_it);
+ return s;
+ }
+ }
+
+ // get or create a reference_schema
+ auto ref = file.unresolved.find(std::string(uri.fragment()));
+ if (ref != file.unresolved.end())
+ {
+ return ref->second; // unresolved, use existing reference
+ }
+ else
+ {
+ auto orig = jsoncons::make_unique<reference_schema<Json>>(uri.string());
+ auto p = file.unresolved.insert(ref,
+ {std::string(uri.fragment()), orig.get()})
+ ->second; // unresolved, create new reference
+
+ subschemas_.emplace_back(std::move(orig));
+ return p;
+ }
+ }
+
+ subschema_registry& get_or_create_file(const std::string& loc)
+ {
+ auto file = subschema_registries_.find(loc);
+ if (file != subschema_registries_.end())
+ return file->second;
+ else
+ return subschema_registries_.insert(file, {loc, {}})->second;
+ }
+
+ };
+
+ template <class Json>
+ std::shared_ptr<json_schema<Json>> make_schema(const Json& schema)
+ {
+ keyword_validator_factory<Json> loader{default_uri_resolver<Json>()};
+ loader.load_root(schema);
+
+ return loader.get_schema();
+ }
+
+ template <class Json,class URIResolver>
+ typename std::enable_if<type_traits::is_unary_function_object_exact<URIResolver,Json,std::string>::value,std::shared_ptr<json_schema<Json>>>::type
+ make_schema(const Json& schema, const URIResolver& resolver)
+ {
+ keyword_validator_factory<Json> loader(resolver);
+ loader.load_root(schema);
+
+ return loader.get_schema();
+ }
+
+} // namespace jsonschema
+} // namespace jsoncons
+
+#endif // JSONCONS_JSONSCHEMA_SCHEMA_LOADER_HPP