Pārlūkot izejas kodu

refactor: begin developing a more proper validation results

Sam Jaffe 1 gadu atpakaļ
vecāks
revīzija
fa5543434d

+ 81 - 4
include/jvalidate/validation_result.h

@@ -1,17 +1,94 @@
 #pragma once
 
+#include <map>
+#include <ostream>
 #include <unordered_set>
+#include <vector>
 
 #include <jvalidate/forward.h>
 
 namespace jvalidate {
 class ValidationResult {
 public:
-  std::unordered_set<std::string> visited_properties;
-  std::unordered_set<size_t> visited_items;
+  struct Errors {
+    std::string constraint;
+    std::string message;
+    std::map<std::string, ValidationResult> properties;
+    std::map<size_t, ValidationResult> items;
+  };
+
+private:
+  std::unordered_set<std::string> visited_properties_;
+  std::unordered_set<size_t> visited_items_;
+
+  std::string message_;
+  std::vector<Errors> errors_;
 
 public:
-  void record(size_t item) { visited_items.insert(item); }
-  void record(std::string const & property) { visited_properties.insert(property); }
+  void constraint(std::string const & name) { errors_.push_back({name}); }
+  void message(std::string const & message) {
+    (errors_.empty() ? message_ : errors_.back().message) = message;
+  }
+
+  void error(size_t item, ValidationResult && result) {
+    errors_.back().items.emplace(item, std::move(result));
+    visited_items_.emplace(item);
+  }
+
+  void error(std::string const & property, ValidationResult && result) {
+    errors_.back().properties.emplace(property, std::move(result));
+    visited_properties_.emplace(property);
+  }
+
+  void visit(size_t item) { visited_items_.emplace(item); }
+  bool has_visited(size_t item) const { return visited_items_.contains(item); }
+
+  void visit(std::string const & property) { visited_properties_.emplace(property); }
+  bool has_visited(std::string const & property) const {
+    return visited_properties_.contains(property);
+  }
+
+  friend std::ostream & operator<<(std::ostream & os, ValidationResult const & result) {
+    result.print(os, 0);
+    return os;
+  }
+
+private:
+  static void indent(std::ostream & os, int depth) {
+    for (int i = 0; i < depth; ++i) {
+      os << ' ' << ' ';
+    }
+  }
+
+  void print(std::ostream & os, int depth) const {
+    if (not message_.empty()) {
+      indent(os, depth);
+      os << message_ << "\n";
+    }
+
+    for (auto const & error : errors_) {
+      if (error.items.empty() and error.properties.empty()) {
+        continue;
+      }
+
+      indent(os, depth);
+      os << "for constraint '" << error.constraint << "'";
+      if (not error.message.empty()) {
+        os << ": " << error.message;
+      }
+      os << "\n";
+
+      for (auto const & [i, r] : error.items) {
+        indent(os, depth + 1);
+        os << "at " << i << ":\n";
+        r.print(os, depth + 2);
+      }
+      for (auto const & [i, r] : error.properties) {
+        indent(os, depth + 1);
+        os << "at '" << i << "':\n";
+        r.print(os, depth + 2);
+      }
+    }
+  }
 };
 }

+ 40 - 37
include/jvalidate/validation_visitor.h

@@ -18,6 +18,13 @@
 #define NOOP_UNLESS_TYPE(etype)                                                                    \
   RETURN_UNLESS(document_.type() == adapter::Type::etype, Status::Noop)
 
+#define BREAK_EARLY_IF_NO_RESULT_TREE()                                                            \
+  do {                                                                                             \
+    if (rval == Status::Reject and not result_) {                                                  \
+      break;                                                                                       \
+    }                                                                                              \
+  } while (false)
+
 namespace jvalidate {
 template <Adapter A, RegexEngine RE>
 class ValidationVisitor : public constraint::ConstraintVisitor {
@@ -69,9 +76,7 @@ public:
 
     for (schema::Node const * subschema : cons.children) {
       rval &= validate_subschema(subschema);
-      if (!rval && result_ == nullptr) {
-        break;
-      }
+      BREAK_EARLY_IF_NO_RESULT_TREE();
     }
 
     return rval;
@@ -161,9 +166,7 @@ public:
     Status rval = Status::Accept;
     for (size_t i = cons.applies_after_nth; i < array.size(); ++i) {
       rval &= validate_subschema_on(cons.subschema, array[i], i);
-      if (rval == Status::Reject && result_ == nullptr) {
-        break;
-      }
+      BREAK_EARLY_IF_NO_RESULT_TREE();
     }
 
     return rval;
@@ -210,9 +213,7 @@ public:
     size_t const n = std::min(cons.items.size(), array.size());
     for (size_t i = 0; i < n; ++i) {
       rval &= validate_subschema_on(cons.items[i], array[i], i);
-      if (rval == Status::Reject && result_ == nullptr) {
-        break;
-      }
+      BREAK_EARLY_IF_NO_RESULT_TREE();
     }
 
     return rval;
@@ -260,9 +261,7 @@ public:
       if (not cons.properties.contains(key) && not matches_any_pattern(key)) {
         rval &= validate_subschema_on(cons.subschema, elem, key);
       }
-      if (rval == Status::Reject && result_ == nullptr) {
-        break;
-      }
+      BREAK_EARLY_IF_NO_RESULT_TREE();
     }
 
     return rval;
@@ -279,9 +278,7 @@ public:
       }
 
       rval &= validate_subschema(subschema);
-      if (rval == Status::Reject && result_ == nullptr) {
-        break;
-      }
+      BREAK_EARLY_IF_NO_RESULT_TREE();
     }
 
     for (auto [key, required] : cons.required) {
@@ -294,9 +291,7 @@ public:
       }
 
       rval &= required.empty();
-      if (rval == Status::Reject && result_ == nullptr) {
-        break;
-      }
+      BREAK_EARLY_IF_NO_RESULT_TREE();
     }
 
     return rval;
@@ -322,9 +317,7 @@ public:
         if (regex.search(key)) {
           rval &= validate_subschema_on(subschema, elem, key);
         }
-        if (rval == Status::Reject && result_ == nullptr) {
-          break;
-        }
+        BREAK_EARLY_IF_NO_RESULT_TREE();
       }
     }
 
@@ -350,9 +343,7 @@ public:
       if (auto it = cons.properties.find(key); it != cons.properties.end()) {
         rval &= validate_subschema_on(it->second, elem, key);
       }
-      if (rval == Status::Reject && result_ == nullptr) {
-        break;
-      }
+      BREAK_EARLY_IF_NO_RESULT_TREE();
     }
 
     return rval;
@@ -388,12 +379,10 @@ public:
     Status rval = Status::Accept;
     auto array = document_.as_array();
     for (size_t i = 0; i < array.size(); ++i) {
-      if (not local_result_->visited_items.contains(i)) {
+      if (not local_result_->has_visited(i)) {
         rval &= validate_subschema_on(cons.subschema, array[i], i);
       }
-      if (rval == Status::Reject && result_ == nullptr) {
-        break;
-      }
+      BREAK_EARLY_IF_NO_RESULT_TREE();
     }
   }
 
@@ -403,12 +392,10 @@ public:
 
     Status rval = Status::Accept;
     for (auto const & [key, elem] : document_.as_object()) {
-      if (not local_result_->visited_properties.contains(key)) {
+      if (not local_result_->has_visited(key)) {
         rval &= validate_subschema_on(cons.subschema, elem, key);
       }
-      if (rval == Status::Reject && result_ == nullptr) {
-        break;
-      }
+      BREAK_EARLY_IF_NO_RESULT_TREE();
     }
   }
 
@@ -429,21 +416,34 @@ public:
     }
 
     for (auto const & [key, p_constraint] : schema_.constraints()) {
-      if (rval != Status::Reject || result_) {
-        rval &= p_constraint->accept(*this);
+      BREAK_EARLY_IF_NO_RESULT_TREE();
+      if (result_) {
+        result_->constraint(key);
       }
+      rval &= p_constraint->accept(*this);
     }
 
     for (auto const & [key, p_constraint] : schema_.post_constraints()) {
-      if (rval != Status::Reject || result_) {
-        rval &= p_constraint->accept(*this);
+      BREAK_EARLY_IF_NO_RESULT_TREE();
+      if (result_) {
+        result_->constraint(key);
       }
+      rval &= p_constraint->accept(*this);
     }
 
     return rval;
   }
 
 private:
+  template <typename... Args> void add_error(Args &&... args) const {
+    if (not result_) {
+      return;
+    }
+    std::stringstream ss;
+    ss << (std::forward<Args>(args) << ...);
+    result_->message(ss.str());
+  }
+
   ValidationVisitor(A const & json, schema::Node const & schema, ValidationConfig const & cfg,
                     std::unordered_map<std::string, RE> & regex_cache,
                     detail::Pointer const & where, ValidationResult * result,
@@ -466,7 +466,10 @@ private:
     auto status =
         ValidationVisitor(document, *subschema, cfg_, regex_cache_, where_ / key, pnext).validate();
     if (status != Status::Noop and local_result_) {
-      local_result_->record(key);
+      local_result_->visit(key);
+    }
+    if (status == Status::Reject and result_) {
+      result_->error(key, std::move(next));
     }
     return status;
   }

+ 7 - 1
tests/json_schema_test_suite.h

@@ -2,6 +2,7 @@
 
 #include <gmock/gmock.h>
 
+#include <jvalidate/validation_result.h>
 #include <jvalidate/validator.h>
 
 inline std::filesystem::path const & JSONSchemaTestSuiteDir() {
@@ -39,7 +40,12 @@ static auto SchemaTestName = [](auto const & info) {
   return name;
 };
 
-MATCHER_P(ValidatesAgainst, schema, "") { return jvalidate::Validator(schema).validate(arg); }
+MATCHER_P(ValidatesAgainst, schema, "") {
+  jvalidate::ValidationResult result;
+  bool valid = jvalidate::Validator(schema).validate(arg, &result);
+  *result_listener << result;
+  return valid;
+}
 
 template <typename T>
 testing::Matcher<T> ValidatesAgainst(jvalidate::Schema const & schema, T const & test) {