Jelajahi Sumber

fix: a number of bugs

Sam Jaffe 1 tahun lalu
induk
melakukan
8bd43eb59a

+ 29 - 5
include/jvalidate/adapter.h

@@ -1,5 +1,6 @@
 #pragma once
 
+#include <cmath>
 #include <cstdint>
 #include <map>
 #include <optional>
@@ -32,6 +33,8 @@ public:
 
   virtual bool equals(Adapter const & lhs, bool strict) const = 0;
 
+  static bool is_integer(double value) { return std::floor(value) == value; }
+
   bool maybe_null(bool strict) const {
     switch (type()) {
     case Type::Null:
@@ -67,11 +70,6 @@ public:
     switch (type()) {
     case Type::Integer:
       return as_integer();
-    case Type::Number: {
-      double number = as_number();
-      int64_t integer = static_cast<int64_t>(number);
-      return static_cast<double>(integer) == number ? std::make_optional(integer) : std::nullopt;
-    }
     case Type::Boolean:
       if (not strict) {
         return as_boolean() ? 1 : 0;
@@ -138,6 +136,32 @@ public:
       return std::nullopt;
     }
   }
+
+  std::optional<size_t> maybe_array_size(bool strict) const {
+    switch (type()) {
+    case Type::Array:
+      return array_size();
+    case Type::Null:
+      return strict ? std::nullopt : std::make_optional(0UL);
+    case Type::Object:
+      return (strict || object_size() != 0) ? std::nullopt : std::make_optional(0UL);
+    default:
+      return std::nullopt;
+    }
+  }
+
+  std::optional<size_t> maybe_object_size(bool strict) const {
+    switch (type()) {
+    case Type::Object:
+      return object_size();
+    case Type::Null:
+      return strict ? std::nullopt : std::make_optional(0UL);
+    case Type::Array:
+      return (strict || array_size() != 0) ? std::nullopt : std::make_optional(0UL);
+    default:
+      return std::nullopt;
+    }
+  }
 };
 
 class Const {

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

@@ -49,6 +49,9 @@ public:
     case Json::booleanValue:
       return Type::Boolean;
     case Json::realValue:
+      if (Adapter::is_integer(as_number())) {
+        return Type::Integer;
+      }
       return Type::Number;
     case Json::stringValue:
       return Type::String;

+ 11 - 7
include/jvalidate/constraint.h

@@ -243,12 +243,14 @@ public:
   // SECTION: String Constraints
 
   static auto minLength(detail::ParserContext<A> const & context) {
-    EXPECT(context.schema.type() == adapter::Type::Integer);
+    EXPECT(context.schema.type() == adapter::Type::Integer ||
+           context.schema.type() == adapter::Type::Number);
     return std::make_unique<constraint::MinLengthConstraint>(context.schema.as_integer());
   }
 
   static auto maxLength(detail::ParserContext<A> const & context) {
-    EXPECT(context.schema.type() == adapter::Type::Integer);
+    EXPECT(context.schema.type() == adapter::Type::Integer ||
+           context.schema.type() == adapter::Type::Number);
     return std::make_unique<constraint::MaxLengthConstraint>(context.schema.as_integer());
   }
 
@@ -355,12 +357,14 @@ public:
   }
 
   static auto minProperties(detail::ParserContext<A> const & context) {
-    EXPECT(context.schema.type() == adapter::Type::Integer);
+    EXPECT(context.schema.type() == adapter::Type::Integer ||
+           context.schema.type() == adapter::Type::Number);
     return std::make_unique<constraint::MinPropertiesConstraint>(context.schema.as_integer());
   }
 
   static auto maxProperties(detail::ParserContext<A> const & context) {
-    EXPECT(context.schema.type() == adapter::Type::Integer);
+    EXPECT(context.schema.type() == adapter::Type::Integer ||
+           context.schema.type() == adapter::Type::Number);
     return std::make_unique<constraint::MaxPropertiesConstraint>(context.schema.as_integer());
   }
 
@@ -425,13 +429,13 @@ public:
     std::map<std::string, schema::Node const *> schemas;
     std::map<std::string, std::unordered_set<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 {
+      if (subschema.type() == adapter::Type::Array) {
         for (auto key : subschema.as_array()) {
           EXPECT(key.type() == adapter::Type::String);
           required[prop].insert(key.as_string());
         }
+      } else {
+        schemas.emplace(prop, context.child(subschema, prop).node());
       }
     }
 

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

@@ -16,6 +16,7 @@ public:
   Pointer() = default;
   Pointer(std::vector<std::variant<std::string, size_t>> const & tokens) : tokens_(tokens) {}
   Pointer(std::string_view path) {
+    path.remove_prefix(1);
     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));

+ 12 - 4
include/jvalidate/detail/simple_adapter.h

@@ -192,12 +192,16 @@ public:
       }
       return false;
     case Type::Array: {
-      auto array = this->as_array();
-      if (rhs.array_size() != array.size()) {
+      auto array_size = maybe_array_size(strict);
+      if (not array_size) {
+        return false;
+      }
+      if (rhs.array_size() != *array_size) {
         return false;
       }
 
       bool rval = true;
+      auto array = this->as_array();
       rhs.apply_array([&, this, index = 0UL](adapter::Adapter const & elem) mutable {
         // Short-Circuit OK
         rval = rval && array[index].equals(elem, strict);
@@ -207,12 +211,16 @@ public:
       return rval;
     }
     case Type::Object: {
-      auto object = this->as_object();
-      if (rhs.object_size() != object.size()) {
+      auto object_size = maybe_object_size(strict);
+      if (not object_size) {
+        return false;
+      }
+      if (rhs.object_size() != *object_size) {
         return false;
       }
 
       bool rval = true;
+      auto object = this->as_object();
       rhs.apply_object([&, this](std::string const & key, adapter::Adapter const & elem) {
         // Short-Circuit OK
         rval = rval && object.contains(key) && object[key].equals(elem, strict);

+ 14 - 1
include/jvalidate/enum.h

@@ -3,7 +3,20 @@
 #include <jvalidate/forward.h>
 
 namespace jvalidate::adapter {
-enum class Type : int8_t { Null, Boolean, Integer, Number, String, Array, Object };
+enum class Type : int8_t {
+  Null = 0b0000001,
+  Boolean = 0b0000010,
+  Integer = 0b0000100,
+  Number = 0b0001100,
+  String = 0b0010000,
+  Array = 0b0100000,
+  Object = 0b1000000,
+};
+
+inline bool operator&(Type lhs, Type rhs) {
+  return static_cast<bool>((static_cast<int>(lhs) & static_cast<int>(rhs)) ==
+                           static_cast<int>(rhs));
+}
 }
 
 namespace jvalidate::schema {

+ 9 - 2
include/jvalidate/validation_visitor.h

@@ -39,7 +39,13 @@ public:
       : ValidationVisitor(json, schema, cfg, regex_cache, {}, result) {}
 
   Status visit(constraint::TypeConstraint const & cons) const {
-    return cons.types.contains(document_.type());
+    adapter::Type const type = document_.type();
+    for (adapter::Type const accept : cons.types) {
+      if (accept & type) {
+        return Status::Accept;
+      }
+    }
+    return Status::Reject;
   }
 
   Status visit(constraint::ExtensionConstraint const & cons) const {
@@ -93,7 +99,7 @@ public:
   }
 
   Status visit(constraint::NotConstraint const & cons) const {
-    return !validate_subschema(cons.child);
+    return validate_subschema(cons.child) == Status::Reject;
   }
 
   Status visit(constraint::ConditionalConstraint const & cons) const {
@@ -361,6 +367,7 @@ public:
       typename A::value_type key_json{key};
       rval &= validate_subschema_on(cons.key_schema, A(key_json), "$$key");
     }
+    return rval;
   }
 
   Status visit(constraint::RequiredConstraint const & cons) const {

+ 2 - 2
tests/json_schema_test_suite.h

@@ -18,7 +18,7 @@ inline auto SchemaTests(std::string_view draft) {
 
     using super::super;
 
-    auto const operator*() const { return super::operator*().path(); }
+    std::filesystem::path operator*() const { return super::operator*().path(); }
 
     fs_iterator operator++() {
       do {
@@ -43,7 +43,7 @@ MATCHER_P(ValidatesAgainst, schema, "") { return jvalidate::Validator(schema).va
 
 template <typename T>
 testing::Matcher<T> ValidatesAgainst(jvalidate::Schema const & schema, T const & test) {
-  if (jvalidate::adapter::AdapterFor<T const>(test)["valid"].as_boolean()) {
+  if (not jvalidate::adapter::AdapterFor<T const>(test)["valid"].as_boolean()) {
     return testing::Not(ValidatesAgainst(std::cref(schema)));
   }
   return ValidatesAgainst(std::cref(schema));

+ 10 - 8
tests/selfvalidate_test.cxx

@@ -54,14 +54,16 @@ TEST_P(JsonSchema, TestSuite) {
 
   for (auto const & suite : spec) {
     std::cout << "\033[0;32m[ SUITE    ] \033[0;0m" << suite["description"].asString() << std::endl;
-    jvalidate::Schema schema(suite["schema"], version, &load_external_for_test);
-    for (auto const & test : suite["tests"]) {
-      try {
-        std::cout << "\033[0;32m[ CASE     ] \033[0;0m    " << test["description"].asString()
-                  << std::endl;
-        EXPECT_THAT(test["data"], ValidatesAgainst(schema, test));
-      } catch (std::exception const & ex) { FAIL() << ex.what() << "\n" << test; }
-    }
+    try {
+      jvalidate::Schema schema(suite["schema"], version, &load_external_for_test);
+      for (auto const & test : suite["tests"]) {
+        try {
+          std::cout << "\033[0;32m[ CASE     ] \033[0;0m    " << test["description"].asString()
+                    << std::endl;
+          EXPECT_THAT(test["data"], ValidatesAgainst(schema, test)) << suite["schema"];
+        } catch (std::exception const & ex) { FAIL() << ex.what() << "\n" << test; }
+      }
+    } catch (std::exception const & ex) { FAIL() << ex.what() << " in parsing schema"; }
   }
 }