Skip to content

Support passing a custom default container to bundle #1633

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Apr 29, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
97 changes: 61 additions & 36 deletions src/core/jsonschema/bundle.cc
Original file line number Diff line number Diff line change
Expand Up @@ -6,41 +6,35 @@

namespace {

auto definitions_keyword(const sourcemeta::core::Vocabularies &vocabularies)
-> std::string {
if (vocabularies.contains(
"https://json-schema.org/draft/2020-12/vocab/core") ||
vocabularies.contains(
"https://json-schema.org/draft/2019-09/vocab/core")) {
return "$defs";
auto embed_schema(sourcemeta::core::JSON &root,
const sourcemeta::core::Pointer &container,
const std::string &identifier,
const sourcemeta::core::JSON &target) -> void {
auto *current{&root};
for (const auto &token : container) {
if (token.is_property()) {
current->assign_if_missing(token.to_property(),
sourcemeta::core::JSON::make_object());
current = &current->at(token.to_property());
} else {
assert(current->is_array() && current->size() >= token.to_index());
current = &current->at(token.to_index());
}
}

if (vocabularies.contains("http://json-schema.org/draft-07/schema#") ||
vocabularies.contains("http://json-schema.org/draft-07/hyper-schema#") ||
vocabularies.contains("http://json-schema.org/draft-06/schema#") ||
vocabularies.contains("http://json-schema.org/draft-06/hyper-schema#") ||
vocabularies.contains("http://json-schema.org/draft-04/schema#") ||
vocabularies.contains("http://json-schema.org/draft-04/hyper-schema#")) {
return "definitions";
if (!current->is_object()) {
throw sourcemeta::core::SchemaError(
"Could not bundle to a container path that is not an object");
}

// We don't attempt to bundle on dialects where we
// don't know where to put the embedded schemas
throw sourcemeta::core::SchemaError(
"Could not determine how to perform bundling in this dialect");
}

auto embed_schema(sourcemeta::core::JSON &definitions,
const std::string &identifier,
const sourcemeta::core::JSON &target) -> void {
std::ostringstream key;
key << identifier;
// Ensure we get a definitions entry that does not exist
while (definitions.defines(key.str())) {
while (current->defines(key.str())) {
key << "/x";
}

definitions.assign(key.str(), target);
current->assign(key.str(), target);
}

auto is_official_metaschema_reference(const sourcemeta::core::Pointer &pointer,
Expand All @@ -50,7 +44,8 @@ auto is_official_metaschema_reference(const sourcemeta::core::Pointer &pointer,
sourcemeta::core::schema_official_resolver(destination).has_value();
}

auto bundle_schema(sourcemeta::core::JSON &root, const std::string &container,
auto bundle_schema(sourcemeta::core::JSON &root,
const sourcemeta::core::Pointer &container,
const sourcemeta::core::JSON &subschema,
sourcemeta::core::SchemaFrame &frame,
const sourcemeta::core::SchemaWalker &walker,
Expand Down Expand Up @@ -78,8 +73,6 @@ auto bundle_schema(sourcemeta::core::JSON &root, const std::string &container,
"Could not resolve schema reference");
}

root.assign_if_missing(container, sourcemeta::core::JSON::make_object());

if (!reference.base.has_value()) {
throw sourcemeta::core::SchemaReferenceError(
reference.destination, key.second,
Expand Down Expand Up @@ -123,7 +116,7 @@ auto bundle_schema(sourcemeta::core::JSON &root, const std::string &container,
sourcemeta::core::reidentify(copy, identifier, resolver, default_dialect);
}

embed_schema(root.at(container), identifier, copy);
embed_schema(root, container, identifier, copy);
bundle_schema(root, container, copy, frame, walker, resolver,
default_dialect);
}
Expand All @@ -135,21 +128,53 @@ namespace sourcemeta::core {

auto bundle(sourcemeta::core::JSON &schema, const SchemaWalker &walker,
const SchemaResolver &resolver,
const std::optional<std::string> &default_dialect) -> void {
const auto vocabularies{
sourcemeta::core::vocabularies(schema, resolver, default_dialect)};
const std::optional<std::string> &default_dialect,
const std::optional<Pointer> &default_container) -> void {
sourcemeta::core::SchemaFrame frame{
sourcemeta::core::SchemaFrame::Mode::References};
bundle_schema(schema, definitions_keyword(vocabularies), schema, frame,
walker, resolver, default_dialect);

if (default_container.has_value()) {
// This is undefined behavior
assert(!default_container.value().empty());
bundle_schema(schema, default_container.value(), schema, frame, walker,
resolver, default_dialect);
return;
}

const auto vocabularies{
sourcemeta::core::vocabularies(schema, resolver, default_dialect)};
if (vocabularies.contains(
"https://json-schema.org/draft/2020-12/vocab/core") ||
vocabularies.contains(
"https://json-schema.org/draft/2019-09/vocab/core")) {
bundle_schema(schema, {"$defs"}, schema, frame, walker, resolver,
default_dialect);
} else if (vocabularies.contains("http://json-schema.org/draft-07/schema#") ||
vocabularies.contains(
"http://json-schema.org/draft-07/hyper-schema#") ||
vocabularies.contains("http://json-schema.org/draft-06/schema#") ||
vocabularies.contains(
"http://json-schema.org/draft-06/hyper-schema#") ||
vocabularies.contains("http://json-schema.org/draft-04/schema#") ||
vocabularies.contains(
"http://json-schema.org/draft-04/hyper-schema#")) {
bundle_schema(schema, {"definitions"}, schema, frame, walker, resolver,
default_dialect);
} else {
// We don't attempt to bundle on dialects where we
// don't know where to put the embedded schemas
throw sourcemeta::core::SchemaError(
"Could not determine how to perform bundling in this dialect");
}
}

auto bundle(const sourcemeta::core::JSON &schema, const SchemaWalker &walker,
const SchemaResolver &resolver,
const std::optional<std::string> &default_dialect)
const std::optional<std::string> &default_dialect,
const std::optional<Pointer> &default_container)
-> sourcemeta::core::JSON {
sourcemeta::core::JSON copy = schema;
bundle(copy, walker, resolver, default_dialect);
bundle(copy, walker, resolver, default_dialect, default_container);
return copy;
}

Expand Down
6 changes: 4 additions & 2 deletions src/core/jsonschema/include/sourcemeta/core/jsonschema.h
Original file line number Diff line number Diff line change
Expand Up @@ -538,7 +538,8 @@ auto reference_visit(
SOURCEMETA_CORE_JSONSCHEMA_EXPORT
auto bundle(JSON &schema, const SchemaWalker &walker,
const SchemaResolver &resolver,
const std::optional<std::string> &default_dialect = std::nullopt)
const std::optional<std::string> &default_dialect = std::nullopt,
const std::optional<Pointer> &default_container = std::nullopt)
-> void;

/// @ingroup jsonschema
Expand Down Expand Up @@ -595,7 +596,8 @@ auto bundle(JSON &schema, const SchemaWalker &walker,
SOURCEMETA_CORE_JSONSCHEMA_EXPORT
auto bundle(const JSON &schema, const SchemaWalker &walker,
const SchemaResolver &resolver,
const std::optional<std::string> &default_dialect = std::nullopt)
const std::optional<std::string> &default_dialect = std::nullopt,
const std::optional<Pointer> &default_container = std::nullopt)
-> JSON;

/// @ingroup jsonschema
Expand Down
140 changes: 140 additions & 0 deletions test/jsonschema/jsonschema_bundle_2020_12_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -623,3 +623,143 @@ TEST(JSONSchema_bundle_2020_12, hyperschema_1) {
EXPECT_TRUE(document.at("$defs").defines(
"https://json-schema.org/draft/2020-12/hyper-schema"));
}

TEST(JSONSchema_bundle_2020_12, bundle_to_definitions) {
sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$ref": "https://www.sourcemeta.com/recursive"
})JSON");

sourcemeta::core::bundle(document, sourcemeta::core::schema_official_walker,
test_resolver, std::nullopt,
sourcemeta::core::Pointer{"definitions"});

const sourcemeta::core::JSON expected = sourcemeta::core::parse_json(R"JSON({
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$ref": "https://www.sourcemeta.com/recursive",
"definitions": {
"https://www.sourcemeta.com/recursive": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://www.sourcemeta.com/recursive",
"properties": {
"foo": { "$ref": "#" }
}
}
}
})JSON");

EXPECT_EQ(document, expected);
}

TEST(JSONSchema_bundle_2020_12, custom_nested_object_path_non_existent) {
sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$ref": "https://www.sourcemeta.com/recursive"
})JSON");

sourcemeta::core::bundle(
document, sourcemeta::core::schema_official_walker, test_resolver,
std::nullopt, sourcemeta::core::Pointer{"x-definitions", "foo", "bar"});

const sourcemeta::core::JSON expected = sourcemeta::core::parse_json(R"JSON({
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$ref": "https://www.sourcemeta.com/recursive",
"x-definitions": {
"foo": {
"bar": {
"https://www.sourcemeta.com/recursive": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://www.sourcemeta.com/recursive",
"properties": {
"foo": { "$ref": "#" }
}
}
}
}
}
})JSON");

EXPECT_EQ(document, expected);
}

TEST(JSONSchema_bundle_2020_12, custom_nested_object_path_half_existent) {
sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$ref": "https://www.sourcemeta.com/recursive",
"x-definitions": { "foo": {} }
})JSON");

sourcemeta::core::bundle(
document, sourcemeta::core::schema_official_walker, test_resolver,
std::nullopt, sourcemeta::core::Pointer{"x-definitions", "foo", "bar"});

const sourcemeta::core::JSON expected = sourcemeta::core::parse_json(R"JSON({
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$ref": "https://www.sourcemeta.com/recursive",
"x-definitions": {
"foo": {
"bar": {
"https://www.sourcemeta.com/recursive": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://www.sourcemeta.com/recursive",
"properties": {
"foo": { "$ref": "#" }
}
}
}
}
}
})JSON");

EXPECT_EQ(document, expected);
}

TEST(JSONSchema_bundle_2020_12,
custom_nested_object_path_half_existent_with_array) {
sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$ref": "https://www.sourcemeta.com/recursive",
"x-definitions": [ { "foo": {} } ]
})JSON");

sourcemeta::core::bundle(
document, sourcemeta::core::schema_official_walker, test_resolver,
std::nullopt,
sourcemeta::core::Pointer{"x-definitions", 0, "foo", "bar"});

const sourcemeta::core::JSON expected = sourcemeta::core::parse_json(R"JSON({
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$ref": "https://www.sourcemeta.com/recursive",
"x-definitions": [
{
"foo": {
"bar": {
"https://www.sourcemeta.com/recursive": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://www.sourcemeta.com/recursive",
"properties": {
"foo": { "$ref": "#" }
}
}
}
}
}
]
})JSON");

EXPECT_EQ(document, expected);
}

TEST(JSONSchema_bundle_2020_12, custom_nested_object_path_not_object) {
sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$ref": "https://www.sourcemeta.com/recursive",
"x-definitions": { "foo": { "bar": [] } }
})JSON");

EXPECT_THROW(sourcemeta::core::bundle(
document, sourcemeta::core::schema_official_walker,
test_resolver, std::nullopt,
sourcemeta::core::Pointer{"x-definitions", "foo", "bar"}),
sourcemeta::core::SchemaError);
}
27 changes: 27 additions & 0 deletions test/jsonschema/jsonschema_bundle_draft7_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -531,3 +531,30 @@ TEST(JSONSchema_bundle_draft7, hyperschema_ref_metaschema) {
EXPECT_TRUE(document.at("definitions")
.defines("http://json-schema.org/draft-07/schema"));
}

TEST(JSONSchema_bundle_draft7, bundle_to_defs) {
sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({
"$schema": "http://json-schema.org/draft-07/schema#",
"$ref": "https://www.sourcemeta.com/recursive"
})JSON");

sourcemeta::core::bundle(document, sourcemeta::core::schema_official_walker,
test_resolver, std::nullopt,
sourcemeta::core::Pointer{"$defs"});

const sourcemeta::core::JSON expected = sourcemeta::core::parse_json(R"JSON({
"$schema": "http://json-schema.org/draft-07/schema#",
"$ref": "https://www.sourcemeta.com/recursive",
"$defs": {
"https://www.sourcemeta.com/recursive": {
"$schema": "http://json-schema.org/draft-07/schema#",
"$id": "https://www.sourcemeta.com/recursive",
"properties": {
"foo": { "$ref": "#" }
}
}
}
})JSON");

EXPECT_EQ(document, expected);
}