| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224 |
- #pragma once
- #include <memory>
- #include <unordered_map>
- #include <vector>
- #include <jvalidate/adapter.h>
- #include <jvalidate/constraint.h>
- #include <jvalidate/detail/expect.h>
- #include <jvalidate/detail/pointer.h>
- #include <jvalidate/detail/reference.h>
- #include <jvalidate/enum.h>
- #include <jvalidate/forward.h>
- #include <jvalidate/parser_context.h>
- namespace jvalidate::schema {
- class Node {
- private:
- std::string description_;
- std::unique_ptr<adapter::Const const> default_{nullptr};
- detail::Reference uri_;
- bool rejects_all_{false};
- schema::Node const * reference_{nullptr};
- std::map<std::string, std::unique_ptr<constraint::Constraint>> constraints_{};
- protected:
- static Version schema_version(std::string_view url);
- static Version schema_version(Adapter auto const & json);
- static Version schema_version(Adapter auto const & json, Version default_version);
- public:
- Node(bool rejects_all = false) : rejects_all_(rejects_all) {}
- template <Adapter A> Node(ParserContext<A> context);
- bool is_pure_reference() const { return reference_ && constraints_.empty() && not default_; }
- };
- inline Version Node::schema_version(std::string_view url) {
- static std::map<std::string_view, Version> const g_schema_ids{
- {"http://json-schema.org/draft-04/schema", Version::Draft04},
- {"http://json-schema.org/draft-06/schema", Version::Draft06},
- {"http://json-schema.org/draft-07/schema", Version::Draft07},
- {"http://json-schema.org/draft/2019-09/schema", Version::Draft2019_09},
- {"http://json-schema.org/draft/2020-12/schema", Version::Draft2020_12},
- };
- if (url.ends_with('#')) {
- url.remove_suffix(1);
- }
- auto it = g_schema_ids.find(url);
- EXPECT_T(it != g_schema_ids.end(), std::invalid_argument, url);
- return it->second;
- }
- Version Node::schema_version(Adapter auto const & json) {
- EXPECT(json.type() == adapter::Type::Object);
- EXPECT(json.as_object().contains("$schema"));
- auto const & schema = json.as_object()["$schema"];
- EXPECT(schema.type() == adapter::Type::String);
- return schema_version(schema.as_string());
- }
- Version Node::schema_version(Adapter auto const & json, Version default_version) {
- RETURN_UNLESS(json.type() == adapter::Type::Object, default_version);
- RETURN_UNLESS(json.as_object().contains("$schema"), default_version);
- auto const & schema = json.as_object()["$schema"];
- RETURN_UNLESS(schema.type() == adapter::Type::String, default_version);
- return schema_version(schema.as_string());
- }
- }
- namespace jvalidate {
- class Schema : public schema::Node {
- private:
- friend class schema::Node;
- template <Adapter A> friend class ParserContext;
- private:
- schema::Node accept_{true};
- schema::Node reject_{false};
- std::map<std::string, detail::Reference> anchors_;
- std::map<detail::Reference, schema::Node> cache_;
- std::map<detail::Reference, schema::Node const *> alias_cache_;
- public:
- explicit Schema(Adapter auto const & json) : Schema(json, schema_version(json)) {}
- template <Adapter A>
- Schema(A const & json, schema::Version version)
- : schema::Node(ParserContext<A>{.root = *this, .schema = json, .version = version}) {}
- private:
- void anchor(std::string anchor, detail::Reference const & from) {}
- schema::Node const * alias(detail::Reference const & where, schema::Node const * schema) {
- EXPECT_M(alias_cache_.try_emplace(where, schema).second,
- "more than one schema found with uri " << where);
- return schema;
- }
- template <Adapter A>
- schema::Node const * resolve(detail::Reference ref, detail::Reference const & from,
- schema::Version default_version) {
- // Special case if the root-level document does not have an $id property
- if (ref == detail::Reference() && ref.anchor() == from.anchor()) {
- return this;
- }
- if (auto it = anchors_.find(ref.anchor()); it != anchors_.end()) {
- ref = it->second / ref.pointer();
- }
- EXPECT_M(ref.anchor().back() != '#', "Unmatched anchor: " << ref.anchor());
- if (auto it = alias_cache_.find(ref); it != alias_cache_.end()) {
- return it->second;
- }
- throw;
- }
- template <Adapter A> schema::Node const * fetch_schema(ParserContext<A> const & context) {
- adapter::Type const type = context.schema.type();
- if (type == adapter::Type::Boolean) {
- // TODO: Legal Context...
- return alias(context.where, context.schema.as_boolean() ? &accept_ : &reject_);
- }
- EXPECT(type == adapter::Type::Object);
- if (context.schema.object_size() == 0) {
- return alias(context.where, &accept_);
- }
- auto [it, created] = cache_.try_emplace(context.where, context);
- EXPECT_M(created, "more than one schema found with uri " << context.where);
- schema::Node const * node = &it->second;
- // Special Case - if the only is the reference constraint, then we don't need
- // to store it uniquely. Draft2019_09 supports directly extending a $ref schema
- // in the same schema, instead of requiring an allOf clause.
- if (node->is_pure_reference()) {
- cache_.erase(it);
- return alias(context.where, node);
- }
- return alias(context.where, node);
- }
- };
- template <Adapter A> schema::Node const * ParserContext<A>::resolve(std::string_view uri) const {
- return root.resolve<A>(detail::Reference(uri), where, version);
- }
- template <Adapter A> schema::Node const * ParserContext<A>::node() const {
- return root.fetch_schema(*this);
- }
- }
- namespace jvalidate::schema {
- template <Adapter A> Node::Node(ParserContext<A> context) {
- EXPECT(context.schema.type() == adapter::Type::Object);
- auto const schema = context.schema.as_object();
- if (schema.contains("$schema")) {
- // At any point in the schema, we're allowed to change versions
- // This means that we're not version-locked to the latest grammar
- // (which is especially important for some breaking changes)
- context.version = schema_version(context.schema);
- }
- if (schema.contains("$id")) {
- context.root.alias(detail::Reference(schema["$id"].as_string(), false), this);
- }
- // TODO(sjaffe): $recursiveAnchor, $dynamicAnchor, $recursiveRef, $dynamicRef
- if (schema.contains("$anchor")) {
- // Create an anchor mapping using the current document and the anchor
- // string. There's no need for special validation/chaining here, because
- // {@see Schema::resolve} will turn all $ref/$dynamicRef anchors into
- // their fully-qualified path.
- context.root.anchor(context.where.anchor() + schema["$anchor"].as_string(), context.where);
- }
- bool has_reference;
- if ((has_reference = schema.contains("$ref"))) {
- auto ref = schema["$ref"];
- EXPECT(ref.type() == adapter::Type::String);
- reference_ = context.resolve(ref.as_string());
- }
- if (schema.contains("default")) {
- default_ = schema["default"].freeze();
- }
- if (schema.contains("description")) {
- description_ = schema["description"].as_string();
- }
- for (auto [key, subschema] : schema) {
- // Using a constraint store allows overriding certain rules, or the creation
- // of user-defined extention vocabularies.
- // TODO(sjaffe): Pass this around instead
- if (auto make_constraint = ConstraintFactory<A>()(key, context.version)) {
- EXPECT_M(not has_reference || context.version >= Version::Draft2019_09,
- "Cannot directly extend $ref schemas before Draft2019-09");
- // A constraint may return null if it is not applicable - but otherwise
- // well-formed. For example, before Draft-06 "exclusiveMaximum" was a
- // modifier property for "maximum", and not a unique constaint on its own.
- // Therefore, we parse it alongside parsing "maximum", and could return
- // nullptr when requesting a constraint pointer for "exclusiveMaximum".
- if (auto constraint = make_constraint(context.child(subschema, key))) {
- constraints_.emplace(key, std::move(constraint));
- }
- }
- }
- }
- }
|