Skip to content

Commit

Permalink
Generalize relativize() as reference_visit()
Browse files Browse the repository at this point in the history
Signed-off-by: Juan Cruz Viotti <[email protected]>
  • Loading branch information
jviotti committed Jan 29, 2025
1 parent 1257bcd commit ecaef83
Show file tree
Hide file tree
Showing 6 changed files with 193 additions and 97 deletions.
2 changes: 1 addition & 1 deletion src/core/jsonschema/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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")

Expand Down
97 changes: 77 additions & 20 deletions src/core/jsonschema/include/sourcemeta/core/jsonschema.h
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,10 @@
#include <sourcemeta/core/jsonschema_unevaluated.h>
#include <sourcemeta/core/jsonschema_walker.h>

#include <map> // std::map
#include <optional> // std::optional
#include <string> // std::string
#include <functional> // std::function
#include <map> // std::map
#include <optional> // std::optional
#include <string> // std::string

/// @defgroup jsonschema JSON Schema
/// @brief A set of JSON Schema utilities across draft versions.
Expand Down Expand Up @@ -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 <sourcemeta/core/json.h>
Expand All @@ -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<std::string> &default_dialect = std::nullopt,
const std::optional<std::string> &default_id = std::nullopt) -> void;
const std::optional<std::string> &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<void(JSON &, const URI &, const JSON::String &,
const JSON::String &, const JSON::String &)>;

/// @ingroup jsonschema
///
/// A reference visitor to try to turn every possible absolute reference in a
/// schema into a relative one. For example:
///
/// ```cpp
/// #include <sourcemeta/core/json.h>
Expand All @@ -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 <sourcemeta/core/json.h>
/// #include <sourcemeta/core/jsonschema.h>
///
/// 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<std::string> &default_dialect = std::nullopt) -> void;
const SchemaVisitorReference &callback,
const std::optional<std::string> &default_dialect = std::nullopt,
const std::optional<std::string> &default_id = std::nullopt) -> void;

} // namespace sourcemeta::core

Expand Down
66 changes: 66 additions & 0 deletions src/core/jsonschema/jsonschema.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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<std::string> &default_dialect,
const std::optional<std::string> &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()});
}
}
49 changes: 0 additions & 49 deletions src/core/jsonschema/relativize.cc

This file was deleted.

7 changes: 4 additions & 3 deletions src/core/jsonschema/resolver.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down
Loading

0 comments on commit ecaef83

Please sign in to comment.