Prechádzať zdrojové kódy

feat: add support for default schema application

Sam Jaffe 1 rok pred
rodič
commit
d59ea7e99d

+ 2 - 0
include/jvalidate/adapter.h

@@ -49,6 +49,8 @@ public:
     return cb(typename AdapterTraits<JSON>::ConstAdapter(value_));
   }
 
+  JSON const & value() const { return value_; }
+
 private:
   JSON value_;
 };

+ 40 - 1
include/jvalidate/adapters/jsoncpp.h

@@ -31,6 +31,11 @@ public:
   JsonCppAdapter<JSON> operator[](std::string const & key) const {
     return this->value() ? &(*this->value())[key] : nullptr;
   }
+
+  void assign(std::string const & key, Const const & frozen) const
+      requires(not std::is_const_v<JSON>) {
+    (*this)[key].assign(frozen);
+  }
 };
 
 template <typename JSON> class JsonCppAdapter final : public detail::SimpleAdapter<JSON> {
@@ -69,7 +74,41 @@ public:
 
   static std::string key(auto it) { return it.key().asString(); }
 
-private:
+  using detail::SimpleAdapter<JSON>::assign;
+  void assign(Adapter const & adapter) const requires(not std::is_const_v<JSON>) {
+    switch (adapter.type()) {
+    case Type::Null:
+      *value() = Json::nullValue;
+      return;
+    case Type::Boolean:
+      *value() = adapter.as_boolean();
+      return;
+    case Type::Integer:
+      *value() = adapter.as_integer();
+      return;
+    case Type::Number:
+      *value() = adapter.as_number();
+      return;
+    case Type::String:
+      *value() = adapter.as_string();
+      return;
+    case Type::Array:
+      adapter.apply_array([this, index = 0](Adapter const & elem) mutable {
+        JsonCppAdapter((*value())[index]).assign(elem);
+        ++index;
+        return Status::Accept;
+      });
+      return;
+    case Type::Object:
+      adapter.apply_object([this](std::string const & key, Adapter const & elem) {
+        JsonCppAdapter((*value())[key]).assign(elem);
+        return Status::Accept;
+      });
+      return;
+    }
+  }
+
+private : using JsonCppAdapter::SimpleAdapter::value;
   using JsonCppAdapter::SimpleAdapter::const_value;
 };
 }

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

@@ -5,8 +5,10 @@
 
 #include <jvalidate/adapter.h>
 #include <jvalidate/detail/array_iterator.h>
+#include <jvalidate/detail/expect.h>
 #include <jvalidate/detail/object_iterator.h>
 #include <jvalidate/forward.h>
+#include <jvalidate/status.h>
 
 namespace jvalidate::adapter::detail {
 template <typename JSON, typename Adapter = AdapterFor<JSON>> class SimpleObjectAdapter {
@@ -87,6 +89,8 @@ private:
 
 template <typename JSON, typename CRTP = AdapterFor<JSON>> class SimpleAdapter : public Adapter {
 public:
+  static constexpr bool is_mutable =
+      not std::is_const_v<JSON> && MutableObject<decltype(std::declval<CRTP>().as_object())>;
   using value_type = std::remove_const_t<JSON>;
 
 public:
@@ -127,6 +131,19 @@ public:
     return std::make_unique<GenericConst<value_type>>(const_value());
   }
 
+  void assign(Const const & frozen) const requires(is_mutable) {
+    EXPECT_M(value_ != nullptr, "Failed to create a new element in parent object");
+    if (auto * this_frozen = dynamic_cast<GenericConst<value_type> const *>(&frozen)) {
+      *value_ = this_frozen->value();
+      return;
+    }
+
+    frozen.apply([this](Adapter const & adapter) {
+      static_cast<CRTP const *>(this)->assign(adapter);
+      return Status::Accept;
+    });
+  }
+
   auto operator<=>(SimpleAdapter const & rhs) const requires std::totally_ordered<JSON> {
     using ord = std::strong_ordering;
     if (value_ == rhs.value_) {

+ 11 - 2
include/jvalidate/forward.h

@@ -102,7 +102,7 @@ concept ObjectAdapter = requires(A const a) {
 };
 
 template <typename A>
-concept Adapter = std::is_base_of_v<adapter::Adapter, A> && requires(A a) {
+concept Adapter = std::is_base_of_v<adapter::Adapter, A> && requires(A const a) {
   { a.type() } -> std::same_as<adapter::Type>;
   { a.as_boolean() } -> std::same_as<bool>;
   { a.as_integer() } -> std::convertible_to<int64_t>;
@@ -116,7 +116,16 @@ concept Adapter = std::is_base_of_v<adapter::Adapter, A> && requires(A a) {
 };
 
 template <typename A>
-concept MutableAdapter = Adapter<A> && false;
+concept MutableObject = ObjectAdapter<A> && requires(A const a) {
+  {a.assign("", std::declval<adapter::Const>())};
+};
+
+template <typename A>
+concept MutableAdapter = Adapter<A> && requires(A const a) {
+  {a.assign(std::declval<adapter::Const>())};
+  {a.assign(std::declval<adapter::Adapter>())};
+  { a.as_object() } -> MutableObject;
+};
 
 template <typename R>
 concept RegexEngine = std::constructible_from<std::string> && requires(R const regex) {

+ 2 - 0
include/jvalidate/schema.h

@@ -43,6 +43,8 @@ public:
   bool requires_result_context() const { return not post_constraints_.empty(); }
   auto const & constraints() const { return constraints_; }
   auto const & post_constraints() const { return constraints_; }
+
+  adapter::Const const * default_value() const { return default_.get(); }
 };
 
 inline Version Node::schema_version(std::string_view url) {

+ 3 - 3
include/jvalidate/validation_config.h

@@ -12,9 +12,9 @@ struct ValidationConfig {
   // equal to one another.
   bool strict_equality = true;
 
-  // When enabled, if a property or item is "Missing", but the associated
-  // subschema in PropertiesConstraint or TupleConstraint has a default
-  // associated with it - then we construct that object into the document.
+  // When enabled, if a property is "Missing", but the associated subschema in
+  // PropertiesConstraint has a default associated with it - then we construct
+  // that object into the document.
   bool construct_default_values = false;
 };
 }

+ 16 - 7
include/jvalidate/validation_visitor.h

@@ -198,13 +198,11 @@ public:
   Status visit(constraint::TupleConstraint const & cons) const {
     NOOP_UNLESS_TYPE(Array);
 
-    auto array = document_.as_array();
-    if (array.size() < cons.items.size()) {
-      return Status::Reject;
-    }
-
     Status rval = Status::Accept;
-    for (size_t i = 0; i < cons.items.size(); ++i) {
+
+    auto array = document_.as_array();
+    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 && result_ == nullptr) {
         break;
@@ -331,7 +329,18 @@ public:
     NOOP_UNLESS_TYPE(Object);
 
     Status rval = Status::Accept;
-    for (auto const & [key, elem] : document_.as_object()) {
+    auto object = document_.as_object();
+
+    if constexpr (MutableAdapter<A>) {
+      for (auto const & [key, subschema] : cons.properties) {
+        auto const * default_value = subschema->default_value();
+        if (default_value && not object.contains(key)) {
+          object.assign(key, *default_value);
+        }
+      }
+    }
+
+    for (auto const & [key, elem] : object) {
       if (auto it = cons.properties.find(key); it != cons.properties.end()) {
         rval &= validate_subschema_on(it->second, elem, key);
       }

+ 5 - 0
include/jvalidate/validator.h

@@ -41,6 +41,11 @@ public:
   template <MutableAdapter A> Status validate(A const & json, ValidationResult * result = nullptr) {
     return ValidationVisitor<A, RE>(json, schema_, cfg_, regex_cache_, result).validate();
   }
+
+  template <typename JSON>
+  requires(not Adapter<JSON>) Status validate(JSON & json, ValidationResult * result = nullptr) {
+    return validate(adapter::AdapterFor<JSON>(json), result);
+  }
 };
 
 class Validator : public ValidatorT<> {