From 3d56ad0acfefe5f2037d9e2b8214782ce0eb3ba2 Mon Sep 17 00:00:00 2001 From: Juan Cruz Viotti Date: Tue, 18 Jun 2024 17:31:54 -0400 Subject: [PATCH] Improve output and error messages from the `validate` command Signed-off-by: Juan Cruz Viotti --- DEPENDENCIES | 2 +- src/command_validate.cc | 41 +++++++----- src/main.cc | 44 ++++++++++--- src/utils.cc | 16 ++--- test/CMakeLists.txt | 45 +++++++++---- test/ci/fail_validate_http_non_200.sh | 30 +++++++++ test/ci/fail_validate_http_non_200_verbose.sh | 31 +++++++++ test/ci/fail_validate_http_non_schema.sh | 30 +++++++++ .../fail_validate_http_non_schema_verbose.sh | 31 +++++++++ test/ci/pass_validate_http.sh | 26 ++++++++ test/ci/pass_validate_http_verbose.sh | 29 ++++++++ test/format_check_single_invalid.sh | 6 -- test/validate/fail_draft4.sh | 39 +++++++++++ test/validate/fail_draft6.sh | 39 +++++++++++ test/validate/fail_draft7.sh | 39 +++++++++++ test/validate/fail_instance_directory.sh | 28 ++++++++ test/validate/fail_instance_enoent.sh | 26 ++++++++ test/validate/fail_instance_invalid_json.sh | 32 +++++++++ .../fail_invalid_ref.sh} | 11 +--- .../fail_no_instance.sh} | 11 +--- .../fail_no_schema.sh} | 11 +--- .../fail_relative_external_ref_missing.sh | 31 +++++++++ ...ail_resolve_directory_with_invalid_json.sh | 41 ++++++++++++ test/validate/fail_resolve_enoent.sh | 31 +++++++++ test/validate/fail_resolve_invalid_json.sh | 35 ++++++++++ test/validate/fail_schema_directory.sh | 25 +++++++ test/validate/fail_schema_enoent.sh | 23 +++++++ test/validate/fail_schema_invalid_json.sh | 29 ++++++++ test/validate/fail_schema_non_schema.sh | 27 ++++++++ test/validate/fail_schema_unknown_dialect.sh | 30 +++++++++ .../fail_schema_unsupported_dialect.sh | 33 ++++++++++ .../pass_draft4.sh} | 0 .../pass_draft6.sh} | 0 .../pass_draft7.sh} | 0 test/validate/pass_resolve.sh | 56 ++++++++++++++++ .../validate/pass_resolve_custom_extension.sh | 56 ++++++++++++++++ test/validate/pass_resolve_verbose.sh | 66 +++++++++++++++++++ .../pass_verbose.sh} | 18 +++-- test/validate_fail_draft6.sh | 34 ---------- test/validate_fail_draft7.sh | 34 ---------- test/validate_fail_remote_no_http.sh | 30 --------- test/validate_non_supported.sh | 34 ---------- .../sourcemeta/jsontoolkit/json_error.h | 4 ++ vendor/jsontoolkit/src/json/json.cc | 20 ++++-- vendor/jsontoolkit/src/jsonschema/bundle.cc | 13 +++- .../src/jsonschema/compile_evaluate.cc | 4 +- .../src/jsonschema/default_compiler.cc | 30 +++++---- .../src/jsonschema/default_compiler_draft4.h | 17 ++--- .../sourcemeta/jsontoolkit/jsonschema_error.h | 20 ++++++ .../jsontoolkit/src/jsonschema/jsonschema.cc | 4 +- 50 files changed, 1059 insertions(+), 253 deletions(-) create mode 100755 test/ci/fail_validate_http_non_200.sh create mode 100755 test/ci/fail_validate_http_non_200_verbose.sh create mode 100755 test/ci/fail_validate_http_non_schema.sh create mode 100755 test/ci/fail_validate_http_non_schema_verbose.sh create mode 100755 test/ci/pass_validate_http.sh create mode 100755 test/ci/pass_validate_http_verbose.sh create mode 100755 test/validate/fail_draft4.sh create mode 100755 test/validate/fail_draft6.sh create mode 100755 test/validate/fail_draft7.sh create mode 100755 test/validate/fail_instance_directory.sh create mode 100755 test/validate/fail_instance_enoent.sh create mode 100755 test/validate/fail_instance_invalid_json.sh rename test/{validate_fail_invalid_ref.sh => validate/fail_invalid_ref.sh} (80%) rename test/{validate_fail_no_instance.sh => validate/fail_no_instance.sh} (75%) rename test/{validate_fail_no_schema.sh => validate/fail_no_schema.sh} (73%) create mode 100755 test/validate/fail_relative_external_ref_missing.sh create mode 100755 test/validate/fail_resolve_directory_with_invalid_json.sh create mode 100755 test/validate/fail_resolve_enoent.sh create mode 100755 test/validate/fail_resolve_invalid_json.sh create mode 100755 test/validate/fail_schema_directory.sh create mode 100755 test/validate/fail_schema_enoent.sh create mode 100755 test/validate/fail_schema_invalid_json.sh create mode 100755 test/validate/fail_schema_non_schema.sh create mode 100755 test/validate/fail_schema_unknown_dialect.sh create mode 100755 test/validate/fail_schema_unsupported_dialect.sh rename test/{validate_pass_draft4.sh => validate/pass_draft4.sh} (100%) rename test/{validate_pass_draft6.sh => validate/pass_draft6.sh} (100%) rename test/{validate_pass_draft7.sh => validate/pass_draft7.sh} (100%) create mode 100755 test/validate/pass_resolve.sh create mode 100755 test/validate/pass_resolve_custom_extension.sh create mode 100755 test/validate/pass_resolve_verbose.sh rename test/{validate_fail_draft4.sh => validate/pass_verbose.sh} (54%) delete mode 100755 test/validate_fail_draft6.sh delete mode 100755 test/validate_fail_draft7.sh delete mode 100755 test/validate_fail_remote_no_http.sh delete mode 100755 test/validate_non_supported.sh diff --git a/DEPENDENCIES b/DEPENDENCIES index c8c5a78d..9fef6670 100644 --- a/DEPENDENCIES +++ b/DEPENDENCIES @@ -1,4 +1,4 @@ vendorpull https://github.com/sourcemeta/vendorpull dea311b5bfb53b6926a4140267959ae334d3ecf4 noa https://github.com/sourcemeta/noa 2bc3138b80e575786bec418c91fc2058c6836993 -jsontoolkit https://github.com/sourcemeta/jsontoolkit 0e2ac8987382685ad6dc50ca33d2e43a6701b023 +jsontoolkit https://github.com/sourcemeta/jsontoolkit 7c229e4243290ad255bba4f3775492147f3972ce hydra https://github.com/sourcemeta/hydra 3c53d3fdef79e9ba603d48470a508cc45472a0dc diff --git a/src/command_validate.cc b/src/command_validate.cc index 6df447f5..0f3d5b80 100644 --- a/src/command_validate.cc +++ b/src/command_validate.cc @@ -32,33 +32,40 @@ auto intelligence::jsonschema::cli::validate( return EXIT_FAILURE; } - CLI_ENSURE(options.at("").size() >= 2, "You must pass an instance") const auto &schema_path{options.at("").at(0)}; const auto custom_resolver{ resolver(options, options.contains("h") || options.contains("http"))}; const auto schema{sourcemeta::jsontoolkit::from_file(schema_path)}; + if (!sourcemeta::jsontoolkit::is_schema(schema)) { + std::cerr << "error: The schema file you provided does not represent a " + "valid JSON Schema\n " + << std::filesystem::canonical(schema_path).string() << "\n"; + return EXIT_FAILURE; + } + bool result{true}; - if (options.at("").size() >= 2) { - const auto &instance_path{options.at("").at(1)}; - const auto schema_template{sourcemeta::jsontoolkit::compile( - schema, sourcemeta::jsontoolkit::default_schema_walker, custom_resolver, - sourcemeta::jsontoolkit::default_schema_compiler)}; + const auto &instance_path{options.at("").at(1)}; + const auto schema_template{sourcemeta::jsontoolkit::compile( + schema, sourcemeta::jsontoolkit::default_schema_walker, custom_resolver, + sourcemeta::jsontoolkit::default_schema_compiler)}; - const auto instance{sourcemeta::jsontoolkit::from_file(instance_path)}; + const auto instance{sourcemeta::jsontoolkit::from_file(instance_path)}; - std::ostringstream error; - result = sourcemeta::jsontoolkit::evaluate( - schema_template, instance, - sourcemeta::jsontoolkit::SchemaCompilerEvaluationMode::Fast, - pretty_evaluate_callback(error)); + std::ostringstream error; + result = sourcemeta::jsontoolkit::evaluate( + schema_template, instance, + sourcemeta::jsontoolkit::SchemaCompilerEvaluationMode::Fast, + pretty_evaluate_callback(error)); - if (result) { - log_verbose(options) << "Valid\n"; - } else { - std::cerr << error.str(); - } + if (result) { + log_verbose(options) + << "ok: " << std::filesystem::weakly_canonical(instance_path).string() + << "\n matches " + << std::filesystem::weakly_canonical(schema_path).string() << "\n"; + } else { + std::cerr << error.str(); } return result ? EXIT_SUCCESS : EXIT_FAILURE; diff --git a/src/main.cc b/src/main.cc index 7d57908e..787d4683 100644 --- a/src/main.cc +++ b/src/main.cc @@ -95,24 +95,52 @@ auto main(int argc, char *argv[]) noexcept -> int { argv + argc}; return jsonschema_main(program, command, arguments); } catch (const sourcemeta::jsontoolkit::SchemaReferenceError &error) { - std::cerr << error.what() << ": " << error.id() << "\n"; - std::cerr << " at schema location \""; + std::cerr << "error: " << error.what() << "\n " << error.id() + << "\n at schema location \""; sourcemeta::jsontoolkit::stringify(error.location(), std::cerr); std::cerr << "\"\n"; return EXIT_FAILURE; } catch (const sourcemeta::jsontoolkit::SchemaResolutionError &error) { - std::cerr << error.what() << ": " << error.id() << "\n"; + std::cerr << "error: " << error.what() << "\n at " << error.id() << "\n"; + return EXIT_FAILURE; + } catch (const sourcemeta::jsontoolkit::SchemaVocabularyError &error) { + std::cerr << "error: " << error.what() << "\n " << error.uri() + << "\n\nTo request support for it, please open an issue " + "at\nhttps://github.com/intelligence-ai/jsonschema\n"; return EXIT_FAILURE; } catch (const sourcemeta::jsontoolkit::FileParseError &error) { - std::cerr << error.path().string() << "\n " << error.what() << " at line " - << error.line() << " and column " << error.column() << "\n"; + std::cerr << "error: " << error.what() << " at line " << error.line() + << " and column " << error.column() << "\n " + << std::filesystem::weakly_canonical(error.path()).string() + << "\n"; return EXIT_FAILURE; } catch (const sourcemeta::jsontoolkit::ParseError &error) { - std::cerr << error.what() << " at line " << error.line() << " and column " - << error.column() << "\n"; + std::cerr << "error: " << error.what() << " at line " << error.line() + << " and column " << error.column() << "\n"; + return EXIT_FAILURE; + } catch (const std::filesystem::filesystem_error &error) { + // See https://en.cppreference.com/w/cpp/error/errc + if (error.code() == std::errc::no_such_file_or_directory) { + std::cerr << "error: " << error.code().message() << "\n " + << std::filesystem::weakly_canonical(error.path1()).string() + << "\n"; + } else if (error.code() == std::errc::is_a_directory) { + std::cerr << "error: The input was supposed to be a file but it is a " + "directory\n " + << std::filesystem::weakly_canonical(error.path1()).string() + << "\n"; + } else { + std::cerr << "error: " << error.what() << "\n"; + } + + return EXIT_FAILURE; + } catch (const std::runtime_error &error) { + std::cerr << "error: " << error.what() << "\n"; return EXIT_FAILURE; } catch (const std::exception &error) { - std::cerr << "Error: " << error.what() << "\n"; + std::cerr << "unexpected error: " << error.what() + << "\nPlease report it at " + << "https://github.com/intelligence-ai/jsonschema\n"; return EXIT_FAILURE; } } diff --git a/src/utils.cc b/src/utils.cc index 2d82de78..e290f57e 100644 --- a/src/utils.cc +++ b/src/utils.cc @@ -187,11 +187,11 @@ auto pretty_evaluate_callback(std::ostringstream &output) } output << "error: " << sourcemeta::jsontoolkit::describe(step) << "\n"; - output << " at instance location \""; + output << " at instance location \""; sourcemeta::jsontoolkit::stringify(instance_location, output); output << "\"\n"; - output << " at evaluate path \""; + output << " at evaluate path \""; sourcemeta::jsontoolkit::stringify(evaluate_path, output); output << "\"\n"; }; @@ -219,15 +219,13 @@ static auto fallback_resolver( return promise.get_future(); } - log_verbose(options) << "Attempting to fetch over HTTP: " << identifier - << "\n"; + log_verbose(options) << "Resolving over HTTP: " << identifier << "\n"; sourcemeta::hydra::http::ClientRequest request{std::string{identifier}}; request.method(sourcemeta::hydra::http::Method::GET); sourcemeta::hydra::http::ClientResponse response{request.send().get()}; if (response.status() != sourcemeta::hydra::http::Status::OK) { std::ostringstream error; - error << "Failed to fetch " << identifier - << " over HTTP. Got status code: " << response.status(); + error << response.status() << "\n at " << identifier; throw std::runtime_error(error.str()); } @@ -251,7 +249,8 @@ auto resolver(const std::map> &options, for (const auto &entry : for_each_json(options.at("resolve"), parse_ignore(options), parse_extensions(options))) { - log_verbose(options) << "Loading schema: " << entry.first << "\n"; + log_verbose(options) << "Importing schema into the resolution context: " + << entry.first.string() << "\n"; dynamic_resolver.add(entry.second); } } @@ -260,7 +259,8 @@ auto resolver(const std::map> &options, for (const auto &entry : for_each_json(options.at("r"), parse_ignore(options), parse_extensions(options))) { - log_verbose(options) << "Loading schema: " << entry.first << "\n"; + log_verbose(options) << "Importing schema into the resolution context: " + << entry.first.string() << "\n"; dynamic_resolver.add(entry.second); } } diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 252980ee..83b9f2e8 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -26,17 +26,6 @@ add_jsonschema_test_unix(format_directory_ignore_directory) add_jsonschema_test_unix(format_directory_ignore_file) add_jsonschema_test_unix(format_check_single_invalid) add_jsonschema_test_unix(frame) -add_jsonschema_test_unix(validate_pass_draft4) -add_jsonschema_test_unix(validate_fail_draft4) -add_jsonschema_test_unix(validate_pass_draft6) -add_jsonschema_test_unix(validate_fail_draft6) -add_jsonschema_test_unix(validate_pass_draft7) -add_jsonschema_test_unix(validate_fail_draft7) -add_jsonschema_test_unix(validate_fail_remote_no_http) -add_jsonschema_test_unix(validate_fail_invalid_ref) -add_jsonschema_test_unix(validate_fail_no_schema) -add_jsonschema_test_unix(validate_fail_no_instance) -add_jsonschema_test_unix(validate_non_supported) add_jsonschema_test_unix(bundle_non_remote) add_jsonschema_test_unix(bundle_into_resolve_directory) add_jsonschema_test_unix(bundle_remote_single_schema) @@ -55,5 +44,39 @@ add_jsonschema_test_unix(metaschema_fail_non_schema) add_jsonschema_test_unix(metaschema_pass_cwd) add_jsonschema_test_unix(metaschema_pass_single) +# Validate +add_jsonschema_test_unix(validate/fail_instance_directory) +add_jsonschema_test_unix(validate/fail_instance_enoent) +add_jsonschema_test_unix(validate/fail_instance_invalid_json) +add_jsonschema_test_unix(validate/fail_invalid_ref) +add_jsonschema_test_unix(validate/fail_no_instance) +add_jsonschema_test_unix(validate/fail_no_schema) +add_jsonschema_test_unix(validate/fail_relative_external_ref_missing) +add_jsonschema_test_unix(validate/fail_resolve_enoent) +add_jsonschema_test_unix(validate/fail_resolve_directory_with_invalid_json) +add_jsonschema_test_unix(validate/fail_resolve_invalid_json) +add_jsonschema_test_unix(validate/fail_schema_directory) +add_jsonschema_test_unix(validate/fail_schema_enoent) +add_jsonschema_test_unix(validate/fail_schema_invalid_json) +add_jsonschema_test_unix(validate/fail_schema_non_schema) +add_jsonschema_test_unix(validate/fail_schema_unknown_dialect) +add_jsonschema_test_unix(validate/fail_schema_unsupported_dialect) +add_jsonschema_test_unix(validate/pass_resolve) +add_jsonschema_test_unix(validate/pass_resolve_custom_extension) +add_jsonschema_test_unix(validate/pass_resolve_verbose) +add_jsonschema_test_unix(validate/pass_verbose) +add_jsonschema_test_unix(validate/pass_draft4) +add_jsonschema_test_unix(validate/pass_draft6) +add_jsonschema_test_unix(validate/pass_draft7) +add_jsonschema_test_unix(validate/fail_draft4) +add_jsonschema_test_unix(validate/fail_draft6) +add_jsonschema_test_unix(validate/fail_draft7) + # CI specific tests add_jsonschema_test_unix_ci(bundle_remote_http) +add_jsonschema_test_unix_ci(fail_validate_http_non_200) +add_jsonschema_test_unix_ci(fail_validate_http_non_200_verbose) +add_jsonschema_test_unix_ci(fail_validate_http_non_schema) +add_jsonschema_test_unix_ci(fail_validate_http_non_schema_verbose) +add_jsonschema_test_unix_ci(pass_validate_http) +add_jsonschema_test_unix_ci(pass_validate_http_verbose) diff --git a/test/ci/fail_validate_http_non_200.sh b/test/ci/fail_validate_http_non_200.sh new file mode 100755 index 00000000..4b64dc34 --- /dev/null +++ b/test/ci/fail_validate_http_non_200.sh @@ -0,0 +1,30 @@ +#!/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-07/schema#", + "allOf": [ { "$ref": "https://example.com" } ] +} +EOF + +cat << 'EOF' > "$TMP/instance.json" +{ "type": "string" } +EOF + +"$1" validate "$TMP/schema.json" "$TMP/instance.json" --http 2> "$TMP/stderr.txt" \ + && CODE="$?" || CODE="$?" +test "$CODE" = "1" || exit 1 + +cat << EOF > "$TMP/expected.txt" +error: 400 Bad Request + at https://example.com +EOF + +diff "$TMP/stderr.txt" "$TMP/expected.txt" diff --git a/test/ci/fail_validate_http_non_200_verbose.sh b/test/ci/fail_validate_http_non_200_verbose.sh new file mode 100755 index 00000000..8d00515e --- /dev/null +++ b/test/ci/fail_validate_http_non_200_verbose.sh @@ -0,0 +1,31 @@ +#!/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-07/schema#", + "allOf": [ { "$ref": "https://example.com" } ] +} +EOF + +cat << 'EOF' > "$TMP/instance.json" +{ "type": "string" } +EOF + +"$1" validate "$TMP/schema.json" "$TMP/instance.json" --http --verbose 2> "$TMP/stderr.txt" \ + && CODE="$?" || CODE="$?" +test "$CODE" = "1" || exit 1 + +cat << EOF > "$TMP/expected.txt" +Resolving over HTTP: https://example.com +error: 400 Bad Request + at https://example.com +EOF + +diff "$TMP/stderr.txt" "$TMP/expected.txt" diff --git a/test/ci/fail_validate_http_non_schema.sh b/test/ci/fail_validate_http_non_schema.sh new file mode 100755 index 00000000..d48ecac9 --- /dev/null +++ b/test/ci/fail_validate_http_non_schema.sh @@ -0,0 +1,30 @@ +#!/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-07/schema#", + "allOf": [ { "$ref": "https://jsonplaceholder.typicode.com/todos/1" } ] +} +EOF + +cat << 'EOF' > "$TMP/instance.json" +{ "type": "string" } +EOF + +"$1" validate "$TMP/schema.json" "$TMP/instance.json" --http 2> "$TMP/stderr.txt" \ + && CODE="$?" || CODE="$?" +test "$CODE" = "1" || exit 1 + +cat << EOF > "$TMP/expected.txt" +error: The JSON document is not a valid JSON Schema + at https://jsonplaceholder.typicode.com/todos/1 +EOF + +diff "$TMP/stderr.txt" "$TMP/expected.txt" diff --git a/test/ci/fail_validate_http_non_schema_verbose.sh b/test/ci/fail_validate_http_non_schema_verbose.sh new file mode 100755 index 00000000..bffcc961 --- /dev/null +++ b/test/ci/fail_validate_http_non_schema_verbose.sh @@ -0,0 +1,31 @@ +#!/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-07/schema#", + "allOf": [ { "$ref": "https://jsonplaceholder.typicode.com/todos/1" } ] +} +EOF + +cat << 'EOF' > "$TMP/instance.json" +{ "type": "string" } +EOF + +"$1" validate "$TMP/schema.json" "$TMP/instance.json" --http --verbose 2> "$TMP/stderr.txt" \ + && CODE="$?" || CODE="$?" +test "$CODE" = "1" || exit 1 + +cat << EOF > "$TMP/expected.txt" +Resolving over HTTP: https://jsonplaceholder.typicode.com/todos/1 +error: The JSON document is not a valid JSON Schema + at https://jsonplaceholder.typicode.com/todos/1 +EOF + +diff "$TMP/stderr.txt" "$TMP/expected.txt" diff --git a/test/ci/pass_validate_http.sh b/test/ci/pass_validate_http.sh new file mode 100755 index 00000000..707aa598 --- /dev/null +++ b/test/ci/pass_validate_http.sh @@ -0,0 +1,26 @@ +#!/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-07/schema#", + "allOf": [ { "$ref": "https://json.schemastore.org/mocharc.json" } ] +} +EOF + +cat << 'EOF' > "$TMP/instance.json" +{ "exit": true } +EOF + +"$1" validate "$TMP/schema.json" "$TMP/instance.json" --http 2> "$TMP/stderr.txt" + +cat << EOF > "$TMP/expected.txt" +EOF + +diff "$TMP/stderr.txt" "$TMP/expected.txt" diff --git a/test/ci/pass_validate_http_verbose.sh b/test/ci/pass_validate_http_verbose.sh new file mode 100755 index 00000000..2aaff508 --- /dev/null +++ b/test/ci/pass_validate_http_verbose.sh @@ -0,0 +1,29 @@ +#!/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-07/schema#", + "allOf": [ { "$ref": "https://json.schemastore.org/mocharc.json" } ] +} +EOF + +cat << 'EOF' > "$TMP/instance.json" +{ "exit": true } +EOF + +"$1" validate "$TMP/schema.json" "$TMP/instance.json" --http --verbose 2> "$TMP/stderr.txt" + +cat << EOF > "$TMP/expected.txt" +Resolving over HTTP: https://json.schemastore.org/mocharc.json +ok: $(realpath "$TMP")/instance.json + matches $(realpath "$TMP")/schema.json +EOF + +diff "$TMP/stderr.txt" "$TMP/expected.txt" diff --git a/test/format_check_single_invalid.sh b/test/format_check_single_invalid.sh index d58f4cc7..a01791b8 100755 --- a/test/format_check_single_invalid.sh +++ b/test/format_check_single_invalid.sh @@ -22,10 +22,4 @@ then exit 1 fi -cat << EOF > "$TMP/expected.txt" -$(realpath "$TMP/schema.json") - Failed to parse the JSON document at line 3 and column 3 -EOF - -diff "$TMP/output.txt" "$TMP/expected.txt" echo "PASS" 1>&2 diff --git a/test/validate/fail_draft4.sh b/test/validate/fail_draft4.sh new file mode 100755 index 00000000..8a5f4459 --- /dev/null +++ b/test/validate/fail_draft4.sh @@ -0,0 +1,39 @@ +#!/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#", + "type": "object", + "properties": { + "foo": { + "type": "string" + } + } +} +EOF + +cat << 'EOF' > "$TMP/instance.json" +{ "foo": 1 } +EOF + +"$1" validate "$TMP/schema.json" "$TMP/instance.json" 2> "$TMP/stderr.txt" \ + && CODE="$?" || CODE="$?" +test "$CODE" = "1" || exit 1 + +cat << EOF > "$TMP/expected.txt" +error: The target document is expected to be of the given type + at instance location "/foo" + at evaluate path "/properties/foo/type" +error: The target is expected to match all of the given assertions + at instance location "" + at evaluate path "/properties" +EOF + +diff "$TMP/stderr.txt" "$TMP/expected.txt" diff --git a/test/validate/fail_draft6.sh b/test/validate/fail_draft6.sh new file mode 100755 index 00000000..0d5efb61 --- /dev/null +++ b/test/validate/fail_draft6.sh @@ -0,0 +1,39 @@ +#!/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-06/schema#", + "type": "object", + "properties": { + "foo": { + "type": "string" + } + } +} +EOF + +cat << 'EOF' > "$TMP/instance.json" +{ "foo": 1 } +EOF + +"$1" validate "$TMP/schema.json" "$TMP/instance.json" 2> "$TMP/stderr.txt" \ + && CODE="$?" || CODE="$?" +test "$CODE" = "1" || exit 1 + +cat << EOF > "$TMP/expected.txt" +error: The target document is expected to be of the given type + at instance location "/foo" + at evaluate path "/properties/foo/type" +error: The target is expected to match all of the given assertions + at instance location "" + at evaluate path "/properties" +EOF + +diff "$TMP/stderr.txt" "$TMP/expected.txt" diff --git a/test/validate/fail_draft7.sh b/test/validate/fail_draft7.sh new file mode 100755 index 00000000..a0135c9a --- /dev/null +++ b/test/validate/fail_draft7.sh @@ -0,0 +1,39 @@ +#!/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-07/schema#", + "type": "object", + "properties": { + "foo": { + "type": "string" + } + } +} +EOF + +cat << 'EOF' > "$TMP/instance.json" +{ "foo": 1 } +EOF + +"$1" validate "$TMP/schema.json" "$TMP/instance.json" 2> "$TMP/stderr.txt" \ + && CODE="$?" || CODE="$?" +test "$CODE" = "1" || exit 1 + +cat << EOF > "$TMP/expected.txt" +error: The target document is expected to be of the given type + at instance location "/foo" + at evaluate path "/properties/foo/type" +error: The target is expected to match all of the given assertions + at instance location "" + at evaluate path "/properties" +EOF + +diff "$TMP/stderr.txt" "$TMP/expected.txt" diff --git a/test/validate/fail_instance_directory.sh b/test/validate/fail_instance_directory.sh new file mode 100755 index 00000000..151fdbc6 --- /dev/null +++ b/test/validate/fail_instance_directory.sh @@ -0,0 +1,28 @@ +#!/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#", + "type": "string" +} +EOF + +mkdir "$TMP/instance" + +"$1" validate "$TMP/schema.json" "$TMP/instance" 2>"$TMP/stderr.txt" \ + && CODE="$?" || CODE="$?" +test "$CODE" = "1" || exit 1 + +cat << EOF > "$TMP/expected.txt" +error: The input was supposed to be a file but it is a directory + $(realpath "$TMP")/instance +EOF + +diff "$TMP/stderr.txt" "$TMP/expected.txt" diff --git a/test/validate/fail_instance_enoent.sh b/test/validate/fail_instance_enoent.sh new file mode 100755 index 00000000..35109701 --- /dev/null +++ b/test/validate/fail_instance_enoent.sh @@ -0,0 +1,26 @@ +#!/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#", + "type": "string" +} +EOF + +"$1" validate "$TMP/schema.json" "$TMP/foo.json" 2>"$TMP/stderr.txt" \ + && CODE="$?" || CODE="$?" +test "$CODE" = "1" || exit 1 + +cat << EOF > "$TMP/expected.txt" +error: No such file or directory + $(realpath "$TMP")/foo.json +EOF + +diff "$TMP/stderr.txt" "$TMP/expected.txt" diff --git a/test/validate/fail_instance_invalid_json.sh b/test/validate/fail_instance_invalid_json.sh new file mode 100755 index 00000000..d6abd965 --- /dev/null +++ b/test/validate/fail_instance_invalid_json.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#", + "type": "string" +} +EOF + +cat << 'EOF' > "$TMP/instance.json" +{ + "foo" 1 +} +EOF + +"$1" validate "$TMP/schema.json" "$TMP/instance.json" 2>"$TMP/stderr.txt" \ + && CODE="$?" || CODE="$?" +test "$CODE" = "1" || exit 1 + +cat << EOF > "$TMP/expected.txt" +error: Failed to parse the JSON document at line 2 and column 9 + $(realpath "$TMP")/instance.json +EOF + +diff "$TMP/stderr.txt" "$TMP/expected.txt" diff --git a/test/validate_fail_invalid_ref.sh b/test/validate/fail_invalid_ref.sh similarity index 80% rename from test/validate_fail_invalid_ref.sh rename to test/validate/fail_invalid_ref.sh index 83363fc9..9f6cfe88 100755 --- a/test/validate_fail_invalid_ref.sh +++ b/test/validate/fail_invalid_ref.sh @@ -24,17 +24,12 @@ EOF "$1" validate "$TMP/schema.json" "$TMP/instance.json" 2>"$TMP/stderr.txt" \ && CODE="$?" || CODE="$?" - -if [ "$CODE" = "0" ] -then - echo "FAIL" 1>&2 - exit 1 -fi +test "$CODE" = "1" || exit 1 cat << 'EOF' > "$TMP/expected.txt" -Could not resolve schema reference: #/definitions/i-dont-exist +error: Could not resolve schema reference + #/definitions/i-dont-exist at schema location "/properties/foo/$ref" EOF diff "$TMP/stderr.txt" "$TMP/expected.txt" -echo "PASS" 1>&2 diff --git a/test/validate_fail_no_instance.sh b/test/validate/fail_no_instance.sh similarity index 75% rename from test/validate_fail_no_instance.sh rename to test/validate/fail_no_instance.sh index d21146b2..487b8d38 100755 --- a/test/validate_fail_no_instance.sh +++ b/test/validate/fail_no_instance.sh @@ -14,14 +14,8 @@ cat << 'EOF' > "$TMP/schema.json" } EOF -"$1" validate "$TMP/schema.json" 2>"$TMP/stderr.txt" \ - && CODE="$?" || CODE="$?" - -if [ "$CODE" = "0" ] -then - echo "FAIL" 1>&2 - exit 1 -fi +"$1" validate "$TMP/schema.json" 2>"$TMP/stderr.txt" && CODE="$?" || CODE="$?" +test "$CODE" = "1" || exit 1 cat << 'EOF' > "$TMP/expected.txt" error: In addition to the schema, you must also pass a argument @@ -31,4 +25,3 @@ that represents the instance to validate against. For example: EOF diff "$TMP/stderr.txt" "$TMP/expected.txt" -echo "PASS" 1>&2 diff --git a/test/validate_fail_no_schema.sh b/test/validate/fail_no_schema.sh similarity index 73% rename from test/validate_fail_no_schema.sh rename to test/validate/fail_no_schema.sh index 617c7e9d..108e7cf9 100755 --- a/test/validate_fail_no_schema.sh +++ b/test/validate/fail_no_schema.sh @@ -7,14 +7,8 @@ TMP="$(mktemp -d)" clean() { rm -rf "$TMP"; } trap clean EXIT -"$1" validate 2>"$TMP/stderr.txt" \ - && CODE="$?" || CODE="$?" - -if [ "$CODE" = "0" ] -then - echo "FAIL" 1>&2 - exit 1 -fi +"$1" validate 2>"$TMP/stderr.txt" && CODE="$?" || CODE="$?" +test "$CODE" = "1" || exit 1 cat << 'EOF' > "$TMP/expected.txt" error: This command expects to pass a path to a schema and a @@ -24,4 +18,3 @@ path to an instance to validate against the schema. For example: EOF diff "$TMP/stderr.txt" "$TMP/expected.txt" -echo "PASS" 1>&2 diff --git a/test/validate/fail_relative_external_ref_missing.sh b/test/validate/fail_relative_external_ref_missing.sh new file mode 100755 index 00000000..d7ee9cfa --- /dev/null +++ b/test/validate/fail_relative_external_ref_missing.sh @@ -0,0 +1,31 @@ +#!/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#", + "id": "https://example.com", + "$ref": "nested" +} +EOF + +cat << 'EOF' > "$TMP/instance.json" +{ "foo": 1 } +EOF + +"$1" validate "$TMP/schema.json" "$TMP/instance.json" 2>"$TMP/stderr.txt" \ + && CODE="$?" || CODE="$?" +test "$CODE" = "1" || exit 1 + +cat << EOF > "$TMP/expected.txt" +error: Could not resolve the requested schema + at https://example.com/nested +EOF + +diff "$TMP/stderr.txt" "$TMP/expected.txt" diff --git a/test/validate/fail_resolve_directory_with_invalid_json.sh b/test/validate/fail_resolve_directory_with_invalid_json.sh new file mode 100755 index 00000000..005d13e8 --- /dev/null +++ b/test/validate/fail_resolve_directory_with_invalid_json.sh @@ -0,0 +1,41 @@ +#!/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#", + "type": "string" +} +EOF + +cat << 'EOF' > "$TMP/instance.json" +"foo" +EOF + +mkdir "$TMP/schemas" + +cat << 'EOF' > "$TMP/schemas/01-valid.json" +{ "foo": 1 } +EOF + +cat << 'EOF' > "$TMP/schemas/02-invalid.json" +{ xxx } +EOF + +"$1" validate "$TMP/schema.json" "$TMP/instance.json" \ + --resolve "$TMP/schemas" 2>"$TMP/stderr.txt" \ + && CODE="$?" || CODE="$?" +test "$CODE" = "1" || exit 1 + +cat << EOF > "$TMP/expected.txt" +error: Failed to parse the JSON document at line 1 and column 3 + $(realpath "$TMP")/schemas/02-invalid.json +EOF + +diff "$TMP/stderr.txt" "$TMP/expected.txt" diff --git a/test/validate/fail_resolve_enoent.sh b/test/validate/fail_resolve_enoent.sh new file mode 100755 index 00000000..c0a67ee4 --- /dev/null +++ b/test/validate/fail_resolve_enoent.sh @@ -0,0 +1,31 @@ +#!/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#", + "type": "string" +} +EOF + +cat << 'EOF' > "$TMP/instance.json" +"foo" +EOF + +"$1" validate "$TMP/schema.json" "$TMP/instance.json" \ + --resolve "$TMP/test" 2>"$TMP/stderr.txt" \ + && CODE="$?" || CODE="$?" +test "$CODE" = "1" || exit 1 + +cat << EOF > "$TMP/expected.txt" +error: No such file or directory + $(realpath "$TMP")/test +EOF + +diff "$TMP/stderr.txt" "$TMP/expected.txt" diff --git a/test/validate/fail_resolve_invalid_json.sh b/test/validate/fail_resolve_invalid_json.sh new file mode 100755 index 00000000..5943bc05 --- /dev/null +++ b/test/validate/fail_resolve_invalid_json.sh @@ -0,0 +1,35 @@ +#!/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#", + "type": "string" +} +EOF + +cat << 'EOF' > "$TMP/instance.json" +"foo" +EOF + +cat << 'EOF' > "$TMP/invalid.json" +{ xxx } +EOF + +"$1" validate "$TMP/schema.json" "$TMP/instance.json" \ + --resolve "$TMP/invalid.json" 2>"$TMP/stderr.txt" \ + && CODE="$?" || CODE="$?" +test "$CODE" = "1" || exit 1 + +cat << EOF > "$TMP/expected.txt" +error: Failed to parse the JSON document at line 1 and column 3 + $(realpath "$TMP")/invalid.json +EOF + +diff "$TMP/stderr.txt" "$TMP/expected.txt" diff --git a/test/validate/fail_schema_directory.sh b/test/validate/fail_schema_directory.sh new file mode 100755 index 00000000..a9d01822 --- /dev/null +++ b/test/validate/fail_schema_directory.sh @@ -0,0 +1,25 @@ +#!/bin/sh + +set -o errexit +set -o nounset + +TMP="$(mktemp -d)" +clean() { rm -rf "$TMP"; } +trap clean EXIT + +mkdir "$TMP/schema-directory" + +cat << 'EOF' > "$TMP/instance.json" +{ "foo": 1 } +EOF + +"$1" validate "$TMP/schema-directory" "$TMP/instance.json" 2>"$TMP/stderr.txt" \ + && CODE="$?" || CODE="$?" +test "$CODE" = "1" || exit 1 + +cat << EOF > "$TMP/expected.txt" +error: The input was supposed to be a file but it is a directory + $(realpath "$TMP")/schema-directory +EOF + +diff "$TMP/stderr.txt" "$TMP/expected.txt" diff --git a/test/validate/fail_schema_enoent.sh b/test/validate/fail_schema_enoent.sh new file mode 100755 index 00000000..cc223f9d --- /dev/null +++ b/test/validate/fail_schema_enoent.sh @@ -0,0 +1,23 @@ +#!/bin/sh + +set -o errexit +set -o nounset + +TMP="$(mktemp -d)" +clean() { rm -rf "$TMP"; } +trap clean EXIT + +cat << 'EOF' > "$TMP/instance.json" +{ "foo": 1 } +EOF + +"$1" validate "$TMP/foo.json" "$TMP/instance.json" 2>"$TMP/stderr.txt" \ + && CODE="$?" || CODE="$?" +test "$CODE" = "1" || exit 1 + +cat << EOF > "$TMP/expected.txt" +error: No such file or directory + $(realpath "$TMP")/foo.json +EOF + +diff "$TMP/stderr.txt" "$TMP/expected.txt" diff --git a/test/validate/fail_schema_invalid_json.sh b/test/validate/fail_schema_invalid_json.sh new file mode 100755 index 00000000..582f9a83 --- /dev/null +++ b/test/validate/fail_schema_invalid_json.sh @@ -0,0 +1,29 @@ +#!/bin/sh + +set -o errexit +set -o nounset + +TMP="$(mktemp -d)" +clean() { rm -rf "$TMP"; } +trap clean EXIT + +cat << 'EOF' > "$TMP/schema.json" +{ + "type" string +} +EOF + +cat << 'EOF' > "$TMP/instance.json" +{ "foo": 1 } +EOF + +"$1" validate "$TMP/schema.json" "$TMP/instance.json" 2>"$TMP/stderr.txt" \ + && CODE="$?" || CODE="$?" +test "$CODE" = "1" || exit 1 + +cat << EOF > "$TMP/expected.txt" +error: Failed to parse the JSON document at line 2 and column 10 + $(realpath "$TMP")/schema.json +EOF + +diff "$TMP/stderr.txt" "$TMP/expected.txt" diff --git a/test/validate/fail_schema_non_schema.sh b/test/validate/fail_schema_non_schema.sh new file mode 100755 index 00000000..1b956c89 --- /dev/null +++ b/test/validate/fail_schema_non_schema.sh @@ -0,0 +1,27 @@ +#!/bin/sh + +set -o errexit +set -o nounset + +TMP="$(mktemp -d)" +clean() { rm -rf "$TMP"; } +trap clean EXIT + +cat << 'EOF' > "$TMP/schema.json" +[ { "type": "string" } ] +EOF + +cat << 'EOF' > "$TMP/instance.json" +{ "foo": "bar" } +EOF + +"$1" validate "$TMP/schema.json" "$TMP/instance.json" 2>"$TMP/stderr.txt" \ + && CODE="$?" || CODE="$?" +test "$CODE" = "1" || exit 1 + +cat << EOF > "$TMP/expected.txt" +error: The schema file you provided does not represent a valid JSON Schema + $(realpath "$TMP")/schema.json +EOF + +diff "$TMP/stderr.txt" "$TMP/expected.txt" diff --git a/test/validate/fail_schema_unknown_dialect.sh b/test/validate/fail_schema_unknown_dialect.sh new file mode 100755 index 00000000..22d29ed6 --- /dev/null +++ b/test/validate/fail_schema_unknown_dialect.sh @@ -0,0 +1,30 @@ +#!/bin/sh + +set -o errexit +set -o nounset + +TMP="$(mktemp -d)" +clean() { rm -rf "$TMP"; } +trap clean EXIT + +cat << 'EOF' > "$TMP/schema.json" +{ + "$schema": "https://example.com/unknown", + "type": "string" +} +EOF + +cat << 'EOF' > "$TMP/instance.json" +{ "foo": "bar" } +EOF + +"$1" validate "$TMP/schema.json" "$TMP/instance.json" 2>"$TMP/stderr.txt" \ + && CODE="$?" || CODE="$?" +test "$CODE" = "1" || exit 1 + +cat << EOF > "$TMP/expected.txt" +error: Could not resolve the requested schema + at https://example.com/unknown +EOF + +diff "$TMP/stderr.txt" "$TMP/expected.txt" diff --git a/test/validate/fail_schema_unsupported_dialect.sh b/test/validate/fail_schema_unsupported_dialect.sh new file mode 100755 index 00000000..323beb2f --- /dev/null +++ b/test/validate/fail_schema_unsupported_dialect.sh @@ -0,0 +1,33 @@ +#!/bin/sh + +set -o errexit +set -o nounset + +TMP="$(mktemp -d)" +clean() { rm -rf "$TMP"; } +trap clean EXIT + +cat << 'EOF' > "$TMP/schema.json" +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "string" +} +EOF + +cat << 'EOF' > "$TMP/instance.json" +{ "foo": "bar" } +EOF + +"$1" validate "$TMP/schema.json" "$TMP/instance.json" 2>"$TMP/stderr.txt" \ + && CODE="$?" || CODE="$?" +test "$CODE" = "1" || exit 1 + +cat << EOF > "$TMP/expected.txt" +error: Cannot compile unsupported vocabulary + https://json-schema.org/draft/2020-12/vocab/applicator + +To request support for it, please open an issue at +https://github.com/intelligence-ai/jsonschema +EOF + +diff "$TMP/stderr.txt" "$TMP/expected.txt" diff --git a/test/validate_pass_draft4.sh b/test/validate/pass_draft4.sh similarity index 100% rename from test/validate_pass_draft4.sh rename to test/validate/pass_draft4.sh diff --git a/test/validate_pass_draft6.sh b/test/validate/pass_draft6.sh similarity index 100% rename from test/validate_pass_draft6.sh rename to test/validate/pass_draft6.sh diff --git a/test/validate_pass_draft7.sh b/test/validate/pass_draft7.sh similarity index 100% rename from test/validate_pass_draft7.sh rename to test/validate/pass_draft7.sh diff --git a/test/validate/pass_resolve.sh b/test/validate/pass_resolve.sh new file mode 100755 index 00000000..397ae075 --- /dev/null +++ b/test/validate/pass_resolve.sh @@ -0,0 +1,56 @@ +#!/bin/sh + +set -o errexit +set -o nounset + +TMP="$(mktemp -d)" +clean() { rm -rf "$TMP"; } +trap clean EXIT + +cat << 'EOF' > "$TMP/schema.json" +{ + "id": "https://example.com", + "$schema": "http://json-schema.org/draft-04/schema#", + "properties": { + "foo": { "$ref": "foo" }, + "bar": { "$ref": "bar" } + } +} +EOF + +cat << 'EOF' > "$TMP/foo.json" +{ + "id": "https://example.com/foo", + "$schema": "http://json-schema.org/draft-04/schema#", + "type": "integer" +} +EOF + +mkdir "$TMP/schemas" +cat << 'EOF' > "$TMP/schemas/baz.json" +{ + "id": "https://example.com/baz", + "$schema": "http://json-schema.org/draft-04/schema#", + "type": "array" +} +EOF + +cat << 'EOF' > "$TMP/schemas/ignore-me.txt" +Foo Bar +EOF + +mkdir "$TMP/schemas/nested" +cat << 'EOF' > "$TMP/schemas/nested/bar.json" +{ + "id": "https://example.com/bar", + "$schema": "http://json-schema.org/draft-04/schema#", + "type": "string" +} +EOF + +cat << 'EOF' > "$TMP/instance.json" +{ "foo": 1 } +EOF + +"$1" validate "$TMP/schema.json" "$TMP/instance.json" \ + --resolve "$TMP/foo.json" --resolve "$TMP/schemas" diff --git a/test/validate/pass_resolve_custom_extension.sh b/test/validate/pass_resolve_custom_extension.sh new file mode 100755 index 00000000..733cd6e5 --- /dev/null +++ b/test/validate/pass_resolve_custom_extension.sh @@ -0,0 +1,56 @@ +#!/bin/sh + +set -o errexit +set -o nounset + +TMP="$(mktemp -d)" +clean() { rm -rf "$TMP"; } +trap clean EXIT + +cat << 'EOF' > "$TMP/schema.json" +{ + "id": "https://example.com", + "$schema": "http://json-schema.org/draft-04/schema#", + "properties": { + "foo": { "$ref": "foo" }, + "bar": { "$ref": "bar" } + } +} +EOF + +cat << 'EOF' > "$TMP/foo.schema" +{ + "id": "https://example.com/foo", + "$schema": "http://json-schema.org/draft-04/schema#", + "type": "integer" +} +EOF + +mkdir "$TMP/schemas" +cat << 'EOF' > "$TMP/schemas/baz.schema" +{ + "id": "https://example.com/baz", + "$schema": "http://json-schema.org/draft-04/schema#", + "type": "array" +} +EOF + +cat << 'EOF' > "$TMP/schemas/ignore-me.txt" +Foo Bar +EOF + +mkdir "$TMP/schemas/nested" +cat << 'EOF' > "$TMP/schemas/nested/bar.schema" +{ + "id": "https://example.com/bar", + "$schema": "http://json-schema.org/draft-04/schema#", + "type": "string" +} +EOF + +cat << 'EOF' > "$TMP/instance.json" +{ "foo": 1 } +EOF + +"$1" validate "$TMP/schema.json" "$TMP/instance.json" \ + --resolve "$TMP/foo.schema" --resolve "$TMP/schemas" --extension .schema diff --git a/test/validate/pass_resolve_verbose.sh b/test/validate/pass_resolve_verbose.sh new file mode 100755 index 00000000..c3fa3b0c --- /dev/null +++ b/test/validate/pass_resolve_verbose.sh @@ -0,0 +1,66 @@ +#!/bin/sh + +set -o errexit +set -o nounset + +TMP="$(mktemp -d)" +clean() { rm -rf "$TMP"; } +trap clean EXIT + +cat << 'EOF' > "$TMP/schema.json" +{ + "id": "https://example.com", + "$schema": "http://json-schema.org/draft-04/schema#", + "properties": { + "foo": { "$ref": "foo" }, + "bar": { "$ref": "bar" } + } +} +EOF + +cat << 'EOF' > "$TMP/foo.json" +{ + "id": "https://example.com/foo", + "$schema": "http://json-schema.org/draft-04/schema#", + "type": "integer" +} +EOF + +mkdir "$TMP/schemas" +cat << 'EOF' > "$TMP/schemas/baz.json" +{ + "id": "https://example.com/baz", + "$schema": "http://json-schema.org/draft-04/schema#", + "type": "array" +} +EOF + +cat << 'EOF' > "$TMP/schemas/ignore-me.txt" +Foo Bar +EOF + +mkdir "$TMP/schemas/nested" +cat << 'EOF' > "$TMP/schemas/nested/bar.json" +{ + "id": "https://example.com/bar", + "$schema": "http://json-schema.org/draft-04/schema#", + "type": "string" +} +EOF + +cat << 'EOF' > "$TMP/instance.json" +{ "foo": 1 } +EOF + +"$1" validate "$TMP/schema.json" "$TMP/instance.json" \ + --resolve "$TMP/foo.json" --resolve "$TMP/schemas" --verbose 2> "$TMP/stderr.txt" + +cat << EOF > "$TMP/expected.txt" +Importing schema into the resolution context: $(realpath "$TMP")/foo.json +Importing schema into the resolution context: $(realpath "$TMP")/schemas/baz.json +Importing schema into the resolution context: $(realpath "$TMP")/schemas/nested/bar.json +ok: $(realpath "$TMP")/instance.json + matches $(realpath "$TMP")/schema.json +EOF + +diff "$TMP/stderr.txt" "$TMP/expected.txt" diff --git a/test/validate_fail_draft4.sh b/test/validate/pass_verbose.sh similarity index 54% rename from test/validate_fail_draft4.sh rename to test/validate/pass_verbose.sh index bf3933d1..7a688da8 100755 --- a/test/validate_fail_draft4.sh +++ b/test/validate/pass_verbose.sh @@ -10,7 +10,6 @@ trap clean EXIT cat << 'EOF' > "$TMP/schema.json" { "$schema": "http://json-schema.org/draft-04/schema#", - "type": "object", "properties": { "foo": { "type": "string" @@ -20,15 +19,14 @@ cat << 'EOF' > "$TMP/schema.json" EOF cat << 'EOF' > "$TMP/instance.json" -{ "foo": 1 } +{ "foo": "bar" } EOF -"$1" validate "$TMP/schema.json" "$TMP/instance.json" && CODE="$?" || CODE="$?" +"$1" validate "$TMP/schema.json" "$TMP/instance.json" --verbose 2> "$TMP/stderr.txt" -if [ "$CODE" = "0" ] -then - echo "FAIL" 1>&2 - exit 1 -else - echo "PASS" 1>&2 -fi +cat << EOF > "$TMP/expected.txt" +ok: $(realpath "$TMP")/instance.json + matches $(realpath "$TMP")/schema.json +EOF + +diff "$TMP/stderr.txt" "$TMP/expected.txt" diff --git a/test/validate_fail_draft6.sh b/test/validate_fail_draft6.sh deleted file mode 100755 index 2b7ec341..00000000 --- a/test/validate_fail_draft6.sh +++ /dev/null @@ -1,34 +0,0 @@ -#!/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-06/schema#", - "type": "object", - "properties": { - "foo": { - "type": "string" - } - } -} -EOF - -cat << 'EOF' > "$TMP/instance.json" -{ "foo": 1 } -EOF - -"$1" validate "$TMP/schema.json" "$TMP/instance.json" && CODE="$?" || CODE="$?" - -if [ "$CODE" = "0" ] -then - echo "FAIL" 1>&2 - exit 1 -else - echo "PASS" 1>&2 -fi diff --git a/test/validate_fail_draft7.sh b/test/validate_fail_draft7.sh deleted file mode 100755 index 5cbb40d3..00000000 --- a/test/validate_fail_draft7.sh +++ /dev/null @@ -1,34 +0,0 @@ -#!/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-07/schema#", - "type": "object", - "properties": { - "foo": { - "type": "string" - } - } -} -EOF - -cat << 'EOF' > "$TMP/instance.json" -{ "foo": 1 } -EOF - -"$1" validate "$TMP/schema.json" "$TMP/instance.json" && CODE="$?" || CODE="$?" - -if [ "$CODE" = "0" ] -then - echo "FAIL" 1>&2 - exit 1 -else - echo "PASS" 1>&2 -fi diff --git a/test/validate_fail_remote_no_http.sh b/test/validate_fail_remote_no_http.sh deleted file mode 100755 index 195c67ed..00000000 --- a/test/validate_fail_remote_no_http.sh +++ /dev/null @@ -1,30 +0,0 @@ -#!/bin/sh - -set -o errexit -set -o nounset - -TMP="$(mktemp -d)" -clean() { rm -rf "$TMP"; } -trap clean EXIT - -cat << 'EOF' > "$TMP/schema.json" -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://example.com", - "$ref": "nested" -} -EOF - -cat << 'EOF' > "$TMP/instance.json" -{ "foo": 1 } -EOF - -"$1" validate "$TMP/schema.json" "$TMP/instance.json" && CODE="$?" || CODE="$?" - -if [ "$CODE" = "0" ] -then - echo "FAIL" 1>&2 - exit 1 -else - echo "PASS" 1>&2 -fi diff --git a/test/validate_non_supported.sh b/test/validate_non_supported.sh deleted file mode 100755 index b1e14ac3..00000000 --- a/test/validate_non_supported.sh +++ /dev/null @@ -1,34 +0,0 @@ -#!/bin/sh - -set -o errexit -set -o nounset - -TMP="$(mktemp -d)" -clean() { rm -rf "$TMP"; } -trap clean EXIT - -cat << 'EOF' > "$TMP/schema.json" -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "type": "object", - "properties": { - "foo": { - "type": "string" - } - } -} -EOF - -cat << 'EOF' > "$TMP/instance.json" -{ "foo": 1 } -EOF - -"$1" validate "$TMP/schema.json" "$TMP/instance.json" && CODE="$?" || CODE="$?" - -if [ "$CODE" = "0" ] -then - echo "FAIL" 1>&2 - exit 1 -else - echo "PASS" 1>&2 -fi diff --git a/vendor/jsontoolkit/src/json/include/sourcemeta/jsontoolkit/json_error.h b/vendor/jsontoolkit/src/json/include/sourcemeta/jsontoolkit/json_error.h index 26ce1217..bc0f0f75 100644 --- a/vendor/jsontoolkit/src/json/include/sourcemeta/jsontoolkit/json_error.h +++ b/vendor/jsontoolkit/src/json/include/sourcemeta/jsontoolkit/json_error.h @@ -54,6 +54,10 @@ class SOURCEMETA_JSONTOOLKIT_JSON_EXPORT FileParseError : public ParseError { const std::uint64_t column) : ParseError{line, column}, path_{path} {} + /// Create a file parsing error from a parse error + FileParseError(const std::filesystem::path &path, const ParseError &parent) + : ParseError{parent.line(), parent.column()}, path_{path} {} + /// Get the fiel path of the error [[nodiscard]] auto path() const noexcept -> const std::filesystem::path { return path_; diff --git a/vendor/jsontoolkit/src/json/json.cc b/vendor/jsontoolkit/src/json/json.cc index 5c576b69..b560c1a6 100644 --- a/vendor/jsontoolkit/src/json/json.cc +++ b/vendor/jsontoolkit/src/json/json.cc @@ -3,9 +3,9 @@ #include -#include // assert -#include // std::ifstream -#include // std::ios_base +#include // assert +#include // std::ifstream +#include // std::make_error_code, std::errc namespace sourcemeta::jsontoolkit { @@ -33,14 +33,22 @@ auto parse(const std::basic_string &input) } auto from_file(const std::filesystem::path &path) -> JSON { - std::ifstream stream{path}; - stream.exceptions(std::ios_base::badbit); + if (std::filesystem::is_directory(path)) { + throw std::filesystem::filesystem_error( + "Cannot parse a directory as JSON", path, + std::make_error_code(std::errc::is_a_directory)); + } + + std::ifstream stream{std::filesystem::canonical(path)}; + stream.exceptions(std::ifstream::failbit | std::ifstream::badbit); + assert(!stream.fail()); + assert(stream.is_open()); try { return parse(stream); } catch (const ParseError &error) { // For producing better error messages - throw FileParseError(path, error.line(), error.column()); + throw FileParseError(path, error); } } diff --git a/vendor/jsontoolkit/src/jsonschema/bundle.cc b/vendor/jsontoolkit/src/jsonschema/bundle.cc index 2827b90c..dfa4aa8b 100644 --- a/vendor/jsontoolkit/src/jsonschema/bundle.cc +++ b/vendor/jsontoolkit/src/jsonschema/bundle.cc @@ -54,8 +54,17 @@ auto upsert_id(sourcemeta::jsontoolkit::JSON &target, const std::string &identifier, const sourcemeta::jsontoolkit::SchemaResolver &resolver, const std::optional &default_dialect) -> void { + if (!sourcemeta::jsontoolkit::is_schema(target)) { + throw sourcemeta::jsontoolkit::SchemaResolutionError( + identifier, "The JSON document is not a valid JSON Schema"); + } + const auto dialect{sourcemeta::jsontoolkit::dialect(target, default_dialect)}; - assert(dialect.has_value()); + if (!dialect.has_value()) { + throw sourcemeta::jsontoolkit::SchemaResolutionError( + identifier, "The JSON document is not a valid JSON Schema"); + } + const auto base_dialect{ sourcemeta::jsontoolkit::base_dialect(target, resolver, dialect).get()}; const auto vocabularies{sourcemeta::jsontoolkit::vocabularies( @@ -130,7 +139,7 @@ auto bundle_schema(sourcemeta::jsontoolkit::JSON &root, } throw sourcemeta::jsontoolkit::SchemaResolutionError( - identifier, "Could not resolve schema"); + identifier, "Could not resolve the requested schema"); } // Otherwise, if the target schema does not declare an inline identifier, diff --git a/vendor/jsontoolkit/src/jsonschema/compile_evaluate.cc b/vendor/jsontoolkit/src/jsonschema/compile_evaluate.cc index 044dbd2b..42a68021 100644 --- a/vendor/jsontoolkit/src/jsonschema/compile_evaluate.cc +++ b/vendor/jsontoolkit/src/jsonschema/compile_evaluate.cc @@ -509,9 +509,7 @@ auto evaluate_step( for (const auto &child : container.children) { if (!evaluate_step(child, instance, mode, callback, context)) { result = false; - if (mode == SchemaCompilerEvaluationMode::Fast) { - break; - } + break; } } diff --git a/vendor/jsontoolkit/src/jsonschema/default_compiler.cc b/vendor/jsontoolkit/src/jsonschema/default_compiler.cc index edf2858b..a66764a3 100644 --- a/vendor/jsontoolkit/src/jsonschema/default_compiler.cc +++ b/vendor/jsontoolkit/src/jsonschema/default_compiler.cc @@ -7,7 +7,6 @@ #include // assert #include // std::set -#include // std::ostringstream #include // std::string // TODO: Support every keyword @@ -23,9 +22,8 @@ auto sourcemeta::jsontoolkit::default_schema_compiler( for (const auto &vocabulary : context.vocabularies) { if (!SUPPORTED_VOCABULARIES.contains(vocabulary.first) && vocabulary.second) { - std::ostringstream error; - error << "Cannot compile unsupported vocabulary: " << vocabulary.first; - throw SchemaError(error.str()); + throw SchemaVocabularyError(vocabulary.first, + "Cannot compile unsupported vocabulary"); } } @@ -104,10 +102,12 @@ auto sourcemeta::jsontoolkit::default_schema_compiler( compiler_draft4_validation_maxproperties); COMPILE("http://json-schema.org/draft-07/schema#", "minProperties", compiler_draft4_validation_minproperties); - COMPILE("http://json-schema.org/draft-07/schema#", "properties", - compiler_draft4_applicator_properties); + COMPILE( + "http://json-schema.org/draft-07/schema#", "properties", + compiler_draft4_applicator_properties); COMPILE("http://json-schema.org/draft-07/schema#", "patternProperties", - compiler_draft4_applicator_patternproperties); + compiler_draft4_applicator_patternproperties< + SchemaCompilerAnnotationPrivate>); COMPILE("http://json-schema.org/draft-07/schema#", "additionalProperties", compiler_draft4_applicator_additionalproperties); COMPILE("http://json-schema.org/draft-07/schema#", "dependencies", @@ -185,10 +185,12 @@ auto sourcemeta::jsontoolkit::default_schema_compiler( compiler_draft4_validation_maxproperties); COMPILE("http://json-schema.org/draft-06/schema#", "minProperties", compiler_draft4_validation_minproperties); - COMPILE("http://json-schema.org/draft-06/schema#", "properties", - compiler_draft4_applicator_properties); + COMPILE( + "http://json-schema.org/draft-06/schema#", "properties", + compiler_draft4_applicator_properties); COMPILE("http://json-schema.org/draft-06/schema#", "patternProperties", - compiler_draft4_applicator_patternproperties); + compiler_draft4_applicator_patternproperties< + SchemaCompilerAnnotationPrivate>); COMPILE("http://json-schema.org/draft-06/schema#", "additionalProperties", compiler_draft4_applicator_additionalproperties); COMPILE("http://json-schema.org/draft-06/schema#", "dependencies", @@ -225,10 +227,12 @@ auto sourcemeta::jsontoolkit::default_schema_compiler( compiler_draft4_applicator_oneof); COMPILE("http://json-schema.org/draft-04/schema#", "not", compiler_draft4_applicator_not); - COMPILE("http://json-schema.org/draft-04/schema#", "properties", - compiler_draft4_applicator_properties); + COMPILE( + "http://json-schema.org/draft-04/schema#", "properties", + compiler_draft4_applicator_properties); COMPILE("http://json-schema.org/draft-04/schema#", "patternProperties", - compiler_draft4_applicator_patternproperties); + compiler_draft4_applicator_patternproperties< + SchemaCompilerAnnotationPrivate>); COMPILE("http://json-schema.org/draft-04/schema#", "additionalProperties", compiler_draft4_applicator_additionalproperties); COMPILE("http://json-schema.org/draft-04/schema#", "items", diff --git a/vendor/jsontoolkit/src/jsonschema/default_compiler_draft4.h b/vendor/jsontoolkit/src/jsonschema/default_compiler_draft4.h index 828d9238..49ce7886 100644 --- a/vendor/jsontoolkit/src/jsonschema/default_compiler_draft4.h +++ b/vendor/jsontoolkit/src/jsonschema/default_compiler_draft4.h @@ -216,6 +216,7 @@ auto compiler_draft4_applicator_oneof(const SchemaCompilerContext &context) SchemaCompilerTemplate{})}; } +template auto compiler_draft4_applicator_properties(const SchemaCompilerContext &context) -> SchemaCompilerTemplate { assert(context.value.is_object()); @@ -228,9 +229,8 @@ auto compiler_draft4_applicator_properties(const SchemaCompilerContext &context) for (auto &[key, subschema] : context.value.as_object()) { auto substeps{compile(subcontext, {key}, {key})}; // TODO: As an optimization, only emit an annotation if - // `additionalProperties` is also declared in the same subschema Annotations - // as such don't exist in Draft 4, so emit a private annotation instead - substeps.push_back(make( + // `additionalProperties` is also declared in the same subschema + substeps.push_back(make( subcontext, JSON{key}, {}, SchemaCompilerTargetType::Instance)); children.push_back(make( subcontext, SchemaCompilerValueNone{}, std::move(substeps), @@ -245,6 +245,7 @@ auto compiler_draft4_applicator_properties(const SchemaCompilerContext &context) type_condition(context, JSON::Type::Object))}; } +template auto compiler_draft4_applicator_patternproperties( const SchemaCompilerContext &context) -> SchemaCompilerTemplate { assert(context.value.is_object()); @@ -260,12 +261,12 @@ auto compiler_draft4_applicator_patternproperties( auto substeps{compile(subcontext, {entry.first}, {})}; // TODO: As an optimization, only emit an annotation if - // `additionalProperties` is also declared in the same subschema Annotations - // as such don't exist in Draft 4, so emit a private annotation instead The - // evaluator will make sure the same annotation is not reported twice. For - // example, if the same property matches more than one subschema in + // `additionalProperties` is also declared in the same subschema + + // The evaluator will make sure the same annotation is not reported twice. + // For example, if the same property matches more than one subschema in // `patternProperties` - substeps.push_back(make( + substeps.push_back(make( subcontext, SchemaCompilerTarget{SchemaCompilerTargetType::InstanceBasename, empty_pointer}, diff --git a/vendor/jsontoolkit/src/jsonschema/include/sourcemeta/jsontoolkit/jsonschema_error.h b/vendor/jsontoolkit/src/jsonschema/include/sourcemeta/jsontoolkit/jsonschema_error.h index 49e07d66..ea7d5b2f 100644 --- a/vendor/jsontoolkit/src/jsonschema/include/sourcemeta/jsontoolkit/jsonschema_error.h +++ b/vendor/jsontoolkit/src/jsonschema/include/sourcemeta/jsontoolkit/jsonschema_error.h @@ -56,6 +56,26 @@ class SOURCEMETA_JSONTOOLKIT_JSONSCHEMA_EXPORT SchemaResolutionError std::string message_; }; +/// @ingroup jsonschema +/// An error that represents a schema vocabulary error +class SOURCEMETA_JSONTOOLKIT_JSONSCHEMA_EXPORT SchemaVocabularyError + : public std::exception { +public: + SchemaVocabularyError(std::string uri, std::string message) + : uri_{std::move(uri)}, message_{std::move(message)} {} + [[nodiscard]] auto what() const noexcept -> const char * override { + return this->message_.c_str(); + } + + [[nodiscard]] auto uri() const noexcept -> std::string_view { + return this->uri_; + } + +private: + std::string uri_; + std::string message_; +}; + /// @ingroup jsonschema /// An error that represents a schema resolution failure event class SOURCEMETA_JSONTOOLKIT_JSONSCHEMA_EXPORT SchemaReferenceError diff --git a/vendor/jsontoolkit/src/jsonschema/jsonschema.cc b/vendor/jsontoolkit/src/jsonschema/jsonschema.cc index 3d257580..a191ef8b 100644 --- a/vendor/jsontoolkit/src/jsonschema/jsonschema.cc +++ b/vendor/jsontoolkit/src/jsonschema/jsonschema.cc @@ -172,7 +172,7 @@ auto sourcemeta::jsontoolkit::base_dialect( resolver(effective_dialect).get()}; if (!metaschema.has_value()) { throw sourcemeta::jsontoolkit::SchemaResolutionError( - effective_dialect, "Could not resolve schema"); + effective_dialect, "Could not resolve the requested schema"); } return base_dialect(metaschema.value(), resolver, effective_dialect); @@ -284,7 +284,7 @@ auto sourcemeta::jsontoolkit::vocabularies( resolver(dialect).get()}; if (!maybe_schema_dialect.has_value()) { throw sourcemeta::jsontoolkit::SchemaResolutionError( - dialect, "Could not resolve schema"); + dialect, "Could not resolve the requested schema"); } const sourcemeta::jsontoolkit::JSON &schema_dialect{ maybe_schema_dialect.value()};