Kaynağa Gözat

feat: implement document cache for external URI fetches

Sam Jaffe 1 yıl önce
ebeveyn
işleme
ef0fd552d0

+ 12 - 7
include/jvalidate/detail/parser_context.h

@@ -11,20 +11,25 @@ template <Adapter A> struct ParserContext {
   Schema & root;
 
   A schema;
-  std::optional<Object> parent = std::nullopt;
 
   schema::Version version;
-  Reference where = {};
   ConstraintFactory<A> const & factory;
+  DocumentCache<A> & external;
 
-  ParserContext child(A const & child, std::string const & key) const {
-    return {root, child, schema.as_object(), version, where / key, factory};
+  std::optional<Object> parent = std::nullopt;
+  Reference where = {};
+
+  ParserContext rebind(A const & new_schema, Reference const & new_loc,
+                       std::optional<Object> parent = std::nullopt) const {
+    return {root, new_schema, version, factory, external, parent, new_loc};
   }
-  ParserContext child(A const & child, size_t index) const {
-    return {root, child, std::nullopt, version, where / index, factory};
+
+  ParserContext child(A const & child, std::string const & key) const {
+    return rebind(child, where / key, schema.as_object());
   }
+  ParserContext child(A const & child, size_t index) const { return rebind(child, where / index); }
   ParserContext neighbor(std::string const & key) const {
-    return {root, (*parent)[key], parent, version, where.parent() / key, factory};
+    return rebind((*parent)[key], where / key, parent);
   }
 
   schema::Node const * node() const;

+ 1 - 1
include/jvalidate/detail/reference.h

@@ -6,7 +6,7 @@
 #include <jvalidate/detail/anchor.h>
 #include <jvalidate/detail/expect.h>
 #include <jvalidate/detail/pointer.h>
-#include <jvalidate/detail/uri.h>
+#include <jvalidate/uri.h>
 
 namespace jvalidate::detail {
 class Reference {

+ 0 - 31
include/jvalidate/detail/uri.h

@@ -1,31 +0,0 @@
-#pragma once
-
-#include <string>
-#include <string_view>
-
-#include <jvalidate/detail/compare.h>
-
-namespace jvalidate::detail {
-class URI {
-private:
-  std::string content_;
-
-public:
-  URI() = default;
-
-  explicit URI(std::string_view content) : content_(content) {
-    if (content_.back() == '#') {
-      content_.pop_back();
-    }
-  }
-
-  explicit operator std::string const &() const { return content_; }
-  bool empty() const { return content_.empty(); }
-
-  friend std::ostream & operator<<(std::ostream & os, URI const & self) {
-    return os << self.content_;
-  }
-
-  auto operator<=>(URI const & lhs) const = default;
-};
-}

+ 38 - 0
include/jvalidate/document_cache.h

@@ -0,0 +1,38 @@
+#pragma once
+
+#include <map>
+#include <optional>
+
+#include <jvalidate/forward.h>
+#include <jvalidate/uri.h>
+
+namespace jvalidate {
+template <Adapter A> class DocumentCache {
+public:
+  using value_type = typename A::value_type;
+
+private:
+  URIResolver<A> resolve_;
+  std::map<URI, value_type> cache_;
+
+public:
+  DocumentCache() = default;
+  DocumentCache(URIResolver<A> const & resolve) : resolve_(resolve) {}
+
+  operator bool() const { return resolve_; }
+
+  std::optional<A> try_load(URI const & uri) {
+    if (not resolve_) {
+      return std::nullopt;
+    }
+
+    auto [it, created] = cache_.try_emplace(uri);
+    if (created && not resolve_(uri, it->second)) {
+      cache_.erase(it);
+      return std::nullopt;
+    }
+
+    return A(it->second);
+  }
+};
+}

+ 9 - 0
include/jvalidate/forward.h

@@ -2,10 +2,12 @@
 
 #include <functional>
 #include <string>
+#include <type_traits>
 
 namespace jvalidate {
 class Schema;
 class Status;
+class URI;
 struct ValidationConfig;
 class ValidationResult;
 }
@@ -103,6 +105,7 @@ concept ObjectAdapter = requires(A const a) {
 
 template <typename A>
 concept Adapter = std::is_base_of_v<adapter::Adapter, A> && requires(A const a) {
+  typename A::value_type;
   { a.type() } -> std::same_as<adapter::Type>;
   { a.as_boolean() } -> std::same_as<bool>;
   { a.as_integer() } -> std::convertible_to<int64_t>;
@@ -135,8 +138,14 @@ concept RegexEngine = std::constructible_from<std::string> && requires(R const r
 
 namespace jvalidate {
 template <Adapter A> class ConstraintFactory;
+template <Adapter A> class DocumentCache;
 template <Adapter A, RegexEngine RE> class ValidationVisitor;
 
 template <RegexEngine RE> class ValidatorT;
 class Validator;
+
+template <Adapter A> using URIResolver = bool (*)(URI const &, typename A::value_type &);
+
+template <typename T, typename S>
+concept Not = not std::is_convertible_v<std::decay_t<S>, T>;
 }

+ 112 - 17
include/jvalidate/schema.h

@@ -1,6 +1,7 @@
 #pragma once
 
 #include <memory>
+#include <type_traits>
 #include <unordered_map>
 #include <vector>
 
@@ -12,6 +13,7 @@
 #include <jvalidate/detail/parser_context.h>
 #include <jvalidate/detail/pointer.h>
 #include <jvalidate/detail/reference.h>
+#include <jvalidate/document_cache.h>
 #include <jvalidate/enum.h>
 #include <jvalidate/forward.h>
 
@@ -118,15 +120,82 @@ private:
   std::map<detail::Reference, schema::Node const *> alias_cache_;
 
 public:
+  /**
+   * @brief Construct a new schema. All other constructors of this type may be
+   * considered syntactic sugar for this constructor.
+   *
+   * As such, the true signature of this class's contructor is:
+   *
+   * Schema(Adapter | JSON [, schema::Version]
+   *        [, URIResolver & | URIResolver &&]
+   *        [, ConstraintFactory<A> const &])
+   *
+   * as long as the order of arguments is preserved - the constructor will work
+   * no matter which arguments are ignored. The only required argument being
+   * the JSON object/Adapter.
+   *
+   * @param json An adapter to a json object
+   *
+   * @param version The json-schema draft version that all schemas will prefer
+   *
+   * @param external An object capable of resolving URIs, and turning them into
+   * Adapter objects. Holds a cache and so must be mutable.
+   *
+   * @param factory An object that manuafactures constraints - allows the user
+   * to provide custom extensions or even modify the behavior of existing
+   * keywords by overridding the virtual accessor function(s).
+   */
   template <Adapter A>
-  explicit Schema(A const & json, ConstraintFactory<A> const & factory = {})
-      : Schema(json, schema_version(json), factory) {}
-
-  template <Adapter A>
-  Schema(A const & json, schema::Version version, ConstraintFactory<A> const & factory = {})
-      : schema::Node(detail::ParserContext<A>{
-            .root = *this, .schema = json, .version = version, .factory = factory}) {}
-
+  Schema(A const & json, schema::Version version, DocumentCache<A> & external,
+         ConstraintFactory<A> const & factory = {})
+      : schema::Node(detail::ParserContext<A>{*this, json, version, factory, external}) {}
+
+  /**
+   * @param json An adapter to a json schema
+   *
+   * @param version The json-schema draft version that all schemas will prefer
+   *
+   * @param external An object capable of resolving URIs, and turning them into
+   * Adapter objects. Holds a cache and so must be mutable. If this constructor
+   * is called, then it means that the cache is a one-off object, and will not
+   * be reused.
+   */
+  template <Adapter A, typename... Args>
+  Schema(A const & json, schema::Version version, DocumentCache<A> && external, Args &&... args)
+      : Schema(json, version, external, std::forward<Args>(args)...) {}
+
+  /**
+   * @param json An adapter to a json schema
+   *
+   * @param version The json-schema draft version that all schemas will prefer
+   *
+   * @param resolve A function capable of resolving URIs, and storing the
+   * contents in a provided concrete JSON object.
+   */
+  template <Adapter A, typename... Args>
+  Schema(A const & json, schema::Version version, URIResolver<A> resolve, Args &&... args)
+      : Schema(json, version, DocumentCache<A>(resolve), std::forward<Args>(args)...) {}
+
+  /**
+   * @param json An adapter to a json schema
+   *
+   * @param version The json-schema draft version that all schemas will prefer
+   */
+  template <Adapter A, Not<DocumentCache<A>>... Args>
+  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, schema_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.
+   */
   template <typename JSON, typename... Args>
   explicit Schema(JSON const & json, Args &&... args)
       : Schema(adapter::AdapterFor<JSON const>(json), std::forward<Args>(args)...) {}
@@ -156,22 +225,48 @@ private:
     return schema;
   }
 
+  std::optional<schema::Node const *> from_cache(detail::Reference ref) {
+    if (auto it = anchors_.find(ref.root()); it != anchors_.end()) {
+      ref = it->second / ref.pointer();
+    }
+
+    if (auto it = alias_cache_.find(ref); it != alias_cache_.end()) {
+      return it->second;
+    }
+
+    return std::nullopt;
+  }
+
   template <Adapter A>
-  schema::Node const * resolve(detail::Reference ref, detail::Reference const & from,
-                               schema::Version default_version) {
+  schema::Node const * resolve(detail::Reference ref, detail::ParserContext<A> const & context) {
     // Special case if the root-level document does not have an $id property
-    if (ref == detail::Reference() && ref.anchor() == from.anchor()) {
+    if (ref == detail::Reference() && ref.anchor() == context.where.anchor()) {
       return this;
     }
 
-    if (auto it = anchors_.find(ref.root()); it != anchors_.end()) {
-      ref = it->second / ref.pointer();
+    if (std::optional cached = from_cache(ref)) {
+      return *cached;
     }
 
-    if (auto it = alias_cache_.find(ref); it != alias_cache_.end()) {
-      return it->second;
+    // SPECIAL RULE: Resolve this URI into the context of the calling URI
+    if (ref.uri().scheme().empty()) {
+      URI const & relative_to = context.where.uri();
+      EXPECT_M(relative_to.resource().rfind('/') != std::string::npos,
+               "Relative URIs require that the current context has a resolved URI");
+      ref = detail::Reference(relative_to.parent() / ref.uri(), ref.anchor(), ref.pointer());
     }
-    throw;
+
+    EXPECT_M(context.external, "Unable to resolve external reference(s) without a URIResolver");
+
+    std::optional schema = context.external.try_load(ref.uri());
+    EXPECT_M(schema.has_value(), "URIResolver could not resolve " << ref.uri());
+
+    (void)fetch_schema(context.rebind(*schema, ref.uri()));
+    std::optional referenced_node = from_cache(ref);
+    EXPECT_M(referenced_node.has_value(),
+             "Could not locate reference '" << ref << "' within external schema.");
+
+    return *referenced_node;
   }
 
   schema::Node const * resolve_dynamic(detail::Anchor const & ref) {
@@ -254,7 +349,7 @@ template <Adapter A> bool Node::resolve_reference(detail::ParserContext<A> const
   if (schema.contains("$ref")) {
     detail::Reference ref(schema["$ref"].as_string());
 
-    reference_ = context.root.template resolve<A>(ref, context.where, context.version);
+    reference_ = context.root.resolve(ref, context);
     return true;
   }
 

+ 44 - 0
include/jvalidate/uri.h

@@ -0,0 +1,44 @@
+#pragma once
+
+#include <string>
+#include <string_view>
+
+#include <jvalidate/detail/compare.h>
+#include <jvalidate/detail/expect.h>
+
+namespace jvalidate {
+class URI {
+private:
+  std::string uri_;
+  std::string_view resource_;
+  std::string_view scheme_;
+
+public:
+  URI() = default;
+
+  explicit URI(std::string_view uri) : uri_(uri), resource_(uri_) {
+    if (uri_.back() == '#') {
+      uri_.pop_back();
+    }
+
+    if (size_t n = uri_.find("://"); n != std::string::npos) {
+      scheme_ = {uri_.c_str(), n};
+      resource_.remove_prefix(n + 3);
+    }
+  }
+
+  URI parent() const { return URI(std::string_view(uri_).substr(0, uri_.rfind('/'))); }
+
+  URI operator/(URI const & relative) const { return URI(uri_ + relative.uri_); }
+
+  std::string_view scheme() const { return scheme_; }
+  std::string_view resource() const { return resource_; }
+
+  explicit operator std::string const &() const { return uri_; }
+  bool empty() const { return uri_.empty(); }
+
+  friend std::ostream & operator<<(std::ostream & os, URI const & self) { return os << self.uri_; }
+
+  auto operator<=>(URI const & lhs) const = default;
+};
+}