diff --git a/DEPENDENCIES b/DEPENDENCIES index 2917f63..3b8ca33 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 c9b844557d3b116b272be5198ec547a7eee18347 +jsontoolkit https://github.com/sourcemeta/jsontoolkit 760aecee95213152bfea907c19cdaa43f616dc9b hydra https://github.com/sourcemeta/hydra 3c53d3fdef79e9ba603d48470a508cc45472a0dc diff --git a/src/command_metaschema.cc b/src/command_metaschema.cc index ee80e18..7ed6e37 100644 --- a/src/command_metaschema.cc +++ b/src/command_metaschema.cc @@ -39,17 +39,16 @@ auto intelligence::jsonschema::cli::metaschema( cache.insert({dialect.value(), metaschema_template}); } - std::ostringstream error; + sourcemeta::jsontoolkit::SchemaCompilerErrorTraceOutput output{metaschema}; if (sourcemeta::jsontoolkit::evaluate( cache.at(dialect.value()), entry.second, sourcemeta::jsontoolkit::SchemaCompilerEvaluationMode::Fast, - pretty_evaluate_callback(error, metaschema, - sourcemeta::jsontoolkit::empty_pointer))) { + std::ref(output))) { log_verbose(options) << entry.first.string() << ": The schema is valid with respect to its metaschema\n"; } else { - std::cerr << error.str(); + print(output, std::cerr); std::cerr << entry.first.string() << ": The schema is NOT valid with respect to its metaschema\n"; result = false; diff --git a/src/command_test.cc b/src/command_test.cc index 8d3dd20..0a2d166 100644 --- a/src/command_test.cc +++ b/src/command_test.cc @@ -237,12 +237,13 @@ auto intelligence::jsonschema::cli::test( return EXIT_FAILURE; } - std::ostringstream error; + sourcemeta::jsontoolkit::SchemaCompilerErrorTraceOutput output{ + schema.value(), {"$ref"}}; const auto case_result{sourcemeta::jsontoolkit::evaluate( schema_template, get_data(test_case, entry.first.parent_path(), verbose), sourcemeta::jsontoolkit::SchemaCompilerEvaluationMode::Fast, - pretty_evaluate_callback(error, schema.value(), {"$ref"}))}; + std::ref(output))}; std::ostringstream test_case_description; if (test_case.defines("description")) { @@ -278,7 +279,7 @@ auto intelligence::jsonschema::cli::test( std::cout << " " << index << "/" << total << " FAIL " << test_case_description.str() << "\n\n"; - std::cout << error.str(); + print(output, std::cout); if (index != total && verbose) { std::cout << "\n"; diff --git a/src/command_validate.cc b/src/command_validate.cc index 3405c15..195317b 100644 --- a/src/command_validate.cc +++ b/src/command_validate.cc @@ -69,6 +69,8 @@ auto intelligence::jsonschema::cli::validate( for (const auto &instance : sourcemeta::jsontoolkit::JSONL{stream}) { index += 1; std::ostringstream error; + sourcemeta::jsontoolkit::SchemaCompilerErrorTraceOutput output{ + instance}; bool subresult = true; if (benchmark) { const auto timestamp_start{ @@ -88,8 +90,7 @@ auto intelligence::jsonschema::cli::validate( subresult = sourcemeta::jsontoolkit::evaluate( schema_template, instance, sourcemeta::jsontoolkit::SchemaCompilerEvaluationMode::Fast, - pretty_evaluate_callback( - error, instance, sourcemeta::jsontoolkit::empty_pointer)); + std::ref(output)); } if (subresult) { @@ -108,6 +109,7 @@ auto intelligence::jsonschema::cli::validate( sourcemeta::jsontoolkit::prettify(instance, std::cerr); std::cerr << "\n\n"; std::cerr << error.str(); + print(output, std::cerr); result = false; break; } @@ -123,6 +125,7 @@ auto intelligence::jsonschema::cli::validate( } else { const auto instance{sourcemeta::jsontoolkit::from_file(instance_path)}; std::ostringstream error; + sourcemeta::jsontoolkit::SchemaCompilerErrorTraceOutput output{instance}; bool subresult{true}; if (benchmark) { const auto timestamp_start{std::chrono::high_resolution_clock::now()}; @@ -142,8 +145,7 @@ auto intelligence::jsonschema::cli::validate( subresult = sourcemeta::jsontoolkit::evaluate( schema_template, instance, sourcemeta::jsontoolkit::SchemaCompilerEvaluationMode::Fast, - pretty_evaluate_callback(error, instance, - sourcemeta::jsontoolkit::empty_pointer)); + std::ref(output)); } if (subresult) { @@ -157,6 +159,7 @@ auto intelligence::jsonschema::cli::validate( << std::filesystem::weakly_canonical(instance_path).string() << "\n"; std::cerr << error.str(); + print(output, std::cerr); result = false; } } diff --git a/src/utils.cc b/src/utils.cc index 60a2505..ac2eb23 100644 --- a/src/utils.cc +++ b/src/utils.cc @@ -175,37 +175,19 @@ auto parse_options(const std::span &arguments, return options; } -auto pretty_evaluate_callback(std::ostringstream &output, - const sourcemeta::jsontoolkit::JSON &instance, - const sourcemeta::jsontoolkit::Pointer &base) - -> sourcemeta::jsontoolkit::SchemaCompilerEvaluationCallback { - output << "error: Schema validation failure\n"; - return [&output, &instance, &base]( - const sourcemeta::jsontoolkit::SchemaCompilerEvaluationType, - const bool result, - const sourcemeta::jsontoolkit::SchemaCompilerTemplate::value_type - &step, - const sourcemeta::jsontoolkit::Pointer &evaluate_path, - const sourcemeta::jsontoolkit::Pointer &instance_location, - const sourcemeta::jsontoolkit::JSON &annotation) -> void { - if (result) { - return; - } - - output << " " - << sourcemeta::jsontoolkit::describe(result, step, evaluate_path, - instance_location, instance, - annotation) - << "\n"; - output << " at instance location \""; - sourcemeta::jsontoolkit::stringify(instance_location, output); - output << "\"\n"; - - output << " at evaluate path \""; - sourcemeta::jsontoolkit::stringify(evaluate_path.resolve_from(base), - output); - output << "\"\n"; - }; +auto print( + const sourcemeta::jsontoolkit::SchemaCompilerErrorTraceOutput &output, + std::ostream &stream) -> void { + stream << "error: Schema validation failure\n"; + for (const auto &entry : output) { + stream << " " << entry.message << "\n"; + stream << " at instance location \""; + sourcemeta::jsontoolkit::stringify(entry.instance_location, stream); + stream << "\"\n"; + stream << " at evaluate path \""; + sourcemeta::jsontoolkit::stringify(entry.evaluate_path, stream); + stream << "\"\n"; + } } static auto fallback_resolver( diff --git a/src/utils.h b/src/utils.h index f7d172e..40da9d4 100644 --- a/src/utils.h +++ b/src/utils.h @@ -27,10 +27,9 @@ auto for_each_json(const std::vector &arguments, -> std::vector< std::pair>; -auto pretty_evaluate_callback(std::ostringstream &, - const sourcemeta::jsontoolkit::JSON &, - const sourcemeta::jsontoolkit::Pointer &) - -> sourcemeta::jsontoolkit::SchemaCompilerEvaluationCallback; +auto print( + const sourcemeta::jsontoolkit::SchemaCompilerErrorTraceOutput &output, + std::ostream &stream) -> void; auto resolver(const std::map> &options, const bool remote = false) diff --git a/test/test/fail_true_resolve_fragment.sh b/test/test/fail_true_resolve_fragment.sh index 2c6f267..db008a8 100755 --- a/test/test/fail_true_resolve_fragment.sh +++ b/test/test/fail_true_resolve_fragment.sh @@ -43,9 +43,6 @@ error: Schema validation failure The value was expected to be of type string but it was of type object at instance location "" at evaluate path "/type" - The object value was expected to validate against the statically referenced schema - at instance location "" - at evaluate path "" EOF diff "$TMP/output.txt" "$TMP/expected.txt" diff --git a/vendor/jsontoolkit/src/jsonschema/compile.cc b/vendor/jsontoolkit/src/jsonschema/compile.cc index 40bfe8d..48982d3 100644 --- a/vendor/jsontoolkit/src/jsonschema/compile.cc +++ b/vendor/jsontoolkit/src/jsonschema/compile.cc @@ -1,7 +1,7 @@ #include #include -#include // std::move +#include // std::move, std::any_of #include // assert #include // std::back_inserter #include // std::move @@ -212,4 +212,65 @@ auto compile(const SchemaCompilerContext &context, entry.dialect); } +SchemaCompilerErrorTraceOutput::SchemaCompilerErrorTraceOutput( + const JSON &instance, const Pointer &base) + : instance_{instance}, base_{base} {} + +auto SchemaCompilerErrorTraceOutput::begin() const -> const_iterator { + return this->output.begin(); +} + +auto SchemaCompilerErrorTraceOutput::end() const -> const_iterator { + return this->output.end(); +} + +auto SchemaCompilerErrorTraceOutput::cbegin() const -> const_iterator { + return this->output.cbegin(); +} + +auto SchemaCompilerErrorTraceOutput::cend() const -> const_iterator { + return this->output.cend(); +} + +auto SchemaCompilerErrorTraceOutput::operator()( + const SchemaCompilerEvaluationType type, const bool result, + const SchemaCompilerTemplate::value_type &step, + const Pointer &evaluate_path, const Pointer &instance_location, + const JSON &annotation) -> void { + assert(!evaluate_path.empty()); + assert(evaluate_path.back().is_property()); + + if (type == sourcemeta::jsontoolkit::SchemaCompilerEvaluationType::Pre) { + assert(result); + const auto &keyword{evaluate_path.back().to_property()}; + // To ease the output + if (keyword == "oneOf" || keyword == "not") { + this->mask.insert(evaluate_path); + } + } else if (type == + sourcemeta::jsontoolkit::SchemaCompilerEvaluationType::Post && + this->mask.contains(evaluate_path)) { + this->mask.erase(evaluate_path); + } + + // Ignore successful or masked steps + if (result || std::any_of(this->mask.cbegin(), this->mask.cend(), + [&evaluate_path](const auto &entry) { + return evaluate_path.starts_with(entry); + })) { + return; + } + + auto effective_evaluate_path{evaluate_path.resolve_from(this->base_)}; + if (effective_evaluate_path.empty()) { + return; + } + + this->output.push_back( + {sourcemeta::jsontoolkit::describe(result, step, evaluate_path, + instance_location, this->instance_, + annotation), + instance_location, std::move(effective_evaluate_path)}); +} + } // namespace sourcemeta::jsontoolkit 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 dae34a3..7af8008 100644 --- a/vendor/jsontoolkit/src/jsonschema/include/sourcemeta/jsontoolkit/jsonschema_compile.h +++ b/vendor/jsontoolkit/src/jsonschema/include/sourcemeta/jsontoolkit/jsonschema_compile.h @@ -524,6 +524,94 @@ using SchemaCompilerEvaluationCallback = const SchemaCompilerTemplate::value_type &, const Pointer &, const Pointer &, const JSON &)>; +// TODO: Support standard output formats too + +/// @ingroup jsonschema +/// +/// A simple evaluation callback that reports a stack trace in the case of +/// validation error that you can report as you with. For example: +/// +/// ```cpp +/// #include +/// #include +/// #include +/// #include +/// +/// const sourcemeta::jsontoolkit::JSON schema = +/// sourcemeta::jsontoolkit::parse(R"JSON({ +/// "$schema": "https://json-schema.org/draft/2020-12/schema", +/// "type": "string" +/// })JSON"); +/// +/// const auto schema_template{sourcemeta::jsontoolkit::compile( +/// schema, sourcemeta::jsontoolkit::default_schema_walker, +/// sourcemeta::jsontoolkit::official_resolver, +/// sourcemeta::jsontoolkit::default_schema_compiler)}; +/// +/// const sourcemeta::jsontoolkit::JSON instance{5}; +/// +/// sourcemeta::jsontoolkit::SchemaCompilerErrorTraceOutput output; +/// const auto result{sourcemeta::jsontoolkit::evaluate( +/// schema_template, instance, +/// sourcemeta::jsontoolkit::SchemaCompilerEvaluationMode::Fast, +/// std::ref(output))}; +/// +/// if (!result) { +/// for (const auto &trace : output) { +/// std::cerr << trace.message << "\n"; +/// sourcemeta::jsontoolkit::stringify(trace.instance_location, std::cerr); +/// std::cerr << "\n"; +/// sourcemeta::jsontoolkit::stringify(trace.evaluate_path, std::cerr); +/// std::cerr << "\n"; +/// } +/// } +/// ``` +class SOURCEMETA_JSONTOOLKIT_JSONSCHEMA_EXPORT SchemaCompilerErrorTraceOutput { +public: + SchemaCompilerErrorTraceOutput(const JSON &instance, + const Pointer &base = empty_pointer); + + // Prevent accidental copies + SchemaCompilerErrorTraceOutput(const SchemaCompilerErrorTraceOutput &) = + delete; + auto operator=(const SchemaCompilerErrorTraceOutput &) + -> SchemaCompilerErrorTraceOutput & = delete; + + struct Entry { + const std::string message; + const Pointer instance_location; + const Pointer evaluate_path; + }; + + auto operator()(const SchemaCompilerEvaluationType type, const bool result, + const SchemaCompilerTemplate::value_type &step, + const Pointer &evaluate_path, + const Pointer &instance_location, + const JSON &annotation) -> void; + + using container_type = typename std::vector; + using const_iterator = typename container_type::const_iterator; + auto begin() const -> const_iterator; + auto end() const -> const_iterator; + auto cbegin() const -> const_iterator; + auto cend() const -> const_iterator; + +private: +// Exporting symbols that depends on the standard C++ library is considered +// safe. +// https://learn.microsoft.com/en-us/cpp/error-messages/compiler-warnings/compiler-warning-level-2-c4275?view=msvc-170&redirectedfrom=MSDN +#if defined(_MSC_VER) +#pragma warning(disable : 4251) +#endif + const JSON &instance_; + const Pointer base_; + container_type output; + std::set mask; +#if defined(_MSC_VER) +#pragma warning(default : 4251) +#endif +}; + /// @ingroup jsonschema /// /// This function translates a step execution into a human-readable string. @@ -533,9 +621,6 @@ 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; -// TODO: Support standard output formats. Maybe through pre-made evaluation -// callbacks? - /// @ingroup jsonschema /// /// This function evaluates a schema compiler template in validation mode,