Browse Source

feat: initial design of schema parsing and constraints

Sam Jaffe 1 year ago
parent
commit
08d68f1ff7

+ 3 - 3
.clang-format

@@ -12,7 +12,7 @@ AllowAllParametersOfDeclarationOnNextLine: true
 AllowShortBlocksOnASingleLine: true
 AllowShortCaseLabelsOnASingleLine: false
 AllowShortFunctionsOnASingleLine: All
-AllowShortIfStatementsOnASingleLine: true
+AllowShortIfStatementsOnASingleLine: false
 AllowShortLoopsOnASingleLine: false
 AlwaysBreakAfterDefinitionReturnType: None
 AlwaysBreakAfterReturnType: None
@@ -43,7 +43,7 @@ BreakConstructorInitializersBeforeComma: false
 BreakConstructorInitializers: BeforeColon
 BreakAfterJavaFieldAnnotations: false
 BreakStringLiterals: true
-ColumnLimit:     80
+ColumnLimit:     100
 CommentPragmas:  '^ IWYU pragma:'
 CompactNamespaces: true
 ConstructorInitializerAllOnOneLineOrOnePerLine: false
@@ -101,7 +101,7 @@ SpacesInContainerLiterals: true
 SpacesInCStyleCastParentheses: false
 SpacesInParentheses: false
 SpacesInSquareBrackets: false
-Standard:        Cpp11
+Standard:        c++20
 TabWidth:        8
 UseTab:          Never
 ...

+ 3 - 0
.gitignore

@@ -30,3 +30,6 @@
 *.exe
 *.out
 *.app
+
+.cache
+compile_commands.json

+ 49 - 0
include/jvalidate/adapter.h

@@ -0,0 +1,49 @@
+#pragma once
+
+#include <cstdint>
+#include <map>
+#include <optional>
+#include <string_view>
+
+#include <jvalidate/detail/array_iterator.h>
+#include <jvalidate/detail/object_iterator.h>
+#include <jvalidate/enum.h>
+#include <jvalidate/forward.h>
+
+namespace jvalidate::adapter {
+class Adapter {
+public:
+  virtual ~Adapter() = default;
+
+  virtual Type type() const = 0;
+  virtual std::unique_ptr<Const const> freeze() const = 0;
+
+  virtual bool as_boolean() const = 0;
+  virtual int64_t as_integer() const = 0;
+  virtual double as_number() const = 0;
+  virtual std::string as_string() const = 0;
+
+  virtual bool apply_array(AdapterCallback const & cb) const = 0;
+  virtual bool apply_object(ObjectAdapterCallback const & cb) const = 0;
+};
+
+class Const {
+public:
+  virtual ~Const() = default;
+  virtual bool apply(AdapterCallback const & cb) const = 0;
+};
+}
+
+namespace jvalidate::adapter::detail {
+template <typename JSON> class GenericConst final : public Const {
+public:
+  explicit GenericConst(JSON const & value) : value_(value) {}
+
+  bool apply(AdapterCallback const & cb) const {
+    return cb(typename AdapterTraits<JSON>::ConstAdapter(value_));
+  }
+
+private:
+  JSON value_;
+};
+}

+ 72 - 0
include/jvalidate/adapters/jsoncpp.h

@@ -0,0 +1,72 @@
+#pragma once
+#include <memory>
+#include <type_traits>
+
+#include <json/value.h>
+
+#include <jvalidate/detail/simple_adapter.h>
+
+namespace jvalidate::adapter {
+template <typename JSON> class JsonCppAdapter;
+
+template <> struct AdapterTraits<Json::Value> {
+  template <typename JSON> using Adapter = adapter::JsonCppAdapter<JSON>;
+  using ConstAdapter = adapter::JsonCppAdapter<Json::Value const>;
+
+  static Json::Value const & const_empty() {
+    static Json::Value const g_value;
+    return g_value;
+  }
+};
+
+template <typename JSON> class JsonCppObjectAdapter : public detail::SimpleObjectAdapter<JSON> {
+public:
+  using JsonCppObjectAdapter::SimpleObjectAdapter::SimpleObjectAdapter;
+
+  bool contains(std::string const & key) const { return this->const_value().isMember(key); }
+
+  JsonCppAdapter<JSON> operator[](std::string const & key) const {
+    return this->value() ? &(*this->value())[key] : nullptr;
+  }
+};
+
+template <typename JSON> class JsonCppAdapter final : public detail::SimpleAdapter<JSON> {
+private:
+  JSON * value_;
+
+public:
+  using JsonCppAdapter::SimpleAdapter::SimpleAdapter;
+
+  Type type() const {
+    switch (const_value().type()) {
+    case Json::nullValue:
+      return Type::Null;
+    case Json::booleanValue:
+      return Type::Boolean;
+    case Json::realValue:
+      return Type::Number;
+    case Json::stringValue:
+      return Type::String;
+    case Json::arrayValue:
+      return Type::Array;
+    case Json::objectValue:
+      return Type::Object;
+    case Json::intValue:
+    case Json::uintValue:
+      return Type::Integer;
+    }
+  }
+
+  bool as_boolean() const { return const_value().asBool(); }
+  int64_t as_integer() const { return const_value().asInt64(); }
+  double as_number() const { return const_value().asDouble(); }
+  std::string as_string() const { return const_value().asString(); }
+
+  JsonCppObjectAdapter<JSON> as_object() const { return value_; }
+
+  static std::string key(auto it) { return it.key().asString(); }
+
+private:
+  using JsonCppAdapter::SimpleAdapter::const_value;
+};
+}

+ 453 - 0
include/jvalidate/constraint.h

@@ -0,0 +1,453 @@
+#pragma once
+
+#include <functional>
+#include <map>
+#include <memory>
+#include <set>
+#include <string_view>
+#include <unordered_map>
+#include <unordered_set>
+
+#include <jvalidate/constraint/array_constraint.h>
+#include <jvalidate/constraint/general_constraint.h>
+#include <jvalidate/constraint/number_constraint.h>
+#include <jvalidate/constraint/object_constraint.h>
+#include <jvalidate/constraint/string_constraint.h>
+
+#include <jvalidate/detail/expect.h>
+#include <jvalidate/detail/reference.h>
+#include <jvalidate/enum.h>
+#include <jvalidate/forward.h>
+#include <jvalidate/parser_context.h>
+
+namespace jvalidate {
+template <Adapter A> class ConstraintFactory {
+public:
+  using pConstraint = std::unique_ptr<constraint::Constraint>;
+  using Object = decltype(std::declval<A>().as_object());
+
+  using MakeConstraint = std::function<pConstraint(ParserContext<A> const &)>;
+  using VersionedMakeConstraint = std::map<schema::Version, MakeConstraint, std::greater<>>;
+
+private:
+  using Self = ConstraintFactory<A>;
+
+private:
+  std::unordered_map<std::string_view, MakeConstraint> constraints_{
+      {"additionalProperties", &Self::additionalProperties},
+      {"enum", &Self::isInEnumuration},
+      {"maxItems", &Self::maxItems},
+      {"maxLength", &Self::maxLength},
+      {"maximum", &Self::maximum},
+      {"minItems", &Self::minItems},
+      {"minLength", &Self::minLength},
+      {"minimum", &Self::minimum},
+      {"pattern", &Self::pattern},
+      {"patternProperties", &Self::patternProperties},
+      {"properties", &Self::properties},
+      {"type", &Self::type},
+      {"uniqueItems", &Self::uniqueItems},
+  };
+
+  std::unordered_map<std::string_view, VersionedMakeConstraint> versioned_constraints_{
+      {"additionalItems",
+       {{schema::Version::Draft04, &Self::additionalItems},
+        {schema::Version::Draft2020_12, nullptr}}},
+      {"allOf", {{schema::Version::Draft04, &Self::allOf}}},
+      {"anyOf", {{schema::Version::Draft04, &Self::anyOf}}},
+      {"const", {{schema::Version::Draft06, &Self::isConstant}}},
+      {"contains", {{schema::Version::Draft06, &Self::contains}}},
+      {"dependencies",
+       {{schema::Version::Draft04, &Self::dependencies}, {schema::Version::Draft2019_09, nullptr}}},
+      {"dependentRequired", {{schema::Version::Draft2019_09, &Self::dependentRequired}}},
+      {"dependentSchemas", {{schema::Version::Draft2019_09, &Self::dependentSchemas}}},
+      {"divisibleBy",
+       {{schema::Version::Draft04, &Self::multipleOf}, {schema::Version::Draft04, nullptr}}},
+      {"exclusiveMaximum", {{schema::Version::Draft06, &Self::exclusiveMaximum}}},
+      {"exclusiveMinimum", {{schema::Version::Draft06, &Self::exclusiveMinimum}}},
+      {"format",
+       {{schema::Version::Draft04, &Self::warnUnimplemented},
+        {schema::Version::Draft2020_12, nullptr}}},
+      {"format-assertion", {{schema::Version::Draft2020_12, &Self::fatalUnimplemented}}},
+      {"if", {{schema::Version::Draft07, &Self::ifThenElse}}},
+      {"items",
+       {{schema::Version::Draft04, &Self::itemsTupleOrVector},
+        {schema::Version::Draft2020_12, &Self::additionalItems}}},
+      {"maxProperties", {{schema::Version::Draft04, &Self::maxProperties}}},
+      {"minProperties", {{schema::Version::Draft04, &Self::minProperties}}},
+      {"multipleOf", {{schema::Version::Draft04, &Self::multipleOf}}},
+      {"not", {{schema::Version::Draft04, &Self::isNot}}},
+      {"oneOf", {{schema::Version::Draft04, &Self::oneOf}}},
+      {"prefixItems", {{schema::Version::Draft2020_12, &Self::prefixItems}}},
+      {"propertyNames", {{schema::Version::Draft06, &Self::propertyNames}}},
+      {"required", {{schema::Version::Draft04, &Self::required}}},
+      {"unevaluatedItems", {{schema::Version::Draft2019_09, &Self::unevaluatedItems}}},
+      {"unevaluatedProperties", {{schema::Version::Draft2019_09, &Self::unevaluatedProperties}}},
+  };
+
+public:
+  MakeConstraint operator()(std::string_view key, schema::Version version) const {
+    if (auto it = constraints_.find(key); it != constraints_.end()) {
+      return it->second;
+    }
+    if (auto it = versioned_constraints_.find(key); it != versioned_constraints_.end()) {
+      if (auto vit = it->second.lower_bound(version); vit != it->second.end()) {
+        return vit->second;
+      }
+    }
+    return nullptr;
+  }
+
+  // SECTION: Untyped Constraints
+
+  static pConstraint warnUnimplemented(ParserContext<A> const & context) {
+    std::cerr << "Unimplemented constraint " << context.where << "\n";
+    return nullptr;
+  }
+
+  static pConstraint fatalUnimplemented(ParserContext<A> const & context) {
+    JVALIDATE_THROW(std::runtime_error, "Unimplemented constraint " << context.where);
+  }
+
+  static auto type(ParserContext<A> const & context) {
+    static std::unordered_map<std::string_view, adapter::Type> const s_type_names{
+        {"null", adapter::Type::Null},       {"boolean", adapter::Type::Boolean},
+        {"integer", adapter::Type::Integer}, {"number", adapter::Type::Number},
+        {"string", adapter::Type::String},   {"array", adapter::Type::Array},
+        {"object", adapter::Type::Object},
+    };
+
+    auto to_type = [](std::string_view type) {
+      EXPECT_M(s_type_names.contains(type), "Unknown type " << type);
+      return s_type_names.at(type);
+    };
+
+    adapter::Type const type = context.schema.type();
+    if (type == adapter::Type::String) {
+      return std::make_unique<constraint::TypeConstraint>(to_type(context.schema.as_string()));
+    }
+
+    EXPECT(type == adapter::Type::Array);
+    std::set<adapter::Type> types;
+    for (auto subschema : context.schema.as_array()) {
+      types.insert(to_type(subschema.as_string()));
+    }
+    return std::make_unique<constraint::TypeConstraint>(types);
+  }
+
+  static auto ifThenElse(ParserContext<A> const & context) {
+    return std::make_unique<constraint::ConditionalConstraint>(
+        context.node(), context.neighbor("then").node(), context.neighbor("else").node());
+  }
+
+  static auto isInEnumuration(ParserContext<A> const & context) {
+    EXPECT(context.schema.type() == adapter::Type::Array);
+
+    std::vector<std::unique_ptr<adapter::Const const>> rval;
+    for (auto subschema : context.schema.as_array()) {
+      rval.push_back(subschema.freeze());
+    }
+
+    return std::make_unique<constraint::EnumConstraint>(std::move(rval));
+  }
+
+  static auto isConstant(ParserContext<A> const & context) {
+    return std::make_unique<constraint::EnumConstraint>(context.schema.freeze());
+  }
+
+  static auto allOf(ParserContext<A> const & context) {
+    EXPECT(context.schema.type() == adapter::Type::Array);
+
+    std::vector<schema::Node const *> rval;
+    size_t index = 0;
+    for (auto subschema : context.schema.as_array()) {
+      rval.push_back(context.child(subschema, index).node());
+      ++index;
+    }
+
+    return std::make_unique<constraint::AllOfConstraint>(rval);
+  }
+
+  static auto anyOf(ParserContext<A> const & context) {
+    EXPECT(context.schema.type() == adapter::Type::Array);
+
+    std::vector<schema::Node const *> rval;
+    size_t index = 0;
+    for (auto subschema : context.schema.as_array()) {
+      rval.push_back(context.child(subschema, index).node());
+      ++index;
+    }
+
+    return std::make_unique<constraint::AnyOfConstraint>(rval);
+  }
+
+  static auto oneOf(ParserContext<A> const & context) {
+    EXPECT(context.schema.type() == adapter::Type::Array);
+
+    std::vector<schema::Node const *> rval;
+    size_t index = 0;
+    for (auto subschema : context.schema.as_array()) {
+      rval.push_back(context.child(subschema, index).node());
+      ++index;
+    }
+
+    return std::make_unique<constraint::OneOfConstraint>(rval);
+  }
+
+  static auto isNot(ParserContext<A> const & context) {
+    return std::make_unique<constraint::NotConstraint>(context.node());
+  }
+
+  // SECTION: Numeric Constraints
+
+  static auto minimum(ParserContext<A> const & context) {
+    double value = context.schema.as_number();
+    if (context.version < schema::Version::Draft06 &&
+        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 std::make_unique<constraint::MinimumConstraint>(value, false);
+  }
+
+  static pConstraint exclusiveMinimum(ParserContext<A> const & context) {
+    double value = context.schema.as_number();
+    return std::make_unique<constraint::MinimumConstraint>(value, true);
+  }
+
+  static auto maximum(ParserContext<A> const & context) {
+    double value = context.schema.as_number();
+    if (context.version < schema::Version::Draft06 &&
+        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 std::make_unique<constraint::MaximumConstraint>(value, false);
+  }
+
+  static pConstraint exclusiveMaximum(ParserContext<A> const & context) {
+    double value = context.schema.as_number();
+    return std::make_unique<constraint::MaximumConstraint>(value, true);
+  }
+
+  static auto multipleOf(ParserContext<A> const & context) {
+    int64_t value = context.schema.as_integer();
+    return std::make_unique<constraint::MultipleOfConstraint>(value);
+  }
+
+  // SECTION: String Constraints
+
+  static auto minLength(ParserContext<A> const & context) {
+    EXPECT(context.schema.type() == adapter::Type::Integer);
+    return std::make_unique<constraint::MinLengthConstraint>(context.schema.as_integer());
+  }
+
+  static auto maxLength(ParserContext<A> const & context) {
+    EXPECT(context.schema.type() == adapter::Type::Integer);
+    return std::make_unique<constraint::MaxLengthConstraint>(context.schema.as_integer());
+  }
+
+  static auto pattern(ParserContext<A> const & context) {
+    return std::make_unique<constraint::PatternConstraint>(context.schema.as_string());
+  }
+
+  // SECTION: Array Constraints
+
+  static auto contains(ParserContext<A> const & context) {
+    if (context.version < schema::Version::Draft2019_09) {
+      return std::make_unique<constraint::ContainsConstraint>(context.schema.freeze());
+    }
+
+    std::optional<size_t> maximum;
+    std::optional<size_t> minimum;
+    if (context.parent->contains("maxContains")) {
+      maximum = (*context.parent)["maxContains"].as_integer();
+    }
+    if (context.parent->contains("minContains")) {
+      minimum = (*context.parent)["minContains"].as_integer();
+    }
+
+    return std::make_unique<constraint::ContainsConstraint>(context.schema.freeze(), minimum,
+                                                            maximum);
+  }
+
+  static auto minItems(ParserContext<A> const & context) {
+    EXPECT(context.schema.type() == adapter::Type::Integer);
+    return std::make_unique<constraint::MinItemsConstraint>(context.schema.as_integer());
+  }
+
+  static auto maxItems(ParserContext<A> const & context) {
+    EXPECT(context.schema.type() == adapter::Type::Integer);
+    return std::make_unique<constraint::MaxItemsConstraint>(context.schema.as_integer());
+  }
+
+  static auto prefixItems(ParserContext<A> const & context) {
+    EXPECT(context.schema.type() == adapter::Type::Array);
+
+    std::vector<schema::Node const *> rval;
+    size_t index = 0;
+    for (auto subschema : context.schema.as_array()) {
+      rval.push_back(context.child(subschema, index).node());
+      ++index;
+    }
+
+    return std::make_unique<constraint::TupleConstraint>(rval);
+  }
+
+  static pConstraint additionalItems(ParserContext<A> const & context) {
+    std::string const prefix =
+        context.version >= schema::Version::Draft2020_12 ? "prefixItems" : "items";
+
+    Object const & parent = *context.parent;
+    size_t start_after = 0;
+    if (not prefix.empty() && parent.contains(prefix)) {
+      start_after = parent[prefix].as_integer();
+    }
+
+    schema::Node const * schema = context.node();
+    return std::make_unique<constraint::AdditionalItemsConstraint>(schema, start_after);
+  }
+
+  static pConstraint itemsTupleOrVector(ParserContext<A> const & context) {
+    if (context.schema.type() == adapter::Type::Array) {
+      return prefixItems(context);
+    }
+
+    return additionalItems(context);
+  }
+
+  static auto unevaluatedItems(ParserContext<A> const & context) {
+    return std::make_unique<constraint::UnevaluatedItemsConstraint>(context.node());
+  }
+
+  static pConstraint uniqueItems(ParserContext<A> const & context) {
+    EXPECT(context.schema.type() == adapter::Type::Boolean);
+    if (not context.schema.as_boolean()) {
+      return nullptr;
+    }
+
+    return std::make_unique<constraint::UniqueItemsConstraint>();
+  }
+
+  // SECTION: Object Constraints
+
+  static auto required(ParserContext<A> const & context) {
+    EXPECT(context.schema.type() == adapter::Type::Array);
+
+    std::unordered_set<std::string> rval;
+    for (auto subschema : context.schema.as_array()) {
+      EXPECT(subschema.type() == adapter::Type::String);
+      rval.insert(subschema.as_string());
+    }
+
+    return std::make_unique<constraint::RequiredConstraint>(rval);
+  }
+
+  static auto minProperties(ParserContext<A> const & context) {
+    EXPECT(context.schema.type() == adapter::Type::Integer);
+    return std::make_unique<constraint::MinPropertiesConstraint>(context.schema.as_integer());
+  }
+
+  static auto maxProperties(ParserContext<A> const & context) {
+    EXPECT(context.schema.type() == adapter::Type::Integer);
+    return std::make_unique<constraint::MaxPropertiesConstraint>(context.schema.as_integer());
+  }
+
+  static auto patternProperties(ParserContext<A> const & context) {
+    EXPECT(context.schema.type() == adapter::Type::Object);
+
+    std::vector<std::pair<std::string, schema::Node const *>> rval;
+    for (auto [prop, subschema] : context.schema.as_object()) {
+      rval.emplace_back(prop, context.child(subschema, prop).node());
+    }
+
+    return std::make_unique<constraint::PatternPropertiesConstraint>(rval);
+  }
+
+  static auto properties(ParserContext<A> const & context) {
+    EXPECT(context.schema.type() == adapter::Type::Object);
+
+    std::map<std::string, schema::Node const *> rval;
+    for (auto [prop, subschema] : context.schema.as_object()) {
+      rval.emplace(prop, context.child(subschema, prop).node());
+    }
+
+    return std::make_unique<constraint::PropertiesConstraint>(rval);
+  }
+
+  static auto propertyNames(ParserContext<A> const & context) {
+    return std::make_unique<constraint::PropertyNamesConstraint>(context.node());
+  }
+
+  static auto unevaluatedProperties(ParserContext<A> const & context) {
+    return std::make_unique<constraint::UnevaluatedPropertiesConstraint>(context.node());
+  }
+
+  static auto additionalProperties(ParserContext<A> const & context) {
+    std::unordered_set<std::string> properties;
+    std::vector<std::string> patterns;
+
+    Object const & parent = *context.parent;
+    if (parent.contains("properties")) {
+      for (auto [key, _] : parent["properties"].as_object()) {
+        properties.insert(key);
+      }
+    }
+    if (parent.contains("patternProperties")) {
+      for (auto [key, _] : parent["patternProperties"].as_object()) {
+        patterns.push_back(key);
+      }
+    }
+
+    using C = constraint::AdditionalPropertiesConstraint;
+    return std::make_unique<C>(context.node(), properties, patterns);
+  }
+
+  static auto dependencies(ParserContext<A> const & context) {
+    EXPECT(context.schema.type() == adapter::Type::Object);
+
+    std::map<std::string, schema::Node const *> schemas;
+    std::map<std::string, std::vector<std::string>> required;
+    for (auto [prop, subschema] : context.schema.as_object()) {
+      if (subschema.type() == adapter::Type::Object) {
+        schemas.emplace(prop, context.child(subschema, prop).node());
+      } else {
+        for (auto key : subschema.as_array()) {
+          EXPECT(key.type() == adapter::Type::String);
+          required[prop].push_back(key.as_string());
+        }
+      }
+    }
+
+    return std::make_unique<constraint::DependenciesConstraint>(schemas, required);
+  }
+
+  static auto dependentSchemas(ParserContext<A> const & context) {
+    EXPECT(context.schema.type() == adapter::Type::Object);
+
+    std::map<std::string, schema::Node const *> rval;
+    for (auto [prop, subschema] : context.schema.as_object()) {
+      rval.emplace(prop, context.child(subschema, prop).node());
+    }
+
+    return std::make_unique<constraint::DependenciesConstraint>(rval);
+  }
+
+  static auto dependentRequired(ParserContext<A> const & context) {
+    EXPECT(context.schema.type() == adapter::Type::Object);
+
+    std::map<std::string, std::vector<std::string>> rval;
+    for (auto [prop, subschema] : context.schema.as_object()) {
+      EXPECT(subschema.type() == adapter::Type::Array);
+      for (auto key : subschema.as_array()) {
+        EXPECT(key.type() == adapter::Type::String);
+        rval[prop].push_back(key.as_string());
+      }
+    }
+
+    return std::make_unique<constraint::DependenciesConstraint>(rval);
+  }
+};
+}

+ 73 - 0
include/jvalidate/constraint/array_constraint.h

@@ -0,0 +1,73 @@
+#pragma once
+
+#include <jvalidate/detail/expect.h>
+#include <optional>
+#include <vector>
+
+#include <jvalidate/adapter.h>
+#include <jvalidate/constraint/constraint.h>
+#include <jvalidate/forward.h>
+#include <jvalidate/validator.h>
+
+namespace jvalidate::constraint {
+class AdditionalItemsConstraint : public Constraint {
+private:
+  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 Constraint {
+private:
+  std::unique_ptr<adapter::Const const> constant_;
+  std::optional<size_t> minimum_;
+  std::optional<size_t> maximum_;
+
+public:
+  ContainsConstraint(std::unique_ptr<adapter::Const const> && constant)
+      : constant_(std::move(constant)) {}
+
+  ContainsConstraint(std::unique_ptr<adapter::Const const> && constant,
+                     std::optional<size_t> minimum, std::optional<size_t> maximum)
+      : constant_(std::move(constant)), minimum_(minimum), maximum_(maximum) {}
+};
+
+class MaxItemsConstraint : public Constraint {
+private:
+  int64_t value_;
+
+public:
+  MaxItemsConstraint(int64_t value) : value_(value) {}
+};
+
+class MinItemsConstraint : public Constraint {
+private:
+  int64_t value_;
+
+public:
+  MinItemsConstraint(int64_t value) : value_(value) {}
+};
+
+class TupleConstraint : public Constraint {
+private:
+  std::vector<schema::Node const *> items_;
+
+public:
+  TupleConstraint(std::vector<schema::Node const *> const & items) : items_(items) {}
+};
+
+class UnevaluatedItemsConstraint : public Constraint {
+private:
+  schema::Node const * subschema_;
+
+public:
+  UnevaluatedItemsConstraint(schema::Node const * subschema) : subschema_(subschema) {}
+};
+
+class UniqueItemsConstraint : public Constraint {
+public:
+};
+}

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

@@ -0,0 +1,10 @@
+#pragma once
+
+#include <jvalidate/forward.h>
+
+namespace jvalidate::constraint {
+class Constraint {
+public:
+  virtual ~Constraint() = default;
+};
+}

+ 77 - 0
include/jvalidate/constraint/general_constraint.h

@@ -0,0 +1,77 @@
+#pragma once
+
+#include <memory>
+#include <set>
+#include <vector>
+
+#include <jvalidate/constraint/constraint.h>
+#include <jvalidate/forward.h>
+
+namespace jvalidate::constraint {
+class AllOfConstraint : public Constraint {
+private:
+  std::vector<schema::Node const *> children_;
+
+public:
+  AllOfConstraint(std::vector<schema::Node const *> const & children) : children_(children) {}
+};
+
+class AnyOfConstraint : public Constraint {
+private:
+  std::vector<schema::Node const *> children_;
+
+public:
+  AnyOfConstraint(std::vector<schema::Node const *> const & children) : children_(children) {}
+};
+
+class EnumConstraint : public Constraint {
+private:
+  std::vector<std::unique_ptr<adapter::Const const>> enums_;
+
+public:
+  EnumConstraint(std::unique_ptr<adapter::Const const> && constant) {
+    enums_.push_back(std::move(constant));
+  }
+
+  EnumConstraint(std::vector<std::unique_ptr<adapter::Const const>> && enums)
+      : enums_(std::move(enums)) {}
+};
+
+class OneOfConstraint : public Constraint {
+private:
+  std::vector<schema::Node const *> children_;
+
+public:
+  OneOfConstraint(std::vector<schema::Node const *> const & children) : children_(children) {}
+};
+
+class ConditionalConstraint : public Constraint {
+private:
+  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 Constraint {
+private:
+  schema::Node const * child_;
+
+public:
+  NotConstraint(schema::Node const * child) : child_(child) {}
+};
+
+class TypeConstraint : public Constraint {
+private:
+  std::set<adapter::Type> types_;
+
+public:
+  TypeConstraint(adapter::Type type) : types_{type} {}
+  TypeConstraint(std::set<adapter::Type> const & types) : types_(types) {}
+};
+}

+ 32 - 0
include/jvalidate/constraint/number_constraint.h

@@ -0,0 +1,32 @@
+#pragma once
+
+#include <jvalidate/constraint/constraint.h>
+#include <jvalidate/forward.h>
+
+namespace jvalidate::constraint {
+class MaximumConstraint : public Constraint {
+private:
+  double value_;
+  bool exclusive_;
+
+public:
+  MaximumConstraint(double value, bool exclusive) : value_(value), exclusive_(exclusive) {}
+};
+
+class MinimumConstraint : public Constraint {
+private:
+  double value_;
+  bool exclusive_;
+
+public:
+  MinimumConstraint(double value, bool exclusive) : value_(value), exclusive_(exclusive) {}
+};
+
+class MultipleOfConstraint : public Constraint {
+private:
+  int64_t value_;
+
+public:
+  MultipleOfConstraint(int64_t value) : value_(value) {}
+};
+}

+ 103 - 0
include/jvalidate/constraint/object_constraint.h

@@ -0,0 +1,103 @@
+#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 Constraint {
+private:
+  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 Constraint {
+private:
+  std::map<std::string, schema::Node const *> subschemas_;
+  std::map<std::string, std::vector<std::string>> required_;
+
+public:
+  DependenciesConstraint(std::map<std::string, schema::Node const *> const & subschemas)
+      : subschemas_(subschemas) {}
+
+  DependenciesConstraint(std::map<std::string, std::vector<std::string>> const & required)
+      : required_(required) {}
+
+  DependenciesConstraint(std::map<std::string, schema::Node const *> const & subschemas,
+                         std::map<std::string, std::vector<std::string>> const & required)
+      : subschemas_(subschemas), required_(required) {}
+};
+
+class MaxPropertiesConstraint : public Constraint {
+private:
+  int64_t value_;
+
+public:
+  MaxPropertiesConstraint(int64_t value) : value_(value) {}
+};
+
+class MinPropertiesConstraint : public Constraint {
+private:
+  int64_t value_;
+
+public:
+  MinPropertiesConstraint(int64_t value) : value_(value) {}
+};
+
+class PatternPropertiesConstraint : public Constraint {
+private:
+  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 Constraint {
+private:
+  std::map<std::string, schema::Node const *> properties_;
+
+public:
+  PropertiesConstraint(std::map<std::string, schema::Node const *> const & properties)
+      : properties_(properties) {}
+};
+
+class PropertyNamesConstraint : public Constraint {
+private:
+  schema::Node const * key_schema_;
+
+public:
+  PropertyNamesConstraint(schema::Node const * key_schema) : key_schema_(key_schema) {}
+};
+
+class RequiredConstraint : public Constraint {
+private:
+  std::unordered_set<std::string> properties_;
+
+public:
+  RequiredConstraint(std::unordered_set<std::string> const & properties)
+      : properties_(properties) {}
+};
+
+class UnevaluatedPropertiesConstraint : public Constraint {
+private:
+  schema::Node const * subschema_;
+
+public:
+  UnevaluatedPropertiesConstraint(schema::Node const * subschema) : subschema_(subschema) {}
+};
+}

+ 32 - 0
include/jvalidate/constraint/string_constraint.h

@@ -0,0 +1,32 @@
+#pragma once
+
+#include <string>
+
+#include <jvalidate/constraint/constraint.h>
+#include <jvalidate/forward.h>
+
+namespace jvalidate::constraint {
+class MinLengthConstraint : public Constraint {
+private:
+  int64_t value_;
+
+public:
+  MinLengthConstraint(int64_t value) : value_(value) {}
+};
+
+class MaxLengthConstraint : public Constraint {
+private:
+  int64_t value_;
+
+public:
+  MaxLengthConstraint(int64_t value) : value_(value) {}
+};
+
+class PatternConstraint : public Constraint {
+private:
+  std::string regex_;
+
+public:
+  PatternConstraint(std::string const & regex) : regex_(regex) {}
+};
+}

+ 37 - 0
include/jvalidate/detail/array_iterator.h

@@ -0,0 +1,37 @@
+#pragma once
+
+#include <iterator>
+#include <string>
+
+#include <jvalidate/detail/deref_proxy.h>
+
+namespace jvalidate::adapter::detail {
+
+template <typename It, typename Adapter> class JsonObjectIterator : public It {
+public:
+  using value_type = std::pair<std::string, Adapter>;
+  using reference = std::pair<std::string, Adapter>;
+  using pointer = ::jvalidate::detail::DerefProxy<reference>;
+  using difference_type = std::ptrdiff_t;
+  using iterator_category = std::forward_iterator_tag;
+
+  JsonObjectIterator() = default;
+  JsonObjectIterator(It it) : It(it) {}
+
+  reference operator*() const { return {Adapter::key(*this), Adapter(It::operator->())}; }
+
+  pointer operator->() const { return {operator*()}; }
+
+  JsonObjectIterator operator++(int) {
+    auto tmp = *this;
+    ++*this;
+    return tmp;
+  }
+
+  JsonObjectIterator & operator++() {
+    It::operator++();
+    return *this;
+  }
+};
+
+}

+ 12 - 0
include/jvalidate/detail/deref_proxy.h

@@ -0,0 +1,12 @@
+#pragma once
+
+namespace jvalidate::detail {
+template <typename T> struct DerefProxy {
+  T & operator*() { return value; }
+  T const & operator*() const { return value; }
+  T * operator->() { return &value; }
+  T const * operator->() const { return &value; }
+
+  T value;
+};
+}

+ 41 - 0
include/jvalidate/detail/expect.h

@@ -0,0 +1,41 @@
+#pragma once
+
+#include <iostream>
+#include <sstream>
+
+#if defined(__clang__) || defined(__GNUC__)
+#define JVALIDATE_LIKELY(x) __builtin_expect(!!(x), 1)
+#define JVALIDATE_UNLIKELY(x) __builtin_expect(!!(x), 0)
+#else
+#define JVALIDATE_LIKELY(x) (x)
+#define JVALIDATE_UNLIKELY(x) (x)
+#endif
+
+#if defined(JVALIDATE_USE_EXCEPTIONS)
+#define JVALIDATE_THROW(extype, message)                                                           \
+  do {                                                                                             \
+    std::stringstream ss;                                                                          \
+    ss << message;                                                                                 \
+    throw extype(ss.str());                                                                        \
+  } while (false)
+#else
+#define JVALIDATE_THROW(extype, message)                                                           \
+  do {                                                                                             \
+    std::cerr << message << std::endl;                                                             \
+    std::terminate();                                                                              \
+  } while (false)
+#endif
+
+#define EXPECT_T(condition, extype, message)                                                       \
+  if (JVALIDATE_UNLIKELY(!(condition))) {                                                          \
+    JVALIDATE_THROW(extype, message);                                                              \
+  }
+
+#define EXPECT_M(condition, message) EXPECT_T(condition, std::runtime_error, message)
+
+#define EXPECT(condition) EXPECT_M(condition, #condition)
+
+#define RETURN_UNLESS(condition, ...)                                                              \
+  if (JVALIDATE_UNLIKELY(!(condition))) {                                                          \
+    return __VA_ARGS__;                                                                            \
+  }

+ 35 - 0
include/jvalidate/detail/object_iterator.h

@@ -0,0 +1,35 @@
+#pragma once
+
+#include <iterator>
+
+#include <jvalidate/detail/deref_proxy.h>
+
+namespace jvalidate::adapter::detail {
+
+template <typename It, typename Adapter> class JsonArrayIterator : public It {
+public:
+  using value_type = Adapter;
+  using reference = Adapter;
+  using pointer = ::jvalidate::detail::DerefProxy<reference>;
+  using difference_type = std::ptrdiff_t;
+  using iterator_category = std::forward_iterator_tag;
+
+  JsonArrayIterator() = default;
+  JsonArrayIterator(It it) : It(it) {}
+
+  reference operator*() const { return {It::operator*()}; }
+
+  pointer operator->() const { return {operator*()}; }
+
+  JsonArrayIterator operator++(int) {
+    auto tmp = *this;
+    ++*this;
+    return tmp;
+  }
+
+  JsonArrayIterator & operator++() {
+    It::operator++();
+    return *this;
+  }
+};
+}

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

@@ -0,0 +1,75 @@
+#pragma once
+
+#include <compare>
+#include <cstdint>
+#include <ostream>
+#include <string>
+#include <string_view>
+#include <variant>
+#include <vector>
+
+namespace std {
+template <typename T> std::strong_ordering operator<=>(T const & lhs, T const & rhs) {
+  if (lhs < rhs) {
+    return std::strong_ordering::less;
+  }
+  if (lhs > rhs) {
+    return std::strong_ordering::greater;
+  }
+  return std::strong_ordering::equal;
+}
+}
+
+namespace jvalidate::detail {
+
+class Pointer {
+public:
+  Pointer() = default;
+  Pointer(std::vector<std::variant<std::string, size_t>> const & tokens) : tokens_(tokens) {}
+  Pointer(std::string_view path) {
+    for (size_t p = path.find('/'); p != std::string::npos;
+         path.remove_prefix(p + 1), p = path.find('/')) {
+      std::string token(path.substr(0, p));
+      if (token.find_first_not_of("0123456789") == std::string::npos) {
+        tokens_.emplace_back(std::stoull(token));
+      } else {
+        tokens_.emplace_back(token);
+      }
+    }
+  }
+
+  Pointer parent() const { return Pointer({tokens_.begin(), tokens_.end() - 1}); }
+
+  Pointer & operator/=(Pointer const & relative) {
+    tokens_.insert(tokens_.end(), relative.tokens_.begin(), relative.tokens_.end());
+    return *this;
+  }
+
+  Pointer operator/(Pointer const & relative) const { return Pointer(*this) /= relative; }
+
+  Pointer & operator/=(std::string_view key) {
+    tokens_.emplace_back(std::string(key));
+    return *this;
+  }
+
+  Pointer operator/(std::string_view key) const { return Pointer(*this) /= key; }
+
+  Pointer & operator/=(size_t index) {
+    tokens_.emplace_back(index);
+    return *this;
+  }
+
+  Pointer operator/(size_t index) const { return Pointer(*this) /= index; }
+
+  friend std::ostream & operator<<(std::ostream & os, Pointer const & self) {
+    for (auto const & elem : self.tokens_) {
+      std::visit([&os](auto const & v) { os << '/' << v; }, elem);
+    }
+    return os;
+  }
+  auto operator<=>(Pointer const &) const = default;
+
+private:
+  std::vector<std::variant<std::string, size_t>> tokens_{};
+};
+}

+ 62 - 0
include/jvalidate/detail/reference.h

@@ -0,0 +1,62 @@
+#pragma once
+
+#include <string>
+#include <string_view>
+
+#include <jvalidate/detail/expect.h>
+#include <jvalidate/detail/pointer.h>
+
+namespace jvalidate::detail {
+class Reference {
+public:
+  Reference() = default;
+  Reference(std::string const & anchor, Pointer const & pointer)
+      : anchor_(anchor), pointer_(pointer) {}
+  Reference(std::string_view fragment, bool allow_anchor = true) {
+    EXPECT_M(fragment.find('#') != std::string::npos, "Reference requires a fragment token '#'");
+
+    size_t const index = fragment.find('/');
+
+    anchor_ = std::string(fragment.substr(0, index));
+    EXPECT_M(allow_anchor && anchor_.back() == '#', "Anchoring is not allowed in this context");
+
+    if (index != std::string::npos) {
+      pointer_ = fragment.substr(index + 1);
+    }
+  }
+
+  std::string const & anchor() const { return anchor_; }
+  Pointer const & pointer() const { return pointer_; }
+  Reference parent() const { return {anchor_, pointer_.parent()}; }
+
+  Reference & operator/=(Pointer const & relative) {
+    pointer_ /= relative;
+    return *this;
+  }
+
+  Reference operator/(Pointer const & relative) const { return Reference(*this) /= relative; }
+
+  Reference & operator/=(std::string_view key) {
+    pointer_ /= key;
+    return *this;
+  }
+
+  Reference operator/(std::string_view key) const { return Reference(*this) /= key; }
+
+  Reference & operator/=(size_t index) {
+    pointer_ /= index;
+    return *this;
+  }
+
+  Reference operator/(size_t index) const { return Reference(*this) /= index; }
+
+  friend std::ostream & operator<<(std::ostream & os, Reference const & self) {
+    return os << self.anchor_ << self.pointer_;
+  }
+  auto operator<=>(Reference const &) const = default;
+
+private:
+  std::string anchor_;
+  Pointer pointer_;
+};
+}

+ 23 - 0
include/jvalidate/detail/regex_engine.h

@@ -0,0 +1,23 @@
+#pragma once
+
+#include <regex>
+#include <string>
+#include <unordered_map>
+
+namespace jvalidate::detail {
+class RegexEngine {
+public:
+  virtual ~RegexEngine() = default;
+  virtual bool operator()(std::string const & regex, std::string const & text) = 0;
+};
+
+class StdRegexEngine final : public RegexEngine {
+private:
+  std::unordered_map<std::string, std::regex> cache_;
+
+public:
+  bool operator()(std::string const & regex, std::string const & text) {
+    return std::regex_match(text, cache_.try_emplace(regex, regex).first->second);
+  }
+};
+}

+ 140 - 0
include/jvalidate/detail/simple_adapter.h

@@ -0,0 +1,140 @@
+#pragma once
+
+#include <unordered_map>
+#include <vector>
+
+#include <jvalidate/adapter.h>
+#include <jvalidate/detail/array_iterator.h>
+#include <jvalidate/detail/object_iterator.h>
+#include <jvalidate/forward.h>
+
+namespace jvalidate::adapter::detail {
+template <typename JSON, typename Adapter = AdapterFor<JSON>> class SimpleObjectAdapter {
+public:
+  using underlying_iterator_t = decltype(std::declval<JSON>().begin());
+  using const_iterator = JsonObjectIterator<underlying_iterator_t, Adapter>;
+
+  SimpleObjectAdapter(JSON * value) : value_(value) {}
+
+  size_t size() const { return value_ ? value_->size() : 0; }
+
+  const_iterator find(std::string const & key) const {
+    return std::find_if(begin(), end(), [key](auto const & kv) { return kv.first == key; });
+  }
+
+  bool contains(std::string const & key) const { return find(key) != end(); }
+
+  Adapter operator[](std::string const & key) const {
+    auto it = find(key);
+    return it != end() ? it->second : Adapter();
+  }
+
+  const_iterator begin() const {
+    return value_ ? const_iterator(value_->begin()) : const_iterator();
+  }
+
+  const_iterator end() const { return value_ ? const_iterator(value_->end()) : const_iterator(); }
+
+  std::map<std::string_view, Adapter> operator*() const {
+    using C = std::map<std::string_view, Adapter>;
+    return value_ ? C(begin(), end()) : C();
+  }
+
+protected:
+  JSON * value() const { return value_; }
+  JSON const & const_value() const { return value_ ? *value_ : AdapterTraits<JSON>::const_empty(); }
+
+private:
+  JSON * value_;
+};
+
+template <typename JSON, typename Adapter = AdapterFor<JSON>> class SimpleArrayAdapter {
+public:
+  using underlying_iterator_t = decltype(std::declval<JSON>().begin());
+  using const_iterator = JsonArrayIterator<underlying_iterator_t, Adapter>;
+
+  SimpleArrayAdapter(JSON * value) : value_(value) {}
+
+  size_t size() const { return value_ ? value_->size() : 0; }
+
+  Adapter operator[](size_t index) const {
+    if (index > size()) {
+      return Adapter();
+    }
+    auto it = begin();
+    std::advance(it, index);
+    return *it;
+  }
+
+  const_iterator begin() const {
+    return value_ ? const_iterator(value_->begin()) : const_iterator();
+  }
+
+  const_iterator end() const { return value_ ? const_iterator(value_->end()) : const_iterator(); }
+
+  std::vector<Adapter> operator*() const {
+    using C = std::vector<Adapter>;
+    return value_ ? C(begin(), end()) : C();
+  }
+
+protected:
+  JSON * value() const { return value_; }
+  JSON const & const_value() const { return value_ ? *value_ : AdapterTraits<JSON>::const_empty(); }
+
+private:
+  JSON * value_;
+};
+
+template <typename JSON, typename CRTP = AdapterFor<JSON>> class SimpleAdapter : public Adapter {
+public:
+  using value_type = std::remove_const_t<JSON>;
+
+public:
+  SimpleAdapter(JSON * value) : value_(value) {}
+  SimpleAdapter(JSON & value) : value_(&value) {}
+
+  size_t array_size() const { return self().as_array().size(); }
+
+  CRTP operator[](size_t index) const { return self().as_array()[index]; }
+
+  detail::SimpleArrayAdapter<JSON> as_array() const { return value_; }
+
+  bool apply_array(AdapterCallback const & cb) const final {
+    bool result = true;
+    for (auto const & child : self().as_array()) {
+      result = cb(child) && result;
+    }
+    return result;
+  }
+
+  size_t object_size() const { return self().as_object().size(); }
+
+  bool contains(std::string const & key) const { return self().as_object().contains(key); }
+
+  CRTP operator[](std::string const & key) const { return self().as_object()[key]; }
+
+  detail::SimpleObjectAdapter<JSON, CRTP> as_object() const { return value_; }
+
+  bool apply_object(ObjectAdapterCallback const & cb) const final {
+    bool result = true;
+    for (auto const & [key, child] : self().as_object()) {
+      result = cb(key, child) && result;
+    }
+    return result;
+  }
+
+  std::unique_ptr<Const const> freeze() const final {
+    return std::make_unique<GenericConst<value_type>>(const_value());
+  }
+
+protected:
+  JSON * value() const { return value_; }
+  JSON const & const_value() const { return value_ ? *value_ : AdapterTraits<JSON>::const_empty(); }
+
+private:
+  CRTP const & self() const { return static_cast<CRTP const &>(*this); }
+
+private:
+  JSON * value_;
+};
+}

+ 53 - 0
include/jvalidate/enum.h

@@ -0,0 +1,53 @@
+#pragma once
+
+#include <jvalidate/forward.h>
+
+namespace jvalidate::adapter {
+enum class Type : int8_t { Null, Boolean, Integer, Number, String, Array, Object };
+}
+
+namespace jvalidate::schema {
+enum class Version : int {
+  // Keywords: type, properties, patternProperties, additionalProperties, items, additionalItems
+  //           required, dependencies, minimum, maximum, exclusiveMinimum, exclusiveMaximum,
+  //           minItems, maxItems, uniqueItems, pattern, minLength, maxLength, enum, default,
+  //           title, description, format, divisibleBy, disallow, extends, id, $ref, $schema
+  /* Draft03, */
+
+  // New: allOf, anyOf, oneOf, not, minProperties, maxProperties, definitions
+  // Changed: required, dependencies
+  // Renamed: divisibleBy -> multipleOf
+  // Removed: type(schema), extends, disallow
+  // Formats: date-time, email, hostname, ipv4, ipv6, uri
+  // https://json-schema.org/draft-04/schema
+  Draft04,
+
+  // New: "boolean as schema", propertyNames, contains, const
+  // Changed: $ref, exclusiveMinimum, exclusiveMaximum, type, required, dependencies, examples
+  // Renamed: id -> $id
+  // Formats: uri-template, uri-reference, json-pointer
+  // https://json-schema.org/draft-06/schema
+  Draft06,
+
+  // New: $comment, if, then, else, readOnly, writeOnly, contentMediaType, contentEncoding
+  // Formats: iri, iri-reference, uri-template*, idn-email, idn-hostname, json-pointer*,
+  //          relative-json-pointer, regex, date, time
+  // https://json-schema.org/draft-07/schema
+  Draft07,
+
+  // New: $anchor, $recursiveAnchor, $recursiveRef, unevaluatedItems, unevaluatedProperties,
+  //      maxContains, minContains
+  // Changed: $id, $ref, format
+  // Renamed: definitions -> $defs
+  // Split: dependencies -> (dependentSchemas, dependentRequired)
+  // Formats: duration, hostname*, idn-hostname*, uuid
+  // https://json-schema.org/draft/2019-09/schema
+  Draft2019_09,
+
+  // Renamed: (items(array), additionalItems) -> (prefixItems, items),
+  //          $recursiveRef -> $dynamicRef, $recursiveAnchor -> $dynamicAnchor
+  // Split: format -> (format-annotation, format-assertion)
+  // https://json-schema.org/draft/2020-12/schema
+  Draft2020_12,
+};
+}

+ 79 - 0
include/jvalidate/forward.h

@@ -0,0 +1,79 @@
+#pragma once
+
+#include <functional>
+#include <string>
+
+namespace jvalidate::adapter {
+enum class Type : int8_t;
+class Adapter;
+class Const;
+
+using AdapterCallback = std::function<bool(adapter::Adapter const &)>;
+using ObjectAdapterCallback = std::function<bool(std::string const &, adapter::Adapter const &)>;
+
+template <typename> struct AdapterTraits;
+template <typename V> struct AdapterTraits<V const> : AdapterTraits<V> {};
+
+template <typename JSON> using AdapterFor = typename AdapterTraits<JSON>::template Adapter<JSON>;
+}
+
+namespace jvalidate::constraint {
+class ConstraintVisitor;
+class Constraint;
+}
+
+namespace jvalidate::schema {
+enum class Version : int;
+class Node;
+}
+
+namespace jvalidate {
+template <typename A>
+concept ScalarAdapter = requires(A const a) {
+  a.operator bool();
+  a.operator int64_t();
+  a.operator double();
+  a.operator std::string();
+};
+
+template <typename It>
+concept ArrayIterator = std::forward_iterator<It> and std::is_default_constructible_v<It> and
+    requires(It const it) {
+  { *it } -> std::convertible_to<adapter::Adapter const &>;
+};
+
+template <typename It>
+concept ObjectIterator = std::forward_iterator<It> and std::is_default_constructible_v<It> and
+    requires(It const it) {
+  { it->first } -> std::convertible_to<std::string_view>;
+  { it->second } -> std::convertible_to<adapter::Adapter const &>;
+};
+
+template <typename A>
+concept ArrayAdapter = requires(A const a) {
+  { a.size() } -> std::convertible_to<std::size_t>;
+  { a[0UL] } -> std::convertible_to<adapter::Adapter const &>;
+  { a.begin() } -> ArrayIterator;
+  { a.end() } -> ArrayIterator;
+};
+
+template <typename A>
+concept ObjectAdapter = requires(A const a) {
+  { a.size() } -> std::convertible_to<std::size_t>;
+  { a.contains("") } -> std::same_as<bool>;
+  { a[""] } -> std::convertible_to<adapter::Adapter const &>;
+  { a.begin() } -> ObjectIterator;
+  { a.end() } -> ObjectIterator;
+};
+
+template <typename A>
+concept Adapter = std::is_base_of_v<adapter::Adapter, A> && requires(A a) {
+  { a.as_object() } -> ObjectAdapter;
+  { a.as_array() } -> ArrayAdapter;
+};
+}
+
+namespace jvalidate {
+class Result;
+class Schema;
+}

+ 32 - 0
include/jvalidate/parser_context.h

@@ -0,0 +1,32 @@
+#pragma once
+
+#include <optional>
+
+#include <jvalidate/detail/reference.h>
+#include <jvalidate/forward.h>
+
+namespace jvalidate {
+template <Adapter A> struct ParserContext {
+  using Object = decltype(std::declval<A>().as_object());
+  Schema & root;
+
+  A schema;
+  std::optional<Object> parent = std::nullopt;
+
+  schema::Version version;
+  detail::Reference where = {};
+
+  ParserContext child(A const & child, std::string const & key) const {
+    return {root, child, schema.as_object(), version, where / key};
+  }
+  ParserContext child(A const & child, size_t index) const {
+    return {root, child, std::nullopt, version, where / index};
+  }
+  ParserContext neighbor(std::string const & key) const {
+    return {root, (*parent)[key], parent, version, where.parent() / key};
+  }
+
+  schema::Node const * resolve(std::string_view uri) const;
+  schema::Node const * node() const;
+};
+}

+ 224 - 0
include/jvalidate/schema.h

@@ -0,0 +1,224 @@
+#pragma once
+
+#include <memory>
+#include <unordered_map>
+#include <vector>
+
+#include <jvalidate/adapter.h>
+#include <jvalidate/constraint.h>
+#include <jvalidate/detail/expect.h>
+#include <jvalidate/detail/pointer.h>
+#include <jvalidate/detail/reference.h>
+#include <jvalidate/enum.h>
+#include <jvalidate/forward.h>
+#include <jvalidate/parser_context.h>
+
+namespace jvalidate::schema {
+class Node {
+private:
+  std::string description_;
+  std::unique_ptr<adapter::Const const> default_{nullptr};
+
+  detail::Reference uri_;
+  bool rejects_all_{false};
+  schema::Node const * reference_{nullptr};
+  std::map<std::string, std::unique_ptr<constraint::Constraint>> constraints_{};
+
+protected:
+  static Version schema_version(std::string_view url);
+  static Version schema_version(Adapter auto const & json);
+  static Version schema_version(Adapter auto const & json, Version default_version);
+
+public:
+  Node(bool rejects_all = false) : rejects_all_(rejects_all) {}
+  template <Adapter A> Node(ParserContext<A> context);
+
+  bool is_pure_reference() const { return reference_ && constraints_.empty() && not default_; }
+};
+
+inline Version Node::schema_version(std::string_view url) {
+  static std::map<std::string_view, Version> const g_schema_ids{
+      {"http://json-schema.org/draft-04/schema", Version::Draft04},
+      {"http://json-schema.org/draft-06/schema", Version::Draft06},
+      {"http://json-schema.org/draft-07/schema", Version::Draft07},
+      {"http://json-schema.org/draft/2019-09/schema", Version::Draft2019_09},
+      {"http://json-schema.org/draft/2020-12/schema", Version::Draft2020_12},
+  };
+
+  if (url.ends_with('#')) {
+    url.remove_suffix(1);
+  }
+
+  auto it = g_schema_ids.find(url);
+  EXPECT_T(it != g_schema_ids.end(), std::invalid_argument, url);
+  return it->second;
+}
+
+Version Node::schema_version(Adapter auto const & json) {
+  EXPECT(json.type() == adapter::Type::Object);
+  EXPECT(json.as_object().contains("$schema"));
+
+  auto const & schema = json.as_object()["$schema"];
+  EXPECT(schema.type() == adapter::Type::String);
+
+  return schema_version(schema.as_string());
+}
+
+Version Node::schema_version(Adapter auto const & json, Version default_version) {
+  RETURN_UNLESS(json.type() == adapter::Type::Object, default_version);
+  RETURN_UNLESS(json.as_object().contains("$schema"), default_version);
+
+  auto const & schema = json.as_object()["$schema"];
+  RETURN_UNLESS(schema.type() == adapter::Type::String, default_version);
+
+  return schema_version(schema.as_string());
+}
+}
+
+namespace jvalidate {
+
+class Schema : public schema::Node {
+private:
+  friend class schema::Node;
+  template <Adapter A> friend class ParserContext;
+
+private:
+  schema::Node accept_{true};
+  schema::Node reject_{false};
+  std::map<std::string, detail::Reference> anchors_;
+  std::map<detail::Reference, schema::Node> cache_;
+
+  std::map<detail::Reference, schema::Node const *> alias_cache_;
+
+public:
+  explicit Schema(Adapter auto const & json) : Schema(json, schema_version(json)) {}
+
+  template <Adapter A>
+  Schema(A const & json, schema::Version version)
+      : schema::Node(ParserContext<A>{.root = *this, .schema = json, .version = version}) {}
+
+private:
+  void anchor(std::string anchor, detail::Reference const & from) {}
+
+  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);
+    return schema;
+  }
+
+  template <Adapter A>
+  schema::Node const * resolve(detail::Reference ref, detail::Reference const & from,
+                               schema::Version default_version) {
+    // Special case if the root-level document does not have an $id property
+    if (ref == detail::Reference() && ref.anchor() == from.anchor()) {
+      return this;
+    }
+
+    if (auto it = anchors_.find(ref.anchor()); it != anchors_.end()) {
+      ref = it->second / ref.pointer();
+    }
+    EXPECT_M(ref.anchor().back() != '#', "Unmatched anchor: " << ref.anchor());
+
+    if (auto it = alias_cache_.find(ref); it != alias_cache_.end()) {
+      return it->second;
+    }
+    throw;
+  }
+
+  template <Adapter A> schema::Node const * fetch_schema(ParserContext<A> const & context) {
+    adapter::Type const type = context.schema.type();
+    if (type == adapter::Type::Boolean) {
+      // TODO: Legal Context...
+      return alias(context.where, context.schema.as_boolean() ? &accept_ : &reject_);
+    }
+
+    EXPECT(type == adapter::Type::Object);
+    if (context.schema.object_size() == 0) {
+      return alias(context.where, &accept_);
+    }
+
+    auto [it, created] = cache_.try_emplace(context.where, context);
+    EXPECT_M(created, "more than one schema found with uri " << context.where);
+
+    schema::Node const * node = &it->second;
+    // Special Case - if the only is the reference constraint, then we don't need
+    // to store it uniquely. Draft2019_09 supports directly extending a $ref schema
+    // in the same schema, instead of requiring an allOf clause.
+    if (node->is_pure_reference()) {
+      cache_.erase(it);
+      return alias(context.where, node);
+    }
+
+    return alias(context.where, node);
+  }
+};
+
+template <Adapter A> schema::Node const * ParserContext<A>::resolve(std::string_view uri) const {
+  return root.resolve<A>(detail::Reference(uri), where, version);
+}
+
+template <Adapter A> schema::Node const * ParserContext<A>::node() const {
+  return root.fetch_schema(*this);
+}
+}
+
+namespace jvalidate::schema {
+template <Adapter A> Node::Node(ParserContext<A> context) {
+  EXPECT(context.schema.type() == adapter::Type::Object);
+
+  auto const schema = context.schema.as_object();
+
+  if (schema.contains("$schema")) {
+    // At any point in the schema, we're allowed to change versions
+    // This means that we're not version-locked to the latest grammar
+    // (which is especially important for some breaking changes)
+    context.version = schema_version(context.schema);
+  }
+
+  if (schema.contains("$id")) {
+    context.root.alias(detail::Reference(schema["$id"].as_string(), false), this);
+  }
+
+  // TODO(sjaffe): $recursiveAnchor, $dynamicAnchor, $recursiveRef, $dynamicRef
+  if (schema.contains("$anchor")) {
+    // Create an anchor mapping using the current document and the anchor
+    // string. There's no need for special validation/chaining here, because
+    // {@see Schema::resolve} will turn all $ref/$dynamicRef anchors into
+    // their fully-qualified path.
+    context.root.anchor(context.where.anchor() + schema["$anchor"].as_string(), context.where);
+  }
+
+  bool has_reference;
+  if ((has_reference = schema.contains("$ref"))) {
+    auto ref = schema["$ref"];
+    EXPECT(ref.type() == adapter::Type::String);
+    reference_ = context.resolve(ref.as_string());
+  }
+
+  if (schema.contains("default")) {
+    default_ = schema["default"].freeze();
+  }
+
+  if (schema.contains("description")) {
+    description_ = schema["description"].as_string();
+  }
+
+  for (auto [key, subschema] : schema) {
+    // Using a constraint store allows overriding certain rules, or the creation
+    // of user-defined extention vocabularies.
+    // TODO(sjaffe): Pass this around instead
+    if (auto make_constraint = ConstraintFactory<A>()(key, context.version)) {
+      EXPECT_M(not has_reference || context.version >= Version::Draft2019_09,
+               "Cannot directly extend $ref schemas before Draft2019-09");
+      // A constraint may return null if it is not applicable - but otherwise
+      // well-formed. For example, before Draft-06 "exclusiveMaximum" was a
+      // modifier property for "maximum", and not a unique constaint on its own.
+      // Therefore, we parse it alongside parsing "maximum", and could return
+      // nullptr when requesting a constraint pointer for "exclusiveMaximum".
+      if (auto constraint = make_constraint(context.child(subschema, key))) {
+        constraints_.emplace(key, std::move(constraint));
+      }
+    }
+  }
+}
+}