From 8d9746fcc761fc21f9241fa1e1193bdd9cb5b223 Mon Sep 17 00:00:00 2001 From: Vaibhav mittal Date: Thu, 15 Jan 2026 09:26:46 +0000 Subject: [PATCH 1/7] Feat: support reading instances from stdin for validate and lint Signed-off-by: Vaibhav mittal --- src/command_validate.cc | 6 +++++- src/input.h | 29 ++++++++++++++++++++++++- test/validate/pass_stdin.sh | 43 +++++++++++++++++++++++++++++++++++++ 3 files changed, 76 insertions(+), 2 deletions(-) create mode 100755 test/validate/pass_stdin.sh diff --git a/src/command_validate.cc b/src/command_validate.cc index 580eeba0..5e1f8d58 100644 --- a/src/command_validate.cc +++ b/src/command_validate.cc @@ -141,6 +141,10 @@ auto sourcemeta::jsonschema::validate(const sourcemeta::core::Options &options) const auto &schema_path{options.positional().at(0)}; + if (schema_path == "-") { + throw std::runtime_error{"Reading schema from stdin is not supported"}; + } + if (std::filesystem::is_directory(schema_path)) { throw std::filesystem::filesystem_error{ "The input was supposed to be a file but it is a directory", @@ -320,7 +324,7 @@ auto sourcemeta::jsonschema::validate(const sourcemeta::core::Options &options) throw std::runtime_error{"The `--benchmark/-b` option is only allowed " "given a single instance"}; } - if (std::filesystem::is_directory(instance_path) || + if (instance_path == "-" || 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 3896e5ae..4c8fcb84 100644 --- a/src/input.h +++ b/src/input.h @@ -143,12 +143,40 @@ inline auto read_file(const std::filesystem::path &path) -> ParsedJSON { } } +inline auto read_from_stdin() -> ParsedJSON { + std::string buffer; + { + std::ostringstream ss; + ss << std::cin.rdbuf(); + buffer = ss.str(); + } + + sourcemeta::core::PointerPositionTracker positions; + try { + return {sourcemeta::core::parse_json(buffer, std::ref(positions)), + std::move(positions), false}; + } catch (const sourcemeta::core::JSONParseError &) { + sourcemeta::core::PointerPositionTracker yaml_positions; + std::istringstream yaml_stream(buffer); + return {sourcemeta::core::parse_yaml(yaml_stream, std::ref(yaml_positions)), + std::move(yaml_positions), true}; + } +} + 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()}; + std::filesystem::path virtual_path{std::filesystem::current_path()}; + result.push_back({std::move(virtual_path), std::move(parsed.document), + std::move(parsed.positions), 0, false, parsed.yaml}); + return; + } + if (std::filesystem::is_directory(entry_path)) { for (auto const &entry : std::filesystem::recursive_directory_iterator{entry_path}) { @@ -261,7 +289,6 @@ handle_json_entry(const std::filesystem::path &entry_path, } } } - } // namespace inline auto for_each_json(const std::vector &arguments, diff --git a/test/validate/pass_stdin.sh b/test/validate/pass_stdin.sh new file mode 100755 index 00000000..1ff7f26c --- /dev/null +++ b/test/validate/pass_stdin.sh @@ -0,0 +1,43 @@ +#!/bin/bash + +# Rules: +# 1. Instance from stdin is allowed: validate schema.json - +# 2. Schema from stdin is FORBIDDEN: validate - instance.json +# 3. YAML from stdin is allowed (internal fallback). + +set -o errexit +set -o nounset + +TMP="$(mktemp -d)" +clean() { rm -rf "$TMP"; } +trap clean EXIT + +cat << 'EOF' > "$TMP/schema.json" +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "foo": { "type": "string" } + } +} +EOF + +echo '{"foo": "bar"}' | "$1" validate "$TMP/schema.json" - +echo 'foo: bar' | "$1" validate "$TMP/schema.json" - +set +o errexit +OUTPUT=$(cat "$TMP/schema.json" | "$1" validate - "$TMP/schema.json" 2>&1) +EXIT_CODE=$? +set -o errexit + +if [ "$EXIT_CODE" -eq 0 ]; then + echo "FAIL: Expected failure when passing schema as stdin, but got success" + exit 1 +fi + +if ! echo "$OUTPUT" | grep -q "Reading schema from stdin is not supported"; then + echo "FAIL: Expected specific error message 'Reading schema from stdin is not supported', got:" + echo "$OUTPUT" + exit 1 +fi + +echo "PASS:stdin tests passed." From 720f5df4661363e9193f5011fe7404d60cea5f31 Mon Sep 17 00:00:00 2001 From: Vaibhav mittal Date: Thu, 15 Jan 2026 10:05:30 +0000 Subject: [PATCH 2/7] test: fix shellcheck SC2002 in stdin validation test Signed-off-by: Vaibhav mittal --- test/validate/pass_stdin.sh | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/test/validate/pass_stdin.sh b/test/validate/pass_stdin.sh index 1ff7f26c..b9676824 100755 --- a/test/validate/pass_stdin.sh +++ b/test/validate/pass_stdin.sh @@ -1,10 +1,5 @@ #!/bin/bash -# Rules: -# 1. Instance from stdin is allowed: validate schema.json - -# 2. Schema from stdin is FORBIDDEN: validate - instance.json -# 3. YAML from stdin is allowed (internal fallback). - set -o errexit set -o nounset @@ -25,7 +20,7 @@ EOF echo '{"foo": "bar"}' | "$1" validate "$TMP/schema.json" - echo 'foo: bar' | "$1" validate "$TMP/schema.json" - set +o errexit -OUTPUT=$(cat "$TMP/schema.json" | "$1" validate - "$TMP/schema.json" 2>&1) +OUTPUT=$("$1" validate - "$TMP/schema.json" < "$TMP/schema.json" 2>&1) EXIT_CODE=$? set -o errexit @@ -40,4 +35,4 @@ if ! echo "$OUTPUT" | grep -q "Reading schema from stdin is not supported"; then exit 1 fi -echo "PASS:stdin tests passed." +echo "PASS: stdin tests passed." From ebc4ea17a9b23752d471facd381c694a9ebd314c Mon Sep 17 00:00:00 2001 From: Vaibhav mittal Date: Thu, 15 Jan 2026 10:13:09 +0000 Subject: [PATCH 3/7] test: fix shellcheck SC2002 in stdin validation test-2 Signed-off-by: Vaibhav mittal --- test/validate/pass_stdin.sh | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/test/validate/pass_stdin.sh b/test/validate/pass_stdin.sh index b9676824..801b2710 100755 --- a/test/validate/pass_stdin.sh +++ b/test/validate/pass_stdin.sh @@ -20,7 +20,8 @@ EOF echo '{"foo": "bar"}' | "$1" validate "$TMP/schema.json" - echo 'foo: bar' | "$1" validate "$TMP/schema.json" - set +o errexit -OUTPUT=$("$1" validate - "$TMP/schema.json" < "$TMP/schema.json" 2>&1) +SCHEMA_CONTENT=$(cat "$TMP/schema.json") +OUTPUT=$(echo "$SCHEMA_CONTENT" | "$1" validate - "$TMP/schema.json" 2>&1) EXIT_CODE=$? set -o errexit @@ -35,4 +36,4 @@ if ! echo "$OUTPUT" | grep -q "Reading schema from stdin is not supported"; then exit 1 fi -echo "PASS: stdin tests passed." +echo "PASS: stdin tests passed." \ No newline at end of file From 44541cbfc0101bd4c5eaf74dc7360fcdf95ef6e3 Mon Sep 17 00:00:00 2001 From: Vaibhav mittal Date: Tue, 20 Jan 2026 02:29:17 +0000 Subject: [PATCH 4/7] feat:centralize stdin handling and add resolution context Signed-off-by: Vaibhav mittal --- src/command_lint.cc | 8 ++++++-- src/command_validate.cc | 4 ---- src/input.h | 40 +++++++++++++++++++++++++++---------- test/validate/pass_stdin.sh | 29 ++++++++++++++++++++------- 4 files changed, 58 insertions(+), 23 deletions(-) diff --git a/src/command_lint.cc b/src/command_lint.cc index 56390a23..24c313d6 100644 --- a/src/command_lint.cc +++ b/src/command_lint.cc @@ -239,7 +239,9 @@ auto sourcemeta::jsonschema::lint(const sourcemeta::core::Options &options) copy, sourcemeta::core::schema_walker, custom_resolver, get_lint_callback(errors_array, entry, output_json, true, printed_progress), - dialect, sourcemeta::jsonschema::default_id(entry.first), + dialect, + sourcemeta::core::URI::from_path(entry.resolution_base) + .recompose(), EXCLUDE_KEYWORD); if (printed_progress) { std::cerr << "\n"; @@ -368,7 +370,9 @@ auto sourcemeta::jsonschema::lint(const sourcemeta::core::Options &options) custom_resolver, get_lint_callback(errors_array, entry, output_json, false, printed_progress), - dialect, sourcemeta::jsonschema::default_id(entry.first), + dialect, + sourcemeta::core::URI::from_path(entry.resolution_base) + .recompose(), EXCLUDE_KEYWORD); scores.emplace_back(subresult.second); if (subresult.first) { diff --git a/src/command_validate.cc b/src/command_validate.cc index 5e1f8d58..b92f72c0 100644 --- a/src/command_validate.cc +++ b/src/command_validate.cc @@ -141,10 +141,6 @@ auto sourcemeta::jsonschema::validate(const sourcemeta::core::Options &options) const auto &schema_path{options.positional().at(0)}; - if (schema_path == "-") { - throw std::runtime_error{"Reading schema from stdin is not supported"}; - } - if (std::filesystem::is_directory(schema_path)) { throw std::filesystem::filesystem_error{ "The input was supposed to be a file but it is a directory", diff --git a/src/input.h b/src/input.h index 4c8fcb84..1cdb2e4a 100644 --- a/src/input.h +++ b/src/input.h @@ -27,6 +27,7 @@ namespace sourcemeta::jsonschema { struct InputJSON { std::filesystem::path first; + std::filesystem::path resolution_base; sourcemeta::core::JSON second; sourcemeta::core::PointerPositionTracker positions; std::size_t index{0}; @@ -171,9 +172,11 @@ handle_json_entry(const std::filesystem::path &entry_path, const sourcemeta::core::Options &options) -> void { if (entry_path == "-") { auto parsed{read_from_stdin()}; - std::filesystem::path virtual_path{std::filesystem::current_path()}; - result.push_back({std::move(virtual_path), std::move(parsed.document), - std::move(parsed.positions), 0, false, parsed.yaml}); + std::filesystem::path display_path{""}; + std::filesystem::path resolution_path{std::filesystem::current_path()}; + result.push_back({std::move(display_path), std::move(resolution_path), + std::move(parsed.document), std::move(parsed.positions), + 0, false, parsed.yaml}); return; } @@ -199,8 +202,10 @@ handle_json_entry(const std::filesystem::path &entry_path, // TODO: Print a verbose message for what is getting parsed auto parsed{read_file(canonical)}; - result.push_back({std::move(canonical), std::move(parsed.document), - std::move(parsed.positions), 0, false, parsed.yaml}); + auto canonical_copy{canonical}; + result.push_back({std::move(canonical), std::move(canonical_copy), + std::move(parsed.document), std::move(parsed.positions), + 0, false, parsed.yaml}); } } } else { @@ -219,7 +224,7 @@ handle_json_entry(const std::filesystem::path &entry_path, // TODO: Get real positions for JSONL sourcemeta::core::PointerPositionTracker positions; result.push_back( - {canonical, document, std::move(positions), index, true}); + {canonical, canonical, document, std::move(positions), index, true}); index += 1; } } catch (const sourcemeta::core::JSONParseError &error) { @@ -268,13 +273,15 @@ handle_json_entry(const std::filesystem::path &entry_path, << canonical.string() << "\n"; std::size_t index{0}; for (auto &entry : documents) { - result.push_back({canonical, std::move(entry.first), + result.push_back({canonical, canonical, std::move(entry.first), std::move(entry.second), index, true, true}); index += 1; } } else if (documents.size() == 1) { + auto canonical_copy{canonical}; result.push_back( - {std::move(canonical), std::move(documents.front().first), + {std::move(canonical), std::move(canonical_copy), + std::move(documents.front().first), std::move(documents.front().second), 0, false, true}); } } else { @@ -283,8 +290,10 @@ handle_json_entry(const std::filesystem::path &entry_path, } // TODO: Print a verbose message for what is getting parsed auto parsed{read_file(canonical)}; - result.push_back({std::move(canonical), std::move(parsed.document), - std::move(parsed.positions), 0, false, parsed.yaml}); + auto canonical_copy{canonical}; + result.push_back({std::move(canonical), std::move(canonical_copy), + std::move(parsed.document), std::move(parsed.positions), + 0, false, parsed.yaml}); } } } @@ -294,6 +303,17 @@ handle_json_entry(const std::filesystem::path &entry_path, inline auto for_each_json(const std::vector &arguments, const sourcemeta::core::Options &options) -> std::vector { + std::size_t stdin_count{0}; + for (const auto &arg : arguments) { + if (arg == "-") { + stdin_count++; + } + } + if (stdin_count > 1) { + throw std::runtime_error{ + "Standard input (-) can only be specified once"}; + } + const auto blacklist{parse_ignore(options)}; std::vector result; diff --git a/test/validate/pass_stdin.sh b/test/validate/pass_stdin.sh index 801b2710..bbbacfcd 100755 --- a/test/validate/pass_stdin.sh +++ b/test/validate/pass_stdin.sh @@ -18,22 +18,37 @@ cat << 'EOF' > "$TMP/schema.json" EOF echo '{"foo": "bar"}' | "$1" validate "$TMP/schema.json" - + echo 'foo: bar' | "$1" validate "$TMP/schema.json" - + set +o errexit -SCHEMA_CONTENT=$(cat "$TMP/schema.json") -OUTPUT=$(echo "$SCHEMA_CONTENT" | "$1" validate - "$TMP/schema.json" 2>&1) +OUTPUT=$(echo '{"foo": 123}' | "$1" validate "$TMP/schema.json" - 2>&1) EXIT_CODE=$? set -o errexit if [ "$EXIT_CODE" -eq 0 ]; then - echo "FAIL: Expected failure when passing schema as stdin, but got success" + echo "FAIL: Expected validation failure for type mismatch" exit 1 fi -if ! echo "$OUTPUT" | grep -q "Reading schema from stdin is not supported"; then - echo "FAIL: Expected specific error message 'Reading schema from stdin is not supported', got:" - echo "$OUTPUT" +set +o errexit +OUTPUT=$(cat "$TMP/schema.json" | "$1" validate - "$TMP/schema.json" 2>&1) +EXIT_CODE=$? +set -o errexit + +if [ "$EXIT_CODE" -eq 0 ]; then + echo "FAIL: Expected failure when passing schema as stdin" + exit 1 +fi + +set +o errexit +OUTPUT=$(echo '{}' | "$1" validate "$TMP/schema.json" - - 2>&1) +EXIT_CODE=$? +set -o errexit + +if [ "$EXIT_CODE" -eq 0 ]; then + echo "FAIL: Expected failure for multiple stdin arguments" exit 1 fi -echo "PASS: stdin tests passed." \ No newline at end of file +echo "PASS: All stdin tests passed." From 2e434c59d1a954ce5e45c7024b9e63dc0cfa2e90 Mon Sep 17 00:00:00 2001 From: Vaibhav mittal Date: Tue, 20 Jan 2026 02:58:44 +0000 Subject: [PATCH 5/7] test: cover stdin edge cases and fix shellcheck warnings Signed-off-by: Vaibhav mittal --- test/validate/pass_stdin.sh | 32 +++++++++++++++++--------------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/test/validate/pass_stdin.sh b/test/validate/pass_stdin.sh index bbbacfcd..9ab48ea0 100755 --- a/test/validate/pass_stdin.sh +++ b/test/validate/pass_stdin.sh @@ -22,33 +22,35 @@ echo '{"foo": "bar"}' | "$1" validate "$TMP/schema.json" - echo 'foo: bar' | "$1" validate "$TMP/schema.json" - set +o errexit -OUTPUT=$(echo '{"foo": 123}' | "$1" validate "$TMP/schema.json" - 2>&1) +OUTPUT=$("$1" validate - "$TMP/schema.json" < "$TMP/schema.json" 2>&1) EXIT_CODE=$? set -o errexit +: "$OUTPUT" + if [ "$EXIT_CODE" -eq 0 ]; then - echo "FAIL: Expected validation failure for type mismatch" - exit 1 + echo "FAIL: Expected failure when passing schema via stdin" + echo "$OUTPUT" + exit 1 fi -set +o errexit -OUTPUT=$(cat "$TMP/schema.json" | "$1" validate - "$TMP/schema.json" 2>&1) -EXIT_CODE=$? -set -o errexit - -if [ "$EXIT_CODE" -eq 0 ]; then - echo "FAIL: Expected failure when passing schema as stdin" - exit 1 +if ! echo "$OUTPUT" | grep -q "Reading schema from stdin is not supported"; then + echo "FAIL: Expected schema-from-stdin error message" + echo "$OUTPUT" + exit 1 fi set +o errexit -OUTPUT=$(echo '{}' | "$1" validate "$TMP/schema.json" - - 2>&1) +OUTPUT=$("$1" validate - - < "$TMP/schema.json" 2>&1) EXIT_CODE=$? set -o errexit +: "$OUTPUT" + if [ "$EXIT_CODE" -eq 0 ]; then - echo "FAIL: Expected failure for multiple stdin arguments" - exit 1 + echo "FAIL: Expected failure when passing multiple stdin inputs" + echo "$OUTPUT" + exit 1 fi -echo "PASS: All stdin tests passed." +echo "PASS: stdin validation tests passed." From 77066a9ac64a3da529cc9869e0dc64d2175951eb Mon Sep 17 00:00:00 2001 From: Vaibhav mittal Date: Tue, 20 Jan 2026 03:06:01 +0000 Subject: [PATCH 6/7] test: cover stdin edge cases and fix shellcheck warnings-2 Signed-off-by: Vaibhav mittal --- test/validate/pass_stdin.sh | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/test/validate/pass_stdin.sh b/test/validate/pass_stdin.sh index 9ab48ea0..045fc355 100755 --- a/test/validate/pass_stdin.sh +++ b/test/validate/pass_stdin.sh @@ -22,7 +22,10 @@ echo '{"foo": "bar"}' | "$1" validate "$TMP/schema.json" - echo 'foo: bar' | "$1" validate "$TMP/schema.json" - set +o errexit -OUTPUT=$("$1" validate - "$TMP/schema.json" < "$TMP/schema.json" 2>&1) +cat "$TMP/schema.json" > "$TMP/schema_stdin.json" + +OUTPUT=$("$1" validate - "$TMP/schema.json" < "$TMP/schema_stdin.json" 2>&1) + EXIT_CODE=$? set -o errexit From 16c70bcb51bd76969e306a9146980b4675340f9e Mon Sep 17 00:00:00 2001 From: Vaibhav mittal Date: Fri, 6 Feb 2026 11:19:30 +0000 Subject: [PATCH 7/7] feat(cli): support stdin for instances and formalize input resolution Signed-off-by: Vaibhav mittal --- src/command_fmt.cc | 44 +++-- src/command_lint.cc | 68 ++++---- src/command_metaschema.cc | 26 ++- src/command_test.cc | 37 ++--- src/command_validate.cc | 238 ++++++++++----------------- src/input.h | 90 +++++----- src/resolver.h | 13 +- src/utils.h | 8 + test/CMakeLists.txt | 3 + test/validate/fail_stdin_multiple.sh | 25 +++ test/validate/fail_stdin_schema.sh | 22 +++ test/validate/pass_stdin.sh | 59 ------- test/validate/pass_stdin_instance.sh | 20 +++ 13 files changed, 304 insertions(+), 349 deletions(-) create mode 100755 test/validate/fail_stdin_multiple.sh create mode 100755 test/validate/fail_stdin_schema.sh delete mode 100755 test/validate/pass_stdin.sh create mode 100755 test/validate/pass_stdin_instance.sh diff --git a/src/command_fmt.cc b/src/command_fmt.cc index f2c2fa42..f5cbc0cd 100644 --- a/src/command_fmt.cc +++ b/src/command_fmt.cc @@ -21,21 +21,26 @@ 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) { + throw std::runtime_error{ + "This command does not support reading from standard input"}; + } + if (entry.yaml) { throw YAMLInputError{"This command does not support YAML input files yet", - entry.first}; + entry.resolution_base}; } if (options.contains("check")) { - LOG_VERBOSE(options) << "Checking: " << entry.first.string() << "\n"; + LOG_VERBOSE(options) << "Checking: " << entry.first << "\n"; } else { - LOG_VERBOSE(options) << "Formatting: " << entry.first.string() << "\n"; + LOG_VERBOSE(options) << "Formatting: " << entry.first << "\n"; } try { - const auto configuration_path{find_configuration(entry.first)}; - const auto &configuration{ - read_configuration(options, configuration_path, entry.first)}; + const auto configuration_path{find_configuration(entry.resolution_base)}; + const auto &configuration{read_configuration(options, configuration_path, + entry.resolution_base)}; const auto dialect{default_dialect(options, configuration)}; const auto &custom_resolver{ resolver(options, options.contains("http"), dialect, configuration)}; @@ -51,43 +56,46 @@ auto sourcemeta::jsonschema::fmt(const sourcemeta::core::Options &options) } expected << "\n"; - std::ifstream current_stream{entry.first}; + std::ifstream current_stream{entry.resolution_base}; std::ostringstream current; current << current_stream.rdbuf(); if (options.contains("check")) { if (current.str() == expected.str()) { - LOG_VERBOSE(options) << "ok: " << entry.first.string() << "\n"; + LOG_VERBOSE(options) << "ok: " << entry.first << "\n"; } else if (output_json) { - failed_files.push_back(entry.first.string()); + failed_files.push_back(entry.first); result = false; } else { - std::cerr << "fail: " << entry.first.string() << "\n"; + std::cerr << "fail: " << entry.first << "\n"; result = false; } } else { if (current.str() != expected.str()) { - std::ofstream output{entry.first}; + std::ofstream output{entry.resolution_base}; output << expected.str(); } } } catch (const sourcemeta::core::SchemaKeywordError &error) { - throw FileError(entry.first, error); + throw FileError( + entry.resolution_base, error); } catch (const sourcemeta::core::SchemaFrameError &error) { - throw FileError(entry.first, error); + throw FileError(entry.resolution_base, + error); } catch (const sourcemeta::core::SchemaRelativeMetaschemaResolutionError &error) { throw FileError< sourcemeta::core::SchemaRelativeMetaschemaResolutionError>( - entry.first, error); + entry.resolution_base, error); } catch (const sourcemeta::core::SchemaResolutionError &error) { - throw FileError(entry.first, - error); + throw FileError( + entry.resolution_base, error); } catch (const sourcemeta::core::SchemaUnknownBaseDialectError &) { throw FileError( - entry.first); + entry.resolution_base); } catch (const sourcemeta::core::SchemaError &error) { - throw FileError(entry.first, error.what()); + throw FileError(entry.resolution_base, + error.what()); } } diff --git a/src/command_lint.cc b/src/command_lint.cc index 24c313d6..33d44ed0 100644 --- a/src/command_lint.cc +++ b/src/command_lint.cc @@ -90,7 +90,7 @@ static auto get_lint_callback(sourcemeta::core::JSON &errors_array, if (output_json) { auto error_obj = sourcemeta::core::JSON::make_object(); - error_obj.assign("path", sourcemeta::core::JSON{entry.first.string()}); + error_obj.assign("path", sourcemeta::core::JSON{entry.first}); error_obj.assign("id", sourcemeta::core::JSON{name}); error_obj.assign("message", sourcemeta::core::JSON{message}); error_obj.assign("description", @@ -106,7 +106,7 @@ static auto get_lint_callback(sourcemeta::core::JSON &errors_array, errors_array.push_back(error_obj); } else { - std::cout << std::filesystem::relative(entry.first).string(); + std::cout << std::filesystem::relative(entry.resolution_base).string(); if (position.has_value()) { std::cout << ":"; std::cout << std::get<0>(position.value()); @@ -215,18 +215,18 @@ auto sourcemeta::jsonschema::lint(const sourcemeta::core::Options &options) if (options.contains("fix")) { for (const auto &entry : for_each_json(options)) { - const auto configuration_path{find_configuration(entry.first)}; - const auto &configuration{ - read_configuration(options, configuration_path, entry.first)}; + const auto configuration_path{find_configuration(entry.resolution_base)}; + const auto &configuration{read_configuration(options, configuration_path, + entry.resolution_base)}; const auto dialect{default_dialect(options, configuration)}; const auto &custom_resolver{ resolver(options, options.contains("http"), dialect, configuration)}; - LOG_VERBOSE(options) << "Linting: " << entry.first.string() << "\n"; + LOG_VERBOSE(options) << "Linting: " << entry.first << "\n"; if (entry.yaml) { throw YAMLInputError{ "The --fix option is not supported for YAML input files", - entry.first}; + entry.resolution_base}; } auto copy = entry.second; @@ -239,9 +239,7 @@ auto sourcemeta::jsonschema::lint(const sourcemeta::core::Options &options) copy, sourcemeta::core::schema_walker, custom_resolver, get_lint_callback(errors_array, entry, output_json, true, printed_progress), - dialect, - sourcemeta::core::URI::from_path(entry.resolution_base) - .recompose(), + dialect, sourcemeta::jsonschema::default_id(entry), EXCLUDE_KEYWORD); if (printed_progress) { std::cerr << "\n"; @@ -259,7 +257,7 @@ auto sourcemeta::jsonschema::lint(const sourcemeta::core::Options &options) std::cerr << "\n"; } - throw LintAutoFixError{error.what(), entry.first, + throw LintAutoFixError{error.what(), entry.resolution_base, error.location()}; } catch ( const sourcemeta::core::SchemaBrokenReferenceError &error) { @@ -270,7 +268,7 @@ auto sourcemeta::jsonschema::lint(const sourcemeta::core::Options &options) throw LintAutoFixError{ "Could not autofix the schema without breaking its internal " "references", - entry.first, error.location()}; + entry.resolution_base, error.location()}; } catch ( const sourcemeta::blaze::CompilerReferenceTargetNotSchemaError &error) { @@ -280,35 +278,35 @@ auto sourcemeta::jsonschema::lint(const sourcemeta::core::Options &options) throw FileError< sourcemeta::blaze::CompilerReferenceTargetNotSchemaError>( - entry.first, error); + entry.resolution_base, error); } catch (const sourcemeta::core::SchemaKeywordError &error) { if (printed_progress) { std::cerr << "\n"; } - throw FileError(entry.first, - error); + throw FileError( + entry.resolution_base, error); } catch (const sourcemeta::core::SchemaFrameError &error) { if (printed_progress) { std::cerr << "\n"; } - throw FileError(entry.first, - error); + throw FileError( + entry.resolution_base, error); } catch (const sourcemeta::core::SchemaUnknownBaseDialectError &) { if (printed_progress) { std::cerr << "\n"; } throw FileError( - entry.first); + entry.resolution_base); } catch (const sourcemeta::core::SchemaResolutionError &error) { if (printed_progress) { std::cerr << "\n"; } throw FileError( - entry.first, error); + entry.resolution_base, error); } catch (...) { if (printed_progress) { std::cerr << "\n"; @@ -333,16 +331,16 @@ auto sourcemeta::jsonschema::lint(const sourcemeta::core::Options &options) sourcemeta::core::prettify(copy, expected, indentation); expected << "\n"; - std::ifstream current_stream{entry.first}; + std::ifstream current_stream{entry.resolution_base}; std::ostringstream current; current << current_stream.rdbuf(); if (current.str() != expected.str()) { - std::ofstream output{entry.first}; + std::ofstream output{entry.resolution_base}; output << expected.str(); } } else if (copy != entry.second) { - std::ofstream output{entry.first}; + std::ofstream output{entry.resolution_base}; sourcemeta::core::prettify(copy, output, indentation); output << "\n"; } @@ -353,13 +351,13 @@ auto sourcemeta::jsonschema::lint(const sourcemeta::core::Options &options) } } else { for (const auto &entry : for_each_json(options)) { - const auto configuration_path{find_configuration(entry.first)}; - const auto &configuration{ - read_configuration(options, configuration_path, entry.first)}; + const auto configuration_path{find_configuration(entry.resolution_base)}; + const auto &configuration{read_configuration(options, configuration_path, + entry.resolution_base)}; const auto dialect{default_dialect(options, configuration)}; const auto &custom_resolver{ resolver(options, options.contains("http"), dialect, configuration)}; - LOG_VERBOSE(options) << "Linting: " << entry.first.string() << "\n"; + LOG_VERBOSE(options) << "Linting: " << entry.first << "\n"; bool printed_progress{false}; const auto wrapper_result = @@ -370,9 +368,7 @@ auto sourcemeta::jsonschema::lint(const sourcemeta::core::Options &options) custom_resolver, get_lint_callback(errors_array, entry, output_json, false, printed_progress), - dialect, - sourcemeta::core::URI::from_path(entry.resolution_base) - .recompose(), + dialect, sourcemeta::jsonschema::default_id(entry), EXCLUDE_KEYWORD); scores.emplace_back(subresult.second); if (subresult.first) { @@ -386,19 +382,19 @@ auto sourcemeta::jsonschema::lint(const sourcemeta::core::Options &options) &error) { throw FileError< sourcemeta::blaze::CompilerReferenceTargetNotSchemaError>( - entry.first, error); + entry.resolution_base, error); } catch (const sourcemeta::core::SchemaKeywordError &error) { - throw FileError(entry.first, - error); + throw FileError( + entry.resolution_base, error); } catch (const sourcemeta::core::SchemaFrameError &error) { - throw FileError(entry.first, - error); + throw FileError( + entry.resolution_base, error); } catch (const sourcemeta::core::SchemaUnknownBaseDialectError &) { throw FileError( - entry.first); + entry.resolution_base); } catch (const sourcemeta::core::SchemaResolutionError &error) { throw FileError( - entry.first, error); + entry.resolution_base, error); } }); diff --git a/src/command_metaschema.cc b/src/command_metaschema.cc index c7bb78f4..8f8cd9fe 100644 --- a/src/command_metaschema.cc +++ b/src/command_metaschema.cc @@ -32,12 +32,12 @@ auto sourcemeta::jsonschema::metaschema( for (const auto &entry : for_each_json(options)) { if (!sourcemeta::core::is_schema(entry.second)) { - throw NotSchemaError{entry.first}; + throw NotSchemaError{entry.resolution_base}; } - const auto configuration_path{find_configuration(entry.first)}; + const auto configuration_path{find_configuration(entry.resolution_base)}; const auto &configuration{ - read_configuration(options, configuration_path, entry.first)}; + read_configuration(options, configuration_path, entry.resolution_base)}; const auto default_dialect_option{default_dialect(options, configuration)}; const auto &custom_resolver{resolver(options, options.contains("http"), @@ -49,7 +49,7 @@ auto sourcemeta::jsonschema::metaschema( sourcemeta::core::dialect(entry.second, default_dialect_option)}; if (dialect.empty()) { throw FileError( - entry.first); + entry.resolution_base); } const auto metaschema{sourcemeta::core::metaschema( @@ -80,7 +80,7 @@ auto sourcemeta::jsonschema::metaschema( } else if (json_output) { // Otherwise its impossible to correlate the output // when validating i.e. a directory of schemas - std::cerr << entry.first.string() << "\n"; + std::cerr << entry.first << "\n"; const auto output{sourcemeta::blaze::standard( evaluator, cache.at(std::string{dialect}), entry.second, sourcemeta::blaze::StandardOutput::Basic, entry.positions)}; @@ -98,13 +98,9 @@ auto sourcemeta::jsonschema::metaschema( if (evaluator.validate(cache.at(std::string{dialect}), entry.second, std::ref(output))) { LOG_VERBOSE(options) - << "ok: " - << sourcemeta::core::weakly_canonical(entry.first).string() - << "\n matches " << dialect << "\n"; + << "ok: " << entry.first << "\n matches " << dialect << "\n"; } else { - std::cerr << "fail: " - << sourcemeta::core::weakly_canonical(entry.first).string() - << "\n"; + std::cerr << "fail: " << entry.first << "\n"; print(output, entry.positions, std::cerr); result = false; } @@ -112,15 +108,15 @@ auto sourcemeta::jsonschema::metaschema( } catch ( const sourcemeta::blaze::CompilerReferenceTargetNotSchemaError &error) { throw FileError( - entry.first, error); + entry.resolution_base, error); } catch (const sourcemeta::core::SchemaRelativeMetaschemaResolutionError &error) { throw FileError< sourcemeta::core::SchemaRelativeMetaschemaResolutionError>( - entry.first, error); + entry.resolution_base, error); } catch (const sourcemeta::core::SchemaResolutionError &error) { - throw FileError(entry.first, - error); + throw FileError( + entry.resolution_base, error); } } diff --git a/src/command_test.cc b/src/command_test.cc index 04b93faf..4d2624e6 100644 --- a/src/command_test.cc +++ b/src/command_test.cc @@ -29,47 +29,47 @@ auto parse_test_suite(const sourcemeta::jsonschema::InputJSON &entry, -> sourcemeta::blaze::TestSuite { try { return sourcemeta::blaze::TestSuite::parse( - entry.second, entry.positions, entry.first.parent_path(), + entry.second, entry.positions, entry.resolution_base.parent_path(), schema_resolver, sourcemeta::core::schema_walker, sourcemeta::blaze::default_schema_compiler, dialect); } catch (const sourcemeta::blaze::TestParseError &error) { if (!json_output) { - std::cout << entry.first.string() << ":\n"; + std::cout << entry.first << ":\n"; } throw sourcemeta::jsonschema::FileError{ - entry.first, error.what(), error.location(), error.line(), + entry.resolution_base, error.what(), error.location(), error.line(), error.column()}; } catch ( const sourcemeta::blaze::CompilerReferenceTargetNotSchemaError &error) { if (!json_output) { - std::cout << entry.first.string() << ":\n"; + std::cout << entry.first << ":\n"; } throw sourcemeta::jsonschema::FileError< - sourcemeta::blaze::CompilerReferenceTargetNotSchemaError>{entry.first, - error}; + sourcemeta::blaze::CompilerReferenceTargetNotSchemaError>{ + entry.resolution_base, error}; } catch ( const sourcemeta::core::SchemaRelativeMetaschemaResolutionError &error) { if (!json_output) { - std::cout << entry.first.string() << ":\n"; + std::cout << entry.first << ":\n"; } throw sourcemeta::jsonschema::FileError< - sourcemeta::core::SchemaRelativeMetaschemaResolutionError>{entry.first, - error}; + sourcemeta::core::SchemaRelativeMetaschemaResolutionError>{ + entry.resolution_base, error}; } catch (const sourcemeta::core::SchemaResolutionError &error) { if (!json_output) { - std::cout << entry.first.string() << ":\n"; + std::cout << entry.first << ":\n"; } throw sourcemeta::jsonschema::FileError< - sourcemeta::core::SchemaResolutionError>{entry.first, error}; + sourcemeta::core::SchemaResolutionError>{entry.resolution_base, error}; } catch (const sourcemeta::core::SchemaUnknownBaseDialectError &) { if (!json_output) { - std::cout << entry.first.string() << ":\n"; + std::cout << entry.first << ":\n"; } throw sourcemeta::jsonschema::FileError< - sourcemeta::core::SchemaUnknownBaseDialectError>{entry.first}; + sourcemeta::core::SchemaUnknownBaseDialectError>{entry.resolution_base}; } catch (...) { if (!json_output) { - std::cout << entry.first.string() << ":\n"; + std::cout << entry.first << ":\n"; } throw; } @@ -81,7 +81,7 @@ auto report_as_text(const sourcemeta::core::Options &options) -> void { for (const auto &entry : sourcemeta::jsonschema::for_each_json(options)) { const auto configuration_path{ - sourcemeta::jsonschema::find_configuration(entry.first)}; + sourcemeta::jsonschema::find_configuration(entry.resolution_base)}; const auto &configuration{sourcemeta::jsonschema::read_configuration( options, configuration_path)}; const auto dialect{ @@ -91,7 +91,7 @@ auto report_as_text(const sourcemeta::core::Options &options) -> void { auto test_suite{parse_test_suite(entry, schema_resolver, dialect, false)}; - std::cout << entry.first.string() << ":"; + std::cout << entry.first << ":"; const auto suite_result{test_suite.run( [&](const sourcemeta::core::JSON::String &, std::size_t index, @@ -193,7 +193,7 @@ auto report_as_ctrf(const sourcemeta::core::Options &options) -> void { for (const auto &entry : sourcemeta::jsonschema::for_each_json(options)) { const auto configuration_path{ - sourcemeta::jsonschema::find_configuration(entry.first)}; + sourcemeta::jsonschema::find_configuration(entry.resolution_base)}; const auto &configuration{sourcemeta::jsonschema::read_configuration( options, configuration_path)}; const auto dialect{ @@ -203,8 +203,7 @@ auto report_as_ctrf(const sourcemeta::core::Options &options) -> void { auto test_suite{parse_test_suite(entry, schema_resolver, dialect, true)}; - const auto file_path{ - sourcemeta::core::weakly_canonical(entry.first).string()}; + const auto file_path{entry.first}; const auto suite_result{test_suite.run( [&](const sourcemeta::core::JSON::String &target, std::size_t, diff --git a/src/command_validate.cc b/src/command_validate.cc index b92f72c0..d4d5a536 100644 --- a/src/command_validate.cc +++ b/src/command_validate.cc @@ -79,8 +79,8 @@ auto parse_loop(const sourcemeta::core::Options &options) -> std::uint64_t { auto run_loop(sourcemeta::blaze::Evaluator &evaluator, const sourcemeta::blaze::Template &schema_template, const sourcemeta::core::JSON &instance, - const std::filesystem::path &instance_path, - const int64_t instance_index, const uint64_t loop) -> bool { + const std::string &instance_path, const int64_t instance_index, + const uint64_t loop) -> bool { const auto iterations = static_cast(loop); double sum = 0.0, sum2 = 0.0, empty = 0.0; bool result = true; @@ -117,7 +117,7 @@ auto run_loop(sourcemeta::blaze::Evaluator &evaluator, auto avg = sum / iterations; auto stdev = loop == 1 ? 0.0 : std::sqrt(sum2 / iterations - avg * avg); - std::cout << instance_path.string(); + std::cout << instance_path; if (instance_index >= 0) std::cout << "[" << instance_index << "]"; std::cout << std::fixed; @@ -141,6 +141,11 @@ auto sourcemeta::jsonschema::validate(const sourcemeta::core::Options &options) const auto &schema_path{options.positional().at(0)}; + if (schema_path == "-") { + throw std::runtime_error{ + "Reading the schema from standard input is not supported"}; + } + if (std::filesystem::is_directory(schema_path)) { throw std::filesystem::filesystem_error{ "The input was supposed to be a file but it is a directory", @@ -304,158 +309,97 @@ auto sourcemeta::jsonschema::validate(const sourcemeta::core::Options &options) "The `--benchmark/-b` option is only allowed given a single instance"}; } - for (const auto &instance_path_view : instance_arguments) { - const std::filesystem::path instance_path{instance_path_view}; - if (trace && instance_path.extension() == ".jsonl") { - throw std::runtime_error{ - "The `--trace/-t` option is only allowed given a single instance"}; - } - - if (trace && std::filesystem::is_directory(instance_path)) { - throw std::runtime_error{ - "The `--trace/-t` option is only allowed given a single instance"}; + for (const auto &arg : instance_arguments) { + const std::filesystem::path arg_path{arg}; + if (arg != "-") { + if (trace && (std::filesystem::is_directory(arg_path) || + arg_path.extension() == ".jsonl")) { + throw std::runtime_error{ + "The `--trace/-t` option is only allowed given a single instance"}; + } + if (benchmark && std::filesystem::is_directory(arg_path)) { + throw std::runtime_error{"The `--benchmark/-b` option is only allowed " + "given a single instance"}; + } } + } - if (benchmark && std::filesystem::is_directory(instance_path)) { - throw std::runtime_error{"The `--benchmark/-b` option is only allowed " - "given a single instance"}; + const auto entries = for_each_json(instance_arguments, options); + 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 (instance_path == "-" || std::filesystem::is_directory(instance_path) || - instance_path.extension() == ".jsonl" || - instance_path.extension() == ".yaml" || - instance_path.extension() == ".yml") { - for (const auto &entry : for_each_json({instance_path_view}, options)) { - std::ostringstream error; - sourcemeta::blaze::SimpleOutput output{entry.second}; - 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 (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 (json_output) { - if (!entry.multidocument) { - std::cerr << entry.first.string() << "\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; - } - } - } else if (subresult) { - LOG_VERBOSE(options) - << "ok: " - << sourcemeta::core::weakly_canonical(entry.first).string(); - if (entry.multidocument) { - LOG_VERBOSE(options) << " (entry #" << entry.index + 1 << ")"; - } - LOG_VERBOSE(options) - << "\n matches " - << sourcemeta::core::weakly_canonical(schema_path).string() - << "\n"; - print_annotations(output, options, entry.positions, std::cerr); - } else { - std::cerr << "fail: " - << sourcemeta::core::weakly_canonical(entry.first).string(); - if (entry.multidocument) { - std::cerr << " (entry #" << entry.index + 1 << ")\n\n"; - sourcemeta::core::prettify(entry.second, std::cerr); - std::cerr << "\n\n"; - } else { - std::cerr << "\n"; - } - std::cerr << error.str(); - print(output, entry.positions, std::cerr); - result = false; - if (entry.multidocument) { - break; - } - } + 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"; } - } else { - sourcemeta::core::PointerPositionTracker tracker; - const auto instance{sourcemeta::core::read_yaml_or_json( - instance_path, std::ref(tracker))}; - std::ostringstream error; - sourcemeta::blaze::SimpleOutput output{instance}; - 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, instance, - instance_path, (int64_t)-1, benchmark_loop); - if (!subresult) { - error << "error: Schema validation failure\n"; - result = false; + 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; } - } else if (trace) { - subresult = evaluator.validate(schema_template, instance, - std::ref(trace_output)); - } else if (fast_mode) { - subresult = evaluator.validate(schema_template, instance); - } else if (!json_output) { - subresult = - evaluator.validate(schema_template, instance, std::ref(output)); } - - if (trace) { - print(trace_output, tracker, std::cout); - result = subresult; - } else if (json_output) { - const auto suboutput{sourcemeta::blaze::standard( - evaluator, schema_template, instance, - fast_mode ? sourcemeta::blaze::StandardOutput::Flag - : sourcemeta::blaze::StandardOutput::Basic, - tracker)}; - assert(suboutput.is_object()); - assert(suboutput.defines("valid")); - assert(suboutput.at("valid").is_boolean()); - if (!suboutput.at("valid").to_boolean()) { - result = false; - } - - sourcemeta::core::prettify(suboutput, std::cout); - std::cout << "\n"; - } else if (subresult) { - LOG_VERBOSE(options) - << "ok: " - << sourcemeta::core::weakly_canonical(instance_path).string() - << "\n matches " - << sourcemeta::core::weakly_canonical(schema_path).string() << "\n"; - print_annotations(output, options, tracker, std::cerr); + } else if (subresult) { + LOG_VERBOSE(options) << "ok: " << entry.first; + if (entry.multidocument) { + LOG_VERBOSE(options) << " (entry #" << entry.index + 1 << ")"; + } + LOG_VERBOSE(options) + << "\n matches " + << sourcemeta::core::weakly_canonical(schema_path).string() << "\n"; + print_annotations(output, options, entry.positions, std::cerr); + } else { + std::cerr << "fail: " << entry.first; + if (entry.multidocument) { + std::cerr << " (entry #" << entry.index + 1 << ")\n\n"; + sourcemeta::core::prettify(entry.second, std::cerr); + std::cerr << "\n\n"; } else { - std::cerr << "fail: " - << sourcemeta::core::weakly_canonical(instance_path).string() - << "\n"; - std::cerr << error.str(); - print(output, tracker, std::cerr); - result = false; + std::cerr << "\n"; + } + std::cerr << error.str(); + print(output, entry.positions, std::cerr); + result = false; + if (entry.multidocument) { + break; } } } diff --git a/src/input.h b/src/input.h index 1cdb2e4a..bf99d01b 100644 --- a/src/input.h +++ b/src/input.h @@ -17,6 +17,7 @@ #include // std::uintptr_t #include // std::filesystem #include // std::ref +#include // std::cin #include // std::set #include // std::ostringstream #include // std::runtime_error @@ -26,8 +27,9 @@ namespace sourcemeta::jsonschema { struct InputJSON { - std::filesystem::path first; + std::string first; std::filesystem::path resolution_base; + bool from_stdin{false}; sourcemeta::core::JSON second; sourcemeta::core::PointerPositionTracker positions; std::size_t index{0}; @@ -145,23 +147,9 @@ inline auto read_file(const std::filesystem::path &path) -> ParsedJSON { } inline auto read_from_stdin() -> ParsedJSON { - std::string buffer; - { - std::ostringstream ss; - ss << std::cin.rdbuf(); - buffer = ss.str(); - } - sourcemeta::core::PointerPositionTracker positions; - try { - return {sourcemeta::core::parse_json(buffer, std::ref(positions)), - std::move(positions), false}; - } catch (const sourcemeta::core::JSONParseError &) { - sourcemeta::core::PointerPositionTracker yaml_positions; - std::istringstream yaml_stream(buffer); - return {sourcemeta::core::parse_yaml(yaml_stream, std::ref(yaml_positions)), - std::move(yaml_positions), true}; - } + return {sourcemeta::core::parse_json(std::cin, std::ref(positions)), + std::move(positions), false}; } inline auto @@ -172,11 +160,9 @@ handle_json_entry(const std::filesystem::path &entry_path, const sourcemeta::core::Options &options) -> void { if (entry_path == "-") { auto parsed{read_from_stdin()}; - std::filesystem::path display_path{""}; - std::filesystem::path resolution_path{std::filesystem::current_path()}; - result.push_back({std::move(display_path), std::move(resolution_path), - std::move(parsed.document), std::move(parsed.positions), - 0, false, parsed.yaml}); + result.emplace_back("", std::filesystem::current_path(), true, + std::move(parsed.document), std::move(parsed.positions), + 0, false, parsed.yaml); return; } @@ -202,10 +188,9 @@ handle_json_entry(const std::filesystem::path &entry_path, // TODO: Print a verbose message for what is getting parsed auto parsed{read_file(canonical)}; - auto canonical_copy{canonical}; - result.push_back({std::move(canonical), std::move(canonical_copy), - std::move(parsed.document), std::move(parsed.positions), - 0, false, parsed.yaml}); + result.push_back({canonical.string(), std::move(canonical), false, + std::move(parsed.document), + std::move(parsed.positions), 0, false, parsed.yaml}); } } } else { @@ -223,8 +208,8 @@ handle_json_entry(const std::filesystem::path &entry_path, for (const auto &document : sourcemeta::core::JSONL{stream}) { // TODO: Get real positions for JSONL sourcemeta::core::PointerPositionTracker positions; - result.push_back( - {canonical, canonical, document, std::move(positions), index, true}); + result.push_back({canonical.string(), canonical, false, document, + std::move(positions), index, true}); index += 1; } } catch (const sourcemeta::core::JSONParseError &error) { @@ -273,46 +258,53 @@ handle_json_entry(const std::filesystem::path &entry_path, << canonical.string() << "\n"; std::size_t index{0}; for (auto &entry : documents) { - result.push_back({canonical, canonical, std::move(entry.first), - std::move(entry.second), index, true, true}); + result.push_back({canonical.string(), canonical, false, + std::move(entry.first), std::move(entry.second), + index, true, true}); index += 1; } } else if (documents.size() == 1) { - auto canonical_copy{canonical}; - result.push_back( - {std::move(canonical), std::move(canonical_copy), - std::move(documents.front().first), - std::move(documents.front().second), 0, false, true}); + result.push_back({canonical.string(), std::move(canonical), false, + std::move(documents.front().first), + std::move(documents.front().second), 0, false, + 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 auto parsed{read_file(canonical)}; - auto canonical_copy{canonical}; - result.push_back({std::move(canonical), std::move(canonical_copy), - std::move(parsed.document), std::move(parsed.positions), - 0, false, parsed.yaml}); + result.push_back({canonical.string(), std::move(canonical), false, + std::move(parsed.document), + std::move(parsed.positions), 0, false, parsed.yaml}); } } } } + } // namespace -inline auto for_each_json(const std::vector &arguments, - const sourcemeta::core::Options &options) - -> std::vector { - std::size_t stdin_count{0}; +inline auto +check_duplicate_stdin(const std::vector &arguments) -> void { + bool seen_stdin{false}; for (const auto &arg : arguments) { if (arg == "-") { - stdin_count++; + if (seen_stdin) { + throw std::runtime_error{ + "The standard input position argument `-` can only be specified " + "once"}; + } + seen_stdin = true; } } - if (stdin_count > 1) { - throw std::runtime_error{ - "Standard input (-) can only be specified once"}; - } +} + +inline auto for_each_json(const std::vector &arguments, + const sourcemeta::core::Options &options) + -> std::vector { + check_duplicate_stdin(arguments); const auto blacklist{parse_ignore(options)}; std::vector result; diff --git a/src/resolver.h b/src/resolver.h index e14c4460..e945d0b8 100644 --- a/src/resolver.h +++ b/src/resolver.h @@ -106,18 +106,18 @@ class CustomResolver { if (options.contains("resolve")) { for (const auto &entry : for_each_json(options.at("resolve"), options)) { LOG_DEBUG(options) << "Detecting schema resources from file: " - << entry.first.string() << "\n"; + << entry.first << "\n"; if (!sourcemeta::core::is_schema(entry.second)) { throw FileError( - entry.first, + entry.resolution_base, "The file you provided does not represent a valid JSON Schema"); } try { const auto result = this->add( entry.second, default_dialect, - sourcemeta::jsonschema::default_id(entry.first), + sourcemeta::jsonschema::default_id(entry), [&options](const auto &identifier) { LOG_DEBUG(options) << "Importing schema into the resolution context: " @@ -126,15 +126,16 @@ class CustomResolver { if (!result) { LOG_WARNING() << "No schema resources were imported from this file\n" - << " at " << entry.first.string() << "\n" + << " at " << entry.first << "\n" << "Are you sure this schema sets any identifiers?\n"; } } catch (const sourcemeta::core::SchemaFrameError &error) { throw FileError( - entry.first, std::string{error.identifier()}, error.what()); + entry.resolution_base, std::string{error.identifier()}, + error.what()); } catch (const sourcemeta::core::SchemaUnknownBaseDialectError &) { throw FileError( - entry.first); + entry.resolution_base); } } } diff --git a/src/utils.h b/src/utils.h index eec8c652..4d788673 100644 --- a/src/utils.h +++ b/src/utils.h @@ -12,6 +12,8 @@ #include #include +#include "input.h" + #include // assert #include // std::filesystem::path #include // std::next @@ -31,6 +33,12 @@ inline auto default_id(const std::filesystem::path &schema_path) .recompose(); } +inline auto default_id(const InputJSON &entry) -> std::string { + return sourcemeta::core::URI::from_path( + sourcemeta::core::weakly_canonical(entry.resolution_base)) + .recompose(); +} + inline auto resolve_entrypoint(const sourcemeta::core::SchemaFrame &frame, const std::string &entrypoint) -> std::string { if (entrypoint.empty()) { diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 07106a25..20acda65 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -208,6 +208,9 @@ add_jsonschema_test_unix(validate/fail_entrypoint_invalid_pointer_escape) add_jsonschema_test_unix(validate/fail_entrypoint_invalid_uri_parse) add_jsonschema_test_unix(validate/fail_entrypoint_mismatch) add_jsonschema_test_unix(validate/fail_entrypoint_with_template) +add_jsonschema_test_unix(validate/pass_stdin_instance) +add_jsonschema_test_unix(validate/fail_stdin_schema) +add_jsonschema_test_unix(validate/fail_stdin_multiple) # Metaschema add_jsonschema_test_unix(metaschema/pass_trace) diff --git a/test/validate/fail_stdin_multiple.sh b/test/validate/fail_stdin_multiple.sh new file mode 100755 index 00000000..f58dff7b --- /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": "object" +} +EOF + +echo '{}' | "$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: The standard input position argument `-` can only be specified once +EOF + +diff "$TMP/stderr.txt" "$TMP/expected.txt" diff --git a/test/validate/fail_stdin_schema.sh b/test/validate/fail_stdin_schema.sh new file mode 100755 index 00000000..c18f7fbd --- /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: Reading the schema from standard input is not supported +EOF + +diff "$TMP/stderr.txt" "$TMP/expected.txt" diff --git a/test/validate/pass_stdin.sh b/test/validate/pass_stdin.sh deleted file mode 100755 index 045fc355..00000000 --- a/test/validate/pass_stdin.sh +++ /dev/null @@ -1,59 +0,0 @@ -#!/bin/bash - -set -o errexit -set -o nounset - -TMP="$(mktemp -d)" -clean() { rm -rf "$TMP"; } -trap clean EXIT - -cat << 'EOF' > "$TMP/schema.json" -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "type": "object", - "properties": { - "foo": { "type": "string" } - } -} -EOF - -echo '{"foo": "bar"}' | "$1" validate "$TMP/schema.json" - - -echo 'foo: bar' | "$1" validate "$TMP/schema.json" - - -set +o errexit -cat "$TMP/schema.json" > "$TMP/schema_stdin.json" - -OUTPUT=$("$1" validate - "$TMP/schema.json" < "$TMP/schema_stdin.json" 2>&1) - -EXIT_CODE=$? -set -o errexit - -: "$OUTPUT" - -if [ "$EXIT_CODE" -eq 0 ]; then - echo "FAIL: Expected failure when passing schema via stdin" - echo "$OUTPUT" - exit 1 -fi - -if ! echo "$OUTPUT" | grep -q "Reading schema from stdin is not supported"; then - echo "FAIL: Expected schema-from-stdin error message" - echo "$OUTPUT" - exit 1 -fi - -set +o errexit -OUTPUT=$("$1" validate - - < "$TMP/schema.json" 2>&1) -EXIT_CODE=$? -set -o errexit - -: "$OUTPUT" - -if [ "$EXIT_CODE" -eq 0 ]; then - echo "FAIL: Expected failure when passing multiple stdin inputs" - echo "$OUTPUT" - exit 1 -fi - -echo "PASS: stdin validation tests passed." diff --git a/test/validate/pass_stdin_instance.sh b/test/validate/pass_stdin_instance.sh new file mode 100755 index 00000000..4a1b279e --- /dev/null +++ b/test/validate/pass_stdin_instance.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' > "$TMP/schema.json" +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "foo": { "type": "string" } + } +} +EOF + +echo '{"foo": "bar"}' | "$1" validate "$TMP/schema.json" -