Quellcode durchsuchen

refactor: split Reference::anchor into URI and Anchor classes

Sam Jaffe vor 1 Jahr
Ursprung
Commit
648f65daab

+ 38 - 0
include/jvalidate/detail/anchor.h

@@ -0,0 +1,38 @@
+#pragma once
+
+#include <algorithm>
+#include <cctype>
+#include <compare>
+#include <string>
+#include <string_view>
+
+#include <jvalidate/detail/compare.h>
+#include <jvalidate/detail/expect.h>
+
+namespace jvalidate::detail {
+class Anchor {
+private:
+  std::string content_;
+
+public:
+  Anchor() = default;
+
+  explicit Anchor(std::string_view content) : content_(content) {
+    EXPECT_M(content.empty() || content[0] == '_' || std::isalpha(content[0]),
+             "First character of an Anchor must be alphabetic or '_'");
+    EXPECT_M(
+        std::all_of(content.begin(), content.end(),
+                    [](char c) { return std::isalnum(c) || c == '_' || c == '.' || c == '-'; }),
+        "Illegal character(s) in anchor");
+  }
+
+  explicit operator std::string const &() const { return content_; }
+  bool empty() const { return content_.empty(); }
+
+  friend std::ostream & operator<<(std::ostream & os, Anchor const & self) {
+    return os << self.content_;
+  }
+
+  auto operator<=>(Anchor const & lhs) const = default;
+};
+}

+ 15 - 0
include/jvalidate/detail/compare.h

@@ -0,0 +1,15 @@
+#pragma once
+
+#include <compare>
+
+namespace std {
+template <typename T> std::strong_ordering operator<=>(T const & lhs, T const & rhs) {
+  if (lhs < rhs) {
+    return std::strong_ordering::less;
+  }
+  if (lhs > rhs) {
+    return std::strong_ordering::greater;
+  }
+  return std::strong_ordering::equal;
+}
+}

+ 19 - 0
include/jvalidate/detail/on_block_exit.h

@@ -0,0 +1,19 @@
+#pragma once
+
+#include <functional>
+namespace jvalidate::detail {
+class OnBlockExit {
+private:
+  std::function<void()> callback_;
+
+public:
+  OnBlockExit() = default;
+  template <typename F> OnBlockExit(F && callback) : callback_(callback) {}
+
+  ~OnBlockExit() {
+    if (callback_) {
+      callback_();
+    }
+  }
+};
+}

+ 1 - 12
include/jvalidate/detail/pointer.h

@@ -1,6 +1,5 @@
 #pragma once
 
-#include <compare>
 #include <cstdint>
 #include <ostream>
 #include <string>
@@ -8,17 +7,7 @@
 #include <variant>
 #include <vector>
 
-namespace std {
-template <typename T> std::strong_ordering operator<=>(T const & lhs, T const & rhs) {
-  if (lhs < rhs) {
-    return std::strong_ordering::less;
-  }
-  if (lhs > rhs) {
-    return std::strong_ordering::greater;
-  }
-  return std::strong_ordering::equal;
-}
-}
+#include <jvalidate/detail/compare.h>
 
 namespace jvalidate::detail {
 

+ 23 - 14
include/jvalidate/detail/reference.h

@@ -3,31 +3,39 @@
 #include <string>
 #include <string_view>
 
+#include <jvalidate/detail/anchor.h>
 #include <jvalidate/detail/expect.h>
 #include <jvalidate/detail/pointer.h>
+#include <jvalidate/detail/uri.h>
 
 namespace jvalidate::detail {
 class Reference {
 public:
   Reference() = default;
-  Reference(std::string const & anchor, Pointer const & pointer)
-      : anchor_(anchor), pointer_(pointer) {}
-  Reference(std::string_view fragment, bool allow_anchor = true) {
-    EXPECT_M(fragment.find('#') != std::string::npos, "Reference requires a fragment token '#'");
 
-    size_t const index = fragment.find('/');
+  Reference(URI const & uri, Anchor const & anchor = {}, Pointer const & pointer = {})
+      : uri_(uri), anchor_(anchor), pointer_(pointer) {}
 
-    anchor_ = std::string(fragment.substr(0, index));
-    EXPECT_M(allow_anchor && anchor_.back() == '#', "Anchoring is not allowed in this context");
+  Reference(std::string_view ref, bool allow_anchor = true) {
+    size_t end_of_uri = ref.find('#');
+    EXPECT(end_of_uri != std::string::npos);
+    uri_ = URI(ref.substr(0, end_of_uri));
+    ref.remove_prefix(end_of_uri + 1);
 
-    if (index != std::string::npos) {
-      pointer_ = fragment.substr(index + 1);
-    }
+    size_t const pointer_start = ref.find('/');
+    anchor_ = Anchor(ref.substr(0, pointer_start));
+    EXPECT_M(allow_anchor || anchor_.empty(), "Anchoring is not allowed in this context");
+    ref.remove_prefix(pointer_start);
+
+    pointer_ = ref;
   }
 
-  std::string const & anchor() const { return anchor_; }
+  URI const & uri() const { return uri_; }
+  Anchor const & anchor() const { return anchor_; }
   Pointer const & pointer() const { return pointer_; }
-  Reference parent() const { return {anchor_, pointer_.parent()}; }
+
+  Reference root() const { return {uri_, anchor_}; }
+  Reference parent() const { return {uri_, anchor_, pointer_.parent()}; }
 
   Reference & operator/=(Pointer const & relative) {
     pointer_ /= relative;
@@ -51,12 +59,13 @@ public:
   Reference operator/(size_t index) const { return Reference(*this) /= index; }
 
   friend std::ostream & operator<<(std::ostream & os, Reference const & self) {
-    return os << self.anchor_ << self.pointer_;
+    return os << self.uri_ << '#' << self.anchor_ << self.pointer_;
   }
   auto operator<=>(Reference const &) const = default;
 
 private:
-  std::string anchor_;
+  URI uri_;
+  Anchor anchor_;
   Pointer pointer_;
 };
 }

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

@@ -0,0 +1,31 @@
+#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;
+};
+}

+ 0 - 1
include/jvalidate/parser_context.h

@@ -27,7 +27,6 @@ template <Adapter A> struct ParserContext {
     return {root, (*parent)[key], parent, version, where.parent() / key, factory};
   }
 
-  schema::Node const * resolve(std::string_view uri) const;
   schema::Node const * node() const;
   schema::Node const * always() const;
 };

+ 103 - 23
include/jvalidate/schema.h

@@ -6,7 +6,9 @@
 
 #include <jvalidate/adapter.h>
 #include <jvalidate/constraint.h>
+#include <jvalidate/detail/anchor.h>
 #include <jvalidate/detail/expect.h>
+#include <jvalidate/detail/on_block_exit.h>
 #include <jvalidate/detail/pointer.h>
 #include <jvalidate/detail/reference.h>
 #include <jvalidate/enum.h>
@@ -45,6 +47,10 @@ public:
   auto const & post_constraints() const { return constraints_; }
 
   adapter::Const const * default_value() const { return default_.get(); }
+
+private:
+  template <Adapter A> detail::OnBlockExit resolve_anchor(ParserContext<A> & context);
+  template <Adapter A> bool resolve_reference(ParserContext<A> const & context);
 };
 
 inline Version Node::schema_version(std::string_view url) {
@@ -92,12 +98,21 @@ class Schema : public schema::Node {
 private:
   friend class schema::Node;
   template <Adapter A> friend class ParserContext;
+  struct DynamicRef {
+    template <typename F>
+    DynamicRef(detail::Reference const & where, F const & reconstruct)
+        : where(where), reconstruct(reconstruct) {}
+
+    detail::Reference where;
+    std::function<schema::Node const *()> reconstruct;
+  };
 
 private:
   schema::Node accept_{true};
   schema::Node reject_{false};
 
-  std::map<std::string, detail::Reference> anchors_;
+  std::map<detail::Reference, detail::Reference> anchors_;
+  std::map<detail::Anchor, DynamicRef> dynamic_anchors_;
   std::map<detail::Reference, schema::Node> cache_;
 
   std::map<detail::Reference, schema::Node const *> alias_cache_;
@@ -117,7 +132,23 @@ public:
       : Schema(adapter::AdapterFor<JSON const>(json), std::forward<Args>(args)...) {}
 
 private:
-  void anchor(std::string anchor, detail::Reference const & from) {}
+  void anchor(detail::Reference const & anchor, detail::Reference const & from) {
+    EXPECT_M(anchors_.try_emplace(anchor.root(), from).second,
+             "more than one anchor found for uri " << anchor);
+  }
+
+  template <Adapter A>
+  void dynamic_anchor(detail::Anchor const & anchor, ParserContext<A> const & context) {
+    dynamic_anchors_.try_emplace(anchor, context.where,
+                                 [this, context]() { return fetch_schema(context); });
+  }
+
+  void remove_dynamic_anchor(detail::Anchor const & anchor, detail::Reference const & where) {
+    if (auto it = dynamic_anchors_.find(anchor);
+        it != dynamic_anchors_.end() && it->second.where == where) {
+      dynamic_anchors_.erase(it);
+    }
+  }
 
   schema::Node const * alias(detail::Reference const & where, schema::Node const * schema) {
     EXPECT_M(alias_cache_.try_emplace(where, schema).second,
@@ -133,10 +164,9 @@ private:
       return this;
     }
 
-    if (auto it = anchors_.find(ref.anchor()); it != anchors_.end()) {
+    if (auto it = anchors_.find(ref.root()); 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;
@@ -144,6 +174,12 @@ private:
     throw;
   }
 
+  schema::Node const * resolve_dynamic(detail::Anchor const & ref) {
+    auto it = dynamic_anchors_.find(ref);
+    EXPECT_M(it != dynamic_anchors_.end(), "Unmatched $dynamicRef '" << ref << "'");
+    return it->second.reconstruct();
+  }
+
   template <Adapter A> schema::Node const * fetch_schema(ParserContext<A> const & context) {
     adapter::Type const type = context.schema.type();
     if (type == adapter::Type::Boolean && context.version >= schema::Version::Draft06) {
@@ -171,10 +207,6 @@ private:
   }
 };
 
-template <Adapter A> schema::Node const * ParserContext<A>::resolve(std::string_view uri) const {
-  return root.resolve<A>(detail::Reference(uri), where, version);
-}
-
 template <Adapter A> schema::Node const * ParserContext<A>::node() const {
   return root.fetch_schema(*this);
 }
@@ -185,6 +217,67 @@ template <Adapter A> schema::Node const * ParserContext<A>::always() const {
 }
 
 namespace jvalidate::schema {
+template <Adapter A> detail::OnBlockExit Node::resolve_anchor(ParserContext<A> & context) {
+  auto const schema = context.schema.as_object();
+
+  // TODO(samjaffe): $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.
+    detail::Anchor anchor(schema["$anchor"].as_string());
+    context.root.anchor(detail::Reference(context.where.uri(), anchor), context.where);
+    return nullptr;
+  }
+
+  if (context.version == Version::Draft2019_09 && schema.contains("$recursiveAnchor")) {
+    EXPECT_M(schema["$recursiveAnchor"].as_boolean(), "$recursiveAnchor MUST be 'true'");
+
+    context.root.dynamic_anchor(detail::Anchor(), context);
+    return [&context]() { context.root.remove_dynamic_anchor(detail::Anchor(), context.where); };
+  }
+
+  if (context.version > Version::Draft2019_09 && schema.contains("$dynamicAnchor")) {
+    detail::Anchor anchor(schema["$dynamicAnchor"].as_string());
+
+    context.root.dynamic_anchor(anchor, context);
+    return [&context, anchor]() { context.root.remove_dynamic_anchor(anchor, context.where); };
+  }
+}
+
+template <Adapter A> bool Node::resolve_reference(ParserContext<A> const & context) {
+  auto const schema = context.schema.as_object();
+
+  if (schema.contains("$ref")) {
+    detail::Reference ref(schema["$ref"].as_string());
+
+    reference_ = context.root.template resolve<A>(ref, context.where, context.version);
+    return true;
+  }
+
+  if (context.version < Version::Draft2019_09) {
+    return false;
+  }
+
+  if (context.version == Version::Draft2019_09 && schema.contains("$recursiveRef")) {
+    detail::Reference ref(schema["$recursiveRef"].as_string());
+    EXPECT_M(ref == detail::Reference(), "Only the root schema is permitted as a $recursiveRef");
+
+    reference_ = context.root.resolve_dynamic(detail::Anchor());
+    return true;
+  }
+
+  if (context.version > Version::Draft2019_09 && schema.contains("$dynamicRef")) {
+    detail::Reference ref(schema["$dynamicRef"].as_string());
+
+    reference_ = context.root.resolve_dynamic(ref.anchor());
+    return true;
+  }
+
+  return false;
+}
+
 template <Adapter A> Node::Node(ParserContext<A> context) {
   EXPECT(context.schema.type() == adapter::Type::Object);
 
@@ -201,21 +294,8 @@ template <Adapter A> Node::Node(ParserContext<A> context) {
     context.root.alias(detail::Reference(schema["$id"].as_string(), false), this);
   }
 
-  // TODO(samjaffe): $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());
-  }
+  [[maybe_unused]] auto _ = resolve_anchor(context);
+  bool const has_reference = resolve_reference(context);
 
   if (schema.contains("default")) {
     default_ = schema["default"].freeze();