#pragma once #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #define VISITED(type) std::get>(*visited_) #define VALIDATE_SUBSCHEMA_AND_MARK_LOCAL_VISIT(subschema, subinstance, path, local_visited) \ do { \ Status const partial = validate_subschema_on(subschema, subinstance, path); \ rval &= partial; \ if (result_ and partial != Status::Noop) { \ local_visited.insert(local_visited.end(), path); \ } \ } while (false) #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 { private: JVALIDATE_TRIBOOL_TYPE(StoreResults, ForValid, ForInvalid, ForAnything); using VisitedAnnotation = std::tuple, std::unordered_set>; friend ExtensionVisitor; private: detail::Pointer where_; detail::Pointer schema_path_; schema::Node const * schema_; ValidationResult * result_; ValidationConfig const & cfg_; ExtensionVisitor extension_; RE & regex_; mutable VisitedAnnotation * visited_ = nullptr; mutable StoreResults tracking_ = StoreResults::ForInvalid; public: /** * @brief Construct a new ValidationVisitor * * @param schema The parsed JSON Schema * @param cfg General configuration settings for how the run is executed * @param regex A cache of string regular expressions to compiled * regular expressions * @param[optional] extension A special visitor for extension constraints. * @param[optional] result A cache of result/annotation info for the user to * receive a detailed summary of why a document is supported/unsupported. */ ValidationVisitor(schema::Node const & schema, ValidationConfig const & cfg, RE & regex, ExtensionVisitor extension, ValidationResult * result) : schema_(&schema), result_(result), cfg_(cfg), extension_(extension), regex_(regex) {} Status visit(constraint::ExtensionConstraint const & cons, Adapter auto const & document) const { if constexpr (std::is_invocable_r_v) { return extension_(cons, document, *this); } annotate("unsupported extension"); return Status::Noop; } Status visit(constraint::TypeConstraint const & cons, Adapter auto const & document) const { adapter::Type const type = document.type(); for (adapter::Type const accept : cons.types) { if (type == accept) { return result(Status::Accept, type, " is in types [", cons.types, "]"); } if (accept == adapter::Type::Number && type == adapter::Type::Integer) { return result(Status::Accept, type, " is in types [", cons.types, "]"); } if (accept == adapter::Type::Integer && type == adapter::Type::Number && detail::is_json_integer(document.as_number())) { return result(Status::Accept, type, " is in types [", cons.types, "]"); } } return result(Status::Reject, type, " is not in types [", cons.types, "]"); } Status visit(constraint::EnumConstraint const & cons, Adapter auto const & document) const { auto is_equal = [this, &document](auto const & frozen) { return document.equals(frozen, cfg_.strict_equality); }; for (auto const & [index, option] : detail::enumerate(cons.enumeration)) { if (option->apply(is_equal)) { return result(Status::Accept, index); } } return Status::Reject; } Status visit(constraint::AllOfConstraint const & cons, Adapter auto const & document) const { Status rval = Status::Accept; std::set unmatched; for (auto const & [index, subschema] : detail::enumerate(cons.children)) { if (auto stat = validate_subschema(subschema, document, index); stat == Status::Reject) { rval = Status::Reject; unmatched.insert(index); } BREAK_EARLY_IF_NO_RESULT_TREE(); } if (rval == Status::Reject) { return result(rval, "does not validate subschemas ", unmatched); } return result(rval, "validates all subschemas"); } Status visit(constraint::AnyOfConstraint const & cons, Adapter auto const & document) const { std::optional first_validated; for (auto const & [index, subschema] : detail::enumerate(cons.children)) { if (validate_subschema(subschema, document, index)) { first_validated = index; } if (not visited_ && first_validated.has_value()) { break; } } if (first_validated.has_value()) { return result(Status::Accept, "validates subschema ", *first_validated); } return result(Status::Reject, "validates none of the subschemas"); } Status visit(constraint::OneOfConstraint const & cons, Adapter auto const & document) const { std::set matches; for (auto const & [index, subschema] : detail::enumerate(cons.children)) { scoped_state(tracking_, StoreResults::ForAnything); if (validate_subschema(subschema, document, index)) { matches.insert(index); } } if (matches.size() == 1) { return result(Status::Accept, "validates subschema ", *matches.begin()); } return result(Status::Reject, "validates multiple subschemas ", matches); } Status visit(constraint::NotConstraint const & cons, Adapter auto const & document) const { scoped_state(visited_, nullptr); scoped_state(tracking_, !tracking_); bool const rejected = validate_subschema(cons.child, document) == Status::Reject; return rejected; } Status visit(constraint::ConditionalConstraint const & cons, Adapter auto const & document) const { Status const if_true = [this, &cons, &document]() { scoped_state(tracking_, StoreResults::ForAnything); return validate_subschema(cons.if_constraint, document); }(); annotate(if_true ? "valid" : "invalid"); if (if_true) { return validate_subschema(cons.then_constraint, document, detail::parent, "then"); } return validate_subschema(cons.else_constraint, document, detail::parent, "else"); } Status visit(constraint::MaximumConstraint const & cons, Adapter auto const & document) const { switch (document.type()) { case adapter::Type::Integer: if (int64_t value = document.as_integer(); not cons(value)) { return result(Status::Reject, value, cons.exclusive ? " >= " : " > ", cons.value); } else { return result(Status::Accept, value, cons.exclusive ? " < " : " <= ", cons.value); } case adapter::Type::Number: if (double value = document.as_number(); not cons(value)) { return result(Status::Reject, value, cons.exclusive ? " >= " : " > ", cons.value); } else { return result(Status::Accept, value, cons.exclusive ? " < " : " <= ", cons.value); } default: return Status::Noop; } } Status visit(constraint::MinimumConstraint const & cons, Adapter auto const & document) const { switch (document.type()) { case adapter::Type::Integer: if (int64_t value = document.as_integer(); not cons(value)) { return result(Status::Reject, value, cons.exclusive ? " <= " : " < ", cons.value); } else { return result(Status::Accept, value, cons.exclusive ? " > " : " >= ", cons.value); } case adapter::Type::Number: if (double value = document.as_number(); not cons(value)) { return result(Status::Reject, value, cons.exclusive ? " <= " : " < ", cons.value); } else { return result(Status::Accept, value, cons.exclusive ? " > " : " >= ", cons.value); } default: return Status::Noop; } } Status visit(constraint::MultipleOfConstraint const & cons, Adapter auto const & document) 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)) { return result(Status::Reject, value, " is not a multiple of ", cons.value); } else { return result(Status::Accept, value, " is a multiple of ", cons.value); } } Status visit(constraint::MaxLengthConstraint const & cons, Adapter auto const & document) const { NOOP_UNLESS_TYPE(String); std::string const str = document.as_string(); if (int64_t len = detail::length(str); len > cons.value) { return result(Status::Reject, "string of length ", len, " is >", cons.value); } else { return result(Status::Accept, "string of length ", len, " is <=", cons.value); } } Status visit(constraint::MinLengthConstraint const & cons, Adapter auto const & document) const { NOOP_UNLESS_TYPE(String); std::string const str = document.as_string(); if (int64_t len = detail::length(str); len < cons.value) { return result(Status::Reject, "string of length ", len, " is <", cons.value); } else { return result(Status::Accept, "string of length ", len, " is >=", cons.value); } } Status visit(constraint::PatternConstraint const & cons, Adapter auto const & document) const { NOOP_UNLESS_TYPE(String); std::string const str = document.as_string(); if (regex_.search(cons.regex, str)) { return result(Status::Accept, "string matches pattern /", cons.regex, "/"); } return result(Status::Reject, "string does not match pattern /", cons.regex, "/"); } Status visit(constraint::FormatConstraint const & cons, Adapter auto const & document) const { // https://json-schema.org/draft/2020-12/draft-bhutton-json-schema-validation-01#name-defined-formats NOOP_UNLESS_TYPE(String); annotate(cons.format); if (not cfg_.validate_format && not cons.is_assertion) { return true; } return result(Status::Reject, " is unimplemented"); } Status visit(constraint::AdditionalItemsConstraint const & cons, Adapter auto const & document) const { NOOP_UNLESS_TYPE(Array); auto array = document.as_array(); Status rval = Status::Accept; std::vector items; for (size_t i = cons.applies_after_nth; i < array.size(); ++i) { VALIDATE_SUBSCHEMA_AND_MARK_LOCAL_VISIT(cons.subschema, array[i], i, items); BREAK_EARLY_IF_NO_RESULT_TREE(); } annotate_list(items); return rval; } Status visit(constraint::ContainsConstraint const & cons, Adapter auto const & document) 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 result(Status::Reject, "array contains <", minimum, " matching items"); } if (matches > maximum) { return result(Status::Reject, "array contains >", maximum, " matching items"); } return result(Status::Accept, "array contains ", matches, " matching items"); } Status visit(constraint::MaxItemsConstraint const & cons, Adapter auto const & document) const { NOOP_UNLESS_TYPE(Array); if (size_t size = document.array_size(); size > cons.value) { return result(Status::Reject, "array of size ", size, " is >", cons.value); } else { return result(Status::Accept, "array of size ", size, " is <=", cons.value); } } Status visit(constraint::MinItemsConstraint const & cons, Adapter auto const & document) const { NOOP_UNLESS_TYPE(Array); if (size_t size = document.array_size(); size < cons.value) { return result(Status::Reject, "array of size ", size, " is <", cons.value); } else { return result(Status::Accept, "array of size ", size, " is >=", cons.value); } } Status visit(constraint::TupleConstraint const & cons, Adapter auto const & document) const { NOOP_UNLESS_TYPE(Array); Status rval = Status::Accept; std::vector items; for (auto const & [index, item] : detail::enumerate(document.as_array())) { if (index >= cons.items.size()) { break; } VALIDATE_SUBSCHEMA_AND_MARK_LOCAL_VISIT(cons.items[index], item, index, items); BREAK_EARLY_IF_NO_RESULT_TREE(); } annotate_list(items); return rval; } template Status visit(constraint::UniqueItemsConstraint const & cons, A const & document) const { NOOP_UNLESS_TYPE(Array); if constexpr (std::totally_ordered) { std::map cache; for (auto const & [index, elem] : detail::enumerate(document.as_array())) { if (auto [it, created] = cache.emplace(elem, index); not created) { return result(Status::Reject, "items ", it->second, " and ", index, " are equal"); } } } 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 result(Status::Reject, "items ", i, " and ", j, " are equal"); } } } } return result(Status::Accept, "all array items are unique"); } Status visit(constraint::AdditionalPropertiesConstraint const & cons, Adapter auto const & document) const { NOOP_UNLESS_TYPE(Object); auto matches_any_pattern = [this, &cons](std::string const & key) { for (auto & pattern : cons.patterns) { if (regex_.search(pattern, key)) { return true; } } return false; }; Status rval = Status::Accept; std::vector properties; for (auto const & [key, elem] : document.as_object()) { if (not cons.properties.contains(key) && not matches_any_pattern(key)) { VALIDATE_SUBSCHEMA_AND_MARK_LOCAL_VISIT(cons.subschema, elem, key, properties); } BREAK_EARLY_IF_NO_RESULT_TREE(); } annotate_list(properties); return rval; } Status visit(constraint::DependenciesConstraint const & cons, Adapter auto const & document) 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, document, 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, Adapter auto const & document) const { NOOP_UNLESS_TYPE(Object); if (size_t size = document.object_size(); size > cons.value) { return result(Status::Reject, "object of size ", size, " is >", cons.value); } else { return result(Status::Accept, "object of size ", size, " is <=", cons.value); } } Status visit(constraint::MinPropertiesConstraint const & cons, Adapter auto const & document) const { NOOP_UNLESS_TYPE(Object); if (size_t size = document.object_size(); size < cons.value) { return result(Status::Reject, "object of size ", size, " is <", cons.value); } else { return result(Status::Accept, "object of size ", size, " is >=", cons.value); } } Status visit(constraint::PatternPropertiesConstraint const & cons, Adapter auto const & document) const { NOOP_UNLESS_TYPE(Object); std::vector properties; Status rval = Status::Accept; for (auto const & [pattern, subschema] : cons.properties) { for (auto const & [key, elem] : document.as_object()) { if (not regex_.search(pattern, key)) { continue; } VALIDATE_SUBSCHEMA_AND_MARK_LOCAL_VISIT(subschema, elem, key, properties); BREAK_EARLY_IF_NO_RESULT_TREE(); } } annotate_list(properties); return rval; } template Status visit(constraint::PropertiesConstraint const & cons, A const & document) 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); } } } std::vector properties; for (auto const & [key, elem] : object) { if (auto it = cons.properties.find(key); it != cons.properties.end()) { VALIDATE_SUBSCHEMA_AND_MARK_LOCAL_VISIT(it->second, elem, key, properties); } BREAK_EARLY_IF_NO_RESULT_TREE(); } annotate_list(properties); return rval; } template Status visit(constraint::PropertyNamesConstraint const & cons, A const & document) 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? rval &= validate_subschema_on(cons.key_schema, detail::StringAdapter(key), std::string("$$key")); } return rval; } Status visit(constraint::RequiredConstraint const & cons, Adapter auto const & document) const { NOOP_UNLESS_TYPE(Object); auto required = cons.properties; for (auto const & [key, _] : document.as_object()) { required.erase(key); } if (required.empty()) { return result(Status::Accept, "contains all required properties ", cons.properties); } return result(Status::Reject, "missing required properties ", required); } Status visit(constraint::UnevaluatedItemsConstraint const & cons, Adapter auto const & document) const { NOOP_UNLESS_TYPE(Array); if (not visited_) { return Status::Reject; } Status rval = Status::Accept; std::vector items; for (auto const & [index, item] : detail::enumerate(document.as_array())) { if (not VISITED(size_t).contains(index)) { VALIDATE_SUBSCHEMA_AND_MARK_LOCAL_VISIT(cons.subschema, item, index, items); } BREAK_EARLY_IF_NO_RESULT_TREE(); } annotate_list(items); return rval; } Status visit(constraint::UnevaluatedPropertiesConstraint const & cons, Adapter auto const & document) const { NOOP_UNLESS_TYPE(Object); if (not visited_) { return Status::Reject; } Status rval = Status::Accept; std::vector properties; for (auto const & [key, elem] : document.as_object()) { if (not VISITED(std::string).contains(key)) { VALIDATE_SUBSCHEMA_AND_MARK_LOCAL_VISIT(cons.subschema, elem, key, properties); } BREAK_EARLY_IF_NO_RESULT_TREE(); } annotate_list(properties); return rval; } /** * @brief The main entry point into the validator. Validates the provided * document according to the schema. */ Status validate(Adapter auto const & document) { // Step 1) Check if this is an always-false schema. Sometimes, this will // have a custom message. if (std::optional const & reject = schema_->rejects_all()) { if (should_annotate(Status::Reject)) { // This will only be run if we are interested in why something is // rejected. For example - `{ "not": false }` doesn't produce a // meaningful annotation... result_->error(where_, schema_path_, "", *reject); } // ...We do always record the result if a result object is present. (result_ ? result_->valid(where_, schema_path_, false) : void()); return Status::Reject; } if (schema_->accepts_all()) { // An accept-all schema is not No-Op for the purpose of unevaluated* (result_ ? result_->valid(where_, schema_path_, true) : void()); return Status::Accept; } // Begin tracking evaluations for unevaluated* keywords. The annotation // object is passed down from parent visitor to child visitor to allow all // schemas to mark whether they visited a certain item or property. VisitedAnnotation annotate; if (schema_->requires_result_context() and not visited_) { visited_ = &annotate; } Status rval = Status::Noop; // Before Draft2019_09, reference schemas could not coexist with other // constraints. This is enforced in the parsing of the schema, rather than // during validation {@see jvalidate::schema::Node::construct}. if (std::optional ref = schema_->reference_schema()) { rval = validate_subschema(*ref, document, "$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 &= std::visit([this, &document](auto & c) { return visit(c, document); }, *p_constraint); } // Post Constraints represent the unevaluatedItems and unevaluatedProperties // keywords. for (auto const & [key, p_constraint] : schema_->post_constraints()) { BREAK_EARLY_IF_NO_RESULT_TREE(); schema_path_ = current_schema / key; rval &= std::visit([this, &document](auto & c) { return visit(c, document); }, *p_constraint); } (result_ ? result_->valid(where_, current_schema, static_cast(rval)) : void()); return rval; } private: template requires(std::is_constructible_v) static std::string fmt(S const & str) { return str; } static std::string fmt(auto const &... args) { std::stringstream ss; using ::jvalidate::operator<<; [[maybe_unused]] int _[] = {(ss << args, 0)...}; return ss.str(); } static std::vector fmtlist(auto const & arg) { std::vector strs; for (auto const & elem : arg) { strs.push_back(fmt(elem)); } return strs; } bool should_annotate(Status stat) const { if (not result_) { return false; } switch (*tracking_) { case StoreResults::ForAnything: return stat != Status::Noop; case StoreResults::ForValid: return stat == Status::Accept; case StoreResults::ForInvalid: return stat == Status::Reject; } } #define ANNOTATION_HELPER(name, ADD, FMT) \ void name(auto const &... args) const { \ if (not result_) { \ /* do nothing if there's no result object to append to */ \ } else if (schema_path_.empty()) { \ result_->ADD(where_, schema_path_, "", FMT(args...)); \ } else { \ result_->ADD(where_, schema_path_.parent(), schema_path_.back(), FMT(args...)); \ } \ } ANNOTATION_HELPER(error, error, fmt) ANNOTATION_HELPER(annotate, annotate, fmt) ANNOTATION_HELPER(annotate_list, annotate, fmtlist) Status result(Status stat, auto const &... args) const { return (should_annotate(stat) ? error(args...) : void(), stat); } /** * @brief Walking function for entering a subschema. * * @param subschema The "subschema" being validated. This is either another * schema object (jvalidate::schema::Node), or a constraint. * @param keys... The path to this subschema, relative to the current schema * evaluation. * * @return The status of validating the current instance against the * subschema. */ template Status validate_subschema(constraint::SubConstraint const & subschema, Adapter auto const & document, K const &... keys) const { if (schema::Node const * const * ppschema = std::get_if<0>(&subschema)) { return validate_subschema(*ppschema, document, keys...); } else { return std::visit([this, &document](auto & c) { return visit(c, document); }, *std::get<1>(subschema)); } } /** * @brief Walking function for entering a subschema. Creates a new validation * visitor in order to continue evaluation. * * @param subschema The subschema being validated. * @param keys... The path to this subschema, relative to the current schema * evaluation. * * @return The status of validating the current instance against the * subschema. */ template Status validate_subschema(schema::Node const * subschema, Adapter auto const & document, K const &... keys) const { VisitedAnnotation annotate; ValidationVisitor next = *this; ((next.schema_path_ /= keys), ...); std::tie(next.schema_, next.visited_) = std::forward_as_tuple(subschema, visited_ ? &annotate : nullptr); Status rval = next.validate(document); if (rval == Status::Accept and visited_) { std::get<0>(*visited_).merge(std::get<0>(annotate)); std::get<1>(*visited_).merge(std::get<1>(annotate)); } return rval; } /** * @brief Walking function for entering a subschema and child document. * Creates a new validation visitor in order to continue evaluation. * * @param subschema The subschema being validated. * @param document The child document being evaluated. * @param key The path to this document instance. * * @return The status of validating the current instance against the * subschema. */ template Status validate_subschema_on(schema::Node const * subschema, Adapter auto const & document, K const & key) const { ValidationResult result; ValidationVisitor next = *this; next.where_ /= key; std::tie(next.schema_, next.result_, next.visited_) = std::forward_as_tuple(subschema, result_ ? &result : nullptr, nullptr); auto status = next.validate(document); if (status == Status::Accept and visited_) { VISITED(K).insert(key); } if (status == Status::Reject and result_) { result_->merge(std::move(result)); } return status; } }; }