From aa980a7d5ebe105b9ff8f026caa409556b790aa3 Mon Sep 17 00:00:00 2001 From: Juan Cruz Viotti Date: Mon, 15 Jul 2024 14:28:00 -0400 Subject: [PATCH] Support passing a schema URI with a fragment as a test target See: https://github.com/Intelligence-AI/jsonschema/discussions/110 Signed-off-by: Juan Cruz Viotti --- src/command_test.cc | 44 +++++++++++++++-- test/CMakeLists.txt | 3 ++ test/test/fail_unresolvable_fragment.sh | 48 +++++++++++++++++++ test/test/pass_single_resolve_fragment.sh | 45 +++++++++++++++++ .../pass_single_resolve_fragment_verbose.sh | 48 +++++++++++++++++++ 5 files changed, 183 insertions(+), 5 deletions(-) create mode 100755 test/test/fail_unresolvable_fragment.sh create mode 100755 test/test/pass_single_resolve_fragment.sh create mode 100755 test/test/pass_single_resolve_fragment_verbose.sh diff --git a/src/command_test.cc b/src/command_test.cc index dd3c39f3..693c70b0 100644 --- a/src/command_test.cc +++ b/src/command_test.cc @@ -1,5 +1,6 @@ #include #include +#include #include // EXIT_SUCCESS, EXIT_FAILURE #include // std::cerr, std::cout @@ -7,6 +8,31 @@ #include "command.h" #include "utils.h" +static auto +get_schema_object(const sourcemeta::jsontoolkit::URI &identifier, + const sourcemeta::jsontoolkit::SchemaResolver &resolver) + -> std::optional { + const auto schema{resolver(identifier.recompose()).get()}; + if (schema.has_value()) { + return schema; + } + + // Resolving a schema identifier that contains a fragment (i.e. a JSON Pointer + // one) can be tricky, as we might end up re-inventing JSON Schema referencing + // all over again. To make it work without much hassle, we do exactly that: + // create an artificial schema wrapper that uses `$ref`. + if (identifier.fragment().has_value()) { + auto result{sourcemeta::jsontoolkit::JSON::make_object()}; + result.assign("$schema", sourcemeta::jsontoolkit::JSON{ + "http://json-schema.org/draft-07/schema#"}); + result.assign("$ref", + sourcemeta::jsontoolkit::JSON{identifier.recompose()}); + return result; + } + + return std::nullopt; +} + auto intelligence::jsonschema::cli::test( const std::span &arguments) -> int { const auto options{parse_options(arguments, {"h", "http"})}; @@ -65,12 +91,11 @@ auto intelligence::jsonschema::cli::test( return EXIT_FAILURE; } - const auto schema{test_resolver(test.at("target").to_string()).get()}; + sourcemeta::jsontoolkit::URI schema_uri{test.at("target").to_string()}; + schema_uri.canonicalize(); + const auto schema{get_schema_object(schema_uri, test_resolver)}; if (!schema.has_value()) { - if (verbose) { - std::cout << "\n"; - } - + std::cout << "\n"; throw sourcemeta::jsontoolkit::SchemaResolutionError( test.at("target").to_string(), "Could not resolve schema under test"); } @@ -90,6 +115,15 @@ auto intelligence::jsonschema::cli::test( schema_template = sourcemeta::jsontoolkit::compile( schema.value(), sourcemeta::jsontoolkit::default_schema_walker, test_resolver, sourcemeta::jsontoolkit::default_schema_compiler); + } catch (const sourcemeta::jsontoolkit::SchemaReferenceError &error) { + if (error.location().empty() && error.id() == schema_uri.recompose()) { + std::cout << "\n"; + throw sourcemeta::jsontoolkit::SchemaResolutionError( + test.at("target").to_string(), + "Could not resolve schema under test"); + } + + throw; } catch (...) { std::cout << "\n"; throw; diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 4bc35a44..bce3d827 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -71,6 +71,7 @@ add_jsonschema_test_unix(test/fail_false_single_resolve_verbose) add_jsonschema_test_unix(test/fail_multi_resolve) add_jsonschema_test_unix(test/fail_multi_resolve_verbose) add_jsonschema_test_unix(test/fail_unresolvable) +add_jsonschema_test_unix(test/fail_unresolvable_fragment) add_jsonschema_test_unix(test/fail_unsupported) add_jsonschema_test_unix(test/fail_unsupported_verbose) add_jsonschema_test_unix(test/fail_not_object) @@ -87,6 +88,8 @@ add_jsonschema_test_unix(test/pass_empty) add_jsonschema_test_unix(test/pass_empty_verbose) add_jsonschema_test_unix(test/pass_single_resolve) add_jsonschema_test_unix(test/pass_single_resolve_verbose) +add_jsonschema_test_unix(test/pass_single_resolve_fragment) +add_jsonschema_test_unix(test/pass_single_resolve_fragment_verbose) add_jsonschema_test_unix(test/pass_single_comment_verbose) add_jsonschema_test_unix(test/pass_single_no_description_verbose) add_jsonschema_test_unix(test/pass_single_no_test_description_verbose) diff --git a/test/test/fail_unresolvable_fragment.sh b/test/test/fail_unresolvable_fragment.sh new file mode 100755 index 00000000..76606439 --- /dev/null +++ b/test/test/fail_unresolvable_fragment.sh @@ -0,0 +1,48 @@ +#!/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#", + "definitions": { + "foo": { "type": "string" }, + "bar": { "type": "integer" } + } +} +EOF + +cat << 'EOF' > "$TMP/test.json" +{ + "target": "https://example.com#/foo", + "tests": [ + { + "valid": true, + "data": {} + }, + { + "valid": true, + "data": { "type": 1 } + } + ] +} +EOF + +"$1" test "$TMP/test.json" --resolve "$TMP/schema.json" --verbose 1> "$TMP/output.txt" 2>&1 \ + && CODE="$?" || CODE="$?" +test "$CODE" = "1" || exit 1 + +cat << EOF > "$TMP/expected.txt" +Importing schema into the resolution context: $(realpath "$TMP")/schema.json +$(realpath "$TMP")/test.json: +error: Could not resolve schema under test + at https://example.com#/foo +EOF + +diff "$TMP/output.txt" "$TMP/expected.txt" diff --git a/test/test/pass_single_resolve_fragment.sh b/test/test/pass_single_resolve_fragment.sh new file mode 100755 index 00000000..a3b32be4 --- /dev/null +++ b/test/test/pass_single_resolve_fragment.sh @@ -0,0 +1,45 @@ +#!/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#", + "definitions": { + "foo": { "type": "string" }, + "bar": { "type": "integer" } + } +} +EOF + +cat << 'EOF' > "$TMP/test.json" +{ + "target": "https://example.com#/definitions/foo", + "tests": [ + { + "description": "First test", + "valid": true, + "data": "foo" + }, + { + "description": "Invalid type", + "valid": false, + "data": 1 + } + ] +} +EOF + +"$1" test "$TMP/test.json" --resolve "$TMP/schema.json" 1> "$TMP/output.txt" 2>&1 + +cat << EOF > "$TMP/expected.txt" +$(realpath "$TMP")/test.json: PASS 2/2 +EOF + +diff "$TMP/output.txt" "$TMP/expected.txt" diff --git a/test/test/pass_single_resolve_fragment_verbose.sh b/test/test/pass_single_resolve_fragment_verbose.sh new file mode 100755 index 00000000..07ac9f63 --- /dev/null +++ b/test/test/pass_single_resolve_fragment_verbose.sh @@ -0,0 +1,48 @@ +#!/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#", + "definitions": { + "foo": { "type": "string" }, + "bar": { "type": "integer" } + } +} +EOF + +cat << 'EOF' > "$TMP/test.json" +{ + "target": "https://example.com#/definitions/foo", + "tests": [ + { + "description": "First test", + "valid": true, + "data": "foo" + }, + { + "description": "Invalid type", + "valid": false, + "data": 1 + } + ] +} +EOF + +"$1" test "$TMP/test.json" --resolve "$TMP/schema.json" --verbose 1> "$TMP/output.txt" 2>&1 + +cat << EOF > "$TMP/expected.txt" +Importing schema into the resolution context: $(realpath "$TMP")/schema.json +$(realpath "$TMP")/test.json: + 1/2 PASS First test + 2/2 PASS Invalid type +EOF + +diff "$TMP/output.txt" "$TMP/expected.txt"