From 63c80f05c1d9789c9ed94e30471003aa5a238c27 Mon Sep 17 00:00:00 2001 From: Balakrishna Avulapati Date: Tue, 22 Jul 2025 19:40:33 +0530 Subject: [PATCH 1/6] Enable bugprone-unchecked-optional-access clang-tidy check Signed-off-by: Balakrishna Avulapati --- cmake/common/clang-tidy.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmake/common/clang-tidy.json b/cmake/common/clang-tidy.json index c69b9009c..eb755462f 100644 --- a/cmake/common/clang-tidy.json +++ b/cmake/common/clang-tidy.json @@ -1,5 +1,5 @@ { - "Checks": "-*, concurrency-*, modernize-*, performance-*, portability-*", + "Checks": "-*, bugprone-unchecked-optional-access, concurrency-*, modernize-*, performance-*, portability-*", "WarningsAsErrors": "*", "FormatStyle": "none", "UseColor": true From e7a95d7fd01afc5fa99cc2e076827c056986b769 Mon Sep 17 00:00:00 2001 From: Balakrishna Avulapati Date: Sun, 27 Jul 2025 17:39:04 +0530 Subject: [PATCH 2/6] Merge main Signed-off-by: Balakrishna Avulapati --- CMakeLists.txt | 12 +- cmake/common/clang-tidy.json | 2 +- src/core/gzip/CMakeLists.txt | 4 +- src/core/gzip/gzip.cc | 136 ++++++-- src/core/gzip/include/sourcemeta/core/gzip.h | 101 +++++- .../gzip/include/sourcemeta/core/gzip_error.h | 40 +++ .../json/include/sourcemeta/core/json_hash.h | 11 +- .../include/sourcemeta/core/json_object.h | 7 +- src/core/json/parser.h | 6 +- src/core/jsonschema/CMakeLists.txt | 2 +- src/core/jsonschema/bundle.cc | 168 +++++++-- src/core/jsonschema/frame.cc | 14 +- .../include/sourcemeta/core/jsonschema.h | 120 +------ .../sourcemeta/core/jsonschema_bundle.h | 195 +++++++++++ .../sourcemeta/core/jsonschema_transform.h | 4 +- src/core/jsonschema/official_resolver.in.cc | 37 +- src/core/jsonschema/transformer.cc | 37 +- .../uri/include/sourcemeta/core/uri_error.h | 2 +- src/core/uri/uri.cc | 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 + .../alterschema_lint_2019_09_test.cc | 160 +++++++++ .../alterschema_lint_2020_12_test.cc | 160 +++++++++ .../alterschema_lint_draft4_test.cc | 79 +++++ .../alterschema_lint_draft6_test.cc | 160 +++++++++ .../alterschema_lint_draft7_test.cc | 119 +++++++ test/gzip/gzip_test.cc | 50 ++- test/json/json_hash_test.cc | 26 +- test/jsonschema/CMakeLists.txt | 1 + test/jsonschema/jsonschema_bundle_test.cc | 62 +++- .../jsonschema_dependencies_test.cc | 329 ++++++++++++++++++ .../jsonschema/jsonschema_transformer_test.cc | 66 +++- 36 files changed, 1940 insertions(+), 282 deletions(-) create mode 100644 src/core/gzip/include/sourcemeta/core/gzip_error.h create mode 100644 src/core/jsonschema/include/sourcemeta/core/jsonschema_bundle.h create mode 100644 src/extension/alterschema/linter/property_names_default.h create mode 100644 src/extension/alterschema/linter/property_names_type_default.h create mode 100644 test/jsonschema/jsonschema_dependencies_test.cc diff --git a/CMakeLists.txt b/CMakeLists.txt index c77e08774..8d9727d45 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -64,13 +64,19 @@ if(SOURCEMETA_CORE_UUID) add_subdirectory(src/core/uuid) endif() -if(SOURCEMETA_CORE_GZIP OR SOURCEMETA_CORE_CONTRIB_ZLIB) +if(SOURCEMETA_CORE_CONTRIB_ZLIB OR SOURCEMETA_CORE_GZIP) find_package(ZLIB REQUIRED) +endif() + +if(SOURCEMETA_CORE_GZIP) add_subdirectory(src/core/gzip) endif() -if(SOURCEMETA_CORE_MD5 OR SOURCEMETA_CORE_CONTRIB_BEARSSL) +if(SOURCEMETA_CORE_CONTRIB_BEARSSL OR SOURCEMETA_CORE_MD5) find_package(BearSSL REQUIRED) +endif() + +if(SOURCEMETA_CORE_MD5) add_subdirectory(src/core/md5) endif() @@ -121,8 +127,6 @@ if(SOURCEMETA_CORE_DOCS) endif() if(PROJECT_IS_TOP_LEVEL) - # TODO: Try once more to enable this per target, - # so, we don't need to manually disable it here sourcemeta_target_clang_format(SOURCES src/*.h src/*.cc benchmark/*.h benchmark/*.cc diff --git a/cmake/common/clang-tidy.json b/cmake/common/clang-tidy.json index eb755462f..49abc79ba 100644 --- a/cmake/common/clang-tidy.json +++ b/cmake/common/clang-tidy.json @@ -1,5 +1,5 @@ { - "Checks": "-*, bugprone-unchecked-optional-access, concurrency-*, modernize-*, performance-*, portability-*", + "Checks": "-*, bugprone-*, -bugprone-easily-swappable-parameters,-bugprone-unchecked-optional-access, concurrency-*, modernize-*, performance-*, portability-*", "WarningsAsErrors": "*", "FormatStyle": "none", "UseColor": true diff --git a/src/core/gzip/CMakeLists.txt b/src/core/gzip/CMakeLists.txt index 1613eca1e..684204ff0 100644 --- a/src/core/gzip/CMakeLists.txt +++ b/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/src/core/gzip/gzip.cc b/src/core/gzip/gzip.cc index 5207af117..267757f42 100644 --- a/src/core/gzip/gzip.cc +++ b/src/core/gzip/gzip.cc @@ -5,43 +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(), 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(const std::string &input) -> std::string { + std::istringstream stream{input}; + return gzip(stream); +} + +auto gunzip(std::istream &stream) -> std::string { + std::ostringstream output; + gunzip(stream, output); + return output.str(); +} + +auto gunzip(const std::string &input) -> std::string { + std::istringstream stream{input}; + return gunzip(stream); } } // namespace sourcemeta::core diff --git a/src/core/gzip/include/sourcemeta/core/gzip.h b/src/core/gzip/include/sourcemeta/core/gzip.h index 977c964ca..40c50dffa 100644 --- a/src/core/gzip/include/sourcemeta/core/gzip.h +++ b/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,99 @@ 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(const 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(const std::string &input) + -> std::string; } // namespace sourcemeta::core diff --git a/src/core/gzip/include/sourcemeta/core/gzip_error.h b/src/core/gzip/include/sourcemeta/core/gzip_error.h new file mode 100644 index 000000000..9e99d10e2 --- /dev/null +++ b/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/src/core/json/include/sourcemeta/core/json_hash.h b/src/core/json/include/sourcemeta/core/json_hash.h index 10a6139b9..5818b8423 100644 --- a/src/core/json/include/sourcemeta/core/json_hash.h +++ b/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/src/core/json/include/sourcemeta/core/json_object.h b/src/core/json/include/sourcemeta/core/json_object.h index d48b5dda4..eeaa11cb3 100644 --- a/src/core/json/include/sourcemeta/core/json_object.h +++ b/src/core/json/include/sourcemeta/core/json_object.h @@ -91,9 +91,7 @@ template class JSONObject { for (const auto &entry : this->data) { const auto *result{other.try_at(entry.first, entry.hash)}; - if (!result) { - return false; - } else if (*result != entry.second) { + if (!result || *result != entry.second) { return false; } } @@ -185,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/src/core/json/parser.h b/src/core/json/parser.h index 1c932cc49..c728309e4 100644 --- a/src/core/json/parser.h +++ b/src/core/json/parser.h @@ -650,21 +650,21 @@ auto parse_number( #define CALLBACK_PRE(value_type, value) \ if (callback) { \ - assert(value.is_null() || value.is_string() || value.is_integer()); \ + assert((value).is_null() || (value).is_string() || (value).is_integer()); \ callback(JSON::ParsePhase::Pre, JSON::Type::value_type, line, column, \ value); \ } #define CALLBACK_PRE_WITH_POSITION(value_type, line, column, value) \ if (callback) { \ - assert(value.is_null() || value.is_string() || value.is_integer()); \ + assert((value).is_null() || (value).is_string() || (value).is_integer()); \ callback(JSON::ParsePhase::Pre, JSON::Type::value_type, line, column, \ value); \ } #define CALLBACK_POST(value_type, value) \ if (callback) { \ - assert(value.type() == JSON::Type::value_type); \ + assert((value).type() == JSON::Type::value_type); \ callback(JSON::ParsePhase::Post, JSON::Type::value_type, line, column, \ value); \ } diff --git a/src/core/jsonschema/CMakeLists.txt b/src/core/jsonschema/CMakeLists.txt index a9161376b..4cd587baa 100644 --- a/src/core/jsonschema/CMakeLists.txt +++ b/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/src/core/jsonschema/bundle.cc b/src/core/jsonschema/bundle.cc index 9849b6bbc..0b58f658a 100644 --- a/src/core/jsonschema/bundle.cc +++ b/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, - const sourcemeta::core::JSON &target) -> void { + sourcemeta::core::JSON &&target) -> void { auto *current{&root}; for (const auto &token : container) { if (token.is_property()) { @@ -34,14 +129,7 @@ auto embed_schema(sourcemeta::core::JSON &root, key << "/x"; } - current->assign(key.str(), target); -} - -auto is_official_metaschema_reference(const sourcemeta::core::Pointer &pointer, - const std::string &destination) -> bool { - return !pointer.empty() && pointer.back().is_property() && - pointer.back().to_property() == "$schema" && - sourcemeta::core::schema_official_resolver(destination).has_value(); + current->assign(key.str(), std::move(target)); } auto bundle_schema(sourcemeta::core::JSON &root, @@ -65,12 +153,12 @@ auto bundle_schema(sourcemeta::core::JSON &root, // We only want to frame in "wrapper" mode for the top level object paths); } else { - // Note that we only apply the default identifier to the top-level frame - frame.analyse(subschema, walker, resolver, default_dialect); + frame.analyse(subschema, walker, resolver, default_dialect, default_id); } // Otherwise, given recursion, we would be modifying the // references list *while* looping on it + // TODO: How can we avoid this very expensive copy? const auto references_copy = frame.references(); for (const auto &[key, reference] : references_copy) { if (frame.traverse(reference.destination).has_value() || @@ -97,8 +185,8 @@ auto bundle_schema(sourcemeta::core::JSON &root, } assert(reference.base.has_value()); - const auto identifier{reference.base.value()}; - const auto remote{resolver(identifier)}; + const auto &identifier{reference.base.value()}; + auto remote{resolver(identifier)}; if (!remote.has_value()) { if (frame.traverse(identifier).has_value()) { throw sourcemeta::core::SchemaReferenceError( @@ -110,32 +198,30 @@ auto bundle_schema(sourcemeta::core::JSON &root, identifier, "Could not resolve the reference to an external schema"); } - // Otherwise, if the target schema does not declare an inline identifier, - // references to that identifier from the outer schema won't resolve. - sourcemeta::core::JSON copy{remote.value()}; - - if (!sourcemeta::core::is_schema(copy)) { + 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 dialect{sourcemeta::core::dialect(copy, default_dialect)}; - if (!dialect.has_value()) { + 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"); } - if (copy.is_object()) { + if (remote.value().is_object()) { // Always insert an identifier, as a schema might refer to another schema // using another URI (i.e. due to relying on HTTP re-directions, etc) - sourcemeta::core::reidentify(copy, identifier, resolver, default_dialect); + sourcemeta::core::reidentify(remote.value(), identifier, + base_dialect.value()); } - embed_schema(root, container, identifier, copy); - bundle_schema(root, container, copy, frame, walker, resolver, - default_dialect, default_id, paths, depth + 1); + bundle_schema(root, container, remote.value(), frame, walker, resolver, + default_dialect, identifier, paths, depth + 1); + embed_schema(root, container, identifier, std::move(remote).value()); } } @@ -143,14 +229,26 @@ auto bundle_schema(sourcemeta::core::JSON &root, namespace sourcemeta::core { -auto bundle(sourcemeta::core::JSON &schema, const SchemaWalker &walker, +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) -> void { - sourcemeta::core::SchemaFrame frame{ - sourcemeta::core::SchemaFrame::Mode::References}; + SchemaFrame frame{SchemaFrame::Mode::References}; if (default_container.has_value()) { // This is undefined behavior @@ -201,17 +299,17 @@ auto bundle(sourcemeta::core::JSON &schema, const SchemaWalker &walker, // We don't attempt to bundle on dialects where we // don't know where to put the embedded schemas - throw sourcemeta::core::SchemaError( + throw SchemaError( "Could not determine how to perform bundling in this dialect"); } -auto bundle(const sourcemeta::core::JSON &schema, const SchemaWalker &walker, +auto bundle(const 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) -> sourcemeta::core::JSON { - sourcemeta::core::JSON copy = schema; + const SchemaFrame::Paths &paths) -> JSON { + JSON copy = schema; bundle(copy, walker, resolver, default_dialect, default_id, default_container, paths); return copy; diff --git a/src/core/jsonschema/frame.cc b/src/core/jsonschema/frame.cc index 8a92de4cb..183c4dc61 100644 --- a/src/core/jsonschema/frame.cc +++ b/src/core/jsonschema/frame.cc @@ -77,10 +77,9 @@ auto find_anchors(const sourcemeta::core::JSON &schema, if (identifier.is_fragment_only()) { result.insert( {sourcemeta::core::JSON::String{ - identifier.fragment() - .value()}, // NOLINT(bugprone-unchecked-optional-access): - // Check for optional is happening - // inside is_fragment_only() + // Check for optional is happening inside is_fragment_only() + // NOLINTNEXTLINE(bugprone-unchecked-optional-access) + identifier.fragment().value()}, AnchorType::Static}); } } @@ -96,10 +95,9 @@ auto find_anchors(const sourcemeta::core::JSON &schema, if (identifier.is_fragment_only()) { result.insert( {sourcemeta::core::JSON::String{ - identifier.fragment() - .value()}, // NOLINT(bugprone-unchecked-optional-access): - // Check for optional is happening - // inside is_fragment_only() + // Check for optional is happening inside is_fragment_only() + // NOLINTNEXTLINE(bugprone-unchecked-optional-access) + identifier.fragment().value()}, AnchorType::Static}); } } diff --git a/src/core/jsonschema/include/sourcemeta/core/jsonschema.h b/src/core/jsonschema/include/sourcemeta/core/jsonschema.h index a731df1a5..6f0252ede 100644 --- a/src/core/jsonschema/include/sourcemeta/core/jsonschema.h +++ b/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,124 +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 -/// -/// 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; - /// @ingroup jsonschema /// /// Given a schema identifier, this function creates a JSON Schema wrapper that diff --git a/src/core/jsonschema/include/sourcemeta/core/jsonschema_bundle.h b/src/core/jsonschema/include/sourcemeta/core/jsonschema_bundle.h new file mode 100644 index 000000000..797f7ccba --- /dev/null +++ b/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/src/core/jsonschema/include/sourcemeta/core/jsonschema_transform.h b/src/core/jsonschema/include/sourcemeta/core/jsonschema_transform.h index 269554737..dac576acb 100644 --- a/src/core/jsonschema/include/sourcemeta/core/jsonschema_transform.h +++ b/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/src/core/jsonschema/official_resolver.in.cc b/src/core/jsonschema/official_resolver.in.cc index d3796f9a3..2205bde25 100644 --- a/src/core/jsonschema/official_resolver.in.cc +++ b/src/core/jsonschema/official_resolver.in.cc @@ -3,11 +3,16 @@ auto sourcemeta::core::schema_official_resolver(std::string_view identifier) -> std::optional { // JSON Schema 2020-12 - if (identifier == "https://json-schema.org/draft/2020-12/schema") { + if (identifier == "https://json-schema.org/draft/2020-12/schema" || + // Just for compatibility given that this is such a common issue + identifier == "https://json-schema.org/draft/2020-12/schema#") { return sourcemeta::core::parse_json( R"EOF(@METASCHEMA_JSONSCHEMA_2020_12@)EOF"); } else if (identifier == - "https://json-schema.org/draft/2020-12/hyper-schema") { + "https://json-schema.org/draft/2020-12/hyper-schema" || + // Just for compatibility given that this is such a common issue + identifier == + "https://json-schema.org/draft/2020-12/hyper-schema#") { return sourcemeta::core::parse_json( R"EOF(@METASCHEMA_HYPERSCHEMA_2020_12@)EOF"); } else if (identifier == @@ -52,21 +57,17 @@ auto sourcemeta::core::schema_official_resolver(std::string_view identifier) return sourcemeta::core::parse_json( R"EOF(@METASCHEMA_JSONSCHEMA_2020_12_OUTPUT@)EOF"); - // Just for compatibility given that this is such a common issue - } else if (identifier == "https://json-schema.org/draft/2020-12/schema#") { - return sourcemeta::core::parse_json( - R"EOF(@METASCHEMA_JSONSCHEMA_2020_12@)EOF"); - } else if (identifier == - "https://json-schema.org/draft/2020-12/hyper-schema#") { - return sourcemeta::core::parse_json( - R"EOF(@METASCHEMA_HYPERSCHEMA_2020_12@)EOF"); - // JSON Schema 2019-09 - } else if (identifier == "https://json-schema.org/draft/2019-09/schema") { + } else if (identifier == "https://json-schema.org/draft/2019-09/schema" || + // Just for compatibility given that this is such a common issue + identifier == "https://json-schema.org/draft/2019-09/schema#") { return sourcemeta::core::parse_json( R"EOF(@METASCHEMA_JSONSCHEMA_2019_09@)EOF"); } else if (identifier == - "https://json-schema.org/draft/2019-09/hyper-schema") { + "https://json-schema.org/draft/2019-09/hyper-schema" || + // Just for compatibility given that this is such a common issue + identifier == + "https://json-schema.org/draft/2019-09/hyper-schema#") { return sourcemeta::core::parse_json( R"EOF(@METASCHEMA_HYPERSCHEMA_2019_09@)EOF"); } else if (identifier == @@ -106,16 +107,6 @@ auto sourcemeta::core::schema_official_resolver(std::string_view identifier) "https://json-schema.org/draft/2019-09/output/hyper-schema") { return sourcemeta::core::parse_json( R"EOF(@METASCHEMA_HYPERSCHEMA_2019_09_OUTPUT@)EOF"); - - // Just for compatibility given that this is such a common issue - } else if (identifier == "https://json-schema.org/draft/2019-09/schema#") { - return sourcemeta::core::parse_json( - R"EOF(@METASCHEMA_JSONSCHEMA_2019_09@)EOF"); - } else if (identifier == - "https://json-schema.org/draft/2019-09/hyper-schema#") { - return sourcemeta::core::parse_json( - R"EOF(@METASCHEMA_HYPERSCHEMA_2019_09@)EOF"); - // JSON Schema Draft7 } else if (identifier == "http://json-schema.org/draft-07/schema#" || identifier == "http://json-schema.org/draft-07/schema") { diff --git a/src/core/jsonschema/transformer.cc b/src/core/jsonschema/transformer.cc index 7be54fa8b..e79cc7be9 100644 --- a/src/core/jsonschema/transformer.cc +++ b/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/src/core/uri/include/sourcemeta/core/uri_error.h b/src/core/uri/include/sourcemeta/core/uri_error.h index 85064d049..d52d7061a 100644 --- a/src/core/uri/include/sourcemeta/core/uri_error.h +++ b/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/src/core/uri/uri.cc b/src/core/uri/uri.cc index 303d36ee7..7744e87b2 100644 --- a/src/core/uri/uri.cc +++ b/src/core/uri/uri.cc @@ -688,8 +688,6 @@ auto URI::try_resolve_from(const URI &base) -> URI & { this->scheme_ = base.scheme_; this->query_ = base.query_; return *this; - } else if (base.path().has_value() && base.path().value().starts_with("..")) { - return *this; } else if (base.is_relative() && this->is_relative() && base.path_.has_value() && this->path_.has_value() && this->path_.value().find('/') == std::string::npos && diff --git a/src/extension/alterschema/CMakeLists.txt b/src/extension/alterschema/CMakeLists.txt index 29e9cb974..9bb24d580 100644 --- a/src/extension/alterschema/CMakeLists.txt +++ b/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/src/extension/alterschema/alterschema.cc b/src/extension/alterschema/alterschema.cc index 6eb8e771f..3312b4825 100644 --- a/src/extension/alterschema/alterschema.cc +++ b/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/src/extension/alterschema/linter/property_names_default.h b/src/extension/alterschema/linter/property_names_default.h new file mode 100644 index 000000000..8c6a9d950 --- /dev/null +++ b/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/src/extension/alterschema/linter/property_names_type_default.h b/src/extension/alterschema/linter/property_names_type_default.h new file mode 100644 index 000000000..54cee777b --- /dev/null +++ b/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/src/extension/alterschema/linter/unnecessary_allof_wrapper_draft.h b/src/extension/alterschema/linter/unnecessary_allof_wrapper_draft.h index d23257336..26b1fc60f 100644 --- a/src/extension/alterschema/linter/unnecessary_allof_wrapper_draft.h +++ b/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/src/extension/alterschema/linter/unnecessary_allof_wrapper_modern.h b/src/extension/alterschema/linter/unnecessary_allof_wrapper_modern.h index a7adc26f3..195eb68a7 100644 --- a/src/extension/alterschema/linter/unnecessary_allof_wrapper_modern.h +++ b/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; } diff --git a/test/alterschema/alterschema_lint_2019_09_test.cc b/test/alterschema/alterschema_lint_2019_09_test.cc index 6c45d58c8..171d82775 100644 --- a/test/alterschema/alterschema_lint_2019_09_test.cc +++ b/test/alterschema/alterschema_lint_2019_09_test.cc @@ -657,6 +657,85 @@ TEST(AlterSchema_lint_2019_09, duplicate_allof_branches_1) { EXPECT_EQ(document, expected); } +TEST(AlterSchema_lint_2019_09, duplicate_allof_branches_2) { + auto document = sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2019-09/schema", + "allOf": [ + { "type": "number" }, + { "type": "string" }, + { "type": "number" } + ] + })JSON"); + + LINT_AND_FIX_FOR_STATIC_ANALYSIS(document); + + const auto expected = sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2019-09/schema", + "type": "number", + "multipleOf": 1, + "allOf": [ + { "type": "string", "minLength": 0 } + ] + })JSON"); + + EXPECT_EQ(document, expected); +} + +TEST(AlterSchema_lint_2019_09, duplicate_allof_branches_3) { + auto document = sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2019-09/schema", + "allOf": [ + { "type": "number" }, + { "type": "number" }, + { "type": "string" } + ] + })JSON"); + + LINT_AND_FIX_FOR_STATIC_ANALYSIS(document); + + const auto expected = sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2019-09/schema", + "type": "number", + "multipleOf": 1, + "allOf": [ + { "type": "string", "minLength": 0 } + ] + })JSON"); + + EXPECT_EQ(document, expected); +} + +TEST(AlterSchema_lint_2019_09, duplicate_allof_branches_4) { + auto document = sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2019-09/schema", + "allOf": [ + { "type": "number" }, + { "type": "string" }, + { "type": "number" }, + { "type": "number" }, + { "type": "number" }, + { "type": "string" }, + { "type": "string" }, + { "type": "string" }, + { "type": "number" }, + { "type": "number" } + ] + })JSON"); + + LINT_AND_FIX_FOR_STATIC_ANALYSIS(document); + + const auto expected = sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2019-09/schema", + "type": "number", + "multipleOf": 1, + "allOf": [ + { "type": "string", "minLength": 0 } + ] + })JSON"); + + EXPECT_EQ(document, expected); +} + TEST(AlterSchema_lint_2019_09, duplicate_anyof_branches_1) { sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({ "$schema": "https://json-schema.org/draft/2019-09/schema", @@ -2134,3 +2213,84 @@ TEST(AlterSchema_lint_2019_09, else_empty_2) { EXPECT_EQ(document, expected); } + +TEST(AlterSchema_lint_2019_09, property_names_type_default_1) { + sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2019-09/schema", + "type": "object", + "propertyNames": { + "type": "string", + "pattern": "^S_" + } + })JSON"); + + LINT_AND_FIX_FOR_READABILITY(document); + + const sourcemeta::core::JSON expected = sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2019-09/schema", + "type": "object", + "propertyNames": { + "pattern": "^S_" + } + })JSON"); + + EXPECT_EQ(document, expected); +} + +TEST(AlterSchema_lint_2019_09, property_names_type_default_2) { + sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2019-09/schema", + "type": "object", + "propertyNames": { + "type": [ "string" ] + } + })JSON"); + + LINT_AND_FIX_FOR_READABILITY(document); + + const sourcemeta::core::JSON expected = sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2019-09/schema", + "type": "object" + })JSON"); + + EXPECT_EQ(document, expected); +} + +TEST(AlterSchema_lint_2019_09, property_names_type_default_3) { + sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2019-09/schema", + "type": "object", + "propertyNames": { + "type": "integer" + } + })JSON"); + + LINT_AND_FIX_FOR_READABILITY(document); + + const sourcemeta::core::JSON expected = sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2019-09/schema", + "type": "object", + "propertyNames": { + "type": "integer" + } + })JSON"); + + EXPECT_EQ(document, expected); +} + +TEST(AlterSchema_lint_2019_09, property_names_default_1) { + sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2019-09/schema", + "type": "object", + "propertyNames": {} + })JSON"); + + LINT_AND_FIX_FOR_READABILITY(document); + + const sourcemeta::core::JSON expected = sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2019-09/schema", + "type": "object" + })JSON"); + + EXPECT_EQ(document, expected); +} diff --git a/test/alterschema/alterschema_lint_2020_12_test.cc b/test/alterschema/alterschema_lint_2020_12_test.cc index c427dabd3..bbad7d002 100644 --- a/test/alterschema/alterschema_lint_2020_12_test.cc +++ b/test/alterschema/alterschema_lint_2020_12_test.cc @@ -676,6 +676,85 @@ TEST(AlterSchema_lint_2020_12, duplicate_allof_branches_1) { EXPECT_EQ(document, expected); } +TEST(AlterSchema_lint_2020_12, duplicate_allof_branches_2) { + auto document = sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "allOf": [ + { "type": "number" }, + { "type": "string" }, + { "type": "number" } + ] + })JSON"); + + LINT_AND_FIX_FOR_STATIC_ANALYSIS(document); + + const auto expected = sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "number", + "multipleOf": 1, + "allOf": [ + { "type": "string", "minLength": 0 } + ] + })JSON"); + + EXPECT_EQ(document, expected); +} + +TEST(AlterSchema_lint_2020_12, duplicate_allof_branches_3) { + auto document = sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "allOf": [ + { "type": "number" }, + { "type": "number" }, + { "type": "string" } + ] + })JSON"); + + LINT_AND_FIX_FOR_STATIC_ANALYSIS(document); + + const auto expected = sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "number", + "multipleOf": 1, + "allOf": [ + { "type": "string", "minLength": 0 } + ] + })JSON"); + + EXPECT_EQ(document, expected); +} + +TEST(AlterSchema_lint_2020_12, duplicate_allof_branches_4) { + auto document = sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "allOf": [ + { "type": "number" }, + { "type": "string" }, + { "type": "number" }, + { "type": "number" }, + { "type": "number" }, + { "type": "string" }, + { "type": "string" }, + { "type": "string" }, + { "type": "number" }, + { "type": "number" } + ] + })JSON"); + + LINT_AND_FIX_FOR_STATIC_ANALYSIS(document); + + const auto expected = sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "number", + "multipleOf": 1, + "allOf": [ + { "type": "string", "minLength": 0 } + ] + })JSON"); + + EXPECT_EQ(document, expected); +} + TEST(AlterSchema_lint_2020_12, duplicate_anyof_branches_1) { sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({ "$schema": "https://json-schema.org/draft/2020-12/schema", @@ -2368,3 +2447,84 @@ TEST(AlterSchema_lint_2020_12, then_empty_4) { EXPECT_EQ(document, expected); } + +TEST(AlterSchema_lint_2020_12, property_names_type_default_1) { + sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "propertyNames": { + "type": "string", + "pattern": "^S_" + } + })JSON"); + + LINT_AND_FIX_FOR_READABILITY(document); + + const sourcemeta::core::JSON expected = sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "propertyNames": { + "pattern": "^S_" + } + })JSON"); + + EXPECT_EQ(document, expected); +} + +TEST(AlterSchema_lint_2020_12, property_names_type_default_2) { + sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "propertyNames": { + "type": [ "string" ] + } + })JSON"); + + LINT_AND_FIX_FOR_READABILITY(document); + + const sourcemeta::core::JSON expected = sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object" + })JSON"); + + EXPECT_EQ(document, expected); +} + +TEST(AlterSchema_lint_2020_12, property_names_type_default_3) { + sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "propertyNames": { + "type": "integer" + } + })JSON"); + + LINT_AND_FIX_FOR_READABILITY(document); + + const sourcemeta::core::JSON expected = sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "propertyNames": { + "type": "integer" + } + })JSON"); + + EXPECT_EQ(document, expected); +} + +TEST(AlterSchema_lint_2020_12, property_names_default_1) { + sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "propertyNames": {} + })JSON"); + + LINT_AND_FIX_FOR_READABILITY(document); + + const sourcemeta::core::JSON expected = sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object" + })JSON"); + + EXPECT_EQ(document, expected); +} diff --git a/test/alterschema/alterschema_lint_draft4_test.cc b/test/alterschema/alterschema_lint_draft4_test.cc index bdeb5a6ba..2321c065b 100644 --- a/test/alterschema/alterschema_lint_draft4_test.cc +++ b/test/alterschema/alterschema_lint_draft4_test.cc @@ -193,6 +193,85 @@ TEST(AlterSchema_lint_draft4, duplicate_allof_branches_1) { EXPECT_EQ(document, expected); } +TEST(AlterSchema_lint_draft4, duplicate_allof_branches_2) { + auto document = sourcemeta::core::parse_json(R"JSON({ + "$schema": "http://json-schema.org/draft-04/schema#", + "allOf": [ + { "type": "number" }, + { "type": "string" }, + { "type": "number" } + ] + })JSON"); + + LINT_AND_FIX_FOR_STATIC_ANALYSIS(document); + + const auto expected = sourcemeta::core::parse_json(R"JSON({ + "$schema": "http://json-schema.org/draft-04/schema#", + "type": "number", + "multipleOf": 1, + "allOf": [ + { "type": "string", "minLength": 0 } + ] + })JSON"); + + EXPECT_EQ(document, expected); +} + +TEST(AlterSchema_lint_draft4, duplicate_allof_branches_3) { + auto document = sourcemeta::core::parse_json(R"JSON({ + "$schema": "http://json-schema.org/draft-04/schema#", + "allOf": [ + { "type": "number" }, + { "type": "number" }, + { "type": "string" } + ] + })JSON"); + + LINT_AND_FIX_FOR_STATIC_ANALYSIS(document); + + const auto expected = sourcemeta::core::parse_json(R"JSON({ + "$schema": "http://json-schema.org/draft-04/schema#", + "type": "number", + "multipleOf": 1, + "allOf": [ + { "type": "string", "minLength": 0 } + ] + })JSON"); + + EXPECT_EQ(document, expected); +} + +TEST(AlterSchema_lint_draft4, duplicate_allof_branches_4) { + auto document = sourcemeta::core::parse_json(R"JSON({ + "$schema": "http://json-schema.org/draft-04/schema#", + "allOf": [ + { "type": "number" }, + { "type": "string" }, + { "type": "number" }, + { "type": "number" }, + { "type": "number" }, + { "type": "string" }, + { "type": "string" }, + { "type": "string" }, + { "type": "number" }, + { "type": "number" } + ] + })JSON"); + + LINT_AND_FIX_FOR_STATIC_ANALYSIS(document); + + const auto expected = sourcemeta::core::parse_json(R"JSON({ + "$schema": "http://json-schema.org/draft-04/schema#", + "type": "number", + "multipleOf": 1, + "allOf": [ + { "type": "string", "minLength": 0 } + ] + })JSON"); + + EXPECT_EQ(document, expected); +} + TEST(AlterSchema_lint_draft4, duplicate_anyof_branches_1) { sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({ "$schema": "http://json-schema.org/draft-04/schema#", diff --git a/test/alterschema/alterschema_lint_draft6_test.cc b/test/alterschema/alterschema_lint_draft6_test.cc index 095098da9..71a90b24b 100644 --- a/test/alterschema/alterschema_lint_draft6_test.cc +++ b/test/alterschema/alterschema_lint_draft6_test.cc @@ -328,6 +328,85 @@ TEST(AlterSchema_lint_draft6, duplicate_allof_branches_1) { EXPECT_EQ(document, expected); } +TEST(AlterSchema_lint_draft6, duplicate_allof_branches_2) { + auto document = sourcemeta::core::parse_json(R"JSON({ + "$schema": "http://json-schema.org/draft-06/schema#", + "allOf": [ + { "type": "number" }, + { "type": "string" }, + { "type": "number" } + ] + })JSON"); + + LINT_AND_FIX_FOR_STATIC_ANALYSIS(document); + + const auto expected = sourcemeta::core::parse_json(R"JSON({ + "$schema": "http://json-schema.org/draft-06/schema#", + "type": "number", + "multipleOf": 1, + "allOf": [ + { "type": "string", "minLength": 0 } + ] + })JSON"); + + EXPECT_EQ(document, expected); +} + +TEST(AlterSchema_lint_draft6, duplicate_allof_branches_3) { + auto document = sourcemeta::core::parse_json(R"JSON({ + "$schema": "http://json-schema.org/draft-06/schema#", + "allOf": [ + { "type": "number" }, + { "type": "number" }, + { "type": "string" } + ] + })JSON"); + + LINT_AND_FIX_FOR_STATIC_ANALYSIS(document); + + const auto expected = sourcemeta::core::parse_json(R"JSON({ + "$schema": "http://json-schema.org/draft-06/schema#", + "type": "number", + "multipleOf": 1, + "allOf": [ + { "type": "string", "minLength": 0 } + ] + })JSON"); + + EXPECT_EQ(document, expected); +} + +TEST(AlterSchema_lint_draft6, duplicate_allof_branches_4) { + auto document = sourcemeta::core::parse_json(R"JSON({ + "$schema": "http://json-schema.org/draft-06/schema#", + "allOf": [ + { "type": "number" }, + { "type": "string" }, + { "type": "number" }, + { "type": "number" }, + { "type": "number" }, + { "type": "string" }, + { "type": "string" }, + { "type": "string" }, + { "type": "number" }, + { "type": "number" } + ] + })JSON"); + + LINT_AND_FIX_FOR_STATIC_ANALYSIS(document); + + const auto expected = sourcemeta::core::parse_json(R"JSON({ + "$schema": "http://json-schema.org/draft-06/schema#", + "type": "number", + "multipleOf": 1, + "allOf": [ + { "type": "string", "minLength": 0 } + ] + })JSON"); + + EXPECT_EQ(document, expected); +} + TEST(AlterSchema_lint_draft6, duplicate_anyof_branches_1) { sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({ "$schema": "http://json-schema.org/draft-06/schema#", @@ -1302,3 +1381,84 @@ TEST(AlterSchema_lint_draft6, additional_items_with_schema_items_array_items) { EXPECT_EQ(document, expected); } + +TEST(AlterSchema_lint_draft6, property_names_type_default_1) { + sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({ + "$schema": "http://json-schema.org/draft-06/schema#", + "type": "object", + "propertyNames": { + "type": "string", + "pattern": "^S_" + } + })JSON"); + + LINT_AND_FIX_FOR_READABILITY(document); + + const sourcemeta::core::JSON expected = sourcemeta::core::parse_json(R"JSON({ + "$schema": "http://json-schema.org/draft-06/schema#", + "type": "object", + "propertyNames": { + "pattern": "^S_" + } + })JSON"); + + EXPECT_EQ(document, expected); +} + +TEST(AlterSchema_lint_draft6, property_names_type_default_2) { + sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({ + "$schema": "http://json-schema.org/draft-06/schema#", + "type": "object", + "propertyNames": { + "type": [ "string" ] + } + })JSON"); + + LINT_AND_FIX_FOR_READABILITY(document); + + const sourcemeta::core::JSON expected = sourcemeta::core::parse_json(R"JSON({ + "$schema": "http://json-schema.org/draft-06/schema#", + "type": "object" + })JSON"); + + EXPECT_EQ(document, expected); +} + +TEST(AlterSchema_lint_draft6, property_names_type_default_3) { + sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({ + "$schema": "http://json-schema.org/draft-06/schema#", + "type": "object", + "propertyNames": { + "type": "integer" + } + })JSON"); + + LINT_AND_FIX_FOR_READABILITY(document); + + const sourcemeta::core::JSON expected = sourcemeta::core::parse_json(R"JSON({ + "$schema": "http://json-schema.org/draft-06/schema#", + "type": "object", + "propertyNames": { + "type": "integer" + } + })JSON"); + + EXPECT_EQ(document, expected); +} + +TEST(AlterSchema_lint_draft6, property_names_default_1) { + sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({ + "$schema": "http://json-schema.org/draft-06/schema#", + "type": "object", + "propertyNames": {} + })JSON"); + + LINT_AND_FIX_FOR_READABILITY(document); + + const sourcemeta::core::JSON expected = sourcemeta::core::parse_json(R"JSON({ + "$schema": "http://json-schema.org/draft-06/schema#", + "type": "object" + })JSON"); + + EXPECT_EQ(document, expected); +} diff --git a/test/alterschema/alterschema_lint_draft7_test.cc b/test/alterschema/alterschema_lint_draft7_test.cc index a189693b0..3a4268637 100644 --- a/test/alterschema/alterschema_lint_draft7_test.cc +++ b/test/alterschema/alterschema_lint_draft7_test.cc @@ -424,6 +424,85 @@ TEST(AlterSchema_lint_draft7, duplicate_allof_branches_1) { EXPECT_EQ(document, expected); } +TEST(AlterSchema_lint_draft7, duplicate_allof_branches_2) { + auto document = sourcemeta::core::parse_json(R"JSON({ + "$schema": "http://json-schema.org/draft-07/schema#", + "allOf": [ + { "type": "number" }, + { "type": "string" }, + { "type": "number" } + ] + })JSON"); + + LINT_AND_FIX_FOR_STATIC_ANALYSIS(document); + + const auto expected = sourcemeta::core::parse_json(R"JSON({ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "number", + "multipleOf": 1, + "allOf": [ + { "type": "string", "minLength": 0 } + ] + })JSON"); + + EXPECT_EQ(document, expected); +} + +TEST(AlterSchema_lint_draft7, duplicate_allof_branches_3) { + auto document = sourcemeta::core::parse_json(R"JSON({ + "$schema": "http://json-schema.org/draft-07/schema#", + "allOf": [ + { "type": "number" }, + { "type": "number" }, + { "type": "string" } + ] + })JSON"); + + LINT_AND_FIX_FOR_STATIC_ANALYSIS(document); + + const auto expected = sourcemeta::core::parse_json(R"JSON({ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "number", + "multipleOf": 1, + "allOf": [ + { "type": "string", "minLength": 0 } + ] + })JSON"); + + EXPECT_EQ(document, expected); +} + +TEST(AlterSchema_lint_draft7, duplicate_allof_branches_4) { + auto document = sourcemeta::core::parse_json(R"JSON({ + "$schema": "http://json-schema.org/draft-07/schema#", + "allOf": [ + { "type": "number" }, + { "type": "string" }, + { "type": "number" }, + { "type": "number" }, + { "type": "number" }, + { "type": "string" }, + { "type": "string" }, + { "type": "string" }, + { "type": "number" }, + { "type": "number" } + ] + })JSON"); + + LINT_AND_FIX_FOR_STATIC_ANALYSIS(document); + + const auto expected = sourcemeta::core::parse_json(R"JSON({ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "number", + "multipleOf": 1, + "allOf": [ + { "type": "string", "minLength": 0 } + ] + })JSON"); + + EXPECT_EQ(document, expected); +} + TEST(AlterSchema_lint_draft7, duplicate_anyof_branches_1) { sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({ "$schema": "http://json-schema.org/draft-07/schema#", @@ -1599,3 +1678,43 @@ TEST(AlterSchema_lint_draft7, then_empty_4) { EXPECT_EQ(document, expected); } + +TEST(AlterSchema_lint_draft7, property_names_type_default_1) { + sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "propertyNames": { + "type": "string", + "pattern": "^S_" + } + })JSON"); + + LINT_AND_FIX_FOR_READABILITY(document); + + const sourcemeta::core::JSON expected = sourcemeta::core::parse_json(R"JSON({ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "propertyNames": { + "pattern": "^S_" + } + })JSON"); + + EXPECT_EQ(document, expected); +} + +TEST(AlterSchema_lint_draft7, property_names_default_1) { + sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "propertyNames": {} + })JSON"); + + LINT_AND_FIX_FOR_READABILITY(document); + + const sourcemeta::core::JSON expected = sourcemeta::core::parse_json(R"JSON({ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object" + })JSON"); + + EXPECT_EQ(document, expected); +} diff --git a/test/gzip/gzip_test.cc b/test/gzip/gzip_test.cc index 3d55a3bb9..32d6d450e 100644 --- a/test/gzip/gzip_test.cc +++ b/test/gzip/gzip_test.cc @@ -2,8 +2,52 @@ #include -TEST(GZIP, compress_string_1) { +#include + +TEST(GZIP, compress_stream_1) { + const auto value{"Hello World"}; + std::istringstream input{value}; + std::ostringstream output; + sourcemeta::core::gzip(input, output); + EXPECT_EQ(output.str().size(), 31); +} + +TEST(GZIP, compress_stream_to_string_1) { + const auto value{"Hello World"}; + std::istringstream input{value}; + const auto result{sourcemeta::core::gzip(input)}; + EXPECT_EQ(result.size(), 31); +} + +TEST(GZIP, compress_string_to_string_1) { const auto result{sourcemeta::core::gzip("Hello World")}; - EXPECT_TRUE(result.has_value()); - EXPECT_EQ(result.value().size(), 31); + EXPECT_EQ(result.size(), 31); +} + +TEST(GZIP, decompress_stream_1) { + const auto value{"Hello World"}; + std::istringstream input{sourcemeta::core::gzip(value)}; + std::ostringstream output; + sourcemeta::core::gunzip(input, output); + EXPECT_EQ(output.str(), value); +} + +TEST(GZIP, decompress_stream_error_1) { + std::istringstream input{"not-zlib-content"}; + std::ostringstream output; + EXPECT_THROW(sourcemeta::core::gunzip(input, output), + sourcemeta::core::GZIPError); +} + +TEST(GZIP, decompress_stream_to_string_1) { + const auto value{"Hello World"}; + std::istringstream input{sourcemeta::core::gzip(value)}; + const auto result{sourcemeta::core::gunzip(input)}; + EXPECT_EQ(result, value); +} + +TEST(GZIP, decompress_string_to_string_1) { + const auto value{"Hello World"}; + const auto result{sourcemeta::core::gunzip(sourcemeta::core::gzip(value))}; + EXPECT_EQ(result, value); } diff --git a/test/json/json_hash_test.cc b/test/json/json_hash_test.cc index 1f3094c1c..03bc3aa5b 100644 --- a/test/json/json_hash_test.cc +++ b/test/json/json_hash_test.cc @@ -7,7 +7,7 @@ TEST(JSON_key_hash, hash_empty) { hasher; const sourcemeta::core::JSON::String value{""}; const auto hash{hasher(value)}; - EXPECT_FALSE(hasher.is_perfect(hash)); + EXPECT_TRUE(hasher.is_perfect(hash)); #if defined(__SIZEOF_INT128__) EXPECT_EQ(hash.a, (__uint128_t{0x0000000000000000} << 64) | 0x0000000000000000); @@ -619,15 +619,15 @@ TEST(JSON_key_hash, hash_fooooooooooooooooooooooooooooooo) { EXPECT_FALSE(hasher.is_perfect(hash)); #if defined(__SIZEOF_INT128__) EXPECT_EQ(hash.a, - (__uint128_t{0x0000000000000000} << 64) | 0x00000000000000f6); + (__uint128_t{0x6f6f6f6f6f6f6f6f} << 64) | 0x6f6f6f6f6f6f66f6); EXPECT_EQ(hash.b, - (__uint128_t{0x0000000000000000} << 64) | 0x0000000000000000); + (__uint128_t{0x6f6f6f6f6f6f6f6f} << 64) | 0x6f6f6f6f6f6f6f6f); #else // 0x20 (length) + 0x66 (f) + 0x6f (o) - EXPECT_EQ(hash.a, 0x00000000000000f6); - EXPECT_EQ(hash.b, 0x0000000000000000); - EXPECT_EQ(hash.c, 0x0000000000000000); - EXPECT_EQ(hash.d, 0x0000000000000000); + EXPECT_EQ(hash.a, 0x6f6f6f6f6f6f66f6); + EXPECT_EQ(hash.b, 0x6f6f6f6f6f6f6f6f); + EXPECT_EQ(hash.c, 0x6f6f6f6f6f6f6f6f); + EXPECT_EQ(hash.d, 0x6f6f6f6f6f6f6f6f); #endif } @@ -643,14 +643,14 @@ TEST(JSON_key_hash, hash_no_collision) { EXPECT_FALSE(hasher.is_perfect(hash)); #if defined(__SIZEOF_INT128__) EXPECT_EQ(hash.a, - (__uint128_t{0x0000000000000000} << 64) | 0x0000000000000003); + (__uint128_t{0x6161616161616161} << 64) | 0x6161616161617A03); EXPECT_EQ(hash.b, - (__uint128_t{0x0000000000000000} << 64) | 0x0000000000000000); + (__uint128_t{0x6161616161616161} << 64) | 0x6161616161616161); #else // 0x10C (length) + 0x7A (z) + 0x7A (z) - EXPECT_EQ(hash.a, 0x0000000000000003); - EXPECT_EQ(hash.b, 0x0000000000000000); - EXPECT_EQ(hash.c, 0x0000000000000000); - EXPECT_EQ(hash.d, 0x0000000000000000); + EXPECT_EQ(hash.a, 0x6161616161617A03); + EXPECT_EQ(hash.b, 0x6161616161616161); + EXPECT_EQ(hash.c, 0x6161616161616161); + EXPECT_EQ(hash.d, 0x6161616161616161); #endif } diff --git a/test/jsonschema/CMakeLists.txt b/test/jsonschema/CMakeLists.txt index 132c3a2ca..88590febf 100644 --- a/test/jsonschema/CMakeLists.txt +++ b/test/jsonschema/CMakeLists.txt @@ -33,6 +33,7 @@ sourcemeta_googletest(NAMESPACE sourcemeta PROJECT core NAME jsonschema jsonschema_bundle_draft1_test.cc jsonschema_bundle_draft0_test.cc jsonschema_bundle_test.cc + jsonschema_dependencies_test.cc jsonschema_unidentify_test.cc jsonschema_metaschema_test.cc jsonschema_transform_rules.h diff --git a/test/jsonschema/jsonschema_bundle_test.cc b/test/jsonschema/jsonschema_bundle_test.cc index 1a82de8f2..63d722178 100644 --- a/test/jsonschema/jsonschema_bundle_test.cc +++ b/test/jsonschema/jsonschema_bundle_test.cc @@ -45,6 +45,56 @@ static auto test_resolver(std::string_view identifier) } } +TEST(JSONSchema_bundle, multiple_refs) { + sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({ + "$id": "https://www.example.com", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "properties": { + "foo": { + "$ref": "https://www.sourcemeta.com/test-3" + }, + "bar": { + "$ref": "https://www.sourcemeta.com/test-1" + } + } + })JSON"); + + sourcemeta::core::bundle(document, sourcemeta::core::schema_official_walker, + test_resolver); + + const sourcemeta::core::JSON expected = sourcemeta::core::parse_json(R"JSON({ + "$id": "https://www.example.com", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "properties": { + "foo": { + "$ref": "https://www.sourcemeta.com/test-3" + }, + "bar": { + "$ref": "https://www.sourcemeta.com/test-1" + } + }, + "$defs": { + "https://www.sourcemeta.com/test-4": { + "$schema": "http://json-schema.org/draft-04/schema#", + "id": "https://www.sourcemeta.com/test-4", + "type": "string" + }, + "https://www.sourcemeta.com/test-3": { + "$schema": "http://json-schema.org/draft-06/schema#", + "$id": "https://www.sourcemeta.com/test-3", + "allOf": [ { "$ref": "test-4" } ] + }, + "https://www.sourcemeta.com/test-1": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://www.sourcemeta.com/test-1", + "type": "string" + } + } + })JSON"); + + EXPECT_EQ(document, expected); +} + TEST(JSONSchema_bundle, across_dialects) { sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({ "$id": "https://www.example.com", @@ -221,7 +271,7 @@ TEST(JSONSchema_bundle, target_array) { } TEST(JSONSchema_bundle, custom_paths_no_external) { - auto schema{sourcemeta::core::parse_json(R"JSON({ + auto document{sourcemeta::core::parse_json(R"JSON({ "wrapper": { "$ref": "#/common/test" }, @@ -238,7 +288,7 @@ TEST(JSONSchema_bundle, custom_paths_no_external) { })JSON")}; sourcemeta::core::bundle( - schema, sourcemeta::core::schema_official_walker, test_resolver, + document, sourcemeta::core::schema_official_walker, test_resolver, "https://json-schema.org/draft/2020-12/schema", std::nullopt, sourcemeta::core::Pointer{"components"}, { @@ -263,11 +313,11 @@ TEST(JSONSchema_bundle, custom_paths_no_external) { } })JSON")}; - EXPECT_EQ(schema, expected); + EXPECT_EQ(document, expected); } TEST(JSONSchema_bundle, custom_paths_with_externals) { - auto schema{sourcemeta::core::parse_json(R"JSON({ + auto document{sourcemeta::core::parse_json(R"JSON({ "wrapper": { "$ref": "#/common/test" }, @@ -284,7 +334,7 @@ TEST(JSONSchema_bundle, custom_paths_with_externals) { })JSON")}; sourcemeta::core::bundle( - schema, sourcemeta::core::schema_official_walker, test_resolver, + document, sourcemeta::core::schema_official_walker, test_resolver, "https://json-schema.org/draft/2020-12/schema", std::nullopt, sourcemeta::core::Pointer{"components"}, { @@ -326,5 +376,5 @@ TEST(JSONSchema_bundle, custom_paths_with_externals) { } })JSON")}; - EXPECT_EQ(schema, expected); + EXPECT_EQ(document, expected); } diff --git a/test/jsonschema/jsonschema_dependencies_test.cc b/test/jsonschema/jsonschema_dependencies_test.cc new file mode 100644 index 000000000..31e7899c2 --- /dev/null +++ b/test/jsonschema/jsonschema_dependencies_test.cc @@ -0,0 +1,329 @@ +#include + +#include +#include + +#include // std::string +#include // std::string_view +#include // std::tuple +#include // std::vector + +#define EXPECT_DEPENDENCY(expected_traces, expected_index, expected_origin, \ + expected_pointer, expected_target) \ + EXPECT_EQ(std::get<0>((expected_traces).at(expected_index)), \ + std::optional{(expected_origin)}); \ + EXPECT_EQ(sourcemeta::core::to_string( \ + std::get<1>((expected_traces).at(expected_index))), \ + (expected_pointer)); \ + EXPECT_EQ(std::get<2>((expected_traces).at(expected_index)), \ + (expected_target)); + +static auto test_resolver(std::string_view identifier) + -> std::optional { + if (identifier == "https://www.sourcemeta.com/test-1") { + return sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://www.sourcemeta.com/test-1", + "type": "string" + })JSON"); + } else if (identifier == "https://www.sourcemeta.com/test-2") { + return sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2019-09/schema", + "$id": "https://www.sourcemeta.com/test-2", + "$ref": "test-3" + })JSON"); + } else if (identifier == "https://www.sourcemeta.com/test-3") { + return sourcemeta::core::parse_json(R"JSON({ + "$schema": "http://json-schema.org/draft-06/schema#", + "$id": "https://www.sourcemeta.com/test-3", + "allOf": [ { "$ref": "test-4" } ] + })JSON"); + } else if (identifier == "https://www.sourcemeta.com/test-4") { + return sourcemeta::core::parse_json(R"JSON({ + "$schema": "http://json-schema.org/draft-04/schema#", + "id": "https://www.sourcemeta.com/test-4", + "type": "string" + })JSON"); + } else if (identifier == "https://www.sourcemeta.com/no-dialect") { + return sourcemeta::core::parse_json(R"JSON({ + "foo": 1 + })JSON"); + } else if (identifier == "https://www.sourcemeta.com/array") { + return sourcemeta::core::parse_json(R"JSON([ + "foo", "bar", "baz" + ])JSON"); + } else { + return sourcemeta::core::schema_official_resolver(identifier); + } +} + +TEST(JSONSchema_dependencies, multiple_refs) { + sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({ + "$id": "https://www.example.com", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "properties": { + "foo": { + "$ref": "https://www.sourcemeta.com/test-3" + }, + "bar": { + "$ref": "https://www.sourcemeta.com/test-1" + } + } + })JSON"); + + std::vector< + std::tuple, + sourcemeta::core::Pointer, sourcemeta::core::JSON::String>> + traces; + + sourcemeta::core::dependencies( + document, sourcemeta::core::schema_official_walker, test_resolver, + [&traces](const auto &origin, const auto &pointer, const auto &target, + const auto &) { + traces.emplace_back(origin, pointer, target); + }); + + EXPECT_EQ(traces.size(), 3); + + EXPECT_DEPENDENCY(traces, 0, "https://www.example.com", + "/properties/bar/$ref", + "https://www.sourcemeta.com/test-1"); + EXPECT_DEPENDENCY(traces, 1, "https://www.example.com", + "/properties/foo/$ref", + "https://www.sourcemeta.com/test-3"); + EXPECT_DEPENDENCY(traces, 2, "https://www.sourcemeta.com/test-3", + "/allOf/0/$ref", "https://www.sourcemeta.com/test-4"); +} + +TEST(JSONSchema_dependencies, across_dialects) { + sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({ + "$id": "https://www.example.com", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "items": { "$ref": "https://www.sourcemeta.com/test-2" } + })JSON"); + + std::vector< + std::tuple, + sourcemeta::core::Pointer, sourcemeta::core::JSON::String>> + traces; + + sourcemeta::core::dependencies( + document, sourcemeta::core::schema_official_walker, test_resolver, + [&traces](const auto &origin, const auto &pointer, const auto &target, + const auto &) { + traces.emplace_back(origin, pointer, target); + }); + + EXPECT_EQ(traces.size(), 3); + + EXPECT_DEPENDENCY(traces, 0, "https://www.example.com", "/items/$ref", + "https://www.sourcemeta.com/test-2"); + EXPECT_DEPENDENCY(traces, 1, "https://www.sourcemeta.com/test-2", "/$ref", + "https://www.sourcemeta.com/test-3"); + EXPECT_DEPENDENCY(traces, 2, "https://www.sourcemeta.com/test-3", + "/allOf/0/$ref", "https://www.sourcemeta.com/test-4"); +} + +TEST(JSONSchema_dependencies, across_dialects_const) { + const sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({ + "$id": "https://www.example.com", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "items": { "$ref": "https://www.sourcemeta.com/test-2" } + })JSON"); + + std::vector< + std::tuple, + sourcemeta::core::Pointer, sourcemeta::core::JSON::String>> + traces; + + sourcemeta::core::dependencies( + document, sourcemeta::core::schema_official_walker, test_resolver, + [&traces](const auto &origin, const auto &pointer, const auto &target, + const auto &) { + traces.emplace_back(origin, pointer, target); + }); + + EXPECT_EQ(traces.size(), 3); + + EXPECT_DEPENDENCY(traces, 0, "https://www.example.com", "/items/$ref", + "https://www.sourcemeta.com/test-2"); + EXPECT_DEPENDENCY(traces, 1, "https://www.sourcemeta.com/test-2", "/$ref", + "https://www.sourcemeta.com/test-3"); + EXPECT_DEPENDENCY(traces, 2, "https://www.sourcemeta.com/test-3", + "/allOf/0/$ref", "https://www.sourcemeta.com/test-4"); +} + +TEST(JSONSchema_dependencies, with_default_id) { + sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "items": { "$ref": "test-2" } + })JSON"); + + std::vector< + std::tuple, + sourcemeta::core::Pointer, sourcemeta::core::JSON::String>> + traces; + + sourcemeta::core::dependencies( + document, sourcemeta::core::schema_official_walker, test_resolver, + [&traces](const auto &origin, const auto &pointer, const auto &target, + const auto &) { traces.emplace_back(origin, pointer, target); }, + std::nullopt, "https://www.sourcemeta.com/default"); + + EXPECT_EQ(traces.size(), 3); + + EXPECT_DEPENDENCY(traces, 0, "https://www.sourcemeta.com/default", + "/items/$ref", "https://www.sourcemeta.com/test-2"); + EXPECT_DEPENDENCY(traces, 1, "https://www.sourcemeta.com/test-2", "/$ref", + "https://www.sourcemeta.com/test-3"); + EXPECT_DEPENDENCY(traces, 2, "https://www.sourcemeta.com/test-3", + "/allOf/0/$ref", "https://www.sourcemeta.com/test-4"); +} + +TEST(JSONSchema_dependencies, with_default_dialect) { + sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({ + "properties": { + "foo": { "$ref": "https://www.sourcemeta.com/test-1" } + } + })JSON"); + + std::vector< + std::tuple, + sourcemeta::core::Pointer, sourcemeta::core::JSON::String>> + traces; + + sourcemeta::core::dependencies( + document, sourcemeta::core::schema_official_walker, test_resolver, + [&traces](const auto &origin, const auto &pointer, const auto &target, + const auto &) { traces.emplace_back(origin, pointer, target); }, + "https://json-schema.org/draft/2020-12/schema"); + + EXPECT_EQ(traces.size(), 1); + + EXPECT_DEPENDENCY(traces, 0, std::nullopt, "/properties/foo/$ref", + "https://www.sourcemeta.com/test-1"); +} + +TEST(JSONSchema_dependencies, without_default_dialect) { + sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({ + "properties": { + "foo": { "$ref": "https://www.sourcemeta.com/test-1" } + } + })JSON"); + + EXPECT_THROW( + sourcemeta::core::dependencies( + document, sourcemeta::core::schema_official_walker, test_resolver, + [](const auto &, const auto &, const auto &, const auto &) {}), + sourcemeta::core::SchemaUnknownBaseDialectError); +} + +TEST(JSONSchema_dependencies, target_no_dialect) { + sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "properties": { + "foo": { "$ref": "https://www.sourcemeta.com/no-dialect" } + } + })JSON"); + + EXPECT_THROW( + sourcemeta::core::dependencies( + document, sourcemeta::core::schema_official_walker, test_resolver, + [](const auto &, const auto &, const auto &, const auto &) {}), + sourcemeta::core::SchemaReferenceError); +} + +TEST(JSONSchema_dependencies, target_array) { + sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "properties": { + "foo": { "$ref": "https://www.sourcemeta.com/array" } + } + })JSON"); + + EXPECT_THROW( + sourcemeta::core::dependencies( + document, sourcemeta::core::schema_official_walker, test_resolver, + [](const auto &, const auto &, const auto &, const auto &) {}), + sourcemeta::core::SchemaReferenceError); +} + +TEST(JSONSchema_dependencies, custom_paths_no_external) { + auto document{sourcemeta::core::parse_json(R"JSON({ + "wrapper": { + "$ref": "#/common/test" + }, + "common": { + "test": { + "$ref": "#/common/with-id" + }, + "with-id": { + "$id": "https://www.sourcemeta.com/schema", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "string" + } + } + })JSON")}; + + std::vector< + std::tuple, + sourcemeta::core::Pointer, sourcemeta::core::JSON::String>> + traces; + + sourcemeta::core::dependencies( + document, sourcemeta::core::schema_official_walker, test_resolver, + [&traces](const auto &origin, const auto &pointer, const auto &target, + const auto &) { traces.emplace_back(origin, pointer, target); }, + "https://json-schema.org/draft/2020-12/schema", std::nullopt, + { + sourcemeta::core::Pointer{"wrapper"}, + sourcemeta::core::Pointer{"common", "test"}, + sourcemeta::core::Pointer{"common", "with-id"}, + }); + + EXPECT_EQ(traces.size(), 0); +} + +TEST(JSONSchema_dependencies, custom_paths_with_externals) { + auto document{sourcemeta::core::parse_json(R"JSON({ + "wrapper": { + "$ref": "#/common/test" + }, + "common": { + "test": { + "$ref": "#/common/with-id" + }, + "with-id": { + "$id": "https://www.sourcemeta.com/schema", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$ref": "test-2" + } + } + })JSON")}; + + std::vector< + std::tuple, + sourcemeta::core::Pointer, sourcemeta::core::JSON::String>> + traces; + + sourcemeta::core::dependencies( + document, sourcemeta::core::schema_official_walker, test_resolver, + [&traces](const auto &origin, const auto &pointer, const auto &target, + const auto &) { traces.emplace_back(origin, pointer, target); }, + "https://json-schema.org/draft/2020-12/schema", std::nullopt, + { + sourcemeta::core::Pointer{"wrapper"}, + sourcemeta::core::Pointer{"common", "test"}, + sourcemeta::core::Pointer{"common", "with-id"}, + }); + + EXPECT_EQ(traces.size(), 3); + + // TODO: We should be getting the nested identifier here + EXPECT_DEPENDENCY(traces, 0, std::nullopt, "/common/with-id/$ref", + "https://www.sourcemeta.com/test-2"); + EXPECT_DEPENDENCY(traces, 1, "https://www.sourcemeta.com/test-2", "/$ref", + "https://www.sourcemeta.com/test-3"); + EXPECT_DEPENDENCY(traces, 2, "https://www.sourcemeta.com/test-3", + "/allOf/0/$ref", "https://www.sourcemeta.com/test-4"); +} diff --git a/test/jsonschema/jsonschema_transformer_test.cc b/test/jsonschema/jsonschema_transformer_test.cc index 3a0201b30..d2630e027 100644 --- a/test/jsonschema/jsonschema_transformer_test.cc +++ b/test/jsonschema/jsonschema_transformer_test.cc @@ -469,12 +469,14 @@ TEST(JSONSchema_transformer, check_top_level) { })JSON"); TestTransformTraces entries; - const bool result = + const auto result = bundle.check(document, sourcemeta::core::schema_official_walker, sourcemeta::core::schema_official_resolver, transformer_callback_trace(entries)); - EXPECT_FALSE(result); + EXPECT_FALSE(result.first); + EXPECT_EQ(result.second, 0); + EXPECT_EQ(entries.size(), 2); EXPECT_EQ(std::get<0>(entries.at(0)), sourcemeta::core::Pointer{}); @@ -504,15 +506,55 @@ TEST(JSONSchema_transformer, check_no_match) { })JSON"); TestTransformTraces entries; - const bool result = + const auto result = bundle.check(document, sourcemeta::core::schema_official_walker, sourcemeta::core::schema_official_resolver, transformer_callback_trace(entries)); - EXPECT_TRUE(result); + EXPECT_TRUE(result.first); + EXPECT_EQ(result.second, 100); + EXPECT_TRUE(entries.empty()); } +TEST(JSONSchema_transformer, check_partial_match) { + sourcemeta::core::SchemaTransformer bundle; + bundle.add(); + + sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "properties": { + "foo": { + "type": "string" + }, + "bar": { + "type": "string", + "foo": 1 + }, + "baz": { + "type": "string" + } + } + })JSON"); + + TestTransformTraces entries; + const auto result = + bundle.check(document, sourcemeta::core::schema_official_walker, + sourcemeta::core::schema_official_resolver, + transformer_callback_trace(entries)); + + EXPECT_FALSE(result.first); + EXPECT_EQ(result.second, 75); + + EXPECT_EQ(entries.size(), 1); + + EXPECT_EQ(std::get<0>(entries.at(0)), + sourcemeta::core::Pointer({"properties", "bar"})); + EXPECT_EQ(std::get<1>(entries.at(0)), "example_rule_1"); + EXPECT_EQ(std::get<2>(entries.at(0)), "Keyword foo is not permitted"); + EXPECT_EQ(std::get<3>(entries.at(0)), ""); +} + TEST(JSONSchema_transformer, check_empty) { sourcemeta::core::SchemaTransformer bundle; sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({ @@ -521,12 +563,14 @@ TEST(JSONSchema_transformer, check_empty) { })JSON"); TestTransformTraces entries; - const bool result = + const auto result = bundle.check(document, sourcemeta::core::schema_official_walker, sourcemeta::core::schema_official_resolver, transformer_callback_trace(entries)); - EXPECT_TRUE(result); + EXPECT_TRUE(result.first); + EXPECT_EQ(result.second, 100); + EXPECT_TRUE(entries.empty()); } @@ -563,13 +607,15 @@ TEST(JSONSchema_transformer, check_with_default_dialect) { })JSON"); TestTransformTraces entries; - const bool result = + const auto result = bundle.check(document, sourcemeta::core::schema_official_walker, sourcemeta::core::schema_official_resolver, transformer_callback_trace(entries), "https://json-schema.org/draft/2020-12/schema"); - EXPECT_FALSE(result); + EXPECT_FALSE(result.first); + EXPECT_EQ(result.second, 0); + EXPECT_EQ(entries.size(), 2); EXPECT_EQ(std::get<0>(entries.at(0)), sourcemeta::core::Pointer{}); @@ -701,12 +747,12 @@ TEST(JSONSchema_transformer, unfixable_check) { })JSON"); TestTransformTraces entries; - const bool result = + const auto result = bundle.check(document, sourcemeta::core::schema_official_walker, sourcemeta::core::schema_official_resolver, transformer_callback_trace(entries)); - EXPECT_FALSE(result); + EXPECT_FALSE(result.first); EXPECT_EQ(entries.size(), 1); EXPECT_EQ(std::get<0>(entries.at(0)), sourcemeta::core::Pointer{}); From ebbfd7d7fb5726e6eec40ade34dd378ec27c3f7a Mon Sep 17 00:00:00 2001 From: Balakrishna Avulapati Date: Sun, 27 Jul 2025 17:42:47 +0530 Subject: [PATCH 3/6] ignore linting error Signed-off-by: Balakrishna Avulapati --- src/core/uri/uri.cc | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/core/uri/uri.cc b/src/core/uri/uri.cc index 7744e87b2..79dff5960 100644 --- a/src/core/uri/uri.cc +++ b/src/core/uri/uri.cc @@ -213,12 +213,14 @@ auto URI::parse() -> void { if (this->is_urn() || this->is_tag()) { const auto part{uri_text_range(&segment->text)}; assert(part.has_value()); + // NOLINTNEXTLINE(bugprone-unchecked-optional-access) path << part.value(); } else { bool first{true}; while (segment) { const auto part{uri_text_range(&segment->text)}; assert(part.has_value()); + // NOLINTNEXTLINE(bugprone-unchecked-optional-access) const auto value{part.value()}; if (first) { From 095e7af1fa2b7e9012e3a1401e7a90757f4ffa8a Mon Sep 17 00:00:00 2001 From: Balakrishna Avulapati Date: Sun, 27 Jul 2025 17:53:59 +0530 Subject: [PATCH 4/6] Fix merge lost enablement Signed-off-by: Balakrishna Avulapati --- cmake/common/clang-tidy.json | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/cmake/common/clang-tidy.json b/cmake/common/clang-tidy.json index 49abc79ba..66c67280b 100644 --- a/cmake/common/clang-tidy.json +++ b/cmake/common/clang-tidy.json @@ -1,5 +1,10 @@ { - "Checks": "-*, bugprone-*, -bugprone-easily-swappable-parameters,-bugprone-unchecked-optional-access, concurrency-*, modernize-*, performance-*, portability-*", + "Checks": "-*, + bugprone-*,-bugprone-easily-swappable-parameters, + concurrency-*, + modernize-*, + performance-*, + portability-*", "WarningsAsErrors": "*", "FormatStyle": "none", "UseColor": true From 46b694b5b05c67e0b0126d43ed6fdd4d95d5e11a Mon Sep 17 00:00:00 2001 From: Balakrishna Avulapati Date: Sun, 27 Jul 2025 18:10:04 +0530 Subject: [PATCH 5/6] fix errors reported on CI Signed-off-by: Balakrishna Avulapati --- src/core/jsonschema/jsonschema.cc | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/core/jsonschema/jsonschema.cc b/src/core/jsonschema/jsonschema.cc index 0c523e084..fb1e588a9 100644 --- a/src/core/jsonschema/jsonschema.cc +++ b/src/core/jsonschema/jsonschema.cc @@ -626,6 +626,7 @@ auto sourcemeta::core::reference_visit( assert(property.second.is_string()); assert(walker_result.vocabulary.has_value()); sourcemeta::core::URI reference{property.second.to_string()}; + // NOLINTNEXTLINE(bugprone-unchecked-optional-access) callback(subschema, base, walker_result.vocabulary.value(), property.first, reference); } @@ -685,6 +686,7 @@ auto sourcemeta::core::unidentify( } assert(entry.base_dialect.has_value()); + // NOLINTNEXTLINE(bugprone-unchecked-optional-access) sourcemeta::core::anonymize(subschema, entry.base_dialect.value()); if (entry.vocabularies.contains( From bf60ec47526efa40ae5bda2d20ad46640efe7013 Mon Sep 17 00:00:00 2001 From: Balakrishna Avulapati Date: Sun, 27 Jul 2025 18:17:48 +0530 Subject: [PATCH 6/6] fix clang-tidy issues reported on CI Signed-off-by: Balakrishna Avulapati --- src/core/jsonschema/walker.cc | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/core/jsonschema/walker.cc b/src/core/jsonschema/walker.cc index 1e19a2602..4b6af5c34 100644 --- a/src/core/jsonschema/walker.cc +++ b/src/core/jsonschema/walker.cc @@ -42,6 +42,7 @@ auto walk(const std::optional &parent, sourcemeta::core::SchemaIdentificationStrategy::Strict, maybe_current_dialect)}; const auto is_schema_resource{level == 0 || id.has_value()}; + // NOLINTNEXTLINE(bugprone-unchecked-optional-access) const auto current_dialect{is_schema_resource ? maybe_current_dialect.value() : dialect}; const auto current_base_dialect{ @@ -422,6 +423,7 @@ sourcemeta::core::SchemaIterator::SchemaIterator( assert(base_dialect.has_value()); walk(std::nullopt, pointer, instance_location, instance_location, this->subschemas, schema, walker, resolver, dialect.value(), + // NOLINTNEXTLINE(bugprone-unchecked-optional-access) base_dialect.value(), SchemaWalkerType_t::Deep, 0, false); } } @@ -441,6 +443,7 @@ sourcemeta::core::SchemaIteratorFlat::SchemaIteratorFlat( assert(base_dialect.has_value()); walk(std::nullopt, pointer, instance_location, instance_location, this->subschemas, schema, walker, resolver, dialect.value(), + // NOLINTNEXTLINE(bugprone-unchecked-optional-access) base_dialect.value(), SchemaWalkerType_t::Flat, 0, false); } }