#pragma once #include #include #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_ and not visited_) { \ break; \ } \ } while (false) namespace jvalidate { template class ValidationVisitor : public constraint::ConstraintVisitor { private: using VisitedAnnotation = std::tuple, std::unordered_set>; 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 VisitedAnnotation * visited_ = nullptr; public: ValidationVisitor(A const & json, schema::Node const & schema, ValidationConfig const & cfg, std::unordered_map & regex_cache, ValidationResult * result) : document_(json), schema_(&schema), result_(result), cfg_(cfg), regex_cache_(regex_cache) {} Status visit(constraint::TypeConstraint const & cons) const { adapter::Type const type = document_.type(); for (adapter::Type const accept : cons.types) { if (type == accept) { return Status::Accept; } if (accept == adapter::Type::Number && type == adapter::Type::Integer) { return Status::Accept; } if (accept == adapter::Type::Integer && type == adapter::Type::Number && detail::is_json_integer(document_.as_number())) { 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; Status rval = Status::Reject; for (schema::Node const * subschema : cons.children) { if (validate_subschema(subschema, i)) { rval = Status::Accept; } if (not visited_ && rval == Status::Accept) { break; } ++i; } return rval; } 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 { VisitedAnnotation * suppress = nullptr; std::swap(suppress, visited_); auto rval = validate_subschema(cons.child, detail::Pointer()) == Status::Reject; std::swap(suppress, visited_); return rval; } Status visit(constraint::ConditionalConstraint const & cons) const { if (validate_subschema(cons.if_constraint, detail::Pointer())) { 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: if (int64_t value = document_.as_integer(); not cons(value)) { add_error("integer ", value, " exceeds ", cons.exclusive ? "exclusive " : "", "maximum of ", cons.value); return false; } return true; case adapter::Type::Number: if (double value = document_.as_number(); not cons(value)) { add_error("number ", value, " exceeds ", cons.exclusive ? "exclusive " : "", "maximum of ", cons.value); return false; } return true; default: return Status::Noop; } } Status visit(constraint::MinimumConstraint const & cons) const { switch (document_.type()) { case adapter::Type::Integer: if (int64_t value = document_.as_integer(); not cons(value)) { add_error("integer ", value, " fails ", cons.exclusive ? "exclusive " : "", "minimum of ", cons.value); return false; } return true; case adapter::Type::Number: if (double value = document_.as_number(); not cons(value)) { add_error("number ", value, " fails ", cons.exclusive ? "exclusive " : "", "minimum of ", cons.value); return false; } return true; default: return Status::Noop; } } Status visit(constraint::MultipleOfConstraint const & cons) const { adapter::Type const type = document_.type(); RETURN_UNLESS(type == adapter::Type::Number || type == adapter::Type::Integer, Status::Noop); if (double value = document_.as_number(); not cons(value)) { add_error("number ", value, " is not a multiple of ", cons.value); return false; } return true; } Status visit(constraint::MaxLengthConstraint const & cons) const { NOOP_UNLESS_TYPE(String); if (auto str = document_.as_string(); detail::length(str) > cons.value) { add_error("string '", str, "' is greater than the maximum length of ", cons.value); return false; } return true; } Status visit(constraint::MinLengthConstraint const & cons) const { NOOP_UNLESS_TYPE(String); if (auto str = document_.as_string(); detail::length(str) < cons.value) { add_error("string '", str, "' is less than the minimum length of ", cons.value); return false; } return true; } Status visit(constraint::PatternConstraint const & cons) const { NOOP_UNLESS_TYPE(String); RE const & regex = regex_cache_.try_emplace(cons.regex, cons.regex).first->second; if (auto str = document_.as_string(); not regex.search(str)) { add_error("string '", str, "' does not match pattern /", cons.regex, "/"); return false; } return true; } Status visit(constraint::FormatConstraint const & cons) const { // https://json-schema.org/draft/2020-12/draft-bhutton-json-schema-validation-01#name-defined-formats NOOP_UNLESS_TYPE(String); if (not cfg_.validate_format) { return true; } std::cerr << "Unimplemented constraint format(" << cons.format << ")" << "\n"; return false; } 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) { add_error("array does not contain at least ", minimum, " matching elements"); return Status::Reject; } if (matches > maximum) { add_error("array contains more than ", maximum, " matching elements"); return Status::Reject; } return Status::Accept; } Status visit(constraint::MaxItemsConstraint const & cons) const { NOOP_UNLESS_TYPE(Array); if (auto size = document_.array_size(); size > cons.value) { add_error("array with ", size, " items is greater than the maximum of ", cons.value); return false; } return true; } Status visit(constraint::MinItemsConstraint const & cons) const { NOOP_UNLESS_TYPE(Array); if (auto size = document_.array_size(); size < cons.value) { add_error("array with ", size, " items is less than the minimum of ", cons.value); return false; } return true; } 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) { add_error("array contains duplicate elements"); 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)) { add_error("array elements ", i, " and ", j, " are equal"); 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); if (auto size = document_.object_size(); size > cons.value) { add_error("object with ", size, " properties is greater than the maximum of ", cons.value); return false; } return true; } Status visit(constraint::MinPropertiesConstraint const & cons) const { NOOP_UNLESS_TYPE(Object); if (auto size = document_.object_size(); size < cons.value) { add_error("object with ", size, " properties is less than the minimum of ", cons.value); return false; } return true; } 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); if (not visited_) { return Status::Reject; } 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); if (not visited_) { return Status::Reject; } 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; } if (schema_->accepts_all()) { // An accept-all schema is not No-Op for the purpose of unevaluated* return Status::Accept; } VisitedAnnotation annotate; if (schema_->requires_result_context() and not visited_) { visited_ = &annotate; } 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()); } template Status validate_subschema(schema::Node const * subschema, K const & key) const { VisitedAnnotation annotate; ValidationVisitor next = *this; next.schema_path_ /= key; std::tie(next.schema_, next.visited_) = std::forward_as_tuple(subschema, visited_ ? &annotate : nullptr); Status rval = next.validate(); if (rval == Status::Accept and visited_) { merge_visited(std::get<0>(*visited_), std::get<0>(annotate)); merge_visited(std::get<1>(*visited_), std::get<1>(annotate)); } return rval; } template Status validate_subschema_on(schema::Node const * subschema, A const & document, K const & key) const { ValidationResult result; ValidationVisitor next = *this; next.where_ /= key; std::tie(next.document_, next.schema_, next.result_, next.visited_) = std::forward_as_tuple(document, subschema, result_ ? &result : nullptr, nullptr); auto status = next.validate(); if (status == Status::Accept and visited_) { VISITED(K).insert(key); } if (status == Status::Reject and result_) { result_->add_error(std::move(result)); } return status; } }; }