|
|
@@ -0,0 +1,389 @@
|
|
|
+#pragma once
|
|
|
+
|
|
|
+#include <unordered_map>
|
|
|
+
|
|
|
+#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/constraint/visitor.h>
|
|
|
+#include <jvalidate/detail/expect.h>
|
|
|
+#include <jvalidate/forward.h>
|
|
|
+#include <jvalidate/result.h>
|
|
|
+#include <jvalidate/schema.h>
|
|
|
+#include <jvalidate/status.h>
|
|
|
+
|
|
|
+#define NOOP_UNLESS_TYPE(etype) \
|
|
|
+ RETURN_UNLESS(document_.type() == adapter::Type::etype, Status::Noop)
|
|
|
+
|
|
|
+namespace jvalidate {
|
|
|
+template <Adapter A, RegexEngine RE, typename CRTP, constraint::Extension... Extensions>
|
|
|
+class ValidationVisitorBase : public constraint::ConstraintVisitor,
|
|
|
+ public constraint::ExtensionConstraintVisitor<Extensions>... {
|
|
|
+private:
|
|
|
+ A document_;
|
|
|
+ schema::Node const & schema_;
|
|
|
+ Result * result_;
|
|
|
+ std::unordered_map<std::string, RE> & regex_cache_;
|
|
|
+
|
|
|
+public:
|
|
|
+ ValidationVisitorBase(A const & json, schema::Node const & schema,
|
|
|
+ std::unordered_map<std::string, RE> & regex_cache, Result * result)
|
|
|
+ : document_(json), schema_(schema), result_(result) {}
|
|
|
+
|
|
|
+ Status visit(constraint::TypeConstraint const & cons) const {
|
|
|
+ return cons.types.contains(document_.type());
|
|
|
+ }
|
|
|
+
|
|
|
+ Status visit(constraint::EnumConstraint const & cons) const {
|
|
|
+ // TODO(samjaffe) Implement
|
|
|
+ /* auto equals = [this](adapter::Adapter const & frozen) { return document_.equals(frozen); };
|
|
|
+ */
|
|
|
+ /* for (auto const & option : cons.enumeration) { */
|
|
|
+ /* if (option->apply(equals)) { */
|
|
|
+ /* return Status::Accept; */
|
|
|
+ /* } */
|
|
|
+ /* } */
|
|
|
+ return Status::Reject;
|
|
|
+ }
|
|
|
+
|
|
|
+ Status visit(constraint::AllOfConstraint const & cons) const {
|
|
|
+ Status rval = Status::Accept;
|
|
|
+
|
|
|
+ for (schema::Node const * subschema : cons.children) {
|
|
|
+ rval &= validate_subschema(subschema);
|
|
|
+ if (!rval && result_ == nullptr) {
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return rval;
|
|
|
+ }
|
|
|
+
|
|
|
+ Status visit(constraint::AnyOfConstraint const & cons) const {
|
|
|
+ for (schema::Node const * subschema : cons.children) {
|
|
|
+ if (validate_subschema(subschema)) {
|
|
|
+ return Status::Accept;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return Status::Reject;
|
|
|
+ }
|
|
|
+
|
|
|
+ Status visit(constraint::OneOfConstraint const & cons) const {
|
|
|
+ size_t matches = 0;
|
|
|
+ for (schema::Node const * subschema : cons.children) {
|
|
|
+ if (validate_subschema(subschema)) {
|
|
|
+ ++matches;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return matches == 1 ? Status::Accept : Status::Reject;
|
|
|
+ }
|
|
|
+
|
|
|
+ Status visit(constraint::NotConstraint const & cons) const {
|
|
|
+ return !validate_subschema(cons.child);
|
|
|
+ }
|
|
|
+
|
|
|
+ Status visit(constraint::ConditionalConstraint const & cons) const {
|
|
|
+ if (validate_subschema(cons.if_constraint)) {
|
|
|
+ return validate_subschema(cons.then_constraint);
|
|
|
+ }
|
|
|
+ return validate_subschema(cons.else_constraint);
|
|
|
+ }
|
|
|
+
|
|
|
+ Status visit(constraint::MaximumConstraint const & cons) const {
|
|
|
+ switch (document_.type()) {
|
|
|
+ case adapter::Type::Integer:
|
|
|
+ return cons(document_.as_integer());
|
|
|
+ case adapter::Type::Number:
|
|
|
+ return cons(document_.as_number());
|
|
|
+ default:
|
|
|
+ return Status::Noop;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ Status visit(constraint::MinimumConstraint const & cons) const {
|
|
|
+ switch (document_.type()) {
|
|
|
+ case adapter::Type::Integer:
|
|
|
+ return cons(document_.as_integer());
|
|
|
+ case adapter::Type::Number:
|
|
|
+ return cons(document_.as_number());
|
|
|
+ default:
|
|
|
+ return Status::Noop;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ Status visit(constraint::MultipleOfConstraint const & cons) const {
|
|
|
+ NOOP_UNLESS_TYPE(Integer);
|
|
|
+ return cons(document_.as_integer());
|
|
|
+ }
|
|
|
+
|
|
|
+ Status visit(constraint::MaxLengthConstraint const & cons) const {
|
|
|
+ NOOP_UNLESS_TYPE(String);
|
|
|
+ return cons(document_.as_string());
|
|
|
+ }
|
|
|
+
|
|
|
+ Status visit(constraint::MinLengthConstraint const & cons) const {
|
|
|
+ NOOP_UNLESS_TYPE(String);
|
|
|
+ return cons(document_.as_string());
|
|
|
+ }
|
|
|
+
|
|
|
+ Status visit(constraint::PatternConstraint const & cons) const {
|
|
|
+ NOOP_UNLESS_TYPE(String);
|
|
|
+
|
|
|
+ RE const & regex = regex_cache_.try_emplace(cons.regex, cons.regex).first->second;
|
|
|
+ return regex.search(document_.as_string());
|
|
|
+ }
|
|
|
+
|
|
|
+ Status visit(constraint::AdditionalItemsConstraint const & cons) const {
|
|
|
+ NOOP_UNLESS_TYPE(Array);
|
|
|
+
|
|
|
+ auto array = document_.as_array();
|
|
|
+
|
|
|
+ Status rval = Status::Accept;
|
|
|
+ for (size_t i = cons.applies_after_nth; i < array.size(); ++i) {
|
|
|
+ rval &= validate_subschema_on(cons.subschema, array[i]);
|
|
|
+ if (!rval && result_ == nullptr) {
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return rval;
|
|
|
+ }
|
|
|
+
|
|
|
+ Status visit(constraint::ContainsConstraint const & cons) const {
|
|
|
+ NOOP_UNLESS_TYPE(Array);
|
|
|
+
|
|
|
+ auto array = document_.as_array();
|
|
|
+ size_t const minimum = cons.minimum.value_or(1);
|
|
|
+ size_t const maximum = cons.maximum.value_or(array.size());
|
|
|
+ size_t matches = 0;
|
|
|
+ for (A const & elem : array) {
|
|
|
+ if (validate_subschema_on(cons.subschema, elem)) {
|
|
|
+ ++matches;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if (matches < minimum) {
|
|
|
+ return Status::Reject;
|
|
|
+ }
|
|
|
+ if (matches > maximum) {
|
|
|
+ return Status::Reject;
|
|
|
+ }
|
|
|
+ return Status::Accept;
|
|
|
+ }
|
|
|
+
|
|
|
+ Status visit(constraint::MaxItemsConstraint const & cons) const {
|
|
|
+ NOOP_UNLESS_TYPE(Array);
|
|
|
+ return cons(document_.as_array());
|
|
|
+ }
|
|
|
+
|
|
|
+ Status visit(constraint::MinItemsConstraint const & cons) const {
|
|
|
+ NOOP_UNLESS_TYPE(Array);
|
|
|
+ return cons(document_.as_array());
|
|
|
+ }
|
|
|
+
|
|
|
+ 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) {
|
|
|
+ rval &= validate_subschema_on(cons.items[i], array[i]);
|
|
|
+ if (!rval && result_ == nullptr) {
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return rval;
|
|
|
+ }
|
|
|
+
|
|
|
+ Status visit(constraint::UniqueItemsConstraint const & cons) const { throw; }
|
|
|
+
|
|
|
+ Status visit(constraint::AdditionalPropertiesConstraint const & cons) const {
|
|
|
+ NOOP_UNLESS_TYPE(Object);
|
|
|
+
|
|
|
+ auto matches_any_pattern = [this, &cons](std::string const & key) {
|
|
|
+ for (auto & pattern : cons.patterns) {
|
|
|
+ RE const & regex = regex_cache_.try_emplace(pattern, pattern).first->second;
|
|
|
+ if (regex.search(key)) {
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return false;
|
|
|
+ };
|
|
|
+
|
|
|
+ Status rval = Status::Accept;
|
|
|
+ for (auto const & [key, elem] : document_.as_object()) {
|
|
|
+ if (not cons.properties.contains(key) && not matches_any_pattern(key)) {
|
|
|
+ rval &= validate_subschema_on(cons.subschema, elem);
|
|
|
+ }
|
|
|
+ if (!rval && result_ == nullptr) {
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return rval;
|
|
|
+ }
|
|
|
+
|
|
|
+ Status visit(constraint::DependenciesConstraint const & cons) const {
|
|
|
+ NOOP_UNLESS_TYPE(Object);
|
|
|
+
|
|
|
+ auto object = document_.as_object();
|
|
|
+ Status rval = Status::Accept;
|
|
|
+ for (auto const & [key, subschema] : cons.subschemas) {
|
|
|
+ if (not object.contains(key)) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ rval &= validate_subschema(subschema);
|
|
|
+ if (!rval && result_ == nullptr) {
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ for (auto [key, required] : cons.required) {
|
|
|
+ if (not object.contains(key)) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ for (auto const & [key, _] : object) {
|
|
|
+ required.erase(key);
|
|
|
+ }
|
|
|
+
|
|
|
+ rval &= required.empty();
|
|
|
+
|
|
|
+ if (!rval && result_ == nullptr) {
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return rval;
|
|
|
+ }
|
|
|
+
|
|
|
+ Status visit(constraint::MaxPropertiesConstraint const & cons) const {
|
|
|
+ NOOP_UNLESS_TYPE(Object);
|
|
|
+ return cons(document_.as_object());
|
|
|
+ }
|
|
|
+
|
|
|
+ Status visit(constraint::MinPropertiesConstraint const & cons) const {
|
|
|
+ NOOP_UNLESS_TYPE(Object);
|
|
|
+ return cons(document_.as_object());
|
|
|
+ }
|
|
|
+
|
|
|
+ Status visit(constraint::PatternPropertiesConstraint const & cons) const {
|
|
|
+ NOOP_UNLESS_TYPE(Object);
|
|
|
+
|
|
|
+ Status rval = Status::Accept;
|
|
|
+ for (auto const & [pattern, subschema] : cons.properties) {
|
|
|
+ RE const & regex = regex_cache_.try_emplace(pattern, pattern).first->second;
|
|
|
+ for (auto const & [key, elem] : document_.as_object()) {
|
|
|
+ if (regex.search(key)) {
|
|
|
+ rval &= validate_subschema_on(subschema, elem);
|
|
|
+ }
|
|
|
+ if (!rval && result_ == nullptr) {
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return rval;
|
|
|
+ }
|
|
|
+
|
|
|
+ Status visit(constraint::PropertiesConstraint const & cons) const {
|
|
|
+ NOOP_UNLESS_TYPE(Object);
|
|
|
+
|
|
|
+ Status rval = Status::Accept;
|
|
|
+ for (auto const & [key, elem] : document_.as_object()) {
|
|
|
+ if (auto it = cons.properties.find(key); it != cons.properties.end()) {
|
|
|
+ rval &= validate_subschema_on(it->second, elem);
|
|
|
+ }
|
|
|
+ if (!rval && result_ == nullptr) {
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return rval;
|
|
|
+ }
|
|
|
+
|
|
|
+ Status visit(constraint::PropertyNamesConstraint const & cons) const {
|
|
|
+ NOOP_UNLESS_TYPE(Object);
|
|
|
+
|
|
|
+ Status rval = Status::Accept;
|
|
|
+ for (auto const & [key, _] : document_.as_object()) {
|
|
|
+ // TODO(samjaffe): Should we prefer a std::string adapter like valijson?
|
|
|
+ typename A::value_type key_json{key};
|
|
|
+ rval &= validate_subschema_on(cons.key_schema, A(key_json));
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ Status visit(constraint::RequiredConstraint const & cons) const {
|
|
|
+ NOOP_UNLESS_TYPE(Object);
|
|
|
+
|
|
|
+ auto required = cons.properties;
|
|
|
+ for (auto const & [key, _] : document_.as_object()) {
|
|
|
+ required.erase(key);
|
|
|
+ }
|
|
|
+
|
|
|
+ return required.empty();
|
|
|
+ }
|
|
|
+
|
|
|
+ Status visit(constraint::UnevaluatedItemsConstraint const & cons) const { throw; }
|
|
|
+ Status visit(constraint::UnevaluatedPropertiesConstraint const & cons) const { throw; }
|
|
|
+
|
|
|
+ Status validate() const {
|
|
|
+ if (schema_.rejects_all()) {
|
|
|
+ return Status::Reject;
|
|
|
+ }
|
|
|
+
|
|
|
+ Status rval = Status::Noop;
|
|
|
+ if (schema_.requires_result_context()) {
|
|
|
+ // Ensure that we store results even if there aren't any...
|
|
|
+ // TODO(samjaffe) Implement this
|
|
|
+ }
|
|
|
+
|
|
|
+ if (auto ref = schema_.reference_schema()) {
|
|
|
+ rval = validate_subschema(*ref);
|
|
|
+ }
|
|
|
+
|
|
|
+ for (auto const & [key, p_constraint] : schema_.constraints()) {
|
|
|
+ if (rval || result_) {
|
|
|
+ rval &= p_constraint->accept(*this);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ for (auto const & [key, p_constraint] : schema_.post_constraints()) {
|
|
|
+ if (rval || result_) {
|
|
|
+ rval &= p_constraint->accept(*this);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return rval;
|
|
|
+ }
|
|
|
+
|
|
|
+private:
|
|
|
+ Status validate(A const & document) const { return validate_subschema_on(&schema_, document_); }
|
|
|
+
|
|
|
+ Status validate_subschema(schema::Node const * subschema) const {
|
|
|
+ return validate_subschema_on(subschema, document_);
|
|
|
+ }
|
|
|
+
|
|
|
+ Status validate_subschema_on(schema::Node const * subschema, A const & document) const {
|
|
|
+ // TODO(samjaffe): Result implementation
|
|
|
+ Result * pnext = nullptr;
|
|
|
+ return CRTP(document, *subschema, regex_cache_, pnext).validate();
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
+template <Adapter A, RegexEngine RE>
|
|
|
+class ValidationVisitor final : public ValidationVisitorBase<A, RE, ValidationVisitor<A, RE>> {
|
|
|
+public:
|
|
|
+ using ValidationVisitor::ValidationVisitorBase::ValidationVisitorBase;
|
|
|
+};
|
|
|
+}
|