Преглед на файлове

Merge branch 'feat/extension-model'

Sam Jaffe преди 3 месеца
родител
ревизия
48089512d6

+ 12 - 2
Makefile

@@ -23,10 +23,10 @@ HEADERS := $(shell find $(INCLUDE_DIR) -name *.h)
 TEST_HEADERS := $(wildcard $(TEST_DIR)*.h)
 TEST_SOURCES := $(wildcard $(TEST_DIR)*.cxx)
 TEST_OBJECTS := $(patsubst %.cxx, .build/%.o, $(TEST_SOURCES))
-TEST_BINARIES := .build/bin/selfvalidate .build/bin/annotation_test
+TEST_BINARIES := .build/bin/selfvalidate .build/bin/annotation_test .build/bin/extension_test
 EXECUTE_TESTS := $(patsubst %, %.done, $(TEST_BINARIES))
 
-EXCLUDED_TESTS := format* content ecmascript_regex zeroTerminatedFloats
+EXCLUDED_TESTS := format* content ecmascript_regex zeroTerminatedFloats non_bmp_regex
 EXCLUDED_TESTS := $(shell printf ":*optional_%s" $(EXCLUDED_TESTS) | cut -c2-)
 
 all: run-test
@@ -67,3 +67,13 @@ run-test: $(EXECUTE_TESTS)
 .build/bin/annotation_test.done: .build/bin/annotation_test
 	.build/bin/annotation_test $(CLEAN_ANSI)
 	@ touch $@
+
+
+.build/bin/extension_test: .build/tests/extension_test.o
+	@ mkdir -p .build/bin
+	@ rm -f .build/test/extension_test.done
+	$(CXX) $< -o $@ $(LD_FLAGS) -ljsoncpp -lgmock -lgtest
+
+.build/bin/extension_test.done: .build/bin/extension_test
+	.build/bin/extension_test $(CLEAN_ANSI)
+	@ touch $@

+ 62 - 56
include/jvalidate/constraint.h

@@ -9,6 +9,7 @@
 #include <unordered_set>
 
 #include <jvalidate/constraint/array_constraint.h>
+#include <jvalidate/constraint/extension_constraint.h>
 #include <jvalidate/constraint/general_constraint.h>
 #include <jvalidate/constraint/number_constraint.h>
 #include <jvalidate/constraint/object_constraint.h>
@@ -157,6 +158,10 @@ private:
 public:
   ConstraintFactory() = default;
 
+  static pConstraint ptr(auto && in) {
+    return std::make_unique<constraint::Constraint>(std::move(in));
+  }
+
   /**
    * @brief Construct a new ConstraintFactory, with a pre-defined list of user
    * keywords to be injected into the vocabulary. Does not override any keywords
@@ -249,7 +254,7 @@ public:
 
     adapter::Type const type = context.schema.type();
     if (type == adapter::Type::String) {
-      return std::make_unique<constraint::TypeConstraint>(to_type(context.schema.as_string()));
+      return ptr(constraint::TypeConstraint{{to_type(context.schema.as_string())}});
     }
 
     EXPECT(type == adapter::Type::Array);
@@ -257,7 +262,7 @@ public:
     for (auto subschema : context.schema.as_array()) {
       types.insert(to_type(subschema.as_string()));
     }
-    return std::make_unique<constraint::TypeConstraint>(types);
+    return ptr(constraint::TypeConstraint{types});
   }
 
   /**
@@ -305,11 +310,11 @@ public:
       if (context.schema.as_string() == "any") {
         return nullptr; // nullptr is a synonym for "always accept"
       }
-      return std::make_unique<constraint::TypeConstraint>(to_type(context.schema.as_string()));
+      return ptr(constraint::TypeConstraint{{to_type(context.schema.as_string())}});
     }
 
     EXPECT(type == adapter::Type::Array);
-    std::vector<schema::Node const *> children;
+    std::vector<constraint::SubConstraint> children;
 
     std::set<adapter::Type> types;
     for (auto const & [index, subschema] : detail::enumerate(context.schema.as_array())) {
@@ -322,9 +327,8 @@ public:
       }
     }
 
-    auto rval = std::make_unique<constraint::AnyOfConstraint>(children);
-    rval->children.push_back(std::make_unique<constraint::TypeConstraint>(types));
-    return rval;
+    children.push_back(ptr(constraint::TypeConstraint{types}));
+    return ptr(constraint::AnyOfConstraint{std::move(children)});
   }
 
   /**
@@ -344,7 +348,7 @@ public:
    * @throws {@see ConstraintFactory::typeDraft3}
    */
   static pConstraint disallowDraft3(detail::ParserContext<A> const & context) {
-    return std::make_unique<constraint::NotConstraint>(typeDraft3(context));
+    return ptr(constraint::NotConstraint{typeDraft3(context)});
   }
 
   /**
@@ -371,7 +375,7 @@ public:
    * @throws any std::exception if precondition #2 is broken
    */
   static pConstraint extendsDraft3(detail::ParserContext<A> const & context) {
-    std::vector<schema::Node const *> children;
+    std::vector<constraint::SubConstraint> children;
     switch (context.schema.type()) {
     case adapter::Type::Object:
       children.push_back(context.node());
@@ -386,7 +390,7 @@ public:
       JVALIDATE_THROW(std::runtime_error, "extends must be a schema of array-of-schemas");
     }
 
-    return std::make_unique<constraint::AllOfConstraint>(children);
+    return ptr(constraint::AllOfConstraint{std::move(children)});
   }
 
   /**
@@ -415,7 +419,7 @@ public:
       else_ = context.neighbor("else").node();
     }
 
-    return std::make_unique<constraint::ConditionalConstraint>(context.node(), then_, else_);
+    return ptr(constraint::ConditionalConstraint{context.node(), then_, else_});
   }
 
   /**
@@ -435,7 +439,7 @@ public:
       rval.push_back(subschema.freeze());
     }
 
-    return std::make_unique<constraint::EnumConstraint>(std::move(rval));
+    return ptr(constraint::EnumConstraint{std::move(rval)});
   }
 
   /**
@@ -448,7 +452,9 @@ public:
    * @returns A constraint that checks equality against a single value.
    */
   static auto isConstant(detail::ParserContext<A> const & context) {
-    return std::make_unique<constraint::EnumConstraint>(context.schema.freeze());
+    constraint::EnumConstraint rval;
+    rval.enumeration.push_back(context.schema.freeze());
+    return ptr(rval);
   }
 
   /**
@@ -466,12 +472,12 @@ public:
   static auto allOf(detail::ParserContext<A> const & context) {
     EXPECT(context.schema.type() == adapter::Type::Array);
 
-    std::vector<schema::Node const *> rval;
+    std::vector<constraint::SubConstraint> rval;
     for (auto const & [index, subschema] : detail::enumerate(context.schema.as_array())) {
       rval.push_back(context.child(subschema, index).node());
     }
 
-    return std::make_unique<constraint::AllOfConstraint>(rval);
+    return ptr(constraint::AllOfConstraint{std::move(rval)});
   }
 
   /**
@@ -489,12 +495,12 @@ public:
   static auto anyOf(detail::ParserContext<A> const & context) {
     EXPECT(context.schema.type() == adapter::Type::Array);
 
-    std::vector<schema::Node const *> rval;
+    std::vector<constraint::SubConstraint> rval;
     for (auto const & [index, subschema] : detail::enumerate(context.schema.as_array())) {
       rval.push_back(context.child(subschema, index).node());
     }
 
-    return std::make_unique<constraint::AnyOfConstraint>(rval);
+    return ptr(constraint::AnyOfConstraint{std::move(rval)});
   }
 
   /**
@@ -517,7 +523,7 @@ public:
       rval.push_back(context.child(subschema, index).node());
     }
 
-    return std::make_unique<constraint::OneOfConstraint>(rval);
+    return ptr(constraint::OneOfConstraint{rval});
   }
 
   /**
@@ -529,7 +535,7 @@ public:
    * @returns A NotConstraint
    */
   static auto isNot(detail::ParserContext<A> const & context) {
-    return std::make_unique<constraint::NotConstraint>(context.node());
+    return ptr(constraint::NotConstraint{context.node()});
   }
 
   // SECTION: Numeric Constraints
@@ -556,9 +562,9 @@ public:
         context.parent->contains("exclusiveMinimum")) {
       auto exclusive = (*context.parent)["exclusiveMinimum"];
       EXPECT(exclusive.type() == adapter::Type::Boolean);
-      return std::make_unique<constraint::MinimumConstraint>(value, exclusive.as_boolean());
+      return ptr(constraint::MinimumConstraint{value, exclusive.as_boolean()});
     }
-    return std::make_unique<constraint::MinimumConstraint>(value, false);
+    return ptr(constraint::MinimumConstraint{value, false});
   }
 
   /**
@@ -574,7 +580,7 @@ public:
    */
   static pConstraint exclusiveMinimum(detail::ParserContext<A> const & context) {
     double value = context.schema.as_number();
-    return std::make_unique<constraint::MinimumConstraint>(value, true);
+    return ptr(constraint::MinimumConstraint{value, true});
   }
 
   /**
@@ -599,9 +605,9 @@ public:
         context.parent->contains("exclusiveMaximum")) {
       auto exclusive = (*context.parent)["exclusiveMaximum"];
       EXPECT(exclusive.type() == adapter::Type::Boolean);
-      return std::make_unique<constraint::MaximumConstraint>(value, exclusive.as_boolean());
+      return ptr(constraint::MaximumConstraint{value, exclusive.as_boolean()});
     }
-    return std::make_unique<constraint::MaximumConstraint>(value, false);
+    return ptr(constraint::MaximumConstraint{value, false});
   }
 
   /**
@@ -617,7 +623,7 @@ public:
    */
   static pConstraint exclusiveMaximum(detail::ParserContext<A> const & context) {
     double value = context.schema.as_number();
-    return std::make_unique<constraint::MaximumConstraint>(value, true);
+    return ptr(constraint::MaximumConstraint{value, true});
   }
 
   /**
@@ -639,7 +645,7 @@ public:
    */
   static auto multipleOf(detail::ParserContext<A> const & context) {
     double value = context.schema.as_number();
-    return std::make_unique<constraint::MultipleOfConstraint>(value);
+    return ptr(constraint::MultipleOfConstraint{value});
   }
 
   // SECTION: String Constraints
@@ -661,7 +667,7 @@ public:
   static auto minLength(detail::ParserContext<A> const & context) {
     EXPECT(context.schema.type() == adapter::Type::Integer ||
            context.schema.type() == adapter::Type::Number);
-    return std::make_unique<constraint::MinLengthConstraint>(context.schema.as_integer());
+    return ptr(constraint::MinLengthConstraint{context.schema.as_integer()});
   }
 
   /**
@@ -681,7 +687,7 @@ public:
   static auto maxLength(detail::ParserContext<A> const & context) {
     EXPECT(context.schema.type() == adapter::Type::Integer ||
            context.schema.type() == adapter::Type::Number);
-    return std::make_unique<constraint::MaxLengthConstraint>(context.schema.as_integer());
+    return ptr(constraint::MaxLengthConstraint{context.schema.as_integer()});
   }
 
   /**
@@ -702,7 +708,7 @@ public:
    * @throws If the contained value is not interpretable as a string
    */
   static auto pattern(detail::ParserContext<A> const & context) {
-    return std::make_unique<constraint::PatternConstraint>(context.schema.as_string());
+    return ptr(constraint::PatternConstraint{context.schema.as_string()});
   }
 
   /**
@@ -723,8 +729,8 @@ public:
    * @throws If the contained value is not interpretable as a string
    */
   static auto format(detail::ParserContext<A> const & context) {
-    return std::make_unique<constraint::FormatConstraint>(context.schema.as_string(),
-                                                          context.vocab->is_format_assertion());
+    return ptr(constraint::FormatConstraint{context.schema.as_string(),
+                                            context.vocab->is_format_assertion()});
   }
 
   // SECTION: Array Constraints
@@ -752,7 +758,7 @@ public:
    */
   static auto contains(detail::ParserContext<A> const & context) {
     if (context.vocab->version() < schema::Version::Draft2019_09) {
-      return std::make_unique<constraint::ContainsConstraint>(context.node());
+      return ptr(constraint::ContainsConstraint{context.node()});
     }
 
     std::optional<size_t> maximum;
@@ -764,7 +770,7 @@ public:
       minimum = (*context.parent)["minContains"].as_integer();
     }
 
-    return std::make_unique<constraint::ContainsConstraint>(context.node(), minimum, maximum);
+    return ptr(constraint::ContainsConstraint{context.node(), minimum, maximum});
   }
 
   /**
@@ -783,7 +789,7 @@ public:
   static auto minItems(detail::ParserContext<A> const & context) {
     EXPECT(context.schema.type() == adapter::Type::Integer ||
            context.schema.type() == adapter::Type::Number);
-    return std::make_unique<constraint::MinItemsConstraint>(context.schema.as_integer());
+    return ptr(constraint::MinItemsConstraint{context.schema.as_integer()});
   }
 
   /**
@@ -802,7 +808,7 @@ public:
   static auto maxItems(detail::ParserContext<A> const & context) {
     EXPECT(context.schema.type() == adapter::Type::Integer ||
            context.schema.type() == adapter::Type::Number);
-    return std::make_unique<constraint::MaxItemsConstraint>(context.schema.as_integer());
+    return ptr(constraint::MaxItemsConstraint{context.schema.as_integer()});
   }
 
   /**
@@ -829,7 +835,7 @@ public:
       rval.push_back(context.child(subschema, index).node());
     }
 
-    return std::make_unique<constraint::TupleConstraint>(rval);
+    return ptr(constraint::TupleConstraint{rval});
   }
 
   /**
@@ -869,10 +875,10 @@ public:
     // "additionalProperties" keywords.
     if (context.vocab->version() < schema::Version::Draft06 &&
         context.schema.type() == adapter::Type::Boolean) {
-      return std::make_unique<constraint::AdditionalItemsConstraint>(context.always(), n);
+      return ptr(constraint::AdditionalItemsConstraint{context.always(), n});
     }
 
-    return std::make_unique<constraint::AdditionalItemsConstraint>(context.node(), n);
+    return ptr(constraint::AdditionalItemsConstraint{context.node(), n});
   }
 
   /**
@@ -900,7 +906,7 @@ public:
       return prefixItems(context);
     }
 
-    return std::make_unique<constraint::AdditionalItemsConstraint>(context.node(), 0);
+    return ptr(constraint::AdditionalItemsConstraint{context.node(), 0});
   }
 
   /**
@@ -917,7 +923,7 @@ public:
    * @returns An AdditionalPropertiesConstraint
    */
   static auto unevaluatedItems(detail::ParserContext<A> const & context) {
-    return std::make_unique<constraint::UnevaluatedItemsConstraint>(context.node());
+    return ptr(constraint::UnevaluatedItemsConstraint{context.node()});
   }
 
   /**
@@ -938,7 +944,7 @@ public:
       return nullptr;
     }
 
-    return std::make_unique<constraint::UniqueItemsConstraint>();
+    return ptr(constraint::UniqueItemsConstraint{});
   }
 
   // SECTION: Object Constraints
@@ -967,7 +973,7 @@ public:
       rval.insert(subschema.as_string());
     }
 
-    return std::make_unique<constraint::RequiredConstraint>(rval);
+    return ptr(constraint::RequiredConstraint{rval});
   }
 
   /**
@@ -986,7 +992,7 @@ public:
   static auto minProperties(detail::ParserContext<A> const & context) {
     EXPECT(context.schema.type() == adapter::Type::Integer ||
            context.schema.type() == adapter::Type::Number);
-    return std::make_unique<constraint::MinPropertiesConstraint>(context.schema.as_integer());
+    return ptr(constraint::MinPropertiesConstraint{context.schema.as_integer()});
   }
 
   /**
@@ -1005,7 +1011,7 @@ public:
   static auto maxProperties(detail::ParserContext<A> const & context) {
     EXPECT(context.schema.type() == adapter::Type::Integer ||
            context.schema.type() == adapter::Type::Number);
-    return std::make_unique<constraint::MaxPropertiesConstraint>(context.schema.as_integer());
+    return ptr(constraint::MaxPropertiesConstraint{context.schema.as_integer()});
   }
 
   /**
@@ -1036,7 +1042,7 @@ public:
       rval.emplace_back(prop, context.child(subschema, prop).node());
     }
 
-    return std::make_unique<constraint::PatternPropertiesConstraint>(rval);
+    return ptr(constraint::PatternPropertiesConstraint{rval});
   }
 
   /**
@@ -1061,7 +1067,7 @@ public:
       rval.emplace(prop, context.child(subschema, prop).node());
     }
 
-    return std::make_unique<constraint::PropertiesConstraint>(rval);
+    return ptr(constraint::PropertiesConstraint{rval});
   }
 
   /**
@@ -1106,10 +1112,10 @@ public:
       return properties(context);
     }
 
-    std::vector<pConstraint> rval;
+    std::vector<constraint::SubConstraint> rval;
     rval.push_back(properties(context));
-    rval.push_back(std::make_unique<constraint::RequiredConstraint>(std::move(required)));
-    return std::make_unique<constraint::AllOfConstraint>(std::move(rval));
+    rval.push_back(ptr(constraint::RequiredConstraint{std::move(required)}));
+    return ptr(constraint::AllOfConstraint{std::move(rval)});
   }
 
   /**
@@ -1122,7 +1128,7 @@ public:
    * @returns A PropertyNamesConstraint
    */
   static auto propertyNames(detail::ParserContext<A> const & context) {
-    return std::make_unique<constraint::PropertyNamesConstraint>(context.node());
+    return ptr(constraint::PropertyNamesConstraint{context.node()});
   }
 
   /**
@@ -1139,7 +1145,7 @@ public:
    * @returns An AdditionalPropertiesConstraint
    */
   static auto unevaluatedProperties(detail::ParserContext<A> const & context) {
-    return std::make_unique<constraint::UnevaluatedPropertiesConstraint>(context.node());
+    return ptr(constraint::UnevaluatedPropertiesConstraint{context.node()});
   }
 
   /**
@@ -1177,10 +1183,10 @@ public:
     // "additionalProperties" keywords.
     if (context.vocab->version() < schema::Version::Draft06 &&
         context.schema.type() == adapter::Type::Boolean) {
-      return std::make_unique<C>(context.always(), properties, patterns);
+      return ptr(C{context.always(), properties, patterns});
     }
 
-    return std::make_unique<C>(context.node(), properties, patterns);
+    return ptr(C{context.node(), properties, patterns});
   }
 
   /**
@@ -1230,7 +1236,7 @@ public:
       }
     }
 
-    return std::make_unique<constraint::DependenciesConstraint>(schemas, required);
+    return ptr(constraint::DependenciesConstraint{schemas, required});
   }
 
   /**
@@ -1261,7 +1267,7 @@ public:
       rval.emplace(prop, context.child(subschema, prop).node());
     }
 
-    return std::make_unique<constraint::DependenciesConstraint>(rval);
+    return ptr(constraint::DependenciesConstraint{rval});
   }
 
   /**
@@ -1295,7 +1301,7 @@ public:
       }
     }
 
-    return std::make_unique<constraint::DependenciesConstraint>(rval);
+    return ptr(constraint::DependenciesConstraint{{}, rval});
   }
 };
 }

+ 9 - 42
include/jvalidate/constraint/array_constraint.h

@@ -1,71 +1,38 @@
 #pragma once
 
-#include <jvalidate/detail/expect.h>
 #include <optional>
 #include <vector>
 
 #include <jvalidate/adapter.h>
-#include <jvalidate/constraint/constraint.h>
 #include <jvalidate/forward.h>
 
 namespace jvalidate::constraint {
-class AdditionalItemsConstraint : public SimpleConstraint<AdditionalItemsConstraint> {
-public:
+struct AdditionalItemsConstraint {
   schema::Node const * subschema;
   size_t applies_after_nth;
-
-public:
-  AdditionalItemsConstraint(schema::Node const * subschema, size_t applies_after_nth)
-      : subschema(subschema), applies_after_nth(applies_after_nth) {}
 };
 
-class ContainsConstraint : public SimpleConstraint<ContainsConstraint> {
-public:
+struct ContainsConstraint {
   schema::Node const * subschema;
-  std::optional<size_t> minimum;
-  std::optional<size_t> maximum;
-
-public:
-  ContainsConstraint(schema::Node const * subschema) : subschema(subschema) {}
-
-  ContainsConstraint(schema::Node const * subschema, std::optional<size_t> minimum,
-                     std::optional<size_t> maximum)
-      : subschema(subschema), minimum(minimum), maximum(maximum) {}
+  std::optional<size_t> minimum = std::nullopt;
+  std::optional<size_t> maximum = std::nullopt;
 };
 
-class MaxItemsConstraint : public SimpleConstraint<MaxItemsConstraint> {
-public:
+struct MaxItemsConstraint {
   int64_t value;
-
-public:
-  MaxItemsConstraint(int64_t value) : value(value) {}
 };
 
-class MinItemsConstraint : public SimpleConstraint<MinItemsConstraint> {
-public:
+struct MinItemsConstraint {
   int64_t value;
-
-public:
-  MinItemsConstraint(int64_t value) : value(value) {}
 };
 
-class TupleConstraint : public SimpleConstraint<TupleConstraint> {
-public:
+struct TupleConstraint {
   std::vector<schema::Node const *> items;
-
-public:
-  TupleConstraint(std::vector<schema::Node const *> const & items) : items(items) {}
 };
 
-class UnevaluatedItemsConstraint : public SimpleConstraint<UnevaluatedItemsConstraint> {
-public:
+struct UnevaluatedItemsConstraint {
   schema::Node const * subschema;
-
-public:
-  UnevaluatedItemsConstraint(schema::Node const * subschema) : subschema(subschema) {}
 };
 
-class UniqueItemsConstraint : public SimpleConstraint<UniqueItemsConstraint> {
-public:
-};
+struct UniqueItemsConstraint {};
 }

+ 0 - 30
include/jvalidate/constraint/constraint.h

@@ -1,30 +0,0 @@
-#pragma once
-
-#include <jvalidate/constraint/visitor.h>
-#include <jvalidate/detail/pointer.h>
-#include <jvalidate/enum.h>
-#include <jvalidate/forward.h>
-#include <jvalidate/status.h>
-
-namespace jvalidate::constraint {
-class Constraint {
-public:
-  virtual ~Constraint() = default;
-  virtual Status accept(ConstraintVisitor const & visitor) const = 0;
-};
-
-template <typename CRTP> class SimpleConstraint : public Constraint {
-public:
-  Status accept(ConstraintVisitor const & visitor) const final {
-    return visitor.visit(*static_cast<CRTP const *>(this));
-  }
-};
-
-class ExtensionConstraint : public Constraint {
-public:
-  Status accept(ConstraintVisitor const & visitor) const final { return visitor.visit(*this); }
-
-  virtual Status validate(adapter::Adapter const & json, detail::Pointer const & where,
-                          ValidationResult * result) const = 0;
-};
-}

+ 25 - 0
include/jvalidate/constraint/extension_constraint.h

@@ -0,0 +1,25 @@
+#pragma once
+
+#include <memory>
+
+#include <jvalidate/forward.h>
+#include <jvalidate/status.h>
+
+namespace jvalidate::constraint {
+class ExtensionConstraint {
+public:
+  struct Impl {
+    virtual ~Impl() = default;
+    virtual Status visit(extension::VisitorBase const &) const = 0;
+  };
+
+public:
+  template <typename T, typename... Args> static std::unique_ptr<Constraint> make(Args &&... args) {
+    return std::make_unique<Constraint>(
+        ExtensionConstraint{std::make_unique<T>(std::forward<Args>(args)...)});
+  }
+
+public:
+  std::unique_ptr<Impl> pimpl;
+};
+}

+ 7 - 61
include/jvalidate/constraint/general_constraint.h

@@ -2,93 +2,39 @@
 
 #include <memory>
 #include <set>
-#include <utility>
 #include <vector>
 
-#include <jvalidate/constraint/constraint.h>
 #include <jvalidate/forward.h>
 #include <jvalidate/status.h>
 
 namespace jvalidate::constraint {
-class AllOfConstraint : public SimpleConstraint<AllOfConstraint> {
-public:
+struct AllOfConstraint {
   std::vector<SubConstraint> children;
-
-public:
-  AllOfConstraint(std::vector<schema::Node const *> const & children)
-      : children(children.begin(), children.end()) {}
-
-  AllOfConstraint(std::vector<std::unique_ptr<Constraint>> && children) {
-    for (auto && child : children) {
-      this->children.push_back(std::move(child));
-    }
-  }
 };
 
-class AnyOfConstraint : public SimpleConstraint<AnyOfConstraint> {
-public:
+struct AnyOfConstraint {
   std::vector<SubConstraint> children;
-
-public:
-  AnyOfConstraint(std::vector<schema::Node const *> const & children)
-      : children(children.begin(), children.end()) {}
-
-  AnyOfConstraint(std::vector<std::unique_ptr<Constraint>> && children) {
-    for (auto && child : children) {
-      this->children.push_back(std::move(child));
-    }
-  }
 };
 
-class EnumConstraint : public SimpleConstraint<EnumConstraint> {
-public:
+struct EnumConstraint {
   std::vector<std::unique_ptr<adapter::Const const>> enumeration;
-
-public:
-  EnumConstraint(std::unique_ptr<adapter::Const const> && constant) {
-    enumeration.push_back(std::move(constant));
-  }
-
-  EnumConstraint(std::vector<std::unique_ptr<adapter::Const const>> && enums)
-      : enumeration(std::move(enums)) {}
 };
 
-class OneOfConstraint : public SimpleConstraint<OneOfConstraint> {
-public:
+struct OneOfConstraint {
   std::vector<schema::Node const *> children;
-
-public:
-  OneOfConstraint(std::vector<schema::Node const *> const & children) : children(children) {}
 };
 
-class ConditionalConstraint : public SimpleConstraint<ConditionalConstraint> {
-public:
+struct ConditionalConstraint {
   schema::Node const * if_constraint;
   schema::Node const * then_constraint;
   schema::Node const * else_constraint;
-
-public:
-  ConditionalConstraint(schema::Node const * if_constraint, schema::Node const * then_constraint,
-                        schema::Node const * else_constraint)
-      : if_constraint(if_constraint), then_constraint(then_constraint),
-        else_constraint(else_constraint) {}
 };
 
-class NotConstraint : public SimpleConstraint<NotConstraint> {
-public:
+struct NotConstraint {
   SubConstraint child;
-
-public:
-  NotConstraint(schema::Node const * child) : child(child) {}
-  NotConstraint(std::unique_ptr<Constraint> && child) : child(std::move(child)) {}
 };
 
-class TypeConstraint : public SimpleConstraint<TypeConstraint> {
-public:
+struct TypeConstraint {
   std::set<adapter::Type> types;
-
-public:
-  TypeConstraint(adapter::Type type) : types{type} {}
-  TypeConstraint(std::set<adapter::Type> const & types) : types(types) {}
 };
 }

+ 4 - 19
include/jvalidate/constraint/number_constraint.h

@@ -1,44 +1,29 @@
 #pragma once
 
 #include <cmath>
-#include <iostream>
+#include <limits>
 
-#include <jvalidate/adapter.h>
-#include <jvalidate/constraint/constraint.h>
 #include <jvalidate/detail/number.h>
 #include <jvalidate/forward.h>
-#include <limits>
 
 namespace jvalidate::constraint {
-class MaximumConstraint : public SimpleConstraint<MaximumConstraint> {
-public:
+struct MaximumConstraint {
   double value;
   bool exclusive;
 
-public:
-  MaximumConstraint(double value, bool exclusive) : value(value), exclusive(exclusive) {}
-
   bool operator()(double arg) const { return exclusive ? arg < value : arg <= value; }
 };
 
-class MinimumConstraint : public SimpleConstraint<MinimumConstraint> {
-public:
+struct MinimumConstraint {
   double value;
   bool exclusive;
 
-public:
-  MinimumConstraint(double value, bool exclusive) : value(value), exclusive(exclusive) {}
-
   bool operator()(double arg) const { return exclusive ? arg > value : arg >= value; }
 };
 
-class MultipleOfConstraint : public SimpleConstraint<MultipleOfConstraint> {
-public:
+struct MultipleOfConstraint {
   double value;
 
-public:
-  MultipleOfConstraint(double value) : value(value) {}
-
   bool operator()(double arg) const {
     if (std::fabs(std::remainder(arg, value)) <= std::numeric_limits<double>::epsilon()) {
       return true;

+ 9 - 61
include/jvalidate/constraint/object_constraint.h

@@ -1,102 +1,50 @@
 #pragma once
 
 #include <map>
-#include <optional>
 #include <string>
 #include <unordered_set>
 #include <utility>
 #include <vector>
 
-#include <jvalidate/constraint/constraint.h>
 #include <jvalidate/forward.h>
 
 namespace jvalidate::constraint {
-class AdditionalPropertiesConstraint : public SimpleConstraint<AdditionalPropertiesConstraint> {
-public:
+struct AdditionalPropertiesConstraint {
   schema::Node const * subschema;
   std::unordered_set<std::string> properties;
   std::vector<std::string> patterns;
-
-public:
-  AdditionalPropertiesConstraint(schema::Node const * subschema,
-                                 std::unordered_set<std::string> const & properties,
-                                 std::vector<std::string> const & patterns)
-      : subschema(subschema), properties(properties), patterns(patterns) {}
 };
 
-class DependenciesConstraint : public SimpleConstraint<DependenciesConstraint> {
-public:
+struct DependenciesConstraint {
   std::map<std::string, schema::Node const *> subschemas;
   std::map<std::string, std::unordered_set<std::string>> required;
-
-public:
-  DependenciesConstraint(std::map<std::string, schema::Node const *> const & subschemas)
-      : subschemas(subschemas) {}
-
-  DependenciesConstraint(std::map<std::string, std::unordered_set<std::string>> const & required)
-      : required(required) {}
-
-  DependenciesConstraint(std::map<std::string, schema::Node const *> const & subschemas,
-                         std::map<std::string, std::unordered_set<std::string>> const & required)
-      : subschemas(subschemas), required(required) {}
 };
 
-class MaxPropertiesConstraint : public SimpleConstraint<MaxPropertiesConstraint> {
-public:
+struct MaxPropertiesConstraint {
   int64_t value;
-
-public:
-  MaxPropertiesConstraint(int64_t value) : value(value) {}
 };
 
-class MinPropertiesConstraint : public SimpleConstraint<MinPropertiesConstraint> {
-public:
+struct MinPropertiesConstraint {
   int64_t value;
-
-public:
-  MinPropertiesConstraint(int64_t value) : value(value) {}
 };
 
-class PatternPropertiesConstraint : public SimpleConstraint<PatternPropertiesConstraint> {
-public:
+struct PatternPropertiesConstraint {
   std::vector<std::pair<std::string, schema::Node const *>> properties;
-
-public:
-  PatternPropertiesConstraint(
-      std::vector<std::pair<std::string, schema::Node const *>> const & properties)
-      : properties(properties) {}
 };
 
-class PropertiesConstraint : public SimpleConstraint<PropertiesConstraint> {
-public:
+struct PropertiesConstraint {
   std::map<std::string, schema::Node const *> properties;
-
-public:
-  PropertiesConstraint(std::map<std::string, schema::Node const *> const & properties)
-      : properties(properties) {}
 };
 
-class PropertyNamesConstraint : public SimpleConstraint<PropertyNamesConstraint> {
-public:
+struct PropertyNamesConstraint {
   schema::Node const * key_schema;
-
-public:
-  PropertyNamesConstraint(schema::Node const * key_schema) : key_schema(key_schema) {}
 };
 
-class RequiredConstraint : public SimpleConstraint<RequiredConstraint> {
-public:
+struct RequiredConstraint {
   std::unordered_set<std::string> properties;
-
-public:
-  RequiredConstraint(std::unordered_set<std::string> const & properties) : properties(properties) {}
 };
 
-class UnevaluatedPropertiesConstraint : public SimpleConstraint<UnevaluatedPropertiesConstraint> {
-public:
+struct UnevaluatedPropertiesConstraint {
   schema::Node const * subschema;
-
-public:
-  UnevaluatedPropertiesConstraint(schema::Node const * subschema) : subschema(subschema) {}
 };
 }

+ 4 - 22
include/jvalidate/constraint/string_constraint.h

@@ -2,42 +2,24 @@
 
 #include <string>
 
-#include <jvalidate/constraint/constraint.h>
 #include <jvalidate/detail/string.h>
 #include <jvalidate/forward.h>
 
 namespace jvalidate::constraint {
-class MinLengthConstraint : public SimpleConstraint<MinLengthConstraint> {
-public:
+struct MinLengthConstraint {
   int64_t value;
-
-public:
-  MinLengthConstraint(int64_t value) : value(value) {}
 };
 
-class MaxLengthConstraint : public SimpleConstraint<MaxLengthConstraint> {
-public:
+struct MaxLengthConstraint {
   int64_t value;
-
-public:
-  MaxLengthConstraint(int64_t value) : value(value) {}
 };
 
-class PatternConstraint : public SimpleConstraint<PatternConstraint> {
-public:
+struct PatternConstraint {
   std::string regex;
-
-public:
-  PatternConstraint(std::string const & regex) : regex(regex) {}
 };
 
-class FormatConstraint : public SimpleConstraint<FormatConstraint> {
-public:
+struct FormatConstraint {
   std::string format;
   bool is_assertion;
-
-public:
-  FormatConstraint(std::string const & format, bool is_assertion)
-      : format(format), is_assertion(is_assertion) {}
 };
 }

+ 0 - 23
include/jvalidate/constraint/visitor.h

@@ -1,23 +0,0 @@
-#pragma once
-
-#include <jvalidate/forward.h>
-
-#define VISITOR_PURE_VIRTUAL(TYPE) virtual Status visit(TYPE const &) const = 0;
-
-namespace jvalidate::constraint {
-/**
- * @brief The base interface for visitors to be implemented off of.
- * Provides a visit function for every provided concrete type of Constraint,
- * as well as the ExtensionConstraint interface.
- */
-struct ConstraintVisitor {
-  virtual ~ConstraintVisitor() = default;
-  CONSTRAINT_IMPLEMENTATION_LIST(VISITOR_PURE_VIRTUAL);
-};
-
-template <typename Cons> struct ExtensionConstraintVisitor {
-  virtual Status visit(Cons const &) const = 0;
-};
-}
-
-#undef VISITOR_PURE_VIRTUAL

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

@@ -2,7 +2,6 @@
 
 #include <algorithm>
 #include <cassert>
-#include <cstdint>
 #include <iostream>
 #include <string>
 #include <string_view>
@@ -179,7 +178,7 @@ public:
     return Pointer(std::vector(tokens_.begin() + other.tokens_.size(), tokens_.end()));
   }
 
-  Pointer parent() const { return Pointer({tokens_.begin(), tokens_.end() - 1}); }
+  Pointer parent(size_t i = 1) const { return Pointer({tokens_.begin(), tokens_.end() - i}); }
 
   Pointer & operator/=(Pointer const & relative) {
     tokens_.insert(tokens_.end(), relative.tokens_.begin(), relative.tokens_.end());

+ 55 - 0
include/jvalidate/detail/relative_pointer.h

@@ -0,0 +1,55 @@
+#pragma once
+
+#include <ostream>
+#include <string>
+#include <string_view>
+
+#include <jvalidate/detail/expect.h>
+#include <jvalidate/detail/pointer.h>
+#include <jvalidate/forward.h>
+
+namespace jvalidate::detail {
+class RelativePointer {
+public:
+  RelativePointer(std::string_view path) {
+    if (path == "0") {
+      return;
+    }
+    if (auto pos = path.find('/'); pos != path.npos) {
+      pointer_ = Pointer(path.substr(pos));
+      path.remove_suffix(path.size() - pos);
+    } else {
+      EXPECT_M(not path.empty() && path.back() == '#',
+               "RelativePointer must end in a pointer, or a '#'");
+      requests_key_ = true;
+      path.remove_suffix(1);
+    }
+    parent_steps_ = std::stoull(std::string(path));
+  }
+
+  template <Adapter A>
+  std::variant<std::string, A> inspect(Pointer const & where, A const & root) const {
+    if (requests_key_) {
+      return where.parent(parent_steps_).back();
+    }
+    auto rval = where.parent(parent_steps_).walk(root);
+    return pointer_ ? pointer_->walk(rval) : rval;
+  }
+
+  friend std::ostream & operator<<(std::ostream & os, RelativePointer const & rel) {
+    os << rel.parent_steps_;
+    if (rel.requests_key_) {
+      return os << '#';
+    }
+    if (rel.pointer_) {
+      os << *rel.pointer_;
+    }
+    return os;
+  }
+
+private:
+  size_t parent_steps_ = 0;
+  bool requests_key_ = false;
+  std::optional<Pointer> pointer_ = std::nullopt;
+};
+}

+ 77 - 0
include/jvalidate/detail/string_adapter.h

@@ -0,0 +1,77 @@
+#pragma once
+
+#include <map>
+#include <stdexcept>
+#include <string_view>
+#include <vector>
+
+#include <jvalidate/adapter.h>
+#include <jvalidate/detail/number.h>
+#include <jvalidate/detail/simple_adapter.h>
+#include <jvalidate/enum.h>
+#include <jvalidate/status.h>
+
+namespace jvalidate::detail {
+template <typename CRTP> class UnsupportedArrayAdapter {
+public:
+  size_t size() const { return 0; }
+  CRTP operator[](size_t) const { throw std::runtime_error("stub implementation"); }
+  std::vector<CRTP>::const_iterator begin() const { return {}; }
+  std::vector<CRTP>::const_iterator end() const { return {}; }
+};
+
+template <typename CRTP> class UnsupportedObjectAdapter {
+public:
+  size_t size() const { return 0; }
+  bool contains(std::string_view) const { return false; }
+  CRTP operator[](std::string_view) const { throw std::runtime_error("stub implementation"); }
+  std::map<std::string, CRTP>::const_iterator begin() const { return {}; }
+  std::map<std::string, CRTP>::const_iterator end() const { return {}; }
+};
+
+class StringAdapter final : public adapter::Adapter {
+public:
+  using value_type = std::string_view;
+
+  StringAdapter(std::string_view value) : value_(value) {}
+
+  adapter::Type type() const { return adapter::Type::String; }
+  bool as_boolean() const { die("boolean"); }
+  int64_t as_integer() const { die("integer"); }
+  double as_number() const { die("number"); }
+  std::string as_string() const { return std::string(value_); }
+
+  size_t array_size() const { die("array"); }
+  UnsupportedArrayAdapter<StringAdapter> as_array() const { die("array"); }
+  Status apply_array(adapter::AdapterCallback const &) const { return Status::Noop; }
+
+  size_t object_size() const { die("object"); }
+  UnsupportedObjectAdapter<StringAdapter> as_object() const { die("object"); }
+  Status apply_object(adapter::ObjectAdapterCallback const &) const { return Status::Noop; }
+
+  bool equals(adapter::Adapter const & rhs, bool strict) const {
+    if (std::optional str = rhs.maybe_string(strict)) {
+      return str == value_;
+    }
+    return false;
+  }
+
+  std::unique_ptr<adapter::Const const> freeze() const final {
+    return std::make_unique<adapter::detail::GenericConst<std::string_view>>(value_);
+  }
+
+private:
+  [[noreturn]] static void die(std::string expected) {
+    throw std::runtime_error("StringAdapter is not an " + expected);
+  }
+
+private:
+  std::string_view value_;
+};
+
+}
+
+template <> struct jvalidate::adapter::AdapterTraits<std::string_view> {
+  template <typename> using Adapter = jvalidate::detail::StringAdapter;
+  using ConstAdapter = jvalidate::detail::StringAdapter;
+};

+ 63 - 0
include/jvalidate/extension.h

@@ -0,0 +1,63 @@
+#pragma once
+
+#include <type_traits>
+
+#include <jvalidate/constraint/extension_constraint.h>
+#include <jvalidate/forward.h>
+#include <jvalidate/status.h>
+
+namespace jvalidate::extension {
+struct VisitorBase {
+  virtual ~VisitorBase() = default;
+};
+
+template <typename E>
+concept Constraint = std::is_base_of_v<constraint::ExtensionConstraint::Impl, E>;
+
+namespace detail {
+template <Constraint E> struct TypedVisitor : VisitorBase {
+  virtual Status visit(E const & cons) const = 0;
+};
+
+template <Constraint E, typename CRTP> struct TypedVisitorImpl : TypedVisitor<E> {
+  Status visit(E const & cons) const final {
+    return static_cast<CRTP const *>(this)->dispatch(cons);
+  }
+};
+}
+
+template <typename CRTP> struct ConstraintBase : constraint::ExtensionConstraint::Impl {
+  Status visit(VisitorBase const & visitor) const final {
+    return dynamic_cast<detail::TypedVisitor<CRTP> const &>(visitor).visit(
+        static_cast<CRTP const &>(*this));
+  }
+};
+
+template <typename CRTP, typename... Es> class Visitor {
+private:
+  template <Adapter A, typename V> class Impl : public detail::TypedVisitorImpl<Es, Impl<A, V>>... {
+  public:
+    Impl(Visitor const * self, A const & document, V const & visitor)
+        : self_(self), document_(document), visitor_(visitor) {}
+
+    using detail::TypedVisitorImpl<Es, Impl>::visit...;
+
+    template <Constraint E> Status dispatch(E const & cons) const {
+      // static_assert(Visitable<CRTP, E, A, V>, "Must implement all visitation functions");
+      return static_cast<CRTP const *>(self_)->visit(cons, document_, visitor_);
+    }
+
+  private:
+    Visitor const * self_;
+    A const & document_;
+    V const & visitor_;
+  };
+
+public:
+  template <Adapter A, typename V>
+  Status operator()(constraint::ExtensionConstraint const & cons, A const & document,
+                    V const & visitor) const {
+    return cons.pimpl->visit(Impl<A, V>{this, document, visitor});
+  }
+};
+}

+ 32 - 15
include/jvalidate/forward.h

@@ -5,6 +5,13 @@
 #include <type_traits>
 #include <variant>
 
+#define DISCARD1_IMPL(_, ...) __VA_ARGS__
+#define DISCARD1(...) DISCARD1_IMPL(__VA_ARGS__)
+
+#define FORWARD_DECLARE_STRUCT(TYPE) struct TYPE;
+
+#define COMMA_NAME(X) , X
+
 namespace jvalidate {
 class Schema;
 class Status;
@@ -33,11 +40,6 @@ class Node;
 }
 
 namespace jvalidate::constraint {
-class ConstraintVisitor;
-class Constraint;
-using SubConstraint = std::variant<schema::Node const *, std::unique_ptr<Constraint>>;
-template <typename> class SimpleConstraint;
-
 #define CONSTRAINT_IMPLEMENTATION_LIST(X)                                                          \
   /* General Constraints - See jvalidate/constraint/general_constraint.h */                        \
   X(TypeConstraint)                                                                                \
@@ -77,10 +79,10 @@ template <typename> class SimpleConstraint;
   /* ExtensionConstraint - A special constraint for all user-defined constraints */                \
   X(ExtensionConstraint)
 
-#define FORWARD_DECLARE_CLASS(TYPE) class TYPE;
-CONSTRAINT_IMPLEMENTATION_LIST(FORWARD_DECLARE_CLASS);
-#undef FORWARD_DECLARE_CLASS
+CONSTRAINT_IMPLEMENTATION_LIST(FORWARD_DECLARE_STRUCT);
 
+using Constraint = std::variant<DISCARD1(CONSTRAINT_IMPLEMENTATION_LIST(COMMA_NAME))>;
+using SubConstraint = std::variant<schema::Node const *, std::unique_ptr<Constraint>>;
 }
 
 namespace jvalidate {
@@ -161,21 +163,36 @@ concept MutableAdapter = Adapter<A> && requires(A const a) {
 };
 
 template <typename R>
-concept RegexEngine = std::constructible_from<std::string> && requires(R const regex) {
-  { regex.search("") } -> std::same_as<bool>;
+concept RegexEngine = requires(R & regex) {
+  { regex.search("" /* pattern */, "" /* text */) } -> std::same_as<bool>;
+};
+
+template <typename E, typename A, typename B, typename V>
+concept Visitable = Adapter<A> && requires(V & v, E const & c, A const & doc, B const & base) {
+  { v.visit(c, doc, base) } -> std::same_as<Status>;
 };
+
+template <typename T, typename S>
+concept Not = not std::is_same_v<std::decay_t<T>, std::decay_t<S>>;
 }
 
 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 <RegexEngine RE, typename ExtensionVisitor> class ValidationVisitor;
+
+template <RegexEngine RE, typename ExtensionVisitor> 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>;
+namespace jvalidate::extension {
+struct VisitorBase;
+template <typename CRTP, typename... Es> class Visitor;
 }
+
+#undef FORWARD_DECLARE_STRUCT
+#undef COMMA_NAME
+#undef DISCARD1_IMPL
+#undef DISCARD1

+ 1 - 0
include/jvalidate/schema.h

@@ -210,6 +210,7 @@ public:
    * into an Adapter object to allow us to walk through it w/o specialization.
    */
   template <typename JSON, typename... Args>
+    requires(not Adapter<JSON>)
   explicit Schema(JSON const & json, Args &&... args)
       : Schema(adapter::AdapterFor<JSON const>(json), std::forward<Args>(args)...) {}
 

+ 3 - 1
include/jvalidate/validation_result.h

@@ -13,7 +13,7 @@ namespace jvalidate {
 class ValidationResult {
 public:
   // Only allow ValidationVisitor to construct the elements of a validation result
-  template <Adapter A, RegexEngine RE> friend class ValidationVisitor;
+  template <RegexEngine, typename> friend class ValidationVisitor;
 
   using DocPointer = detail::Pointer;
   using SchemaPointer = detail::Pointer;
@@ -134,6 +134,8 @@ public:
     os << indent(i) << '}';
   }
 
+  bool valid() const { return valid_; }
+
   /**
    * @brief Are there any validation details associated with the given document
    * location and schema section.

+ 125 - 109
include/jvalidate/validation_visitor.h

@@ -11,12 +11,12 @@
 #include <jvalidate/constraint/number_constraint.h>
 #include <jvalidate/constraint/object_constraint.h>
 #include <jvalidate/constraint/string_constraint.h>
-#include <jvalidate/constraint/visitor.h>
 #include <jvalidate/detail/expect.h>
 #include <jvalidate/detail/iostream.h>
 #include <jvalidate/detail/number.h>
 #include <jvalidate/detail/pointer.h>
 #include <jvalidate/detail/scoped_state.h>
+#include <jvalidate/detail/string_adapter.h>
 #include <jvalidate/forward.h>
 #include <jvalidate/schema.h>
 #include <jvalidate/status.h>
@@ -34,8 +34,7 @@
     }                                                                                              \
   } while (false)
 
-#define NOOP_UNLESS_TYPE(etype)                                                                    \
-  RETURN_UNLESS(adapter::Type::etype == document_.type(), Status::Noop)
+#define NOOP_UNLESS_TYPE(etype) RETURN_UNLESS(adapter::Type::etype == document.type(), Status::Noop)
 
 #define BREAK_EARLY_IF_NO_RESULT_TREE()                                                            \
   do {                                                                                             \
@@ -45,14 +44,13 @@
   } while (false)
 
 namespace jvalidate {
-template <Adapter A, RegexEngine RE>
-class ValidationVisitor : public constraint::ConstraintVisitor {
+template <RegexEngine RE, typename ExtensionVisitor> class ValidationVisitor {
 private:
-  using VisitedAnnotation = std::tuple<std::unordered_set<size_t>, std::unordered_set<std::string>>;
   JVALIDATE_TRIBOOL_TYPE(StoreResults, ForValid, ForInvalid, ForAnything);
+  using VisitedAnnotation = std::tuple<std::unordered_set<size_t>, std::unordered_set<std::string>>;
+  friend ExtensionVisitor;
 
 private:
-  A document_;
   detail::Pointer where_;
   detail::Pointer schema_path_;
 
@@ -61,7 +59,8 @@ private:
   ValidationResult * result_;
 
   ValidationConfig const & cfg_;
-  std::unordered_map<std::string, RE> & regex_cache_;
+  ExtensionVisitor extension_;
+  RE & regex_;
 
   mutable VisitedAnnotation * visited_ = nullptr;
   mutable StoreResults tracking_ = StoreResults::ForInvalid;
@@ -70,20 +69,29 @@ public:
   /**
    * @brief Construct a new ValidationVisitor
    *
-   * @param json The JSON document instance being validated
    * @param schema The parsed JSON Schema
    * @param cfg General configuration settings for how the run is executed
-   * @param regex_cache A cache of string regular expressions to compiled
+   * @param regex A cache of string regular expressions to compiled
    * regular expressions
+   * @param[optional] extension A special visitor for extension constraints.
    * @param[optional] result A cache of result/annotation info for the user to
    * receive a detailed summary of why a document is supported/unsupported.
    */
-  ValidationVisitor(A const & json, schema::Node const & schema, ValidationConfig const & cfg,
-                    std::unordered_map<std::string, RE> & regex_cache, ValidationResult * result)
-      : document_(json), schema_(&schema), result_(result), cfg_(cfg), regex_cache_(regex_cache) {}
+  ValidationVisitor(schema::Node const & schema, ValidationConfig const & cfg, RE & regex,
+                    ExtensionVisitor extension, ValidationResult * result)
+      : schema_(&schema), result_(result), cfg_(cfg), extension_(extension), regex_(regex) {}
 
-  Status visit(constraint::TypeConstraint const & cons) const {
-    adapter::Type const type = document_.type();
+  Status visit(constraint::ExtensionConstraint const & cons, Adapter auto const & document) const {
+    if constexpr (std::is_invocable_r_v<Status, ExtensionVisitor, decltype(cons),
+                                        decltype(document), ValidationVisitor const &>) {
+      return extension_(cons, document, *this);
+    }
+    annotate("unsupported extension");
+    return Status::Noop;
+  }
+
+  Status visit(constraint::TypeConstraint const & cons, Adapter auto const & document) const {
+    adapter::Type const type = document.type();
 
     for (adapter::Type const accept : cons.types) {
       if (type == accept) {
@@ -93,20 +101,16 @@ public:
         return result(Status::Accept, type, " is in types [", cons.types, "]");
       }
       if (accept == adapter::Type::Integer && type == adapter::Type::Number &&
-          detail::is_json_integer(document_.as_number())) {
+          detail::is_json_integer(document.as_number())) {
         return result(Status::Accept, type, " is in types [", cons.types, "]");
       }
     }
     return result(Status::Reject, type, " is not in types [", cons.types, "]");
   }
 
-  Status visit(constraint::ExtensionConstraint const & cons) const {
-    return cons.validate(document_, where_, result_);
-  }
-
-  Status visit(constraint::EnumConstraint const & cons) const {
-    auto is_equal = [this](auto const & frozen) {
-      return document_.equals(frozen, cfg_.strict_equality);
+  Status visit(constraint::EnumConstraint const & cons, Adapter auto const & document) const {
+    auto is_equal = [this, &document](auto const & frozen) {
+      return document.equals(frozen, cfg_.strict_equality);
     };
     for (auto const & [index, option] : detail::enumerate(cons.enumeration)) {
       if (option->apply(is_equal)) {
@@ -116,12 +120,12 @@ public:
     return Status::Reject;
   }
 
-  Status visit(constraint::AllOfConstraint const & cons) const {
+  Status visit(constraint::AllOfConstraint const & cons, Adapter auto const & document) const {
     Status rval = Status::Accept;
 
     std::set<size_t> unmatched;
     for (auto const & [index, subschema] : detail::enumerate(cons.children)) {
-      if (auto stat = validate_subschema(subschema, index); stat == Status::Reject) {
+      if (auto stat = validate_subschema(subschema, document, index); stat == Status::Reject) {
         rval = Status::Reject;
         unmatched.insert(index);
       }
@@ -134,10 +138,10 @@ public:
     return result(rval, "validates all subschemas");
   }
 
-  Status visit(constraint::AnyOfConstraint const & cons) const {
+  Status visit(constraint::AnyOfConstraint const & cons, Adapter auto const & document) const {
     std::optional<size_t> first_validated;
     for (auto const & [index, subschema] : detail::enumerate(cons.children)) {
-      if (validate_subschema(subschema, index)) {
+      if (validate_subschema(subschema, document, index)) {
         first_validated = index;
       }
       if (not visited_ && first_validated.has_value()) {
@@ -151,12 +155,12 @@ public:
     return result(Status::Reject, "validates none of the subschemas");
   }
 
-  Status visit(constraint::OneOfConstraint const & cons) const {
+  Status visit(constraint::OneOfConstraint const & cons, Adapter auto const & document) const {
     std::set<size_t> matches;
 
     for (auto const & [index, subschema] : detail::enumerate(cons.children)) {
       scoped_state(tracking_, StoreResults::ForAnything);
-      if (validate_subschema(subschema, index)) {
+      if (validate_subschema(subschema, document, index)) {
         matches.insert(index);
       }
     }
@@ -167,37 +171,38 @@ public:
     return result(Status::Reject, "validates multiple subschemas ", matches);
   }
 
-  Status visit(constraint::NotConstraint const & cons) const {
+  Status visit(constraint::NotConstraint const & cons, Adapter auto const & document) const {
     scoped_state(visited_, nullptr);
     scoped_state(tracking_, !tracking_);
-    bool const rejected = validate_subschema(cons.child) == Status::Reject;
+    bool const rejected = validate_subschema(cons.child, document) == Status::Reject;
 
     return rejected;
   }
 
-  Status visit(constraint::ConditionalConstraint const & cons) const {
-    Status const if_true = [this, &cons]() {
+  Status visit(constraint::ConditionalConstraint const & cons,
+               Adapter auto const & document) const {
+    Status const if_true = [this, &cons, &document]() {
       scoped_state(tracking_, StoreResults::ForAnything);
-      return validate_subschema(cons.if_constraint);
+      return validate_subschema(cons.if_constraint, document);
     }();
 
     annotate(if_true ? "valid" : "invalid");
     if (if_true) {
-      return validate_subschema(cons.then_constraint, detail::parent, "then");
+      return validate_subschema(cons.then_constraint, document, detail::parent, "then");
     }
-    return validate_subschema(cons.else_constraint, detail::parent, "else");
+    return validate_subschema(cons.else_constraint, document, detail::parent, "else");
   }
 
-  Status visit(constraint::MaximumConstraint const & cons) const {
-    switch (document_.type()) {
+  Status visit(constraint::MaximumConstraint const & cons, Adapter auto const & document) const {
+    switch (document.type()) {
     case adapter::Type::Integer:
-      if (int64_t value = document_.as_integer(); not cons(value)) {
+      if (int64_t value = document.as_integer(); not cons(value)) {
         return result(Status::Reject, value, cons.exclusive ? " >= " : " > ", cons.value);
       } else {
         return result(Status::Accept, value, cons.exclusive ? " < " : " <= ", cons.value);
       }
     case adapter::Type::Number:
-      if (double value = document_.as_number(); not cons(value)) {
+      if (double value = document.as_number(); not cons(value)) {
         return result(Status::Reject, value, cons.exclusive ? " >= " : " > ", cons.value);
       } else {
         return result(Status::Accept, value, cons.exclusive ? " < " : " <= ", cons.value);
@@ -207,16 +212,16 @@ public:
     }
   }
 
-  Status visit(constraint::MinimumConstraint const & cons) const {
-    switch (document_.type()) {
+  Status visit(constraint::MinimumConstraint const & cons, Adapter auto const & document) const {
+    switch (document.type()) {
     case adapter::Type::Integer:
-      if (int64_t value = document_.as_integer(); not cons(value)) {
+      if (int64_t value = document.as_integer(); not cons(value)) {
         return result(Status::Reject, value, cons.exclusive ? " <= " : " < ", cons.value);
       } else {
         return result(Status::Accept, value, cons.exclusive ? " > " : " >= ", cons.value);
       }
     case adapter::Type::Number:
-      if (double value = document_.as_number(); not cons(value)) {
+      if (double value = document.as_number(); not cons(value)) {
         return result(Status::Reject, value, cons.exclusive ? " <= " : " < ", cons.value);
       } else {
         return result(Status::Accept, value, cons.exclusive ? " > " : " >= ", cons.value);
@@ -226,20 +231,20 @@ public:
     }
   }
 
-  Status visit(constraint::MultipleOfConstraint const & cons) const {
-    adapter::Type const type = document_.type();
+  Status visit(constraint::MultipleOfConstraint const & cons, Adapter auto const & document) const {
+    adapter::Type const type = document.type();
     RETURN_UNLESS(type == adapter::Type::Number || type == adapter::Type::Integer, Status::Noop);
 
-    if (double value = document_.as_number(); not cons(value)) {
+    if (double value = document.as_number(); not cons(value)) {
       return result(Status::Reject, value, " is not a multiple of ", cons.value);
     } else {
       return result(Status::Accept, value, " is a multiple of ", cons.value);
     }
   }
 
-  Status visit(constraint::MaxLengthConstraint const & cons) const {
+  Status visit(constraint::MaxLengthConstraint const & cons, Adapter auto const & document) const {
     NOOP_UNLESS_TYPE(String);
-    std::string const str = document_.as_string();
+    std::string const str = document.as_string();
     if (int64_t len = detail::length(str); len > cons.value) {
       return result(Status::Reject, "string of length ", len, " is >", cons.value);
     } else {
@@ -247,9 +252,9 @@ public:
     }
   }
 
-  Status visit(constraint::MinLengthConstraint const & cons) const {
+  Status visit(constraint::MinLengthConstraint const & cons, Adapter auto const & document) const {
     NOOP_UNLESS_TYPE(String);
-    std::string const str = document_.as_string();
+    std::string const str = document.as_string();
     if (int64_t len = detail::length(str); len < cons.value) {
       return result(Status::Reject, "string of length ", len, " is <", cons.value);
     } else {
@@ -257,18 +262,17 @@ public:
     }
   }
 
-  Status visit(constraint::PatternConstraint const & cons) const {
+  Status visit(constraint::PatternConstraint const & cons, Adapter auto const & document) const {
     NOOP_UNLESS_TYPE(String);
 
-    RE const & regex = regex_cache_.try_emplace(cons.regex, cons.regex).first->second;
-    std::string const str = document_.as_string();
-    if (regex.search(str)) {
+    std::string const str = document.as_string();
+    if (regex_.search(cons.regex, str)) {
       return result(Status::Accept, "string matches pattern /", cons.regex, "/");
     }
     return result(Status::Reject, "string does not match pattern /", cons.regex, "/");
   }
 
-  Status visit(constraint::FormatConstraint const & cons) const {
+  Status visit(constraint::FormatConstraint const & cons, Adapter auto const & document) const {
     // https://json-schema.org/draft/2020-12/draft-bhutton-json-schema-validation-01#name-defined-formats
     NOOP_UNLESS_TYPE(String);
 
@@ -280,10 +284,11 @@ public:
     return result(Status::Reject, " is unimplemented");
   }
 
-  Status visit(constraint::AdditionalItemsConstraint const & cons) const {
+  Status visit(constraint::AdditionalItemsConstraint const & cons,
+               Adapter auto const & document) const {
     NOOP_UNLESS_TYPE(Array);
 
-    auto array = document_.as_array();
+    auto array = document.as_array();
 
     Status rval = Status::Accept;
     std::vector<size_t> items;
@@ -296,10 +301,10 @@ public:
     return rval;
   }
 
-  Status visit(constraint::ContainsConstraint const & cons) const {
+  Status visit(constraint::ContainsConstraint const & cons, Adapter auto const & document) const {
     NOOP_UNLESS_TYPE(Array);
 
-    auto array = document_.as_array();
+    auto array = document.as_array();
     size_t const minimum = cons.minimum.value_or(1);
     size_t const maximum = cons.maximum.value_or(array.size());
     size_t matches = 0;
@@ -318,31 +323,31 @@ public:
     return result(Status::Accept, "array contains ", matches, " matching items");
   }
 
-  Status visit(constraint::MaxItemsConstraint const & cons) const {
+  Status visit(constraint::MaxItemsConstraint const & cons, Adapter auto const & document) const {
     NOOP_UNLESS_TYPE(Array);
-    if (size_t size = document_.array_size(); size > cons.value) {
+    if (size_t size = document.array_size(); size > cons.value) {
       return result(Status::Reject, "array of size ", size, " is >", cons.value);
     } else {
       return result(Status::Accept, "array of size ", size, " is <=", cons.value);
     }
   }
 
-  Status visit(constraint::MinItemsConstraint const & cons) const {
+  Status visit(constraint::MinItemsConstraint const & cons, Adapter auto const & document) const {
     NOOP_UNLESS_TYPE(Array);
-    if (size_t size = document_.array_size(); size < cons.value) {
+    if (size_t size = document.array_size(); size < cons.value) {
       return result(Status::Reject, "array of size ", size, " is <", cons.value);
     } else {
       return result(Status::Accept, "array of size ", size, " is >=", cons.value);
     }
   }
 
-  Status visit(constraint::TupleConstraint const & cons) const {
+  Status visit(constraint::TupleConstraint const & cons, Adapter auto const & document) const {
     NOOP_UNLESS_TYPE(Array);
 
     Status rval = Status::Accept;
 
     std::vector<size_t> items;
-    for (auto const & [index, item] : detail::enumerate(document_.as_array())) {
+    for (auto const & [index, item] : detail::enumerate(document.as_array())) {
       if (index >= cons.items.size()) {
         break;
       }
@@ -354,18 +359,19 @@ public:
     return rval;
   }
 
-  Status visit(constraint::UniqueItemsConstraint const & cons) const {
+  template <Adapter A>
+  Status visit(constraint::UniqueItemsConstraint const & cons, A const & document) const {
     NOOP_UNLESS_TYPE(Array);
 
     if constexpr (std::totally_ordered<A>) {
       std::map<A, size_t> cache;
-      for (auto const & [index, elem] : detail::enumerate(document_.as_array())) {
+      for (auto const & [index, elem] : detail::enumerate(document.as_array())) {
         if (auto [it, created] = cache.emplace(elem, index); not created) {
           return result(Status::Reject, "items ", it->second, " and ", index, " are equal");
         }
       }
     } else {
-      auto array = document_.as_array();
+      auto array = document.as_array();
       for (size_t i = 0; i < array.size(); ++i) {
         for (size_t j = i + 1; j < array.size(); ++j) {
           if (array[i].equals(array[j], true)) {
@@ -378,13 +384,13 @@ public:
     return result(Status::Accept, "all array items are unique");
   }
 
-  Status visit(constraint::AdditionalPropertiesConstraint const & cons) const {
+  Status visit(constraint::AdditionalPropertiesConstraint const & cons,
+               Adapter auto const & document) const {
     NOOP_UNLESS_TYPE(Object);
 
     auto matches_any_pattern = [this, &cons](std::string const & key) {
       for (auto & pattern : cons.patterns) {
-        RE const & regex = regex_cache_.try_emplace(pattern, pattern).first->second;
-        if (regex.search(key)) {
+        if (regex_.search(pattern, key)) {
           return true;
         }
       }
@@ -393,7 +399,7 @@ public:
 
     Status rval = Status::Accept;
     std::vector<std::string> properties;
-    for (auto const & [key, elem] : document_.as_object()) {
+    for (auto const & [key, elem] : document.as_object()) {
       if (not cons.properties.contains(key) && not matches_any_pattern(key)) {
         VALIDATE_SUBSCHEMA_AND_MARK_LOCAL_VISIT(cons.subschema, elem, key, properties);
       }
@@ -404,17 +410,18 @@ public:
     return rval;
   }
 
-  Status visit(constraint::DependenciesConstraint const & cons) const {
+  Status visit(constraint::DependenciesConstraint const & cons,
+               Adapter auto const & document) const {
     NOOP_UNLESS_TYPE(Object);
 
-    auto object = document_.as_object();
+    auto object = document.as_object();
     Status rval = Status::Accept;
     for (auto const & [key, subschema] : cons.subschemas) {
       if (not object.contains(key)) {
         continue;
       }
 
-      rval &= validate_subschema(subschema, key);
+      rval &= validate_subschema(subschema, document, key);
       BREAK_EARLY_IF_NO_RESULT_TREE();
     }
 
@@ -434,33 +441,35 @@ public:
     return rval;
   }
 
-  Status visit(constraint::MaxPropertiesConstraint const & cons) const {
+  Status visit(constraint::MaxPropertiesConstraint const & cons,
+               Adapter auto const & document) const {
     NOOP_UNLESS_TYPE(Object);
-    if (size_t size = document_.object_size(); size > cons.value) {
+    if (size_t size = document.object_size(); size > cons.value) {
       return result(Status::Reject, "object of size ", size, " is >", cons.value);
     } else {
       return result(Status::Accept, "object of size ", size, " is <=", cons.value);
     }
   }
 
-  Status visit(constraint::MinPropertiesConstraint const & cons) const {
+  Status visit(constraint::MinPropertiesConstraint const & cons,
+               Adapter auto const & document) const {
     NOOP_UNLESS_TYPE(Object);
-    if (size_t size = document_.object_size(); size < cons.value) {
+    if (size_t size = document.object_size(); size < cons.value) {
       return result(Status::Reject, "object of size ", size, " is <", cons.value);
     } else {
       return result(Status::Accept, "object of size ", size, " is >=", cons.value);
     }
   }
 
-  Status visit(constraint::PatternPropertiesConstraint const & cons) const {
+  Status visit(constraint::PatternPropertiesConstraint const & cons,
+               Adapter auto const & document) const {
     NOOP_UNLESS_TYPE(Object);
 
     std::vector<std::string> properties;
     Status rval = Status::Accept;
     for (auto const & [pattern, subschema] : cons.properties) {
-      RE const & regex = regex_cache_.try_emplace(pattern, pattern).first->second;
-      for (auto const & [key, elem] : document_.as_object()) {
-        if (not regex.search(key)) {
+      for (auto const & [key, elem] : document.as_object()) {
+        if (not regex_.search(pattern, key)) {
           continue;
         }
         VALIDATE_SUBSCHEMA_AND_MARK_LOCAL_VISIT(subschema, elem, key, properties);
@@ -472,11 +481,12 @@ public:
     return rval;
   }
 
-  Status visit(constraint::PropertiesConstraint const & cons) const {
+  template <Adapter A>
+  Status visit(constraint::PropertiesConstraint const & cons, A const & document) const {
     NOOP_UNLESS_TYPE(Object);
 
     Status rval = Status::Accept;
-    auto object = document_.as_object();
+    auto object = document.as_object();
 
     if constexpr (MutableAdapter<A>) {
       for (auto const & [key, subschema] : cons.properties) {
@@ -499,23 +509,24 @@ public:
     return rval;
   }
 
-  Status visit(constraint::PropertyNamesConstraint const & cons) const {
+  template <Adapter A>
+  Status visit(constraint::PropertyNamesConstraint const & cons, A const & document) const {
     NOOP_UNLESS_TYPE(Object);
 
     Status rval = Status::Accept;
-    for (auto const & [key, _] : document_.as_object()) {
+    for (auto const & [key, _] : document.as_object()) {
       // TODO(samjaffe): Should we prefer a std::string adapter like valijson?
-      typename A::value_type key_json{key};
-      rval &= validate_subschema_on(cons.key_schema, A(key_json), std::string("$$key"));
+      rval &=
+          validate_subschema_on(cons.key_schema, detail::StringAdapter(key), std::string("$$key"));
     }
     return rval;
   }
 
-  Status visit(constraint::RequiredConstraint const & cons) const {
+  Status visit(constraint::RequiredConstraint const & cons, Adapter auto const & document) const {
     NOOP_UNLESS_TYPE(Object);
 
     auto required = cons.properties;
-    for (auto const & [key, _] : document_.as_object()) {
+    for (auto const & [key, _] : document.as_object()) {
       required.erase(key);
     }
 
@@ -526,7 +537,8 @@ public:
     return result(Status::Reject, "missing required properties ", required);
   }
 
-  Status visit(constraint::UnevaluatedItemsConstraint const & cons) const {
+  Status visit(constraint::UnevaluatedItemsConstraint const & cons,
+               Adapter auto const & document) const {
     NOOP_UNLESS_TYPE(Array);
     if (not visited_) {
       return Status::Reject;
@@ -534,7 +546,7 @@ public:
 
     Status rval = Status::Accept;
     std::vector<size_t> items;
-    for (auto const & [index, item] : detail::enumerate(document_.as_array())) {
+    for (auto const & [index, item] : detail::enumerate(document.as_array())) {
       if (not VISITED(size_t).contains(index)) {
         VALIDATE_SUBSCHEMA_AND_MARK_LOCAL_VISIT(cons.subschema, item, index, items);
       }
@@ -545,7 +557,8 @@ public:
     return rval;
   }
 
-  Status visit(constraint::UnevaluatedPropertiesConstraint const & cons) const {
+  Status visit(constraint::UnevaluatedPropertiesConstraint const & cons,
+               Adapter auto const & document) const {
     NOOP_UNLESS_TYPE(Object);
     if (not visited_) {
       return Status::Reject;
@@ -553,7 +566,7 @@ public:
 
     Status rval = Status::Accept;
     std::vector<std::string> properties;
-    for (auto const & [key, elem] : document_.as_object()) {
+    for (auto const & [key, elem] : document.as_object()) {
       if (not VISITED(std::string).contains(key)) {
         VALIDATE_SUBSCHEMA_AND_MARK_LOCAL_VISIT(cons.subschema, elem, key, properties);
       }
@@ -568,7 +581,7 @@ public:
    * @brief The main entry point into the validator. Validates the provided
    * document according to the schema.
    */
-  Status validate() {
+  Status validate(Adapter auto const & document) {
     // Step 1) Check if this is an always-false schema. Sometimes, this will
     // have a custom message.
     if (std::optional<std::string> const & reject = schema_->rejects_all()) {
@@ -602,14 +615,14 @@ public:
     // constraints. This is enforced in the parsing of the schema, rather than
     // during validation {@see jvalidate::schema::Node::construct}.
     if (std::optional<schema::Node const *> ref = schema_->reference_schema()) {
-      rval = validate_subschema(*ref, "$ref");
+      rval = validate_subschema(*ref, document, "$ref");
     }
 
     detail::Pointer const current_schema = schema_path_;
     for (auto const & [key, p_constraint] : schema_->constraints()) {
       BREAK_EARLY_IF_NO_RESULT_TREE();
       schema_path_ = current_schema / key;
-      rval &= p_constraint->accept(*this);
+      rval &= std::visit([this, &document](auto & c) { return visit(c, document); }, *p_constraint);
     }
 
     // Post Constraints represent the unevaluatedItems and unevaluatedProperties
@@ -617,7 +630,7 @@ public:
     for (auto const & [key, p_constraint] : schema_->post_constraints()) {
       BREAK_EARLY_IF_NO_RESULT_TREE();
       schema_path_ = current_schema / key;
-      rval &= p_constraint->accept(*this);
+      rval &= std::visit([this, &document](auto & c) { return visit(c, document); }, *p_constraint);
     }
 
     (result_ ? result_->valid(where_, current_schema, static_cast<bool>(rval)) : void());
@@ -691,11 +704,13 @@ private:
    * subschema.
    */
   template <typename... K>
-  Status validate_subschema(constraint::SubConstraint const & subschema, K const &... keys) const {
+  Status validate_subschema(constraint::SubConstraint const & subschema,
+                            Adapter auto const & document, K const &... keys) const {
     if (schema::Node const * const * ppschema = std::get_if<0>(&subschema)) {
-      return validate_subschema(*ppschema, keys...);
+      return validate_subschema(*ppschema, document, keys...);
     } else {
-      return std::get<1>(subschema)->accept(*this);
+      return std::visit([this, &document](auto & c) { return visit(c, document); },
+                        *std::get<1>(subschema));
     }
   }
 
@@ -711,7 +726,8 @@ private:
    * subschema.
    */
   template <typename... K>
-  Status validate_subschema(schema::Node const * subschema, K const &... keys) const {
+  Status validate_subschema(schema::Node const * subschema, Adapter auto const & document,
+                            K const &... keys) const {
     VisitedAnnotation annotate;
 
     ValidationVisitor next = *this;
@@ -719,7 +735,7 @@ private:
     std::tie(next.schema_, next.visited_) =
         std::forward_as_tuple(subschema, visited_ ? &annotate : nullptr);
 
-    Status rval = next.validate();
+    Status rval = next.validate(document);
 
     if (rval == Status::Accept and visited_) {
       std::get<0>(*visited_).merge(std::get<0>(annotate));
@@ -740,16 +756,16 @@ private:
    * subschema.
    */
   template <typename K>
-  Status validate_subschema_on(schema::Node const * subschema, A const & document,
+  Status validate_subschema_on(schema::Node const * subschema, Adapter auto const & document,
                                K const & key) const {
     ValidationResult result;
 
     ValidationVisitor next = *this;
     next.where_ /= key;
-    std::tie(next.document_, next.schema_, next.result_, next.visited_) =
-        std::forward_as_tuple(document, subschema, result_ ? &result : nullptr, nullptr);
+    std::tie(next.schema_, next.result_, next.visited_) =
+        std::forward_as_tuple(subschema, result_ ? &result : nullptr, nullptr);
 
-    auto status = next.validate();
+    auto status = next.validate(document);
     if (status == Status::Accept and visited_) {
       VISITED(K).insert(key);
     }

+ 26 - 17
include/jvalidate/validator.h

@@ -23,13 +23,21 @@ namespace jvalidate::detail {
  * RegexEngine compatible wrapper for a different library, such as re2.
  */
 class StdRegexEngine {
-public:
-  std::regex regex_;
+private:
+  std::unordered_map<std::string, std::regex> cache_;
 
 public:
-  StdRegexEngine(std::string const & regex) : regex_(detail::regex_escape(regex)) {}
-  bool search(std::string const & text) const { return std::regex_search(text, regex_); }
+  bool search(std::string const & regex, std::string const & text) {
+    auto const & re = cache_.try_emplace(regex, regex).first->second;
+    return std::regex_search(text, re);
+  }
 };
+
+/**
+ * @brief An implementation of an "Extension Constraint Visitor" plugin that
+ * does nothing.
+ */
+struct StubExtensionVisitor {};
 }
 
 namespace jvalidate {
@@ -39,11 +47,14 @@ namespace jvalidate {
  *
  * @tparam RE A type that can be used to solve regular expressions
  */
-template <RegexEngine RE = detail::StdRegexEngine> class ValidatorT {
+template <RegexEngine RE = detail::StdRegexEngine,
+          typename ExtensionVisitor = detail::StubExtensionVisitor>
+class Validator {
 private:
   schema::Node const & schema_;
   ValidationConfig cfg_;
-  std::unordered_map<std::string, RE> regex_cache_;
+  ExtensionVisitor extension_;
+  RE regex_;
 
 public:
   /**
@@ -54,9 +65,15 @@ public:
    * @param cfg Any special (runtime) configuration rules being applied to the
    * validator.
    */
-  ValidatorT(schema::Node const & schema, ValidationConfig const & cfg = {})
+  Validator(schema::Node const & schema, ExtensionVisitor extension = {},
+            ValidationConfig const & cfg = {})
+      : schema_(schema), cfg_(cfg), extension_(extension) {}
+
+  Validator(schema::Node const & schema, ValidationConfig const & cfg)
       : schema_(schema), cfg_(cfg) {}
 
+  template <typename... Args> Validator(schema::Node &&, Args &&...) = delete;
+
   /**
    * @brief Run validation on the given JSON
    *
@@ -76,7 +93,7 @@ public:
     EXPECT_M(not cfg_.construct_default_values,
              "Cannot perform mutations on an immutable JSON Adapter");
     return static_cast<bool>(
-        ValidationVisitor<A, RE>(json, schema_, cfg_, regex_cache_, result).validate());
+        ValidationVisitor(schema_, cfg_, regex_, extension_, result).validate(json));
   }
 
   /**
@@ -95,7 +112,7 @@ public:
    */
   template <MutableAdapter A> bool validate(A const & json, ValidationResult * result = nullptr) {
     return static_cast<bool>(
-        ValidationVisitor<A, RE>(json, schema_, cfg_, regex_cache_, result).validate());
+        ValidationVisitor(schema_, cfg_, regex_, extension_, result).validate(json));
   }
 
   /**
@@ -117,12 +134,4 @@ public:
     return validate(adapter::AdapterFor<JSON>(json), result);
   }
 };
-
-/**
- * @brief Syntactic sugar for ValidatorT<>.
- */
-class Validator : public ValidatorT<> {
-public:
-  using Validator::ValidatorT::ValidatorT;
-};
 }

+ 1 - 57
tests/annotation_test.cxx

@@ -5,7 +5,6 @@
 
 #include <jvalidate/adapter.h>
 #include <jvalidate/adapters/jsoncpp.h>
-#include <jvalidate/detail/pointer.h>
 #include <jvalidate/enum.h>
 #include <jvalidate/schema.h>
 #include <jvalidate/status.h>
@@ -13,30 +12,11 @@
 #include <jvalidate/validation_result.h>
 #include <jvalidate/validator.h>
 
-#include <json/reader.h>
-#include <json/value.h>
+#include "matchers.h"
 
 using enum jvalidate::schema::Version;
 using testing::Not;
 
-auto operator""_jptr(char const * data, size_t len) {
-  return jvalidate::detail::Pointer(std::string_view{data, len});
-}
-
-Json::Value operator""_json(char const * data, size_t len) {
-  Json::Value value;
-
-  Json::CharReaderBuilder builder;
-  std::unique_ptr<Json::CharReader> reader(builder.newCharReader());
-
-  std::string error;
-  if (not reader->parse(data, data + len, &value, &error)) {
-    throw std::runtime_error(error);
-  }
-
-  return value;
-}
-
 auto validate(Json::Value const & schema_doc, Json::Value const & instance_doc,
               jvalidate::schema::Version version = Draft2020_12) {
   jvalidate::Schema const schema(schema_doc, version);
@@ -47,42 +27,6 @@ auto validate(Json::Value const & schema_doc, Json::Value const & instance_doc,
   return result;
 }
 
-MATCHER_P(HasAnnotationsFor, doc_path, "") { return arg.has(doc_path); }
-
-MATCHER_P2(HasAnnotationAt, doc_path, schema_path, "") { return arg.has(doc_path, schema_path); }
-
-MATCHER_P2(AnnotationAt, key, matcher, "") {
-  auto const * anno = arg.annotation({}, {}, key);
-  if (not anno) {
-    return false;
-  }
-  return testing::ExplainMatchResult(matcher, *anno, result_listener);
-}
-
-MATCHER_P4(AnnotationAt, doc_path, schema_path, key, matcher, "") {
-  auto const * anno = arg.annotation(doc_path, schema_path, key);
-  if (not anno) {
-    return false;
-  }
-  return testing::ExplainMatchResult(matcher, *anno, result_listener);
-}
-
-MATCHER_P2(ErrorAt, key, matcher, "") {
-  auto const * anno = arg.error({}, {}, key);
-  if (not anno) {
-    return false;
-  }
-  return testing::ExplainMatchResult(matcher, *anno, result_listener);
-}
-
-MATCHER_P4(ErrorAt, doc_path, schema_path, key, matcher, "") {
-  auto const * anno = arg.error(doc_path, schema_path, key);
-  if (not anno) {
-    return false;
-  }
-  return testing::ExplainMatchResult(matcher, *anno, result_listener);
-}
-
 TEST(Annotation, AttachesFormattingAnnotation) {
   auto const schema = R"({
     "format": "uri"

+ 149 - 0
tests/extension_test.cxx

@@ -0,0 +1,149 @@
+#include <jvalidate/extension.h>
+
+#include <gmock/gmock.h>
+#include <gtest/gtest.h>
+#include <json/value.h>
+
+#include <jvalidate/adapters/jsoncpp.h>
+#include <jvalidate/constraint/extension_constraint.h>
+#include <jvalidate/detail/expect.h>
+#include <jvalidate/detail/relative_pointer.h>
+#include <jvalidate/forward.h>
+#include <jvalidate/status.h>
+#include <jvalidate/validator.h>
+
+#include "matchers.h"
+
+using enum jvalidate::schema::Version;
+using jvalidate::Status;
+using jvalidate::constraint::ExtensionConstraint;
+using testing::Not;
+
+struct IsKeyOfConstraint : jvalidate::extension::ConstraintBase<IsKeyOfConstraint> {
+  IsKeyOfConstraint(std::string_view ptr) : ptr(ptr) {
+    EXPECT_M(ptr.find('/') != std::string_view::npos,
+             "IsKeyOfConstraint requires a value-relative-pointer, not a key-relative-pointer");
+  }
+
+  jvalidate::detail::RelativePointer ptr;
+};
+
+template <jvalidate::Adapter A>
+class Visitor : public jvalidate::extension::Visitor<Visitor<A>, IsKeyOfConstraint> {
+public:
+  Visitor(A const & root_document) : root_document_(root_document) {}
+
+  template <jvalidate::Adapter A2>
+  Status visit(IsKeyOfConstraint const & cons, A2 const & document, auto const & validator) const {
+    validator.annotate(cons.ptr);
+    auto const & object =
+        std::get<1>(cons.ptr.inspect(validator.where_, root_document_)).as_object();
+    if (object.find(document.as_string()) != object.end()) {
+      return Status::Accept;
+    }
+    return Status::Reject;
+  }
+
+private:
+  A root_document_;
+};
+
+auto validate(Json::Value const & schema_doc, Json::Value const & instance_doc,
+              jvalidate::schema::Version version = Draft2020_12) {
+  using A = jvalidate::adapter::AdapterFor<Json::Value const>;
+  jvalidate::ConstraintFactory<A> factory{{"is_key_of", [](auto const & context) {
+                                             return ExtensionConstraint::make<IsKeyOfConstraint>(
+                                                 context.schema.as_string());
+                                           }}};
+  jvalidate::Schema const schema(schema_doc, version, factory);
+
+  jvalidate::ValidationResult result;
+  (void)jvalidate::Validator(schema, Visitor(A(instance_doc))).validate(instance_doc, &result);
+
+  return result;
+}
+
+TEST(ExtensionConstraint, CanReportSuccess) {
+  auto schema = R"({
+    "properties": {
+      "nodes": {
+        "type": "object"
+      },
+      "edges": {
+        "items": {
+          "properties": {
+            "destination": {
+              "is_key_of": "3/nodes",
+              "type": "string"
+            },
+            "source": {
+              "is_key_of": "3/nodes",
+              "type": "string"
+            }
+          },
+          "type": "object"
+        },
+        "type": "array"
+      }
+    },
+    "type": "object"
+  })"_json;
+
+  auto instance = R"({
+    "nodes": {
+      "A": {},
+      "B": {}
+    },
+    "edges": [
+      { "source": "A", "destination": "B" }
+    ]
+  })"_json;
+
+  jvalidate::ValidationResult result = validate(schema, instance);
+  EXPECT_THAT(result, Valid());
+}
+
+TEST(ExtensionConstraint, CanReportFailure) {
+  auto schema = R"({
+    "properties": {
+      "nodes": {
+        "type": "object"
+      },
+      "edges": {
+        "items": {
+          "properties": {
+            "destination": {
+              "is_key_of": "3/nodes",
+              "type": "string"
+            },
+            "source": {
+              "is_key_of": "3/nodes",
+              "type": "string"
+            }
+          },
+          "type": "object"
+        },
+        "type": "array"
+      }
+    },
+    "type": "object"
+  })"_json;
+
+  auto instance = R"({
+    "nodes": {
+      "A": {},
+      "B": {}
+    },
+    "edges": [
+      { "source": "A", "destination": "C" }
+    ]
+  })"_json;
+
+  jvalidate::ValidationResult result = validate(schema, instance);
+  EXPECT_THAT(result, Not(Valid()));
+}
+
+int main(int argc, char ** argv) {
+  testing::InitGoogleMock(&argc, argv);
+  return RUN_ALL_TESTS();
+}

+ 64 - 0
tests/matchers.h

@@ -0,0 +1,64 @@
+#include <gmock/gmock.h>
+#include <gtest/gtest.h>
+
+#include <jvalidate/detail/pointer.h>
+#include <jvalidate/validation_result.h>
+
+#include <json/reader.h>
+#include <json/value.h>
+
+inline auto operator""_jptr(char const * data, size_t len) {
+  return jvalidate::detail::Pointer(std::string_view{data, len});
+}
+
+inline Json::Value operator""_json(char const * data, size_t len) {
+  Json::Value value;
+
+  Json::CharReaderBuilder builder;
+  std::unique_ptr<Json::CharReader> reader(builder.newCharReader());
+
+  std::string error;
+  if (not reader->parse(data, data + len, &value, &error)) {
+    throw std::runtime_error(error);
+  }
+
+  return value;
+}
+
+MATCHER(Valid, "") { return arg.valid(); }
+
+MATCHER_P(HasAnnotationsFor, doc_path, "") { return arg.has(doc_path); }
+
+MATCHER_P2(HasAnnotationAt, doc_path, schema_path, "") { return arg.has(doc_path, schema_path); }
+
+MATCHER_P2(AnnotationAt, key, matcher, "") {
+  auto const * anno = arg.annotation({}, {}, key);
+  if (not anno) {
+    return false;
+  }
+  return testing::ExplainMatchResult(matcher, *anno, result_listener);
+}
+
+MATCHER_P4(AnnotationAt, doc_path, schema_path, key, matcher, "") {
+  auto const * anno = arg.annotation(doc_path, schema_path, key);
+  if (not anno) {
+    return false;
+  }
+  return testing::ExplainMatchResult(matcher, *anno, result_listener);
+}
+
+MATCHER_P2(ErrorAt, key, matcher, "") {
+  auto const * anno = arg.error({}, {}, key);
+  if (not anno) {
+    return false;
+  }
+  return testing::ExplainMatchResult(matcher, *anno, result_listener);
+}
+
+MATCHER_P4(ErrorAt, doc_path, schema_path, key, matcher, "") {
+  auto const * anno = arg.error(doc_path, schema_path, key);
+  if (not anno) {
+    return false;
+  }
+  return testing::ExplainMatchResult(matcher, *anno, result_listener);
+}

+ 0 - 2
tests/selfvalidate_test.cxx

@@ -3,8 +3,6 @@
 #include <filesystem>
 #include <fstream>
 #include <iostream>
-#include <regex>
-#include <unordered_set>
 
 #include <curl/curl.h>
 #include <gmock/gmock.h>