#pragma once #include #include #include #include #include #include #include #include #include #include #include #include #define NOOP_UNLESS_TYPE(etype) \ RETURN_UNLESS(document_.type() == adapter::Type::etype, Status::Noop) namespace jvalidate { template class ValidationVisitor : public constraint::ConstraintVisitor { private: A document_; detail::Pointer where_; schema::Node const & schema_; ValidationResult * result_; ValidationResult * local_result_; std::unordered_map & regex_cache_; public: ValidationVisitor(A const & json, schema::Node const & schema, std::unordered_map & regex_cache, ValidationResult * result) : ValidationVisitor(json, schema, {}, regex_cache, result) {} Status visit(constraint::TypeConstraint const & cons) const { return cons.types.contains(document_.type()); } Status visit(constraint::ExtensionConstraint const & cons) const { return cons.validate(document_, where_, result_); } Status visit(constraint::EnumConstraint const & cons) const { for (auto const & option : cons.enumeration) { // TODO(samjaffe): Strictness if (option->apply([this](auto const & frozen) { return document_.equals(frozen); })) { 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], 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 (size_t i = 0; i < array.size(); ++i) { if (validate_subschema_on(cons.subschema, array[i], i)) { ++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], i); if (!rval && result_ == nullptr) { break; } } return rval; } Status visit(constraint::UniqueItemsConstraint const & cons) const { NOOP_UNLESS_TYPE(Array); if constexpr (std::totally_ordered) { std::set cache; for (A const & elem : document_.as_array()) { if (not cache.insert(elem).second) { return Status::Reject; } } } else { auto array = document_.as_array(); 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 Status::Reject; } } } } return Status::Accept; } 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, key); } 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, key); } 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, key); } 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), "$$key"); } } 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 { EXPECT_M(local_result_, "Invalid State - no result object for post-constraint"); NOOP_UNLESS_TYPE(Array); 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)) { rval &= validate_subschema_on(cons.subschema, array[i], i); } if (!rval && result_ == nullptr) { break; } } } Status visit(constraint::UnevaluatedPropertiesConstraint const & cons) const { EXPECT_M(local_result_, "Invalid State - no result object for post-constraint"); NOOP_UNLESS_TYPE(Object); Status rval = Status::Accept; for (auto const & [key, elem] : document_.as_object()) { if (not local_result_->visited_properties.contains(key)) { rval &= validate_subschema_on(cons.subschema, elem, key); } if (!rval && result_ == nullptr) { break; } } } Status validate() const { if (schema_.rejects_all()) { return Status::Reject; } ValidationResult local_result; if (schema_.requires_result_context() && not local_result_) { // Ensure that we store results even if there aren't any... local_result_ = &local_result; } Status rval = Status::Noop; 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: ValidationVisitor(A const & json, detail::Pointer const & where, schema::Node const & schema, std::unordered_map & regex_cache, ValidationResult * result, ValidationResult * local_result = nullptr) : document_(json), where_(where), schema_(schema), regex_cache_(regex_cache), result_(result), local_result_(local_result ?: result_) {} Status validate_subschema(schema::Node const * subschema) const { return CRTP(document_, *subschema, where_, regex_cache_, result_, local_result_).validate(); } template Status validate_subschema_on(schema::Node const * subschema, A const & document, K const & key) const { ValidationResult next; ValidationResult * pnext = result_ ? &next : nullptr; auto status = CRTP(document, *subschema, where_ / key, regex_cache_, pnext).validate(); if (status != Status::Noop and local_result_) { local_result_->record(key); } return status; } }; }