From 69b9284a36ebbabde0c60f2c56df2435e86e0e9a Mon Sep 17 00:00:00 2001 From: Juan Cruz Viotti Date: Wed, 20 May 2026 11:51:39 -0400 Subject: [PATCH] [WIP] Experiment with better meta-schema bundling Signed-off-by: Juan Cruz Viotti --- src/core/jsonschema/bundle.cc | 218 +++++- .../sourcemeta/core/jsonschema_error.h | 23 + test/editorschema/editorschema_test.cc | 56 +- .../jsonschema_bundle_2019_09_test.cc | 450 +++++++++++- .../jsonschema_bundle_2020_12_test.cc | 688 +++++++++++++++--- .../jsonschema_bundle_draft4_test.cc | 44 +- .../jsonschema_bundle_draft6_test.cc | 44 +- .../jsonschema_bundle_draft7_test.cc | 46 +- 8 files changed, 1344 insertions(+), 225 deletions(-) diff --git a/src/core/jsonschema/bundle.cc b/src/core/jsonschema/bundle.cc index f28414a60..e4eafc324 100644 --- a/src/core/jsonschema/bundle.cc +++ b/src/core/jsonschema/bundle.cc @@ -13,15 +13,6 @@ namespace { -auto is_official_metaschema_reference( - const sourcemeta::core::WeakPointer &pointer, - const std::string &destination) -> bool { - assert(!pointer.empty()); - assert(pointer.back().is_property()); - return pointer.back().to_property() == "$schema" && - sourcemeta::core::is_official_schema(destination); -} - auto dependencies_internal(const sourcemeta::core::JSON &schema, const sourcemeta::core::SchemaWalker &walker, const sourcemeta::core::SchemaResolver &resolver, @@ -29,7 +20,8 @@ auto dependencies_internal(const sourcemeta::core::JSON &schema, std::string_view default_dialect, std::string_view default_id, const sourcemeta::core::SchemaFrame::Paths &paths, - std::unordered_set &visited) -> void { + std::unordered_set &visited, + const bool include_official_refs) -> void { sourcemeta::core::SchemaFrame frame{ sourcemeta::core::SchemaFrame::Mode::References}; frame.analyse(schema, walker, resolver, default_dialect, default_id, paths); @@ -43,8 +35,12 @@ auto dependencies_internal(const sourcemeta::core::JSON &schema, frame.for_each_unresolved_reference([&](const auto &pointer, const auto &reference) { // We don't want to report official schemas, as we can expect - // virtually all implementations to understand them out of the box - if (is_official_metaschema_reference(pointer, reference.destination)) { + // virtually all implementations to understand them out of the box. + // The exception is when the top-level input itself has an official + // identifier: reporting dependencies of such a document is an + // explicit ask for the full graph of officials it depends on + if (!include_official_refs && + sourcemeta::core::is_official_schema(reference.destination)) { return; } @@ -98,14 +94,22 @@ auto dependencies_internal(const sourcemeta::core::JSON &schema, for (const auto &entry : found) { dependencies_internal(std::get<0>(entry), walker, resolver, callback, default_dialect, std::get<1>(entry), - {sourcemeta::core::empty_weak_pointer}, visited); + {sourcemeta::core::empty_weak_pointer}, visited, + include_official_refs); } } auto embed_schema(sourcemeta::core::JSON &root, const sourcemeta::core::Pointer &container, const std::string_view identifier, + const std::string_view reserved_identifier, sourcemeta::core::JSON &&target) -> void { + if (!reserved_identifier.empty() && identifier == reserved_identifier) { + throw sourcemeta::core::SchemaReservedIdentifierError( + identifier, + "This identifier is reserved by the bundler for internal use"); + } + auto *current{&root}; for (const auto &token : container) { if (token.is_property()) { @@ -137,7 +141,7 @@ auto elevate_embedded_resources( const sourcemeta::core::Pointer &container, const sourcemeta::core::SchemaBaseDialect remote_dialect, const sourcemeta::core::SchemaResolver &resolver, - std::string_view default_dialect, + std::string_view default_dialect, std::string_view reserved_identifier, std::unordered_map &bundled) -> void { const auto keyword{sourcemeta::core::definitions_keyword(remote_dialect)}; @@ -216,7 +220,7 @@ auto elevate_embedded_resources( for (const auto &key : to_extract) { auto value{std::move(defs.at(key))}; defs.erase(key); - embed_schema(root, container, key, std::move(value)); + embed_schema(root, container, key, reserved_identifier, std::move(value)); } for (const auto &key : to_remove) { @@ -238,6 +242,8 @@ auto bundle_schema(sourcemeta::core::JSON &root, const sourcemeta::core::SchemaFrame::Paths &paths, std::unordered_map &bundled, + const bool include_official_refs, + std::string_view reserved_identifier, const std::size_t depth = 0) -> void { // Create a fresh frame for each schema we analyze to avoid key collisions // between different schemas that have references at the same pointer paths @@ -262,19 +268,15 @@ auto bundle_schema(sourcemeta::core::JSON &root, frame.for_each_unresolved_reference([&](const auto &pointer, const auto &reference) { // We don't want to bundle official schemas, as we can expect - // virtually all implementations to understand them out of the box - if (is_official_metaschema_reference(pointer, reference.destination)) { + // virtually all implementations to understand them out of the box. + // The exception is when the top-level input itself has an official + // identifier: bundling such a document is an explicit ask for the + // full graph of official references it depends on + if (!include_official_refs && + sourcemeta::core::is_official_schema(reference.destination)) { return; } - // If we can't find the destination but there is a base and we can - // find base, then we are facing an unresolved fragment - if (!reference.base.empty() && frame.traverse(reference.base).has_value()) { - throw sourcemeta::core::SchemaReferenceError( - reference.destination, sourcemeta::core::to_pointer(pointer), - "Could not resolve schema reference"); - } - if (reference.base.empty()) { throw sourcemeta::core::SchemaReferenceError( reference.destination, sourcemeta::core::to_pointer(pointer), @@ -294,11 +296,29 @@ auto bundle_schema(sourcemeta::core::JSON &root, ref_rewrites.emplace_back(sourcemeta::core::to_pointer(pointer), rewrite_uri.recompose()); + return; + } + + // Identity mapping: the base resource is in the bundle but the + // destination fragment could not be traversed, which is a broken + // reference + if (frame.traverse(reference.base).has_value()) { + throw sourcemeta::core::SchemaReferenceError( + reference.destination, sourcemeta::core::to_pointer(pointer), + "Could not resolve schema reference"); } return; } + // If we can't find the destination but there is a base and we can + // find base, then we are facing an unresolved fragment + if (frame.traverse(reference.base).has_value()) { + throw sourcemeta::core::SchemaReferenceError( + reference.destination, sourcemeta::core::to_pointer(pointer), + "Could not resolve schema reference"); + } + auto remote{resolver(identifier)}; if (!remote.has_value()) { if (frame.traverse(identifier).has_value()) { @@ -376,10 +396,13 @@ auto bundle_schema(sourcemeta::core::JSON &root, for (auto &[remote, effective_id, remote_dialect] : deferred) { bundle_schema(root, container, remote, walker, resolver, default_dialect, - effective_id, paths, bundled, depth + 1); + effective_id, paths, bundled, include_official_refs, + reserved_identifier, depth + 1); elevate_embedded_resources(remote, root, container, remote_dialect, - resolver, default_dialect, bundled); - embed_schema(root, container, effective_id, std::move(remote)); + resolver, default_dialect, reserved_identifier, + bundled); + embed_schema(root, container, effective_id, reserved_identifier, + std::move(remote)); } } @@ -393,8 +416,10 @@ auto dependencies(const JSON &schema, const SchemaWalker &walker, std::string_view default_dialect, std::string_view default_id, const SchemaFrame::Paths &paths) -> void { std::unordered_set visited; + const bool include_official_refs{ + is_official_schema(identify(schema, resolver, default_dialect))}; dependencies_internal(schema, walker, resolver, callback, default_dialect, - default_id, paths, visited); + default_id, paths, visited, include_official_refs); } // TODO: Refactor this function to internally rely on the `.dependencies()` @@ -404,10 +429,137 @@ auto bundle(JSON &schema, const SchemaWalker &walker, std::string_view default_id, const std::optional &default_container, const SchemaFrame::Paths &paths) -> void { + std::unordered_map bundled; + + constexpr std::string_view BUNDLE_FALLBACK_IDENTIFIER{ + "__sourcemeta-core-bundle__"}; + std::string_view reserved_identifier; + + // If the user's input itself has an identifier that corresponds to an + // official schema (e.g. a JSON Schema meta-schema being bundled + // explicitly), then the user is asking for the full graph of officials + // they depend on. The default filter that skips official references + // does not apply + const bool include_official_refs{ + is_official_schema(identify(schema, resolver, default_dialect))}; + + // When the outer schema declares a non-official meta-schema, wrap the + // document in an envelope whose dialect matches the input's base + // dialect so the resulting compound document has an unambiguous outer + // dialect (and thus a deterministic container keyword). The original + // schema lives in the container with its identifier rephrased via an + // extra `/x` path segment, and any reference targeting the original + // identifier is rewritten on the fly via the shared `bundled` map below + if (!default_container.has_value()) { + const auto outer_dialect{dialect(schema, default_dialect)}; + if (!outer_dialect.empty() && !is_official_schema(outer_dialect)) { + // Inject `default_id` ahead of elevation so the envelope can carry + // it as its top-level identifier + if (!default_id.empty() && + identify(schema, resolver, default_dialect).empty()) { + reidentify(schema, default_id, resolver, default_dialect); + } + + const auto inner_base_dialect{ + base_dialect(schema, resolver, default_dialect)}; + if (!inner_base_dialect.has_value()) { + throw SchemaError( + "Could not determine how to perform bundling in this dialect"); + } + + const auto envelope_container{ + definitions_keyword(inner_base_dialect.value())}; + if (envelope_container.empty()) { + throw SchemaError( + "Could not determine how to perform bundling in this dialect"); + } + + const JSON::String original_id{ + identify(schema, resolver, default_dialect)}; + + // Discover existing resource URIs in the input so the rephrased + // inner URI does not collide with a sub-resource the user authored + std::unordered_set existing_uris; + { + SchemaFrame discovery_frame{SchemaFrame::Mode::Locations}; + discovery_frame.analyse(schema, walker, resolver, default_dialect, + default_id, paths); + discovery_frame.for_each_resource_uri( + [&](const auto &uri) { existing_uris.emplace(JSON::String{uri}); }); + } + + JSON::String inner_id; + if (!original_id.empty()) { + URI inner_uri{std::string{original_id}}; + inner_uri.append_path("x"); + inner_uri.canonicalize(); + inner_id = inner_uri.recompose(); + while (existing_uris.contains(inner_id)) { + inner_uri.append_path("x"); + inner_id = inner_uri.recompose(); + } + } else { + inner_id = JSON::String{BUNDLE_FALLBACK_IDENTIFIER}; + URI inner_uri{inner_id}; + while (existing_uris.contains(inner_id)) { + inner_uri.append_path("x"); + inner_id = inner_uri.recompose(); + } + // The bundler is using its reserved fallback identifier as the + // identity of the embedded inner. From this point on, any other + // schema we bundle must not declare this same identifier + reserved_identifier = BUNDLE_FALLBACK_IDENTIFIER; + } + + auto inner_copy{schema}; + reidentify(inner_copy, inner_id, resolver, default_dialect); + + auto envelope{JSON::make_object()}; + envelope.assign_assume_new("$schema", + JSON{to_string(inner_base_dialect.value())}); + if (!original_id.empty()) { + envelope.assign_assume_new( + JSON::String{id_keyword(inner_base_dialect.value())}, + JSON{original_id}); + } + + // In Draft 7 and older, `$ref` overrides sibling keywords, so we + // route the redirect through `allOf` (or `extends` for Draft 3) + // instead of using a top-level `$ref` + if (ref_overrides_adjacent_keywords(inner_base_dialect.value())) { + auto ref_branch{JSON::make_object()}; + ref_branch.assign_assume_new("$ref", JSON{inner_id}); + auto branches{JSON::make_array()}; + branches.push_back(std::move(ref_branch)); + const bool is_draft3{inner_base_dialect.value() == + SchemaBaseDialect::JSON_Schema_Draft_3 || + inner_base_dialect.value() == + SchemaBaseDialect::JSON_Schema_Draft_3_Hyper}; + envelope.assign_assume_new(is_draft3 ? "extends" : "allOf", + std::move(branches)); + } else { + envelope.assign_assume_new("$ref", JSON{inner_id}); + } + + envelope.assign_assume_new(JSON::String{envelope_container}, + JSON::make_object()); + envelope.at(JSON::String{envelope_container}) + .assign_assume_new(inner_id, std::move(inner_copy)); + + schema = std::move(envelope); + + // Seed the rewrite mapping. Every reference (in the inner itself + // and in every schema bundle subsequently pulls in) that targets + // `original_id` will be retargeted at `inner_id` + if (!original_id.empty()) { + bundled.emplace(original_id, inner_id); + } + } + } + // Pre-scan the schema to find any already-embedded schemas and mark them // as bundled to avoid re-embedding them. This includes the root schema itself // and any schemas already embedded within it - std::unordered_map bundled; SchemaFrame initial_frame{SchemaFrame::Mode::Locations}; initial_frame.analyse(schema, walker, resolver, default_dialect, default_id, paths); @@ -418,7 +570,8 @@ auto bundle(JSON &schema, const SchemaWalker &walker, // This is undefined behavior assert(!default_container.value().empty()); bundle_schema(schema, default_container.value(), schema, walker, resolver, - default_dialect, default_id, paths, bundled); + default_dialect, default_id, paths, bundled, + include_official_refs, reserved_identifier); return; } @@ -471,7 +624,8 @@ auto bundle(JSON &schema, const SchemaWalker &walker, } bundle_schema(schema, {JSON::String{container_keyword}}, schema, walker, - resolver, default_dialect, default_id, paths, bundled); + resolver, default_dialect, default_id, paths, bundled, + include_official_refs, reserved_identifier); } auto bundle(const JSON &schema, const SchemaWalker &walker, diff --git a/src/core/jsonschema/include/sourcemeta/core/jsonschema_error.h b/src/core/jsonschema/include/sourcemeta/core/jsonschema_error.h index 6dd586c16..b583b285d 100644 --- a/src/core/jsonschema/include/sourcemeta/core/jsonschema_error.h +++ b/src/core/jsonschema/include/sourcemeta/core/jsonschema_error.h @@ -71,6 +71,29 @@ class SOURCEMETA_CORE_JSONSCHEMA_EXPORT SchemaRelativeMetaschemaResolutionError "according to the JSON Schema specification"} {} }; +/// @ingroup jsonschema +/// An error that represents a schema identifier that collides with one +/// reserved by this implementation for internal bookkeeping +class SOURCEMETA_CORE_JSONSCHEMA_EXPORT SchemaReservedIdentifierError + : public std::exception { +public: + SchemaReservedIdentifierError(const std::string_view identifier, + const char *message) + : identifier_{identifier}, message_{message} {} + + [[nodiscard]] auto what() const noexcept -> const char * override { + return this->message_; + } + + [[nodiscard]] auto identifier() const noexcept -> std::string_view { + return this->identifier_; + } + +private: + std::string identifier_; + const char *message_; +}; + /// @ingroup jsonschema /// An error that represents a schema vocabulary error class SOURCEMETA_CORE_JSONSCHEMA_EXPORT SchemaVocabularyError diff --git a/test/editorschema/editorschema_test.cc b/test/editorschema/editorschema_test.cc index 7477fff01..1fa913d35 100644 --- a/test/editorschema/editorschema_test.cc +++ b/test/editorschema/editorschema_test.cc @@ -404,13 +404,17 @@ TEST(EditorSchema, 2020_12_bundle_metaschema) { const auto expected = sourcemeta::core::parse_json(R"JSON({ "$schema": "https://json-schema.org/draft/2020-12/schema", - "type": "string", + "$ref": "#/$defs/__sourcemeta-core-bundle__", "$defs": { - "https://example.com/meta/1.json": { - "$schema": "https://json-schema.org/draft/2020-12/schema" + "__sourcemeta-core-bundle__": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "string" }, "https://example.com/meta/2.json": { "$schema": "https://json-schema.org/draft/2020-12/schema" + }, + "https://example.com/meta/1.json": { + "$schema": "https://json-schema.org/draft/2020-12/schema" } } })JSON"); @@ -483,13 +487,17 @@ TEST(EditorSchema, 2019_09_bundle_metaschema) { const auto expected = sourcemeta::core::parse_json(R"JSON({ "$schema": "https://json-schema.org/draft/2019-09/schema", - "type": "string", + "$ref": "#/$defs/__sourcemeta-core-bundle__", "$defs": { - "https://example.com/meta/1.json": { - "$schema": "https://json-schema.org/draft/2019-09/schema" + "__sourcemeta-core-bundle__": { + "$schema": "https://json-schema.org/draft/2019-09/schema", + "type": "string" }, "https://example.com/meta/2.json": { "$schema": "https://json-schema.org/draft/2019-09/schema" + }, + "https://example.com/meta/1.json": { + "$schema": "https://json-schema.org/draft/2019-09/schema" } } })JSON"); @@ -622,13 +630,19 @@ TEST(EditorSchema, draft7_bundle_metaschema) { const auto expected = sourcemeta::core::parse_json(R"JSON({ "$schema": "http://json-schema.org/draft-07/schema#", - "type": "string", + "allOf": [ + { "$ref": "#/definitions/__sourcemeta-core-bundle__" } + ], "definitions": { - "https://example.com/meta/1.json": { - "$schema": "http://json-schema.org/draft-07/schema#" + "__sourcemeta-core-bundle__": { + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "string" }, "https://example.com/meta/2.json": { "$schema": "http://json-schema.org/draft-07/schema#" + }, + "https://example.com/meta/1.json": { + "$schema": "http://json-schema.org/draft-07/schema#" } } })JSON"); @@ -687,13 +701,19 @@ TEST(EditorSchema, draft6_bundle_metaschema) { const auto expected = sourcemeta::core::parse_json(R"JSON({ "$schema": "http://json-schema.org/draft-06/schema#", - "type": "string", + "allOf": [ + { "$ref": "#/definitions/__sourcemeta-core-bundle__" } + ], "definitions": { - "https://example.com/meta/1.json": { - "$schema": "http://json-schema.org/draft-06/schema#" + "__sourcemeta-core-bundle__": { + "$schema": "http://json-schema.org/draft-06/schema#", + "type": "string" }, "https://example.com/meta/2.json": { "$schema": "http://json-schema.org/draft-06/schema#" + }, + "https://example.com/meta/1.json": { + "$schema": "http://json-schema.org/draft-06/schema#" } } })JSON"); @@ -752,13 +772,19 @@ TEST(EditorSchema, draft4_bundle_metaschema) { const auto expected = sourcemeta::core::parse_json(R"JSON({ "$schema": "http://json-schema.org/draft-04/schema#", - "type": "string", + "allOf": [ + { "$ref": "#/definitions/__sourcemeta-core-bundle__" } + ], "definitions": { - "https://example.com/meta/1.json": { - "$schema": "http://json-schema.org/draft-04/schema#" + "__sourcemeta-core-bundle__": { + "$schema": "http://json-schema.org/draft-04/schema#", + "type": "string" }, "https://example.com/meta/2.json": { "$schema": "http://json-schema.org/draft-04/schema#" + }, + "https://example.com/meta/1.json": { + "$schema": "http://json-schema.org/draft-04/schema#" } } })JSON"); diff --git a/test/jsonschema/jsonschema_bundle_2019_09_test.cc b/test/jsonschema/jsonschema_bundle_2019_09_test.cc index 0862e09d6..7be8339f9 100644 --- a/test/jsonschema/jsonschema_bundle_2019_09_test.cc +++ b/test/jsonschema/jsonschema_bundle_2019_09_test.cc @@ -453,18 +453,23 @@ TEST(JSONSchema_bundle_2019_09, metaschema) { test_resolver); const sourcemeta::core::JSON expected = sourcemeta::core::parse_json(R"JSON({ - "$schema": "https://example.com/meta/1.json", - "type": "string", + "$schema": "https://json-schema.org/draft/2019-09/schema", + "$ref": "__sourcemeta-core-bundle__", "$defs": { - "https://example.com/meta/1.json": { - "$schema": "https://example.com/meta/2.json", - "$id": "https://example.com/meta/1.json", - "$vocabulary": { "https://json-schema.org/draft/2019-09/vocab/core": true } + "__sourcemeta-core-bundle__": { + "$schema": "https://example.com/meta/1.json", + "$id": "__sourcemeta-core-bundle__", + "type": "string" }, "https://example.com/meta/2.json": { "$schema": "https://json-schema.org/draft/2019-09/schema", "$id": "https://example.com/meta/2.json", "$vocabulary": { "https://json-schema.org/draft/2019-09/vocab/core": true } + }, + "https://example.com/meta/1.json": { + "$schema": "https://example.com/meta/2.json", + "$id": "https://example.com/meta/1.json", + "$vocabulary": { "https://json-schema.org/draft/2019-09/vocab/core": true } } } })JSON"); @@ -525,42 +530,413 @@ TEST(JSONSchema_bundle_2019_09, hyperschema_1) { sourcemeta::core::bundle(document, sourcemeta::core::schema_walker, test_resolver); - EXPECT_TRUE(document.defines("$defs")); - EXPECT_TRUE(document.at("$defs").is_object()); - EXPECT_EQ(document.at("$defs").size(), 10); - - EXPECT_TRUE(document.at("$defs").defines( - "https://json-schema.org/draft/2019-09/schema")); - EXPECT_TRUE(document.at("$defs").defines( - "https://json-schema.org/draft/2019-09/meta/core")); - EXPECT_TRUE(document.at("$defs").defines( - "https://json-schema.org/draft/2019-09/meta/applicator")); - EXPECT_TRUE(document.at("$defs").defines( - "https://json-schema.org/draft/2019-09/meta/validation")); - EXPECT_TRUE(document.at("$defs").defines( - "https://json-schema.org/draft/2019-09/meta/meta-data")); - EXPECT_TRUE(document.at("$defs").defines( - "https://json-schema.org/draft/2019-09/meta/format")); - EXPECT_TRUE(document.at("$defs").defines( - "https://json-schema.org/draft/2019-09/meta/content")); - EXPECT_TRUE(document.at("$defs").defines( - "https://json-schema.org/draft/2019-09/meta/hyper-schema")); - EXPECT_TRUE(document.at("$defs").defines( - "https://json-schema.org/draft/2019-09/links")); - EXPECT_TRUE(document.at("$defs").defines( - "https://json-schema.org/draft/2019-09/hyper-schema")); + const sourcemeta::core::JSON expected = sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2019-09/schema", + "allOf": [ + { "$ref": "https://json-schema.org/draft/2019-09/schema" }, + { "$ref": "https://json-schema.org/draft/2019-09/meta/hyper-schema" } + ] + })JSON"); + + EXPECT_EQ(document, expected); } TEST(JSONSchema_bundle_2019_09, hyperschema_2) { - sourcemeta::core::JSON document = - sourcemeta::core::schema_resolver( - "https://json-schema.org/draft/2019-09/hyper-schema") - .value(); + sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2019-09/hyper-schema", + "$id": "https://json-schema.org/draft/2019-09/hyper-schema", + "$vocabulary": { + "https://json-schema.org/draft/2019-09/vocab/core": true, + "https://json-schema.org/draft/2019-09/vocab/applicator": true, + "https://json-schema.org/draft/2019-09/vocab/validation": true, + "https://json-schema.org/draft/2019-09/vocab/meta-data": true, + "https://json-schema.org/draft/2019-09/vocab/format": false, + "https://json-schema.org/draft/2019-09/vocab/content": true, + "https://json-schema.org/draft/2019-09/vocab/hyper-schema": true + }, + "$recursiveAnchor": true, + "title": "JSON Hyper-Schema", + "allOf": [ + { "$ref": "https://json-schema.org/draft/2019-09/schema" }, + { "$ref": "https://json-schema.org/draft/2019-09/meta/hyper-schema" } + ], + "links": [ + { + "rel": "self", + "href": "{+%24id}" + } + ] + })JSON"); sourcemeta::core::bundle(document, sourcemeta::core::schema_walker, test_resolver); - EXPECT_TRUE(document.defines("$defs")); - EXPECT_TRUE(document.at("$defs").is_object()); - EXPECT_EQ(document.at("$defs").size(), 9); + const sourcemeta::core::JSON expected = sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2019-09/hyper-schema", + "$id": "https://json-schema.org/draft/2019-09/hyper-schema", + "$vocabulary": { + "https://json-schema.org/draft/2019-09/vocab/core": true, + "https://json-schema.org/draft/2019-09/vocab/applicator": true, + "https://json-schema.org/draft/2019-09/vocab/validation": true, + "https://json-schema.org/draft/2019-09/vocab/meta-data": true, + "https://json-schema.org/draft/2019-09/vocab/format": false, + "https://json-schema.org/draft/2019-09/vocab/content": true, + "https://json-schema.org/draft/2019-09/vocab/hyper-schema": true + }, + "$recursiveAnchor": true, + "title": "JSON Hyper-Schema", + "allOf": [ + { "$ref": "https://json-schema.org/draft/2019-09/schema" }, + { "$ref": "https://json-schema.org/draft/2019-09/meta/hyper-schema" } + ], + "links": [ + { "rel": "self", "href": "{+%24id}" } + ], + "$defs": { + "https://json-schema.org/draft/2019-09/meta/core": { + "$schema": "https://json-schema.org/draft/2019-09/schema", + "$id": "https://json-schema.org/draft/2019-09/meta/core", + "$vocabulary": { + "https://json-schema.org/draft/2019-09/vocab/core": true + }, + "$recursiveAnchor": true, + "title": "Core vocabulary meta-schema", + "type": ["object", "boolean"], + "properties": { + "$id": { + "type": "string", + "format": "uri-reference", + "$comment": "Non-empty fragments not allowed.", + "pattern": "^[^#]*#?$" + }, + "$schema": {"type": "string", "format": "uri"}, + "$anchor": { + "type": "string", + "pattern": "^[A-Za-z][-A-Za-z0-9.:_]*$" + }, + "$ref": {"type": "string", "format": "uri-reference"}, + "$recursiveRef": {"type": "string", "format": "uri-reference"}, + "$recursiveAnchor": {"type": "boolean", "default": false}, + "$vocabulary": { + "type": "object", + "propertyNames": {"type": "string", "format": "uri"}, + "additionalProperties": {"type": "boolean"} + }, + "$comment": {"type": "string"}, + "$defs": { + "type": "object", + "additionalProperties": {"$recursiveRef": "#"}, + "default": {} + } + } + }, + "https://json-schema.org/draft/2019-09/meta/applicator": { + "$schema": "https://json-schema.org/draft/2019-09/schema", + "$id": "https://json-schema.org/draft/2019-09/meta/applicator", + "$vocabulary": { + "https://json-schema.org/draft/2019-09/vocab/applicator": true + }, + "$recursiveAnchor": true, + "title": "Applicator vocabulary meta-schema", + "type": ["object", "boolean"], + "properties": { + "additionalItems": {"$recursiveRef": "#"}, + "unevaluatedItems": {"$recursiveRef": "#"}, + "items": { + "anyOf": [ + {"$recursiveRef": "#"}, + {"$ref": "#/$defs/schemaArray"} + ] + }, + "contains": {"$recursiveRef": "#"}, + "additionalProperties": {"$recursiveRef": "#"}, + "unevaluatedProperties": {"$recursiveRef": "#"}, + "properties": { + "type": "object", + "additionalProperties": {"$recursiveRef": "#"}, + "default": {} + }, + "patternProperties": { + "type": "object", + "additionalProperties": {"$recursiveRef": "#"}, + "propertyNames": {"format": "regex"}, + "default": {} + }, + "dependentSchemas": { + "type": "object", + "additionalProperties": {"$recursiveRef": "#"} + }, + "propertyNames": {"$recursiveRef": "#"}, + "if": {"$recursiveRef": "#"}, + "then": {"$recursiveRef": "#"}, + "else": {"$recursiveRef": "#"}, + "allOf": {"$ref": "#/$defs/schemaArray"}, + "anyOf": {"$ref": "#/$defs/schemaArray"}, + "oneOf": {"$ref": "#/$defs/schemaArray"}, + "not": {"$recursiveRef": "#"} + }, + "$defs": { + "schemaArray": { + "type": "array", + "minItems": 1, + "items": {"$recursiveRef": "#"} + } + } + }, + "https://json-schema.org/draft/2019-09/meta/validation": { + "$schema": "https://json-schema.org/draft/2019-09/schema", + "$id": "https://json-schema.org/draft/2019-09/meta/validation", + "$vocabulary": { + "https://json-schema.org/draft/2019-09/vocab/validation": true + }, + "$recursiveAnchor": true, + "title": "Validation vocabulary meta-schema", + "type": ["object", "boolean"], + "properties": { + "multipleOf": {"type": "number", "exclusiveMinimum": 0}, + "maximum": {"type": "number"}, + "exclusiveMaximum": {"type": "number"}, + "minimum": {"type": "number"}, + "exclusiveMinimum": {"type": "number"}, + "maxLength": {"$ref": "#/$defs/nonNegativeInteger"}, + "minLength": {"$ref": "#/$defs/nonNegativeIntegerDefault0"}, + "pattern": {"type": "string", "format": "regex"}, + "maxItems": {"$ref": "#/$defs/nonNegativeInteger"}, + "minItems": {"$ref": "#/$defs/nonNegativeIntegerDefault0"}, + "uniqueItems": {"type": "boolean", "default": false}, + "maxContains": {"$ref": "#/$defs/nonNegativeInteger"}, + "minContains": {"$ref": "#/$defs/nonNegativeInteger", "default": 1}, + "maxProperties": {"$ref": "#/$defs/nonNegativeInteger"}, + "minProperties": {"$ref": "#/$defs/nonNegativeIntegerDefault0"}, + "required": {"$ref": "#/$defs/stringArray"}, + "dependentRequired": { + "type": "object", + "additionalProperties": {"$ref": "#/$defs/stringArray"} + }, + "const": true, + "enum": {"type": "array", "items": true}, + "type": { + "anyOf": [ + {"$ref": "#/$defs/simpleTypes"}, + { + "type": "array", + "items": {"$ref": "#/$defs/simpleTypes"}, + "minItems": 1, + "uniqueItems": true + } + ] + } + }, + "$defs": { + "nonNegativeInteger": {"type": "integer", "minimum": 0}, + "nonNegativeIntegerDefault0": { + "$ref": "#/$defs/nonNegativeInteger", + "default": 0 + }, + "simpleTypes": { + "enum": [ + "array", + "boolean", + "integer", + "null", + "number", + "object", + "string" + ] + }, + "stringArray": { + "type": "array", + "items": {"type": "string"}, + "uniqueItems": true, + "default": [] + } + } + }, + "https://json-schema.org/draft/2019-09/meta/meta-data": { + "$schema": "https://json-schema.org/draft/2019-09/schema", + "$id": "https://json-schema.org/draft/2019-09/meta/meta-data", + "$vocabulary": { + "https://json-schema.org/draft/2019-09/vocab/meta-data": true + }, + "$recursiveAnchor": true, + "title": "Meta-data vocabulary meta-schema", + "type": ["object", "boolean"], + "properties": { + "title": {"type": "string"}, + "description": {"type": "string"}, + "default": true, + "deprecated": {"type": "boolean", "default": false}, + "readOnly": {"type": "boolean", "default": false}, + "writeOnly": {"type": "boolean", "default": false}, + "examples": {"type": "array", "items": true} + } + }, + "https://json-schema.org/draft/2019-09/meta/format": { + "$schema": "https://json-schema.org/draft/2019-09/schema", + "$id": "https://json-schema.org/draft/2019-09/meta/format", + "$vocabulary": { + "https://json-schema.org/draft/2019-09/vocab/format": true + }, + "$recursiveAnchor": true, + "title": "Format vocabulary meta-schema", + "type": ["object", "boolean"], + "properties": {"format": {"type": "string"}} + }, + "https://json-schema.org/draft/2019-09/meta/content": { + "$schema": "https://json-schema.org/draft/2019-09/schema", + "$id": "https://json-schema.org/draft/2019-09/meta/content", + "$vocabulary": { + "https://json-schema.org/draft/2019-09/vocab/content": true + }, + "$recursiveAnchor": true, + "title": "Content vocabulary meta-schema", + "type": ["object", "boolean"], + "properties": { + "contentMediaType": {"type": "string"}, + "contentEncoding": {"type": "string"}, + "contentSchema": {"$recursiveRef": "#"} + } + }, + "https://json-schema.org/draft/2019-09/schema": { + "$schema": "https://json-schema.org/draft/2019-09/schema", + "$id": "https://json-schema.org/draft/2019-09/schema", + "$vocabulary": { + "https://json-schema.org/draft/2019-09/vocab/core": true, + "https://json-schema.org/draft/2019-09/vocab/applicator": true, + "https://json-schema.org/draft/2019-09/vocab/validation": true, + "https://json-schema.org/draft/2019-09/vocab/meta-data": true, + "https://json-schema.org/draft/2019-09/vocab/format": false, + "https://json-schema.org/draft/2019-09/vocab/content": true + }, + "$recursiveAnchor": true, + "title": "Core and Validation specifications meta-schema", + "allOf": [ + {"$ref": "meta/core"}, + {"$ref": "meta/applicator"}, + {"$ref": "meta/validation"}, + {"$ref": "meta/meta-data"}, + {"$ref": "meta/format"}, + {"$ref": "meta/content"} + ], + "type": ["object", "boolean"], + "properties": { + "definitions": { + "$comment": "While no longer an official keyword as it is replaced by $defs, this keyword is retained in the meta-schema to prevent incompatible extensions as it remains in common use.", + "type": "object", + "additionalProperties": {"$recursiveRef": "#"}, + "default": {} + }, + "dependencies": { + "$comment": "\"dependencies\" is no longer a keyword, but schema authors should avoid redefining it to facilitate a smooth transition to \"dependentSchemas\" and \"dependentRequired\"", + "type": "object", + "additionalProperties": { + "anyOf": [ + {"$recursiveRef": "#"}, + {"$ref": "meta/validation#/$defs/stringArray"} + ] + } + } + } + }, + "https://json-schema.org/draft/2019-09/links": { + "$schema": "https://json-schema.org/draft/2019-09/schema", + "$id": "https://json-schema.org/draft/2019-09/links", + "title": "Link Description Object", + "allOf": [ + {"required": ["rel", "href"]}, + {"$ref": "#/$defs/noRequiredFields"} + ], + "$defs": { + "noRequiredFields": { + "type": "object", + "properties": { + "anchor": {"type": "string", "format": "uri-template"}, + "anchorPointer": { + "type": "string", + "anyOf": [ + {"format": "json-pointer"}, + {"format": "relative-json-pointer"} + ] + }, + "rel": { + "anyOf": [ + {"type": "string"}, + { + "type": "array", + "items": {"type": "string"}, + "minItems": 1 + } + ] + }, + "href": {"type": "string", "format": "uri-template"}, + "hrefSchema": { + "$ref": "https://json-schema.org/draft/2019-09/hyper-schema", + "default": false + }, + "templatePointers": { + "type": "object", + "additionalProperties": { + "type": "string", + "anyOf": [ + {"format": "json-pointer"}, + {"format": "relative-json-pointer"} + ] + } + }, + "templateRequired": { + "type": "array", + "items": {"type": "string"}, + "uniqueItems": true + }, + "title": {"type": "string"}, + "description": {"type": "string"}, + "targetSchema": { + "$ref": "https://json-schema.org/draft/2019-09/hyper-schema", + "default": true + }, + "targetMediaType": {"type": "string"}, + "targetHints": {}, + "headerSchema": { + "$ref": "https://json-schema.org/draft/2019-09/hyper-schema", + "default": true + }, + "submissionMediaType": { + "type": "string", + "default": "application/json" + }, + "submissionSchema": { + "$ref": "https://json-schema.org/draft/2019-09/hyper-schema", + "default": true + }, + "$comment": {"type": "string"} + } + } + } + }, + "https://json-schema.org/draft/2019-09/meta/hyper-schema": { + "$schema": "https://json-schema.org/draft/2019-09/hyper-schema", + "$id": "https://json-schema.org/draft/2019-09/meta/hyper-schema", + "$vocabulary": { + "https://json-schema.org/draft/2019-09/vocab/hyper-schema": true + }, + "$recursiveAnchor": true, + "title": "JSON Hyper-Schema Vocabulary Schema", + "type": ["object", "boolean"], + "properties": { + "base": {"type": "string", "format": "uri-template"}, + "links": { + "type": "array", + "items": { + "$ref": "https://json-schema.org/draft/2019-09/links" + } + } + }, + "links": [ + {"rel": "self", "href": "{+%24id}"} + ] + } + } + })JSON"); + + EXPECT_EQ(document, expected); } diff --git a/test/jsonschema/jsonschema_bundle_2020_12_test.cc b/test/jsonschema/jsonschema_bundle_2020_12_test.cc index 3ceb7ec87..83e4329a6 100644 --- a/test/jsonschema/jsonschema_bundle_2020_12_test.cc +++ b/test/jsonschema/jsonschema_bundle_2020_12_test.cc @@ -249,6 +249,29 @@ static auto test_resolver(std::string_view identifier) } } })JSON"); + } else if (identifier == "https://example.com/custom-cross-ref-meta") { + return sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://example.com/custom-cross-ref-meta", + "allOf": [ + { "$ref": "https://example.com/main#/$defs/Bar" } + ] + })JSON"); + } else if (identifier == "https://example.com/custom-full-meta") { + return sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://example.com/custom-full-meta", + "$vocabulary": { + "https://json-schema.org/draft/2020-12/vocab/core": true, + "https://json-schema.org/draft/2020-12/vocab/applicator": true, + "https://json-schema.org/draft/2020-12/vocab/validation": true + } + })JSON"); + } else if (identifier == "https://example.com/reserved-id-meta") { + return sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "__sourcemeta-core-bundle__" + })JSON"); } else { return sourcemeta::core::schema_resolver(identifier); } @@ -708,18 +731,23 @@ TEST(JSONSchema_bundle_2020_12, metaschema) { test_resolver); const sourcemeta::core::JSON expected = sourcemeta::core::parse_json(R"JSON({ - "$schema": "https://example.com/meta/1.json", - "type": "string", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$ref": "__sourcemeta-core-bundle__", "$defs": { - "https://example.com/meta/1.json": { - "$schema": "https://example.com/meta/2.json", - "$id": "https://example.com/meta/1.json", - "$vocabulary": { "https://json-schema.org/draft/2020-12/vocab/core": true } + "__sourcemeta-core-bundle__": { + "$schema": "https://example.com/meta/1.json", + "$id": "__sourcemeta-core-bundle__", + "type": "string" }, "https://example.com/meta/2.json": { "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://example.com/meta/2.json", "$vocabulary": { "https://json-schema.org/draft/2020-12/vocab/core": true } + }, + "https://example.com/meta/1.json": { + "$schema": "https://example.com/meta/2.json", + "$id": "https://example.com/meta/1.json", + "$vocabulary": { "https://json-schema.org/draft/2020-12/vocab/core": true } } } })JSON"); @@ -736,62 +764,95 @@ TEST(JSONSchema_bundle_2020_12, openapi_3_1_dialect) { sourcemeta::core::bundle(document, sourcemeta::core::schema_walker, test_resolver); - EXPECT_EQ(document.at("$schema").to_string(), - "https://spec.openapis.org/oas/3.1/dialect/base"); - EXPECT_TRUE(document.defines("$defs")); - EXPECT_TRUE(document.at("$defs").is_object()); - EXPECT_EQ(document.at("$defs").size(), 10); - - EXPECT_EQ( - document.at("$defs").at("https://spec.openapis.org/oas/3.1/dialect/base"), - sourcemeta::core::schema_resolver( - "https://spec.openapis.org/oas/3.1/dialect/base") - .value()); - EXPECT_EQ( - document.at("$defs").at("https://spec.openapis.org/oas/3.1/meta/base"), - sourcemeta::core::schema_resolver( - "https://spec.openapis.org/oas/3.1/meta/base") - .value()); - EXPECT_EQ( - document.at("$defs").at("https://json-schema.org/draft/2020-12/schema"), - sourcemeta::core::schema_resolver( - "https://json-schema.org/draft/2020-12/schema") - .value()); - EXPECT_EQ(document.at("$defs").at( - "https://json-schema.org/draft/2020-12/meta/core"), - sourcemeta::core::schema_resolver( - "https://json-schema.org/draft/2020-12/meta/core") - .value()); - EXPECT_EQ(document.at("$defs").at( - "https://json-schema.org/draft/2020-12/meta/applicator"), - sourcemeta::core::schema_resolver( - "https://json-schema.org/draft/2020-12/meta/applicator") - .value()); - EXPECT_EQ(document.at("$defs").at( - "https://json-schema.org/draft/2020-12/meta/unevaluated"), - sourcemeta::core::schema_resolver( - "https://json-schema.org/draft/2020-12/meta/unevaluated") - .value()); - EXPECT_EQ(document.at("$defs").at( - "https://json-schema.org/draft/2020-12/meta/validation"), - sourcemeta::core::schema_resolver( - "https://json-schema.org/draft/2020-12/meta/validation") - .value()); - EXPECT_EQ(document.at("$defs").at( - "https://json-schema.org/draft/2020-12/meta/meta-data"), - sourcemeta::core::schema_resolver( - "https://json-schema.org/draft/2020-12/meta/meta-data") - .value()); - EXPECT_EQ(document.at("$defs").at( - "https://json-schema.org/draft/2020-12/meta/format-annotation"), - sourcemeta::core::schema_resolver( - "https://json-schema.org/draft/2020-12/meta/format-annotation") - .value()); - EXPECT_EQ(document.at("$defs").at( - "https://json-schema.org/draft/2020-12/meta/content"), - sourcemeta::core::schema_resolver( - "https://json-schema.org/draft/2020-12/meta/content") - .value()); + const sourcemeta::core::JSON expected = sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$ref": "__sourcemeta-core-bundle__", + "$defs": { + "__sourcemeta-core-bundle__": { + "$schema": "https://spec.openapis.org/oas/3.1/dialect/base", + "$id": "__sourcemeta-core-bundle__", + "type": "object" + }, + "https://spec.openapis.org/oas/3.1/meta/base": { + "$id": "https://spec.openapis.org/oas/3.1/meta/base", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "OAS Base vocabulary", + "description": "A JSON Schema Vocabulary used in the OpenAPI Schema Dialect", + "$vocabulary": { + "https://spec.openapis.org/oas/3.1/vocab/base": true + }, + "$dynamicAnchor": "meta", + "type": ["object", "boolean"], + "properties": { + "example": true, + "discriminator": {"$ref": "#/$defs/discriminator"}, + "externalDocs": {"$ref": "#/$defs/external-docs"}, + "xml": {"$ref": "#/$defs/xml"} + }, + "$defs": { + "extensible": {"patternProperties": {"^x-": true}}, + "discriminator": { + "$ref": "#/$defs/extensible", + "type": "object", + "properties": { + "propertyName": {"type": "string"}, + "mapping": { + "type": "object", + "additionalProperties": {"type": "string"} + } + }, + "required": ["propertyName"], + "unevaluatedProperties": false + }, + "external-docs": { + "$ref": "#/$defs/extensible", + "type": "object", + "properties": { + "url": {"type": "string", "format": "uri-reference"}, + "description": {"type": "string"} + }, + "required": ["url"], + "unevaluatedProperties": false + }, + "xml": { + "$ref": "#/$defs/extensible", + "type": "object", + "properties": { + "name": {"type": "string"}, + "namespace": {"type": "string", "format": "uri"}, + "prefix": {"type": "string"}, + "attribute": {"type": "boolean"}, + "wrapped": {"type": "boolean"} + }, + "unevaluatedProperties": false + } + } + }, + "https://spec.openapis.org/oas/3.1/dialect/base": { + "$id": "https://spec.openapis.org/oas/3.1/dialect/base", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "OpenAPI 3.1 Schema Object Dialect", + "description": "A JSON Schema dialect describing schemas found in OpenAPI documents", + "$vocabulary": { + "https://json-schema.org/draft/2020-12/vocab/core": true, + "https://json-schema.org/draft/2020-12/vocab/applicator": true, + "https://json-schema.org/draft/2020-12/vocab/unevaluated": true, + "https://json-schema.org/draft/2020-12/vocab/validation": true, + "https://json-schema.org/draft/2020-12/vocab/meta-data": true, + "https://json-schema.org/draft/2020-12/vocab/format-annotation": true, + "https://json-schema.org/draft/2020-12/vocab/content": true, + "https://spec.openapis.org/oas/3.1/vocab/base": false + }, + "$dynamicAnchor": "meta", + "allOf": [ + {"$ref": "https://json-schema.org/draft/2020-12/schema"}, + {"$ref": "https://spec.openapis.org/oas/3.1/meta/base"} + ] + } + } + })JSON"); + + EXPECT_EQ(document, expected); } TEST(JSONSchema_bundle_2020_12, hyperschema_smoke) { @@ -818,32 +879,15 @@ TEST(JSONSchema_bundle_2020_12, hyperschema_1) { sourcemeta::core::bundle(document, sourcemeta::core::schema_walker, test_resolver); - EXPECT_TRUE(document.defines("$defs")); - EXPECT_TRUE(document.at("$defs").is_object()); - EXPECT_EQ(document.at("$defs").size(), 11); - - EXPECT_TRUE(document.at("$defs").defines( - "https://json-schema.org/draft/2020-12/schema")); - EXPECT_TRUE(document.at("$defs").defines( - "https://json-schema.org/draft/2020-12/meta/core")); - EXPECT_TRUE(document.at("$defs").defines( - "https://json-schema.org/draft/2020-12/meta/applicator")); - EXPECT_TRUE(document.at("$defs").defines( - "https://json-schema.org/draft/2020-12/meta/unevaluated")); - EXPECT_TRUE(document.at("$defs").defines( - "https://json-schema.org/draft/2020-12/meta/validation")); - EXPECT_TRUE(document.at("$defs").defines( - "https://json-schema.org/draft/2020-12/meta/meta-data")); - EXPECT_TRUE(document.at("$defs").defines( - "https://json-schema.org/draft/2020-12/meta/format-annotation")); - EXPECT_TRUE(document.at("$defs").defines( - "https://json-schema.org/draft/2020-12/meta/content")); - EXPECT_TRUE(document.at("$defs").defines( - "https://json-schema.org/draft/2020-12/meta/hyper-schema")); - EXPECT_TRUE(document.at("$defs").defines( - "https://json-schema.org/draft/2020-12/links")); - EXPECT_TRUE(document.at("$defs").defines( - "https://json-schema.org/draft/2020-12/hyper-schema")); + const sourcemeta::core::JSON expected = sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "allOf": [ + { "$ref": "https://json-schema.org/draft/2020-12/schema" }, + { "$ref": "https://json-schema.org/draft/2020-12/meta/hyper-schema" } + ] + })JSON"); + + EXPECT_EQ(document, expected); } TEST(JSONSchema_bundle_2020_12, bundle_to_definitions) { @@ -1324,3 +1368,471 @@ TEST(JSONSchema_bundle_2020_12, EXPECT_EQ(document, expected); } + +TEST(JSONSchema_bundle_2020_12, elevate_inner_with_absolute_id) { + sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://example.com/meta/1.json", + "$id": "https://example.com/main", + "type": "string" + })JSON"); + + sourcemeta::core::bundle(document, sourcemeta::core::schema_walker, + test_resolver); + + const sourcemeta::core::JSON expected = sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://example.com/main", + "$ref": "https://example.com/main/x", + "$defs": { + "https://example.com/main/x": { + "$schema": "https://example.com/meta/1.json", + "$id": "https://example.com/main/x", + "type": "string" + }, + "https://example.com/meta/2.json": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://example.com/meta/2.json", + "$vocabulary": { "https://json-schema.org/draft/2020-12/vocab/core": true } + }, + "https://example.com/meta/1.json": { + "$schema": "https://example.com/meta/2.json", + "$id": "https://example.com/meta/1.json", + "$vocabulary": { "https://json-schema.org/draft/2020-12/vocab/core": true } + } + } + })JSON"); + + EXPECT_EQ(document, expected); +} + +TEST(JSONSchema_bundle_2020_12, elevate_inner_self_ref_with_fragment) { + sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://example.com/custom-full-meta", + "$id": "https://example.com/main", + "type": "object", + "properties": { + "child": { "$ref": "https://example.com/main#/$defs/Bar" } + }, + "$defs": { + "Bar": { "type": "string" } + } + })JSON"); + + sourcemeta::core::bundle(document, sourcemeta::core::schema_walker, + test_resolver); + + const sourcemeta::core::JSON expected = sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://example.com/main", + "$ref": "https://example.com/main/x", + "$defs": { + "https://example.com/main/x": { + "$schema": "https://example.com/custom-full-meta", + "$id": "https://example.com/main/x", + "type": "object", + "properties": { + "child": { "$ref": "https://example.com/main/x#/$defs/Bar" } + }, + "$defs": { + "Bar": { "type": "string" } + } + }, + "https://example.com/custom-full-meta": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://example.com/custom-full-meta", + "$vocabulary": { + "https://json-schema.org/draft/2020-12/vocab/core": true, + "https://json-schema.org/draft/2020-12/vocab/applicator": true, + "https://json-schema.org/draft/2020-12/vocab/validation": true + } + } + } + })JSON"); + + EXPECT_EQ(document, expected); +} + +TEST(JSONSchema_bundle_2020_12, elevate_no_id_with_default_id) { + sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://example.com/meta/1.json", + "type": "string" + })JSON"); + + sourcemeta::core::bundle(document, sourcemeta::core::schema_walker, + test_resolver, "", "https://example.com/default"); + + const sourcemeta::core::JSON expected = sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://example.com/default", + "$ref": "https://example.com/default/x", + "$defs": { + "https://example.com/default/x": { + "$schema": "https://example.com/meta/1.json", + "$id": "https://example.com/default/x", + "type": "string" + }, + "https://example.com/meta/2.json": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://example.com/meta/2.json", + "$vocabulary": { "https://json-schema.org/draft/2020-12/vocab/core": true } + }, + "https://example.com/meta/1.json": { + "$schema": "https://example.com/meta/2.json", + "$id": "https://example.com/meta/1.json", + "$vocabulary": { "https://json-schema.org/draft/2020-12/vocab/core": true } + } + } + })JSON"); + + EXPECT_EQ(document, expected); +} + +TEST(JSONSchema_bundle_2020_12, elevate_collision_uses_x_x) { + sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://example.com/meta/1.json", + "$id": "https://example.com/main", + "type": "object", + "$defs": { + "sub": { + "$id": "https://example.com/main/x", + "type": "integer" + } + } + })JSON"); + + sourcemeta::core::bundle(document, sourcemeta::core::schema_walker, + test_resolver); + + const sourcemeta::core::JSON expected = sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://example.com/main", + "$ref": "https://example.com/main/x/x", + "$defs": { + "https://example.com/main/x/x": { + "$schema": "https://example.com/meta/1.json", + "$id": "https://example.com/main/x/x", + "type": "object", + "$defs": { + "sub": { + "$id": "https://example.com/main/x", + "type": "integer" + } + } + }, + "https://example.com/meta/2.json": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://example.com/meta/2.json", + "$vocabulary": { "https://json-schema.org/draft/2020-12/vocab/core": true } + }, + "https://example.com/meta/1.json": { + "$schema": "https://example.com/meta/2.json", + "$id": "https://example.com/meta/1.json", + "$vocabulary": { "https://json-schema.org/draft/2020-12/vocab/core": true } + } + } + })JSON"); + + EXPECT_EQ(document, expected); +} + +TEST(JSONSchema_bundle_2020_12, elevate_idempotent) { + sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://example.com/meta/1.json", + "$id": "https://example.com/main", + "type": "string" + })JSON"); + + sourcemeta::core::bundle(document, sourcemeta::core::schema_walker, + test_resolver); + sourcemeta::core::bundle(document, sourcemeta::core::schema_walker, + test_resolver); + + const sourcemeta::core::JSON expected = sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://example.com/main", + "$ref": "https://example.com/main/x", + "$defs": { + "https://example.com/main/x": { + "$schema": "https://example.com/meta/1.json", + "$id": "https://example.com/main/x", + "type": "string" + }, + "https://example.com/meta/2.json": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://example.com/meta/2.json", + "$vocabulary": { "https://json-schema.org/draft/2020-12/vocab/core": true } + }, + "https://example.com/meta/1.json": { + "$schema": "https://example.com/meta/2.json", + "$id": "https://example.com/meta/1.json", + "$vocabulary": { "https://json-schema.org/draft/2020-12/vocab/core": true } + } + } + })JSON"); + + EXPECT_EQ(document, expected); +} + +TEST(JSONSchema_bundle_2020_12, elevate_cross_referencing_meta_schema) { + sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://example.com/custom-cross-ref-meta", + "$id": "https://example.com/main", + "$defs": { + "Bar": { "type": "string" } + } + })JSON"); + + sourcemeta::core::bundle(document, sourcemeta::core::schema_walker, + test_resolver); + + const sourcemeta::core::JSON expected = sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://example.com/main", + "$ref": "https://example.com/main/x", + "$defs": { + "https://example.com/main/x": { + "$schema": "https://example.com/custom-cross-ref-meta", + "$id": "https://example.com/main/x", + "$defs": { + "Bar": { "type": "string" } + } + }, + "https://example.com/custom-cross-ref-meta": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://example.com/custom-cross-ref-meta", + "allOf": [ + { "$ref": "https://example.com/main/x#/$defs/Bar" } + ] + } + } + })JSON"); + + EXPECT_EQ(document, expected); +} + +TEST(JSONSchema_bundle_2020_12, + elevate_anonymous_inner_keeps_fragment_only_refs_working) { + sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://example.com/custom-full-meta", + "type": "object", + "properties": { + "child": { "$ref": "#/$defs/Bar" } + }, + "$defs": { + "Bar": { "type": "string" } + } + })JSON"); + + sourcemeta::core::bundle(document, sourcemeta::core::schema_walker, + test_resolver); + + const sourcemeta::core::JSON expected = sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$ref": "__sourcemeta-core-bundle__", + "$defs": { + "__sourcemeta-core-bundle__": { + "$schema": "https://example.com/custom-full-meta", + "$id": "__sourcemeta-core-bundle__", + "type": "object", + "properties": { + "child": { "$ref": "#/$defs/Bar" } + }, + "$defs": { + "Bar": { "type": "string" } + } + }, + "https://example.com/custom-full-meta": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://example.com/custom-full-meta", + "$vocabulary": { + "https://json-schema.org/draft/2020-12/vocab/core": true, + "https://json-schema.org/draft/2020-12/vocab/applicator": true, + "https://json-schema.org/draft/2020-12/vocab/validation": true + } + } + } + })JSON"); + + EXPECT_EQ(document, expected); +} + +TEST(JSONSchema_bundle_2020_12, elevate_default_container_skips_elevation) { + sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://example.com/meta/1.json", + "$id": "https://example.com/main", + "type": "string" + })JSON"); + + sourcemeta::core::bundle(document, sourcemeta::core::schema_walker, + test_resolver, "", "", + sourcemeta::core::Pointer{"components"}); + + const sourcemeta::core::JSON expected = sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://example.com/meta/1.json", + "$id": "https://example.com/main", + "type": "string", + "components": { + "https://example.com/meta/2.json": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://example.com/meta/2.json", + "$vocabulary": { "https://json-schema.org/draft/2020-12/vocab/core": true } + }, + "https://example.com/meta/1.json": { + "$schema": "https://example.com/meta/2.json", + "$id": "https://example.com/meta/1.json", + "$vocabulary": { "https://json-schema.org/draft/2020-12/vocab/core": true } + } + } + })JSON"); + + EXPECT_EQ(document, expected); +} + +TEST(JSONSchema_bundle_2020_12, elevate_openapi_with_inner_id) { + sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://spec.openapis.org/oas/3.1/dialect/base", + "$id": "https://example.com/my-api", + "type": "object" + })JSON"); + + sourcemeta::core::bundle(document, sourcemeta::core::schema_walker, + test_resolver); + + const sourcemeta::core::JSON expected = sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://example.com/my-api", + "$ref": "https://example.com/my-api/x", + "$defs": { + "https://example.com/my-api/x": { + "$schema": "https://spec.openapis.org/oas/3.1/dialect/base", + "$id": "https://example.com/my-api/x", + "type": "object" + }, + "https://spec.openapis.org/oas/3.1/meta/base": { + "$id": "https://spec.openapis.org/oas/3.1/meta/base", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "OAS Base vocabulary", + "description": "A JSON Schema Vocabulary used in the OpenAPI Schema Dialect", + "$vocabulary": { + "https://spec.openapis.org/oas/3.1/vocab/base": true + }, + "$dynamicAnchor": "meta", + "type": ["object", "boolean"], + "properties": { + "example": true, + "discriminator": {"$ref": "#/$defs/discriminator"}, + "externalDocs": {"$ref": "#/$defs/external-docs"}, + "xml": {"$ref": "#/$defs/xml"} + }, + "$defs": { + "extensible": {"patternProperties": {"^x-": true}}, + "discriminator": { + "$ref": "#/$defs/extensible", + "type": "object", + "properties": { + "propertyName": {"type": "string"}, + "mapping": { + "type": "object", + "additionalProperties": {"type": "string"} + } + }, + "required": ["propertyName"], + "unevaluatedProperties": false + }, + "external-docs": { + "$ref": "#/$defs/extensible", + "type": "object", + "properties": { + "url": {"type": "string", "format": "uri-reference"}, + "description": {"type": "string"} + }, + "required": ["url"], + "unevaluatedProperties": false + }, + "xml": { + "$ref": "#/$defs/extensible", + "type": "object", + "properties": { + "name": {"type": "string"}, + "namespace": {"type": "string", "format": "uri"}, + "prefix": {"type": "string"}, + "attribute": {"type": "boolean"}, + "wrapped": {"type": "boolean"} + }, + "unevaluatedProperties": false + } + } + }, + "https://spec.openapis.org/oas/3.1/dialect/base": { + "$id": "https://spec.openapis.org/oas/3.1/dialect/base", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "OpenAPI 3.1 Schema Object Dialect", + "description": "A JSON Schema dialect describing schemas found in OpenAPI documents", + "$vocabulary": { + "https://json-schema.org/draft/2020-12/vocab/core": true, + "https://json-schema.org/draft/2020-12/vocab/applicator": true, + "https://json-schema.org/draft/2020-12/vocab/unevaluated": true, + "https://json-schema.org/draft/2020-12/vocab/validation": true, + "https://json-schema.org/draft/2020-12/vocab/meta-data": true, + "https://json-schema.org/draft/2020-12/vocab/format-annotation": true, + "https://json-schema.org/draft/2020-12/vocab/content": true, + "https://spec.openapis.org/oas/3.1/vocab/base": false + }, + "$dynamicAnchor": "meta", + "allOf": [ + {"$ref": "https://json-schema.org/draft/2020-12/schema"}, + {"$ref": "https://spec.openapis.org/oas/3.1/meta/base"} + ] + } + } + })JSON"); + + EXPECT_EQ(document, expected); +} + +TEST(JSONSchema_bundle_2020_12, elevate_fallback_collision_throws) { + sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://example.com/reserved-id-meta", + "type": "string" + })JSON"); + + try { + sourcemeta::core::bundle(document, sourcemeta::core::schema_walker, + test_resolver); + FAIL(); + } catch (const sourcemeta::core::SchemaReservedIdentifierError &error) { + EXPECT_EQ(error.identifier(), "__sourcemeta-core-bundle__"); + } catch (...) { + FAIL(); + } +} + +TEST(JSONSchema_bundle_2020_12, fallback_collision_allowed_when_input_has_id) { + sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://example.com/reserved-id-meta", + "$id": "https://example.com/main", + "type": "string" + })JSON"); + + sourcemeta::core::bundle(document, sourcemeta::core::schema_walker, + test_resolver); + + const sourcemeta::core::JSON expected = sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://example.com/main", + "$ref": "https://example.com/main/x", + "$defs": { + "https://example.com/main/x": { + "$schema": "__sourcemeta-core-bundle__", + "$id": "https://example.com/main/x", + "type": "string" + }, + "__sourcemeta-core-bundle__": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "__sourcemeta-core-bundle__" + } + } + })JSON"); + + EXPECT_EQ(document, expected); +} diff --git a/test/jsonschema/jsonschema_bundle_draft4_test.cc b/test/jsonschema/jsonschema_bundle_draft4_test.cc index 6a3721957..ae09934cb 100644 --- a/test/jsonschema/jsonschema_bundle_draft4_test.cc +++ b/test/jsonschema/jsonschema_bundle_draft4_test.cc @@ -477,16 +477,23 @@ TEST(JSONSchema_bundle_draft4, metaschema) { test_resolver); const sourcemeta::core::JSON expected = sourcemeta::core::parse_json(R"JSON({ - "$schema": "https://example.com/meta/1.json", - "type": "string", + "$schema": "http://json-schema.org/draft-04/schema#", + "allOf": [ + { "$ref": "__sourcemeta-core-bundle__" } + ], "definitions": { - "https://example.com/meta/1.json": { - "$schema": "https://example.com/meta/2.json", - "id": "https://example.com/meta/1.json" + "__sourcemeta-core-bundle__": { + "$schema": "https://example.com/meta/1.json", + "id": "__sourcemeta-core-bundle__", + "type": "string" }, "https://example.com/meta/2.json": { "$schema": "http://json-schema.org/draft-04/schema#", "id": "https://example.com/meta/2.json" + }, + "https://example.com/meta/1.json": { + "$schema": "https://example.com/meta/2.json", + "id": "https://example.com/meta/1.json" } } })JSON"); @@ -566,14 +573,15 @@ TEST(JSONSchema_bundle_draft4, hyperschema_1) { sourcemeta::core::bundle(document, sourcemeta::core::schema_walker, test_resolver); - EXPECT_TRUE(document.defines("definitions")); - EXPECT_TRUE(document.at("definitions").is_object()); - EXPECT_EQ(document.at("definitions").size(), 2); + const sourcemeta::core::JSON expected = sourcemeta::core::parse_json(R"JSON({ + "$schema": "http://json-schema.org/draft-04/schema#", + "allOf": [ + { "$ref": "http://json-schema.org/draft-04/schema#" }, + { "$ref": "http://json-schema.org/draft-04/hyper-schema#" } + ] + })JSON"); - EXPECT_TRUE(document.at("definitions") - .defines("http://json-schema.org/draft-04/schema#")); - EXPECT_TRUE(document.at("definitions") - .defines("http://json-schema.org/draft-04/hyper-schema#")); + EXPECT_EQ(document, expected); } TEST(JSONSchema_bundle_draft4, hyperschema_ref_metaschema) { @@ -587,12 +595,14 @@ TEST(JSONSchema_bundle_draft4, hyperschema_ref_metaschema) { sourcemeta::core::bundle(document, sourcemeta::core::schema_walker, test_resolver); - EXPECT_TRUE(document.defines("definitions")); - EXPECT_TRUE(document.at("definitions").is_object()); - EXPECT_EQ(document.at("definitions").size(), 1); + const sourcemeta::core::JSON expected = sourcemeta::core::parse_json(R"JSON({ + "$schema": "http://json-schema.org/draft-04/hyper-schema#", + "allOf": [ + { "$ref": "http://json-schema.org/draft-04/schema#" } + ] + })JSON"); - EXPECT_TRUE(document.at("definitions") - .defines("http://json-schema.org/draft-04/schema#")); + EXPECT_EQ(document, expected); } TEST(JSONSchema_bundle_draft4, standalone_ref_with_default_dialect) { diff --git a/test/jsonschema/jsonschema_bundle_draft6_test.cc b/test/jsonschema/jsonschema_bundle_draft6_test.cc index cecfed5f7..0d14676a9 100644 --- a/test/jsonschema/jsonschema_bundle_draft6_test.cc +++ b/test/jsonschema/jsonschema_bundle_draft6_test.cc @@ -436,16 +436,23 @@ TEST(JSONSchema_bundle_draft6, metaschema) { test_resolver); const sourcemeta::core::JSON expected = sourcemeta::core::parse_json(R"JSON({ - "$schema": "https://example.com/meta/1.json", - "type": "string", + "$schema": "http://json-schema.org/draft-06/schema#", + "allOf": [ + { "$ref": "__sourcemeta-core-bundle__" } + ], "definitions": { - "https://example.com/meta/1.json": { - "$schema": "https://example.com/meta/2.json", - "$id": "https://example.com/meta/1.json" + "__sourcemeta-core-bundle__": { + "$schema": "https://example.com/meta/1.json", + "$id": "__sourcemeta-core-bundle__", + "type": "string" }, "https://example.com/meta/2.json": { "$schema": "http://json-schema.org/draft-06/schema#", "$id": "https://example.com/meta/2.json" + }, + "https://example.com/meta/1.json": { + "$schema": "https://example.com/meta/2.json", + "$id": "https://example.com/meta/1.json" } } })JSON"); @@ -508,14 +515,15 @@ TEST(JSONSchema_bundle_draft6, hyperschema_1) { sourcemeta::core::bundle(document, sourcemeta::core::schema_walker, test_resolver); - EXPECT_TRUE(document.defines("definitions")); - EXPECT_TRUE(document.at("definitions").is_object()); - EXPECT_EQ(document.at("definitions").size(), 2); + const sourcemeta::core::JSON expected = sourcemeta::core::parse_json(R"JSON({ + "$schema": "http://json-schema.org/draft-06/schema#", + "allOf": [ + { "$ref": "http://json-schema.org/draft-06/schema#" }, + { "$ref": "http://json-schema.org/draft-06/hyper-schema#" } + ] + })JSON"); - EXPECT_TRUE(document.at("definitions") - .defines("http://json-schema.org/draft-06/schema#")); - EXPECT_TRUE(document.at("definitions") - .defines("http://json-schema.org/draft-06/hyper-schema#")); + EXPECT_EQ(document, expected); } TEST(JSONSchema_bundle_draft6, hyperschema_ref_metaschema) { @@ -529,12 +537,14 @@ TEST(JSONSchema_bundle_draft6, hyperschema_ref_metaschema) { sourcemeta::core::bundle(document, sourcemeta::core::schema_walker, test_resolver); - EXPECT_TRUE(document.defines("definitions")); - EXPECT_TRUE(document.at("definitions").is_object()); - EXPECT_EQ(document.at("definitions").size(), 1); + const sourcemeta::core::JSON expected = sourcemeta::core::parse_json(R"JSON({ + "$schema": "http://json-schema.org/draft-06/hyper-schema#", + "allOf": [ + { "$ref": "http://json-schema.org/draft-06/schema#" } + ] + })JSON"); - EXPECT_TRUE(document.at("definitions") - .defines("http://json-schema.org/draft-06/schema#")); + EXPECT_EQ(document, expected); } TEST(JSONSchema_bundle_draft6, standalone_ref_with_default_dialect) { diff --git a/test/jsonschema/jsonschema_bundle_draft7_test.cc b/test/jsonschema/jsonschema_bundle_draft7_test.cc index e989ead09..6d62adc9a 100644 --- a/test/jsonschema/jsonschema_bundle_draft7_test.cc +++ b/test/jsonschema/jsonschema_bundle_draft7_test.cc @@ -475,16 +475,23 @@ TEST(JSONSchema_bundle_draft7, metaschema) { test_resolver); const sourcemeta::core::JSON expected = sourcemeta::core::parse_json(R"JSON({ - "$schema": "https://example.com/meta/1.json", - "type": "string", + "$schema": "http://json-schema.org/draft-07/schema#", + "allOf": [ + { "$ref": "__sourcemeta-core-bundle__" } + ], "definitions": { - "https://example.com/meta/1.json": { - "$schema": "https://example.com/meta/2.json", - "$id": "https://example.com/meta/1.json" + "__sourcemeta-core-bundle__": { + "$schema": "https://example.com/meta/1.json", + "$id": "__sourcemeta-core-bundle__", + "type": "string" }, "https://example.com/meta/2.json": { "$schema": "http://json-schema.org/draft-07/schema#", "$id": "https://example.com/meta/2.json" + }, + "https://example.com/meta/1.json": { + "$schema": "https://example.com/meta/2.json", + "$id": "https://example.com/meta/1.json" } } })JSON"); @@ -547,16 +554,15 @@ TEST(JSONSchema_bundle_draft7, hyperschema_1) { sourcemeta::core::bundle(document, sourcemeta::core::schema_walker, test_resolver); - EXPECT_TRUE(document.defines("definitions")); - EXPECT_TRUE(document.at("definitions").is_object()); - EXPECT_EQ(document.at("definitions").size(), 3); + const sourcemeta::core::JSON expected = sourcemeta::core::parse_json(R"JSON({ + "$schema": "http://json-schema.org/draft-07/schema#", + "allOf": [ + { "$ref": "http://json-schema.org/draft-07/schema#" }, + { "$ref": "http://json-schema.org/draft-07/hyper-schema#" } + ] + })JSON"); - EXPECT_TRUE(document.at("definitions") - .defines("http://json-schema.org/draft-07/schema#")); - EXPECT_TRUE(document.at("definitions") - .defines("http://json-schema.org/draft-07/links#")); - EXPECT_TRUE(document.at("definitions") - .defines("http://json-schema.org/draft-07/hyper-schema#")); + EXPECT_EQ(document, expected); } TEST(JSONSchema_bundle_draft7, hyperschema_ref_metaschema) { @@ -570,12 +576,14 @@ TEST(JSONSchema_bundle_draft7, hyperschema_ref_metaschema) { sourcemeta::core::bundle(document, sourcemeta::core::schema_walker, test_resolver); - EXPECT_TRUE(document.defines("definitions")); - EXPECT_TRUE(document.at("definitions").is_object()); - EXPECT_EQ(document.at("definitions").size(), 1); + const sourcemeta::core::JSON expected = sourcemeta::core::parse_json(R"JSON({ + "$schema": "http://json-schema.org/draft-07/hyper-schema#", + "allOf": [ + { "$ref": "http://json-schema.org/draft-07/schema#" } + ] + })JSON"); - EXPECT_TRUE(document.at("definitions") - .defines("http://json-schema.org/draft-07/schema#")); + EXPECT_EQ(document, expected); } TEST(JSONSchema_bundle_draft7, bundle_to_defs) {