#pragma once #include #include #include #include #include #include #include #include #include #include #include namespace jvalidate::schema { class Node { private: std::string description_; std::unique_ptr default_{nullptr}; detail::Reference uri_; bool rejects_all_{false}; schema::Node const * reference_{nullptr}; std::map> 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 Node(ParserContext context); bool is_pure_reference() const { return reference_ && constraints_.empty() && not default_; } }; inline Version Node::schema_version(std::string_view url) { static std::map 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 friend class ParserContext; private: schema::Node accept_{true}; schema::Node reject_{false}; std::map anchors_; std::map cache_; std::map alias_cache_; public: explicit Schema(Adapter auto const & json) : Schema(json, schema_version(json)) {} template Schema(A const & json, schema::Version version) : schema::Node(ParserContext{.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 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 schema::Node const * fetch_schema(ParserContext 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 schema::Node const * ParserContext::resolve(std::string_view uri) const { return root.resolve(detail::Reference(uri), where, version); } template schema::Node const * ParserContext::node() const { return root.fetch_schema(*this); } } namespace jvalidate::schema { template Node::Node(ParserContext 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()(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)); } } } } }