From 338e625183e09b9e72783dd8018f0bd94e371741 Mon Sep 17 00:00:00 2001 From: Juan Cruz Viotti Date: Fri, 12 Jun 2026 14:17:20 -0400 Subject: [PATCH] Support bundled schemas with custom meta-schemas Signed-off-by: Juan Cruz Viotti --- DEPENDENCIES | 2 +- docs/install.markdown | 7 - test/CMakeLists.txt | 9 + test/bundle/pass_bundled_metaschema.sh | 58 ++++ test/codegen/pass_bundled_metaschema.sh | 42 +++ test/compile/pass_bundled_metaschema.sh | 57 ++++ test/format/pass_bundled_metaschema.sh | 62 ++++ test/inspect/pass_bundled_metaschema.sh | 299 ++++++++++++++++++ test/lint/pass_bundled_metaschema.sh | 34 ++ test/metaschema/pass_bundled_metaschema.sh | 34 ++ test/test/pass_bundled_metaschema.sh | 53 ++++ test/validate/pass_bundled_metaschema.sh | 38 +++ vendor/blaze/src/bundle/bundle.cc | 9 +- .../bundle/include/sourcemeta/blaze/bundle.h | 3 +- vendor/blaze/src/compiler/compile.cc | 17 +- vendor/blaze/src/compiler/compile_helpers.h | 31 +- .../src/compiler/default_compiler_draft4.h | 12 +- vendor/blaze/src/compiler/unevaluated.cc | 3 +- vendor/blaze/src/foundation/foundation.cc | 145 ++++++++- vendor/blaze/src/foundation/helpers.h | 132 +++++++- .../include/sourcemeta/blaze/foundation.h | 37 +++ .../sourcemeta/blaze/foundation_walker.h | 12 +- vendor/blaze/src/foundation/walker.cc | 18 +- vendor/blaze/src/frame/frame.cc | 50 ++- .../frame/include/sourcemeta/blaze/frame.h | 6 + 25 files changed, 1098 insertions(+), 72 deletions(-) create mode 100755 test/bundle/pass_bundled_metaschema.sh create mode 100755 test/codegen/pass_bundled_metaschema.sh create mode 100755 test/compile/pass_bundled_metaschema.sh create mode 100755 test/format/pass_bundled_metaschema.sh create mode 100755 test/inspect/pass_bundled_metaschema.sh create mode 100755 test/lint/pass_bundled_metaschema.sh create mode 100755 test/metaschema/pass_bundled_metaschema.sh create mode 100755 test/test/pass_bundled_metaschema.sh create mode 100755 test/validate/pass_bundled_metaschema.sh diff --git a/DEPENDENCIES b/DEPENDENCIES index 0f2baeca..bbae9f56 100644 --- a/DEPENDENCIES +++ b/DEPENDENCIES @@ -1,7 +1,7 @@ vendorpull https://github.com/sourcemeta/vendorpull 1dcbac42809cf87cb5b045106b863e17ad84ba02 core https://github.com/sourcemeta/core eb361a9f3a01f7cffb5da7946a9516ee11dc9ede jsonbinpack https://github.com/sourcemeta/jsonbinpack ac8e1af733a781fc4c94a14157f80970ea569479 -blaze https://github.com/sourcemeta/blaze 7b214cff6d575831c16a2ce33f55d97c02eb6338 +blaze https://github.com/sourcemeta/blaze 208a2b74eb10bd81e5c0a0e24500676f30c6c97d mbedtls https://github.com/Mbed-TLS/mbedtls v3.6.6 curl https://github.com/curl/curl curl-8_20_0 nghttp2 https://github.com/nghttp2/nghttp2 v1.67.1 diff --git a/docs/install.markdown b/docs/install.markdown index 107dec24..7dfb421f 100644 --- a/docs/install.markdown +++ b/docs/install.markdown @@ -68,13 +68,6 @@ tracked in the lock file are fetched again. > We recommend committing `jsonschema.lock.json` to version control (similar to > `package-lock.json`) to enable reproducible installs across environments. -> [!WARNING] -> If a dependency uses a custom meta-schema that is itself an external -> dependency, make sure to list both the meta-schema and the schema that uses -> it in the `dependencies` map. The install command will resolve the -> meta-schema during bundling regardless of processing order. We are working -> on resolving this automatically in a future release. - Use the global `--header/-H` flag to attach custom HTTP headers to outgoing requests. For example: diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 0f1d71c5..1c4c32a3 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -104,6 +104,7 @@ add_jsonschema_test_unix(format/pass_test_document_with_comment) add_jsonschema_test_unix(format/pass_test_document_explicit_dialect) add_jsonschema_test_unix(format/pass_test_document_directory) add_jsonschema_test_unix(format/pass_test_document_schema_keyword) +add_jsonschema_test_unix(format/pass_bundled_metaschema) # Validate add_jsonschema_test_unix(validate/fail_instance_enoent) @@ -326,6 +327,7 @@ add_jsonschema_test_unix(validate/pass_stdin_yaml) add_jsonschema_test_unix(validate/fail_stdin_trace_multi_instance) add_jsonschema_test_unix(validate/fail_trace_benchmark) add_jsonschema_test_unix(validate/fail_invalid_header) +add_jsonschema_test_unix(validate/pass_bundled_metaschema) # Metaschema add_jsonschema_test_unix(metaschema/pass_trace) @@ -378,6 +380,7 @@ add_jsonschema_test_unix(metaschema/fail_stdin_not_schema) add_jsonschema_test_unix(metaschema/fail_stdin_duplicate) add_jsonschema_test_unix(metaschema/fail_stdin_invalid_json) add_jsonschema_test_unix(metaschema/fail_invalid_header) +add_jsonschema_test_unix(metaschema/pass_bundled_metaschema) # Test add_jsonschema_test_unix(test/fail_resolve_directory_non_schema) @@ -474,6 +477,7 @@ add_jsonschema_test_unix(test/fail_stdin_invalid_json) add_jsonschema_test_unix(test/fail_stdin_not_object) add_jsonschema_test_unix(test/fail_stdin_test_failure) add_jsonschema_test_unix(test/fail_invalid_header) +add_jsonschema_test_unix(test/pass_bundled_metaschema) # Bundle add_jsonschema_test_unix(bundle/pass_into_resolve_directory) @@ -523,6 +527,7 @@ add_jsonschema_test_unix(bundle/pass_stdin) add_jsonschema_test_unix(bundle/fail_stdin_not_schema) add_jsonschema_test_unix(bundle/fail_stdin_invalid_json) add_jsonschema_test_unix(bundle/fail_invalid_header) +add_jsonschema_test_unix(bundle/pass_bundled_metaschema) # Inspect add_jsonschema_test_unix(inspect/pass) @@ -561,6 +566,7 @@ add_jsonschema_test_unix(inspect/pass_stdin_json) add_jsonschema_test_unix(inspect/pass_stdin_no_id) add_jsonschema_test_unix(inspect/fail_stdin_not_schema) add_jsonschema_test_unix(inspect/fail_stdin_invalid_json) +add_jsonschema_test_unix(inspect/pass_bundled_metaschema) # Compile add_jsonschema_test_unix(compile/pass) @@ -609,6 +615,7 @@ add_jsonschema_test_unix(compile/fail_entrypoint_invalid_uri_parse) add_jsonschema_test_unix(compile/pass_evaluate_port_js_fast) add_jsonschema_test_unix(compile/pass_evaluate_port_js_exhaustive) add_jsonschema_test_unix(compile/fail_invalid_header) +add_jsonschema_test_unix(compile/pass_bundled_metaschema) # Codegen add_jsonschema_test_unix(codegen/pass_typescript) @@ -642,6 +649,7 @@ add_jsonschema_test_unix(codegen/fail_unsupported_schema) add_jsonschema_test_unix(codegen/fail_invalid_id_type) add_jsonschema_test_unix(codegen/fail_invalid_schema_uri) add_jsonschema_test_unix(codegen/fail_anchor_collision) +add_jsonschema_test_unix(codegen/pass_bundled_metaschema) # Lint add_jsonschema_test_unix(lint/pass_lint_fix) @@ -809,6 +817,7 @@ add_jsonschema_test_unix(lint/fail_stdin_lint) add_jsonschema_test_unix(lint/fail_stdin_duplicate) add_jsonschema_test_unix(lint/fail_stdin_invalid_json) add_jsonschema_test_unix(lint/fail_invalid_header) +add_jsonschema_test_unix(lint/pass_bundled_metaschema) # Install add_jsonschema_test_unix(install/fail_no_configuration) diff --git a/test/bundle/pass_bundled_metaschema.sh b/test/bundle/pass_bundled_metaschema.sh new file mode 100755 index 00000000..59702181 --- /dev/null +++ b/test/bundle/pass_bundled_metaschema.sh @@ -0,0 +1,58 @@ +#!/bin/sh + +set -o errexit +set -o nounset + +TMP="$(mktemp -d)" +clean() { rm -rf "$TMP"; } +trap clean EXIT + +cat << 'EOF' > "$TMP/schema.json" +{ + "$schema": "https://example.com/meta", + "$id": "https://example.com/schema", + "type": "string", + "$defs": { + "https://example.com/meta": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://example.com/meta", + "$vocabulary": { + "https://json-schema.org/draft/2020-12/vocab/core": true, + "https://json-schema.org/draft/2020-12/vocab/validation": true + }, + "type": "object" + } + } +} +EOF + +"$1" bundle "$TMP/schema.json" > "$TMP/result.json" 2> "$TMP/stderr.txt" + +cat << 'EOF' > "$TMP/expected_stderr.txt" +EOF + +diff "$TMP/stderr.txt" "$TMP/expected_stderr.txt" + +cat << 'EOF' > "$TMP/expected.json" +{ + "$schema": "https://example.com/meta", + "$id": "https://example.com/schema", + "type": "string", + "$defs": { + "https://example.com/meta": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://example.com/meta", + "$vocabulary": { + "https://json-schema.org/draft/2020-12/vocab/core": true, + "https://json-schema.org/draft/2020-12/vocab/validation": true + }, + "type": "object" + } + } +} +EOF + +diff "$TMP/result.json" "$TMP/expected.json" + +# Must come out formatted +"$1" fmt "$TMP/result.json" --check diff --git a/test/codegen/pass_bundled_metaschema.sh b/test/codegen/pass_bundled_metaschema.sh new file mode 100755 index 00000000..7d1e6b70 --- /dev/null +++ b/test/codegen/pass_bundled_metaschema.sh @@ -0,0 +1,42 @@ +#!/bin/sh + +set -o errexit +set -o nounset + +TMP="$(mktemp -d)" +clean() { rm -rf "$TMP"; } +trap clean EXIT + +cat << 'EOF' > "$TMP/schema.json" +{ + "$schema": "https://example.com/meta", + "$id": "https://example.com/schema", + "type": "string", + "$defs": { + "https://example.com/meta": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://example.com/meta", + "$vocabulary": { + "https://json-schema.org/draft/2020-12/vocab/core": true, + "https://json-schema.org/draft/2020-12/vocab/validation": true + }, + "type": "object" + } + } +} +EOF + +"$1" codegen "$TMP/schema.json" --target typescript > "$TMP/result.txt" 2> "$TMP/stderr.txt" + +cat << 'EOF' > "$TMP/expected_stderr.txt" +EOF + +diff "$TMP/stderr.txt" "$TMP/expected_stderr.txt" + +cat << 'EOF' > "$TMP/expected.txt" +export type SchemaMeta = Record; + +export type Schema = string; +EOF + +diff "$TMP/result.txt" "$TMP/expected.txt" diff --git a/test/compile/pass_bundled_metaschema.sh b/test/compile/pass_bundled_metaschema.sh new file mode 100755 index 00000000..08233d13 --- /dev/null +++ b/test/compile/pass_bundled_metaschema.sh @@ -0,0 +1,57 @@ +#!/bin/sh + +set -o errexit +set -o nounset + +TMP="$(mktemp -d)" +clean() { rm -rf "$TMP"; } +trap clean EXIT + +cat << 'EOF' > "$TMP/schema.json" +{ + "$schema": "https://example.com/meta", + "$id": "https://example.com/schema", + "type": "string", + "$defs": { + "https://example.com/meta": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://example.com/meta", + "$vocabulary": { + "https://json-schema.org/draft/2020-12/vocab/core": true, + "https://json-schema.org/draft/2020-12/vocab/validation": true + }, + "type": "object" + } + } +} +EOF + +"$1" compile "$TMP/schema.json" > "$TMP/template.json" 2> "$TMP/stderr.txt" + +cat << 'EOF' > "$TMP/expected_stderr.txt" +EOF + +diff "$TMP/stderr.txt" "$TMP/expected_stderr.txt" + +cat << 'EOF' > "$TMP/expected.json" +[ + 5, + false, + true, + [ + [ + [ + 11, + [ "type" ], + [], + "https://example.com/schema#/type", + 3, + [ 8, 4 ] + ] + ] + ], + [] +] +EOF + +diff "$TMP/template.json" "$TMP/expected.json" diff --git a/test/format/pass_bundled_metaschema.sh b/test/format/pass_bundled_metaschema.sh new file mode 100755 index 00000000..944b46af --- /dev/null +++ b/test/format/pass_bundled_metaschema.sh @@ -0,0 +1,62 @@ +#!/bin/sh + +set -o errexit +set -o nounset + +TMP="$(mktemp -d)" +clean() { rm -rf "$TMP"; } +trap clean EXIT + +cat << 'EOF' > "$TMP/schema.json" +{ + "$id": "https://example.com/schema", + "$schema": "https://example.com/meta", + "$defs": { + "https://example.com/meta": { + "$id": "https://example.com/meta", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$vocabulary": { + "https://json-schema.org/draft/2020-12/vocab/core": true, + "https://json-schema.org/draft/2020-12/vocab/validation": true + }, + "type": "object" + } + }, + "type": "string" +} +EOF + +"$1" fmt "$TMP/schema.json" > "$TMP/output.txt" 2>&1 + +cat << 'EOF' > "$TMP/expected_output.txt" +EOF + +diff "$TMP/output.txt" "$TMP/expected_output.txt" + +cat << 'EOF' > "$TMP/expected.json" +{ + "$schema": "https://example.com/meta", + "$id": "https://example.com/schema", + "type": "string", + "$defs": { + "https://example.com/meta": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://example.com/meta", + "$vocabulary": { + "https://json-schema.org/draft/2020-12/vocab/core": true, + "https://json-schema.org/draft/2020-12/vocab/validation": true + }, + "type": "object" + } + } +} +EOF + +diff "$TMP/schema.json" "$TMP/expected.json" + +"$1" fmt "$TMP/schema.json" --check > "$TMP/check_output.txt" 2>&1 + +cat << 'EOF' > "$TMP/expected_check_output.txt" +EOF + +diff "$TMP/check_output.txt" "$TMP/expected_check_output.txt" diff --git a/test/inspect/pass_bundled_metaschema.sh b/test/inspect/pass_bundled_metaschema.sh new file mode 100755 index 00000000..84239f4c --- /dev/null +++ b/test/inspect/pass_bundled_metaschema.sh @@ -0,0 +1,299 @@ +#!/bin/sh + +set -o errexit +set -o nounset + +TMP="$(mktemp -d)" +clean() { rm -rf "$TMP"; } +trap clean EXIT + +cat << 'EOF' > "$TMP/schema.json" +{ + "$schema": "https://example.com/meta", + "$id": "https://example.com/schema", + "type": "string", + "$defs": { + "https://example.com/meta": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://example.com/meta", + "$vocabulary": { + "https://json-schema.org/draft/2020-12/vocab/core": true, + "https://json-schema.org/draft/2020-12/vocab/validation": true + }, + "type": "object" + } + } +} +EOF + +"$1" inspect "$TMP/schema.json" > "$TMP/result.txt" 2> "$TMP/stderr.txt" + +cat << 'EOF' > "$TMP/expected_stderr.txt" +EOF + +diff "$TMP/stderr.txt" "$TMP/expected_stderr.txt" + +cat << 'EOF' > "$TMP/expected.txt" +(RESOURCE) URI: https://example.com/meta + Type : Static + Root : https://example.com/schema + Pointer : /$defs/https:~1~1example.com~1meta + File Position : 6:5 + Base : https://example.com/meta + Relative Pointer : + Dialect : https://json-schema.org/draft/2020-12/schema + Base Dialect : https://json-schema.org/draft/2020-12/schema + Parent : + Property Name : no + Orphan : yes + +(POINTER) URI: https://example.com/meta#/$id + Type : Static + Root : https://example.com/schema + Pointer : /$defs/https:~1~1example.com~1meta/$id + File Position : 8:7 + Base : https://example.com/meta + Relative Pointer : /$id + Dialect : https://json-schema.org/draft/2020-12/schema + Base Dialect : https://json-schema.org/draft/2020-12/schema + Parent : /$defs/https:~1~1example.com~1meta + Property Name : no + Orphan : yes + +(POINTER) URI: https://example.com/meta#/$schema + Type : Static + Root : https://example.com/schema + Pointer : /$defs/https:~1~1example.com~1meta/$schema + File Position : 7:7 + Base : https://example.com/meta + Relative Pointer : /$schema + Dialect : https://json-schema.org/draft/2020-12/schema + Base Dialect : https://json-schema.org/draft/2020-12/schema + Parent : /$defs/https:~1~1example.com~1meta + Property Name : no + Orphan : yes + +(POINTER) URI: https://example.com/meta#/$vocabulary + Type : Static + Root : https://example.com/schema + Pointer : /$defs/https:~1~1example.com~1meta/$vocabulary + File Position : 9:7 + Base : https://example.com/meta + Relative Pointer : /$vocabulary + Dialect : https://json-schema.org/draft/2020-12/schema + Base Dialect : https://json-schema.org/draft/2020-12/schema + Parent : /$defs/https:~1~1example.com~1meta + Property Name : no + Orphan : yes + +(POINTER) URI: https://example.com/meta#/$vocabulary/https:~1~1json-schema.org~1draft~12020-12~1vocab~1core + Type : Static + Root : https://example.com/schema + Pointer : /$defs/https:~1~1example.com~1meta/$vocabulary/https:~1~1json-schema.org~1draft~12020-12~1vocab~1core + File Position : 10:9 + Base : https://example.com/meta + Relative Pointer : /$vocabulary/https:~1~1json-schema.org~1draft~12020-12~1vocab~1core + Dialect : https://json-schema.org/draft/2020-12/schema + Base Dialect : https://json-schema.org/draft/2020-12/schema + Parent : /$defs/https:~1~1example.com~1meta + Property Name : no + Orphan : yes + +(POINTER) URI: https://example.com/meta#/$vocabulary/https:~1~1json-schema.org~1draft~12020-12~1vocab~1validation + Type : Static + Root : https://example.com/schema + Pointer : /$defs/https:~1~1example.com~1meta/$vocabulary/https:~1~1json-schema.org~1draft~12020-12~1vocab~1validation + File Position : 11:9 + Base : https://example.com/meta + Relative Pointer : /$vocabulary/https:~1~1json-schema.org~1draft~12020-12~1vocab~1validation + Dialect : https://json-schema.org/draft/2020-12/schema + Base Dialect : https://json-schema.org/draft/2020-12/schema + Parent : /$defs/https:~1~1example.com~1meta + Property Name : no + Orphan : yes + +(POINTER) URI: https://example.com/meta#/type + Type : Static + Root : https://example.com/schema + Pointer : /$defs/https:~1~1example.com~1meta/type + File Position : 13:7 + Base : https://example.com/meta + Relative Pointer : /type + Dialect : https://json-schema.org/draft/2020-12/schema + Base Dialect : https://json-schema.org/draft/2020-12/schema + Parent : /$defs/https:~1~1example.com~1meta + Property Name : no + Orphan : yes + +(RESOURCE) URI: https://example.com/schema + Type : Static + Root : https://example.com/schema + Pointer : + File Position : 1:1 + Base : https://example.com/schema + Relative Pointer : + Dialect : https://example.com/meta + Base Dialect : https://json-schema.org/draft/2020-12/schema + Parent : + Property Name : no + Orphan : no + +(POINTER) URI: https://example.com/schema#/$defs + Type : Static + Root : https://example.com/schema + Pointer : /$defs + File Position : 5:3 + Base : https://example.com/schema + Relative Pointer : /$defs + Dialect : https://example.com/meta + Base Dialect : https://json-schema.org/draft/2020-12/schema + Parent : + Property Name : no + Orphan : no + +(SUBSCHEMA) URI: https://example.com/schema#/$defs/https:~1~1example.com~1meta + Type : Static + Root : https://example.com/schema + Pointer : /$defs/https:~1~1example.com~1meta + File Position : 6:5 + Base : https://example.com/meta + Relative Pointer : + Dialect : https://json-schema.org/draft/2020-12/schema + Base Dialect : https://json-schema.org/draft/2020-12/schema + Parent : + Property Name : no + Orphan : yes + +(POINTER) URI: https://example.com/schema#/$defs/https:~1~1example.com~1meta/$id + Type : Static + Root : https://example.com/schema + Pointer : /$defs/https:~1~1example.com~1meta/$id + File Position : 8:7 + Base : https://example.com/meta + Relative Pointer : /$id + Dialect : https://json-schema.org/draft/2020-12/schema + Base Dialect : https://json-schema.org/draft/2020-12/schema + Parent : /$defs/https:~1~1example.com~1meta + Property Name : no + Orphan : yes + +(POINTER) URI: https://example.com/schema#/$defs/https:~1~1example.com~1meta/$schema + Type : Static + Root : https://example.com/schema + Pointer : /$defs/https:~1~1example.com~1meta/$schema + File Position : 7:7 + Base : https://example.com/meta + Relative Pointer : /$schema + Dialect : https://json-schema.org/draft/2020-12/schema + Base Dialect : https://json-schema.org/draft/2020-12/schema + Parent : /$defs/https:~1~1example.com~1meta + Property Name : no + Orphan : yes + +(POINTER) URI: https://example.com/schema#/$defs/https:~1~1example.com~1meta/$vocabulary + Type : Static + Root : https://example.com/schema + Pointer : /$defs/https:~1~1example.com~1meta/$vocabulary + File Position : 9:7 + Base : https://example.com/meta + Relative Pointer : /$vocabulary + Dialect : https://json-schema.org/draft/2020-12/schema + Base Dialect : https://json-schema.org/draft/2020-12/schema + Parent : /$defs/https:~1~1example.com~1meta + Property Name : no + Orphan : yes + +(POINTER) URI: https://example.com/schema#/$defs/https:~1~1example.com~1meta/$vocabulary/https:~1~1json-schema.org~1draft~12020-12~1vocab~1core + Type : Static + Root : https://example.com/schema + Pointer : /$defs/https:~1~1example.com~1meta/$vocabulary/https:~1~1json-schema.org~1draft~12020-12~1vocab~1core + File Position : 10:9 + Base : https://example.com/meta + Relative Pointer : /$vocabulary/https:~1~1json-schema.org~1draft~12020-12~1vocab~1core + Dialect : https://json-schema.org/draft/2020-12/schema + Base Dialect : https://json-schema.org/draft/2020-12/schema + Parent : /$defs/https:~1~1example.com~1meta + Property Name : no + Orphan : yes + +(POINTER) URI: https://example.com/schema#/$defs/https:~1~1example.com~1meta/$vocabulary/https:~1~1json-schema.org~1draft~12020-12~1vocab~1validation + Type : Static + Root : https://example.com/schema + Pointer : /$defs/https:~1~1example.com~1meta/$vocabulary/https:~1~1json-schema.org~1draft~12020-12~1vocab~1validation + File Position : 11:9 + Base : https://example.com/meta + Relative Pointer : /$vocabulary/https:~1~1json-schema.org~1draft~12020-12~1vocab~1validation + Dialect : https://json-schema.org/draft/2020-12/schema + Base Dialect : https://json-schema.org/draft/2020-12/schema + Parent : /$defs/https:~1~1example.com~1meta + Property Name : no + Orphan : yes + +(POINTER) URI: https://example.com/schema#/$defs/https:~1~1example.com~1meta/type + Type : Static + Root : https://example.com/schema + Pointer : /$defs/https:~1~1example.com~1meta/type + File Position : 13:7 + Base : https://example.com/meta + Relative Pointer : /type + Dialect : https://json-schema.org/draft/2020-12/schema + Base Dialect : https://json-schema.org/draft/2020-12/schema + Parent : /$defs/https:~1~1example.com~1meta + Property Name : no + Orphan : yes + +(POINTER) URI: https://example.com/schema#/$id + Type : Static + Root : https://example.com/schema + Pointer : /$id + File Position : 3:3 + Base : https://example.com/schema + Relative Pointer : /$id + Dialect : https://example.com/meta + Base Dialect : https://json-schema.org/draft/2020-12/schema + Parent : + Property Name : no + Orphan : no + +(POINTER) URI: https://example.com/schema#/$schema + Type : Static + Root : https://example.com/schema + Pointer : /$schema + File Position : 2:3 + Base : https://example.com/schema + Relative Pointer : /$schema + Dialect : https://example.com/meta + Base Dialect : https://json-schema.org/draft/2020-12/schema + Parent : + Property Name : no + Orphan : no + +(POINTER) URI: https://example.com/schema#/type + Type : Static + Root : https://example.com/schema + Pointer : /type + File Position : 4:3 + Base : https://example.com/schema + Relative Pointer : /type + Dialect : https://example.com/meta + Base Dialect : https://json-schema.org/draft/2020-12/schema + Parent : + Property Name : no + Orphan : no + +(REFERENCE) ORIGIN: /$defs/https:~1~1example.com~1meta/$schema + Type : Static + File Position : 7:7 + Destination : https://json-schema.org/draft/2020-12/schema + - (w/o fragment) : https://json-schema.org/draft/2020-12/schema + - (fragment) : + +(REFERENCE) ORIGIN: /$schema + Type : Static + File Position : 2:3 + Destination : https://example.com/meta + - (w/o fragment) : https://example.com/meta + - (fragment) : +EOF + +diff "$TMP/result.txt" "$TMP/expected.txt" diff --git a/test/lint/pass_bundled_metaschema.sh b/test/lint/pass_bundled_metaschema.sh new file mode 100755 index 00000000..001f3b89 --- /dev/null +++ b/test/lint/pass_bundled_metaschema.sh @@ -0,0 +1,34 @@ +#!/bin/sh + +set -o errexit +set -o nounset + +TMP="$(mktemp -d)" +clean() { rm -rf "$TMP"; } +trap clean EXIT + +cat << 'EOF' > "$TMP/schema.json" +{ + "$schema": "https://example.com/meta", + "$id": "https://example.com/schema", + "type": "string", + "$defs": { + "https://example.com/meta": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://example.com/meta", + "$vocabulary": { + "https://json-schema.org/draft/2020-12/vocab/core": true, + "https://json-schema.org/draft/2020-12/vocab/validation": true + }, + "type": "object" + } + } +} +EOF + +"$1" lint "$TMP/schema.json" > "$TMP/result.txt" 2>&1 + +cat << 'EOF' > "$TMP/output.txt" +EOF + +diff "$TMP/result.txt" "$TMP/output.txt" diff --git a/test/metaschema/pass_bundled_metaschema.sh b/test/metaschema/pass_bundled_metaschema.sh new file mode 100755 index 00000000..49195149 --- /dev/null +++ b/test/metaschema/pass_bundled_metaschema.sh @@ -0,0 +1,34 @@ +#!/bin/sh + +set -o errexit +set -o nounset + +TMP="$(mktemp -d)" +clean() { rm -rf "$TMP"; } +trap clean EXIT + +cat << 'EOF' > "$TMP/schema.json" +{ + "$schema": "https://example.com/meta", + "$id": "https://example.com/schema", + "type": "string", + "$defs": { + "https://example.com/meta": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://example.com/meta", + "$vocabulary": { + "https://json-schema.org/draft/2020-12/vocab/core": true, + "https://json-schema.org/draft/2020-12/vocab/validation": true + }, + "type": "object" + } + } +} +EOF + +"$1" metaschema "$TMP/schema.json" > "$TMP/output.txt" 2>&1 + +cat << 'EOF' > "$TMP/expected.txt" +EOF + +diff "$TMP/output.txt" "$TMP/expected.txt" diff --git a/test/test/pass_bundled_metaschema.sh b/test/test/pass_bundled_metaschema.sh new file mode 100755 index 00000000..cfc31f70 --- /dev/null +++ b/test/test/pass_bundled_metaschema.sh @@ -0,0 +1,53 @@ +#!/bin/sh + +set -o errexit +set -o nounset + +TMP="$(mktemp -d)" +clean() { rm -rf "$TMP"; } +trap clean EXIT + +cat << 'EOF' > "$TMP/schema.json" +{ + "$schema": "https://example.com/meta", + "$id": "https://example.com/schema", + "type": "string", + "$defs": { + "https://example.com/meta": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://example.com/meta", + "$vocabulary": { + "https://json-schema.org/draft/2020-12/vocab/core": true, + "https://json-schema.org/draft/2020-12/vocab/validation": true + }, + "type": "object" + } + } +} +EOF + +cat << 'EOF' > "$TMP/test.json" +{ + "target": "./schema.json", + "tests": [ + { + "description": "Valid string", + "valid": true, + "data": "hello" + }, + { + "description": "Invalid type", + "valid": false, + "data": 1 + } + ] +} +EOF + +"$1" test "$TMP/test.json" 1> "$TMP/output.txt" 2>&1 + +cat << EOF > "$TMP/expected.txt" +$(realpath "$TMP")/test.json: PASS 2/2 +EOF + +diff "$TMP/output.txt" "$TMP/expected.txt" diff --git a/test/validate/pass_bundled_metaschema.sh b/test/validate/pass_bundled_metaschema.sh new file mode 100755 index 00000000..d62ca15d --- /dev/null +++ b/test/validate/pass_bundled_metaschema.sh @@ -0,0 +1,38 @@ +#!/bin/sh + +set -o errexit +set -o nounset + +TMP="$(mktemp -d)" +clean() { rm -rf "$TMP"; } +trap clean EXIT + +cat << 'EOF' > "$TMP/schema.json" +{ + "$schema": "https://example.com/meta", + "$id": "https://example.com/schema", + "type": "string", + "$defs": { + "https://example.com/meta": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://example.com/meta", + "$vocabulary": { + "https://json-schema.org/draft/2020-12/vocab/core": true, + "https://json-schema.org/draft/2020-12/vocab/validation": true + }, + "type": "object" + } + } +} +EOF + +cat << 'EOF' > "$TMP/instance.json" +"hello" +EOF + +"$1" validate "$TMP/schema.json" "$TMP/instance.json" > "$TMP/output.txt" 2>&1 + +cat << 'EOF' > "$TMP/expected.txt" +EOF + +diff "$TMP/output.txt" "$TMP/expected.txt" diff --git a/vendor/blaze/src/bundle/bundle.cc b/vendor/blaze/src/bundle/bundle.cc index 74ee2c58..d7d633d0 100644 --- a/vendor/blaze/src/bundle/bundle.cc +++ b/vendor/blaze/src/bundle/bundle.cc @@ -100,9 +100,16 @@ auto dependencies_internal( } callback(origin, pointer, identifier, remote.value()); + visited.emplace(identifier); + + // Official schemas can only reference other official schemas, so + // recursing into them can never surface further dependencies + if (sourcemeta::blaze::is_official_schema(identifier)) { + return; + } + found.emplace_back(std::move(remote).value(), sourcemeta::core::JSON::String{identifier}); - visited.emplace(identifier); }); for (const auto &entry : found) { diff --git a/vendor/blaze/src/bundle/include/sourcemeta/blaze/bundle.h b/vendor/blaze/src/bundle/include/sourcemeta/blaze/bundle.h index b5160e79..e7d90c8f 100644 --- a/vendor/blaze/src/bundle/include/sourcemeta/blaze/bundle.h +++ b/vendor/blaze/src/bundle/include/sourcemeta/blaze/bundle.h @@ -52,7 +52,8 @@ enum class BundleMode : std::uint8_t { /// @ingroup bundle /// /// This function recursively traverses and reports the external references in a -/// schema. For example: +/// schema. References to official schemas are reported but not traversed into, +/// as official schemas can only reference other official schemas. For example: /// /// ```cpp /// #include diff --git a/vendor/blaze/src/compiler/compile.cc b/vendor/blaze/src/compiler/compile.cc index 7cf3354a..2e3a3e16 100644 --- a/vendor/blaze/src/compiler/compile.cc +++ b/vendor/blaze/src/compiler/compile.cc @@ -21,8 +21,7 @@ namespace { auto compile_subschema(const sourcemeta::blaze::Context &context, const sourcemeta::blaze::SchemaContext &schema_context, - const sourcemeta::blaze::DynamicContext &dynamic_context, - const std::string_view default_dialect) + const sourcemeta::blaze::DynamicContext &dynamic_context) -> sourcemeta::blaze::Instructions { using namespace sourcemeta::blaze; assert(is_schema(schema_context.schema)); @@ -45,8 +44,8 @@ auto compile_subschema(const sourcemeta::blaze::Context &context, Instructions steps; for (const auto &entry : sourcemeta::blaze::SchemaKeywordIterator{ - schema_context.schema, context.walker, context.resolver, - default_dialect}) { + schema_context.schema, context.walker, + schema_context.vocabularies}) { assert(entry.pointer.back().is_property()); const auto &keyword{entry.pointer.back().to_property()}; // Bases must not contain fragments @@ -381,8 +380,8 @@ auto compile(const sourcemeta::core::JSON &schema, } auto subschema{sourcemeta::core::get(context.root, entry.pointer)}; - auto nested_vocabularies{sourcemeta::blaze::vocabularies( - subschema, context.resolver, entry.dialect)}; + auto nested_vocabularies{ + context.frame.vocabularies(entry, context.resolver)}; const auto nested_relative_pointer{ entry.pointer.slice(entry.relative_pointer)}; const sourcemeta::core::URI nested_base{entry.base}; @@ -496,15 +495,13 @@ auto compile(const Context &context, const SchemaContext &schema_context, context, {.relative_pointer = new_relative_pointer, .schema = new_schema, - .vocabularies = - vocabularies(new_schema, context.resolver, entry.dialect), + .vocabularies = context.frame.vocabularies(entry, context.resolver), .base = new_base, .is_property_name = schema_context.is_property_name}, {.keyword = dynamic_context.keyword, .base_schema_location = destination_pointer, .base_instance_location = - dynamic_context.base_instance_location.concat(instance_suffix)}, - entry.dialect); + dynamic_context.base_instance_location.concat(instance_suffix)}); } } // namespace sourcemeta::blaze diff --git a/vendor/blaze/src/compiler/compile_helpers.h b/vendor/blaze/src/compiler/compile_helpers.h index 1a93740b..8bfb3341 100644 --- a/vendor/blaze/src/compiler/compile_helpers.h +++ b/vendor/blaze/src/compiler/compile_helpers.h @@ -9,6 +9,7 @@ #include // assert #include // std::cref #include // std::distance +#include // std::optional #include // std::regex, std::regex_match, std::smatch #include // std::declval, std::move @@ -186,13 +187,27 @@ inline auto static_frame_entry(const Context &context, return context.frame.locations().at({type, current}); } -inline auto walk_subschemas(const Context &context, - const SchemaContext &schema_context, - const DynamicContext &dynamic_context) -> auto { +// Whether the current keyword value, as a schema, contains any nested +// subschema. Note that while the schema of the schema context of a keyword +// compiler is the parent subschema, its relative pointer already targets +// the keyword value, so the frame entry we look up corresponds to the +// keyword value and not to the parent subschema +inline auto defines_nested_subschemas(const Context &context, + const SchemaContext &schema_context) + -> bool { const auto &entry{static_frame_entry(context, schema_context)}; - return sourcemeta::blaze::SchemaIterator{ - schema_context.schema.at(dynamic_context.keyword), context.walker, - context.resolver, entry.dialect}; + for (const auto &location : context.frame.locations()) { + if ((location.second.type == + sourcemeta::blaze::SchemaFrame::LocationType::Subschema || + location.second.type == + sourcemeta::blaze::SchemaFrame::LocationType::Resource) && + location.second.pointer.starts_with(entry.pointer) && + location.second.pointer.size() > entry.pointer.size()) { + return true; + } + } + + return false; } // TODO: Get rid of this given the new Core regex optimisations @@ -266,8 +281,8 @@ inline auto find_adjacent(const Context &context, possible_keyword_uri})}; const auto &subschema{ sourcemeta::core::get(context.root, frame_entry.pointer)}; - const auto &subschema_vocabularies{sourcemeta::blaze::vocabularies( - subschema, context.resolver, frame_entry.dialect)}; + const auto subschema_vocabularies{ + context.frame.vocabularies(frame_entry, context.resolver)}; if (std::ranges::any_of(vocabularies, [&subschema_vocabularies](const auto &vocabulary) { diff --git a/vendor/blaze/src/compiler/default_compiler_draft4.h b/vendor/blaze/src/compiler/default_compiler_draft4.h index 08f9a551..995c3f96 100644 --- a/vendor/blaze/src/compiler/default_compiler_draft4.h +++ b/vendor/blaze/src/compiler/default_compiler_draft4.h @@ -182,15 +182,7 @@ auto compiler_draft4_applicator_not(const Context &context, const SchemaContext &schema_context, const DynamicContext &dynamic_context, const Instructions &) -> Instructions { - std::size_t subschemas{0}; - for (const auto &subschema : - walk_subschemas(context, schema_context, dynamic_context)) { - if (subschema.pointer.empty()) { - continue; - } - - subschemas += 1; - } + const auto subschemas{defines_nested_subschemas(context, schema_context)}; Instructions children{compile(context, schema_context, relative_dynamic_context(), @@ -208,7 +200,7 @@ auto compiler_draft4_applicator_not(const Context &context, // evaluation if we really need it. If the "not" subschema // does not define applicators, then that's an easy case // we can skip - if (subschemas > 0 && + if (subschemas && (requires_evaluation(context, schema_context) || track_items)) { return {make(sourcemeta::blaze::InstructionIndex::LogicalNotEvaluate, context, schema_context, dynamic_context, ValueNone{}, diff --git a/vendor/blaze/src/compiler/unevaluated.cc b/vendor/blaze/src/compiler/unevaluated.cc index 1cc8f61a..66d2f747 100644 --- a/vendor/blaze/src/compiler/unevaluated.cc +++ b/vendor/blaze/src/compiler/unevaluated.cc @@ -22,8 +22,7 @@ auto find_adjacent_dependencies( return; } - const auto subschema_vocabularies{ - vocabularies(subschema, resolver, entry.dialect)}; + const auto subschema_vocabularies{frame.vocabularies(entry, resolver)}; for (const auto &property : subschema.as_object()) { if (property.first == current && entry.pointer == root.pointer) { diff --git a/vendor/blaze/src/foundation/foundation.cc b/vendor/blaze/src/foundation/foundation.cc index 438ffba3..e3f54b0a 100644 --- a/vendor/blaze/src/foundation/foundation.cc +++ b/vendor/blaze/src/foundation/foundation.cc @@ -265,6 +265,102 @@ auto sourcemeta::blaze::dialect(const sourcemeta::core::JSON &schema, return dialect_value.to_string(); } +// A meta-schema that is not known to the resolver may still be embedded in +// the document itself. Across every official base dialect, the only +// containers that can hold embedded resources are `$defs` and `definitions`, +// which no custom dialect can redefine away. A candidate only counts if its +// entire meta-schema chain terminates at an official base dialect and every +// embedded link declares its identifier and sits in a container in a way +// that is valid for such base dialect +auto sourcemeta::blaze::metaschema_try_embedded( + const sourcemeta::core::JSON &schema, const std::string_view identifier, + const SchemaResolver &resolver) -> const sourcemeta::core::JSON * { + // Relative or invalid meta-schema references are not acceptable + // according to the JSON Schema specifications + if (!sourcemeta::core::URI::is_uri(identifier)) { + return nullptr; + } + + const auto candidate{ + sourcemeta::blaze::embedded_metaschema_candidate(schema, identifier)}; + if (!candidate.first) { + return nullptr; + } + + std::unordered_set visited; + std::vector links{ + {.schema = candidate.first, + .identifier = identifier, + .container = candidate.second}}; + // Chain links that the resolver knows about are returned by value, so we + // keep them alive while we walk the chain, in a container that never + // relocates its elements, as we hold views into them + std::deque resolved; + const auto *current{candidate.first}; + std::string_view current_identifier{identifier}; + std::optional terminal; + + while (true) { + // The meta-schema is present, but its chain can never terminate at an + // official base dialect, just like a self-descriptive or cyclic + // meta-schema that the resolver knows about + if (!visited.emplace(current_identifier).second) { + throw sourcemeta::blaze::SchemaUnknownBaseDialectError(); + } + + if (!current->is_object()) { + throw sourcemeta::blaze::SchemaUnknownBaseDialectError(); + } + + const auto *metaschema_dialect{current->try_at("$schema")}; + if (!metaschema_dialect || !metaschema_dialect->is_string()) { + throw sourcemeta::blaze::SchemaUnknownBaseDialectError(); + } + + const auto &dialect_uri{metaschema_dialect->to_string()}; + const auto known{sourcemeta::blaze::to_base_dialect(dialect_uri)}; + if (known.has_value()) { + terminal = known; + break; + } + + auto remote{resolver(dialect_uri)}; + if (remote.has_value()) { + resolved.push_back(std::move(remote).value()); + current = &resolved.back(); + current_identifier = dialect_uri; + continue; + } + + if (!sourcemeta::core::URI::is_uri(dialect_uri)) { + return nullptr; + } + + const auto next{ + sourcemeta::blaze::embedded_metaschema_candidate(schema, dialect_uri)}; + if (!next.first) { + return nullptr; + } + + links.push_back({.schema = next.first, + .identifier = dialect_uri, + .container = next.second}); + current = next.first; + current_identifier = dialect_uri; + } + + assert(terminal.has_value()); + for (const auto &link : links) { + if (!sourcemeta::blaze::embedded_metaschema_link_valid( + *(link.schema), link.identifier, link.container, + terminal.value())) { + return nullptr; + } + } + + return candidate.first; +} + auto sourcemeta::blaze::metaschema( const sourcemeta::core::JSON &schema, const sourcemeta::blaze::SchemaResolver &resolver, @@ -275,6 +371,15 @@ auto sourcemeta::blaze::metaschema( throw sourcemeta::blaze::SchemaUnknownDialectError(); } + // A meta-schema that is embedded in the schema itself takes precedence + // over what the resolver knows about, as the schema pins the exact + // meta-schema it is described by + const auto *embedded{sourcemeta::blaze::metaschema_try_embedded( + schema, effective_dialect, resolver)}; + if (embedded) { + return *embedded; + } + const auto maybe_metaschema{resolver(effective_dialect)}; if (!maybe_metaschema.has_value()) { // Relative meta-schema references are invalid according to the @@ -297,7 +402,8 @@ base_dialect_with_visited(const sourcemeta::core::JSON &schema, const sourcemeta::blaze::SchemaResolver &resolver, std::string_view default_dialect, std::unordered_set &visited, - const bool allow_dialect_override) + const bool allow_dialect_override, + const sourcemeta::core::JSON &document) -> std::optional { assert(sourcemeta::blaze::is_schema(schema)); const std::string_view effective_dialect{sourcemeta::blaze::dialect( @@ -320,6 +426,22 @@ base_dialect_with_visited(const sourcemeta::core::JSON &schema, throw sourcemeta::blaze::SchemaUnknownBaseDialectError(); } + // A meta-schema that is embedded in the original document itself takes + // precedence over what the resolver knows about, as the document pins + // the exact meta-schema it is described by + const auto *embedded{sourcemeta::blaze::metaschema_try_embedded( + document, effective_dialect, resolver)}; + if (embedded) { + const std::string_view embedded_dialect{sourcemeta::blaze::dialect( + *embedded, effective_dialect, allow_dialect_override)}; + if (embedded_dialect == effective_dialect) { + throw sourcemeta::blaze::SchemaUnknownBaseDialectError(); + } + + return base_dialect_with_visited(*embedded, resolver, effective_dialect, + visited, allow_dialect_override, document); + } + // Otherwise, traverse the metaschema hierarchy up const std::optional metaschema{ resolver(effective_dialect)}; @@ -353,7 +475,7 @@ base_dialect_with_visited(const sourcemeta::core::JSON &schema, return base_dialect_with_visited(metaschema.value(), resolver, effective_dialect, visited, - allow_dialect_override); + allow_dialect_override, document); } auto sourcemeta::blaze::base_dialect( @@ -363,7 +485,7 @@ auto sourcemeta::blaze::base_dialect( -> std::optional { std::unordered_set visited; return base_dialect_with_visited(schema, resolver, default_dialect, visited, - allow_dialect_override); + allow_dialect_override, schema); } namespace { @@ -562,8 +684,21 @@ auto sourcemeta::blaze::vocabularies( throw sourcemeta::blaze::SchemaUnknownDialectError(); } - return vocabularies(resolver, resolved_base_dialect.value(), - resolved_dialect); + // A meta-schema that is embedded in the schema itself takes precedence + // over what the resolver knows about, as the schema pins the exact + // meta-schema it is described by + return vocabularies( + [&schema, &resolver](const std::string_view identifier) + -> std::optional { + const auto *embedded{sourcemeta::blaze::metaschema_try_embedded( + schema, identifier, resolver)}; + if (embedded) { + return *embedded; + } + + return resolver(identifier); + }, + resolved_base_dialect.value(), resolved_dialect); } auto sourcemeta::blaze::vocabularies(const SchemaResolver &resolver, diff --git a/vendor/blaze/src/foundation/helpers.h b/vendor/blaze/src/foundation/helpers.h index 8a3d3b19..9345c288 100644 --- a/vendor/blaze/src/foundation/helpers.h +++ b/vendor/blaze/src/foundation/helpers.h @@ -3,8 +3,16 @@ #include -#include // assert -#include // std::string_view +#include + +#include // assert +#include // std::deque +#include // std::initializer_list +#include // std::optional +#include // std::string_view +#include // std::unordered_set +#include // std::pair, std::move +#include // std::vector namespace sourcemeta::blaze { @@ -84,6 +92,126 @@ ref_overrides_adjacent_keywords(const SchemaBaseDialect base_dialect) -> bool { } } +inline auto embedded_metaschema_identifier_matches( + const sourcemeta::core::JSON &candidate, const std::string_view keyword, + const std::string_view identifier, + const std::optional &canonical) -> bool { + const auto *value{ + candidate.try_at(sourcemeta::core::JSON::StringView{keyword})}; + if (!value || !value->is_string()) { + return false; + } + + const auto ¤t{value->to_string()}; + if (current == identifier) { + return true; + } + + if (canonical.has_value()) { + try { + return sourcemeta::core::URI::canonicalize(current) == canonical.value(); + } catch (const sourcemeta::core::URIParseError &) { + return false; + } + } + + return false; +} + +inline auto embedded_metaschema_matches( + const sourcemeta::core::JSON &candidate, const std::string_view identifier, + const std::optional &canonical) -> bool { + if (!candidate.is_object()) { + return false; + } + + for (const auto *const keyword : {"$id", "id"}) { + if (embedded_metaschema_identifier_matches(candidate, keyword, identifier, + canonical)) { + return true; + } + } + + return false; +} + +inline auto +embedded_metaschema_candidate(const sourcemeta::core::JSON &document, + const std::string_view identifier) + -> std::pair { + if (!document.is_object()) { + return {nullptr, ""}; + } + + std::optional canonical; + try { + canonical = sourcemeta::core::URI::canonicalize(identifier); + } catch (const sourcemeta::core::URIParseError &) { + canonical = std::nullopt; + } + + for (const auto *const container : {"$defs", "definitions"}) { + const auto *entries{document.try_at(container)}; + if (!entries || !entries->is_object()) { + continue; + } + + const auto *direct{ + entries->try_at(sourcemeta::core::JSON::StringView{identifier})}; + if (direct && embedded_metaschema_matches(*direct, identifier, canonical)) { + return {direct, container}; + } + + for (const auto &entry : entries->as_object()) { + if (embedded_metaschema_matches(entry.second, identifier, canonical)) { + return {&entry.second, container}; + } + } + } + + return {nullptr, ""}; +} + +inline auto embedded_metaschema_link_valid(const sourcemeta::core::JSON &link, + const std::string_view identifier, + const std::string_view container, + const SchemaBaseDialect base_dialect) + -> bool { + // In 2019-09 and 2020-12, `definitions` is still supported + // for backwards compatibility + switch (base_dialect) { + case SchemaBaseDialect::JSON_Schema_2020_12: + case SchemaBaseDialect::JSON_Schema_2020_12_Hyper: + case SchemaBaseDialect::JSON_Schema_2019_09: + case SchemaBaseDialect::JSON_Schema_2019_09_Hyper: + if (container != "$defs" && container != "definitions") { + return false; + } + + break; + default: + if (container != definitions_keyword(base_dialect)) { + return false; + } + } + + std::optional canonical; + try { + canonical = sourcemeta::core::URI::canonicalize(identifier); + } catch (const sourcemeta::core::URIParseError &) { + canonical = std::nullopt; + } + + return embedded_metaschema_identifier_matches(link, id_keyword(base_dialect), + identifier, canonical); +} + +struct EmbeddedMetaschemaLink { + const sourcemeta::core::JSON *schema; + sourcemeta::core::JSON::StringView identifier; + std::string_view container; +}; + } // namespace sourcemeta::blaze #endif diff --git a/vendor/blaze/src/foundation/include/sourcemeta/blaze/foundation.h b/vendor/blaze/src/foundation/include/sourcemeta/blaze/foundation.h index 4f8cc5cd..ee394516 100644 --- a/vendor/blaze/src/foundation/include/sourcemeta/blaze/foundation.h +++ b/vendor/blaze/src/foundation/include/sourcemeta/blaze/foundation.h @@ -268,6 +268,43 @@ auto dialect(const sourcemeta::core::JSON &schema, std::string_view default_dialect = "", bool allow_dialect_override = true) -> std::string_view; +/// @ingroup foundation +/// +/// Try to locate the meta-schema that the given schema declares from within +/// the schema itself, as self-contained schemas embed the meta-schemas they +/// depend on. The result points into the given document and is null if no +/// valid embedded meta-schema could be found. For example: +/// +/// ```cpp +/// #include +/// #include +/// #include +/// +/// const sourcemeta::core::JSON schema = +/// sourcemeta::core::parse_json(R"JSON({ +/// "$schema": "https://example.com/meta", +/// "$defs": { +/// "https://example.com/meta": { +/// "$id": "https://example.com/meta", +/// "$schema": "https://json-schema.org/draft/2020-12/schema", +/// "type": "object" +/// } +/// } +/// })JSON"); +/// +/// const auto *metaschema{sourcemeta::blaze::metaschema_try_embedded( +/// schema, "https://example.com/meta", +/// sourcemeta::blaze::schema_resolver)}; +/// +/// assert(metaschema); +/// assert(metaschema == &schema.at("$defs").at("https://example.com/meta")); +/// ``` +SOURCEMETA_BLAZE_FOUNDATION_EXPORT +auto metaschema_try_embedded(const sourcemeta::core::JSON &schema, + std::string_view identifier, + const SchemaResolver &resolver) + -> const sourcemeta::core::JSON *; + /// @ingroup foundation /// /// Get the metaschema document that describes the given schema. For example: diff --git a/vendor/blaze/src/foundation/include/sourcemeta/blaze/foundation_walker.h b/vendor/blaze/src/foundation/include/sourcemeta/blaze/foundation_walker.h index 4ac40dd6..9c6daa99 100644 --- a/vendor/blaze/src/foundation/include/sourcemeta/blaze/foundation_walker.h +++ b/vendor/blaze/src/foundation/include/sourcemeta/blaze/foundation_walker.h @@ -150,7 +150,8 @@ class SOURCEMETA_BLAZE_FOUNDATION_EXPORT SchemaIteratorFlat { /// @ingroup foundation /// /// Return an iterator over the top-level keywords of a given JSON Schema -/// definition in the order in which an implementation must evaluate them. +/// definition in the order in which an implementation must evaluate them, +/// given the vocabularies in use by the schema. /// /// For example: /// @@ -168,10 +169,12 @@ class SOURCEMETA_BLAZE_FOUNDATION_EXPORT SchemaIteratorFlat { /// "patternProperties": {} /// })JSON"); /// +/// const auto vocabularies{sourcemeta::blaze::vocabularies( +/// document, sourcemeta::blaze::schema_resolver)}; +/// /// for (const auto &entry : /// sourcemeta::blaze::SchemaKeywordIterator{ -/// document, sourcemeta::blaze::schema_walker, -/// sourcemeta::blaze::schema_resolver}) { +/// document, sourcemeta::blaze::schema_walker, vocabularies}) { /// sourcemeta::core::stringify(entry.pointer, std::cout); /// std::cout << "\n"; /// } @@ -184,8 +187,7 @@ class SOURCEMETA_BLAZE_FOUNDATION_EXPORT SchemaKeywordIterator { using const_iterator = typename internal::const_iterator; SchemaKeywordIterator(const sourcemeta::core::JSON &input, const SchemaWalker &walker, - const SchemaResolver &resolver, - std::string_view default_dialect = ""); + const Vocabularies &vocabularies); [[nodiscard]] auto begin() const -> const_iterator; [[nodiscard]] auto end() const -> const_iterator; [[nodiscard]] auto cbegin() const -> const_iterator; diff --git a/vendor/blaze/src/foundation/walker.cc b/vendor/blaze/src/foundation/walker.cc index de982ce2..39463db0 100644 --- a/vendor/blaze/src/foundation/walker.cc +++ b/vendor/blaze/src/foundation/walker.cc @@ -419,24 +419,12 @@ sourcemeta::blaze::SchemaIteratorFlat::SchemaIteratorFlat( sourcemeta::blaze::SchemaKeywordIterator::SchemaKeywordIterator( const sourcemeta::core::JSON &schema, const sourcemeta::blaze::SchemaWalker &walker, - const sourcemeta::blaze::SchemaResolver &resolver, - const std::string_view default_dialect) { + const sourcemeta::blaze::Vocabularies &vocabularies) { assert(is_schema(schema)); if (schema.is_boolean()) { return; } - const std::string_view resolved_dialect{ - sourcemeta::blaze::dialect(schema, default_dialect)}; - const auto maybe_base_dialect{ - sourcemeta::blaze::base_dialect(schema, resolver, resolved_dialect)}; - - Vocabularies vocabularies{ - maybe_base_dialect.has_value() && !resolved_dialect.empty() - ? sourcemeta::blaze::vocabularies( - resolver, maybe_base_dialect.value(), resolved_dialect) - : Vocabularies{}}; - // TODO: Use std::ranges::to() once libc++ supports it // (__cpp_lib_ranges_to_container) for (const auto &entry : schema.as_object()) { @@ -445,9 +433,9 @@ sourcemeta::blaze::SchemaKeywordIterator::SchemaKeywordIterator( sourcemeta::blaze::SchemaIteratorEntry subschema_entry{ .parent = std::nullopt, .pointer = std::move(entry_pointer), - .dialect = resolved_dialect, + .dialect = "", .vocabularies = vocabularies, - .base_dialect = maybe_base_dialect, + .base_dialect = std::nullopt, .subschema = entry.second, .orphan = false, .property_name = false}; diff --git a/vendor/blaze/src/frame/frame.cc b/vendor/blaze/src/frame/frame.cc index dd5aa18f..e5d24fbc 100644 --- a/vendor/blaze/src/frame/frame.cc +++ b/vendor/blaze/src/frame/frame.cc @@ -567,6 +567,28 @@ auto SchemaFrame::analyse(const sourcemeta::core::JSON &root, sourcemeta::core::WeakPointer::Hasher>( paths.cbegin(), paths.cend()) .size() == paths.size())); + + // A meta-schema that is embedded in the document itself takes precedence + // over what the resolver knows about, as the document pins the exact + // meta-schema it is described by + const SchemaResolver effective_resolver{ + [&root, &resolver, this](const std::string_view identifier) + -> std::optional { + const sourcemeta::core::JSON::String key{identifier}; + const auto hit{this->probed_metaschemas_.find(key)}; + if (hit != this->probed_metaschemas_.cend()) { + return *(hit->second); + } + + const auto *match{ + sourcemeta::blaze::metaschema_try_embedded(root, key, resolver)}; + if (match) { + this->probed_metaschemas_.emplace(key, match); + return *match; + } + + return resolver(identifier); + }}; std::vector subschema_entries; std::unordered_map @@ -588,8 +610,8 @@ auto SchemaFrame::analyse(const sourcemeta::core::JSON &root, const auto &schema{sourcemeta::core::get(root, path)}; - const auto root_base_dialect{ - sourcemeta::blaze::base_dialect(schema, resolver, default_dialect)}; + const auto root_base_dialect{sourcemeta::blaze::base_dialect( + schema, effective_resolver, default_dialect)}; if (!root_base_dialect.has_value()) { throw SchemaUnknownBaseDialectError(); } @@ -637,7 +659,7 @@ auto SchemaFrame::analyse(const sourcemeta::core::JSON &root, std::vector current_subschema_entries; for (const auto &relative_entry : sourcemeta::blaze::SchemaIterator{ - schema, walker, resolver, default_dialect}) { + schema, walker, effective_resolver, default_dialect}) { // Rephrase the iterator entry as being for the current base auto entry{relative_entry}; entry.pointer = path.concat(relative_entry.pointer); @@ -1283,8 +1305,25 @@ auto SchemaFrame::root() const noexcept auto SchemaFrame::vocabularies(const Location &location, const SchemaResolver &resolver) const -> Vocabularies { - return sourcemeta::blaze::vocabularies(resolver, location.base_dialect, - location.dialect); + if (this->probed_metaschemas_.empty()) { + return sourcemeta::blaze::vocabularies(resolver, location.base_dialect, + location.dialect); + } + + // Meta-schemas embedded in the analysed document take precedence + // over what the caller's resolver knows about + return sourcemeta::blaze::vocabularies( + [this, &resolver](const std::string_view identifier) + -> std::optional { + const auto hit{this->probed_metaschemas_.find( + sourcemeta::core::JSON::String{identifier})}; + if (hit != this->probed_metaschemas_.cend()) { + return *(hit->second); + } + + return resolver(identifier); + }, + location.base_dialect, location.dialect); } auto SchemaFrame::uri( @@ -1535,6 +1574,7 @@ auto SchemaFrame::reset() -> void { this->root_.clear(); this->locations_.clear(); this->references_.clear(); + this->probed_metaschemas_.clear(); this->standalone_ = false; } diff --git a/vendor/blaze/src/frame/include/sourcemeta/blaze/frame.h b/vendor/blaze/src/frame/include/sourcemeta/blaze/frame.h index 7481f64d..d78a02e2 100644 --- a/vendor/blaze/src/frame/include/sourcemeta/blaze/frame.h +++ b/vendor/blaze/src/frame/include/sourcemeta/blaze/frame.h @@ -298,6 +298,12 @@ class SOURCEMETA_BLAZE_FRAME_EXPORT SchemaFrame { sourcemeta::core::JSON::String root_; Locations locations_; References references_; + // Custom meta-schemas that the resolver could not resolve but that were + // found embedded in the analysed document itself. The values point into + // the analysed document, which the frame must not outlive anyway + std::unordered_map + probed_metaschemas_; mutable std::unordered_map< std::reference_wrapper, std::vector, sourcemeta::core::WeakPointer::Hasher,