Просмотр исходного кода

feat: support for recursiveRef properly, including some bugfixes in resolution of references

Sam Jaffe 1 год назад
Родитель
Сommit
0c91faef53

+ 10 - 5
include/jvalidate/detail/parser_context.h

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

+ 4 - 5
include/jvalidate/detail/reference_cache.h

@@ -14,7 +14,10 @@ private:
   std::map<RootReference, Reference, std::greater<>> to_absolute_;
   std::map<RootReference, Reference, std::greater<>> to_absolute_;
 
 
 public:
 public:
-  void emplace(URI const & root) { to_absolute_.emplace(root, root); }
+  void emplace(URI const & root) {
+    to_absolute_.emplace(root, root);
+    to_anchor_.emplace(root, root);
+  }
 
 
   Reference emplace(Reference const & absolute, RootReference const & canonical) {
   Reference emplace(Reference const & absolute, RootReference const & canonical) {
     for (Reference where = absolute; not where.pointer().empty();) {
     for (Reference where = absolute; not where.pointer().empty();) {
@@ -42,10 +45,6 @@ public:
 
 
   Reference relative_to_nearest_anchor(Reference const & ref,
   Reference relative_to_nearest_anchor(Reference const & ref,
                                        bool for_parent_reference = false) const {
                                        bool for_parent_reference = false) const {
-    if (to_absolute_.contains(ref.root())) {
-      return ref;
-    }
-
     auto it = for_parent_reference ? to_anchor_.upper_bound(ref) : to_anchor_.lower_bound(ref);
     auto it = for_parent_reference ? to_anchor_.upper_bound(ref) : to_anchor_.lower_bound(ref);
     if (it == to_anchor_.end()) {
     if (it == to_anchor_.end()) {
       return ref;
       return ref;

+ 8 - 1
include/jvalidate/reference_handler.h

@@ -68,7 +68,14 @@ public:
       URI base = references_.actual_parent_uri(parent);
       URI base = references_.actual_parent_uri(parent);
       EXPECT_M(base.resource().rfind('/') != std::string::npos,
       EXPECT_M(base.resource().rfind('/') != std::string::npos,
                "Unable to deduce root for relative uri " << uri << " (" << base << ")");
                "Unable to deduce root for relative uri " << uri << " (" << base << ")");
-      return (uri.is_relative() ? base.parent() : base.root()) / uri;
+      if (not uri.is_relative()) {
+        return base.root() / uri;
+      }
+      if (auto br = base.resource(), ur = uri.resource();
+          br.ends_with(ur) && br[br.size() - ur.size() - 1] == '/') {
+        return base;
+      }
+      return base.parent() / uri;
     }();
     }();
 
 
     detail::Reference rval(uri, ref.anchor(), ref.pointer());
     detail::Reference rval(uri, ref.anchor(), ref.pointer());

+ 59 - 31
include/jvalidate/schema.h

@@ -161,6 +161,8 @@ public:
 
 
     ReferenceManager<A> ref(json, version, factory.keywords(version));
     ReferenceManager<A> ref(json, version, factory.keywords(version));
     detail::ParserContext<A> root{*this, json, version, factory, external, ref};
     detail::ParserContext<A> root{*this, json, version, factory, external, ref};
+
+    root.where = root.dynamic_where = ref.canonicalize({}, {});
     construct(root);
     construct(root);
   }
   }
 
 
@@ -229,55 +231,59 @@ private:
   }
   }
 
 
   template <Adapter A>
   template <Adapter A>
-  schema::Node const * resolve(detail::Reference ref, detail::ParserContext<A> const & context) {
-    ref = context.ref.canonicalize(ref, context.where);
-
-    // Special case if the root-level document does not have an $id property
-    if (std::optional root = context.ref.root(ref.root())) {
-      return fetch_schema(context.rebind(ref.pointer().walk(*root), ref));
+  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";
+
+    if (std::optional cached = from_cache(dynamic)) {
+      return *cached;
     }
     }
 
 
-    if (std::optional cached = from_cache(ref)) {
-      return *cached;
+    // Special case if the root-level document does not have an $id property
+    if (std::optional root = context.ref.root(lexical.root())) {
+      return fetch_schema(context.rebind(lexical.pointer().walk(*root), lexical, dynamic));
     }
     }
 
 
-    std::optional schema = context.external.try_load(ref.uri());
+    std::optional schema = context.external.try_load(lexical.uri());
     if (not schema.has_value()) {
     if (not schema.has_value()) {
-      std::string error = "URIResolver could not resolve " + std::string(ref.uri());
-      return alias(ref, &cache_.try_emplace(ref, error).first->second);
+      std::string error = "URIResolver could not resolve " + std::string(lexical.uri());
+      return alias(dynamic, &cache_.try_emplace(dynamic, error).first->second);
     }
     }
 
 
-    context.ref.prime(*schema, ref.uri(), context.version,
+    context.ref.prime(*schema, lexical.uri(), context.version,
                       context.factory.keywords(context.version));
                       context.factory.keywords(context.version));
 
 
-    if (std::optional root = context.ref.root(ref.root())) {
-      return fetch_schema(context.rebind(ref.pointer().walk(*root), ref));
+    if (std::optional root = context.ref.root(lexical.root())) {
+      return fetch_schema(context.rebind(lexical.pointer().walk(*root), lexical, dynamic));
     }
     }
 
 
-    return fetch_schema(context.rebind(ref.pointer().walk(*schema), ref));
+    return fetch_schema(context.rebind(lexical.pointer().walk(*schema), lexical, dynamic));
   }
   }
 
 
   template <Adapter A> schema::Node const * fetch_schema(detail::ParserContext<A> const & context) {
   template <Adapter A> schema::Node const * fetch_schema(detail::ParserContext<A> const & context) {
     // TODO(samjaffe): No longer promises uniqueness - instead track unique URI's
     // TODO(samjaffe): No longer promises uniqueness - instead track unique URI's
-    if (std::optional cached = from_cache(context.where)) {
+    if (std::optional cached = from_cache(context.dynamic_where)) {
       return *cached;
       return *cached;
     }
     }
 
 
     adapter::Type const type = context.schema.type();
     adapter::Type const type = context.schema.type();
     if (type == adapter::Type::Boolean && context.version >= schema::Version::Draft06) {
     if (type == adapter::Type::Boolean && context.version >= schema::Version::Draft06) {
-      return alias(context.where, context.schema.as_boolean() ? &accept_ : &reject_);
+      return alias(context.dynamic_where, context.schema.as_boolean() ? &accept_ : &reject_);
     }
     }
 
 
-    EXPECT_M(type == adapter::Type::Object, "invalid schema at " << context.where);
+    EXPECT_M(type == adapter::Type::Object, "invalid schema at " << context.dynamic_where);
     if (context.schema.object_size() == 0) {
     if (context.schema.object_size() == 0) {
-      return alias(context.where, &accept_);
+      return alias(context.dynamic_where, &accept_);
     }
     }
 
 
-    auto [it, created] = cache_.try_emplace(context.where);
-    EXPECT_M(created, "creating duplicate schema at... " << context.where);
+    auto [it, created] = cache_.try_emplace(context.dynamic_where);
+    EXPECT_M(created, "creating duplicate schema at... " << context.dynamic_where);
 
 
     // Do this here first in order to protect from infinite loops
     // Do this here first in order to protect from infinite loops
-    alias(context.where, &it->second);
+    alias(context.dynamic_where, &it->second);
     it->second.construct(context);
     it->second.construct(context);
     /* if (not it->second.is_pure_reference()) { */
     /* if (not it->second.is_pure_reference()) { */
     return &it->second;
     return &it->second;
@@ -311,23 +317,43 @@ 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> & context) {
   auto const schema = context.schema.as_object();
   auto const schema = context.schema.as_object();
 
 
-  if (schema.contains("$anchor")) {
+  if (schema.contains("$anchor") || context.version < schema::Version::Draft2019_09) {
     return nullptr;
     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 && schema.contains("$recursiveAnchor") &&
   if (context.version == schema::Version::Draft2019_09 && schema.contains("$recursiveAnchor") &&
       schema["$recursiveAnchor"].as_boolean()) {
       schema["$recursiveAnchor"].as_boolean()) {
-    detail::Reference actual_location = context.ref.canonicalize({}, context.where);
-    if (context.root.dynamic_anchors_.emplace(detail::Anchor(), actual_location).second) {
+    if (context.root.dynamic_anchors_.emplace(detail::Anchor(), context.where).second) {
       return [&root = context.root]() { root.dynamic_anchors_.erase(detail::Anchor()); };
       return [&root = context.root]() { root.dynamic_anchors_.erase(detail::Anchor()); };
     }
     }
   }
   }
 
 
   if (context.version > Version::Draft2019_09 && schema.contains("$dynamicAnchor")) {
   if (context.version > Version::Draft2019_09 && schema.contains("$dynamicAnchor")) {
     detail::Anchor anchor(schema["$dynamicAnchor"].as_string());
     detail::Anchor anchor(schema["$dynamicAnchor"].as_string());
-    detail::Reference actual_location = context.ref.canonicalize({}, context.where);
 
 
-    if (context.root.dynamic_anchors_.emplace(anchor, actual_location).second) {
+    if (context.root.dynamic_anchors_.emplace(anchor, context.where).second) {
       return [&root = context.root, anchor]() { root.dynamic_anchors_.erase(anchor); };
       return [&root = context.root, anchor]() { root.dynamic_anchors_.erase(anchor); };
     }
     }
   }
   }
@@ -341,7 +367,9 @@ template <Adapter A> bool Node::resolve_reference(detail::ParserContext<A> const
   if (schema.contains("$ref")) {
   if (schema.contains("$ref")) {
     detail::Reference ref(schema["$ref"].as_string());
     detail::Reference ref(schema["$ref"].as_string());
 
 
-    reference_ = context.root.resolve(ref, context);
+    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);
     return true;
     return true;
   }
   }
 
 
@@ -352,15 +380,15 @@ template <Adapter A> bool Node::resolve_reference(detail::ParserContext<A> const
   std::string const dyn_ref =
   std::string const dyn_ref =
       context.version > schema::Version::Draft2019_09 ? "$dynamicRef" : "$recursiveRef";
       context.version > schema::Version::Draft2019_09 ? "$dynamicRef" : "$recursiveRef";
   if (schema.contains(dyn_ref)) {
   if (schema.contains(dyn_ref)) {
-    detail::RootReference ref(schema[dyn_ref].as_string());
+    detail::Reference ref(schema[dyn_ref].as_string());
 
 
     // TODO(samjaffe): Relocate...
     // TODO(samjaffe): Relocate...
     if (auto it = context.root.dynamic_anchors_.find(ref.anchor());
     if (auto it = context.root.dynamic_anchors_.find(ref.anchor());
         it != context.root.dynamic_anchors_.end()) {
         it != context.root.dynamic_anchors_.end()) {
       // TODO(samjaffe): This does not re-compute things...
       // TODO(samjaffe): This does not re-compute things...
-      reference_ = context.root.resolve(it->second, context);
+      reference_ = context.root.resolve(it->second, context, true);
     } else {
     } else {
-      reference_ = context.root.resolve(detail::Reference(ref), context);
+      reference_ = context.root.resolve(ref, context);
     }
     }
     return true;
     return true;
   }
   }