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

Replace --metaschema from lint/test with a new metaschema command #89

Merged
merged 1 commit into from
Jun 13, 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
1 change: 1 addition & 0 deletions README.markdown
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ Schema CLI without passing a command will print convenient reference
documentation:

- [Validating](./docs/validate.markdown)
- [Metaschema](./docs/metaschema.markdown) (ensure a schema is valid)
- [Testing](./docs/test.markdown)
- [Formatting](./docs/format.markdown)
- [Linting](./docs/lint.markdown)
Expand Down
75 changes: 75 additions & 0 deletions docs/metaschema.markdown
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
Formatting
==========

```sh
jsonschema metaschema [schemas-or-directories...]
[--verbose/-v] [--http/-h] [--extension/-e <extension>]
[--ignore/-i <schemas-or-directories>]
```

Ensure that a schema or a set of schemas are considered valid with regards to
their metaschemas.

Examples
--------

For example, consider this fictitious JSON Schema that follows the Draft 4
dialect but sets the `type` property to an invalid value:

```json
{
"$schema": "http://json-schema.org/draft-04/schema#",
"type": 1
}
```

Running the `metaschema` command on it will surface an error:

```sh
$ jsonschema metaschema schema.json
error: The target document is expected to be one of the given values
at instance location "/type"
at evaluate path "/properties/type/anyOf/0/$ref/enum"
error: Mark the current position of the evaluation process for future jumps
at instance location "/type"
at evaluate path "/properties/type/anyOf/0/$ref"
error: The target document is expected to be of the given type
at instance location "/type"
at evaluate path "/properties/type/anyOf/1/type"
error: The target is expected to match at least one of the given assertions
at instance location "/type"
at evaluate path "/properties/type/anyOf"
error: The target is expected to match all of the given assertions
at instance location ""
at evaluate path "/properties"
```

### Validate the metaschema of JSON Schemas in-place

```sh
jsonschema metaschema path/to/my/schema_1.json path/to/my/schema_2.json
```

### Validate the metaschema of every `.json` file in a given directory (recursively)

```sh
jsonschema metaschema path/to/schemas/
```

### Validate the metaschema of every `.json` file in the current directory (recursively)

```sh
jsonschema metaschema
```

### Validate the metaschema of every `.json` file in a given directory while ignoring another

```sh
jsonschema metaschema path/to/schemas/ --ignore path/to/schemas/nested
```

### Validate the metaschema of every `.schema.json` file in the current directory (recursively)

```sh
jsonschema metaschema --extension .schema.json
```
14 changes: 5 additions & 9 deletions docs/test.markdown
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,8 @@ Testing

```sh
jsonschema test [schemas-or-directories...]
[--http/-h] [--metaschema/-m] [--verbose/-v]
[--resolve/-r <schemas-or-directories> ...] [--extension/-e <extension>]
[--ignore/-i <schemas-or-directories>]
[--http/-h] [--verbose/-v] [--resolve/-r <schemas-or-directories> ...]
[--extension/-e <extension>] [--ignore/-i <schemas-or-directories>]
```

Schemas are code. As such, you should run an automated unit testing suite
Expand All @@ -20,6 +19,9 @@ against them. Just like popular test frameworks like [Jest](https://jestjs.io),
schema-oriented test runner inspired by the [official JSON Schema test
suite](https://github.com/json-schema-org/JSON-Schema-Test-Suite).

**If you want to validate that a schema adheres to its metaschema, use the
[`metaschema`](./metaschema.markdown) command instead.**

Examples
--------

Expand Down Expand Up @@ -82,12 +84,6 @@ jsonschema test --ignore dist
jsonschema test --extension .test.json
```

### Run a single test definition validating the schemas against their metaschemas

```sh
jsonschema test path/to/test.json --metaschema
```

### Run a single test definition enabling HTTP resolution

```sh
Expand Down
12 changes: 4 additions & 8 deletions docs/validate.markdown
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,7 @@ Validating
> Draft 2020-12 soon.

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

Expand All @@ -17,6 +16,9 @@ JSON Schema CLI offers a `validate` command to evaluate a JSON instance against
a JSON Schema or a JSON Schema against its meta-schema, presenting
human-friendly information on unsuccessful validation.

**If you want to validate that a schema adheres to its metaschema, use the
[`metaschema`](./metaschema.markdown) command instead.**

Examples
--------

Expand Down Expand Up @@ -59,12 +61,6 @@ jsonschema validate path/to/my/schema.json path/to/my/instance.json
jsonschema validate path/to/my/schema.json
```

### Validate a JSON instance against a schema plus the schema against its meta-schema

```sh
jsonschema validate path/to/my/schema.json path/to/my/instance.json --metaschema
```

### Validate a JSON instance enabling HTTP resolution

```sh
Expand Down
1 change: 1 addition & 0 deletions src/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ add_executable(jsonschema_cli
command_bundle.cc
command_test.cc
command_lint.cc
command_metaschema.cc
command_validate.cc)

noa_add_default_options(PRIVATE jsonschema_cli)
Expand Down
1 change: 1 addition & 0 deletions src/command.h
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ auto bundle(const std::span<const std::string> &arguments) -> int;
auto test(const std::span<const std::string> &arguments) -> int;
auto lint(const std::span<const std::string> &arguments) -> int;
auto validate(const std::span<const std::string> &arguments) -> int;
auto metaschema(const std::span<const std::string> &arguments) -> int;
} // namespace intelligence::jsonschema::cli

#endif
50 changes: 50 additions & 0 deletions src/command_metaschema.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
#include <sourcemeta/jsontoolkit/json.h>
#include <sourcemeta/jsontoolkit/jsonschema.h>

#include <cstdlib> // EXIT_SUCCESS, EXIT_FAILURE
#include <iostream> // std::cerr
#include <set> // std::set
#include <string> // std::string

#include "command.h"
#include "utils.h"

// TODO: Add a flag to emit output using the standard JSON Schema output format
auto intelligence::jsonschema::cli::metaschema(
const std::span<const std::string> &arguments) -> int {
const auto options{parse_options(arguments, {"h", "http"})};
const auto custom_resolver{
resolver(options, options.contains("h") || options.contains("http"))};
bool result{true};

for (const auto &entry : for_each_json(options.at(""), parse_ignore(options),
parse_extensions(options))) {
if (!sourcemeta::jsontoolkit::is_schema(entry.second)) {
std::cerr << "Not a schema: " << entry.first.string() << "\n";
return EXIT_FAILURE;
}

// TODO: Cache this somehow for performance reasons?
const auto metaschema_template{sourcemeta::jsontoolkit::compile(
sourcemeta::jsontoolkit::metaschema(entry.second, custom_resolver),
sourcemeta::jsontoolkit::default_schema_walker, custom_resolver,
sourcemeta::jsontoolkit::default_schema_compiler)};

std::ostringstream error;
if (sourcemeta::jsontoolkit::evaluate(
metaschema_template, entry.second,
sourcemeta::jsontoolkit::SchemaCompilerEvaluationMode::Fast,
pretty_evaluate_callback(error))) {
log_verbose(options)
<< entry.first.string()
<< ": The schema is valid with respect to its metaschema\n";
} else {
std::cerr << error.str();
std::cerr << entry.first.string()
<< ": The schema is NOT valid with respect to its metaschema\n";
result = false;
}
}

return result ? EXIT_SUCCESS : EXIT_FAILURE;
}
24 changes: 1 addition & 23 deletions src/command_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,7 @@

auto intelligence::jsonschema::cli::test(
const std::span<const std::string> &arguments) -> int {
const auto options{
parse_options(arguments, {"h", "http", "m", "metaschema"})};
const auto options{parse_options(arguments, {"h", "http"})};
bool result{true};
const auto test_resolver{
resolver(options, options.contains("h") || options.contains("http"))};
Expand Down Expand Up @@ -42,27 +41,6 @@ auto intelligence::jsonschema::cli::test(
return EXIT_FAILURE;
}

if (options.contains("m") || options.contains("metaschema")) {
std::cout << " Metaschema - "
<< sourcemeta::jsontoolkit::dialect(schema.value())
.value_or("<unknown>")
<< "\n";
const auto metaschema_template{sourcemeta::jsontoolkit::compile(
sourcemeta::jsontoolkit::metaschema(schema.value(), test_resolver),
sourcemeta::jsontoolkit::default_schema_walker, test_resolver,
sourcemeta::jsontoolkit::default_schema_compiler)};
std::ostringstream error;
if (sourcemeta::jsontoolkit::evaluate(
metaschema_template, schema.value(),
sourcemeta::jsontoolkit::SchemaCompilerEvaluationMode::Fast,
pretty_evaluate_callback(error))) {
std::cout << " PASS\n";
} else {
std::cout << " FAIL\n";
result = false;
}
}

const auto schema_template{sourcemeta::jsontoolkit::compile(
schema.value(), sourcemeta::jsontoolkit::default_schema_walker,
test_resolver, sourcemeta::jsontoolkit::default_schema_compiler)};
Expand Down
26 changes: 2 additions & 24 deletions src/command_validate.cc
Original file line number Diff line number Diff line change
Expand Up @@ -13,37 +13,15 @@
// TODO: Add a flag to collect annotations
auto intelligence::jsonschema::cli::validate(
const std::span<const std::string> &arguments) -> int {
const auto options{
parse_options(arguments, {"h", "http", "m", "metaschema"})};
const auto options{parse_options(arguments, {"h", "http"})};
CLI_ENSURE(options.at("").size() >= 1, "You must pass a schema")
CLI_ENSURE(options.at("").size() >= 2, "You must pass an instance")
const auto &schema_path{options.at("").at(0)};
const auto custom_resolver{
resolver(options, options.contains("h") || options.contains("http"))};

const auto schema{sourcemeta::jsontoolkit::from_file(schema_path)};

if (options.contains("m") || options.contains("metaschema") ||
options.at("").size() < 2) {
const auto metaschema_template{sourcemeta::jsontoolkit::compile(
sourcemeta::jsontoolkit::metaschema(schema, custom_resolver),
sourcemeta::jsontoolkit::default_schema_walker, custom_resolver,
sourcemeta::jsontoolkit::default_schema_compiler)};
std::ostringstream error;
if (sourcemeta::jsontoolkit::evaluate(
metaschema_template, schema,
sourcemeta::jsontoolkit::SchemaCompilerEvaluationMode::Fast,
pretty_evaluate_callback(error))) {
log_verbose(options)
<< schema_path
<< ": The schema is valid with respect to its metaschema\n";
} else {
std::cerr << error.str();
std::cerr << schema_path
<< ": The schema is NOT valid with respect to its metaschema\n";
return EXIT_FAILURE;
}
}

bool result{true};
if (options.at("").size() >= 2) {
const auto &instance_path{options.at("").at(1)};
Expand Down
34 changes: 24 additions & 10 deletions src/main.cc
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
#include "command.h"
#include "configure.h"

// TODO: Stop duplicating most of the docs here. Provide very brief
// info here and link to the docs on GitHub instead
constexpr std::string_view USAGE_DETAILS{R"EOF(
Global Options:

Expand All @@ -21,17 +23,28 @@ Global Options:

Commands:

validate <schema.json> [instance.json] [--http/-h] [--metaschema/-m]
validate <schema.json> [instance.json] [--http/-h]

If an instance is passed, validate it against the given schema.
Otherwise, validate the schema against 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 even if an instance
was passed.
protocol.

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

Validate that a schema or a set of schemas are valid with respect
to their metaschemas. If no argument is passed, validate every
`.json` file in the current working directory (recursively). The
`--ignore/-i` option can be set to files or directories to ignore.
The `--http/-h` option enables resolving remote schemas over the HTTP
protocol. When scanning directories, the `--extension/-e` option is
used to prefer a file extension other than `.json`. This option can
be set multiple times.

test [schemas-or-directories...] [--http/-h] [--extension/-e <extension>]
[--ignore/-i <schemas-or-directories>]

A schema test runner inspired by the official JSON Schema test suite.
Passing directories as input will run every `.json` file in such
Expand All @@ -40,10 +53,9 @@ Global Options:
The `--ignore/-i` option can be set to files or directories to ignore.

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.
protocol. 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] [--extension/-e <extension>]
[--ignore/-i <schemas-or-directories>]
Expand Down Expand Up @@ -101,6 +113,8 @@ auto jsonschema_main(const std::string &program, const std::string &command,
return intelligence::jsonschema::cli::lint(arguments);
} else if (command == "validate") {
return intelligence::jsonschema::cli::validate(arguments);
} else if (command == "metaschema") {
return intelligence::jsonschema::cli::metaschema(arguments);
} else if (command == "test") {
return intelligence::jsonschema::cli::test(arguments);
} else {
Expand Down
10 changes: 5 additions & 5 deletions test/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -35,21 +35,21 @@ add_jsonschema_test_unix(validate_fail_draft7)
add_jsonschema_test_unix(validate_fail_remote_no_http)
add_jsonschema_test_unix(validate_fail_invalid_ref)
add_jsonschema_test_unix(validate_non_supported)
add_jsonschema_test_unix(validate_pass_with_metaschema)
add_jsonschema_test_unix(validate_pass_only_metaschema)
add_jsonschema_test_unix(validate_fail_only_metaschema)
add_jsonschema_test_unix(validate_fail_only_metaschema_without_option)
add_jsonschema_test_unix(bundle_non_remote)
add_jsonschema_test_unix(bundle_remote_single_schema)
add_jsonschema_test_unix(bundle_remote_no_http)
add_jsonschema_test_unix(bundle_remote_directory)
add_jsonschema_test_unix(test_single_pass)
add_jsonschema_test_unix(test_single_fail)
add_jsonschema_test_unix(test_single_unsupported)
add_jsonschema_test_unix(test_single_pass_with_metaschema)
add_jsonschema_test_unix(lint_pass)
add_jsonschema_test_unix(lint_fail)
add_jsonschema_test_unix(lint_fix)
add_jsonschema_test_unix(metaschema_fail_directory)
add_jsonschema_test_unix(metaschema_fail_single)
add_jsonschema_test_unix(metaschema_fail_non_schema)
add_jsonschema_test_unix(metaschema_pass_cwd)
add_jsonschema_test_unix(metaschema_pass_single)

# CI specific tests
add_jsonschema_test_unix_ci(bundle_remote_http)
Loading