Skip to content

Commit

Permalink
Implement an --extension/-e option to prefer custom file extensions
Browse files Browse the repository at this point in the history
Signed-off-by: Juan Cruz Viotti <[email protected]>
  • Loading branch information
jviotti committed Jun 4, 2024
1 parent d6b5ae1 commit 6e9b7a0
Show file tree
Hide file tree
Showing 13 changed files with 152 additions and 24 deletions.
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"

0 comments on commit 6e9b7a0

Please sign in to comment.