From 5867c647727c3398e622a87a55d9fd0b72900478 Mon Sep 17 00:00:00 2001 From: Juan Cruz Viotti Date: Fri, 25 Jul 2025 11:42:01 -0400 Subject: [PATCH] Upgrade Core to `9d5e6374e3c74d605144c611b757c81a9108ba4f` Signed-off-by: Juan Cruz Viotti --- DEPENDENCIES | 2 +- vendor/core/src/core/gzip/CMakeLists.txt | 4 +- vendor/core/src/core/gzip/gzip.cc | 137 +++++++++--- .../core/gzip/include/sourcemeta/core/gzip.h | 100 ++++++++- .../gzip/include/sourcemeta/core/gzip_error.h | 40 ++++ .../json/include/sourcemeta/core/json_hash.h | 11 +- .../include/sourcemeta/core/json_object.h | 3 +- .../core/src/core/jsonschema/CMakeLists.txt | 2 +- vendor/core/src/core/jsonschema/bundle.cc | 153 +++++++++++--- .../include/sourcemeta/core/jsonschema.h | 132 +----------- .../sourcemeta/core/jsonschema_bundle.h | 195 ++++++++++++++++++ .../sourcemeta/core/jsonschema_transform.h | 4 +- .../core/src/core/jsonschema/transformer.cc | 37 +++- .../uri/include/sourcemeta/core/uri_error.h | 2 +- .../src/extension/alterschema/CMakeLists.txt | 4 +- .../src/extension/alterschema/alterschema.cc | 4 + .../linter/property_names_default.h | 32 +++ .../linter/property_names_type_default.h | 41 ++++ .../linter/unnecessary_allof_wrapper_draft.h | 11 + .../linter/unnecessary_allof_wrapper_modern.h | 18 ++ 20 files changed, 714 insertions(+), 218 deletions(-) create mode 100644 vendor/core/src/core/gzip/include/sourcemeta/core/gzip_error.h create mode 100644 vendor/core/src/core/jsonschema/include/sourcemeta/core/jsonschema_bundle.h create mode 100644 vendor/core/src/extension/alterschema/linter/property_names_default.h create mode 100644 vendor/core/src/extension/alterschema/linter/property_names_type_default.h diff --git a/DEPENDENCIES b/DEPENDENCIES index d1858788..1f6879fa 100644 --- a/DEPENDENCIES +++ b/DEPENDENCIES @@ -1,3 +1,3 @@ vendorpull https://github.com/sourcemeta/vendorpull 70342aaf458e6cb80baeb5b718901075fc42ede6 -core https://github.com/sourcemeta/core e1cd255bdd8a0cff32e237b95070548f203edbe7 +core https://github.com/sourcemeta/core 9d5e6374e3c74d605144c611b757c81a9108ba4f bootstrap https://github.com/twbs/bootstrap 1a6fdfae6be09b09eaced8f0e442ca6f7680a61e diff --git a/vendor/core/src/core/gzip/CMakeLists.txt b/vendor/core/src/core/gzip/CMakeLists.txt index 1613eca1..684204ff 100644 --- a/vendor/core/src/core/gzip/CMakeLists.txt +++ b/vendor/core/src/core/gzip/CMakeLists.txt @@ -1,4 +1,6 @@ -sourcemeta_library(NAMESPACE sourcemeta PROJECT core NAME gzip SOURCES gzip.cc) +sourcemeta_library(NAMESPACE sourcemeta PROJECT core NAME gzip + PRIVATE_HEADERS error.h + SOURCES gzip.cc) if(SOURCEMETA_CORE_INSTALL) sourcemeta_library_install(NAMESPACE sourcemeta PROJECT core NAME gzip) diff --git a/vendor/core/src/core/gzip/gzip.cc b/vendor/core/src/core/gzip/gzip.cc index e0f30b5d..050f9d27 100644 --- a/vendor/core/src/core/gzip/gzip.cc +++ b/vendor/core/src/core/gzip/gzip.cc @@ -5,44 +5,129 @@ extern "C" { } #include // std::array -#include // std::memset -#include // std::ostringstream +#include // std::istringstream, std::ostringstream +#include // std::move namespace sourcemeta::core { -auto gzip(std::string_view input) -> std::optional { - z_stream stream; - std::memset(&stream, 0, sizeof(stream)); - int code = deflateInit2(&stream, Z_DEFAULT_COMPRESSION, Z_DEFLATED, - 16 + MAX_WBITS, 8, Z_DEFAULT_STRATEGY); - if (code != Z_OK) { - return std::nullopt; +constexpr auto ZLIB_BUFFER_SIZE{4096}; + +auto gzip(std::istream &input, std::ostream &output) -> void { + z_stream zstream{}; + if (deflateInit2(&zstream, Z_DEFAULT_COMPRESSION, Z_DEFLATED, 16 + MAX_WBITS, + 8, Z_DEFAULT_STRATEGY) != Z_OK) { + throw GZIPError{"Could not compress input"}; } - stream.next_in = reinterpret_cast(const_cast(input.data())); - stream.avail_in = static_cast(input.size()); + std::array buffer_input; + std::array buffer_output; + bool reached_end_of_input{false}; + auto code{Z_OK}; - std::array buffer; - std::ostringstream compressed; + while (code != Z_STREAM_END) { + if (zstream.avail_in == 0 && !reached_end_of_input) { + input.read(buffer_input.data(), buffer_input.size()); + const auto bytes_read = input.gcount(); + if (bytes_read > 0) { + zstream.next_in = reinterpret_cast(buffer_input.data()); + zstream.avail_in = static_cast(bytes_read); + } else { + reached_end_of_input = true; + } + } - do { - stream.next_out = reinterpret_cast(buffer.data()); - stream.avail_out = sizeof(buffer); - code = deflate(&stream, Z_FINISH); - compressed.write(buffer.data(), - static_cast(sizeof(buffer)) - stream.avail_out); - } while (code == Z_OK); + zstream.next_out = reinterpret_cast(buffer_output.data()); + zstream.avail_out = static_cast(buffer_output.size()); - if (code != Z_STREAM_END) { - return std::nullopt; + const int flush_mode = reached_end_of_input ? Z_FINISH : Z_NO_FLUSH; + code = deflate(&zstream, flush_mode); + if (code == Z_STREAM_ERROR) { + deflateEnd(&zstream); + throw GZIPError{"Could not compress input"}; + } + + const auto bytes_written = buffer_output.size() - zstream.avail_out; + if (bytes_written > 0) { + output.write(buffer_output.data(), + static_cast(bytes_written)); + if (!output) { + deflateEnd(&zstream); + throw GZIPError{"Could not compress input"}; + } + } } - code = deflateEnd(&stream); - if (code != Z_OK) { - return std::nullopt; + if (deflateEnd(&zstream) != Z_OK) { + throw GZIPError{"Could not compress input"}; + } +} + +auto gunzip(std::istream &input, std::ostream &output) -> void { + z_stream zstream{}; + if (inflateInit2(&zstream, 16 + MAX_WBITS) != Z_OK) { + throw GZIPError("Could not decompress input"); + } + + std::array buffer_input; + std::array buffer_output; + + auto code{Z_OK}; + while (code != Z_STREAM_END) { + if (zstream.avail_in == 0 && input) { + input.read(buffer_input.data(), buffer_input.size()); + const auto bytes_read = input.gcount(); + if (bytes_read > 0) { + zstream.next_in = reinterpret_cast(buffer_input.data()); + zstream.avail_in = static_cast(bytes_read); + } else { + break; + } + } + + zstream.next_out = reinterpret_cast(buffer_output.data()); + zstream.avail_out = static_cast(buffer_output.size()); + + code = inflate(&zstream, Z_NO_FLUSH); + if (code == Z_NEED_DICT || code == Z_DATA_ERROR || code == Z_MEM_ERROR) { + inflateEnd(&zstream); + throw GZIPError("Could not decompress input"); + } else { + const auto bytes_written = buffer_output.size() - zstream.avail_out; + output.write(buffer_output.data(), + static_cast(bytes_written)); + if (!output) { + inflateEnd(&zstream); + throw GZIPError("Could not decompress input"); + } + } } - return compressed.str(); + inflateEnd(&zstream); + if (code != Z_STREAM_END) { + throw GZIPError("Could not decompress input"); + } +} + +auto gzip(std::istream &stream) -> std::string { + std::ostringstream output; + gzip(stream, output); + return output.str(); +} + +auto gzip(std::string input) -> std::string { + std::istringstream stream{std::move(input)}; + return gzip(stream); +} + +auto gunzip(std::istream &stream) -> std::string { + std::ostringstream output; + gunzip(stream, output); + return output.str(); +} + +auto gunzip(std::string input) -> std::string { + std::istringstream stream{std::move(input)}; + return gunzip(stream); } } // namespace sourcemeta::core diff --git a/vendor/core/src/core/gzip/include/sourcemeta/core/gzip.h b/vendor/core/src/core/gzip/include/sourcemeta/core/gzip.h index 977c964c..cfbe10df 100644 --- a/vendor/core/src/core/gzip/include/sourcemeta/core/gzip.h +++ b/vendor/core/src/core/gzip/include/sourcemeta/core/gzip.h @@ -5,12 +5,17 @@ #include #endif -#include // std::optional +// NOLINTBEGIN(misc-include-cleaner) +#include +// NOLINTEND(misc-include-cleaner) + +#include // std::istream +#include // std::ostream #include // std::string #include // std::string_view /// @defgroup gzip GZIP -/// @brief A growing implementation of RFC 1952 GZIP. +/// @brief An implementation of RFC 1952 GZIP. /// /// This functionality is included as follows: /// @@ -22,19 +27,98 @@ namespace sourcemeta::core { /// @ingroup gzip /// -/// Compress an input string into a sequence of bytes represented using a -/// string. For example: +/// Compress an input stream into an output stream. For example: +/// +/// ```cpp +/// #include +/// #include +/// #include +/// +/// std::istringstream input{"Hello World"}; +/// std::ostringstream output; +/// sourcemeta::core::gzip(input, output); +/// assert(!output.str().empty()); +/// ``` +SOURCEMETA_CORE_GZIP_EXPORT auto gzip(std::istream &input, std::ostream &output) + -> void; + +/// @ingroup gzip +/// +/// A convenience function to compress an input stream into a sequence of +/// bytes represented using a string. For example: +/// +/// ```cpp +/// #include +/// #include +/// #include +/// +/// std::istringstream stream{"Hello World"}; +/// const auto result{sourcemeta::core::gzip(stream)}; +/// assert(result == "Hello World"); +/// ``` +SOURCEMETA_CORE_GZIP_EXPORT auto gzip(std::istream &stream) -> std::string; + +/// @ingroup gzip +/// +/// A convenience function to compress an input string into a sequence of bytes +/// represented using a string. For example: /// /// ```cpp /// #include /// #include /// /// const auto result{sourcemeta::core::gzip("Hello World")}; -/// assert(result.has_value()); -/// assert(!result.value().empty()); +/// assert(!result.empty()); +/// ``` +SOURCEMETA_CORE_GZIP_EXPORT auto gzip(std::string input) -> std::string; + +/// @ingroup gzip +/// +/// Decompress an input stream into an output stream. For example: +/// +/// ```cpp +/// #include +/// #include +/// #include +/// +/// std::istringstream input{sourcemeta::core::gzip("Hello World")}; +/// std::ostringstream output; +/// sourcemeta::core::gunzip(input, output); +/// assert(output.str() == "Hello World"); +/// ``` +SOURCEMETA_CORE_GZIP_EXPORT auto gunzip(std::istream &input, + std::ostream &output) -> void; + +/// @ingroup gzip +/// +/// A convenience function to decompress an input stream into a sequence of +/// bytes represented using a string. For example: +/// +/// ```cpp +/// #include +/// #include +/// #include +/// +/// const auto input{sourcemeta::core::gzip("Hello World")}; +/// std::istringstream stream{input}; +/// const auto result{sourcemeta::core::gunzip(stream)}; +/// assert(result == "Hello World"); +/// ``` +SOURCEMETA_CORE_GZIP_EXPORT auto gunzip(std::istream &stream) -> std::string; + +/// @ingroup gzip +/// +/// A convenience function to decompress an input string into a sequence of +/// bytes represented using a string. For example: +/// +/// ```cpp +/// #include +/// #include +/// +/// const auto result{sourcemeta::core::gunzip("Hello World")}; +/// assert(result == "Hello World"); /// ``` -SOURCEMETA_CORE_GZIP_EXPORT auto gzip(std::string_view input) - -> std::optional; +SOURCEMETA_CORE_GZIP_EXPORT auto gunzip(std::string input) -> std::string; } // namespace sourcemeta::core diff --git a/vendor/core/src/core/gzip/include/sourcemeta/core/gzip_error.h b/vendor/core/src/core/gzip/include/sourcemeta/core/gzip_error.h new file mode 100644 index 00000000..9e99d10e --- /dev/null +++ b/vendor/core/src/core/gzip/include/sourcemeta/core/gzip_error.h @@ -0,0 +1,40 @@ +#ifndef SOURCEMETA_CORE_GZIP_ERROR_H_ +#define SOURCEMETA_CORE_GZIP_ERROR_H_ + +#ifndef SOURCEMETA_CORE_GZIP_EXPORT +#include +#endif + +#include // std::exception +#include // std::string +#include // std::move + +namespace sourcemeta::core { + +// 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 4275) +#endif + +/// @ingroup gzip +/// An error that represents a general GZIP error event +class SOURCEMETA_CORE_GZIP_EXPORT GZIPError : public std::exception { +public: + GZIPError(std::string message) : message_{std::move(message)} {} + [[nodiscard]] auto what() const noexcept -> const char * override { + return this->message_.c_str(); + } + +private: + std::string message_; +}; + +#if defined(_MSC_VER) +#pragma warning(default : 4251 4275) +#endif + +} // namespace sourcemeta::core + +#endif diff --git a/vendor/core/src/core/json/include/sourcemeta/core/json_hash.h b/vendor/core/src/core/json/include/sourcemeta/core/json_hash.h index 10a6139b..5818b842 100644 --- a/vendor/core/src/core/json/include/sourcemeta/core/json_hash.h +++ b/vendor/core/src/core/json/include/sourcemeta/core/json_hash.h @@ -51,7 +51,6 @@ template struct PropertyHashJSON { -> hash_type { hash_type result; assert(!value.empty()); - assert(value.size() <= 31); // Copy starting a byte 2 std::memcpy(reinterpret_cast(&result) + 1, value.data(), size); return result; @@ -128,11 +127,13 @@ template struct PropertyHashJSON { // This case is specifically designed to be constant with regards to // string length, and to exploit the fact that most JSON objects don't // have a lot of entries, so hash collision is not as common - return {1 + - (size + static_cast(value.front()) + + auto hash = this->perfect(value, 31); + hash.a |= + 1 + (size + static_cast(value.front()) + static_cast(value.back())) % // Make sure the property hash can never exceed 8 bits - 255}; + 255; + return hash; } } @@ -140,7 +141,7 @@ template struct PropertyHashJSON { inline auto is_perfect(const hash_type &hash) const noexcept -> bool { // If there is anything written past the first byte, // then it is a perfect hash - return hash.a > 255; + return (hash.a & 255) == 0; } }; diff --git a/vendor/core/src/core/json/include/sourcemeta/core/json_object.h b/vendor/core/src/core/json/include/sourcemeta/core/json_object.h index 8815b779..eeaa11cb 100644 --- a/vendor/core/src/core/json/include/sourcemeta/core/json_object.h +++ b/vendor/core/src/core/json/include/sourcemeta/core/json_object.h @@ -183,8 +183,7 @@ template class JSONObject { [[nodiscard]] inline auto empty() const -> bool { return this->data.empty(); } /// Access an object entry by its underlying positional index - [[nodiscard]] inline auto at(const size_type index) const noexcept - -> const Entry & { + [[nodiscard]] inline auto at(const size_type index) const -> const Entry & { return this->data.at(index); } diff --git a/vendor/core/src/core/jsonschema/CMakeLists.txt b/vendor/core/src/core/jsonschema/CMakeLists.txt index a9161376..4cd587ba 100644 --- a/vendor/core/src/core/jsonschema/CMakeLists.txt +++ b/vendor/core/src/core/jsonschema/CMakeLists.txt @@ -3,7 +3,7 @@ set(OFFICIAL_RESOLVER_OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/official_resolver.cc") include(./official_resolver.cmake) sourcemeta_library(NAMESPACE sourcemeta PROJECT core NAME jsonschema - PRIVATE_HEADERS resolver.h walker.h frame.h error.h types.h transform.h + PRIVATE_HEADERS bundle.h resolver.h walker.h frame.h error.h types.h transform.h SOURCES jsonschema.cc official_walker.cc frame.cc resolver.cc walker.cc bundle.cc transformer.cc "${CMAKE_CURRENT_BINARY_DIR}/official_resolver.cc") diff --git a/vendor/core/src/core/jsonschema/bundle.cc b/vendor/core/src/core/jsonschema/bundle.cc index f951c193..0b58f658 100644 --- a/vendor/core/src/core/jsonschema/bundle.cc +++ b/vendor/core/src/core/jsonschema/bundle.cc @@ -1,15 +1,110 @@ #include -#include // assert -#include // std::ostringstream -#include // std::move +#include // assert +#include // std::reference_wrapper +#include // std::ostringstream +#include // std::tuple +#include // std::unordered_set +#include // std::move +#include // std::vector namespace { +auto is_official_metaschema_reference(const sourcemeta::core::Pointer &pointer, + const std::string &destination) -> bool { + assert(!pointer.empty()); + assert(pointer.back().is_property()); + return pointer.back().to_property() == "$schema" && + sourcemeta::core::schema_official_resolver(destination).has_value(); +} + +auto dependencies_internal( + const sourcemeta::core::JSON &schema, + const sourcemeta::core::SchemaWalker &walker, + const sourcemeta::core::SchemaResolver &resolver, + const sourcemeta::core::DependencyCallback &callback, + const std::optional &default_dialect, + const std::optional &default_id, + const sourcemeta::core::SchemaFrame::Paths &paths, + std::unordered_set &visited) -> void { + sourcemeta::core::SchemaFrame frame{ + sourcemeta::core::SchemaFrame::Mode::References}; + frame.analyse(schema, walker, resolver, default_dialect, default_id, paths); + const auto origin{sourcemeta::core::identify( + schema, resolver, sourcemeta::core::SchemaIdentificationStrategy::Strict, + default_dialect, default_id)}; + + std::vector< + std::tuple>> + found; + + for (const auto &[key, reference] : frame.references()) { + if (frame.traverse(reference.destination).has_value() || + + // We don't want to report official schemas, as we can expect + // virtually all implementations to understand them out of the box + is_official_metaschema_reference(key.second, reference.destination)) { + continue; + } + + if (!reference.base.has_value()) { + throw sourcemeta::core::SchemaReferenceError( + reference.destination, key.second, + "Could not resolve schema reference"); + } + + // To not infinitely loop on circular references + if (visited.contains(reference.base.value())) { + continue; + } + + // If we can't find the destination but there is a base and we can + // find the base, then we are facing an unresolved fragment + if (frame.traverse(reference.base.value()).has_value()) { + throw sourcemeta::core::SchemaReferenceError( + reference.destination, key.second, + "Could not resolve schema reference"); + } + + assert(reference.base.has_value()); + const auto &identifier{reference.base.value()}; + auto remote{resolver(identifier)}; + if (!remote.has_value()) { + throw sourcemeta::core::SchemaResolutionError( + identifier, "Could not resolve the reference to an external schema"); + } + + if (!sourcemeta::core::is_schema(remote.value())) { + throw sourcemeta::core::SchemaReferenceError( + identifier, key.second, + "The JSON document is not a valid JSON Schema"); + } + + const auto base_dialect{sourcemeta::core::base_dialect( + remote.value(), resolver, default_dialect)}; + if (!base_dialect.has_value()) { + throw sourcemeta::core::SchemaReferenceError( + identifier, key.second, + "The JSON document is not a valid JSON Schema"); + } + + callback(origin, key.second, identifier, remote.value()); + found.emplace_back(std::move(remote).value(), identifier); + visited.emplace(identifier); + } + + for (const auto &entry : found) { + dependencies_internal(std::get<0>(entry), walker, resolver, callback, + default_dialect, std::get<1>(entry).get(), + {sourcemeta::core::empty_pointer}, visited); + } +} + auto embed_schema(sourcemeta::core::JSON &root, const sourcemeta::core::Pointer &container, const std::string &identifier, - sourcemeta::core::JSON &&target) -> std::string { + sourcemeta::core::JSON &&target) -> void { auto *current{&root}; for (const auto &token : container) { if (token.is_property()) { @@ -35,15 +130,6 @@ auto embed_schema(sourcemeta::core::JSON &root, } current->assign(key.str(), std::move(target)); - return key.str(); -} - -auto is_official_metaschema_reference(const sourcemeta::core::Pointer &pointer, - const std::string &destination) -> bool { - assert(!pointer.empty()); - assert(pointer.back().is_property()); - return pointer.back().to_property() == "$schema" && - sourcemeta::core::schema_official_resolver(destination).has_value(); } auto bundle_schema(sourcemeta::core::JSON &root, @@ -55,7 +141,6 @@ auto bundle_schema(sourcemeta::core::JSON &root, const std::optional &default_dialect, const std::optional &default_id, const sourcemeta::core::SchemaFrame::Paths &paths, - const sourcemeta::core::BundleCallback &callback, const std::size_t depth = 0) -> void { // Keep in mind that the resulting frame does miss some information. For // example, when we recurse to framing embedded schemas, we will frame them @@ -135,17 +220,8 @@ auto bundle_schema(sourcemeta::core::JSON &root, } bundle_schema(root, container, remote.value(), frame, walker, resolver, - default_dialect, identifier, paths, callback, depth + 1); - auto embed_key{ - embed_schema(root, container, identifier, std::move(remote).value())}; - - if (callback) { - const auto origin{sourcemeta::core::identify( - subschema, resolver, - sourcemeta::core::SchemaIdentificationStrategy::Strict, - default_dialect, default_id)}; - callback(origin, key.second, identifier, container.concat({embed_key})); - } + default_dialect, identifier, paths, depth + 1); + embed_schema(root, container, identifier, std::move(remote).value()); } } @@ -153,20 +229,32 @@ auto bundle_schema(sourcemeta::core::JSON &root, namespace sourcemeta::core { +auto dependencies(const JSON &schema, const SchemaWalker &walker, + const SchemaResolver &resolver, + const DependencyCallback &callback, + const std::optional &default_dialect, + const std::optional &default_id, + const SchemaFrame::Paths &paths) -> void { + std::unordered_set visited; + dependencies_internal(schema, walker, resolver, callback, default_dialect, + default_id, paths, visited); +} + +// TODO: Refactor this function to internally rely on the `.dependencies()` +// function auto bundle(JSON &schema, const SchemaWalker &walker, const SchemaResolver &resolver, const std::optional &default_dialect, const std::optional &default_id, const std::optional &default_container, - const SchemaFrame::Paths &paths, const BundleCallback &callback) - -> void { + const SchemaFrame::Paths &paths) -> void { SchemaFrame frame{SchemaFrame::Mode::References}; if (default_container.has_value()) { // This is undefined behavior assert(!default_container.value().empty()); bundle_schema(schema, default_container.value(), schema, frame, walker, - resolver, default_dialect, default_id, paths, callback); + resolver, default_dialect, default_id, paths); return; } @@ -177,7 +265,7 @@ auto bundle(JSON &schema, const SchemaWalker &walker, vocabularies.contains( "https://json-schema.org/draft/2019-09/vocab/core")) { bundle_schema(schema, {"$defs"}, schema, frame, walker, resolver, - default_dialect, default_id, paths, callback); + default_dialect, default_id, paths); return; } else if (vocabularies.contains("http://json-schema.org/draft-07/schema#") || vocabularies.contains( @@ -189,7 +277,7 @@ auto bundle(JSON &schema, const SchemaWalker &walker, vocabularies.contains( "http://json-schema.org/draft-04/hyper-schema#")) { bundle_schema(schema, {"definitions"}, schema, frame, walker, resolver, - default_dialect, default_id, paths, callback); + default_dialect, default_id, paths); return; } else if (vocabularies.contains( "http://json-schema.org/draft-03/hyper-schema#") || @@ -220,11 +308,10 @@ auto bundle(const JSON &schema, const SchemaWalker &walker, const std::optional &default_dialect, const std::optional &default_id, const std::optional &default_container, - const SchemaFrame::Paths &paths, const BundleCallback &callback) - -> JSON { + const SchemaFrame::Paths &paths) -> JSON { JSON copy = schema; bundle(copy, walker, resolver, default_dialect, default_id, default_container, - paths, callback); + paths); return copy; } diff --git a/vendor/core/src/core/jsonschema/include/sourcemeta/core/jsonschema.h b/vendor/core/src/core/jsonschema/include/sourcemeta/core/jsonschema.h index 1c8d96e8..6f0252ed 100644 --- a/vendor/core/src/core/jsonschema/include/sourcemeta/core/jsonschema.h +++ b/vendor/core/src/core/jsonschema/include/sourcemeta/core/jsonschema.h @@ -9,6 +9,7 @@ #include // NOLINTBEGIN(misc-include-cleaner) +#include #include #include #include @@ -18,7 +19,6 @@ // NOLINTEND(misc-include-cleaner) #include // std::uint8_t -#include // std::function #include // std::optional, std::nullopt #include // std::set #include // std::string @@ -505,136 +505,6 @@ auto reference_visit( const std::optional &default_dialect = std::nullopt, const std::optional &default_id = std::nullopt) -> void; -// TODO: Optionally let users bundle the metaschema too - -/// @ingroup jsonschema -/// A callback to get runtime reporting out of schema bundling: -/// - Origin URI -/// - Pointer (reference keyword from the origin) -/// - Target URI -/// - Embed pointer -using BundleCallback = - std::function &, const Pointer &, - const JSON::String &, const Pointer &)>; - -/// @ingroup jsonschema -/// -/// This function bundles a JSON Schema (starting from Draft 4) by embedding -/// every remote reference into the top level schema resource, handling circular -/// dependencies and more. This overload mutates the input schema. For example: -/// -/// ```cpp -/// #include -/// #include -/// #include -/// -/// // A custom resolver that knows about an additional schema -/// static auto test_resolver(std::string_view identifier) -/// -> std::optional { -/// if (identifier == "https://www.example.com/test") { -/// return sourcemeta::core::parse_json(R"JSON({ -/// "$id": "https://www.example.com/test", -/// "$schema": "https://json-schema.org/draft/2020-12/schema", -/// "type": "string" -/// })JSON"); -/// } else { -/// return sourcemeta::core::schema_official_resolver(identifier); -/// } -/// } -/// -/// sourcemeta::core::JSON document = -/// sourcemeta::core::parse_json(R"JSON({ -/// "$schema": "https://json-schema.org/draft/2020-12/schema", -/// "items": { "$ref": "https://www.example.com/test" } -/// })JSON"); -/// -/// sourcemeta::core::bundle(document, -/// sourcemeta::core::schema_official_walker, test_resolver); -/// -/// const sourcemeta::core::JSON expected = -/// sourcemeta::core::parse_json(R"JSON({ -/// "$schema": "https://json-schema.org/draft/2020-12/schema", -/// "items": { "$ref": "https://www.example.com/test" }, -/// "$defs": { -/// "https://www.example.com/test": { -/// "$id": "https://www.example.com/test", -/// "$schema": "https://json-schema.org/draft/2020-12/schema", -/// "type": "string" -/// } -/// } -/// })JSON"); -/// -/// assert(document == expected); -/// ``` -SOURCEMETA_CORE_JSONSCHEMA_EXPORT -auto bundle(JSON &schema, const SchemaWalker &walker, - const SchemaResolver &resolver, - const std::optional &default_dialect = std::nullopt, - const std::optional &default_id = std::nullopt, - const std::optional &default_container = std::nullopt, - const SchemaFrame::Paths &paths = {empty_pointer}, - const BundleCallback &callback = nullptr) -> void; - -/// @ingroup jsonschema -/// -/// This function bundles a JSON Schema (starting from Draft 4) by embedding -/// every remote reference into the top level schema resource, handling circular -/// dependencies and more. This overload returns a new schema, without mutating -/// the input schema. For example: -/// -/// ```cpp -/// #include -/// #include -/// #include -/// -/// // A custom resolver that knows about an additional schema -/// static auto test_resolver(std::string_view identifier) -/// -> std::optional { -/// if (identifier == "https://www.example.com/test") { -/// return sourcemeta::core::parse_json(R"JSON({ -/// "$id": "https://www.example.com/test", -/// "$schema": "https://json-schema.org/draft/2020-12/schema", -/// "type": "string" -/// })JSON"); -/// } else { -/// return sourcemeta::core::schema_official_resolver(identifier); -/// } -/// } -/// -/// const sourcemeta::core::JSON document = -/// sourcemeta::core::parse_json(R"JSON({ -/// "$schema": "https://json-schema.org/draft/2020-12/schema", -/// "items": { "$ref": "https://www.example.com/test" } -/// })JSON"); -/// -/// const sourcemeta::core::JSON result = -/// sourcemeta::core::bundle(document, -/// sourcemeta::core::schema_official_walker, test_resolver); -/// -/// const sourcemeta::core::JSON expected = -/// sourcemeta::core::parse_json(R"JSON({ -/// "$schema": "https://json-schema.org/draft/2020-12/schema", -/// "items": { "$ref": "https://www.example.com/test" }, -/// "$defs": { -/// "https://www.example.com/test": { -/// "$id": "https://www.example.com/test", -/// "$schema": "https://json-schema.org/draft/2020-12/schema", -/// "type": "string" -/// } -/// } -/// })JSON"); -/// -/// assert(result == expected); -/// ``` -SOURCEMETA_CORE_JSONSCHEMA_EXPORT -auto bundle(const JSON &schema, const SchemaWalker &walker, - const SchemaResolver &resolver, - const std::optional &default_dialect = std::nullopt, - const std::optional &default_id = std::nullopt, - const std::optional &default_container = std::nullopt, - const SchemaFrame::Paths &paths = {empty_pointer}, - const BundleCallback &callback = nullptr) -> JSON; - /// @ingroup jsonschema /// /// Given a schema identifier, this function creates a JSON Schema wrapper that diff --git a/vendor/core/src/core/jsonschema/include/sourcemeta/core/jsonschema_bundle.h b/vendor/core/src/core/jsonschema/include/sourcemeta/core/jsonschema_bundle.h new file mode 100644 index 00000000..797f7ccb --- /dev/null +++ b/vendor/core/src/core/jsonschema/include/sourcemeta/core/jsonschema_bundle.h @@ -0,0 +1,195 @@ +#ifndef SOURCEMETA_CORE_JSONSCHEMA_BUNDLE_H +#define SOURCEMETA_CORE_JSONSCHEMA_BUNDLE_H + +#ifndef SOURCEMETA_CORE_JSONSCHEMA_EXPORT +#include +#endif + +#include +#include + +// NOLINTBEGIN(misc-include-cleaner) +#include +#include +// NOLINTEND(misc-include-cleaner) + +#include // std::function +#include // std::optional, std::nullopt + +namespace sourcemeta::core { + +/// @ingroup jsonschema +/// A callback to get dependency information +/// - Origin URI +/// - Pointer (reference keyword from the origin) +/// - Target URI +/// - Target schema +using DependencyCallback = + std::function &, const Pointer &, + const JSON::String &, const JSON &)>; + +/// @ingroup jsonschema +/// +/// This function recursively traverses and reports the external references in a +/// schema. For example: +/// +/// ```cpp +/// #include +/// #include +/// +/// // A custom resolver that knows about an additional schema +/// static auto test_resolver(std::string_view identifier) +/// -> std::optional { +/// if (identifier == "https://www.example.com/test") { +/// return sourcemeta::core::parse_json(R"JSON({ +/// "$id": "https://www.example.com/test", +/// "$schema": "https://json-schema.org/draft/2020-12/schema", +/// "type": "string" +/// })JSON"); +/// } else { +/// return sourcemeta::core::schema_official_resolver(identifier); +/// } +/// } +/// +/// sourcemeta::core::JSON document = +/// sourcemeta::core::parse_json(R"JSON({ +/// "$schema": "https://json-schema.org/draft/2020-12/schema", +/// "items": { "$ref": "https://www.example.com/test" } +/// })JSON"); +/// +/// sourcemeta::core::dependencies(document, +/// sourcemeta::core::schema_official_walker, test_resolver, +/// [](const auto &origin, +/// const auto &pointer, +/// const auto &target, +/// const auto &schema) { +/// // Do something with the information +/// }); +/// ``` +SOURCEMETA_CORE_JSONSCHEMA_EXPORT +auto dependencies( + const JSON &schema, const SchemaWalker &walker, + const SchemaResolver &resolver, const DependencyCallback &callback, + const std::optional &default_dialect = std::nullopt, + const std::optional &default_id = std::nullopt, + const SchemaFrame::Paths &paths = {empty_pointer}) -> void; + +/// @ingroup jsonschema +/// +/// This function bundles a JSON Schema (starting from Draft 4) by embedding +/// every remote reference into the top level schema resource, handling circular +/// dependencies and more. This overload mutates the input schema. For example: +/// +/// ```cpp +/// #include +/// #include +/// #include +/// +/// // A custom resolver that knows about an additional schema +/// static auto test_resolver(std::string_view identifier) +/// -> std::optional { +/// if (identifier == "https://www.example.com/test") { +/// return sourcemeta::core::parse_json(R"JSON({ +/// "$id": "https://www.example.com/test", +/// "$schema": "https://json-schema.org/draft/2020-12/schema", +/// "type": "string" +/// })JSON"); +/// } else { +/// return sourcemeta::core::schema_official_resolver(identifier); +/// } +/// } +/// +/// sourcemeta::core::JSON document = +/// sourcemeta::core::parse_json(R"JSON({ +/// "$schema": "https://json-schema.org/draft/2020-12/schema", +/// "items": { "$ref": "https://www.example.com/test" } +/// })JSON"); +/// +/// sourcemeta::core::bundle(document, +/// sourcemeta::core::schema_official_walker, test_resolver); +/// +/// const sourcemeta::core::JSON expected = +/// sourcemeta::core::parse_json(R"JSON({ +/// "$schema": "https://json-schema.org/draft/2020-12/schema", +/// "items": { "$ref": "https://www.example.com/test" }, +/// "$defs": { +/// "https://www.example.com/test": { +/// "$id": "https://www.example.com/test", +/// "$schema": "https://json-schema.org/draft/2020-12/schema", +/// "type": "string" +/// } +/// } +/// })JSON"); +/// +/// assert(document == expected); +/// ``` +SOURCEMETA_CORE_JSONSCHEMA_EXPORT +auto bundle(JSON &schema, const SchemaWalker &walker, + const SchemaResolver &resolver, + const std::optional &default_dialect = std::nullopt, + const std::optional &default_id = std::nullopt, + const std::optional &default_container = std::nullopt, + const SchemaFrame::Paths &paths = {empty_pointer}) -> void; + +/// @ingroup jsonschema +/// +/// This function bundles a JSON Schema (starting from Draft 4) by embedding +/// every remote reference into the top level schema resource, handling circular +/// dependencies and more. This overload returns a new schema, without mutating +/// the input schema. For example: +/// +/// ```cpp +/// #include +/// #include +/// #include +/// +/// // A custom resolver that knows about an additional schema +/// static auto test_resolver(std::string_view identifier) +/// -> std::optional { +/// if (identifier == "https://www.example.com/test") { +/// return sourcemeta::core::parse_json(R"JSON({ +/// "$id": "https://www.example.com/test", +/// "$schema": "https://json-schema.org/draft/2020-12/schema", +/// "type": "string" +/// })JSON"); +/// } else { +/// return sourcemeta::core::schema_official_resolver(identifier); +/// } +/// } +/// +/// const sourcemeta::core::JSON document = +/// sourcemeta::core::parse_json(R"JSON({ +/// "$schema": "https://json-schema.org/draft/2020-12/schema", +/// "items": { "$ref": "https://www.example.com/test" } +/// })JSON"); +/// +/// const sourcemeta::core::JSON result = +/// sourcemeta::core::bundle(document, +/// sourcemeta::core::schema_official_walker, test_resolver); +/// +/// const sourcemeta::core::JSON expected = +/// sourcemeta::core::parse_json(R"JSON({ +/// "$schema": "https://json-schema.org/draft/2020-12/schema", +/// "items": { "$ref": "https://www.example.com/test" }, +/// "$defs": { +/// "https://www.example.com/test": { +/// "$id": "https://www.example.com/test", +/// "$schema": "https://json-schema.org/draft/2020-12/schema", +/// "type": "string" +/// } +/// } +/// })JSON"); +/// +/// assert(result == expected); +/// ``` +SOURCEMETA_CORE_JSONSCHEMA_EXPORT +auto bundle(const JSON &schema, const SchemaWalker &walker, + const SchemaResolver &resolver, + const std::optional &default_dialect = std::nullopt, + const std::optional &default_id = std::nullopt, + const std::optional &default_container = std::nullopt, + const SchemaFrame::Paths &paths = {empty_pointer}) -> JSON; + +} // namespace sourcemeta::core + +#endif diff --git a/vendor/core/src/core/jsonschema/include/sourcemeta/core/jsonschema_transform.h b/vendor/core/src/core/jsonschema/include/sourcemeta/core/jsonschema_transform.h index 26955473..dac576ac 100644 --- a/vendor/core/src/core/jsonschema/include/sourcemeta/core/jsonschema_transform.h +++ b/vendor/core/src/core/jsonschema/include/sourcemeta/core/jsonschema_transform.h @@ -242,7 +242,9 @@ class SOURCEMETA_CORE_JSONSCHEMA_EXPORT SchemaTransformer { const SchemaResolver &resolver, const Callback &callback, const std::optional &default_dialect = std::nullopt, const std::optional &default_id = std::nullopt) const - -> bool; + // Note that we only calculate a health score on "check", as "apply" would + // by definition change the score + -> std::pair; [[nodiscard]] auto begin() const -> auto { return this->rules.cbegin(); } [[nodiscard]] auto end() const -> auto { return this->rules.cend(); } diff --git a/vendor/core/src/core/jsonschema/transformer.cc b/vendor/core/src/core/jsonschema/transformer.cc index 7be54fa8..e79cc7be 100644 --- a/vendor/core/src/core/jsonschema/transformer.cc +++ b/vendor/core/src/core/jsonschema/transformer.cc @@ -21,6 +21,16 @@ auto is_true(const sourcemeta::core::SchemaTransformRule::Result &result) } } +auto calculate_health_percentage(const std::size_t subschemas, + const std::size_t failed_subschemas) + -> std::uint8_t { + assert(failed_subschemas <= subschemas); + const auto result{100 - (failed_subschemas * 100 / subschemas)}; + assert(result >= 0); + assert(result <= 100); + return static_cast(result); +} + } // namespace namespace sourcemeta::core { @@ -101,20 +111,26 @@ auto SchemaTransformer::check( const JSON &schema, const SchemaWalker &walker, const SchemaResolver &resolver, const SchemaTransformer::Callback &callback, const std::optional &default_dialect, - const std::optional &default_id) const -> bool { + const std::optional &default_id) const + -> std::pair { SchemaFrame frame{SchemaFrame::Mode::Locations}; frame.analyse(schema, walker, resolver, default_dialect, default_id); bool result{true}; + std::size_t subschema_count{0}; + std::size_t subschema_failures{0}; for (const auto &entry : frame.locations()) { if (entry.second.type != SchemaFrame::LocationType::Resource && entry.second.type != SchemaFrame::LocationType::Subschema) { continue; } + subschema_count += 1; + const auto ¤t{get(schema, entry.second.pointer)}; const auto current_vocabularies{ vocabularies(schema, resolver, entry.second.dialect)}; + bool subresult{true}; for (const auto &[name, rule] : this->rules) { const auto outcome{rule->check(current, schema, current_vocabularies, walker, resolver, frame, entry.second)}; @@ -122,22 +138,28 @@ auto SchemaTransformer::check( case 0: assert(std::holds_alternative(outcome)); if (*std::get_if(&outcome)) { - result = false; + subresult = false; callback(entry.second.pointer, name, rule->message(), ""); } break; default: assert(std::holds_alternative(outcome)); - result = false; + subresult = false; callback(entry.second.pointer, name, rule->message(), *std::get_if(&outcome)); break; } } + + if (!subresult) { + subschema_failures += 1; + result = false; + } } - return result; + return {result, + calculate_health_percentage(subschema_count, subschema_failures)}; } auto SchemaTransformer::apply( @@ -147,7 +169,7 @@ auto SchemaTransformer::apply( const std::optional &default_id) const -> bool { // There is no point in applying an empty bundle assert(!this->rules.empty()); - std::set> processed_rules; + std::set> processed_rules; bool result{true}; while (true) { @@ -183,7 +205,8 @@ auto SchemaTransformer::apply( continue; } - if (processed_rules.contains({entry.second.pointer, name})) { + std::pair mark{¤t, &name}; + if (processed_rules.contains(mark)) { // TODO: Throw a better custom error that also highlights the schema // location std::ostringstream error; @@ -221,7 +244,7 @@ auto SchemaTransformer::apply( set(schema, reference.first.second, JSON{original.recompose()}); } - processed_rules.emplace(entry.second.pointer, name); + processed_rules.emplace(std::move(mark)); goto core_transformer_start_again; } } diff --git a/vendor/core/src/core/uri/include/sourcemeta/core/uri_error.h b/vendor/core/src/core/uri/include/sourcemeta/core/uri_error.h index 85064d04..d52d7061 100644 --- a/vendor/core/src/core/uri/include/sourcemeta/core/uri_error.h +++ b/vendor/core/src/core/uri/include/sourcemeta/core/uri_error.h @@ -8,7 +8,7 @@ #include // std::uint64_t #include // std::exception #include // std::string -#include // std::string +#include // std::move namespace sourcemeta::core { diff --git a/vendor/core/src/extension/alterschema/CMakeLists.txt b/vendor/core/src/extension/alterschema/CMakeLists.txt index 29e9cb97..9bb24d58 100644 --- a/vendor/core/src/extension/alterschema/CMakeLists.txt +++ b/vendor/core/src/extension/alterschema/CMakeLists.txt @@ -62,7 +62,9 @@ sourcemeta_library(NAMESPACE sourcemeta PROJECT core NAME alterschema linter/modern_official_dialect_with_empty_fragment.h linter/then_empty.h linter/else_empty.h - linter/then_without_if.h) + linter/then_without_if.h + linter/property_names_type_default.h + linter/property_names_default.h) if(SOURCEMETA_CORE_INSTALL) sourcemeta_library_install(NAMESPACE sourcemeta PROJECT core NAME alterschema) diff --git a/vendor/core/src/extension/alterschema/alterschema.cc b/vendor/core/src/extension/alterschema/alterschema.cc index 6eb8e771..3312b482 100644 --- a/vendor/core/src/extension/alterschema/alterschema.cc +++ b/vendor/core/src/extension/alterschema/alterschema.cc @@ -69,6 +69,8 @@ contains_any(const Vocabularies &container, #include "linter/non_applicable_type_specific_keywords.h" #include "linter/pattern_properties_default.h" #include "linter/properties_default.h" +#include "linter/property_names_default.h" +#include "linter/property_names_type_default.h" #include "linter/single_type_array.h" #include "linter/then_empty.h" #include "linter/then_without_if.h" @@ -147,6 +149,8 @@ auto add(SchemaTransformer &bundle, const AlterSchemaMode mode) bundle.add(); bundle.add(); bundle.add(); + bundle.add(); + bundle.add(); bundle.add(); bundle.add(); bundle.add(); diff --git a/vendor/core/src/extension/alterschema/linter/property_names_default.h b/vendor/core/src/extension/alterschema/linter/property_names_default.h new file mode 100644 index 00000000..8c6a9d95 --- /dev/null +++ b/vendor/core/src/extension/alterschema/linter/property_names_default.h @@ -0,0 +1,32 @@ +class PropertyNamesDefault final : public SchemaTransformRule { +public: + PropertyNamesDefault() + : SchemaTransformRule{ + "property_names_default", + "Setting the `propertyNames` keyword to the empty object " + "does not add any further constraint"} {}; + + [[nodiscard]] auto + condition(const sourcemeta::core::JSON &schema, + const sourcemeta::core::JSON &, + const sourcemeta::core::Vocabularies &vocabularies, + const sourcemeta::core::SchemaFrame &, + const sourcemeta::core::SchemaFrame::Location &, + const sourcemeta::core::SchemaWalker &, + const sourcemeta::core::SchemaResolver &) const + -> sourcemeta::core::SchemaTransformRule::Result override { + return contains_any( + vocabularies, + {"https://json-schema.org/draft/2020-12/vocab/applicator", + "https://json-schema.org/draft/2019-09/vocab/applicator", + "http://json-schema.org/draft-07/schema#", + "http://json-schema.org/draft-06/schema#"}) && + schema.is_object() && schema.defines("propertyNames") && + schema.at("propertyNames").is_object() && + schema.at("propertyNames").empty(); + } + + auto transform(JSON &schema) const -> void override { + schema.erase("propertyNames"); + } +}; diff --git a/vendor/core/src/extension/alterschema/linter/property_names_type_default.h b/vendor/core/src/extension/alterschema/linter/property_names_type_default.h new file mode 100644 index 00000000..54cee777 --- /dev/null +++ b/vendor/core/src/extension/alterschema/linter/property_names_type_default.h @@ -0,0 +1,41 @@ +class PropertyNamesTypeDefault final : public SchemaTransformRule { +public: + PropertyNamesTypeDefault() + : SchemaTransformRule{ + "property_names_type_default", + "Setting the `type` keyword to `string` inside " + "`propertyNames` does not add any further constraint"} {}; + + [[nodiscard]] auto + condition(const sourcemeta::core::JSON &schema, + const sourcemeta::core::JSON &, + const sourcemeta::core::Vocabularies &vocabularies, + const sourcemeta::core::SchemaFrame &, + const sourcemeta::core::SchemaFrame::Location &, + const sourcemeta::core::SchemaWalker &, + const sourcemeta::core::SchemaResolver &) const + -> sourcemeta::core::SchemaTransformRule::Result override { + return contains_any( + vocabularies, + {"https://json-schema.org/draft/2020-12/vocab/applicator", + "https://json-schema.org/draft/2019-09/vocab/applicator", + "http://json-schema.org/draft-07/schema#", + "http://json-schema.org/draft-06/schema#"}) && + schema.is_object() && schema.defines("propertyNames") && + schema.at("propertyNames").is_object() && + schema.at("propertyNames").defines("type") && + ((schema.at("propertyNames").at("type").is_string() && + schema.at("propertyNames").at("type").to_string() == "string") || + (schema.at("propertyNames").at("type").is_array() && + std::all_of( + schema.at("propertyNames").at("type").as_array().begin(), + schema.at("propertyNames").at("type").as_array().end(), + [](const auto &item) { + return item.is_string() && item.to_string() == "string"; + }))); + } + + auto transform(JSON &schema) const -> void override { + schema.at("propertyNames").erase("type"); + } +}; diff --git a/vendor/core/src/extension/alterschema/linter/unnecessary_allof_wrapper_draft.h b/vendor/core/src/extension/alterschema/linter/unnecessary_allof_wrapper_draft.h index d2325733..26b1fc60 100644 --- a/vendor/core/src/extension/alterschema/linter/unnecessary_allof_wrapper_draft.h +++ b/vendor/core/src/extension/alterschema/linter/unnecessary_allof_wrapper_draft.h @@ -28,6 +28,17 @@ class UnnecessaryAllOfWrapperDraft final : public SchemaTransformRule { for (const auto &entry : schema.at("allOf").as_array()) { if (entry.is_object()) { + // It is dangerous to extract type-specific keywords from a schema that + // declares a type into another schema that also declares a type if + // the types are different. As we might lead to those type-keywords + // getting incorrectly removed if they don't apply to the target type + if (schema.defines("type") && entry.defines("type") && + // TODO: Ideally we also check for intersection of types in type + // arrays or whether one is contained in the other + schema.at("type") != entry.at("type")) { + continue; + } + for (const auto &subentry : entry.as_object()) { if (subentry.first != "$ref" && !schema.defines(subentry.first)) { return true; diff --git a/vendor/core/src/extension/alterschema/linter/unnecessary_allof_wrapper_modern.h b/vendor/core/src/extension/alterschema/linter/unnecessary_allof_wrapper_modern.h index a7adc26f..195eb68a 100644 --- a/vendor/core/src/extension/alterschema/linter/unnecessary_allof_wrapper_modern.h +++ b/vendor/core/src/extension/alterschema/linter/unnecessary_allof_wrapper_modern.h @@ -26,9 +26,27 @@ class UnnecessaryAllOfWrapperModern final : public SchemaTransformRule { return false; } + const auto has_validation{contains_any( + vocabularies, + {"https://json-schema.org/draft/2020-12/vocab/validation", + "https://json-schema.org/draft/2019-09/vocab/validation"})}; + for (const auto &entry : schema.at("allOf").as_array()) { if (entry.is_object()) { + // It is dangerous to extract type-specific keywords from a schema that + // declares a type into another schema that also declares a type if + // the types are different. As we might lead to those type-keywords + // getting incorrectly removed if they don't apply to the target type + if (has_validation && schema.defines("type") && entry.defines("type") && + // TODO: Ideally we also check for intersection of types in type + // arrays or whether one is contained in the other + schema.at("type") != entry.at("type")) { + continue; + } + for (const auto &subentry : entry.as_object()) { + // TODO: Have another rule that removes a keyword if its exactly + // equal to an instance of the same keyword outside the wrapper if (!schema.defines(subentry.first)) { return true; }