Browse Source

fix: reference handling

Sam Jaffe 1 năm trước cách đây
mục cha
commit
7389450bff

+ 3 - 0
include/jvalidate/constraint.h

@@ -97,6 +97,8 @@ private:
       {"dependencies",
        {{schema::Version::Draft04, {schema::Wraps::Object}}, {schema::Version::Draft2019_09, {}}}},
       {"dependentSchemas", {{schema::Version::Draft2019_09, {schema::Wraps::Object}}}},
+      {"else", {{schema::Version::Draft04, {schema::Wraps::Schema}}}},
+      {"if", {{schema::Version::Draft04, {schema::Wraps::Schema}}}},
       {"items",
        {{schema::Version::Draft04, {schema::Wraps::Array, schema::Wraps::Schema}},
         {schema::Version::Draft2020_12, {schema::Wraps::Schema}}}},
@@ -105,6 +107,7 @@ private:
       {"patternProperties", {{schema::Version::Draft04, {schema::Wraps::Object}}}},
       {"prefixItems", {{schema::Version::Draft2020_12, {schema::Wraps::Array}}}},
       {"properties", {{schema::Version::Draft04, {schema::Wraps::Object}}}},
+      {"then", {{schema::Version::Draft04, {schema::Wraps::Schema}}}},
       {"unevaluatedItems", {{schema::Version::Draft2020_12, {schema::Wraps::Schema}}}},
       {"unevaluatedProperties", {{schema::Version::Draft2020_12, {schema::Wraps::Schema}}}},
   };

+ 34 - 0
include/jvalidate/detail/out.h

@@ -0,0 +1,34 @@
+#pragma once
+
+#include <utility>
+
+namespace jvalidate::detail {
+constexpr struct discard_out_t {
+} discard_out;
+
+template <typename T> class out {
+private:
+  T * ref_ = nullptr;
+
+public:
+  out() = default;
+  out(discard_out_t) {}
+  out(T & ref) : ref_(&ref) {}
+
+  explicit operator bool() const { return ref_; }
+
+  void operator=(T && val) {
+    if (ref_) {
+      *ref_ = std::move(val);
+    }
+    return *this;
+  }
+
+  void operator=(T const & val) {
+    if (ref_) {
+      *ref_ = val;
+    }
+    return *this;
+  }
+};
+}

+ 3 - 1
include/jvalidate/detail/parser_context.h

@@ -4,6 +4,7 @@
 
 #include <jvalidate/detail/reference.h>
 #include <jvalidate/forward.h>
+#include <jvalidate/reference_handler.h>
 
 namespace jvalidate::detail {
 template <Adapter A> struct ParserContext {
@@ -15,13 +16,14 @@ template <Adapter A> struct ParserContext {
   schema::Version version;
   ConstraintFactory<A> const & factory;
   DocumentCache<A> & external;
+  ReferenceManager<A> & ref;
 
   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};
+    return {root, new_schema, version, factory, external, ref, parent, new_loc};
   }
 
   ParserContext child(A const & child, std::string const & key) const {

+ 14 - 0
include/jvalidate/detail/pointer.h

@@ -1,5 +1,7 @@
 #pragma once
 
+#include <algorithm>
+#include <cassert>
 #include <cstdint>
 #include <iostream>
 #include <string>
@@ -58,6 +60,18 @@ public:
     return document;
   }
 
+  bool empty() const { return tokens_.empty(); }
+
+  bool starts_with(Pointer const & other) const {
+    return other.tokens_.size() <= tokens_.size() &&
+           std::equal(other.tokens_.begin(), other.tokens_.end(), tokens_.begin());
+  }
+
+  Pointer relative_to(Pointer const & other) const {
+    assert(starts_with(other));
+    return Pointer(std::vector(tokens_.begin() + other.tokens_.size(), tokens_.end()));
+  }
+
   Pointer parent() const { return Pointer({tokens_.begin(), tokens_.end() - 1}); }
 
   Pointer & operator/=(Pointer const & relative) {

+ 53 - 16
include/jvalidate/detail/reference.h

@@ -5,18 +5,40 @@
 
 #include <jvalidate/detail/anchor.h>
 #include <jvalidate/detail/expect.h>
+#include <jvalidate/detail/out.h>
 #include <jvalidate/detail/pointer.h>
 #include <jvalidate/uri.h>
 
 namespace jvalidate::detail {
-class Reference {
+class Reference;
+
+class RootReference {
+private:
+  friend class Reference;
+  URI uri_;
+  Anchor anchor_;
+
 public:
-  Reference() = default;
+  RootReference() = default;
+
+  explicit RootReference(URI const & uri, Anchor const & anchor = {})
+      : uri_(uri), anchor_(anchor) {}
+
+  explicit RootReference(std::string_view ref) : RootReference(ref, discard_out) {}
 
-  Reference(URI const & uri, Anchor const & anchor = {}, Pointer const & pointer = {})
-      : uri_(uri), anchor_(anchor), pointer_(pointer) {}
+  bool is_relative() const { return uri_.is_relative(); }
+  URI const & uri() const { return uri_; }
+  Anchor const & anchor() const { return anchor_; }
+
+  friend std::ostream & operator<<(std::ostream & os, RootReference const & self) {
+    return os << self.uri_ << '#' << self.anchor_;
+  }
+  auto operator<=>(RootReference const &) const = default;
+
+private:
+  RootReference(std::string_view ref, out<size_t> end) {
+    end = std::string::npos;
 
-  Reference(std::string_view ref, bool allow_anchor = true) {
     size_t end_of_uri = ref.find('#');
     uri_ = URI(ref.substr(0, end_of_uri));
     if (end_of_uri == std::string::npos) {
@@ -26,19 +48,39 @@ public:
     ref.remove_prefix(end_of_uri + 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");
 
+    EXPECT_M(end || pointer_start == std::string::npos, "JSON-Pointer is illegal in this context");
+    if (pointer_start != std::string::npos) {
+      end = pointer_start + end_of_uri + 1;
+    }
+  }
+};
+
+class Reference {
+private:
+  RootReference root_;
+  Pointer pointer_;
+
+public:
+  Reference() = default;
+
+  explicit Reference(RootReference const & root, Pointer const & pointer = {})
+      : root_(root), pointer_(pointer) {}
+
+  explicit Reference(std::string_view ref) {
+    size_t pointer_start = 0;
+    root_ = RootReference(ref, pointer_start);
     if (pointer_start != std::string::npos) {
       pointer_ = ref.substr(pointer_start);
     }
   }
 
-  URI const & uri() const { return uri_; }
-  Anchor const & anchor() const { return anchor_; }
+  URI const & uri() const { return root_.uri(); }
+  Anchor const & anchor() const { return root_.anchor(); }
   Pointer const & pointer() const { return pointer_; }
 
-  Reference root() const { return {uri_, anchor_}; }
-  Reference parent() const { return {uri_, anchor_, pointer_.parent()}; }
+  RootReference const & root() const { return root_; }
+  Reference parent() const { return Reference(root_, pointer_.parent()); }
 
   Reference & operator/=(Pointer const & relative) {
     pointer_ /= relative;
@@ -62,13 +104,8 @@ public:
   Reference operator/(size_t index) const { return Reference(*this) /= index; }
 
   friend std::ostream & operator<<(std::ostream & os, Reference const & self) {
-    return os << self.uri_ << '#' << self.anchor_ << self.pointer_;
+    return os << self.root_ << self.pointer_;
   }
   auto operator<=>(Reference const &) const = default;
-
-private:
-  URI uri_;
-  Anchor anchor_;
-  Pointer pointer_;
 };
 }

+ 4 - 15
include/jvalidate/document_cache.h

@@ -4,8 +4,6 @@
 #include <optional>
 
 #include <jvalidate/detail/expect.h>
-#include <jvalidate/detail/pointer.h>
-#include <jvalidate/detail/reference.h>
 #include <jvalidate/forward.h>
 #include <jvalidate/uri.h>
 
@@ -16,35 +14,26 @@ public:
 
 private:
   URIResolver<A> resolve_;
-  std::map<URI, A> references_;
   std::map<URI, value_type> cache_;
 
 public:
   DocumentCache() = default;
   DocumentCache(URIResolver<A> const & resolve) : resolve_(resolve) {}
 
-  void cache_reference(URI const & where, A const & handle) {
-    EXPECT(references_.emplace(where, handle).second);
-  }
-
   operator bool() const { return resolve_; }
 
-  std::optional<A> try_load(detail::Reference const & ref) {
-    if (auto it = references_.find(ref.uri()); it != references_.end()) {
-      return ref.pointer().walk(it->second);
-    }
-
+  std::optional<A> try_load(URI const & uri) {
     if (not resolve_) {
       return std::nullopt;
     }
 
-    auto [it, created] = cache_.try_emplace(ref.uri());
-    if (created && not resolve_(ref.uri(), it->second)) {
+    auto [it, created] = cache_.try_emplace(uri);
+    if (created && not resolve_(uri, it->second)) {
       cache_.erase(it);
       return std::nullopt;
     }
 
-    return ref.pointer().walk(A(it->second));
+    return A(it->second);
   }
 };
 }

+ 183 - 0
include/jvalidate/reference_handler.h

@@ -0,0 +1,183 @@
+#pragma once
+
+#include <functional>
+#include <map>
+#include <set>
+#include <unordered_map>
+
+#include <jvalidate/detail/anchor.h>
+#include <jvalidate/detail/pointer.h>
+#include <jvalidate/detail/reference.h>
+#include <jvalidate/enum.h>
+#include <jvalidate/forward.h>
+#include <jvalidate/uri.h>
+
+namespace jvalidate {
+template <Adapter A> class ReferenceManager {
+public:
+  using Keywords = std::unordered_map<std::string_view, std::set<schema::Wraps>>;
+
+private:
+  std::map<detail::RootReference, A> roots_;
+
+  std::map<detail::Reference, detail::RootReference, std::greater<>> absolute_to_canonical_;
+  std::map<detail::RootReference, detail::Reference, std::greater<>> canonical_to_absolute_;
+
+public:
+  ReferenceManager(A const & root, Keywords const & keywords) : roots_{{{}, root}} {
+    prime_impl(root, {}, keywords);
+  }
+
+  std::optional<A> root(detail::RootReference const & root) const {
+    if (auto it = roots_.find(root); it != roots_.end()) {
+      return it->second;
+    }
+    return std::nullopt;
+  }
+
+  detail::Reference canonicalize(detail::Reference const & ref) const {
+    if (canonical_to_absolute_.contains(ref.root())) {
+      return ref;
+    }
+
+    auto it = absolute_to_canonical_.upper_bound(ref);
+    if (it == absolute_to_canonical_.end()) {
+      return ref;
+    }
+
+    auto const & [absolute, anchor] = *it;
+    if (not ref.pointer().starts_with(absolute.pointer())) {
+      return ref;
+    }
+
+    return detail::Reference(anchor, ref.pointer().relative_to(absolute.pointer()));
+  }
+
+  detail::Reference canonicalize(detail::Reference const & ref,
+                                 detail::Reference const & parent) const {
+
+    // Relative URI, not in the HEREDOC (or we set an $id)
+    if (ref.uri().empty() and ref.anchor().empty()) {
+      return detail::Reference(canonicalize(parent).root(), ref.pointer());
+    }
+
+    // TODO(samjaffe): Clean this clause up
+    URI uri = ref.uri().empty() ? parent.root().uri() : ref.uri();
+    if (uri.is_rootless() && not uri.empty()) {
+      auto parent_uri = canonicalize(parent).uri();
+      if (parent_uri == parent.uri() && parent.uri().empty()) {
+        auto it = absolute_to_canonical_.find(detail::Reference());
+        if (it != absolute_to_canonical_.end()) {
+          parent_uri = it->second.uri();
+        }
+      }
+      EXPECT_M(parent_uri.resource().rfind('/') != std::string::npos,
+               "Unable to deduce root for relative uri " << uri << " (" << parent_uri << ")");
+      uri = (uri.is_relative() ? parent_uri.parent() : parent_uri.root()) / uri;
+    }
+
+    detail::Reference rval(detail::RootReference(uri, ref.anchor()), ref.pointer());
+
+    // Will now need to go make an external fetch...
+    // TODO(samjaffe): Make that process internal?
+    return rval;
+  }
+
+  void prime(Adapter auto const & json, detail::RootReference const & where,
+             Keywords const & keywords) {
+    canonical_to_absolute_.emplace(where, detail::Reference(where));
+    prime_impl(json, detail::Reference(where), keywords);
+  }
+
+private:
+  void prime_impl(Adapter auto const & json, detail::Reference where, Keywords const & keywords) {
+    if (json.type() != adapter::Type::Object) {
+      return;
+    }
+
+    canonicalize(where, json);
+
+    for (auto const & [key, value] : json.as_object()) {
+      auto vit = keywords.find(key);
+      if (vit == keywords.end()) {
+        continue;
+      }
+
+      if (vit->second.contains(schema::Wraps::Array) && value.type() == adapter::Type::Array) {
+        size_t index = 0;
+        for (auto const & elem : value.as_array()) {
+          prime_impl(elem, where / key / index, keywords);
+          ++index;
+        }
+      } else if (vit->second.contains(schema::Wraps::Object) &&
+                 value.type() == adapter::Type::Object) {
+        for (auto const & [prop, elem] : value.as_object()) {
+          prime_impl(elem, where / key / prop, keywords);
+        }
+      } else if (vit->second.contains(schema::Wraps::Schema)) {
+        prime_impl(value, where / key, keywords);
+      }
+    }
+  }
+
+  void canonicalize(detail::Reference & where, A const & json) {
+    auto const schema = json.as_object();
+
+    detail::RootReference root = where.root();
+    if (schema.contains("$id")) {
+      root = detail::RootReference(schema["$id"].as_string());
+      if (root.uri().empty()) {
+        root = detail::RootReference(where.uri(), root.anchor());
+      } else if (root.uri().is_rootless() && not where.uri().empty()) {
+        // TODO(samjaffe): Should there also be something for is_relative?
+        root = detail::RootReference(where.uri().parent() / root.uri(), root.anchor());
+      }
+
+      cache(root, where, json);
+    }
+
+    if (not schema.contains("$anchor")) {
+      return;
+    }
+
+    EXPECT_M(root.anchor().empty(), "Cannot have $id with anchor and $anchor tags at same time");
+    root = detail::RootReference(root.uri(), detail::Anchor(schema["$anchor"].as_string()));
+
+    cache(root, where, json);
+  }
+
+  template <size_t I>
+  bool extract_relative(detail::Reference const & where, auto const & tup,
+                        detail::Pointer & out) const {
+    auto const & ptr = std::get<I>(tup).pointer();
+    if (where.pointer().starts_with(ptr)) {
+      out = where.pointer().relative_to(ptr);
+      return true;
+    }
+    return false;
+  }
+
+  void recursive_cache(detail::RootReference const & root, detail::Reference const & where) {
+    if (where.pointer().empty()) {
+      return;
+    }
+    detail::Pointer relative;
+    if (auto it = canonical_to_absolute_.lower_bound(where.root());
+        it != canonical_to_absolute_.end() && where.root() != it->second.root() &&
+        extract_relative<1>(where, *it, relative)) {
+      absolute_to_canonical_.emplace(it->second / relative, root);
+      recursive_cache(root, it->second / relative);
+    }
+  }
+
+  void cache(detail::RootReference const & root, detail::Reference & where,
+             Adapter auto const & json) {
+    recursive_cache(root, where);
+    absolute_to_canonical_.emplace(where, root);
+    canonical_to_absolute_.emplace(root, where);
+
+    roots_.emplace(root, json);
+    where = detail::Reference(root);
+  }
+};
+}

+ 15 - 60
include/jvalidate/schema.h

@@ -16,6 +16,7 @@
 #include <jvalidate/document_cache.h>
 #include <jvalidate/enum.h>
 #include <jvalidate/forward.h>
+#include <jvalidate/reference_handler.h>
 
 namespace jvalidate::schema {
 class Node {
@@ -47,7 +48,7 @@ public:
 
   bool requires_result_context() const { return not post_constraints_.empty(); }
   auto const & constraints() const { return constraints_; }
-  auto const & post_constraints() const { return constraints_; }
+  auto const & post_constraints() const { return post_constraints_; }
 
   adapter::Const const * default_value() const { return default_.get(); }
 
@@ -104,6 +105,7 @@ class Schema : public schema::Node {
 private:
   friend class schema::Node;
   template <Adapter A> friend class detail::ParserContext;
+
   struct DynamicRef {
     template <typename F>
     DynamicRef(detail::Reference const & where, F const & reconstruct)
@@ -117,10 +119,6 @@ private:
   schema::Node accept_;
   schema::Node reject_{"always false"};
 
-  // A map of (URI, Anchor) => (URI, Pointer), binding an anchor reference
-  // to it's fully resolved path.
-  std::map<detail::Reference, detail::Reference> anchors_;
-
   // A map of anchors to DynamicRef info - note that DynamicRef.reconstruct is
   // an unsafe object, because it holds an object which may hold references to
   // temporary objects.
@@ -174,8 +172,8 @@ public:
       return;
     }
 
-    external.cache_reference(URI(), json);
-    detail::ParserContext<A> root{*this, json, version, factory, external};
+    ReferenceManager<A> ref(json, factory.keywords(version));
+    detail::ParserContext<A> root{*this, json, version, factory, external, ref};
     construct(root);
   }
 
@@ -230,18 +228,6 @@ public:
       : Schema(adapter::AdapterFor<JSON const>(json), std::forward<Args>(args)...) {}
 
 private:
-  /**
-   * @brief Associate an anchor with its absolute path
-   * @pre We should not already have an anchor associated with this anchor
-   *
-   * @param anchor A URI-Reference containing only a URI and Anchor
-   * @param from A URI-Reference representing the absolute path to this Anchor
-   */
-  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, detail::ParserContext<A> const & context) {
     dynamic_anchors_.try_emplace(anchor, context.where,
@@ -256,16 +242,11 @@ private:
   }
 
   schema::Node const * alias(detail::Reference const & where, schema::Node const * schema) {
-    EXPECT_M(alias_cache_.try_emplace(where, schema).second,
-             "more than one schema found with uri " << where);
+    alias_cache_.emplace(where, schema);
     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();
-    }
-
+  std::optional<schema::Node const *> from_cache(detail::Reference const & ref) {
     if (auto it = alias_cache_.find(ref); it != alias_cache_.end()) {
       return it->second;
     }
@@ -275,34 +256,26 @@ private:
 
   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 (ref == detail::Reference() && context.where.uri().empty()) {
-      return this;
-    }
-    if (ref.uri().empty()) {
-      ref = detail::Reference(context.where.uri(), ref.anchor(), ref.pointer());
+    if (std::optional root = context.ref.root(ref.root())) {
+      return fetch_schema(context.rebind(ref.pointer().walk(*root), ref));
     }
 
     if (std::optional cached = from_cache(ref)) {
       return *cached;
     }
 
-    // SPECIAL RULE: Resolve this URI into the context of the calling URI
-    if (not ref.uri().empty() && ref.uri().scheme().empty()) {
-      URI const & relative_to = context.where.uri();
-      EXPECT_M(relative_to.resource().rfind('/') != std::string::npos,
-               "Unable to deduce root for relative uri " << ref.uri() << " (" << relative_to
-                                                         << ")");
-      ref = detail::Reference(relative_to.parent() / ref.uri(), ref.anchor(), ref.pointer());
-    }
-
-    std::optional schema = context.external.try_load(ref);
+    std::optional schema = context.external.try_load(ref.uri());
     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);
     }
 
-    return fetch_schema(context.rebind(*schema, ref));
+    context.ref.prime(*schema, ref.root(), context.factory.keywords(context.version));
+    ref = context.ref.canonicalize(ref, context.where);
+    return fetch_schema(context.rebind(ref.pointer().walk(*schema), ref));
   }
 
   schema::Node const * resolve_dynamic(detail::Anchor const & ref) {
@@ -366,12 +339,6 @@ template <Adapter A> detail::OnBlockExit Node::resolve_anchor(detail::ParserCont
   auto const schema = context.schema.as_object();
 
   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;
   }
 
@@ -436,18 +403,6 @@ template <Adapter A> void Node::construct(detail::ParserContext<A> context) {
     context.version = schema_version(context.schema);
   }
 
-  if (schema.contains("$id")) {
-    detail::Reference id(schema["$id"].as_string(), false);
-    if (id.uri().scheme().empty() and not context.where.uri().empty()) {
-      id = detail::Reference(context.where.uri().parent() / id.uri(), {}, id.pointer());
-    }
-
-    if (id != context.where) {
-      context.external.cache_reference(id.uri(), context.schema);
-      context.root.alias(context.where = id, this);
-    }
-  }
-
   [[maybe_unused]] auto _ = resolve_anchor(context);
   bool const has_reference = resolve_reference(context);
 

+ 3 - 0
include/jvalidate/uri.h

@@ -32,12 +32,15 @@ public:
   }
 
   URI parent() const { return URI(std::string_view(uri_).substr(0, uri_.rfind('/'))); }
+  URI root() const { return URI(std::string_view(uri_).substr(0, uri_.find('/', resource_))); }
 
   URI operator/(URI const & relative) const {
     std::string div = uri_.ends_with("/") || relative.uri_.starts_with("/") ? "" : "/";
     return URI(uri_ + div + relative.uri_);
   }
 
+  bool is_rootless() const { return scheme_ == 0; }
+  bool is_relative() const { return uri_[resource_] != '/'; }
   std::string_view scheme() const { return std::string_view(uri_).substr(0, scheme_); }
   std::string_view resource() const { return std::string_view(uri_).substr(resource_); }
 

+ 13 - 3
include/jvalidate/validation_result.h

@@ -13,6 +13,8 @@ public:
   template <Adapter A, RegexEngine RE> friend class ValidationVisitor;
 
   struct Errors {
+    bool empty() const { return message.empty() && properties.empty() && items.empty(); }
+
     std::string constraint;
     std::string message;
     std::map<std::string, ValidationResult> properties;
@@ -31,6 +33,10 @@ public:
     (errors_.empty() ? message_ : errors_.back().message) = message;
   }
 
+  bool empty() const {
+    return message_.empty() &&
+           std::all_of(errors_.begin(), errors_.end(), [](auto & e) { return e.empty(); });
+  }
   bool has_visited(size_t item) const { return visited_items_.contains(item); }
   bool has_visited(std::string const & property) const {
     return visited_properties_.contains(property);
@@ -51,7 +57,9 @@ private:
     if (errors_.empty()) {
       return;
     }
-    errors_.back().items.emplace(item, std::move(result));
+    if (!result.empty()) {
+      errors_.back().items.emplace(item, std::move(result));
+    }
     visited_items_.emplace(item);
   }
 
@@ -59,7 +67,9 @@ private:
     if (errors_.empty()) {
       return;
     }
-    errors_.back().properties.emplace(property, std::move(result));
+    if (!result.empty()) {
+      errors_.back().properties.emplace(property, std::move(result));
+    }
     visited_properties_.emplace(property);
   }
 
@@ -77,7 +87,7 @@ private:
     }
 
     for (auto const & error : errors_) {
-      if (error.items.empty() and error.properties.empty()) {
+      if (error.empty()) {
         continue;
       }
 

+ 1 - 1
include/jvalidate/validation_visitor.h

@@ -52,7 +52,7 @@ public:
         return Status::Accept;
       }
     }
-    add_error("type ", type, " is not allowed ", cons.types);
+    add_error("type ", type, " is not one of {", cons.types, '}');
     return Status::Reject;
   }
 

+ 60 - 1
tests/selfvalidate_test.cxx

@@ -3,8 +3,11 @@
 #include <filesystem>
 #include <fstream>
 #include <iostream>
+#include <unordered_set>
 
 #include <curl/curl.h>
+#include <gmock/gmock.h>
+#include <gtest/gtest.h>
 
 #include <jvalidate/adapter.h>
 #include <jvalidate/adapters/jsoncpp.h>
@@ -26,6 +29,15 @@ using testing::Combine;
 using testing::TestWithParam;
 using testing::Values;
 
+struct RecursiveTestFilter {
+  std::unordered_set<std::string> whitelist;
+  std::unordered_set<std::string> blacklist;
+
+  bool accepts(std::string const & str) const {
+    return not blacklist.contains(str) and (whitelist.empty() or whitelist.contains(str));
+  }
+};
+
 bool load_stream(std::istream & in, Json::Value & out) {
   Json::CharReaderBuilder builder;
   std::string error;
@@ -72,7 +84,48 @@ bool load_external_for_test(jvalidate::URI const & uri, Json::Value & out) {
   }
 }
 
-struct JsonSchema : TestWithParam<std::tuple<Version, std::filesystem::path>> {};
+class JsonSchema : public TestWithParam<std::tuple<Version, std::filesystem::path>> {
+private:
+  static RecursiveTestFilter s_suite_filter;
+  static RecursiveTestFilter s_case_filter;
+
+protected:
+  bool skip_suite(std::string const & desc) const { return not s_suite_filter.accepts(desc); }
+  bool skip_case(std::string const & desc) const { return not s_case_filter.accepts(desc); }
+
+  static void SetUpTestCase() {
+    for (std::string_view str : testing::internal::GetArgvs()) {
+      RecursiveTestFilter * ptr;
+      if (str.starts_with("--json_suite_filter=")) {
+        str.remove_prefix(20);
+        ptr = &s_suite_filter;
+      } else if (str.starts_with("--json_case_filter=")) {
+        str.remove_prefix(19);
+        ptr = &s_case_filter;
+      } else {
+        continue;
+      }
+
+      size_t const pos_end = str[0] == '-' ? 0 : str.find(":-");
+      size_t const neg_end =
+          pos_end == 0 ? 1 : (pos_end == std::string::npos ? pos_end : pos_end + 2);
+
+      if (pos_end > 0) {
+        std::vector<std::string> tokens;
+        testing::internal::SplitString(std::string(str.substr(0, pos_end)), ':', &tokens);
+        ptr->whitelist.insert(tokens.begin(), tokens.end());
+      }
+      if (neg_end != std::string::npos) {
+        std::vector<std::string> tokens;
+        testing::internal::SplitString(std::string(str.substr(neg_end)), ':', &tokens);
+        ptr->blacklist.insert(tokens.begin(), tokens.end());
+      }
+    }
+  }
+};
+
+RecursiveTestFilter JsonSchema::s_suite_filter;
+RecursiveTestFilter JsonSchema::s_case_filter;
 
 TEST_P(JsonSchema, TestSuite) {
   auto const & [version, file] = GetParam();
@@ -81,10 +134,16 @@ TEST_P(JsonSchema, TestSuite) {
   EXPECT_TRUE(load_file(file, spec));
 
   for (auto const & suite : spec) {
+    if (skip_suite(suite["description"].asString())) {
+      continue;
+    }
     std::cout << "\033[0;32m[ SUITE    ] \033[0;0m" << suite["description"].asString() << std::endl;
     try {
       jvalidate::Schema schema(suite["schema"], version, &load_external_for_test);
       for (auto const & test : suite["tests"]) {
+        if (skip_case(test["description"].asString())) {
+          continue;
+        }
         try {
           std::cout << "\033[0;32m[ CASE     ] \033[0;0m    " << test["description"].asString()
                     << std::endl;