Skip to content

Commit 7a0ac9f

Browse files
committed
feat(validate): add --path option to target sub-schema via JSON Pointer
Signed-off-by: Vaibhav mittal <vaibhavmittal929@gmail.com>
1 parent de5d932 commit 7a0ac9f

7 files changed

Lines changed: 195 additions & 5 deletions

File tree

src/command_validate.cc

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -247,6 +247,12 @@ auto sourcemeta::jsonschema::validate(const sourcemeta::core::Options &options)
247247
schema_path, std::make_error_code(std::errc::is_a_directory)};
248248
}
249249

250+
if (options.contains("path") && !options.at("path").empty() &&
251+
options.contains("template") && !options.at("template").empty()) {
252+
throw OptionConflictError{
253+
"The --path option cannot be used with --template"};
254+
}
255+
250256
const auto schema_config_base{schema_from_stdin
251257
? std::filesystem::current_path()
252258
: std::filesystem::path(schema_path)};
@@ -258,10 +264,27 @@ auto sourcemeta::jsonschema::validate(const sourcemeta::core::Options &options)
258264
read_configuration(options, configuration_path, schema_config_base)};
259265
const auto dialect{default_dialect(options, configuration)};
260266

261-
const auto schema{schema_from_stdin
262-
? read_from_stdin().document
267+
sourcemeta::core::JSON schema{
268+
schema_from_stdin ? read_from_stdin().document
263269
: sourcemeta::core::read_yaml_or_json(schema_path)};
264270

271+
if (options.contains("path") && !options.at("path").empty()) {
272+
const auto path_string{std::string{options.at("path").front()}};
273+
const auto pointer{sourcemeta::core::to_pointer(path_string)};
274+
const auto *const result{sourcemeta::core::try_get(schema, pointer)};
275+
// We intentionally reuse NotSchemaError here to align with existing CLI
276+
// error semantics without introducing a new error type.
277+
if (!result) {
278+
throw NotSchemaError{schema_from_stdin ? stdin_path()
279+
: schema_resolution_base};
280+
}
281+
// `result` points into `schema`, so we must copy before reassigning to
282+
// avoid a use-after-free (the copy assignment destroys schema's storage
283+
// before reading from other when they alias).
284+
sourcemeta::core::JSON subschema{*result};
285+
schema = std::move(subschema);
286+
}
287+
265288
if (!sourcemeta::core::is_schema(schema)) {
266289
throw NotSchemaError{schema_from_stdin ? stdin_path()
267290
: schema_resolution_base};
@@ -291,9 +314,9 @@ auto sourcemeta::jsonschema::validate(const sourcemeta::core::Options &options)
291314

292315
const sourcemeta::core::JSON bundled{[&]() {
293316
try {
294-
return sourcemeta::core::bundle(schema, sourcemeta::core::schema_walker,
295-
custom_resolver, dialect,
296-
schema_default_id);
317+
return sourcemeta::core::bundle(
318+
std::as_const(schema), sourcemeta::core::schema_walker,
319+
custom_resolver, dialect, schema_default_id);
297320
} catch (const sourcemeta::core::SchemaKeywordError &error) {
298321
throw sourcemeta::core::FileError<sourcemeta::core::SchemaKeywordError>(
299322
schema_resolution_base, error);

src/main.cc

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ Global Options:
3838
[--benchmark/-b] [--loop <iterations>] [--extension/-e <extension>]
3939
[--ignore/-i <schemas-or-directories>] [--trace/-t] [--fast/-f]
4040
[--template/-m <template.json>] [--entrypoint/-p <pointer|uri>]
41+
[--path <pointer>]
4142
4243
Validate one or more instances against the given schema.
4344
@@ -52,6 +53,10 @@ Global Options:
5253
for error reporting purposes. Make sure they match or you will get
5354
non-sense results.
5455
56+
Use --path to extract a sub-schema from the input document using
57+
a JSON Pointer before validation. This is useful for validating
58+
against schemas embedded in larger documents, such as OpenAPI.
59+
5560
metaschema [schemas-or-directories...] [--extension/-e <extension>]
5661
[--ignore/-i <schemas-or-directories>] [--trace/-t]
5762
@@ -182,6 +187,7 @@ auto jsonschema_main(const std::string &program, const std::string &command,
182187
app.option("template", {"m"});
183188
app.option("loop", {"l"});
184189
app.option("entrypoint", {"p"});
190+
app.option("path", {});
185191
app.parse(argc, argv, {.skip = 1});
186192
sourcemeta::jsonschema::validate(app);
187193
return EXIT_SUCCESS;

test/CMakeLists.txt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -242,6 +242,10 @@ add_jsonschema_test_unix(validate/fail_entrypoint_invalid_pointer_escape)
242242
add_jsonschema_test_unix(validate/fail_entrypoint_invalid_uri_parse)
243243
add_jsonschema_test_unix(validate/fail_entrypoint_mismatch)
244244
add_jsonschema_test_unix(validate/fail_entrypoint_with_template)
245+
add_jsonschema_test_unix(validate/pass_path_openapi)
246+
add_jsonschema_test_unix(validate/fail_path_not_found)
247+
add_jsonschema_test_unix(validate/fail_path_not_schema)
248+
add_jsonschema_test_unix(validate/fail_path_with_template)
245249
add_jsonschema_test_unix(validate/pass_config_ignore)
246250
add_jsonschema_test_unix(validate/pass_config_ignore_with_cli)
247251
add_jsonschema_test_unix(validate/pass_stdin_instance)
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
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/document.json"
11+
{
12+
"components": {
13+
"schemas": {
14+
"User": {
15+
"$schema": "https://json-schema.org/draft/2020-12/schema",
16+
"type": "object"
17+
}
18+
}
19+
}
20+
}
21+
EOF
22+
23+
cat << 'EOF' > "$TMP/instance.json"
24+
{}
25+
EOF
26+
27+
"$1" validate "$TMP/document.json" "$TMP/instance.json" \
28+
--path "/components/schemas/NonExistent" 2> "$TMP/stderr.txt" \
29+
&& EXIT_CODE="$?" || EXIT_CODE="$?"
30+
# Schema input error
31+
test "$EXIT_CODE" = "4"
32+
33+
cat << EOF > "$TMP/expected.txt"
34+
error: The schema file you provided does not represent a valid JSON Schema
35+
at file path $(realpath "$TMP")/document.json
36+
EOF
37+
38+
diff "$TMP/stderr.txt" "$TMP/expected.txt"
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
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/document.json"
11+
{
12+
"components": {
13+
"schemas": {
14+
"User": [ "not", "a", "schema" ]
15+
}
16+
}
17+
}
18+
EOF
19+
20+
cat << 'EOF' > "$TMP/instance.json"
21+
{ "foo": "bar" }
22+
EOF
23+
24+
"$1" validate "$TMP/document.json" "$TMP/instance.json" \
25+
--path "/components/schemas/User" 2> "$TMP/stderr.txt" \
26+
&& EXIT_CODE="$?" || EXIT_CODE="$?"
27+
# Schema input error
28+
test "$EXIT_CODE" = "4"
29+
30+
cat << EOF > "$TMP/expected.txt"
31+
error: The schema file you provided does not represent a valid JSON Schema
32+
at file path $(realpath "$TMP")/document.json
33+
EOF
34+
35+
diff "$TMP/stderr.txt" "$TMP/expected.txt"
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
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": "https://json-schema.org/draft/2020-12/schema",
13+
"type": "object"
14+
}
15+
EOF
16+
17+
cat << 'EOF' > "$TMP/instance.json"
18+
{}
19+
EOF
20+
21+
cat << 'EOF' > "$TMP/template.json"
22+
[]
23+
EOF
24+
25+
"$1" validate "$TMP/schema.json" "$TMP/instance.json" \
26+
--path "/foo" --template "$TMP/template.json" \
27+
> "$TMP/output.txt" 2>&1 \
28+
&& EXIT_CODE="$?" || EXIT_CODE="$?"
29+
# Invalid CLI arguments
30+
test "$EXIT_CODE" = "5"
31+
32+
cat << EOF > "$TMP/expected.txt"
33+
error: The --path option cannot be used with --template
34+
EOF
35+
36+
diff "$TMP/output.txt" "$TMP/expected.txt"
37+
38+
"$1" validate "$TMP/schema.json" "$TMP/instance.json" \
39+
--path "/foo" --template "$TMP/template.json" --json \
40+
> "$TMP/output.txt" 2>&1 \
41+
&& EXIT_CODE="$?" || EXIT_CODE="$?"
42+
# Invalid CLI arguments
43+
test "$EXIT_CODE" = "5"
44+
45+
cat << EOF > "$TMP/expected.txt"
46+
{
47+
"error": "The --path option cannot be used with --template"
48+
}
49+
EOF
50+
51+
diff "$TMP/output.txt" "$TMP/expected.txt"

test/validate/pass_path_openapi.sh

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
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/openapi.json"
11+
{
12+
"openapi": "3.0.0",
13+
"info": {
14+
"title": "Test API",
15+
"version": "1.0.0"
16+
},
17+
"components": {
18+
"schemas": {
19+
"User": {
20+
"$schema": "https://json-schema.org/draft/2020-12/schema",
21+
"type": "object"
22+
}
23+
}
24+
}
25+
}
26+
EOF
27+
28+
cat << 'EOF' > "$TMP/instance.json"
29+
{ "name": "John", "age": 30 }
30+
EOF
31+
32+
"$1" validate "$TMP/openapi.json" "$TMP/instance.json" \
33+
--path "/components/schemas/User"

0 commit comments

Comments
 (0)