Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement an --extension/-e option to prefer custom file extensions #54

Merged
merged 1 commit into from
Jun 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion docs/bundle.markdown
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ Bundling
========

```sh
jsonschema bundle <schema.json> [--http/-h] [--verbose/-v] [--resolve/-r <schema.json> ...]
jsonschema bundle <schema.json>
[--http/-h] [--verbose/-v] [--resolve/-r <schema.json> ...]
```

A schema may contain references to remote schemas outside the scope of the
Expand Down
9 changes: 8 additions & 1 deletion docs/format.markdown
Original file line number Diff line number Diff line change
Expand Up @@ -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 <extension>]
```

Schemas are code. As such, they are expected follow consistent stylistic
Expand Down Expand Up @@ -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
Expand Down
9 changes: 8 additions & 1 deletion docs/lint.markdown
Original file line number Diff line number Diff line change
Expand Up @@ -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 <extension>]
```

JSON Schema is a surprisingly expressive schema language. Like with traditional
Expand Down Expand Up @@ -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
Expand Down
10 changes: 9 additions & 1 deletion docs/test.markdown
Original file line number Diff line number Diff line change
Expand Up @@ -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 <schema.json> ...]
jsonschema test [schemas-or-directories...]
[--http/-h] [--metaschema/-m] [--verbose/-v] [--resolve/-r <schema.json> ...]
[--extension/-e <extension>]
```

Schemas are code. As such, you should run an automated unit testing suite
Expand Down Expand Up @@ -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
Expand Down
5 changes: 3 additions & 2 deletions docs/validate.markdown
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ Validating
> to support *every* dialect of JSON Schema from Draft 0 to Draft 2020-12 soon.

```sh
jsonschema validate <schema.json> [instance.json] [--http/-h] [--metaschema/-m] [--verbose/-v] [--resolve/-r <schema.json> ...]
jsonschema validate <schema.json>
[instance.json] [--http/-h] [--metaschema/-m] [--verbose/-v] [--resolve/-r <schema.json> ...]
```

The most popular use case of JSON Schema is to validate JSON documents. The
Expand Down Expand Up @@ -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
Expand Down
3 changes: 2 additions & 1 deletion src/command_fmt.cc
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ auto intelligence::jsonschema::cli::fmt(
const std::span<const std::string> &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};
Expand Down
6 changes: 4 additions & 2 deletions src/command_lint.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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,
Expand Down
3 changes: 2 additions & 1 deletion src/command_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
21 changes: 14 additions & 7 deletions src/main.cc
Original file line number Diff line number Diff line change
Expand Up @@ -30,30 +30,37 @@ Global Options:
was passed.

test [schemas-or-directories...] [--http/-h] [--metaschema/-m]
[--extension/-e <extension>]

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 <extension>]

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 <extension>]

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 <schema.json> [--http/-h]

Expand Down
59 changes: 53 additions & 6 deletions src/utils.cc
Original file line number Diff line number Diff line change
Expand Up @@ -5,24 +5,30 @@

#include "utils.h"

#include <algorithm> // std::any_of
#include <cassert> // assert
#include <fstream> // std::ofstream
#include <iostream> // std::cerr
#include <optional> // std::optional, std::nullopt
#include <set> // std::set
#include <sstream> // std::ostringstream
#include <stdexcept> // std::runtime_error

namespace {

auto handle_json_entry(
const std::filesystem::path &entry_path,
const std::set<std::string> &extensions,
std::vector<std::pair<std::filesystem::path, sourcemeta::jsontoolkit::JSON>>
&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()));
}
Expand All @@ -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<std::string> &arguments)
auto for_each_json(const std::vector<std::string> &arguments,
const std::set<std::string> &extensions)
-> std::vector<
std::pair<std::filesystem::path, sourcemeta::jsontoolkit::JSON>> {
std::vector<std::pair<std::filesystem::path, sourcemeta::jsontoolkit::JSON>>
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);
}
}

Expand Down Expand Up @@ -206,4 +228,29 @@ auto log_verbose(const std::map<std::string, std::vector<std::string>> &options)
return null_stream;
}

auto parse_extensions(const std::map<std::string, std::vector<std::string>>
&options) -> std::set<std::string> {
std::set<std::string> 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
6 changes: 5 additions & 1 deletion src/utils.h
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@ auto parse_options(const std::span<const std::string> &arguments,
const std::set<std::string> &flags)
-> std::map<std::string, std::vector<std::string>>;

auto for_each_json(const std::vector<std::string> &arguments)
auto for_each_json(const std::vector<std::string> &arguments,
const std::set<std::string> &extensions)
-> std::vector<
std::pair<std::filesystem::path, sourcemeta::jsontoolkit::JSON>>;

Expand All @@ -45,6 +46,9 @@ auto resolver(const std::map<std::string, std::vector<std::string>> &options,
auto log_verbose(const std::map<std::string, std::vector<std::string>> &options)
-> std::ostream &;

auto parse_extensions(const std::map<std::string, std::vector<std::string>>
&options) -> std::set<std::string>;

} // namespace intelligence::jsonschema::cli

#endif
1 change: 1 addition & 0 deletions test/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
41 changes: 41 additions & 0 deletions test/format_multi_extension.sh
Original file line number Diff line number Diff line change
@@ -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"