Przeglądaj źródła

feat: add support for loading custom vocabularies

Sam Jaffe 1 rok temu
rodzic
commit
a016b3eb1b

+ 67 - 8
include/jvalidate/detail/reference_manager.h

@@ -1,23 +1,25 @@
 #pragma once
 
 #include <functional>
-#include <jvalidate/detail/vocabulary.h>
 #include <map>
 #include <set>
 #include <unordered_map>
 
 #include <jvalidate/detail/anchor.h>
 #include <jvalidate/detail/dynamic_reference_context.h>
+#include <jvalidate/detail/expect.h>
 #include <jvalidate/detail/on_block_exit.h>
 #include <jvalidate/detail/out.h>
 #include <jvalidate/detail/parser_context.h>
 #include <jvalidate/detail/pointer.h>
 #include <jvalidate/detail/reference.h>
 #include <jvalidate/detail/reference_cache.h>
+#include <jvalidate/detail/vocabulary.h>
 #include <jvalidate/document_cache.h>
 #include <jvalidate/enum.h>
 #include <jvalidate/forward.h>
 #include <jvalidate/uri.h>
+#include <unordered_set>
 
 namespace jvalidate::detail {
 template <Adapter A> class ReferenceManager {
@@ -25,11 +27,20 @@ public:
   using Keywords = std::unordered_map<std::string_view, std::set<schema::Wraps>>;
 
 private:
+  static inline std::map<std::string_view, schema::Version> const g_schema_ids{
+      {"json-schema.org/draft-04/schema", schema::Version::Draft04},
+      {"json-schema.org/draft-06/schema", schema::Version::Draft06},
+      {"json-schema.org/draft-07/schema", schema::Version::Draft07},
+      {"json-schema.org/draft/2019-09/schema", schema::Version::Draft2019_09},
+      {"json-schema.org/draft/2020-12/schema", schema::Version::Draft2020_12},
+  };
+
   ConstraintFactory<A> const & constraints_;
   DocumentCache<A> & external_;
 
   ReferenceCache references_;
   std::map<schema::Version, Vocabulary<A>> vocabularies_;
+  std::map<URI, Vocabulary<A>> user_vocabularies_;
   std::map<RootReference, A> roots_;
   std::map<URI, std::map<Anchor, Reference>> dynamic_anchors_;
 
@@ -39,7 +50,7 @@ public:
   ReferenceManager(DocumentCache<A> & external, A const & root, schema::Version version,
                    ConstraintFactory<A> const & constraints)
       : external_(external), constraints_(constraints), roots_{{{}, root}} {
-    prime(root, {}, vocab(version));
+    prime(root, {}, &vocab(version));
   }
 
   Vocabulary<A> const & vocab(schema::Version version) {
@@ -49,6 +60,33 @@ public:
     return vocabularies_.at(version);
   }
 
+  Vocabulary<A> const & vocab(URI schema) {
+    if (auto it = g_schema_ids.find(schema.resource()); it != g_schema_ids.end()) {
+      return vocab(it->second);
+    }
+
+    if (auto it = user_vocabularies_.find(schema); it != user_vocabularies_.end()) {
+      return it->second;
+    }
+
+    std::optional<A> external = external_.try_load(schema);
+    EXPECT_M(external.has_value(), "Unable to load external meta-schema " << schema);
+    EXPECT_M(external->type() == adapter::Type::Object, "meta-schema must be an object");
+
+    auto metaschema = external->as_object();
+    EXPECT_M(metaschema.contains("$schema"), "meta-schema must reference an");
+
+    // Initialize first to prevent recursion
+    Vocabulary<A> & parent = user_vocabularies_[schema];
+    parent = vocab(URI(metaschema["$schema"].as_string()));
+
+    if (metaschema.contains("$vocabulary")) {
+      parent.restrict(extract_keywords(metaschema["$vocabulary"].as_object()));
+    }
+
+    return parent;
+  }
+
   auto dynamic_scope(Reference const & ref) {
     URI const uri =
         ref.pointer().empty() ? ref.uri() : references_.relative_to_nearest_anchor(ref).uri();
@@ -67,7 +105,7 @@ public:
 
     // TODO(samjaffe): Change Versions if needed...
     references_.emplace(ref.uri());
-    prime(*external, ref, vocab(version));
+    prime(*external, ref, &vocab(version));
 
     // May have a sub-id that we map to
     if (auto it = roots_.find(ref.root()); it != roots_.end()) {
@@ -142,15 +180,20 @@ private:
     return active_dynamic_anchors_.lookup(uri, ref.anchor());
   }
 
-  void prime(Adapter auto const & json, Reference where, Vocabulary<A> const & vocab) {
+  void prime(Adapter auto const & json, Reference where, Vocabulary<A> const * vocab) {
     if (json.type() != adapter::Type::Object) {
       return;
     }
 
-    canonicalize(where, vocab.version(), json);
+    canonicalize(where, vocab->version(), json);
+
+    auto schema = json.as_object();
+    if (schema.contains("$schema")) {
+      vocab = &this->vocab(URI(schema["$schema"].as_string()));
+    }
 
-    for (auto const & [key, value] : json.as_object()) {
-      if (not vocab.is_keyword(key)) {
+    for (auto const & [key, value] : schema) {
+      if (not vocab->is_keyword(key)) {
         continue;
       }
       switch (value.type()) {
@@ -163,7 +206,7 @@ private:
         break;
       }
       case adapter::Type::Object:
-        if (not vocab.is_property_keyword(key)) {
+        if (not vocab->is_property_keyword(key)) {
           prime(value, where / key, vocab);
           break;
         }
@@ -235,5 +278,21 @@ private:
       }
     }
   }
+
+  std::unordered_set<std::string> extract_keywords(ObjectAdapter auto const & vocabularies) const {
+    std::unordered_set<std::string> keywords;
+    for (auto [vocab, enabled] : vocabularies) {
+      if (not enabled.as_boolean()) {
+        continue;
+      }
+
+      vocab.replace(vocab.find("/vocab/"), 7, "/meta/");
+      auto vocab_object = external_.try_load(URI(vocab));
+      for (auto const & [keyword, _] : vocab_object->as_object()["properties"].as_object()) {
+        keywords.insert(keyword);
+      }
+    }
+    return keywords;
+  }
 };
 }

+ 0 - 49
include/jvalidate/detail/version.h

@@ -1,49 +0,0 @@
-#pragma once
-
-#include <map>
-
-#include <jvalidate/detail/expect.h>
-#include <jvalidate/enum.h>
-
-namespace jvalidate::detail {
-inline schema::Version version(std::string_view url) {
-  static std::map<std::string_view, schema::Version> const g_schema_ids{
-      {"json-schema.org/draft-04/schema", schema::Version::Draft04},
-      {"json-schema.org/draft-06/schema", schema::Version::Draft06},
-      {"json-schema.org/draft-07/schema", schema::Version::Draft07},
-      {"json-schema.org/draft/2019-09/schema", schema::Version::Draft2019_09},
-      {"json-schema.org/draft/2020-12/schema", schema::Version::Draft2020_12},
-  };
-
-  if (url.ends_with('#')) {
-    url.remove_suffix(1);
-  }
-  if (url.starts_with("http://") || url.starts_with("https://")) {
-    url.remove_prefix(url.find(':') + 3);
-  }
-
-  auto it = g_schema_ids.find(url);
-  EXPECT_T(it != g_schema_ids.end(), std::invalid_argument, url);
-  return it->second;
-}
-
-schema::Version 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 version(schema.as_string());
-}
-
-schema::Version version(Adapter auto const & json, schema::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 version(schema.as_string());
-}
-}

+ 11 - 0
include/jvalidate/detail/vocabulary.h

@@ -47,9 +47,20 @@ private:
                                                          "unevaluatedProperties"};
 
 public:
+  Vocabulary() = default;
   Vocabulary(schema::Version version, std::unordered_map<std::string_view, MakeConstraint> make)
       : version_(version), make_(std::move(make)) {}
 
+  void restrict(std::unordered_set<std::string> const & permitted_keywords) & {
+    for (auto it = make_.begin(); it != make_.end();) {
+      if (permitted_keywords.contains(std::string(it->first))) {
+        ++it;
+      } else {
+        it = make_.erase(it);
+      }
+    }
+  }
+
   schema::Version version() const { return version_; }
 
   /**

+ 1 - 9
include/jvalidate/schema.h

@@ -14,7 +14,6 @@
 #include <jvalidate/detail/pointer.h>
 #include <jvalidate/detail/reference.h>
 #include <jvalidate/detail/reference_manager.h>
-#include <jvalidate/detail/version.h>
 #include <jvalidate/document_cache.h>
 #include <jvalidate/enum.h>
 #include <jvalidate/forward.h>
@@ -154,13 +153,6 @@ public:
   Schema(A const & json, schema::Version version, Args &&... args)
       : Schema(json, version, DocumentCache<A>(), std::forward<Args>(args)...) {}
 
-  /**
-   * @param json An adapter to a json schema
-   */
-  template <Adapter A, Not<schema::Version>... Args>
-  explicit Schema(A const & json, Args &&... args)
-      : Schema(json, detail::version(json), std::forward<Args>(args)...) {}
-
   /**
    * @param json Any non-adapter (JSON) object. Will be immedately converted
    * into an Adapter object to allow us to walk through it w/o specialization.
@@ -289,7 +281,7 @@ template <Adapter A> void Node::construct(detail::ParserContext<A> context) {
     // 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.vocab = &context.ref.vocab(detail::version(context.schema));
+    context.vocab = &context.ref.vocab(URI(schema["$schema"].as_string()));
   }
 
   auto _ = resolve_anchor(context);