From 41c434e0b0740bb8f7fba62b6ebbb6a52b9e7d80 Mon Sep 17 00:00:00 2001 From: Adam Basfop Cavendish Date: Mon, 15 Jun 2026 14:31:03 +0800 Subject: [PATCH] fix: optional request bodies across generated clients Rust: - Preserve requestBody.required in shared API body planning - Emit Option<&T> for optional request bodies and skip body setup on None - Keep ureq POST-compatible by sending an empty body when optional input is absent Tests: - Regenerate multipart edge-case goldens for reqwest, ureq, and aioduct - Add optional non-multipart request body fixture across all golden suites - Pin JSON, text, binary, and required-body behavior in generated goldens --- src/generators/rust/aioduct/sigil_emit_api.rs | 67 ++- src/generators/rust/common/emit_api.rs | 9 +- src/generators/rust/reqwest/sigil_emit_api.rs | 67 ++- src/generators/rust/ureq/sigil_emit_api.rs | 108 ++-- .../valid/optional-request-bodies.yaml | 66 +++ .../optional-request-bodies/README.md.golden | 7 + .../apis/default.go.golden | 169 +++++++ .../optional-request-bodies/go.mod.golden | 3 + .../models/payload.go.golden | 11 + .../runtime/auth.go.golden | 113 +++++ .../runtime/client.go.golden | 99 ++++ .../runtime/errors.go.golden | 31 ++ .../optional-request-bodies/README.md.golden | 7 + .../apis/DefaultApi.java.golden | 198 ++++++++ .../build.gradle.golden | 21 + .../models/Payload.java.golden | 24 + .../runtime/ApiClient.java.golden | 79 +++ .../runtime/ApiException.java.golden | 31 ++ .../runtime/ApiKeyAuth.java.golden | 42 ++ .../runtime/ApiKeyLocation.java.golden | 11 + .../runtime/Authenticator.java.golden | 12 + .../runtime/BearerAuth.java.golden | 30 ++ .../optional-request-bodies/README.md.golden | 7 + .../apis/DefaultApi.kt.golden | 121 +++++ .../build.gradle.kts.golden | 19 + .../models/Payload.kt.golden | 9 + .../runtime/ApiClient.kt.golden | 62 +++ .../runtime/ApiException.kt.golden | 12 + .../runtime/Auth.kt.golden | 71 +++ .../optional-request-bodies/README.md.golden | 7 + .../__init__.py.golden | 10 + .../apis/__init__.py.golden | 6 + .../apis/default_api.py.golden | 50 ++ .../models/__init__.py.golden | 6 + .../models/payload.py.golden | 27 + .../optional_request_bodies/py.typed.golden | 0 .../runtime/__init__.py.golden | 10 + .../runtime/auth.py.golden | 51 ++ .../runtime/client.py.golden | 71 +++ .../runtime/errors.py.golden | 18 + .../pyproject.toml.golden | 10 + .../optional-request-bodies/README.md.golden | 7 + .../__init__.py.golden | 10 + .../apis/__init__.py.golden | 6 + .../apis/default_api.py.golden | 50 ++ .../models/__init__.py.golden | 6 + .../models/payload.py.golden | 27 + .../optional_request_bodies/py.typed.golden | 0 .../runtime/__init__.py.golden | 10 + .../runtime/auth.py.golden | 51 ++ .../runtime/client.py.golden | 99 ++++ .../runtime/errors.py.golden | 18 + .../pyproject.toml.golden | 10 + .../src/apis/multipart.rs.golden | 36 +- .../optional-request-bodies/Cargo.toml.golden | 12 + .../optional-request-bodies/README.md.golden | 7 + .../src/apis/default.rs.golden | 106 ++++ .../src/apis/mod.rs.golden | 8 + .../optional-request-bodies/src/lib.rs.golden | 11 + .../src/models/mod.rs.golden | 8 + .../src/models/payload.rs.golden | 14 + .../src/runtime/auth.rs.golden | 180 +++++++ .../src/runtime/client.rs.golden | 112 +++++ .../src/runtime/error.rs.golden | 60 +++ .../src/runtime/mod.rs.golden | 9 + .../src/apis/multipart.rs.golden | 36 +- .../optional-request-bodies/Cargo.toml.golden | 12 + .../optional-request-bodies/README.md.golden | 7 + .../src/apis/default.rs.golden | 106 ++++ .../src/apis/mod.rs.golden | 8 + .../optional-request-bodies/src/lib.rs.golden | 11 + .../src/models/mod.rs.golden | 8 + .../src/models/payload.rs.golden | 14 + .../src/runtime/auth.rs.golden | 189 +++++++ .../src/runtime/client.rs.golden | 73 +++ .../src/runtime/error.rs.golden | 65 +++ .../src/runtime/mod.rs.golden | 9 + .../src/apis/multipart.rs.golden | 74 +-- .../optional-request-bodies/Cargo.toml.golden | 12 + .../optional-request-bodies/README.md.golden | 7 + .../src/apis/default.rs.golden | 114 +++++ .../src/apis/mod.rs.golden | 8 + .../optional-request-bodies/src/lib.rs.golden | 11 + .../src/models/mod.rs.golden | 8 + .../src/models/payload.rs.golden | 14 + .../src/runtime/auth.rs.golden | 169 +++++++ .../src/runtime/client.rs.golden | 94 ++++ .../src/runtime/error.rs.golden | 64 +++ .../src/runtime/mod.rs.golden | 9 + .../optional-request-bodies/README.md.golden | 248 ++++++++++ .../apis/DefaultApi.ts.golden | 216 ++++++++ .../apis/index.ts.golden | 18 + .../optional-request-bodies/index.ts.golden | 9 + .../models/Payload.ts.golden | 10 + .../models/index.ts.golden | 7 + .../package.json.golden | 26 + .../runtime/runtime.ts.golden | 461 ++++++++++++++++++ .../tsconfig.esm.json.golden | 7 + .../tsconfig.json.golden | 31 ++ tests/golden_tests_go_http.rs | 2 + tests/golden_tests_java_okhttp.rs | 2 + tests/golden_tests_kotlin_okhttp.rs | 2 + tests/golden_tests_python_httpx.rs | 2 + tests/golden_tests_python_requests.rs | 2 + tests/golden_tests_rust_aioduct.rs | 2 + tests/golden_tests_rust_reqwest.rs | 2 + tests/golden_tests_rust_ureq.rs | 2 + tests/golden_tests_typescript_fetch.rs | 2 + 108 files changed, 4655 insertions(+), 147 deletions(-) create mode 100644 tests/fixtures/valid/optional-request-bodies.yaml create mode 100644 tests/golden/go/go-http/optional-request-bodies/README.md.golden create mode 100644 tests/golden/go/go-http/optional-request-bodies/apis/default.go.golden create mode 100644 tests/golden/go/go-http/optional-request-bodies/go.mod.golden create mode 100644 tests/golden/go/go-http/optional-request-bodies/models/payload.go.golden create mode 100644 tests/golden/go/go-http/optional-request-bodies/runtime/auth.go.golden create mode 100644 tests/golden/go/go-http/optional-request-bodies/runtime/client.go.golden create mode 100644 tests/golden/go/go-http/optional-request-bodies/runtime/errors.go.golden create mode 100644 tests/golden/java/java-okhttp/optional-request-bodies/README.md.golden create mode 100644 tests/golden/java/java-okhttp/optional-request-bodies/apis/DefaultApi.java.golden create mode 100644 tests/golden/java/java-okhttp/optional-request-bodies/build.gradle.golden create mode 100644 tests/golden/java/java-okhttp/optional-request-bodies/models/Payload.java.golden create mode 100644 tests/golden/java/java-okhttp/optional-request-bodies/runtime/ApiClient.java.golden create mode 100644 tests/golden/java/java-okhttp/optional-request-bodies/runtime/ApiException.java.golden create mode 100644 tests/golden/java/java-okhttp/optional-request-bodies/runtime/ApiKeyAuth.java.golden create mode 100644 tests/golden/java/java-okhttp/optional-request-bodies/runtime/ApiKeyLocation.java.golden create mode 100644 tests/golden/java/java-okhttp/optional-request-bodies/runtime/Authenticator.java.golden create mode 100644 tests/golden/java/java-okhttp/optional-request-bodies/runtime/BearerAuth.java.golden create mode 100644 tests/golden/kotlin/kotlin-okhttp/optional-request-bodies/README.md.golden create mode 100644 tests/golden/kotlin/kotlin-okhttp/optional-request-bodies/apis/DefaultApi.kt.golden create mode 100644 tests/golden/kotlin/kotlin-okhttp/optional-request-bodies/build.gradle.kts.golden create mode 100644 tests/golden/kotlin/kotlin-okhttp/optional-request-bodies/models/Payload.kt.golden create mode 100644 tests/golden/kotlin/kotlin-okhttp/optional-request-bodies/runtime/ApiClient.kt.golden create mode 100644 tests/golden/kotlin/kotlin-okhttp/optional-request-bodies/runtime/ApiException.kt.golden create mode 100644 tests/golden/kotlin/kotlin-okhttp/optional-request-bodies/runtime/Auth.kt.golden create mode 100644 tests/golden/python/python-httpx/optional-request-bodies/README.md.golden create mode 100644 tests/golden/python/python-httpx/optional-request-bodies/optional_request_bodies/__init__.py.golden create mode 100644 tests/golden/python/python-httpx/optional-request-bodies/optional_request_bodies/apis/__init__.py.golden create mode 100644 tests/golden/python/python-httpx/optional-request-bodies/optional_request_bodies/apis/default_api.py.golden create mode 100644 tests/golden/python/python-httpx/optional-request-bodies/optional_request_bodies/models/__init__.py.golden create mode 100644 tests/golden/python/python-httpx/optional-request-bodies/optional_request_bodies/models/payload.py.golden create mode 100644 tests/golden/python/python-httpx/optional-request-bodies/optional_request_bodies/py.typed.golden create mode 100644 tests/golden/python/python-httpx/optional-request-bodies/optional_request_bodies/runtime/__init__.py.golden create mode 100644 tests/golden/python/python-httpx/optional-request-bodies/optional_request_bodies/runtime/auth.py.golden create mode 100644 tests/golden/python/python-httpx/optional-request-bodies/optional_request_bodies/runtime/client.py.golden create mode 100644 tests/golden/python/python-httpx/optional-request-bodies/optional_request_bodies/runtime/errors.py.golden create mode 100644 tests/golden/python/python-httpx/optional-request-bodies/pyproject.toml.golden create mode 100644 tests/golden/python/python-requests/optional-request-bodies/README.md.golden create mode 100644 tests/golden/python/python-requests/optional-request-bodies/optional_request_bodies/__init__.py.golden create mode 100644 tests/golden/python/python-requests/optional-request-bodies/optional_request_bodies/apis/__init__.py.golden create mode 100644 tests/golden/python/python-requests/optional-request-bodies/optional_request_bodies/apis/default_api.py.golden create mode 100644 tests/golden/python/python-requests/optional-request-bodies/optional_request_bodies/models/__init__.py.golden create mode 100644 tests/golden/python/python-requests/optional-request-bodies/optional_request_bodies/models/payload.py.golden create mode 100644 tests/golden/python/python-requests/optional-request-bodies/optional_request_bodies/py.typed.golden create mode 100644 tests/golden/python/python-requests/optional-request-bodies/optional_request_bodies/runtime/__init__.py.golden create mode 100644 tests/golden/python/python-requests/optional-request-bodies/optional_request_bodies/runtime/auth.py.golden create mode 100644 tests/golden/python/python-requests/optional-request-bodies/optional_request_bodies/runtime/client.py.golden create mode 100644 tests/golden/python/python-requests/optional-request-bodies/optional_request_bodies/runtime/errors.py.golden create mode 100644 tests/golden/python/python-requests/optional-request-bodies/pyproject.toml.golden create mode 100644 tests/golden/rust/rust-aioduct/optional-request-bodies/Cargo.toml.golden create mode 100644 tests/golden/rust/rust-aioduct/optional-request-bodies/README.md.golden create mode 100644 tests/golden/rust/rust-aioduct/optional-request-bodies/src/apis/default.rs.golden create mode 100644 tests/golden/rust/rust-aioduct/optional-request-bodies/src/apis/mod.rs.golden create mode 100644 tests/golden/rust/rust-aioduct/optional-request-bodies/src/lib.rs.golden create mode 100644 tests/golden/rust/rust-aioduct/optional-request-bodies/src/models/mod.rs.golden create mode 100644 tests/golden/rust/rust-aioduct/optional-request-bodies/src/models/payload.rs.golden create mode 100644 tests/golden/rust/rust-aioduct/optional-request-bodies/src/runtime/auth.rs.golden create mode 100644 tests/golden/rust/rust-aioduct/optional-request-bodies/src/runtime/client.rs.golden create mode 100644 tests/golden/rust/rust-aioduct/optional-request-bodies/src/runtime/error.rs.golden create mode 100644 tests/golden/rust/rust-aioduct/optional-request-bodies/src/runtime/mod.rs.golden create mode 100644 tests/golden/rust/rust-reqwest/optional-request-bodies/Cargo.toml.golden create mode 100644 tests/golden/rust/rust-reqwest/optional-request-bodies/README.md.golden create mode 100644 tests/golden/rust/rust-reqwest/optional-request-bodies/src/apis/default.rs.golden create mode 100644 tests/golden/rust/rust-reqwest/optional-request-bodies/src/apis/mod.rs.golden create mode 100644 tests/golden/rust/rust-reqwest/optional-request-bodies/src/lib.rs.golden create mode 100644 tests/golden/rust/rust-reqwest/optional-request-bodies/src/models/mod.rs.golden create mode 100644 tests/golden/rust/rust-reqwest/optional-request-bodies/src/models/payload.rs.golden create mode 100644 tests/golden/rust/rust-reqwest/optional-request-bodies/src/runtime/auth.rs.golden create mode 100644 tests/golden/rust/rust-reqwest/optional-request-bodies/src/runtime/client.rs.golden create mode 100644 tests/golden/rust/rust-reqwest/optional-request-bodies/src/runtime/error.rs.golden create mode 100644 tests/golden/rust/rust-reqwest/optional-request-bodies/src/runtime/mod.rs.golden create mode 100644 tests/golden/rust/rust-ureq/optional-request-bodies/Cargo.toml.golden create mode 100644 tests/golden/rust/rust-ureq/optional-request-bodies/README.md.golden create mode 100644 tests/golden/rust/rust-ureq/optional-request-bodies/src/apis/default.rs.golden create mode 100644 tests/golden/rust/rust-ureq/optional-request-bodies/src/apis/mod.rs.golden create mode 100644 tests/golden/rust/rust-ureq/optional-request-bodies/src/lib.rs.golden create mode 100644 tests/golden/rust/rust-ureq/optional-request-bodies/src/models/mod.rs.golden create mode 100644 tests/golden/rust/rust-ureq/optional-request-bodies/src/models/payload.rs.golden create mode 100644 tests/golden/rust/rust-ureq/optional-request-bodies/src/runtime/auth.rs.golden create mode 100644 tests/golden/rust/rust-ureq/optional-request-bodies/src/runtime/client.rs.golden create mode 100644 tests/golden/rust/rust-ureq/optional-request-bodies/src/runtime/error.rs.golden create mode 100644 tests/golden/rust/rust-ureq/optional-request-bodies/src/runtime/mod.rs.golden create mode 100644 tests/golden/typescript/typescript-fetch/optional-request-bodies/README.md.golden create mode 100644 tests/golden/typescript/typescript-fetch/optional-request-bodies/apis/DefaultApi.ts.golden create mode 100644 tests/golden/typescript/typescript-fetch/optional-request-bodies/apis/index.ts.golden create mode 100644 tests/golden/typescript/typescript-fetch/optional-request-bodies/index.ts.golden create mode 100644 tests/golden/typescript/typescript-fetch/optional-request-bodies/models/Payload.ts.golden create mode 100644 tests/golden/typescript/typescript-fetch/optional-request-bodies/models/index.ts.golden create mode 100644 tests/golden/typescript/typescript-fetch/optional-request-bodies/package.json.golden create mode 100644 tests/golden/typescript/typescript-fetch/optional-request-bodies/runtime/runtime.ts.golden create mode 100644 tests/golden/typescript/typescript-fetch/optional-request-bodies/tsconfig.esm.json.golden create mode 100644 tests/golden/typescript/typescript-fetch/optional-request-bodies/tsconfig.json.golden diff --git a/src/generators/rust/aioduct/sigil_emit_api.rs b/src/generators/rust/aioduct/sigil_emit_api.rs index ffec087ab..331343574 100644 --- a/src/generators/rust/aioduct/sigil_emit_api.rs +++ b/src/generators/rust/aioduct/sigil_emit_api.rs @@ -38,8 +38,13 @@ pub fn emit_method_body(plan: &OpPlan<'_>) -> CodeBlock { && matches!(&body.encoding, BodyEncoding::Multipart) && !body.multipart_supported { + if body.required { + b.add_code(unsupported_multipart_body(&body.var_name)); + return b.build().unwrap(); + } + b.begin_control_flow(&format!("if {}.is_some()", body.var_name), ()); b.add_code(unsupported_multipart_body(&body.var_name)); - return b.build().unwrap(); + b.end_control_flow(); } // Build path @@ -130,27 +135,18 @@ pub fn emit_method_body(plan: &OpPlan<'_>) -> CodeBlock { // Body if let Some(body) = body { - match &body.encoding { - BodyEncoding::Json => { - b.add_code(json_body(&body.var_name)); - } - BodyEncoding::FormUrlEncoded => { - b.add_code(form_body(&body.var_name)); - } - BodyEncoding::Xml => { - b.add_code(xml_body(&body.var_name, &body.media_type)); - } - BodyEncoding::TextPlain => { - b.add_code(text_body(&body.var_name, &body.media_type)); - } - BodyEncoding::OctetStream => { - b.add_code(octet_stream_body(&body.var_name, &body.media_type)); - } - BodyEncoding::Multipart => { - emit_multipart_body(&mut b, &body.var_name, &body.multipart_parts); + let can_emit_body = + !matches!(&body.encoding, BodyEncoding::Multipart) || body.multipart_supported; + if can_emit_body { + if !body.required { + b.begin_control_flow( + &format!("if let Some({}) = {}", body.var_name, body.var_name), + (), + ); } - BodyEncoding::Other(media_type) => { - b.add_code(unsupported_media_type_body(media_type)); + emit_body(&mut b, body); + if !body.required { + b.end_control_flow(); } } } @@ -174,6 +170,35 @@ pub fn emit_method_body(plan: &OpPlan<'_>) -> CodeBlock { b.build().unwrap() } +fn emit_body( + b: &mut sigil_stitch::code_block::CodeBlockBuilder, + body: &crate::generators::rust::common::emit_api::BodyBinding, +) { + match &body.encoding { + BodyEncoding::Json => { + b.add_code(json_body(&body.var_name)); + } + BodyEncoding::FormUrlEncoded => { + b.add_code(form_body(&body.var_name)); + } + BodyEncoding::Xml => { + b.add_code(xml_body(&body.var_name, &body.media_type)); + } + BodyEncoding::TextPlain => { + b.add_code(text_body(&body.var_name, &body.media_type)); + } + BodyEncoding::OctetStream => { + b.add_code(octet_stream_body(&body.var_name, &body.media_type)); + } + BodyEncoding::Multipart => { + emit_multipart_body(b, &body.var_name, &body.multipart_parts); + } + BodyEncoding::Other(media_type) => { + b.add_code(unsupported_media_type_body(media_type)); + } + } +} + fn unsupported_multipart_body(body_var: &str) -> CodeBlock { sigil_quote!(RustLang { let _ = self.client; diff --git a/src/generators/rust/common/emit_api.rs b/src/generators/rust/common/emit_api.rs index 4603aab26..0ce6e8e07 100644 --- a/src/generators/rust/common/emit_api.rs +++ b/src/generators/rust/common/emit_api.rs @@ -224,6 +224,7 @@ pub struct BodyBinding { pub var_name: String, pub rust_type: String, pub media_type: String, + pub required: bool, pub encoding: BodyEncoding, pub multipart_supported: bool, pub multipart_parts: Vec, @@ -331,6 +332,7 @@ pub fn plan_body( var_name, rust_type, media_type, + required: b.required, encoding, multipart_supported, multipart_parts, @@ -447,7 +449,12 @@ fn emit_operation( params.push(format!("{}: {ty}", p.var_name)); } if let Some(body) = &plan.body { - params.push(format!("{}: &{}", body.var_name, body.rust_type)); + let ty = if body.required { + format!("&{}", body.rust_type) + } else { + format!("Option<&{}>", body.rust_type) + }; + params.push(format!("{}: {ty}", body.var_name)); } let async_kw = if config.is_async { "async " } else { "" }; diff --git a/src/generators/rust/reqwest/sigil_emit_api.rs b/src/generators/rust/reqwest/sigil_emit_api.rs index 658125800..92ff93010 100644 --- a/src/generators/rust/reqwest/sigil_emit_api.rs +++ b/src/generators/rust/reqwest/sigil_emit_api.rs @@ -38,8 +38,13 @@ pub fn emit_method_body(plan: &OpPlan<'_>) -> CodeBlock { && matches!(&body.encoding, BodyEncoding::Multipart) && !body.multipart_supported { + if body.required { + b.add_code(unsupported_multipart_body(&body.var_name)); + return b.build().unwrap(); + } + b.begin_control_flow(&format!("if {}.is_some()", body.var_name), ()); b.add_code(unsupported_multipart_body(&body.var_name)); - return b.build().unwrap(); + b.end_control_flow(); } // Build path @@ -127,27 +132,18 @@ pub fn emit_method_body(plan: &OpPlan<'_>) -> CodeBlock { // Body if let Some(body) = body { - match &body.encoding { - BodyEncoding::Json => { - b.add_code(json_body(&body.var_name)); - } - BodyEncoding::FormUrlEncoded => { - b.add_code(form_body(&body.var_name)); - } - BodyEncoding::Xml => { - b.add_code(xml_body(&body.var_name, &body.media_type)); - } - BodyEncoding::TextPlain => { - b.add_code(text_body(&body.var_name, &body.media_type)); - } - BodyEncoding::OctetStream => { - b.add_code(octet_stream_body(&body.var_name, &body.media_type)); - } - BodyEncoding::Multipart => { - emit_multipart_body(&mut b, &body.var_name, &body.multipart_parts); + let can_emit_body = + !matches!(&body.encoding, BodyEncoding::Multipart) || body.multipart_supported; + if can_emit_body { + if !body.required { + b.begin_control_flow( + &format!("if let Some({}) = {}", body.var_name, body.var_name), + (), + ); } - BodyEncoding::Other(media_type) => { - b.add_code(unsupported_media_type_body(media_type)); + emit_body(&mut b, body); + if !body.required { + b.end_control_flow(); } } } @@ -174,6 +170,35 @@ pub fn emit_method_body(plan: &OpPlan<'_>) -> CodeBlock { b.build().unwrap() } +fn emit_body( + b: &mut sigil_stitch::code_block::CodeBlockBuilder, + body: &crate::generators::rust::common::emit_api::BodyBinding, +) { + match &body.encoding { + BodyEncoding::Json => { + b.add_code(json_body(&body.var_name)); + } + BodyEncoding::FormUrlEncoded => { + b.add_code(form_body(&body.var_name)); + } + BodyEncoding::Xml => { + b.add_code(xml_body(&body.var_name, &body.media_type)); + } + BodyEncoding::TextPlain => { + b.add_code(text_body(&body.var_name, &body.media_type)); + } + BodyEncoding::OctetStream => { + b.add_code(octet_stream_body(&body.var_name, &body.media_type)); + } + BodyEncoding::Multipart => { + emit_multipart_body(b, &body.var_name, &body.multipart_parts); + } + BodyEncoding::Other(media_type) => { + b.add_code(unsupported_media_type_body(media_type)); + } + } +} + fn unsupported_multipart_body(body_var: &str) -> CodeBlock { sigil_quote!(RustLang { let _ = self.client; diff --git a/src/generators/rust/ureq/sigil_emit_api.rs b/src/generators/rust/ureq/sigil_emit_api.rs index 11b072572..d55985bbd 100644 --- a/src/generators/rust/ureq/sigil_emit_api.rs +++ b/src/generators/rust/ureq/sigil_emit_api.rs @@ -42,8 +42,13 @@ pub fn emit_method_body(plan: &OpPlan<'_>) -> CodeBlock { && matches!(&body.encoding, BodyEncoding::Multipart) && !body.multipart_supported { + if body.required { + b.add_code(unsupported_multipart_body(&body.var_name)); + return b.build().unwrap(); + } + b.begin_control_flow(&format!("if {}.is_some()", body.var_name), ()); b.add_code(unsupported_multipart_body(&body.var_name)); - return b.build().unwrap(); + b.end_control_flow(); } // Build path @@ -143,30 +148,25 @@ pub fn emit_method_body(plan: &OpPlan<'_>) -> CodeBlock { // Send if is_body_method { if let Some(body) = body { - match &body.encoding { - BodyEncoding::Json => { - b.add_code(json_send(&body.var_name)); - } - BodyEncoding::FormUrlEncoded => { - emit_form_body(&mut b, &body.var_name); - b.add_code(form_send()); - } - BodyEncoding::Xml => { - b.add_code(xml_send(&body.var_name, &body.media_type)); - } - BodyEncoding::TextPlain => { - b.add_code(text_send(&body.var_name, &body.media_type)); - } - BodyEncoding::OctetStream => { - b.add_code(octet_stream_send(&body.var_name, &body.media_type)); - } - BodyEncoding::Multipart => { - emit_multipart_body(&mut b, &body.var_name, &body.multipart_parts); - b.add_code(multipart_send()); - } - BodyEncoding::Other(media_type) => { - b.add_code(unsupported_media_type_body(media_type)); + let can_emit_body = + !matches!(&body.encoding, BodyEncoding::Multipart) || body.multipart_supported; + if can_emit_body { + if body.required { + emit_body_send(&mut b, body, "let resp ="); + } else { + b.add("let resp;\n", ()); + b.begin_control_flow( + &format!("if let Some({}) = {}", body.var_name, body.var_name), + (), + ); + emit_body_send(&mut b, body, "resp ="); + b.end_control_flow(); + b.begin_control_flow("else", ()); + b.add("resp = req.send_empty()?;\n", ()); + b.end_control_flow(); } + } else { + b.add("let resp = req.send_empty()?;\n", ()); } } else { b.add("let resp = req.send_empty()?;\n", ()); @@ -191,6 +191,42 @@ pub fn emit_method_body(plan: &OpPlan<'_>) -> CodeBlock { b.build().unwrap() } +fn emit_body_send( + b: &mut sigil_stitch::code_block::CodeBlockBuilder, + body: &crate::generators::rust::common::emit_api::BodyBinding, + resp_prefix: &str, +) { + match &body.encoding { + BodyEncoding::Json => { + b.add_code(json_send(&body.var_name, resp_prefix)); + } + BodyEncoding::FormUrlEncoded => { + emit_form_body(b, &body.var_name); + b.add_code(form_send(resp_prefix)); + } + BodyEncoding::Xml => { + b.add_code(xml_send(&body.var_name, &body.media_type, resp_prefix)); + } + BodyEncoding::TextPlain => { + b.add_code(text_send(&body.var_name, &body.media_type, resp_prefix)); + } + BodyEncoding::OctetStream => { + b.add_code(octet_stream_send( + &body.var_name, + &body.media_type, + resp_prefix, + )); + } + BodyEncoding::Multipart => { + emit_multipart_body(b, &body.var_name, &body.multipart_parts); + b.add_code(multipart_send(resp_prefix)); + } + BodyEncoding::Other(media_type) => { + b.add_code(unsupported_media_type_body(media_type)); + } + } +} + fn unsupported_multipart_body(body_var: &str) -> CodeBlock { sigil_quote!(RustLang { let _ = self.client; @@ -208,49 +244,49 @@ fn unsupported_media_type_body(media_type: &str) -> CodeBlock { .expect("unsupported media type body builds") } -fn json_send(body_var: &str) -> CodeBlock { +fn json_send(body_var: &str, resp_prefix: &str) -> CodeBlock { sigil_quote!(RustLang { - let resp = req.send_json(&$L(body_var))?; + $L(resp_prefix) req.send_json(&$L(body_var))?; }) .expect("json send builds") } -fn form_send() -> CodeBlock { +fn form_send(resp_prefix: &str) -> CodeBlock { let pairs_iter = "form_pairs.iter().map(|(key, value)| (key.as_str(), value.as_str()))"; sigil_quote!(RustLang { - let resp = req.send_form($L(pairs_iter))?; + $L(resp_prefix) req.send_form($L(pairs_iter))?; }) .expect("form send builds") } -fn xml_send(body_var: &str, media_type: &str) -> CodeBlock { +fn xml_send(body_var: &str, media_type: &str, resp_prefix: &str) -> CodeBlock { sigil_quote!(RustLang { let body_xml = serde_xml_rs::to_string($L(body_var))?; req = req.header("Content-Type", $L(rust_string_literal(media_type))); - let resp = req.send(body_xml)?; + $L(resp_prefix) req.send(body_xml)?; }) .expect("xml send builds") } -fn text_send(body_var: &str, media_type: &str) -> CodeBlock { +fn text_send(body_var: &str, media_type: &str, resp_prefix: &str) -> CodeBlock { sigil_quote!(RustLang { req = req.header("Content-Type", $L(rust_string_literal(media_type))); - let resp = req.send($L(body_var).as_str())?; + $L(resp_prefix) req.send($L(body_var).as_str())?; }) .expect("text send builds") } -fn octet_stream_send(body_var: &str, media_type: &str) -> CodeBlock { +fn octet_stream_send(body_var: &str, media_type: &str, resp_prefix: &str) -> CodeBlock { sigil_quote!(RustLang { req = req.header("Content-Type", $L(rust_string_literal(media_type))); - let resp = req.send($L(body_var).clone())?; + $L(resp_prefix) req.send($L(body_var).clone())?; }) .expect("octet-stream send builds") } -fn multipart_send() -> CodeBlock { +fn multipart_send(resp_prefix: &str) -> CodeBlock { sigil_quote!(RustLang { - let resp = req.send(multipart_body)?; + $L(resp_prefix) req.send(multipart_body)?; }) .expect("multipart send builds") } diff --git a/tests/fixtures/valid/optional-request-bodies.yaml b/tests/fixtures/valid/optional-request-bodies.yaml new file mode 100644 index 000000000..294cf0c16 --- /dev/null +++ b/tests/fixtures/valid/optional-request-bodies.yaml @@ -0,0 +1,66 @@ +openapi: 3.1.0 +info: + title: Optional Request Bodies + version: 1.0.0 + description: Covers optional non-multipart request bodies. +paths: + /optional-json: + post: + operationId: send_optional_json + requestBody: + required: false + content: + application/json: + schema: + $ref: '#/components/schemas/Payload' + responses: + '204': + description: Accepted + /optional-text: + post: + operationId: send_optional_text + requestBody: + required: false + content: + text/plain: + schema: + type: string + responses: + '204': + description: Accepted + /optional-binary: + post: + operationId: send_optional_binary + requestBody: + required: false + content: + application/octet-stream: + schema: + type: string + format: binary + responses: + '204': + description: Accepted + /required-json: + post: + operationId: send_required_json + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/Payload' + responses: + '204': + description: Accepted +components: + schemas: + Payload: + type: object + required: + - message + properties: + message: + type: string + count: + type: integer diff --git a/tests/golden/go/go-http/optional-request-bodies/README.md.golden b/tests/golden/go/go-http/optional-request-bodies/README.md.golden new file mode 100644 index 000000000..a60de19e5 --- /dev/null +++ b/tests/golden/go/go-http/optional-request-bodies/README.md.golden @@ -0,0 +1,7 @@ +# Optional Request Bodies + +Covers optional non-multipart request bodies. + +Version: `1.0.0` + +Generated by [openapi-nexus](https://github.com/adamcavendish/openapi-nexus) for `optional-request-bodies`. diff --git a/tests/golden/go/go-http/optional-request-bodies/apis/default.go.golden b/tests/golden/go/go-http/optional-request-bodies/apis/default.go.golden new file mode 100644 index 000000000..00bc56965 --- /dev/null +++ b/tests/golden/go/go-http/optional-request-bodies/apis/default.go.golden @@ -0,0 +1,169 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Optional Request Bodies — 1.0.0 +// Covers optional non-multipart request bodies. + +package apis + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "strings" + + "example.com/sdk/models" + "example.com/sdk/runtime" +) + +// DefaultAPI groups operations under the corresponding tag. +type DefaultAPI struct { + client *runtime.Client +} + +// NewDefaultAPI constructs a DefaultAPI bound to client. +func NewDefaultAPI(client *runtime.Client) *DefaultAPI { + return &DefaultAPI{client: client} +} + +// SendOptionalBinaryResponse carries the response from the corresponding operation. +type SendOptionalBinaryResponse struct { + StatusCode int + Raw *http.Response +} + +// SendOptionalBinary calls POST /optional-binary. +func (a *DefaultAPI) SendOptionalBinary(ctx context.Context, body []byte) (*SendOptionalBinaryResponse, error) { + path := "/optional-binary" + var bodyReader io.Reader + if body != nil { + bodyReader = bytes.NewReader(body) + } + req, err := a.client.NewRequest(ctx, "POST", path, nil, bodyReader) + if err != nil { + return nil, err + } + req.Header.Set("Content-Type", "application/octet-stream") + req.Header.Set("Accept", "application/json") + httpResp, err := a.client.Do(req) + if err != nil { + return nil, err + } + defer httpResp.Body.Close() + + resp := &SendOptionalBinaryResponse{StatusCode: httpResp.StatusCode, Raw: httpResp} + if httpResp.StatusCode >= 400 { + body, _ := io.ReadAll(httpResp.Body) + return nil, &runtime.APIError{StatusCode: httpResp.StatusCode, Status: httpResp.Status, Body: body} + } + return resp, nil +} + +// SendOptionalJsonResponse carries the response from the corresponding operation. +type SendOptionalJsonResponse struct { + StatusCode int + Raw *http.Response +} + +// SendOptionalJson calls POST /optional-json. +func (a *DefaultAPI) SendOptionalJson(ctx context.Context, body *models.Payload) (*SendOptionalJsonResponse, error) { + path := "/optional-json" + var bodyReader io.Reader + if body != nil { + buf, err := json.Marshal(body) + if err != nil { + return nil, fmt.Errorf("marshal body: %w", err) + } + bodyReader = bytes.NewReader(buf) + } + req, err := a.client.NewRequest(ctx, "POST", path, nil, bodyReader) + if err != nil { + return nil, err + } + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Accept", "application/json") + httpResp, err := a.client.Do(req) + if err != nil { + return nil, err + } + defer httpResp.Body.Close() + + resp := &SendOptionalJsonResponse{StatusCode: httpResp.StatusCode, Raw: httpResp} + if httpResp.StatusCode >= 400 { + body, _ := io.ReadAll(httpResp.Body) + return nil, &runtime.APIError{StatusCode: httpResp.StatusCode, Status: httpResp.Status, Body: body} + } + return resp, nil +} + +// SendOptionalTextResponse carries the response from the corresponding operation. +type SendOptionalTextResponse struct { + StatusCode int + Raw *http.Response +} + +// SendOptionalText calls POST /optional-text. +func (a *DefaultAPI) SendOptionalText(ctx context.Context, body *string) (*SendOptionalTextResponse, error) { + path := "/optional-text" + var bodyReader io.Reader + if body != nil { + bodyReader = strings.NewReader(*body) + } + req, err := a.client.NewRequest(ctx, "POST", path, nil, bodyReader) + if err != nil { + return nil, err + } + req.Header.Set("Content-Type", "text/plain") + req.Header.Set("Accept", "application/json") + httpResp, err := a.client.Do(req) + if err != nil { + return nil, err + } + defer httpResp.Body.Close() + + resp := &SendOptionalTextResponse{StatusCode: httpResp.StatusCode, Raw: httpResp} + if httpResp.StatusCode >= 400 { + body, _ := io.ReadAll(httpResp.Body) + return nil, &runtime.APIError{StatusCode: httpResp.StatusCode, Status: httpResp.Status, Body: body} + } + return resp, nil +} + +// SendRequiredJsonResponse carries the response from the corresponding operation. +type SendRequiredJsonResponse struct { + StatusCode int + Raw *http.Response +} + +// SendRequiredJson calls POST /required-json. +func (a *DefaultAPI) SendRequiredJson(ctx context.Context, body *models.Payload) (*SendRequiredJsonResponse, error) { + path := "/required-json" + var bodyReader io.Reader + if body != nil { + buf, err := json.Marshal(body) + if err != nil { + return nil, fmt.Errorf("marshal body: %w", err) + } + bodyReader = bytes.NewReader(buf) + } + req, err := a.client.NewRequest(ctx, "POST", path, nil, bodyReader) + if err != nil { + return nil, err + } + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Accept", "application/json") + httpResp, err := a.client.Do(req) + if err != nil { + return nil, err + } + defer httpResp.Body.Close() + + resp := &SendRequiredJsonResponse{StatusCode: httpResp.StatusCode, Raw: httpResp} + if httpResp.StatusCode >= 400 { + body, _ := io.ReadAll(httpResp.Body) + return nil, &runtime.APIError{StatusCode: httpResp.StatusCode, Status: httpResp.Status, Body: body} + } + return resp, nil +} diff --git a/tests/golden/go/go-http/optional-request-bodies/go.mod.golden b/tests/golden/go/go-http/optional-request-bodies/go.mod.golden new file mode 100644 index 000000000..54d0d116e --- /dev/null +++ b/tests/golden/go/go-http/optional-request-bodies/go.mod.golden @@ -0,0 +1,3 @@ +module example.com/sdk + +go 1.21 diff --git a/tests/golden/go/go-http/optional-request-bodies/models/payload.go.golden b/tests/golden/go/go-http/optional-request-bodies/models/payload.go.golden new file mode 100644 index 000000000..7082987f4 --- /dev/null +++ b/tests/golden/go/go-http/optional-request-bodies/models/payload.go.golden @@ -0,0 +1,11 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Optional Request Bodies — 1.0.0 +// Covers optional non-multipart request bodies. + +package models + +type Payload struct { + Count *int `json:"count,omitempty"` + Message string `json:"message"` +} diff --git a/tests/golden/go/go-http/optional-request-bodies/runtime/auth.go.golden b/tests/golden/go/go-http/optional-request-bodies/runtime/auth.go.golden new file mode 100644 index 000000000..08333d5e9 --- /dev/null +++ b/tests/golden/go/go-http/optional-request-bodies/runtime/auth.go.golden @@ -0,0 +1,113 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Optional Request Bodies — 1.0.0 +// Covers optional non-multipart request bodies. + +package runtime + +import ( + "encoding/base64" + "fmt" + "net/http" +) + +// Authenticator attaches credentials to an outgoing request. +// +// Return a non-nil error to abort the request before it leaves the client. +type Authenticator interface { + AuthenticateRequest(req *http.Request) error +} + +// BearerAuth sends `Authorization: Bearer `. +// +// Use the Token field for a static token, or TokenProvider for a +// function that returns the current token (evaluated per-request). +// If both are set, TokenProvider takes precedence. +type BearerAuth struct { + Token string + TokenProvider func() string +} + +func (b BearerAuth) AuthenticateRequest(req *http.Request) error { + token := b.Token + if b.TokenProvider != nil { + token = b.TokenProvider() + } + if token == "" { + return fmt.Errorf("bearer auth: empty token") + } + req.Header.Set("Authorization", "Bearer "+token) + return nil +} + +// APIKeyLocation enumerates where an API key is placed on the wire. +type APIKeyLocation int + +const ( + APIKeyInHeader APIKeyLocation = iota + APIKeyInQuery + APIKeyInCookie +) + +// APIKeyAuth attaches a named API key to a request. +// +// Use the Key field for a static key, or KeyProvider for a function +// that returns the current key (evaluated per-request). If both are +// set, KeyProvider takes precedence. +type APIKeyAuth struct { + Key string + KeyProvider func() string + Name string + Location APIKeyLocation +} + +func (a APIKeyAuth) AuthenticateRequest(req *http.Request) error { + key := a.Key + if a.KeyProvider != nil { + key = a.KeyProvider() + } + if key == "" || a.Name == "" { + return fmt.Errorf("api key auth: empty key or name") + } + switch a.Location { + case APIKeyInHeader: + req.Header.Set(a.Name, key) + case APIKeyInQuery: + q := req.URL.Query() + q.Set(a.Name, key) + req.URL.RawQuery = q.Encode() + case APIKeyInCookie: + req.AddCookie(&http.Cookie{Name: a.Name, Value: key}) + default: + return fmt.Errorf("api key auth: unknown location %d", a.Location) + } + return nil +} + +// BasicAuth sends `Authorization: Basic `. +// +// Use the Username/Password fields for static credentials, or +// UsernameProvider/PasswordProvider for functions that return the +// current credentials (evaluated per-request). If a provider is set, +// it takes precedence over the corresponding static field. +type BasicAuth struct { + Username string + Password string + UsernameProvider func() string + PasswordProvider func() string +} + +func (b BasicAuth) AuthenticateRequest(req *http.Request) error { + user := b.Username + if b.UsernameProvider != nil { + user = b.UsernameProvider() + } + pass := b.Password + if b.PasswordProvider != nil { + pass = b.PasswordProvider() + } + creds := user + ":" + pass + encoded := base64.StdEncoding.EncodeToString([]byte(creds)) + req.Header.Set("Authorization", "Basic "+encoded) + return nil +} diff --git a/tests/golden/go/go-http/optional-request-bodies/runtime/client.go.golden b/tests/golden/go/go-http/optional-request-bodies/runtime/client.go.golden new file mode 100644 index 000000000..108a0c6b1 --- /dev/null +++ b/tests/golden/go/go-http/optional-request-bodies/runtime/client.go.golden @@ -0,0 +1,99 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Optional Request Bodies — 1.0.0 +// Covers optional non-multipart request bodies. + +package runtime + +import ( + "context" + "fmt" + "io" + "net/http" + "net/url" + "strings" +) + +// Client is the transport-layer HTTP client shared by all APIs. +type Client struct { + baseURL string + httpClient *http.Client + authenticator Authenticator + defaultHeaders http.Header +} + +// Option configures a Client. +type Option func(*Client) + +// NewClient builds a Client rooted at baseURL. Options override defaults. +func NewClient(baseURL string, opts ...Option) *Client { + c := &Client{ + baseURL: strings.TrimRight(baseURL, "/"), + httpClient: http.DefaultClient, + defaultHeaders: make(http.Header), + } + for _, opt := range opts { + opt(c) + } + return c +} + +// WithHTTPClient swaps the underlying *http.Client. +func WithHTTPClient(hc *http.Client) Option { + return func(c *Client) { + if hc != nil { + c.httpClient = hc + } + } +} + +// WithAuth installs an Authenticator applied to every request. +func WithAuth(a Authenticator) Option { + return func(c *Client) { c.authenticator = a } +} + +// WithDefaultHeader sets a header applied to every request. Per-request +// headers take precedence. +func WithDefaultHeader(key, value string) Option { + return func(c *Client) { c.defaultHeaders.Set(key, value) } +} + +// BaseURL returns the base URL configured on the client. +func (c *Client) BaseURL() string { return c.baseURL } + +// HTTPClient returns the underlying *http.Client. +func (c *Client) HTTPClient() *http.Client { return c.httpClient } + +// NewRequest builds an *http.Request joined to the client's base URL. Query +// pairs (flat key=value list) are appended when non-empty. +func (c *Client) NewRequest( + ctx context.Context, + method, path string, + query url.Values, + body io.Reader, +) (*http.Request, error) { + full := c.baseURL + path + if len(query) > 0 { + full += "?" + query.Encode() + } + req, err := http.NewRequestWithContext(ctx, method, full, body) + if err != nil { + return nil, fmt.Errorf("build request: %w", err) + } + for k, vals := range c.defaultHeaders { + for _, v := range vals { + req.Header.Add(k, v) + } + } + return req, nil +} + +// Do authenticates and executes a request. +func (c *Client) Do(req *http.Request) (*http.Response, error) { + if c.authenticator != nil { + if err := c.authenticator.AuthenticateRequest(req); err != nil { + return nil, fmt.Errorf("authenticate: %w", err) + } + } + return c.httpClient.Do(req) +} diff --git a/tests/golden/go/go-http/optional-request-bodies/runtime/errors.go.golden b/tests/golden/go/go-http/optional-request-bodies/runtime/errors.go.golden new file mode 100644 index 000000000..fcb59c228 --- /dev/null +++ b/tests/golden/go/go-http/optional-request-bodies/runtime/errors.go.golden @@ -0,0 +1,31 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Optional Request Bodies — 1.0.0 +// Covers optional non-multipart request bodies. + +package runtime + +import ( + "errors" + "fmt" +) + +// APIError represents a non-2xx HTTP response from the API. +// +// Callers can type-assert via `errors.As` to inspect StatusCode and the raw +// response body. +type APIError struct { + StatusCode int + Status string + Body []byte +} + +func (e *APIError) Error() string { + return fmt.Sprintf("api error: %s", e.Status) +} + +// IsAPIError reports whether err is (or wraps) an *APIError. +func IsAPIError(err error) bool { + var apiErr *APIError + return errors.As(err, &apiErr) +} diff --git a/tests/golden/java/java-okhttp/optional-request-bodies/README.md.golden b/tests/golden/java/java-okhttp/optional-request-bodies/README.md.golden new file mode 100644 index 000000000..a60de19e5 --- /dev/null +++ b/tests/golden/java/java-okhttp/optional-request-bodies/README.md.golden @@ -0,0 +1,7 @@ +# Optional Request Bodies + +Covers optional non-multipart request bodies. + +Version: `1.0.0` + +Generated by [openapi-nexus](https://github.com/adamcavendish/openapi-nexus) for `optional-request-bodies`. diff --git a/tests/golden/java/java-okhttp/optional-request-bodies/apis/DefaultApi.java.golden b/tests/golden/java/java-okhttp/optional-request-bodies/apis/DefaultApi.java.golden new file mode 100644 index 000000000..342cedb11 --- /dev/null +++ b/tests/golden/java/java-okhttp/optional-request-bodies/apis/DefaultApi.java.golden @@ -0,0 +1,198 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Optional Request Bodies — 1.0.0 +// Covers optional non-multipart request bodies. + +package com.example.sdk.apis; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import com.example.sdk.models.*; +import com.example.sdk.runtime.ApiClient; +import com.example.sdk.runtime.ApiException; +import com.google.gson.Gson; +import com.google.gson.reflect.TypeToken; +import okhttp3.MediaType; +import okhttp3.Request; +import okhttp3.RequestBody; +import okhttp3.Response; + +/** + * SendOptionalBinaryResponse carries the response from sendOptionalBinary. + */ +public class SendOptionalBinaryResponse { + private final int statusCode; + private final Response raw; + + public SendOptionalBinaryResponse(int statusCode, Response raw) { + this.statusCode = statusCode; + this.raw = raw; + } + + public int getStatusCode() { + return this.statusCode; + } + + public Response getRaw() { + return this.raw; + } +} + +/** + * SendOptionalJsonResponse carries the response from sendOptionalJson. + */ +public class SendOptionalJsonResponse { + private final int statusCode; + private final Response raw; + + public SendOptionalJsonResponse(int statusCode, Response raw) { + this.statusCode = statusCode; + this.raw = raw; + } + + public int getStatusCode() { + return this.statusCode; + } + + public Response getRaw() { + return this.raw; + } +} + +/** + * SendOptionalTextResponse carries the response from sendOptionalText. + */ +public class SendOptionalTextResponse { + private final int statusCode; + private final Response raw; + + public SendOptionalTextResponse(int statusCode, Response raw) { + this.statusCode = statusCode; + this.raw = raw; + } + + public int getStatusCode() { + return this.statusCode; + } + + public Response getRaw() { + return this.raw; + } +} + +/** + * SendRequiredJsonResponse carries the response from sendRequiredJson. + */ +public class SendRequiredJsonResponse { + private final int statusCode; + private final Response raw; + + public SendRequiredJsonResponse(int statusCode, Response raw) { + this.statusCode = statusCode; + this.raw = raw; + } + + public int getStatusCode() { + return this.statusCode; + } + + public Response getRaw() { + return this.raw; + } +} + +/** + * DefaultApi groups operations under the default tag. + */ +public class DefaultApi { + private final ApiClient client; + private final Gson gson = new Gson(); + + public DefaultApi(ApiClient client) { + this.client = client; + } + + /** + * sendOptionalBinary POST /optional-binary. + */ + public SendOptionalBinaryResponse sendOptionalBinary(byte[] body) throws IOException { + String path = "/optional-binary"; + Request request; + RequestBody requestBody = RequestBody.create(new byte[0], null); + if (body != null) { + requestBody = RequestBody.create(body, MediaType.get("application/octet-stream")); + } + request = client.newRequestWithBody("POST", path, null, requestBody); + Response response = client.execute(request); + + if (!response.isSuccessful()) { + String errorBody = response.body() != null ? response.body().string() : ""; + throw new ApiException(response.code(), response.message(), errorBody); + } + return new SendOptionalBinaryResponse(response.code(), response); + } + + /** + * sendOptionalJson POST /optional-json. + */ + public SendOptionalJsonResponse sendOptionalJson(Payload body) throws IOException { + String path = "/optional-json"; + Request request; + RequestBody requestBody = RequestBody.create(new byte[0], null); + if (body != null) { + String jsonBody = gson.toJson(body); + requestBody = RequestBody.create(jsonBody, MediaType.get("application/json")); + } + request = client.newRequestWithBody("POST", path, null, requestBody); + Response response = client.execute(request); + + if (!response.isSuccessful()) { + String errorBody = response.body() != null ? response.body().string() : ""; + throw new ApiException(response.code(), response.message(), errorBody); + } + return new SendOptionalJsonResponse(response.code(), response); + } + + /** + * sendOptionalText POST /optional-text. + */ + public SendOptionalTextResponse sendOptionalText(String body) throws IOException { + String path = "/optional-text"; + Request request; + RequestBody requestBody = RequestBody.create(new byte[0], null); + if (body != null) { + requestBody = RequestBody.create(body, MediaType.get("text/plain")); + } + request = client.newRequestWithBody("POST", path, null, requestBody); + Response response = client.execute(request); + + if (!response.isSuccessful()) { + String errorBody = response.body() != null ? response.body().string() : ""; + throw new ApiException(response.code(), response.message(), errorBody); + } + return new SendOptionalTextResponse(response.code(), response); + } + + /** + * sendRequiredJson POST /required-json. + */ + public SendRequiredJsonResponse sendRequiredJson(Payload body) throws IOException { + String path = "/required-json"; + Request request; + String jsonBody = gson.toJson(body); + RequestBody requestBody = RequestBody.create(jsonBody, MediaType.get("application/json")); + request = client.newRequestWithBody("POST", path, null, requestBody); + Response response = client.execute(request); + + if (!response.isSuccessful()) { + String errorBody = response.body() != null ? response.body().string() : ""; + throw new ApiException(response.code(), response.message(), errorBody); + } + return new SendRequiredJsonResponse(response.code(), response); + } +} diff --git a/tests/golden/java/java-okhttp/optional-request-bodies/build.gradle.golden b/tests/golden/java/java-okhttp/optional-request-bodies/build.gradle.golden new file mode 100644 index 000000000..ec7d84dad --- /dev/null +++ b/tests/golden/java/java-okhttp/optional-request-bodies/build.gradle.golden @@ -0,0 +1,21 @@ +plugins { + id("java-library") +} + +group = "com.example.sdk" +version = "1.0.0" + +repositories { + mavenCentral() +} + +dependencies { + implementation("com.squareup.okhttp3:okhttp:4.12.0") + implementation("com.google.code.gson:gson:2.11.0") +} + +java { + toolchain { + languageVersion.set(JavaLanguageVersion.of(17)) + } +} diff --git a/tests/golden/java/java-okhttp/optional-request-bodies/models/Payload.java.golden b/tests/golden/java/java-okhttp/optional-request-bodies/models/Payload.java.golden new file mode 100644 index 000000000..41584ccad --- /dev/null +++ b/tests/golden/java/java-okhttp/optional-request-bodies/models/Payload.java.golden @@ -0,0 +1,24 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Optional Request Bodies — 1.0.0 +// Covers optional non-multipart request bodies. + +package com.example.sdk.models; + +public class Payload { + private Integer count; + private String message; + + public Payload(Integer count, String message) { + this.count = count; + this.message = message; + } + + public Integer getCount() { + return this.count; + } + + public String getMessage() { + return this.message; + } +} diff --git a/tests/golden/java/java-okhttp/optional-request-bodies/runtime/ApiClient.java.golden b/tests/golden/java/java-okhttp/optional-request-bodies/runtime/ApiClient.java.golden new file mode 100644 index 000000000..f497befed --- /dev/null +++ b/tests/golden/java/java-okhttp/optional-request-bodies/runtime/ApiClient.java.golden @@ -0,0 +1,79 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Optional Request Bodies — 1.0.0 +// Covers optional non-multipart request bodies. + +package com.example.sdk.runtime; + +import java.io.IOException; +import java.util.Map; + +import okhttp3.HttpUrl; +import okhttp3.MediaType; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.RequestBody; +import okhttp3.Response; + +public class ApiClient { + private static final MediaType JSON = MediaType.get("application/json"); + + private final String baseUrl; + private final OkHttpClient client; + private final Authenticator authenticator; + private final Map defaultHeaders; + + public ApiClient(String baseUrl) { + this(baseUrl, new OkHttpClient(), null, Map.of()); + } + + public ApiClient(String baseUrl, OkHttpClient client, Authenticator authenticator, Map defaultHeaders) { + this.baseUrl = baseUrl; + this.client = client; + this.authenticator = authenticator; + this.defaultHeaders = defaultHeaders; + } + + public Request newRequest(String method, String path, Map query, String body) { + RequestBody requestBody = null; + if (body != null) { + requestBody = RequestBody.create(body, JSON); + } + return buildRequest(method, path, query, requestBody); + } + + public Request newRequestWithBody(String method, String path, Map query, RequestBody body) { + return buildRequest(method, path, query, body); + } + + private Request buildRequest(String method, String path, Map query, RequestBody requestBody) { + String url = baseUrl.replaceAll("/+$", "") + path; + HttpUrl parsed = HttpUrl.parse(url); + if (parsed == null) { + throw new IllegalArgumentException("Invalid URL: " + url); + } + HttpUrl.Builder urlBuilder = parsed.newBuilder(); + if (query != null) { + for (Map.Entry entry : query.entrySet()) { + urlBuilder.addQueryParameter(entry.getKey(), entry.getValue()); + } + } + + Request.Builder builder = new Request.Builder() + .url(urlBuilder.build()) + .method(method, requestBody); + + for (Map.Entry entry : defaultHeaders.entrySet()) { + builder.header(entry.getKey(), entry.getValue()); + } + if (authenticator != null) { + authenticator.authenticate(builder); + } + + return builder.build(); + } + + public Response execute(Request request) throws IOException { + return client.newCall(request).execute(); + } +} diff --git a/tests/golden/java/java-okhttp/optional-request-bodies/runtime/ApiException.java.golden b/tests/golden/java/java-okhttp/optional-request-bodies/runtime/ApiException.java.golden new file mode 100644 index 000000000..6853792ba --- /dev/null +++ b/tests/golden/java/java-okhttp/optional-request-bodies/runtime/ApiException.java.golden @@ -0,0 +1,31 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Optional Request Bodies — 1.0.0 +// Covers optional non-multipart request bodies. + +package com.example.sdk.runtime; + +public class ApiException extends RuntimeException { + private final int statusCode; + private final String status; + private final String body; + + public ApiException(int statusCode, String status, String body) { + super("API error " + statusCode + ": " + status); + this.statusCode = statusCode; + this.status = status; + this.body = body; + } + + public int getStatusCode() { + return statusCode; + } + + public String getStatus() { + return status; + } + + public String getBody() { + return body; + } +} diff --git a/tests/golden/java/java-okhttp/optional-request-bodies/runtime/ApiKeyAuth.java.golden b/tests/golden/java/java-okhttp/optional-request-bodies/runtime/ApiKeyAuth.java.golden new file mode 100644 index 000000000..2da45f455 --- /dev/null +++ b/tests/golden/java/java-okhttp/optional-request-bodies/runtime/ApiKeyAuth.java.golden @@ -0,0 +1,42 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Optional Request Bodies — 1.0.0 +// Covers optional non-multipart request bodies. + +package com.example.sdk.runtime; + +import java.util.function.Supplier; +import okhttp3.Request; + +public class ApiKeyAuth implements Authenticator { + private final String key; + private final Supplier keyProvider; + private final String name; + private final ApiKeyLocation location; + + public ApiKeyAuth(String key, String name, ApiKeyLocation location) { + this.key = key; + this.keyProvider = null; + this.name = name; + this.location = location; + } + + public ApiKeyAuth(Supplier keyProvider, String name, ApiKeyLocation location) { + this.key = null; + this.keyProvider = keyProvider; + this.name = name; + this.location = location; + } + + @Override + public void authenticate(Request.Builder builder) { + String k = keyProvider != null ? keyProvider.get() : key; + if (location == ApiKeyLocation.HEADER) { + builder.header(name, k); + } else if (location == ApiKeyLocation.QUERY) { + builder.url(builder.build().url().newBuilder() + .addQueryParameter(name, k) + .build()); + } + } +} diff --git a/tests/golden/java/java-okhttp/optional-request-bodies/runtime/ApiKeyLocation.java.golden b/tests/golden/java/java-okhttp/optional-request-bodies/runtime/ApiKeyLocation.java.golden new file mode 100644 index 000000000..d7275f95b --- /dev/null +++ b/tests/golden/java/java-okhttp/optional-request-bodies/runtime/ApiKeyLocation.java.golden @@ -0,0 +1,11 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Optional Request Bodies — 1.0.0 +// Covers optional non-multipart request bodies. + +package com.example.sdk.runtime; + +public enum ApiKeyLocation { + HEADER, + QUERY +} diff --git a/tests/golden/java/java-okhttp/optional-request-bodies/runtime/Authenticator.java.golden b/tests/golden/java/java-okhttp/optional-request-bodies/runtime/Authenticator.java.golden new file mode 100644 index 000000000..ab0d79e73 --- /dev/null +++ b/tests/golden/java/java-okhttp/optional-request-bodies/runtime/Authenticator.java.golden @@ -0,0 +1,12 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Optional Request Bodies — 1.0.0 +// Covers optional non-multipart request bodies. + +package com.example.sdk.runtime; + +import okhttp3.Request; + +public interface Authenticator { + void authenticate(Request.Builder builder); +} diff --git a/tests/golden/java/java-okhttp/optional-request-bodies/runtime/BearerAuth.java.golden b/tests/golden/java/java-okhttp/optional-request-bodies/runtime/BearerAuth.java.golden new file mode 100644 index 000000000..892050bf5 --- /dev/null +++ b/tests/golden/java/java-okhttp/optional-request-bodies/runtime/BearerAuth.java.golden @@ -0,0 +1,30 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Optional Request Bodies — 1.0.0 +// Covers optional non-multipart request bodies. + +package com.example.sdk.runtime; + +import java.util.function.Supplier; +import okhttp3.Request; + +public class BearerAuth implements Authenticator { + private final String token; + private final Supplier tokenProvider; + + public BearerAuth(String token) { + this.token = token; + this.tokenProvider = null; + } + + public BearerAuth(Supplier tokenProvider) { + this.token = null; + this.tokenProvider = tokenProvider; + } + + @Override + public void authenticate(Request.Builder builder) { + String t = tokenProvider != null ? tokenProvider.get() : token; + builder.header("Authorization", "Bearer " + t); + } +} diff --git a/tests/golden/kotlin/kotlin-okhttp/optional-request-bodies/README.md.golden b/tests/golden/kotlin/kotlin-okhttp/optional-request-bodies/README.md.golden new file mode 100644 index 000000000..a60de19e5 --- /dev/null +++ b/tests/golden/kotlin/kotlin-okhttp/optional-request-bodies/README.md.golden @@ -0,0 +1,7 @@ +# Optional Request Bodies + +Covers optional non-multipart request bodies. + +Version: `1.0.0` + +Generated by [openapi-nexus](https://github.com/adamcavendish/openapi-nexus) for `optional-request-bodies`. diff --git a/tests/golden/kotlin/kotlin-okhttp/optional-request-bodies/apis/DefaultApi.kt.golden b/tests/golden/kotlin/kotlin-okhttp/optional-request-bodies/apis/DefaultApi.kt.golden new file mode 100644 index 000000000..4cd032f13 --- /dev/null +++ b/tests/golden/kotlin/kotlin-okhttp/optional-request-bodies/apis/DefaultApi.kt.golden @@ -0,0 +1,121 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Optional Request Bodies — 1.0.0 +// Covers optional non-multipart request bodies. + +package com.example.sdk.apis + +import com.example.sdk.models.* +import com.example.sdk.runtime.ApiClient +import com.example.sdk.runtime.ApiException +import com.google.gson.Gson +import com.google.gson.reflect.TypeToken +import okhttp3.MediaType.Companion.toMediaType +import okhttp3.RequestBody.Companion.toRequestBody +import okhttp3.Response + +/** + * SendOptionalBinaryResponse carries the response from sendOptionalBinary. + */ +data class SendOptionalBinaryResponse(val statusCode: Int, val raw: Response) { +} + +/** + * SendOptionalJsonResponse carries the response from sendOptionalJson. + */ +data class SendOptionalJsonResponse(val statusCode: Int, val raw: Response) { +} + +/** + * SendOptionalTextResponse carries the response from sendOptionalText. + */ +data class SendOptionalTextResponse(val statusCode: Int, val raw: Response) { +} + +/** + * SendRequiredJsonResponse carries the response from sendRequiredJson. + */ +data class SendRequiredJsonResponse(val statusCode: Int, val raw: Response) { +} + +/** + * DefaultApi groups operations under the default tag. + */ +class DefaultApi(private val client: ApiClient) { + private val gson: Gson = Gson() + + /** + * sendOptionalBinary POST /optional-binary. + */ + fun sendOptionalBinary(body: ByteArray?): SendOptionalBinaryResponse { + val path = "/optional-binary" + var requestBody = ByteArray(0).toRequestBody(null) + if (body != null) { + requestBody = body.toRequestBody("application/octet-stream".toMediaType()) + } + val request = client.newRequestWithBody("POST", path, null, requestBody) + val response = client.execute(request) + + if (!response.isSuccessful) { + val errorBody = response.body?.string() ?: "" + throw ApiException(response.code, response.message, errorBody) + } + return SendOptionalBinaryResponse(statusCode = response.code, raw = response) + } + + /** + * sendOptionalJson POST /optional-json. + */ + fun sendOptionalJson(body: Payload?): SendOptionalJsonResponse { + val path = "/optional-json" + var requestBody = ByteArray(0).toRequestBody(null) + if (body != null) { + val jsonBody = gson.toJson(body) + requestBody = jsonBody.toRequestBody("application/json".toMediaType()) + } + val request = client.newRequestWithBody("POST", path, null, requestBody) + val response = client.execute(request) + + if (!response.isSuccessful) { + val errorBody = response.body?.string() ?: "" + throw ApiException(response.code, response.message, errorBody) + } + return SendOptionalJsonResponse(statusCode = response.code, raw = response) + } + + /** + * sendOptionalText POST /optional-text. + */ + fun sendOptionalText(body: String?): SendOptionalTextResponse { + val path = "/optional-text" + var requestBody = ByteArray(0).toRequestBody(null) + if (body != null) { + requestBody = body.toRequestBody("text/plain".toMediaType()) + } + val request = client.newRequestWithBody("POST", path, null, requestBody) + val response = client.execute(request) + + if (!response.isSuccessful) { + val errorBody = response.body?.string() ?: "" + throw ApiException(response.code, response.message, errorBody) + } + return SendOptionalTextResponse(statusCode = response.code, raw = response) + } + + /** + * sendRequiredJson POST /required-json. + */ + fun sendRequiredJson(body: Payload): SendRequiredJsonResponse { + val path = "/required-json" + val jsonBody = gson.toJson(body) + val requestBody = jsonBody.toRequestBody("application/json".toMediaType()) + val request = client.newRequestWithBody("POST", path, null, requestBody) + val response = client.execute(request) + + if (!response.isSuccessful) { + val errorBody = response.body?.string() ?: "" + throw ApiException(response.code, response.message, errorBody) + } + return SendRequiredJsonResponse(statusCode = response.code, raw = response) + } +} diff --git a/tests/golden/kotlin/kotlin-okhttp/optional-request-bodies/build.gradle.kts.golden b/tests/golden/kotlin/kotlin-okhttp/optional-request-bodies/build.gradle.kts.golden new file mode 100644 index 000000000..5c6106455 --- /dev/null +++ b/tests/golden/kotlin/kotlin-okhttp/optional-request-bodies/build.gradle.kts.golden @@ -0,0 +1,19 @@ +plugins { + kotlin("jvm") version "1.9.22" +} + +group = "com.example.sdk" +version = "1.0.0" + +repositories { + mavenCentral() +} + +dependencies { + implementation("com.squareup.okhttp3:okhttp:4.12.0") + implementation("com.google.code.gson:gson:2.11.0") +} + +kotlin { + jvmToolchain(17) +} diff --git a/tests/golden/kotlin/kotlin-okhttp/optional-request-bodies/models/Payload.kt.golden b/tests/golden/kotlin/kotlin-okhttp/optional-request-bodies/models/Payload.kt.golden new file mode 100644 index 000000000..ae881907e --- /dev/null +++ b/tests/golden/kotlin/kotlin-okhttp/optional-request-bodies/models/Payload.kt.golden @@ -0,0 +1,9 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Optional Request Bodies — 1.0.0 +// Covers optional non-multipart request bodies. + +package com.example.sdk.models + +data class Payload(val count: Int? = null, val message: String) { +} diff --git a/tests/golden/kotlin/kotlin-okhttp/optional-request-bodies/runtime/ApiClient.kt.golden b/tests/golden/kotlin/kotlin-okhttp/optional-request-bodies/runtime/ApiClient.kt.golden new file mode 100644 index 000000000..f65fda37a --- /dev/null +++ b/tests/golden/kotlin/kotlin-okhttp/optional-request-bodies/runtime/ApiClient.kt.golden @@ -0,0 +1,62 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Optional Request Bodies — 1.0.0 +// Covers optional non-multipart request bodies. + +package com.example.sdk.runtime + +import okhttp3.HttpUrl +import okhttp3.HttpUrl.Companion.toHttpUrl +import okhttp3.MediaType.Companion.toMediaType +import okhttp3.OkHttpClient +import okhttp3.Request +import okhttp3.RequestBody.Companion.toRequestBody +import okhttp3.Response + +class ApiClient( + private val baseUrl: String, + private val client: OkHttpClient = OkHttpClient(), + private val authenticator: Authenticator? = null, + private val defaultHeaders: Map = emptyMap(), +) { + fun newRequest( + method: String, + path: String, + query: Map? = null, + body: String? = null, + ): Request { + return buildRequest(method, path, query, body?.toRequestBody("application/json".toMediaType())) + } + + fun newRequestWithBody( + method: String, + path: String, + query: Map? = null, + body: okhttp3.RequestBody?, + ): Request { + return buildRequest(method, path, query, body) + } + + private fun buildRequest( + method: String, + path: String, + query: Map? = null, + body: okhttp3.RequestBody?, + ): Request { + val urlBuilder: HttpUrl.Builder = (baseUrl.trimEnd('/') + path).toHttpUrl().newBuilder() + query?.forEach { (k, v) -> urlBuilder.addQueryParameter(k, v) } + + val builder = Request.Builder() + .url(urlBuilder.build()) + .method(method, body) + + defaultHeaders.forEach { (k, v) -> builder.header(k, v) } + authenticator?.authenticate(builder) + + return builder.build() + } + + fun execute(request: Request): Response { + return client.newCall(request).execute() + } +} diff --git a/tests/golden/kotlin/kotlin-okhttp/optional-request-bodies/runtime/ApiException.kt.golden b/tests/golden/kotlin/kotlin-okhttp/optional-request-bodies/runtime/ApiException.kt.golden new file mode 100644 index 000000000..25dc94cea --- /dev/null +++ b/tests/golden/kotlin/kotlin-okhttp/optional-request-bodies/runtime/ApiException.kt.golden @@ -0,0 +1,12 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Optional Request Bodies — 1.0.0 +// Covers optional non-multipart request bodies. + +package com.example.sdk.runtime + +class ApiException( + val statusCode: Int, + val status: String, + val body: String, +) : RuntimeException("API error $statusCode: $status") diff --git a/tests/golden/kotlin/kotlin-okhttp/optional-request-bodies/runtime/Auth.kt.golden b/tests/golden/kotlin/kotlin-okhttp/optional-request-bodies/runtime/Auth.kt.golden new file mode 100644 index 000000000..20af00288 --- /dev/null +++ b/tests/golden/kotlin/kotlin-okhttp/optional-request-bodies/runtime/Auth.kt.golden @@ -0,0 +1,71 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Optional Request Bodies — 1.0.0 +// Covers optional non-multipart request bodies. + +package com.example.sdk.runtime + +import okhttp3.Request + +interface Authenticator { + fun authenticate(builder: Request.Builder) +} + +class BearerAuth : Authenticator { + private val token: String? + private val tokenProvider: (() -> String)? + + constructor(token: String) { + this.token = token + this.tokenProvider = null + } + + constructor(tokenProvider: () -> String) { + this.token = null + this.tokenProvider = tokenProvider + } + + override fun authenticate(builder: Request.Builder) { + val t = tokenProvider?.invoke() ?: token!! + builder.header("Authorization", "Bearer $t") + } +} + +class ApiKeyAuth : Authenticator { + private val key: String? + private val keyProvider: (() -> String)? + private val name: String + private val location: ApiKeyLocation + + constructor(key: String, name: String, location: ApiKeyLocation) { + this.key = key + this.keyProvider = null + this.name = name + this.location = location + } + + constructor(keyProvider: () -> String, name: String, location: ApiKeyLocation) { + this.key = null + this.keyProvider = keyProvider + this.name = name + this.location = location + } + + override fun authenticate(builder: Request.Builder) { + val k = keyProvider?.invoke() ?: key!! + when (location) { + ApiKeyLocation.HEADER -> builder.header(name, k) + ApiKeyLocation.QUERY -> { + val url = builder.build().url.newBuilder() + .addQueryParameter(name, k) + .build() + builder.url(url) + } + } + } +} + +enum class ApiKeyLocation { + HEADER, + QUERY, +} diff --git a/tests/golden/python/python-httpx/optional-request-bodies/README.md.golden b/tests/golden/python/python-httpx/optional-request-bodies/README.md.golden new file mode 100644 index 000000000..b0d354cf4 --- /dev/null +++ b/tests/golden/python/python-httpx/optional-request-bodies/README.md.golden @@ -0,0 +1,7 @@ +# Optional Request Bodies + +Covers optional non-multipart request bodies. + +Version: `1.0.0` + +Generated by [openapi-nexus](https://github.com/adamcavendish/openapi-nexus) for `optional_request_bodies`. diff --git a/tests/golden/python/python-httpx/optional-request-bodies/optional_request_bodies/__init__.py.golden b/tests/golden/python/python-httpx/optional-request-bodies/optional_request_bodies/__init__.py.golden new file mode 100644 index 000000000..a3fb15d9a --- /dev/null +++ b/tests/golden/python/python-httpx/optional-request-bodies/optional_request_bodies/__init__.py.golden @@ -0,0 +1,10 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Optional Request Bodies — 1.0.0 +# Covers optional non-multipart request bodies. + +from .runtime import ApiKeyAuth as ApiKeyAuth +from .runtime import Authenticator as Authenticator +from .runtime import BearerAuth as BearerAuth +from .runtime import Client as Client +from .runtime import ApiError as ApiError diff --git a/tests/golden/python/python-httpx/optional-request-bodies/optional_request_bodies/apis/__init__.py.golden b/tests/golden/python/python-httpx/optional-request-bodies/optional_request_bodies/apis/__init__.py.golden new file mode 100644 index 000000000..25c398b60 --- /dev/null +++ b/tests/golden/python/python-httpx/optional-request-bodies/optional_request_bodies/apis/__init__.py.golden @@ -0,0 +1,6 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Optional Request Bodies — 1.0.0 +# Covers optional non-multipart request bodies. + +from .default_api import DefaultApi as DefaultApi diff --git a/tests/golden/python/python-httpx/optional-request-bodies/optional_request_bodies/apis/default_api.py.golden b/tests/golden/python/python-httpx/optional-request-bodies/optional_request_bodies/apis/default_api.py.golden new file mode 100644 index 000000000..98efe6309 --- /dev/null +++ b/tests/golden/python/python-httpx/optional-request-bodies/optional_request_bodies/apis/default_api.py.golden @@ -0,0 +1,50 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Optional Request Bodies — 1.0.0 +# Covers optional non-multipart request bodies. + +from __future__ import annotations + +from ..models.payload import Payload +from ..runtime.client import Client +from ..runtime.errors import ApiError + +class DefaultApi: + def __init__(self, client: Client) -> None: + self._client = client + + def send_optional_binary(self, *, body: bytes | None = None) -> None: + path = "/optional-binary" + headers: dict[str, str] = {} + headers["Content-Type"] = "application/octet-stream" + response = self._client.request("POST", path, content=body, headers=headers) + if response.status_code >= 400: + raise ApiError(response.status_code, response.reason_phrase, response.content) + return None + + def send_optional_json(self, *, body: Payload | None = None) -> None: + path = "/optional-json" + headers: dict[str, str] = {} + headers["Content-Type"] = "application/json" + response = self._client.request("POST", path, json=body.to_dict() if body is not None else None, headers=headers) + if response.status_code >= 400: + raise ApiError(response.status_code, response.reason_phrase, response.content) + return None + + def send_optional_text(self, *, body: str | None = None) -> None: + path = "/optional-text" + headers: dict[str, str] = {} + headers["Content-Type"] = "text/plain" + response = self._client.request("POST", path, content=body, headers=headers) + if response.status_code >= 400: + raise ApiError(response.status_code, response.reason_phrase, response.content) + return None + + def send_required_json(self, *, body: Payload) -> None: + path = "/required-json" + headers: dict[str, str] = {} + headers["Content-Type"] = "application/json" + response = self._client.request("POST", path, json=body.to_dict(), headers=headers) + if response.status_code >= 400: + raise ApiError(response.status_code, response.reason_phrase, response.content) + return None diff --git a/tests/golden/python/python-httpx/optional-request-bodies/optional_request_bodies/models/__init__.py.golden b/tests/golden/python/python-httpx/optional-request-bodies/optional_request_bodies/models/__init__.py.golden new file mode 100644 index 000000000..e73d0462b --- /dev/null +++ b/tests/golden/python/python-httpx/optional-request-bodies/optional_request_bodies/models/__init__.py.golden @@ -0,0 +1,6 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Optional Request Bodies — 1.0.0 +# Covers optional non-multipart request bodies. + +from .payload import Payload as Payload diff --git a/tests/golden/python/python-httpx/optional-request-bodies/optional_request_bodies/models/payload.py.golden b/tests/golden/python/python-httpx/optional-request-bodies/optional_request_bodies/models/payload.py.golden new file mode 100644 index 000000000..9d73ed29c --- /dev/null +++ b/tests/golden/python/python-httpx/optional-request-bodies/optional_request_bodies/models/payload.py.golden @@ -0,0 +1,27 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Optional Request Bodies — 1.0.0 +# Covers optional non-multipart request bodies. + +from __future__ import annotations + +from dataclasses import dataclass + +@dataclass +class Payload: + message: str + count: int | None = None + + def to_dict(self) -> dict[str, object]: + result: dict[str, object] = {} + result["message"] = self.message + if self.count is not None: + result["count"] = self.count + return result + + @classmethod + def from_dict(cls, data: dict[str, object]) -> Payload: + return cls( + message=data["message"], # type: ignore[assignment] + count=data.get("count"), # type: ignore[assignment] + ) diff --git a/tests/golden/python/python-httpx/optional-request-bodies/optional_request_bodies/py.typed.golden b/tests/golden/python/python-httpx/optional-request-bodies/optional_request_bodies/py.typed.golden new file mode 100644 index 000000000..e69de29bb diff --git a/tests/golden/python/python-httpx/optional-request-bodies/optional_request_bodies/runtime/__init__.py.golden b/tests/golden/python/python-httpx/optional-request-bodies/optional_request_bodies/runtime/__init__.py.golden new file mode 100644 index 000000000..7eb7e6147 --- /dev/null +++ b/tests/golden/python/python-httpx/optional-request-bodies/optional_request_bodies/runtime/__init__.py.golden @@ -0,0 +1,10 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Optional Request Bodies — 1.0.0 +# Covers optional non-multipart request bodies. + +from .auth import ApiKeyAuth as ApiKeyAuth +from .auth import Authenticator as Authenticator +from .auth import BearerAuth as BearerAuth +from .client import Client as Client +from .errors import ApiError as ApiError diff --git a/tests/golden/python/python-httpx/optional-request-bodies/optional_request_bodies/runtime/auth.py.golden b/tests/golden/python/python-httpx/optional-request-bodies/optional_request_bodies/runtime/auth.py.golden new file mode 100644 index 000000000..867f9c029 --- /dev/null +++ b/tests/golden/python/python-httpx/optional-request-bodies/optional_request_bodies/runtime/auth.py.golden @@ -0,0 +1,51 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Optional Request Bodies — 1.0.0 +# Covers optional non-multipart request bodies. + +"""Authentication helpers.""" + +from __future__ import annotations + +from abc import ABC, abstractmethod +from collections.abc import Callable + + +class Authenticator(ABC): + """Base class for request authenticators.""" + + @abstractmethod + def auth_headers(self) -> dict[str, str]: + """Return headers to attach to every request.""" + ... + + +class BearerAuth(Authenticator): + """Bearer token authentication. + + Accepts a static token string or a callable that returns the + current token (evaluated on every request). + """ + + def __init__(self, token: str | Callable[[], str]) -> None: + self._token = token + + def auth_headers(self) -> dict[str, str]: + token = self._token() if callable(self._token) else self._token + return {"Authorization": f"Bearer {token}"} + + +class ApiKeyAuth(Authenticator): + """API key authentication via a custom header. + + Accepts a static key string or a callable that returns the + current key (evaluated on every request). + """ + + def __init__(self, header_name: str, api_key: str | Callable[[], str]) -> None: + self._header_name = header_name + self._api_key = api_key + + def auth_headers(self) -> dict[str, str]: + key = self._api_key() if callable(self._api_key) else self._api_key + return {self._header_name: key} diff --git a/tests/golden/python/python-httpx/optional-request-bodies/optional_request_bodies/runtime/client.py.golden b/tests/golden/python/python-httpx/optional-request-bodies/optional_request_bodies/runtime/client.py.golden new file mode 100644 index 000000000..b9b3e3f07 --- /dev/null +++ b/tests/golden/python/python-httpx/optional-request-bodies/optional_request_bodies/runtime/client.py.golden @@ -0,0 +1,71 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Optional Request Bodies — 1.0.0 +# Covers optional non-multipart request bodies. + +"""HTTP client wrapping httpx.""" + +from __future__ import annotations + +from typing import Any + +import httpx + +from .auth import Authenticator + + +class Client: + """Synchronous HTTP client for the generated SDK.""" + + def __init__( + self, + base_url: str, + *, + http_client: httpx.Client | None = None, + authenticator: Authenticator | None = None, + ) -> None: + self._base_url = base_url.rstrip("/") + self._http_client = http_client or httpx.Client() + self._authenticator = authenticator + + def request( + self, + method: str, + path: str, + *, + params: dict[str, str] | None = None, + json: Any = None, + content: Any = None, + data: Any = None, + files: Any = None, + headers: dict[str, str] | None = None, + ) -> httpx.Response: + """Send an HTTP request and return the raw response.""" + url = f"{self._base_url}{path}" + req_headers: dict[str, str] = {"Accept": "application/json"} + if json is not None: + req_headers["Content-Type"] = "application/json" + if headers: + req_headers.update(headers) + if self._authenticator is not None: + req_headers.update(self._authenticator.auth_headers()) + return self._http_client.request( + method, + url, + params=params, + json=json, + content=content, + data=data, + files=files, + headers=req_headers, + ) + + def close(self) -> None: + """Close the underlying HTTP client.""" + self._http_client.close() + + def __enter__(self) -> Client: + return self + + def __exit__(self, *args: object) -> None: + self.close() diff --git a/tests/golden/python/python-httpx/optional-request-bodies/optional_request_bodies/runtime/errors.py.golden b/tests/golden/python/python-httpx/optional-request-bodies/optional_request_bodies/runtime/errors.py.golden new file mode 100644 index 000000000..93e172470 --- /dev/null +++ b/tests/golden/python/python-httpx/optional-request-bodies/optional_request_bodies/runtime/errors.py.golden @@ -0,0 +1,18 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Optional Request Bodies — 1.0.0 +# Covers optional non-multipart request bodies. + +"""API error type.""" + +from __future__ import annotations + + +class ApiError(Exception): + """Raised when the server returns a 4xx or 5xx status code.""" + + def __init__(self, status_code: int, status: str, body: bytes) -> None: + self.status_code = status_code + self.status = status + self.body = body + super().__init__(f"{status_code} {status}") diff --git a/tests/golden/python/python-httpx/optional-request-bodies/pyproject.toml.golden b/tests/golden/python/python-httpx/optional-request-bodies/pyproject.toml.golden new file mode 100644 index 000000000..c534686f1 --- /dev/null +++ b/tests/golden/python/python-httpx/optional-request-bodies/pyproject.toml.golden @@ -0,0 +1,10 @@ +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[project] +name = "optional_request_bodies" +version = "1.0.0" +description = "Covers optional non-multipart request bodies." +requires-python = ">=3.12" +dependencies = ["httpx>=0.27"] diff --git a/tests/golden/python/python-requests/optional-request-bodies/README.md.golden b/tests/golden/python/python-requests/optional-request-bodies/README.md.golden new file mode 100644 index 000000000..b0d354cf4 --- /dev/null +++ b/tests/golden/python/python-requests/optional-request-bodies/README.md.golden @@ -0,0 +1,7 @@ +# Optional Request Bodies + +Covers optional non-multipart request bodies. + +Version: `1.0.0` + +Generated by [openapi-nexus](https://github.com/adamcavendish/openapi-nexus) for `optional_request_bodies`. diff --git a/tests/golden/python/python-requests/optional-request-bodies/optional_request_bodies/__init__.py.golden b/tests/golden/python/python-requests/optional-request-bodies/optional_request_bodies/__init__.py.golden new file mode 100644 index 000000000..a3fb15d9a --- /dev/null +++ b/tests/golden/python/python-requests/optional-request-bodies/optional_request_bodies/__init__.py.golden @@ -0,0 +1,10 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Optional Request Bodies — 1.0.0 +# Covers optional non-multipart request bodies. + +from .runtime import ApiKeyAuth as ApiKeyAuth +from .runtime import Authenticator as Authenticator +from .runtime import BearerAuth as BearerAuth +from .runtime import Client as Client +from .runtime import ApiError as ApiError diff --git a/tests/golden/python/python-requests/optional-request-bodies/optional_request_bodies/apis/__init__.py.golden b/tests/golden/python/python-requests/optional-request-bodies/optional_request_bodies/apis/__init__.py.golden new file mode 100644 index 000000000..25c398b60 --- /dev/null +++ b/tests/golden/python/python-requests/optional-request-bodies/optional_request_bodies/apis/__init__.py.golden @@ -0,0 +1,6 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Optional Request Bodies — 1.0.0 +# Covers optional non-multipart request bodies. + +from .default_api import DefaultApi as DefaultApi diff --git a/tests/golden/python/python-requests/optional-request-bodies/optional_request_bodies/apis/default_api.py.golden b/tests/golden/python/python-requests/optional-request-bodies/optional_request_bodies/apis/default_api.py.golden new file mode 100644 index 000000000..88ec7b169 --- /dev/null +++ b/tests/golden/python/python-requests/optional-request-bodies/optional_request_bodies/apis/default_api.py.golden @@ -0,0 +1,50 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Optional Request Bodies — 1.0.0 +# Covers optional non-multipart request bodies. + +from __future__ import annotations + +from ..models.payload import Payload +from ..runtime.client import Client +from ..runtime.errors import ApiError + +class DefaultApi: + def __init__(self, client: Client) -> None: + self._client = client + + def send_optional_binary(self, *, body: bytes | None = None) -> None: + path = "/optional-binary" + headers: dict[str, str] = {} + headers["Content-Type"] = "application/octet-stream" + response = self._client.request("POST", path, data=body, headers=headers) + if response.status_code >= 400: + raise ApiError(response.status_code, response.reason, response.content) + return None + + def send_optional_json(self, *, body: Payload | None = None) -> None: + path = "/optional-json" + headers: dict[str, str] = {} + headers["Content-Type"] = "application/json" + response = self._client.request("POST", path, json=body.to_dict() if body is not None else None, headers=headers) + if response.status_code >= 400: + raise ApiError(response.status_code, response.reason, response.content) + return None + + def send_optional_text(self, *, body: str | None = None) -> None: + path = "/optional-text" + headers: dict[str, str] = {} + headers["Content-Type"] = "text/plain" + response = self._client.request("POST", path, data=body, headers=headers) + if response.status_code >= 400: + raise ApiError(response.status_code, response.reason, response.content) + return None + + def send_required_json(self, *, body: Payload) -> None: + path = "/required-json" + headers: dict[str, str] = {} + headers["Content-Type"] = "application/json" + response = self._client.request("POST", path, json=body.to_dict(), headers=headers) + if response.status_code >= 400: + raise ApiError(response.status_code, response.reason, response.content) + return None diff --git a/tests/golden/python/python-requests/optional-request-bodies/optional_request_bodies/models/__init__.py.golden b/tests/golden/python/python-requests/optional-request-bodies/optional_request_bodies/models/__init__.py.golden new file mode 100644 index 000000000..e73d0462b --- /dev/null +++ b/tests/golden/python/python-requests/optional-request-bodies/optional_request_bodies/models/__init__.py.golden @@ -0,0 +1,6 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Optional Request Bodies — 1.0.0 +# Covers optional non-multipart request bodies. + +from .payload import Payload as Payload diff --git a/tests/golden/python/python-requests/optional-request-bodies/optional_request_bodies/models/payload.py.golden b/tests/golden/python/python-requests/optional-request-bodies/optional_request_bodies/models/payload.py.golden new file mode 100644 index 000000000..9d73ed29c --- /dev/null +++ b/tests/golden/python/python-requests/optional-request-bodies/optional_request_bodies/models/payload.py.golden @@ -0,0 +1,27 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Optional Request Bodies — 1.0.0 +# Covers optional non-multipart request bodies. + +from __future__ import annotations + +from dataclasses import dataclass + +@dataclass +class Payload: + message: str + count: int | None = None + + def to_dict(self) -> dict[str, object]: + result: dict[str, object] = {} + result["message"] = self.message + if self.count is not None: + result["count"] = self.count + return result + + @classmethod + def from_dict(cls, data: dict[str, object]) -> Payload: + return cls( + message=data["message"], # type: ignore[assignment] + count=data.get("count"), # type: ignore[assignment] + ) diff --git a/tests/golden/python/python-requests/optional-request-bodies/optional_request_bodies/py.typed.golden b/tests/golden/python/python-requests/optional-request-bodies/optional_request_bodies/py.typed.golden new file mode 100644 index 000000000..e69de29bb diff --git a/tests/golden/python/python-requests/optional-request-bodies/optional_request_bodies/runtime/__init__.py.golden b/tests/golden/python/python-requests/optional-request-bodies/optional_request_bodies/runtime/__init__.py.golden new file mode 100644 index 000000000..7eb7e6147 --- /dev/null +++ b/tests/golden/python/python-requests/optional-request-bodies/optional_request_bodies/runtime/__init__.py.golden @@ -0,0 +1,10 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Optional Request Bodies — 1.0.0 +# Covers optional non-multipart request bodies. + +from .auth import ApiKeyAuth as ApiKeyAuth +from .auth import Authenticator as Authenticator +from .auth import BearerAuth as BearerAuth +from .client import Client as Client +from .errors import ApiError as ApiError diff --git a/tests/golden/python/python-requests/optional-request-bodies/optional_request_bodies/runtime/auth.py.golden b/tests/golden/python/python-requests/optional-request-bodies/optional_request_bodies/runtime/auth.py.golden new file mode 100644 index 000000000..867f9c029 --- /dev/null +++ b/tests/golden/python/python-requests/optional-request-bodies/optional_request_bodies/runtime/auth.py.golden @@ -0,0 +1,51 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Optional Request Bodies — 1.0.0 +# Covers optional non-multipart request bodies. + +"""Authentication helpers.""" + +from __future__ import annotations + +from abc import ABC, abstractmethod +from collections.abc import Callable + + +class Authenticator(ABC): + """Base class for request authenticators.""" + + @abstractmethod + def auth_headers(self) -> dict[str, str]: + """Return headers to attach to every request.""" + ... + + +class BearerAuth(Authenticator): + """Bearer token authentication. + + Accepts a static token string or a callable that returns the + current token (evaluated on every request). + """ + + def __init__(self, token: str | Callable[[], str]) -> None: + self._token = token + + def auth_headers(self) -> dict[str, str]: + token = self._token() if callable(self._token) else self._token + return {"Authorization": f"Bearer {token}"} + + +class ApiKeyAuth(Authenticator): + """API key authentication via a custom header. + + Accepts a static key string or a callable that returns the + current key (evaluated on every request). + """ + + def __init__(self, header_name: str, api_key: str | Callable[[], str]) -> None: + self._header_name = header_name + self._api_key = api_key + + def auth_headers(self) -> dict[str, str]: + key = self._api_key() if callable(self._api_key) else self._api_key + return {self._header_name: key} diff --git a/tests/golden/python/python-requests/optional-request-bodies/optional_request_bodies/runtime/client.py.golden b/tests/golden/python/python-requests/optional-request-bodies/optional_request_bodies/runtime/client.py.golden new file mode 100644 index 000000000..cda3aa168 --- /dev/null +++ b/tests/golden/python/python-requests/optional-request-bodies/optional_request_bodies/runtime/client.py.golden @@ -0,0 +1,99 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Optional Request Bodies — 1.0.0 +# Covers optional non-multipart request bodies. + +"""HTTP client wrapping requests.""" + +from __future__ import annotations + +from typing import Any, cast + +import requests + +from .auth import Authenticator + + +class Response: + """Thin wrapper over requests.Response with strict types.""" + + def __init__(self, raw: requests.Response) -> None: + self._raw = raw + + @property + def status_code(self) -> int: + return self._raw.status_code # type: ignore[return-value] + + @property + def reason(self) -> str: + return self._raw.reason or "" + + @property + def content(self) -> bytes: + return self._raw.content # type: ignore[return-value] + + @property + def text(self) -> str: + return self._raw.text + + def json(self) -> Any: # type: ignore[explicit-override] + return self._raw.json() # type: ignore[reportUnknownMemberType] + + +class Client: + """Synchronous HTTP client for the generated SDK.""" + + def __init__( + self, + base_url: str, + *, + http_client: requests.Session | None = None, + authenticator: Authenticator | None = None, + ) -> None: + self._base_url = base_url.rstrip("/") + self._http_client = http_client or requests.Session() + self._authenticator = authenticator + + def request( + self, + method: str, + path: str, + *, + params: dict[str, str] | None = None, + json: Any = None, + data: Any = None, + files: Any = None, + headers: dict[str, str] | None = None, + ) -> Response: + """Send an HTTP request and return the raw response.""" + url = f"{self._base_url}{path}" + req_headers: dict[str, str] = {"Accept": "application/json"} + if json is not None: + req_headers["Content-Type"] = "application/json" + if headers: + req_headers.update(headers) + if self._authenticator is not None: + req_headers.update(self._authenticator.auth_headers()) + raw = cast( + requests.Response, + self._http_client.request( # type: ignore[reportUnknownMemberType] + method, + url, + params=params, + json=json, + data=data, + files=files, + headers=req_headers, + ), + ) + return Response(raw) + + def close(self) -> None: + """Close the underlying HTTP client.""" + self._http_client.close() + + def __enter__(self) -> Client: + return self + + def __exit__(self, *args: object) -> None: + self.close() diff --git a/tests/golden/python/python-requests/optional-request-bodies/optional_request_bodies/runtime/errors.py.golden b/tests/golden/python/python-requests/optional-request-bodies/optional_request_bodies/runtime/errors.py.golden new file mode 100644 index 000000000..93e172470 --- /dev/null +++ b/tests/golden/python/python-requests/optional-request-bodies/optional_request_bodies/runtime/errors.py.golden @@ -0,0 +1,18 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Optional Request Bodies — 1.0.0 +# Covers optional non-multipart request bodies. + +"""API error type.""" + +from __future__ import annotations + + +class ApiError(Exception): + """Raised when the server returns a 4xx or 5xx status code.""" + + def __init__(self, status_code: int, status: str, body: bytes) -> None: + self.status_code = status_code + self.status = status + self.body = body + super().__init__(f"{status_code} {status}") diff --git a/tests/golden/python/python-requests/optional-request-bodies/pyproject.toml.golden b/tests/golden/python/python-requests/optional-request-bodies/pyproject.toml.golden new file mode 100644 index 000000000..871f690b6 --- /dev/null +++ b/tests/golden/python/python-requests/optional-request-bodies/pyproject.toml.golden @@ -0,0 +1,10 @@ +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[project] +name = "optional_request_bodies" +version = "1.0.0" +description = "Covers optional non-multipart request bodies." +requires-python = ">=3.12" +dependencies = ["requests>=2.32"] diff --git a/tests/golden/rust/rust-aioduct/multipart-edge-cases/src/apis/multipart.rs.golden b/tests/golden/rust/rust-aioduct/multipart-edge-cases/src/apis/multipart.rs.golden index ffce1eb8e..627b47ad0 100644 --- a/tests/golden/rust/rust-aioduct/multipart-edge-cases/src/apis/multipart.rs.golden +++ b/tests/golden/rust/rust-aioduct/multipart-edge-cases/src/apis/multipart.rs.golden @@ -23,27 +23,29 @@ impl<'a, R: aioduct::Runtime> MultipartApi<'a, R> { /// POST /multipart/optional pub async fn send_optional_parts( &self, - body: &crate::models::OptionalUpload, + body: Option<&crate::models::OptionalUpload>, ) -> Result { let path = "/multipart/optional".to_string(); let mut req = self.client.post(&path)?; - let mut multipart = aioduct::multipart::Multipart::new(); - if let Some(value) = &body.attributes { - multipart = multipart.part(aioduct::multipart::Part::text("attributes", serde_json::to_string(value)?).mime_str("application/json")); - } - if let Some(value) = &body.enabled { - multipart = multipart.part(aioduct::multipart::Part::text("enabled", value.to_string()).mime_str("text/plain")); - } - if let Some(value) = &body.file { - multipart = multipart.file("file", "file", "application/octet-stream", value.clone()); + if let Some(body) = body { + let mut multipart = aioduct::multipart::Multipart::new(); + if let Some(value) = &body.attributes { + multipart = multipart.part(aioduct::multipart::Part::text("attributes", serde_json::to_string(value)?).mime_str("application/json")); + } + if let Some(value) = &body.enabled { + multipart = multipart.part(aioduct::multipart::Part::text("enabled", value.to_string()).mime_str("text/plain")); + } + if let Some(value) = &body.file { + multipart = multipart.file("file", "file", "application/octet-stream", value.clone()); + } + if let Some(value) = &body.retry_count { + multipart = multipart.part(aioduct::multipart::Part::text("retry_count", value.to_string()).mime_str("text/plain")); + } + if let Some(value) = &body.title { + multipart = multipart.part(aioduct::multipart::Part::text("title", value.to_string()).mime_str("text/plain")); + } + req = req.multipart(multipart); } - if let Some(value) = &body.retry_count { - multipart = multipart.part(aioduct::multipart::Part::text("retry_count", value.to_string()).mime_str("text/plain")); - } - if let Some(value) = &body.title { - multipart = multipart.part(aioduct::multipart::Part::text("title", value.to_string()).mime_str("text/plain")); - } - req = req.multipart(multipart); let resp = req.send().await?; let status_code = resp.status().as_u16(); Ok(SendOptionalPartsResponse { status_code }) diff --git a/tests/golden/rust/rust-aioduct/optional-request-bodies/Cargo.toml.golden b/tests/golden/rust/rust-aioduct/optional-request-bodies/Cargo.toml.golden new file mode 100644 index 000000000..b034d03d9 --- /dev/null +++ b/tests/golden/rust/rust-aioduct/optional-request-bodies/Cargo.toml.golden @@ -0,0 +1,12 @@ +[package] +name = "optional-request-bodies" +version = "0.1.0" +edition = "2024" +description = "Covers optional non-multipart request bodies." + +[dependencies] +aioduct = { version = "0.1.8", features = ["tokio", "rustls", "rustls-ring", "json"] } +serde = { version = "1", features = ["derive"] } +serde_json = "1" +serde-xml-rs = "0.8.2" +serde_repr = "0.1" diff --git a/tests/golden/rust/rust-aioduct/optional-request-bodies/README.md.golden b/tests/golden/rust/rust-aioduct/optional-request-bodies/README.md.golden new file mode 100644 index 000000000..a60de19e5 --- /dev/null +++ b/tests/golden/rust/rust-aioduct/optional-request-bodies/README.md.golden @@ -0,0 +1,7 @@ +# Optional Request Bodies + +Covers optional non-multipart request bodies. + +Version: `1.0.0` + +Generated by [openapi-nexus](https://github.com/adamcavendish/openapi-nexus) for `optional-request-bodies`. diff --git a/tests/golden/rust/rust-aioduct/optional-request-bodies/src/apis/default.rs.golden b/tests/golden/rust/rust-aioduct/optional-request-bodies/src/apis/default.rs.golden new file mode 100644 index 000000000..23ccea225 --- /dev/null +++ b/tests/golden/rust/rust-aioduct/optional-request-bodies/src/apis/default.rs.golden @@ -0,0 +1,106 @@ +// @generated +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Optional Request Bodies — 1.0.0 +// Covers optional non-multipart request bodies. + +use crate::runtime::client::Client; +use crate::runtime::error::Error; + +/// API operations under the "default" tag. +pub struct DefaultApi<'a, R: aioduct::Runtime> { + client: &'a Client, +} + +impl<'a, R: aioduct::Runtime> DefaultApi<'a, R> { + /// Create a new `DefaultApi` bound to the given client. + pub fn new(client: &'a Client) -> Self { + Self { + client, + } + } + + /// POST /optional-binary + pub async fn send_optional_binary( + &self, + body: Option<&Vec>, + ) -> Result { + let path = "/optional-binary".to_string(); + let mut req = self.client.post(&path)?; + if let Some(body) = body { + req = req.header_str("Content-Type", "application/octet-stream")?; + req = req.body(body.clone()); + } + let resp = req.send().await?; + let status_code = resp.status().as_u16(); + Ok(SendOptionalBinaryResponse { status_code }) + } + + /// POST /optional-json + pub async fn send_optional_json( + &self, + body: Option<&crate::models::Payload>, + ) -> Result { + let path = "/optional-json".to_string(); + let mut req = self.client.post(&path)?; + if let Some(body) = body { + req = req.json(&body)?; + } + let resp = req.send().await?; + let status_code = resp.status().as_u16(); + Ok(SendOptionalJsonResponse { status_code }) + } + + /// POST /optional-text + pub async fn send_optional_text( + &self, + body: Option<&String>, + ) -> Result { + let path = "/optional-text".to_string(); + let mut req = self.client.post(&path)?; + if let Some(body) = body { + req = req.header_str("Content-Type", "text/plain")?; + req = req.body(body.clone().into_bytes()); + } + let resp = req.send().await?; + let status_code = resp.status().as_u16(); + Ok(SendOptionalTextResponse { status_code }) + } + + /// POST /required-json + pub async fn send_required_json( + &self, + body: &crate::models::Payload, + ) -> Result { + let path = "/required-json".to_string(); + let mut req = self.client.post(&path)?; + req = req.json(&body)?; + let resp = req.send().await?; + let status_code = resp.status().as_u16(); + Ok(SendRequiredJsonResponse { status_code }) + } +} + +/// Response from `send_optional_binary`. +#[derive(Debug)] +pub struct SendOptionalBinaryResponse { + pub status_code: u16, +} + +/// Response from `send_optional_json`. +#[derive(Debug)] +pub struct SendOptionalJsonResponse { + pub status_code: u16, +} + +/// Response from `send_optional_text`. +#[derive(Debug)] +pub struct SendOptionalTextResponse { + pub status_code: u16, +} + +/// Response from `send_required_json`. +#[derive(Debug)] +pub struct SendRequiredJsonResponse { + pub status_code: u16, +} diff --git a/tests/golden/rust/rust-aioduct/optional-request-bodies/src/apis/mod.rs.golden b/tests/golden/rust/rust-aioduct/optional-request-bodies/src/apis/mod.rs.golden new file mode 100644 index 000000000..53674c52d --- /dev/null +++ b/tests/golden/rust/rust-aioduct/optional-request-bodies/src/apis/mod.rs.golden @@ -0,0 +1,8 @@ +// @generated +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Optional Request Bodies — 1.0.0 +// Covers optional non-multipart request bodies. + +mod default; +pub use default::*; diff --git a/tests/golden/rust/rust-aioduct/optional-request-bodies/src/lib.rs.golden b/tests/golden/rust/rust-aioduct/optional-request-bodies/src/lib.rs.golden new file mode 100644 index 000000000..dac0974a2 --- /dev/null +++ b/tests/golden/rust/rust-aioduct/optional-request-bodies/src/lib.rs.golden @@ -0,0 +1,11 @@ +// @generated +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Optional Request Bodies — 1.0.0 +// Covers optional non-multipart request bodies. + +#![allow(clippy::all)] + +pub mod apis; +pub mod models; +pub mod runtime; diff --git a/tests/golden/rust/rust-aioduct/optional-request-bodies/src/models/mod.rs.golden b/tests/golden/rust/rust-aioduct/optional-request-bodies/src/models/mod.rs.golden new file mode 100644 index 000000000..d7518154a --- /dev/null +++ b/tests/golden/rust/rust-aioduct/optional-request-bodies/src/models/mod.rs.golden @@ -0,0 +1,8 @@ +// @generated +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Optional Request Bodies — 1.0.0 +// Covers optional non-multipart request bodies. + +mod payload; +pub use payload::*; diff --git a/tests/golden/rust/rust-aioduct/optional-request-bodies/src/models/payload.rs.golden b/tests/golden/rust/rust-aioduct/optional-request-bodies/src/models/payload.rs.golden new file mode 100644 index 000000000..4bb3b0bc7 --- /dev/null +++ b/tests/golden/rust/rust-aioduct/optional-request-bodies/src/models/payload.rs.golden @@ -0,0 +1,14 @@ +// @generated +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Optional Request Bodies — 1.0.0 +// Covers optional non-multipart request bodies. + +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Payload { + #[serde(skip_serializing_if = "Option::is_none", default)] + pub count: Option, + pub message: String, +} diff --git a/tests/golden/rust/rust-aioduct/optional-request-bodies/src/runtime/auth.rs.golden b/tests/golden/rust/rust-aioduct/optional-request-bodies/src/runtime/auth.rs.golden new file mode 100644 index 000000000..976a4d8d0 --- /dev/null +++ b/tests/golden/rust/rust-aioduct/optional-request-bodies/src/runtime/auth.rs.golden @@ -0,0 +1,180 @@ +// @generated +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Optional Request Bodies — 1.0.0 +// Covers optional non-multipart request bodies. + +use std::sync::Arc; + +use crate::runtime::error::Error; + +/// Trait for authenticating requests. +pub trait Authenticator: Send + Sync + std::fmt::Debug { + /// Apply authentication to the request builder. + fn authenticate<'a>( + &self, + req: aioduct::RequestBuilder<'a, R>, + ) -> Result, Error>; +} + +/// Bearer token authentication. +/// +/// Use [`BearerAuth::new`] with a static token, or +/// [`BearerAuth::from_provider`] with a function that returns the +/// current token (evaluated on every request). +pub struct BearerAuth { + token: TokenSource, +} + +enum TokenSource { + Static(String), + Dynamic(Arc String + Send + Sync>), +} + +impl std::fmt::Debug for TokenSource { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Static(t) => f.debug_tuple("Static").field(&t).finish(), + Self::Dynamic(_) => f.debug_tuple("Dynamic").field(&"").finish(), + } + } +} + +impl Clone for TokenSource { + fn clone(&self) -> Self { + match self { + Self::Static(t) => Self::Static(t.clone()), + Self::Dynamic(f) => Self::Dynamic(Arc::clone(f)), + } + } +} + +impl std::fmt::Debug for BearerAuth { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("BearerAuth").field("token", &self.token).finish() + } +} + +impl Clone for BearerAuth { + fn clone(&self) -> Self { + Self { + token: self.token.clone(), + } + } +} + +impl BearerAuth { + /// Create a new bearer token authenticator with a static token. + pub fn new(token: impl Into) -> Self { + Self { + token: TokenSource::Static(token.into()), + } + } + + /// Create a bearer token authenticator that evaluates the given + /// function on every request to obtain the current token. + pub fn from_provider(f: impl Fn() -> String + Send + Sync + 'static) -> Self { + Self { + token: TokenSource::Dynamic(Arc::new(f)), + } + } +} + +impl Authenticator for BearerAuth { + fn authenticate<'a>( + &self, + req: aioduct::RequestBuilder<'a, R>, + ) -> Result, Error> { + let token = match &self.token { + TokenSource::Static(t) => t.clone(), + TokenSource::Dynamic(f) => f(), + }; + Ok(req.bearer_auth(&token)) + } +} + +/// API key authentication. +/// +/// Use [`ApiKeyAuth::new`] with a static key, or +/// [`ApiKeyAuth::from_provider`] with a function that returns the +/// current key (evaluated on every request). +pub struct ApiKeyAuth { + header_name: String, + api_key: KeySource, +} + +enum KeySource { + Static(String), + Dynamic(Arc String + Send + Sync>), +} + +impl std::fmt::Debug for KeySource { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Static(k) => f.debug_tuple("Static").field(&k).finish(), + Self::Dynamic(_) => f.debug_tuple("Dynamic").field(&"").finish(), + } + } +} + +impl Clone for KeySource { + fn clone(&self) -> Self { + match self { + Self::Static(k) => Self::Static(k.clone()), + Self::Dynamic(f) => Self::Dynamic(Arc::clone(f)), + } + } +} + +impl std::fmt::Debug for ApiKeyAuth { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("ApiKeyAuth") + .field("header_name", &self.header_name) + .field("api_key", &self.api_key) + .finish() + } +} + +impl Clone for ApiKeyAuth { + fn clone(&self) -> Self { + Self { + header_name: self.header_name.clone(), + api_key: self.api_key.clone(), + } + } +} + +impl ApiKeyAuth { + /// Create a new API key authenticator with a static key. + pub fn new(header_name: impl Into, api_key: impl Into) -> Self { + Self { + header_name: header_name.into(), + api_key: KeySource::Static(api_key.into()), + } + } + + /// Create an API key authenticator that evaluates the given + /// function on every request to obtain the current key. + pub fn from_provider( + header_name: impl Into, + f: impl Fn() -> String + Send + Sync + 'static, + ) -> Self { + Self { + header_name: header_name.into(), + api_key: KeySource::Dynamic(Arc::new(f)), + } + } +} + +impl Authenticator for ApiKeyAuth { + fn authenticate<'a>( + &self, + req: aioduct::RequestBuilder<'a, R>, + ) -> Result, Error> { + let key = match &self.api_key { + KeySource::Static(k) => k.clone(), + KeySource::Dynamic(f) => f(), + }; + Ok(req.header_str(&self.header_name, &key)?) + } +} diff --git a/tests/golden/rust/rust-aioduct/optional-request-bodies/src/runtime/client.rs.golden b/tests/golden/rust/rust-aioduct/optional-request-bodies/src/runtime/client.rs.golden new file mode 100644 index 000000000..aa8c6353a --- /dev/null +++ b/tests/golden/rust/rust-aioduct/optional-request-bodies/src/runtime/client.rs.golden @@ -0,0 +1,112 @@ +// @generated +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Optional Request Bodies — 1.0.0 +// Covers optional non-multipart request bodies. + +use std::fmt; +use std::sync::Arc; + +use aioduct::Runtime; + +use crate::runtime::auth::Authenticator; +use crate::runtime::error::Error; + +/// Async HTTP client wrapping `aioduct::Client`. +pub struct Client { + inner: aioduct::Client, + base_url: String, + authenticator: Option>>, +} + +impl Client { + /// Create a new client with the given base URL. + pub fn new(base_url: &str) -> Self { + Self { + inner: aioduct::Client::::with_rustls(), + base_url: base_url.trim_end_matches('/').to_string(), + authenticator: None, + } + } + + /// Create a client with a custom `aioduct::Client`. + pub fn with_client(inner: aioduct::Client, base_url: &str) -> Self { + Self { + inner, + base_url: base_url.trim_end_matches('/').to_string(), + authenticator: None, + } + } + + /// Attach an authenticator to the client. + pub fn with_auth(mut self, auth: Arc>) -> Self { + self.authenticator = Some(auth); + self + } + + fn full_url(&self, path: &str) -> String { + format!("{}{}", self.base_url, path) + } + + /// Build a GET request. + pub fn get(&self, path: &str) -> Result, Error> { + let mut req = self.inner.get(&self.full_url(path))?; + if let Some(auth) = &self.authenticator { + req = auth.authenticate(req)?; + } + Ok(req) + } + + /// Build a POST request. + pub fn post(&self, path: &str) -> Result, Error> { + let mut req = self.inner.post(&self.full_url(path))?; + if let Some(auth) = &self.authenticator { + req = auth.authenticate(req)?; + } + Ok(req) + } + + /// Build a PUT request. + pub fn put(&self, path: &str) -> Result, Error> { + let mut req = self.inner.put(&self.full_url(path))?; + if let Some(auth) = &self.authenticator { + req = auth.authenticate(req)?; + } + Ok(req) + } + + /// Build a DELETE request. + pub fn delete(&self, path: &str) -> Result, Error> { + let mut req = self.inner.delete(&self.full_url(path))?; + if let Some(auth) = &self.authenticator { + req = auth.authenticate(req)?; + } + Ok(req) + } + + /// Build a PATCH request. + pub fn patch(&self, path: &str) -> Result, Error> { + let mut req = self.inner.patch(&self.full_url(path))?; + if let Some(auth) = &self.authenticator { + req = auth.authenticate(req)?; + } + Ok(req) + } + + /// Build a HEAD request. + pub fn head(&self, path: &str) -> Result, Error> { + let mut req = self.inner.head(&self.full_url(path))?; + if let Some(auth) = &self.authenticator { + req = auth.authenticate(req)?; + } + Ok(req) + } +} + +impl fmt::Debug for Client { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("Client") + .field("base_url", &self.base_url) + .finish() + } +} diff --git a/tests/golden/rust/rust-aioduct/optional-request-bodies/src/runtime/error.rs.golden b/tests/golden/rust/rust-aioduct/optional-request-bodies/src/runtime/error.rs.golden new file mode 100644 index 000000000..4e56f2aa9 --- /dev/null +++ b/tests/golden/rust/rust-aioduct/optional-request-bodies/src/runtime/error.rs.golden @@ -0,0 +1,60 @@ +// @generated +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Optional Request Bodies — 1.0.0 +// Covers optional non-multipart request bodies. + +use std::fmt; + +/// Error types for the generated SDK. +#[derive(Debug)] +pub enum Error { + /// HTTP/transport error from aioduct. + Http(aioduct::Error), + /// JSON deserialization error. + Deserialize(serde_json::Error), + /// XML serialization error. + Xml(serde_xml_rs::Error), + /// Unsupported generated operation. + Unsupported(&'static str), +} + +impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Error::Http(e) => write!(f, "HTTP error: {e}"), + Error::Deserialize(e) => write!(f, "Deserialization error: {e}"), + Error::Xml(e) => write!(f, "XML serialization error: {e}"), + Error::Unsupported(e) => write!(f, "Unsupported operation: {e}"), + } + } +} + +impl std::error::Error for Error { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + Error::Http(e) => Some(e), + Error::Deserialize(e) => Some(e), + Error::Xml(e) => Some(e), + Error::Unsupported(_) => None, + } + } +} + +impl From for Error { + fn from(e: aioduct::Error) -> Self { + Error::Http(e) + } +} + +impl From for Error { + fn from(e: serde_json::Error) -> Self { + Error::Deserialize(e) + } +} + +impl From for Error { + fn from(e: serde_xml_rs::Error) -> Self { + Error::Xml(e) + } +} diff --git a/tests/golden/rust/rust-aioduct/optional-request-bodies/src/runtime/mod.rs.golden b/tests/golden/rust/rust-aioduct/optional-request-bodies/src/runtime/mod.rs.golden new file mode 100644 index 000000000..b9d8a0aac --- /dev/null +++ b/tests/golden/rust/rust-aioduct/optional-request-bodies/src/runtime/mod.rs.golden @@ -0,0 +1,9 @@ +// @generated +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Optional Request Bodies — 1.0.0 +// Covers optional non-multipart request bodies. + +pub mod auth; +pub mod client; +pub mod error; diff --git a/tests/golden/rust/rust-reqwest/multipart-edge-cases/src/apis/multipart.rs.golden b/tests/golden/rust/rust-reqwest/multipart-edge-cases/src/apis/multipart.rs.golden index bcc68eda9..68197c4bf 100644 --- a/tests/golden/rust/rust-reqwest/multipart-edge-cases/src/apis/multipart.rs.golden +++ b/tests/golden/rust/rust-reqwest/multipart-edge-cases/src/apis/multipart.rs.golden @@ -23,27 +23,29 @@ impl<'a> MultipartApi<'a> { /// POST /multipart/optional pub async fn send_optional_parts( &self, - body: &crate::models::OptionalUpload, + body: Option<&crate::models::OptionalUpload>, ) -> Result { let path = "/multipart/optional".to_string(); let mut req = self.client.request(reqwest::Method::POST, &path).await?; - let mut multipart = reqwest::multipart::Form::new(); - if let Some(value) = &body.attributes { - multipart = multipart.part("attributes", reqwest::multipart::Part::text(serde_json::to_string(value)?).mime_str("application/json").map_err(Error::Network)?); - } - if let Some(value) = &body.enabled { - multipart = multipart.part("enabled", reqwest::multipart::Part::text(value.to_string()).mime_str("text/plain").map_err(Error::Network)?); - } - if let Some(value) = &body.file { - multipart = multipart.part("file", reqwest::multipart::Part::bytes(value.clone()).file_name("file").mime_str("application/octet-stream").map_err(Error::Network)?); + if let Some(body) = body { + let mut multipart = reqwest::multipart::Form::new(); + if let Some(value) = &body.attributes { + multipart = multipart.part("attributes", reqwest::multipart::Part::text(serde_json::to_string(value)?).mime_str("application/json").map_err(Error::Network)?); + } + if let Some(value) = &body.enabled { + multipart = multipart.part("enabled", reqwest::multipart::Part::text(value.to_string()).mime_str("text/plain").map_err(Error::Network)?); + } + if let Some(value) = &body.file { + multipart = multipart.part("file", reqwest::multipart::Part::bytes(value.clone()).file_name("file").mime_str("application/octet-stream").map_err(Error::Network)?); + } + if let Some(value) = &body.retry_count { + multipart = multipart.part("retry_count", reqwest::multipart::Part::text(value.to_string()).mime_str("text/plain").map_err(Error::Network)?); + } + if let Some(value) = &body.title { + multipart = multipart.part("title", reqwest::multipart::Part::text(value.to_string()).mime_str("text/plain").map_err(Error::Network)?); + } + req = req.multipart(multipart); } - if let Some(value) = &body.retry_count { - multipart = multipart.part("retry_count", reqwest::multipart::Part::text(value.to_string()).mime_str("text/plain").map_err(Error::Network)?); - } - if let Some(value) = &body.title { - multipart = multipart.part("title", reqwest::multipart::Part::text(value.to_string()).mime_str("text/plain").map_err(Error::Network)?); - } - req = req.multipart(multipart); let resp = self.client.send(req).await?; let status_code = resp.status().as_u16(); Ok(SendOptionalPartsResponse { status_code }) diff --git a/tests/golden/rust/rust-reqwest/optional-request-bodies/Cargo.toml.golden b/tests/golden/rust/rust-reqwest/optional-request-bodies/Cargo.toml.golden new file mode 100644 index 000000000..4cd00334b --- /dev/null +++ b/tests/golden/rust/rust-reqwest/optional-request-bodies/Cargo.toml.golden @@ -0,0 +1,12 @@ +[package] +name = "optional-request-bodies" +version = "0.1.0" +edition = "2024" +description = "Covers optional non-multipart request bodies." + +[dependencies] +reqwest = { version = "0.12", features = ["json", "multipart"] } +serde = { version = "1", features = ["derive"] } +serde_json = "1" +serde-xml-rs = "0.8.2" +serde_repr = "0.1" diff --git a/tests/golden/rust/rust-reqwest/optional-request-bodies/README.md.golden b/tests/golden/rust/rust-reqwest/optional-request-bodies/README.md.golden new file mode 100644 index 000000000..a60de19e5 --- /dev/null +++ b/tests/golden/rust/rust-reqwest/optional-request-bodies/README.md.golden @@ -0,0 +1,7 @@ +# Optional Request Bodies + +Covers optional non-multipart request bodies. + +Version: `1.0.0` + +Generated by [openapi-nexus](https://github.com/adamcavendish/openapi-nexus) for `optional-request-bodies`. diff --git a/tests/golden/rust/rust-reqwest/optional-request-bodies/src/apis/default.rs.golden b/tests/golden/rust/rust-reqwest/optional-request-bodies/src/apis/default.rs.golden new file mode 100644 index 000000000..a44431b90 --- /dev/null +++ b/tests/golden/rust/rust-reqwest/optional-request-bodies/src/apis/default.rs.golden @@ -0,0 +1,106 @@ +// @generated +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Optional Request Bodies — 1.0.0 +// Covers optional non-multipart request bodies. + +use crate::runtime::client::Client; +use crate::runtime::error::Error; + +/// API operations under the "default" tag. +pub struct DefaultApi<'a> { + client: &'a Client, +} + +impl<'a> DefaultApi<'a> { + /// Create a new `DefaultApi` bound to the given client. + pub fn new(client: &'a Client) -> Self { + Self { + client, + } + } + + /// POST /optional-binary + pub async fn send_optional_binary( + &self, + body: Option<&Vec>, + ) -> Result { + let path = "/optional-binary".to_string(); + let mut req = self.client.request(reqwest::Method::POST, &path).await?; + if let Some(body) = body { + req = req.header("Content-Type", "application/octet-stream"); + req = req.body(body.clone()); + } + let resp = self.client.send(req).await?; + let status_code = resp.status().as_u16(); + Ok(SendOptionalBinaryResponse { status_code }) + } + + /// POST /optional-json + pub async fn send_optional_json( + &self, + body: Option<&crate::models::Payload>, + ) -> Result { + let path = "/optional-json".to_string(); + let mut req = self.client.request(reqwest::Method::POST, &path).await?; + if let Some(body) = body { + req = req.json(&body); + } + let resp = self.client.send(req).await?; + let status_code = resp.status().as_u16(); + Ok(SendOptionalJsonResponse { status_code }) + } + + /// POST /optional-text + pub async fn send_optional_text( + &self, + body: Option<&String>, + ) -> Result { + let path = "/optional-text".to_string(); + let mut req = self.client.request(reqwest::Method::POST, &path).await?; + if let Some(body) = body { + req = req.header("Content-Type", "text/plain"); + req = req.body(body.clone()); + } + let resp = self.client.send(req).await?; + let status_code = resp.status().as_u16(); + Ok(SendOptionalTextResponse { status_code }) + } + + /// POST /required-json + pub async fn send_required_json( + &self, + body: &crate::models::Payload, + ) -> Result { + let path = "/required-json".to_string(); + let mut req = self.client.request(reqwest::Method::POST, &path).await?; + req = req.json(&body); + let resp = self.client.send(req).await?; + let status_code = resp.status().as_u16(); + Ok(SendRequiredJsonResponse { status_code }) + } +} + +/// Response from `send_optional_binary`. +#[derive(Debug)] +pub struct SendOptionalBinaryResponse { + pub status_code: u16, +} + +/// Response from `send_optional_json`. +#[derive(Debug)] +pub struct SendOptionalJsonResponse { + pub status_code: u16, +} + +/// Response from `send_optional_text`. +#[derive(Debug)] +pub struct SendOptionalTextResponse { + pub status_code: u16, +} + +/// Response from `send_required_json`. +#[derive(Debug)] +pub struct SendRequiredJsonResponse { + pub status_code: u16, +} diff --git a/tests/golden/rust/rust-reqwest/optional-request-bodies/src/apis/mod.rs.golden b/tests/golden/rust/rust-reqwest/optional-request-bodies/src/apis/mod.rs.golden new file mode 100644 index 000000000..53674c52d --- /dev/null +++ b/tests/golden/rust/rust-reqwest/optional-request-bodies/src/apis/mod.rs.golden @@ -0,0 +1,8 @@ +// @generated +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Optional Request Bodies — 1.0.0 +// Covers optional non-multipart request bodies. + +mod default; +pub use default::*; diff --git a/tests/golden/rust/rust-reqwest/optional-request-bodies/src/lib.rs.golden b/tests/golden/rust/rust-reqwest/optional-request-bodies/src/lib.rs.golden new file mode 100644 index 000000000..dac0974a2 --- /dev/null +++ b/tests/golden/rust/rust-reqwest/optional-request-bodies/src/lib.rs.golden @@ -0,0 +1,11 @@ +// @generated +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Optional Request Bodies — 1.0.0 +// Covers optional non-multipart request bodies. + +#![allow(clippy::all)] + +pub mod apis; +pub mod models; +pub mod runtime; diff --git a/tests/golden/rust/rust-reqwest/optional-request-bodies/src/models/mod.rs.golden b/tests/golden/rust/rust-reqwest/optional-request-bodies/src/models/mod.rs.golden new file mode 100644 index 000000000..d7518154a --- /dev/null +++ b/tests/golden/rust/rust-reqwest/optional-request-bodies/src/models/mod.rs.golden @@ -0,0 +1,8 @@ +// @generated +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Optional Request Bodies — 1.0.0 +// Covers optional non-multipart request bodies. + +mod payload; +pub use payload::*; diff --git a/tests/golden/rust/rust-reqwest/optional-request-bodies/src/models/payload.rs.golden b/tests/golden/rust/rust-reqwest/optional-request-bodies/src/models/payload.rs.golden new file mode 100644 index 000000000..4bb3b0bc7 --- /dev/null +++ b/tests/golden/rust/rust-reqwest/optional-request-bodies/src/models/payload.rs.golden @@ -0,0 +1,14 @@ +// @generated +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Optional Request Bodies — 1.0.0 +// Covers optional non-multipart request bodies. + +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Payload { + #[serde(skip_serializing_if = "Option::is_none", default)] + pub count: Option, + pub message: String, +} diff --git a/tests/golden/rust/rust-reqwest/optional-request-bodies/src/runtime/auth.rs.golden b/tests/golden/rust/rust-reqwest/optional-request-bodies/src/runtime/auth.rs.golden new file mode 100644 index 000000000..ca938d09c --- /dev/null +++ b/tests/golden/rust/rust-reqwest/optional-request-bodies/src/runtime/auth.rs.golden @@ -0,0 +1,189 @@ +// @generated +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Optional Request Bodies — 1.0.0 +// Covers optional non-multipart request bodies. + +use std::sync::Arc; + +use reqwest::header::{HeaderMap, HeaderValue}; + +use crate::runtime::error::Error; + +/// Trait for authenticating HTTP requests. +pub trait Authenticator: Send + Sync + std::fmt::Debug { + /// Apply authentication to the given headers. + fn authenticate(&self, headers: &mut HeaderMap) -> Result<(), Error>; +} + +/// Bearer token authentication. +/// +/// Use [`BearerAuth::new`] with a static token, or +/// [`BearerAuth::from_provider`] with a function that returns the +/// current token (evaluated on every request). +pub struct BearerAuth { + token: TokenSource, +} + +enum TokenSource { + Static(String), + Dynamic(Arc String + Send + Sync>), +} + +impl std::fmt::Debug for TokenSource { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Static(t) => f.debug_tuple("Static").field(&t).finish(), + Self::Dynamic(_) => f.debug_tuple("Dynamic").field(&"").finish(), + } + } +} + +impl Clone for TokenSource { + fn clone(&self) -> Self { + match self { + Self::Static(t) => Self::Static(t.clone()), + Self::Dynamic(f) => Self::Dynamic(Arc::clone(f)), + } + } +} + +impl std::fmt::Debug for BearerAuth { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("BearerAuth").field("token", &self.token).finish() + } +} + +impl Clone for BearerAuth { + fn clone(&self) -> Self { + Self { + token: self.token.clone(), + } + } +} + +impl BearerAuth { + /// Create a new bearer token authenticator with a static token. + pub fn new(token: impl Into) -> Self { + Self { + token: TokenSource::Static(token.into()), + } + } + + /// Create a bearer token authenticator that evaluates the given + /// function on every request to obtain the current token. + pub fn from_provider(f: impl Fn() -> String + Send + Sync + 'static) -> Self { + Self { + token: TokenSource::Dynamic(Arc::new(f)), + } + } +} + +impl Authenticator for BearerAuth { + fn authenticate(&self, headers: &mut HeaderMap) -> Result<(), Error> { + let token = match &self.token { + TokenSource::Static(t) => t.clone(), + TokenSource::Dynamic(f) => f(), + }; + let value = HeaderValue::from_str(&format!("Bearer {}", token)) + .map_err(|e| Error::Api { + status: reqwest::StatusCode::INTERNAL_SERVER_ERROR, + body: format!("invalid auth header: {e}"), + })?; + headers.insert(reqwest::header::AUTHORIZATION, value); + Ok(()) + } +} + +/// API key authentication via header. +/// +/// Use [`ApiKeyAuth::new`] with a static key, or +/// [`ApiKeyAuth::from_provider`] with a function that returns the +/// current key (evaluated on every request). +pub struct ApiKeyAuth { + header_name: String, + api_key: KeySource, +} + +enum KeySource { + Static(String), + Dynamic(Arc String + Send + Sync>), +} + +impl std::fmt::Debug for KeySource { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Static(k) => f.debug_tuple("Static").field(&k).finish(), + Self::Dynamic(_) => f.debug_tuple("Dynamic").field(&"").finish(), + } + } +} + +impl Clone for KeySource { + fn clone(&self) -> Self { + match self { + Self::Static(k) => Self::Static(k.clone()), + Self::Dynamic(f) => Self::Dynamic(Arc::clone(f)), + } + } +} + +impl std::fmt::Debug for ApiKeyAuth { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("ApiKeyAuth") + .field("header_name", &self.header_name) + .field("api_key", &self.api_key) + .finish() + } +} + +impl Clone for ApiKeyAuth { + fn clone(&self) -> Self { + Self { + header_name: self.header_name.clone(), + api_key: self.api_key.clone(), + } + } +} + +impl ApiKeyAuth { + /// Create a new API key authenticator with a static key. + pub fn new(header_name: impl Into, api_key: impl Into) -> Self { + Self { + header_name: header_name.into(), + api_key: KeySource::Static(api_key.into()), + } + } + + /// Create an API key authenticator that evaluates the given + /// function on every request to obtain the current key. + pub fn from_provider( + header_name: impl Into, + f: impl Fn() -> String + Send + Sync + 'static, + ) -> Self { + Self { + header_name: header_name.into(), + api_key: KeySource::Dynamic(Arc::new(f)), + } + } +} + +impl Authenticator for ApiKeyAuth { + fn authenticate(&self, headers: &mut HeaderMap) -> Result<(), Error> { + let key = match &self.api_key { + KeySource::Static(k) => k.clone(), + KeySource::Dynamic(f) => f(), + }; + let name = reqwest::header::HeaderName::from_bytes(self.header_name.as_bytes()) + .map_err(|e| Error::Api { + status: reqwest::StatusCode::INTERNAL_SERVER_ERROR, + body: format!("invalid header name: {e}"), + })?; + let value = HeaderValue::from_str(&key).map_err(|e| Error::Api { + status: reqwest::StatusCode::INTERNAL_SERVER_ERROR, + body: format!("invalid header value: {e}"), + })?; + headers.insert(name, value); + Ok(()) + } +} diff --git a/tests/golden/rust/rust-reqwest/optional-request-bodies/src/runtime/client.rs.golden b/tests/golden/rust/rust-reqwest/optional-request-bodies/src/runtime/client.rs.golden new file mode 100644 index 000000000..03e2efbf5 --- /dev/null +++ b/tests/golden/rust/rust-reqwest/optional-request-bodies/src/runtime/client.rs.golden @@ -0,0 +1,73 @@ +// @generated +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Optional Request Bodies — 1.0.0 +// Covers optional non-multipart request bodies. + +use std::sync::Arc; + +use reqwest::header::HeaderMap; + +use crate::runtime::auth::Authenticator; +use crate::runtime::error::Error; + +/// HTTP client wrapper around reqwest. +#[derive(Debug, Clone)] +pub struct Client { + inner: reqwest::Client, + base_url: String, + authenticator: Option>, +} + +impl Client { + /// Create a new client with the given base URL. + pub fn new(base_url: &str) -> Self { + Self { + inner: reqwest::Client::new(), + base_url: base_url.trim_end_matches('/').to_string(), + authenticator: None, + } + } + + /// Create a new client with a custom reqwest::Client. + pub fn with_client(inner: reqwest::Client, base_url: &str) -> Self { + Self { + inner, + base_url: base_url.trim_end_matches('/').to_string(), + authenticator: None, + } + } + + /// Set the authenticator for this client. + pub fn with_auth(mut self, auth: impl Authenticator + 'static) -> Self { + self.authenticator = Some(Arc::new(auth)); + self + } + + /// Build a request with authentication applied. + pub async fn request( + &self, + method: reqwest::Method, + path: &str, + ) -> Result { + let url = format!("{}{}", self.base_url, path); + let mut builder = self.inner.request(method, &url); + + if let Some(auth) = &self.authenticator { + let mut headers = HeaderMap::new(); + auth.authenticate(&mut headers)?; + builder = builder.headers(headers); + } + + Ok(builder) + } + + /// Send a request and return the response. + pub async fn send( + &self, + request: reqwest::RequestBuilder, + ) -> Result { + let response = request.send().await.map_err(Error::Network)?; + Ok(response) + } +} diff --git a/tests/golden/rust/rust-reqwest/optional-request-bodies/src/runtime/error.rs.golden b/tests/golden/rust/rust-reqwest/optional-request-bodies/src/runtime/error.rs.golden new file mode 100644 index 000000000..4771d4c55 --- /dev/null +++ b/tests/golden/rust/rust-reqwest/optional-request-bodies/src/runtime/error.rs.golden @@ -0,0 +1,65 @@ +// @generated +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Optional Request Bodies — 1.0.0 +// Covers optional non-multipart request bodies. + +/// Errors that can occur during API calls. +#[derive(Debug)] +pub enum Error { + /// Network-level error from reqwest. + Network(reqwest::Error), + /// API returned a non-success status code. + Api { + status: reqwest::StatusCode, + body: String, + }, + /// Failed to deserialize the response body. + Deserialize(serde_json::Error), + /// Failed to serialize the request body as XML. + Xml(serde_xml_rs::Error), + /// Unsupported generated operation. + Unsupported(&'static str), +} + +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::Network(e) => write!(f, "network error: {e}"), + Error::Api { status, body } => write!(f, "API error {status}: {body}"), + Error::Deserialize(e) => write!(f, "deserialization error: {e}"), + Error::Xml(e) => write!(f, "XML serialization error: {e}"), + Error::Unsupported(e) => write!(f, "Unsupported operation: {e}"), + } + } +} + +impl std::error::Error for Error { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + Error::Network(e) => Some(e), + Error::Deserialize(e) => Some(e), + Error::Xml(e) => Some(e), + Error::Api { .. } => None, + Error::Unsupported(_) => None, + } + } +} + +impl From for Error { + fn from(e: reqwest::Error) -> Self { + Error::Network(e) + } +} + +impl From for Error { + fn from(e: serde_json::Error) -> Self { + Error::Deserialize(e) + } +} + +impl From for Error { + fn from(e: serde_xml_rs::Error) -> Self { + Error::Xml(e) + } +} diff --git a/tests/golden/rust/rust-reqwest/optional-request-bodies/src/runtime/mod.rs.golden b/tests/golden/rust/rust-reqwest/optional-request-bodies/src/runtime/mod.rs.golden new file mode 100644 index 000000000..b9d8a0aac --- /dev/null +++ b/tests/golden/rust/rust-reqwest/optional-request-bodies/src/runtime/mod.rs.golden @@ -0,0 +1,9 @@ +// @generated +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Optional Request Bodies — 1.0.0 +// Covers optional non-multipart request bodies. + +pub mod auth; +pub mod client; +pub mod error; diff --git a/tests/golden/rust/rust-ureq/multipart-edge-cases/src/apis/multipart.rs.golden b/tests/golden/rust/rust-ureq/multipart-edge-cases/src/apis/multipart.rs.golden index 283b6ba8e..325e6c28a 100644 --- a/tests/golden/rust/rust-ureq/multipart-edge-cases/src/apis/multipart.rs.golden +++ b/tests/golden/rust/rust-ureq/multipart-edge-cases/src/apis/multipart.rs.golden @@ -23,45 +23,51 @@ impl<'a> MultipartApi<'a> { /// POST /multipart/optional pub fn send_optional_parts( &self, - body: &crate::models::OptionalUpload, + body: Option<&crate::models::OptionalUpload>, ) -> Result { let path = "/multipart/optional".to_string(); let mut req = self.client.post(&path); - let boundary = format!("openapi-nexus-{}", std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).map(|duration| duration.as_nanos()).unwrap_or(0)); - let mut multipart_body: Vec = Vec::new(); - if let Some(value) = &body.attributes { - multipart_body.extend_from_slice(format!("--{}\r\n", boundary).as_bytes()); - multipart_body.extend_from_slice(format!("Content-Disposition: form-data; name=\"{}\"\r\nContent-Type: {}\r\n\r\n", "attributes", "application/json").as_bytes()); - multipart_body.extend_from_slice(serde_json::to_string(value)?.as_bytes()); - multipart_body.extend_from_slice(b"\r\n"); - } - if let Some(value) = &body.enabled { - multipart_body.extend_from_slice(format!("--{}\r\n", boundary).as_bytes()); - multipart_body.extend_from_slice(format!("Content-Disposition: form-data; name=\"{}\"\r\nContent-Type: {}\r\n\r\n", "enabled", "text/plain").as_bytes()); - multipart_body.extend_from_slice(value.to_string().as_bytes()); - multipart_body.extend_from_slice(b"\r\n"); - } - if let Some(value) = &body.file { - multipart_body.extend_from_slice(format!("--{}\r\n", boundary).as_bytes()); - multipart_body.extend_from_slice(format!("Content-Disposition: form-data; name=\"{}\"; filename=\"{}\"\r\nContent-Type: {}\r\n\r\n", "file", "file", "application/octet-stream").as_bytes()); - multipart_body.extend_from_slice(&value.clone()); - multipart_body.extend_from_slice(b"\r\n"); + let resp; + if let Some(body) = body { + let boundary = format!("openapi-nexus-{}", std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).map(|duration| duration.as_nanos()).unwrap_or(0)); + let mut multipart_body: Vec = Vec::new(); + if let Some(value) = &body.attributes { + multipart_body.extend_from_slice(format!("--{}\r\n", boundary).as_bytes()); + multipart_body.extend_from_slice(format!("Content-Disposition: form-data; name=\"{}\"\r\nContent-Type: {}\r\n\r\n", "attributes", "application/json").as_bytes()); + multipart_body.extend_from_slice(serde_json::to_string(value)?.as_bytes()); + multipart_body.extend_from_slice(b"\r\n"); + } + if let Some(value) = &body.enabled { + multipart_body.extend_from_slice(format!("--{}\r\n", boundary).as_bytes()); + multipart_body.extend_from_slice(format!("Content-Disposition: form-data; name=\"{}\"\r\nContent-Type: {}\r\n\r\n", "enabled", "text/plain").as_bytes()); + multipart_body.extend_from_slice(value.to_string().as_bytes()); + multipart_body.extend_from_slice(b"\r\n"); + } + if let Some(value) = &body.file { + multipart_body.extend_from_slice(format!("--{}\r\n", boundary).as_bytes()); + multipart_body.extend_from_slice(format!("Content-Disposition: form-data; name=\"{}\"; filename=\"{}\"\r\nContent-Type: {}\r\n\r\n", "file", "file", "application/octet-stream").as_bytes()); + multipart_body.extend_from_slice(&value.clone()); + multipart_body.extend_from_slice(b"\r\n"); + } + if let Some(value) = &body.retry_count { + multipart_body.extend_from_slice(format!("--{}\r\n", boundary).as_bytes()); + multipart_body.extend_from_slice(format!("Content-Disposition: form-data; name=\"{}\"\r\nContent-Type: {}\r\n\r\n", "retry_count", "text/plain").as_bytes()); + multipart_body.extend_from_slice(value.to_string().as_bytes()); + multipart_body.extend_from_slice(b"\r\n"); + } + if let Some(value) = &body.title { + multipart_body.extend_from_slice(format!("--{}\r\n", boundary).as_bytes()); + multipart_body.extend_from_slice(format!("Content-Disposition: form-data; name=\"{}\"\r\nContent-Type: {}\r\n\r\n", "title", "text/plain").as_bytes()); + multipart_body.extend_from_slice(value.to_string().as_bytes()); + multipart_body.extend_from_slice(b"\r\n"); + } + multipart_body.extend_from_slice(format!("--{}--\r\n", boundary).as_bytes()); + req = req.header("Content-Type", &format!("multipart/form-data; boundary={}", boundary)); + resp = req.send(multipart_body)?; } - if let Some(value) = &body.retry_count { - multipart_body.extend_from_slice(format!("--{}\r\n", boundary).as_bytes()); - multipart_body.extend_from_slice(format!("Content-Disposition: form-data; name=\"{}\"\r\nContent-Type: {}\r\n\r\n", "retry_count", "text/plain").as_bytes()); - multipart_body.extend_from_slice(value.to_string().as_bytes()); - multipart_body.extend_from_slice(b"\r\n"); + else { + resp = req.send_empty()?; } - if let Some(value) = &body.title { - multipart_body.extend_from_slice(format!("--{}\r\n", boundary).as_bytes()); - multipart_body.extend_from_slice(format!("Content-Disposition: form-data; name=\"{}\"\r\nContent-Type: {}\r\n\r\n", "title", "text/plain").as_bytes()); - multipart_body.extend_from_slice(value.to_string().as_bytes()); - multipart_body.extend_from_slice(b"\r\n"); - } - multipart_body.extend_from_slice(format!("--{}--\r\n", boundary).as_bytes()); - req = req.header("Content-Type", &format!("multipart/form-data; boundary={}", boundary)); - let resp = req.send(multipart_body)?; let status_code = resp.status().as_u16(); Ok(SendOptionalPartsResponse { status_code }) } diff --git a/tests/golden/rust/rust-ureq/optional-request-bodies/Cargo.toml.golden b/tests/golden/rust/rust-ureq/optional-request-bodies/Cargo.toml.golden new file mode 100644 index 000000000..8fab76dbc --- /dev/null +++ b/tests/golden/rust/rust-ureq/optional-request-bodies/Cargo.toml.golden @@ -0,0 +1,12 @@ +[package] +name = "optional-request-bodies" +version = "0.1.0" +edition = "2024" +description = "Covers optional non-multipart request bodies." + +[dependencies] +ureq = { version = "3", features = ["json"] } +serde = { version = "1", features = ["derive"] } +serde_json = "1" +serde-xml-rs = "0.8.2" +serde_repr = "0.1" diff --git a/tests/golden/rust/rust-ureq/optional-request-bodies/README.md.golden b/tests/golden/rust/rust-ureq/optional-request-bodies/README.md.golden new file mode 100644 index 000000000..a60de19e5 --- /dev/null +++ b/tests/golden/rust/rust-ureq/optional-request-bodies/README.md.golden @@ -0,0 +1,7 @@ +# Optional Request Bodies + +Covers optional non-multipart request bodies. + +Version: `1.0.0` + +Generated by [openapi-nexus](https://github.com/adamcavendish/openapi-nexus) for `optional-request-bodies`. diff --git a/tests/golden/rust/rust-ureq/optional-request-bodies/src/apis/default.rs.golden b/tests/golden/rust/rust-ureq/optional-request-bodies/src/apis/default.rs.golden new file mode 100644 index 000000000..e4a7c3004 --- /dev/null +++ b/tests/golden/rust/rust-ureq/optional-request-bodies/src/apis/default.rs.golden @@ -0,0 +1,114 @@ +// @generated +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Optional Request Bodies — 1.0.0 +// Covers optional non-multipart request bodies. + +use crate::runtime::client::Client; +use crate::runtime::error::Error; + +/// API operations under the "default" tag. +pub struct DefaultApi<'a> { + client: &'a Client, +} + +impl<'a> DefaultApi<'a> { + /// Create a new `DefaultApi` bound to the given client. + pub fn new(client: &'a Client) -> Self { + Self { + client, + } + } + + /// POST /optional-binary + pub fn send_optional_binary( + &self, + body: Option<&Vec>, + ) -> Result { + let path = "/optional-binary".to_string(); + let mut req = self.client.post(&path); + let resp; + if let Some(body) = body { + req = req.header("Content-Type", "application/octet-stream"); + resp = req.send(body.clone())?; + } + else { + resp = req.send_empty()?; + } + let status_code = resp.status().as_u16(); + Ok(SendOptionalBinaryResponse { status_code }) + } + + /// POST /optional-json + pub fn send_optional_json( + &self, + body: Option<&crate::models::Payload>, + ) -> Result { + let path = "/optional-json".to_string(); + let req = self.client.post(&path); + let resp; + if let Some(body) = body { + resp = req.send_json(&body)?; + } + else { + resp = req.send_empty()?; + } + let status_code = resp.status().as_u16(); + Ok(SendOptionalJsonResponse { status_code }) + } + + /// POST /optional-text + pub fn send_optional_text( + &self, + body: Option<&String>, + ) -> Result { + let path = "/optional-text".to_string(); + let mut req = self.client.post(&path); + let resp; + if let Some(body) = body { + req = req.header("Content-Type", "text/plain"); + resp = req.send(body.as_str())?; + } + else { + resp = req.send_empty()?; + } + let status_code = resp.status().as_u16(); + Ok(SendOptionalTextResponse { status_code }) + } + + /// POST /required-json + pub fn send_required_json( + &self, + body: &crate::models::Payload, + ) -> Result { + let path = "/required-json".to_string(); + let req = self.client.post(&path); + let resp = req.send_json(&body)?; + let status_code = resp.status().as_u16(); + Ok(SendRequiredJsonResponse { status_code }) + } +} + +/// Response from `send_optional_binary`. +#[derive(Debug)] +pub struct SendOptionalBinaryResponse { + pub status_code: u16, +} + +/// Response from `send_optional_json`. +#[derive(Debug)] +pub struct SendOptionalJsonResponse { + pub status_code: u16, +} + +/// Response from `send_optional_text`. +#[derive(Debug)] +pub struct SendOptionalTextResponse { + pub status_code: u16, +} + +/// Response from `send_required_json`. +#[derive(Debug)] +pub struct SendRequiredJsonResponse { + pub status_code: u16, +} diff --git a/tests/golden/rust/rust-ureq/optional-request-bodies/src/apis/mod.rs.golden b/tests/golden/rust/rust-ureq/optional-request-bodies/src/apis/mod.rs.golden new file mode 100644 index 000000000..53674c52d --- /dev/null +++ b/tests/golden/rust/rust-ureq/optional-request-bodies/src/apis/mod.rs.golden @@ -0,0 +1,8 @@ +// @generated +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Optional Request Bodies — 1.0.0 +// Covers optional non-multipart request bodies. + +mod default; +pub use default::*; diff --git a/tests/golden/rust/rust-ureq/optional-request-bodies/src/lib.rs.golden b/tests/golden/rust/rust-ureq/optional-request-bodies/src/lib.rs.golden new file mode 100644 index 000000000..dac0974a2 --- /dev/null +++ b/tests/golden/rust/rust-ureq/optional-request-bodies/src/lib.rs.golden @@ -0,0 +1,11 @@ +// @generated +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Optional Request Bodies — 1.0.0 +// Covers optional non-multipart request bodies. + +#![allow(clippy::all)] + +pub mod apis; +pub mod models; +pub mod runtime; diff --git a/tests/golden/rust/rust-ureq/optional-request-bodies/src/models/mod.rs.golden b/tests/golden/rust/rust-ureq/optional-request-bodies/src/models/mod.rs.golden new file mode 100644 index 000000000..d7518154a --- /dev/null +++ b/tests/golden/rust/rust-ureq/optional-request-bodies/src/models/mod.rs.golden @@ -0,0 +1,8 @@ +// @generated +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Optional Request Bodies — 1.0.0 +// Covers optional non-multipart request bodies. + +mod payload; +pub use payload::*; diff --git a/tests/golden/rust/rust-ureq/optional-request-bodies/src/models/payload.rs.golden b/tests/golden/rust/rust-ureq/optional-request-bodies/src/models/payload.rs.golden new file mode 100644 index 000000000..4bb3b0bc7 --- /dev/null +++ b/tests/golden/rust/rust-ureq/optional-request-bodies/src/models/payload.rs.golden @@ -0,0 +1,14 @@ +// @generated +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Optional Request Bodies — 1.0.0 +// Covers optional non-multipart request bodies. + +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Payload { + #[serde(skip_serializing_if = "Option::is_none", default)] + pub count: Option, + pub message: String, +} diff --git a/tests/golden/rust/rust-ureq/optional-request-bodies/src/runtime/auth.rs.golden b/tests/golden/rust/rust-ureq/optional-request-bodies/src/runtime/auth.rs.golden new file mode 100644 index 000000000..fb40bbc97 --- /dev/null +++ b/tests/golden/rust/rust-ureq/optional-request-bodies/src/runtime/auth.rs.golden @@ -0,0 +1,169 @@ +// @generated +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Optional Request Bodies — 1.0.0 +// Covers optional non-multipart request bodies. + +use std::sync::Arc; + +/// Trait for authenticating requests. +pub trait Authenticator: Send + Sync + std::fmt::Debug { + /// Return header key-value pairs to apply to the request. + fn auth_headers(&self) -> Vec<(&str, String)>; +} + +/// Bearer token authentication. +/// +/// Use [`BearerAuth::new`] with a static token, or +/// [`BearerAuth::from_provider`] with a function that returns the +/// current token (evaluated on every request). +pub struct BearerAuth { + token: TokenSource, +} + +enum TokenSource { + Static(String), + Dynamic(Arc String + Send + Sync>), +} + +impl std::fmt::Debug for TokenSource { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Static(t) => f.debug_tuple("Static").field(&t).finish(), + Self::Dynamic(_) => f.debug_tuple("Dynamic").field(&"").finish(), + } + } +} + +impl Clone for TokenSource { + fn clone(&self) -> Self { + match self { + Self::Static(t) => Self::Static(t.clone()), + Self::Dynamic(f) => Self::Dynamic(Arc::clone(f)), + } + } +} + +impl std::fmt::Debug for BearerAuth { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("BearerAuth").field("token", &self.token).finish() + } +} + +impl Clone for BearerAuth { + fn clone(&self) -> Self { + Self { + token: self.token.clone(), + } + } +} + +impl BearerAuth { + /// Create a new bearer token authenticator with a static token. + pub fn new(token: impl Into) -> Self { + Self { + token: TokenSource::Static(token.into()), + } + } + + /// Create a bearer token authenticator that evaluates the given + /// function on every request to obtain the current token. + pub fn from_provider(f: impl Fn() -> String + Send + Sync + 'static) -> Self { + Self { + token: TokenSource::Dynamic(Arc::new(f)), + } + } +} + +impl Authenticator for BearerAuth { + fn auth_headers(&self) -> Vec<(&str, String)> { + let token = match &self.token { + TokenSource::Static(t) => t.clone(), + TokenSource::Dynamic(f) => f(), + }; + vec![("Authorization", format!("Bearer {}", token))] + } +} + +/// API key authentication. +/// +/// Use [`ApiKeyAuth::new`] with a static key, or +/// [`ApiKeyAuth::from_provider`] with a function that returns the +/// current key (evaluated on every request). +pub struct ApiKeyAuth { + header_name: String, + api_key: KeySource, +} + +enum KeySource { + Static(String), + Dynamic(Arc String + Send + Sync>), +} + +impl std::fmt::Debug for KeySource { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Static(k) => f.debug_tuple("Static").field(&k).finish(), + Self::Dynamic(_) => f.debug_tuple("Dynamic").field(&"").finish(), + } + } +} + +impl Clone for KeySource { + fn clone(&self) -> Self { + match self { + Self::Static(k) => Self::Static(k.clone()), + Self::Dynamic(f) => Self::Dynamic(Arc::clone(f)), + } + } +} + +impl std::fmt::Debug for ApiKeyAuth { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("ApiKeyAuth") + .field("header_name", &self.header_name) + .field("api_key", &self.api_key) + .finish() + } +} + +impl Clone for ApiKeyAuth { + fn clone(&self) -> Self { + Self { + header_name: self.header_name.clone(), + api_key: self.api_key.clone(), + } + } +} + +impl ApiKeyAuth { + /// Create a new API key authenticator with a static key. + pub fn new(header_name: impl Into, api_key: impl Into) -> Self { + Self { + header_name: header_name.into(), + api_key: KeySource::Static(api_key.into()), + } + } + + /// Create an API key authenticator that evaluates the given + /// function on every request to obtain the current key. + pub fn from_provider( + header_name: impl Into, + f: impl Fn() -> String + Send + Sync + 'static, + ) -> Self { + Self { + header_name: header_name.into(), + api_key: KeySource::Dynamic(Arc::new(f)), + } + } +} + +impl Authenticator for ApiKeyAuth { + fn auth_headers(&self) -> Vec<(&str, String)> { + let key = match &self.api_key { + KeySource::Static(k) => k.clone(), + KeySource::Dynamic(f) => f(), + }; + vec![(&self.header_name, key)] + } +} diff --git a/tests/golden/rust/rust-ureq/optional-request-bodies/src/runtime/client.rs.golden b/tests/golden/rust/rust-ureq/optional-request-bodies/src/runtime/client.rs.golden new file mode 100644 index 000000000..6161ba9e3 --- /dev/null +++ b/tests/golden/rust/rust-ureq/optional-request-bodies/src/runtime/client.rs.golden @@ -0,0 +1,94 @@ +// @generated +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Optional Request Bodies — 1.0.0 +// Covers optional non-multipart request bodies. + +use std::fmt; +use std::sync::Arc; + +use crate::runtime::auth::Authenticator; + +/// Synchronous HTTP client wrapping `ureq::Agent`. +pub struct Client { + inner: ureq::Agent, + base_url: String, + authenticator: Option>, +} + +impl Client { + /// Create a new client with the given base URL. + pub fn new(base_url: &str) -> Self { + Self { + inner: ureq::Agent::new_with_defaults(), + base_url: base_url.trim_end_matches('/').to_string(), + authenticator: None, + } + } + + /// Create a client with a custom `ureq::Agent`. + pub fn with_agent(inner: ureq::Agent, base_url: &str) -> Self { + Self { + inner, + base_url: base_url.trim_end_matches('/').to_string(), + authenticator: None, + } + } + + /// Attach an authenticator to the client. + pub fn with_auth(mut self, auth: Arc) -> Self { + self.authenticator = Some(auth); + self + } + + fn full_url(&self, path: &str) -> String { + format!("{}{}", self.base_url, path) + } + + fn apply_auth(&self, mut req: ureq::RequestBuilder) -> ureq::RequestBuilder { + if let Some(auth) = &self.authenticator { + for (key, value) in auth.auth_headers() { + req = req.header(key, &value); + } + } + req + } + + /// Build a GET request. + pub fn get(&self, path: &str) -> ureq::RequestBuilder { + self.apply_auth(self.inner.get(&self.full_url(path))) + } + + /// Build a POST request. + pub fn post(&self, path: &str) -> ureq::RequestBuilder { + self.apply_auth(self.inner.post(&self.full_url(path))) + } + + /// Build a PUT request. + pub fn put(&self, path: &str) -> ureq::RequestBuilder { + self.apply_auth(self.inner.put(&self.full_url(path))) + } + + /// Build a DELETE request. + pub fn delete(&self, path: &str) -> ureq::RequestBuilder { + self.apply_auth(self.inner.delete(&self.full_url(path))) + } + + /// Build a PATCH request. + pub fn patch(&self, path: &str) -> ureq::RequestBuilder { + self.apply_auth(self.inner.patch(&self.full_url(path))) + } + + /// Build a HEAD request. + pub fn head(&self, path: &str) -> ureq::RequestBuilder { + self.apply_auth(self.inner.head(&self.full_url(path))) + } +} + +impl fmt::Debug for Client { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("Client") + .field("base_url", &self.base_url) + .finish() + } +} diff --git a/tests/golden/rust/rust-ureq/optional-request-bodies/src/runtime/error.rs.golden b/tests/golden/rust/rust-ureq/optional-request-bodies/src/runtime/error.rs.golden new file mode 100644 index 000000000..dd312e6bd --- /dev/null +++ b/tests/golden/rust/rust-ureq/optional-request-bodies/src/runtime/error.rs.golden @@ -0,0 +1,64 @@ +// @generated +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Optional Request Bodies — 1.0.0 +// Covers optional non-multipart request bodies. + +use std::fmt; + +/// Error types for the generated SDK. +#[derive(Debug)] +pub enum Error { + /// HTTP transport error. + Http(ureq::Error), + /// I/O error while reading response body. + Io(std::io::Error), + /// JSON deserialization error. + Deserialize(serde_json::Error), + /// XML serialization error. + Xml(serde_xml_rs::Error), + /// Unsupported generated operation. + Unsupported(&'static str), +} + +impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Error::Http(e) => write!(f, "HTTP error: {e}"), + Error::Io(e) => write!(f, "IO error: {e}"), + Error::Deserialize(e) => write!(f, "Deserialization error: {e}"), + Error::Xml(e) => write!(f, "XML serialization error: {e}"), + Error::Unsupported(e) => write!(f, "Unsupported operation: {e}"), + } + } +} + +impl std::error::Error for Error { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + Error::Http(e) => Some(e), + Error::Io(e) => Some(e), + Error::Deserialize(e) => Some(e), + Error::Xml(e) => Some(e), + Error::Unsupported(_) => None, + } + } +} + +impl From for Error { + fn from(e: ureq::Error) -> Self { + Error::Http(e) + } +} + +impl From for Error { + fn from(e: serde_json::Error) -> Self { + Error::Deserialize(e) + } +} + +impl From for Error { + fn from(e: serde_xml_rs::Error) -> Self { + Error::Xml(e) + } +} diff --git a/tests/golden/rust/rust-ureq/optional-request-bodies/src/runtime/mod.rs.golden b/tests/golden/rust/rust-ureq/optional-request-bodies/src/runtime/mod.rs.golden new file mode 100644 index 000000000..b9d8a0aac --- /dev/null +++ b/tests/golden/rust/rust-ureq/optional-request-bodies/src/runtime/mod.rs.golden @@ -0,0 +1,9 @@ +// @generated +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Optional Request Bodies — 1.0.0 +// Covers optional non-multipart request bodies. + +pub mod auth; +pub mod client; +pub mod error; diff --git a/tests/golden/typescript/typescript-fetch/optional-request-bodies/README.md.golden b/tests/golden/typescript/typescript-fetch/optional-request-bodies/README.md.golden new file mode 100644 index 000000000..3c141c32e --- /dev/null +++ b/tests/golden/typescript/typescript-fetch/optional-request-bodies/README.md.golden @@ -0,0 +1,248 @@ +# optional-request-bodies + +Covers optional non-multipart request bodies. + +**Version:** 1.0.0 + +## Overview + +This package provides a TypeScript/JavaScript client for the Optional Request Bodies API. It uses the native [Fetch API](https://fetch.spec.whatwg.org/) for HTTP requests and works in both Node.js and browser environments. + +## Features + +- ✨ **Type-safe** - Full TypeScript support with generated types +- 🚀 **Modern** - Uses native Fetch API, no external HTTP dependencies +- 🔧 **Configurable** - Flexible configuration options +- 🎯 **Middleware** - Support for request/response interceptors +- 📦 **Tree-shakeable** - Import only what you need +- 🌐 **Universal** - Works in Node.js and browsers + +## Installation + +### From npm (published package) + +```bash +npm install optional-request-bodies +``` + +### From local path (development) + +Add the package to your `package.json` using the `file:` protocol: + +```json +{ + "dependencies": { + "optional-request-bodies": "file:../../path/to/generated/package" + } +} +``` + +Then run: + +```bash +npm install +``` + +## Quick Start + +```typescript +import { Configuration, DefaultApi } from 'optional-request-bodies'; + +// Create a configuration +const config = new Configuration({ + basePath: 'https://api.example.com', + headers: { + 'Authorization': 'Bearer YOUR_TOKEN' + } +}); + +// Initialize the API client +const api = new DefaultApi(config); + +// Make API calls +try { + const result = await api.someMethod(); + console.log(result); +} catch (error) { + console.error('API Error:', error); +} +``` + +## Configuration + +The `Configuration` class accepts the following options: + +```typescript +interface ConfigurationParameters { + /** Base URL for API requests */ + basePath?: string; + + /** Custom fetch implementation */ + fetchApi?: typeof fetch; + + /** Request/response middleware */ + middleware?: Middleware[]; + + /** Custom query string serializer */ + queryParamsStringify?: (params: HTTPQuery) => string; + + /** Default headers for all requests */ + headers?: Record; + + /** Credentials mode for requests */ + credentials?: RequestCredentials; +} +``` + +### Example with custom configuration + +```typescript +const config = new Configuration({ + basePath: 'https://api.example.com', + headers: { + 'X-API-Key': 'your-api-key', + 'Content-Type': 'application/json' + }, + credentials: 'include' +}); +``` + +## Middleware + +Add custom middleware to intercept requests and responses: + +```typescript +import { Configuration, Middleware } from 'optional-request-bodies'; + +const loggingMiddleware: Middleware = { + pre: async (context) => { + console.log('Request:', context.url); + return context; + }, + post: async (context) => { + console.log('Response:', context.response.status); + return context.response; + }, + onError: async (context) => { + console.error('Error:', context.error); + return undefined; + } +}; + +const config = new Configuration({ + basePath: 'https://api.example.com', + middleware: [loggingMiddleware] +}); +``` + +## Error Handling + +The client throws typed errors for different failure scenarios: + +```typescript +import { ResponseError, FetchError, RequiredError } from 'optional-request-bodies'; + +try { + const result = await api.someMethod(); +} catch (error) { + if (error instanceof ResponseError) { + // HTTP error response (4xx, 5xx) + console.error('HTTP Error:', error.response.status); + } else if (error instanceof FetchError) { + // Network or fetch error + console.error('Network Error:', error.cause); + } else if (error instanceof RequiredError) { + // Missing required parameter + console.error('Missing field:', error.field); + } +} +``` + +## API Reference + +This package exports the following: + +- **Configuration** - Client configuration class +- **BaseAPI** - Base class for all API clients +- **API Classes** - Generated API client classes (e.g., `UserApi`, `PostApi`) +- **Models** - Generated TypeScript interfaces for request/response types +- **Errors** - `ResponseError`, `FetchError`, `RequiredError` +- **Types** - TypeScript type definitions + +## Development + +### Building + +To build the package: + +```bash +npm install +npm run build +``` + +This will compile TypeScript to JavaScript in the `dist/` directory. + +### Building for ESM + +To build ES modules: + +```bash +npm run build +``` + +## TypeScript Support + +This package includes TypeScript type definitions. No additional `@types` package is needed. + +### TypeScript Configuration + +This package works with standard TypeScript configurations. If you're using a bundler-based setup, you may want to configure: + +```json +{ + "compilerOptions": { + "moduleResolution": "bundler" + } +} +``` + +### Type Imports + +```typescript +import type { User, CreateUserRequest } from 'optional-request-bodies'; + +const user: User = { + id: 1, + name: 'John Doe', + email: 'john@example.com' +}; +``` + +## Browser Support + +This package uses the native Fetch API, which is supported in: + +- Chrome 42+ +- Firefox 39+ +- Safari 10.1+ +- Edge 14+ +- Node.js 18+ (native fetch) +- Node.js <18 (with `node-fetch` polyfill) + +For older browsers, you may need to include a fetch polyfill. + +## License + +This is an auto-generated API client. Please refer to your API documentation for license information. + +## Support + +For issues related to the API itself, please contact the API provider. + +For issues with this generated client, please check the OpenAPI specification used to generate it. + +--- + +**Generated by OpenAPI Generator** + +API Version: 1.0.0 diff --git a/tests/golden/typescript/typescript-fetch/optional-request-bodies/apis/DefaultApi.ts.golden b/tests/golden/typescript/typescript-fetch/optional-request-bodies/apis/DefaultApi.ts.golden new file mode 100644 index 000000000..577386378 --- /dev/null +++ b/tests/golden/typescript/typescript-fetch/optional-request-bodies/apis/DefaultApi.ts.golden @@ -0,0 +1,216 @@ +/** + * @generated by openapi-nexus. Do not edit. + * + * Optional Request Bodies — 1.0.0 + * Covers optional non-multipart request bodies. + */ +import type { Payload } from '../models/Payload'; +import type { Configuration, HTTPQuery, InitOverrideFunction } from '../runtime/runtime'; +import { BaseAPI, DefaultConfig, RequiredError, VoidApiResponse } from '../runtime/runtime'; + +export interface ApiSendOptionalBinaryRequest { + body?: Blob | File; +} + +export interface ApiSendOptionalJsonRequest { + body?: Payload; +} + +export interface ApiSendOptionalTextRequest { + body?: string; +} + +export interface ApiSendRequiredJsonRequest { + body: Payload; +} + +export type SendOptionalBinaryRawResponse = + | VoidApiResponse & { status: 204 } + | VoidApiResponse & { status: number }; + +export type SendOptionalJsonRawResponse = + | VoidApiResponse & { status: 204 } + | VoidApiResponse & { status: number }; + +export type SendOptionalTextRawResponse = + | VoidApiResponse & { status: 204 } + | VoidApiResponse & { status: number }; + +export type SendRequiredJsonRawResponse = + | VoidApiResponse & { status: 204 } + | VoidApiResponse & { status: number }; + + +export interface DefaultApiInterface { + sendOptionalBinaryRaw: (requestParameters: ApiSendOptionalBinaryRequest, initOverrides?: RequestInit | InitOverrideFunction) => Promise; + sendOptionalBinary: (requestParameters: ApiSendOptionalBinaryRequest, initOverrides?: RequestInit | InitOverrideFunction) => Promise; + sendOptionalJsonRaw: (requestParameters: ApiSendOptionalJsonRequest, initOverrides?: RequestInit | InitOverrideFunction) => Promise; + sendOptionalJson: (requestParameters: ApiSendOptionalJsonRequest, initOverrides?: RequestInit | InitOverrideFunction) => Promise; + sendOptionalTextRaw: (requestParameters: ApiSendOptionalTextRequest, initOverrides?: RequestInit | InitOverrideFunction) => Promise; + sendOptionalText: (requestParameters: ApiSendOptionalTextRequest, initOverrides?: RequestInit | InitOverrideFunction) => Promise; + sendRequiredJsonRaw: (requestParameters: ApiSendRequiredJsonRequest, initOverrides?: RequestInit | InitOverrideFunction) => Promise; + sendRequiredJson: (requestParameters: ApiSendRequiredJsonRequest, initOverrides?: RequestInit | InitOverrideFunction) => Promise; +} + +export class DefaultApi extends BaseAPI implements DefaultApiInterface { + /** + * Initialize the API client + */ + constructor(configuration?: Configuration) { + super(configuration ?? DefaultConfig); + } + + async sendOptionalBinaryRaw(requestParameters: ApiSendOptionalBinaryRequest, + initOverrides?: RequestInit | InitOverrideFunction): Promise { + // Build path with path parameters + const urlPath = `/optional-binary`; + // Build query parameters + const queryParameters: HTTPQuery = {}; + // Build headers + const headerParameters: Record = { + 'Content-Type': 'application/octet-stream', + }; + + // Prepare request body + const requestBody = requestParameters.body; + // Make request + const response = await this.request({ + path: urlPath, + method: 'POST', + headers: headerParameters, + query: queryParameters, + body: requestBody, + }, initOverrides); + + // Handle responses + if (response.status === 204) { + return new VoidApiResponse(response) as VoidApiResponse & { status: 204 }; + } + else { + return new VoidApiResponse(response) as VoidApiResponse & { status: number }; + } + } + + async sendOptionalBinary(requestParameters: ApiSendOptionalBinaryRequest, + initOverrides?: RequestInit | InitOverrideFunction): Promise { + const response = await this.sendOptionalBinaryRaw(requestParameters, initOverrides); + return await response.value(); + } + + async sendOptionalJsonRaw(requestParameters: ApiSendOptionalJsonRequest, + initOverrides?: RequestInit | InitOverrideFunction): Promise { + // Build path with path parameters + const urlPath = `/optional-json`; + // Build query parameters + const queryParameters: HTTPQuery = {}; + // Build headers + const headerParameters: Record = { + 'Content-Type': 'application/json', + }; + + // Prepare request body + const requestBody = requestParameters.body; + // Make request + const response = await this.request({ + path: urlPath, + method: 'POST', + headers: headerParameters, + query: queryParameters, + body: requestBody, + }, initOverrides); + + // Handle responses + if (response.status === 204) { + return new VoidApiResponse(response) as VoidApiResponse & { status: 204 }; + } + else { + return new VoidApiResponse(response) as VoidApiResponse & { status: number }; + } + } + + async sendOptionalJson(requestParameters: ApiSendOptionalJsonRequest, initOverrides?: RequestInit + | InitOverrideFunction): Promise { + const response = await this.sendOptionalJsonRaw(requestParameters, initOverrides); + return await response.value(); + } + + async sendOptionalTextRaw(requestParameters: ApiSendOptionalTextRequest, + initOverrides?: RequestInit | InitOverrideFunction): Promise { + // Build path with path parameters + const urlPath = `/optional-text`; + // Build query parameters + const queryParameters: HTTPQuery = {}; + // Build headers + const headerParameters: Record = { + 'Content-Type': 'text/plain', + }; + + // Prepare request body + const requestBody = requestParameters.body; + // Make request + const response = await this.request({ + path: urlPath, + method: 'POST', + headers: headerParameters, + query: queryParameters, + body: requestBody, + }, initOverrides); + + // Handle responses + if (response.status === 204) { + return new VoidApiResponse(response) as VoidApiResponse & { status: 204 }; + } + else { + return new VoidApiResponse(response) as VoidApiResponse & { status: number }; + } + } + + async sendOptionalText(requestParameters: ApiSendOptionalTextRequest, initOverrides?: RequestInit + | InitOverrideFunction): Promise { + const response = await this.sendOptionalTextRaw(requestParameters, initOverrides); + return await response.value(); + } + + async sendRequiredJsonRaw(requestParameters: ApiSendRequiredJsonRequest, + initOverrides?: RequestInit | InitOverrideFunction): Promise { + if (requestParameters.body === undefined || requestParameters.body === null) { + throw new RequiredError( + 'body', + 'Required parameter "body" was null or undefined when calling sendRequiredJsonRaw().' + ); + } + // Build path with path parameters + const urlPath = `/required-json`; + // Build query parameters + const queryParameters: HTTPQuery = {}; + // Build headers + const headerParameters: Record = { + 'Content-Type': 'application/json', + }; + + // Prepare request body + const requestBody = requestParameters.body; + // Make request + const response = await this.request({ + path: urlPath, + method: 'POST', + headers: headerParameters, + query: queryParameters, + body: requestBody, + }, initOverrides); + + // Handle responses + if (response.status === 204) { + return new VoidApiResponse(response) as VoidApiResponse & { status: 204 }; + } + else { + return new VoidApiResponse(response) as VoidApiResponse & { status: number }; + } + } + + async sendRequiredJson(requestParameters: ApiSendRequiredJsonRequest, initOverrides?: RequestInit + | InitOverrideFunction): Promise { + const response = await this.sendRequiredJsonRaw(requestParameters, initOverrides); + return await response.value(); + } +} diff --git a/tests/golden/typescript/typescript-fetch/optional-request-bodies/apis/index.ts.golden b/tests/golden/typescript/typescript-fetch/optional-request-bodies/apis/index.ts.golden new file mode 100644 index 000000000..1ab100155 --- /dev/null +++ b/tests/golden/typescript/typescript-fetch/optional-request-bodies/apis/index.ts.golden @@ -0,0 +1,18 @@ +/** + * @generated by openapi-nexus. Do not edit. + * + * Optional Request Bodies — 1.0.0 + * Covers optional non-multipart request bodies. + */ +export type { + ApiSendOptionalBinaryRequest, + SendOptionalBinaryRawResponse, + ApiSendOptionalJsonRequest, + SendOptionalJsonRawResponse, + ApiSendOptionalTextRequest, + SendOptionalTextRawResponse, + ApiSendRequiredJsonRequest, + SendRequiredJsonRawResponse, + DefaultApiInterface, +} from './DefaultApi'; +export { DefaultApi } from './DefaultApi'; diff --git a/tests/golden/typescript/typescript-fetch/optional-request-bodies/index.ts.golden b/tests/golden/typescript/typescript-fetch/optional-request-bodies/index.ts.golden new file mode 100644 index 000000000..0dcca29a8 --- /dev/null +++ b/tests/golden/typescript/typescript-fetch/optional-request-bodies/index.ts.golden @@ -0,0 +1,9 @@ +/** + * @generated by openapi-nexus. Do not edit. + * + * Optional Request Bodies — 1.0.0 + * Covers optional non-multipart request bodies. + */ +export * from './runtime/runtime'; +export * from './apis'; +export * from './models'; diff --git a/tests/golden/typescript/typescript-fetch/optional-request-bodies/models/Payload.ts.golden b/tests/golden/typescript/typescript-fetch/optional-request-bodies/models/Payload.ts.golden new file mode 100644 index 000000000..95482fe1e --- /dev/null +++ b/tests/golden/typescript/typescript-fetch/optional-request-bodies/models/Payload.ts.golden @@ -0,0 +1,10 @@ +/** + * @generated by openapi-nexus. Do not edit. + * + * Optional Request Bodies — 1.0.0 + * Covers optional non-multipart request bodies. + */ +export interface Payload { + readonly count?: number; + readonly message: string; +} diff --git a/tests/golden/typescript/typescript-fetch/optional-request-bodies/models/index.ts.golden b/tests/golden/typescript/typescript-fetch/optional-request-bodies/models/index.ts.golden new file mode 100644 index 000000000..36e0f5232 --- /dev/null +++ b/tests/golden/typescript/typescript-fetch/optional-request-bodies/models/index.ts.golden @@ -0,0 +1,7 @@ +/** + * @generated by openapi-nexus. Do not edit. + * + * Optional Request Bodies — 1.0.0 + * Covers optional non-multipart request bodies. + */ +export type { Payload } from './Payload'; diff --git a/tests/golden/typescript/typescript-fetch/optional-request-bodies/package.json.golden b/tests/golden/typescript/typescript-fetch/optional-request-bodies/package.json.golden new file mode 100644 index 000000000..55aee9ccc --- /dev/null +++ b/tests/golden/typescript/typescript-fetch/optional-request-bodies/package.json.golden @@ -0,0 +1,26 @@ +{ + "description": "Covers optional non-multipart request bodies.", + "exports": { + ".": { + "default": "./dist/index.js", + "types": "./dist/index.d.ts" + } + }, + "files": [ + "dist" + ], + "keywords": [ + "openapi", + "api-client", + "typescript", + "generated" + ], + "main": "./dist/index.js", + "name": "optional-request-bodies", + "scripts": { + "build": "tsc" + }, + "type": "module", + "types": "./dist/index.d.ts", + "version": "1.0.0" +} \ No newline at end of file diff --git a/tests/golden/typescript/typescript-fetch/optional-request-bodies/runtime/runtime.ts.golden b/tests/golden/typescript/typescript-fetch/optional-request-bodies/runtime/runtime.ts.golden new file mode 100644 index 000000000..3e764044b --- /dev/null +++ b/tests/golden/typescript/typescript-fetch/optional-request-bodies/runtime/runtime.ts.golden @@ -0,0 +1,461 @@ +/** + * @generated by openapi-nexus. Do not edit. + * + * Optional Request Bodies — 1.0.0 + * Covers optional non-multipart request bodies. + */ +export const BASE_PATH = "http://localhost".replace(/\/+$/, ""); + +export interface ConfigurationParameters { + basePath?: string; // override base path + fetchApi?: FetchAPI; // override for fetch implementation + middleware?: Middleware[]; // middleware to apply before/after fetch requests + queryParamsStringify?: (params: HTTPQuery) => string; // stringify function for query strings + username?: string | (() => string | Promise); // parameter for basic security + password?: string | (() => string | Promise); // parameter for basic security + apiKey?: string | Promise | ((name: string) => string | Promise); // parameter for apiKey security + accessToken?: string | Promise | ((name?: string, scopes?: string[]) => string | Promise); // parameter for oauth2 security + headers?: HTTPHeaders; //header params we want to use on every request + credentials?: RequestCredentials; //value for the credentials param we want to use on each request +} + +export class Configuration { + constructor(private configuration: ConfigurationParameters = {}) {} + + set config(configuration: Configuration) { + this.configuration = configuration; + } + + get basePath(): string { + return this.configuration.basePath != null ? this.configuration.basePath : BASE_PATH; + } + + get fetchApi(): FetchAPI | undefined { + return this.configuration.fetchApi; + } + + get middleware(): Middleware[] { + return this.configuration.middleware || []; + } + + get queryParamsStringify(): (params: HTTPQuery) => string { + return this.configuration.queryParamsStringify || querystring; + } + + get username(): (() => string | Promise) | undefined { + const username = this.configuration.username; + if (username) { + return typeof username === 'function' ? username : async () => username; + } + return undefined; + } + + get password(): (() => string | Promise) | undefined { + const password = this.configuration.password; + if (password) { + return typeof password === 'function' ? password : async () => password; + } + return undefined; + } + + get apiKey(): ((name: string) => string | Promise) | undefined { + const apiKey = this.configuration.apiKey; + if (apiKey) { + return typeof apiKey === 'function' ? apiKey : () => apiKey; + } + return undefined; + } + + get accessToken(): ((name?: string, scopes?: string[]) => string | Promise) | undefined { + const accessToken = this.configuration.accessToken; + if (accessToken) { + return typeof accessToken === 'function' ? accessToken : async () => accessToken; + } + return undefined; + } + + get headers(): HTTPHeaders | undefined { + return this.configuration.headers; + } + + get credentials(): RequestCredentials | undefined { + return this.configuration.credentials; + } +} + +export const DefaultConfig = new Configuration(); + +/** + * This is the base class for all generated API classes. + */ +export class BaseAPI { + + private static readonly jsonRegex = new RegExp('^(:?application/json|[^;/ \t]+/[^;/ \t]+[+]json)[ \t]*(:?;.*)?$', 'i'); + private middleware: Middleware[]; + + constructor(protected configuration = DefaultConfig) { + this.middleware = configuration.middleware; + } + + withMiddleware(this: T, ...middlewares: Middleware[]) { + const next = this.clone(); + next.middleware = next.middleware.concat(...middlewares); + return next; + } + + withPreMiddleware(this: T, ...preMiddlewares: Array) { + const middlewares = preMiddlewares.map((pre) => ({ pre })); + return this.withMiddleware(...middlewares); + } + + withPostMiddleware(this: T, ...postMiddlewares: Array) { + const middlewares = postMiddlewares.map((post) => ({ post })); + return this.withMiddleware(...middlewares); + } + + /** + * Check if the given MIME is a JSON MIME. + * JSON MIME examples: + * application/json + * application/json; charset=UTF8 + * APPLICATION/JSON + * application/vnd.company+json + * @param mime - MIME (Multipurpose Internet Mail Extensions) + * @return True if the given MIME is JSON, false otherwise. + */ + protected isJsonMime(mime: string | null | undefined): boolean { + if (!mime) { + return false; + } + return BaseAPI.jsonRegex.test(mime); + } + + protected async request(context: RequestOpts, initOverrides?: RequestInit | InitOverrideFunction): Promise { + const { url, init } = await this.createFetchParams(context, initOverrides); + const response = await this.fetchApi(url, init); + if (response && (response.status >= 200 && response.status < 300)) { + return response; + } + throw new ResponseError(response, 'Response returned an error code'); + } + + private async createFetchParams(context: RequestOpts, initOverrides?: RequestInit | InitOverrideFunction) { + let url = this.configuration.basePath + context.path; + if (context.query !== undefined && Object.keys(context.query).length !== 0) { + // only add the querystring to the URL if there are query parameters. + // this is done to avoid urls ending with a "?" character which buggy webservers + // do not handle correctly sometimes. + url += '?' + this.configuration.queryParamsStringify(context.query); + } + + const headers = Object.assign({}, this.configuration.headers, context.headers); + Object.keys(headers).forEach(key => headers[key] === undefined ? delete headers[key] : {}); + + const token = await this.configuration.accessToken?.(); + if (token) { + headers['Authorization'] = `Bearer ${token}`; + } + + if (!headers['Authorization']) { + const username = await this.configuration.username?.(); + const password = await this.configuration.password?.(); + if (username && password) { + headers['Authorization'] = `Basic ${btoa(`${username}:${password}`)}`; + } + } + + const initOverrideFn = + typeof initOverrides === "function" + ? initOverrides + : async () => initOverrides; + + const initParams: HTTPRequestInit = { + method: context.method, + headers, + body: context.body, + credentials: this.configuration.credentials, + signal: context.signal, + }; + + const overriddenInit = { + ...initParams, + ...(await initOverrideFn({ + init: initParams, + context, + })) + }; + + let body: BodyInit | null | undefined; + if (isFormData(overriddenInit.body) + || (overriddenInit.body instanceof URLSearchParams) + || isBlob(overriddenInit.body)) { + body = overriddenInit.body; + } else if (this.isJsonMime(headers['Content-Type'])) { + body = JSON.stringify(overriddenInit.body); + } else { + body = overriddenInit.body as BodyInit | null | undefined; + } + + const init: RequestInit = { + ...overriddenInit, + body + }; + + return { url, init }; + } + + private fetchApi = async (url: string, init: RequestInit) => { + let fetchParams = { url, init }; + for (const middleware of this.middleware) { + if (middleware.pre) { + fetchParams = await middleware.pre({ + fetch: this.fetchApi, + ...fetchParams, + }) || fetchParams; + } + } + let response: Response | undefined = undefined; + try { + response = await (this.configuration.fetchApi || fetch)(fetchParams.url, fetchParams.init); + } catch (e) { + for (const middleware of this.middleware) { + if (middleware.onError) { + response = await middleware.onError({ + fetch: this.fetchApi, + url: fetchParams.url, + init: fetchParams.init, + error: e, + response: response ? response.clone() : undefined, + }) || response; + } + } + if (response === undefined) { + if (e instanceof Error) { + throw new FetchError(e, 'The request failed and the interceptors did not return an alternative response'); + } else { + throw e; + } + } + } + for (const middleware of this.middleware) { + if (middleware.post) { + response = await middleware.post({ + fetch: this.fetchApi, + url: fetchParams.url, + init: fetchParams.init, + response: response.clone(), + }) || response; + } + } + return response; + } + + /** + * Create a shallow clone of `this` by constructing a new instance + * and then shallow cloning data members. + */ + private clone(this: T): T { + const constructor = this.constructor as new (configuration: Configuration) => T; + const next = new constructor(this.configuration); + next.middleware = this.middleware.slice(); + return next; + } +} + +function isBlob(value: unknown): value is Blob { + return typeof Blob !== 'undefined' && value instanceof Blob; +} + +function isFormData(value: unknown): value is FormData { + return typeof FormData !== "undefined" && value instanceof FormData; +} + +export class ResponseError extends Error { + override name = "ResponseError" as const; + constructor(public response: Response, msg?: string) { + super(msg); + } +} + +export class FetchError extends Error { + override name = "FetchError" as const; + constructor(public cause: Error, msg?: string) { + super(msg); + } +} + +export class RequiredError extends Error { + override name = "RequiredError" as const; + constructor(public field: string, msg?: string) { + super(msg); + } +} + +export type FetchAPI = WindowOrWorkerGlobalScope['fetch']; + +export type Json = unknown; +export type HTTPMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'OPTIONS' | 'HEAD'; +export type HTTPHeaders = { [key: string]: string }; +export type HTTPQuery = { [key: string]: string | number | null | boolean | Array | Set | HTTPQuery }; +export type HTTPBody = Json | Blob | FormData | URLSearchParams; // eslint-disable-line @typescript-eslint/no-redundant-type-constituents +export type HTTPRequestInit = { headers?: HTTPHeaders; method: HTTPMethod; credentials?: RequestCredentials; body?: HTTPBody; signal?: AbortSignal }; +export type ModelPropertyNaming = 'camelCase' | 'snake_case' | 'PascalCase' | 'original'; + +export type InitOverrideFunction = (requestContext: { init: HTTPRequestInit, context: RequestOpts }) => Promise + +export interface FetchParams { + url: string; + init: RequestInit; +} + +export interface RequestOpts { + path: string; + method: HTTPMethod; + headers: HTTPHeaders; + query?: HTTPQuery; + body?: HTTPBody; + signal?: AbortSignal; +} + +export function querystring(params: HTTPQuery, prefix: string = ''): string { + return Object.keys(params) + .map(key => querystringSingleKey(key, params[key], prefix)) + .filter(part => part.length > 0) + .join('&'); +} + +function querystringSingleKey(key: string, value: string | number | null | undefined | boolean | Array | Set | HTTPQuery, keyPrefix: string = ''): string { + const fullKey = keyPrefix + (keyPrefix.length ? `[${key}]` : key); + if (value instanceof Array) { + const multiValue = value.map(singleValue => encodeURIComponent(String(singleValue))) + .join(`&${encodeURIComponent(fullKey)}=`); + return `${encodeURIComponent(fullKey)}=${multiValue}`; + } + if (value instanceof Set) { + const valueAsArray = Array.from(value); + return querystringSingleKey(key, valueAsArray, keyPrefix); + } + if (value instanceof Date) { + return `${encodeURIComponent(fullKey)}=${encodeURIComponent(value.toISOString())}`; + } + if (value instanceof Object) { + return querystring(value as HTTPQuery, fullKey); + } + return `${encodeURIComponent(fullKey)}=${encodeURIComponent(String(value))}`; +} + +export function exists(json: Record, key: string): boolean { + const value = json[key]; + return value !== null && value !== undefined; +} + +export function mapValues(data: Record, fn: (item: T) => U): Record { + const result: Record = {}; + for (const key of Object.keys(data)) { + result[key] = fn(data[key]); + } + return result; +} + +export function canConsumeForm(consumes: Consume[]): boolean { + for (const consume of consumes) { + if ('multipart/form-data' === consume.contentType) { + return true; + } + } + return false; +} + +export interface Consume { + contentType: string; +} + +export interface RequestContext { + fetch: FetchAPI; + url: string; + init: RequestInit; +} + +export interface ResponseContext { + fetch: FetchAPI; + url: string; + init: RequestInit; + response: Response; +} + +export interface ErrorContext { + fetch: FetchAPI; + url: string; + init: RequestInit; + error: unknown; + response?: Response; +} + +export interface Middleware { + pre?(context: RequestContext): Promise; + post?(context: ResponseContext): Promise; + onError?(context: ErrorContext): Promise; +} + +export interface ApiResponse { + raw: Response; + value(): Promise; +} + +export interface ResponseTransformer { + (json: unknown): T; +} + +class ApiResponseBase { + public readonly status: number; + public readonly ok: boolean; + public readonly statusText: string; + public readonly headers: Headers; + + constructor(public readonly raw: Response) { + this.status = raw.status; + this.ok = raw.ok; + this.statusText = raw.statusText; + this.headers = raw.headers; + } +} + +export class JSONApiResponse extends ApiResponseBase { + constructor(raw: Response, private transformer: ResponseTransformer = (jsonValue: unknown) => jsonValue as T) { + super(raw); + } + + async value(): Promise { + return this.transformer(await this.raw.json()); + } +} + +export class VoidApiResponse extends ApiResponseBase { + constructor(raw: Response) { + super(raw); + } + + async value(): Promise { + return undefined; + } +} + +export class BlobApiResponse extends ApiResponseBase { + constructor(raw: Response) { + super(raw); + } + + async value(): Promise { + return await this.raw.blob(); + }; +} + +export class TextApiResponse extends ApiResponseBase { + constructor(raw: Response) { + super(raw); + } + + async value(): Promise { + return await this.raw.text(); + }; +} diff --git a/tests/golden/typescript/typescript-fetch/optional-request-bodies/tsconfig.esm.json.golden b/tests/golden/typescript/typescript-fetch/optional-request-bodies/tsconfig.esm.json.golden new file mode 100644 index 000000000..bb8350cb6 --- /dev/null +++ b/tests/golden/typescript/typescript-fetch/optional-request-bodies/tsconfig.esm.json.golden @@ -0,0 +1,7 @@ +{ + "compilerOptions": { + "module": "ES2020", + "outDir": "dist/esm" + }, + "extends": "./tsconfig.json" +} \ No newline at end of file diff --git a/tests/golden/typescript/typescript-fetch/optional-request-bodies/tsconfig.json.golden b/tests/golden/typescript/typescript-fetch/optional-request-bodies/tsconfig.json.golden new file mode 100644 index 000000000..4b3a8cf7f --- /dev/null +++ b/tests/golden/typescript/typescript-fetch/optional-request-bodies/tsconfig.json.golden @@ -0,0 +1,31 @@ +{ + "compilerOptions": { + "declaration": true, + "declarationMap": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "lib": [ + "ES2020", + "DOM" + ], + "module": "ES2020", + "moduleResolution": "bundler", + "outDir": "./dist", + "resolveJsonModule": true, + "rootDir": "./", + "skipLibCheck": true, + "sourceMap": true, + "strict": true, + "target": "ES2020", + "typeRoots": [ + "node_modules/@types" + ] + }, + "exclude": [ + "dist", + "node_modules" + ], + "include": [ + "**/*.ts" + ] +} \ No newline at end of file diff --git a/tests/golden_tests_go_http.rs b/tests/golden_tests_go_http.rs index 5ed8808ca..8c7b5111d 100644 --- a/tests/golden_tests_go_http.rs +++ b/tests/golden_tests_go_http.rs @@ -31,6 +31,7 @@ fn get_golden_test_cases() -> HashMap<&'static str, &'static str> { ("multiple-similar-request-schemas", "valid/multiple-similar-request-schemas.yaml"), ("naming-conventions", "valid/naming-conventions.yaml"), ("request-body-content-types", "valid/request-body-content-types.yaml"), + ("optional-request-bodies", "valid/optional-request-bodies.yaml"), ("binary-transfer-media-types", "valid/binary-transfer-media-types.yaml"), ("media-type-selection", "valid/media-type-selection.yaml"), ("multipart-explicit-encoding", "valid/multipart-explicit-encoding.yaml"), @@ -116,6 +117,7 @@ generate_golden_tests! { test_multiple_similar_request_schemas_golden: "multiple-similar-request-schemas", test_naming_conventions_golden: "naming-conventions", test_request_body_content_types_golden: "request-body-content-types", + test_optional_request_bodies_golden: "optional-request-bodies", test_binary_transfer_media_types_golden: "binary-transfer-media-types", test_media_type_selection_golden: "media-type-selection", test_multipart_explicit_encoding_golden: "multipart-explicit-encoding", diff --git a/tests/golden_tests_java_okhttp.rs b/tests/golden_tests_java_okhttp.rs index ba7ad74c3..089f14980 100644 --- a/tests/golden_tests_java_okhttp.rs +++ b/tests/golden_tests_java_okhttp.rs @@ -31,6 +31,7 @@ fn get_golden_test_cases() -> HashMap<&'static str, &'static str> { ("multiple-similar-request-schemas", "valid/multiple-similar-request-schemas.yaml"), ("naming-conventions", "valid/naming-conventions.yaml"), ("request-body-content-types", "valid/request-body-content-types.yaml"), + ("optional-request-bodies", "valid/optional-request-bodies.yaml"), ("binary-transfer-media-types", "valid/binary-transfer-media-types.yaml"), ("media-type-selection", "valid/media-type-selection.yaml"), ("multipart-explicit-encoding", "valid/multipart-explicit-encoding.yaml"), @@ -117,6 +118,7 @@ generate_golden_tests! { test_multiple_similar_request_schemas_golden: "multiple-similar-request-schemas", test_naming_conventions_golden: "naming-conventions", test_request_body_content_types_golden: "request-body-content-types", + test_optional_request_bodies_golden: "optional-request-bodies", test_binary_transfer_media_types_golden: "binary-transfer-media-types", test_media_type_selection_golden: "media-type-selection", test_multipart_explicit_encoding_golden: "multipart-explicit-encoding", diff --git a/tests/golden_tests_kotlin_okhttp.rs b/tests/golden_tests_kotlin_okhttp.rs index 8cfff7713..495c71dce 100644 --- a/tests/golden_tests_kotlin_okhttp.rs +++ b/tests/golden_tests_kotlin_okhttp.rs @@ -31,6 +31,7 @@ fn get_golden_test_cases() -> HashMap<&'static str, &'static str> { ("multiple-similar-request-schemas", "valid/multiple-similar-request-schemas.yaml"), ("naming-conventions", "valid/naming-conventions.yaml"), ("request-body-content-types", "valid/request-body-content-types.yaml"), + ("optional-request-bodies", "valid/optional-request-bodies.yaml"), ("binary-transfer-media-types", "valid/binary-transfer-media-types.yaml"), ("media-type-selection", "valid/media-type-selection.yaml"), ("multipart-explicit-encoding", "valid/multipart-explicit-encoding.yaml"), @@ -117,6 +118,7 @@ generate_golden_tests! { test_multiple_similar_request_schemas_golden: "multiple-similar-request-schemas", test_naming_conventions_golden: "naming-conventions", test_request_body_content_types_golden: "request-body-content-types", + test_optional_request_bodies_golden: "optional-request-bodies", test_binary_transfer_media_types_golden: "binary-transfer-media-types", test_media_type_selection_golden: "media-type-selection", test_multipart_explicit_encoding_golden: "multipart-explicit-encoding", diff --git a/tests/golden_tests_python_httpx.rs b/tests/golden_tests_python_httpx.rs index a26221b7f..6912ccb8a 100644 --- a/tests/golden_tests_python_httpx.rs +++ b/tests/golden_tests_python_httpx.rs @@ -31,6 +31,7 @@ fn get_golden_test_cases() -> HashMap<&'static str, &'static str> { ("multiple-similar-request-schemas", "valid/multiple-similar-request-schemas.yaml"), ("naming-conventions", "valid/naming-conventions.yaml"), ("request-body-content-types", "valid/request-body-content-types.yaml"), + ("optional-request-bodies", "valid/optional-request-bodies.yaml"), ("binary-transfer-media-types", "valid/binary-transfer-media-types.yaml"), ("media-type-selection", "valid/media-type-selection.yaml"), ("multipart-explicit-encoding", "valid/multipart-explicit-encoding.yaml"), @@ -116,6 +117,7 @@ generate_golden_tests! { test_multiple_similar_request_schemas_golden: "multiple-similar-request-schemas", test_naming_conventions_golden: "naming-conventions", test_request_body_content_types_golden: "request-body-content-types", + test_optional_request_bodies_golden: "optional-request-bodies", test_binary_transfer_media_types_golden: "binary-transfer-media-types", test_media_type_selection_golden: "media-type-selection", test_multipart_explicit_encoding_golden: "multipart-explicit-encoding", diff --git a/tests/golden_tests_python_requests.rs b/tests/golden_tests_python_requests.rs index feb7dcc66..c53e9e158 100644 --- a/tests/golden_tests_python_requests.rs +++ b/tests/golden_tests_python_requests.rs @@ -31,6 +31,7 @@ fn get_golden_test_cases() -> HashMap<&'static str, &'static str> { ("multiple-similar-request-schemas", "valid/multiple-similar-request-schemas.yaml"), ("naming-conventions", "valid/naming-conventions.yaml"), ("request-body-content-types", "valid/request-body-content-types.yaml"), + ("optional-request-bodies", "valid/optional-request-bodies.yaml"), ("binary-transfer-media-types", "valid/binary-transfer-media-types.yaml"), ("media-type-selection", "valid/media-type-selection.yaml"), ("multipart-explicit-encoding", "valid/multipart-explicit-encoding.yaml"), @@ -116,6 +117,7 @@ generate_golden_tests! { test_multiple_similar_request_schemas_golden: "multiple-similar-request-schemas", test_naming_conventions_golden: "naming-conventions", test_request_body_content_types_golden: "request-body-content-types", + test_optional_request_bodies_golden: "optional-request-bodies", test_binary_transfer_media_types_golden: "binary-transfer-media-types", test_media_type_selection_golden: "media-type-selection", test_multipart_explicit_encoding_golden: "multipart-explicit-encoding", diff --git a/tests/golden_tests_rust_aioduct.rs b/tests/golden_tests_rust_aioduct.rs index 2086e36ca..59206fafc 100644 --- a/tests/golden_tests_rust_aioduct.rs +++ b/tests/golden_tests_rust_aioduct.rs @@ -33,6 +33,7 @@ fn get_golden_test_cases() -> HashMap<&'static str, &'static str> { ("interface-with-enum-reference", "valid/interface-with-enum-reference.yaml"), ("multiple-similar-request-schemas", "valid/multiple-similar-request-schemas.yaml"), ("request-body-content-types", "valid/request-body-content-types.yaml"), + ("optional-request-bodies", "valid/optional-request-bodies.yaml"), ("binary-transfer-media-types", "valid/binary-transfer-media-types.yaml"), ("media-type-selection", "valid/media-type-selection.yaml"), ("multipart-explicit-encoding", "valid/multipart-explicit-encoding.yaml"), @@ -118,6 +119,7 @@ generate_golden_tests! { test_interface_with_enum_reference_golden: "interface-with-enum-reference", test_multiple_similar_request_schemas_golden: "multiple-similar-request-schemas", test_request_body_content_types_golden: "request-body-content-types", + test_optional_request_bodies_golden: "optional-request-bodies", test_binary_transfer_media_types_golden: "binary-transfer-media-types", test_media_type_selection_golden: "media-type-selection", test_multipart_explicit_encoding_golden: "multipart-explicit-encoding", diff --git a/tests/golden_tests_rust_reqwest.rs b/tests/golden_tests_rust_reqwest.rs index 3228a85b7..cda5d8cb9 100644 --- a/tests/golden_tests_rust_reqwest.rs +++ b/tests/golden_tests_rust_reqwest.rs @@ -33,6 +33,7 @@ fn get_golden_test_cases() -> HashMap<&'static str, &'static str> { ("interface-with-enum-reference", "valid/interface-with-enum-reference.yaml"), ("multiple-similar-request-schemas", "valid/multiple-similar-request-schemas.yaml"), ("request-body-content-types", "valid/request-body-content-types.yaml"), + ("optional-request-bodies", "valid/optional-request-bodies.yaml"), ("binary-transfer-media-types", "valid/binary-transfer-media-types.yaml"), ("media-type-selection", "valid/media-type-selection.yaml"), ("multipart-explicit-encoding", "valid/multipart-explicit-encoding.yaml"), @@ -118,6 +119,7 @@ generate_golden_tests! { test_interface_with_enum_reference_golden: "interface-with-enum-reference", test_multiple_similar_request_schemas_golden: "multiple-similar-request-schemas", test_request_body_content_types_golden: "request-body-content-types", + test_optional_request_bodies_golden: "optional-request-bodies", test_binary_transfer_media_types_golden: "binary-transfer-media-types", test_media_type_selection_golden: "media-type-selection", test_multipart_explicit_encoding_golden: "multipart-explicit-encoding", diff --git a/tests/golden_tests_rust_ureq.rs b/tests/golden_tests_rust_ureq.rs index 72d5b0a4c..8979392c7 100644 --- a/tests/golden_tests_rust_ureq.rs +++ b/tests/golden_tests_rust_ureq.rs @@ -33,6 +33,7 @@ fn get_golden_test_cases() -> HashMap<&'static str, &'static str> { ("interface-with-enum-reference", "valid/interface-with-enum-reference.yaml"), ("multiple-similar-request-schemas", "valid/multiple-similar-request-schemas.yaml"), ("request-body-content-types", "valid/request-body-content-types.yaml"), + ("optional-request-bodies", "valid/optional-request-bodies.yaml"), ("binary-transfer-media-types", "valid/binary-transfer-media-types.yaml"), ("media-type-selection", "valid/media-type-selection.yaml"), ("multipart-explicit-encoding", "valid/multipart-explicit-encoding.yaml"), @@ -118,6 +119,7 @@ generate_golden_tests! { test_interface_with_enum_reference_golden: "interface-with-enum-reference", test_multiple_similar_request_schemas_golden: "multiple-similar-request-schemas", test_request_body_content_types_golden: "request-body-content-types", + test_optional_request_bodies_golden: "optional-request-bodies", test_binary_transfer_media_types_golden: "binary-transfer-media-types", test_media_type_selection_golden: "media-type-selection", test_multipart_explicit_encoding_golden: "multipart-explicit-encoding", diff --git a/tests/golden_tests_typescript_fetch.rs b/tests/golden_tests_typescript_fetch.rs index 55d32bb88..3ac1c3b6b 100644 --- a/tests/golden_tests_typescript_fetch.rs +++ b/tests/golden_tests_typescript_fetch.rs @@ -31,6 +31,7 @@ fn get_golden_test_cases() -> HashMap<&'static str, &'static str> { ("multiple-similar-request-schemas", "valid/multiple-similar-request-schemas.yaml"), ("naming-conventions", "valid/naming-conventions.yaml"), ("request-body-content-types", "valid/request-body-content-types.yaml"), + ("optional-request-bodies", "valid/optional-request-bodies.yaml"), ("binary-transfer-media-types", "valid/binary-transfer-media-types.yaml"), ("media-type-selection", "valid/media-type-selection.yaml"), ("multipart-explicit-encoding", "valid/multipart-explicit-encoding.yaml"), @@ -116,6 +117,7 @@ generate_golden_tests! { test_multiple_similar_request_schemas_golden: "multiple-similar-request-schemas", test_naming_conventions_golden: "naming-conventions", test_request_body_content_types_golden: "request-body-content-types", + test_optional_request_bodies_golden: "optional-request-bodies", test_binary_transfer_media_types_golden: "binary-transfer-media-types", test_media_type_selection_golden: "media-type-selection", test_multipart_explicit_encoding_golden: "multipart-explicit-encoding",