diff --git a/src/jsonschema/CMakeLists.txt b/src/jsonschema/CMakeLists.txt index 8de677a11..068c9aa09 100644 --- a/src/jsonschema/CMakeLists.txt +++ b/src/jsonschema/CMakeLists.txt @@ -7,7 +7,8 @@ noa_library(NAMESPACE sourcemeta PROJECT jsontoolkit NAME jsonschema PRIVATE_HEADERS anchor.h bundle.h resolver.h 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 + anchor.cc resolver.cc walker.cc bundle.cc + unevaluated.cc relativize.cc "${CMAKE_CURRENT_BINARY_DIR}/official_resolver.cc") if(JSONTOOLKIT_INSTALL) diff --git a/src/jsonschema/include/sourcemeta/jsontoolkit/jsonschema.h b/src/jsonschema/include/sourcemeta/jsontoolkit/jsonschema.h index 3f18f1648..a3fd5f3e8 100644 --- a/src/jsonschema/include/sourcemeta/jsontoolkit/jsonschema.h +++ b/src/jsonschema/include/sourcemeta/jsontoolkit/jsonschema.h @@ -322,6 +322,42 @@ SOURCEMETA_JSONTOOLKIT_JSONSCHEMA_EXPORT auto schema_format_compare(const JSON::String &left, const JSON::String &right) -> bool; +/// @ingroup jsonschema +/// +/// Try to turn every possible absolute reference in a schema into a relative +/// one. For example: +/// +/// ```cpp +/// #include +/// #include +/// #include +/// +/// sourcemeta::jsontoolkit::JSON document = +/// 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::official_resolver); +/// +/// const sourcemeta::jsontoolkit::JSON expected = +/// sourcemeta::jsontoolkit::parse(R"JSON({ +/// "$id": "https://www.example.com/schema", +/// "$schema": "https://json-schema.org/draft/2020-12/schema", +/// "$ref": "another", +/// })JSON"); +/// +/// assert(document == expected); +/// ``` +SOURCEMETA_JSONTOOLKIT_JSONSCHEMA_EXPORT +auto relativize( + JSON &schema, const SchemaWalker &walker, const SchemaResolver &resolver, + const std::optional &default_dialect = std::nullopt, + const std::optional &default_id = std::nullopt) -> void; + } // namespace sourcemeta::jsontoolkit #endif diff --git a/src/jsonschema/relativize.cc b/src/jsonschema/relativize.cc new file mode 100644 index 000000000..664c39817 --- /dev/null +++ b/src/jsonschema/relativize.cc @@ -0,0 +1,43 @@ +#include + +namespace sourcemeta::jsontoolkit { + +auto relativize(JSON &schema, const SchemaWalker &walker, + const SchemaResolver &resolver, + const std::optional &default_dialect, + const std::optional &default_id) -> void { + Frame frame; + frame.analyse(schema, walker, resolver, default_dialect, default_id); + + for (const auto &entry : frame.locations()) { + if (entry.second.type != Frame::LocationType::Resource && + entry.second.type != Frame::LocationType::Subschema) { + continue; + } + + auto &subschema{get(schema, entry.second.pointer)}; + assert(is_schema(subschema)); + if (!subschema.is_object()) { + continue; + } + + const auto base{URI{entry.second.base}.canonicalize()}; + for (const auto &property : subschema.as_object()) { + if (walker(property.first, frame.vocabularies(entry.second, resolver)) + .type != KeywordType::Reference || + !property.second.is_string()) { + continue; + } + + URI reference{property.second.to_string()}; + reference.canonicalize(); + reference.relative_to(base); + + if (reference.is_relative()) { + subschema.assign(property.first, JSON{reference.recompose()}); + } + } + } +} + +} // namespace sourcemeta::jsontoolkit diff --git a/test/jsonschema/CMakeLists.txt b/test/jsonschema/CMakeLists.txt index 88ed38e91..c36e83754 100644 --- a/test/jsonschema/CMakeLists.txt +++ b/test/jsonschema/CMakeLists.txt @@ -83,6 +83,7 @@ add_executable(sourcemeta_jsontoolkit_jsonschema_unit jsonschema_error_test.cc jsonschema_keyword_iterator_test.cc jsonschema_official_resolver_test.cc + jsonschema_relativize_test.cc jsonschema_map_resolver_test.cc jsonschema_flat_file_resolver_test.cc jsonschema_format_test.cc) diff --git a/test/jsonschema/jsonschema_relativize_test.cc b/test/jsonschema/jsonschema_relativize_test.cc new file mode 100644 index 000000000..d19406c6b --- /dev/null +++ b/test/jsonschema/jsonschema_relativize_test.cc @@ -0,0 +1,298 @@ +#include + +#include +#include + +TEST(JSONSchema_relativize, draft4_1) { + auto schema = sourcemeta::jsontoolkit::parse(R"JSON({ + "id": "http://asyncapi.com/definitions/1.0.0/asyncapi.json", + "$schema": "http://json-schema.org/draft-04/schema", + "title": "AsyncAPI 1.0 schema.", + "type": "object", + "required": [ + "asyncapi", + "info", + "topics" + ], + "additionalProperties": false, + "patternProperties": { + "^x-": { + "$ref": "http://asyncapi.com/definitions/1.0.0/vendorExtension.json" + } + } + })JSON"); + + sourcemeta::jsontoolkit::relativize( + schema, sourcemeta::jsontoolkit::default_schema_walker, + sourcemeta::jsontoolkit::official_resolver); + + const auto expected = sourcemeta::jsontoolkit::parse(R"JSON({ + "id": "http://asyncapi.com/definitions/1.0.0/asyncapi.json", + "$schema": "http://json-schema.org/draft-04/schema", + "title": "AsyncAPI 1.0 schema.", + "type": "object", + "required": [ + "asyncapi", + "info", + "topics" + ], + "additionalProperties": false, + "patternProperties": { + "^x-": { + "$ref": "vendorExtension.json" + } + } + })JSON"); + + EXPECT_EQ(schema, expected); +} + +TEST(JSONSchema_relativize, draft4_2) { + auto schema = sourcemeta::jsontoolkit::parse(R"JSON({ + "id": "http://example.com", + "$schema": "http://json-schema.org/draft-04/schema", + "properties": { + "foo": { + "$ref": "http://asyncapi.com/definitions/1.0.0/vendorExtension.json" + } + } + })JSON"); + + sourcemeta::jsontoolkit::relativize( + schema, sourcemeta::jsontoolkit::default_schema_walker, + sourcemeta::jsontoolkit::official_resolver); + + const auto expected = sourcemeta::jsontoolkit::parse(R"JSON({ + "id": "http://example.com", + "$schema": "http://json-schema.org/draft-04/schema", + "properties": { + "foo": { + "$ref": "http://asyncapi.com/definitions/1.0.0/vendorExtension.json" + } + } + })JSON"); + + EXPECT_EQ(schema, expected); +} + +TEST(JSONSchema_relativize, draft4_3) { + auto schema = sourcemeta::jsontoolkit::parse(R"JSON({ + "id": "http://example.com", + "$schema": "http://json-schema.org/draft-04/schema", + "properties": { + "foo": { + "$ref": "http://example.com/nested" + }, + "bar": { + "id": "http://sourcemeta.com", + "properties": { + "bar": { + "$ref": "http://example.com/nested" + }, + "baz": { + "$ref": "http://sourcemeta.com/nested" + } + } + } + } + })JSON"); + + sourcemeta::jsontoolkit::relativize( + schema, sourcemeta::jsontoolkit::default_schema_walker, + sourcemeta::jsontoolkit::official_resolver); + + const auto expected = sourcemeta::jsontoolkit::parse(R"JSON({ + "id": "http://example.com", + "$schema": "http://json-schema.org/draft-04/schema", + "properties": { + "foo": { + "$ref": "nested" + }, + "bar": { + "id": "http://sourcemeta.com", + "properties": { + "bar": { + "$ref": "http://example.com/nested" + }, + "baz": { + "$ref": "nested" + } + } + } + } + })JSON"); + + EXPECT_EQ(schema, expected); +} + +TEST(JSONSchema_relativize, draft4_4) { + auto schema = sourcemeta::jsontoolkit::parse(R"JSON({ + "$schema": "http://json-schema.org/draft-04/schema", + "properties": { + "foo": { + "$ref": "http://asyncapi.com/definitions/1.0.0/vendorExtension.json" + } + } + })JSON"); + + sourcemeta::jsontoolkit::relativize( + schema, sourcemeta::jsontoolkit::default_schema_walker, + sourcemeta::jsontoolkit::official_resolver); + + const auto expected = sourcemeta::jsontoolkit::parse(R"JSON({ + "$schema": "http://json-schema.org/draft-04/schema", + "properties": { + "foo": { + "$ref": "http://asyncapi.com/definitions/1.0.0/vendorExtension.json" + } + } + })JSON"); + + EXPECT_EQ(schema, expected); +} + +TEST(JSONSchema_relativize, draft4_5) { + auto schema = sourcemeta::jsontoolkit::parse(R"JSON({ + "$schema": "http://json-schema.org/draft-04/schema", + "properties": { + "foo": { + "$ref": "http://example.com/nested" + }, + "bar": { + "id": "http://sourcemeta.com", + "properties": { + "bar": { + "$ref": "http://example.com/nested" + }, + "baz": { + "$ref": "http://sourcemeta.com/nested" + } + } + } + } + })JSON"); + + sourcemeta::jsontoolkit::relativize( + schema, sourcemeta::jsontoolkit::default_schema_walker, + sourcemeta::jsontoolkit::official_resolver); + + const auto expected = sourcemeta::jsontoolkit::parse(R"JSON({ + "$schema": "http://json-schema.org/draft-04/schema", + "properties": { + "foo": { + "$ref": "http://example.com/nested" + }, + "bar": { + "id": "http://sourcemeta.com", + "properties": { + "bar": { + "$ref": "http://example.com/nested" + }, + "baz": { + "$ref": "nested" + } + } + } + } + })JSON"); + + EXPECT_EQ(schema, expected); +} + +TEST(JSONSchema_relativize, draft4_6) { + auto schema = sourcemeta::jsontoolkit::parse(R"JSON({ + "$schema": "http://json-schema.org/draft-04/schema", + "properties": { + "foo": { + "$ref": "http://asyncapi.com/definitions/1.0.0/vendorExtension.json" + } + } + })JSON"); + + sourcemeta::jsontoolkit::relativize( + schema, sourcemeta::jsontoolkit::default_schema_walker, + sourcemeta::jsontoolkit::official_resolver, std::nullopt, + "http://asyncapi.com/definitions"); + + const auto expected = sourcemeta::jsontoolkit::parse(R"JSON({ + "$schema": "http://json-schema.org/draft-04/schema", + "properties": { + "foo": { + "$ref": "1.0.0/vendorExtension.json" + } + } + })JSON"); + + EXPECT_EQ(schema, expected); +} + +TEST(JSONSchema_relativize, draft4_7) { + auto schema = sourcemeta::jsontoolkit::parse(R"JSON({ + "id": "http://asyncapi.com/definitions", + "properties": { + "foo": { + "$ref": "http://asyncapi.com/definitions/1.0.0/vendorExtension.json" + } + } + })JSON"); + + sourcemeta::jsontoolkit::relativize( + schema, sourcemeta::jsontoolkit::default_schema_walker, + sourcemeta::jsontoolkit::official_resolver, + "http://json-schema.org/draft-04/schema"); + + const auto expected = sourcemeta::jsontoolkit::parse(R"JSON({ + "id": "http://asyncapi.com/definitions", + "properties": { + "foo": { + "$ref": "1.0.0/vendorExtension.json" + } + } + })JSON"); + + EXPECT_EQ(schema, expected); +} + +TEST(JSONSchema_relativize, draft4_8) { + auto schema = sourcemeta::jsontoolkit::parse(R"JSON({ + "id": "http://asyncapi.com/definitions", + "properties": { + "foo": { + "$ref": "http://asyncapi.com/definitions/1.0.0/vendorExtension.json" + } + } + })JSON"); + + EXPECT_THROW(sourcemeta::jsontoolkit::relativize( + schema, sourcemeta::jsontoolkit::default_schema_walker, + sourcemeta::jsontoolkit::official_resolver), + sourcemeta::jsontoolkit::SchemaError); +} + +TEST(JSONSchema_relativize, 2020_12_1) { + auto schema = sourcemeta::jsontoolkit::parse(R"JSON({ + "$id": "http://example.com", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "properties": { + "foo": { + "$dynamicRef": "http://example.com/foo#bar" + } + } + })JSON"); + + sourcemeta::jsontoolkit::relativize( + schema, sourcemeta::jsontoolkit::default_schema_walker, + sourcemeta::jsontoolkit::official_resolver); + + const auto expected = sourcemeta::jsontoolkit::parse(R"JSON({ + "$id": "http://example.com", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "properties": { + "foo": { + "$dynamicRef": "foo#bar" + } + } + })JSON"); + + EXPECT_EQ(schema, expected); +}