diff --git a/src/core/jsonschema/CMakeLists.txt b/src/core/jsonschema/CMakeLists.txt index 228b279a9..34ce26406 100644 --- a/src/core/jsonschema/CMakeLists.txt +++ b/src/core/jsonschema/CMakeLists.txt @@ -8,7 +8,7 @@ sourcemeta_library(NAMESPACE sourcemeta PROJECT core NAME jsonschema keywords.h transform.h SOURCES jsonschema.cc default_walker.cc frame.cc resolver.cc walker.cc bundle.cc - unevaluated.cc relativize.cc unidentify.cc + unevaluated.cc unidentify.cc transform_rule.cc transformer.cc "${CMAKE_CURRENT_BINARY_DIR}/official_resolver.cc") diff --git a/src/core/jsonschema/include/sourcemeta/core/jsonschema.h b/src/core/jsonschema/include/sourcemeta/core/jsonschema.h index cd54175c6..10835af79 100644 --- a/src/core/jsonschema/include/sourcemeta/core/jsonschema.h +++ b/src/core/jsonschema/include/sourcemeta/core/jsonschema.h @@ -15,9 +15,10 @@ #include #include -#include // std::map -#include // std::optional -#include // std::string +#include // std::function +#include // std::map +#include // std::optional +#include // std::string /// @defgroup jsonschema JSON Schema /// @brief A set of JSON Schema utilities across draft versions. @@ -323,8 +324,8 @@ auto schema_format_compare(const JSON::String &left, const JSON::String &right) /// @ingroup jsonschema /// -/// Try to turn every possible absolute reference in a schema into a relative -/// one. For example: +/// Remove every identifer from a schema, rephrasing references (if any) as +/// needed. For example: /// /// ```cpp /// #include @@ -335,32 +336,43 @@ auto schema_format_compare(const JSON::String &left, const JSON::String &right) /// sourcemeta::core::parse_json(R"JSON({ /// "$id": "https://www.example.com/schema", /// "$schema": "https://json-schema.org/draft/2020-12/schema", -/// "$ref": "https://www.example.com/another", +/// "$ref": "another", /// })JSON"); /// -/// sourcemeta::core::relativize(schema, +/// sourcemeta::core::unidentify(schema, /// sourcemeta::core::schema_official_walker, /// sourcemeta::core::official_resolver); /// /// const sourcemeta::core::JSON expected = /// sourcemeta::core::parse_json(R"JSON({ -/// "$id": "https://www.example.com/schema", /// "$schema": "https://json-schema.org/draft/2020-12/schema", -/// "$ref": "another", +/// "$ref": "https://www.example.com/another", /// })JSON"); /// /// assert(schema == expected); /// ``` SOURCEMETA_CORE_JSONSCHEMA_EXPORT -auto relativize( +auto unidentify( JSON &schema, const SchemaWalker &walker, const SchemaResolver &resolver, - const std::optional &default_dialect = std::nullopt, - const std::optional &default_id = std::nullopt) -> void; + const std::optional &default_dialect = std::nullopt) -> void; /// @ingroup jsonschema /// -/// Remove every identifer from a schema, rephrasing references (if any) as -/// needed. For example: +/// Visit every reference in a schema. The arguments are as follows: +/// +/// - The current subschema +/// - The base URI of the current subschema +/// - The reference vocabulary +/// - The reference keyword name +/// - The reference keyword value +using SchemaVisitorReference = + std::function; + +/// @ingroup jsonschema +/// +/// A reference visitor to try to turn every possible absolute reference in a +/// schema into a relative one. For example: /// /// ```cpp /// #include @@ -371,25 +383,70 @@ auto relativize( /// sourcemeta::core::parse_json(R"JSON({ /// "$id": "https://www.example.com/schema", /// "$schema": "https://json-schema.org/draft/2020-12/schema", -/// "$ref": "another", +/// "$ref": "https://www.example.com/another", /// })JSON"); /// -/// sourcemeta::core::unidentify(schema, +/// sourcemeta::core::reference_visit(schema, /// sourcemeta::core::schema_official_walker, -/// sourcemeta::core::official_resolver); +/// sourcemeta::core::official_resolver, +/// sourcemeta::core::reference_visitor_relativize); /// /// const sourcemeta::core::JSON expected = /// sourcemeta::core::parse_json(R"JSON({ +/// "$id": "https://www.example.com/schema", /// "$schema": "https://json-schema.org/draft/2020-12/schema", -/// "$ref": "https://www.example.com/another", +/// "$ref": "another", /// })JSON"); /// /// assert(schema == expected); /// ``` SOURCEMETA_CORE_JSONSCHEMA_EXPORT -auto unidentify( +auto reference_visitor_relativize(JSON &subschema, const URI &base, + const JSON::String &vocabulary, + const JSON::String &keyword, + const JSON::String &value) -> void; + +/// @ingroup jsonschema +/// +/// A utility function to loop over every reference in a schema, allowing +/// modifications to their subschemas if desired. Note that the consumer is +/// responsible for not making the schema invalid. For example: +/// +/// ```cpp +/// #include +/// #include +/// +/// sourcemeta::core::JSON schema = +/// sourcemeta::core::parse_json(R"JSON({ +/// "$id": "https://www.example.com/schema", +/// "$schema": "https://json-schema.org/draft/2020-12/schema", +/// "$ref": "https://www.example.com/another", +/// })JSON"); +/// +/// static auto visitor(JSON &subschema, +/// const URI &base, +/// const JSON::String &vocabulary, +/// const JSON::String &keyword, +/// const JSON::String &value) -> void { +/// sourcemeta::core::prettify(subschema, std::cerr); +/// std::cerr << "\n"; +/// std::cerr << base.recompose() << "\n"; +/// std::cerr << vocabulary << "\n"; +/// std::cerr << keyword << "\n"; +/// std::cerr << value << "\n"; +/// } +/// +/// sourcemeta::core::reference_visit(schema, +/// sourcemeta::core::schema_official_walker, +/// sourcemeta::core::official_resolver, +/// visitor); +/// ``` +SOURCEMETA_CORE_JSONSCHEMA_EXPORT +auto reference_visit( JSON &schema, const SchemaWalker &walker, const SchemaResolver &resolver, - const std::optional &default_dialect = std::nullopt) -> void; + const SchemaVisitorReference &callback, + const std::optional &default_dialect = std::nullopt, + const std::optional &default_id = std::nullopt) -> void; } // namespace sourcemeta::core diff --git a/src/core/jsonschema/jsonschema.cc b/src/core/jsonschema/jsonschema.cc index 34e2d5966..b21240814 100644 --- a/src/core/jsonschema/jsonschema.cc +++ b/src/core/jsonschema/jsonschema.cc @@ -547,3 +547,69 @@ auto sourcemeta::core::schema_format_compare( return left < right; } } + +auto sourcemeta::core::reference_visit( + sourcemeta::core::JSON &schema, + const sourcemeta::core::SchemaWalker &walker, + const sourcemeta::core::SchemaResolver &resolver, + const sourcemeta::core::SchemaVisitorReference &callback, + const std::optional &default_dialect, + const std::optional &default_id) -> void { + sourcemeta::core::SchemaFrame frame; + frame.analyse(schema, walker, resolver, default_dialect, default_id); + for (const auto &entry : frame.locations()) { + if (entry.second.type != + sourcemeta::core::SchemaFrame::LocationType::Resource && + entry.second.type != + sourcemeta::core::SchemaFrame::LocationType::Subschema) { + continue; + } + + auto &subschema{sourcemeta::core::get(schema, entry.second.pointer)}; + assert(sourcemeta::core::is_schema(subschema)); + if (!subschema.is_object()) { + continue; + } + + const sourcemeta::core::URI base{entry.second.base}; + // Assume the base is canonicalized already + assert( + sourcemeta::core::URI{entry.second.base}.canonicalize().recompose() == + base.recompose()); + for (const auto &property : subschema.as_object()) { + const auto walker_result{ + walker(property.first, frame.vocabularies(entry.second, resolver))}; + if (walker_result.type != + sourcemeta::core::SchemaKeywordType::Reference || + !property.second.is_string()) { + continue; + } + + assert(property.second.is_string()); + assert(walker_result.vocabulary.has_value()); + callback(subschema, base, walker_result.vocabulary.value(), + property.first, property.second.to_string()); + } + } +} + +auto sourcemeta::core::reference_visitor_relativize( + sourcemeta::core::JSON &subschema, const sourcemeta::core::URI &base, + const sourcemeta::core::JSON::String &vocabulary, + const sourcemeta::core::JSON::String &keyword, + const sourcemeta::core::JSON::String &value) -> void { + // In 2019-09, `$recursiveRef` can only be `#`, so there + // is nothing else we can possibly do + if (vocabulary == "https://json-schema.org/draft/2019-09/vocab/core" && + keyword == "$recursiveRef") { + return; + } + + sourcemeta::core::URI reference{value}; + reference.relative_to(base); + reference.canonicalize(); + + if (reference.is_relative()) { + subschema.assign(keyword, sourcemeta::core::JSON{reference.recompose()}); + } +} diff --git a/src/core/jsonschema/relativize.cc b/src/core/jsonschema/relativize.cc deleted file mode 100644 index 809dccaad..000000000 --- a/src/core/jsonschema/relativize.cc +++ /dev/null @@ -1,49 +0,0 @@ -#include - -namespace sourcemeta::core { - -auto relativize(JSON &schema, const SchemaWalker &walker, - const SchemaResolver &resolver, - const std::optional &default_dialect, - const std::optional &default_id) -> void { - SchemaFrame frame; - frame.analyse(schema, walker, resolver, default_dialect, default_id); - - for (const auto &entry : frame.locations()) { - if (entry.second.type != SchemaFrame::LocationType::Resource && - entry.second.type != SchemaFrame::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 != SchemaKeywordType::Reference || - !property.second.is_string()) { - continue; - } - - // In 2019-09, `$recursiveRef` can only be `#`, so there - // is nothing else we can possibly do - if (property.first == "$recursiveRef") { - continue; - } - - URI reference{property.second.to_string()}; - reference.relative_to(base); - reference.canonicalize(); - - if (reference.is_relative()) { - subschema.assign(property.first, JSON{reference.recompose()}); - } - } - } -} - -} // namespace sourcemeta::core diff --git a/src/core/jsonschema/resolver.cc b/src/core/jsonschema/resolver.cc index 4b70602c0..b3dbda716 100644 --- a/src/core/jsonschema/resolver.cc +++ b/src/core/jsonschema/resolver.cc @@ -150,9 +150,10 @@ auto SchemaFlatFileResolver::operator()(std::string_view identifier) const *this, result->second.default_dialect); // Because we allow re-identification, we can get into issues unless we // always try to relativize references - sourcemeta::core::relativize(schema, schema_official_walker, *this, - result->second.default_dialect, - result->second.original_identifier); + sourcemeta::core::reference_visit( + schema, schema_official_walker, *this, + sourcemeta::core::reference_visitor_relativize, + result->second.default_dialect, result->second.original_identifier); sourcemeta::core::reidentify(schema, result->first, *this, result->second.default_dialect); diff --git a/test/jsonschema/jsonschema_relativize_test.cc b/test/jsonschema/jsonschema_relativize_test.cc index 6c4620201..8e5752fc3 100644 --- a/test/jsonschema/jsonschema_relativize_test.cc +++ b/test/jsonschema/jsonschema_relativize_test.cc @@ -22,8 +22,10 @@ TEST(JSONSchema_relativize, draft4_1) { } })JSON"); - sourcemeta::core::relativize(schema, sourcemeta::core::schema_official_walker, - sourcemeta::core::official_resolver); + sourcemeta::core::reference_visit( + schema, sourcemeta::core::schema_official_walker, + sourcemeta::core::official_resolver, + sourcemeta::core::reference_visitor_relativize); const auto expected = sourcemeta::core::parse_json(R"JSON({ "id": "http://asyncapi.com/definitions/1.0.0/asyncapi.json", @@ -57,8 +59,10 @@ TEST(JSONSchema_relativize, draft4_2) { } })JSON"); - sourcemeta::core::relativize(schema, sourcemeta::core::schema_official_walker, - sourcemeta::core::official_resolver); + sourcemeta::core::reference_visit( + schema, sourcemeta::core::schema_official_walker, + sourcemeta::core::official_resolver, + sourcemeta::core::reference_visitor_relativize); const auto expected = sourcemeta::core::parse_json(R"JSON({ "id": "http://example.com", @@ -95,8 +99,10 @@ TEST(JSONSchema_relativize, draft4_3) { } })JSON"); - sourcemeta::core::relativize(schema, sourcemeta::core::schema_official_walker, - sourcemeta::core::official_resolver); + sourcemeta::core::reference_visit( + schema, sourcemeta::core::schema_official_walker, + sourcemeta::core::official_resolver, + sourcemeta::core::reference_visitor_relativize); const auto expected = sourcemeta::core::parse_json(R"JSON({ "id": "http://example.com", @@ -132,8 +138,10 @@ TEST(JSONSchema_relativize, draft4_4) { } })JSON"); - sourcemeta::core::relativize(schema, sourcemeta::core::schema_official_walker, - sourcemeta::core::official_resolver); + sourcemeta::core::reference_visit( + schema, sourcemeta::core::schema_official_walker, + sourcemeta::core::official_resolver, + sourcemeta::core::reference_visitor_relativize); const auto expected = sourcemeta::core::parse_json(R"JSON({ "$schema": "http://json-schema.org/draft-04/schema", @@ -168,8 +176,10 @@ TEST(JSONSchema_relativize, draft4_5) { } })JSON"); - sourcemeta::core::relativize(schema, sourcemeta::core::schema_official_walker, - sourcemeta::core::official_resolver); + sourcemeta::core::reference_visit( + schema, sourcemeta::core::schema_official_walker, + sourcemeta::core::official_resolver, + sourcemeta::core::reference_visitor_relativize); const auto expected = sourcemeta::core::parse_json(R"JSON({ "$schema": "http://json-schema.org/draft-04/schema", @@ -204,9 +214,11 @@ TEST(JSONSchema_relativize, draft4_6) { } })JSON"); - sourcemeta::core::relativize(schema, sourcemeta::core::schema_official_walker, - sourcemeta::core::official_resolver, - std::nullopt, "http://asyncapi.com/definitions"); + sourcemeta::core::reference_visit( + schema, sourcemeta::core::schema_official_walker, + sourcemeta::core::official_resolver, + sourcemeta::core::reference_visitor_relativize, std::nullopt, + "http://asyncapi.com/definitions"); const auto expected = sourcemeta::core::parse_json(R"JSON({ "$schema": "http://json-schema.org/draft-04/schema", @@ -230,9 +242,11 @@ TEST(JSONSchema_relativize, draft4_7) { } })JSON"); - sourcemeta::core::relativize(schema, sourcemeta::core::schema_official_walker, - sourcemeta::core::official_resolver, - "http://json-schema.org/draft-04/schema"); + sourcemeta::core::reference_visit( + schema, sourcemeta::core::schema_official_walker, + sourcemeta::core::official_resolver, + sourcemeta::core::reference_visitor_relativize, + "http://json-schema.org/draft-04/schema"); const auto expected = sourcemeta::core::parse_json(R"JSON({ "id": "http://asyncapi.com/definitions", @@ -256,9 +270,10 @@ TEST(JSONSchema_relativize, draft4_8) { } })JSON"); - EXPECT_THROW(sourcemeta::core::relativize( + EXPECT_THROW(sourcemeta::core::reference_visit( schema, sourcemeta::core::schema_official_walker, - sourcemeta::core::official_resolver), + sourcemeta::core::official_resolver, + sourcemeta::core::reference_visitor_relativize), sourcemeta::core::SchemaError); } @@ -273,8 +288,10 @@ TEST(JSONSchema_relativize, 2020_12_1) { } })JSON"); - sourcemeta::core::relativize(schema, sourcemeta::core::schema_official_walker, - sourcemeta::core::official_resolver); + sourcemeta::core::reference_visit( + schema, sourcemeta::core::schema_official_walker, + sourcemeta::core::official_resolver, + sourcemeta::core::reference_visitor_relativize); const auto expected = sourcemeta::core::parse_json(R"JSON({ "$id": "http://example.com", @@ -296,8 +313,10 @@ TEST(JSONSchema_relativize, 2020_12_2) { "$ref": "../../schema.json" })JSON"); - sourcemeta::core::relativize(schema, sourcemeta::core::schema_official_walker, - sourcemeta::core::official_resolver); + sourcemeta::core::reference_visit( + schema, sourcemeta::core::schema_official_walker, + sourcemeta::core::official_resolver, + sourcemeta::core::reference_visitor_relativize); const auto expected = sourcemeta::core::parse_json(R"JSON({ "$id": "https://example.com/foo/bar/baz/qux", @@ -318,8 +337,10 @@ TEST(JSONSchema_relativize, recursive_ref) { } })JSON"); - sourcemeta::core::relativize(schema, sourcemeta::core::schema_official_walker, - sourcemeta::core::official_resolver); + sourcemeta::core::reference_visit( + schema, sourcemeta::core::schema_official_walker, + sourcemeta::core::official_resolver, + sourcemeta::core::reference_visitor_relativize); const auto expected = sourcemeta::core::parse_json(R"JSON({ "$schema": "https://json-schema.org/draft/2019-09/schema",