diff --git a/src/command_fmt.cc b/src/command_fmt.cc index 75f34ded..3dfa66f0 100644 --- a/src/command_fmt.cc +++ b/src/command_fmt.cc @@ -20,7 +20,86 @@ auto sourcemeta::jsonschema::fmt(const sourcemeta::core::Options &options) bool result{true}; std::vector failed_files; const auto indentation{parse_indentation(options)}; - for (const auto &entry : for_each_json(options)) { + + // Helper lambda to handle a single stdin entry. + // For --check, we must locally buffer stdin to compare raw input against + // expected formatted output. This is the same approach used for files + // (which re-read the file for comparison). For non-check, we parse + // directly from std::cin and write straight to stdout (no buffering). + const auto handle_stdin = [&]() { + try { + const auto current_path{std::filesystem::current_path()}; + const auto configuration_path{find_configuration(current_path)}; + const auto &configuration{ + read_configuration(options, configuration_path, current_path)}; + const auto dialect{default_dialect(options, configuration)}; + const auto &custom_resolver{ + resolver(options, options.contains("http"), dialect, configuration)}; + + if (options.contains("check")) { + std::ostringstream stdin_buf; + stdin_buf << std::cin.rdbuf(); + const auto raw_stdin{stdin_buf.str()}; + std::istringstream parse_stream{raw_stdin}; + const auto document{sourcemeta::core::parse_json(parse_stream)}; + + std::ostringstream expected; + if (options.contains("keep-ordering")) { + sourcemeta::core::prettify(document, expected, indentation); + } else { + auto copy = document; + sourcemeta::core::format(copy, sourcemeta::core::schema_walker, + custom_resolver, dialect); + sourcemeta::core::prettify(copy, expected, indentation); + } + expected << "\n"; + + if (raw_stdin == expected.str()) { + LOG_VERBOSE(options) << "ok: (stdin)\n"; + } else if (output_json) { + failed_files.push_back("(stdin)"); + result = false; + } else { + std::cerr << "fail: (stdin)\n"; + result = false; + } + } else { + const auto document{sourcemeta::core::parse_json(std::cin)}; + if (options.contains("keep-ordering")) { + sourcemeta::core::prettify(document, std::cout, indentation); + } else { + auto copy = document; + sourcemeta::core::format(copy, sourcemeta::core::schema_walker, + custom_resolver, dialect); + sourcemeta::core::prettify(copy, std::cout, indentation); + } + std::cout << "\n"; + } + } catch (const sourcemeta::core::SchemaKeywordError &error) { + throw FileError( + std::filesystem::current_path(), error); + } catch (const sourcemeta::core::SchemaFrameError &error) { + throw FileError( + std::filesystem::current_path(), error); + } catch (const sourcemeta::core::SchemaRelativeMetaschemaResolutionError + &error) { + throw FileError< + sourcemeta::core::SchemaRelativeMetaschemaResolutionError>( + std::filesystem::current_path(), error); + } catch (const sourcemeta::core::SchemaResolutionError &error) { + throw FileError( + std::filesystem::current_path(), error); + } catch (const sourcemeta::core::SchemaUnknownBaseDialectError &) { + throw FileError( + std::filesystem::current_path()); + } catch (const sourcemeta::core::SchemaError &error) { + throw FileError( + std::filesystem::current_path(), error.what()); + } + }; + + // Helper lambda to handle a single file entry from for_each_json + const auto handle_file_entry = [&](const InputJSON &entry) { if (entry.yaml) { throw YAMLInputError{"This command does not support YAML input files yet", entry.resolution_base}; @@ -95,6 +174,26 @@ auto sourcemeta::jsonschema::fmt(const sourcemeta::core::Options &options) throw FileError(entry.resolution_base, error.what()); } + }; + + // Process arguments in order to preserve argument ordering semantics. + // When no positional arguments are given, default to for_each_json(options) + // which scans the current directory. + if (options.positional().empty()) { + for (const auto &entry : for_each_json(options)) { + handle_file_entry(entry); + } + } else { + check_no_duplicate_stdin(options.positional()); + for (const auto &arg : options.positional()) { + if (arg == "-") { + handle_stdin(); + } else { + for (const auto &entry : for_each_json({arg}, options)) { + handle_file_entry(entry); + } + } + } } if (options.contains("check") && output_json) { diff --git a/src/command_lint.cc b/src/command_lint.cc index 8824ec44..d5767de1 100644 --- a/src/command_lint.cc +++ b/src/command_lint.cc @@ -206,7 +206,11 @@ auto sourcemeta::jsonschema::lint(const sourcemeta::core::Options &options) input_paths.emplace_back(std::filesystem::current_path()); } else { for (const auto &argument : options.positional()) { - input_paths.emplace_back(std::filesystem::weakly_canonical(argument)); + if (argument == "-") { + input_paths.emplace_back(std::filesystem::current_path()); + } else { + input_paths.emplace_back(std::filesystem::weakly_canonical(argument)); + } } } @@ -324,7 +328,9 @@ auto sourcemeta::jsonschema::lint(const sourcemeta::core::Options &options) const auto indentation{parse_indentation(options)}; if (options.contains("fix")) { - for (const auto &entry : for_each_json(options)) { + const auto entries = for_each_json(options); + + for (const auto &entry : entries) { const auto configuration_path{find_configuration(entry.resolution_base)}; const auto &configuration{read_configuration(options, configuration_path, entry.resolution_base)}; @@ -439,7 +445,17 @@ auto sourcemeta::jsonschema::lint(const sourcemeta::core::Options &options) result = false; } - if (format_output) { + if (entry.from_stdin) { + if (format_output) { + if (!keep_ordering) { + sourcemeta::core::format(copy, sourcemeta::core::schema_walker, + custom_resolver, dialect); + } + } + + sourcemeta::core::prettify(copy, std::cout, indentation); + std::cout << "\n"; + } else if (format_output) { if (!keep_ordering) { sourcemeta::core::format(copy, sourcemeta::core::schema_walker, custom_resolver, dialect); diff --git a/src/command_validate.cc b/src/command_validate.cc index 6a997b9c..7fc391b4 100644 --- a/src/command_validate.cc +++ b/src/command_validate.cc @@ -140,22 +140,41 @@ auto sourcemeta::jsonschema::validate(const sourcemeta::core::Options &options) } const auto &schema_path{options.positional().at(0)}; + const bool schema_from_stdin = (schema_path == "-"); + + // Cannot use stdin for both schema and instances + if (schema_from_stdin) { + for (std::size_t i = 1; i < options.positional().size(); ++i) { + if (options.positional().at(i) == "-") { + throw StdinError{ + "Cannot read both schema and instance from standard input"}; + } + } + } - if (std::filesystem::is_directory(schema_path)) { + if (!schema_from_stdin && 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 configuration_path{find_configuration(schema_path)}; + // For stdin, use the current working directory as the resolution base + const auto schema_resolution_base{ + schema_from_stdin ? std::filesystem::current_path() + : std::filesystem::path{std::string{schema_path}}}; + + const auto configuration_path{find_configuration(schema_resolution_base)}; const auto &configuration{ - read_configuration(options, configuration_path, schema_path)}; + read_configuration(options, configuration_path, schema_resolution_base)}; const auto dialect{default_dialect(options, configuration)}; - const auto schema{sourcemeta::core::read_yaml_or_json(schema_path)}; + // stdin only supports JSON (cannot retry parsing for YAML) + const auto schema{schema_from_stdin + ? sourcemeta::core::parse_json(std::cin) + : sourcemeta::core::read_yaml_or_json(schema_path)}; if (!sourcemeta::core::is_schema(schema)) { - throw NotSchemaError{schema_path}; + throw NotSchemaError{schema_resolution_base}; } const auto &custom_resolver{ @@ -177,7 +196,8 @@ auto sourcemeta::jsonschema::validate(const sourcemeta::core::Options &options) "The --entrypoint option cannot be used with --template"}; } - const auto schema_default_id{sourcemeta::jsonschema::default_id(schema_path)}; + const auto schema_default_id{ + sourcemeta::jsonschema::default_id(schema_resolution_base)}; const sourcemeta::core::JSON bundled{[&]() { try { @@ -185,32 +205,36 @@ auto sourcemeta::jsonschema::validate(const sourcemeta::core::Options &options) custom_resolver, dialect, schema_default_id); } catch (const sourcemeta::core::SchemaKeywordError &error) { - throw FileError(schema_path, error); + throw FileError( + schema_resolution_base, error); } catch (const sourcemeta::core::SchemaFrameError &error) { - throw FileError(schema_path, error); + throw FileError( + schema_resolution_base, error); } catch (const sourcemeta::core::SchemaReferenceError &error) { throw FileError( - schema_path, std::string{error.identifier()}, error.location(), - error.what()); + schema_resolution_base, std::string{error.identifier()}, + error.location(), error.what()); } catch (const sourcemeta::core::SchemaRelativeMetaschemaResolutionError &error) { throw FileError< sourcemeta::core::SchemaRelativeMetaschemaResolutionError>( - schema_path, error); + schema_resolution_base, error); } catch (const sourcemeta::core::SchemaResolutionError &error) { - throw FileError(schema_path, - error); + throw FileError( + schema_resolution_base, error); } catch (const sourcemeta::core::SchemaUnknownBaseDialectError &) { throw FileError( - schema_path); + schema_resolution_base); } catch (const sourcemeta::core::SchemaUnknownDialectError &) { - throw FileError(schema_path); + throw FileError( + schema_resolution_base); } catch (const sourcemeta::core::SchemaError &error) { - throw FileError(schema_path, error.what()); + throw FileError(schema_resolution_base, + error.what()); } catch ( const sourcemeta::core::SchemaReferenceObjectResourceError &error) { throw FileError( - schema_path, error.identifier()); + schema_resolution_base, error.identifier()); } }()}; @@ -221,23 +245,27 @@ auto sourcemeta::jsonschema::validate(const sourcemeta::core::Options &options) frame.analyse(bundled, sourcemeta::core::schema_walker, custom_resolver, dialect, schema_default_id); } catch (const sourcemeta::core::SchemaKeywordError &error) { - throw FileError(schema_path, error); + throw FileError( + schema_resolution_base, error); } catch (const sourcemeta::core::SchemaFrameError &error) { - throw FileError(schema_path, error); + throw FileError(schema_resolution_base, + error); } catch ( const sourcemeta::core::SchemaRelativeMetaschemaResolutionError &error) { throw FileError( - schema_path, error); + schema_resolution_base, error); } catch (const sourcemeta::core::SchemaResolutionError &error) { - throw FileError(schema_path, - error); + throw FileError( + schema_resolution_base, error); } catch (const sourcemeta::core::SchemaUnknownBaseDialectError &) { throw FileError( - schema_path); + schema_resolution_base); } catch (const sourcemeta::core::SchemaUnknownDialectError &) { - throw FileError(schema_path); + throw FileError( + schema_resolution_base); } catch (const sourcemeta::core::SchemaError &error) { - throw FileError(schema_path, error.what()); + throw FileError(schema_resolution_base, + error.what()); } std::string entrypoint_uri{frame.root()}; @@ -246,8 +274,8 @@ auto sourcemeta::jsonschema::validate(const sourcemeta::core::Options &options) entrypoint_uri = resolve_entrypoint( frame, std::string{options.at("entrypoint").front()}); } catch (const sourcemeta::blaze::CompilerInvalidEntryPoint &error) { - throw FileError(schema_path, - error); + throw FileError( + schema_resolution_base, error); } } @@ -256,35 +284,39 @@ auto sourcemeta::jsonschema::validate(const sourcemeta::core::Options &options) return get_schema_template(bundled, custom_resolver, frame, entrypoint_uri, fast_mode, options); } catch (const sourcemeta::blaze::CompilerInvalidEntryPoint &error) { - throw FileError(schema_path, - error); + throw FileError( + schema_resolution_base, error); } catch ( const sourcemeta::blaze::CompilerReferenceTargetNotSchemaError &error) { throw FileError( - schema_path, error); + schema_resolution_base, error); } catch (const sourcemeta::core::SchemaKeywordError &error) { - throw FileError(schema_path, error); + throw FileError( + schema_resolution_base, error); } catch (const sourcemeta::core::SchemaFrameError &error) { - throw FileError(schema_path, error); + throw FileError( + schema_resolution_base, error); } catch (const sourcemeta::core::SchemaReferenceError &error) { throw FileError( - schema_path, std::string{error.identifier()}, error.location(), - error.what()); + schema_resolution_base, std::string{error.identifier()}, + error.location(), error.what()); } catch (const sourcemeta::core::SchemaRelativeMetaschemaResolutionError &error) { throw FileError< sourcemeta::core::SchemaRelativeMetaschemaResolutionError>( - schema_path, error); + schema_resolution_base, error); } catch (const sourcemeta::core::SchemaResolutionError &error) { - throw FileError(schema_path, - error); + throw FileError( + schema_resolution_base, error); } catch (const sourcemeta::core::SchemaUnknownBaseDialectError &) { throw FileError( - schema_path); + schema_resolution_base); } catch (const sourcemeta::core::SchemaUnknownDialectError &) { - throw FileError(schema_path); + throw FileError( + schema_resolution_base); } catch (const sourcemeta::core::SchemaError &error) { - throw FileError(schema_path, error.what()); + throw FileError(schema_resolution_base, + error.what()); } }()}; @@ -300,6 +332,8 @@ auto sourcemeta::jsonschema::validate(const sourcemeta::core::Options &options) instance_arguments.push_back("."); } + check_no_duplicate_stdin(instance_arguments); + if (trace && instance_arguments.size() > 1) { throw OptionConflictError{ "The `--trace/-t` option is only allowed given a single instance"}; @@ -317,16 +351,19 @@ auto sourcemeta::jsonschema::validate(const sourcemeta::core::Options &options) "The `--trace/-t` option is only allowed given a single instance"}; } - if (trace && std::filesystem::is_directory(instance_path)) { + if (trace && instance_path_view != "-" && + std::filesystem::is_directory(instance_path)) { throw OptionConflictError{ "The `--trace/-t` option is only allowed given a single instance"}; } - if (benchmark && std::filesystem::is_directory(instance_path)) { + if (benchmark && instance_path_view != "-" && + std::filesystem::is_directory(instance_path)) { throw OptionConflictError{"The `--benchmark/-b` option is only allowed " "given a single instance"}; } - if (std::filesystem::is_directory(instance_path) || + if (instance_path_view == "-" || + std::filesystem::is_directory(instance_path) || instance_path.extension() == ".jsonl" || instance_path.extension() == ".yaml" || instance_path.extension() == ".yml") { @@ -380,7 +417,8 @@ auto sourcemeta::jsonschema::validate(const sourcemeta::core::Options &options) } LOG_VERBOSE(options) << "\n matches " - << sourcemeta::core::weakly_canonical(schema_path).string() + << sourcemeta::core::weakly_canonical(schema_resolution_base) + .string() << "\n"; print_annotations(output, options, entry.positions, std::cerr); } else { @@ -451,7 +489,9 @@ auto sourcemeta::jsonschema::validate(const sourcemeta::core::Options &options) << "ok: " << sourcemeta::core::weakly_canonical(instance_path).string() << "\n matches " - << sourcemeta::core::weakly_canonical(schema_path).string() << "\n"; + << sourcemeta::core::weakly_canonical(schema_resolution_base) + .string() + << "\n"; print_annotations(output, options, tracker, std::cerr); } else { std::cerr << "fail: " diff --git a/src/error.h b/src/error.h index b58ac92d..cd891949 100644 --- a/src/error.h +++ b/src/error.h @@ -94,6 +94,11 @@ class OptionConflictError : public std::runtime_error { : std::runtime_error{std::move(message)} {} }; +class StdinError : public std::runtime_error { +public: + StdinError(std::string message) : std::runtime_error{std::move(message)} {} +}; + class InvalidLintRuleError : public std::runtime_error { public: InvalidLintRuleError(std::string message, std::string rule) @@ -671,6 +676,10 @@ inline auto try_catch(const sourcemeta::core::Options &options, return EXIT_OTHER_INPUT_ERROR; // Command line parsing handling + } catch (const StdinError &error) { + const auto is_json{options.contains("json")}; + print_exception(is_json, error); + return EXIT_FAILURE; } catch (const OptionConflictError &error) { const auto is_json{options.contains("json")}; print_exception(is_json, error); diff --git a/src/input.h b/src/input.h index 3957e487..bbbd8f64 100644 --- a/src/input.h +++ b/src/input.h @@ -12,13 +12,15 @@ #include "configuration.h" #include "logger.h" -#include // std::any_of, std::none_of, std::sort +#include // std::any_of, std::none_of, std::sort, std::count #include // std::size_t #include // std::uintptr_t #include // std::filesystem #include // std::ref, std::hash +#include // std::cin +#include // std::optional #include // std::set -#include // std::ostringstream +#include // std::runtime_error #include // std::string #include // std::unordered_set #include // std::vector @@ -33,6 +35,7 @@ struct InputJSON { std::size_t index{0}; bool multidocument{false}; bool yaml{false}; + bool from_stdin{false}; auto operator<(const InputJSON &other) const noexcept -> bool { return this->first < other.first; } @@ -162,12 +165,30 @@ inline auto read_file(const std::filesystem::path &path) -> ParsedJSON { } } +// stdin is a non-seekable stream, so we cannot retry parsing after failure. +// Unlike file-based input where we detect format by extension or try JSON +// then YAML, stdin only supports JSON input. +inline auto read_from_stdin() -> ParsedJSON { + sourcemeta::core::PointerPositionTracker positions; + return {sourcemeta::core::parse_json(std::cin, std::ref(positions)), + std::move(positions), false}; +} + inline auto handle_json_entry(const std::filesystem::path &entry_path, const std::set &blacklist, const std::set &extensions, std::vector &result, const sourcemeta::core::Options &options) -> void { + if (entry_path == "-") { + auto parsed{read_from_stdin()}; + const auto current_path{std::filesystem::current_path()}; + result.push_back({current_path.string(), current_path, + std::move(parsed.document), std::move(parsed.positions), + 0, false, parsed.yaml, true}); + return; + } + if (std::filesystem::is_directory(entry_path)) { for (auto const &entry : std::filesystem::recursive_directory_iterator{entry_path}) { @@ -272,7 +293,8 @@ handle_json_entry(const std::filesystem::path &entry_path, true}); } } else { - if (std::filesystem::is_empty(canonical)) { + if (std::filesystem::is_regular_file(canonical) && + std::filesystem::is_empty(canonical)) { return; } // TODO: Print a verbose message for what is getting parsed @@ -287,9 +309,19 @@ handle_json_entry(const std::filesystem::path &entry_path, } // namespace +inline auto +check_no_duplicate_stdin(const std::vector &arguments) + -> void { + if (std::count(arguments.cbegin(), arguments.cend(), "-") > 1) { + throw StdinError("Cannot read from standard input more than once"); + } +} + inline auto for_each_json(const std::vector &arguments, const sourcemeta::core::Options &options) -> std::vector { + check_no_duplicate_stdin(arguments); + auto blacklist{parse_ignore(options)}; std::vector result; @@ -314,6 +346,11 @@ inline auto for_each_json(const std::vector &arguments, } else { std::unordered_set seen_configurations; for (const auto &entry : arguments) { + // Skip stdin when looking for configurations + if (entry == "-") { + continue; + } + const auto entry_path{ sourcemeta::core::weakly_canonical(std::filesystem::path{entry})}; const auto configuration_path{ diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 0c340c4a..4d4c97c3 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -72,6 +72,10 @@ add_jsonschema_test_unix(format/fail_custom_extension_yaml) add_jsonschema_test_unix(format/fail_invalid_id_type) add_jsonschema_test_unix(format/fail_invalid_schema_uri) add_jsonschema_test_unix(format/pass_config_ignore) +add_jsonschema_test_unix(format/pass_stdin) +add_jsonschema_test_unix(format/pass_stdin_check) +add_jsonschema_test_unix(format/fail_stdin_check) +add_jsonschema_test_unix(format/fail_stdin_check_json) # Validate add_jsonschema_test_unix(validate/fail_instance_enoent) @@ -217,6 +221,14 @@ add_jsonschema_test_unix(validate/fail_entrypoint_mismatch) add_jsonschema_test_unix(validate/fail_entrypoint_with_template) add_jsonschema_test_unix(validate/pass_config_ignore) add_jsonschema_test_unix(validate/pass_config_ignore_with_cli) +add_jsonschema_test_unix(validate/pass_stdin_instance) +add_jsonschema_test_unix(validate/fail_stdin_instance) +add_jsonschema_test_unix(validate/fail_stdin_instance_json) +add_jsonschema_test_unix(validate/pass_stdin_schema) +add_jsonschema_test_unix(validate/fail_stdin_schema) +add_jsonschema_test_unix(validate/fail_stdin_schema_json) +add_jsonschema_test_unix(validate/fail_stdin_multiple) +add_jsonschema_test_unix(validate/fail_stdin_multiple_json) # Metaschema add_jsonschema_test_unix(metaschema/pass_trace) @@ -253,6 +265,7 @@ add_jsonschema_test_unix(metaschema/pass_custom_extension_yaml) add_jsonschema_test_unix(metaschema/fail_draft7_defs_ref_target) add_jsonschema_test_unix(metaschema/fail_draft4_x_keyword_ref_target) add_jsonschema_test_unix(metaschema/pass_config_ignore) +add_jsonschema_test_unix(metaschema/pass_stdin_metaschema) # Test add_jsonschema_test_unix(test/fail_resolve_directory_non_schema) @@ -620,6 +633,8 @@ add_jsonschema_test_unix(lint/pass_lint_config_rule_other_directory) add_jsonschema_test_unix(lint/fail_lint_config_rule_other_directory) add_jsonschema_test_unix(lint/fail_lint_config_rule_extension_mismatch) add_jsonschema_test_unix(lint/pass_lint_config_ignore) +add_jsonschema_test_unix(lint/pass_stdin_lint) +add_jsonschema_test_unix(lint/pass_stdin_fix) # Install add_jsonschema_test_unix(install/fail_no_configuration) diff --git a/test/format/fail_stdin_check.sh b/test/format/fail_stdin_check.sh new file mode 100755 index 00000000..877222ca --- /dev/null +++ b/test/format/fail_stdin_check.sh @@ -0,0 +1,22 @@ +#!/bin/sh + +set -o errexit +set -o nounset + +TMP="$(mktemp -d)" +clean() { rm -rf "$TMP"; } +trap clean EXIT + +cat << 'EOF' | "$1" fmt --check - >"$TMP/output.txt" 2>&1 \ + && EXIT_CODE="$?" || EXIT_CODE="$?" +{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"string"} +EOF +test "$EXIT_CODE" = "2" || exit 1 + +cat << 'EOF' > "$TMP/expected.txt" +fail: (stdin) + +Run the `fmt` command without `--check/-c` to fix the formatting +EOF + +diff "$TMP/output.txt" "$TMP/expected.txt" diff --git a/test/format/fail_stdin_check_json.sh b/test/format/fail_stdin_check_json.sh new file mode 100755 index 00000000..6e69afa2 --- /dev/null +++ b/test/format/fail_stdin_check_json.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' | "$1" fmt --check - --json >"$TMP/output.json" 2>&1 \ + && EXIT_CODE="$?" || EXIT_CODE="$?" +{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"string"} +EOF +test "$EXIT_CODE" = "2" || exit 1 + +cat << 'EOF' > "$TMP/expected.json" +{ + "valid": false, + "errors": [ "(stdin)" ] +} +EOF + +diff "$TMP/output.json" "$TMP/expected.json" diff --git a/test/format/pass_stdin.sh b/test/format/pass_stdin.sh new file mode 100755 index 00000000..60f2b064 --- /dev/null +++ b/test/format/pass_stdin.sh @@ -0,0 +1,21 @@ +#!/bin/sh + +set -o errexit +set -o nounset + +TMP="$(mktemp -d)" +clean() { rm -rf "$TMP"; } +trap clean EXIT + +cat << 'EOF' | "$1" fmt - > "$TMP/output.json" +{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"string"} +EOF + +cat << 'EOF' > "$TMP/expected.json" +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "string" +} +EOF + +diff "$TMP/output.json" "$TMP/expected.json" diff --git a/test/format/pass_stdin_check.sh b/test/format/pass_stdin_check.sh new file mode 100755 index 00000000..203bedd5 --- /dev/null +++ b/test/format/pass_stdin_check.sh @@ -0,0 +1,20 @@ +#!/bin/sh + +set -o errexit +set -o nounset + +TMP="$(mktemp -d)" +clean() { rm -rf "$TMP"; } +trap clean EXIT + +cat << 'EOF' | "$1" fmt --check - >"$TMP/output.txt" 2>&1 +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "string" +} +EOF + +cat << 'EOF' > "$TMP/expected.txt" +EOF + +diff "$TMP/output.txt" "$TMP/expected.txt" diff --git a/test/lint/pass_stdin_fix.sh b/test/lint/pass_stdin_fix.sh new file mode 100755 index 00000000..6d7c9f45 --- /dev/null +++ b/test/lint/pass_stdin_fix.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' | "$1" lint --fix - > "$TMP/output.json" +{ + "$schema": "http://json-schema.org/draft-06/schema#", + "description": "Test schema", + "examples": [ "foo" ], + "type": "string", + "const": "foo", + "title": "I should not be moved up" +} +EOF + +cat << 'EOF' > "$TMP/expected.json" +{ + "$schema": "http://json-schema.org/draft-06/schema#", + "description": "Test schema", + "examples": [ "foo" ], + "const": "foo", + "title": "I should not be moved up" +} +EOF + +diff "$TMP/output.json" "$TMP/expected.json" diff --git a/test/lint/pass_stdin_lint.sh b/test/lint/pass_stdin_lint.sh new file mode 100755 index 00000000..89af1b14 --- /dev/null +++ b/test/lint/pass_stdin_lint.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' | "$1" lint - >"$TMP/output.txt" 2>&1 +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "Test", + "description": "Test description", + "examples": ["foo"], + "type": "string" +} +EOF + +cat << 'EOF' > "$TMP/expected.txt" +EOF + +diff "$TMP/output.txt" "$TMP/expected.txt" diff --git a/test/metaschema/pass_stdin_metaschema.sh b/test/metaschema/pass_stdin_metaschema.sh new file mode 100755 index 00000000..893d49e0 --- /dev/null +++ b/test/metaschema/pass_stdin_metaschema.sh @@ -0,0 +1,20 @@ +#!/bin/sh + +set -o errexit +set -o nounset + +TMP="$(mktemp -d)" +clean() { rm -rf "$TMP"; } +trap clean EXIT + +cat << 'EOF' | "$1" metaschema - >"$TMP/output.txt" 2>&1 +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "string" +} +EOF + +cat << 'EOF' > "$TMP/expected.txt" +EOF + +diff "$TMP/output.txt" "$TMP/expected.txt" diff --git a/test/validate/fail_stdin_instance.sh b/test/validate/fail_stdin_instance.sh new file mode 100755 index 00000000..a94861c1 --- /dev/null +++ b/test/validate/fail_stdin_instance.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": "https://json-schema.org/draft/2020-12/schema", + "type": "string" +} +EOF + +echo '123' | "$1" validate "$TMP/schema.json" - >"$TMP/output.txt" 2>&1 \ + && EXIT_CODE="$?" || EXIT_CODE="$?" +test "$EXIT_CODE" = "2" || exit 1 + +cat << EOF > "$TMP/expected.txt" +fail: $(pwd) +error: Schema validation failure + The value was expected to be of type string but it was of type integer + at instance location "" (line 1, column 1) + at evaluate path "/type" +EOF + +diff "$TMP/output.txt" "$TMP/expected.txt" diff --git a/test/validate/fail_stdin_instance_json.sh b/test/validate/fail_stdin_instance_json.sh new file mode 100755 index 00000000..6c71d122 --- /dev/null +++ b/test/validate/fail_stdin_instance_json.sh @@ -0,0 +1,42 @@ +#!/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 + +echo '123' | "$1" validate "$TMP/schema.json" - --json >"$TMP/stdout.json" 2>"$TMP/stderr.txt" \ + && EXIT_CODE="$?" || EXIT_CODE="$?" +test "$EXIT_CODE" = "2" || exit 1 + +cat << EOF > "$TMP/expected_stderr.txt" +$(pwd) +EOF + +diff "$TMP/stderr.txt" "$TMP/expected_stderr.txt" + +cat << EOF > "$TMP/expected.json" +{ + "valid": false, + "errors": [ + { + "keywordLocation": "/type", + "absoluteKeywordLocation": "file://$(realpath "$TMP")/schema.json#/type", + "instanceLocation": "", + "instancePosition": [ 1, 1, 1, 3 ], + "error": "The value was expected to be of type string but it was of type integer" + } + ] +} +EOF + +diff "$TMP/stdout.json" "$TMP/expected.json" diff --git a/test/validate/fail_stdin_multiple.sh b/test/validate/fail_stdin_multiple.sh new file mode 100755 index 00000000..9f67d335 --- /dev/null +++ b/test/validate/fail_stdin_multiple.sh @@ -0,0 +1,25 @@ +#!/bin/sh + +set -o errexit +set -o nounset + +TMP="$(mktemp -d)" +clean() { rm -rf "$TMP"; } +trap clean EXIT + +cat << 'EOF' > "$TMP/schema.json" +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "string" +} +EOF + +echo '"foo"' | "$1" validate "$TMP/schema.json" - - 2>"$TMP/stderr.txt" \ + && EXIT_CODE="$?" || EXIT_CODE="$?" +test "$EXIT_CODE" = "1" || exit 1 + +cat << 'EOF' > "$TMP/expected.txt" +error: Cannot read from standard input more than once +EOF + +diff "$TMP/stderr.txt" "$TMP/expected.txt" diff --git a/test/validate/fail_stdin_multiple_json.sh b/test/validate/fail_stdin_multiple_json.sh new file mode 100755 index 00000000..4cc81139 --- /dev/null +++ b/test/validate/fail_stdin_multiple_json.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" +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "string" +} +EOF + +echo '"foo"' | "$1" validate "$TMP/schema.json" - - --json >"$TMP/output.json" 2>&1 \ + && EXIT_CODE="$?" || EXIT_CODE="$?" +test "$EXIT_CODE" = "1" || exit 1 + +cat << 'EOF' > "$TMP/expected.json" +{ + "error": "Cannot read from standard input more than once" +} +EOF + +diff "$TMP/output.json" "$TMP/expected.json" diff --git a/test/validate/fail_stdin_schema.sh b/test/validate/fail_stdin_schema.sh new file mode 100755 index 00000000..d8437efd --- /dev/null +++ b/test/validate/fail_stdin_schema.sh @@ -0,0 +1,22 @@ +#!/bin/sh + +set -o errexit +set -o nounset + +TMP="$(mktemp -d)" +clean() { rm -rf "$TMP"; } +trap clean EXIT + +cat << 'EOF' > "$TMP/instance.json" +{ "foo": "bar" } +EOF + +"$1" validate - "$TMP/instance.json" - 2>"$TMP/stderr.txt" \ + && EXIT_CODE="$?" || EXIT_CODE="$?" +test "$EXIT_CODE" = "1" || exit 1 + +cat << 'EOF' > "$TMP/expected.txt" +error: Cannot read both schema and instance from standard input +EOF + +diff "$TMP/stderr.txt" "$TMP/expected.txt" diff --git a/test/validate/fail_stdin_schema_json.sh b/test/validate/fail_stdin_schema_json.sh new file mode 100755 index 00000000..4adf2904 --- /dev/null +++ b/test/validate/fail_stdin_schema_json.sh @@ -0,0 +1,24 @@ +#!/bin/sh + +set -o errexit +set -o nounset + +TMP="$(mktemp -d)" +clean() { rm -rf "$TMP"; } +trap clean EXIT + +cat << 'EOF' > "$TMP/instance.json" +{ "foo": "bar" } +EOF + +"$1" validate - "$TMP/instance.json" - --json >"$TMP/output.json" 2>&1 \ + && EXIT_CODE="$?" || EXIT_CODE="$?" +test "$EXIT_CODE" = "1" || exit 1 + +cat << 'EOF' > "$TMP/expected.json" +{ + "error": "Cannot read both schema and instance from standard input" +} +EOF + +diff "$TMP/output.json" "$TMP/expected.json" diff --git a/test/validate/pass_stdin_instance.sh b/test/validate/pass_stdin_instance.sh new file mode 100755 index 00000000..9d840ab1 --- /dev/null +++ b/test/validate/pass_stdin_instance.sh @@ -0,0 +1,22 @@ +#!/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 + +echo '"foo"' | "$1" validate "$TMP/schema.json" - >"$TMP/output.txt" 2>&1 + +cat << 'EOF' > "$TMP/expected.txt" +EOF + +diff "$TMP/output.txt" "$TMP/expected.txt" diff --git a/test/validate/pass_stdin_schema.sh b/test/validate/pass_stdin_schema.sh new file mode 100755 index 00000000..4b68b6a3 --- /dev/null +++ b/test/validate/pass_stdin_schema.sh @@ -0,0 +1,22 @@ +#!/bin/sh + +set -o errexit +set -o nounset + +TMP="$(mktemp -d)" +clean() { rm -rf "$TMP"; } +trap clean EXIT + +cat << 'EOF' > "$TMP/instance.json" +{ "foo": "bar" } +EOF + +cat << 'EOF' | "$1" validate - "$TMP/instance.json" +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "foo": { "type": "string" } + } +} +EOF