From a19dbbb6b0bd14155d21077a90b73244aa173659 Mon Sep 17 00:00:00 2001 From: Juan Cruz Viotti Date: Tue, 28 May 2024 17:55:10 +0100 Subject: [PATCH] Better implement human friendly validate error messages Signed-off-by: Juan Cruz Viotti --- DEPENDENCIES | 2 +- src/utils.cc | 13 +- src/utils.h | 2 + test/validate_fail.sh | 1 + vendor/jsontoolkit/Makefile.uk | 1 + .../jsontoolkit/src/jsonschema/CMakeLists.txt | 2 +- .../src/jsonschema/compile_describe.cc | 111 +++++++++++++++ .../src/jsonschema/compile_evaluate.cc | 21 ++- .../src/jsonschema/compile_json.cc | 16 ++- .../src/jsonschema/default_compiler_draft4.h | 134 +++++++++++------- .../jsontoolkit/jsonschema_compile.h | 49 +++++-- 11 files changed, 269 insertions(+), 83 deletions(-) create mode 100644 vendor/jsontoolkit/src/jsonschema/compile_describe.cc diff --git a/DEPENDENCIES b/DEPENDENCIES index e63ee5f1..2b208b48 100644 --- a/DEPENDENCIES +++ b/DEPENDENCIES @@ -1,3 +1,3 @@ vendorpull https://github.com/sourcemeta/vendorpull dea311b5bfb53b6926a4140267959ae334d3ecf4 noa https://github.com/sourcemeta/noa 5ff4024902642afc9cc2f9a9e02ae9dff9d15d4f -jsontoolkit https://github.com/sourcemeta/jsontoolkit dc7e1e21853f2b26cac572d5dc4c3dc50eeda935 +jsontoolkit https://github.com/sourcemeta/jsontoolkit 2775ccc05af7be64dd2532694bb17274185aeec1 diff --git a/src/utils.cc b/src/utils.cc index d6e30487..d1925920 100644 --- a/src/utils.cc +++ b/src/utils.cc @@ -101,20 +101,23 @@ auto parse_options(const std::span &arguments, auto pretty_evaluate_callback( bool result, - const sourcemeta::jsontoolkit::SchemaCompilerTemplate::value_type &, + const sourcemeta::jsontoolkit::SchemaCompilerTemplate::value_type &step, const sourcemeta::jsontoolkit::Pointer &evaluate_path, const sourcemeta::jsontoolkit::Pointer &instance_location, + const sourcemeta::jsontoolkit::JSON &, const sourcemeta::jsontoolkit::JSON &) -> void { if (result) { return; } - // TODO: Improve this pretty terrible output - std::cerr << "✗ \""; + std::cerr << "error: " << sourcemeta::jsontoolkit::describe(step) << "\n"; + std::cerr << " at instance location \""; sourcemeta::jsontoolkit::stringify(instance_location, std::cerr); - std::cerr << "\" at evaluate path (\""; + std::cerr << "\"\n"; + + std::cerr << " at evaluate path \""; sourcemeta::jsontoolkit::stringify(evaluate_path, std::cerr); - std::cerr << "\")\n"; + std::cerr << "\"\n"; } auto resolver(const std::map> &options) diff --git a/src/utils.h b/src/utils.h index 2fa6094a..5c4f09ef 100644 --- a/src/utils.h +++ b/src/utils.h @@ -2,6 +2,7 @@ #define INTELLIGENCE_JSONSCHEMA_CLI_UTILS_H_ #include +#include #include #include // std::filesystem @@ -37,6 +38,7 @@ auto pretty_evaluate_callback( const sourcemeta::jsontoolkit::SchemaCompilerTemplate::value_type &, const sourcemeta::jsontoolkit::Pointer &evaluate_path, const sourcemeta::jsontoolkit::Pointer &instance_location, + const sourcemeta::jsontoolkit::JSON &, const sourcemeta::jsontoolkit::JSON &) -> void; auto resolver(const std::map> &options) diff --git a/test/validate_fail.sh b/test/validate_fail.sh index 0cea7776..781778be 100755 --- a/test/validate_fail.sh +++ b/test/validate_fail.sh @@ -11,6 +11,7 @@ trap clean EXIT cat << 'EOF' > "$TMP/schema.json" { "$schema": "http://json-schema.org/draft-04/schema#", + "type": "object", "properties": { "foo": { "type": "string" diff --git a/vendor/jsontoolkit/Makefile.uk b/vendor/jsontoolkit/Makefile.uk index a2c864fd..ca1d1062 100644 --- a/vendor/jsontoolkit/Makefile.uk +++ b/vendor/jsontoolkit/Makefile.uk @@ -57,6 +57,7 @@ LIBJSONTOOLKIT_SRCS-y += $(LIBJSONTOOLKIT_SRC)/jsonschema/resolver.cc LIBJSONTOOLKIT_SRCS-y += $(LIBJSONTOOLKIT_SRC)/jsonschema/compile.cc LIBJSONTOOLKIT_SRCS-y += $(LIBJSONTOOLKIT_SRC)/jsonschema/compile_evaluate.cc LIBJSONTOOLKIT_SRCS-y += $(LIBJSONTOOLKIT_SRC)/jsonschema/compile_json.cc +LIBJSONTOOLKIT_SRCS-y += $(LIBJSONTOOLKIT_SRC)/jsonschema/compile_describe.cc LIBJSONTOOLKIT_SRCS-y += $(LIBJSONTOOLKIT_SRC)/jsonschema/default_compiler.cc # TODO: Can we do this with standard POSIX tools? diff --git a/vendor/jsontoolkit/src/jsonschema/CMakeLists.txt b/vendor/jsontoolkit/src/jsonschema/CMakeLists.txt index 3d3b1592..412f8d1e 100644 --- a/vendor/jsontoolkit/src/jsonschema/CMakeLists.txt +++ b/vendor/jsontoolkit/src/jsonschema/CMakeLists.txt @@ -8,7 +8,7 @@ noa_library(NAMESPACE sourcemeta PROJECT jsontoolkit NAME jsonschema error.h transformer.h transform_rule.h transform_bundle.h compile.h SOURCES jsonschema.cc default_walker.cc reference.cc anchor.cc resolver.cc walker.cc bundle.cc transformer.cc transform_rule.cc transform_bundle.cc - compile.cc compile_evaluate.cc compile_json.cc + compile.cc compile_evaluate.cc compile_json.cc compile_describe.cc compile_helpers.h default_compiler.cc default_compiler_2020_12.h default_compiler_draft6.h diff --git a/vendor/jsontoolkit/src/jsonschema/compile_describe.cc b/vendor/jsontoolkit/src/jsonschema/compile_describe.cc new file mode 100644 index 00000000..d777ba9c --- /dev/null +++ b/vendor/jsontoolkit/src/jsonschema/compile_describe.cc @@ -0,0 +1,111 @@ +#include + +#include // std::visit + +namespace { +using namespace sourcemeta::jsontoolkit; + +struct DescribeVisitor { + auto operator()(const SchemaCompilerLogicalOr &) const -> std::string { + return "The target is expected to match at least one of the given " + "assertions"; + } + auto operator()(const SchemaCompilerLogicalAnd &) const -> std::string { + return "The target is expected to match all of the given assertions"; + } + 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 SchemaCompilerLogicalNot &) const -> std::string { + return "The given schema is expected to not validate successfully"; + } + auto operator()(const SchemaCompilerControlLabel &) const -> std::string { + return "Mark the current position of the evaluation process for future " + "jumps"; + } + auto operator()(const SchemaCompilerControlJump &) const -> std::string { + return "Jump to another point of the evaluation process"; + } + auto operator()(const SchemaCompilerAnnotationPublic &) const -> std::string { + return "Emit an annotation"; + } + auto + operator()(const SchemaCompilerAnnotationPrivate &) const -> std::string { + return "Emit an internal annotation"; + } + auto operator()(const SchemaCompilerLoopProperties &) const -> std::string { + return "Loop over the properties of the target object"; + } + auto operator()(const SchemaCompilerLoopItems &) const -> std::string { + return "Loop over the items of the target array"; + } + auto operator()(const SchemaCompilerAssertionFail &) const -> std::string { + return "Abort evaluation on failure"; + } + auto operator()(const SchemaCompilerAssertionDefines &) const -> std::string { + return "The target object is expected to define the given property"; + } + auto operator()(const SchemaCompilerAssertionType &) const -> std::string { + return "The target document is expected to be of the given type"; + } + auto operator()(const SchemaCompilerAssertionTypeAny &) const -> std::string { + return "The target document is expected to be of one of the given types"; + } + auto operator()(const SchemaCompilerAssertionRegex &) const -> std::string { + return "The target string is expected to match the given regular " + "expression"; + } + auto + operator()(const SchemaCompilerAssertionNotContains &) const -> std::string { + return "The target array is expected to not contain the given value"; + } + auto + operator()(const SchemaCompilerAssertionSizeGreater &) const -> std::string { + return "The target size is expected to be greater than the given number"; + } + auto + operator()(const SchemaCompilerAssertionSizeLess &) const -> std::string { + return "The target size is expected to be less than the given number"; + } + + auto operator()(const SchemaCompilerAssertionEqual &) const -> std::string { + return "The target size is expected to be equal to the given number"; + } + 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"; + } + auto operator()(const SchemaCompilerAssertionGreater &) const -> std::string { + return "The target number is expected to be greater than the given number"; + } + auto operator()(const SchemaCompilerAssertionLess &) const -> std::string { + return "The target number is expected to be less than the given number"; + } + auto operator()(const SchemaCompilerAssertionUnique &) const -> std::string { + return "The target array is expected to not contain duplicates"; + } + auto + operator()(const SchemaCompilerAssertionDivisible &) const -> std::string { + return "The target number is expected to be divisible by the given number"; + } + auto + operator()(const SchemaCompilerAssertionStringType &) const -> std::string { + return "The target string is expected to match the given logical type"; + } +}; + +} // namespace + +namespace sourcemeta::jsontoolkit { + +auto describe(const SchemaCompilerTemplate::value_type &step) -> std::string { + return std::visit(DescribeVisitor{}, step); +} + +} // namespace sourcemeta::jsontoolkit diff --git a/vendor/jsontoolkit/src/jsonschema/compile_evaluate.cc b/vendor/jsontoolkit/src/jsonschema/compile_evaluate.cc index ea8ac090..af0b0046 100644 --- a/vendor/jsontoolkit/src/jsonschema/compile_evaluate.cc +++ b/vendor/jsontoolkit/src/jsonschema/compile_evaluate.cc @@ -124,9 +124,9 @@ class EvaluationContext { } template - auto - resolve_value(const sourcemeta::jsontoolkit::SchemaCompilerValue &value, - const JSON &instance) -> T { + auto resolve_value( + const sourcemeta::jsontoolkit::SchemaCompilerStepValue &value, + const JSON &instance) -> T { using namespace sourcemeta::jsontoolkit; // We only define target resolution for JSON documents, at least for now if constexpr (std::is_same_v) { @@ -166,6 +166,7 @@ auto callback_noop( bool, const sourcemeta::jsontoolkit::SchemaCompilerTemplate::value_type &, const sourcemeta::jsontoolkit::Pointer &, const sourcemeta::jsontoolkit::Pointer &, + const sourcemeta::jsontoolkit::JSON &, const sourcemeta::jsontoolkit::JSON &) noexcept -> void {} auto evaluate_step( @@ -207,6 +208,14 @@ auto evaluate_step( const auto &target{ context.resolve_target(assertion.target, instance)}; result = target.type() == value; + } else if (std::holds_alternative(step)) { + const auto &assertion{std::get(step)}; + context.push(assertion); + EVALUATE_CONDITION_GUARD(assertion.condition, instance); + const auto &value{context.resolve_value(assertion.value, instance)}; + const auto &target{ + context.resolve_target(assertion.target, instance)}; + result = value.contains(target.type()); } else if (std::holds_alternative(step)) { const auto &assertion{std::get(step)}; context.push(assertion); @@ -450,7 +459,7 @@ auto evaluate_step( // Otherwise we risk confusing consumers if (value.second) { callback(result, step, context.evaluate_path(), current_instance_location, - value.first); + instance, value.first); } context.pop(); @@ -472,7 +481,7 @@ auto evaluate_step( // While this is a private annotation, we still emit it on the callback // for implementing debugging-related tools, etc callback(result, step, context.evaluate_path(), current_instance_location, - value.first); + instance, value.first); } context.pop(); @@ -543,7 +552,7 @@ auto evaluate_step( #undef EVALUATE_CONDITION_GUARD evaluate_step_end: callback(result, step, context.evaluate_path(), context.instance_location(), - context.value(nullptr)); + instance, context.value(nullptr)); context.pop(); return result; } diff --git a/vendor/jsontoolkit/src/jsonschema/compile_json.cc b/vendor/jsontoolkit/src/jsonschema/compile_json.cc index c282d444..6e59fc0c 100644 --- a/vendor/jsontoolkit/src/jsonschema/compile_json.cc +++ b/vendor/jsontoolkit/src/jsonschema/compile_json.cc @@ -35,8 +35,8 @@ auto target_to_json(const sourcemeta::jsontoolkit::SchemaCompilerTarget &target) } template -auto value_to_json(const sourcemeta::jsontoolkit::SchemaCompilerValue &value) - -> sourcemeta::jsontoolkit::JSON { +auto value_to_json(const sourcemeta::jsontoolkit::SchemaCompilerStepValue + &value) -> sourcemeta::jsontoolkit::JSON { using namespace sourcemeta::jsontoolkit; if (std::holds_alternative(value)) { return target_to_json(std::get(value)); @@ -59,6 +59,17 @@ auto value_to_json(const sourcemeta::jsontoolkit::SchemaCompilerValue &value) type_string << std::get(value); result.assign("value", JSON{type_string.str()}); return result; + } else if constexpr (std::is_same_v) { + result.assign("type", JSON{"types"}); + JSON types{JSON::make_array()}; + for (const auto type : std::get(value)) { + std::ostringstream type_string; + type_string << type; + types.push_back(JSON{type_string.str()}); + } + + result.assign("value", std::move(types)); + return result; } else if constexpr (std::is_same_v) { result.assign("type", JSON{"string"}); result.assign("value", JSON{std::get(value)}); @@ -145,6 +156,7 @@ struct StepVisitor { HANDLE_STEP("assertion", "fail", SchemaCompilerAssertionFail) HANDLE_STEP("assertion", "defines", SchemaCompilerAssertionDefines) HANDLE_STEP("assertion", "type", SchemaCompilerAssertionType) + HANDLE_STEP("assertion", "type-any", SchemaCompilerAssertionTypeAny) HANDLE_STEP("assertion", "regex", SchemaCompilerAssertionRegex) HANDLE_STEP("assertion", "not-contains", SchemaCompilerAssertionNotContains) HANDLE_STEP("assertion", "size-greater", SchemaCompilerAssertionSizeGreater) diff --git a/vendor/jsontoolkit/src/jsonschema/default_compiler_draft4.h b/vendor/jsontoolkit/src/jsonschema/default_compiler_draft4.h index 1599f79d..c34d638a 100644 --- a/vendor/jsontoolkit/src/jsonschema/default_compiler_draft4.h +++ b/vendor/jsontoolkit/src/jsonschema/default_compiler_draft4.h @@ -6,51 +6,11 @@ #include // assert #include // std::regex +#include // std::set #include // std::move #include "compile_helpers.h" -namespace { - -auto type_string_to_assertion( - const sourcemeta::jsontoolkit::SchemaCompilerContext &context, - const std::string &type) - -> sourcemeta::jsontoolkit::SchemaCompilerTemplate { - using namespace sourcemeta::jsontoolkit; - if (type == "null") { - return {make( - context, JSON::Type::Null, {}, SchemaCompilerTargetType::Instance)}; - } else if (type == "boolean") { - return {make( - context, JSON::Type::Boolean, {}, SchemaCompilerTargetType::Instance)}; - } else if (type == "object") { - return {make( - context, JSON::Type::Object, {}, SchemaCompilerTargetType::Instance)}; - } else if (type == "array") { - return {make( - context, JSON::Type::Array, {}, SchemaCompilerTargetType::Instance)}; - } else if (type == "number") { - const auto subcontext{applicate(context)}; - return {make( - context, SchemaCompilerValueNone{}, - {make(subcontext, JSON::Type::Real, {}, - SchemaCompilerTargetType::Instance), - make(subcontext, JSON::Type::Integer, {}, - SchemaCompilerTargetType::Instance)}, - SchemaCompilerTemplate{})}; - } else if (type == "integer") { - return {make( - context, JSON::Type::Integer, {}, SchemaCompilerTargetType::Instance)}; - } else if (type == "string") { - return {make( - context, JSON::Type::String, {}, SchemaCompilerTargetType::Instance)}; - } else { - return {}; - } -} - -} // namespace - namespace internal { using namespace sourcemeta::jsontoolkit; @@ -90,23 +50,90 @@ auto compiler_draft4_core_ref(const SchemaCompilerContext &context) auto compiler_draft4_validation_type(const SchemaCompilerContext &context) -> SchemaCompilerTemplate { if (context.value.is_string()) { - return type_string_to_assertion(context, context.value.to_string()); + const auto &type{context.value.to_string()}; + if (type == "null") { + return {make( + context, JSON::Type::Null, {}, SchemaCompilerTargetType::Instance)}; + } else if (type == "boolean") { + return {make( + context, JSON::Type::Boolean, {}, + SchemaCompilerTargetType::Instance)}; + } else if (type == "object") { + return {make( + context, JSON::Type::Object, {}, SchemaCompilerTargetType::Instance)}; + } else if (type == "array") { + return {make( + context, JSON::Type::Array, {}, SchemaCompilerTargetType::Instance)}; + } else if (type == "number") { + return {make( + context, std::set{JSON::Type::Real, JSON::Type::Integer}, + {}, SchemaCompilerTargetType::Instance)}; + } else if (type == "integer") { + return {make( + context, JSON::Type::Integer, {}, + SchemaCompilerTargetType::Instance)}; + } else if (type == "string") { + return {make( + context, JSON::Type::String, {}, SchemaCompilerTargetType::Instance)}; + } else { + return {}; + } + } else if (context.value.is_array() && context.value.size() == 1 && + context.value.front().is_string()) { + const auto &type{context.value.front().to_string()}; + if (type == "null") { + return {make( + context, JSON::Type::Null, {}, SchemaCompilerTargetType::Instance)}; + } else if (type == "boolean") { + return {make( + context, JSON::Type::Boolean, {}, + SchemaCompilerTargetType::Instance)}; + } else if (type == "object") { + return {make( + context, JSON::Type::Object, {}, SchemaCompilerTargetType::Instance)}; + } else if (type == "array") { + return {make( + context, JSON::Type::Array, {}, SchemaCompilerTargetType::Instance)}; + } else if (type == "number") { + return {make( + context, std::set{JSON::Type::Real, JSON::Type::Integer}, + {}, SchemaCompilerTargetType::Instance)}; + } else if (type == "integer") { + return {make( + context, JSON::Type::Integer, {}, + SchemaCompilerTargetType::Instance)}; + } else if (type == "string") { + return {make( + context, JSON::Type::String, {}, SchemaCompilerTargetType::Instance)}; + } else { + return {}; + } } else if (context.value.is_array()) { - assert(!context.value.empty()); - SchemaCompilerTemplate disjunctors; - const auto subcontext{applicate(context)}; + std::set types; for (const auto &type : context.value.as_array()) { assert(type.is_string()); - SchemaCompilerTemplate disjunctor{ - type_string_to_assertion(subcontext, type.to_string())}; - assert(disjunctor.size() == 1); - disjunctors.push_back(std::move(disjunctor).front()); + const auto &type_string{type.to_string()}; + if (type_string == "null") { + types.emplace(JSON::Type::Null); + } else if (type_string == "boolean") { + types.emplace(JSON::Type::Boolean); + } else if (type_string == "object") { + types.emplace(JSON::Type::Object); + } else if (type_string == "array") { + types.emplace(JSON::Type::Array); + } else if (type_string == "number") { + types.emplace(JSON::Type::Integer); + types.emplace(JSON::Type::Real); + } else if (type_string == "integer") { + types.emplace(JSON::Type::Integer); + } else if (type_string == "string") { + types.emplace(JSON::Type::String); + } } - assert(disjunctors.size() == context.value.size()); - return {make(context, SchemaCompilerValueNone{}, - std::move(disjunctors), - SchemaCompilerTemplate{})}; + assert(types.size() >= context.value.size()); + return {make( + context, std::move(types), {}, SchemaCompilerTargetType::Instance)}; } return {}; @@ -496,6 +523,7 @@ auto compiler_draft4_validation_enum(const SchemaCompilerContext &context) SchemaCompilerTargetType::Instance)}; } + // TODO: Create a higher level "contains" step SchemaCompilerTemplate children; const auto subcontext{applicate(context)}; for (const auto &choice : context.value.as_array()) { 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 79fbdd2d..5d874332 100644 --- a/vendor/jsontoolkit/src/jsonschema/include/sourcemeta/jsontoolkit/jsonschema_compile.h +++ b/vendor/jsontoolkit/src/jsonschema/include/sourcemeta/jsontoolkit/jsonschema_compile.h @@ -64,6 +64,10 @@ using SchemaCompilerValueString = JSON::String; /// Represents a compiler step JSON type value using SchemaCompilerValueType = JSON::Type; +/// @ingroup jsonschema +/// Represents a compiler step JSON types value +using SchemaCompilerValueTypes = std::set; + /// @ingroup jsonschema /// Represents a compiler step ECMA regular expression value. We store both the /// original string and the regular expression as standard regular expressions @@ -82,7 +86,7 @@ enum class SchemaCompilerValueStringType { URI }; /// @ingroup jsonschema /// Represents a value in a compiler step template -using SchemaCompilerValue = std::variant; +using SchemaCompilerStepValue = std::variant; /// @ingroup jsonschema /// Represents a compiler assertion step that always fails @@ -98,6 +102,11 @@ struct SchemaCompilerAssertionDefines; /// given type struct SchemaCompilerAssertionType; +/// @ingroup jsonschema +/// Represents a compiler assertion step that checks if a document is of any of +/// the given types +struct SchemaCompilerAssertionTypeAny; + /// @ingroup jsonschema /// Represents a compiler assertion step that checks a string against an ECMA /// regular expression @@ -206,15 +215,15 @@ struct SchemaCompilerControlJump; /// Represents a schema compilation step that can be evaluated using SchemaCompilerTemplate = std::vector>; @@ -226,7 +235,7 @@ using SchemaCompilerTemplate = std::vector value; \ + const SchemaCompilerStepValue value; \ const SchemaCompilerTemplate condition; \ }; @@ -236,7 +245,7 @@ using SchemaCompilerTemplate = std::vector value; \ + const SchemaCompilerStepValue value; \ const SchemaCompilerTemplate children; \ const SchemaCompilerTemplate condition; \ }; @@ -253,6 +262,7 @@ using SchemaCompilerTemplate = std::vector; +using SchemaCompilerEvaluationCallback = std::function; + +/// @ingroup jsonschema +/// +/// This function translates a step execution into a human-readable string. +/// Useful as the building block for producing user-friendly evaluation results. +auto SOURCEMETA_JSONTOOLKIT_JSONSCHEMA_EXPORT +describe(const SchemaCompilerTemplate::value_type &step) -> std::string; // TODO: Support standard output formats. Maybe through pre-made evaluation // callbacks? @@ -416,6 +434,7 @@ evaluate(const SchemaCompilerTemplate &steps, const JSON &instance) -> bool; /// const sourcemeta::jsontoolkit::SchemaCompilerTemplate::value_type &step, /// const sourcemeta::jsontoolkit::Pointer &evaluate_path, /// const sourcemeta::jsontoolkit::Pointer &instance_location, +/// const sourcemeta::jsontoolkit::JSON &document, /// const sourcemeta::jsontoolkit::JSON &annotation) -> void { /// std::cout << "TYPE: " << (result ? "Success" : "Failure") << "\n"; /// std::cout << "STEP:\n";