Skip to content

Commit ca4723a

Browse files
authored
Prototype validate taking directories as input (#546)
Signed-off-by: Juan Cruz Viotti <[email protected]>
1 parent 738495f commit ca4723a

File tree

66 files changed

+1250
-170
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

66 files changed

+1250
-170
lines changed

docs/validate.markdown

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ Validating
55
> JSON Schema Draft 3 and older are not supported at this point in time.
66
77
```sh
8-
jsonschema validate <schema.json|.yaml> <instance.json|.jsonl|.yaml...>
8+
jsonschema validate <schema.json|.yaml> <instance.json|.jsonl|.yaml|directory...>
99
[--http/-h] [--verbose/-v] [--resolve/-r <schemas-or-directories> ...]
1010
[--benchmark/-b] [--loop <iterations>] [--extension/-e <extension>]
1111
[--ignore/-i <schemas-or-directories>] [--trace/-t] [--fast/-f]
@@ -14,8 +14,8 @@ jsonschema validate <schema.json|.yaml> <instance.json|.jsonl|.yaml...>
1414

1515
The most popular use case of JSON Schema is to validate JSON documents. The
1616
JSON Schema CLI offers a `validate` command to evaluate one or many JSON
17-
instances or JSONL datasets against a JSON Schema, presenting human-friendly
18-
information on unsuccessful validation.
17+
instances, directories of instances, or JSONL datasets against a JSON Schema,
18+
presenting human-friendly information on unsuccessful validation.
1919

2020
The `--json`/`-j` option outputs the evaluation result using the JSON Schema
2121
[`Flag`](https://json-schema.org/draft/2020-12/json-schema-core#section-12.4.1) or
@@ -163,3 +163,22 @@ jsonschema validate path/to/my/schema.json path/to/my/instance.json --benchmark
163163
```sh
164164
jsonschema validate path/to/my/schema.json path/to/my/instance.json --trace
165165
```
166+
167+
### Validate a directory of instances against a schema
168+
169+
```sh
170+
jsonschema validate path/to/my/schema.json path/to/instances/
171+
```
172+
173+
### Validate a directory of instances with a specific extension
174+
175+
```sh
176+
jsonschema validate path/to/my/schema.json path/to/instances/ --extension .data.json
177+
```
178+
179+
### Validate a directory of instances while ignoring certain paths
180+
181+
```sh
182+
jsonschema validate path/to/my/schema.json path/to/instances/ \
183+
--ignore path/to/instances/drafts
184+
```

src/command_validate.cc

Lines changed: 85 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -140,14 +140,14 @@ auto sourcemeta::jsonschema::validate(const sourcemeta::core::Options &options)
140140
"jsonschema validate path/to/schema.json path/to/instance.json"};
141141
}
142142

143-
if (options.positional().size() < 2) {
144-
throw PositionalArgumentError{
145-
"In addition to the schema, you must also pass an argument\n"
146-
"that represents the instance to validate against",
147-
"jsonschema validate path/to/schema.json path/to/instance.json"};
143+
const auto &schema_path{options.positional().at(0)};
144+
145+
if (std::filesystem::is_directory(schema_path)) {
146+
throw std::filesystem::filesystem_error{
147+
"The input was supposed to be a file but it is a directory",
148+
schema_path, std::make_error_code(std::errc::is_a_directory)};
148149
}
149150

150-
const auto &schema_path{options.positional().at(0)};
151151
const auto configuration_path{find_configuration(schema_path)};
152152
const auto &configuration{read_configuration(options, configuration_path)};
153153
const auto dialect{default_dialect(options, configuration)};
@@ -188,10 +188,40 @@ auto sourcemeta::jsonschema::validate(const sourcemeta::core::Options &options)
188188

189189
bool result{true};
190190

191-
auto iterator{options.positional().cbegin()};
192-
std::advance(iterator, 1);
193-
for (; iterator != options.positional().cend(); ++iterator) {
194-
const std::filesystem::path instance_path{*iterator};
191+
std::vector<std::string_view> instance_arguments;
192+
if (options.positional().size() > 1) {
193+
instance_arguments.assign(options.positional().cbegin() + 1,
194+
options.positional().cend());
195+
} else {
196+
instance_arguments.push_back(".");
197+
}
198+
199+
if (trace && instance_arguments.size() > 1) {
200+
throw std::runtime_error{
201+
"The `--trace/-t` option is only allowed given a single instance"};
202+
}
203+
204+
if (benchmark && instance_arguments.size() > 1) {
205+
throw std::runtime_error{
206+
"The `--benchmark/-b` option is only allowed given a single instance"};
207+
}
208+
209+
for (const auto &instance_path_view : instance_arguments) {
210+
const std::filesystem::path instance_path{instance_path_view};
211+
if (trace && instance_path.extension() == ".jsonl") {
212+
throw std::runtime_error{
213+
"The `--trace/-t` option is only allowed given a single instance"};
214+
}
215+
216+
if (trace && std::filesystem::is_directory(instance_path)) {
217+
throw std::runtime_error{
218+
"The `--trace/-t` option is only allowed given a single instance"};
219+
}
220+
221+
if (benchmark && std::filesystem::is_directory(instance_path)) {
222+
throw std::runtime_error{"The `--benchmark/-b` option is only allowed "
223+
"given a single instance"};
224+
}
195225
if (instance_path.extension() == ".jsonl") {
196226
LOG_VERBOSE(options)
197227
<< "Interpreting input as JSONL: "
@@ -288,6 +318,51 @@ auto sourcemeta::jsonschema::validate(const sourcemeta::core::Options &options)
288318
if (index == 0) {
289319
sourcemeta::jsonschema::LOG_WARNING() << "The JSONL file is empty\n";
290320
}
321+
} else if (std::filesystem::is_directory(instance_path)) {
322+
for (const auto &entry : for_each_json({instance_path_view}, options)) {
323+
std::ostringstream error;
324+
sourcemeta::blaze::SimpleOutput output{entry.second};
325+
bool subresult{true};
326+
if (fast_mode) {
327+
subresult = evaluator.validate(schema_template, entry.second);
328+
} else if (!json_output) {
329+
subresult = evaluator.validate(schema_template, entry.second,
330+
std::ref(output));
331+
}
332+
333+
if (json_output) {
334+
std::cerr << entry.first.string() << "\n";
335+
const auto suboutput{sourcemeta::blaze::standard(
336+
evaluator, schema_template, entry.second,
337+
fast_mode ? sourcemeta::blaze::StandardOutput::Flag
338+
: sourcemeta::blaze::StandardOutput::Basic,
339+
entry.positions)};
340+
assert(suboutput.is_object());
341+
assert(suboutput.defines("valid"));
342+
assert(suboutput.at("valid").is_boolean());
343+
if (!suboutput.at("valid").to_boolean()) {
344+
result = false;
345+
}
346+
347+
sourcemeta::core::prettify(suboutput, std::cout);
348+
std::cout << "\n";
349+
} else if (subresult) {
350+
LOG_VERBOSE(options)
351+
<< "ok: "
352+
<< sourcemeta::core::weakly_canonical(entry.first).string()
353+
<< "\n matches "
354+
<< sourcemeta::core::weakly_canonical(schema_path).string()
355+
<< "\n";
356+
print_annotations(output, options, entry.positions, std::cerr);
357+
} else {
358+
std::cerr << "fail: "
359+
<< sourcemeta::core::weakly_canonical(entry.first).string()
360+
<< "\n";
361+
std::cerr << error.str();
362+
print(output, entry.positions, std::cerr);
363+
result = false;
364+
}
365+
}
291366
} else {
292367
sourcemeta::core::PointerPositionTracker tracker;
293368
const auto instance{sourcemeta::core::read_yaml_or_json(

src/main.cc

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ Global Options:
3232
3333
Print this command reference help.
3434
35-
validate <schema.json|.yaml> <instance.json|.jsonl|.yaml...> [--http/-h]
35+
validate <schema.json|.yaml> <instance.json|.jsonl|.yaml|directory...> [--http/-h]
3636
[--benchmark/-b] [--loop <iterations>]
3737
[--extension/-e <extension>]
3838
[--ignore/-i <schemas-or-directories>] [--trace/-t] [--fast/-f]
@@ -148,6 +148,7 @@ auto jsonschema_main(const std::string &program, const std::string &command,
148148
app.flag("trace", {"t"});
149149
app.flag("fast", {"f"});
150150
app.option("extension", {"e"});
151+
app.option("ignore", {"i"});
151152
app.option("template", {"m"});
152153
app.option("loop", {"l"});
153154
app.parse(argc, argv, {.skip = 1});

test/CMakeLists.txt

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,25 +55,43 @@ add_jsonschema_test_unix(format/pass_default_dialect_config_relative)
5555
add_jsonschema_test_unix(format/pass_bignum_real)
5656

5757
# Validate
58-
add_jsonschema_test_unix(validate/fail_instance_directory)
5958
add_jsonschema_test_unix(validate/fail_instance_enoent)
6059
add_jsonschema_test_unix(validate/fail_instance_invalid_json)
6160
add_jsonschema_test_unix(validate/fail_invalid_ref)
62-
add_jsonschema_test_unix(validate/fail_no_instance)
6361
add_jsonschema_test_unix(validate/fail_no_schema)
6462
add_jsonschema_test_unix(validate/fail_relative_external_ref_missing)
6563
add_jsonschema_test_unix(validate/fail_resolve_enoent)
6664
add_jsonschema_test_unix(validate/fail_resolve_directory_with_invalid_json)
6765
add_jsonschema_test_unix(validate/fail_resolve_invalid_json)
6866
add_jsonschema_test_unix(validate/fail_resolve_missing_open_brace)
6967
add_jsonschema_test_unix(validate/fail_schema_directory)
68+
add_jsonschema_test_unix(validate/pass_directory)
69+
add_jsonschema_test_unix(validate/pass_directory_verbose)
70+
add_jsonschema_test_unix(validate/fail_directory)
71+
add_jsonschema_test_unix(validate/fail_directory_verbose)
72+
add_jsonschema_test_unix(validate/fail_directory_json)
73+
add_jsonschema_test_unix(validate/pass_directory_extension)
74+
add_jsonschema_test_unix(validate/pass_directory_extension_verbose)
75+
add_jsonschema_test_unix(validate/pass_directory_ignore)
76+
add_jsonschema_test_unix(validate/pass_directory_ignore_verbose)
77+
add_jsonschema_test_unix(validate/pass_directory_multiple)
78+
add_jsonschema_test_unix(validate/pass_directory_and_file)
79+
add_jsonschema_test_unix(validate/pass_directory_ignore_jsonl)
80+
add_jsonschema_test_unix(validate/pass_cwd)
81+
add_jsonschema_test_unix(validate/pass_directory_fast)
82+
add_jsonschema_test_unix(validate/fail_directory_fast)
83+
add_jsonschema_test_unix(validate/pass_directory_fast_json)
84+
add_jsonschema_test_unix(validate/fail_directory_fast_json)
7085
add_jsonschema_test_unix(validate/fail_schema_enoent)
7186
add_jsonschema_test_unix(validate/fail_schema_invalid_json)
7287
add_jsonschema_test_unix(validate/fail_schema_non_schema)
7388
add_jsonschema_test_unix(validate/fail_schema_unknown_dialect)
7489
add_jsonschema_test_unix(validate/pass_jsonl_bigint)
7590
add_jsonschema_test_unix(validate/fail_trace)
7691
add_jsonschema_test_unix(validate/fail_trace_fast)
92+
add_jsonschema_test_unix(validate/fail_trace_jsonl)
93+
add_jsonschema_test_unix(validate/fail_trace_multiple)
94+
add_jsonschema_test_unix(validate/fail_trace_directory)
7795
add_jsonschema_test_unix(validate/pass_trace)
7896
add_jsonschema_test_unix(validate/pass_trace_fast)
7997
add_jsonschema_test_unix(validate/pass_trace_unknown_keyword)
@@ -136,6 +154,8 @@ add_jsonschema_test_unix(validate/fail_jsonl_one_json)
136154
add_jsonschema_test_unix(validate/fail_jsonl_one_json_verbose)
137155
add_jsonschema_test_unix(validate/pass_benchmark)
138156
add_jsonschema_test_unix(validate/fail_benchmark)
157+
add_jsonschema_test_unix(validate/fail_benchmark_multiple)
158+
add_jsonschema_test_unix(validate/fail_benchmark_directory)
139159
add_jsonschema_test_unix(validate/pass_benchmark_loop)
140160
add_jsonschema_test_unix(validate/pass_benchmark_loop_jsonl)
141161
add_jsonschema_test_unix(validate/fail_benchmark_zero)

test/validate/fail_2019_09.sh

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,8 @@ cat << 'EOF' > "$TMP/instance.json"
2424
EOF
2525

2626
"$1" validate "$TMP/schema.json" "$TMP/instance.json" 2> "$TMP/stderr.txt" \
27-
&& CODE="$?" || CODE="$?"
28-
test "$CODE" = "2" || exit 1
27+
&& EXIT_CODE="$?" || EXIT_CODE="$?"
28+
test "$EXIT_CODE" = "2" || exit 1
2929

3030
cat << EOF > "$TMP/expected.txt"
3131
fail: $(realpath "$TMP")/instance.json

test/validate/fail_2020_12.sh

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,8 @@ cat << 'EOF' > "$TMP/instance.json"
2424
EOF
2525

2626
"$1" validate "$TMP/schema.json" "$TMP/instance.json" 2> "$TMP/stderr.txt" \
27-
&& CODE="$?" || CODE="$?"
28-
test "$CODE" = "2" || exit 1
27+
&& EXIT_CODE="$?" || EXIT_CODE="$?"
28+
test "$EXIT_CODE" = "2" || exit 1
2929

3030
cat << EOF > "$TMP/expected.txt"
3131
fail: $(realpath "$TMP")/instance.json

test/validate/fail_2020_12_fast.sh

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,8 @@ cat << 'EOF' > "$TMP/instance.json"
2424
EOF
2525

2626
"$1" validate "$TMP/schema.json" "$TMP/instance.json" --fast 2> "$TMP/stderr.txt" \
27-
&& CODE="$?" || CODE="$?"
28-
test "$CODE" = "2" || exit 1
27+
&& EXIT_CODE="$?" || EXIT_CODE="$?"
28+
test "$EXIT_CODE" = "2" || exit 1
2929

3030
cat << EOF > "$TMP/expected.txt"
3131
fail: $(realpath "$TMP")/instance.json

test/validate/fail_benchmark.sh

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,8 @@ cat << 'EOF' > "$TMP/instance.json"
2424
EOF
2525

2626
"$1" validate "$TMP/schema.json" "$TMP/instance.json" --benchmark > "$TMP/output.txt" 2>&1 \
27-
&& CODE="$?" || CODE="$?"
28-
test "$CODE" = "2" || exit 1
27+
&& EXIT_CODE="$?" || EXIT_CODE="$?"
28+
test "$EXIT_CODE" = "2" || exit 1
2929

3030
if ! grep -E "/instance\.json: FAIL [0-9]+\.[0-9]+ \+- [0-9]+\.[0-9]+ us \([0-9]+\.[0-9]+\)$" "$TMP/output.txt" > /dev/null
3131
then
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
#!/bin/sh
2+
3+
set -o errexit
4+
set -o nounset
5+
6+
TMP="$(mktemp -d)"
7+
clean() { rm -rf "$TMP"; }
8+
trap clean EXIT
9+
10+
cat << 'EOF' > "$TMP/schema.json"
11+
{
12+
"$schema": "http://json-schema.org/draft-04/schema#",
13+
"type": "string"
14+
}
15+
EOF
16+
17+
mkdir "$TMP/instances"
18+
19+
cat << 'EOF' > "$TMP/instances/instance_1.json"
20+
"foo"
21+
EOF
22+
23+
cat << 'EOF' > "$TMP/instances/instance_2.json"
24+
"bar"
25+
EOF
26+
27+
"$1" validate "$TMP/schema.json" "$TMP/instances" --benchmark 2> "$TMP/stderr.txt" && EXIT_CODE="$?" || EXIT_CODE="$?"
28+
test "$EXIT_CODE" = "1" || exit 1
29+
30+
cat << 'EOF' > "$TMP/expected.txt"
31+
error: The `--benchmark/-b` option is only allowed given a single instance
32+
EOF
33+
34+
diff "$TMP/stderr.txt" "$TMP/expected.txt"
35+
36+
# JSON error
37+
"$1" validate "$TMP/schema.json" "$TMP/instances" --benchmark --json > "$TMP/stdout.txt" 2>&1 && EXIT_CODE="$?" || EXIT_CODE="$?"
38+
test "$EXIT_CODE" = "1" || exit 1
39+
40+
cat << 'EOF' > "$TMP/expected.txt"
41+
{
42+
"error": "The `--benchmark/-b` option is only allowed given a single instance"
43+
}
44+
EOF
45+
46+
diff "$TMP/stdout.txt" "$TMP/expected.txt"
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
#!/bin/sh
2+
3+
set -o errexit
4+
set -o nounset
5+
6+
TMP="$(mktemp -d)"
7+
clean() { rm -rf "$TMP"; }
8+
trap clean EXIT
9+
10+
cat << 'EOF' > "$TMP/schema.json"
11+
{
12+
"$schema": "http://json-schema.org/draft-04/schema#",
13+
"type": "string"
14+
}
15+
EOF
16+
17+
cat << 'EOF' > "$TMP/instance_1.json"
18+
"foo"
19+
EOF
20+
21+
cat << 'EOF' > "$TMP/instance_2.json"
22+
"bar"
23+
EOF
24+
25+
"$1" validate "$TMP/schema.json" "$TMP/instance_1.json" "$TMP/instance_2.json" --benchmark 2> "$TMP/stderr.txt" && EXIT_CODE="$?" || EXIT_CODE="$?"
26+
test "$EXIT_CODE" = "1" || exit 1
27+
28+
cat << 'EOF' > "$TMP/expected.txt"
29+
error: The `--benchmark/-b` option is only allowed given a single instance
30+
EOF
31+
32+
diff "$TMP/stderr.txt" "$TMP/expected.txt"
33+
34+
# JSON error
35+
"$1" validate "$TMP/schema.json" "$TMP/instance_1.json" "$TMP/instance_2.json" --benchmark --json > "$TMP/stdout.txt" 2>&1 && EXIT_CODE="$?" || EXIT_CODE="$?"
36+
test "$EXIT_CODE" = "1" || exit 1
37+
38+
cat << 'EOF' > "$TMP/expected.txt"
39+
{
40+
"error": "The `--benchmark/-b` option is only allowed given a single instance"
41+
}
42+
EOF
43+
44+
diff "$TMP/stdout.txt" "$TMP/expected.txt"

0 commit comments

Comments
 (0)