From d6705019ece8b6707065f46db92e3018905c13ec Mon Sep 17 00:00:00 2001 From: Vaibhav mittal Date: Tue, 17 Feb 2026 10:53:48 +0000 Subject: [PATCH 1/5] feat: implement stdin support for validate, lint, metaschema Signed-off-by: Vaibhav mittal --- src/command_validate.cc | 8 +++- src/input.h | 54 +++++++++++++++++++++++ test/CMakeLists.txt | 7 +++ test/lint/pass_stdin_lint.sh | 11 +++++ test/metaschema/pass_stdin_metaschema.sh | 8 ++++ test/validate/fail_stdin_instance.sh | 17 +++++++ test/validate/fail_stdin_multiple.sh | 14 ++++++ test/validate/fail_stdin_multiple_json.sh | 24 ++++++++++ test/validate/fail_stdin_schema.sh | 20 +++++++++ test/validate/pass_stdin_instance.sh | 13 ++++++ 10 files changed, 175 insertions(+), 1 deletion(-) create mode 100755 test/lint/pass_stdin_lint.sh create mode 100755 test/metaschema/pass_stdin_metaschema.sh create mode 100755 test/validate/fail_stdin_instance.sh create mode 100755 test/validate/fail_stdin_multiple.sh create mode 100755 test/validate/fail_stdin_multiple_json.sh create mode 100755 test/validate/fail_stdin_schema.sh create mode 100755 test/validate/pass_stdin_instance.sh diff --git a/src/command_validate.cc b/src/command_validate.cc index 6a997b9c..8159876b 100644 --- a/src/command_validate.cc +++ b/src/command_validate.cc @@ -139,6 +139,11 @@ auto sourcemeta::jsonschema::validate(const sourcemeta::core::Options &options) "jsonschema validate path/to/schema.json path/to/instance.json"}; } + // We explicitly check for duplicate stdin here because this command iterates + // over arguments manually and calls for_each_json incrementally, which + // prevents for_each_json from detecting global duplicates across iterations. + sourcemeta::jsonschema::check_duplicate_stdin(options); + const auto &schema_path{options.positional().at(0)}; if (std::filesystem::is_directory(schema_path)) { @@ -326,7 +331,8 @@ auto sourcemeta::jsonschema::validate(const sourcemeta::core::Options &options) 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") { diff --git a/src/input.h b/src/input.h index 3957e487..9001dd76 100644 --- a/src/input.h +++ b/src/input.h @@ -17,8 +17,10 @@ #include // std::uintptr_t #include // std::filesystem #include // std::ref, std::hash +#include // std::cin #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,62 @@ inline auto read_file(const std::filesystem::path &path) -> ParsedJSON { } } +// We buffer stdin because we need to attempt parsing as JSON first, +// and if that fails, rewind/reset to attempt parsing as YAML. +// Streaming directly from std::cin would prevent this retry logic. +inline auto read_from_stdin() -> ParsedJSON { + std::ostringstream buffer; + buffer << std::cin.rdbuf(); + const auto content{buffer.str()}; + + if (content.empty()) { + throw std::runtime_error("Standard input is empty"); + } + + // We are trying to determine if the input is JSON or YAML + try { + sourcemeta::core::PointerPositionTracker json_positions; + return {sourcemeta::core::parse_json(content, std::ref(json_positions)), + std::move(json_positions), false}; + } catch (const sourcemeta::core::JSONParseError &) { + sourcemeta::core::PointerPositionTracker yaml_positions; + return {sourcemeta::core::parse_yaml(content, std::ref(yaml_positions)), + std::move(yaml_positions), true}; + } +} + +inline auto check_duplicate_stdin(const sourcemeta::core::Options &options) + -> void { + std::size_t stdin_count{0}; + for (const auto &argument : options.positional()) { + if (argument == "-") { + stdin_count++; + } + } + + if (stdin_count > 1) { + throw std::runtime_error("Cannot read from standard input more than once"); + } +} + 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 == "-") { + // We treat stdin as a single file, and its "resolution base" is + // the current working directory, so relative references are resolved + // against where the command is being run + auto parsed{read_from_stdin()}; + const auto current_path{std::filesystem::current_path()}; + result.push_back({"", 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}) { @@ -343,6 +396,7 @@ inline auto for_each_json(const std::vector &arguments, inline auto for_each_json(const sourcemeta::core::Options &options) -> std::vector { + check_duplicate_stdin(options); return for_each_json(options.positional(), options); } diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 0c340c4a..57d36a02 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -217,6 +217,11 @@ 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_multiple) +add_jsonschema_test_unix(validate/fail_stdin_multiple_json) +add_jsonschema_test_unix(validate/fail_stdin_schema) # Metaschema add_jsonschema_test_unix(metaschema/pass_trace) @@ -253,6 +258,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 +626,7 @@ 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) # Install add_jsonschema_test_unix(install/fail_no_configuration) diff --git a/test/lint/pass_stdin_lint.sh b/test/lint/pass_stdin_lint.sh new file mode 100755 index 00000000..14e2caff --- /dev/null +++ b/test/lint/pass_stdin_lint.sh @@ -0,0 +1,11 @@ +#!/bin/sh +set -e +cat << 'EOF' | "$1" lint - +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "Test", + "description": "Test description", + "examples": ["foo"], + "type": "string" +} +EOF diff --git a/test/metaschema/pass_stdin_metaschema.sh b/test/metaschema/pass_stdin_metaschema.sh new file mode 100755 index 00000000..2dfe0e89 --- /dev/null +++ b/test/metaschema/pass_stdin_metaschema.sh @@ -0,0 +1,8 @@ +#!/bin/sh +set -e +cat << 'EOF' | "$1" metaschema - +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "string" +} +EOF diff --git a/test/validate/fail_stdin_instance.sh b/test/validate/fail_stdin_instance.sh new file mode 100755 index 00000000..838f2ba2 --- /dev/null +++ b/test/validate/fail_stdin_instance.sh @@ -0,0 +1,17 @@ +#!/bin/sh +set -e +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 +# Should fail validation (exit code 2) +if echo '123' | "$1" validate "$TMP/schema.json" -; then + echo "Validation should have failed" + exit 1 +fi diff --git a/test/validate/fail_stdin_multiple.sh b/test/validate/fail_stdin_multiple.sh new file mode 100755 index 00000000..5e6e7910 --- /dev/null +++ b/test/validate/fail_stdin_multiple.sh @@ -0,0 +1,14 @@ +#!/bin/sh +set -e +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 +# Should fail with duplicate stdin error +echo '"foo"' | "$1" validate "$TMP/schema.json" - - 2>&1 | grep "Cannot read from standard input more than once" diff --git a/test/validate/fail_stdin_multiple_json.sh b/test/validate/fail_stdin_multiple_json.sh new file mode 100755 index 00000000..929ced13 --- /dev/null +++ b/test/validate/fail_stdin_multiple_json.sh @@ -0,0 +1,24 @@ +#!/bin/sh +set -e +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 +# Should fail with duplicate stdin error in JSON format +if echo '"foo"' | "$1" validate "$TMP/schema.json" - - --json > "$TMP/output.json" 2>&1; then + echo "Validation succeeded unexpectedly" + exit 1 +fi + +# Check if output is valid JSON and contains error message +if ! grep -q "Cannot read from standard input more than once" "$TMP/output.json"; then + echo "JSON output does not contain expected error message" + cat "$TMP/output.json" + exit 1 +fi diff --git a/test/validate/fail_stdin_schema.sh b/test/validate/fail_stdin_schema.sh new file mode 100755 index 00000000..a4e242f0 --- /dev/null +++ b/test/validate/fail_stdin_schema.sh @@ -0,0 +1,20 @@ +#!/bin/sh +set -e +TMP="$(mktemp -d)" +clean() { rm -rf "$TMP"; } +trap clean EXIT + +echo '{}' > "$TMP/instance.json" + +cat << 'EOF' > "$TMP/schema_input.json" +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object" +} +EOF + +# Expected to fail reading schema '-' +if "$1" validate - "$TMP/instance.json" < "$TMP/schema_input.json" >/dev/null 2>&1; then + echo "Validation succeeded unexpectedly (schema from stdin not supported yet)" + exit 1 +fi diff --git a/test/validate/pass_stdin_instance.sh b/test/validate/pass_stdin_instance.sh new file mode 100755 index 00000000..f443d7ee --- /dev/null +++ b/test/validate/pass_stdin_instance.sh @@ -0,0 +1,13 @@ +#!/bin/sh +set -e +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" - From cb5440c04601530fc61cac675f4cb8fc8274ff0b Mon Sep 17 00:00:00 2001 From: Vaibhav mittal Date: Tue, 17 Feb 2026 13:05:49 +0000 Subject: [PATCH 2/5] feat: add standard input (-) support across CLI commands Signed-off-by: Vaibhav mittal --- src/command_fmt.cc | 24 ++++ src/command_lint.cc | 4 + src/command_validate.cc | 130 +++++++++++------- src/error.h | 9 ++ src/input.h | 61 +++----- test/CMakeLists.txt | 9 +- test/format/fail_stdin_check.sh | 24 ++++ test/format/pass_stdin.sh | 21 +++ test/lint/fail_stdin_fix.sh | 24 ++++ test/validate/fail_stdin_instance_json.sh | 25 ++++ test/validate/fail_stdin_multiple.sh | 12 +- test/validate/fail_stdin_multiple_json.sh | 22 +-- test/validate/fail_stdin_schema.sh | 23 ++-- .../fail_stdin_schema_and_instance.sh | 23 ++++ .../fail_stdin_schema_and_instance_json.sh | 25 ++++ test/validate/pass_stdin_schema.sh | 22 +++ 16 files changed, 344 insertions(+), 114 deletions(-) create mode 100755 test/format/fail_stdin_check.sh create mode 100755 test/format/pass_stdin.sh create mode 100755 test/lint/fail_stdin_fix.sh create mode 100755 test/validate/fail_stdin_instance_json.sh create mode 100755 test/validate/fail_stdin_schema_and_instance.sh create mode 100755 test/validate/fail_stdin_schema_and_instance_json.sh create mode 100755 test/validate/pass_stdin_schema.sh diff --git a/src/command_fmt.cc b/src/command_fmt.cc index 75f34ded..ddf41735 100644 --- a/src/command_fmt.cc +++ b/src/command_fmt.cc @@ -21,6 +21,30 @@ 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)) { + if (entry.from_stdin) { + if (options.contains("check")) { + throw StdinError{"The --check option does not support standard input"}; + } + + const auto configuration_path{find_configuration(entry.first)}; + const auto &configuration{ + read_configuration(options, configuration_path, entry.first)}; + const auto dialect{default_dialect(options, configuration)}; + const auto &custom_resolver{ + resolver(options, options.contains("http"), dialect, configuration)}; + + if (options.contains("keep-ordering")) { + sourcemeta::core::prettify(entry.second, std::cout, indentation); + } else { + auto copy = entry.second; + sourcemeta::core::format(copy, sourcemeta::core::schema_walker, + custom_resolver, dialect); + sourcemeta::core::prettify(copy, std::cout, indentation); + } + std::cout << "\n"; + continue; + } + if (entry.yaml) { throw YAMLInputError{"This command does not support YAML input files yet", entry.resolution_base}; diff --git a/src/command_lint.cc b/src/command_lint.cc index 8824ec44..94882698 100644 --- a/src/command_lint.cc +++ b/src/command_lint.cc @@ -325,6 +325,10 @@ auto sourcemeta::jsonschema::lint(const sourcemeta::core::Options &options) if (options.contains("fix")) { for (const auto &entry : for_each_json(options)) { + if (entry.from_stdin) { + throw StdinError{"The --fix option does not support standard input"}; + } + const auto configuration_path{find_configuration(entry.resolution_base)}; const auto &configuration{read_configuration(options, configuration_path, entry.resolution_base)}; diff --git a/src/command_validate.cc b/src/command_validate.cc index 8159876b..5139834c 100644 --- a/src/command_validate.cc +++ b/src/command_validate.cc @@ -139,28 +139,42 @@ auto sourcemeta::jsonschema::validate(const sourcemeta::core::Options &options) "jsonschema validate path/to/schema.json path/to/instance.json"}; } - // We explicitly check for duplicate stdin here because this command iterates - // over arguments manually and calls for_each_json incrementally, which - // prevents for_each_json from detecting global duplicates across iterations. - sourcemeta::jsonschema::check_duplicate_stdin(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{ @@ -182,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 { @@ -190,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()); } }()}; @@ -226,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()}; @@ -251,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); } } @@ -261,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()); } }()}; @@ -322,12 +349,14 @@ 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"}; } @@ -386,7 +415,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 { @@ -457,7 +487,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 9001dd76..849de0b6 100644 --- a/src/input.h +++ b/src/input.h @@ -12,7 +12,7 @@ #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 @@ -165,42 +165,13 @@ inline auto read_file(const std::filesystem::path &path) -> ParsedJSON { } } -// We buffer stdin because we need to attempt parsing as JSON first, -// and if that fails, rewind/reset to attempt parsing as YAML. -// Streaming directly from std::cin would prevent this retry logic. +// 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 { - std::ostringstream buffer; - buffer << std::cin.rdbuf(); - const auto content{buffer.str()}; - - if (content.empty()) { - throw std::runtime_error("Standard input is empty"); - } - - // We are trying to determine if the input is JSON or YAML - try { - sourcemeta::core::PointerPositionTracker json_positions; - return {sourcemeta::core::parse_json(content, std::ref(json_positions)), - std::move(json_positions), false}; - } catch (const sourcemeta::core::JSONParseError &) { - sourcemeta::core::PointerPositionTracker yaml_positions; - return {sourcemeta::core::parse_yaml(content, std::ref(yaml_positions)), - std::move(yaml_positions), true}; - } -} - -inline auto check_duplicate_stdin(const sourcemeta::core::Options &options) - -> void { - std::size_t stdin_count{0}; - for (const auto &argument : options.positional()) { - if (argument == "-") { - stdin_count++; - } - } - - if (stdin_count > 1) { - throw std::runtime_error("Cannot read from standard input more than once"); - } + sourcemeta::core::PointerPositionTracker positions; + return {sourcemeta::core::parse_json(std::cin, std::ref(positions)), + std::move(positions), false}; } inline auto @@ -215,7 +186,7 @@ handle_json_entry(const std::filesystem::path &entry_path, // against where the command is being run auto parsed{read_from_stdin()}; const auto current_path{std::filesystem::current_path()}; - result.push_back({"", current_path, std::move(parsed.document), + result.push_back({current_path, std::move(parsed.document), std::move(parsed.positions), 0, false, parsed.yaml, true}); return; @@ -325,7 +296,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 @@ -340,10 +312,20 @@ 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 { - auto blacklist{parse_ignore(options)}; + check_no_duplicate_stdin(arguments); + + const auto blacklist{parse_ignore(options)}; std::vector result; if (arguments.empty()) { @@ -396,7 +378,6 @@ inline auto for_each_json(const std::vector &arguments, inline auto for_each_json(const sourcemeta::core::Options &options) -> std::vector { - check_duplicate_stdin(options); return for_each_json(options.positional(), options); } diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 57d36a02..a470b58d 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -72,6 +72,8 @@ 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/fail_stdin_check) # Validate add_jsonschema_test_unix(validate/fail_instance_enoent) @@ -219,9 +221,13 @@ 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_and_instance) +add_jsonschema_test_unix(validate/fail_stdin_schema_and_instance_json) add_jsonschema_test_unix(validate/fail_stdin_multiple) add_jsonschema_test_unix(validate/fail_stdin_multiple_json) -add_jsonschema_test_unix(validate/fail_stdin_schema) # Metaschema add_jsonschema_test_unix(metaschema/pass_trace) @@ -627,6 +633,7 @@ 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/fail_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..7fcc2400 --- /dev/null +++ b/test/format/fail_stdin_check.sh @@ -0,0 +1,24 @@ +#!/bin/sh + +set -o errexit +set -o nounset + +TMP="$(mktemp -d)" +clean() { rm -rf "$TMP"; } +trap clean EXIT + +# fmt --check does not support stdin +cat << 'EOF' | "$1" fmt --check - 2>"$TMP/stderr.txt" \ + && EXIT_CODE="$?" || EXIT_CODE="$?" +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "string" +} +EOF +test "$EXIT_CODE" = "1" || exit 1 + +cat << 'EOF' > "$TMP/expected.txt" +error: The --check option does not support standard input +EOF + +diff "$TMP/stderr.txt" "$TMP/expected.txt" 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/lint/fail_stdin_fix.sh b/test/lint/fail_stdin_fix.sh new file mode 100755 index 00000000..42c14de6 --- /dev/null +++ b/test/lint/fail_stdin_fix.sh @@ -0,0 +1,24 @@ +#!/bin/sh + +set -o errexit +set -o nounset + +TMP="$(mktemp -d)" +clean() { rm -rf "$TMP"; } +trap clean EXIT + +# lint --fix does not support stdin +cat << 'EOF' | "$1" lint --fix - 2>"$TMP/stderr.txt" \ + && EXIT_CODE="$?" || EXIT_CODE="$?" +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "string" +} +EOF +test "$EXIT_CODE" = "1" || exit 1 + +cat << 'EOF' > "$TMP/expected.txt" +error: The --fix option does not support standard input +EOF + +diff "$TMP/stderr.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..5a6f4d2c --- /dev/null +++ b/test/validate/fail_stdin_instance_json.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 + +# Should fail validation via stdin (exit code 2) +echo '123' | "$1" validate "$TMP/schema.json" - --json > "$TMP/output.json" 2>&1 \ + && EXIT_CODE="$?" || EXIT_CODE="$?" +test "$EXIT_CODE" = "2" || exit 1 + +# Validate that JSON output has the correct structure +grep -q '"valid": false' "$TMP/output.json" || exit 1 +grep -q '"errors"' "$TMP/output.json" || exit 1 +grep -q '"keywordLocation"' "$TMP/output.json" || exit 1 diff --git a/test/validate/fail_stdin_multiple.sh b/test/validate/fail_stdin_multiple.sh index 5e6e7910..38fdaa18 100755 --- a/test/validate/fail_stdin_multiple.sh +++ b/test/validate/fail_stdin_multiple.sh @@ -10,5 +10,13 @@ cat << 'EOF' > "$TMP/schema.json" "type": "string" } EOF -# Should fail with duplicate stdin error -echo '"foo"' | "$1" validate "$TMP/schema.json" - - 2>&1 | grep "Cannot read from standard input more than once" + +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 index 929ced13..24128932 100755 --- a/test/validate/fail_stdin_multiple_json.sh +++ b/test/validate/fail_stdin_multiple_json.sh @@ -10,15 +10,15 @@ cat << 'EOF' > "$TMP/schema.json" "type": "string" } EOF -# Should fail with duplicate stdin error in JSON format -if echo '"foo"' | "$1" validate "$TMP/schema.json" - - --json > "$TMP/output.json" 2>&1; then - echo "Validation succeeded unexpectedly" - exit 1 -fi -# Check if output is valid JSON and contains error message -if ! grep -q "Cannot read from standard input more than once" "$TMP/output.json"; then - echo "JSON output does not contain expected error message" - cat "$TMP/output.json" - exit 1 -fi +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 index a4e242f0..d4e3b4f7 100755 --- a/test/validate/fail_stdin_schema.sh +++ b/test/validate/fail_stdin_schema.sh @@ -4,17 +4,18 @@ TMP="$(mktemp -d)" clean() { rm -rf "$TMP"; } trap clean EXIT -echo '{}' > "$TMP/instance.json" +cat << 'EOF' > "$TMP/instance.json" +{ "foo": "bar" } +EOF + +# Schema from stdin should now be supported. Test that it fails +# when both schema and instance are from stdin. +"$1" validate - "$TMP/instance.json" - 2>"$TMP/stderr.txt" \ + && EXIT_CODE="$?" || EXIT_CODE="$?" +test "$EXIT_CODE" = "1" || exit 1 -cat << 'EOF' > "$TMP/schema_input.json" -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "type": "object" -} +cat << 'EOF' > "$TMP/expected.txt" +error: Cannot read both schema and instance from standard input EOF -# Expected to fail reading schema '-' -if "$1" validate - "$TMP/instance.json" < "$TMP/schema_input.json" >/dev/null 2>&1; then - echo "Validation succeeded unexpectedly (schema from stdin not supported yet)" - exit 1 -fi +diff "$TMP/stderr.txt" "$TMP/expected.txt" diff --git a/test/validate/fail_stdin_schema_and_instance.sh b/test/validate/fail_stdin_schema_and_instance.sh new file mode 100755 index 00000000..03529fb9 --- /dev/null +++ b/test/validate/fail_stdin_schema_and_instance.sh @@ -0,0 +1,23 @@ +#!/bin/sh + +set -o errexit +set -o nounset + +TMP="$(mktemp -d)" +clean() { rm -rf "$TMP"; } +trap clean EXIT + +cat << 'EOF' > "$TMP/instance.json" +{ "foo": "bar" } +EOF + +# Schema from stdin + instance from stdin is not allowed +"$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_and_instance_json.sh b/test/validate/fail_stdin_schema_and_instance_json.sh new file mode 100755 index 00000000..0e7783bd --- /dev/null +++ b/test/validate/fail_stdin_schema_and_instance_json.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/instance.json" +{ "foo": "bar" } +EOF + +# Schema from stdin + instance from stdin JSON error output +"$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_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 From 3e437126c97f57dd1a0d29454985618093aeef62 Mon Sep 17 00:00:00 2001 From: Vaibhav mittal Date: Tue, 17 Feb 2026 19:23:44 +0000 Subject: [PATCH 3/5] fix: address stdin support PR review issues Signed-off-by: Vaibhav mittal --- src/command_fmt.cc | 52 +++++++++++++++++------- src/command_lint.cc | 11 ++++- src/command_validate.cc | 53 +++++++++++++++++++++++++ src/input.h | 3 +- test/CMakeLists.txt | 3 ++ test/format/fail_stdin_check_json.sh | 26 ++++++++++++ test/lint/fail_stdin_fix_json.sh | 26 ++++++++++++ test/validate/fail_stdin_instance.sh | 7 ++-- test/validate/fail_stdin_schema_json.sh | 25 ++++++++++++ 9 files changed, 184 insertions(+), 22 deletions(-) create mode 100755 test/format/fail_stdin_check_json.sh create mode 100755 test/lint/fail_stdin_fix_json.sh create mode 100755 test/validate/fail_stdin_schema_json.sh diff --git a/src/command_fmt.cc b/src/command_fmt.cc index ddf41735..50eaa30d 100644 --- a/src/command_fmt.cc +++ b/src/command_fmt.cc @@ -26,22 +26,44 @@ auto sourcemeta::jsonschema::fmt(const sourcemeta::core::Options &options) throw StdinError{"The --check option does not support standard input"}; } - const auto configuration_path{find_configuration(entry.first)}; - const auto &configuration{ - read_configuration(options, configuration_path, entry.first)}; - const auto dialect{default_dialect(options, configuration)}; - const auto &custom_resolver{ - resolver(options, options.contains("http"), dialect, configuration)}; - - if (options.contains("keep-ordering")) { - sourcemeta::core::prettify(entry.second, std::cout, indentation); - } else { - auto copy = entry.second; - sourcemeta::core::format(copy, sourcemeta::core::schema_walker, - custom_resolver, dialect); - sourcemeta::core::prettify(copy, std::cout, indentation); + try { + const auto configuration_path{find_configuration(entry.first)}; + const auto &configuration{ + read_configuration(options, configuration_path, entry.first)}; + const auto dialect{default_dialect(options, configuration)}; + const auto &custom_resolver{ + resolver(options, options.contains("http"), dialect, configuration)}; + + if (options.contains("keep-ordering")) { + sourcemeta::core::prettify(entry.second, std::cout, indentation); + } else { + auto copy = entry.second; + 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( + entry.resolution_base, error); + } catch (const sourcemeta::core::SchemaFrameError &error) { + throw FileError( + entry.resolution_base, error); + } catch (const sourcemeta::core::SchemaRelativeMetaschemaResolutionError + &error) { + throw FileError< + sourcemeta::core::SchemaRelativeMetaschemaResolutionError>( + entry.resolution_base, error); + } catch (const sourcemeta::core::SchemaResolutionError &error) { + throw FileError( + entry.resolution_base, error); + } catch (const sourcemeta::core::SchemaUnknownBaseDialectError &) { + throw FileError( + entry.resolution_base); + } catch (const sourcemeta::core::SchemaError &error) { + throw FileError(entry.resolution_base, + error.what()); } - std::cout << "\n"; continue; } diff --git a/src/command_lint.cc b/src/command_lint.cc index 94882698..00f8b9f4 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,11 +328,14 @@ 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) { if (entry.from_stdin) { throw StdinError{"The --fix option does not support standard input"}; } + } + 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)}; diff --git a/src/command_validate.cc b/src/command_validate.cc index 5139834c..c4013f66 100644 --- a/src/command_validate.cc +++ b/src/command_validate.cc @@ -342,6 +342,7 @@ auto sourcemeta::jsonschema::validate(const sourcemeta::core::Options &options) "The `--benchmark/-b` option is only allowed given a single instance"}; } +<<<<<<< HEAD for (const auto &instance_path_view : instance_arguments) { const std::filesystem::path instance_path{instance_path_view}; if (trace && instance_path.extension() == ".jsonl") { @@ -434,6 +435,58 @@ auto sourcemeta::jsonschema::validate(const sourcemeta::core::Options &options) if (entry.multidocument) { break; } +======= + for (const auto &entry : entries) { + std::ostringstream error; + sourcemeta::blaze::SimpleOutput output{entry.second}; + sourcemeta::blaze::TraceOutput trace_output{ + sourcemeta::core::schema_walker, custom_resolver, + sourcemeta::core::empty_weak_pointer, frame}; + bool subresult{true}; + if (benchmark) { + subresult = run_loop( + evaluator, schema_template, entry.second, entry.first, + entry.multidocument ? static_cast(entry.index + 1) + : static_cast(-1), + benchmark_loop); + if (!subresult) { + error << "error: Schema validation failure\n"; + result = false; + } + } else if (trace) { + subresult = evaluator.validate(schema_template, entry.second, + std::ref(trace_output)); + } else if (fast_mode) { + subresult = evaluator.validate(schema_template, entry.second); + } else if (!json_output) { + subresult = + evaluator.validate(schema_template, entry.second, std::ref(output)); + } + + if (benchmark) { + continue; + } else if (trace) { + print(trace_output, entry.positions, std::cout); + result = subresult; + } else if (json_output) { + if (!entry.multidocument && entries.size() > 1) { + std::cerr << entry.first << "\n"; + } + const auto suboutput{sourcemeta::blaze::standard( + evaluator, schema_template, entry.second, + fast_mode ? sourcemeta::blaze::StandardOutput::Flag + : sourcemeta::blaze::StandardOutput::Basic, + entry.positions)}; + assert(suboutput.is_object()); + assert(suboutput.defines("valid")); + assert(suboutput.at("valid").is_boolean()); + sourcemeta::core::prettify(suboutput, std::cout); + std::cout << "\n"; + if (!suboutput.at("valid").to_boolean()) { + result = false; + if (entry.multidocument) { + break; +>>>>>>> b624d7ae (fix: address stdin support PR review issues) } } } else { diff --git a/src/input.h b/src/input.h index 849de0b6..62f959d3 100644 --- a/src/input.h +++ b/src/input.h @@ -186,7 +186,8 @@ handle_json_entry(const std::filesystem::path &entry_path, // against where the command is being run auto parsed{read_from_stdin()}; const auto current_path{std::filesystem::current_path()}; - result.push_back({current_path, std::move(parsed.document), + result.push_back({current_path.string(), current_path, + std::move(parsed.document), std::move(parsed.positions), 0, false, parsed.yaml, true}); return; diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index a470b58d..a5c7a3f5 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -74,6 +74,7 @@ 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/fail_stdin_check) +add_jsonschema_test_unix(format/fail_stdin_check_json) # Validate add_jsonschema_test_unix(validate/fail_instance_enoent) @@ -224,6 +225,7 @@ 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_schema_and_instance) add_jsonschema_test_unix(validate/fail_stdin_schema_and_instance_json) add_jsonschema_test_unix(validate/fail_stdin_multiple) @@ -634,6 +636,7 @@ 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/fail_stdin_fix) +add_jsonschema_test_unix(lint/fail_stdin_fix_json) # Install add_jsonschema_test_unix(install/fail_no_configuration) diff --git a/test/format/fail_stdin_check_json.sh b/test/format/fail_stdin_check_json.sh new file mode 100755 index 00000000..a5f3b443 --- /dev/null +++ b/test/format/fail_stdin_check_json.sh @@ -0,0 +1,26 @@ +#!/bin/sh + +set -o errexit +set -o nounset + +TMP="$(mktemp -d)" +clean() { rm -rf "$TMP"; } +trap clean EXIT + +# fmt --check does not support stdin (JSON error output) +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" = "1" || exit 1 + +cat << 'EOF' > "$TMP/expected.json" +{ + "error": "The --check option does not support standard input" +} +EOF + +diff "$TMP/output.json" "$TMP/expected.json" diff --git a/test/lint/fail_stdin_fix_json.sh b/test/lint/fail_stdin_fix_json.sh new file mode 100755 index 00000000..42186698 --- /dev/null +++ b/test/lint/fail_stdin_fix_json.sh @@ -0,0 +1,26 @@ +#!/bin/sh + +set -o errexit +set -o nounset + +TMP="$(mktemp -d)" +clean() { rm -rf "$TMP"; } +trap clean EXIT + +# lint --fix does not support stdin (JSON error output) +cat << 'EOF' | "$1" lint --fix - --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" = "1" || exit 1 + +cat << 'EOF' > "$TMP/expected.json" +{ + "error": "The --fix option does not support standard input" +} +EOF + +diff "$TMP/output.json" "$TMP/expected.json" diff --git a/test/validate/fail_stdin_instance.sh b/test/validate/fail_stdin_instance.sh index 838f2ba2..707b6b7b 100755 --- a/test/validate/fail_stdin_instance.sh +++ b/test/validate/fail_stdin_instance.sh @@ -11,7 +11,6 @@ cat << 'EOF' > "$TMP/schema.json" } EOF # Should fail validation (exit code 2) -if echo '123' | "$1" validate "$TMP/schema.json" -; then - echo "Validation should have failed" - exit 1 -fi +echo '123' | "$1" validate "$TMP/schema.json" - \ + && EXIT_CODE="$?" || EXIT_CODE="$?" +test "$EXIT_CODE" = "2" || exit 1 diff --git a/test/validate/fail_stdin_schema_json.sh b/test/validate/fail_stdin_schema_json.sh new file mode 100755 index 00000000..0e7783bd --- /dev/null +++ b/test/validate/fail_stdin_schema_json.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/instance.json" +{ "foo": "bar" } +EOF + +# Schema from stdin + instance from stdin JSON error output +"$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" From 2bdb85824b35cf45623b714c4499c8d1dee704c5 Mon Sep 17 00:00:00 2001 From: Vaibhav mittal Date: Fri, 20 Feb 2026 08:47:35 +0000 Subject: [PATCH 4/5] fix: skip stdin when finding configurations to avoid path canonicalization issues Signed-off-by: Vaibhav mittal --- src/command_fmt.cc | 4 ++-- src/input.h | 12 ++++++++---- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/src/command_fmt.cc b/src/command_fmt.cc index 50eaa30d..deb80032 100644 --- a/src/command_fmt.cc +++ b/src/command_fmt.cc @@ -31,8 +31,8 @@ auto sourcemeta::jsonschema::fmt(const sourcemeta::core::Options &options) const auto &configuration{ read_configuration(options, configuration_path, entry.first)}; const auto dialect{default_dialect(options, configuration)}; - const auto &custom_resolver{ - resolver(options, options.contains("http"), dialect, configuration)}; + const auto &custom_resolver{resolver(options, options.contains("http"), + dialect, configuration)}; if (options.contains("keep-ordering")) { sourcemeta::core::prettify(entry.second, std::cout, indentation); diff --git a/src/input.h b/src/input.h index 62f959d3..3e278955 100644 --- a/src/input.h +++ b/src/input.h @@ -187,9 +187,8 @@ handle_json_entry(const std::filesystem::path &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}); + std::move(parsed.document), std::move(parsed.positions), + 0, false, parsed.yaml, true}); return; } @@ -326,7 +325,7 @@ inline auto for_each_json(const std::vector &arguments, -> std::vector { check_no_duplicate_stdin(arguments); - const auto blacklist{parse_ignore(options)}; + auto blacklist{parse_ignore(options)}; std::vector result; if (arguments.empty()) { @@ -350,6 +349,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{ From df9c10d268d31369f265b86fd779790560498dd1 Mon Sep 17 00:00:00 2001 From: Vaibhav mittal Date: Wed, 25 Feb 2026 12:56:28 +0000 Subject: [PATCH 5/5] fix(stdin): address review feedback, enable stdin for fmt --check and lint --fix, align tests with repository conventions Signed-off-by: Vaibhav mittal --- src/command_fmt.cc | 121 +++++++++++++----- src/command_lint.cc | 17 ++- src/command_validate.cc | 63 +-------- src/input.h | 5 +- test/CMakeLists.txt | 6 +- test/format/fail_stdin_check.sh | 16 +-- test/format/fail_stdin_check_json.sh | 11 +- test/format/pass_stdin_check.sh | 20 +++ test/lint/fail_stdin_fix.sh | 24 ---- test/lint/fail_stdin_fix_json.sh | 26 ---- test/lint/pass_stdin_fix.sh | 31 +++++ test/lint/pass_stdin_lint.sh | 16 ++- test/metaschema/pass_stdin_metaschema.sh | 16 ++- test/validate/fail_stdin_instance.sh | 19 ++- test/validate/fail_stdin_instance_json.sh | 29 ++++- test/validate/fail_stdin_multiple.sh | 5 +- test/validate/fail_stdin_multiple_json.sh | 7 +- test/validate/fail_stdin_schema.sh | 7 +- .../fail_stdin_schema_and_instance.sh | 23 ---- .../fail_stdin_schema_and_instance_json.sh | 25 ---- test/validate/fail_stdin_schema_json.sh | 1 - test/validate/pass_stdin_instance.sh | 13 +- 22 files changed, 260 insertions(+), 241 deletions(-) create mode 100755 test/format/pass_stdin_check.sh delete mode 100755 test/lint/fail_stdin_fix.sh delete mode 100755 test/lint/fail_stdin_fix_json.sh create mode 100755 test/lint/pass_stdin_fix.sh delete mode 100755 test/validate/fail_stdin_schema_and_instance.sh delete mode 100755 test/validate/fail_stdin_schema_and_instance_json.sh diff --git a/src/command_fmt.cc b/src/command_fmt.cc index deb80032..3dfa66f0 100644 --- a/src/command_fmt.cc +++ b/src/command_fmt.cc @@ -20,53 +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)) { - if (entry.from_stdin) { + + // 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")) { - throw StdinError{"The --check option does not support standard input"}; - } + 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)}; - try { - const auto configuration_path{find_configuration(entry.first)}; - const auto &configuration{ - read_configuration(options, configuration_path, entry.first)}; - const auto dialect{default_dialect(options, configuration)}; - const auto &custom_resolver{resolver(options, options.contains("http"), - dialect, configuration)}; + 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(entry.second, std::cout, indentation); + sourcemeta::core::prettify(document, std::cout, indentation); } else { - auto copy = entry.second; + 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( - entry.resolution_base, error); - } catch (const sourcemeta::core::SchemaFrameError &error) { - throw FileError( - entry.resolution_base, error); - } catch (const sourcemeta::core::SchemaRelativeMetaschemaResolutionError - &error) { - throw FileError< - sourcemeta::core::SchemaRelativeMetaschemaResolutionError>( - entry.resolution_base, error); - } catch (const sourcemeta::core::SchemaResolutionError &error) { - throw FileError( - entry.resolution_base, error); - } catch (const sourcemeta::core::SchemaUnknownBaseDialectError &) { - throw FileError( - entry.resolution_base); - } catch (const sourcemeta::core::SchemaError &error) { - throw FileError(entry.resolution_base, - error.what()); } - continue; + } 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}; @@ -141,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 00f8b9f4..d5767de1 100644 --- a/src/command_lint.cc +++ b/src/command_lint.cc @@ -329,11 +329,6 @@ auto sourcemeta::jsonschema::lint(const sourcemeta::core::Options &options) if (options.contains("fix")) { const auto entries = for_each_json(options); - for (const auto &entry : entries) { - if (entry.from_stdin) { - throw StdinError{"The --fix option does not support standard input"}; - } - } for (const auto &entry : entries) { const auto configuration_path{find_configuration(entry.resolution_base)}; @@ -450,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 c4013f66..7fc391b4 100644 --- a/src/command_validate.cc +++ b/src/command_validate.cc @@ -332,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"}; @@ -342,7 +344,6 @@ auto sourcemeta::jsonschema::validate(const sourcemeta::core::Options &options) "The `--benchmark/-b` option is only allowed given a single instance"}; } -<<<<<<< HEAD for (const auto &instance_path_view : instance_arguments) { const std::filesystem::path instance_path{instance_path_view}; if (trace && instance_path.extension() == ".jsonl") { @@ -416,8 +417,8 @@ auto sourcemeta::jsonschema::validate(const sourcemeta::core::Options &options) } LOG_VERBOSE(options) << "\n matches " - << sourcemeta::core::weakly_canonical( - schema_resolution_base).string() + << sourcemeta::core::weakly_canonical(schema_resolution_base) + .string() << "\n"; print_annotations(output, options, entry.positions, std::cerr); } else { @@ -435,58 +436,6 @@ auto sourcemeta::jsonschema::validate(const sourcemeta::core::Options &options) if (entry.multidocument) { break; } -======= - for (const auto &entry : entries) { - std::ostringstream error; - sourcemeta::blaze::SimpleOutput output{entry.second}; - sourcemeta::blaze::TraceOutput trace_output{ - sourcemeta::core::schema_walker, custom_resolver, - sourcemeta::core::empty_weak_pointer, frame}; - bool subresult{true}; - if (benchmark) { - subresult = run_loop( - evaluator, schema_template, entry.second, entry.first, - entry.multidocument ? static_cast(entry.index + 1) - : static_cast(-1), - benchmark_loop); - if (!subresult) { - error << "error: Schema validation failure\n"; - result = false; - } - } else if (trace) { - subresult = evaluator.validate(schema_template, entry.second, - std::ref(trace_output)); - } else if (fast_mode) { - subresult = evaluator.validate(schema_template, entry.second); - } else if (!json_output) { - subresult = - evaluator.validate(schema_template, entry.second, std::ref(output)); - } - - if (benchmark) { - continue; - } else if (trace) { - print(trace_output, entry.positions, std::cout); - result = subresult; - } else if (json_output) { - if (!entry.multidocument && entries.size() > 1) { - std::cerr << entry.first << "\n"; - } - const auto suboutput{sourcemeta::blaze::standard( - evaluator, schema_template, entry.second, - fast_mode ? sourcemeta::blaze::StandardOutput::Flag - : sourcemeta::blaze::StandardOutput::Basic, - entry.positions)}; - assert(suboutput.is_object()); - assert(suboutput.defines("valid")); - assert(suboutput.at("valid").is_boolean()); - sourcemeta::core::prettify(suboutput, std::cout); - std::cout << "\n"; - if (!suboutput.at("valid").to_boolean()) { - result = false; - if (entry.multidocument) { - break; ->>>>>>> b624d7ae (fix: address stdin support PR review issues) } } } else { @@ -540,8 +489,8 @@ auto sourcemeta::jsonschema::validate(const sourcemeta::core::Options &options) << "ok: " << sourcemeta::core::weakly_canonical(instance_path).string() << "\n matches " - << sourcemeta::core::weakly_canonical( - schema_resolution_base).string() + << sourcemeta::core::weakly_canonical(schema_resolution_base) + .string() << "\n"; print_annotations(output, options, tracker, std::cerr); } else { diff --git a/src/input.h b/src/input.h index 3e278955..bbbd8f64 100644 --- a/src/input.h +++ b/src/input.h @@ -18,8 +18,8 @@ #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 @@ -181,9 +181,6 @@ handle_json_entry(const std::filesystem::path &entry_path, std::vector &result, const sourcemeta::core::Options &options) -> void { if (entry_path == "-") { - // We treat stdin as a single file, and its "resolution base" is - // the current working directory, so relative references are resolved - // against where the command is being run auto parsed{read_from_stdin()}; const auto current_path{std::filesystem::current_path()}; result.push_back({current_path.string(), current_path, diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index a5c7a3f5..4d4c97c3 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -73,6 +73,7 @@ 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) @@ -226,8 +227,6 @@ 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_schema_and_instance) -add_jsonschema_test_unix(validate/fail_stdin_schema_and_instance_json) add_jsonschema_test_unix(validate/fail_stdin_multiple) add_jsonschema_test_unix(validate/fail_stdin_multiple_json) @@ -635,8 +634,7 @@ 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/fail_stdin_fix) -add_jsonschema_test_unix(lint/fail_stdin_fix_json) +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 index 7fcc2400..877222ca 100755 --- a/test/format/fail_stdin_check.sh +++ b/test/format/fail_stdin_check.sh @@ -7,18 +7,16 @@ TMP="$(mktemp -d)" clean() { rm -rf "$TMP"; } trap clean EXIT -# fmt --check does not support stdin -cat << 'EOF' | "$1" fmt --check - 2>"$TMP/stderr.txt" \ +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" -} +{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"string"} EOF -test "$EXIT_CODE" = "1" || exit 1 +test "$EXIT_CODE" = "2" || exit 1 cat << 'EOF' > "$TMP/expected.txt" -error: The --check option does not support standard input +fail: (stdin) + +Run the `fmt` command without `--check/-c` to fix the formatting EOF -diff "$TMP/stderr.txt" "$TMP/expected.txt" +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 index a5f3b443..6e69afa2 100755 --- a/test/format/fail_stdin_check_json.sh +++ b/test/format/fail_stdin_check_json.sh @@ -7,19 +7,16 @@ TMP="$(mktemp -d)" clean() { rm -rf "$TMP"; } trap clean EXIT -# fmt --check does not support stdin (JSON error output) 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" -} +{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"string"} EOF -test "$EXIT_CODE" = "1" || exit 1 +test "$EXIT_CODE" = "2" || exit 1 cat << 'EOF' > "$TMP/expected.json" { - "error": "The --check option does not support standard input" + "valid": false, + "errors": [ "(stdin)" ] } EOF 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/fail_stdin_fix.sh b/test/lint/fail_stdin_fix.sh deleted file mode 100755 index 42c14de6..00000000 --- a/test/lint/fail_stdin_fix.sh +++ /dev/null @@ -1,24 +0,0 @@ -#!/bin/sh - -set -o errexit -set -o nounset - -TMP="$(mktemp -d)" -clean() { rm -rf "$TMP"; } -trap clean EXIT - -# lint --fix does not support stdin -cat << 'EOF' | "$1" lint --fix - 2>"$TMP/stderr.txt" \ - && EXIT_CODE="$?" || EXIT_CODE="$?" -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "type": "string" -} -EOF -test "$EXIT_CODE" = "1" || exit 1 - -cat << 'EOF' > "$TMP/expected.txt" -error: The --fix option does not support standard input -EOF - -diff "$TMP/stderr.txt" "$TMP/expected.txt" diff --git a/test/lint/fail_stdin_fix_json.sh b/test/lint/fail_stdin_fix_json.sh deleted file mode 100755 index 42186698..00000000 --- a/test/lint/fail_stdin_fix_json.sh +++ /dev/null @@ -1,26 +0,0 @@ -#!/bin/sh - -set -o errexit -set -o nounset - -TMP="$(mktemp -d)" -clean() { rm -rf "$TMP"; } -trap clean EXIT - -# lint --fix does not support stdin (JSON error output) -cat << 'EOF' | "$1" lint --fix - --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" = "1" || exit 1 - -cat << 'EOF' > "$TMP/expected.json" -{ - "error": "The --fix option does not support standard input" -} -EOF - -diff "$TMP/output.json" "$TMP/expected.json" 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 index 14e2caff..89af1b14 100755 --- a/test/lint/pass_stdin_lint.sh +++ b/test/lint/pass_stdin_lint.sh @@ -1,6 +1,13 @@ #!/bin/sh -set -e -cat << 'EOF' | "$1" lint - + +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", @@ -9,3 +16,8 @@ cat << 'EOF' | "$1" lint - "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 index 2dfe0e89..893d49e0 100755 --- a/test/metaschema/pass_stdin_metaschema.sh +++ b/test/metaschema/pass_stdin_metaschema.sh @@ -1,8 +1,20 @@ #!/bin/sh -set -e -cat << 'EOF' | "$1" metaschema - + +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 index 707b6b7b..a94861c1 100755 --- a/test/validate/fail_stdin_instance.sh +++ b/test/validate/fail_stdin_instance.sh @@ -1,5 +1,8 @@ #!/bin/sh -set -e + +set -o errexit +set -o nounset + TMP="$(mktemp -d)" clean() { rm -rf "$TMP"; } trap clean EXIT @@ -10,7 +13,17 @@ cat << 'EOF' > "$TMP/schema.json" "type": "string" } EOF -# Should fail validation (exit code 2) -echo '123' | "$1" validate "$TMP/schema.json" - \ + +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 index 5a6f4d2c..6c71d122 100755 --- a/test/validate/fail_stdin_instance_json.sh +++ b/test/validate/fail_stdin_instance_json.sh @@ -14,12 +14,29 @@ cat << 'EOF' > "$TMP/schema.json" } EOF -# Should fail validation via stdin (exit code 2) -echo '123' | "$1" validate "$TMP/schema.json" - --json > "$TMP/output.json" 2>&1 \ +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 -# Validate that JSON output has the correct structure -grep -q '"valid": false' "$TMP/output.json" || exit 1 -grep -q '"errors"' "$TMP/output.json" || exit 1 -grep -q '"keywordLocation"' "$TMP/output.json" || 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 index 38fdaa18..9f67d335 100755 --- a/test/validate/fail_stdin_multiple.sh +++ b/test/validate/fail_stdin_multiple.sh @@ -1,5 +1,8 @@ #!/bin/sh -set -e + +set -o errexit +set -o nounset + TMP="$(mktemp -d)" clean() { rm -rf "$TMP"; } trap clean EXIT diff --git a/test/validate/fail_stdin_multiple_json.sh b/test/validate/fail_stdin_multiple_json.sh index 24128932..4cc81139 100755 --- a/test/validate/fail_stdin_multiple_json.sh +++ b/test/validate/fail_stdin_multiple_json.sh @@ -1,5 +1,8 @@ #!/bin/sh -set -e + +set -o errexit +set -o nounset + TMP="$(mktemp -d)" clean() { rm -rf "$TMP"; } trap clean EXIT @@ -11,7 +14,7 @@ cat << 'EOF' > "$TMP/schema.json" } EOF -echo '"foo"' | "$1" validate "$TMP/schema.json" - - --json > "$TMP/output.json" 2>&1 \ +echo '"foo"' | "$1" validate "$TMP/schema.json" - - --json >"$TMP/output.json" 2>&1 \ && EXIT_CODE="$?" || EXIT_CODE="$?" test "$EXIT_CODE" = "1" || exit 1 diff --git a/test/validate/fail_stdin_schema.sh b/test/validate/fail_stdin_schema.sh index d4e3b4f7..d8437efd 100755 --- a/test/validate/fail_stdin_schema.sh +++ b/test/validate/fail_stdin_schema.sh @@ -1,5 +1,8 @@ #!/bin/sh -set -e + +set -o errexit +set -o nounset + TMP="$(mktemp -d)" clean() { rm -rf "$TMP"; } trap clean EXIT @@ -8,8 +11,6 @@ cat << 'EOF' > "$TMP/instance.json" { "foo": "bar" } EOF -# Schema from stdin should now be supported. Test that it fails -# when both schema and instance are from stdin. "$1" validate - "$TMP/instance.json" - 2>"$TMP/stderr.txt" \ && EXIT_CODE="$?" || EXIT_CODE="$?" test "$EXIT_CODE" = "1" || exit 1 diff --git a/test/validate/fail_stdin_schema_and_instance.sh b/test/validate/fail_stdin_schema_and_instance.sh deleted file mode 100755 index 03529fb9..00000000 --- a/test/validate/fail_stdin_schema_and_instance.sh +++ /dev/null @@ -1,23 +0,0 @@ -#!/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 - -# Schema from stdin + instance from stdin is not allowed -"$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_and_instance_json.sh b/test/validate/fail_stdin_schema_and_instance_json.sh deleted file mode 100755 index 0e7783bd..00000000 --- a/test/validate/fail_stdin_schema_and_instance_json.sh +++ /dev/null @@ -1,25 +0,0 @@ -#!/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 - -# Schema from stdin + instance from stdin JSON error output -"$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/fail_stdin_schema_json.sh b/test/validate/fail_stdin_schema_json.sh index 0e7783bd..4adf2904 100755 --- a/test/validate/fail_stdin_schema_json.sh +++ b/test/validate/fail_stdin_schema_json.sh @@ -11,7 +11,6 @@ cat << 'EOF' > "$TMP/instance.json" { "foo": "bar" } EOF -# Schema from stdin + instance from stdin JSON error output "$1" validate - "$TMP/instance.json" - --json >"$TMP/output.json" 2>&1 \ && EXIT_CODE="$?" || EXIT_CODE="$?" test "$EXIT_CODE" = "1" || exit 1 diff --git a/test/validate/pass_stdin_instance.sh b/test/validate/pass_stdin_instance.sh index f443d7ee..9d840ab1 100755 --- a/test/validate/pass_stdin_instance.sh +++ b/test/validate/pass_stdin_instance.sh @@ -1,5 +1,8 @@ #!/bin/sh -set -e + +set -o errexit +set -o nounset + TMP="$(mktemp -d)" clean() { rm -rf "$TMP"; } trap clean EXIT @@ -10,4 +13,10 @@ cat << 'EOF' > "$TMP/schema.json" "type": "string" } EOF -echo '"foo"' | "$1" validate "$TMP/schema.json" - + +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"