diff --git a/src/command_fmt.cc b/src/command_fmt.cc index 4a819ba8..dd688b30 100644 --- a/src/command_fmt.cc +++ b/src/command_fmt.cc @@ -21,21 +21,22 @@ auto sourcemeta::jsonschema::fmt(const sourcemeta::core::Options &options) std::vector failed_files; const auto indentation{parse_indentation(options)}; for (const auto &entry : for_each_json(options)) { + const auto &path{entry.local_path_or_throw("fmt")}; if (entry.yaml) { throw YAMLInputError{"This command does not support YAML input files yet", - entry.first}; + path}; } if (options.contains("check")) { - LOG_VERBOSE(options) << "Checking: " << entry.first.string() << "\n"; + LOG_VERBOSE(options) << "Checking: " << path.string() << "\n"; } else { - LOG_VERBOSE(options) << "Formatting: " << entry.first.string() << "\n"; + LOG_VERBOSE(options) << "Formatting: " << path.string() << "\n"; } try { - const auto configuration_path{find_configuration(entry.first)}; + const auto configuration_path{find_configuration(path)}; const auto &configuration{ - read_configuration(options, configuration_path, entry.first)}; + read_configuration(options, configuration_path, path)}; const auto dialect{default_dialect(options, configuration)}; const auto &custom_resolver{ resolver(options, options.contains("http"), dialect, configuration)}; @@ -51,39 +52,37 @@ auto sourcemeta::jsonschema::fmt(const sourcemeta::core::Options &options) } expected << "\n"; - std::ifstream current_stream{entry.first}; + std::ifstream current_stream{path}; std::ostringstream current; current << current_stream.rdbuf(); if (options.contains("check")) { if (current.str() == expected.str()) { - LOG_VERBOSE(options) << "ok: " << entry.first.string() << "\n"; + LOG_VERBOSE(options) << "ok: " << path.string() << "\n"; } else if (output_json) { - failed_files.push_back(entry.first.string()); + failed_files.push_back(path.string()); result = false; } else { - std::cerr << "fail: " << entry.first.string() << "\n"; + std::cerr << "fail: " << path.string() << "\n"; result = false; } } else { if (current.str() != expected.str()) { - std::ofstream output{entry.first}; + std::ofstream output{path}; output << expected.str(); } } } catch (const sourcemeta::core::SchemaRelativeMetaschemaResolutionError &error) { throw FileError< - sourcemeta::core::SchemaRelativeMetaschemaResolutionError>( - entry.first, error); + sourcemeta::core::SchemaRelativeMetaschemaResolutionError>(path, + error); } catch (const sourcemeta::core::SchemaResolutionError &error) { - throw FileError(entry.first, - error); + throw FileError(path, error); } catch (const sourcemeta::core::SchemaUnknownBaseDialectError &) { - throw FileError( - entry.first); + throw FileError(path); } catch (const sourcemeta::core::SchemaError &error) { - throw FileError(entry.first, error.what()); + throw FileError(path, error.what()); } } diff --git a/src/command_lint.cc b/src/command_lint.cc index 763f09d2..6b399ac8 100644 --- a/src/command_lint.cc +++ b/src/command_lint.cc @@ -72,7 +72,7 @@ static auto get_lint_callback(sourcemeta::core::JSON &errors_array, if (output_json) { auto error_obj = sourcemeta::core::JSON::make_object(); - error_obj.assign("path", sourcemeta::core::JSON{entry.first.string()}); + error_obj.assign("path", sourcemeta::core::JSON{entry.first}); error_obj.assign("id", sourcemeta::core::JSON{name}); error_obj.assign("message", sourcemeta::core::JSON{message}); error_obj.assign("description", @@ -88,7 +88,11 @@ static auto get_lint_callback(sourcemeta::core::JSON &errors_array, errors_array.push_back(error_obj); } else { - std::cout << std::filesystem::relative(entry.first).string(); + if (entry.path.has_value()) { + std::cout << std::filesystem::relative(entry.path.value()).string(); + } else { + std::cout << entry.first; + } if (position.has_value()) { std::cout << ":"; std::cout << std::get<0>(position.value()); @@ -184,18 +188,18 @@ auto sourcemeta::jsonschema::lint(const sourcemeta::core::Options &options) if (options.contains("fix")) { for (const auto &entry : for_each_json(options)) { - const auto configuration_path{find_configuration(entry.first)}; + const auto &path{entry.local_path_or_throw("lint --fix")}; + const auto configuration_path{find_configuration(path)}; const auto &configuration{ - read_configuration(options, configuration_path, entry.first)}; + read_configuration(options, configuration_path, path)}; const auto dialect{default_dialect(options, configuration)}; const auto &custom_resolver{ resolver(options, options.contains("http"), dialect, configuration)}; - LOG_VERBOSE(options) << "Linting: " << entry.first.string() << "\n"; + LOG_VERBOSE(options) << "Linting: " << entry.first << "\n"; if (entry.yaml) { throw YAMLInputError{ - "The --fix option is not supported for YAML input files", - entry.first}; + "The --fix option is not supported for YAML input files", path}; } auto copy = entry.second; @@ -206,7 +210,7 @@ auto sourcemeta::jsonschema::lint(const sourcemeta::core::Options &options) const auto apply_result = bundle.apply( copy, sourcemeta::core::schema_walker, custom_resolver, get_lint_callback(errors_array, entry, output_json), dialect, - sourcemeta::core::URI::from_path(entry.first).recompose()); + entry.base_uri()); scores.emplace_back(apply_result.second); if (!apply_result.first) { return 2; @@ -216,20 +220,19 @@ auto sourcemeta::jsonschema::lint(const sourcemeta::core::Options &options) } catch ( const sourcemeta::core::SchemaTransformRuleProcessedTwiceError &error) { - throw LintAutoFixError{error.what(), entry.first, - error.location()}; + throw LintAutoFixError{error.what(), path, error.location()}; } catch ( const sourcemeta::core::SchemaBrokenReferenceError &error) { throw LintAutoFixError{ "Could not autofix the schema without breaking its internal " "references", - entry.first, error.location()}; + path, error.location()}; } catch (const sourcemeta::core::SchemaUnknownBaseDialectError &) { throw FileError( - entry.first); + path); } catch (const sourcemeta::core::SchemaResolutionError &error) { - throw FileError( - entry.first, error); + throw FileError(path, + error); } }); @@ -239,7 +242,7 @@ auto sourcemeta::jsonschema::lint(const sourcemeta::core::Options &options) } if (copy != entry.second) { - std::ofstream output{entry.first}; + std::ofstream output{path}; sourcemeta::core::prettify(copy, output, indentation); output << "\n"; } @@ -250,13 +253,17 @@ auto sourcemeta::jsonschema::lint(const sourcemeta::core::Options &options) } } else { for (const auto &entry : for_each_json(options)) { - const auto configuration_path{find_configuration(entry.first)}; + const bool is_remote{!entry.path.has_value()}; + const auto configuration_path{find_configuration( + is_remote ? std::filesystem::current_path() : entry.path.value())}; const auto &configuration{ - read_configuration(options, configuration_path, entry.first)}; + is_remote ? read_configuration(options, configuration_path) + : read_configuration(options, configuration_path, + entry.path.value())}; const auto dialect{default_dialect(options, configuration)}; const auto &custom_resolver{ resolver(options, options.contains("http"), dialect, configuration)}; - LOG_VERBOSE(options) << "Linting: " << entry.first.string() << "\n"; + LOG_VERBOSE(options) << "Linting: " << entry.first << "\n"; const auto wrapper_result = sourcemeta::jsonschema::try_catch(options, [&]() { @@ -265,7 +272,7 @@ auto sourcemeta::jsonschema::lint(const sourcemeta::core::Options &options) entry.second, sourcemeta::core::schema_walker, custom_resolver, get_lint_callback(errors_array, entry, output_json), dialect, - sourcemeta::core::URI::from_path(entry.first).recompose()); + entry.base_uri()); scores.emplace_back(subresult.second); if (subresult.first) { return EXIT_SUCCESS; @@ -274,11 +281,20 @@ auto sourcemeta::jsonschema::lint(const sourcemeta::core::Options &options) return 2; } } catch (const sourcemeta::core::SchemaUnknownBaseDialectError &) { - throw FileError( - entry.first); + if (entry.path.has_value()) { + throw FileError< + sourcemeta::core::SchemaUnknownBaseDialectError>( + entry.path.value()); + } + + throw; } catch (const sourcemeta::core::SchemaResolutionError &error) { - throw FileError( - entry.first, error); + if (entry.path.has_value()) { + throw FileError( + entry.path.value(), error); + } + + throw; } }); diff --git a/src/command_metaschema.cc b/src/command_metaschema.cc index 5c0f46e5..27b9061f 100644 --- a/src/command_metaschema.cc +++ b/src/command_metaschema.cc @@ -7,10 +7,15 @@ #include #include -#include // assert +#include // assert +#include #include // std::cerr #include // std::map -#include // std::string +#include +#include +#include // std::string +#include +#include #include "command.h" #include "configuration.h" @@ -29,31 +34,43 @@ auto sourcemeta::jsonschema::metaschema( sourcemeta::blaze::Evaluator evaluator; std::map cache; + const auto current_path{std::filesystem::current_path()}; + const auto remote_configuration_path{find_configuration(current_path)}; + const auto &remote_configuration{ + read_configuration(options, remote_configuration_path)}; - for (const auto &entry : for_each_json(options)) { - if (!sourcemeta::core::is_schema(entry.second)) { - throw NotSchemaError{entry.first}; - } - - const auto configuration_path{find_configuration(entry.first)}; - const auto &configuration{ - read_configuration(options, configuration_path, entry.first)}; - const auto default_dialect_option{default_dialect(options, configuration)}; + const auto process_schema = + [&](const sourcemeta::core::JSON &schema, + const sourcemeta::core::PointerPositionTracker &positions, + const std::optional &schema_path, + const std::string_view schema_display, + const sourcemeta::jsonschema::CustomResolver &custom_resolver, + const std::string_view default_dialect_option) -> void { + if (!sourcemeta::core::is_schema(schema)) { + if (schema_path.has_value()) { + throw NotSchemaError{schema_path.value()}; + } - const auto &custom_resolver{resolver(options, options.contains("http"), - default_dialect_option, - configuration)}; + throw RemoteSchemaNotSchemaError{std::string{schema_display}}; + } try { const auto dialect{ - sourcemeta::core::dialect(entry.second, default_dialect_option)}; + sourcemeta::core::dialect(schema, default_dialect_option)}; if (dialect.empty()) { - throw FileError( - entry.first); + if (schema_path.has_value()) { + throw FileError( + schema_path.value()); + } + + std::ostringstream error; + error << "Could not resolve the metaschema of the schema\n at uri " + << schema_display; + throw std::runtime_error(error.str()); } const auto metaschema{sourcemeta::core::metaschema( - entry.second, custom_resolver, default_dialect_option)}; + schema, custom_resolver, default_dialect_option)}; const sourcemeta::core::JSON bundled{ sourcemeta::core::bundle(metaschema, sourcemeta::core::schema_walker, custom_resolver, default_dialect_option)}; @@ -74,16 +91,16 @@ auto sourcemeta::jsonschema::metaschema( sourcemeta::blaze::TraceOutput output{ sourcemeta::core::schema_walker, custom_resolver, sourcemeta::core::empty_weak_pointer, frame}; - result = evaluator.validate(cache.at(std::string{dialect}), - entry.second, std::ref(output)); - print(output, entry.positions, std::cout); + result = evaluator.validate(cache.at(std::string{dialect}), schema, + std::ref(output)); + print(output, positions, std::cout); } else if (json_output) { // Otherwise its impossible to correlate the output // when validating i.e. a directory of schemas - std::cerr << entry.first.string() << "\n"; + std::cerr << schema_display << "\n"; const auto output{sourcemeta::blaze::standard( - evaluator, cache.at(std::string{dialect}), entry.second, - sourcemeta::blaze::StandardOutput::Basic, entry.positions)}; + evaluator, cache.at(std::string{dialect}), schema, + sourcemeta::blaze::StandardOutput::Basic, positions)}; assert(output.is_object()); assert(output.defines("valid")); assert(output.at("valid").is_boolean()); @@ -94,29 +111,56 @@ auto sourcemeta::jsonschema::metaschema( sourcemeta::core::prettify(output, std::cout); std::cout << "\n"; } else { - sourcemeta::blaze::SimpleOutput output{entry.second}; - if (evaluator.validate(cache.at(std::string{dialect}), entry.second, + sourcemeta::blaze::SimpleOutput output{schema}; + if (evaluator.validate(cache.at(std::string{dialect}), schema, std::ref(output))) { LOG_VERBOSE(options) - << "ok: " - << sourcemeta::core::weakly_canonical(entry.first).string() - << "\n matches " << dialect << "\n"; + << "ok: " << schema_display << "\n matches " << dialect << "\n"; } else { - std::cerr << "fail: " - << sourcemeta::core::weakly_canonical(entry.first).string() - << "\n"; - print(output, entry.positions, std::cerr); + std::cerr << "fail: " << schema_display << "\n"; + print(output, positions, std::cerr); result = false; } } } catch (const sourcemeta::core::SchemaRelativeMetaschemaResolutionError &error) { - throw FileError< - sourcemeta::core::SchemaRelativeMetaschemaResolutionError>( - entry.first, error); + if (schema_path.has_value()) { + throw FileError< + sourcemeta::core::SchemaRelativeMetaschemaResolutionError>( + schema_path.value(), error); + } + + throw; } catch (const sourcemeta::core::SchemaResolutionError &error) { - throw FileError(entry.first, - error); + if (schema_path.has_value()) { + throw FileError( + schema_path.value(), error); + } + + throw; + } + }; + + for (const auto &entry : for_each_json(options)) { + if (entry.path.has_value()) { + const auto configuration_path{find_configuration(entry.path.value())}; + const auto &configuration{ + read_configuration(options, configuration_path, entry.path.value())}; + const auto default_dialect_option{ + default_dialect(options, configuration)}; + const auto &custom_resolver{resolver(options, options.contains("http"), + default_dialect_option, + configuration)}; + process_schema(entry.second, entry.positions, entry.path, entry.first, + custom_resolver, default_dialect_option); + } else { + const auto remote_default_dialect_option{ + default_dialect(options, remote_configuration)}; + const auto &remote_resolver{resolver(options, options.contains("http"), + remote_default_dialect_option, + remote_configuration)}; + process_schema(entry.second, entry.positions, std::nullopt, entry.first, + remote_resolver, remote_default_dialect_option); } } diff --git a/src/command_test.cc b/src/command_test.cc index 46d82c10..98daa922 100644 --- a/src/command_test.cc +++ b/src/command_test.cc @@ -26,41 +26,40 @@ auto parse_test_suite(const sourcemeta::jsonschema::InputJSON &entry, const sourcemeta::core::SchemaResolver &schema_resolver, const std::string_view dialect, const bool json_output) -> sourcemeta::blaze::TestSuite { + const auto &path{entry.local_path_or_throw("test")}; try { return sourcemeta::blaze::TestSuite::parse( - entry.second, entry.positions, entry.first.parent_path(), - schema_resolver, sourcemeta::core::schema_walker, + entry.second, entry.positions, path.parent_path(), schema_resolver, + sourcemeta::core::schema_walker, sourcemeta::blaze::default_schema_compiler, dialect); } catch (const sourcemeta::blaze::TestParseError &error) { if (!json_output) { - std::cout << entry.first.string() << ":\n"; + std::cout << entry.first << ":\n"; } throw sourcemeta::jsonschema::FileError{ - entry.first, error.what(), error.location(), error.line(), - error.column()}; + path, error.what(), error.location(), error.line(), error.column()}; } catch ( const sourcemeta::core::SchemaRelativeMetaschemaResolutionError &error) { if (!json_output) { - std::cout << entry.first.string() << ":\n"; + std::cout << entry.first << ":\n"; } throw sourcemeta::jsonschema::FileError< - sourcemeta::core::SchemaRelativeMetaschemaResolutionError>{entry.first, - error}; + sourcemeta::core::SchemaRelativeMetaschemaResolutionError>{path, error}; } catch (const sourcemeta::core::SchemaResolutionError &error) { if (!json_output) { - std::cout << entry.first.string() << ":\n"; + std::cout << entry.first << ":\n"; } throw sourcemeta::jsonschema::FileError< - sourcemeta::core::SchemaResolutionError>{entry.first, error}; + sourcemeta::core::SchemaResolutionError>{path, error}; } catch (const sourcemeta::core::SchemaUnknownBaseDialectError &) { if (!json_output) { - std::cout << entry.first.string() << ":\n"; + std::cout << entry.first << ":\n"; } throw sourcemeta::jsonschema::FileError< - sourcemeta::core::SchemaUnknownBaseDialectError>{entry.first}; + sourcemeta::core::SchemaUnknownBaseDialectError>{path}; } catch (...) { if (!json_output) { - std::cout << entry.first.string() << ":\n"; + std::cout << entry.first << ":\n"; } throw; } @@ -71,8 +70,9 @@ auto report_as_text(const sourcemeta::core::Options &options) -> void { const auto verbose{options.contains("verbose")}; for (const auto &entry : sourcemeta::jsonschema::for_each_json(options)) { + const auto &path{entry.local_path_or_throw("test")}; const auto configuration_path{ - sourcemeta::jsonschema::find_configuration(entry.first)}; + sourcemeta::jsonschema::find_configuration(path)}; const auto &configuration{sourcemeta::jsonschema::read_configuration( options, configuration_path)}; const auto dialect{ @@ -82,7 +82,7 @@ auto report_as_text(const sourcemeta::core::Options &options) -> void { auto test_suite{parse_test_suite(entry, schema_resolver, dialect, false)}; - std::cout << entry.first.string() << ":"; + std::cout << entry.first << ":"; const auto suite_result{test_suite.run( [&](const sourcemeta::core::JSON::String &, std::size_t index, @@ -183,8 +183,9 @@ auto report_as_ctrf(const sourcemeta::core::Options &options) -> void { bool first_suite{true}; for (const auto &entry : sourcemeta::jsonschema::for_each_json(options)) { + const auto &path{entry.local_path_or_throw("test")}; const auto configuration_path{ - sourcemeta::jsonschema::find_configuration(entry.first)}; + sourcemeta::jsonschema::find_configuration(path)}; const auto &configuration{sourcemeta::jsonschema::read_configuration( options, configuration_path)}; const auto dialect{ @@ -194,8 +195,7 @@ auto report_as_ctrf(const sourcemeta::core::Options &options) -> void { auto test_suite{parse_test_suite(entry, schema_resolver, dialect, true)}; - const auto file_path{ - sourcemeta::core::weakly_canonical(entry.first).string()}; + const auto file_path{sourcemeta::core::weakly_canonical(path).string()}; const auto suite_result{test_suite.run( [&](const sourcemeta::core::JSON::String &target, std::size_t, diff --git a/src/command_validate.cc b/src/command_validate.cc index afb86981..8233ac42 100644 --- a/src/command_validate.cc +++ b/src/command_validate.cc @@ -141,27 +141,65 @@ auto sourcemeta::jsonschema::validate(const sourcemeta::core::Options &options) "jsonschema validate path/to/schema.json path/to/instance.json"}; } - const auto &schema_path{options.positional().at(0)}; + const auto &schema_input{options.positional().at(0)}; + const bool schema_is_url{sourcemeta::jsonschema::is_http_url(schema_input)}; + + std::optional schema_path{std::nullopt}; + const std::optional *configuration{nullptr}; + const sourcemeta::jsonschema::CustomResolver *custom_resolver{nullptr}; + std::string_view dialect; + sourcemeta::core::JSON schema{sourcemeta::core::JSON::make_object()}; + std::string schema_display; + std::string default_id; + + if (schema_is_url) { + if (!options.contains("http")) { + throw std::runtime_error{ + "Remote schema inputs require network access. Pass `--http/-h`"}; + } - if (std::filesystem::is_directory(schema_path)) { - throw std::filesystem::filesystem_error{ - "The input was supposed to be a file but it is a directory", - schema_path, std::make_error_code(std::errc::is_a_directory)}; - } + const auto current_path{std::filesystem::current_path()}; + const auto configuration_path{find_configuration(current_path)}; + configuration = &read_configuration(options, configuration_path); + dialect = default_dialect(options, *configuration); + custom_resolver = + &resolver(options, /* remote */ true, dialect, *configuration); - const auto configuration_path{find_configuration(schema_path)}; - const auto &configuration{ - read_configuration(options, configuration_path, schema_path)}; - const auto dialect{default_dialect(options, configuration)}; + schema = sourcemeta::jsonschema::fetch_http_schema(options, schema_input); + schema_display = std::string{schema_input}; + default_id = schema_display; + } else { + schema_path = std::filesystem::path{schema_input}; - const auto schema{sourcemeta::core::read_yaml_or_json(schema_path)}; + if (std::filesystem::is_directory(schema_path.value())) { + throw std::filesystem::filesystem_error{ + "The input was supposed to be a file but it is a directory", + schema_path.value(), std::make_error_code(std::errc::is_a_directory)}; + } + + const auto configuration_path{find_configuration(schema_path.value())}; + configuration = + &read_configuration(options, configuration_path, schema_path.value()); + dialect = default_dialect(options, *configuration); + schema = sourcemeta::core::read_yaml_or_json(schema_path.value()); + schema_display = + sourcemeta::core::weakly_canonical(schema_path.value()).string(); + default_id = sourcemeta::core::URI::from_path( + sourcemeta::core::weakly_canonical(schema_path.value())) + .recompose(); + custom_resolver = + &resolver(options, options.contains("http"), dialect, *configuration); + } if (!sourcemeta::core::is_schema(schema)) { - throw NotSchemaError{schema_path}; + if (schema_path.has_value()) { + throw NotSchemaError{schema_path.value()}; + } + + throw RemoteSchemaNotSchemaError{std::string{schema_input}}; } - const auto &custom_resolver{ - resolver(options, options.contains("http"), dialect, configuration)}; + assert(custom_resolver != nullptr); const auto fast_mode{options.contains("fast")}; const auto benchmark{options.contains("benchmark")}; @@ -173,35 +211,58 @@ auto sourcemeta::jsonschema::validate(const sourcemeta::core::Options &options) const auto trace{options.contains("trace")}; const auto json_output{options.contains("json")}; - const auto default_id{sourcemeta::core::URI::from_path( - sourcemeta::core::weakly_canonical(schema_path)) - .recompose()}; - const sourcemeta::core::JSON bundled{[&]() { try { - return sourcemeta::core::bundle(schema, sourcemeta::core::schema_walker, - custom_resolver, dialect, default_id); + return sourcemeta::core::bundle( + static_cast(schema), + sourcemeta::core::schema_walker, *custom_resolver, dialect, + default_id); } catch (const sourcemeta::core::SchemaReferenceError &error) { - throw FileError( - schema_path, std::string{error.identifier()}, error.location(), - error.what()); + if (schema_path.has_value()) { + throw FileError( + schema_path.value(), std::string{error.identifier()}, + error.location(), error.what()); + } + + throw; } catch (const sourcemeta::core::SchemaRelativeMetaschemaResolutionError &error) { - throw FileError< - sourcemeta::core::SchemaRelativeMetaschemaResolutionError>( - schema_path, error); + if (schema_path.has_value()) { + throw FileError< + sourcemeta::core::SchemaRelativeMetaschemaResolutionError>( + schema_path.value(), error); + } + + throw; } catch (const sourcemeta::core::SchemaResolutionError &error) { - throw FileError(schema_path, - error); + if (schema_path.has_value()) { + throw FileError( + schema_path.value(), error); + } + + throw; } catch (const sourcemeta::core::SchemaUnknownBaseDialectError &) { - throw FileError( - schema_path); + if (schema_path.has_value()) { + throw FileError( + schema_path.value()); + } + + throw; } catch (const sourcemeta::core::SchemaError &error) { - throw FileError(schema_path, error.what()); + if (schema_path.has_value()) { + throw FileError(schema_path.value(), + error.what()); + } + + throw; } catch ( const sourcemeta::core::SchemaReferenceObjectResourceError &error) { - throw FileError( - schema_path, error.identifier()); + if (schema_path.has_value()) { + throw FileError( + schema_path.value(), error.identifier()); + } + + throw; } }()}; @@ -209,43 +270,82 @@ auto sourcemeta::jsonschema::validate(const sourcemeta::core::Options &options) sourcemeta::core::SchemaFrame::Mode::References}; try { - frame.analyse(bundled, sourcemeta::core::schema_walker, custom_resolver, + frame.analyse(bundled, sourcemeta::core::schema_walker, *custom_resolver, dialect, default_id); } catch ( const sourcemeta::core::SchemaRelativeMetaschemaResolutionError &error) { - throw FileError( - schema_path, error); + if (schema_path.has_value()) { + throw FileError< + sourcemeta::core::SchemaRelativeMetaschemaResolutionError>( + schema_path.value(), error); + } + + throw; } catch (const sourcemeta::core::SchemaResolutionError &error) { - throw FileError(schema_path, - error); + if (schema_path.has_value()) { + throw FileError( + schema_path.value(), error); + } + + throw; } catch (const sourcemeta::core::SchemaUnknownBaseDialectError &) { - throw FileError( - schema_path); + if (schema_path.has_value()) { + throw FileError( + schema_path.value()); + } + + throw; } catch (const sourcemeta::core::SchemaError &error) { - throw FileError(schema_path, error.what()); + if (schema_path.has_value()) { + throw FileError(schema_path.value(), + error.what()); + } + + throw; } const auto schema_template{[&]() { try { - return get_schema_template(bundled, custom_resolver, frame, dialect, + return get_schema_template(bundled, *custom_resolver, frame, dialect, default_id, fast_mode, options); } catch (const sourcemeta::core::SchemaReferenceError &error) { - throw FileError( - schema_path, std::string{error.identifier()}, error.location(), - error.what()); + if (schema_path.has_value()) { + throw FileError( + schema_path.value(), std::string{error.identifier()}, + error.location(), error.what()); + } + + throw; } catch (const sourcemeta::core::SchemaRelativeMetaschemaResolutionError &error) { - throw FileError< - sourcemeta::core::SchemaRelativeMetaschemaResolutionError>( - schema_path, error); + if (schema_path.has_value()) { + throw FileError< + sourcemeta::core::SchemaRelativeMetaschemaResolutionError>( + schema_path.value(), error); + } + + throw; } catch (const sourcemeta::core::SchemaResolutionError &error) { - throw FileError(schema_path, - error); + if (schema_path.has_value()) { + throw FileError( + schema_path.value(), error); + } + + throw; } catch (const sourcemeta::core::SchemaUnknownBaseDialectError &) { - throw FileError( - schema_path); + if (schema_path.has_value()) { + throw FileError( + schema_path.value()); + } + + throw; } catch (const sourcemeta::core::SchemaError &error) { - throw FileError(schema_path, error.what()); + if (schema_path.has_value()) { + throw FileError(schema_path.value(), + error.what()); + } + + throw; } }()}; @@ -292,12 +392,13 @@ auto sourcemeta::jsonschema::validate(const sourcemeta::core::Options &options) instance_path.extension() == ".yaml" || instance_path.extension() == ".yml") { for (const auto &entry : for_each_json({instance_path_view}, options)) { + const auto &entry_path{entry.local_path_or_throw("validate")}; std::ostringstream error; sourcemeta::blaze::SimpleOutput output{entry.second}; bool subresult{true}; if (benchmark) { subresult = run_loop( - evaluator, schema_template, entry.second, entry.first, + evaluator, schema_template, entry.second, entry_path, entry.multidocument ? static_cast(entry.index + 1) : static_cast(-1), benchmark_loop); @@ -316,7 +417,7 @@ auto sourcemeta::jsonschema::validate(const sourcemeta::core::Options &options) continue; } else if (json_output) { if (!entry.multidocument) { - std::cerr << entry.first.string() << "\n"; + std::cerr << entry.first << "\n"; } const auto suboutput{sourcemeta::blaze::standard( evaluator, schema_template, entry.second, @@ -337,18 +438,15 @@ auto sourcemeta::jsonschema::validate(const sourcemeta::core::Options &options) } else if (subresult) { LOG_VERBOSE(options) << "ok: " - << sourcemeta::core::weakly_canonical(entry.first).string(); + << sourcemeta::core::weakly_canonical(entry_path).string(); if (entry.multidocument) { LOG_VERBOSE(options) << " (entry #" << entry.index + 1 << ")"; } - LOG_VERBOSE(options) - << "\n matches " - << sourcemeta::core::weakly_canonical(schema_path).string() - << "\n"; + LOG_VERBOSE(options) << "\n matches " << schema_display << "\n"; print_annotations(output, options, entry.positions, std::cerr); } else { std::cerr << "fail: " - << sourcemeta::core::weakly_canonical(entry.first).string(); + << sourcemeta::core::weakly_canonical(entry_path).string(); if (entry.multidocument) { std::cerr << " (entry #" << entry.index + 1 << ")\n\n"; sourcemeta::core::prettify(entry.second, std::cerr); @@ -371,7 +469,7 @@ auto sourcemeta::jsonschema::validate(const sourcemeta::core::Options &options) std::ostringstream error; sourcemeta::blaze::SimpleOutput output{instance}; sourcemeta::blaze::TraceOutput trace_output{ - sourcemeta::core::schema_walker, custom_resolver, + sourcemeta::core::schema_walker, *custom_resolver, sourcemeta::core::empty_weak_pointer, frame}; bool subresult{true}; if (benchmark) { @@ -413,8 +511,7 @@ auto sourcemeta::jsonschema::validate(const sourcemeta::core::Options &options) LOG_VERBOSE(options) << "ok: " << sourcemeta::core::weakly_canonical(instance_path).string() - << "\n matches " - << sourcemeta::core::weakly_canonical(schema_path).string() << "\n"; + << "\n matches " << schema_display << "\n"; print_annotations(output, options, tracker, std::cerr); } else { std::cerr << "fail: " diff --git a/src/error.h b/src/error.h index 996ad225..b89585c8 100644 --- a/src/error.h +++ b/src/error.h @@ -142,6 +142,75 @@ class Fail : public std::runtime_error { int exit_code_; }; +class RemoteSchemaFetchError : public std::runtime_error { +public: + RemoteSchemaFetchError(std::string uri, const long status_code) + : std::runtime_error{"Could not fetch the schema over HTTP (HTTP " + + std::to_string(status_code) + ")"}, + uri_{std::move(uri)} {} + + [[nodiscard]] auto uri() const noexcept -> const std::string & { + return this->uri_; + } + +private: + std::string uri_; +}; + +class RemoteSchemaJSONParseError : public std::runtime_error { +public: + RemoteSchemaJSONParseError(std::string uri, const std::uint64_t line, + const std::uint64_t column, + const std::string_view message) + : std::runtime_error{std::string{message}}, uri_{std::move(uri)}, + line_{line}, column_{column} {} + + [[nodiscard]] auto uri() const noexcept -> const std::string & { + return this->uri_; + } + + [[nodiscard]] auto line() const noexcept -> std::uint64_t { + return this->line_; + } + + [[nodiscard]] auto column() const noexcept -> std::uint64_t { + return this->column_; + } + +private: + std::string uri_; + std::uint64_t line_; + std::uint64_t column_; +}; + +class RemoteSchemaYAMLParseError : public std::runtime_error { +public: + RemoteSchemaYAMLParseError(std::string uri, const std::string_view message) + : std::runtime_error{std::string{message}}, uri_{std::move(uri)} {} + + [[nodiscard]] auto uri() const noexcept -> const std::string & { + return this->uri_; + } + +private: + std::string uri_; +}; + +class RemoteSchemaNotSchemaError : public std::runtime_error { +public: + RemoteSchemaNotSchemaError(std::string uri) + : std::runtime_error{"The fetched document does not represent a valid " + "JSON Schema"}, + uri_{std::move(uri)} {} + + [[nodiscard]] auto uri() const noexcept -> const std::string & { + return this->uri_; + } + +private: + std::string uri_; +}; + template class FileError : public T { public: template @@ -328,6 +397,22 @@ inline auto try_catch(const sourcemeta::core::Options &options, const auto is_json{options.contains("json")}; print_exception(is_json, error); return EXIT_FAILURE; + } catch (const RemoteSchemaFetchError &error) { + const auto is_json{options.contains("json")}; + print_exception(is_json, error); + return EXIT_FAILURE; + } catch (const RemoteSchemaJSONParseError &error) { + const auto is_json{options.contains("json")}; + print_exception(is_json, error); + return EXIT_FAILURE; + } catch (const RemoteSchemaYAMLParseError &error) { + const auto is_json{options.contains("json")}; + print_exception(is_json, error); + return EXIT_FAILURE; + } catch (const RemoteSchemaNotSchemaError &error) { + const auto is_json{options.contains("json")}; + print_exception(is_json, error); + return EXIT_FAILURE; } catch (const InvalidLintRuleError &error) { const auto is_json{options.contains("json")}; print_exception(is_json, error); @@ -422,6 +507,54 @@ inline auto try_catch(const sourcemeta::core::Options &options, const auto is_json{options.contains("json")}; print_exception(is_json, error); return EXIT_FAILURE; + } catch (const sourcemeta::core::SchemaReferenceError &error) { + const auto is_json{options.contains("json")}; + print_exception(is_json, error); + return EXIT_FAILURE; + } catch ( + const sourcemeta::core::SchemaRelativeMetaschemaResolutionError &error) { + const auto is_json{options.contains("json")}; + print_exception(is_json, error); + return EXIT_FAILURE; + } catch (const sourcemeta::core::SchemaResolutionError &error) { + const auto is_json{options.contains("json")}; + print_exception(is_json, error); + if (!is_json) { + if (error.identifier().starts_with("file://")) { + std::cerr << "\nThis is likely because the file does not exist\n"; + } else { + std::cerr << "\nThis is likely because you forgot to import such " + "schema using `--resolve/-r`\n"; + } + } + + return EXIT_FAILURE; + } catch (const sourcemeta::core::SchemaUnknownBaseDialectError &error) { + const auto is_json{options.contains("json")}; + print_exception(is_json, error); + if (!is_json) { + std::cerr << "\nAre you sure the input is a valid JSON Schema and its " + "base dialect is known?\n"; + std::cerr + << "If the input does not declare the `$schema` keyword, you might " + "want to\n"; + std::cerr << "explicitly declare a default dialect using " + "`--default-dialect/-d`\n"; + } + + return EXIT_FAILURE; + } catch (const sourcemeta::core::SchemaFrameError &error) { + const auto is_json{options.contains("json")}; + print_exception(is_json, error); + return EXIT_FAILURE; + } catch (const sourcemeta::core::SchemaReferenceObjectResourceError &error) { + const auto is_json{options.contains("json")}; + print_exception(is_json, error); + return EXIT_FAILURE; + } catch (const sourcemeta::core::SchemaError &error) { + const auto is_json{options.contains("json")}; + print_exception(is_json, error); + return EXIT_FAILURE; } catch (const sourcemeta::core::JSONFileParseError &error) { const auto is_json{options.contains("json")}; print_exception(is_json, error); diff --git a/src/http.h b/src/http.h new file mode 100644 index 00000000..15234953 --- /dev/null +++ b/src/http.h @@ -0,0 +1,133 @@ +#ifndef SOURCEMETA_JSONSCHEMA_CLI_HTTP_H_ +#define SOURCEMETA_JSONSCHEMA_CLI_HTTP_H_ + +#ifndef NOMINMAX +#define NOMINMAX +#endif + +#if defined(__clang__) +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wnewline-eof" +#endif +#include +#if defined(__clang__) +#pragma clang diagnostic pop +#endif + +#include +#include +#include + +#include "error.h" +#include "logger.h" + +#include // std::uint64_t +#include // std::ref +#include // std::ostringstream +#include // std::string +#include // std::string_view + +namespace sourcemeta::jsonschema { + +inline auto is_http_url(std::string_view identifier) -> bool { + const sourcemeta::core::URI uri{std::string{identifier}}; + const auto maybe_scheme{uri.scheme()}; + return maybe_scheme.has_value() && + (maybe_scheme.value() == "https" || maybe_scheme.value() == "http"); +} + +inline auto uri_path_without_query_or_fragment(std::string_view uri) + -> std::string_view { + const auto fragment = uri.find('#'); + if (fragment != std::string_view::npos) { + uri = uri.substr(0, fragment); + } + + const auto query = uri.find('?'); + if (query != std::string_view::npos) { + uri = uri.substr(0, query); + } + + return uri; +} + +inline auto is_likely_yaml(std::string_view uri, const cpr::Header &headers) + -> bool { + const auto content_type_iterator = headers.find("content-type"); + if (content_type_iterator != headers.end()) { + const auto &content_type = content_type_iterator->second; + if (content_type.starts_with("text/yaml") || + content_type.starts_with("text/x-yaml") || + content_type.starts_with("application/yaml") || + content_type.starts_with("application/x-yaml")) { + return true; + } + } + + const auto path = uri_path_without_query_or_fragment(uri); + return path.ends_with(".yaml") || path.ends_with(".yml"); +} + +inline auto fetch_http_response(const sourcemeta::core::Options &options, + std::string_view uri) -> cpr::Response { + if (!is_http_url(uri)) { + throw std::runtime_error{"The input is not an HTTP URL"}; + } + + LOG_VERBOSE(options) << "Fetching over HTTP: " << uri << "\n"; + const cpr::Response response{ + cpr::Get(cpr::Url{std::string{uri}}, cpr::Redirect{true})}; + + if (response.status_code < 200 || response.status_code >= 300) { + throw RemoteSchemaFetchError{std::string{uri}, response.status_code}; + } + + return response; +} + +struct HTTPParsedJSON { + sourcemeta::core::JSON document; + sourcemeta::core::PointerPositionTracker positions; + bool yaml{false}; +}; + +inline auto parse_http_yaml_or_json(const sourcemeta::core::Options &options, + std::string_view uri) -> HTTPParsedJSON { + const cpr::Response response{fetch_http_response(options, uri)}; + + sourcemeta::core::PointerPositionTracker positions; + if (is_likely_yaml(uri, response.header)) { + try { + return {sourcemeta::core::parse_yaml(response.text, std::ref(positions)), + std::move(positions), true}; + } catch (const sourcemeta::core::YAMLParseError &error) { + throw RemoteSchemaYAMLParseError{std::string{uri}, error.what()}; + } + } + + try { + return {sourcemeta::core::parse_json(response.text, std::ref(positions)), + std::move(positions), false}; + } catch (const sourcemeta::core::JSONParseError &error) { + throw RemoteSchemaJSONParseError{std::string{uri}, error.line(), + error.column(), error.what()}; + } +} + +inline auto fetch_http_schema(const sourcemeta::core::Options &options, + std::string_view uri) -> sourcemeta::core::JSON { + auto official_result{sourcemeta::core::schema_resolver(uri)}; + if (official_result.has_value()) { + return official_result.value(); + } + + if (!is_http_url(uri)) { + throw std::runtime_error{"The schema URI is not an HTTP URL"}; + } + + return parse_http_yaml_or_json(options, uri).document; +} + +} // namespace sourcemeta::jsonschema + +#endif diff --git a/src/input.h b/src/input.h index 4f65d68e..9644448a 100644 --- a/src/input.h +++ b/src/input.h @@ -7,9 +7,11 @@ #include #include #include +#include #include #include "configuration.h" +#include "http.h" #include "logger.h" #include // std::any_of, std::none_of, std::sort @@ -17,6 +19,7 @@ #include // std::uintptr_t #include // std::filesystem #include // std::ref +#include // std::optional #include // std::set #include // std::ostringstream #include // std::runtime_error @@ -26,14 +29,40 @@ namespace sourcemeta::jsonschema { struct InputJSON { - std::filesystem::path first; + std::string first; + std::optional path; sourcemeta::core::JSON second; sourcemeta::core::PointerPositionTracker positions; std::size_t index{0}; bool multidocument{false}; bool yaml{false}; auto operator<(const InputJSON &other) const noexcept -> bool { - return this->first < other.first; + return this->first < other.first || + (this->first == other.first && this->index < other.index); + } + + [[nodiscard]] auto is_local() const noexcept -> bool { + return this->path.has_value(); + } + + [[nodiscard]] auto local_path_or_throw(std::string_view command) const + -> const std::filesystem::path & { + if (!this->path.has_value()) { + std::ostringstream error; + error << "The `" << command << "` command does not support HTTP inputs"; + throw std::runtime_error(error.str()); + } + + return this->path.value(); + } + + [[nodiscard]] auto base_uri() const -> std::string { + if (sourcemeta::jsonschema::is_http_url(this->first)) { + return this->first; + } + + assert(this->path.has_value()); + return sourcemeta::core::URI::from_path(this->path.value()).recompose(); } }; @@ -144,16 +173,132 @@ inline auto read_file(const std::filesystem::path &path) -> ParsedJSON { } inline auto -handle_json_entry(const std::filesystem::path &entry_path, +handle_json_entry(const std::string_view entry_identifier, const std::set &blacklist, const std::set &extensions, std::vector &result, const sourcemeta::core::Options &options) -> void { + if (sourcemeta::jsonschema::is_http_url(entry_identifier)) { + if (!options.contains("http")) { + throw std::runtime_error{ + "Remote schema inputs require network access. Pass `--http/-h`"}; + } + + const cpr::Response response{ + sourcemeta::jsonschema::fetch_http_response(options, entry_identifier)}; + + const auto path_without_query_or_fragment = + sourcemeta::jsonschema::uri_path_without_query_or_fragment( + entry_identifier); + if (path_without_query_or_fragment.ends_with(".jsonl")) { + LOG_VERBOSE(options) << "Interpreting input as JSONL: " + << entry_identifier << "\n"; + std::istringstream json_stream{response.text}; + std::size_t index{0}; + try { + for (const auto &document : sourcemeta::core::JSONL{json_stream}) { + sourcemeta::core::PointerPositionTracker positions; + result.push_back({std::string{entry_identifier}, std::nullopt, + document, std::move(positions), index, true, + false}); + index += 1; + } + } catch (const sourcemeta::core::JSONParseError &error) { + throw RemoteSchemaJSONParseError{std::string{entry_identifier}, + error.line(), error.column(), + error.what()}; + } + + if (index == 0) { + LOG_WARNING() << "The JSONL URL is empty\n"; + } + return; + } + + if (sourcemeta::jsonschema::is_likely_yaml(entry_identifier, + response.header)) { + if (response.text.empty()) { + return; + } + + std::istringstream stream{response.text}; + std::vector> + documents; + std::uint64_t line_offset{0}; + std::uint64_t max_line{0}; + while (stream.peek() != std::char_traits::eof()) { + sourcemeta::core::PointerPositionTracker positions; + const std::uint64_t current_offset{line_offset}; + max_line = 0; + auto callback = [&positions, current_offset, &max_line]( + const sourcemeta::core::JSON::ParsePhase phase, + const sourcemeta::core::JSON::Type type, + const std::uint64_t line, + const std::uint64_t column, + const sourcemeta::core::JSON &value) { + max_line = std::max(max_line, line); + positions(phase, type, line + current_offset, column, value); + }; + try { + documents.emplace_back(sourcemeta::core::parse_yaml(stream, callback), + std::move(positions)); + } catch (const sourcemeta::core::YAMLParseError &error) { + throw RemoteSchemaYAMLParseError{std::string{entry_identifier}, + error.what()}; + } + line_offset += max_line > 0 ? max_line - 1 : 0; + } + + if (documents.size() > 1) { + LOG_VERBOSE(options) + << "Interpreting input as YAML multi-document: " << entry_identifier + << "\n"; + std::size_t index{0}; + for (auto &entry_document : documents) { + result.push_back({std::string{entry_identifier}, std::nullopt, + std::move(entry_document.first), + std::move(entry_document.second), index, true, + true}); + index += 1; + } + } else if (documents.size() == 1) { + result.push_back({std::string{entry_identifier}, std::nullopt, + std::move(documents.front().first), + std::move(documents.front().second), 0, false, true}); + } + return; + } + + sourcemeta::core::PointerPositionTracker positions; + try { + const auto document = + sourcemeta::core::parse_json(response.text, std::ref(positions)); + result.push_back({std::string{entry_identifier}, std::nullopt, document, + std::move(positions), 0, false, false}); + } catch (const sourcemeta::core::JSONParseError &error) { + throw RemoteSchemaJSONParseError{std::string{entry_identifier}, + error.line(), error.column(), + error.what()}; + } + + return; + } + + const sourcemeta::core::URI uri{std::string{entry_identifier}}; + if (uri.is_file()) { + handle_json_entry(uri.to_path().string(), blacklist, extensions, result, + options); + return; + } + + const std::filesystem::path entry_path{std::string{entry_identifier}}; if (std::filesystem::is_directory(entry_path)) { - for (auto const &entry : + for (auto const &directory_entry : std::filesystem::recursive_directory_iterator{entry_path}) { - auto canonical{sourcemeta::core::weakly_canonical(entry.path())}; - if (!std::filesystem::is_directory(entry) && + auto canonical{ + sourcemeta::core::weakly_canonical(directory_entry.path())}; + if (!std::filesystem::is_directory(directory_entry) && std::any_of(extensions.cbegin(), extensions.cend(), [&canonical](const auto &extension) { return extension.empty() @@ -171,7 +316,8 @@ handle_json_entry(const std::filesystem::path &entry_path, // TODO: Print a verbose message for what is getting parsed auto parsed{read_file(canonical)}; - result.push_back({std::move(canonical), std::move(parsed.document), + result.push_back({canonical.string(), canonical, + std::move(parsed.document), std::move(parsed.positions), 0, false, parsed.yaml}); } } @@ -190,8 +336,8 @@ handle_json_entry(const std::filesystem::path &entry_path, for (const auto &document : sourcemeta::core::JSONL{stream}) { // TODO: Get real positions for JSONL sourcemeta::core::PointerPositionTracker positions; - result.push_back( - {canonical, document, std::move(positions), index, true}); + result.push_back({canonical.string(), canonical, document, + std::move(positions), index, true}); index += 1; } } catch (const sourcemeta::core::JSONParseError &error) { @@ -236,15 +382,17 @@ handle_json_entry(const std::filesystem::path &entry_path, LOG_VERBOSE(options) << "Interpreting input as YAML multi-document: " << canonical.string() << "\n"; std::size_t index{0}; - for (auto &entry : documents) { - result.push_back({canonical, std::move(entry.first), - std::move(entry.second), index, true, true}); + for (auto &document_entry : documents) { + result.push_back( + {canonical.string(), canonical, std::move(document_entry.first), + std::move(document_entry.second), index, true, true}); index += 1; } } else if (documents.size() == 1) { - result.push_back( - {std::move(canonical), std::move(documents.front().first), - std::move(documents.front().second), 0, false, true}); + result.push_back({canonical.string(), canonical, + std::move(documents.front().first), + std::move(documents.front().second), 0, false, + true}); } } else { if (std::filesystem::is_empty(canonical)) { @@ -252,7 +400,8 @@ handle_json_entry(const std::filesystem::path &entry_path, } // TODO: Print a verbose message for what is getting parsed auto parsed{read_file(canonical)}; - result.push_back({std::move(canonical), std::move(parsed.document), + result.push_back({canonical.string(), canonical, + std::move(parsed.document), std::move(parsed.positions), 0, false, parsed.yaml}); } } @@ -274,8 +423,8 @@ inline auto for_each_json(const std::vector &arguments, const auto extensions{parse_extensions(options, configuration)}; handle_json_entry(configuration.has_value() - ? configuration.value().absolute_path - : current_path, + ? configuration.value().absolute_path.string() + : current_path.string(), blacklist, extensions, result, options); } else { const auto extensions{parse_extensions(options, std::nullopt)}; diff --git a/src/resolver.h b/src/resolver.h index a7b56340..9e3c9900 100644 --- a/src/resolver.h +++ b/src/resolver.h @@ -23,6 +23,7 @@ #include #include "error.h" +#include "http.h" #include "input.h" #include "logger.h" @@ -54,10 +55,7 @@ static inline auto fallback_resolver(const sourcemeta::core::Options &options, } // If the URI is not an HTTP URL, then abort - const sourcemeta::core::URI uri{std::string{identifier}}; - const auto maybe_scheme{uri.scheme()}; - if (uri.is_urn() || !maybe_scheme.has_value() || - (maybe_scheme.value() != "https" && maybe_scheme.value() != "http")) { + if (!is_http_url(identifier)) { return std::nullopt; } @@ -86,9 +84,7 @@ static inline auto fallback_resolver(const sourcemeta::core::Options &options, throw std::runtime_error(error.str()); } - const auto content_type_iterator{response.header.find("content-type")}; - if (content_type_iterator != response.header.end() && - content_type_iterator->second.starts_with("text/yaml")) { + if (is_likely_yaml(identifier, response.header)) { return sourcemeta::core::parse_yaml(response.text); } else { return sourcemeta::core::parse_json(response.text); @@ -104,20 +100,27 @@ class CustomResolver { : options_{options}, configuration_{configuration}, remote_{remote} { if (options.contains("resolve")) { for (const auto &entry : for_each_json(options.at("resolve"), options)) { - LOG_VERBOSE(options) - << "Detecting schema resources from file: " << entry.first.string() - << "\n"; + if (entry.path.has_value()) { + LOG_VERBOSE(options) << "Detecting schema resources from file: " + << entry.path.value().string() << "\n"; + } else { + LOG_VERBOSE(options) + << "Detecting schema resources from url: " << entry.first << "\n"; + } if (!sourcemeta::core::is_schema(entry.second)) { - throw FileError( - entry.first, - "The file you provided does not represent a valid JSON Schema"); + if (entry.path.has_value()) { + throw FileError( + entry.path.value(), + "The file you provided does not represent a valid JSON Schema"); + } + + throw RemoteSchemaNotSchemaError{std::string{entry.first}}; } try { const auto result = this->add( - entry.second, default_dialect, - sourcemeta::core::URI::from_path(entry.first).recompose(), + entry.second, default_dialect, entry.base_uri(), [&options](const auto &identifier) { LOG_VERBOSE(options) << "Importing schema into the resolution context: " @@ -126,15 +129,24 @@ class CustomResolver { if (!result) { LOG_WARNING() << "No schema resources were imported from this file\n" - << " at " << entry.first.string() << "\n" + << " at " << entry.first << "\n" << "Are you sure this schema sets any identifiers?\n"; } } catch (const sourcemeta::core::SchemaFrameError &error) { - throw FileError( - entry.first, std::string{error.identifier()}, error.what()); + if (entry.path.has_value()) { + throw FileError( + entry.path.value(), std::string{error.identifier()}, + error.what()); + } + + throw; } catch (const sourcemeta::core::SchemaUnknownBaseDialectError &) { - throw FileError( - entry.first); + if (entry.path.has_value()) { + throw FileError( + entry.path.value()); + } + + throw; } } } diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 22f11fcd..16792771 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -101,6 +101,7 @@ 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_url_without_http) add_jsonschema_test_unix(validate/pass_jsonl_bigint) add_jsonschema_test_unix(validate/fail_trace) add_jsonschema_test_unix(validate/fail_trace_fast) @@ -202,6 +203,7 @@ add_jsonschema_test_unix(metaschema/fail_no_dialect) add_jsonschema_test_unix(metaschema/fail_default_dialect_config_extension_mismatch) add_jsonschema_test_unix(metaschema/pass_cwd) add_jsonschema_test_unix(metaschema/pass_single) +add_jsonschema_test_unix(metaschema/pass_file_uri) add_jsonschema_test_unix(metaschema/pass_2020_12) add_jsonschema_test_unix(metaschema/pass_2020_12_default_dialect) add_jsonschema_test_unix(metaschema/pass_2020_12_default_dialect_config) @@ -216,6 +218,7 @@ add_jsonschema_test_unix(metaschema/fail_relative_file_metaschema_ref) add_jsonschema_test_unix(metaschema/pass_custom_config_resolve) add_jsonschema_test_unix(metaschema/pass_config_path) add_jsonschema_test_unix(metaschema/fail_config_path_enoent) +add_jsonschema_test_unix(metaschema/fail_schema_url_without_http) add_jsonschema_test_unix(metaschema/pass_without_extension_json) add_jsonschema_test_unix(metaschema/pass_without_extension_yaml) add_jsonschema_test_unix(metaschema/pass_extension_empty_json) @@ -413,6 +416,7 @@ add_jsonschema_test_unix(lint/fail_lint_disable_many) add_jsonschema_test_unix(lint/fail_lint_examples) add_jsonschema_test_unix(lint/fail_lint_default) add_jsonschema_test_unix(lint/pass_lint_json) +add_jsonschema_test_unix(lint/pass_lint_file_uri) add_jsonschema_test_unix(lint/pass_lint_fix_json) add_jsonschema_test_unix(lint/pass_lint_fix_indentation) add_jsonschema_test_unix(lint/pass_lint_ignore_short) @@ -472,6 +476,15 @@ add_jsonschema_test_unix_ci(fail_bundle_http_non_schema) add_jsonschema_test_unix_ci(fail_validate_http_non_200) add_jsonschema_test_unix_ci(fail_validate_http_non_schema) add_jsonschema_test_unix_ci(pass_validate_http) +add_jsonschema_test_unix_ci(fail_validate_schema_http_404) +add_jsonschema_test_unix_ci(fail_validate_schema_http_non_schema) +add_jsonschema_test_unix_ci(fail_metaschema_schema_http_404) +add_jsonschema_test_unix_ci(fail_metaschema_schema_http_non_schema) +add_jsonschema_test_unix_ci(pass_validate_schema_http) +add_jsonschema_test_unix_ci(pass_metaschema_schema_http) +add_jsonschema_test_unix_ci(pass_validate_schema_http_content_type_yaml_over_json_suffix) +add_jsonschema_test_unix_ci(pass_validate_schema_http_suffix_yaml_over_plain_content_type) +add_jsonschema_test_unix_ci(pass_validate_schema_http_suffix_json_over_plain_content_type) add_jsonschema_test_unix_ci(precommit_lint) # TODO: Make this test pass on Linux. It hangs for some reason diff --git a/test/ci/fail_metaschema_schema_http_404.sh b/test/ci/fail_metaschema_schema_http_404.sh new file mode 100755 index 00000000..6da5e7c0 --- /dev/null +++ b/test/ci/fail_metaschema_schema_http_404.sh @@ -0,0 +1,49 @@ +#!/bin/sh + +set -o errexit +set -o nounset + +TMP="$(mktemp -d)" +# shellcheck disable=SC2329,SC2317 +clean() { rm -rf "$TMP"; } +trap clean EXIT + +mkdir "$TMP/server" + +PORT_FILE="$TMP/port.txt" +SERVER_LOG="$TMP/server.log" +SERVERPY="$(dirname "$(readlink -f "$0")")/http_server.py" +python3 "$SERVERPY" "$TMP/server" "$PORT_FILE" >"$SERVER_LOG" 2>&1 & +SERVER_PID="$!" + +clean() { + kill "$SERVER_PID" 2>/dev/null || true + rm -rf "$TMP" +} +trap clean EXIT + +TRIES=0 +while [ ! -s "$PORT_FILE" ]; do + if ! kill -0 "$SERVER_PID" 2>/dev/null; then + cat "$SERVER_LOG" >&2 || true + exit 1 + fi + TRIES=$((TRIES + 1)) + if [ "$TRIES" -gt 100 ]; then + cat "$SERVER_LOG" >&2 || true + exit 1 + fi + sleep 0.1 +done +PORT="$(cat "$PORT_FILE")" + +"$1" metaschema "http://127.0.0.1:$PORT/missing.json" --http \ + 2>"$TMP/stderr.txt" && CODE="$?" || CODE="$?" +test "$CODE" = "1" || exit 1 + +cat << EOF > "$TMP/expected.txt" +error: Could not fetch the schema over HTTP (HTTP 404) + at uri http://127.0.0.1:$PORT/missing.json +EOF + +diff "$TMP/stderr.txt" "$TMP/expected.txt" diff --git a/test/ci/fail_metaschema_schema_http_non_schema.sh b/test/ci/fail_metaschema_schema_http_non_schema.sh new file mode 100755 index 00000000..7d0c2c18 --- /dev/null +++ b/test/ci/fail_metaschema_schema_http_non_schema.sh @@ -0,0 +1,53 @@ +#!/bin/sh + +set -o errexit +set -o nounset + +TMP="$(mktemp -d)" +# shellcheck disable=SC2329,SC2317 +clean() { rm -rf "$TMP"; } +trap clean EXIT + +mkdir "$TMP/server" + +cat << 'EOF' > "$TMP/server/schema.json" +"not a schema" +EOF + +PORT_FILE="$TMP/port.txt" +SERVER_LOG="$TMP/server.log" +SERVERPY="$(dirname "$(readlink -f "$0")")/http_server.py" +python3 "$SERVERPY" "$TMP/server" "$PORT_FILE" >"$SERVER_LOG" 2>&1 & +SERVER_PID="$!" + +clean() { + kill "$SERVER_PID" 2>/dev/null || true + rm -rf "$TMP" +} +trap clean EXIT + +TRIES=0 +while [ ! -s "$PORT_FILE" ]; do + if ! kill -0 "$SERVER_PID" 2>/dev/null; then + cat "$SERVER_LOG" >&2 || true + exit 1 + fi + TRIES=$((TRIES + 1)) + if [ "$TRIES" -gt 100 ]; then + cat "$SERVER_LOG" >&2 || true + exit 1 + fi + sleep 0.1 +done +PORT="$(cat "$PORT_FILE")" + +"$1" metaschema "http://127.0.0.1:$PORT/schema.json" --http \ + 2>"$TMP/stderr.txt" && CODE="$?" || CODE="$?" +test "$CODE" = "1" || exit 1 + +cat << EOF > "$TMP/expected.txt" +error: The fetched document does not represent a valid JSON Schema + at uri http://127.0.0.1:$PORT/schema.json +EOF + +diff "$TMP/stderr.txt" "$TMP/expected.txt" diff --git a/test/ci/fail_validate_schema_http_404.sh b/test/ci/fail_validate_schema_http_404.sh new file mode 100755 index 00000000..0d6734d9 --- /dev/null +++ b/test/ci/fail_validate_schema_http_404.sh @@ -0,0 +1,53 @@ +#!/bin/sh + +set -o errexit +set -o nounset + +TMP="$(mktemp -d)" +# shellcheck disable=SC2329,SC2317 +clean() { rm -rf "$TMP"; } +trap clean EXIT + +mkdir "$TMP/server" + +PORT_FILE="$TMP/port.txt" +SERVER_LOG="$TMP/server.log" +SERVERPY="$(dirname "$(readlink -f "$0")")/http_server.py" +python3 "$SERVERPY" "$TMP/server" "$PORT_FILE" >"$SERVER_LOG" 2>&1 & +SERVER_PID="$!" + +clean() { + kill "$SERVER_PID" 2>/dev/null || true + rm -rf "$TMP" +} +trap clean EXIT + +TRIES=0 +while [ ! -s "$PORT_FILE" ]; do + if ! kill -0 "$SERVER_PID" 2>/dev/null; then + cat "$SERVER_LOG" >&2 || true + exit 1 + fi + TRIES=$((TRIES + 1)) + if [ "$TRIES" -gt 100 ]; then + cat "$SERVER_LOG" >&2 || true + exit 1 + fi + sleep 0.1 +done +PORT="$(cat "$PORT_FILE")" + +cat << 'EOF' > "$TMP/instance.json" +{ "name": "foo", "kind": "example" } +EOF + +"$1" validate "http://127.0.0.1:$PORT/missing.json" "$TMP/instance.json" --http \ + 2>"$TMP/stderr.txt" && CODE="$?" || CODE="$?" +test "$CODE" = "1" || exit 1 + +cat << EOF > "$TMP/expected.txt" +error: Could not fetch the schema over HTTP (HTTP 404) + at uri http://127.0.0.1:$PORT/missing.json +EOF + +diff "$TMP/stderr.txt" "$TMP/expected.txt" diff --git a/test/ci/fail_validate_schema_http_non_schema.sh b/test/ci/fail_validate_schema_http_non_schema.sh new file mode 100755 index 00000000..79e11e32 --- /dev/null +++ b/test/ci/fail_validate_schema_http_non_schema.sh @@ -0,0 +1,57 @@ +#!/bin/sh + +set -o errexit +set -o nounset + +TMP="$(mktemp -d)" +# shellcheck disable=SC2329,SC2317 +clean() { rm -rf "$TMP"; } +trap clean EXIT + +mkdir "$TMP/server" + +cat << 'EOF' > "$TMP/server/schema.json" +"not a schema" +EOF + +PORT_FILE="$TMP/port.txt" +SERVER_LOG="$TMP/server.log" +SERVERPY="$(dirname "$(readlink -f "$0")")/http_server.py" +python3 "$SERVERPY" "$TMP/server" "$PORT_FILE" >"$SERVER_LOG" 2>&1 & +SERVER_PID="$!" + +clean() { + kill "$SERVER_PID" 2>/dev/null || true + rm -rf "$TMP" +} +trap clean EXIT + +TRIES=0 +while [ ! -s "$PORT_FILE" ]; do + if ! kill -0 "$SERVER_PID" 2>/dev/null; then + cat "$SERVER_LOG" >&2 || true + exit 1 + fi + TRIES=$((TRIES + 1)) + if [ "$TRIES" -gt 100 ]; then + cat "$SERVER_LOG" >&2 || true + exit 1 + fi + sleep 0.1 +done +PORT="$(cat "$PORT_FILE")" + +cat << 'EOF' > "$TMP/instance.json" +{ "name": "foo", "kind": "example" } +EOF + +"$1" validate "http://127.0.0.1:$PORT/schema.json" "$TMP/instance.json" --http \ + 2>"$TMP/stderr.txt" && CODE="$?" || CODE="$?" +test "$CODE" = "1" || exit 1 + +cat << EOF > "$TMP/expected.txt" +error: The fetched document does not represent a valid JSON Schema + at uri http://127.0.0.1:$PORT/schema.json +EOF + +diff "$TMP/stderr.txt" "$TMP/expected.txt" diff --git a/test/ci/http_server.py b/test/ci/http_server.py new file mode 100755 index 00000000..5aa3da33 --- /dev/null +++ b/test/ci/http_server.py @@ -0,0 +1,43 @@ +import os +import socketserver +import sys + +import http.server + + +class _Handler(http.server.SimpleHTTPRequestHandler): + def __init__(self, *args, forced_content_type: str | None = None, **kwargs): + self._forced_content_type = forced_content_type + super().__init__(*args, **kwargs) + + def guess_type(self, path: str) -> str: + if self._forced_content_type: + return self._forced_content_type + return super().guess_type(path) + + +def main() -> int: + if len(sys.argv) < 3: + raise SystemExit( + "usage: http_server.py [bind_host] [forced_content_type]" + ) + + directory = sys.argv[1] + port_file = sys.argv[2] + bind_host = sys.argv[3] if len(sys.argv) > 3 else "127.0.0.1" + forced_content_type = sys.argv[4] if len(sys.argv) > 4 else None + + os.chdir(directory) + handler = lambda *args, **kwargs: _Handler( # noqa: E731 + *args, forced_content_type=forced_content_type, **kwargs + ) + with socketserver.TCPServer((bind_host, 0), handler) as httpd: + with open(port_file, "w", encoding="utf-8") as f: + f.write(str(httpd.server_address[1])) + httpd.serve_forever() + + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/test/ci/pass_metaschema_schema_http.sh b/test/ci/pass_metaschema_schema_http.sh new file mode 100755 index 00000000..fe69c7f6 --- /dev/null +++ b/test/ci/pass_metaschema_schema_http.sh @@ -0,0 +1,66 @@ +#!/bin/sh + +set -o errexit +set -o nounset + +TMP="$(mktemp -d)" +# shellcheck disable=SC2329,SC2317 +clean() { rm -rf "$TMP"; } +trap clean EXIT + +mkdir "$TMP/server" + +cat << 'EOF' > "$TMP/server/schema.json" +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "name": { "$ref": "#/$defs/nonEmptyLowercase" }, + "kind": { "enum": [ "example", "test" ] } + }, + "required": [ "name", "kind" ], + "additionalProperties": false, + "$defs": { + "nonEmptyLowercase": { + "type": "string", + "minLength": 1, + "pattern": "^[a-z]+$" + } + } +} +EOF + +PORT_FILE="$TMP/port.txt" +SERVER_LOG="$TMP/server.log" +SERVERPY="$(dirname "$(readlink -f "$0")")/http_server.py" +python3 "$SERVERPY" "$TMP/server" "$PORT_FILE" >"$SERVER_LOG" 2>&1 & +SERVER_PID="$!" + +clean() { + kill "$SERVER_PID" 2>/dev/null || true + rm -rf "$TMP" +} +trap clean EXIT + +TRIES=0 +while [ ! -s "$PORT_FILE" ]; do + if ! kill -0 "$SERVER_PID" 2>/dev/null; then + cat "$SERVER_LOG" >&2 || true + exit 1 + fi + TRIES=$((TRIES + 1)) + if [ "$TRIES" -gt 100 ]; then + cat "$SERVER_LOG" >&2 || true + exit 1 + fi + sleep 0.1 +done +PORT="$(cat "$PORT_FILE")" + +"$1" metaschema "http://127.0.0.1:$PORT/schema.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_schema_http.sh b/test/ci/pass_validate_schema_http.sh new file mode 100755 index 00000000..835aad5f --- /dev/null +++ b/test/ci/pass_validate_schema_http.sh @@ -0,0 +1,70 @@ +#!/bin/sh + +set -o errexit +set -o nounset + +TMP="$(mktemp -d)" +# shellcheck disable=SC2329,SC2317 +clean() { rm -rf "$TMP"; } +trap clean EXIT + +mkdir "$TMP/server" + +cat << 'EOF' > "$TMP/server/schema.json" +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "name": { "$ref": "#/$defs/nonEmptyLowercase" }, + "kind": { "enum": [ "example", "test" ] } + }, + "required": [ "name", "kind" ], + "additionalProperties": false, + "$defs": { + "nonEmptyLowercase": { + "type": "string", + "minLength": 1, + "pattern": "^[a-z]+$" + } + } +} +EOF + +PORT_FILE="$TMP/port.txt" +SERVER_LOG="$TMP/server.log" +SERVERPY="$(dirname "$(readlink -f "$0")")/http_server.py" +python3 "$SERVERPY" "$TMP/server" "$PORT_FILE" >"$SERVER_LOG" 2>&1 & +SERVER_PID="$!" + +clean() { + kill "$SERVER_PID" 2>/dev/null || true + rm -rf "$TMP" +} +trap clean EXIT + +TRIES=0 +while [ ! -s "$PORT_FILE" ]; do + if ! kill -0 "$SERVER_PID" 2>/dev/null; then + cat "$SERVER_LOG" >&2 || true + exit 1 + fi + TRIES=$((TRIES + 1)) + if [ "$TRIES" -gt 100 ]; then + cat "$SERVER_LOG" >&2 || true + exit 1 + fi + sleep 0.1 +done +PORT="$(cat "$PORT_FILE")" + +cat << 'EOF' > "$TMP/instance.json" +{ "name": "foo", "kind": "example" } +EOF + +"$1" validate "http://127.0.0.1:$PORT/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_schema_http_content_type_yaml_over_json_suffix.sh b/test/ci/pass_validate_schema_http_content_type_yaml_over_json_suffix.sh new file mode 100755 index 00000000..65bd2511 --- /dev/null +++ b/test/ci/pass_validate_schema_http_content_type_yaml_over_json_suffix.sh @@ -0,0 +1,61 @@ +#!/bin/sh + +set -o errexit +set -o nounset + +TMP="$(mktemp -d)" +# shellcheck disable=SC2329,SC2317 +clean() { rm -rf "$TMP"; } +trap clean EXIT + +mkdir "$TMP/server" + +cat << 'EOF' > "$TMP/server/schema.json" +$schema: https://json-schema.org/draft/2020-12/schema +type: object +properties: + name: + type: string +required: [ name ] +additionalProperties: false +EOF + +PORT_FILE="$TMP/port.txt" +SERVER_LOG="$TMP/server.log" +SERVERPY="$(dirname "$(readlink -f "$0")")/http_server.py" +python3 "$SERVERPY" "$TMP/server" "$PORT_FILE" 127.0.0.1 "text/yaml" \ + >"$SERVER_LOG" 2>&1 & +SERVER_PID="$!" + +clean() { + kill "$SERVER_PID" 2>/dev/null || true + rm -rf "$TMP" +} +trap clean EXIT + +TRIES=0 +while [ ! -s "$PORT_FILE" ]; do + if ! kill -0 "$SERVER_PID" 2>/dev/null; then + cat "$SERVER_LOG" >&2 || true + exit 1 + fi + TRIES=$((TRIES + 1)) + if [ "$TRIES" -gt 100 ]; then + cat "$SERVER_LOG" >&2 || true + exit 1 + fi + sleep 0.1 +done +PORT="$(cat "$PORT_FILE")" + +cat << 'EOF' > "$TMP/instance.json" +{ "name": "foo" } +EOF + +"$1" validate "http://127.0.0.1:$PORT/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_schema_http_suffix_json_over_plain_content_type.sh b/test/ci/pass_validate_schema_http_suffix_json_over_plain_content_type.sh new file mode 100755 index 00000000..534a443d --- /dev/null +++ b/test/ci/pass_validate_schema_http_suffix_json_over_plain_content_type.sh @@ -0,0 +1,63 @@ +#!/bin/sh + +set -o errexit +set -o nounset + +TMP="$(mktemp -d)" +# shellcheck disable=SC2329,SC2317 +clean() { rm -rf "$TMP"; } +trap clean EXIT + +mkdir "$TMP/server" + +cat << 'EOF' > "$TMP/server/schema.json" +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "name": { "type": "string" } + }, + "required": [ "name" ], + "additionalProperties": false +} +EOF + +PORT_FILE="$TMP/port.txt" +SERVER_LOG="$TMP/server.log" +SERVERPY="$(dirname "$(readlink -f "$0")")/http_server.py" +python3 "$SERVERPY" "$TMP/server" "$PORT_FILE" 127.0.0.1 "text/plain" \ + >"$SERVER_LOG" 2>&1 & +SERVER_PID="$!" + +clean() { + kill "$SERVER_PID" 2>/dev/null || true + rm -rf "$TMP" +} +trap clean EXIT + +TRIES=0 +while [ ! -s "$PORT_FILE" ]; do + if ! kill -0 "$SERVER_PID" 2>/dev/null; then + cat "$SERVER_LOG" >&2 || true + exit 1 + fi + TRIES=$((TRIES + 1)) + if [ "$TRIES" -gt 100 ]; then + cat "$SERVER_LOG" >&2 || true + exit 1 + fi + sleep 0.1 +done +PORT="$(cat "$PORT_FILE")" + +cat << 'EOF' > "$TMP/instance.json" +{ "name": "foo" } +EOF + +"$1" validate "http://127.0.0.1:$PORT/schema.json?ignored=1" "$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_schema_http_suffix_yaml_over_plain_content_type.sh b/test/ci/pass_validate_schema_http_suffix_yaml_over_plain_content_type.sh new file mode 100755 index 00000000..ae38829e --- /dev/null +++ b/test/ci/pass_validate_schema_http_suffix_yaml_over_plain_content_type.sh @@ -0,0 +1,61 @@ +#!/bin/sh + +set -o errexit +set -o nounset + +TMP="$(mktemp -d)" +# shellcheck disable=SC2329,SC2317 +clean() { rm -rf "$TMP"; } +trap clean EXIT + +mkdir "$TMP/server" + +cat << 'EOF' > "$TMP/server/schema.yaml" +$schema: https://json-schema.org/draft/2020-12/schema +type: object +properties: + name: + type: string +required: [ name ] +additionalProperties: false +EOF + +PORT_FILE="$TMP/port.txt" +SERVER_LOG="$TMP/server.log" +SERVERPY="$(dirname "$(readlink -f "$0")")/http_server.py" +python3 "$SERVERPY" "$TMP/server" "$PORT_FILE" 127.0.0.1 "text/plain" \ + >"$SERVER_LOG" 2>&1 & +SERVER_PID="$!" + +clean() { + kill "$SERVER_PID" 2>/dev/null || true + rm -rf "$TMP" +} +trap clean EXIT + +TRIES=0 +while [ ! -s "$PORT_FILE" ]; do + if ! kill -0 "$SERVER_PID" 2>/dev/null; then + cat "$SERVER_LOG" >&2 || true + exit 1 + fi + TRIES=$((TRIES + 1)) + if [ "$TRIES" -gt 100 ]; then + cat "$SERVER_LOG" >&2 || true + exit 1 + fi + sleep 0.1 +done +PORT="$(cat "$PORT_FILE")" + +cat << 'EOF' > "$TMP/instance.json" +{ "name": "foo" } +EOF + +"$1" validate "http://127.0.0.1:$PORT/schema.yaml?ignored=1" "$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/lint/pass_lint_file_uri.sh b/test/lint/pass_lint_file_uri.sh new file mode 100755 index 00000000..df7c9695 --- /dev/null +++ b/test/lint/pass_lint_file_uri.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": "http://json-schema.org/draft-04/schema#", + "title": "Test", + "description": "Test schema", + "enum": [ "foo" ] +} +EOF + +SCHEMA_PATH="$(cd "$TMP" && pwd)/schema.json" +SCHEMA_URI="file://$SCHEMA_PATH" + +"$1" lint "$SCHEMA_URI" --json >"$TMP/output.json" 2>&1 + +cat << EOF > "$TMP/expected.json" +{ + "valid": true, + "health": 100, + "errors": [] +} +EOF + +diff "$TMP/output.json" "$TMP/expected.json" + diff --git a/test/metaschema/fail_schema_url_without_http.sh b/test/metaschema/fail_schema_url_without_http.sh new file mode 100755 index 00000000..7f69623a --- /dev/null +++ b/test/metaschema/fail_schema_url_without_http.sh @@ -0,0 +1,32 @@ +#!/bin/sh + +set -o errexit +set -o nounset + +TMP="$(mktemp -d)" +# shellcheck disable=SC2329,SC2317 +clean() { rm -rf "$TMP"; } +trap clean EXIT + +"$1" metaschema "https://example.com/schema.json" 2>"$TMP/stderr.txt" \ + && CODE="$?" || CODE="$?" +test "$CODE" = "1" || exit 1 + +cat << 'EOF' > "$TMP/expected.txt" +error: Remote schema inputs require network access. Pass `--http/-h` +EOF + +diff "$TMP/stderr.txt" "$TMP/expected.txt" + +# JSON error +"$1" metaschema "https://example.com/schema.json" --json >"$TMP/stdout.txt" \ + 2>&1 && CODE="$?" || CODE="$?" +test "$CODE" = "1" || exit 1 + +cat << 'EOF' > "$TMP/expected.json" +{ + "error": "Remote schema inputs require network access. Pass `--http/-h`" +} +EOF + +diff "$TMP/stdout.txt" "$TMP/expected.json" diff --git a/test/metaschema/pass_file_uri.sh b/test/metaschema/pass_file_uri.sh new file mode 100755 index 00000000..a35cb8a4 --- /dev/null +++ b/test/metaschema/pass_file_uri.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#", + "title": "Test", + "description": "Test schema", + "type": "string" +} +EOF + +SCHEMA_PATH="$(cd "$TMP" && pwd)/schema.json" +SCHEMA_URI="file://$SCHEMA_PATH" + +"$1" metaschema "$SCHEMA_URI" > "$TMP/output.txt" 2>&1 + +cat << EOF > "$TMP/expected.txt" +EOF + +diff "$TMP/output.txt" "$TMP/expected.txt" + diff --git a/test/validate/fail_schema_url_without_http.sh b/test/validate/fail_schema_url_without_http.sh new file mode 100755 index 00000000..401fefdd --- /dev/null +++ b/test/validate/fail_schema_url_without_http.sh @@ -0,0 +1,36 @@ +#!/bin/sh + +set -o errexit +set -o nounset + +TMP="$(mktemp -d)" +# shellcheck disable=SC2329,SC2317 +clean() { rm -rf "$TMP"; } +trap clean EXIT + +cat << 'EOF' > "$TMP/instance.json" +"foo" +EOF + +"$1" validate "https://example.com/schema.json" "$TMP/instance.json" \ + 2>"$TMP/stderr.txt" && CODE="$?" || CODE="$?" +test "$CODE" = "1" || exit 1 + +cat << 'EOF' > "$TMP/expected.txt" +error: Remote schema inputs require network access. Pass `--http/-h` +EOF + +diff "$TMP/stderr.txt" "$TMP/expected.txt" + +# JSON error +"$1" validate "https://example.com/schema.json" "$TMP/instance.json" --json \ + >"$TMP/stdout.txt" 2>&1 && CODE="$?" || CODE="$?" +test "$CODE" = "1" || exit 1 + +cat << 'EOF' > "$TMP/expected.json" +{ + "error": "Remote schema inputs require network access. Pass `--http/-h`" +} +EOF + +diff "$TMP/stdout.txt" "$TMP/expected.json"