|
|
@@ -1,8 +1,8 @@
|
|
|
#pragma once
|
|
|
|
|
|
+#include <algorithm>
|
|
|
#include <tuple>
|
|
|
#include <type_traits>
|
|
|
-#include <unordered_map>
|
|
|
#include <vector>
|
|
|
|
|
|
#include <jvalidate/compat/enumerate.h>
|
|
|
@@ -26,9 +26,10 @@
|
|
|
|
|
|
#define VISITED(type) std::get<std::unordered_set<type>>(*visited_)
|
|
|
|
|
|
-#define VALIDATE_SUBSCHEMA_AND_MARK_LOCAL_VISIT(subschema, subinstance, path, local_visited) \
|
|
|
+#define VALIDATE_SUBSCHEMA_AND_MARK_LOCAL_VISIT(subschema, subinstance, path, local_visited, ...) \
|
|
|
do { \
|
|
|
- Status const partial = validate_subschema_on(subschema, subinstance, path); \
|
|
|
+ Status const partial = \
|
|
|
+ validate_subschema_on(subschema, subinstance, path __VA_OPT__(, ) __VA_ARGS__); \
|
|
|
rval &= partial; \
|
|
|
if (result_ and partial != Status::Noop) { \
|
|
|
local_visited.insert(local_visited.end(), path); \
|
|
|
@@ -83,6 +84,11 @@ public:
|
|
|
: schema_(&schema), result_(result), cfg_(cfg), extension_(extension), regex_(regex) {}
|
|
|
|
|
|
Status visit(constraint::ExtensionConstraint const & cons, Adapter auto const & document) const {
|
|
|
+ // Because we don't provide any contract constraint on our ExtensionVisitor,
|
|
|
+ // we instead defer it to here where we validate that the extension can be
|
|
|
+ // validated given the input document.
|
|
|
+ // This covers a case where we write the extension around a specific adapter
|
|
|
+ // instead of generically.
|
|
|
if constexpr (std::is_invocable_r_v<Status, ExtensionVisitor, decltype(cons),
|
|
|
decltype(document), ValidationVisitor const &>) {
|
|
|
return extension_(cons, document, *this);
|
|
|
@@ -95,20 +101,35 @@ public:
|
|
|
adapter::Type const type = document.type();
|
|
|
|
|
|
for (adapter::Type const accept : cons.types) {
|
|
|
- if (type == accept) {
|
|
|
+ if (type == accept) { // Simple case, types are equal
|
|
|
return result(Status::Accept, type, " is in types [", cons.types, "]");
|
|
|
}
|
|
|
if (accept == adapter::Type::Number && type == adapter::Type::Integer) {
|
|
|
+ // Number is a super-type of Integer, therefore all Integer values are
|
|
|
+ // accepted by a `"type": "number"` schema.
|
|
|
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())) {
|
|
|
+ // Since the JSON specification does not distinguish between Number
|
|
|
+ // and Integer, but JSON Schema does, we need to check that the number
|
|
|
+ // is a whole integer that is representable within the system (64-bit).
|
|
|
return result(Status::Accept, type, " is in types [", cons.types, "]");
|
|
|
}
|
|
|
}
|
|
|
return result(Status::Reject, type, " is not in types [", cons.types, "]");
|
|
|
}
|
|
|
|
|
|
+ Status visit(constraint::ConstConstraint const & cons, Adapter auto const & document) const {
|
|
|
+ auto is_equal = [this, &document](auto const & frozen) {
|
|
|
+ return document.equals(frozen, cfg_.strict_equality);
|
|
|
+ };
|
|
|
+ if (cons.value->apply(is_equal)) {
|
|
|
+ return result(Status::Accept, "matches value");
|
|
|
+ }
|
|
|
+ return result(Status::Reject, cons.value, " was expected");
|
|
|
+ }
|
|
|
+
|
|
|
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);
|
|
|
@@ -118,7 +139,7 @@ public:
|
|
|
return result(Status::Accept, index);
|
|
|
}
|
|
|
}
|
|
|
- return Status::Reject;
|
|
|
+ return result(Status::Reject, document, " value is not one of ", cons.enumeration);
|
|
|
}
|
|
|
|
|
|
Status visit(constraint::AllOfConstraint const & cons, Adapter auto const & document) const {
|
|
|
@@ -143,6 +164,9 @@ public:
|
|
|
std::optional<size_t> first_validated;
|
|
|
for (auto const & [index, subschema] : detail::enumerate(cons.children)) {
|
|
|
if (validate_subschema(subschema, document, index)) {
|
|
|
+ // This technically will produce different results when we're tracking
|
|
|
+ // visited nodes, but in practice it doesn't actually matter which
|
|
|
+ // subschema index we record in the annotation.
|
|
|
first_validated = index;
|
|
|
}
|
|
|
if (not visited_ && first_validated.has_value()) {
|
|
|
@@ -274,12 +298,15 @@ public:
|
|
|
}
|
|
|
|
|
|
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
|
|
|
+ // https://json-schema.org/draft/2020-12/json-schema-validation#name-defined-formats
|
|
|
NOOP_UNLESS_TYPE(String);
|
|
|
|
|
|
annotate(cons.format);
|
|
|
if (not cfg_.validate_format && not cons.is_assertion) {
|
|
|
- return true;
|
|
|
+ // Don't both validating formats if we're not in assertion mode
|
|
|
+ // Assertion mode is specified either by using the appropriate "$vocab"
|
|
|
+ // meta-schema or by requesting it in the ValidationConfig.
|
|
|
+ return true; // TODO: I think this can be made into Noop
|
|
|
}
|
|
|
|
|
|
switch (FormatValidator(&RE::is_regex)(cons.format, document.as_string())) {
|
|
|
@@ -373,6 +400,8 @@ public:
|
|
|
NOOP_UNLESS_TYPE(Array);
|
|
|
|
|
|
if constexpr (std::totally_ordered<A>) {
|
|
|
+ // If the adapter defines comparison operators, then it becomes possible
|
|
|
+ // to compute uniqueness in O(n*log(n)) checks.
|
|
|
std::map<A, size_t> cache;
|
|
|
for (auto const & [index, elem] : detail::enumerate(document.as_array())) {
|
|
|
if (auto [it, created] = cache.emplace(elem, index); not created) {
|
|
|
@@ -380,6 +409,9 @@ public:
|
|
|
}
|
|
|
}
|
|
|
} else {
|
|
|
+ // Otherwise, we need to run an O(n^2) triangular array search comparing
|
|
|
+ // equality for each element. This still guarantees that each element is
|
|
|
+ // compared against each other element no more than once.
|
|
|
auto array = document.as_array();
|
|
|
for (size_t i = 0; i < array.size(); ++i) {
|
|
|
for (size_t j = i + 1; j < array.size(); ++j) {
|
|
|
@@ -398,12 +430,9 @@ public:
|
|
|
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;
|
|
|
+ return std::ranges::any_of(cons.patterns, [this, &key](auto const & pattern) {
|
|
|
+ return regex_.search(pattern, key);
|
|
|
+ });
|
|
|
};
|
|
|
|
|
|
Status rval = Status::Accept;
|
|
|
@@ -498,6 +527,15 @@ public:
|
|
|
auto object = document.as_object();
|
|
|
|
|
|
if constexpr (MutableAdapter<A>) {
|
|
|
+ // Special Rule - if the adapter is of a mutable json document (wraps a
|
|
|
+ // non-const reference and exposes the assign function) we will process
|
|
|
+ // the "default" annotation will be applied.
|
|
|
+ // https://json-schema.org/draft/2020-12/json-schema-validation#section-9.2
|
|
|
+ //
|
|
|
+ // Although the JSON Schema draft only says the the default value ought be
|
|
|
+ // valid against the schema, this implementation will assure that it is
|
|
|
+ // valid against this PropertiesConstraint, and any other constraints that
|
|
|
+ // are run after this one.
|
|
|
for (auto const & [key, subschema] : cons.properties) {
|
|
|
auto const * default_value = subschema->default_value();
|
|
|
if (default_value && not object.contains(key)) {
|
|
|
@@ -509,7 +547,7 @@ public:
|
|
|
std::vector<std::string> 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);
|
|
|
+ VALIDATE_SUBSCHEMA_AND_MARK_LOCAL_VISIT(it->second, elem, key, properties, key);
|
|
|
}
|
|
|
BREAK_EARLY_IF_NO_RESULT_TREE();
|
|
|
}
|
|
|
@@ -524,7 +562,6 @@ public:
|
|
|
|
|
|
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"));
|
|
|
}
|
|
|
@@ -624,14 +661,20 @@ public:
|
|
|
// constraints. This is enforced in the parsing of the schema, rather than
|
|
|
// during validation {@see jvalidate::schema::Node::construct}.
|
|
|
if (std::optional<schema::Node const *> ref = schema_->reference_schema()) {
|
|
|
+ // TODO: Investigate why this seems to produce .../$ref/$ref pointers
|
|
|
rval = validate_subschema(*ref, document, "$ref");
|
|
|
}
|
|
|
|
|
|
+ if (result_ && !schema_->description().empty()) {
|
|
|
+ result_->annotate(where_, schema_path_, "description", schema_->description());
|
|
|
+ }
|
|
|
+
|
|
|
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);
|
|
|
+ rval &= std::visit([this, &document](auto & c) { return this->visit(c, document); },
|
|
|
+ *p_constraint);
|
|
|
}
|
|
|
|
|
|
// Post Constraints represent the unevaluatedItems and unevaluatedProperties
|
|
|
@@ -639,7 +682,8 @@ public:
|
|
|
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);
|
|
|
+ rval &= std::visit([this, &document](auto & c) { return this->visit(c, document); },
|
|
|
+ *p_constraint);
|
|
|
}
|
|
|
|
|
|
(result_ ? result_->valid(where_, current_schema, static_cast<bool>(rval)) : void());
|
|
|
@@ -649,10 +693,13 @@ public:
|
|
|
private:
|
|
|
template <typename S>
|
|
|
requires(std::is_constructible_v<std::string, S>)
|
|
|
+ // Optimization to avoid running string-like objects through a
|
|
|
+ // std::stringstream in fmtlist.
|
|
|
static std::string fmt(S const & str) {
|
|
|
return str;
|
|
|
}
|
|
|
|
|
|
+ // Format va_args into a single string to annotate or mark an error message
|
|
|
static std::string fmt(auto const &... args) {
|
|
|
std::stringstream ss;
|
|
|
using ::jvalidate::operator<<;
|
|
|
@@ -660,6 +707,8 @@ private:
|
|
|
return ss.str();
|
|
|
}
|
|
|
|
|
|
+ // Format an iterable argument into a vector of strings to annotate or mark
|
|
|
+ // an error.
|
|
|
static std::vector<std::string> fmtlist(auto const & arg) {
|
|
|
std::vector<std::string> strs;
|
|
|
for (auto const & elem : arg) {
|
|
|
@@ -718,7 +767,7 @@ private:
|
|
|
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); },
|
|
|
+ return std::visit([this, &document](auto & c) { return this->visit(c, document); },
|
|
|
*std::get<1>(subschema));
|
|
|
}
|
|
|
}
|
|
|
@@ -746,6 +795,8 @@ private:
|
|
|
|
|
|
Status rval = next.validate(document);
|
|
|
|
|
|
+ // Only update the visited annotation of the current context if the
|
|
|
+ // subschema validates as Accepted.
|
|
|
if (rval == Status::Accept and visited_) {
|
|
|
std::get<0>(*visited_).merge(std::get<0>(annotate));
|
|
|
std::get<1>(*visited_).merge(std::get<1>(annotate));
|
|
|
@@ -760,28 +811,35 @@ private:
|
|
|
* @param subschema The subschema being validated.
|
|
|
* @param document The child document being evaluated.
|
|
|
* @param key The path to this document instance.
|
|
|
+ * @param schema_keys... The path to this subschema, relative to the current
|
|
|
+ * schema evaluation.
|
|
|
*
|
|
|
* @return The status of validating the current instance against the
|
|
|
* subschema.
|
|
|
*/
|
|
|
template <typename K>
|
|
|
Status validate_subschema_on(schema::Node const * subschema, Adapter auto const & document,
|
|
|
- K const & key) const {
|
|
|
+ K const & key, auto const &... schema_keys) const {
|
|
|
ValidationResult result;
|
|
|
|
|
|
ValidationVisitor next = *this;
|
|
|
next.where_ /= key;
|
|
|
+ ((next.schema_path_ /= schema_keys), ...);
|
|
|
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_) {
|
|
|
+ Status rval = next.validate(document);
|
|
|
+ // Only update the visited annotation of the current context if the
|
|
|
+ // subschema validates as Accepted.
|
|
|
+ if (rval == Status::Accept and visited_) {
|
|
|
VISITED(K).insert(key);
|
|
|
}
|
|
|
- if (status == Status::Reject and result_) {
|
|
|
+ // Update the annotation/error content only if a failure is being reported,
|
|
|
+ // or if we are in an "if" schema.
|
|
|
+ if ((rval == Status::Reject or tracking_ == StoreResults::ForAnything) and result_) {
|
|
|
result_->merge(std::move(result));
|
|
|
}
|
|
|
- return status;
|
|
|
+ return rval;
|
|
|
}
|
|
|
};
|
|
|
}
|