#pragma once #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #define VISITED(type) std::get>(visited_) #define NOOP_UNLESS_TYPE(etype) RETURN_UNLESS(adapter::Type::etype & document_.type(), Status::Noop) #define BREAK_EARLY_IF_NO_RESULT_TREE() \ do { \ if (rval == Status::Reject and not result_) { \ break; \ } \ } while (false) namespace jvalidate { template class ValidationVisitor : public constraint::ConstraintVisitor { private: A document_; detail::Pointer where_; detail::Pointer schema_path_; schema::Node const & schema_; ValidationResult * result_; ValidationConfig const & cfg_; std::unordered_map & regex_cache_; mutable std::tuple, std::unordered_set> visited_; public: ValidationVisitor(A const & json, schema::Node const & schema, ValidationConfig const & cfg, std::unordered_map & regex_cache, ValidationResult * result) : ValidationVisitor(json, schema, cfg, regex_cache, {}, {}, result) {} Status visit(constraint::TypeConstraint const & cons) const { adapter::Type const type = document_.type(); for (adapter::Type const accept : cons.types) { if (accept & type) { return Status::Accept; } } add_error("type ", type, " is not one of {", cons.types, '}'); return Status::Reject; } Status visit(constraint::ExtensionConstraint const & cons) const { return cons.validate(document_, where_, result_); } Status visit(constraint::EnumConstraint const & cons) const { auto is_equal = [this](auto const & frozen) { return document_.equals(frozen, cfg_.strict_equality); }; for (auto const & option : cons.enumeration) { if (option->apply(is_equal)) { return Status::Accept; } } add_error("equals none of the values"); return Status::Reject; } Status visit(constraint::AllOfConstraint const & cons) const { Status rval = Status::Accept; size_t i = 0; for (schema::Node const * subschema : cons.children) { rval &= validate_subschema(subschema, i); ++i; BREAK_EARLY_IF_NO_RESULT_TREE(); } return rval; } Status visit(constraint::AnyOfConstraint const & cons) const { size_t i = 0; for (schema::Node const * subschema : cons.children) { if (validate_subschema(subschema, i)) { return Status::Accept; } ++i; } return Status::Reject; } Status visit(constraint::OneOfConstraint const & cons) const { size_t matches = 0; size_t i = 0; for (schema::Node const * subschema : cons.children) { if (validate_subschema(subschema, i)) { ++matches; } ++i; } return matches == 1 ? Status::Accept : Status::Reject; } Status visit(constraint::NotConstraint const & cons) const { return validate_subschema(cons.child, "not") == Status::Reject; } Status visit(constraint::ConditionalConstraint const & cons) const { if (validate_subschema(cons.if_constraint, "if")) { return validate_subschema(cons.then_constraint, "then"); } return validate_subschema(cons.else_constraint, "else"); } 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(Number); return cons(document_.as_number()); } 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); BREAK_EARLY_IF_NO_RESULT_TREE(); } 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); Status rval = Status::Accept; 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); BREAK_EARLY_IF_NO_RESULT_TREE(); } 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); } BREAK_EARLY_IF_NO_RESULT_TREE(); } 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, key); BREAK_EARLY_IF_NO_RESULT_TREE(); } for (auto [key, required] : cons.required) { if (not object.contains(key)) { continue; } for (auto const & [key, _] : object) { required.erase(key); } rval &= required.empty(); BREAK_EARLY_IF_NO_RESULT_TREE(); } 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); } BREAK_EARLY_IF_NO_RESULT_TREE(); } } return rval; } Status visit(constraint::PropertiesConstraint const & cons) const { NOOP_UNLESS_TYPE(Object); Status rval = Status::Accept; auto object = document_.as_object(); if constexpr (MutableAdapter) { 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); } BREAK_EARLY_IF_NO_RESULT_TREE(); } 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), std::string("$$key")); } return rval; } 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); } if (required.empty()) { return Status::Accept; } add_error("missing required properties ", required); return Status::Reject; } Status visit(constraint::UnevaluatedItemsConstraint const & cons) const { NOOP_UNLESS_TYPE(Array); Status rval = Status::Accept; auto array = document_.as_array(); for (size_t i = 0; i < array.size(); ++i) { if (not VISITED(size_t).contains(i)) { rval &= validate_subschema_on(cons.subschema, array[i], i); } BREAK_EARLY_IF_NO_RESULT_TREE(); } return rval; } Status visit(constraint::UnevaluatedPropertiesConstraint const & cons) const { NOOP_UNLESS_TYPE(Object); Status rval = Status::Accept; for (auto const & [key, elem] : document_.as_object()) { if (not VISITED(std::string).contains(key)) { rval &= validate_subschema_on(cons.subschema, elem, key); } BREAK_EARLY_IF_NO_RESULT_TREE(); } return rval; } Status validate() { if (auto const & reject = schema_.rejects_all()) { add_error(*reject); return Status::Reject; } Status rval = Status::Noop; if (auto ref = schema_.reference_schema()) { rval = validate_subschema(*ref, "$ref"); } detail::Pointer const current_schema = schema_path_; for (auto const & [key, p_constraint] : schema_.constraints()) { BREAK_EARLY_IF_NO_RESULT_TREE(); schema_path_ = current_schema / key; rval &= p_constraint->accept(*this); } for (auto const & [key, p_constraint] : schema_.post_constraints()) { BREAK_EARLY_IF_NO_RESULT_TREE(); schema_path_ = current_schema / key; rval &= p_constraint->accept(*this); } return rval; } private: template void add_error(Args &&... args) const { if (not result_) { return; } std::stringstream ss; using ::jvalidate::operator<<; [[maybe_unused]] int _[] = {(ss << std::forward(args), 0)...}; result_->add_error(where_, schema_path_, ss.str()); } template static void merge_visited(C & to, C const & from) { to.insert(from.begin(), from.end()); } ValidationVisitor(A const & json, schema::Node const & schema, ValidationConfig const & cfg, std::unordered_map & regex_cache, detail::Pointer const & where, detail::Pointer const & schema_path, ValidationResult * result) : document_(json), where_(where), schema_path_(schema_path), schema_(schema), cfg_(cfg), regex_cache_(regex_cache), result_(result) {} template Status validate_subschema(schema::Node const * subschema, K const & key) const { EXPECT(subschema != &schema_); // TODO(samjaffe) - Figure out what's causing this infinite loop ValidationVisitor next(document_, *subschema, cfg_, regex_cache_, where_, schema_path_ / key, result_); Status rval = next.validate(); merge_visited(std::get<0>(visited_), std::get<0>(next.visited_)); merge_visited(std::get<1>(visited_), std::get<1>(next.visited_)); return rval; } 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 = ValidationVisitor(document, *subschema, cfg_, regex_cache_, where_ / key, schema_path_, pnext) .validate(); if (status != Status::Noop) { VISITED(K).insert(key); } if (status == Status::Reject and result_) { result_->add_error(std::move(next)); } return status; } }; }