diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 15da97833..3a4e23dd6 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -23,6 +23,7 @@ set_target_properties(jsonschema_cli PROPERTIES OUTPUT_NAME jsonschema) target_link_libraries(jsonschema_cli PRIVATE sourcemeta::core::error) target_link_libraries(jsonschema_cli PRIVATE sourcemeta::core::io) target_link_libraries(jsonschema_cli PRIVATE sourcemeta::core::uri) +target_link_libraries(jsonschema_cli PRIVATE sourcemeta::core::diff) target_link_libraries(jsonschema_cli PRIVATE sourcemeta::core::json) target_link_libraries(jsonschema_cli PRIVATE sourcemeta::core::jsonl) target_link_libraries(jsonschema_cli PRIVATE sourcemeta::core::gzip) diff --git a/src/command_fmt.cc b/src/command_fmt.cc index c45fbf768..7132f9b8c 100644 --- a/src/command_fmt.cc +++ b/src/command_fmt.cc @@ -1,12 +1,14 @@ #include #include +#include #include #include -#include // std::cerr, std::cout -#include // std::ostringstream -#include // std::move -#include // std::vector +#include // std::cerr, std::cout +#include // std::ostringstream +#include // std::string +#include // std::string_view +#include // std::move, std::unreachable #include "command.h" #include "error.h" @@ -15,12 +17,80 @@ #include "resolver.h" #include "utils.h" +namespace { + +auto diff_operation_name(const sourcemeta::core::Diff::Operation::Type type) + -> std::string_view { + switch (type) { + case sourcemeta::core::Diff::Operation::Type::Equal: + return "equal"; + case sourcemeta::core::Diff::Operation::Type::Delete: + return "delete"; + case sourcemeta::core::Diff::Operation::Type::Insert: + return "insert"; + default: + std::unreachable(); + } +} + +auto to_diff_json(const sourcemeta::core::Diff &difference) + -> sourcemeta::core::JSON { + auto operations{sourcemeta::core::JSON::make_array()}; + + for (const auto &operation : difference.operations) { + // An insertion is the only operation that reads the modified input, as + // the lines it introduces are not present in the original one + const auto insertion{operation.type == + sourcemeta::core::Diff::Operation::Type::Insert}; + const auto &tokens{insertion ? difference.modified : difference.original}; + const auto start{insertion ? operation.modified_start + : operation.original_start}; + const auto end{insertion ? operation.modified_end : operation.original_end}; + + auto lines{sourcemeta::core::JSON::make_array()}; + for (auto index{start}; index < end; ++index) { + lines.push_back(sourcemeta::core::JSON{tokens[index]}); + } + + auto entry{sourcemeta::core::JSON::make_object()}; + entry.assign("type", + sourcemeta::core::JSON{diff_operation_name(operation.type)}); + entry.assign("lines", std::move(lines)); + operations.push_back(std::move(entry)); + } + + return operations; +} + +auto report_check_failure(const std::string ¤t, + const std::string &expected, const std::string &label, + const bool output_json, + sourcemeta::core::JSON &errors) -> void { + const auto difference{sourcemeta::core::diff( + current, expected, sourcemeta::core::Diff::Mode::Line, + sourcemeta::core::Diff::Algorithm::Myers)}; + + if (output_json) { + auto entry{sourcemeta::core::JSON::make_object()}; + entry.assign("path", sourcemeta::core::JSON{label}); + entry.assign("diff", to_diff_json(difference)); + errors.push_back(std::move(entry)); + } else { + std::cerr << "fail: " << label << "\n"; + sourcemeta::core::stringify( + difference, std::cerr, sourcemeta::core::Diff::Format::Unified, + {.original_label = "current", .modified_label = "expected"}); + } +} + +} // namespace + auto sourcemeta::jsonschema::fmt(const sourcemeta::core::Options &options) -> void { validate_http_headers(options); const bool output_json{options.contains("json")}; bool result{true}; - std::vector failed_files; + auto errors{sourcemeta::core::JSON::make_array()}; const auto indentation{parse_indentation(options)}; const auto handle_stdin = [&]() { @@ -66,11 +136,9 @@ auto sourcemeta::jsonschema::fmt(const sourcemeta::core::Options &options) if (raw_stdin == expected.str()) { LOG_VERBOSE(options) << "ok: " << stdin_label << "\n"; - } else if (output_json) { - failed_files.push_back(stdin_label); - result = false; } else { - std::cerr << "fail: " << stdin_label << "\n"; + report_check_failure(raw_stdin, expected.str(), stdin_label, + output_json, errors); result = false; } } else { @@ -162,11 +230,9 @@ auto sourcemeta::jsonschema::fmt(const sourcemeta::core::Options &options) if (options.contains("check")) { if (current == expected.str()) { LOG_VERBOSE(options) << "ok: " << entry.first << "\n"; - } else if (output_json) { - failed_files.push_back(entry.first); - result = false; } else { - std::cerr << "fail: " << entry.first << "\n"; + report_check_failure(current, expected.str(), entry.first, + output_json, errors); result = false; } } else { @@ -240,12 +306,7 @@ auto sourcemeta::jsonschema::fmt(const sourcemeta::core::Options &options) output_json_object.assign("valid", sourcemeta::core::JSON{result}); if (!result) { - auto errors_array{sourcemeta::core::JSON::make_array()}; - for (auto &file_path : failed_files) { - errors_array.push_back(sourcemeta::core::JSON{std::move(file_path)}); - } - - output_json_object.assign("errors", sourcemeta::core::JSON{errors_array}); + output_json_object.assign("errors", std::move(errors)); } sourcemeta::core::prettify(output_json_object, std::cout, indentation); diff --git a/test/format/fail_check_many.clitest b/test/format/fail_check_many.clitest index b58e8339d..1172433b3 100644 --- a/test/format/fail_check_many.clitest +++ b/test/format/fail_check_many.clitest @@ -31,7 +31,29 @@ REPLACE $CWD WITH '[CWD]' IN result.txt WRITE expected.txt UNTIL EOF 2> fail: [CWD]/schemas/1.json +2> --- current +2> +++ expected +2> @@ -1,6 +1,6 @@ +2> { +2> - "type": "string", +2> "$schema": "http://json-schema.org/draft-04/schema#", +2> "title": "Test", +2> - "description": "Test schema" +2> + "description": "Test schema", +2> + "type": "string" +2> } 2> fail: [CWD]/schemas/2.json +2> --- current +2> +++ expected +2> @@ -1,6 +1,6 @@ +2> { +2> - "type": "string", +2> "$schema": "http://json-schema.org/draft-04/schema#", +2> "title": "Test", +2> - "description": "Test schema" +2> + "description": "Test schema", +2> + "type": "string" +2> } 2> 2> Run the `fmt` command without `--check/-c` to fix the formatting EOF diff --git a/test/format/fail_check_many_json.clitest b/test/format/fail_check_many_json.clitest index e6c60f217..864c1230e 100644 --- a/test/format/fail_check_many_json.clitest +++ b/test/format/fail_check_many_json.clitest @@ -3,7 +3,7 @@ WRITE schemas/1.json UNTIL EOF "type": "string", "$schema": "http://json-schema.org/draft-04/schema#", "title": "Test", - "description": "Test schema" + "description": "Test schema for formatting" } EOF @@ -12,7 +12,7 @@ WRITE schemas/2.json UNTIL EOF "type": "string", "$schema": "http://json-schema.org/draft-04/schema#", "title": "Test", - "description": "Test schema" + "description": "Test schema for formatting" } EOF @@ -20,7 +20,7 @@ WRITE schemas/3.json UNTIL EOF { "$schema": "http://json-schema.org/draft-04/schema#", "title": "Test", - "description": "Test schema" + "description": "Test schema for formatting" } EOF @@ -33,8 +33,76 @@ WRITE expected.txt UNTIL EOF 1> { 1> "valid": false, 1> "errors": [ -1> "[CWD]/schemas/1.json", -1> "[CWD]/schemas/2.json" +1> { +1> "path": "[CWD]/schemas/1.json", +1> "diff": [ +1> { +1> "type": "equal", +1> "lines": [ "{" ] +1> }, +1> { +1> "type": "delete", +1> "lines": [ " \"type\": \"string\"," ] +1> }, +1> { +1> "type": "equal", +1> "lines": [ +1> " \"$schema\": \"http://json-schema.org/draft-04/schema#\",", +1> " \"title\": \"Test\"," +1> ] +1> }, +1> { +1> "type": "delete", +1> "lines": [ " \"description\": \"Test schema for formatting\"" ] +1> }, +1> { +1> "type": "insert", +1> "lines": [ +1> " \"description\": \"Test schema for formatting\",", +1> " \"type\": \"string\"" +1> ] +1> }, +1> { +1> "type": "equal", +1> "lines": [ "}" ] +1> } +1> ] +1> }, +1> { +1> "path": "[CWD]/schemas/2.json", +1> "diff": [ +1> { +1> "type": "equal", +1> "lines": [ "{" ] +1> }, +1> { +1> "type": "delete", +1> "lines": [ " \"type\": \"string\"," ] +1> }, +1> { +1> "type": "equal", +1> "lines": [ +1> " \"$schema\": \"http://json-schema.org/draft-04/schema#\",", +1> " \"title\": \"Test\"," +1> ] +1> }, +1> { +1> "type": "delete", +1> "lines": [ " \"description\": \"Test schema for formatting\"" ] +1> }, +1> { +1> "type": "insert", +1> "lines": [ +1> " \"description\": \"Test schema for formatting\",", +1> " \"type\": \"string\"" +1> ] +1> }, +1> { +1> "type": "equal", +1> "lines": [ "}" ] +1> } +1> ] +1> } 1> ] 1> } EOF diff --git a/test/format/fail_check_single.clitest b/test/format/fail_check_single.clitest index b4744f0b6..a6d1b35c4 100644 --- a/test/format/fail_check_single.clitest +++ b/test/format/fail_check_single.clitest @@ -14,6 +14,17 @@ REPLACE $CWD WITH '[CWD]' IN result.txt WRITE expected.txt UNTIL EOF 2> fail: [CWD]/schema.json +2> --- current +2> +++ expected +2> @@ -1,6 +1,6 @@ +2> { +2> - "type": "string", +2> "$schema": "http://json-schema.org/draft-04/schema#", +2> "title": "Test", +2> - "description": "Test schema" +2> + "description": "Test schema", +2> + "type": "string" +2> } 2> 2> Run the `fmt` command without `--check/-c` to fix the formatting EOF diff --git a/test/format/fail_check_single_indentation.clitest b/test/format/fail_check_single_indentation.clitest index c17607f63..dc839714a 100644 --- a/test/format/fail_check_single_indentation.clitest +++ b/test/format/fail_check_single_indentation.clitest @@ -14,6 +14,19 @@ REPLACE $CWD WITH '[CWD]' IN result.txt WRITE expected.txt UNTIL EOF 2> fail: [CWD]/schema.json +2> --- current +2> +++ expected +2> @@ -1,6 +1,6 @@ +2> { +2> - "$schema": "http://json-schema.org/draft-04/schema#", +2> - "title": "Test", +2> - "description": "Test schema", +2> - "type": 1 +2> + "$schema": "http://json-schema.org/draft-04/schema#", +2> + "title": "Test", +2> + "description": "Test schema", +2> + "type": 1 +2> } 2> 2> Run the `fmt` command without `--check/-c` to fix the formatting EOF diff --git a/test/format/fail_check_single_json.clitest b/test/format/fail_check_single_json.clitest index d0a190fe4..ded6c37c5 100644 --- a/test/format/fail_check_single_json.clitest +++ b/test/format/fail_check_single_json.clitest @@ -3,7 +3,7 @@ WRITE this/is/a/very/very/very/very/very/very/long/path/schema.json UNTIL EOF "type": "string", "$schema": "http://json-schema.org/draft-04/schema#", "title": "Test", - "description": "Test schema" + "description": "Test schema for formatting" } EOF @@ -16,7 +16,41 @@ WRITE expected.txt UNTIL EOF 1> { 1> "valid": false, 1> "errors": [ -1> "[CWD]/this/is/a/very/very/very/very/very/very/long/path/schema.json" +1> { +1> "path": "[CWD]/this/is/a/very/very/very/very/very/very/long/path/schema.json", +1> "diff": [ +1> { +1> "type": "equal", +1> "lines": [ "{" ] +1> }, +1> { +1> "type": "delete", +1> "lines": [ " \"type\": \"string\"," ] +1> }, +1> { +1> "type": "equal", +1> "lines": [ +1> " \"$schema\": \"http://json-schema.org/draft-04/schema#\",", +1> " \"title\": \"Test\"," +1> ] +1> }, +1> { +1> "type": "delete", +1> "lines": [ " \"description\": \"Test schema for formatting\"" ] +1> }, +1> { +1> "type": "insert", +1> "lines": [ +1> " \"description\": \"Test schema for formatting\",", +1> " \"type\": \"string\"" +1> ] +1> }, +1> { +1> "type": "equal", +1> "lines": [ "}" ] +1> } +1> ] +1> } 1> ] 1> } EOF diff --git a/test/format/fail_check_single_json_indentation.clitest b/test/format/fail_check_single_json_indentation.clitest index 5645ef7df..4e5d1a60e 100644 --- a/test/format/fail_check_single_json_indentation.clitest +++ b/test/format/fail_check_single_json_indentation.clitest @@ -16,7 +16,37 @@ WRITE expected.txt UNTIL EOF 1> { 1> "valid": false, 1> "errors": [ -1> "[CWD]/this/is/a/very/very/very/very/very/very/very/very/long/nested/path/schema.json" +1> { +1> "path": "[CWD]/this/is/a/very/very/very/very/very/very/very/very/long/nested/path/schema.json", +1> "diff": [ +1> { +1> "type": "equal", +1> "lines": [ "{" ] +1> }, +1> { +1> "type": "delete", +1> "lines": [ +1> " \"type\": \"string\",", +1> " \"$schema\": \"http://json-schema.org/draft-04/schema#\",", +1> " \"title\": \"Test\",", +1> " \"description\": \"Test schema\"" +1> ] +1> }, +1> { +1> "type": "insert", +1> "lines": [ +1> " \"$schema\": \"http://json-schema.org/draft-04/schema#\",", +1> " \"title\": \"Test\",", +1> " \"description\": \"Test schema\",", +1> " \"type\": \"string\"" +1> ] +1> }, +1> { +1> "type": "equal", +1> "lines": [ "}" ] +1> } +1> ] +1> } 1> ] 1> } EOF diff --git a/test/format/fail_check_test_document.clitest b/test/format/fail_check_test_document.clitest index 882bb55dd..c77602d4a 100644 --- a/test/format/fail_check_test_document.clitest +++ b/test/format/fail_check_test_document.clitest @@ -22,6 +22,22 @@ REPLACE $CWD WITH '[CWD]' IN result.txt WRITE expected.txt UNTIL EOF 2> Interpreting as a test file: [CWD]/test.json 2> fail: [CWD]/test.json +2> --- current +2> +++ expected +2> @@ -1,4 +1,5 @@ +2> { +2> + "target": "https://example.com/my-schema", +2> "tests": [ +2> { +2> "description": "I expect to pass", +2> @@ -8,6 +9,5 @@ +2> "foo": 1 +2> } +2> } +2> - ], +2> - "target": "https://example.com/my-schema" +2> + ] +2> } 2> 2> Run the `fmt` command without `--check/-c` to fix the formatting EOF diff --git a/test/format/fail_stdin_check.clitest b/test/format/fail_stdin_check.clitest index d6099f533..4420e9595 100644 --- a/test/format/fail_stdin_check.clitest +++ b/test/format/fail_stdin_check.clitest @@ -7,6 +7,14 @@ RUN fmt --check - STDIN input.json IN . INTO result.txt EXPECTING 2 WRITE expected.txt UNTIL EOF 2> fail: tag:sourcemeta.com,2026:jsonschema/stdin +2> --- current +2> +++ expected +2> @@ -1 +1,4 @@ +2> -{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"string"} +2> +{ +2> + "$schema": "https://json-schema.org/draft/2020-12/schema", +2> + "type": "string" +2> +} 2> 2> Run the `fmt` command without `--check/-c` to fix the formatting EOF @@ -19,7 +27,28 @@ RUN fmt --check - --json STDIN input.json IN . INTO result_json.txt EXPECTING 2 WRITE expected_json.txt UNTIL EOF 1> { 1> "valid": false, -1> "errors": [ "tag:sourcemeta.com,2026:jsonschema/stdin" ] +1> "errors": [ +1> { +1> "path": "tag:sourcemeta.com,2026:jsonschema/stdin", +1> "diff": [ +1> { +1> "type": "delete", +1> "lines": [ +1> "{\"$schema\":\"https://json-schema.org/draft/2020-12/schema\",\"type\":\"string\"}" +1> ] +1> }, +1> { +1> "type": "insert", +1> "lines": [ +1> "{", +1> " \"$schema\": \"https://json-schema.org/draft/2020-12/schema\",", +1> " \"type\": \"string\"", +1> "}" +1> ] +1> } +1> ] +1> } +1> ] 1> } EOF diff --git a/test/format/fail_stdin_missing_newline.sh b/test/format/fail_stdin_missing_newline.sh index e1f872ab6..ad793a73d 100755 --- a/test/format/fail_stdin_missing_newline.sh +++ b/test/format/fail_stdin_missing_newline.sh @@ -19,6 +19,15 @@ test "$EXIT_CODE" = "2" cat << 'EOF' > "$TMP/expected.txt" fail: tag:sourcemeta.com,2026:jsonschema/stdin +--- current ++++ expected +@@ -1 +1,4 @@ +-{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"string"} +\ No newline at end of file ++{ ++ "$schema": "https://json-schema.org/draft/2020-12/schema", ++ "type": "string" ++} Run the `fmt` command without `--check/-c` to fix the formatting EOF @@ -35,7 +44,28 @@ test "$EXIT_CODE" = "2" cat << 'EOF' > "$TMP/expected.txt" { "valid": false, - "errors": [ "tag:sourcemeta.com,2026:jsonschema/stdin" ] + "errors": [ + { + "path": "tag:sourcemeta.com,2026:jsonschema/stdin", + "diff": [ + { + "type": "delete", + "lines": [ + "{\"$schema\":\"https://json-schema.org/draft/2020-12/schema\",\"type\":\"string\"}" + ] + }, + { + "type": "insert", + "lines": [ + "{", + " \"$schema\": \"https://json-schema.org/draft/2020-12/schema\",", + " \"type\": \"string\"", + "}" + ] + } + ] + } + ] } EOF