Переглянути джерело

refactor: move recursive/dynamic reference handling into ReferenceManager

Sam Jaffe 1 рік тому
батько
коміт
66de76262d

+ 35 - 7
include/jvalidate/detail/out.h

@@ -1,8 +1,12 @@
 #pragma once
 
+#include <type_traits>
 #include <utility>
+#include <variant>
 
 namespace jvalidate::detail {
+template <typename T> class out;
+
 constexpr struct discard_out_t {
 } discard_out;
 
@@ -17,18 +21,42 @@ public:
 
   explicit operator bool() const { return ref_; }
 
-  void operator=(T && val) {
+  template <typename U>
+  requires std::is_constructible_v<T, U>
+  void operator=(U && val) {
     if (ref_) {
-      *ref_ = std::move(val);
+      *ref_ = std::forward<U>(val);
     }
     return *this;
   }
+};
 
-  void operator=(T const & val) {
-    if (ref_) {
-      *ref_ = val;
-    }
-    return *this;
+template <typename T>
+requires(std::is_same_v<T, std::decay_t<T>>) class inout {
+private:
+  std::variant<T, T *> ref_;
+
+public:
+  inout(T && value) : ref_(std::move(value)) {}
+  inout(T & ref) : ref_(&ref) {}
+
+  operator T const &() const {
+    struct {
+      T const & operator()(T const & in) const { return in; }
+      T const & operator()(T * in) const { return *in; }
+    } visitor;
+    return std::visit(visitor, ref_);
+  }
+
+  template <typename U>
+  requires std::is_constructible_v<T, U> T const & operator=(U && val) {
+    struct {
+      U && val;
+      void operator()(T & in) const { in = std::forward<U>(val); }
+      void operator()(T * in) const { *in = std::forward<U>(val); }
+    } visitor{std::forward<U>(val)};
+    std::visit(visitor, ref_);
+    return static_cast<T const &>(*this);
   }
 };
 }

+ 70 - 10
include/jvalidate/reference_handler.h

@@ -6,6 +6,8 @@
 #include <unordered_map>
 
 #include <jvalidate/detail/anchor.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>
@@ -25,7 +27,9 @@ private:
   detail::ReferenceCache references_;
 
   std::map<detail::RootReference, A> roots_;
-  std::multimap<URI, detail::Anchor> dynamic_anchors_;
+
+  std::map<detail::Anchor, detail::Reference> active_dynamic_anchors_;
+  std::map<URI, std::map<detail::Anchor, detail::Reference>> dynamic_anchors_;
 
 public:
   ReferenceManager(DocumentCache<A> & external, A const & root, schema::Version version,
@@ -34,14 +38,36 @@ public:
     prime(root, {}, version, keywords);
   }
 
-  std::optional<A> load(detail::Reference const & ref, detail::ParserContext<A> const & context) {
+  detail::OnBlockExit suppress(detail::Anchor const & anchor) {
+    if (auto it = active_dynamic_anchors_.find(anchor); it != active_dynamic_anchors_.end()) {
+      detail::Reference where = it->second;
+      active_dynamic_anchors_.erase(it);
+      return [this, anchor, where]() { active_dynamic_anchors_[anchor] = where; };
+    }
+    return nullptr;
+  }
+
+  auto scoped_activate(detail::Reference ref) {
+    URI const uri = references_.relative_to_nearest_anchor(ref).uri();
+    activate_dynamic(uri);
+    return [this, uri]() { deactivate_dynamic(uri); };
+  }
+
+  auto load(detail::Reference const & ref, detail::ParserContext<A> const & context)
+      -> std::pair<std::optional<A>, detail::OnBlockExit> {
+    auto scoped_schema = [this, &ref](A const & schema) {
+      activate_dynamic(ref.uri());
+      return std::make_pair(ref.pointer().walk(schema),
+                            [this, uri = ref.uri()] { deactivate_dynamic(uri); });
+    };
+
     if (auto it = roots_.find(ref.root()); it != roots_.end()) {
-      return ref.pointer().walk(it->second);
+      return scoped_schema(it->second);
     }
 
     std::optional<A> external = external_.try_load(ref.uri());
     if (not external) {
-      return std::nullopt;
+      return std::make_pair(std::nullopt, nullptr);
     }
 
     // TODO(samjaffe): Change Versions if needed...
@@ -50,15 +76,26 @@ public:
 
     // May have a sub-id that we map to
     if (auto it = roots_.find(ref.root()); it != roots_.end()) {
-      return ref.pointer().walk(it->second);
+      return scoped_schema(it->second);
     }
 
     // Will get called if the external schema does not declare a root document id?
-    return ref.pointer().walk(*external);
+    return scoped_schema(*external);
   }
 
-  detail::Reference canonicalize(detail::Reference const & ref,
-                                 detail::Reference const & parent) const {
+  detail::Reference canonicalize(detail::Reference const & ref, detail::Reference const & parent,
+                                 detail::inout<bool> dynamic_reference) const {
+    if (auto it = active_dynamic_anchors_.find(ref.anchor()); it != active_dynamic_anchors_.end()) {
+      if (dynamic_reference) {
+        return it->second;
+      }
+      dynamic_reference = ref.uri().empty() && ref.pointer().empty();
+    }
+
+    if (active_dynamic_anchors_.empty()) {
+      dynamic_reference = true;
+    }
+
     // Relative URI, not in the HEREDOC (or we set an $id)
     if (ref.uri().empty() and ref.anchor().empty()) {
       return detail::Reference(references_.relative_to_nearest_anchor(parent).root(),
@@ -96,6 +133,21 @@ public:
   }
 
 private:
+  void activate_dynamic(URI const & uri) {
+    active_dynamic_anchors_.insert(dynamic_anchors_[uri].begin(), dynamic_anchors_[uri].end());
+  }
+
+  void deactivate_dynamic(URI const & uri) {
+    std::map<detail::Anchor, detail::Reference> const & candidates = dynamic_anchors_[uri];
+    for (auto it = active_dynamic_anchors_.begin(); it != active_dynamic_anchors_.end();) {
+      if (auto cit = candidates.find(it->first); it->second == cit->second) {
+        it = active_dynamic_anchors_.erase(it);
+      } else {
+        ++it;
+      }
+    }
+  }
+
   void prime(Adapter auto const & json, detail::Reference where, schema::Version version,
              Keywords const & keywords) {
     if (json.type() != adapter::Type::Object) {
@@ -166,7 +218,11 @@ private:
 
       roots_.emplace(root, json);
       where = references_.emplace(where, root);
-      dynamic_anchors_.emplace(root.uri(), anchor);
+
+      if (detail::Reference & dynamic = dynamic_anchors_[root.uri()][anchor];
+          dynamic == detail::Reference() || where < dynamic) {
+        dynamic = where;
+      }
     }
 
     if (schema.contains("$dynamicAnchor") && version > schema::Version::Draft2019_09) {
@@ -175,7 +231,11 @@ private:
 
       roots_.emplace(root, json);
       where = references_.emplace(where, root);
-      dynamic_anchors_.emplace(root.uri(), anchor);
+
+      if (detail::Reference & dynamic = dynamic_anchors_[root.uri()][anchor];
+          dynamic == detail::Reference() || where < dynamic) {
+        dynamic = where;
+      }
     }
   }
 };

+ 18 - 57
include/jvalidate/schema.h

@@ -55,7 +55,7 @@ public:
   adapter::Const const * default_value() const { return default_.get(); }
 
 private:
-  template <Adapter A> detail::OnBlockExit resolve_anchor(detail::ParserContext<A> & context);
+  template <Adapter A> detail::OnBlockExit resolve_anchor(detail::ParserContext<A> const & context);
   template <Adapter A> bool resolve_reference(detail::ParserContext<A> const & context);
 };
 
@@ -112,8 +112,6 @@ private:
   schema::Node accept_;
   schema::Node reject_{"always false"};
 
-  std::map<detail::Anchor, detail::Reference> dynamic_anchors_;
-
   // An owning cache of all created schemas. Avoids storing duplicates such as
   // the "always-true" schema, "always-false" schema, and schemas whose only
   // meaningful field is "$ref", "$recursiveRef", or "$dynamicRef".
@@ -162,7 +160,7 @@ public:
     ReferenceManager<A> ref(external, json, version, factory.keywords(version));
     detail::ParserContext<A> root{*this, json, version, factory, ref};
 
-    root.where = root.dynamic_where = ref.canonicalize({}, {});
+    root.where = root.dynamic_where = ref.canonicalize({}, {}, false);
     construct(root);
   }
 
@@ -232,18 +230,16 @@ private:
 
   template <Adapter A>
   schema::Node const * resolve(detail::Reference const & ref,
-                               detail::ParserContext<A> const & context,
-                               bool is_dynamic_recursion = false) {
-    detail::Reference lexical = context.ref.canonicalize(ref, context.where);
-    detail::Reference dynamic =
-        dynamic_anchors_.empty() || is_dynamic_recursion ? lexical : context.dynamic_where / "$ref";
+                               detail::ParserContext<A> const & context, bool dynamic_reference) {
+    detail::Reference lexical = context.ref.canonicalize(ref, context.where, dynamic_reference);
+    detail::Reference dynamic = dynamic_reference ? lexical : context.dynamic_where / "$ref";
 
     if (std::optional cached = from_cache(dynamic)) {
       return *cached;
     }
 
     // Special case if the root-level document does not have an $id property
-    if (std::optional root = context.ref.load(lexical, context)) {
+    if (auto [root, scope] = context.ref.load(lexical, context); root.has_value()) {
       return fetch_schema(context.rebind(*root, lexical, dynamic));
     }
 
@@ -293,51 +289,25 @@ template <Adapter A> schema::Node const * ParserContext<A>::fixed_schema(bool ac
 }
 
 namespace jvalidate::schema {
-template <Adapter A> detail::OnBlockExit Node::resolve_anchor(detail::ParserContext<A> & context) {
+template <Adapter A>
+detail::OnBlockExit Node::resolve_anchor(detail::ParserContext<A> const & context) {
   auto const schema = context.schema.as_object();
 
   if (schema.contains("$anchor") || context.version < schema::Version::Draft2019_09) {
     return nullptr;
   }
 
-  if (context.version == schema::Version::Draft2019_09) {
-    if (not schema.contains("$recursiveAnchor") || not schema["$recursiveAnchor"].as_boolean()) {
-      if (not schema.contains("$id") && not schema.contains("$recursiveAnchor")) {
-        return nullptr;
-      }
-
-      auto it = context.root.dynamic_anchors_.find(detail::Anchor());
-      if (it == context.root.dynamic_anchors_.end()) {
-        return nullptr;
-      }
-
-      detail::Reference where = it->second;
-      context.root.dynamic_anchors_.erase(it);
-
-      return [&root = context.root, where]() {
-        root.dynamic_anchors_.emplace(detail::Anchor(), where);
-      };
-    } else if (context.root.dynamic_anchors_.emplace(detail::Anchor(), context.where).second) {
-      return [&root = context.root]() { root.dynamic_anchors_.erase(detail::Anchor()); };
-    }
+  if (context.version != schema::Version::Draft2019_09) {
+    return nullptr;
   }
-
-  if (context.version == schema::Version::Draft2019_09 && schema.contains("$recursiveAnchor") &&
-      schema["$recursiveAnchor"].as_boolean()) {
-    if (context.root.dynamic_anchors_.emplace(detail::Anchor(), context.where).second) {
-      return [&root = context.root]() { root.dynamic_anchors_.erase(detail::Anchor()); };
-    }
+  if (schema.contains("$recursiveAnchor") && schema["$recursiveAnchor"].as_boolean()) {
+    return context.ref.scoped_activate(context.where);
   }
-
-  if (context.version > Version::Draft2019_09 && schema.contains("$dynamicAnchor")) {
-    detail::Anchor anchor(schema["$dynamicAnchor"].as_string());
-
-    if (context.root.dynamic_anchors_.emplace(anchor, context.where).second) {
-      return [&root = context.root, anchor]() { root.dynamic_anchors_.erase(anchor); };
-    }
+  if (not schema.contains("$id") && not schema.contains("$recursiveAnchor")) {
+    return nullptr;
   }
 
-  return nullptr;
+  return context.ref.suppress(detail::Anchor());
 }
 
 template <Adapter A> bool Node::resolve_reference(detail::ParserContext<A> const & context) {
@@ -346,9 +316,7 @@ template <Adapter A> bool Node::resolve_reference(detail::ParserContext<A> const
   if (schema.contains("$ref")) {
     detail::Reference ref(schema["$ref"].as_string());
 
-    bool is_dynamic_ref_cludge = context.root.dynamic_anchors_.contains(ref.anchor()) &&
-                                 ref.uri().empty() && ref.pointer().empty();
-    reference_ = context.root.resolve(ref, context, is_dynamic_ref_cludge);
+    reference_ = context.root.resolve(ref, context, false);
     return true;
   }
 
@@ -361,14 +329,7 @@ template <Adapter A> bool Node::resolve_reference(detail::ParserContext<A> const
   if (schema.contains(dyn_ref)) {
     detail::Reference ref(schema[dyn_ref].as_string());
 
-    // TODO(samjaffe): Relocate...
-    if (auto it = context.root.dynamic_anchors_.find(ref.anchor());
-        it != context.root.dynamic_anchors_.end()) {
-      // TODO(samjaffe): This does not re-compute things...
-      reference_ = context.root.resolve(it->second, context, true);
-    } else {
-      reference_ = context.root.resolve(ref, context);
-    }
+    reference_ = context.root.resolve(ref, context, true);
     return true;
   }
 
@@ -387,7 +348,7 @@ template <Adapter A> void Node::construct(detail::ParserContext<A> context) {
     context.version = schema_version(context.schema);
   }
 
-  [[maybe_unused]] auto _ = resolve_anchor(context);
+  auto _ = resolve_anchor(context);
   bool const has_reference = resolve_reference(context);
 
   if (schema.contains("default")) {