From dec45be3eef0297175b7add5885b92a58e9c71bd Mon Sep 17 00:00:00 2001 From: Juan Cruz Viotti Date: Fri, 31 May 2024 21:16:25 -0300 Subject: [PATCH] Support metaschema validation in `validate` and `test` (#22) Signed-off-by: Juan Cruz Viotti --- src/command_test.cc | 17 ++++++++++++-- src/command_validate.cc | 24 ++++++++++++++++---- src/main.cc | 11 +++++---- src/utils.cc | 27 ++++++++++++++++++++++ src/utils.h | 4 ++++ test/CMakeLists.txt | 1 + test/validate_fail_at_metaschema.sh | 32 +++++++++++++++++++++++++++ test/validate_pass_with_metaschema.sh | 25 +++++++++++++++++++++ 8 files changed, 131 insertions(+), 10 deletions(-) create mode 100755 test/validate_fail_at_metaschema.sh create mode 100755 test/validate_pass_with_metaschema.sh diff --git a/src/command_test.cc b/src/command_test.cc index 6bdce4e1..6085d232 100644 --- a/src/command_test.cc +++ b/src/command_test.cc @@ -7,10 +7,10 @@ #include "command.h" #include "utils.h" -// TODO: Add a flag to first validate schema against its metaschema auto intelligence::jsonschema::cli::test( const std::span &arguments) -> int { - const auto options{parse_options(arguments, {"h", "http"})}; + const auto options{ + parse_options(arguments, {"h", "http", "m", "metaschema"})}; bool result{true}; const auto test_resolver{ resolver(options, options.contains("h") || options.contains("http"))}; @@ -41,6 +41,19 @@ auto intelligence::jsonschema::cli::test( return EXIT_FAILURE; } + if (options.contains("m") || options.contains("metaschema")) { + const auto metaschema_result{ + validate_against_metaschema(schema.value(), test_resolver)}; + if (metaschema_result) { + log_verbose(options) + << "The schema is valid with respect to its metaschema\n"; + ; + } else { + std::cerr << "The schema is NOT valid with respect to its metaschema\n"; + return EXIT_FAILURE; + } + } + const auto schema_template{sourcemeta::jsontoolkit::compile( schema.value(), sourcemeta::jsontoolkit::default_schema_walker, test_resolver, sourcemeta::jsontoolkit::default_schema_compiler)}; diff --git a/src/command_validate.cc b/src/command_validate.cc index f87a9ef7..3eff442a 100644 --- a/src/command_validate.cc +++ b/src/command_validate.cc @@ -9,21 +9,37 @@ #include "command.h" #include "utils.h" -// TODO: Add a flag to first validate schema against its metaschema // TODO: Add a flag to emit output using the standard JSON Schema output format auto intelligence::jsonschema::cli::validate( const std::span &arguments) -> int { - const auto options{parse_options(arguments, {"h", "http"})}; + const auto options{ + parse_options(arguments, {"h", "http", "m", "metaschema"})}; CLI_ENSURE(options.at("").size() >= 2, "You must pass a schema followed by an instance") const auto &schema_path{options.at("").at(0)}; const auto &instance_path{options.at("").at(1)}; + const auto custom_resolver{ + resolver(options, options.contains("h") || options.contains("http"))}; const auto schema{sourcemeta::jsontoolkit::from_file(schema_path)}; + // TODO: If not instance is passed, just validate the schema against its + // metaschema? + if (options.contains("m") || options.contains("metaschema")) { + const auto metaschema_result{ + validate_against_metaschema(schema, custom_resolver)}; + if (metaschema_result) { + log_verbose(options) + << "The schema is valid with respect to its metaschema\n"; + ; + } else { + std::cerr << "The schema is NOT valid with respect to its metaschema\n"; + return EXIT_FAILURE; + } + } + const auto schema_template{sourcemeta::jsontoolkit::compile( - schema, sourcemeta::jsontoolkit::default_schema_walker, - resolver(options, options.contains("h") || options.contains("http")), + schema, sourcemeta::jsontoolkit::default_schema_walker, custom_resolver, sourcemeta::jsontoolkit::default_schema_compiler)}; const auto instance{sourcemeta::jsontoolkit::from_file(instance_path)}; diff --git a/src/main.cc b/src/main.cc index 809f9dc6..fecb16f1 100644 --- a/src/main.cc +++ b/src/main.cc @@ -20,20 +20,23 @@ Global Options: Commands: - validate [--http/-h] + validate [--http/-h] [--metaschema/-m] Validate an instance against a schema, printing error information, if any, in a human-readable manner. The `--http/-h` option enables resolving - remote schemas over the HTTP protocol. + remote schemas over the HTTP protocol. The `--metaschema/-m` option + checks that the given schema is valid with respects to its dialect + metaschema. - test [schema.json...] [--http/-h] + test [schema.json...] [--http/-h] [--metaschema/-m] A schema test runner inspired by the official JSON Schema test suite. Passing directories as input will run every `.json` file in such directory (recursively) as a test. If no argument is passed, run every `.json` file in the current working directory (recursively) as a test. The `--http/-h` option enables resolving remote schemas over the HTTP - protocol. + protocol. The `--metaschema/-m` option checks that the given schema is + valid with respects to its dialect metaschema. fmt [schema.json...] [--check/-c] diff --git a/src/utils.cc b/src/utils.cc index ec9478a2..234824e0 100644 --- a/src/utils.cc +++ b/src/utils.cc @@ -1,4 +1,6 @@ #include +#include +#include #include #include "utils.h" @@ -204,4 +206,29 @@ auto log_verbose(const std::map> &options) return null_stream; } +auto validate_against_metaschema( + const sourcemeta::jsontoolkit::JSON &schema, + const sourcemeta::jsontoolkit::SchemaResolver &resolver) -> bool { + const std::optional dialect{ + sourcemeta::jsontoolkit::dialect(schema)}; + if (!dialect.has_value()) { + throw std::runtime_error( + "Cannot determine the dialect of the input schema"); + } + + const auto metaschema{resolver(dialect.value()).get()}; + if (!metaschema.has_value()) { + throw sourcemeta::jsontoolkit::SchemaResolutionError( + dialect.value(), "Could not resolve metaschema"); + } + + const auto metaschema_template{sourcemeta::jsontoolkit::compile( + metaschema.value(), sourcemeta::jsontoolkit::default_schema_walker, + resolver, sourcemeta::jsontoolkit::default_schema_compiler)}; + return sourcemeta::jsontoolkit::evaluate( + metaschema_template, schema, + sourcemeta::jsontoolkit::SchemaCompilerEvaluationMode::Fast, + pretty_evaluate_callback); +} + } // namespace intelligence::jsonschema::cli diff --git a/src/utils.h b/src/utils.h index 63a3259f..bcd223ee 100644 --- a/src/utils.h +++ b/src/utils.h @@ -45,6 +45,10 @@ auto resolver(const std::map> &options, auto log_verbose(const std::map> &options) -> std::ostream &; +auto validate_against_metaschema( + const sourcemeta::jsontoolkit::JSON &schema, + const sourcemeta::jsontoolkit::SchemaResolver &resolver) -> bool; + } // namespace intelligence::jsonschema::cli #endif diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index ce987f90..4445675b 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -16,6 +16,7 @@ add_jsonschema_test_unix(validate_pass) add_jsonschema_test_unix(validate_fail) add_jsonschema_test_unix(validate_fail_remote_no_http) add_jsonschema_test_unix(validate_non_supported) +add_jsonschema_test_unix(validate_pass_with_metaschema) add_jsonschema_test_unix(bundle_non_remote) add_jsonschema_test_unix(bundle_remote_single_schema) add_jsonschema_test_unix(bundle_remote_no_http) diff --git a/test/validate_fail_at_metaschema.sh b/test/validate_fail_at_metaschema.sh new file mode 100755 index 00000000..ab7e2f99 --- /dev/null +++ b/test/validate_fail_at_metaschema.sh @@ -0,0 +1,32 @@ +#!/bin/sh + +set -o errexit +set -o nounset + +TMP="$(mktemp -d)" +clean() { rm -rf "$TMP"; } +trap clean EXIT + +cat << 'EOF' > "$TMP/schema.json" +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "properties": { + "foo": true, + "bar": 1 + } +} +EOF + +cat << 'EOF' > "$TMP/instance.json" +{ "foo": "bar" } +EOF + +"$1" validate "$TMP/schema.json" "$TMP/instance.json" --metaschema && CODE="$?" || CODE="$?" + +if [ "$CODE" = "0" ] +then + echo "FAIL" 1>&2 + exit 1 +else + echo "PASS" 1>&2 +fi diff --git a/test/validate_pass_with_metaschema.sh b/test/validate_pass_with_metaschema.sh new file mode 100755 index 00000000..971ad038 --- /dev/null +++ b/test/validate_pass_with_metaschema.sh @@ -0,0 +1,25 @@ +#!/bin/sh + +set -o errexit +set -o nounset + +TMP="$(mktemp -d)" +clean() { rm -rf "$TMP"; } +trap clean EXIT + +cat << 'EOF' > "$TMP/schema.json" +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "properties": { + "foo": { + "type": "string" + } + } +} +EOF + +cat << 'EOF' > "$TMP/instance.json" +{ "foo": "bar" } +EOF + +"$1" validate "$TMP/schema.json" "$TMP/instance.json" --metaschema