From 9ae3921842faa40aeedf9c1e4047c61aabdf969f Mon Sep 17 00:00:00 2001 From: Juan Cruz Viotti Date: Fri, 30 Jan 2026 16:55:44 -0400 Subject: [PATCH 1/4] [WIP] Optimise wrapping to subschemas with no references See: https://github.com/sourcemeta/jsonschema/issues/629 Signed-off-by: Juan Cruz Viotti --- .../include/sourcemeta/core/jsonschema.h | 15 +- src/core/jsonschema/jsonschema.cc | 40 +- test/jsonschema/jsonschema_wrap_test.cc | 421 ++++++------------ 3 files changed, 171 insertions(+), 305 deletions(-) diff --git a/src/core/jsonschema/include/sourcemeta/core/jsonschema.h b/src/core/jsonschema/include/sourcemeta/core/jsonschema.h index a18eeadab..c27287f39 100644 --- a/src/core/jsonschema/include/sourcemeta/core/jsonschema.h +++ b/src/core/jsonschema/include/sourcemeta/core/jsonschema.h @@ -422,16 +422,25 @@ auto wrap(std::string_view identifier) -> JSON; /// "items": { "type": "string" } /// })JSON"); /// +/// sourcemeta::core::SchemaFrame frame{ +/// sourcemeta::core::SchemaFrame::Mode::References}; +/// frame.analyse(document, sourcemeta::core::schema_walker, +/// sourcemeta::core::schema_resolver); +/// +/// const auto location{frame.traverse( +/// sourcemeta::core::Pointer{"items"}, +/// sourcemeta::core::SchemaFrame::LocationType::Subschema)}; +/// /// const sourcemeta::core::JSON result = -/// sourcemeta::core::wrap(document, { "items" }, +/// sourcemeta::core::wrap(document, frame, location.value().get(), /// sourcemeta::core::schema_resolver); /// /// sourcemeta::core::prettify(result, std::cerr); /// std::cerr << "\n"; /// ``` SOURCEMETA_CORE_JSONSCHEMA_EXPORT -auto wrap(const JSON &schema, const Pointer &pointer, - const SchemaResolver &resolver, std::string_view default_dialect = "") +auto wrap(const JSON &schema, const SchemaFrame &frame, + const SchemaFrame::Location &location, const SchemaResolver &resolver) -> JSON; /// @ingroup jsonschema diff --git a/src/core/jsonschema/jsonschema.cc b/src/core/jsonschema/jsonschema.cc index aad330940..5a688328e 100644 --- a/src/core/jsonschema/jsonschema.cc +++ b/src/core/jsonschema/jsonschema.cc @@ -590,24 +590,34 @@ auto sourcemeta::core::wrap(const std::string_view identifier) return result; } -auto sourcemeta::core::wrap(const sourcemeta::core::JSON &schema, - const sourcemeta::core::Pointer &pointer, - const sourcemeta::core::SchemaResolver &resolver, - std::string_view default_dialect) +auto sourcemeta::core::wrap( + const sourcemeta::core::JSON &schema, + const sourcemeta::core::SchemaFrame &frame, + const sourcemeta::core::SchemaFrame::Location &location, + const sourcemeta::core::SchemaResolver &resolver) -> sourcemeta::core::JSON { + assert(location.type != SchemaFrame::LocationType::Pointer); + const auto &pointer{location.pointer}; assert(try_get(schema, pointer)); if (pointer.empty()) { return schema; } - auto copy = schema; - const auto effective_dialect{dialect(copy, default_dialect)}; - if (!effective_dialect.empty()) { - copy.assign("$schema", JSON{effective_dialect}); - } else { - throw SchemaUnknownBaseDialectError(); + const auto has_internal_references{ + std::any_of(frame.references().cbegin(), frame.references().cend(), + [&pointer](const auto &reference) { + return reference.first.second.starts_with(pointer); + })}; + + if (!has_internal_references) { + auto subschema{get(schema, pointer)}; + subschema.assign("$schema", JSON{JSON::String{location.dialect}}); + return subschema; } + auto copy = schema; + copy.assign("$schema", JSON{JSON::String{location.dialect}}); + auto result{JSON::make_object()}; // JSON Schema 2020-12 is the first dialect that truly supports // cross-dialect references In practice, others do, but we can @@ -622,13 +632,13 @@ auto sourcemeta::core::wrap(const sourcemeta::core::JSON &schema, // other schemas whose top-level identifiers are relative URIs don't // get affected. Otherwise, we would cause unintended base resolution. constexpr std::string_view WRAPPER_IDENTIFIER{"__sourcemeta-core-wrap__"}; - const auto maybe_id{identify(copy, resolver, default_dialect)}; + const auto maybe_id{identify(copy, resolver, location.dialect)}; const auto id{maybe_id.empty() ? WRAPPER_IDENTIFIER : maybe_id}; URI uri{id}; try { - reidentify(copy, id, resolver, default_dialect); + reidentify(copy, id, resolver, location.dialect); // Otherwise we will get an error with the `WRAPPER_IDENTIFIER`, which will // be confusing to end users @@ -647,9 +657,13 @@ auto sourcemeta::core::wrap(const sourcemeta::core::JSON &schema, uri.fragment(to_string(pointer)); result.assign_assume_new("$ref", JSON{uri.recompose()}); } else { + static const JSON::String DEFS{"$defs"}; + static const JSON::String SCHEMA{"schema"}; result.assign_assume_new( "$ref", - JSON{to_uri(Pointer{"$defs", "schema"}.concat(pointer)).recompose()}); + JSON{to_uri(WeakPointer{std::cref(DEFS), std::cref(SCHEMA)}.concat( + pointer)) + .recompose()}); } return result; diff --git a/test/jsonschema/jsonschema_wrap_test.cc b/test/jsonschema/jsonschema_wrap_test.cc index a9b5fdbcc..35bc2afac 100644 --- a/test/jsonschema/jsonschema_wrap_test.cc +++ b/test/jsonschema/jsonschema_wrap_test.cc @@ -3,6 +3,21 @@ #include #include +static auto wrap_schema(const sourcemeta::core::JSON &schema, + const sourcemeta::core::Pointer &pointer, + std::string_view default_dialect = "") + -> sourcemeta::core::JSON { + sourcemeta::core::SchemaFrame frame{ + sourcemeta::core::SchemaFrame::Mode::References}; + frame.analyse(schema, sourcemeta::core::schema_walker, + sourcemeta::core::schema_resolver, default_dialect); + const auto location{ + frame.traverse(sourcemeta::core::to_weak_pointer(pointer))}; + assert(location.has_value()); + return sourcemeta::core::wrap(schema, frame, location.value().get(), + sourcemeta::core::schema_resolver); +} + TEST(JSONSchema_wrap, identifier_without_fragment) { const auto identifier{"https://www.example.com"}; const auto result{sourcemeta::core::wrap(identifier)}; @@ -49,21 +64,11 @@ TEST(JSONSchema_wrap, schema_without_identifier) { } })JSON")}; - const auto result{sourcemeta::core::wrap(schema, {"items"}, - sourcemeta::core::schema_resolver)}; + const auto result{wrap_schema(schema, {"items"})}; const auto expected{sourcemeta::core::parse_json(R"JSON({ "$schema": "https://json-schema.org/draft/2020-12/schema", - "$ref": "__sourcemeta-core-wrap__#/items", - "$defs": { - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "__sourcemeta-core-wrap__", - "items": { - "type": "string" - } - } - } + "type": "string" })JSON")}; EXPECT_EQ(result, expected); @@ -77,8 +82,7 @@ TEST(JSONSchema_wrap, schema_without_identifier_and_relative_uri) { } })JSON")}; - const auto result{sourcemeta::core::wrap(schema, {"items"}, - sourcemeta::core::schema_resolver)}; + const auto result{wrap_schema(schema, {"items"})}; // We don't want the relative reference to be resolved against // an absolute base @@ -106,22 +110,12 @@ TEST(JSONSchema_wrap, schema_without_identifier_with_default_dialect) { } })JSON")}; - const auto result{sourcemeta::core::wrap( - schema, {"items"}, sourcemeta::core::schema_resolver, - "https://json-schema.org/draft/2020-12/schema")}; + const auto result{wrap_schema( + schema, {"items"}, "https://json-schema.org/draft/2020-12/schema")}; const auto expected{sourcemeta::core::parse_json(R"JSON({ "$schema": "https://json-schema.org/draft/2020-12/schema", - "$ref": "__sourcemeta-core-wrap__#/items", - "$defs": { - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "__sourcemeta-core-wrap__", - "items": { - "type": "string" - } - } - } + "type": "string" })JSON")}; EXPECT_EQ(result, expected); @@ -136,22 +130,12 @@ TEST(JSONSchema_wrap, } })JSON")}; - const auto result{sourcemeta::core::wrap( - schema, {"items"}, sourcemeta::core::schema_resolver, - "https://json-schema.org/draft/2019-09/schema")}; + const auto result{wrap_schema( + schema, {"items"}, "https://json-schema.org/draft/2019-09/schema")}; const auto expected{sourcemeta::core::parse_json(R"JSON({ "$schema": "https://json-schema.org/draft/2020-12/schema", - "$ref": "__sourcemeta-core-wrap__#/items", - "$defs": { - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "__sourcemeta-core-wrap__", - "items": { - "type": "string" - } - } - } + "type": "string" })JSON")}; EXPECT_EQ(result, expected); @@ -165,8 +149,7 @@ TEST(JSONSchema_wrap, schema_without_identifier_empty_pointer) { } })JSON")}; - const auto result{ - sourcemeta::core::wrap(schema, {}, sourcemeta::core::schema_resolver)}; + const auto result{wrap_schema(schema, {})}; const auto expected{sourcemeta::core::parse_json(R"JSON({ "$schema": "https://json-schema.org/draft/2020-12/schema", @@ -185,8 +168,7 @@ TEST(JSONSchema_wrap, schema_without_identifier_without_dialect) { } })JSON")}; - EXPECT_THROW(sourcemeta::core::wrap(schema, {"items"}, - sourcemeta::core::schema_resolver), + EXPECT_THROW(wrap_schema(schema, {"items"}), sourcemeta::core::SchemaUnknownBaseDialectError); } @@ -199,21 +181,11 @@ TEST(JSONSchema_wrap, schema_with_identifier) { } })JSON")}; - const auto result{sourcemeta::core::wrap(schema, {"items"}, - sourcemeta::core::schema_resolver)}; + const auto result{wrap_schema(schema, {"items"})}; const auto expected{sourcemeta::core::parse_json(R"JSON({ "$schema": "https://json-schema.org/draft/2020-12/schema", - "$ref": "https://sourcemeta.com/1#/items", - "$defs": { - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://sourcemeta.com/1", - "items": { - "type": "string" - } - } - } + "type": "string" })JSON")}; EXPECT_EQ(result, expected); @@ -228,21 +200,11 @@ TEST(JSONSchema_wrap, schema_with_identifier_trailing_empty_fragment) { } })JSON")}; - const auto result{sourcemeta::core::wrap(schema, {"items"}, - sourcemeta::core::schema_resolver)}; + const auto result{wrap_schema(schema, {"items"})}; const auto expected{sourcemeta::core::parse_json(R"JSON({ "$schema": "https://json-schema.org/draft/2020-12/schema", - "$ref": "https://sourcemeta.com/1#/items", - "$defs": { - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://sourcemeta.com/1#", - "items": { - "type": "string" - } - } - } + "type": "string" })JSON")}; EXPECT_EQ(result, expected); @@ -257,22 +219,12 @@ TEST(JSONSchema_wrap, schema_with_identifier_different_default_dialect) { } })JSON")}; - const auto result{sourcemeta::core::wrap( - schema, {"items"}, sourcemeta::core::schema_resolver, - "https://json-schema.org/draft/2019-09/schema")}; + const auto result{wrap_schema( + schema, {"items"}, "https://json-schema.org/draft/2019-09/schema")}; const auto expected{sourcemeta::core::parse_json(R"JSON({ "$schema": "https://json-schema.org/draft/2020-12/schema", - "$ref": "https://sourcemeta.com/1#/items", - "$defs": { - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://sourcemeta.com/1", - "items": { - "type": "string" - } - } - } + "type": "string" })JSON")}; EXPECT_EQ(result, expected); @@ -286,22 +238,12 @@ TEST(JSONSchema_wrap, schema_with_identifier_default_dialect) { } })JSON")}; - const auto result{sourcemeta::core::wrap( - schema, {"items"}, sourcemeta::core::schema_resolver, - "https://json-schema.org/draft/2019-09/schema")}; + const auto result{wrap_schema( + schema, {"items"}, "https://json-schema.org/draft/2019-09/schema")}; const auto expected{sourcemeta::core::parse_json(R"JSON({ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$ref": "https://sourcemeta.com/1#/items", - "$defs": { - "schema": { - "$schema": "https://json-schema.org/draft/2019-09/schema", - "$id": "https://sourcemeta.com/1", - "items": { - "type": "string" - } - } - } + "$schema": "https://json-schema.org/draft/2019-09/schema", + "type": "string" })JSON")}; EXPECT_EQ(result, expected); @@ -309,16 +251,17 @@ TEST(JSONSchema_wrap, schema_with_identifier_default_dialect) { TEST(JSONSchema_wrap, schema_with_identifier_empty_pointer) { const auto schema{sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://sourcemeta.com/1", "items": { "type": "string" } })JSON")}; - const auto result{ - sourcemeta::core::wrap(schema, {}, sourcemeta::core::schema_resolver)}; + const auto result{wrap_schema(schema, {})}; const auto expected{sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://sourcemeta.com/1", "items": { "type": "string" @@ -336,48 +279,17 @@ TEST(JSONSchema_wrap, schema_with_identifier_no_dialect) { } })JSON")}; - EXPECT_THROW(sourcemeta::core::wrap(schema, {"items"}, - sourcemeta::core::schema_resolver), + EXPECT_THROW(wrap_schema(schema, {"items"}), sourcemeta::core::SchemaUnknownBaseDialectError); } -TEST(JSONSchema_wrap, schema_with_identifier_with_fragment) { - const auto schema{sourcemeta::core::parse_json(R"JSON({ - "$schema": "http://json-schema.org/draft-04/schema#", - "id": "https://sourcemeta.com/1#foo", - "items": { - "type": "string" - } - })JSON")}; - - const auto result{sourcemeta::core::wrap(schema, {"items"}, - sourcemeta::core::schema_resolver)}; - - const auto expected{sourcemeta::core::parse_json(R"JSON({ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$ref": "#/$defs/schema/items", - "$defs": { - "schema": { - "$schema": "http://json-schema.org/draft-04/schema#", - "id": "https://sourcemeta.com/1#foo", - "items": { - "type": "string" - } - } - } - })JSON")}; - - EXPECT_EQ(result, expected); -} - TEST(JSONSchema_wrap, draft4_standalone_ref_with_default_dialect) { const auto schema{sourcemeta::core::parse_json(R"JSON({ "$ref": "https://example.com" })JSON")}; const auto result{ - sourcemeta::core::wrap(schema, {}, sourcemeta::core::schema_resolver, - "http://json-schema.org/draft-04/schema#")}; + wrap_schema(schema, {}, "http://json-schema.org/draft-04/schema#")}; const auto expected{sourcemeta::core::parse_json(R"JSON({ "$ref": "https://example.com" @@ -396,8 +308,7 @@ TEST(JSONSchema_wrap, draft4_top_level_ref_with_id_empty) { } })JSON")}; - const auto result{ - sourcemeta::core::wrap(schema, {}, sourcemeta::core::schema_resolver)}; + const auto result{wrap_schema(schema, {})}; const auto expected{sourcemeta::core::parse_json(R"JSON({ "$schema": "http://json-schema.org/draft-04/schema#", @@ -411,21 +322,6 @@ TEST(JSONSchema_wrap, draft4_top_level_ref_with_id_empty) { EXPECT_EQ(result, expected); } -TEST(JSONSchema_wrap, draft4_top_level_ref_with_id_definitions_foo) { - const auto schema{sourcemeta::core::parse_json(R"JSON({ - "$schema": "http://json-schema.org/draft-04/schema#", - "id": "https://example.com", - "$ref": "#/definitions/foo", - "definitions": { - "foo": {} - } - })JSON")}; - - EXPECT_THROW(sourcemeta::core::wrap(schema, {"definitions", "foo"}, - sourcemeta::core::schema_resolver), - sourcemeta::core::SchemaError); -} - TEST(JSONSchema_wrap, draft4_top_level_ref_without_id_empty) { const auto schema{sourcemeta::core::parse_json(R"JSON({ "$schema": "http://json-schema.org/draft-04/schema#", @@ -435,8 +331,7 @@ TEST(JSONSchema_wrap, draft4_top_level_ref_without_id_empty) { } })JSON")}; - const auto result{ - sourcemeta::core::wrap(schema, {}, sourcemeta::core::schema_resolver)}; + const auto result{wrap_schema(schema, {})}; const auto expected{sourcemeta::core::parse_json(R"JSON({ "$schema": "http://json-schema.org/draft-04/schema#", @@ -449,28 +344,13 @@ TEST(JSONSchema_wrap, draft4_top_level_ref_without_id_empty) { EXPECT_EQ(result, expected); } -TEST(JSONSchema_wrap, draft4_top_level_ref_without_id_definitions_foo) { - const auto schema{sourcemeta::core::parse_json(R"JSON({ - "$schema": "http://json-schema.org/draft-04/schema#", - "$ref": "#/definitions/foo", - "definitions": { - "foo": {} - } - })JSON")}; - - EXPECT_THROW(sourcemeta::core::wrap(schema, {"definitions", "foo"}, - sourcemeta::core::schema_resolver), - sourcemeta::core::SchemaError); -} - TEST(JSONSchema_wrap, draft6_standalone_ref_with_default_dialect) { const auto schema{sourcemeta::core::parse_json(R"JSON({ "$ref": "https://example.com" })JSON")}; const auto result{ - sourcemeta::core::wrap(schema, {}, sourcemeta::core::schema_resolver, - "http://json-schema.org/draft-06/schema#")}; + wrap_schema(schema, {}, "http://json-schema.org/draft-06/schema#")}; const auto expected{sourcemeta::core::parse_json(R"JSON({ "$ref": "https://example.com" @@ -489,8 +369,7 @@ TEST(JSONSchema_wrap, draft6_top_level_ref_with_id_empty) { } })JSON")}; - const auto result{ - sourcemeta::core::wrap(schema, {}, sourcemeta::core::schema_resolver)}; + const auto result{wrap_schema(schema, {})}; const auto expected{sourcemeta::core::parse_json(R"JSON({ "$schema": "http://json-schema.org/draft-06/schema#", @@ -504,21 +383,6 @@ TEST(JSONSchema_wrap, draft6_top_level_ref_with_id_empty) { EXPECT_EQ(result, expected); } -TEST(JSONSchema_wrap, draft6_top_level_ref_with_id_definitions_foo) { - const auto schema{sourcemeta::core::parse_json(R"JSON({ - "$schema": "http://json-schema.org/draft-06/schema#", - "$id": "https://example.com", - "$ref": "#/definitions/foo", - "definitions": { - "foo": {} - } - })JSON")}; - - EXPECT_THROW(sourcemeta::core::wrap(schema, {"definitions", "foo"}, - sourcemeta::core::schema_resolver), - sourcemeta::core::SchemaError); -} - TEST(JSONSchema_wrap, draft6_top_level_ref_without_id_empty) { const auto schema{sourcemeta::core::parse_json(R"JSON({ "$schema": "http://json-schema.org/draft-06/schema#", @@ -528,8 +392,7 @@ TEST(JSONSchema_wrap, draft6_top_level_ref_without_id_empty) { } })JSON")}; - const auto result{ - sourcemeta::core::wrap(schema, {}, sourcemeta::core::schema_resolver)}; + const auto result{wrap_schema(schema, {})}; const auto expected{sourcemeta::core::parse_json(R"JSON({ "$schema": "http://json-schema.org/draft-06/schema#", @@ -542,28 +405,13 @@ TEST(JSONSchema_wrap, draft6_top_level_ref_without_id_empty) { EXPECT_EQ(result, expected); } -TEST(JSONSchema_wrap, draft6_top_level_ref_without_id_definitions_foo) { - const auto schema{sourcemeta::core::parse_json(R"JSON({ - "$schema": "http://json-schema.org/draft-06/schema#", - "$ref": "#/definitions/foo", - "definitions": { - "foo": {} - } - })JSON")}; - - EXPECT_THROW(sourcemeta::core::wrap(schema, {"definitions", "foo"}, - sourcemeta::core::schema_resolver), - sourcemeta::core::SchemaError); -} - TEST(JSONSchema_wrap, draft7_standalone_ref_with_default_dialect) { const auto schema{sourcemeta::core::parse_json(R"JSON({ "$ref": "https://example.com" })JSON")}; const auto result{ - sourcemeta::core::wrap(schema, {}, sourcemeta::core::schema_resolver, - "http://json-schema.org/draft-07/schema#")}; + wrap_schema(schema, {}, "http://json-schema.org/draft-07/schema#")}; const auto expected{sourcemeta::core::parse_json(R"JSON({ "$ref": "https://example.com" @@ -582,8 +430,7 @@ TEST(JSONSchema_wrap, draft7_top_level_ref_with_id_empty) { } })JSON")}; - const auto result{ - sourcemeta::core::wrap(schema, {}, sourcemeta::core::schema_resolver)}; + const auto result{wrap_schema(schema, {})}; const auto expected{sourcemeta::core::parse_json(R"JSON({ "$schema": "http://json-schema.org/draft-07/schema#", @@ -597,21 +444,6 @@ TEST(JSONSchema_wrap, draft7_top_level_ref_with_id_empty) { EXPECT_EQ(result, expected); } -TEST(JSONSchema_wrap, draft7_top_level_ref_with_id_definitions_foo) { - const auto schema{sourcemeta::core::parse_json(R"JSON({ - "$schema": "http://json-schema.org/draft-07/schema#", - "$id": "https://example.com", - "$ref": "#/definitions/foo", - "definitions": { - "foo": {} - } - })JSON")}; - - EXPECT_THROW(sourcemeta::core::wrap(schema, {"definitions", "foo"}, - sourcemeta::core::schema_resolver), - sourcemeta::core::SchemaError); -} - TEST(JSONSchema_wrap, draft7_top_level_ref_without_id_empty) { const auto schema{sourcemeta::core::parse_json(R"JSON({ "$schema": "http://json-schema.org/draft-07/schema#", @@ -621,8 +453,7 @@ TEST(JSONSchema_wrap, draft7_top_level_ref_without_id_empty) { } })JSON")}; - const auto result{ - sourcemeta::core::wrap(schema, {}, sourcemeta::core::schema_resolver)}; + const auto result{wrap_schema(schema, {})}; const auto expected{sourcemeta::core::parse_json(R"JSON({ "$schema": "http://json-schema.org/draft-07/schema#", @@ -635,20 +466,6 @@ TEST(JSONSchema_wrap, draft7_top_level_ref_without_id_empty) { EXPECT_EQ(result, expected); } -TEST(JSONSchema_wrap, draft7_top_level_ref_without_id_definitions_foo) { - const auto schema{sourcemeta::core::parse_json(R"JSON({ - "$schema": "http://json-schema.org/draft-07/schema#", - "$ref": "#/definitions/foo", - "definitions": { - "foo": {} - } - })JSON")}; - - EXPECT_THROW(sourcemeta::core::wrap(schema, {"definitions", "foo"}, - sourcemeta::core::schema_resolver), - sourcemeta::core::SchemaError); -} - TEST(JSONSchema_wrap, 2019_09_top_level_ref_with_id_empty) { const auto schema{sourcemeta::core::parse_json(R"JSON({ "$schema": "https://json-schema.org/draft/2019-09/schema", @@ -659,8 +476,7 @@ TEST(JSONSchema_wrap, 2019_09_top_level_ref_with_id_empty) { } })JSON")}; - const auto result{ - sourcemeta::core::wrap(schema, {}, sourcemeta::core::schema_resolver)}; + const auto result{wrap_schema(schema, {})}; const auto expected{sourcemeta::core::parse_json(R"JSON({ "$schema": "https://json-schema.org/draft/2019-09/schema", @@ -684,22 +500,10 @@ TEST(JSONSchema_wrap, 2019_09_top_level_ref_with_id_defs_foo) { } })JSON")}; - const auto result{sourcemeta::core::wrap(schema, {"$defs", "foo"}, - sourcemeta::core::schema_resolver)}; + const auto result{wrap_schema(schema, {"$defs", "foo"})}; const auto expected{sourcemeta::core::parse_json(R"JSON({ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$ref": "https://example.com#/$defs/foo", - "$defs": { - "schema": { - "$schema": "https://json-schema.org/draft/2019-09/schema", - "$id": "https://example.com", - "$ref": "#/$defs/foo", - "$defs": { - "foo": {} - } - } - } + "$schema": "https://json-schema.org/draft/2019-09/schema" })JSON")}; EXPECT_EQ(result, expected); @@ -714,8 +518,7 @@ TEST(JSONSchema_wrap, 2019_09_top_level_ref_without_id_empty) { } })JSON")}; - const auto result{ - sourcemeta::core::wrap(schema, {}, sourcemeta::core::schema_resolver)}; + const auto result{wrap_schema(schema, {})}; const auto expected{sourcemeta::core::parse_json(R"JSON({ "$schema": "https://json-schema.org/draft/2019-09/schema", @@ -737,22 +540,10 @@ TEST(JSONSchema_wrap, 2019_09_top_level_ref_without_id_defs_foo) { } })JSON")}; - const auto result{sourcemeta::core::wrap(schema, {"$defs", "foo"}, - sourcemeta::core::schema_resolver)}; + const auto result{wrap_schema(schema, {"$defs", "foo"})}; const auto expected{sourcemeta::core::parse_json(R"JSON({ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$ref": "__sourcemeta-core-wrap__#/$defs/foo", - "$defs": { - "schema": { - "$schema": "https://json-schema.org/draft/2019-09/schema", - "$id": "__sourcemeta-core-wrap__", - "$ref": "#/$defs/foo", - "$defs": { - "foo": {} - } - } - } + "$schema": "https://json-schema.org/draft/2019-09/schema" })JSON")}; EXPECT_EQ(result, expected); @@ -768,8 +559,7 @@ TEST(JSONSchema_wrap, 2020_12_top_level_ref_with_id_empty) { } })JSON")}; - const auto result{ - sourcemeta::core::wrap(schema, {}, sourcemeta::core::schema_resolver)}; + const auto result{wrap_schema(schema, {})}; const auto expected{sourcemeta::core::parse_json(R"JSON({ "$schema": "https://json-schema.org/draft/2020-12/schema", @@ -793,22 +583,10 @@ TEST(JSONSchema_wrap, 2020_12_top_level_ref_with_id_defs_foo) { } })JSON")}; - const auto result{sourcemeta::core::wrap(schema, {"$defs", "foo"}, - sourcemeta::core::schema_resolver)}; + const auto result{wrap_schema(schema, {"$defs", "foo"})}; const auto expected{sourcemeta::core::parse_json(R"JSON({ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$ref": "https://example.com#/$defs/foo", - "$defs": { - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://example.com", - "$ref": "#/$defs/foo", - "$defs": { - "foo": {} - } - } - } + "$schema": "https://json-schema.org/draft/2020-12/schema" })JSON")}; EXPECT_EQ(result, expected); @@ -823,8 +601,7 @@ TEST(JSONSchema_wrap, 2020_12_top_level_ref_without_id_empty) { } })JSON")}; - const auto result{ - sourcemeta::core::wrap(schema, {}, sourcemeta::core::schema_resolver)}; + const auto result{wrap_schema(schema, {})}; const auto expected{sourcemeta::core::parse_json(R"JSON({ "$schema": "https://json-schema.org/draft/2020-12/schema", @@ -846,19 +623,85 @@ TEST(JSONSchema_wrap, 2020_12_top_level_ref_without_id_defs_foo) { } })JSON")}; - const auto result{sourcemeta::core::wrap(schema, {"$defs", "foo"}, - sourcemeta::core::schema_resolver)}; + const auto result{wrap_schema(schema, {"$defs", "foo"})}; + + const auto expected{sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2020-12/schema" + })JSON")}; + + EXPECT_EQ(result, expected); +} + +TEST(JSONSchema_wrap, subschema_with_direct_ref) { + const auto schema{sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://example.com", + "$defs": { + "string": { "type": "string" } + }, + "properties": { + "foo": { "$ref": "#/$defs/string" } + } + })JSON")}; + + const auto result{wrap_schema(schema, {"properties", "foo"})}; const auto expected{sourcemeta::core::parse_json(R"JSON({ "$schema": "https://json-schema.org/draft/2020-12/schema", - "$ref": "__sourcemeta-core-wrap__#/$defs/foo", + "$ref": "https://example.com#/properties/foo", "$defs": { "schema": { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "__sourcemeta-core-wrap__", - "$ref": "#/$defs/foo", + "$id": "https://example.com", + "$defs": { + "string": { "type": "string" } + }, + "properties": { + "foo": { "$ref": "#/$defs/string" } + } + } + } + })JSON")}; + + EXPECT_EQ(result, expected); +} + +TEST(JSONSchema_wrap, subschema_with_nested_ref) { + const auto schema{sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://example.com", + "$defs": { + "string": { "type": "string" } + }, + "properties": { + "foo": { + "type": "object", + "properties": { + "bar": { "$ref": "#/$defs/string" } + } + } + } + })JSON")}; + + const auto result{wrap_schema(schema, {"properties", "foo"})}; + + const auto expected{sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$ref": "https://example.com#/properties/foo", + "$defs": { + "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://example.com", "$defs": { - "foo": {} + "string": { "type": "string" } + }, + "properties": { + "foo": { + "type": "object", + "properties": { + "bar": { "$ref": "#/$defs/string" } + } + } } } } From 235f820effac93df80e494c258342657d4cf3a42 Mon Sep 17 00:00:00 2001 From: Juan Cruz Viotti Date: Mon, 2 Feb 2026 09:14:19 -0400 Subject: [PATCH 2/4] Base Signed-off-by: Juan Cruz Viotti --- .../include/sourcemeta/core/jsonschema.h | 7 +- src/core/jsonschema/jsonschema.cc | 6 +- test/jsonschema/jsonschema_wrap_test.cc | 103 ++++++++++++------ 3 files changed, 79 insertions(+), 37 deletions(-) diff --git a/src/core/jsonschema/include/sourcemeta/core/jsonschema.h b/src/core/jsonschema/include/sourcemeta/core/jsonschema.h index c27287f39..2d21a8215 100644 --- a/src/core/jsonschema/include/sourcemeta/core/jsonschema.h +++ b/src/core/jsonschema/include/sourcemeta/core/jsonschema.h @@ -431,17 +431,18 @@ auto wrap(std::string_view identifier) -> JSON; /// sourcemeta::core::Pointer{"items"}, /// sourcemeta::core::SchemaFrame::LocationType::Subschema)}; /// +/// sourcemeta::core::WeakPointer base; /// const sourcemeta::core::JSON result = /// sourcemeta::core::wrap(document, frame, location.value().get(), -/// sourcemeta::core::schema_resolver); +/// sourcemeta::core::schema_resolver, base); /// /// sourcemeta::core::prettify(result, std::cerr); /// std::cerr << "\n"; /// ``` SOURCEMETA_CORE_JSONSCHEMA_EXPORT auto wrap(const JSON &schema, const SchemaFrame &frame, - const SchemaFrame::Location &location, const SchemaResolver &resolver) - -> JSON; + const SchemaFrame::Location &location, const SchemaResolver &resolver, + WeakPointer &base) -> JSON; /// @ingroup jsonschema /// diff --git a/src/core/jsonschema/jsonschema.cc b/src/core/jsonschema/jsonschema.cc index 5a688328e..45776d7c1 100644 --- a/src/core/jsonschema/jsonschema.cc +++ b/src/core/jsonschema/jsonschema.cc @@ -594,8 +594,8 @@ auto sourcemeta::core::wrap( const sourcemeta::core::JSON &schema, const sourcemeta::core::SchemaFrame &frame, const sourcemeta::core::SchemaFrame::Location &location, - const sourcemeta::core::SchemaResolver &resolver) - -> sourcemeta::core::JSON { + const sourcemeta::core::SchemaResolver &resolver, + sourcemeta::core::WeakPointer &base) -> sourcemeta::core::JSON { assert(location.type != SchemaFrame::LocationType::Pointer); const auto &pointer{location.pointer}; assert(try_get(schema, pointer)); @@ -666,6 +666,8 @@ auto sourcemeta::core::wrap( .recompose()}); } + static const JSON::String REF{"$ref"}; + base.push_back(REF); return result; } diff --git a/test/jsonschema/jsonschema_wrap_test.cc b/test/jsonschema/jsonschema_wrap_test.cc index 35bc2afac..1c680ec21 100644 --- a/test/jsonschema/jsonschema_wrap_test.cc +++ b/test/jsonschema/jsonschema_wrap_test.cc @@ -3,10 +3,12 @@ #include #include +#include // std::pair + static auto wrap_schema(const sourcemeta::core::JSON &schema, const sourcemeta::core::Pointer &pointer, std::string_view default_dialect = "") - -> sourcemeta::core::JSON { + -> std::pair { sourcemeta::core::SchemaFrame frame{ sourcemeta::core::SchemaFrame::Mode::References}; frame.analyse(schema, sourcemeta::core::schema_walker, @@ -14,8 +16,10 @@ static auto wrap_schema(const sourcemeta::core::JSON &schema, const auto location{ frame.traverse(sourcemeta::core::to_weak_pointer(pointer))}; assert(location.has_value()); - return sourcemeta::core::wrap(schema, frame, location.value().get(), - sourcemeta::core::schema_resolver); + sourcemeta::core::WeakPointer base; + auto result{sourcemeta::core::wrap(schema, frame, location.value().get(), + sourcemeta::core::schema_resolver, base)}; + return {std::move(result), sourcemeta::core::to_pointer(base)}; } TEST(JSONSchema_wrap, identifier_without_fragment) { @@ -64,7 +68,7 @@ TEST(JSONSchema_wrap, schema_without_identifier) { } })JSON")}; - const auto result{wrap_schema(schema, {"items"})}; + const auto [result, base]{wrap_schema(schema, {"items"})}; const auto expected{sourcemeta::core::parse_json(R"JSON({ "$schema": "https://json-schema.org/draft/2020-12/schema", @@ -72,6 +76,7 @@ TEST(JSONSchema_wrap, schema_without_identifier) { })JSON")}; EXPECT_EQ(result, expected); + EXPECT_TRUE(base.empty()); } TEST(JSONSchema_wrap, schema_without_identifier_and_relative_uri) { @@ -82,7 +87,7 @@ TEST(JSONSchema_wrap, schema_without_identifier_and_relative_uri) { } })JSON")}; - const auto result{wrap_schema(schema, {"items"})}; + const auto [result, base]{wrap_schema(schema, {"items"})}; // We don't want the relative reference to be resolved against // an absolute base @@ -101,6 +106,9 @@ TEST(JSONSchema_wrap, schema_without_identifier_and_relative_uri) { })JSON")}; EXPECT_EQ(result, expected); + EXPECT_EQ(base.size(), 1); + EXPECT_TRUE(base.at(0).is_property()); + EXPECT_EQ(base.at(0).to_property(), "$ref"); } TEST(JSONSchema_wrap, schema_without_identifier_with_default_dialect) { @@ -110,7 +118,7 @@ TEST(JSONSchema_wrap, schema_without_identifier_with_default_dialect) { } })JSON")}; - const auto result{wrap_schema( + const auto [result, base]{wrap_schema( schema, {"items"}, "https://json-schema.org/draft/2020-12/schema")}; const auto expected{sourcemeta::core::parse_json(R"JSON({ @@ -119,6 +127,7 @@ TEST(JSONSchema_wrap, schema_without_identifier_with_default_dialect) { })JSON")}; EXPECT_EQ(result, expected); + EXPECT_TRUE(base.empty()); } TEST(JSONSchema_wrap, @@ -130,7 +139,7 @@ TEST(JSONSchema_wrap, } })JSON")}; - const auto result{wrap_schema( + const auto [result, base]{wrap_schema( schema, {"items"}, "https://json-schema.org/draft/2019-09/schema")}; const auto expected{sourcemeta::core::parse_json(R"JSON({ @@ -139,6 +148,7 @@ TEST(JSONSchema_wrap, })JSON")}; EXPECT_EQ(result, expected); + EXPECT_TRUE(base.empty()); } TEST(JSONSchema_wrap, schema_without_identifier_empty_pointer) { @@ -149,7 +159,7 @@ TEST(JSONSchema_wrap, schema_without_identifier_empty_pointer) { } })JSON")}; - const auto result{wrap_schema(schema, {})}; + const auto [result, base]{wrap_schema(schema, {})}; const auto expected{sourcemeta::core::parse_json(R"JSON({ "$schema": "https://json-schema.org/draft/2020-12/schema", @@ -159,6 +169,7 @@ TEST(JSONSchema_wrap, schema_without_identifier_empty_pointer) { })JSON")}; EXPECT_EQ(result, expected); + EXPECT_TRUE(base.empty()); } TEST(JSONSchema_wrap, schema_without_identifier_without_dialect) { @@ -181,7 +192,7 @@ TEST(JSONSchema_wrap, schema_with_identifier) { } })JSON")}; - const auto result{wrap_schema(schema, {"items"})}; + const auto [result, base]{wrap_schema(schema, {"items"})}; const auto expected{sourcemeta::core::parse_json(R"JSON({ "$schema": "https://json-schema.org/draft/2020-12/schema", @@ -189,6 +200,7 @@ TEST(JSONSchema_wrap, schema_with_identifier) { })JSON")}; EXPECT_EQ(result, expected); + EXPECT_TRUE(base.empty()); } TEST(JSONSchema_wrap, schema_with_identifier_trailing_empty_fragment) { @@ -200,7 +212,7 @@ TEST(JSONSchema_wrap, schema_with_identifier_trailing_empty_fragment) { } })JSON")}; - const auto result{wrap_schema(schema, {"items"})}; + const auto [result, base]{wrap_schema(schema, {"items"})}; const auto expected{sourcemeta::core::parse_json(R"JSON({ "$schema": "https://json-schema.org/draft/2020-12/schema", @@ -208,6 +220,7 @@ TEST(JSONSchema_wrap, schema_with_identifier_trailing_empty_fragment) { })JSON")}; EXPECT_EQ(result, expected); + EXPECT_TRUE(base.empty()); } TEST(JSONSchema_wrap, schema_with_identifier_different_default_dialect) { @@ -219,7 +232,7 @@ TEST(JSONSchema_wrap, schema_with_identifier_different_default_dialect) { } })JSON")}; - const auto result{wrap_schema( + const auto [result, base]{wrap_schema( schema, {"items"}, "https://json-schema.org/draft/2019-09/schema")}; const auto expected{sourcemeta::core::parse_json(R"JSON({ @@ -228,6 +241,7 @@ TEST(JSONSchema_wrap, schema_with_identifier_different_default_dialect) { })JSON")}; EXPECT_EQ(result, expected); + EXPECT_TRUE(base.empty()); } TEST(JSONSchema_wrap, schema_with_identifier_default_dialect) { @@ -238,7 +252,7 @@ TEST(JSONSchema_wrap, schema_with_identifier_default_dialect) { } })JSON")}; - const auto result{wrap_schema( + const auto [result, base]{wrap_schema( schema, {"items"}, "https://json-schema.org/draft/2019-09/schema")}; const auto expected{sourcemeta::core::parse_json(R"JSON({ @@ -247,6 +261,7 @@ TEST(JSONSchema_wrap, schema_with_identifier_default_dialect) { })JSON")}; EXPECT_EQ(result, expected); + EXPECT_TRUE(base.empty()); } TEST(JSONSchema_wrap, schema_with_identifier_empty_pointer) { @@ -258,7 +273,7 @@ TEST(JSONSchema_wrap, schema_with_identifier_empty_pointer) { } })JSON")}; - const auto result{wrap_schema(schema, {})}; + const auto [result, base]{wrap_schema(schema, {})}; const auto expected{sourcemeta::core::parse_json(R"JSON({ "$schema": "https://json-schema.org/draft/2020-12/schema", @@ -269,6 +284,7 @@ TEST(JSONSchema_wrap, schema_with_identifier_empty_pointer) { })JSON")}; EXPECT_EQ(result, expected); + EXPECT_TRUE(base.empty()); } TEST(JSONSchema_wrap, schema_with_identifier_no_dialect) { @@ -288,7 +304,7 @@ TEST(JSONSchema_wrap, draft4_standalone_ref_with_default_dialect) { "$ref": "https://example.com" })JSON")}; - const auto result{ + const auto [result, base]{ wrap_schema(schema, {}, "http://json-schema.org/draft-04/schema#")}; const auto expected{sourcemeta::core::parse_json(R"JSON({ @@ -296,6 +312,7 @@ TEST(JSONSchema_wrap, draft4_standalone_ref_with_default_dialect) { })JSON")}; EXPECT_EQ(result, expected); + EXPECT_TRUE(base.empty()); } TEST(JSONSchema_wrap, draft4_top_level_ref_with_id_empty) { @@ -308,7 +325,7 @@ TEST(JSONSchema_wrap, draft4_top_level_ref_with_id_empty) { } })JSON")}; - const auto result{wrap_schema(schema, {})}; + const auto [result, base]{wrap_schema(schema, {})}; const auto expected{sourcemeta::core::parse_json(R"JSON({ "$schema": "http://json-schema.org/draft-04/schema#", @@ -320,6 +337,7 @@ TEST(JSONSchema_wrap, draft4_top_level_ref_with_id_empty) { })JSON")}; EXPECT_EQ(result, expected); + EXPECT_TRUE(base.empty()); } TEST(JSONSchema_wrap, draft4_top_level_ref_without_id_empty) { @@ -331,7 +349,7 @@ TEST(JSONSchema_wrap, draft4_top_level_ref_without_id_empty) { } })JSON")}; - const auto result{wrap_schema(schema, {})}; + const auto [result, base]{wrap_schema(schema, {})}; const auto expected{sourcemeta::core::parse_json(R"JSON({ "$schema": "http://json-schema.org/draft-04/schema#", @@ -342,6 +360,7 @@ TEST(JSONSchema_wrap, draft4_top_level_ref_without_id_empty) { })JSON")}; EXPECT_EQ(result, expected); + EXPECT_TRUE(base.empty()); } TEST(JSONSchema_wrap, draft6_standalone_ref_with_default_dialect) { @@ -349,7 +368,7 @@ TEST(JSONSchema_wrap, draft6_standalone_ref_with_default_dialect) { "$ref": "https://example.com" })JSON")}; - const auto result{ + const auto [result, base]{ wrap_schema(schema, {}, "http://json-schema.org/draft-06/schema#")}; const auto expected{sourcemeta::core::parse_json(R"JSON({ @@ -357,6 +376,7 @@ TEST(JSONSchema_wrap, draft6_standalone_ref_with_default_dialect) { })JSON")}; EXPECT_EQ(result, expected); + EXPECT_TRUE(base.empty()); } TEST(JSONSchema_wrap, draft6_top_level_ref_with_id_empty) { @@ -369,7 +389,7 @@ TEST(JSONSchema_wrap, draft6_top_level_ref_with_id_empty) { } })JSON")}; - const auto result{wrap_schema(schema, {})}; + const auto [result, base]{wrap_schema(schema, {})}; const auto expected{sourcemeta::core::parse_json(R"JSON({ "$schema": "http://json-schema.org/draft-06/schema#", @@ -381,6 +401,7 @@ TEST(JSONSchema_wrap, draft6_top_level_ref_with_id_empty) { })JSON")}; EXPECT_EQ(result, expected); + EXPECT_TRUE(base.empty()); } TEST(JSONSchema_wrap, draft6_top_level_ref_without_id_empty) { @@ -392,7 +413,7 @@ TEST(JSONSchema_wrap, draft6_top_level_ref_without_id_empty) { } })JSON")}; - const auto result{wrap_schema(schema, {})}; + const auto [result, base]{wrap_schema(schema, {})}; const auto expected{sourcemeta::core::parse_json(R"JSON({ "$schema": "http://json-schema.org/draft-06/schema#", @@ -403,6 +424,7 @@ TEST(JSONSchema_wrap, draft6_top_level_ref_without_id_empty) { })JSON")}; EXPECT_EQ(result, expected); + EXPECT_TRUE(base.empty()); } TEST(JSONSchema_wrap, draft7_standalone_ref_with_default_dialect) { @@ -410,7 +432,7 @@ TEST(JSONSchema_wrap, draft7_standalone_ref_with_default_dialect) { "$ref": "https://example.com" })JSON")}; - const auto result{ + const auto [result, base]{ wrap_schema(schema, {}, "http://json-schema.org/draft-07/schema#")}; const auto expected{sourcemeta::core::parse_json(R"JSON({ @@ -418,6 +440,7 @@ TEST(JSONSchema_wrap, draft7_standalone_ref_with_default_dialect) { })JSON")}; EXPECT_EQ(result, expected); + EXPECT_TRUE(base.empty()); } TEST(JSONSchema_wrap, draft7_top_level_ref_with_id_empty) { @@ -430,7 +453,7 @@ TEST(JSONSchema_wrap, draft7_top_level_ref_with_id_empty) { } })JSON")}; - const auto result{wrap_schema(schema, {})}; + const auto [result, base]{wrap_schema(schema, {})}; const auto expected{sourcemeta::core::parse_json(R"JSON({ "$schema": "http://json-schema.org/draft-07/schema#", @@ -442,6 +465,7 @@ TEST(JSONSchema_wrap, draft7_top_level_ref_with_id_empty) { })JSON")}; EXPECT_EQ(result, expected); + EXPECT_TRUE(base.empty()); } TEST(JSONSchema_wrap, draft7_top_level_ref_without_id_empty) { @@ -453,7 +477,7 @@ TEST(JSONSchema_wrap, draft7_top_level_ref_without_id_empty) { } })JSON")}; - const auto result{wrap_schema(schema, {})}; + const auto [result, base]{wrap_schema(schema, {})}; const auto expected{sourcemeta::core::parse_json(R"JSON({ "$schema": "http://json-schema.org/draft-07/schema#", @@ -464,6 +488,7 @@ TEST(JSONSchema_wrap, draft7_top_level_ref_without_id_empty) { })JSON")}; EXPECT_EQ(result, expected); + EXPECT_TRUE(base.empty()); } TEST(JSONSchema_wrap, 2019_09_top_level_ref_with_id_empty) { @@ -476,7 +501,7 @@ TEST(JSONSchema_wrap, 2019_09_top_level_ref_with_id_empty) { } })JSON")}; - const auto result{wrap_schema(schema, {})}; + const auto [result, base]{wrap_schema(schema, {})}; const auto expected{sourcemeta::core::parse_json(R"JSON({ "$schema": "https://json-schema.org/draft/2019-09/schema", @@ -488,6 +513,7 @@ TEST(JSONSchema_wrap, 2019_09_top_level_ref_with_id_empty) { })JSON")}; EXPECT_EQ(result, expected); + EXPECT_TRUE(base.empty()); } TEST(JSONSchema_wrap, 2019_09_top_level_ref_with_id_defs_foo) { @@ -500,13 +526,14 @@ TEST(JSONSchema_wrap, 2019_09_top_level_ref_with_id_defs_foo) { } })JSON")}; - const auto result{wrap_schema(schema, {"$defs", "foo"})}; + const auto [result, base]{wrap_schema(schema, {"$defs", "foo"})}; const auto expected{sourcemeta::core::parse_json(R"JSON({ "$schema": "https://json-schema.org/draft/2019-09/schema" })JSON")}; EXPECT_EQ(result, expected); + EXPECT_TRUE(base.empty()); } TEST(JSONSchema_wrap, 2019_09_top_level_ref_without_id_empty) { @@ -518,7 +545,7 @@ TEST(JSONSchema_wrap, 2019_09_top_level_ref_without_id_empty) { } })JSON")}; - const auto result{wrap_schema(schema, {})}; + const auto [result, base]{wrap_schema(schema, {})}; const auto expected{sourcemeta::core::parse_json(R"JSON({ "$schema": "https://json-schema.org/draft/2019-09/schema", @@ -529,6 +556,7 @@ TEST(JSONSchema_wrap, 2019_09_top_level_ref_without_id_empty) { })JSON")}; EXPECT_EQ(result, expected); + EXPECT_TRUE(base.empty()); } TEST(JSONSchema_wrap, 2019_09_top_level_ref_without_id_defs_foo) { @@ -540,13 +568,14 @@ TEST(JSONSchema_wrap, 2019_09_top_level_ref_without_id_defs_foo) { } })JSON")}; - const auto result{wrap_schema(schema, {"$defs", "foo"})}; + const auto [result, base]{wrap_schema(schema, {"$defs", "foo"})}; const auto expected{sourcemeta::core::parse_json(R"JSON({ "$schema": "https://json-schema.org/draft/2019-09/schema" })JSON")}; EXPECT_EQ(result, expected); + EXPECT_TRUE(base.empty()); } TEST(JSONSchema_wrap, 2020_12_top_level_ref_with_id_empty) { @@ -559,7 +588,7 @@ TEST(JSONSchema_wrap, 2020_12_top_level_ref_with_id_empty) { } })JSON")}; - const auto result{wrap_schema(schema, {})}; + const auto [result, base]{wrap_schema(schema, {})}; const auto expected{sourcemeta::core::parse_json(R"JSON({ "$schema": "https://json-schema.org/draft/2020-12/schema", @@ -571,6 +600,7 @@ TEST(JSONSchema_wrap, 2020_12_top_level_ref_with_id_empty) { })JSON")}; EXPECT_EQ(result, expected); + EXPECT_TRUE(base.empty()); } TEST(JSONSchema_wrap, 2020_12_top_level_ref_with_id_defs_foo) { @@ -583,13 +613,14 @@ TEST(JSONSchema_wrap, 2020_12_top_level_ref_with_id_defs_foo) { } })JSON")}; - const auto result{wrap_schema(schema, {"$defs", "foo"})}; + const auto [result, base]{wrap_schema(schema, {"$defs", "foo"})}; const auto expected{sourcemeta::core::parse_json(R"JSON({ "$schema": "https://json-schema.org/draft/2020-12/schema" })JSON")}; EXPECT_EQ(result, expected); + EXPECT_TRUE(base.empty()); } TEST(JSONSchema_wrap, 2020_12_top_level_ref_without_id_empty) { @@ -601,7 +632,7 @@ TEST(JSONSchema_wrap, 2020_12_top_level_ref_without_id_empty) { } })JSON")}; - const auto result{wrap_schema(schema, {})}; + const auto [result, base]{wrap_schema(schema, {})}; const auto expected{sourcemeta::core::parse_json(R"JSON({ "$schema": "https://json-schema.org/draft/2020-12/schema", @@ -612,6 +643,7 @@ TEST(JSONSchema_wrap, 2020_12_top_level_ref_without_id_empty) { })JSON")}; EXPECT_EQ(result, expected); + EXPECT_TRUE(base.empty()); } TEST(JSONSchema_wrap, 2020_12_top_level_ref_without_id_defs_foo) { @@ -623,13 +655,14 @@ TEST(JSONSchema_wrap, 2020_12_top_level_ref_without_id_defs_foo) { } })JSON")}; - const auto result{wrap_schema(schema, {"$defs", "foo"})}; + const auto [result, base]{wrap_schema(schema, {"$defs", "foo"})}; const auto expected{sourcemeta::core::parse_json(R"JSON({ "$schema": "https://json-schema.org/draft/2020-12/schema" })JSON")}; EXPECT_EQ(result, expected); + EXPECT_TRUE(base.empty()); } TEST(JSONSchema_wrap, subschema_with_direct_ref) { @@ -644,7 +677,7 @@ TEST(JSONSchema_wrap, subschema_with_direct_ref) { } })JSON")}; - const auto result{wrap_schema(schema, {"properties", "foo"})}; + const auto [result, base]{wrap_schema(schema, {"properties", "foo"})}; const auto expected{sourcemeta::core::parse_json(R"JSON({ "$schema": "https://json-schema.org/draft/2020-12/schema", @@ -664,6 +697,9 @@ TEST(JSONSchema_wrap, subschema_with_direct_ref) { })JSON")}; EXPECT_EQ(result, expected); + EXPECT_EQ(base.size(), 1); + EXPECT_TRUE(base.at(0).is_property()); + EXPECT_EQ(base.at(0).to_property(), "$ref"); } TEST(JSONSchema_wrap, subschema_with_nested_ref) { @@ -683,7 +719,7 @@ TEST(JSONSchema_wrap, subschema_with_nested_ref) { } })JSON")}; - const auto result{wrap_schema(schema, {"properties", "foo"})}; + const auto [result, base]{wrap_schema(schema, {"properties", "foo"})}; const auto expected{sourcemeta::core::parse_json(R"JSON({ "$schema": "https://json-schema.org/draft/2020-12/schema", @@ -708,4 +744,7 @@ TEST(JSONSchema_wrap, subschema_with_nested_ref) { })JSON")}; EXPECT_EQ(result, expected); + EXPECT_EQ(base.size(), 1); + EXPECT_TRUE(base.at(0).is_property()); + EXPECT_EQ(base.at(0).to_property(), "$ref"); } From cf72495629ff9d4c49f8efaed179526691915d04 Mon Sep 17 00:00:00 2001 From: Juan Cruz Viotti Date: Mon, 2 Feb 2026 09:24:39 -0400 Subject: [PATCH 3/4] Better Signed-off-by: Juan Cruz Viotti --- src/core/jsonschema/jsonschema.cc | 10 ++++++---- test/jsonschema/jsonschema_wrap_test.cc | 3 +++ 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/src/core/jsonschema/jsonschema.cc b/src/core/jsonschema/jsonschema.cc index 45776d7c1..11b25bc31 100644 --- a/src/core/jsonschema/jsonschema.cc +++ b/src/core/jsonschema/jsonschema.cc @@ -598,11 +598,13 @@ auto sourcemeta::core::wrap( sourcemeta::core::WeakPointer &base) -> sourcemeta::core::JSON { assert(location.type != SchemaFrame::LocationType::Pointer); const auto &pointer{location.pointer}; - assert(try_get(schema, pointer)); if (pointer.empty()) { - return schema; + auto copy = schema; + copy.assign("$schema", JSON{location.dialect}); + return copy; } + assert(try_get(schema, pointer)); const auto has_internal_references{ std::any_of(frame.references().cbegin(), frame.references().cend(), [&pointer](const auto &reference) { @@ -611,12 +613,12 @@ auto sourcemeta::core::wrap( if (!has_internal_references) { auto subschema{get(schema, pointer)}; - subschema.assign("$schema", JSON{JSON::String{location.dialect}}); + subschema.assign("$schema", JSON{location.dialect}); return subschema; } auto copy = schema; - copy.assign("$schema", JSON{JSON::String{location.dialect}}); + copy.assign("$schema", JSON{location.dialect}); auto result{JSON::make_object()}; // JSON Schema 2020-12 is the first dialect that truly supports diff --git a/test/jsonschema/jsonschema_wrap_test.cc b/test/jsonschema/jsonschema_wrap_test.cc index 1c680ec21..9832c461f 100644 --- a/test/jsonschema/jsonschema_wrap_test.cc +++ b/test/jsonschema/jsonschema_wrap_test.cc @@ -308,6 +308,7 @@ TEST(JSONSchema_wrap, draft4_standalone_ref_with_default_dialect) { wrap_schema(schema, {}, "http://json-schema.org/draft-04/schema#")}; const auto expected{sourcemeta::core::parse_json(R"JSON({ + "$schema": "http://json-schema.org/draft-04/schema#", "$ref": "https://example.com" })JSON")}; @@ -372,6 +373,7 @@ TEST(JSONSchema_wrap, draft6_standalone_ref_with_default_dialect) { wrap_schema(schema, {}, "http://json-schema.org/draft-06/schema#")}; const auto expected{sourcemeta::core::parse_json(R"JSON({ + "$schema": "http://json-schema.org/draft-06/schema#", "$ref": "https://example.com" })JSON")}; @@ -436,6 +438,7 @@ TEST(JSONSchema_wrap, draft7_standalone_ref_with_default_dialect) { wrap_schema(schema, {}, "http://json-schema.org/draft-07/schema#")}; const auto expected{sourcemeta::core::parse_json(R"JSON({ + "$schema": "http://json-schema.org/draft-07/schema#", "$ref": "https://example.com" })JSON")}; From 139f978b289ac7200316269293e90d3271a8a44d Mon Sep 17 00:00:00 2001 From: Juan Cruz Viotti Date: Mon, 2 Feb 2026 09:32:59 -0400 Subject: [PATCH 4/4] Fixes Signed-off-by: Juan Cruz Viotti --- src/core/jsonschema/include/sourcemeta/core/jsonschema.h | 2 +- src/core/jsonschema/jsonschema.cc | 7 ++++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/core/jsonschema/include/sourcemeta/core/jsonschema.h b/src/core/jsonschema/include/sourcemeta/core/jsonschema.h index 2d21a8215..54005110b 100644 --- a/src/core/jsonschema/include/sourcemeta/core/jsonschema.h +++ b/src/core/jsonschema/include/sourcemeta/core/jsonschema.h @@ -428,7 +428,7 @@ auto wrap(std::string_view identifier) -> JSON; /// sourcemeta::core::schema_resolver); /// /// const auto location{frame.traverse( -/// sourcemeta::core::Pointer{"items"}, +/// sourcemeta::core::WeakPointer{"items"}, /// sourcemeta::core::SchemaFrame::LocationType::Subschema)}; /// /// sourcemeta::core::WeakPointer base; diff --git a/src/core/jsonschema/jsonschema.cc b/src/core/jsonschema/jsonschema.cc index 11b25bc31..78814050b 100644 --- a/src/core/jsonschema/jsonschema.cc +++ b/src/core/jsonschema/jsonschema.cc @@ -596,11 +596,16 @@ auto sourcemeta::core::wrap( const sourcemeta::core::SchemaFrame::Location &location, const sourcemeta::core::SchemaResolver &resolver, sourcemeta::core::WeakPointer &base) -> sourcemeta::core::JSON { + assert(frame.mode() == SchemaFrame::Mode::References); assert(location.type != SchemaFrame::LocationType::Pointer); + const auto &pointer{location.pointer}; if (pointer.empty()) { auto copy = schema; - copy.assign("$schema", JSON{location.dialect}); + if (copy.is_object()) { + copy.assign("$schema", JSON{location.dialect}); + } + return copy; }