From fd6710f4bba48f27db94e48318608a2723f74e35 Mon Sep 17 00:00:00 2001 From: Juan Cruz Viotti Date: Tue, 28 May 2024 12:58:48 +0100 Subject: [PATCH] Implement proper custom resolution support Signed-off-by: Juan Cruz Viotti --- DEPENDENCIES | 2 +- src/utils.cc | 41 +++------ vendor/jsontoolkit/Makefile.uk | 1 + .../jsontoolkit/src/jsonschema/CMakeLists.txt | 2 +- .../jsontoolkit/jsonschema_reference.h | 14 +++ .../jsontoolkit/jsonschema_resolver.h | 59 +++++++++++++ .../jsontoolkit/jsonschema_walker.h | 2 +- .../jsontoolkit/src/jsonschema/reference.cc | 35 ++++---- vendor/jsontoolkit/src/jsonschema/resolver.cc | 88 +++++++++++++++++++ 9 files changed, 196 insertions(+), 48 deletions(-) create mode 100644 vendor/jsontoolkit/src/jsonschema/resolver.cc diff --git a/DEPENDENCIES b/DEPENDENCIES index ced6821..e63ee5f 100644 --- a/DEPENDENCIES +++ b/DEPENDENCIES @@ -1,3 +1,3 @@ vendorpull https://github.com/sourcemeta/vendorpull dea311b5bfb53b6926a4140267959ae334d3ecf4 noa https://github.com/sourcemeta/noa 5ff4024902642afc9cc2f9a9e02ae9dff9d15d4f -jsontoolkit https://github.com/sourcemeta/jsontoolkit 162faf07f453c88849f8259bdfa8a6d74c2f279b +jsontoolkit https://github.com/sourcemeta/jsontoolkit dc7e1e21853f2b26cac572d5dc4c3dc50eeda935 diff --git a/src/utils.cc b/src/utils.cc index 57efb96..d6e3048 100644 --- a/src/utils.cc +++ b/src/utils.cc @@ -119,41 +119,24 @@ auto pretty_evaluate_callback( auto resolver(const std::map> &options) -> sourcemeta::jsontoolkit::SchemaResolver { - if (!options.contains("resolve") && !options.contains("r")) { - return sourcemeta::jsontoolkit::official_resolver; - } + sourcemeta::jsontoolkit::MapSchemaResolver dynamic_resolver{ + sourcemeta::jsontoolkit::official_resolver}; - std::map schemas; - const std::string option{options.contains("resolve") ? "resolve" : "r"}; - for (const auto &schema_path : options.at(option)) { - const auto schema{sourcemeta::jsontoolkit::from_file(schema_path)}; - // TODO: Use the current resolver as its building up - const auto id{sourcemeta::jsontoolkit::id( - schema, sourcemeta::jsontoolkit::official_resolver) - .get()}; - if (!id.has_value()) { - std::ostringstream error; - error << "Cannot determine the identifier of the schema: " << schema_path; - throw std::runtime_error(error.str()); + if (options.contains("resolve")) { + for (const auto &schema_path : options.at("resolve")) { + log_verbose(options) << "Loading schema: " << schema_path << "\n"; + dynamic_resolver.add(sourcemeta::jsontoolkit::from_file(schema_path)); } - - // TODO: Throw if we are overriding with a duplicate id - // TODO: We need to frame to add subschemas too? - schemas.insert({id.value(), schema}); - log_verbose(options) << "Loading schema: " << schema_path << "\n"; } - return [schemas](std::string_view identifier) - -> std::future> { - const std::string string_identifier{identifier}; - if (schemas.contains(string_identifier)) { - std::promise> promise; - promise.set_value(schemas.at(string_identifier)); - return promise.get_future(); + if (options.contains("r")) { + for (const auto &schema_path : options.at("r")) { + log_verbose(options) << "Loading schema: " << schema_path << "\n"; + dynamic_resolver.add(sourcemeta::jsontoolkit::from_file(schema_path)); } + } - return sourcemeta::jsontoolkit::official_resolver(identifier); - }; + return dynamic_resolver; } auto log_verbose(const std::map> &options) diff --git a/vendor/jsontoolkit/Makefile.uk b/vendor/jsontoolkit/Makefile.uk index 5d04f0a..a2c864f 100644 --- a/vendor/jsontoolkit/Makefile.uk +++ b/vendor/jsontoolkit/Makefile.uk @@ -53,6 +53,7 @@ LIBJSONTOOLKIT_SRCS-y += $(LIBJSONTOOLKIT_SRC)/jsonschema/reference.cc LIBJSONTOOLKIT_SRCS-y += $(LIBJSONTOOLKIT_SRC)/jsonschema/transform_rule.cc LIBJSONTOOLKIT_SRCS-y += $(LIBJSONTOOLKIT_SRC)/jsonschema/transform_bundle.cc LIBJSONTOOLKIT_SRCS-y += $(LIBJSONTOOLKIT_SRC)/jsonschema/transformer.cc +LIBJSONTOOLKIT_SRCS-y += $(LIBJSONTOOLKIT_SRC)/jsonschema/resolver.cc LIBJSONTOOLKIT_SRCS-y += $(LIBJSONTOOLKIT_SRC)/jsonschema/compile.cc LIBJSONTOOLKIT_SRCS-y += $(LIBJSONTOOLKIT_SRC)/jsonschema/compile_evaluate.cc LIBJSONTOOLKIT_SRCS-y += $(LIBJSONTOOLKIT_SRC)/jsonschema/compile_json.cc diff --git a/vendor/jsontoolkit/src/jsonschema/CMakeLists.txt b/vendor/jsontoolkit/src/jsonschema/CMakeLists.txt index f4ac8d2..3d3b159 100644 --- a/vendor/jsontoolkit/src/jsonschema/CMakeLists.txt +++ b/vendor/jsontoolkit/src/jsonschema/CMakeLists.txt @@ -6,7 +6,7 @@ noa_library(NAMESPACE sourcemeta PROJECT jsontoolkit NAME jsonschema FOLDER "JSON Toolkit/JSON Schema" PRIVATE_HEADERS anchor.h bundle.h resolver.h walker.h reference.h error.h transformer.h transform_rule.h transform_bundle.h compile.h - SOURCES jsonschema.cc default_walker.cc reference.cc anchor.cc + SOURCES jsonschema.cc default_walker.cc reference.cc anchor.cc resolver.cc walker.cc bundle.cc transformer.cc transform_rule.cc transform_bundle.cc compile.cc compile_evaluate.cc compile_json.cc compile_helpers.h default_compiler.cc diff --git a/vendor/jsontoolkit/src/jsonschema/include/sourcemeta/jsontoolkit/jsonschema_reference.h b/vendor/jsontoolkit/src/jsonschema/include/sourcemeta/jsontoolkit/jsonschema_reference.h index 3c48e30..3cbcc06 100644 --- a/vendor/jsontoolkit/src/jsonschema/include/sourcemeta/jsontoolkit/jsonschema_reference.h +++ b/vendor/jsontoolkit/src/jsonschema/include/sourcemeta/jsontoolkit/jsonschema_reference.h @@ -24,9 +24,23 @@ namespace sourcemeta::jsontoolkit { /// The reference type enum class ReferenceType { Static, Dynamic }; +#if defined(__GNUC__) +#pragma GCC diagnostic push +// GCC believes that a member of an enum class (which is namespaced by +// definition), can shadow an alias defined even on a different namespace. +#pragma GCC diagnostic ignored "-Wshadow" +#endif +/// @ingroup jsonschema +/// The reference entry type +enum class ReferenceEntryType { Resource, Anchor, Pointer }; +#if defined(__GNUC__) +#pragma GCC diagnostic pop +#endif + /// @ingroup jsonschema /// A single frame in a JSON Schema reference frame struct ReferenceFrameEntry { + const ReferenceEntryType type; const std::optional root; const std::string base; const Pointer pointer; diff --git a/vendor/jsontoolkit/src/jsonschema/include/sourcemeta/jsontoolkit/jsonschema_resolver.h b/vendor/jsontoolkit/src/jsonschema/include/sourcemeta/jsontoolkit/jsonschema_resolver.h index e3264ce..3e508c9 100644 --- a/vendor/jsontoolkit/src/jsonschema/include/sourcemeta/jsontoolkit/jsonschema_resolver.h +++ b/vendor/jsontoolkit/src/jsonschema/include/sourcemeta/jsontoolkit/jsonschema_resolver.h @@ -11,6 +11,7 @@ #include // std::function #include // std::future +#include // std::map #include // std::optional #include // std::string_view @@ -40,6 +41,64 @@ SOURCEMETA_JSONTOOLKIT_JSONSCHEMA_EXPORT auto official_resolver(std::string_view identifier) -> std::future>; +/// @ingroup jsonschema +/// This is a convenient helper for constructing schema resolvers at runtime. +/// For example: +/// +/// ```cpp +/// #include +/// #include +/// #include +/// +/// // (1) Create a map resolver that falls back to the official resolver +/// sourcemeta::jsontoolkit::MapSchemaResolver +/// resolver{sourcemeta::jsontoolkit::official_resolver}; +/// +/// const sourcemeta::jsontoolkit::JSON schema = +/// sourcemeta::jsontoolkit::parse(R"JSON({ +/// "$id": "https://www.example.com" +/// "$schema": "https://json-schema.org/draft/2020-12/schema", +/// "type": "string" +/// })JSON"); +/// +/// // (2) Register a schema +/// resolver.add(schema); +/// +/// assert(resolver("https://www.example.com").get().has_value()); +/// ``` +class SOURCEMETA_JSONTOOLKIT_JSONSCHEMA_EXPORT MapSchemaResolver { +public: + /// Construct an empty map resolver. If you don't add schemas to it, it will + /// always resolve to nothing + MapSchemaResolver(); + + /// Construct an empty map resolver that has another schema resolver as a + /// fallback + MapSchemaResolver(const SchemaResolver &resolver); + + /// Register a schema to the map resolver + auto add(const JSON &schema, + const std::optional &default_dialect = std::nullopt, + const std::optional &default_id = std::nullopt) -> void; + + /// Attempt to resolve a schema + auto operator()(std::string_view identifier) const + -> std::future>; + +private: +// Exporting symbols that depends on the standard C++ library is considered +// safe. +// https://learn.microsoft.com/en-us/cpp/error-messages/compiler-warnings/compiler-warning-level-2-c4275?view=msvc-170&redirectedfrom=MSDN +#if defined(_MSC_VER) +#pragma warning(disable : 4251) +#endif + std::map schemas; + SchemaResolver default_resolver = nullptr; +#if defined(_MSC_VER) +#pragma warning(default : 4251) +#endif +}; + } // namespace sourcemeta::jsontoolkit #endif diff --git a/vendor/jsontoolkit/src/jsonschema/include/sourcemeta/jsontoolkit/jsonschema_walker.h b/vendor/jsontoolkit/src/jsonschema/include/sourcemeta/jsontoolkit/jsonschema_walker.h index ca791e1..5ebc038 100644 --- a/vendor/jsontoolkit/src/jsonschema/include/sourcemeta/jsontoolkit/jsonschema_walker.h +++ b/vendor/jsontoolkit/src/jsonschema/include/sourcemeta/jsontoolkit/jsonschema_walker.h @@ -23,7 +23,7 @@ namespace sourcemeta::jsontoolkit { #if defined(__GNUC__) #pragma GCC diagnostic push -// For some strang reason, GCC on Debian 11 believes that a member of +// For some strange reason, GCC on Debian 11 believes that a member of // an enum class (which is namespaced by definition), can shadow an // alias defined even on a different namespace. #pragma GCC diagnostic ignored "-Wshadow" diff --git a/vendor/jsontoolkit/src/jsonschema/reference.cc b/vendor/jsontoolkit/src/jsonschema/reference.cc index 683bb15..e24d25b 100644 --- a/vendor/jsontoolkit/src/jsonschema/reference.cc +++ b/vendor/jsontoolkit/src/jsonschema/reference.cc @@ -91,6 +91,7 @@ static auto fragment_string(const sourcemeta::jsontoolkit::URI uri) static auto store(sourcemeta::jsontoolkit::ReferenceFrame &frame, const sourcemeta::jsontoolkit::ReferenceType type, + const sourcemeta::jsontoolkit::ReferenceEntryType entry_type, const std::string &uri, const std::optional &root_id, const std::string &base_id, @@ -101,8 +102,8 @@ static auto store(sourcemeta::jsontoolkit::ReferenceFrame &frame, sourcemeta::jsontoolkit::URI{uri}.canonicalize().recompose()}; if (!frame .insert({{type, canonical}, - {root_id, base_id, pointer_from_root, pointer_from_base, - dialect}}) + {entry_type, root_id, base_id, pointer_from_root, + pointer_from_base, dialect}}) .second) { std::ostringstream error; error << "Schema identifier already exists: " << uri; @@ -150,8 +151,9 @@ auto sourcemeta::jsontoolkit::frame( default_id.has_value() && root_id.value() != default_id.value()}; if (has_explicit_different_id) { - store(frame, ReferenceType::Static, default_id.value(), root_id.value(), - root_id.value(), sourcemeta::jsontoolkit::empty_pointer, + store(frame, ReferenceType::Static, ReferenceEntryType::Resource, + default_id.value(), root_id.value(), root_id.value(), + sourcemeta::jsontoolkit::empty_pointer, sourcemeta::jsontoolkit::empty_pointer, root_dialect.value()); base_uris.insert( {sourcemeta::jsontoolkit::empty_pointer, {default_id.value()}}); @@ -211,8 +213,9 @@ auto sourcemeta::jsontoolkit::frame( if (!maybe_relative_is_absolute || !frame.contains({ReferenceType::Static, new_id})) { - store(frame, ReferenceType::Static, new_id, root_id, new_id, - entry.common.pointer, sourcemeta::jsontoolkit::empty_pointer, + store(frame, ReferenceType::Static, ReferenceEntryType::Resource, + new_id, root_id, new_id, entry.common.pointer, + sourcemeta::jsontoolkit::empty_pointer, entry.common.dialect.value()); } @@ -239,16 +242,16 @@ auto sourcemeta::jsontoolkit::frame( if (type == sourcemeta::jsontoolkit::AnchorType::Static || type == sourcemeta::jsontoolkit::AnchorType::All) { - store(frame, ReferenceType::Static, relative_anchor_uri, root_id, "", - entry.common.pointer, + store(frame, ReferenceType::Static, ReferenceEntryType::Anchor, + relative_anchor_uri, root_id, "", entry.common.pointer, entry.common.pointer.resolve_from(bases.second), entry.common.dialect.value()); } if (type == sourcemeta::jsontoolkit::AnchorType::Dynamic || type == sourcemeta::jsontoolkit::AnchorType::All) { - store(frame, ReferenceType::Dynamic, relative_anchor_uri, root_id, "", - entry.common.pointer, + store(frame, ReferenceType::Dynamic, ReferenceEntryType::Anchor, + relative_anchor_uri, root_id, "", entry.common.pointer, entry.common.pointer.resolve_from(bases.second), entry.common.dialect.value()); } @@ -268,8 +271,8 @@ auto sourcemeta::jsontoolkit::frame( if (type == sourcemeta::jsontoolkit::AnchorType::Static || type == sourcemeta::jsontoolkit::AnchorType::All) { store(frame, sourcemeta::jsontoolkit::ReferenceType::Static, - absolute_anchor_uri, root_id, base_string, - entry.common.pointer, + ReferenceEntryType::Anchor, absolute_anchor_uri, root_id, + base_string, entry.common.pointer, entry.common.pointer.resolve_from(bases.second), entry.common.dialect.value()); } @@ -277,8 +280,8 @@ auto sourcemeta::jsontoolkit::frame( if (type == sourcemeta::jsontoolkit::AnchorType::Dynamic || type == sourcemeta::jsontoolkit::AnchorType::All) { store(frame, sourcemeta::jsontoolkit::ReferenceType::Dynamic, - absolute_anchor_uri, root_id, base_string, - entry.common.pointer, + ReferenceEntryType::Anchor, absolute_anchor_uri, root_id, + base_string, entry.common.pointer, entry.common.pointer.resolve_from(bases.second), entry.common.dialect.value()); } @@ -309,8 +312,8 @@ auto sourcemeta::jsontoolkit::frame( const auto nearest_bases{ find_nearest_bases(base_uris, pointer, base.first)}; assert(!nearest_bases.first.empty()); - store(frame, ReferenceType::Static, result, root_id, - nearest_bases.first.front(), pointer, + store(frame, ReferenceType::Static, ReferenceEntryType::Pointer, result, + root_id, nearest_bases.first.front(), pointer, pointer.resolve_from(nearest_bases.second), dialects.first.front()); } diff --git a/vendor/jsontoolkit/src/jsonschema/resolver.cc b/vendor/jsontoolkit/src/jsonschema/resolver.cc new file mode 100644 index 0000000..990e32a --- /dev/null +++ b/vendor/jsontoolkit/src/jsonschema/resolver.cc @@ -0,0 +1,88 @@ +#include +#include + +#include // assert +#include // std::ostringstream + +namespace sourcemeta::jsontoolkit { + +MapSchemaResolver::MapSchemaResolver() {} + +MapSchemaResolver::MapSchemaResolver(const SchemaResolver &resolver) + : default_resolver{resolver} {} + +auto MapSchemaResolver::add( + const JSON &schema, const std::optional &default_dialect, + const std::optional &default_id) -> void { + assert(sourcemeta::jsontoolkit::is_schema(schema)); + + // Registering the top-level schema is not enough. We need to check + // and register every embedded schema resource too + ReferenceFrame entries; + ReferenceMap references; + frame(schema, entries, references, default_schema_walker, *this, + default_dialect, default_id) + .wait(); + + for (const auto &[key, entry] : entries) { + if (entry.type != ReferenceEntryType::Resource) { + continue; + } + + auto subschema{get(schema, entry.pointer)}; + // TODO: Set the base dialect in the frame entries + const auto subschema_base_dialect{ + base_dialect(subschema, *this, entry.dialect).get()}; + assert(subschema_base_dialect.has_value()); + const auto subschema_vocabularies{ + vocabularies(*this, subschema_base_dialect.value(), entry.dialect) + .get()}; + + // Given we might be resolving embedded resources, we fully + // resolve their dialect and identifiers, otherwise the + // consumer might have no idea what to do with them + subschema.assign("$schema", JSON{entry.dialect}); + // TODO: De-duplicate this id-set functionality from bundle.cc too + if (subschema_vocabularies.contains( + "http://json-schema.org/draft-04/schema#") || + subschema_vocabularies.contains( + "http://json-schema.org/draft-03/schema#") || + subschema_vocabularies.contains( + "http://json-schema.org/draft-02/schema#") || + subschema_vocabularies.contains( + "http://json-schema.org/draft-01/schema#") || + subschema_vocabularies.contains( + "http://json-schema.org/draft-00/schema#")) { + subschema.assign("id", JSON{key.second}); + } else { + subschema.assign("$id", JSON{key.second}); + } + + const auto result{this->schemas.emplace(key.second, subschema)}; + if (!result.second && result.first->second != schema) { + std::ostringstream error; + error << "Cannot register the same identifier twice: " << key.second; + throw SchemaError(error.str()); + } + } +} + +auto MapSchemaResolver::operator()(std::string_view identifier) const + -> std::future> { + const std::string string_identifier{identifier}; + if (this->schemas.contains(string_identifier)) { + std::promise> promise; + promise.set_value(this->schemas.at(string_identifier)); + return promise.get_future(); + } + + if (this->default_resolver) { + return this->default_resolver(identifier); + } + + std::promise> promise; + promise.set_value(std::nullopt); + return promise.get_future(); +} + +} // namespace sourcemeta::jsontoolkit