From 6e9b7a07f5ce17f5b17992b108830d35e843d9f9 Mon Sep 17 00:00:00 2001 From: Juan Cruz Viotti Date: Tue, 4 Jun 2024 09:58:21 -0400 Subject: [PATCH] Implement an `--extension/-e` option to prefer custom file extensions Signed-off-by: Juan Cruz Viotti --- docs/bundle.markdown | 3 +- docs/format.markdown | 9 +++++- docs/lint.markdown | 9 +++++- docs/test.markdown | 10 +++++- docs/validate.markdown | 5 +-- src/command_fmt.cc | 3 +- src/command_lint.cc | 6 ++-- src/command_test.cc | 3 +- src/main.cc | 21 ++++++++---- src/utils.cc | 59 ++++++++++++++++++++++++++++++---- src/utils.h | 6 +++- test/CMakeLists.txt | 1 + test/format_multi_extension.sh | 41 +++++++++++++++++++++++ 13 files changed, 152 insertions(+), 24 deletions(-) create mode 100755 test/format_multi_extension.sh diff --git a/docs/bundle.markdown b/docs/bundle.markdown index f306df81..2f10b433 100644 --- a/docs/bundle.markdown +++ b/docs/bundle.markdown @@ -2,7 +2,8 @@ Bundling ======== ```sh -jsonschema bundle [--http/-h] [--verbose/-v] [--resolve/-r ...] +jsonschema bundle + [--http/-h] [--verbose/-v] [--resolve/-r ...] ``` A schema may contain references to remote schemas outside the scope of the diff --git a/docs/format.markdown b/docs/format.markdown index db8a9595..48dd35ba 100644 --- a/docs/format.markdown +++ b/docs/format.markdown @@ -2,7 +2,8 @@ Formatting ========== ```sh -jsonschema fmt [schemas-or-directories...] [--check|-c] [--verbose/-v] +jsonschema fmt [schemas-or-directories...] + [--check/-c] [--verbose/-v] [--extension/-e ] ``` Schemas are code. As such, they are expected follow consistent stylistic @@ -56,6 +57,12 @@ jsonschema fmt path/to/schemas/ jsonschema fmt ``` +### Format every `.schema.json` file in the current directory (recursively) + +```sh +jsonschema fmt --extension .schema.json +``` + ### Check that a single JSON Schema is properly formatted ```sh diff --git a/docs/lint.markdown b/docs/lint.markdown index 988766a4..e9ae1335 100644 --- a/docs/lint.markdown +++ b/docs/lint.markdown @@ -2,7 +2,8 @@ Linting ======= ```sh -jsonschema lint [schemas-or-directories...] [--fix|-f] [--verbose/-v] +jsonschema lint [schemas-or-directories...] + [--fix/-f] [--verbose/-v] [--extension/-e ] ``` JSON Schema is a surprisingly expressive schema language. Like with traditional @@ -61,6 +62,12 @@ jsonschema lint path/to/schemas/ jsonschema lint ``` +### Lint every `.schema.json` file in the current directory (recursively) + +```sh +jsonschema lint --extension .schema.json +``` + ### Fix lint warnings on a single schema ```sh diff --git a/docs/test.markdown b/docs/test.markdown index 35270346..28ce5345 100644 --- a/docs/test.markdown +++ b/docs/test.markdown @@ -6,7 +6,9 @@ Testing > to support *every* dialect of JSON Schema from Draft 0 to Draft 2020-12 soon. ```sh -jsonschema test [schemas-or-directories...] [--http/-h] [--metaschema/-m] [--verbose/-v] [--resolve/-r ...] +jsonschema test [schemas-or-directories...] + [--http/-h] [--metaschema/-m] [--verbose/-v] [--resolve/-r ...] + [--extension/-e ] ``` Schemas are code. As such, you should run an automated unit testing suite @@ -66,6 +68,12 @@ jsonschema test path/to/tests/ jsonschema test ``` +### Run every `.test.json` test definition in the current directory (recursively) + +```sh +jsonschema test --extension .test.json +``` + ### Run a single test definition validating the schemas against their metaschemas ```sh diff --git a/docs/validate.markdown b/docs/validate.markdown index aeb1432d..53dc8370 100644 --- a/docs/validate.markdown +++ b/docs/validate.markdown @@ -6,7 +6,8 @@ Validating > to support *every* dialect of JSON Schema from Draft 0 to Draft 2020-12 soon. ```sh -jsonschema validate [instance.json] [--http/-h] [--metaschema/-m] [--verbose/-v] [--resolve/-r ...] +jsonschema validate + [instance.json] [--http/-h] [--metaschema/-m] [--verbose/-v] [--resolve/-r ...] ``` The most popular use case of JSON Schema is to validate JSON documents. The @@ -53,7 +54,7 @@ jsonschema validate path/to/my/schema.json path/to/my/instance.json ### Validate a JSON Schema against it meta-schema ```sh -jsonschema validate path/to/my/schema.json +jsonschema validate path/to/my/schema.json ``` ### Validate a JSON instance against a schema plus the schema against its meta-schema diff --git a/src/command_fmt.cc b/src/command_fmt.cc index f255cd1f..efad6777 100644 --- a/src/command_fmt.cc +++ b/src/command_fmt.cc @@ -13,7 +13,8 @@ auto intelligence::jsonschema::cli::fmt( const std::span &arguments) -> int { const auto options{parse_options(arguments, {"c", "check"})}; - for (const auto &entry : for_each_json(options.at(""))) { + for (const auto &entry : + for_each_json(options.at(""), parse_extensions(options))) { if (options.contains("c") || options.contains("check")) { log_verbose(options) << "Checking: " << entry.first.string() << "\n"; std::ifstream input{entry.first}; diff --git a/src/command_lint.cc b/src/command_lint.cc index 55e951a7..bad94cd2 100644 --- a/src/command_lint.cc +++ b/src/command_lint.cc @@ -21,7 +21,8 @@ auto intelligence::jsonschema::cli::lint( bool result{true}; if (options.contains("f") || options.contains("fix")) { - for (const auto &entry : for_each_json(options.at(""))) { + for (const auto &entry : + for_each_json(options.at(""), parse_extensions(options))) { log_verbose(options) << "Linting: " << entry.first.string() << "\n"; auto copy = entry.second; bundle.apply(copy, sourcemeta::jsontoolkit::default_schema_walker, @@ -32,7 +33,8 @@ auto intelligence::jsonschema::cli::lint( output << std::endl; } } else { - for (const auto &entry : for_each_json(options.at(""))) { + for (const auto &entry : + for_each_json(options.at(""), parse_extensions(options))) { log_verbose(options) << "Linting: " << entry.first.string() << "\n"; const bool subresult = bundle.check( entry.second, sourcemeta::jsontoolkit::default_schema_walker, diff --git a/src/command_test.cc b/src/command_test.cc index e9b1ffa4..053ec55d 100644 --- a/src/command_test.cc +++ b/src/command_test.cc @@ -15,7 +15,8 @@ auto intelligence::jsonschema::cli::test( const auto test_resolver{ resolver(options, options.contains("h") || options.contains("http"))}; - for (const auto &entry : for_each_json(options.at(""))) { + for (const auto &entry : + for_each_json(options.at(""), parse_extensions(options))) { const sourcemeta::jsontoolkit::JSON test{ sourcemeta::jsontoolkit::from_file(entry.first)}; CLI_ENSURE(test.is_object(), "The test document must be an object") diff --git a/src/main.cc b/src/main.cc index e02ca0f3..5b2c8fde 100644 --- a/src/main.cc +++ b/src/main.cc @@ -30,30 +30,37 @@ Global Options: was passed. test [schemas-or-directories...] [--http/-h] [--metaschema/-m] + [--extension/-e ] A schema test runner inspired by the official JSON Schema test suite. Passing directories as input will run every `.json` file in such directory (recursively) as a test. If no argument is passed, run every `.json` file in the current working directory (recursively) as a test. - The `--http/-h` option enables resolving remote schemas over the HTTP - protocol. The `--metaschema/-m` option checks that the given schema is - valid with respects to its dialect metaschema. + The `--http/-h` option enables resolving remote schemas over the HTTP + protocol. The `--metaschema/-m` option checks that the given schema is + valid with respects to its dialect metaschema. When scanning + directories, the `--extension/-e` option is used to prefer a file + extension other than `.json`. This option can be set multiple times. - fmt [schemas-or-directories...] [--check/-c] + fmt [schemas-or-directories...] [--check/-c] [--extension/-e ] Format the input schemas in-place. Passing directories as input means to format every `.json` file in such directory (recursively). If no argument is passed, format every `.json` file in the current working directory (recursively). The `--check/-c` option will check if the given - schemas adhere to the desired formatting without modifying them. + schemas adhere to the desired formatting without modifying them. When + scanning directories, the `--extension/-e` option is used to prefer a + file extension other than `.json`. This option can be set multiple times. - lint [schemas-or-directories...] [--fix/-f] + lint [schemas-or-directories...] [--fix/-f] [--extension/-e ] Lint the input schemas. Passing directories as input means to lint every `.json` file in such directory (recursively). If no argument is passed, lint every `.json` file in the current working directory (recursively). The `--fix/-f` option will attempt to automatically - fix the linter errors. + fix the linter errors. When scanning directories, the `--extension/-e` + option is used to prefer a file extension other than `.json`. This option + can be set multiple times. bundle [--http/-h] diff --git a/src/utils.cc b/src/utils.cc index 961d4a82..4c6ca411 100644 --- a/src/utils.cc +++ b/src/utils.cc @@ -5,10 +5,12 @@ #include "utils.h" +#include // std::any_of #include // assert #include // std::ofstream #include // std::cerr #include // std::optional, std::nullopt +#include // std::set #include // std::ostringstream #include // std::runtime_error @@ -16,13 +18,17 @@ namespace { auto handle_json_entry( const std::filesystem::path &entry_path, + const std::set &extensions, std::vector> &result) -> void { if (std::filesystem::is_directory(entry_path)) { for (auto const &entry : std::filesystem::recursive_directory_iterator{entry_path}) { if (!std::filesystem::is_directory(entry) && - entry.path().extension() == ".json") { + std::any_of(extensions.cbegin(), extensions.cend(), + [&entry](const auto &extension) { + return entry.path().string().ends_with(extension); + })) { result.emplace_back(entry.path(), sourcemeta::jsontoolkit::from_file(entry.path())); } @@ -34,26 +40,42 @@ auto handle_json_entry( throw std::runtime_error(error.str()); } - result.emplace_back(entry_path, - sourcemeta::jsontoolkit::from_file(entry_path)); + if (std::any_of(extensions.cbegin(), extensions.cend(), + [&entry_path](const auto &extension) { + return entry_path.string().ends_with(extension); + })) { + result.emplace_back(entry_path, + sourcemeta::jsontoolkit::from_file(entry_path)); + } } } +auto normalize_extension(const std::string &extension) -> std::string { + if (extension.starts_with('.')) { + return extension; + } + + std::ostringstream result; + result << '.' << extension; + return result.str(); +} + } // namespace namespace intelligence::jsonschema::cli { -auto for_each_json(const std::vector &arguments) +auto for_each_json(const std::vector &arguments, + const std::set &extensions) -> std::vector< std::pair> { std::vector> result; if (arguments.empty()) { - handle_json_entry(std::filesystem::current_path(), result); + handle_json_entry(std::filesystem::current_path(), extensions, result); } else { for (const auto &entry : arguments) { - handle_json_entry(entry, result); + handle_json_entry(entry, extensions, result); } } @@ -206,4 +228,29 @@ auto log_verbose(const std::map> &options) return null_stream; } +auto parse_extensions(const std::map> + &options) -> std::set { + std::set result; + + if (options.contains("extension")) { + for (const auto &extension : options.at("extension")) { + log_verbose(options) << "Using extension: " << extension << "\n"; + result.insert(normalize_extension(extension)); + } + } + + if (options.contains("e")) { + for (const auto &extension : options.at("e")) { + log_verbose(options) << "Using extension: " << extension << "\n"; + result.insert(normalize_extension(extension)); + } + } + + if (result.empty()) { + result.insert({".json"}); + } + + return result; +} + } // namespace intelligence::jsonschema::cli diff --git a/src/utils.h b/src/utils.h index 63a3259f..530c68d8 100644 --- a/src/utils.h +++ b/src/utils.h @@ -26,7 +26,8 @@ auto parse_options(const std::span &arguments, const std::set &flags) -> std::map>; -auto for_each_json(const std::vector &arguments) +auto for_each_json(const std::vector &arguments, + const std::set &extensions) -> std::vector< std::pair>; @@ -45,6 +46,9 @@ auto resolver(const std::map> &options, auto log_verbose(const std::map> &options) -> std::ostream &; +auto parse_extensions(const std::map> + &options) -> std::set; + } // namespace intelligence::jsonschema::cli #endif diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 26b7f914..2b53398e 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -19,6 +19,7 @@ add_jsonschema_test_unix(format_single) add_jsonschema_test_unix(format_invalid_path) add_jsonschema_test_unix(format_directory) add_jsonschema_test_unix(format_cwd) +add_jsonschema_test_unix(format_multi_extension) add_jsonschema_test_unix(format_check_single_fail) add_jsonschema_test_unix(format_check_single_pass) add_jsonschema_test_unix(frame) diff --git a/test/format_multi_extension.sh b/test/format_multi_extension.sh new file mode 100755 index 00000000..111f6b36 --- /dev/null +++ b/test/format_multi_extension.sh @@ -0,0 +1,41 @@ +#!/bin/sh + +set -o errexit +set -o nounset + +TMP="$(mktemp -d)" +clean() { rm -rf "$TMP"; } +trap clean EXIT + +cat << 'EOF' > "$TMP/schema_1.json" +{ + "additionalProperties": false, + "title": "Hello World", + "properties": {"foo": {}, "bar": {}} +} +EOF + +cat << 'EOF' > "$TMP/schema_2.schema.json" +{"type": "string", "title": "My String"} +EOF + +cd "$TMP" +"$1" fmt --extension .schema.json -v + +cat << 'EOF' > "$TMP/expected_1.json" +{ + "additionalProperties": false, + "title": "Hello World", + "properties": {"foo": {}, "bar": {}} +} +EOF + +cat << 'EOF' > "$TMP/expected_2.json" +{ + "title": "My String", + "type": "string" +} +EOF + +diff "$TMP/schema_1.json" "$TMP/expected_1.json" +diff "$TMP/schema_2.schema.json" "$TMP/expected_2.json"