Browse Source

refactor: separate annotation/error

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

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

@@ -62,6 +62,14 @@ public:
     return document;
   }
 
+  std::string back() const {
+    struct {
+      std::string operator()(std::string const & in) const { return in; }
+      std::string operator()(size_t in) const { return std::to_string(in); }
+    } g_as_str;
+    return tokens_.empty() ? "" : std::visit(g_as_str, tokens_.back());
+  }
+
   bool empty() const { return tokens_.empty(); }
 
   bool starts_with(Pointer const & other) const {

+ 96 - 25
include/jvalidate/validation_result.h

@@ -3,6 +3,7 @@
 #include <map>
 #include <ostream>
 #include <unordered_set>
+#include <variant>
 #include <vector>
 
 #include <jvalidate/detail/pointer.h>
@@ -12,56 +13,126 @@ namespace jvalidate {
 class ValidationResult {
 public:
   template <Adapter A, RegexEngine RE> friend class ValidationVisitor;
+  using DocPointer = detail::Pointer;
+  using SchemaPointer = detail::Pointer;
+  using Annotation = std::variant<std::string, std::vector<std::string>>;
+  struct LocalResult {
+    /* bool valid; */
+    std::map<std::string, Annotation> errors;
+    std::map<std::string, Annotation> annotations;
+  };
+
+  struct indent {
+    indent(int i) : i(i) {}
+
+    friend std::ostream & operator<<(std::ostream & os, indent id) {
+      while (id.i-- > 0)
+        os << "  ";
+      return os;
+    }
+
+    int i;
+  };
 
 private:
-  std::map<detail::Pointer, std::map<detail::Pointer, std::string>> annotations_;
+  std::map<DocPointer, std::map<SchemaPointer, LocalResult>> results_;
 
 public:
   friend std::ostream & operator<<(std::ostream & os, ValidationResult const & result) {
-    for (auto const & [where, annotations] : result.annotations_) {
-      if (annotations.size() == 1) {
-        auto const & [schema_path, message] = *annotations.begin();
-        std::cout << where << ": " << schema_path << ": " << message << "\n";
-      } else {
-        std::cout << where << "\n";
-        for (auto const & [schema_path, message] : annotations) {
-          std::cout << "  " << schema_path << ": " << message << "\n";
+    os << "{\n" << indent(1) << R"("details": [)" << '\n';
+    for (auto const & [doc_path, by_schema] : result.results_) {
+      for (auto const & [schema_path, local] : by_schema) {
+        os << indent(2) << '{' << '\n';
+        os << indent(3) << R"("evaluationPath": ")" << schema_path << '"' << ',' << '\n';
+        os << indent(3) << R"("instanceLocation": ")" << doc_path << '"';
+        print(os, local.annotations, "annotations", 3);
+        print(os, local.errors, "errors", 3);
+        os << '\n' << indent(2) << '}' << '\n';
+      }
+    }
+    return os << indent(1) << ']' << '\n' << '}';
+  }
+
+  static void print(std::ostream & os, std::map<std::string, Annotation> const & named,
+                    std::string_view name, int const i) {
+    if (named.empty()) {
+      return;
+    }
+    os << ',' << '\n';
+    os << indent(i) << '"' << name << '"' << ':' << ' ' << '{' << '\n';
+    for (auto const & [key, anno] : named) {
+      os << indent(i + 1) << '"' << key << '"' << ':' << ' ';
+      if (auto const * str = std::get_if<0>(&anno)) {
+        os << '"' << *str << '"';
+      } else if (auto const * vec = std::get_if<1>(&anno)) {
+        os << '[';
+        char const * div = "\n";
+        for (size_t i = 0; i < vec->size(); ++i) {
+          os << std::exchange(div, ",\n") << indent(i + 2) << '"' << vec->at(i) << '"';
         }
+        os << indent(i + 1) << ']';
       }
+      os << '\n';
     }
-    return os;
+    os << indent(i) << '}';
   }
 
-  bool has_annotation(detail::Pointer const & where, detail::Pointer const & schema_path) const {
-    return annotation(where, schema_path) != nullptr;
+  bool has(detail::Pointer const & where, detail::Pointer const & schema_path) const {
+    return has(where) && results_.at(where).contains(schema_path);
   }
 
-  bool has_annotation(detail::Pointer const & where) const { return annotations_.contains(where); }
+  bool has(detail::Pointer const & where) const { return results_.contains(where); }
 
-  std::string const * annotation(detail::Pointer const & where,
-                                 detail::Pointer const & schema_path) const {
-    if (not annotations_.contains(where)) {
+  Annotation const * annotation(detail::Pointer const & where, detail::Pointer const & schema_path,
+                                std::string const & name) const {
+    if (not results_.contains(where)) {
+      return nullptr;
+    }
+    auto const & by_schema = results_.at(where);
+    if (not by_schema.contains(schema_path)) {
       return nullptr;
     }
-    auto const & anno = annotations_.at(where);
-    if (not anno.contains(schema_path)) {
+    auto const & local = by_schema.at(schema_path);
+    if (not local.annotations.contains(name)) {
       return nullptr;
     }
-    return &anno.at(schema_path);
+    return &local.annotations.at(name);
+  }
+
+  Annotation const * error(detail::Pointer const & where, detail::Pointer const & schema_path,
+                           std::string const & name) const {
+    if (not results_.contains(where)) {
+      return nullptr;
+    }
+    auto const & by_schema = results_.at(where);
+    if (not by_schema.contains(schema_path)) {
+      return nullptr;
+    }
+    auto const & local = by_schema.at(schema_path);
+    if (not local.errors.contains(name)) {
+      return nullptr;
+    }
+    return &local.errors.at(name);
   }
 
 private:
-  void annotate(ValidationResult && result) {
-    for (auto const & [where, errors] : result.annotations_) {
-      for (auto const & [schema_path, message] : errors) {
-        annotations_[where].emplace(schema_path, message);
+  void merge(ValidationResult && result) {
+    for (auto && [where, by_schema] : result.results_) {
+      for (auto && [schema_path, local] : by_schema) {
+        results_[where][schema_path].annotations.merge(local.annotations);
+        results_[where][schema_path].errors.merge(local.errors);
       }
     }
   }
 
+  void error(detail::Pointer const & where, detail::Pointer const & schema_path,
+             std::string const & name, Annotation message) {
+    results_[where][schema_path].errors.emplace(name, std::move(message));
+  }
+
   void annotate(detail::Pointer const & where, detail::Pointer const & schema_path,
-                std::string const & message) {
-    annotations_[where].emplace(schema_path, message);
+                std::string const & name, Annotation message) {
+    results_[where][schema_path].annotations.emplace(name, std::move(message));
   }
 };
 }

+ 90 - 70
include/jvalidate/validation_visitor.h

@@ -1,6 +1,7 @@
 #pragma once
 
 #include <tuple>
+#include <type_traits>
 #include <unordered_map>
 
 #include <jvalidate/constraint/array_constraint.h>
@@ -62,19 +63,20 @@ public:
 
   Status visit(constraint::TypeConstraint const & cons) const {
     adapter::Type const type = document_.type();
+
     for (adapter::Type const accept : cons.types) {
       if (type == accept) {
-        return note(Status::Accept, "type (", type, ") is one of [", cons.types, "]");
+        return result(Status::Accept, type, " is in types [", cons.types, "]");
       }
       if (accept == adapter::Type::Number && type == adapter::Type::Integer) {
-        return note(Status::Accept, "type (", type, ") is one of [", cons.types, "]");
+        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())) {
-        return note(Status::Accept, "type (", type, ") is one of [", cons.types, "]");
+        return result(Status::Accept, type, " is in types [", cons.types, "]");
       }
     }
-    return note(Status::Reject, "type (", type, ") is not one of [", cons.types, "]");
+    return result(Status::Reject, type, " is not in types [", cons.types, "]");
   }
 
   Status visit(constraint::ExtensionConstraint const & cons) const {
@@ -87,10 +89,10 @@ public:
     };
     for (auto const & [index, option] : detail::enumerate(cons.enumeration)) {
       if (option->apply(is_equal)) {
-        return note(Status::Accept, "value is enum ", index);
+        return result(Status::Accept, index);
       }
     }
-    return note(Status::Reject, "value is none of the enums");
+    return Status::Reject;
   }
 
   Status visit(constraint::AllOfConstraint const & cons) const {
@@ -106,9 +108,9 @@ public:
     }
 
     if (rval == Status::Reject) {
-      return note(rval, "does not validate subschemas ", unmatched);
+      return result(rval, "does not validate subschemas ", unmatched);
     }
-    return note(rval, "validates all subschemas");
+    return result(rval, "validates all subschemas");
   }
 
   Status visit(constraint::AnyOfConstraint const & cons) const {
@@ -123,9 +125,9 @@ public:
     }
 
     if (first_validated.has_value()) {
-      return note(Status::Accept, "validates subschema ", *first_validated);
+      return result(Status::Accept, "validates subschema ", *first_validated);
     }
-    return note(Status::Reject, "validates none of the subschemas");
+    return result(Status::Reject, "validates none of the subschemas");
   }
 
   Status visit(constraint::OneOfConstraint const & cons) const {
@@ -139,9 +141,9 @@ public:
     }
 
     if (matches.size() == 1) {
-      return note(Status::Accept, "validates subschema ", *matches.begin());
+      return result(Status::Accept, "validates subschema ", *matches.begin());
     }
-    return note(Status::Reject, "validates multiple subschemas ", matches);
+    return result(Status::Reject, "validates multiple subschemas ", matches);
   }
 
   Status visit(constraint::NotConstraint const & cons) const {
@@ -149,9 +151,6 @@ public:
     scoped_state(tracking_, !tracking_);
     bool const rejected = validate_subschema(cons.child) == Status::Reject;
 
-    if (not rejected) {
-      annotate("actually validates subschema");
-    }
     return rejected;
   }
 
@@ -170,15 +169,15 @@ public:
     switch (document_.type()) {
     case adapter::Type::Integer:
       if (int64_t value = document_.as_integer(); not cons(value)) {
-        return note(Status::Reject, value, cons.exclusive ? " >= " : " > ", cons.value);
+        return result(Status::Reject, value, cons.exclusive ? " >= " : " > ", cons.value);
       } else {
-        return note(Status::Accept, value, cons.exclusive ? " < " : " <= ", cons.value);
+        return result(Status::Accept, value, cons.exclusive ? " < " : " <= ", cons.value);
       }
     case adapter::Type::Number:
       if (double value = document_.as_number(); not cons(value)) {
-        return note(Status::Reject, value, cons.exclusive ? " >= " : " > ", cons.value);
+        return result(Status::Reject, value, cons.exclusive ? " >= " : " > ", cons.value);
       } else {
-        return note(Status::Accept, value, cons.exclusive ? " < " : " <= ", cons.value);
+        return result(Status::Accept, value, cons.exclusive ? " < " : " <= ", cons.value);
       }
     default:
       return Status::Noop;
@@ -189,15 +188,15 @@ public:
     switch (document_.type()) {
     case adapter::Type::Integer:
       if (int64_t value = document_.as_integer(); not cons(value)) {
-        return note(Status::Reject, value, cons.exclusive ? " <= " : " < ", cons.value);
+        return result(Status::Reject, value, cons.exclusive ? " <= " : " < ", cons.value);
       } else {
-        return note(Status::Accept, value, cons.exclusive ? " > " : " >= ", cons.value);
+        return result(Status::Accept, value, cons.exclusive ? " > " : " >= ", cons.value);
       }
     case adapter::Type::Number:
       if (double value = document_.as_number(); not cons(value)) {
-        return note(Status::Reject, value, cons.exclusive ? " <= " : " < ", cons.value);
+        return result(Status::Reject, value, cons.exclusive ? " <= " : " < ", cons.value);
       } else {
-        return note(Status::Accept, value, cons.exclusive ? " > " : " >= ", cons.value);
+        return result(Status::Accept, value, cons.exclusive ? " > " : " >= ", cons.value);
       }
     default:
       return Status::Noop;
@@ -209,9 +208,9 @@ public:
     RETURN_UNLESS(type == adapter::Type::Number || type == adapter::Type::Integer, Status::Noop);
 
     if (double value = document_.as_number(); not cons(value)) {
-      return note(Status::Reject, value, " is not a multiple of ", cons.value);
+      return result(Status::Reject, value, " is not a multiple of ", cons.value);
     } else {
-      return note(Status::Accept, value, " is a multiple of ", cons.value);
+      return result(Status::Accept, value, " is a multiple of ", cons.value);
     }
   }
 
@@ -219,9 +218,9 @@ public:
     NOOP_UNLESS_TYPE(String);
     std::string const str = document_.as_string();
     if (int64_t len = detail::length(str); len > cons.value) {
-      return note(Status::Reject, "'", str, "' of length ", len, " is >", cons.value);
+      return result(Status::Reject, "'", str, "' of length ", len, " is >", cons.value);
     } else {
-      return note(Status::Accept, "'", str, "' of length ", len, " is <=", cons.value);
+      return result(Status::Accept, "'", str, "' of length ", len, " is <=", cons.value);
     }
   }
 
@@ -229,9 +228,9 @@ public:
     NOOP_UNLESS_TYPE(String);
     std::string const str = document_.as_string();
     if (int64_t len = detail::length(str); len < cons.value) {
-      return note(Status::Reject, "'", str, "' of length ", len, " is <", cons.value);
+      return result(Status::Reject, "'", str, "' of length ", len, " is <", cons.value);
     } else {
-      return note(Status::Accept, "'", str, "' of length ", len, " is >=", cons.value);
+      return result(Status::Accept, "'", str, "' of length ", len, " is >=", cons.value);
     }
   }
 
@@ -241,9 +240,9 @@ public:
     RE const & regex = regex_cache_.try_emplace(cons.regex, cons.regex).first->second;
     std::string const str = document_.as_string();
     if (regex.search(str)) {
-      return note(Status::Accept, "'", str, "' matches pattern /", cons.regex, "/");
+      return result(Status::Accept, "'", str, "' matches pattern /", cons.regex, "/");
     }
-    return note(Status::Reject, "'", str, "' does not match pattern /", cons.regex, "/");
+    return result(Status::Reject, "'", str, "' does not match pattern /", cons.regex, "/");
   }
 
   Status visit(constraint::FormatConstraint const & cons) const {
@@ -251,12 +250,12 @@ public:
     NOOP_UNLESS_TYPE(String);
 
     // TODO(samjaffe): annotate(cons.format)
-    annotate("format '", cons.format, "'");
+    annotate(cons.format);
     if (not cfg_.validate_format && not cons.is_assertion) {
       return true;
     }
 
-    return note(Status::Reject, " is unimplemented");
+    return result(Status::Reject, " is unimplemented");
   }
 
   Status visit(constraint::AdditionalItemsConstraint const & cons) const {
@@ -287,10 +286,10 @@ public:
     }
 
     if (matches < minimum) {
-      return note(Status::Reject, "array contains <", minimum, " matching elements");
+      return result(Status::Reject, "array contains <", minimum, " matching elements");
     }
     if (matches > maximum) {
-      return note(Status::Reject, "array contains >", maximum, " matching elements");
+      return result(Status::Reject, "array contains >", maximum, " matching elements");
     }
     return Status::Accept;
   }
@@ -298,18 +297,18 @@ public:
   Status visit(constraint::MaxItemsConstraint const & cons) const {
     NOOP_UNLESS_TYPE(Array);
     if (size_t size = document_.array_size(); size > cons.value) {
-      return note(Status::Reject, "array of size ", size, " is >", cons.value);
+      return result(Status::Reject, "array of size ", size, " is >", cons.value);
     } else {
-      return note(Status::Accept, "array of size ", size, " is <=", cons.value);
+      return result(Status::Accept, "array of size ", size, " is <=", cons.value);
     }
   }
 
   Status visit(constraint::MinItemsConstraint const & cons) const {
     NOOP_UNLESS_TYPE(Array);
     if (size_t size = document_.array_size(); size < cons.value) {
-      return note(Status::Reject, "array of size ", size, " is <", cons.value);
+      return result(Status::Reject, "array of size ", size, " is <", cons.value);
     } else {
-      return note(Status::Accept, "array of size ", size, " is >=", cons.value);
+      return result(Status::Accept, "array of size ", size, " is >=", cons.value);
     }
   }
 
@@ -335,7 +334,7 @@ public:
       std::map<A, size_t> cache;
       for (auto const & [index, elem] : detail::enumerate(document_.as_array())) {
         if (auto [it, created] = cache.emplace(elem, index); not created) {
-          return note(Status::Reject, "items ", it->second, " and ", index, " are equal");
+          return result(Status::Reject, "items ", it->second, " and ", index, " are equal");
         }
       }
     } else {
@@ -343,13 +342,13 @@ public:
       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)) {
-            return note(Status::Reject, "items ", i, " and ", j, " are equal");
+            return result(Status::Reject, "items ", i, " and ", j, " are equal");
           }
         }
       }
     }
 
-    return note(Status::Accept, "all array items are unique");
+    return result(Status::Accept, "all array items are unique");
   }
 
   Status visit(constraint::AdditionalPropertiesConstraint const & cons) const {
@@ -409,18 +408,18 @@ public:
   Status visit(constraint::MaxPropertiesConstraint const & cons) const {
     NOOP_UNLESS_TYPE(Object);
     if (size_t size = document_.object_size(); size > cons.value) {
-      return note(Status::Reject, "object of size ", size, " is >", cons.value);
+      return result(Status::Reject, "object of size ", size, " is >", cons.value);
     } else {
-      return note(Status::Accept, "object of size ", size, " is <=", cons.value);
+      return result(Status::Accept, "object of size ", size, " is <=", cons.value);
     }
   }
 
   Status visit(constraint::MinPropertiesConstraint const & cons) const {
     NOOP_UNLESS_TYPE(Object);
     if (size_t size = document_.object_size(); size < cons.value) {
-      return note(Status::Reject, "object of size ", size, " is <", cons.value);
+      return result(Status::Reject, "object of size ", size, " is <", cons.value);
     } else {
-      return note(Status::Accept, "object of size ", size, " is >=", cons.value);
+      return result(Status::Accept, "object of size ", size, " is >=", cons.value);
     }
   }
 
@@ -487,10 +486,10 @@ public:
     }
 
     if (required.empty()) {
-      return note(Status::Accept, "contains all required properties ", cons.properties);
+      return result(Status::Accept, "contains all required properties ", cons.properties);
     }
 
-    return note(Status::Reject, "missing required properties ", required);
+    return result(Status::Reject, "missing required properties ", required);
   }
 
   Status visit(constraint::UnevaluatedItemsConstraint const & cons) const {
@@ -530,7 +529,9 @@ public:
 
   Status validate() {
     if (std::optional<std::string> const & reject = schema_->rejects_all()) {
-      annotate(*reject);
+      if (should_annotate(Status::Reject) && result_) {
+        result_->error(where_, schema_path_, "", *reject);
+      }
       return Status::Reject;
     }
 
@@ -566,36 +567,55 @@ public:
   }
 
 private:
-  template <typename... Args> void annotate(Args &&... args) const {
-    if (not result_) {
-      return;
-    }
+  template <typename S>
+  requires(std::is_constructible_v<std::string, S>) static std::string fmt(S const & str) {
+    return str;
+  }
+
+  static std::string fmt(auto const &... args) {
     std::stringstream ss;
     using ::jvalidate::operator<<;
-    [[maybe_unused]] int _[] = {(ss << std::forward<Args>(args), 0)...};
-    result_->annotate(where_, schema_path_, ss.str());
+    [[maybe_unused]] int _[] = {(ss << args, 0)...};
+    return ss.str();
+  }
+
+  static std::vector<std::string> fmtlist(auto const & arg) {
+    std::vector<std::string> strs;
+    for (auto const & elem : arg) {
+      strs.push_back(fmt(elem));
+    }
+    return strs;
   }
 
-  template <typename... Args> Status note(Status stat, Args &&... args) const {
+  bool should_annotate(Status stat) const {
     switch (tracking_) {
     case StoreResults::ForAnything:
-      if (stat != Status::Noop) {
-        annotate(std::forward<Args>(args)...);
-      }
-      break;
+      return stat != Status::Noop;
     case StoreResults::ForValid:
-      if (stat == Status::Accept) {
-        annotate(std::forward<Args>(args)...);
-      }
-      break;
+      return stat == Status::Accept;
     case StoreResults::ForInvalid:
-      if (stat == Status::Reject) {
-        annotate(std::forward<Args>(args)...);
-      }
-      break;
+      return stat == Status::Reject;
     }
+  }
+
+#define ANNOTATION_HELPER(name, ADD, FMT)                                                          \
+  void name(auto const &... args) const {                                                          \
+    if (not result_) {                                                                             \
+      return;                                                                                      \
+    }                                                                                              \
+    if (schema_path_.empty()) {                                                                    \
+      result_->ADD(where_, schema_path_, "", FMT(args...));                                        \
+    } else {                                                                                       \
+      result_->ADD(where_, schema_path_.parent(), schema_path_.back(), FMT(args...));              \
+    }                                                                                              \
+  }
+
+  ANNOTATION_HELPER(error, error, fmt)
+  ANNOTATION_HELPER(annotate, annotate, fmt)
+  ANNOTATION_HELPER(annotate_list, annotate, fmtlist)
 
-    return stat;
+  Status result(Status stat, auto const &... args) const {
+    return (should_annotate(stat) ? error(args...) : void(), stat);
   }
 
   template <typename C> static void merge_visited(C & to, C const & from) {
@@ -645,7 +665,7 @@ private:
       VISITED(K).insert(key);
     }
     if (status == Status::Reject and result_) {
-      result_->annotate(std::move(result));
+      result_->merge(std::move(result));
     }
     return status;
   }

+ 33 - 11
tests/annotation_test.cxx

@@ -47,14 +47,36 @@ auto validate(Json::Value const & schema_doc, Json::Value const & instance_doc,
   return result;
 }
 
-MATCHER_P(HasAnnotationsFor, doc_path, "") { return arg.has_annotation(doc_path); }
+MATCHER_P(HasAnnotationsFor, doc_path, "") { return arg.has(doc_path); }
 
-MATCHER_P2(HasAnnotationAt, doc_path, schema_path, "") {
-  return arg.has_annotation(doc_path, schema_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_P3(AnnotationAt, doc_path, schema_path, matcher, "") {
-  auto const * anno = arg.annotation(doc_path, schema_path);
+MATCHER_P4(ErrorAt, doc_path, schema_path, key, matcher, "") {
+  auto const * anno = arg.error(doc_path, schema_path, key);
   if (not anno) {
     return false;
   }
@@ -70,7 +92,7 @@ TEST(Annotation, AttachesFormattingAnnotation) {
 
   jvalidate::ValidationResult result = validate(schema, instance);
 
-  EXPECT_THAT(result, AnnotationAt(""_jptr, "/format"_jptr, "format 'uri'"));
+  EXPECT_THAT(result, AnnotationAt("format", "uri"));
 }
 
 TEST(Annotation, AnnotatesErrors) {
@@ -82,7 +104,7 @@ TEST(Annotation, AnnotatesErrors) {
 
   jvalidate::ValidationResult result = validate(schema, instance);
 
-  EXPECT_THAT(result, AnnotationAt(""_jptr, "/minimum"_jptr, "4 < 5"));
+  EXPECT_THAT(result, ErrorAt("minimum", "4 < 5"));
 }
 
 TEST(Annotation, DoesNotAnnotatesValid) {
@@ -106,7 +128,7 @@ TEST(Annotation, NotSchemaFlipsAnnotationRule) {
 
   jvalidate::ValidationResult result = validate(schema, instance);
 
-  EXPECT_THAT(result, AnnotationAt(""_jptr, "/not/minimum"_jptr, "6 >= 5"));
+  EXPECT_THAT(result, ErrorAt(""_jptr, "/not"_jptr, "minimum", "6 >= 5"));
 }
 
 TEST(Annotation, PathFollowsSchemaNotConstraintModel) {
@@ -119,7 +141,7 @@ TEST(Annotation, PathFollowsSchemaNotConstraintModel) {
 
   jvalidate::ValidationResult result = validate(schema, instance, Draft03);
 
-  EXPECT_THAT(result, AnnotationAt(""_jptr, "/disallow"_jptr, "type (string) is one of [string]"));
+  EXPECT_THAT(result, ErrorAt("disallow", "string is in types [string]"));
 }
 
 TEST(Annotation, SomeConstraintsAnnotateBothValidAndInvalid) {
@@ -135,8 +157,8 @@ TEST(Annotation, SomeConstraintsAnnotateBothValidAndInvalid) {
   jvalidate::ValidationResult result = validate(schema, instance);
 
   EXPECT_THAT(result, Not(HasAnnotationAt(""_jptr, "/oneOf"_jptr)));
-  EXPECT_THAT(result, AnnotationAt(""_jptr, "/oneOf/0/minimum"_jptr, "-1 < 10"));
-  EXPECT_THAT(result, AnnotationAt(""_jptr, "/oneOf/1/maximum"_jptr, "-1 <= 0"));
+  EXPECT_THAT(result, ErrorAt(""_jptr, "/oneOf/0"_jptr, "minimum", "-1 < 10"));
+  EXPECT_THAT(result, ErrorAt(""_jptr, "/oneOf/1"_jptr, "maximum", "-1 <= 0"));
 }
 
 int main(int argc, char ** argv) {