From 370760e4d9f5a8582aa33262253321f50c226266 Mon Sep 17 00:00:00 2001 From: Juan Cruz Viotti Date: Wed, 21 Aug 2024 18:04:39 -0400 Subject: [PATCH] Hugely improve human-readable validation descriptions Signed-off-by: Juan Cruz Viotti --- DEPENDENCIES | 2 +- .../src/jsonschema/compile_describe.cc | 1139 +++++++++++++++-- .../src/jsonschema/compile_evaluate.cc | 4 +- .../src/jsonschema/compile_json.cc | 2 +- .../src/jsonschema/default_compiler_draft4.h | 4 +- .../jsontoolkit/jsonschema_compile.h | 29 +- 6 files changed, 1052 insertions(+), 128 deletions(-) diff --git a/DEPENDENCIES b/DEPENDENCIES index f2ee8c85..2917f639 100644 --- a/DEPENDENCIES +++ b/DEPENDENCIES @@ -1,4 +1,4 @@ vendorpull https://github.com/sourcemeta/vendorpull dea311b5bfb53b6926a4140267959ae334d3ecf4 noa https://github.com/sourcemeta/noa 7e26abce7a4e31e86a16ef2851702a56773ca527 -jsontoolkit https://github.com/sourcemeta/jsontoolkit 4d1dfef7be91ecadd810370b3d8a1d2e591bf574 +jsontoolkit https://github.com/sourcemeta/jsontoolkit c9b844557d3b116b272be5198ec547a7eee18347 hydra https://github.com/sourcemeta/hydra 3c53d3fdef79e9ba603d48470a508cc45472a0dc diff --git a/vendor/jsontoolkit/src/jsonschema/compile_describe.cc b/vendor/jsontoolkit/src/jsonschema/compile_describe.cc index ec37149f..6dba65d8 100644 --- a/vendor/jsontoolkit/src/jsonschema/compile_describe.cc +++ b/vendor/jsontoolkit/src/jsonschema/compile_describe.cc @@ -15,7 +15,11 @@ auto step_value(const SchemaCompilerStepValue &value) -> const T & { } template auto step_value(const T &step) -> decltype(auto) { - return step_value(step.value); + if constexpr (requires { step.value; }) { + return step_value(step.value); + } else { + return step.id; + } } auto to_string(const JSON::Type type) -> std::string { @@ -111,6 +115,12 @@ auto is_within_keyword(const Pointer &evaluate_path, }); } +auto unknown() -> std::string { + // In theory we should never get here + assert(false); + return ""; +} + struct DescribeVisitor { const bool valid; const Pointer &evaluate_path; @@ -119,9 +129,38 @@ struct DescribeVisitor { const JSON ⌖ const JSON &annotation; - auto operator()(const SchemaCompilerLogicalOr &) const -> std::string { - return "The target is expected to match at least one of the given " - "assertions"; + auto operator()(const SchemaCompilerAssertionFail &) const -> std::string { + if (this->keyword == "contains") { + return "The constraints declared for this keyword were not satisfiable"; + } + + if (this->keyword == "additionalProperties" || + this->keyword == "unevaluatedProperties") { + std::ostringstream message; + assert(!this->instance_location.empty()); + assert(this->instance_location.back().is_property()); + message << "The object value was not expected to define the property " + << escape_string(this->instance_location.back().to_property()); + return message.str(); + } + + assert(this->keyword.empty()); + return "No instance is expected to succeed against the false schema"; + } + + auto operator()(const SchemaCompilerLogicalOr &step) const -> std::string { + assert(!step.children.empty()); + std::ostringstream message; + message << "The " << to_string(this->target.type()) + << " value was expected to validate against "; + if (step.children.size() > 1) { + message << "at least one of the " << step.children.size() + << " given subschemas"; + } else { + message << "the given subschema"; + } + + return message.str(); } auto operator()(const SchemaCompilerLogicalAnd &step) const -> std::string { @@ -168,47 +207,366 @@ struct DescribeVisitor { return message.str(); } - return "The target is expected to match all of the given assertions"; - } + if (this->keyword == "patternProperties") { + assert(!step.children.empty()); + assert(this->target.is_object()); + std::ostringstream message; + message << "The object value was expected to validate against the "; + if (step.children.size() == 1) { + message << "single defined pattern property subschema"; + } else { + message << step.children.size() + << " defined pattern properties subschemas"; + } - auto operator()(const SchemaCompilerLogicalXor &) const -> std::string { - return "The target is expected to match one and only one of the given " - "assertions"; - } - auto operator()(const SchemaCompilerLogicalTry &) const -> std::string { - if (this->keyword == "if") { + return message.str(); + } + + if (this->keyword == "items" || this->keyword == "prefixItems") { + assert(!step.children.empty()); + assert(this->target.is_array()); std::ostringstream message; - message << "The " << to_string(this->target.type()) - << " value was tested against the conditional subschema"; + message << "The first "; + if (step.children.size() == 1) { + message << "item of the array value was"; + } else { + message << step.children.size() << " items of the array value were"; + } + + message << " expected to validate against the corresponding subschemas"; return message.str(); } - return "The target might match all of the given assertions"; - } - auto operator()(const SchemaCompilerLogicalNot &) const -> std::string { - return "The given schema is expected to not validate successfully"; - } - auto - operator()(const SchemaCompilerInternalContainer &) const -> std::string { - return "Internal"; - } - auto - operator()(const SchemaCompilerInternalAnnotation &) const -> std::string { - return "The target was annotated with the given value"; + if (this->keyword == "dependencies") { + assert(this->target.is_object()); + assert(!step.children.empty()); + + std::set present; + std::set present_with_schemas; + std::set present_with_properties; + std::set all_dependencies; + std::set required_properties; + + for (const auto &child : step.children) { + // Schema + if (std::holds_alternative(child)) { + const auto &substep{std::get(child)}; + assert(substep.condition.size() == 1); + assert(std::holds_alternative( + substep.condition.front())); + const auto &define{std::get( + substep.condition.front())}; + const auto &property{step_value(define)}; + all_dependencies.insert(property); + if (!this->target.defines(property)) { + continue; + } + + present.insert(property); + present_with_schemas.insert(property); + + // Properties + } else { + assert( + std::holds_alternative(child)); + const auto &substep{ + std::get(child)}; + assert(substep.condition.size() == 1); + assert(std::holds_alternative( + substep.condition.front())); + const auto &define{std::get( + substep.condition.front())}; + const auto &property{step_value(define)}; + all_dependencies.insert(property); + if (!this->target.defines(property)) { + continue; + } + + present.insert(property); + present_with_properties.insert(property); + + const auto &requirements{step_value(substep)}; + for (const auto &requirement : requirements) { + if (this->valid || !this->target.defines(requirement)) { + required_properties.insert(requirement); + } + } + } + } + + std::ostringstream message; + + if (present_with_schemas.empty() && present_with_properties.empty()) { + message << "The object value did not define the"; + assert(!all_dependencies.empty()); + if (all_dependencies.size() == 1) { + message << " property " + << escape_string(*(all_dependencies.cbegin())); + } else { + message << " properties "; + for (auto iterator = all_dependencies.cbegin(); + iterator != all_dependencies.cend(); ++iterator) { + if (std::next(iterator) == all_dependencies.cend()) { + message << "or " << escape_string(*iterator); + } else { + message << escape_string(*iterator) << ", "; + } + } + } + + return message.str(); + } + + if (present.size() == 1) { + message << "Because the object value defined the"; + message << " property " << escape_string(*(present.cbegin())); + } else { + message << "Because the object value defined the"; + message << " properties "; + for (auto iterator = present.cbegin(); iterator != present.cend(); + ++iterator) { + if (std::next(iterator) == present.cend()) { + message << "and " << escape_string(*iterator); + } else { + message << escape_string(*iterator) << ", "; + } + } + } + + if (!required_properties.empty()) { + message << ", it was also expected to define the"; + if (required_properties.size() == 1) { + message << " property " + << escape_string(*(required_properties.cbegin())); + } else { + message << " properties "; + for (auto iterator = required_properties.cbegin(); + iterator != required_properties.cend(); ++iterator) { + if (std::next(iterator) == required_properties.cend()) { + message << "and " << escape_string(*iterator); + } else { + message << escape_string(*iterator) << ", "; + } + } + } + } + + if (!present_with_schemas.empty()) { + message << ", "; + if (!required_properties.empty()) { + message << "and "; + } + + message << "it was also expected to successfully validate against the " + "corresponding "; + if (present_with_schemas.size() == 1) { + message << escape_string(*(present_with_schemas.cbegin())); + message << " subschema"; + } else { + for (auto iterator = present_with_schemas.cbegin(); + iterator != present_with_schemas.cend(); ++iterator) { + if (std::next(iterator) == present_with_schemas.cend()) { + message << "and " << escape_string(*iterator); + } else { + message << escape_string(*iterator) << ", "; + } + } + + message << " subschemas"; + } + } + + return message.str(); + } + + if (this->keyword == "dependentRequired") { + assert(!step.children.empty()); + assert(this->target.is_object()); + std::set present; + std::set all_dependencies; + std::set required; + for (const auto &child : step.children) { + assert(std::holds_alternative(child)); + const auto &substep{std::get(child)}; + assert(substep.condition.size() == 1); + assert(std::holds_alternative( + substep.condition.front())); + const auto &define{std::get( + substep.condition.front())}; + const auto &property{step_value(define)}; + all_dependencies.insert(property); + if (!this->target.defines(property)) { + continue; + } + + present.insert(property); + const auto &requirements{step_value(substep)}; + for (const auto &requirement : requirements) { + if (this->valid || !this->target.defines(requirement)) { + required.insert(requirement); + } + } + } + + std::ostringstream message; + + if (present.empty()) { + message << "The object value did not define the"; + assert(!all_dependencies.empty()); + if (all_dependencies.size() == 1) { + message << " property " + << escape_string(*(all_dependencies.cbegin())); + } else { + message << " properties "; + for (auto iterator = all_dependencies.cbegin(); + iterator != all_dependencies.cend(); ++iterator) { + if (std::next(iterator) == all_dependencies.cend()) { + message << "or " << escape_string(*iterator); + } else { + message << escape_string(*iterator) << ", "; + } + } + } + + return message.str(); + } else if (present.size() == 1) { + message << "Because the object value defined the"; + message << " property " << escape_string(*(present.cbegin())); + } else { + message << "Because the object value defined the"; + message << " properties "; + for (auto iterator = present.cbegin(); iterator != present.cend(); + ++iterator) { + if (std::next(iterator) == present.cend()) { + message << "and " << escape_string(*iterator); + } else { + message << escape_string(*iterator) << ", "; + } + } + } + + assert(!required.empty()); + message << ", it was also expected to define the"; + if (required.size() == 1) { + message << " property " << escape_string(*(required.cbegin())); + } else { + message << " properties "; + for (auto iterator = required.cbegin(); iterator != required.cend(); + ++iterator) { + if (std::next(iterator) == required.cend()) { + message << "and " << escape_string(*iterator); + } else { + message << escape_string(*iterator) << ", "; + } + } + } + + return message.str(); + } + + if (this->keyword == "dependentSchemas") { + assert(this->target.is_object()); + assert(!step.children.empty()); + std::set present; + std::set all_dependencies; + for (const auto &child : step.children) { + assert(std::holds_alternative(child)); + const auto &substep{std::get(child)}; + assert(substep.condition.size() == 1); + assert(std::holds_alternative( + substep.condition.front())); + const auto &define{std::get( + substep.condition.front())}; + const auto &property{step_value(define)}; + all_dependencies.insert(property); + if (!this->target.defines(property)) { + continue; + } + + present.insert(property); + } + + std::ostringstream message; + + if (present.empty()) { + message << "The object value did not define the"; + assert(!all_dependencies.empty()); + if (all_dependencies.size() == 1) { + message << " property " + << escape_string(*(all_dependencies.cbegin())); + } else { + message << " properties "; + for (auto iterator = all_dependencies.cbegin(); + iterator != all_dependencies.cend(); ++iterator) { + if (std::next(iterator) == all_dependencies.cend()) { + message << "or " << escape_string(*iterator); + } else { + message << escape_string(*iterator) << ", "; + } + } + } + } else if (present.size() == 1) { + message << "Because the object value defined the"; + message << " property " << escape_string(*(present.cbegin())); + message + << ", it was also expected to validate against the corresponding " + "subschema"; + } else { + message << "Because the object value defined the"; + message << " properties "; + for (auto iterator = present.cbegin(); iterator != present.cend(); + ++iterator) { + if (std::next(iterator) == present.cend()) { + message << "and " << escape_string(*iterator); + } else { + message << escape_string(*iterator) << ", "; + } + } + + message + << ", it was also expected to validate against the corresponding " + "subschemas"; + } + + return message.str(); + } + + return unknown(); } - auto operator()(const SchemaCompilerInternalNoAdjacentAnnotation &) const - -> std::string { - return "The target was not annotated with the given value at the same " - "schema location"; + + auto operator()(const SchemaCompilerLogicalXor &step) const -> std::string { + assert(!step.children.empty()); + std::ostringstream message; + message << "The " << to_string(this->target.type()) + << " value was expected to validate against "; + if (step.children.size() > 1) { + message << "one and only one of the " << step.children.size() + << " given subschemas"; + } else { + message << "the given subschema"; + } + + return message.str(); } - auto - operator()(const SchemaCompilerInternalNoAnnotation &) const -> std::string { - return "The target was not annotated with the given value"; + + auto operator()(const SchemaCompilerLogicalTry &) const -> std::string { + assert(this->keyword == "if"); + std::ostringstream message; + message << "The " << to_string(this->target.type()) + << " value was tested against the conditional subschema"; + return message.str(); } - auto - operator()(const SchemaCompilerInternalDefinesAll &) const -> std::string { - return "The target object is expected to define all of the given " - "properties"; + + auto operator()(const SchemaCompilerLogicalNot &) const -> std::string { + std::ostringstream message; + message + << "The " << to_string(this->target.type()) + << " value was expected to not validate against the given subschema"; + if (!this->valid) { + message << ", but it did"; + } + + return message.str(); } auto operator()(const SchemaCompilerControlLabel &) const -> std::string { @@ -223,9 +581,24 @@ struct DescribeVisitor { return describe_reference(this->target); } - auto operator()(const SchemaCompilerControlDynamicAnchorJump &) const + auto operator()(const SchemaCompilerControlDynamicAnchorJump &step) const -> std::string { - return "Jump to a dynamic anchor"; + if (this->keyword == "$dynamicRef") { + const auto &value{step_value(step)}; + std::ostringstream message; + message << "The " << to_string(target.type()) + << " value was expected to validate against the first subschema " + "in scope that declared the dynamic anchor " + << escape_string(value); + return message.str(); + } + + assert(this->keyword == "$recursiveRef"); + std::ostringstream message; + message << "The " << to_string(target.type()) + << " value was expected to validate against the first subschema " + "in scope that declared a recursive anchor"; + return message.str(); } auto operator()(const SchemaCompilerAnnotationPublic &) const -> std::string { @@ -238,53 +611,364 @@ struct DescribeVisitor { return message.str(); } - return "Emit an annotation"; - } + if (this->keyword == "properties") { + assert(this->annotation.is_string()); + std::ostringstream message; + message << "The object property " + << escape_string(this->annotation.to_string()) + << " successfully validated against its property " + "subschema"; + return message.str(); + } - auto operator()(const SchemaCompilerLoopProperties &) const -> std::string { - return "Loop over the properties of the target object"; - } + if (this->keyword == "unevaluatedProperties") { + assert(this->annotation.is_string()); + std::ostringstream message; + message << "The object property " + << escape_string(this->annotation.to_string()) + << " successfully validated against the subschema for " + "unevaluated properties"; + return message.str(); + } - auto operator()(const SchemaCompilerLoopKeys &) const -> std::string { - if (this->keyword == "propertyNames") { - assert(this->target.is_object()); + if (this->keyword == "patternProperties") { + assert(this->annotation.is_string()); std::ostringstream message; + message << "The object property " + << escape_string(this->annotation.to_string()) + << " successfully validated against its pattern property " + "subschema"; + return message.str(); + } - if (this->target.size() == 0) { - assert(this->valid); - message << "The object is empty and no properties are expected to " - "validate against the given subschema"; - } else if (this->target.size() == 1) { - message << "The object property "; - message << escape_string(this->target.as_object().cbegin()->first); - message << " is expected to validate against the given subschema"; + if (this->keyword == "additionalProperties") { + assert(this->annotation.is_string()); + std::ostringstream message; + message << "The object property " + << escape_string(this->annotation.to_string()) + << " successfully validated against the additional properties " + "subschema"; + return message.str(); + } + + if ((this->keyword == "items" || this->keyword == "additionalItems") && + this->annotation.is_boolean() && this->annotation.to_boolean()) { + assert(this->target.is_array()); + std::ostringstream message; + message << "At least one item of the array value successfully validated " + "against the given subschema"; + return message.str(); + } + + if (this->keyword == "unevaluatedItems" && this->annotation.is_boolean() && + this->annotation.to_boolean()) { + assert(this->target.is_array()); + std::ostringstream message; + message << "At least one item of the array value successfully validated " + "against the subschema for unevaluated items"; + return message.str(); + } + + if (this->keyword == "prefixItems" && this->annotation.is_boolean() && + this->annotation.to_boolean()) { + assert(this->target.is_array()); + std::ostringstream message; + message << "Every item of the array value validated against the given " + "positional subschemas"; + return message.str(); + } + + if ((this->keyword == "prefixItems" || this->keyword == "items") && + this->annotation.is_integer()) { + assert(this->target.is_array()); + assert(this->annotation.is_positive()); + std::ostringstream message; + if (this->annotation.to_integer() == 0) { + message << "The first item of the array value successfully validated " + "against the first " + "positional subschema"; } else { - message << "The object properties "; - for (auto iterator = this->target.as_object().cbegin(); - iterator != this->target.as_object().cend(); ++iterator) { - if (std::next(iterator) == this->target.as_object().cend()) { - message << "and " << escape_string(iterator->first); - } else { - message << escape_string(iterator->first) << ", "; - } + message << "The first " << this->annotation.to_integer() + 1 + << " items of the array value successfully validated against " + "the given " + "positional subschemas"; + } + + return message.str(); + } + + if (this->keyword == "contains" && this->annotation.is_integer()) { + assert(this->target.is_array()); + assert(this->annotation.is_positive()); + std::ostringstream message; + message << "The item at index " << this->annotation.to_integer() + << " of the array value successfully validated against the " + "containment check subschema"; + return message.str(); + } + + if (this->keyword == "title" || this->keyword == "description") { + assert(this->annotation.is_string()); + std::ostringstream message; + message << "The " << this->keyword << " of the"; + if (this->instance_location.empty()) { + message << " instance"; + } else { + message << " instance location \""; + stringify(this->instance_location, message); + message << "\""; + } + + message << " was " << escape_string(this->annotation.to_string()); + return message.str(); + } + + if (this->keyword == "default") { + std::ostringstream message; + message << "The default value of the"; + if (this->instance_location.empty()) { + message << " instance"; + } else { + message << " instance location \""; + stringify(this->instance_location, message); + message << "\""; + } + + message << " was "; + stringify(this->annotation, message); + return message.str(); + } + + if (this->keyword == "deprecated" && this->annotation.is_boolean()) { + std::ostringstream message; + if (this->instance_location.empty()) { + message << "The instance"; + } else { + message << "The instance location \""; + stringify(this->instance_location, message); + message << "\""; + } + + if (this->annotation.to_boolean()) { + message << " was considered deprecated"; + } else { + message << " was not considered deprecated"; + } + + return message.str(); + } + + if (this->keyword == "readOnly" && this->annotation.is_boolean()) { + std::ostringstream message; + if (this->instance_location.empty()) { + message << "The instance"; + } else { + message << "The instance location \""; + stringify(this->instance_location, message); + message << "\""; + } + + if (this->annotation.to_boolean()) { + message << " was considered read-only"; + } else { + message << " was not considered read-only"; + } + + return message.str(); + } + + if (this->keyword == "writeOnly" && this->annotation.is_boolean()) { + std::ostringstream message; + if (this->instance_location.empty()) { + message << "The instance"; + } else { + message << "The instance location \""; + stringify(this->instance_location, message); + message << "\""; + } + + if (this->annotation.to_boolean()) { + message << " was considered write-only"; + } else { + message << " was not considered write-only"; + } + + return message.str(); + } + + if (this->keyword == "examples") { + assert(this->annotation.is_array()); + std::ostringstream message; + if (this->instance_location.empty()) { + message << "Examples of the instance"; + } else { + message << "Examples of the instance location \""; + stringify(this->instance_location, message); + message << "\""; + } + + message << " were "; + for (auto iterator = this->annotation.as_array().cbegin(); + iterator != this->annotation.as_array().cend(); ++iterator) { + if (std::next(iterator) == this->annotation.as_array().cend()) { + message << "and "; + stringify(*iterator, message); + } else { + stringify(*iterator, message); + message << ", "; } + } + + return message.str(); + } + + if (this->keyword == "contentEncoding") { + assert(this->annotation.is_string()); + std::ostringstream message; + message << "The content encoding of the"; + if (this->instance_location.empty()) { + message << " instance"; + } else { + message << " instance location \""; + stringify(this->instance_location, message); + message << "\""; + } + + message << " was " << escape_string(this->annotation.to_string()); + return message.str(); + } - message << " are expected to validate against the given subschema"; + if (this->keyword == "contentMediaType") { + assert(this->annotation.is_string()); + std::ostringstream message; + message << "The content media type of the"; + if (this->instance_location.empty()) { + message << " instance"; + } else { + message << " instance location \""; + stringify(this->instance_location, message); + message << "\""; } + message << " was " << escape_string(this->annotation.to_string()); return message.str(); } - return "Loop over the property keys of the target object"; + if (this->keyword == "contentSchema") { + std::ostringstream message; + message << "When decoded, the"; + if (this->instance_location.empty()) { + message << " instance"; + } else { + message << " instance location \""; + stringify(this->instance_location, message); + message << "\""; + } + + message << " was expected to validate against the schema "; + stringify(this->annotation, message); + return message.str(); + } + + std::ostringstream message; + message << "The unrecognized keyword " << escape_string(this->keyword) + << " was collected as the annotation "; + stringify(this->annotation, message); + return message.str(); + } + + auto + operator()(const SchemaCompilerLoopProperties &step) const -> std::string { + if (this->keyword == "unevaluatedProperties") { + std::ostringstream message; + if (step.children.size() == 1 && + std::holds_alternative( + step.children.front()) && + std::holds_alternative( + std::get(step.children.front()) + .children.front())) { + message << "The object value was not expected to define unevaluated " + "properties"; + } else { + message << "The object properties not covered by other object " + "keywords were expected to validate against this subschema"; + } + + return message.str(); + } + + assert(this->keyword == "additionalProperties"); + std::ostringstream message; + if (step.children.size() == 1 && + std::holds_alternative( + step.children.front()) && + std::holds_alternative( + std::get(step.children.front()) + .children.front())) { + message << "The object value was not expected to define additional " + "properties"; + } else { + message << "The object properties not covered by other adjacent object " + "keywords were expected to validate against this subschema"; + } + + return message.str(); + } + + auto operator()(const SchemaCompilerLoopKeys &) const -> std::string { + assert(this->keyword == "propertyNames"); + assert(this->target.is_object()); + std::ostringstream message; + + if (this->target.size() == 0) { + assert(this->valid); + message << "The object is empty and no properties were expected to " + "validate against the given subschema"; + } else if (this->target.size() == 1) { + message << "The object property "; + message << escape_string(this->target.as_object().cbegin()->first); + message << " was expected to validate against the given subschema"; + } else { + message << "The object properties "; + for (auto iterator = this->target.as_object().cbegin(); + iterator != this->target.as_object().cend(); ++iterator) { + if (std::next(iterator) == this->target.as_object().cend()) { + message << "and " << escape_string(iterator->first); + } else { + message << escape_string(iterator->first) << ", "; + } + } + + message << " were expected to validate against the given subschema"; + } + + return message.str(); } - auto operator()(const SchemaCompilerLoopItems &) const -> std::string { - return "Loop over the items of the target array"; + auto operator()(const SchemaCompilerLoopItems &step) const -> std::string { + assert(this->target.is_array()); + const auto &value{step_value(step)}; + std::ostringstream message; + message << "Every item in the array value"; + if (value == 1) { + message << " except for the first one"; + } else if (value > 0) { + message << " except for the first " << value; + } + + message << " was expected to validate against the given subschema"; + return message.str(); } - auto operator()(const SchemaCompilerLoopItemsFromAnnotationIndex &) const + + auto operator()(const SchemaCompilerLoopItemsFromAnnotationIndex &step) const -> std::string { - return "Loop over the items of the target array potentially bound by an " - "annotation result"; + assert(this->keyword == "unevaluatedItems"); + const auto &value{step_value(step)}; + std::ostringstream message; + message << "The array items not evaluated by the keyword " + << escape_string(value) + << ", if any, were expected to validate against this subschema"; + return message.str(); } auto operator()(const SchemaCompilerLoopContains &step) const -> std::string { @@ -331,10 +1015,6 @@ struct DescribeVisitor { return message.str(); } - auto operator()(const SchemaCompilerAssertionFail &) const -> std::string { - return "Abort evaluation on failure"; - } - auto operator()(const SchemaCompilerAssertionDefines &step) const -> std::string { std::ostringstream message; @@ -480,7 +1160,73 @@ struct DescribeVisitor { return message.str(); } - return "The target size is expected to be greater than the given number"; + if (this->keyword == "minItems") { + assert(this->target.is_array()); + std::ostringstream message; + const auto minimum{step_value(step) + 1}; + message << "The array value was expected to contain at least " << minimum; + assert(minimum > 0); + if (minimum == 1) { + message << " item"; + } else { + message << " items"; + } + + if (this->valid) { + message << " and"; + } else { + message << " but"; + } + + message << " it contained " << this->target.size(); + if (this->target.size() == 1) { + message << " item"; + } else { + message << " items"; + } + + return message.str(); + } + + if (this->keyword == "minProperties") { + assert(this->target.is_object()); + std::ostringstream message; + const auto minimum{step_value(step) + 1}; + message << "The object value was expected to contain at least " + << minimum; + assert(minimum > 0); + if (minimum == 1) { + message << " property"; + } else { + message << " properties"; + } + + if (this->valid) { + message << " and"; + } else { + message << " but"; + } + + message << " it contained " << this->target.size(); + if (this->target.size() == 1) { + message << " property: "; + message << escape_string(this->target.as_object().cbegin()->first); + } else { + message << " properties: "; + for (auto iterator = this->target.as_object().cbegin(); + iterator != this->target.as_object().cend(); ++iterator) { + if (std::next(iterator) == this->target.as_object().cend()) { + message << "and " << escape_string(iterator->first); + } else { + message << escape_string(iterator->first) << ", "; + } + } + } + + return message.str(); + } + + return unknown(); } auto @@ -522,38 +1268,107 @@ struct DescribeVisitor { return message.str(); } - return "The target size is expected to be less than the given number"; - } + if (this->keyword == "maxItems") { + assert(this->target.is_array()); + std::ostringstream message; + const auto maximum{step_value(step) - 1}; + message << "The array value was expected to contain at most " << maximum; + assert(maximum > 0); + if (maximum == 1) { + message << " item"; + } else { + message << " items"; + } - auto - operator()(const SchemaCompilerAssertionSizeEqual &) const -> std::string { - return "The target size is expected to be equal to the given number"; - } + if (this->valid) { + message << " and"; + } else { + message << " but"; + } - auto - operator()(const SchemaCompilerAssertionEqual &step) const -> std::string { - if (this->keyword == "const") { + message << " it contained " << this->target.size(); + if (this->target.size() == 1) { + message << " item"; + } else { + message << " items"; + } + + return message.str(); + } + + if (this->keyword == "maxProperties") { + assert(this->target.is_object()); std::ostringstream message; - const auto &value{step_value(step)}; - message << "The " << to_string(this->target.type()) << " value "; - stringify(this->target, message); - message << " was expected to equal the " << to_string(value.type()) - << " constant "; - stringify(value, message); + const auto maximum{step_value(step) - 1}; + message << "The object value was expected to contain at most " << maximum; + assert(maximum > 0); + if (maximum == 1) { + message << " property"; + } else { + message << " properties"; + } + + if (this->valid) { + message << " and"; + } else { + message << " but"; + } + + message << " it contained " << this->target.size(); + if (this->target.size() == 1) { + message << " property: "; + message << escape_string(this->target.as_object().cbegin()->first); + } else { + message << " properties: "; + for (auto iterator = this->target.as_object().cbegin(); + iterator != this->target.as_object().cend(); ++iterator) { + if (std::next(iterator) == this->target.as_object().cend()) { + message << "and " << escape_string(iterator->first); + } else { + message << escape_string(iterator->first) << ", "; + } + } + } + return message.str(); } - return "The target is expected to be equal to the given value"; + return unknown(); } - auto operator()(const SchemaCompilerAssertionGreaterEqual &) const { - return "The target number is expected to be greater than or equal to the " - "given number"; - } auto - operator()(const SchemaCompilerAssertionLessEqual &) const -> std::string { - return "The target number is expected to be less than or equal to the " - "given number"; + operator()(const SchemaCompilerAssertionEqual &step) const -> std::string { + std::ostringstream message; + const auto &value{step_value(step)}; + message << "The " << to_string(this->target.type()) << " value "; + stringify(this->target, message); + message << " was expected to equal the " << to_string(value.type()) + << " constant "; + stringify(value, message); + return message.str(); + } + + auto operator()(const SchemaCompilerAssertionGreaterEqual &step) const { + std::ostringstream message; + const auto &value{step_value(step)}; + message << "The " << to_string(this->target.type()) << " value "; + stringify(this->target, message); + message << " was expected to be greater than or equal to the " + << to_string(value.type()) << " "; + stringify(value, message); + return message.str(); + } + + auto operator()(const SchemaCompilerAssertionLessEqual &step) const + -> std::string { + std::ostringstream message; + const auto &value{step_value(step)}; + message << "The " << to_string(this->target.type()) << " value "; + stringify(this->target, message); + message << " was expected to be less than or equal to the " + << to_string(value.type()) << " "; + stringify(value, message); + return message.str(); } auto @@ -589,7 +1404,44 @@ struct DescribeVisitor { } auto operator()(const SchemaCompilerAssertionUnique &) const -> std::string { - return "The target array is expected to not contain duplicates"; + assert(this->target.is_array()); + auto array{this->target.as_array()}; + std::ostringstream message; + if (this->valid) { + message << "The array value was expected to not contain duplicate items"; + } else { + std::set duplicates; + for (auto iterator = array.cbegin(); iterator != array.cend(); + ++iterator) { + for (auto subiterator = std::next(iterator); + subiterator != array.cend(); ++subiterator) { + if (*iterator == *subiterator) { + duplicates.insert(*iterator); + } + } + } + + assert(!duplicates.empty()); + message << "The array value contained the following duplicate"; + if (duplicates.size() == 1) { + message << " item: "; + stringify(*(duplicates.cbegin()), message); + } else { + message << " items: "; + for (auto subiterator = duplicates.cbegin(); + subiterator != duplicates.cend(); ++subiterator) { + if (std::next(subiterator) == duplicates.cend()) { + message << "and "; + stringify(*subiterator, message); + } else { + stringify(*subiterator, message); + message << ", "; + } + } + } + } + + return message.str(); } auto operator()(const SchemaCompilerAssertionDivisible &step) const @@ -604,13 +1456,83 @@ struct DescribeVisitor { return message.str(); } + auto operator()(const SchemaCompilerAssertionEqualsAny &step) const + -> std::string { + std::ostringstream message; + const auto &value{step_value(step)}; + message << "The " << to_string(this->target.type()) << " value "; + stringify(this->target, message); + assert(!value.empty()); + + if (value.size() == 1) { + message << " was expected to equal the " + << to_string(value.cbegin()->type()) << " constant "; + stringify(*(value.cbegin()), message); + } else { + if (this->valid) { + message << " was expected to equal one of the " << value.size() + << " declared values"; + } else { + message << " was expected to equal one of the following values: "; + for (auto iterator = value.cbegin(); iterator != value.cend(); + ++iterator) { + if (std::next(iterator) == value.cend()) { + message << "and "; + stringify(*iterator, message); + } else { + stringify(*iterator, message); + message << ", "; + } + } + } + } + + return message.str(); + } + + auto operator()(const SchemaCompilerAssertionStringType &step) const + -> std::string { + assert(this->target.is_string()); + std::ostringstream message; + message << "The string value " << escape_string(this->target.to_string()) + << " was expected to represent a valid"; + switch (step_value(step)) { + case SchemaCompilerValueStringType::URI: + message << " URI"; + break; + default: + return unknown(); + } + + return message.str(); + } + + // Internal steps that should never be described + // TODO: Can we get rid of these somehow? + + auto + operator()(const SchemaCompilerInternalSizeEqual &) const -> std::string { + return unknown(); + } auto - operator()(const SchemaCompilerAssertionStringType &) const -> std::string { - return "The target string is expected to match the given logical type"; + operator()(const SchemaCompilerInternalContainer &) const -> std::string { + return unknown(); } auto - operator()(const SchemaCompilerAssertionEqualsAny &) const -> std::string { - return "The target document is expected to be one of the given values"; + operator()(const SchemaCompilerInternalAnnotation &) const -> std::string { + return unknown(); + } + auto operator()(const SchemaCompilerInternalNoAdjacentAnnotation &) const + -> std::string { + return unknown(); + } + auto + operator()(const SchemaCompilerInternalNoAnnotation &) const -> std::string { + return unknown(); + } + auto + operator()(const SchemaCompilerInternalDefinesAll &) const -> std::string { + return unknown(); } }; @@ -618,14 +1540,17 @@ struct DescribeVisitor { namespace sourcemeta::jsontoolkit { +// TODO: What will unlock even better error messages is being able to +// get the subschema being evaluated along with the keyword auto describe(const bool valid, const SchemaCompilerTemplate::value_type &step, const Pointer &evaluate_path, const Pointer &instance_location, const JSON &instance, const JSON &annotation) -> std::string { - assert(evaluate_path.back().is_property()); + assert(evaluate_path.empty() || evaluate_path.back().is_property()); return std::visit( - DescribeVisitor{valid, evaluate_path, evaluate_path.back().to_property(), - instance_location, get(instance, instance_location), - annotation}, + DescribeVisitor{ + valid, evaluate_path, + evaluate_path.empty() ? "" : evaluate_path.back().to_property(), + instance_location, get(instance, instance_location), annotation}, step); } diff --git a/vendor/jsontoolkit/src/jsonschema/compile_evaluate.cc b/vendor/jsontoolkit/src/jsonschema/compile_evaluate.cc index 6263b721..10d83206 100644 --- a/vendor/jsontoolkit/src/jsonschema/compile_evaluate.cc +++ b/vendor/jsontoolkit/src/jsonschema/compile_evaluate.cc @@ -424,8 +424,8 @@ auto evaluate_step( result = (target.is_array() || target.is_object() || target.is_string()) && (target.size() < value); CALLBACK_POST(assertion); - } else if (std::holds_alternative(step)) { - const auto &assertion{std::get(step)}; + } else if (std::holds_alternative(step)) { + const auto &assertion{std::get(step)}; context.push(assertion); EVALUATE_CONDITION_GUARD(assertion, instance); CALLBACK_PRE(context.instance_location()); diff --git a/vendor/jsontoolkit/src/jsonschema/compile_json.cc b/vendor/jsontoolkit/src/jsonschema/compile_json.cc index d724a4b3..0efc651a 100644 --- a/vendor/jsontoolkit/src/jsonschema/compile_json.cc +++ b/vendor/jsontoolkit/src/jsonschema/compile_json.cc @@ -235,7 +235,6 @@ struct StepVisitor { HANDLE_STEP("assertion", "regex", SchemaCompilerAssertionRegex) HANDLE_STEP("assertion", "size-greater", SchemaCompilerAssertionSizeGreater) HANDLE_STEP("assertion", "size-less", SchemaCompilerAssertionSizeLess) - HANDLE_STEP("assertion", "size-equal", SchemaCompilerAssertionSizeEqual) HANDLE_STEP("assertion", "equal", SchemaCompilerAssertionEqual) HANDLE_STEP("assertion", "greater-equal", SchemaCompilerAssertionGreaterEqual) HANDLE_STEP("assertion", "less-equal", SchemaCompilerAssertionLessEqual) @@ -251,6 +250,7 @@ struct StepVisitor { HANDLE_STEP("logical", "xor", SchemaCompilerLogicalXor) HANDLE_STEP("logical", "try", SchemaCompilerLogicalTry) HANDLE_STEP("logical", "not", SchemaCompilerLogicalNot) + HANDLE_STEP("internal", "size-equal", SchemaCompilerInternalSizeEqual) HANDLE_STEP("internal", "annotation", SchemaCompilerInternalAnnotation) HANDLE_STEP("internal", "no-adjacent-annotation", SchemaCompilerInternalNoAdjacentAnnotation) diff --git a/vendor/jsontoolkit/src/jsonschema/default_compiler_draft4.h b/vendor/jsontoolkit/src/jsonschema/default_compiler_draft4.h index fb251e62..abcc8723 100644 --- a/vendor/jsontoolkit/src/jsonschema/default_compiler_draft4.h +++ b/vendor/jsontoolkit/src/jsonschema/default_compiler_draft4.h @@ -563,7 +563,7 @@ auto compiler_draft4_applicator_items_array( if (annotate) { subchildren.push_back(make( context, schema_context, relative_dynamic_context, JSON{true}, - {make( + {make( context, schema_context, relative_dynamic_context, cursor, {}, SchemaCompilerTargetType::Instance)}, SchemaCompilerTargetType::Instance)); @@ -591,7 +591,7 @@ auto compiler_draft4_applicator_items_array( children.push_back(make( context, schema_context, relative_dynamic_context, SchemaCompilerValueNone{}, std::move(subchildren), - {make( + {make( context, schema_context, relative_dynamic_context, cursor, {}, SchemaCompilerTargetType::Instance)})); } diff --git a/vendor/jsontoolkit/src/jsonschema/include/sourcemeta/jsontoolkit/jsonschema_compile.h b/vendor/jsontoolkit/src/jsonschema/include/sourcemeta/jsontoolkit/jsonschema_compile.h index 978996cb..dae34a3f 100644 --- a/vendor/jsontoolkit/src/jsonschema/include/sourcemeta/jsontoolkit/jsonschema_compile.h +++ b/vendor/jsontoolkit/src/jsonschema/include/sourcemeta/jsontoolkit/jsonschema_compile.h @@ -165,12 +165,6 @@ struct SchemaCompilerAssertionSizeGreater; /// respectively struct SchemaCompilerAssertionSizeLess; -/// @ingroup jsonschema -/// Represents a compiler assertion step that checks a given array, object, or -/// string has a certain number of items, properties, or characters, -/// respectively -struct SchemaCompilerAssertionSizeEqual; - /// @ingroup jsonschema /// Represents a compiler assertion step that checks the instance equals a given /// JSON document @@ -241,6 +235,12 @@ struct SchemaCompilerLogicalTry; /// Represents a compiler logical step that represents a negation struct SchemaCompilerLogicalNot; +/// @ingroup jsonschema +/// Represents a compiler assertion step that checks a given array, object, or +/// string has a certain number of items, properties, or characters, +/// respectively +struct SchemaCompilerInternalSizeEqual; + /// @ingroup jsonschema /// Represents a hidden compiler assertion step that checks a certain /// annotation was produced @@ -314,15 +314,14 @@ using SchemaCompilerTemplate = std::vector