Skip to content
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

Extract BundleOptions::WithoutIdentifiers into its own unidentify() #1429

Merged
merged 1 commit into from
Jan 10, 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
2 changes: 1 addition & 1 deletion src/jsonschema/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ noa_library(NAMESPACE sourcemeta PROJECT jsontoolkit NAME jsonschema
walker.h reference.h frame.h error.h unevaluated.h keywords.h
SOURCES jsonschema.cc default_walker.cc frame.cc
anchor.cc resolver.cc walker.cc bundle.cc
unevaluated.cc relativize.cc
unevaluated.cc relativize.cc unidentify.cc
"${CMAKE_CURRENT_BINARY_DIR}/official_resolver.cc")

if(JSONTOOLKIT_INSTALL)
Expand Down
60 changes: 2 additions & 58 deletions src/jsonschema/bundle.cc
Original file line number Diff line number Diff line change
Expand Up @@ -127,82 +127,26 @@ auto bundle_schema(sourcemeta::jsontoolkit::JSON &root,
}
}

auto remove_identifiers(sourcemeta::jsontoolkit::JSON &schema,
const sourcemeta::jsontoolkit::SchemaWalker &walker,
const sourcemeta::jsontoolkit::SchemaResolver &resolver,
const std::optional<std::string> &default_dialect)
-> void {
// (1) Re-frame before changing anything
sourcemeta::jsontoolkit::Frame frame;
frame.analyse(schema, walker, resolver, default_dialect);

// (2) Remove all identifiers and anchors
for (const auto &entry : sourcemeta::jsontoolkit::SchemaIterator{
schema, walker, resolver, default_dialect}) {
auto &subschema{sourcemeta::jsontoolkit::get(schema, entry.pointer)};
if (subschema.is_boolean()) {
continue;
}

assert(entry.base_dialect.has_value());
sourcemeta::jsontoolkit::anonymize(subschema, entry.base_dialect.value());

if (entry.vocabularies.contains(
"https://json-schema.org/draft/2020-12/vocab/core")) {
subschema.erase("$anchor");
subschema.erase("$dynamicAnchor");
}

if (entry.vocabularies.contains(
"https://json-schema.org/draft/2019-09/vocab/core")) {
subschema.erase("$anchor");
subschema.erase("$recursiveAnchor");
}
}

// (3) Fix-up reference based on pointers from the root
for (const auto &[key, reference] : frame.references()) {
// 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(key.second, reference.destination)) {
continue;
}

const auto result{frame.traverse(reference.destination)};
assert(result.has_value());
sourcemeta::jsontoolkit::set(
schema, key.second,
sourcemeta::jsontoolkit::JSON{
sourcemeta::jsontoolkit::to_uri(result.value().get().pointer)
.recompose()});
}
}

} // namespace

namespace sourcemeta::jsontoolkit {

auto bundle(sourcemeta::jsontoolkit::JSON &schema, const SchemaWalker &walker,
const SchemaResolver &resolver, const BundleOptions options,
const SchemaResolver &resolver,
const std::optional<std::string> &default_dialect) -> void {
const auto vocabularies{
sourcemeta::jsontoolkit::vocabularies(schema, resolver, default_dialect)};
sourcemeta::jsontoolkit::Frame frame;
bundle_schema(schema, definitions_keyword(vocabularies), schema, frame,
walker, resolver, default_dialect);

if (options == BundleOptions::WithoutIdentifiers) {
remove_identifiers(schema, walker, resolver, default_dialect);
}
}

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

Expand Down
40 changes: 37 additions & 3 deletions src/jsonschema/include/sourcemeta/jsontoolkit/jsonschema.h
Original file line number Diff line number Diff line change
Expand Up @@ -332,15 +332,15 @@ auto schema_format_compare(const JSON::String &left, const JSON::String &right)
/// #include <sourcemeta/jsontoolkit/jsonschema.h>
/// #include <cassert>
///
/// sourcemeta::jsontoolkit::JSON document =
/// sourcemeta::jsontoolkit::JSON schema =
/// sourcemeta::jsontoolkit::parse(R"JSON({
/// "$id": "https://www.example.com/schema",
/// "$schema": "https://json-schema.org/draft/2020-12/schema",
/// "$ref": "https://www.example.com/another",
/// })JSON");
///
/// sourcemeta::jsontoolkit::relativize(schema,
/// sourcemeta::jsontoolkit::default_dialect,
/// sourcemeta::jsontoolkit::default_walker,
/// sourcemeta::jsontoolkit::official_resolver);
///
/// const sourcemeta::jsontoolkit::JSON expected =
Expand All @@ -350,14 +350,48 @@ auto schema_format_compare(const JSON::String &left, const JSON::String &right)
/// "$ref": "another",
/// })JSON");
///
/// assert(document == expected);
/// assert(schema == expected);
/// ```
SOURCEMETA_JSONTOOLKIT_JSONSCHEMA_EXPORT
auto relativize(
JSON &schema, const SchemaWalker &walker, const SchemaResolver &resolver,
const std::optional<std::string> &default_dialect = std::nullopt,
const std::optional<std::string> &default_id = std::nullopt) -> void;

/// @ingroup jsonschema
///
/// Remove every identifer from a schema, rephrasing references (if any) as
/// needed. For example:
///
/// ```cpp
/// #include <sourcemeta/jsontoolkit/json.h>
/// #include <sourcemeta/jsontoolkit/jsonschema.h>
/// #include <cassert>
///
/// sourcemeta::jsontoolkit::JSON schema =
/// sourcemeta::jsontoolkit::parse(R"JSON({
/// "$id": "https://www.example.com/schema",
/// "$schema": "https://json-schema.org/draft/2020-12/schema",
/// "$ref": "another",
/// })JSON");
///
/// sourcemeta::jsontoolkit::unidentify(schema,
/// sourcemeta::jsontoolkit::default_walker,
/// sourcemeta::jsontoolkit::official_resolver);
///
/// const sourcemeta::jsontoolkit::JSON expected =
/// sourcemeta::jsontoolkit::parse(R"JSON({
/// "$schema": "https://json-schema.org/draft/2020-12/schema",
/// "$ref": "https://www.example.com/another",
/// })JSON");
///
/// assert(schema == expected);
/// ```
SOURCEMETA_JSONTOOLKIT_JSONSCHEMA_EXPORT
auto unidentify(
JSON &schema, const SchemaWalker &walker, const SchemaResolver &resolver,
const std::optional<std::string> &default_dialect = std::nullopt) -> void;

} // namespace sourcemeta::jsontoolkit

#endif
16 changes: 0 additions & 16 deletions src/jsonschema/include/sourcemeta/jsontoolkit/jsonschema_bundle.h
Original file line number Diff line number Diff line change
Expand Up @@ -17,20 +17,6 @@ namespace sourcemeta::jsontoolkit {

// TODO: Optionally let users bundle the metaschema too

/// @ingroup jsonschema
/// A set of options that modify the behavior of bundling
enum class BundleOptions : std::uint8_t {
/// Perform standard JSON Schema bundling
Default,

/// Perform standard JSON Schema bundling but without making
/// use of identifiers. This is helpful for delivering
/// schemas to some non-compliant implementations that do not
/// recognize identifiers (like Visua Studio Code at the time
/// of this writing)
WithoutIdentifiers
};

/// @ingroup jsonschema
///
/// This function bundles a JSON Schema (starting from Draft 4) by embedding
Expand Down Expand Up @@ -83,7 +69,6 @@ enum class BundleOptions : std::uint8_t {
SOURCEMETA_JSONTOOLKIT_JSONSCHEMA_EXPORT
auto bundle(sourcemeta::jsontoolkit::JSON &schema, const SchemaWalker &walker,
const SchemaResolver &resolver,
const BundleOptions options = BundleOptions::Default,
const std::optional<std::string> &default_dialect = std::nullopt)
-> void;

Expand Down Expand Up @@ -141,7 +126,6 @@ auto bundle(sourcemeta::jsontoolkit::JSON &schema, const SchemaWalker &walker,
SOURCEMETA_JSONTOOLKIT_JSONSCHEMA_EXPORT
auto bundle(const sourcemeta::jsontoolkit::JSON &schema,
const SchemaWalker &walker, const SchemaResolver &resolver,
const BundleOptions options = BundleOptions::Default,
const std::optional<std::string> &default_dialect = std::nullopt)
-> sourcemeta::jsontoolkit::JSON;

Expand Down
49 changes: 49 additions & 0 deletions src/jsonschema/unidentify.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
#include <sourcemeta/jsontoolkit/jsonschema.h>

namespace sourcemeta::jsontoolkit {

auto unidentify(JSON &schema, const SchemaWalker &walker,
const SchemaResolver &resolver,
const std::optional<std::string> &default_dialect) -> void {
// (1) Re-frame before changing anything
Frame frame;
frame.analyse(schema, walker, resolver, default_dialect);

// (2) Remove all identifiers and anchors
for (const auto &entry :
SchemaIterator{schema, walker, resolver, default_dialect}) {
auto &subschema{get(schema, entry.pointer)};
if (subschema.is_boolean()) {
continue;
}

assert(entry.base_dialect.has_value());
anonymize(subschema, entry.base_dialect.value());

if (entry.vocabularies.contains(
"https://json-schema.org/draft/2020-12/vocab/core")) {
subschema.erase("$anchor");
subschema.erase("$dynamicAnchor");
}

if (entry.vocabularies.contains(
"https://json-schema.org/draft/2019-09/vocab/core")) {
subschema.erase("$anchor");
subschema.erase("$recursiveAnchor");
}
}

// (3) Fix-up reference based on pointers from the root
for (const auto &[key, reference] : frame.references()) {
const auto result{frame.traverse(reference.destination)};
if (result.has_value()) {
set(schema, key.second,
JSON{to_uri(result.value().get().pointer).recompose()});
} else if (!key.second.empty() && key.second.back().is_property() &&
key.second.back().to_property() != "$schema") {
set(schema, key.second, JSON{reference.destination});
}
}
}

} // namespace sourcemeta::jsontoolkit
1 change: 1 addition & 0 deletions test/jsonschema/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ add_executable(sourcemeta_jsontoolkit_jsonschema_unit
jsonschema_bundle_draft1_test.cc
jsonschema_bundle_draft0_test.cc
jsonschema_bundle_test.cc
jsonschema_unidentify_test.cc
jsonschema_metaschema_test.cc
jsonschema_dialect_test.cc
jsonschema_dialect_2020_12_test.cc
Expand Down
84 changes: 0 additions & 84 deletions test/jsonschema/jsonschema_bundle_2019_09_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -449,7 +449,6 @@ TEST(JSONSchema_bundle_2019_09, anonymous_no_dialect) {

sourcemeta::jsontoolkit::bundle(
document, sourcemeta::jsontoolkit::default_schema_walker, test_resolver,
sourcemeta::jsontoolkit::BundleOptions::Default,
"https://json-schema.org/draft/2019-09/schema");

const sourcemeta::jsontoolkit::JSON expected =
Expand All @@ -466,59 +465,6 @@ TEST(JSONSchema_bundle_2019_09, anonymous_no_dialect) {
EXPECT_EQ(document, expected);
}

TEST(JSONSchema_bundle_2019_09, without_id) {
sourcemeta::jsontoolkit::JSON document =
sourcemeta::jsontoolkit::parse(R"JSON({
"$id": "https://www.sourcemeta.com/top-level",
"$schema": "https://json-schema.org/draft/2019-09/schema",
"properties": {
"foo": {
"$ref": "recursive#/properties/foo"
},
"baz": {
"$ref": "https://example.com/baz-anchor#baz"
}
}
})JSON");

sourcemeta::jsontoolkit::bundle(
document, sourcemeta::jsontoolkit::default_schema_walker, test_resolver,
sourcemeta::jsontoolkit::BundleOptions::WithoutIdentifiers);

const sourcemeta::jsontoolkit::JSON expected =
sourcemeta::jsontoolkit::parse(R"JSON({
"$schema": "https://json-schema.org/draft/2019-09/schema",
"properties": {
"foo": {
"$ref": "#/$defs/https%3A~1~1www.sourcemeta.com~1recursive/properties/foo"
},
"baz": {
"$ref": "#/$defs/https%3A~1~1example.com~1baz-anchor/$defs/baz"
}
},
"$defs": {
"https://www.sourcemeta.com/recursive": {
"$schema": "https://json-schema.org/draft/2019-09/schema",
"properties": {
"foo": {
"$ref": "#/$defs/https%3A~1~1www.sourcemeta.com~1recursive"
}
}
},
"https://example.com/baz-anchor": {
"$schema": "https://json-schema.org/draft/2019-09/schema",
"$defs": {
"baz": {
"type": "string"
}
}
}
}
})JSON");

EXPECT_EQ(document, expected);
}

TEST(JSONSchema_bundle_2019_09, metaschema) {
sourcemeta::jsontoolkit::JSON document =
sourcemeta::jsontoolkit::parse(R"JSON({
Expand Down Expand Up @@ -550,36 +496,6 @@ TEST(JSONSchema_bundle_2019_09, metaschema) {
EXPECT_EQ(document, expected);
}

TEST(JSONSchema_bundle_2019_09, metaschema_without_id) {
sourcemeta::jsontoolkit::JSON document =
sourcemeta::jsontoolkit::parse(R"JSON({
"$schema": "https://example.com/meta/1.json",
"type": "string"
})JSON");

sourcemeta::jsontoolkit::bundle(
document, sourcemeta::jsontoolkit::default_schema_walker, test_resolver,
sourcemeta::jsontoolkit::BundleOptions::WithoutIdentifiers);

const sourcemeta::jsontoolkit::JSON expected =
sourcemeta::jsontoolkit::parse(R"JSON({
"$schema": "#/$defs/https%3A~1~1example.com~1meta~11.json",
"type": "string",
"$defs": {
"https://example.com/meta/1.json": {
"$schema": "#/$defs/https%3A~1~1example.com~1meta~12.json",
"$vocabulary": { "https://json-schema.org/draft/2019-09/vocab/core": true }
},
"https://example.com/meta/2.json": {
"$schema": "https://json-schema.org/draft/2019-09/schema",
"$vocabulary": { "https://json-schema.org/draft/2019-09/vocab/core": true }
}
}
})JSON");

EXPECT_EQ(document, expected);
}

TEST(JSONSchema_bundle_2019_09, relative_base_uri_with_ref) {
sourcemeta::jsontoolkit::JSON document =
sourcemeta::jsontoolkit::parse(R"JSON({
Expand Down
Loading
Loading