From fb308092c2d3d7e7ba0d03457b66fd652cdf76cf Mon Sep 17 00:00:00 2001 From: Juan Cruz Viotti Date: Fri, 25 Sep 2026 10:20:37 -0300 Subject: [PATCH 1/7] Support bundling OpenAPI descriptions in the `bundle` command Signed-off-by: Juan Cruz Viotti --- docs/bundle.markdown | 21 ++ src/command_bundle.cc | 54 ++++- src/command_lint.cc | 15 +- src/error.h | 40 +++- src/resolver.h | 209 ++++++++++++++++-- src/utils.h | 15 ++ test/CMakeLists.txt | 5 + .../fail_openapi_unresolved_document.clitest | 49 ++++ test/bundle/fail_openapi_without_id.clitest | 33 +++ .../pass_openapi_schema_reference.clitest | 77 +++++++ test/bundle/pass_openapi_self.clitest | 60 +++++ test/bundle/pass_openapi_yaml.clitest | 51 +++++ test/lint/fail_lint_openapi_invalid.clitest | 8 +- ..._no_identifier_ref_without_resolve.clitest | 2 +- 14 files changed, 597 insertions(+), 42 deletions(-) create mode 100644 test/bundle/fail_openapi_unresolved_document.clitest create mode 100644 test/bundle/fail_openapi_without_id.clitest create mode 100644 test/bundle/pass_openapi_schema_reference.clitest create mode 100644 test/bundle/pass_openapi_self.clitest create mode 100644 test/bundle/pass_openapi_yaml.clitest diff --git a/docs/bundle.markdown b/docs/bundle.markdown index d33b8441d..6515a9669 100644 --- a/docs/bundle.markdown +++ b/docs/bundle.markdown @@ -53,6 +53,27 @@ two spaces. Use `--indentation/-n` to set the width in either case. > implementations like Visual Studio Code, and avoid using the resulting schema > in any other way. +OpenAPI Descriptions +-------------------- + +If the input is an OpenAPI spec v3.1 or v3.2, the `bundle` command treats it as +an [OpenAPI](https://spec.openapis.org/oas/latest.html) description rather than +as a schema, and bundles it as one: every other document the description spans +is embedded into the Components Object, and every reference that reached one is +rewritten to name where it landed. Any other revision, such as v3.0, is +rejected rather than read as a schema. + +Use `--resolve/-r` for the other documents of the description exactly as you +would for schemas. Each input is sorted by what it turns out to be, so one set +of `--resolve` options may name both descriptions and schemas, and a Schema +Object reaching an external schema is bundled as JSON Schema bundling would +bundle it. + +> [!NOTE] +> The `--without-id`/`-w` option is not supported for OpenAPI descriptions, as +> removing identifiers is a schema operation and there is no description left to +> read once its Schema Objects can no longer say who they are. + Examples -------- diff --git a/src/command_bundle.cc b/src/command_bundle.cc index 4729e5e05..a3b316ec3 100644 --- a/src/command_bundle.cc +++ b/src/command_bundle.cc @@ -33,8 +33,6 @@ auto sourcemeta::jsonschema::bundle(const sourcemeta::core::Options &options) const auto schema_config_base{ schema_from_stdin ? std::filesystem::current_path() : schema_path}; - const auto schema_display_path{schema_from_stdin ? stdin_path() - : schema_path}; const auto configuration_path{ find_configuration(options, schema_config_base)}; @@ -45,6 +43,15 @@ auto sourcemeta::jsonschema::bundle(const sourcemeta::core::Options &options) ? read_from_stdin(nullptr, InputFormatting::Preserve) : read_file(schema_path, InputFormatting::Preserve)}; + // An OpenAPI description is not a schema, so what it goes by wherever we + // report on it is an identity of its own rather than the one a schema from + // the same place would take + const auto is_openapi{ + sourcemeta::core::openapi_version(parsed_schema.document).has_value()}; + const auto schema_display_path{ + schema_from_stdin ? (is_openapi ? openapi_stdin_path() : stdin_path()) + : schema_path}; + if (parsed_schema.multidocument) { throw MultiDocumentInputError{ "This command does not support input with multiple documents", @@ -58,12 +65,36 @@ auto sourcemeta::jsonschema::bundle(const sourcemeta::core::Options &options) reject_unsupported_openapi(parsed_schema.document, schema_display_path); + // Removing identifiers is a schema operation, and there is no description to + // read once the Schema Objects it holds can no longer say who they are + if (is_openapi && options.contains("without-id")) { + throw sourcemeta::core::FileError( + schema_display_path); + } + auto &schema{parsed_schema.document}; + const auto openapi_base{sourcemeta::jsonschema::openapi_default_id( + schema_path, schema_from_stdin)}; + const auto &custom_resolver{ resolver(options, options.contains("http"), dialect, configuration)}; try { + if (is_openapi) { + sourcemeta::core::openapi_bundle( + schema, sourcemeta::core::schema_walker, custom_resolver, + openapi_resolver(options, options.contains("http"), dialect, + configuration), + {.default_base = openapi_base}); + // TODO: Order the keys of a bundled description once Core grows the + // equivalent of `sourcemeta::core::schema_format` for one. The shell of + // a description has no ordering of its own to apply here + sourcemeta::jsonschema::write_schema(schema, std::cout, indentation, + parsed_schema.roundtrip); + return; + } + sourcemeta::core::schema_bundle( schema, sourcemeta::core::schema_walker, custom_resolver, dialect, sourcemeta::jsonschema::default_id(schema_path, schema_from_stdin)); @@ -87,6 +118,25 @@ auto sourcemeta::jsonschema::bundle(const sourcemeta::core::Options &options) } sourcemeta::jsonschema::format_schema(schema, custom_resolver, dialect); + } catch (const sourcemeta::core::OpenAPIResolutionError &error) { + throw sourcemeta::core::FileError( + schema_display_path, error); + } catch (const sourcemeta::core::OpenAPIReferenceError &error) { + throw sourcemeta::core::FileError( + schema_display_path, error); + } catch (const sourcemeta::core::OpenAPIError &error) { + const auto position{error.base() == openapi_base + ? parsed_schema.positions.get(error.location()) + : std::nullopt}; + if (position.has_value()) { + throw PositionError< + sourcemeta::core::FileError>( + std::get<0>(position.value()), std::get<1>(position.value()), + schema_display_path, error); + } + + throw sourcemeta::core::FileError( + schema_display_path, error); } catch (const sourcemeta::core::SchemaKeywordError &error) { throw sourcemeta::core::FileError( schema_display_path, error); diff --git a/src/command_lint.cc b/src/command_lint.cc index 47bd868ae..f0ae106b8 100644 --- a/src/command_lint.cc +++ b/src/command_lint.cc @@ -170,17 +170,6 @@ static auto get_lint_callback(sourcemeta::core::JSON &errors_array, }; } -// An OpenAPI description declares no identifier of its own under the revisions -// we support, so the one it is linted under is where it came from -static auto openapi_default_id(const sourcemeta::jsonschema::InputJSON &entry) - -> std::string { - if (entry.from_stdin) { - return std::string{sourcemeta::jsonschema::STDIN_OPENAPI_DEFAULT_ID}; - } - - return sourcemeta::jsonschema::default_id(entry); -} - // An OpenAPI description that comes from standard input is not a schema, so it // goes by an identifier of its own wherever we report on it static auto @@ -204,7 +193,7 @@ check_openapi(const sourcemeta::blaze::SchemaTransformer &bundle, -> std::pair { const sourcemeta::core::OpenAPIFrame frame{ entry.second, sourcemeta::core::schema_walker, resolver, - openapi_default_id(entry)}; + sourcemeta::jsonschema::openapi_default_id(entry)}; return bundle.check(entry.second, frame.schemas(), sourcemeta::core::schema_walker, resolver, callback, sourcemeta::core::JSON::String{EXCLUDE_KEYWORD}); @@ -217,7 +206,7 @@ apply_openapi(const sourcemeta::blaze::SchemaTransformer &bundle, const sourcemeta::core::SchemaResolver &resolver, const sourcemeta::blaze::SchemaTransformer::Callback &callback) -> std::pair { - const auto default_base{openapi_default_id(entry)}; + const auto default_base{sourcemeta::jsonschema::openapi_default_id(entry)}; std::optional frame; return bundle.apply( document, diff --git a/src/error.h b/src/error.h index 4f6848eea..e5a10f040 100644 --- a/src/error.h +++ b/src/error.h @@ -178,6 +178,13 @@ class UnsupportedOpenAPIFormatError : public std::runtime_error { "The --format option is not supported for OpenAPI descriptions"} {} }; +class UnsupportedOpenAPIWithoutIdError : public std::runtime_error { +public: + UnsupportedOpenAPIWithoutIdError() + : std::runtime_error{"The --without-id option is not supported for " + "OpenAPI descriptions"} {} +}; + class UnsupportedOpenAPIVersionError : public std::runtime_error { public: UnsupportedOpenAPIVersionError(std::string value) @@ -1034,6 +1041,20 @@ inline auto print_exception(const bool is_json, const Exception &exception) } } + // An OpenAPI error names which document of a description is at fault, which + // is not always the one the command was handed, and it spells that as a URI + // rather than holding one + if constexpr (requires(const Exception ¤t) { + { current.base() } -> std::convertible_to; + }) { + if (is_json) { + error_json.assign("baseURI", + sourcemeta::core::JSON{std::string{exception.base()}}); + } else { + std::cerr << " at base uri " << exception.base() << "\n"; + } + } + if constexpr (requires(const Exception ¤t) { current.base().recompose(); }) { @@ -1195,6 +1216,11 @@ inline auto try_catch(const sourcemeta::core::Options &options, const auto is_json{options.contains("json")}; print_exception(is_json, error); return EXIT_NOT_SUPPORTED; + } catch (const sourcemeta::core::FileError + &error) { + const auto is_json{options.contains("json")}; + print_exception(is_json, error); + return EXIT_NOT_SUPPORTED; } catch (const UnsupportedDialectCodegenError &error) { const auto is_json{options.contains("json")}; print_exception(is_json, error); @@ -1511,7 +1537,8 @@ inline auto try_catch(const sourcemeta::core::Options &options, print_exception(is_json, error); if (!is_json) { if (error.identifier().starts_with("file://")) { - std::cerr << "\nThis is likely because the file does not exist\n"; + std::cerr << "\nThis is likely because the file does not exist, or " + "does not hold a JSON Schema\n"; } else { std::cerr << "\nThis is likely because you forgot to import such schema " @@ -1592,6 +1619,17 @@ inline auto try_catch(const sourcemeta::core::Options &options, const auto is_json{options.contains("json")}; print_exception(is_json, error); return EXIT_SCHEMA_INPUT_ERROR; + } catch (const sourcemeta::core::FileError< + sourcemeta::core::OpenAPIResolutionError> &error) { + const auto is_json{options.contains("json")}; + print_exception(is_json, error); + return EXIT_SCHEMA_INPUT_ERROR; + } catch ( + const sourcemeta::core::FileError + &error) { + const auto is_json{options.contains("json")}; + print_exception(is_json, error); + return EXIT_SCHEMA_INPUT_ERROR; } catch (const sourcemeta::core::FileError &error) { const auto is_json{options.contains("json")}; diff --git a/src/resolver.h b/src/resolver.h index 78bb2bc2e..411f6ae46 100644 --- a/src/resolver.h +++ b/src/resolver.h @@ -6,6 +6,7 @@ #include #include #include +#include #include #include #include @@ -408,6 +409,32 @@ declares_identifier(const std::unordered_set &identifiers, return canonical != identifier && identifiers.contains(canonical); } +// OpenAPI Specification 3.2.1, Section 4.1 lets a document name itself with +// `$self`, "which also serves as its base URI", resolved against wherever the +// document was retrieved from. RFC 3986 Section 5.2.2 never resolves a +// reference against a fragment, so one written there is no part of the base +static inline auto openapi_self_identity(const sourcemeta::core::JSON &document, + const std::string &retrieval) + -> std::optional { + if (!document.is_object()) { + return std::nullopt; + } + + const auto *self{document.try_at("$self")}; + if (self == nullptr || !self->is_string()) { + return std::nullopt; + } + + try { + sourcemeta::core::URI uri{self->to_string()}; + uri.resolve_from(sourcemeta::core::URI{retrieval}); + uri.canonicalize(); + return uri.recompose_without_fragment(); + } catch (const sourcemeta::core::URIParseError &) { + return std::nullopt; + } +} + class CustomResolver { public: CustomResolver( @@ -597,7 +624,10 @@ class CustomResolver { return added_any_schema; } - auto operator()(std::string_view identifier) const + // An OpenAPI Description is never a schema, so this hands one back under no + // circumstance. What a fetch turns up that is one is deposited among the + // descriptions instead, where only the resolver below can reach it + auto operator()(std::string_view identifier) -> sourcemeta::core::SchemaResolverResult { const std::string string_identifier{identifier}; const auto mapped_result = this->configuration_.and_then( @@ -624,24 +654,76 @@ class CustomResolver { return std::nullopt; } + // An OpenAPI description is not a schema, so what was sorted among them is + // unavailable here. Saying so is what tells a reference that goes looking + // for one apart from a reference to something nobody supplied + if (this->descriptions_.contains(target)) { + LOG_VERBOSE(this->options_) + << "Not available as a schema, as this was read as an OpenAPI " + "description: " + << target << "\n"; + return std::nullopt; + } + + const auto cached{this->fetched_.find(target)}; + if (cached != this->fetched_.cend()) { + return cached->second; + } + auto fetched{fetch_schema(this->options_, target, this->remote_)}; if (!fetched.has_value()) { return fetched; } - // Only a schema that declares no identifier of its own needs one, and - // taking ownership just to set it would defeat handing schemas back by - // reference - const auto base_dialect{ - anonymous_base_dialect(fetched.value(), std::ref(*this))}; - if (!base_dialect.has_value()) { - return fetched; + auto document{std::move(fetched).to_owned()}; + if (sourcemeta::core::openapi_version(document).has_value()) { + LOG_VERBOSE(this->options_) + << "Not available as a schema, as this was read as an OpenAPI " + "description: " + << target << "\n"; + this->descriptions_.emplace(target, std::move(document)); + return std::nullopt; + } + + // Only a schema that declares no identifier of its own needs one + const auto base_dialect{anonymous_base_dialect(document, std::ref(*this))}; + if (base_dialect.has_value()) { + sourcemeta::core::schema_reidentify(document, string_identifier, + base_dialect.value()); + } + + return this->fetched_.emplace(target, std::move(document)).first->second; + } + + // The other half of the split. An OpenAPI Description is what this hands + // back and the only thing it ever does, so a schema that a fetch turns up + // goes to the resolver above rather than being reported from here + auto openapi(std::string_view identifier) + -> sourcemeta::core::OpenAPIResolverResult { + const std::string target{canonical_resolve_key(identifier)}; + + const auto match{this->descriptions_.find(target)}; + if (match != this->descriptions_.cend()) { + return match->second; + } + + if (this->fetched_.contains(target)) { + return std::nullopt; } - auto schema{std::move(fetched).to_owned()}; - sourcemeta::core::schema_reidentify(schema, string_identifier, - base_dialect.value()); - return schema; + auto fetched{fetch_schema(this->options_, target, this->remote_)}; + if (!fetched.has_value()) { + return std::nullopt; + } + + auto document{std::move(fetched).to_owned()}; + if (!sourcemeta::core::openapi_version(document).has_value()) { + this->fetched_.emplace(target, std::move(document)); + return std::nullopt; + } + + return this->descriptions_.emplace(target, std::move(document)) + .first->second; } private: @@ -685,8 +767,37 @@ class CustomResolver { } } + // A description answers to the `$self` it declares, and to where it came + // from either way, mirroring how an installed dependency is registered both + // by the identifier it declares and by the URI it was imported under + auto import_description(const InputJSON &entry) -> void { + const auto retrieval{sourcemeta::jsonschema::default_id(entry)}; + LOG_DEBUG(this->options_) + << "Importing OpenAPI description into the resolution context: " + << retrieval << "\n"; + + const auto self{openapi_self_identity(entry.second, retrieval)}; + if (self.has_value()) { + this->descriptions_.emplace(canonical_resolve_key(self.value()), + entry.second); + } + + this->descriptions_.emplace(canonical_resolve_key(retrieval), entry.second); + } + auto import_entry(const InputJSON &entry, const std::string_view default_dialect) -> void { + // What a description holds is the business of the OpenAPI resolver, and + // framing it as a schema would both fail and register nonsense. Only a + // revision we can read is taken, as one we cannot is no more a description + // we can answer for than a schema + if (sourcemeta::core::openapi_version(entry.second).has_value()) { + this->import_description(entry); + return; + } + + reject_unsupported_openapi(entry.second, entry.resolution_base); + LOG_DEBUG(this->options_) << "Detecting schema resources from file: " << entry.first << "\n"; @@ -768,6 +879,12 @@ class CustomResolver { } std::map schemas_{}; + // Kept wholly apart from the schemas above, so that neither resolver can + // reach what the other answers for + std::map descriptions_{}; + // What resolution has already retrieved and found not to be a description, + // so that one URL is fetched once however many times it is asked for + std::map fetched_{}; std::map> origins_{}; @@ -778,30 +895,76 @@ class CustomResolver { std::unordered_set pending_identifiers_{}; }; +using ResolverCacheKey = std::pair; + +// Both halves of the split answer from one of these, so that what the schemas +// and what the descriptions were sorted into stays one set rather than two +// that disagree +inline auto resolver_instance( + const sourcemeta::core::Options &options, const bool remote, + const std::string_view default_dialect, + const std::optional &configuration) + -> CustomResolver & { + static std::map resolver_cache; + const ResolverCacheKey cache_key{remote, std::string{default_dialect}}; + const auto match{resolver_cache.find(cache_key)}; + if (match != resolver_cache.cend()) { + return match->second; + } + + return resolver_cache + .emplace(std::piecewise_construct, std::forward_as_tuple(cache_key), + std::forward_as_tuple(options, configuration, remote, + default_dialect)) + .first->second; +} + inline auto resolver(const sourcemeta::core::Options &options, const bool remote, const std::string_view default_dialect, const std::optional &configuration) -> const sourcemeta::core::SchemaResolver & { - using CacheKey = std::pair; - static std::map resolver_cache; // What callers get is a handle that refers back to the cached resolver, // as the resolver itself must never be copied into the callee - static std::map handle_cache; - const CacheKey cache_key{remote, std::string{default_dialect}}; + static std::map + handle_cache; + const ResolverCacheKey cache_key{remote, std::string{default_dialect}}; + + const auto handle{handle_cache.find(cache_key)}; + if (handle != handle_cache.cend()) { + return handle->second; + } + + return handle_cache + .emplace(cache_key, std::ref(resolver_instance( + options, remote, default_dialect, configuration))) + .first->second; +} + +// The OpenAPI half, which answers from the same instance as the schema half +// above and holds to the same separation: a schema is never reported from here +inline auto openapi_resolver( + const sourcemeta::core::Options &options, const bool remote, + const std::string_view default_dialect, + const std::optional &configuration) + -> const sourcemeta::core::OpenAPIResolver & { + static std::map + handle_cache; + const ResolverCacheKey cache_key{remote, std::string{default_dialect}}; const auto handle{handle_cache.find(cache_key)}; if (handle != handle_cache.cend()) { return handle->second; } - const auto iterator{ - resolver_cache - .emplace(std::piecewise_construct, std::forward_as_tuple(cache_key), - std::forward_as_tuple(options, configuration, remote, - default_dialect)) - .first}; - return handle_cache.emplace(cache_key, std::ref(iterator->second)) + auto &instance{ + resolver_instance(options, remote, default_dialect, configuration)}; + return handle_cache + .emplace(cache_key, + [&instance](const std::string_view identifier) + -> sourcemeta::core::OpenAPIResolverResult { + return instance.openapi(identifier); + }) .first->second; } diff --git a/src/utils.h b/src/utils.h index 86fdcdd43..636cab975 100644 --- a/src/utils.h +++ b/src/utils.h @@ -77,6 +77,21 @@ inline auto default_id(const InputJSON &entry) -> std::string { return default_id(entry.resolution_base, entry.from_stdin); } +// An OpenAPI description declares no identifier of its own under the revisions +// we support, so the one it is read under is where it came from +inline auto openapi_default_id(const std::filesystem::path &schema_path, + const bool from_stdin) -> std::string { + if (from_stdin) { + return std::string{STDIN_OPENAPI_DEFAULT_ID}; + } + + return default_id(schema_path, from_stdin); +} + +inline auto openapi_default_id(const InputJSON &entry) -> std::string { + return openapi_default_id(entry.resolution_base, entry.from_stdin); +} + inline auto resolve_entrypoint(const sourcemeta::core::SchemaFrame &frame, const std::string_view entrypoint) -> std::string { diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 60e495325..e352c7ca1 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -702,6 +702,11 @@ add_jsonschema_test(bundle/fail_openapi_version) add_jsonschema_test(bundle/fail_openapi_version_newer) add_jsonschema_test(bundle/fail_openapi_version_malformed) add_jsonschema_test(bundle/pass_openapi_version_not_a_string) +add_jsonschema_test(bundle/pass_openapi_schema_reference) +add_jsonschema_test(bundle/pass_openapi_self) +add_jsonschema_test(bundle/pass_openapi_yaml) +add_jsonschema_test(bundle/fail_openapi_without_id) +add_jsonschema_test(bundle/fail_openapi_unresolved_document) add_jsonschema_test(bundle/fail_default_dialect_config_extension_mismatch) add_jsonschema_test(bundle/pass_bigint) add_jsonschema_test(bundle/pass_resolve_default_dialect_config) diff --git a/test/bundle/fail_openapi_unresolved_document.clitest b/test/bundle/fail_openapi_unresolved_document.clitest new file mode 100644 index 000000000..80785b0ed --- /dev/null +++ b/test/bundle/fail_openapi_unresolved_document.clitest @@ -0,0 +1,49 @@ +WRITE openapi.json UNTIL EOF +{ + "openapi": "3.2.0", + "info": { "title": "Example", "version": "1.0.0" }, + "paths": { + "/pets": { + "get": { + "responses": { + "200": { "$ref": "https://example.com/shared#/components/responses/Ok" } + } + } + } + } +} +EOF + +// Schema input error +RUN bundle openapi.json STDIN /dev/null IN . INTO result_0.txt EXPECTING 4 + +REPLACE $CWD_URI WITH '[CWD_URI]' IN result_0.txt +REPLACE $CWD WITH '[CWD]' IN result_0.txt + +WRITE expected_0.txt UNTIL EOF +2> error: Could not resolve the reference to an external document +2> at identifier https://example.com/shared +2> at file path [CWD]/openapi.json +2> at location "/paths/~1pets/get/responses/200/$ref" +2> at base uri [CWD_URI]/openapi.json +EOF + +COMPARE result_0.txt AGAINST expected_0.txt + +// Schema input error +RUN bundle openapi.json --json STDIN /dev/null IN . INTO result_1.txt EXPECTING 4 + +REPLACE $CWD_URI WITH '[CWD_URI]' IN result_1.txt +REPLACE $CWD WITH '[CWD]' IN result_1.txt + +WRITE expected_1.txt UNTIL EOF +1> { +1> "error": "Could not resolve the reference to an external document", +1> "identifier": "https://example.com/shared", +1> "filePath": "[CWD]/openapi.json", +1> "location": "/paths/~1pets/get/responses/200/$ref", +1> "baseURI": "[CWD_URI]/openapi.json" +1> } +EOF + +COMPARE result_1.txt AGAINST expected_1.txt diff --git a/test/bundle/fail_openapi_without_id.clitest b/test/bundle/fail_openapi_without_id.clitest new file mode 100644 index 000000000..5a479e5a0 --- /dev/null +++ b/test/bundle/fail_openapi_without_id.clitest @@ -0,0 +1,33 @@ +WRITE openapi.json UNTIL EOF +{ + "openapi": "3.1.1", + "info": { "title": "Example", "version": "1.0.0" }, + "paths": {} +} +EOF + +// Not supported +RUN bundle openapi.json --without-id STDIN /dev/null IN . INTO result_0.txt EXPECTING 3 + +REPLACE $CWD WITH '[CWD]' IN result_0.txt + +WRITE expected_0.txt UNTIL EOF +2> error: The --without-id option is not supported for OpenAPI descriptions +2> at file path [CWD]/openapi.json +EOF + +COMPARE result_0.txt AGAINST expected_0.txt + +// Not supported +RUN bundle openapi.json --without-id --json STDIN /dev/null IN . INTO result_1.txt EXPECTING 3 + +REPLACE $CWD WITH '[CWD]' IN result_1.txt + +WRITE expected_1.txt UNTIL EOF +1> { +1> "error": "The --without-id option is not supported for OpenAPI descriptions", +1> "filePath": "[CWD]/openapi.json" +1> } +EOF + +COMPARE result_1.txt AGAINST expected_1.txt diff --git a/test/bundle/pass_openapi_schema_reference.clitest b/test/bundle/pass_openapi_schema_reference.clitest new file mode 100644 index 000000000..335caacfb --- /dev/null +++ b/test/bundle/pass_openapi_schema_reference.clitest @@ -0,0 +1,77 @@ +WRITE openapi.json UNTIL EOF +{ + "openapi": "3.1.1", + "info": { "title": "Example", "version": "1.0.0" }, + "paths": { + "/pets": { + "get": { + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { "$ref": "https://example.com/schemas/pet" } + } + } + } + } + } + } + } +} +EOF + +WRITE pet.json UNTIL EOF +{ + "$id": "https://example.com/schemas/pet", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { "name": { "type": "string" } } +} +EOF + +RUN bundle openapi.json --resolve pet.json STDIN /dev/null IN . INTO result.txt EXPECTING 0 + +WRITE expected.txt UNTIL EOF +1> { +1> "openapi": "3.1.1", +1> "info": { +1> "title": "Example", +1> "version": "1.0.0" +1> }, +1> "paths": { +1> "/pets": { +1> "get": { +1> "responses": { +1> "200": { +1> "description": "OK", +1> "content": { +1> "application/json": { +1> "schema": { +1> "$ref": "https://example.com/schemas/pet" +1> } +1> } +1> } +1> } +1> } +1> } +1> } +1> }, +1> "components": { +1> "schemas": { +1> "pet": { +1> "$id": "https://example.com/schemas/pet", +1> "$schema": "https://json-schema.org/draft/2020-12/schema", +1> "type": "object", +1> "properties": { +1> "name": { +1> "type": "string" +1> } +1> } +1> } +1> } +1> } +1> } +EOF + +COMPARE result.txt AGAINST expected.txt diff --git a/test/bundle/pass_openapi_self.clitest b/test/bundle/pass_openapi_self.clitest new file mode 100644 index 000000000..1b23cb169 --- /dev/null +++ b/test/bundle/pass_openapi_self.clitest @@ -0,0 +1,60 @@ +WRITE openapi.json UNTIL EOF +{ + "openapi": "3.2.0", + "info": { "title": "Example", "version": "1.0.0" }, + "paths": { + "/pets": { + "get": { + "responses": { + "200": { "$ref": "https://example.com/shared#/components/responses/Ok" } + } + } + } + } +} +EOF + +WRITE shared.json UNTIL EOF +{ + "openapi": "3.2.0", + "$self": "https://example.com/shared", + "info": { "title": "Shared", "version": "1.0.0" }, + "components": { + "responses": { + "Ok": { "description": "All good" } + } + } +} +EOF + +RUN bundle openapi.json --resolve shared.json STDIN /dev/null IN . INTO result.txt EXPECTING 0 + +WRITE expected.txt UNTIL EOF +1> { +1> "openapi": "3.2.0", +1> "info": { +1> "title": "Example", +1> "version": "1.0.0" +1> }, +1> "paths": { +1> "/pets": { +1> "get": { +1> "responses": { +1> "200": { +1> "$ref": "#/components/responses/Ok" +1> } +1> } +1> } +1> } +1> }, +1> "components": { +1> "responses": { +1> "Ok": { +1> "description": "All good" +1> } +1> } +1> } +1> } +EOF + +COMPARE result.txt AGAINST expected.txt diff --git a/test/bundle/pass_openapi_yaml.clitest b/test/bundle/pass_openapi_yaml.clitest new file mode 100644 index 000000000..77b3e771e --- /dev/null +++ b/test/bundle/pass_openapi_yaml.clitest @@ -0,0 +1,51 @@ +WRITE openapi.yaml UNTIL EOF +openapi: 3.1.1 +info: + title: Example + version: 1.0.0 +paths: + /pets: + get: + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: https://example.com/schemas/pet +EOF + +WRITE pet.json UNTIL EOF +{ + "$id": "https://example.com/schemas/pet", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "string" +} +EOF + +RUN bundle openapi.yaml --resolve pet.json STDIN /dev/null IN . INTO result.txt EXPECTING 0 + +WRITE expected.txt UNTIL EOF +1> openapi: 3.1.1 +1> info: +1> title: Example +1> version: 1.0.0 +1> paths: +1> /pets: +1> get: +1> responses: +1> '200': +1> description: OK +1> content: +1> application/json: +1> schema: +1> $ref: https://example.com/schemas/pet +1> components: +1> schemas: +1> pet: +1> $id: https://example.com/schemas/pet +1> $schema: https://json-schema.org/draft/2020-12/schema +1> type: string +EOF + +COMPARE result.txt AGAINST expected.txt diff --git a/test/lint/fail_lint_openapi_invalid.clitest b/test/lint/fail_lint_openapi_invalid.clitest index 3c636e434..66e73adab 100644 --- a/test/lint/fail_lint_openapi_invalid.clitest +++ b/test/lint/fail_lint_openapi_invalid.clitest @@ -16,6 +16,7 @@ WRITE expected_0.txt UNTIL EOF 2> at column 1 2> at file path [CWD]/openapi.json 2> at location "" +2> at base uri file://[CWD]/openapi.json EOF COMPARE result_0.txt AGAINST expected_0.txt @@ -31,7 +32,8 @@ WRITE expected_1.txt UNTIL EOF 1> "line": 1, 1> "column": 1, 1> "filePath": "[CWD]/openapi.json", -1> "location": "" +1> "location": "", +1> "baseURI": "file://[CWD]/openapi.json" 1> } EOF @@ -53,6 +55,7 @@ WRITE expected_2.txt UNTIL EOF 2> at column 1 2> at file path tag:sourcemeta.com,2026:openapi/stdin 2> at location "" +2> at base uri tag:sourcemeta.com,2026:openapi/stdin EOF COMPARE result_2.txt AGAINST expected_2.txt @@ -66,7 +69,8 @@ WRITE expected_3.txt UNTIL EOF 1> "line": 1, 1> "column": 1, 1> "filePath": "tag:sourcemeta.com,2026:openapi/stdin", -1> "location": "" +1> "location": "", +1> "baseURI": "tag:sourcemeta.com,2026:openapi/stdin" 1> } EOF diff --git a/test/validate/fail_no_identifier_ref_without_resolve.clitest b/test/validate/fail_no_identifier_ref_without_resolve.clitest index b2788c9c7..dba7edb1d 100644 --- a/test/validate/fail_no_identifier_ref_without_resolve.clitest +++ b/test/validate/fail_no_identifier_ref_without_resolve.clitest @@ -22,7 +22,7 @@ WRITE expected_0.txt UNTIL EOF 2> at identifier [CWD_URI]/schemas/other.json 2> at file path [CWD]/schema.json 2> -2> This is likely because the file does not exist +2> This is likely because the file does not exist, or does not hold a JSON Schema EOF COMPARE result_0.txt AGAINST expected_0.txt From 60db4a76d6989d6c3026085c841d1b32340a92c1 Mon Sep 17 00:00:00 2001 From: Juan Cruz Viotti Date: Fri, 25 Sep 2026 10:43:09 -0300 Subject: [PATCH 2/7] More Signed-off-by: Juan Cruz Viotti --- src/error.h | 28 +++++ src/resolver.h | 55 +++++++-- src/utils.h | 26 ++-- test/CMakeLists.txt | 13 ++ ...l_openapi_description_not_a_schema.clitest | 51 ++++++++ ...l_openapi_reference_names_a_schema.clitest | 39 ++++++ .../fail_openapi_referenced_revision.clitest | 53 +++++++++ .../fail_openapi_revision_mismatch.clitest | 61 ++++++++++ ...il_openapi_revision_mismatch_older.clitest | 43 +++++++ .../bundle/fail_openapi_self_conflict.clitest | 71 +++++++++++ test/bundle/fail_openapi_self_in_3_1.clitest | 43 +++++++ .../pass_openapi_document_chain.clitest | 74 ++++++++++++ .../bundle/pass_openapi_mixed_resolve.clitest | 111 ++++++++++++++++++ .../pass_openapi_resolve_directory.clitest | 104 ++++++++++++++++ test/bundle/pass_openapi_schema_chain.clitest | 84 +++++++++++++ .../bundle/pass_openapi_self_relative.clitest | 60 ++++++++++ test/bundle/pass_openapi_stdin.clitest | 60 ++++++++++ 17 files changed, 958 insertions(+), 18 deletions(-) create mode 100644 test/bundle/fail_openapi_description_not_a_schema.clitest create mode 100644 test/bundle/fail_openapi_reference_names_a_schema.clitest create mode 100644 test/bundle/fail_openapi_referenced_revision.clitest create mode 100644 test/bundle/fail_openapi_revision_mismatch.clitest create mode 100644 test/bundle/fail_openapi_revision_mismatch_older.clitest create mode 100644 test/bundle/fail_openapi_self_conflict.clitest create mode 100644 test/bundle/fail_openapi_self_in_3_1.clitest create mode 100644 test/bundle/pass_openapi_document_chain.clitest create mode 100644 test/bundle/pass_openapi_mixed_resolve.clitest create mode 100644 test/bundle/pass_openapi_resolve_directory.clitest create mode 100644 test/bundle/pass_openapi_schema_chain.clitest create mode 100644 test/bundle/pass_openapi_self_relative.clitest create mode 100644 test/bundle/pass_openapi_stdin.clitest diff --git a/src/error.h b/src/error.h index e5a10f040..72f3a6143 100644 --- a/src/error.h +++ b/src/error.h @@ -92,6 +92,29 @@ class SchemaIdentifierConflictError : public std::runtime_error { sourcemeta::core::Pointer other_; }; +class OpenAPIIdentifierConflictError : public std::runtime_error { +public: + OpenAPIIdentifierConflictError(std::string identifier, + std::filesystem::path other_path) + : std::runtime_error{"Conflicting OpenAPI descriptions for the same " + "identifier"}, + identifier_{std::move(identifier)}, other_path_{std::move(other_path)} { + } + + [[nodiscard]] auto identifier() const noexcept -> const std::string & { + return this->identifier_; + } + + [[nodiscard]] auto other_path() const noexcept + -> const std::filesystem::path & { + return this->other_path_; + } + +private: + std::string identifier_; + std::filesystem::path other_path_; +}; + class PositionalArgumentError : public std::runtime_error { public: PositionalArgumentError(const std::string &message, std::string example) @@ -1598,6 +1621,11 @@ inline auto try_catch(const sourcemeta::core::Options &options, const auto is_json{options.contains("json")}; print_exception(is_json, error); return EXIT_SCHEMA_INPUT_ERROR; + } catch (const sourcemeta::core::FileError + &error) { + const auto is_json{options.contains("json")}; + print_exception(is_json, error); + return EXIT_SCHEMA_INPUT_ERROR; } catch (const PositionError> &error) { const auto is_json{options.contains("json")}; diff --git a/src/resolver.h b/src/resolver.h index 411f6ae46..a44c6f1e7 100644 --- a/src/resolver.h +++ b/src/resolver.h @@ -409,6 +409,21 @@ declares_identifier(const std::unordered_set &identifiers, return canonical != identifier && identifiers.contains(canonical); } +// How resolution names what it reached is a URI, and a `file://` one names a +// place on disk that whoever reads an error would rather see spelled as a path +static inline auto identifier_path(const std::string &identifier) + -> std::filesystem::path { + std::optional uri; + try { + uri.emplace(identifier); + } catch (const sourcemeta::core::URIParseError &) { + return std::filesystem::path{identifier}; + } + + return uri.value().is_file() ? uri.value().to_path() + : std::filesystem::path{identifier}; +} + // OpenAPI Specification 3.2.1, Section 4.1 lets a document name itself with // `$self`, "which also serves as its base URI", resolved against wherever the // document was retrieved from. RFC 3986 Section 5.2.2 never resolves a @@ -676,7 +691,7 @@ class CustomResolver { } auto document{std::move(fetched).to_owned()}; - if (sourcemeta::core::openapi_version(document).has_value()) { + if (is_openapi_document(document)) { LOG_VERBOSE(this->options_) << "Not available as a schema, as this was read as an OpenAPI " "description: " @@ -704,6 +719,7 @@ class CustomResolver { const auto match{this->descriptions_.find(target)}; if (match != this->descriptions_.cend()) { + reject_unsupported_openapi(match->second, identifier_path(target)); return match->second; } @@ -717,13 +733,15 @@ class CustomResolver { } auto document{std::move(fetched).to_owned()}; - if (!sourcemeta::core::openapi_version(document).has_value()) { + if (!is_openapi_document(document)) { this->fetched_.emplace(target, std::move(document)); return std::nullopt; } - return this->descriptions_.emplace(target, std::move(document)) - .first->second; + const auto &stored{ + this->descriptions_.emplace(target, std::move(document)).first->second}; + reject_unsupported_openapi(stored, identifier_path(target)); + return stored; } private: @@ -778,11 +796,28 @@ class CustomResolver { const auto self{openapi_self_identity(entry.second, retrieval)}; if (self.has_value()) { - this->descriptions_.emplace(canonical_resolve_key(self.value()), - entry.second); + this->register_description(canonical_resolve_key(self.value()), entry); } - this->descriptions_.emplace(canonical_resolve_key(retrieval), entry.second); + this->register_description(canonical_resolve_key(retrieval), entry); + } + + // Two descriptions that answer to one identifier leave which of them a + // reference reaches to the order they happened to be given in, so this is + // reported rather than settled by whichever arrived first + auto register_description(const std::string &identifier, + const InputJSON &entry) -> void { + const auto result{this->descriptions_.emplace(identifier, entry.second)}; + if (!result.second && result.first->second != entry.second) { + const auto other{this->description_origins_.find(identifier)}; + assert(other != this->description_origins_.cend()); + throw sourcemeta::core::FileError( + entry.resolution_base, identifier, other->second); + } + + if (result.second) { + this->description_origins_.emplace(identifier, entry.resolution_base); + } } auto import_entry(const InputJSON &entry, @@ -791,13 +826,12 @@ class CustomResolver { // framing it as a schema would both fail and register nonsense. Only a // revision we can read is taken, as one we cannot is no more a description // we can answer for than a schema - if (sourcemeta::core::openapi_version(entry.second).has_value()) { + if (is_openapi_document(entry.second)) { + reject_unsupported_openapi(entry.second, entry.resolution_base); this->import_description(entry); return; } - reject_unsupported_openapi(entry.second, entry.resolution_base); - LOG_DEBUG(this->options_) << "Detecting schema resources from file: " << entry.first << "\n"; @@ -882,6 +916,7 @@ class CustomResolver { // Kept wholly apart from the schemas above, so that neither resolver can // reach what the other answers for std::map descriptions_{}; + std::map description_origins_{}; // What resolution has already retrieved and found not to be a description, // so that one URL is fetched once however many times it is asked for std::map fetched_{}; diff --git a/src/utils.h b/src/utils.h index 636cab975..e00712007 100644 --- a/src/utils.h +++ b/src/utils.h @@ -132,23 +132,33 @@ inline auto looks_like_test_document(const sourcemeta::core::JSON &document) document.defines("tests") && document.at("tests").is_array(); } +// Whether a document holds an OpenAPI Description at all, whichever revision it +// declares. The `openapi` field is what the specification identifies one by, +// and one we cannot read is a description all the same, so this is what tells a +// description from a schema rather than what tells a readable one from the rest +inline auto is_openapi_document(const sourcemeta::core::JSON &document) + -> bool { + if (!document.is_object()) { + return false; + } + + const auto *version{document.try_at("openapi")}; + return version != nullptr && version->is_string(); +} + // The revision an OpenAPI description declares, when it is one we cannot read. // A document that declares the field as anything but a string is no OpenAPI // description by any reading of the specification, so it goes on being read as // a schema rather than being turned down here inline auto unsupported_openapi_version(const sourcemeta::core::JSON &document) -> const sourcemeta::core::JSON * { - if (!document.is_object()) { - return nullptr; - } - - const auto *version{document.try_at("openapi")}; - if (version == nullptr || !version->is_string()) { + if (!is_openapi_document(document)) { return nullptr; } - return sourcemeta::core::openapi_version(document).has_value() ? nullptr - : version; + return sourcemeta::core::openapi_version(document).has_value() + ? nullptr + : document.try_at("openapi"); } // A description of a revision we cannot read is no JSON Schema either, so it is diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index e352c7ca1..ef6121246 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -707,6 +707,19 @@ add_jsonschema_test(bundle/pass_openapi_self) add_jsonschema_test(bundle/pass_openapi_yaml) add_jsonschema_test(bundle/fail_openapi_without_id) add_jsonschema_test(bundle/fail_openapi_unresolved_document) +add_jsonschema_test(bundle/fail_openapi_revision_mismatch) +add_jsonschema_test(bundle/fail_openapi_revision_mismatch_older) +add_jsonschema_test(bundle/fail_openapi_referenced_revision) +add_jsonschema_test(bundle/fail_openapi_description_not_a_schema) +add_jsonschema_test(bundle/fail_openapi_reference_names_a_schema) +add_jsonschema_test(bundle/fail_openapi_self_conflict) +add_jsonschema_test(bundle/fail_openapi_self_in_3_1) +add_jsonschema_test(bundle/pass_openapi_mixed_resolve) +add_jsonschema_test(bundle/pass_openapi_schema_chain) +add_jsonschema_test(bundle/pass_openapi_document_chain) +add_jsonschema_test(bundle/pass_openapi_self_relative) +add_jsonschema_test(bundle/pass_openapi_resolve_directory) +add_jsonschema_test(bundle/pass_openapi_stdin) add_jsonschema_test(bundle/fail_default_dialect_config_extension_mismatch) add_jsonschema_test(bundle/pass_bigint) add_jsonschema_test(bundle/pass_resolve_default_dialect_config) diff --git a/test/bundle/fail_openapi_description_not_a_schema.clitest b/test/bundle/fail_openapi_description_not_a_schema.clitest new file mode 100644 index 000000000..fae771f6c --- /dev/null +++ b/test/bundle/fail_openapi_description_not_a_schema.clitest @@ -0,0 +1,51 @@ +WRITE schema.json UNTIL EOF +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$ref": "description.json" +} +EOF + +WRITE description.json UNTIL EOF +{ + "openapi": "3.1.1", + "info": { "title": "Shared", "version": "1.0.0" }, + "components": { + "responses": { + "Ok": { "description": "All good" } + } + } +} +EOF + +// Schema input error +RUN bundle schema.json STDIN /dev/null IN . INTO result_0.txt EXPECTING 4 + +REPLACE $CWD_URI WITH '[CWD_URI]' IN result_0.txt +REPLACE $CWD WITH '[CWD]' IN result_0.txt + +WRITE expected_0.txt UNTIL EOF +2> error: Could not resolve the reference to an external schema +2> at identifier [CWD_URI]/description.json +2> at file path [CWD]/schema.json +2> +2> This is likely because the file does not exist, or does not hold a JSON Schema +EOF + +COMPARE result_0.txt AGAINST expected_0.txt + +// Schema input error +RUN bundle schema.json --verbose STDIN /dev/null IN . INTO result_1.txt EXPECTING 4 + +REPLACE $CWD_URI WITH '[CWD_URI]' IN result_1.txt +REPLACE $CWD WITH '[CWD]' IN result_1.txt + +WRITE expected_1.txt UNTIL EOF +2> Not available as a schema, as this was read as an OpenAPI description: [CWD_URI]/description.json +2> error: Could not resolve the reference to an external schema +2> at identifier [CWD_URI]/description.json +2> at file path [CWD]/schema.json +2> +2> This is likely because the file does not exist, or does not hold a JSON Schema +EOF + +COMPARE result_1.txt AGAINST expected_1.txt diff --git a/test/bundle/fail_openapi_reference_names_a_schema.clitest b/test/bundle/fail_openapi_reference_names_a_schema.clitest new file mode 100644 index 000000000..d944338ef --- /dev/null +++ b/test/bundle/fail_openapi_reference_names_a_schema.clitest @@ -0,0 +1,39 @@ +WRITE entry.json UNTIL EOF +{ + "openapi": "3.1.1", + "info": { "title": "Example", "version": "1.0.0" }, + "paths": { + "/pets": { + "get": { + "responses": { + "200": { "$ref": "https://example.com/schemas/pet" } + } + } + } + } +} +EOF + +WRITE pet.json UNTIL EOF +{ + "$id": "https://example.com/schemas/pet", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "string" +} +EOF + +// Schema input error +RUN bundle entry.json --resolve pet.json STDIN /dev/null IN . INTO result_0.txt EXPECTING 4 + +REPLACE $CWD_URI WITH '[CWD_URI]' IN result_0.txt +REPLACE $CWD WITH '[CWD]' IN result_0.txt + +WRITE expected_0.txt UNTIL EOF +2> error: This reference must name a place within the document it points at +2> at identifier https://example.com/schemas/pet +2> at file path [CWD]/entry.json +2> at location "/paths/~1pets/get/responses/200/$ref" +2> at base uri [CWD_URI]/entry.json +EOF + +COMPARE result_0.txt AGAINST expected_0.txt diff --git a/test/bundle/fail_openapi_referenced_revision.clitest b/test/bundle/fail_openapi_referenced_revision.clitest new file mode 100644 index 000000000..a711b8095 --- /dev/null +++ b/test/bundle/fail_openapi_referenced_revision.clitest @@ -0,0 +1,53 @@ +WRITE entry.json UNTIL EOF +{ + "openapi": "3.1.1", + "info": { "title": "Example", "version": "1.0.0" }, + "paths": { + "/pets": { + "get": { + "responses": { + "200": { "$ref": "shared.json#/components/responses/Ok" } + } + } + } + } +} +EOF + +WRITE shared.json UNTIL EOF +{ + "openapi": "3.0.3", + "info": { "title": "Shared", "version": "1.0.0" }, + "components": { + "responses": { + "Ok": { "description": "All good" } + } + } +} +EOF + +// Not supported +RUN bundle entry.json STDIN /dev/null IN . INTO result_0.txt EXPECTING 3 + +REPLACE $CWD WITH '[CWD]' IN result_0.txt + +WRITE expected_0.txt UNTIL EOF +2> error: This OpenAPI Specification revision is not supported +2> at value 3.0.3 +2> at file path [CWD]/shared.json +EOF + +COMPARE result_0.txt AGAINST expected_0.txt + +// Not supported +RUN bundle entry.json --resolve shared.json STDIN /dev/null IN . INTO result_1.txt EXPECTING 3 + +REPLACE $CWD WITH '[CWD]' IN result_1.txt + +WRITE expected_1.txt UNTIL EOF +2> error: This OpenAPI Specification revision is not supported +2> at value 3.0.3 +2> at file path [CWD]/shared.json +EOF + +COMPARE result_1.txt AGAINST expected_1.txt diff --git a/test/bundle/fail_openapi_revision_mismatch.clitest b/test/bundle/fail_openapi_revision_mismatch.clitest new file mode 100644 index 000000000..cd7047cec --- /dev/null +++ b/test/bundle/fail_openapi_revision_mismatch.clitest @@ -0,0 +1,61 @@ +WRITE entry.json UNTIL EOF +{ + "openapi": "3.1.1", + "info": { "title": "Example", "version": "1.0.0" }, + "paths": { + "/pets": { + "get": { + "responses": { + "200": { "$ref": "shared.json#/components/responses/Ok" } + } + } + } + } +} +EOF + +WRITE shared.json UNTIL EOF +{ + "openapi": "3.2.0", + "info": { "title": "Shared", "version": "1.0.0" }, + "components": { + "responses": { + "Ok": { "description": "All good" } + } + } +} +EOF + +// Schema input error +RUN bundle entry.json STDIN /dev/null IN . INTO result_0.txt EXPECTING 4 + +REPLACE $CWD_URI WITH '[CWD_URI]' IN result_0.txt +REPLACE $CWD WITH '[CWD]' IN result_0.txt + +WRITE expected_0.txt UNTIL EOF +2> error: This reference must name a document of the same OpenAPI Specification revision +2> at identifier [CWD_URI]/shared.json +2> at file path [CWD]/entry.json +2> at location "/paths/~1pets/get/responses/200/$ref" +2> at base uri [CWD_URI]/entry.json +EOF + +COMPARE result_0.txt AGAINST expected_0.txt + +// Schema input error +RUN bundle entry.json --json STDIN /dev/null IN . INTO result_1.txt EXPECTING 4 + +REPLACE $CWD_URI WITH '[CWD_URI]' IN result_1.txt +REPLACE $CWD WITH '[CWD]' IN result_1.txt + +WRITE expected_1.txt UNTIL EOF +1> { +1> "error": "This reference must name a document of the same OpenAPI Specification revision", +1> "identifier": "[CWD_URI]/shared.json", +1> "filePath": "[CWD]/entry.json", +1> "location": "/paths/~1pets/get/responses/200/$ref", +1> "baseURI": "[CWD_URI]/entry.json" +1> } +EOF + +COMPARE result_1.txt AGAINST expected_1.txt diff --git a/test/bundle/fail_openapi_revision_mismatch_older.clitest b/test/bundle/fail_openapi_revision_mismatch_older.clitest new file mode 100644 index 000000000..2bbb38c04 --- /dev/null +++ b/test/bundle/fail_openapi_revision_mismatch_older.clitest @@ -0,0 +1,43 @@ +WRITE entry.json UNTIL EOF +{ + "openapi": "3.2.0", + "info": { "title": "Example", "version": "1.0.0" }, + "paths": { + "/pets": { + "get": { + "responses": { + "200": { "$ref": "shared.json#/components/responses/Ok" } + } + } + } + } +} +EOF + +WRITE shared.json UNTIL EOF +{ + "openapi": "3.1.1", + "info": { "title": "Shared", "version": "1.0.0" }, + "components": { + "responses": { + "Ok": { "description": "All good" } + } + } +} +EOF + +// Schema input error +RUN bundle entry.json STDIN /dev/null IN . INTO result_0.txt EXPECTING 4 + +REPLACE $CWD_URI WITH '[CWD_URI]' IN result_0.txt +REPLACE $CWD WITH '[CWD]' IN result_0.txt + +WRITE expected_0.txt UNTIL EOF +2> error: This reference must name a document of the same OpenAPI Specification revision +2> at identifier [CWD_URI]/shared.json +2> at file path [CWD]/entry.json +2> at location "/paths/~1pets/get/responses/200/$ref" +2> at base uri [CWD_URI]/entry.json +EOF + +COMPARE result_0.txt AGAINST expected_0.txt diff --git a/test/bundle/fail_openapi_self_conflict.clitest b/test/bundle/fail_openapi_self_conflict.clitest new file mode 100644 index 000000000..9d50f683f --- /dev/null +++ b/test/bundle/fail_openapi_self_conflict.clitest @@ -0,0 +1,71 @@ +WRITE entry.json UNTIL EOF +{ + "openapi": "3.2.0", + "info": { "title": "Example", "version": "1.0.0" }, + "paths": { + "/pets": { + "get": { + "responses": { + "200": { "$ref": "https://example.com/shared#/components/responses/Ok" } + } + } + } + } +} +EOF + +WRITE one.json UNTIL EOF +{ + "openapi": "3.2.0", + "$self": "https://example.com/shared", + "info": { "title": "One", "version": "1.0.0" }, + "components": { + "responses": { + "Ok": { "description": "from one" } + } + } +} +EOF + +WRITE two.json UNTIL EOF +{ + "openapi": "3.2.0", + "$self": "https://example.com/shared", + "info": { "title": "Two", "version": "1.0.0" }, + "components": { + "responses": { + "Ok": { "description": "from two" } + } + } +} +EOF + +// Schema input error +RUN bundle entry.json --resolve one.json --resolve two.json STDIN /dev/null IN . INTO result_0.txt EXPECTING 4 + +REPLACE $CWD WITH '[CWD]' IN result_0.txt + +WRITE expected_0.txt UNTIL EOF +2> error: Conflicting OpenAPI descriptions for the same identifier +2> at identifier https://example.com/shared +2> at file path [CWD]/two.json +2> at other path [CWD]/one.json +EOF + +COMPARE result_0.txt AGAINST expected_0.txt + +// Schema input error +RUN bundle entry.json --resolve one.json --resolve two.json --json STDIN /dev/null IN . INTO result_1.txt EXPECTING 4 + +REPLACE $CWD WITH '[CWD]' IN result_1.txt + +WRITE expected_1.txt UNTIL EOF +1> { +1> "error": "Conflicting OpenAPI descriptions for the same identifier", +1> "identifier": "https://example.com/shared", +1> "filePath": "[CWD]/two.json", +1> "otherPath": "[CWD]/one.json" +1> } +EOF + +COMPARE result_1.txt AGAINST expected_1.txt diff --git a/test/bundle/fail_openapi_self_in_3_1.clitest b/test/bundle/fail_openapi_self_in_3_1.clitest new file mode 100644 index 000000000..d52d17f2b --- /dev/null +++ b/test/bundle/fail_openapi_self_in_3_1.clitest @@ -0,0 +1,43 @@ +WRITE entry.json UNTIL EOF +{ + "openapi": "3.1.1", + "info": { "title": "Example", "version": "1.0.0" }, + "paths": { + "/pets": { + "get": { + "responses": { + "200": { "$ref": "shared.json#/components/responses/Ok" } + } + } + } + } +} +EOF + +WRITE shared.json UNTIL EOF +{ + "openapi": "3.1.1", + "$self": "https://example.com/shared", + "info": { "title": "Shared", "version": "1.0.0" }, + "components": { + "responses": { + "Ok": { "description": "All good" } + } + } +} +EOF + +// Schema input error +RUN bundle entry.json STDIN /dev/null IN . INTO result_0.txt EXPECTING 4 + +REPLACE $CWD_URI WITH '[CWD_URI]' IN result_0.txt +REPLACE $CWD WITH '[CWD]' IN result_0.txt + +WRITE expected_0.txt UNTIL EOF +2> error: The OpenAPI Object does not define this field +2> at file path [CWD]/entry.json +2> at location "/$self" +2> at base uri [CWD_URI]/shared.json +EOF + +COMPARE result_0.txt AGAINST expected_0.txt diff --git a/test/bundle/pass_openapi_document_chain.clitest b/test/bundle/pass_openapi_document_chain.clitest new file mode 100644 index 000000000..d85bdb819 --- /dev/null +++ b/test/bundle/pass_openapi_document_chain.clitest @@ -0,0 +1,74 @@ +WRITE entry.json UNTIL EOF +{ + "openapi": "3.1.1", + "info": { "title": "Example", "version": "1.0.0" }, + "paths": { + "/pets": { + "get": { + "responses": { + "200": { "$ref": "middle.json#/components/responses/Middle" } + } + } + } + } +} +EOF + +WRITE middle.json UNTIL EOF +{ + "openapi": "3.1.1", + "info": { "title": "Middle", "version": "1.0.0" }, + "components": { + "responses": { + "Middle": { "$ref": "last.json#/components/responses/Last" } + } + } +} +EOF + +WRITE last.json UNTIL EOF +{ + "openapi": "3.1.1", + "info": { "title": "Last", "version": "1.0.0" }, + "components": { + "responses": { + "Last": { "description": "All good" } + } + } +} +EOF + +RUN bundle entry.json STDIN /dev/null IN . INTO result.txt EXPECTING 0 + +WRITE expected.txt UNTIL EOF +1> { +1> "openapi": "3.1.1", +1> "info": { +1> "title": "Example", +1> "version": "1.0.0" +1> }, +1> "paths": { +1> "/pets": { +1> "get": { +1> "responses": { +1> "200": { +1> "$ref": "#/components/responses/Middle" +1> } +1> } +1> } +1> } +1> }, +1> "components": { +1> "responses": { +1> "Middle": { +1> "$ref": "#/components/responses/Last" +1> }, +1> "Last": { +1> "description": "All good" +1> } +1> } +1> } +1> } +EOF + +COMPARE result.txt AGAINST expected.txt diff --git a/test/bundle/pass_openapi_mixed_resolve.clitest b/test/bundle/pass_openapi_mixed_resolve.clitest new file mode 100644 index 000000000..626b62f6b --- /dev/null +++ b/test/bundle/pass_openapi_mixed_resolve.clitest @@ -0,0 +1,111 @@ +WRITE entry.json UNTIL EOF +{ + "openapi": "3.1.1", + "info": { "title": "Example", "version": "1.0.0" }, + "paths": { + "/a": { + "get": { + "responses": { + "200": { "$ref": "shared.json#/components/responses/Ok" } + } + } + }, + "/b": { + "get": { + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { "$ref": "https://example.com/withid" } + } + } + } + } + } + } + } +} +EOF + +WRITE shared.json UNTIL EOF +{ + "openapi": "3.1.1", + "info": { "title": "Shared", "version": "1.0.0" }, + "components": { + "responses": { + "Ok": { "description": "All good" } + } + } +} +EOF + +WRITE withid.json UNTIL EOF +{ + "$id": "https://example.com/withid", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "string" +} +EOF + +WRITE noid.json UNTIL EOF +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "integer" +} +EOF + +RUN bundle entry.json --resolve shared.json --resolve withid.json --resolve noid.json STDIN /dev/null IN . INTO result.txt EXPECTING 0 + +WRITE expected.txt UNTIL EOF +1> { +1> "openapi": "3.1.1", +1> "info": { +1> "title": "Example", +1> "version": "1.0.0" +1> }, +1> "paths": { +1> "/a": { +1> "get": { +1> "responses": { +1> "200": { +1> "$ref": "#/components/responses/Ok" +1> } +1> } +1> } +1> }, +1> "/b": { +1> "get": { +1> "responses": { +1> "200": { +1> "description": "OK", +1> "content": { +1> "application/json": { +1> "schema": { +1> "$ref": "https://example.com/withid" +1> } +1> } +1> } +1> } +1> } +1> } +1> } +1> }, +1> "components": { +1> "responses": { +1> "Ok": { +1> "description": "All good" +1> } +1> }, +1> "schemas": { +1> "withid": { +1> "$id": "https://example.com/withid", +1> "$schema": "https://json-schema.org/draft/2020-12/schema", +1> "type": "string" +1> } +1> } +1> } +1> } +EOF + +COMPARE result.txt AGAINST expected.txt diff --git a/test/bundle/pass_openapi_resolve_directory.clitest b/test/bundle/pass_openapi_resolve_directory.clitest new file mode 100644 index 000000000..b9f0c2eef --- /dev/null +++ b/test/bundle/pass_openapi_resolve_directory.clitest @@ -0,0 +1,104 @@ +WRITE entry.json UNTIL EOF +{ + "openapi": "3.1.1", + "info": { "title": "Example", "version": "1.0.0" }, + "paths": { + "/a": { + "get": { + "responses": { + "200": { "$ref": "schemas/shared.json#/components/responses/Ok" } + } + } + }, + "/b": { + "get": { + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { "$ref": "https://example.com/pet" } + } + } + } + } + } + } + } +} +EOF + +WRITE schemas/shared.json UNTIL EOF +{ + "openapi": "3.1.1", + "info": { "title": "Shared", "version": "1.0.0" }, + "components": { + "responses": { + "Ok": { "description": "All good" } + } + } +} +EOF + +WRITE schemas/pet.json UNTIL EOF +{ + "$id": "https://example.com/pet", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "string" +} +EOF + +RUN bundle entry.json --resolve schemas STDIN /dev/null IN . INTO result.txt EXPECTING 0 + +WRITE expected.txt UNTIL EOF +1> { +1> "openapi": "3.1.1", +1> "info": { +1> "title": "Example", +1> "version": "1.0.0" +1> }, +1> "paths": { +1> "/a": { +1> "get": { +1> "responses": { +1> "200": { +1> "$ref": "#/components/responses/Ok" +1> } +1> } +1> } +1> }, +1> "/b": { +1> "get": { +1> "responses": { +1> "200": { +1> "description": "OK", +1> "content": { +1> "application/json": { +1> "schema": { +1> "$ref": "https://example.com/pet" +1> } +1> } +1> } +1> } +1> } +1> } +1> } +1> }, +1> "components": { +1> "responses": { +1> "Ok": { +1> "description": "All good" +1> } +1> }, +1> "schemas": { +1> "pet": { +1> "$id": "https://example.com/pet", +1> "$schema": "https://json-schema.org/draft/2020-12/schema", +1> "type": "string" +1> } +1> } +1> } +1> } +EOF + +COMPARE result.txt AGAINST expected.txt diff --git a/test/bundle/pass_openapi_schema_chain.clitest b/test/bundle/pass_openapi_schema_chain.clitest new file mode 100644 index 000000000..17b3697c8 --- /dev/null +++ b/test/bundle/pass_openapi_schema_chain.clitest @@ -0,0 +1,84 @@ +WRITE entry.json UNTIL EOF +{ + "openapi": "3.1.1", + "info": { "title": "Example", "version": "1.0.0" }, + "paths": { + "/pets": { + "get": { + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { "$ref": "https://example.com/a" } + } + } + } + } + } + } + } +} +EOF + +WRITE a.json UNTIL EOF +{ + "$id": "https://example.com/a", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$ref": "https://example.com/b" +} +EOF + +WRITE b.json UNTIL EOF +{ + "$id": "https://example.com/b", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "string" +} +EOF + +RUN bundle entry.json --resolve a.json --resolve b.json STDIN /dev/null IN . INTO result.txt EXPECTING 0 + +WRITE expected.txt UNTIL EOF +1> { +1> "openapi": "3.1.1", +1> "info": { +1> "title": "Example", +1> "version": "1.0.0" +1> }, +1> "paths": { +1> "/pets": { +1> "get": { +1> "responses": { +1> "200": { +1> "description": "OK", +1> "content": { +1> "application/json": { +1> "schema": { +1> "$ref": "https://example.com/a" +1> } +1> } +1> } +1> } +1> } +1> } +1> } +1> }, +1> "components": { +1> "schemas": { +1> "b": { +1> "$id": "https://example.com/b", +1> "$schema": "https://json-schema.org/draft/2020-12/schema", +1> "type": "string" +1> }, +1> "a": { +1> "$id": "https://example.com/a", +1> "$schema": "https://json-schema.org/draft/2020-12/schema", +1> "$ref": "https://example.com/b" +1> } +1> } +1> } +1> } +EOF + +COMPARE result.txt AGAINST expected.txt diff --git a/test/bundle/pass_openapi_self_relative.clitest b/test/bundle/pass_openapi_self_relative.clitest new file mode 100644 index 000000000..94f547137 --- /dev/null +++ b/test/bundle/pass_openapi_self_relative.clitest @@ -0,0 +1,60 @@ +WRITE entry.json UNTIL EOF +{ + "openapi": "3.2.0", + "info": { "title": "Example", "version": "1.0.0" }, + "paths": { + "/pets": { + "get": { + "responses": { + "200": { "$ref": "sub/shared#/components/responses/Ok" } + } + } + } + } +} +EOF + +WRITE shared.json UNTIL EOF +{ + "openapi": "3.2.0", + "$self": "sub/shared", + "info": { "title": "Shared", "version": "1.0.0" }, + "components": { + "responses": { + "Ok": { "description": "All good" } + } + } +} +EOF + +RUN bundle entry.json --resolve shared.json STDIN /dev/null IN . INTO result.txt EXPECTING 0 + +WRITE expected.txt UNTIL EOF +1> { +1> "openapi": "3.2.0", +1> "info": { +1> "title": "Example", +1> "version": "1.0.0" +1> }, +1> "paths": { +1> "/pets": { +1> "get": { +1> "responses": { +1> "200": { +1> "$ref": "#/components/responses/Ok" +1> } +1> } +1> } +1> } +1> }, +1> "components": { +1> "responses": { +1> "Ok": { +1> "description": "All good" +1> } +1> } +1> } +1> } +EOF + +COMPARE result.txt AGAINST expected.txt diff --git a/test/bundle/pass_openapi_stdin.clitest b/test/bundle/pass_openapi_stdin.clitest new file mode 100644 index 000000000..d2296fc3f --- /dev/null +++ b/test/bundle/pass_openapi_stdin.clitest @@ -0,0 +1,60 @@ +WRITE stdin_input UNTIL EOF +{ + "openapi": "3.2.0", + "info": { "title": "Example", "version": "1.0.0" }, + "paths": { + "/pets": { + "get": { + "responses": { + "200": { "$ref": "https://example.com/shared#/components/responses/Ok" } + } + } + } + } +} +EOF + +WRITE shared.json UNTIL EOF +{ + "openapi": "3.2.0", + "$self": "https://example.com/shared", + "info": { "title": "Shared", "version": "1.0.0" }, + "components": { + "responses": { + "Ok": { "description": "All good" } + } + } +} +EOF + +RUN bundle - --resolve shared.json STDIN stdin_input IN . INTO result.txt EXPECTING 0 + +WRITE expected.txt UNTIL EOF +1> { +1> "openapi": "3.2.0", +1> "info": { +1> "title": "Example", +1> "version": "1.0.0" +1> }, +1> "paths": { +1> "/pets": { +1> "get": { +1> "responses": { +1> "200": { +1> "$ref": "#/components/responses/Ok" +1> } +1> } +1> } +1> } +1> }, +1> "components": { +1> "responses": { +1> "Ok": { +1> "description": "All good" +1> } +1> } +1> } +1> } +EOF + +COMPARE result.txt AGAINST expected.txt From 5411fece02c3083986f9d612852da87039f2fd0b Mon Sep 17 00:00:00 2001 From: Juan Cruz Viotti Date: Fri, 25 Sep 2026 11:00:52 -0300 Subject: [PATCH 3/7] More Signed-off-by: Juan Cruz Viotti --- test/CMakeLists.txt | 5 + .../pass_openapi_json_from_yaml.clitest | 101 ++++++++++++++++++ test/bundle/pass_openapi_yaml_all.clitest | 73 +++++++++++++ .../pass_openapi_yaml_indentation.clitest | 49 +++++++++ .../pass_resolve_json_from_yaml.clitest | 28 +++++ .../pass_resolve_yaml_from_json.clitest | 32 ++++++ 6 files changed, 288 insertions(+) create mode 100644 test/bundle/pass_openapi_json_from_yaml.clitest create mode 100644 test/bundle/pass_openapi_yaml_all.clitest create mode 100644 test/bundle/pass_openapi_yaml_indentation.clitest create mode 100644 test/bundle/pass_resolve_json_from_yaml.clitest create mode 100644 test/bundle/pass_resolve_yaml_from_json.clitest diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index ef6121246..84c3a0ddd 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -720,6 +720,11 @@ add_jsonschema_test(bundle/pass_openapi_document_chain) add_jsonschema_test(bundle/pass_openapi_self_relative) add_jsonschema_test(bundle/pass_openapi_resolve_directory) add_jsonschema_test(bundle/pass_openapi_stdin) +add_jsonschema_test(bundle/pass_openapi_yaml_all) +add_jsonschema_test(bundle/pass_openapi_json_from_yaml) +add_jsonschema_test(bundle/pass_openapi_yaml_indentation) +add_jsonschema_test(bundle/pass_resolve_yaml_from_json) +add_jsonschema_test(bundle/pass_resolve_json_from_yaml) add_jsonschema_test(bundle/fail_default_dialect_config_extension_mismatch) add_jsonschema_test(bundle/pass_bigint) add_jsonschema_test(bundle/pass_resolve_default_dialect_config) diff --git a/test/bundle/pass_openapi_json_from_yaml.clitest b/test/bundle/pass_openapi_json_from_yaml.clitest new file mode 100644 index 000000000..06238f7e2 --- /dev/null +++ b/test/bundle/pass_openapi_json_from_yaml.clitest @@ -0,0 +1,101 @@ +WRITE entry.json UNTIL EOF +{ + "openapi": "3.1.1", + "info": { "title": "Example", "version": "1.0.0" }, + "paths": { + "/a": { + "get": { + "responses": { + "200": { "$ref": "shared.yaml#/components/responses/Ok" } + } + } + }, + "/b": { + "get": { + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { "$ref": "https://example.com/pet" } + } + } + } + } + } + } + } +} +EOF + +WRITE shared.yaml UNTIL EOF +openapi: 3.1.1 +info: + title: Shared + version: 1.0.0 +components: + responses: + Ok: + description: All good +EOF + +WRITE pet.yaml UNTIL EOF +$id: https://example.com/pet +$schema: https://json-schema.org/draft/2020-12/schema +type: string +EOF + +RUN bundle entry.json --resolve shared.yaml --resolve pet.yaml STDIN /dev/null IN . INTO result.txt EXPECTING 0 + +WRITE expected.txt UNTIL EOF +1> { +1> "openapi": "3.1.1", +1> "info": { +1> "title": "Example", +1> "version": "1.0.0" +1> }, +1> "paths": { +1> "/a": { +1> "get": { +1> "responses": { +1> "200": { +1> "$ref": "#/components/responses/Ok" +1> } +1> } +1> } +1> }, +1> "/b": { +1> "get": { +1> "responses": { +1> "200": { +1> "description": "OK", +1> "content": { +1> "application/json": { +1> "schema": { +1> "$ref": "https://example.com/pet" +1> } +1> } +1> } +1> } +1> } +1> } +1> } +1> }, +1> "components": { +1> "responses": { +1> "Ok": { +1> "description": "All good" +1> } +1> }, +1> "schemas": { +1> "pet": { +1> "$id": "https://example.com/pet", +1> "$schema": "https://json-schema.org/draft/2020-12/schema", +1> "type": "string" +1> } +1> } +1> } +1> } +EOF + +COMPARE result.txt AGAINST expected.txt diff --git a/test/bundle/pass_openapi_yaml_all.clitest b/test/bundle/pass_openapi_yaml_all.clitest new file mode 100644 index 000000000..321410b51 --- /dev/null +++ b/test/bundle/pass_openapi_yaml_all.clitest @@ -0,0 +1,73 @@ +WRITE entry.yaml UNTIL EOF +openapi: 3.1.1 +info: + title: Example + version: 1.0.0 +paths: + /a: + get: + responses: + '200': + $ref: shared.yaml#/components/responses/Ok + /b: + get: + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: https://example.com/pet +EOF + +WRITE shared.yaml UNTIL EOF +openapi: 3.1.1 +info: + title: Shared + version: 1.0.0 +components: + responses: + Ok: + description: All good +EOF + +WRITE pet.yaml UNTIL EOF +$id: https://example.com/pet +$schema: https://json-schema.org/draft/2020-12/schema +type: string +EOF + +RUN bundle entry.yaml --resolve shared.yaml --resolve pet.yaml STDIN /dev/null IN . INTO result.txt EXPECTING 0 + +WRITE expected.txt UNTIL EOF +1> openapi: 3.1.1 +1> info: +1> title: Example +1> version: 1.0.0 +1> paths: +1> /a: +1> get: +1> responses: +1> '200': +1> $ref: "#/components/responses/Ok" +1> /b: +1> get: +1> responses: +1> '200': +1> description: OK +1> content: +1> application/json: +1> schema: +1> $ref: https://example.com/pet +1> components: +1> responses: +1> Ok: +1> description: All good +1> schemas: +1> pet: +1> $id: https://example.com/pet +1> $schema: https://json-schema.org/draft/2020-12/schema +1> type: string +EOF + +COMPARE result.txt AGAINST expected.txt diff --git a/test/bundle/pass_openapi_yaml_indentation.clitest b/test/bundle/pass_openapi_yaml_indentation.clitest new file mode 100644 index 000000000..b2e48d687 --- /dev/null +++ b/test/bundle/pass_openapi_yaml_indentation.clitest @@ -0,0 +1,49 @@ +WRITE entry.yaml UNTIL EOF +openapi: 3.1.1 +info: + title: Example + version: 1.0.0 +paths: + /pets: + get: + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: https://example.com/pet +EOF + +WRITE pet.yaml UNTIL EOF +$id: https://example.com/pet +$schema: https://json-schema.org/draft/2020-12/schema +type: string +EOF + +RUN bundle entry.yaml --resolve pet.yaml --indentation 4 STDIN /dev/null IN . INTO result.txt EXPECTING 0 + +WRITE expected.txt UNTIL EOF +1> openapi: 3.1.1 +1> info: +1> title: Example +1> version: 1.0.0 +1> paths: +1> /pets: +1> get: +1> responses: +1> '200': +1> description: OK +1> content: +1> application/json: +1> schema: +1> $ref: https://example.com/pet +1> components: +1> schemas: +1> pet: +1> $id: https://example.com/pet +1> $schema: https://json-schema.org/draft/2020-12/schema +1> type: string +EOF + +COMPARE result.txt AGAINST expected.txt diff --git a/test/bundle/pass_resolve_json_from_yaml.clitest b/test/bundle/pass_resolve_json_from_yaml.clitest new file mode 100644 index 000000000..63943ab8f --- /dev/null +++ b/test/bundle/pass_resolve_json_from_yaml.clitest @@ -0,0 +1,28 @@ +WRITE schema.yaml UNTIL EOF +$schema: https://json-schema.org/draft/2020-12/schema +$id: https://example.com/root +$ref: https://example.com/pet +EOF + +WRITE pet.json UNTIL EOF +{ + "$id": "https://example.com/pet", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "integer" +} +EOF + +RUN bundle schema.yaml --resolve pet.json STDIN /dev/null IN . INTO result.txt EXPECTING 0 + +WRITE expected.txt UNTIL EOF +1> $schema: https://json-schema.org/draft/2020-12/schema +1> $id: https://example.com/root +1> $ref: https://example.com/pet +1> $defs: +1> https://example.com/pet: +1> $schema: https://json-schema.org/draft/2020-12/schema +1> $id: https://example.com/pet +1> type: integer +EOF + +COMPARE result.txt AGAINST expected.txt diff --git a/test/bundle/pass_resolve_yaml_from_json.clitest b/test/bundle/pass_resolve_yaml_from_json.clitest new file mode 100644 index 000000000..ec1a12482 --- /dev/null +++ b/test/bundle/pass_resolve_yaml_from_json.clitest @@ -0,0 +1,32 @@ +WRITE schema.json UNTIL EOF +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://example.com/root", + "$ref": "https://example.com/pet" +} +EOF + +WRITE pet.yaml UNTIL EOF +$id: https://example.com/pet +$schema: https://json-schema.org/draft/2020-12/schema +type: string +EOF + +RUN bundle schema.json --resolve pet.yaml STDIN /dev/null IN . INTO result.txt EXPECTING 0 + +WRITE expected.txt UNTIL EOF +1> { +1> "$schema": "https://json-schema.org/draft/2020-12/schema", +1> "$id": "https://example.com/root", +1> "$ref": "https://example.com/pet", +1> "$defs": { +1> "https://example.com/pet": { +1> "$schema": "https://json-schema.org/draft/2020-12/schema", +1> "$id": "https://example.com/pet", +1> "type": "string" +1> } +1> } +1> } +EOF + +COMPARE result.txt AGAINST expected.txt From 5ed0f927403ddadbd3cb77f78186ced32b6d7280 Mon Sep 17 00:00:00 2001 From: Juan Cruz Viotti Date: Fri, 25 Sep 2026 11:20:12 -0300 Subject: [PATCH 4/7] Fix Signed-off-by: Juan Cruz Viotti --- test/lint/fail_lint_openapi_invalid.clitest | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/test/lint/fail_lint_openapi_invalid.clitest b/test/lint/fail_lint_openapi_invalid.clitest index 66e73adab..b4a561426 100644 --- a/test/lint/fail_lint_openapi_invalid.clitest +++ b/test/lint/fail_lint_openapi_invalid.clitest @@ -8,6 +8,7 @@ EOF // Schema input error RUN lint openapi.json STDIN /dev/null IN . INTO result_0.txt EXPECTING 4 +REPLACE $CWD_URI WITH '[CWD_URI]' IN result_0.txt REPLACE $CWD WITH '[CWD]' IN result_0.txt WRITE expected_0.txt UNTIL EOF @@ -16,7 +17,7 @@ WRITE expected_0.txt UNTIL EOF 2> at column 1 2> at file path [CWD]/openapi.json 2> at location "" -2> at base uri file://[CWD]/openapi.json +2> at base uri [CWD_URI]/openapi.json EOF COMPARE result_0.txt AGAINST expected_0.txt @@ -24,6 +25,7 @@ COMPARE result_0.txt AGAINST expected_0.txt // Schema input error RUN lint openapi.json --json STDIN /dev/null IN . INTO result_1.txt EXPECTING 4 +REPLACE $CWD_URI WITH '[CWD_URI]' IN result_1.txt REPLACE $CWD WITH '[CWD]' IN result_1.txt WRITE expected_1.txt UNTIL EOF @@ -33,7 +35,7 @@ WRITE expected_1.txt UNTIL EOF 1> "column": 1, 1> "filePath": "[CWD]/openapi.json", 1> "location": "", -1> "baseURI": "file://[CWD]/openapi.json" +1> "baseURI": "[CWD_URI]/openapi.json" 1> } EOF From 835a1befbce580e7189cd709aa8fee6ded960843 Mon Sep 17 00:00:00 2001 From: Juan Cruz Viotti Date: Fri, 25 Sep 2026 12:52:08 -0300 Subject: [PATCH 5/7] More Signed-off-by: Juan Cruz Viotti --- src/command_bundle.cc | 16 +++-- src/command_lint.cc | 2 +- src/resolver.h | 19 +++++- test/CMakeLists.txt | 5 +- ...l_openapi_description_not_a_schema.clitest | 16 +++++ ...l_openapi_reference_names_a_schema.clitest | 18 +++++ .../fail_openapi_referenced_revision.clitest | 15 +++++ ...il_openapi_revision_mismatch_older.clitest | 18 +++++ test/bundle/fail_openapi_self_in_3_1.clitest | 17 +++++ .../bundle/fail_openapi_self_position.clitest | 41 ++++++++++++ .../bundle/fail_openapi_version_stdin.clitest | 31 +++++++++ ...pass_openapi_configuration_resolve.clitest | 67 +++++++++++++++++++ ...=> pass_openapi_yaml_resolve_yaml.clitest} | 0 13 files changed, 258 insertions(+), 7 deletions(-) create mode 100644 test/bundle/fail_openapi_self_position.clitest create mode 100644 test/bundle/fail_openapi_version_stdin.clitest create mode 100644 test/bundle/pass_openapi_configuration_resolve.clitest rename test/bundle/{pass_openapi_yaml_all.clitest => pass_openapi_yaml_resolve_yaml.clitest} (100%) diff --git a/src/command_bundle.cc b/src/command_bundle.cc index a3b316ec3..56eeea4d2 100644 --- a/src/command_bundle.cc +++ b/src/command_bundle.cc @@ -46,10 +46,11 @@ auto sourcemeta::jsonschema::bundle(const sourcemeta::core::Options &options) // An OpenAPI description is not a schema, so what it goes by wherever we // report on it is an identity of its own rather than the one a schema from // the same place would take + const auto is_description{is_openapi_document(parsed_schema.document)}; const auto is_openapi{ sourcemeta::core::openapi_version(parsed_schema.document).has_value()}; const auto schema_display_path{ - schema_from_stdin ? (is_openapi ? openapi_stdin_path() : stdin_path()) + schema_from_stdin ? (is_description ? openapi_stdin_path() : stdin_path()) : schema_path}; if (parsed_schema.multidocument) { @@ -76,6 +77,11 @@ auto sourcemeta::jsonschema::bundle(const sourcemeta::core::Options &options) const auto openapi_base{sourcemeta::jsonschema::openapi_default_id( schema_path, schema_from_stdin)}; + // A description that names itself with `$self` answers to that rather than to + // where it was read from, and a fault in it reports that base, so both are + // what tells a fault of ours from one in a document bundling went and fetched + const auto openapi_self{ + openapi_self_identity(parsed_schema.document, openapi_base)}; const auto &custom_resolver{ resolver(options, options.contains("http"), dialect, configuration)}; @@ -125,9 +131,11 @@ auto sourcemeta::jsonschema::bundle(const sourcemeta::core::Options &options) throw sourcemeta::core::FileError( schema_display_path, error); } catch (const sourcemeta::core::OpenAPIError &error) { - const auto position{error.base() == openapi_base - ? parsed_schema.positions.get(error.location()) - : std::nullopt}; + const auto ours{ + error.base() == openapi_base || + (openapi_self.has_value() && error.base() == openapi_self.value())}; + const auto position{ours ? parsed_schema.positions.get(error.location()) + : std::nullopt}; if (position.has_value()) { throw PositionError< sourcemeta::core::FileError>( diff --git a/src/command_lint.cc b/src/command_lint.cc index f0ae106b8..6115b7cf4 100644 --- a/src/command_lint.cc +++ b/src/command_lint.cc @@ -177,7 +177,7 @@ retag_openapi_stdin(std::vector &entries) -> void { for (auto &entry : entries) { if (entry.from_stdin && - sourcemeta::core::openapi_version(entry.second).has_value()) { + sourcemeta::jsonschema::is_openapi_document(entry.second)) { entry.first = std::string{sourcemeta::jsonschema::STDIN_OPENAPI_DEFAULT_ID}; entry.resolution_base = sourcemeta::jsonschema::openapi_stdin_path(); diff --git a/src/resolver.h b/src/resolver.h index a44c6f1e7..c593c9ef8 100644 --- a/src/resolver.h +++ b/src/resolver.h @@ -715,7 +715,24 @@ class CustomResolver { // goes to the resolver above rather than being reported from here auto openapi(std::string_view identifier) -> sourcemeta::core::OpenAPIResolverResult { - const std::string target{canonical_resolve_key(identifier)}; + // What a configuration remaps an identifier to holds for a description just + // as it does for a schema, so this is settled before anything is looked up + const std::string string_identifier{identifier}; + const auto mapped_result = this->configuration_.and_then( + [this, + &string_identifier](const sourcemeta::blaze::Configuration &config) + -> std::optional { + return resolve_map_uri(this->canonical_resolve_, config.base_path, + string_identifier); + }); + if (mapped_result.has_value()) { + LOG_DEBUG(this->options_) + << "Resolving " << identifier << " as " << mapped_result.value() + << " given the configuration file\n"; + } + + const std::string target{canonical_resolve_key( + mapped_result.has_value() ? mapped_result.value() : string_identifier)}; const auto match{this->descriptions_.find(target)}; if (match != this->descriptions_.cend()) { diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 84c3a0ddd..db6b164e7 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -720,11 +720,14 @@ add_jsonschema_test(bundle/pass_openapi_document_chain) add_jsonschema_test(bundle/pass_openapi_self_relative) add_jsonschema_test(bundle/pass_openapi_resolve_directory) add_jsonschema_test(bundle/pass_openapi_stdin) -add_jsonschema_test(bundle/pass_openapi_yaml_all) +add_jsonschema_test(bundle/pass_openapi_yaml_resolve_yaml) add_jsonschema_test(bundle/pass_openapi_json_from_yaml) add_jsonschema_test(bundle/pass_openapi_yaml_indentation) add_jsonschema_test(bundle/pass_resolve_yaml_from_json) add_jsonschema_test(bundle/pass_resolve_json_from_yaml) +add_jsonschema_test(bundle/pass_openapi_configuration_resolve) +add_jsonschema_test(bundle/fail_openapi_self_position) +add_jsonschema_test(bundle/fail_openapi_version_stdin) add_jsonschema_test(bundle/fail_default_dialect_config_extension_mismatch) add_jsonschema_test(bundle/pass_bigint) add_jsonschema_test(bundle/pass_resolve_default_dialect_config) diff --git a/test/bundle/fail_openapi_description_not_a_schema.clitest b/test/bundle/fail_openapi_description_not_a_schema.clitest index fae771f6c..9d0a04bee 100644 --- a/test/bundle/fail_openapi_description_not_a_schema.clitest +++ b/test/bundle/fail_openapi_description_not_a_schema.clitest @@ -49,3 +49,19 @@ WRITE expected_1.txt UNTIL EOF EOF COMPARE result_1.txt AGAINST expected_1.txt + +// Schema input error +RUN bundle schema.json --json STDIN /dev/null IN . INTO result_2.txt EXPECTING 4 + +REPLACE $CWD_URI WITH '[CWD_URI]' IN result_2.txt +REPLACE $CWD WITH '[CWD]' IN result_2.txt + +WRITE expected_2.txt UNTIL EOF +1> { +1> "error": "Could not resolve the reference to an external schema", +1> "identifier": "[CWD_URI]/description.json", +1> "filePath": "[CWD]/schema.json" +1> } +EOF + +COMPARE result_2.txt AGAINST expected_2.txt diff --git a/test/bundle/fail_openapi_reference_names_a_schema.clitest b/test/bundle/fail_openapi_reference_names_a_schema.clitest index d944338ef..a5d7de6b6 100644 --- a/test/bundle/fail_openapi_reference_names_a_schema.clitest +++ b/test/bundle/fail_openapi_reference_names_a_schema.clitest @@ -37,3 +37,21 @@ WRITE expected_0.txt UNTIL EOF EOF COMPARE result_0.txt AGAINST expected_0.txt + +// Schema input error +RUN bundle entry.json --resolve pet.json --json STDIN /dev/null IN . INTO result_1.txt EXPECTING 4 + +REPLACE $CWD_URI WITH '[CWD_URI]' IN result_1.txt +REPLACE $CWD WITH '[CWD]' IN result_1.txt + +WRITE expected_1.txt UNTIL EOF +1> { +1> "error": "This reference must name a place within the document it points at", +1> "identifier": "https://example.com/schemas/pet", +1> "filePath": "[CWD]/entry.json", +1> "location": "/paths/~1pets/get/responses/200/$ref", +1> "baseURI": "[CWD_URI]/entry.json" +1> } +EOF + +COMPARE result_1.txt AGAINST expected_1.txt diff --git a/test/bundle/fail_openapi_referenced_revision.clitest b/test/bundle/fail_openapi_referenced_revision.clitest index a711b8095..38edd18da 100644 --- a/test/bundle/fail_openapi_referenced_revision.clitest +++ b/test/bundle/fail_openapi_referenced_revision.clitest @@ -51,3 +51,18 @@ WRITE expected_1.txt UNTIL EOF EOF COMPARE result_1.txt AGAINST expected_1.txt + +// Not supported +RUN bundle entry.json --json STDIN /dev/null IN . INTO result_2.txt EXPECTING 3 + +REPLACE $CWD WITH '[CWD]' IN result_2.txt + +WRITE expected_2.txt UNTIL EOF +1> { +1> "error": "This OpenAPI Specification revision is not supported", +1> "value": "3.0.3", +1> "filePath": "[CWD]/shared.json" +1> } +EOF + +COMPARE result_2.txt AGAINST expected_2.txt diff --git a/test/bundle/fail_openapi_revision_mismatch_older.clitest b/test/bundle/fail_openapi_revision_mismatch_older.clitest index 2bbb38c04..fb3ed5f1a 100644 --- a/test/bundle/fail_openapi_revision_mismatch_older.clitest +++ b/test/bundle/fail_openapi_revision_mismatch_older.clitest @@ -41,3 +41,21 @@ WRITE expected_0.txt UNTIL EOF EOF COMPARE result_0.txt AGAINST expected_0.txt + +// Schema input error +RUN bundle entry.json --json STDIN /dev/null IN . INTO result_1.txt EXPECTING 4 + +REPLACE $CWD_URI WITH '[CWD_URI]' IN result_1.txt +REPLACE $CWD WITH '[CWD]' IN result_1.txt + +WRITE expected_1.txt UNTIL EOF +1> { +1> "error": "This reference must name a document of the same OpenAPI Specification revision", +1> "identifier": "[CWD_URI]/shared.json", +1> "filePath": "[CWD]/entry.json", +1> "location": "/paths/~1pets/get/responses/200/$ref", +1> "baseURI": "[CWD_URI]/entry.json" +1> } +EOF + +COMPARE result_1.txt AGAINST expected_1.txt diff --git a/test/bundle/fail_openapi_self_in_3_1.clitest b/test/bundle/fail_openapi_self_in_3_1.clitest index d52d17f2b..494a55367 100644 --- a/test/bundle/fail_openapi_self_in_3_1.clitest +++ b/test/bundle/fail_openapi_self_in_3_1.clitest @@ -41,3 +41,20 @@ WRITE expected_0.txt UNTIL EOF EOF COMPARE result_0.txt AGAINST expected_0.txt + +// Schema input error +RUN bundle entry.json --json STDIN /dev/null IN . INTO result_1.txt EXPECTING 4 + +REPLACE $CWD_URI WITH '[CWD_URI]' IN result_1.txt +REPLACE $CWD WITH '[CWD]' IN result_1.txt + +WRITE expected_1.txt UNTIL EOF +1> { +1> "error": "The OpenAPI Object does not define this field", +1> "filePath": "[CWD]/entry.json", +1> "location": "/$self", +1> "baseURI": "[CWD_URI]/shared.json" +1> } +EOF + +COMPARE result_1.txt AGAINST expected_1.txt diff --git a/test/bundle/fail_openapi_self_position.clitest b/test/bundle/fail_openapi_self_position.clitest new file mode 100644 index 000000000..4c2945045 --- /dev/null +++ b/test/bundle/fail_openapi_self_position.clitest @@ -0,0 +1,41 @@ +WRITE entry.json UNTIL EOF +{ + "openapi": "3.2.0", + "$self": "https://example.com/api", + "info": { "title": "Example", "version": "1.0.0" } +} +EOF + +// Schema input error +RUN bundle entry.json STDIN /dev/null IN . INTO result_0.txt EXPECTING 4 + +REPLACE $CWD WITH '[CWD]' IN result_0.txt + +WRITE expected_0.txt UNTIL EOF +2> error: The OpenAPI Description must declare paths, components or webhooks +2> at line 1 +2> at column 1 +2> at file path [CWD]/entry.json +2> at location "" +2> at base uri https://example.com/api +EOF + +COMPARE result_0.txt AGAINST expected_0.txt + +// Schema input error +RUN bundle entry.json --json STDIN /dev/null IN . INTO result_1.txt EXPECTING 4 + +REPLACE $CWD WITH '[CWD]' IN result_1.txt + +WRITE expected_1.txt UNTIL EOF +1> { +1> "error": "The OpenAPI Description must declare paths, components or webhooks", +1> "line": 1, +1> "column": 1, +1> "filePath": "[CWD]/entry.json", +1> "location": "", +1> "baseURI": "https://example.com/api" +1> } +EOF + +COMPARE result_1.txt AGAINST expected_1.txt diff --git a/test/bundle/fail_openapi_version_stdin.clitest b/test/bundle/fail_openapi_version_stdin.clitest new file mode 100644 index 000000000..257fdc7ce --- /dev/null +++ b/test/bundle/fail_openapi_version_stdin.clitest @@ -0,0 +1,31 @@ +WRITE stdin_input UNTIL EOF +{ + "openapi": "3.0.3", + "info": { "title": "Example", "version": "1.0.0" }, + "paths": {} +} +EOF + +// Not supported +RUN bundle - STDIN stdin_input IN . INTO result_0.txt EXPECTING 3 + +WRITE expected_0.txt UNTIL EOF +2> error: This OpenAPI Specification revision is not supported +2> at value 3.0.3 +2> at file path tag:sourcemeta.com,2026:openapi/stdin +EOF + +COMPARE result_0.txt AGAINST expected_0.txt + +// Not supported +RUN bundle - --json STDIN stdin_input IN . INTO result_1.txt EXPECTING 3 + +WRITE expected_1.txt UNTIL EOF +1> { +1> "error": "This OpenAPI Specification revision is not supported", +1> "value": "3.0.3", +1> "filePath": "tag:sourcemeta.com,2026:openapi/stdin" +1> } +EOF + +COMPARE result_1.txt AGAINST expected_1.txt diff --git a/test/bundle/pass_openapi_configuration_resolve.clitest b/test/bundle/pass_openapi_configuration_resolve.clitest new file mode 100644 index 000000000..ed9fa50db --- /dev/null +++ b/test/bundle/pass_openapi_configuration_resolve.clitest @@ -0,0 +1,67 @@ +WRITE jsonschema.json UNTIL EOF +{ + "resolve": { + "https://example.com/shared": "./shared.json" + } +} +EOF + +WRITE shared.json UNTIL EOF +{ + "openapi": "3.1.1", + "info": { "title": "Shared", "version": "1.0.0" }, + "components": { + "responses": { + "Ok": { "description": "All good" } + } + } +} +EOF + +WRITE entry.json UNTIL EOF +{ + "openapi": "3.1.1", + "info": { "title": "Example", "version": "1.0.0" }, + "paths": { + "/pets": { + "get": { + "responses": { + "200": { "$ref": "https://example.com/shared#/components/responses/Ok" } + } + } + } + } +} +EOF + +RUN bundle entry.json STDIN /dev/null IN . INTO result.txt EXPECTING 0 + +WRITE expected.txt UNTIL EOF +1> { +1> "openapi": "3.1.1", +1> "info": { +1> "title": "Example", +1> "version": "1.0.0" +1> }, +1> "paths": { +1> "/pets": { +1> "get": { +1> "responses": { +1> "200": { +1> "$ref": "#/components/responses/Ok" +1> } +1> } +1> } +1> } +1> }, +1> "components": { +1> "responses": { +1> "Ok": { +1> "description": "All good" +1> } +1> } +1> } +1> } +EOF + +COMPARE result.txt AGAINST expected.txt diff --git a/test/bundle/pass_openapi_yaml_all.clitest b/test/bundle/pass_openapi_yaml_resolve_yaml.clitest similarity index 100% rename from test/bundle/pass_openapi_yaml_all.clitest rename to test/bundle/pass_openapi_yaml_resolve_yaml.clitest From d115ce232c5a4df816755a54fec8c9b800a494dc Mon Sep 17 00:00:00 2001 From: Juan Cruz Viotti Date: Fri, 25 Sep 2026 15:11:17 -0300 Subject: [PATCH 6/7] Simpler docs Signed-off-by: Juan Cruz Viotti --- docs/bundle.markdown | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/docs/bundle.markdown b/docs/bundle.markdown index 6515a9669..9051f62f1 100644 --- a/docs/bundle.markdown +++ b/docs/bundle.markdown @@ -64,15 +64,7 @@ rewritten to name where it landed. Any other revision, such as v3.0, is rejected rather than read as a schema. Use `--resolve/-r` for the other documents of the description exactly as you -would for schemas. Each input is sorted by what it turns out to be, so one set -of `--resolve` options may name both descriptions and schemas, and a Schema -Object reaching an external schema is bundled as JSON Schema bundling would -bundle it. - -> [!NOTE] -> The `--without-id`/`-w` option is not supported for OpenAPI descriptions, as -> removing identifiers is a schema operation and there is no description left to -> read once its Schema Objects can no longer say who they are. +would for schemas. Examples -------- From 28d6e61ba9f4cf80152950b78fd320667888b79b Mon Sep 17 00:00:00 2001 From: Juan Cruz Viotti Date: Fri, 25 Sep 2026 15:18:32 -0300 Subject: [PATCH 7/7] Nicer Signed-off-by: Juan Cruz Viotti --- docs/bundle.markdown | 12 +++++++++++ src/resolver.h | 11 +--------- ...l_openapi_description_not_a_schema.clitest | 21 ++----------------- 3 files changed, 15 insertions(+), 29 deletions(-) diff --git a/docs/bundle.markdown b/docs/bundle.markdown index 9051f62f1..d8d7ce9a1 100644 --- a/docs/bundle.markdown +++ b/docs/bundle.markdown @@ -160,3 +160,15 @@ jsonschema bundle path/to/my/schema.json \ ```sh jsonschema bundle path/to/my/schema.json --resolve path/to/external.json --without-id ``` + +### Bundle an OpenAPI description split across other documents + +```sh +jsonschema bundle path/to/my/openapi.json +``` + +### Bundle an OpenAPI description importing a single local schema + +```sh +jsonschema bundle path/to/my/openapi.json --resolve path/to/external.json +``` diff --git a/src/resolver.h b/src/resolver.h index c593c9ef8..a7c6a0dd1 100644 --- a/src/resolver.h +++ b/src/resolver.h @@ -670,13 +670,8 @@ class CustomResolver { } // An OpenAPI description is not a schema, so what was sorted among them is - // unavailable here. Saying so is what tells a reference that goes looking - // for one apart from a reference to something nobody supplied + // unavailable here if (this->descriptions_.contains(target)) { - LOG_VERBOSE(this->options_) - << "Not available as a schema, as this was read as an OpenAPI " - "description: " - << target << "\n"; return std::nullopt; } @@ -692,10 +687,6 @@ class CustomResolver { auto document{std::move(fetched).to_owned()}; if (is_openapi_document(document)) { - LOG_VERBOSE(this->options_) - << "Not available as a schema, as this was read as an OpenAPI " - "description: " - << target << "\n"; this->descriptions_.emplace(target, std::move(document)); return std::nullopt; } diff --git a/test/bundle/fail_openapi_description_not_a_schema.clitest b/test/bundle/fail_openapi_description_not_a_schema.clitest index 9d0a04bee..8488bbe66 100644 --- a/test/bundle/fail_openapi_description_not_a_schema.clitest +++ b/test/bundle/fail_openapi_description_not_a_schema.clitest @@ -34,29 +34,12 @@ EOF COMPARE result_0.txt AGAINST expected_0.txt // Schema input error -RUN bundle schema.json --verbose STDIN /dev/null IN . INTO result_1.txt EXPECTING 4 +RUN bundle schema.json --json STDIN /dev/null IN . INTO result_1.txt EXPECTING 4 REPLACE $CWD_URI WITH '[CWD_URI]' IN result_1.txt REPLACE $CWD WITH '[CWD]' IN result_1.txt WRITE expected_1.txt UNTIL EOF -2> Not available as a schema, as this was read as an OpenAPI description: [CWD_URI]/description.json -2> error: Could not resolve the reference to an external schema -2> at identifier [CWD_URI]/description.json -2> at file path [CWD]/schema.json -2> -2> This is likely because the file does not exist, or does not hold a JSON Schema -EOF - -COMPARE result_1.txt AGAINST expected_1.txt - -// Schema input error -RUN bundle schema.json --json STDIN /dev/null IN . INTO result_2.txt EXPECTING 4 - -REPLACE $CWD_URI WITH '[CWD_URI]' IN result_2.txt -REPLACE $CWD WITH '[CWD]' IN result_2.txt - -WRITE expected_2.txt UNTIL EOF 1> { 1> "error": "Could not resolve the reference to an external schema", 1> "identifier": "[CWD_URI]/description.json", @@ -64,4 +47,4 @@ WRITE expected_2.txt UNTIL EOF 1> } EOF -COMPARE result_2.txt AGAINST expected_2.txt +COMPARE result_1.txt AGAINST expected_1.txt