From 92617cf9b73e617b95b1f4f9b6da1305f68f1d2c Mon Sep 17 00:00:00 2001 From: Adam Basfop Cavendish Date: Mon, 15 Jun 2026 13:31:35 +0800 Subject: [PATCH] fix: align multipart part content types across clients IR: - Preserve OpenAPI multipart encoding contentType in request bodies - Share multipart part planning across generator backends - Reject non-JSON complex part encodings consistently Clients: - Emit per-part Content-Type for TypeScript, Go, Python, OkHttp, and Rust - Use controllable TypeScript Blob multipart bodies and backend-native part builders - Keep public SDK method signatures unchanged Tests: - Add explicit multipart encoding fixture across all golden suites - Regenerate multipart goldens with default and explicit part content types - Verify golden checks and generated-client build checks --- src/generators/go/http/sigil_emit_api.rs | 157 +++--- src/generators/java/okhttp/sigil_emit_api.rs | 132 ++--- .../kotlin/okhttp/sigil_emit_api.rs | 132 ++--- src/generators/mod.rs | 1 + src/generators/multipart.rs | 132 +++++ src/generators/python/httpx/emit_api.rs | 102 ++-- src/generators/python/requests/emit_api.rs | 102 ++-- src/generators/rust/aioduct/sigil_emit_api.rs | 30 +- src/generators/rust/common/emit_api.rs | 101 +--- src/generators/rust/reqwest/sigil_emit_api.rs | 30 +- src/generators/rust/ureq/sigil_emit_api.rs | 37 +- .../typescript/fetch/sigil_emit_api.rs | 209 ++++---- src/ir/lower/v30.rs | 22 +- src/ir/lower/v31.rs | 22 +- src/ir/lower/v32.rs | 22 +- src/ir/types/mod.rs | 4 +- src/ir/types/operation.rs | 8 + .../valid/multipart-explicit-encoding.yaml | 62 +++ .../apis/transfer.go.golden | 32 +- .../media-type-selection/apis/media.go.golden | 21 +- .../apis/multipart.go.golden | 87 +++- .../README.md.golden | 7 + .../apis/transfer.go.golden | 128 +++++ .../multipart-explicit-encoding/go.mod.golden | 3 + .../models/audit_attributes.go.golden | 11 + .../models/upload_attributes.go.golden | 11 + .../upload_encoded_asset_request.go.golden | 15 + .../runtime/auth.go.golden | 113 +++++ .../runtime/client.go.golden | 99 ++++ .../runtime/errors.go.golden | 31 ++ .../apis/multipart.go.golden | 21 +- .../apis/TransferApi.java.golden | 4 +- .../apis/MediaApi.java.golden | 2 +- .../apis/MultipartApi.java.golden | 14 +- .../README.md.golden | 7 + .../apis/TransferApi.java.golden | 79 +++ .../build.gradle.golden | 21 + .../models/AuditAttributes.java.golden | 27 + .../models/UploadAttributes.java.golden | 27 + .../UploadEncodedAssetRequest.java.golden | 37 ++ .../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 ++ .../apis/MultipartApi.java.golden | 2 +- .../apis/TransferApi.kt.golden | 7 +- .../apis/MediaApi.kt.golden | 4 +- .../apis/MultipartApi.kt.golden | 22 +- .../README.md.golden | 7 + .../apis/TransferApi.kt.golden | 54 ++ .../build.gradle.kts.golden | 19 + .../models/AuditAttributes.kt.golden | 12 + .../models/UploadAttributes.kt.golden | 12 + .../UploadEncodedAssetRequest.kt.golden | 10 + .../runtime/ApiClient.kt.golden | 62 +++ .../runtime/ApiException.kt.golden | 12 + .../runtime/Auth.kt.golden | 71 +++ .../apis/MultipartApi.kt.golden | 4 +- .../apis/transfer_api.py.golden | 2 +- .../apis/media_api.py.golden | 2 +- .../apis/multipart_api.py.golden | 12 +- .../README.md.golden | 7 + .../__init__.py.golden | 10 + .../apis/__init__.py.golden | 6 + .../apis/transfer_api.py.golden | 28 ++ .../models/__init__.py.golden | 8 + .../models/audit_attributes.py.golden | 27 + .../models/upload_attributes.py.golden | 27 + .../upload_encoded_asset_request.py.golden | 35 ++ .../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 + .../apis/transfer_api.py.golden | 2 +- .../apis/media_api.py.golden | 2 +- .../apis/multipart_api.py.golden | 12 +- .../README.md.golden | 7 + .../__init__.py.golden | 10 + .../apis/__init__.py.golden | 6 + .../apis/transfer_api.py.golden | 28 ++ .../models/__init__.py.golden | 8 + .../models/audit_attributes.py.golden | 27 + .../models/upload_attributes.py.golden | 27 + .../upload_encoded_asset_request.py.golden | 35 ++ .../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/transfer.rs.golden | 4 +- .../src/apis/media.rs.golden | 2 +- .../src/apis/multipart.rs.golden | 14 +- .../Cargo.toml.golden | 12 + .../README.md.golden | 7 + .../src/apis/mod.rs.golden | 8 + .../src/apis/transfer.rs.golden | 46 ++ .../src/lib.rs.golden | 11 + .../src/models/audit_attributes.rs.golden | 14 + .../src/models/mod.rs.golden | 12 + .../src/models/upload_attributes.rs.golden | 14 + .../upload_encoded_asset_request.rs.golden | 17 + .../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 | 2 +- .../src/apis/transfer.rs.golden | 4 +- .../src/apis/media.rs.golden | 2 +- .../src/apis/multipart.rs.golden | 14 +- .../Cargo.toml.golden | 12 + .../README.md.golden | 7 + .../src/apis/mod.rs.golden | 8 + .../src/apis/transfer.rs.golden | 46 ++ .../src/lib.rs.golden | 11 + .../src/models/audit_attributes.rs.golden | 14 + .../src/models/mod.rs.golden | 12 + .../src/models/upload_attributes.rs.golden | 14 + .../upload_encoded_asset_request.rs.golden | 17 + .../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 | 2 +- .../src/apis/transfer.rs.golden | 6 +- .../src/apis/media.rs.golden | 4 +- .../src/apis/multipart.rs.golden | 16 +- .../Cargo.toml.golden | 12 + .../README.md.golden | 7 + .../src/apis/mod.rs.golden | 8 + .../src/apis/transfer.rs.golden | 60 +++ .../src/lib.rs.golden | 11 + .../src/models/audit_attributes.rs.golden | 14 + .../src/models/mod.rs.golden | 12 + .../src/models/upload_attributes.rs.golden | 14 + .../upload_encoded_asset_request.rs.golden | 17 + .../src/runtime/auth.rs.golden | 169 +++++++ .../src/runtime/client.rs.golden | 94 ++++ .../src/runtime/error.rs.golden | 64 +++ .../src/runtime/mod.rs.golden | 9 + .../src/apis/multipart.rs.golden | 4 +- .../apis/TransferApi.ts.golden | 18 +- .../apis/MediaApi.ts.golden | 14 +- .../apis/MultipartApi.ts.golden | 46 +- .../README.md.golden | 248 ++++++++++ .../apis/TransferApi.ts.golden | 90 ++++ .../apis/index.ts.golden | 12 + .../index.ts.golden | 9 + .../models/AuditAttributes.ts.golden | 10 + .../models/UploadAttributes.ts.golden | 10 + .../UploadEncodedAssetRequest.ts.golden | 21 + .../models/index.ts.golden | 9 + .../package.json.golden | 26 + .../runtime/runtime.ts.golden | 461 ++++++++++++++++++ .../tsconfig.esm.json.golden | 7 + .../tsconfig.json.golden | 31 ++ .../apis/MultipartApi.ts.golden | 14 +- .../apis/MultipartApi.ts.golden | 14 +- 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 + tests/multipart_runtime_smoke.rs | 17 +- 172 files changed, 5173 insertions(+), 727 deletions(-) create mode 100644 src/generators/multipart.rs create mode 100644 tests/fixtures/valid/multipart-explicit-encoding.yaml create mode 100644 tests/golden/go/go-http/multipart-explicit-encoding/README.md.golden create mode 100644 tests/golden/go/go-http/multipart-explicit-encoding/apis/transfer.go.golden create mode 100644 tests/golden/go/go-http/multipart-explicit-encoding/go.mod.golden create mode 100644 tests/golden/go/go-http/multipart-explicit-encoding/models/audit_attributes.go.golden create mode 100644 tests/golden/go/go-http/multipart-explicit-encoding/models/upload_attributes.go.golden create mode 100644 tests/golden/go/go-http/multipart-explicit-encoding/models/upload_encoded_asset_request.go.golden create mode 100644 tests/golden/go/go-http/multipart-explicit-encoding/runtime/auth.go.golden create mode 100644 tests/golden/go/go-http/multipart-explicit-encoding/runtime/client.go.golden create mode 100644 tests/golden/go/go-http/multipart-explicit-encoding/runtime/errors.go.golden create mode 100644 tests/golden/java/java-okhttp/multipart-explicit-encoding/README.md.golden create mode 100644 tests/golden/java/java-okhttp/multipart-explicit-encoding/apis/TransferApi.java.golden create mode 100644 tests/golden/java/java-okhttp/multipart-explicit-encoding/build.gradle.golden create mode 100644 tests/golden/java/java-okhttp/multipart-explicit-encoding/models/AuditAttributes.java.golden create mode 100644 tests/golden/java/java-okhttp/multipart-explicit-encoding/models/UploadAttributes.java.golden create mode 100644 tests/golden/java/java-okhttp/multipart-explicit-encoding/models/UploadEncodedAssetRequest.java.golden create mode 100644 tests/golden/java/java-okhttp/multipart-explicit-encoding/runtime/ApiClient.java.golden create mode 100644 tests/golden/java/java-okhttp/multipart-explicit-encoding/runtime/ApiException.java.golden create mode 100644 tests/golden/java/java-okhttp/multipart-explicit-encoding/runtime/ApiKeyAuth.java.golden create mode 100644 tests/golden/java/java-okhttp/multipart-explicit-encoding/runtime/ApiKeyLocation.java.golden create mode 100644 tests/golden/java/java-okhttp/multipart-explicit-encoding/runtime/Authenticator.java.golden create mode 100644 tests/golden/java/java-okhttp/multipart-explicit-encoding/runtime/BearerAuth.java.golden create mode 100644 tests/golden/kotlin/kotlin-okhttp/multipart-explicit-encoding/README.md.golden create mode 100644 tests/golden/kotlin/kotlin-okhttp/multipart-explicit-encoding/apis/TransferApi.kt.golden create mode 100644 tests/golden/kotlin/kotlin-okhttp/multipart-explicit-encoding/build.gradle.kts.golden create mode 100644 tests/golden/kotlin/kotlin-okhttp/multipart-explicit-encoding/models/AuditAttributes.kt.golden create mode 100644 tests/golden/kotlin/kotlin-okhttp/multipart-explicit-encoding/models/UploadAttributes.kt.golden create mode 100644 tests/golden/kotlin/kotlin-okhttp/multipart-explicit-encoding/models/UploadEncodedAssetRequest.kt.golden create mode 100644 tests/golden/kotlin/kotlin-okhttp/multipart-explicit-encoding/runtime/ApiClient.kt.golden create mode 100644 tests/golden/kotlin/kotlin-okhttp/multipart-explicit-encoding/runtime/ApiException.kt.golden create mode 100644 tests/golden/kotlin/kotlin-okhttp/multipart-explicit-encoding/runtime/Auth.kt.golden create mode 100644 tests/golden/python/python-httpx/multipart-explicit-encoding/README.md.golden create mode 100644 tests/golden/python/python-httpx/multipart-explicit-encoding/multipart_explicit_encoding/__init__.py.golden create mode 100644 tests/golden/python/python-httpx/multipart-explicit-encoding/multipart_explicit_encoding/apis/__init__.py.golden create mode 100644 tests/golden/python/python-httpx/multipart-explicit-encoding/multipart_explicit_encoding/apis/transfer_api.py.golden create mode 100644 tests/golden/python/python-httpx/multipart-explicit-encoding/multipart_explicit_encoding/models/__init__.py.golden create mode 100644 tests/golden/python/python-httpx/multipart-explicit-encoding/multipart_explicit_encoding/models/audit_attributes.py.golden create mode 100644 tests/golden/python/python-httpx/multipart-explicit-encoding/multipart_explicit_encoding/models/upload_attributes.py.golden create mode 100644 tests/golden/python/python-httpx/multipart-explicit-encoding/multipart_explicit_encoding/models/upload_encoded_asset_request.py.golden create mode 100644 tests/golden/python/python-httpx/multipart-explicit-encoding/multipart_explicit_encoding/py.typed.golden create mode 100644 tests/golden/python/python-httpx/multipart-explicit-encoding/multipart_explicit_encoding/runtime/__init__.py.golden create mode 100644 tests/golden/python/python-httpx/multipart-explicit-encoding/multipart_explicit_encoding/runtime/auth.py.golden create mode 100644 tests/golden/python/python-httpx/multipart-explicit-encoding/multipart_explicit_encoding/runtime/client.py.golden create mode 100644 tests/golden/python/python-httpx/multipart-explicit-encoding/multipart_explicit_encoding/runtime/errors.py.golden create mode 100644 tests/golden/python/python-httpx/multipart-explicit-encoding/pyproject.toml.golden create mode 100644 tests/golden/python/python-requests/multipart-explicit-encoding/README.md.golden create mode 100644 tests/golden/python/python-requests/multipart-explicit-encoding/multipart_explicit_encoding/__init__.py.golden create mode 100644 tests/golden/python/python-requests/multipart-explicit-encoding/multipart_explicit_encoding/apis/__init__.py.golden create mode 100644 tests/golden/python/python-requests/multipart-explicit-encoding/multipart_explicit_encoding/apis/transfer_api.py.golden create mode 100644 tests/golden/python/python-requests/multipart-explicit-encoding/multipart_explicit_encoding/models/__init__.py.golden create mode 100644 tests/golden/python/python-requests/multipart-explicit-encoding/multipart_explicit_encoding/models/audit_attributes.py.golden create mode 100644 tests/golden/python/python-requests/multipart-explicit-encoding/multipart_explicit_encoding/models/upload_attributes.py.golden create mode 100644 tests/golden/python/python-requests/multipart-explicit-encoding/multipart_explicit_encoding/models/upload_encoded_asset_request.py.golden create mode 100644 tests/golden/python/python-requests/multipart-explicit-encoding/multipart_explicit_encoding/py.typed.golden create mode 100644 tests/golden/python/python-requests/multipart-explicit-encoding/multipart_explicit_encoding/runtime/__init__.py.golden create mode 100644 tests/golden/python/python-requests/multipart-explicit-encoding/multipart_explicit_encoding/runtime/auth.py.golden create mode 100644 tests/golden/python/python-requests/multipart-explicit-encoding/multipart_explicit_encoding/runtime/client.py.golden create mode 100644 tests/golden/python/python-requests/multipart-explicit-encoding/multipart_explicit_encoding/runtime/errors.py.golden create mode 100644 tests/golden/python/python-requests/multipart-explicit-encoding/pyproject.toml.golden create mode 100644 tests/golden/rust/rust-aioduct/multipart-explicit-encoding/Cargo.toml.golden create mode 100644 tests/golden/rust/rust-aioduct/multipart-explicit-encoding/README.md.golden create mode 100644 tests/golden/rust/rust-aioduct/multipart-explicit-encoding/src/apis/mod.rs.golden create mode 100644 tests/golden/rust/rust-aioduct/multipart-explicit-encoding/src/apis/transfer.rs.golden create mode 100644 tests/golden/rust/rust-aioduct/multipart-explicit-encoding/src/lib.rs.golden create mode 100644 tests/golden/rust/rust-aioduct/multipart-explicit-encoding/src/models/audit_attributes.rs.golden create mode 100644 tests/golden/rust/rust-aioduct/multipart-explicit-encoding/src/models/mod.rs.golden create mode 100644 tests/golden/rust/rust-aioduct/multipart-explicit-encoding/src/models/upload_attributes.rs.golden create mode 100644 tests/golden/rust/rust-aioduct/multipart-explicit-encoding/src/models/upload_encoded_asset_request.rs.golden create mode 100644 tests/golden/rust/rust-aioduct/multipart-explicit-encoding/src/runtime/auth.rs.golden create mode 100644 tests/golden/rust/rust-aioduct/multipart-explicit-encoding/src/runtime/client.rs.golden create mode 100644 tests/golden/rust/rust-aioduct/multipart-explicit-encoding/src/runtime/error.rs.golden create mode 100644 tests/golden/rust/rust-aioduct/multipart-explicit-encoding/src/runtime/mod.rs.golden create mode 100644 tests/golden/rust/rust-reqwest/multipart-explicit-encoding/Cargo.toml.golden create mode 100644 tests/golden/rust/rust-reqwest/multipart-explicit-encoding/README.md.golden create mode 100644 tests/golden/rust/rust-reqwest/multipart-explicit-encoding/src/apis/mod.rs.golden create mode 100644 tests/golden/rust/rust-reqwest/multipart-explicit-encoding/src/apis/transfer.rs.golden create mode 100644 tests/golden/rust/rust-reqwest/multipart-explicit-encoding/src/lib.rs.golden create mode 100644 tests/golden/rust/rust-reqwest/multipart-explicit-encoding/src/models/audit_attributes.rs.golden create mode 100644 tests/golden/rust/rust-reqwest/multipart-explicit-encoding/src/models/mod.rs.golden create mode 100644 tests/golden/rust/rust-reqwest/multipart-explicit-encoding/src/models/upload_attributes.rs.golden create mode 100644 tests/golden/rust/rust-reqwest/multipart-explicit-encoding/src/models/upload_encoded_asset_request.rs.golden create mode 100644 tests/golden/rust/rust-reqwest/multipart-explicit-encoding/src/runtime/auth.rs.golden create mode 100644 tests/golden/rust/rust-reqwest/multipart-explicit-encoding/src/runtime/client.rs.golden create mode 100644 tests/golden/rust/rust-reqwest/multipart-explicit-encoding/src/runtime/error.rs.golden create mode 100644 tests/golden/rust/rust-reqwest/multipart-explicit-encoding/src/runtime/mod.rs.golden create mode 100644 tests/golden/rust/rust-ureq/multipart-explicit-encoding/Cargo.toml.golden create mode 100644 tests/golden/rust/rust-ureq/multipart-explicit-encoding/README.md.golden create mode 100644 tests/golden/rust/rust-ureq/multipart-explicit-encoding/src/apis/mod.rs.golden create mode 100644 tests/golden/rust/rust-ureq/multipart-explicit-encoding/src/apis/transfer.rs.golden create mode 100644 tests/golden/rust/rust-ureq/multipart-explicit-encoding/src/lib.rs.golden create mode 100644 tests/golden/rust/rust-ureq/multipart-explicit-encoding/src/models/audit_attributes.rs.golden create mode 100644 tests/golden/rust/rust-ureq/multipart-explicit-encoding/src/models/mod.rs.golden create mode 100644 tests/golden/rust/rust-ureq/multipart-explicit-encoding/src/models/upload_attributes.rs.golden create mode 100644 tests/golden/rust/rust-ureq/multipart-explicit-encoding/src/models/upload_encoded_asset_request.rs.golden create mode 100644 tests/golden/rust/rust-ureq/multipart-explicit-encoding/src/runtime/auth.rs.golden create mode 100644 tests/golden/rust/rust-ureq/multipart-explicit-encoding/src/runtime/client.rs.golden create mode 100644 tests/golden/rust/rust-ureq/multipart-explicit-encoding/src/runtime/error.rs.golden create mode 100644 tests/golden/rust/rust-ureq/multipart-explicit-encoding/src/runtime/mod.rs.golden create mode 100644 tests/golden/typescript/typescript-fetch/multipart-explicit-encoding/README.md.golden create mode 100644 tests/golden/typescript/typescript-fetch/multipart-explicit-encoding/apis/TransferApi.ts.golden create mode 100644 tests/golden/typescript/typescript-fetch/multipart-explicit-encoding/apis/index.ts.golden create mode 100644 tests/golden/typescript/typescript-fetch/multipart-explicit-encoding/index.ts.golden create mode 100644 tests/golden/typescript/typescript-fetch/multipart-explicit-encoding/models/AuditAttributes.ts.golden create mode 100644 tests/golden/typescript/typescript-fetch/multipart-explicit-encoding/models/UploadAttributes.ts.golden create mode 100644 tests/golden/typescript/typescript-fetch/multipart-explicit-encoding/models/UploadEncodedAssetRequest.ts.golden create mode 100644 tests/golden/typescript/typescript-fetch/multipart-explicit-encoding/models/index.ts.golden create mode 100644 tests/golden/typescript/typescript-fetch/multipart-explicit-encoding/package.json.golden create mode 100644 tests/golden/typescript/typescript-fetch/multipart-explicit-encoding/runtime/runtime.ts.golden create mode 100644 tests/golden/typescript/typescript-fetch/multipart-explicit-encoding/tsconfig.esm.json.golden create mode 100644 tests/golden/typescript/typescript-fetch/multipart-explicit-encoding/tsconfig.json.golden diff --git a/src/generators/go/http/sigil_emit_api.rs b/src/generators/go/http/sigil_emit_api.rs index f0dfe170d..d46c9667c 100644 --- a/src/generators/go/http/sigil_emit_api.rs +++ b/src/generators/go/http/sigil_emit_api.rs @@ -19,9 +19,10 @@ use std::collections::{BTreeMap, BTreeSet, HashSet}; use crate::codegen::traits::file_writer::FileInfo; +use crate::generators::multipart::{MultipartValueEncoding, multipart_parts_for_request_body}; use crate::ir::types::{ - IrObject, IrOperation, IrParameter, IrPrimitive, IrRequestBody, IrResponse, IrSchemaKind, - IrSpec, IrTypeExpr, ParameterLocation, + IrOperation, IrParameter, IrPrimitive, IrRequestBody, IrResponse, IrSpec, IrTypeExpr, + ParameterLocation, }; use heck::{ToLowerCamelCase, ToPascalCase, ToSnakeCase}; use sigil_stitch::code_block::CodeBlock; @@ -141,6 +142,7 @@ fn collect_body_imports(plans: &[OpPlan<'_>], module_path: &str) -> Vec CodeBlock { @@ -1174,63 +1195,29 @@ fn is_xml_media_type(media_type: &str) -> bool { base == "application/xml" || base == "text/xml" || base.ends_with("+xml") } -fn multipart_parts_for(t: &IrTypeExpr, ir: &IrSpec) -> Option> { - // TODO: Honor OpenAPI multipart encoding metadata once it is represented in the IR. - resolve_object(t, ir).map(|obj| { - obj.properties - .iter() - .map(|(wire_name, prop)| MultipartPart { - wire_name: wire_name.clone(), - field_name: go_field_name(wire_name), - type_expr: prop.type_expr.clone(), - is_binary: is_binary_type(&prop.type_expr, ir), - required: prop.required && !prop.nullable, - value_encoding: multipart_value_encoding(&prop.type_expr, ir), +fn multipart_parts_for( + body: &IrRequestBody, + media_type: &str, + ir: &IrSpec, +) -> Option> { + multipart_parts_for_request_body(body, media_type, ir).map(|parts| { + parts + .into_iter() + .map(|part| MultipartPart { + field_name: go_field_name(&part.wire_name), + wire_name: part.wire_name, + type_expr: part.type_expr, + is_binary: part.is_binary, + required: part.required, + content_type: part.content_type, + value_encoding: part.value_encoding, }) .collect() }) } -fn resolve_object<'a>(expr: &IrTypeExpr, ir: &'a IrSpec) -> Option<&'a IrObject> { - match expr { - IrTypeExpr::Named(name) => match ir.schemas.get(name).map(|schema| &schema.kind) { - Some(IrSchemaKind::Object(obj)) => Some(obj), - Some(IrSchemaKind::Alias(inner)) => resolve_object(inner, ir), - _ => None, - }, - IrTypeExpr::Nullable(inner) => resolve_object(inner, ir), - _ => None, - } -} - -fn is_binary_type(expr: &IrTypeExpr, ir: &IrSpec) -> bool { - match expr { - IrTypeExpr::Primitive(IrPrimitive::Binary) => true, - IrTypeExpr::Nullable(inner) => is_binary_type(inner, ir), - IrTypeExpr::Named(name) => ir.schemas.get(name).is_some_and(|schema| { - matches!(&schema.kind, IrSchemaKind::Alias(inner) if is_binary_type(inner, ir)) - }), - _ => false, - } -} - -fn multipart_value_encoding(expr: &IrTypeExpr, ir: &IrSpec) -> MultipartValueEncoding { - if is_multipart_text_type(expr, ir) { - MultipartValueEncoding::Text - } else { - MultipartValueEncoding::Json - } -} - -fn is_multipart_text_type(expr: &IrTypeExpr, ir: &IrSpec) -> bool { - match expr { - IrTypeExpr::Primitive(_) | IrTypeExpr::StringLiteral(_) | IrTypeExpr::StringEnum(_) => true, - IrTypeExpr::Nullable(inner) => is_multipart_text_type(inner, ir), - IrTypeExpr::Named(name) => ir.schemas.get(name).is_some_and(|schema| { - matches!(&schema.kind, IrSchemaKind::Alias(inner) if is_multipart_text_type(inner, ir)) - }), - _ => false, - } +fn go_string_literal(value: &str) -> String { + format!("{value:?}") } // --------------------------------------------------------------------------- diff --git a/src/generators/java/okhttp/sigil_emit_api.rs b/src/generators/java/okhttp/sigil_emit_api.rs index 764f00b3c..a73d501b7 100644 --- a/src/generators/java/okhttp/sigil_emit_api.rs +++ b/src/generators/java/okhttp/sigil_emit_api.rs @@ -1,9 +1,9 @@ use std::collections::{BTreeMap, HashSet}; use crate::codegen::traits::file_writer::FileInfo; +use crate::generators::multipart::{MultipartValueEncoding, multipart_parts_for_request_body}; use crate::ir::types::{ - IrObject, IrOperation, IrParameter, IrPrimitive, IrRequestBody, IrResponse, IrSchemaKind, - IrSpec, IrTypeExpr, ParameterLocation, + IrOperation, IrParameter, IrRequestBody, IrResponse, IrSpec, IrTypeExpr, ParameterLocation, }; use heck::{ToLowerCamelCase, ToPascalCase}; use sigil_stitch::lang::java::Java; @@ -84,13 +84,6 @@ fn emit_api_file(tag: &str, ops: &[&IrOperation], ir: &IrSpec, package_name: &st && body.multipart_parts.is_some() }) }); - let has_binary_multipart_part = plans.iter().any(|plan| { - plan.body.as_ref().is_some_and(|body| { - body.multipart_parts - .as_ref() - .is_some_and(|parts| parts.iter().any(|part| part.is_binary)) - }) - }); let has_raw_request_body = plans.iter().any(|plan| plan.body.is_some()); if has_supported_multipart_body { fb = fb.add_import(ImportSpec::named("okhttp3", "MultipartBody")); @@ -98,7 +91,7 @@ fn emit_api_file(tag: &str, ops: &[&IrOperation], ir: &IrSpec, package_name: &st if has_raw_request_body { fb = fb.add_import(ImportSpec::named("okhttp3", "RequestBody")); } - if has_binary_multipart_part || has_raw_request_body { + if has_supported_multipart_body || has_raw_request_body { fb = fb.add_import(ImportSpec::named("okhttp3", "MediaType")); } @@ -609,29 +602,35 @@ fn emit_required_multipart_part( part: &MultipartPart, access: &str, ) { + let wire_name = part.wire_name.as_str(); + let content_type = part.content_type.as_str(); if part.is_binary { - cb.add_statement( - &format!( - "multipartBuilder.addFormDataPart(\"{}\", \"{}\", RequestBody.create({}, MediaType.get(\"application/octet-stream\")))", - part.wire_name, part.wire_name, access - ), - (), + cb.add_code( + sigil_quote!(Java { + multipartBuilder.addFormDataPart($S(wire_name), $S(wire_name), RequestBody.create($L(access), MediaType.get($S(content_type)))); + }) + .expect("binary multipart part block builds"), ); } else if part.value_encoding == MultipartValueEncoding::Json { - cb.add_statement( - &format!( - "multipartBuilder.addFormDataPart(\"{}\", gson.toJson({access}))", - part.wire_name - ), - (), + cb.add_code( + sigil_quote!(Java { + multipartBuilder.addFormDataPart($S(wire_name), null, RequestBody.create(gson.toJson($L(access)), MediaType.get($S(content_type)))); + }) + .expect("json multipart part block builds"), + ); + } else if part.value_encoding == MultipartValueEncoding::Unsupported { + cb.add_code( + sigil_quote!(Java { + throw new IllegalArgumentException($S("unsupported multipart part content type")); + }) + .expect("unsupported multipart part block builds"), ); } else { - cb.add_statement( - &format!( - "multipartBuilder.addFormDataPart(\"{}\", String.valueOf({access}))", - part.wire_name - ), - (), + cb.add_code( + sigil_quote!(Java { + multipartBuilder.addFormDataPart($S(wire_name), null, RequestBody.create(String.valueOf($L(access)), MediaType.get($S(content_type)))); + }) + .expect("text multipart part block builds"), ); } } @@ -671,15 +670,10 @@ struct MultipartPart { field_name: String, is_binary: bool, required: bool, + content_type: String, value_encoding: MultipartValueEncoding, } -#[derive(Clone, Copy, PartialEq, Eq)] -enum MultipartValueEncoding { - Text, - Json, -} - #[derive(Clone, Copy, PartialEq, Eq)] enum BodyEncoding { Json, @@ -768,7 +762,7 @@ fn plan_body( }; let var_name = unique_name("body", used_names); let multipart_parts = if media_type_base(&media_type) == "multipart/form-data" { - multipart_parts_for(&t, ir) + multipart_parts_for(b, &media_type, ir) } else { None }; @@ -936,60 +930,22 @@ fn is_xml_media_type(media_type: &str) -> bool { base == "application/xml" || base == "text/xml" || base.ends_with("+xml") } -fn multipart_parts_for(t: &IrTypeExpr, ir: &IrSpec) -> Option> { - // TODO: Honor OpenAPI multipart encoding metadata once it is represented in the IR. - resolve_object(t, ir).map(|obj| { - obj.properties - .iter() - .map(|(wire_name, prop)| MultipartPart { - wire_name: wire_name.clone(), - field_name: java_field_name(wire_name), - is_binary: is_binary_type(&prop.type_expr, ir), - required: prop.required && !prop.nullable, - value_encoding: multipart_value_encoding(&prop.type_expr, ir), +fn multipart_parts_for( + body: &IrRequestBody, + media_type: &str, + ir: &IrSpec, +) -> Option> { + multipart_parts_for_request_body(body, media_type, ir).map(|parts| { + parts + .into_iter() + .map(|part| MultipartPart { + field_name: java_field_name(&part.wire_name), + wire_name: part.wire_name, + is_binary: part.is_binary, + required: part.required, + content_type: part.content_type, + value_encoding: part.value_encoding, }) .collect() }) } - -fn resolve_object<'a>(expr: &IrTypeExpr, ir: &'a IrSpec) -> Option<&'a IrObject> { - match expr { - IrTypeExpr::Named(name) => match ir.schemas.get(name).map(|schema| &schema.kind) { - Some(IrSchemaKind::Object(obj)) => Some(obj), - Some(IrSchemaKind::Alias(inner)) => resolve_object(inner, ir), - _ => None, - }, - IrTypeExpr::Nullable(inner) => resolve_object(inner, ir), - _ => None, - } -} - -fn is_binary_type(expr: &IrTypeExpr, ir: &IrSpec) -> bool { - match expr { - IrTypeExpr::Primitive(IrPrimitive::Binary) => true, - IrTypeExpr::Nullable(inner) => is_binary_type(inner, ir), - IrTypeExpr::Named(name) => ir.schemas.get(name).is_some_and(|schema| { - matches!(&schema.kind, IrSchemaKind::Alias(inner) if is_binary_type(inner, ir)) - }), - _ => false, - } -} - -fn multipart_value_encoding(expr: &IrTypeExpr, ir: &IrSpec) -> MultipartValueEncoding { - if is_multipart_text_type(expr, ir) { - MultipartValueEncoding::Text - } else { - MultipartValueEncoding::Json - } -} - -fn is_multipart_text_type(expr: &IrTypeExpr, ir: &IrSpec) -> bool { - match expr { - IrTypeExpr::Primitive(_) | IrTypeExpr::StringLiteral(_) | IrTypeExpr::StringEnum(_) => true, - IrTypeExpr::Nullable(inner) => is_multipart_text_type(inner, ir), - IrTypeExpr::Named(name) => ir.schemas.get(name).is_some_and(|schema| { - matches!(&schema.kind, IrSchemaKind::Alias(inner) if is_multipart_text_type(inner, ir)) - }), - _ => false, - } -} diff --git a/src/generators/kotlin/okhttp/sigil_emit_api.rs b/src/generators/kotlin/okhttp/sigil_emit_api.rs index 97096371c..958c00d62 100644 --- a/src/generators/kotlin/okhttp/sigil_emit_api.rs +++ b/src/generators/kotlin/okhttp/sigil_emit_api.rs @@ -1,9 +1,9 @@ use std::collections::{BTreeMap, HashSet}; use crate::codegen::traits::file_writer::FileInfo; +use crate::generators::multipart::{MultipartValueEncoding, multipart_parts_for_request_body}; use crate::ir::types::{ - IrObject, IrOperation, IrParameter, IrPrimitive, IrRequestBody, IrResponse, IrSchemaKind, - IrSpec, IrTypeExpr, ParameterLocation, + IrOperation, IrParameter, IrRequestBody, IrResponse, IrSpec, IrTypeExpr, ParameterLocation, }; use heck::{ToLowerCamelCase, ToPascalCase}; use sigil_stitch::lang::kotlin::Kotlin; @@ -77,18 +77,11 @@ fn emit_api_file(tag: &str, ops: &[&IrOperation], ir: &IrSpec, package_name: &st && body.multipart_parts.is_some() }) }); - let has_binary_multipart_part = plans.iter().any(|plan| { - plan.body.as_ref().is_some_and(|body| { - body.multipart_parts - .as_ref() - .is_some_and(|parts| parts.iter().any(|part| part.is_binary)) - }) - }); let has_raw_request_body = plans.iter().any(|plan| plan.body.is_some()); if has_supported_multipart_body { fb = fb.add_import(ImportSpec::named("okhttp3", "MultipartBody")); } - if has_binary_multipart_part || has_raw_request_body { + if has_supported_multipart_body || has_raw_request_body { fb = fb .add_import(ImportSpec::named( "okhttp3.MediaType.Companion", @@ -544,29 +537,35 @@ fn emit_required_multipart_part( part: &MultipartPart, access: &str, ) { + let wire_name = part.wire_name.as_str(); + let content_type = part.content_type.as_str(); if part.is_binary { - cb.add( - &format!( - "multipartBuilder.addFormDataPart(\"{}\", \"{}\", {}.toRequestBody(\"application/octet-stream\".toMediaType()))", - part.wire_name, part.wire_name, access - ), - (), + cb.add_code( + sigil_quote!(Kotlin { + multipartBuilder.addFormDataPart($S(wire_name), $S(wire_name), $L(access).toRequestBody($S(content_type).toMediaType())) + }) + .expect("binary multipart part block builds"), ); } else if part.value_encoding == MultipartValueEncoding::Json { - cb.add( - &format!( - "multipartBuilder.addFormDataPart(\"{}\", gson.toJson({access}))", - part.wire_name - ), - (), + cb.add_code( + sigil_quote!(Kotlin { + multipartBuilder.addFormDataPart($S(wire_name), null, gson.toJson($L(access)).toRequestBody($S(content_type).toMediaType())) + }) + .expect("json multipart part block builds"), + ); + } else if part.value_encoding == MultipartValueEncoding::Unsupported { + cb.add_code( + sigil_quote!(Kotlin { + throw IllegalArgumentException($S("unsupported multipart part content type")) + }) + .expect("unsupported multipart part block builds"), ); } else { - cb.add( - &format!( - "multipartBuilder.addFormDataPart(\"{}\", {}.toString())", - part.wire_name, access - ), - (), + cb.add_code( + sigil_quote!(Kotlin { + multipartBuilder.addFormDataPart($S(wire_name), null, $L(access).toString().toRequestBody($S(content_type).toMediaType())) + }) + .expect("text multipart part block builds"), ); } cb.add_line(); @@ -607,15 +606,10 @@ struct MultipartPart { field_name: String, is_binary: bool, required: bool, + content_type: String, value_encoding: MultipartValueEncoding, } -#[derive(Clone, Copy, PartialEq, Eq)] -enum MultipartValueEncoding { - Text, - Json, -} - #[derive(Clone, Copy, PartialEq, Eq)] enum BodyEncoding { Json, @@ -707,7 +701,7 @@ fn plan_body( } let var_name = unique_name("body", used_names); let multipart_parts = if media_type_base(&media_type) == "multipart/form-data" { - multipart_parts_for(&t, ir) + multipart_parts_for(b, &media_type, ir) } else { None }; @@ -875,60 +869,22 @@ fn is_xml_media_type(media_type: &str) -> bool { base == "application/xml" || base == "text/xml" || base.ends_with("+xml") } -fn multipart_parts_for(t: &IrTypeExpr, ir: &IrSpec) -> Option> { - // TODO: Honor OpenAPI multipart encoding metadata once it is represented in the IR. - resolve_object(t, ir).map(|obj| { - obj.properties - .iter() - .map(|(wire_name, prop)| MultipartPart { - wire_name: wire_name.clone(), - field_name: kt_field_name(wire_name), - is_binary: is_binary_type(&prop.type_expr, ir), - required: prop.required && !prop.nullable, - value_encoding: multipart_value_encoding(&prop.type_expr, ir), +fn multipart_parts_for( + body: &IrRequestBody, + media_type: &str, + ir: &IrSpec, +) -> Option> { + multipart_parts_for_request_body(body, media_type, ir).map(|parts| { + parts + .into_iter() + .map(|part| MultipartPart { + field_name: kt_field_name(&part.wire_name), + wire_name: part.wire_name, + is_binary: part.is_binary, + required: part.required, + content_type: part.content_type, + value_encoding: part.value_encoding, }) .collect() }) } - -fn resolve_object<'a>(expr: &IrTypeExpr, ir: &'a IrSpec) -> Option<&'a IrObject> { - match expr { - IrTypeExpr::Named(name) => match ir.schemas.get(name).map(|schema| &schema.kind) { - Some(IrSchemaKind::Object(obj)) => Some(obj), - Some(IrSchemaKind::Alias(inner)) => resolve_object(inner, ir), - _ => None, - }, - IrTypeExpr::Nullable(inner) => resolve_object(inner, ir), - _ => None, - } -} - -fn is_binary_type(expr: &IrTypeExpr, ir: &IrSpec) -> bool { - match expr { - IrTypeExpr::Primitive(IrPrimitive::Binary) => true, - IrTypeExpr::Nullable(inner) => is_binary_type(inner, ir), - IrTypeExpr::Named(name) => ir.schemas.get(name).is_some_and(|schema| { - matches!(&schema.kind, IrSchemaKind::Alias(inner) if is_binary_type(inner, ir)) - }), - _ => false, - } -} - -fn multipart_value_encoding(expr: &IrTypeExpr, ir: &IrSpec) -> MultipartValueEncoding { - if is_multipart_text_type(expr, ir) { - MultipartValueEncoding::Text - } else { - MultipartValueEncoding::Json - } -} - -fn is_multipart_text_type(expr: &IrTypeExpr, ir: &IrSpec) -> bool { - match expr { - IrTypeExpr::Primitive(_) | IrTypeExpr::StringLiteral(_) | IrTypeExpr::StringEnum(_) => true, - IrTypeExpr::Nullable(inner) => is_multipart_text_type(inner, ir), - IrTypeExpr::Named(name) => ir.schemas.get(name).is_some_and(|schema| { - matches!(&schema.kind, IrSchemaKind::Alias(inner) if is_multipart_text_type(inner, ir)) - }), - _ => false, - } -} diff --git a/src/generators/mod.rs b/src/generators/mod.rs index e5073d2aa..e1c816061 100644 --- a/src/generators/mod.rs +++ b/src/generators/mod.rs @@ -1,6 +1,7 @@ pub mod go; pub mod java; pub mod kotlin; +pub mod multipart; pub mod python; pub mod registry; pub mod rust; diff --git a/src/generators/multipart.rs b/src/generators/multipart.rs new file mode 100644 index 000000000..a2b26976f --- /dev/null +++ b/src/generators/multipart.rs @@ -0,0 +1,132 @@ +//! Shared multipart/form-data request-body planning. + +use crate::ir::types::{IrObject, IrPrimitive, IrRequestBody, IrSchemaKind, IrSpec, IrTypeExpr}; + +#[derive(Debug, Clone)] +pub struct MultipartPart { + pub wire_name: String, + pub type_expr: IrTypeExpr, + pub is_binary: bool, + pub required: bool, + pub content_type: String, + pub value_encoding: MultipartValueEncoding, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum MultipartValueEncoding { + Text, + Json, + Unsupported, +} + +pub fn multipart_parts_for_request_body( + body: &IrRequestBody, + media_type: &str, + ir: &IrSpec, +) -> Option> { + let t = body.content.get(media_type)?; + let media_encoding = body.encoding.get(media_type); + resolve_object(t, ir).map(|obj| { + obj.properties + .iter() + .map(|(wire_name, prop)| { + let explicit_content_type = media_encoding + .and_then(|encoding| encoding.get(wire_name)) + .and_then(|encoding| encoding.content_type.clone()); + multipart_part_from_property( + wire_name, + &prop.type_expr, + prop.required && !prop.nullable, + explicit_content_type, + ir, + ) + }) + .collect() + }) +} + +fn multipart_part_from_property( + wire_name: &str, + type_expr: &IrTypeExpr, + required: bool, + explicit_content_type: Option, + ir: &IrSpec, +) -> MultipartPart { + let is_binary = is_binary_type(type_expr, ir); + let is_text = is_multipart_text_type(type_expr, ir); + let content_type = explicit_content_type.unwrap_or_else(|| { + if is_binary { + "application/octet-stream".to_string() + } else if is_text { + "text/plain".to_string() + } else { + "application/json".to_string() + } + }); + let value_encoding = if is_binary { + MultipartValueEncoding::Text + } else if is_json_media_type(&content_type) { + MultipartValueEncoding::Json + } else if is_text { + MultipartValueEncoding::Text + } else { + MultipartValueEncoding::Unsupported + }; + + MultipartPart { + wire_name: wire_name.to_string(), + type_expr: type_expr.clone(), + is_binary, + required, + content_type, + value_encoding, + } +} + +fn resolve_object<'a>(expr: &IrTypeExpr, ir: &'a IrSpec) -> Option<&'a IrObject> { + match expr { + IrTypeExpr::Named(name) => match ir.schemas.get(name).map(|schema| &schema.kind) { + Some(IrSchemaKind::Object(obj)) => Some(obj), + Some(IrSchemaKind::Alias(inner)) => resolve_object(inner, ir), + _ => None, + }, + IrTypeExpr::Nullable(inner) => resolve_object(inner, ir), + _ => None, + } +} + +fn is_binary_type(expr: &IrTypeExpr, ir: &IrSpec) -> bool { + match expr { + IrTypeExpr::Primitive(IrPrimitive::Binary) => true, + IrTypeExpr::Nullable(inner) => is_binary_type(inner, ir), + IrTypeExpr::Named(name) => ir.schemas.get(name).is_some_and(|schema| { + matches!(&schema.kind, IrSchemaKind::Alias(inner) if is_binary_type(inner, ir)) + }), + _ => false, + } +} + +fn is_multipart_text_type(expr: &IrTypeExpr, ir: &IrSpec) -> bool { + match expr { + IrTypeExpr::Primitive(_) | IrTypeExpr::StringLiteral(_) | IrTypeExpr::StringEnum(_) => true, + IrTypeExpr::Nullable(inner) => is_multipart_text_type(inner, ir), + IrTypeExpr::Named(name) => ir.schemas.get(name).is_some_and(|schema| { + matches!(&schema.kind, IrSchemaKind::Alias(inner) if is_multipart_text_type(inner, ir)) + }), + _ => false, + } +} + +pub fn is_json_media_type(media_type: &str) -> bool { + let base = media_type_base(media_type); + base == "application/json" || base.ends_with("+json") +} + +fn media_type_base(media_type: &str) -> String { + media_type + .split(';') + .next() + .unwrap_or(media_type) + .trim() + .to_ascii_lowercase() +} diff --git a/src/generators/python/httpx/emit_api.rs b/src/generators/python/httpx/emit_api.rs index 25d125362..8cf7af1fe 100644 --- a/src/generators/python/httpx/emit_api.rs +++ b/src/generators/python/httpx/emit_api.rs @@ -7,9 +7,10 @@ use std::collections::{BTreeMap, HashSet}; use crate::codegen::traits::file_writer::FileInfo; +use crate::generators::multipart::{MultipartValueEncoding, multipart_parts_for_request_body}; use crate::ir::types::{ - IrObject, IrOperation, IrParameter, IrPrimitive, IrRequestBody, IrResponse, IrSchemaKind, - IrSpec, IrTypeExpr, ParameterLocation, + IrOperation, IrParameter, IrPrimitive, IrRequestBody, IrResponse, IrSpec, IrTypeExpr, + ParameterLocation, }; use heck::{ToPascalCase, ToSnakeCase}; use sigil_stitch::code_block::CodeBlock; @@ -379,8 +380,8 @@ fn emit_required_multipart_part( if part.is_binary { cb.add_statement( &format!( - "files[\"{}\"] = (\"{}\", {access}, \"application/octet-stream\")", - part.wire_name, part.wire_name + "files[\"{}\"] = (\"{}\", {access}, \"{}\")", + part.wire_name, part.wire_name, part.content_type ), (), ); @@ -388,14 +389,22 @@ fn emit_required_multipart_part( let json_value = render_multipart_json_value(access, &part.type_expr, ir); cb.add_statement( &format!( - "files[\"{}\"] = (None, json.dumps({json_value}), \"application/json\")", - part.wire_name + "files[\"{}\"] = (None, json.dumps({json_value}), \"{}\")", + part.wire_name, part.content_type ), (), ); + } else if part.value_encoding == MultipartValueEncoding::Unsupported { + cb.add_statement( + "raise ValueError(\"unsupported multipart part content type\")", + (), + ); } else { cb.add_statement( - &format!("files[\"{}\"] = (None, str({access}))", part.wire_name), + &format!( + "files[\"{}\"] = (None, str({access}), \"{}\")", + part.wire_name, part.content_type + ), (), ); } @@ -535,15 +544,10 @@ struct MultipartPart { type_expr: IrTypeExpr, is_binary: bool, required: bool, + content_type: String, value_encoding: MultipartValueEncoding, } -#[derive(Clone, Copy, PartialEq, Eq)] -enum MultipartValueEncoding { - Text, - Json, -} - #[derive(Clone, Copy, PartialEq, Eq)] enum BodyEncoding { Json, @@ -616,7 +620,7 @@ fn plan_body( let encoding = body_encoding(&media_type); let var_name = unique_name("body", used_names); let multipart_parts = if media_type_base(&media_type) == "multipart/form-data" { - multipart_parts_for(&t, ir) + multipart_parts_for(b, &media_type, ir) } else { None }; @@ -754,65 +758,27 @@ fn is_xml_media_type(media_type: &str) -> bool { base == "application/xml" || base == "text/xml" || base.ends_with("+xml") } -fn multipart_parts_for(t: &IrTypeExpr, ir: &IrSpec) -> Option> { - // TODO: Honor OpenAPI multipart encoding metadata once it is represented in the IR. - resolve_object(t, ir).map(|obj| { - obj.properties - .iter() - .map(|(wire_name, prop)| MultipartPart { - wire_name: wire_name.clone(), - field_name: python_field_name(wire_name), - type_expr: prop.type_expr.clone(), - is_binary: is_binary_type(&prop.type_expr, ir), - required: prop.required && !prop.nullable, - value_encoding: multipart_value_encoding(&prop.type_expr, ir), +fn multipart_parts_for( + body: &IrRequestBody, + media_type: &str, + ir: &IrSpec, +) -> Option> { + multipart_parts_for_request_body(body, media_type, ir).map(|parts| { + parts + .into_iter() + .map(|part| MultipartPart { + field_name: python_field_name(&part.wire_name), + wire_name: part.wire_name, + type_expr: part.type_expr, + is_binary: part.is_binary, + required: part.required, + content_type: part.content_type, + value_encoding: part.value_encoding, }) .collect() }) } -fn resolve_object<'a>(expr: &IrTypeExpr, ir: &'a IrSpec) -> Option<&'a IrObject> { - match expr { - IrTypeExpr::Named(name) => match ir.schemas.get(name).map(|schema| &schema.kind) { - Some(IrSchemaKind::Object(obj)) => Some(obj), - Some(IrSchemaKind::Alias(inner)) => resolve_object(inner, ir), - _ => None, - }, - IrTypeExpr::Nullable(inner) => resolve_object(inner, ir), - _ => None, - } -} - -fn is_binary_type(expr: &IrTypeExpr, ir: &IrSpec) -> bool { - match expr { - IrTypeExpr::Primitive(IrPrimitive::Binary) => true, - IrTypeExpr::Nullable(inner) => is_binary_type(inner, ir), - IrTypeExpr::Named(name) => ir.schemas.get(name).is_some_and(|schema| { - matches!(&schema.kind, IrSchemaKind::Alias(inner) if is_binary_type(inner, ir)) - }), - _ => false, - } -} - -fn multipart_value_encoding(expr: &IrTypeExpr, ir: &IrSpec) -> MultipartValueEncoding { - if is_multipart_text_type(expr, ir) { - MultipartValueEncoding::Text - } else { - MultipartValueEncoding::Json - } -} - -fn is_multipart_text_type(expr: &IrTypeExpr, ir: &IrSpec) -> bool { - match expr { - IrTypeExpr::Primitive(_) | IrTypeExpr::StringLiteral(_) | IrTypeExpr::StringEnum(_) => true, - IrTypeExpr::Nullable(inner) => is_multipart_text_type(inner, ir), - IrTypeExpr::Named(name) => ir.schemas.get(name).is_some_and(|schema| { - matches!(&schema.kind, IrSchemaKind::Alias(inner) if is_multipart_text_type(inner, ir)) - }), - _ => false, - } -} - fn python_param_name(name: &str) -> String { let snake = name.to_snake_case(); if snake.is_empty() { diff --git a/src/generators/python/requests/emit_api.rs b/src/generators/python/requests/emit_api.rs index 7978ee07d..ad6203695 100644 --- a/src/generators/python/requests/emit_api.rs +++ b/src/generators/python/requests/emit_api.rs @@ -7,9 +7,10 @@ use std::collections::{BTreeMap, HashSet}; use crate::codegen::traits::file_writer::FileInfo; +use crate::generators::multipart::{MultipartValueEncoding, multipart_parts_for_request_body}; use crate::ir::types::{ - IrObject, IrOperation, IrParameter, IrPrimitive, IrRequestBody, IrResponse, IrSchemaKind, - IrSpec, IrTypeExpr, ParameterLocation, + IrOperation, IrParameter, IrPrimitive, IrRequestBody, IrResponse, IrSpec, IrTypeExpr, + ParameterLocation, }; use heck::{ToPascalCase, ToSnakeCase}; use sigil_stitch::code_block::CodeBlock; @@ -378,8 +379,8 @@ fn emit_required_multipart_part( if part.is_binary { cb.add_statement( &format!( - "files[\"{}\"] = (\"{}\", {access}, \"application/octet-stream\")", - part.wire_name, part.wire_name + "files[\"{}\"] = (\"{}\", {access}, \"{}\")", + part.wire_name, part.wire_name, part.content_type ), (), ); @@ -387,14 +388,22 @@ fn emit_required_multipart_part( let json_value = render_multipart_json_value(access, &part.type_expr, ir); cb.add_statement( &format!( - "files[\"{}\"] = (None, json.dumps({json_value}), \"application/json\")", - part.wire_name + "files[\"{}\"] = (None, json.dumps({json_value}), \"{}\")", + part.wire_name, part.content_type ), (), ); + } else if part.value_encoding == MultipartValueEncoding::Unsupported { + cb.add_statement( + "raise ValueError(\"unsupported multipart part content type\")", + (), + ); } else { cb.add_statement( - &format!("files[\"{}\"] = (None, str({access}))", part.wire_name), + &format!( + "files[\"{}\"] = (None, str({access}), \"{}\")", + part.wire_name, part.content_type + ), (), ); } @@ -534,15 +543,10 @@ struct MultipartPart { type_expr: IrTypeExpr, is_binary: bool, required: bool, + content_type: String, value_encoding: MultipartValueEncoding, } -#[derive(Clone, Copy, PartialEq, Eq)] -enum MultipartValueEncoding { - Text, - Json, -} - #[derive(Clone, Copy, PartialEq, Eq)] enum BodyEncoding { Json, @@ -615,7 +619,7 @@ fn plan_body( let encoding = body_encoding(&media_type); let var_name = unique_name("body", used_names); let multipart_parts = if media_type_base(&media_type) == "multipart/form-data" { - multipart_parts_for(&t, ir) + multipart_parts_for(b, &media_type, ir) } else { None }; @@ -753,65 +757,27 @@ fn is_xml_media_type(media_type: &str) -> bool { base == "application/xml" || base == "text/xml" || base.ends_with("+xml") } -fn multipart_parts_for(t: &IrTypeExpr, ir: &IrSpec) -> Option> { - // TODO: Honor OpenAPI multipart encoding metadata once it is represented in the IR. - resolve_object(t, ir).map(|obj| { - obj.properties - .iter() - .map(|(wire_name, prop)| MultipartPart { - wire_name: wire_name.clone(), - field_name: python_field_name(wire_name), - type_expr: prop.type_expr.clone(), - is_binary: is_binary_type(&prop.type_expr, ir), - required: prop.required && !prop.nullable, - value_encoding: multipart_value_encoding(&prop.type_expr, ir), +fn multipart_parts_for( + body: &IrRequestBody, + media_type: &str, + ir: &IrSpec, +) -> Option> { + multipart_parts_for_request_body(body, media_type, ir).map(|parts| { + parts + .into_iter() + .map(|part| MultipartPart { + field_name: python_field_name(&part.wire_name), + wire_name: part.wire_name, + type_expr: part.type_expr, + is_binary: part.is_binary, + required: part.required, + content_type: part.content_type, + value_encoding: part.value_encoding, }) .collect() }) } -fn resolve_object<'a>(expr: &IrTypeExpr, ir: &'a IrSpec) -> Option<&'a IrObject> { - match expr { - IrTypeExpr::Named(name) => match ir.schemas.get(name).map(|schema| &schema.kind) { - Some(IrSchemaKind::Object(obj)) => Some(obj), - Some(IrSchemaKind::Alias(inner)) => resolve_object(inner, ir), - _ => None, - }, - IrTypeExpr::Nullable(inner) => resolve_object(inner, ir), - _ => None, - } -} - -fn is_binary_type(expr: &IrTypeExpr, ir: &IrSpec) -> bool { - match expr { - IrTypeExpr::Primitive(IrPrimitive::Binary) => true, - IrTypeExpr::Nullable(inner) => is_binary_type(inner, ir), - IrTypeExpr::Named(name) => ir.schemas.get(name).is_some_and(|schema| { - matches!(&schema.kind, IrSchemaKind::Alias(inner) if is_binary_type(inner, ir)) - }), - _ => false, - } -} - -fn multipart_value_encoding(expr: &IrTypeExpr, ir: &IrSpec) -> MultipartValueEncoding { - if is_multipart_text_type(expr, ir) { - MultipartValueEncoding::Text - } else { - MultipartValueEncoding::Json - } -} - -fn is_multipart_text_type(expr: &IrTypeExpr, ir: &IrSpec) -> bool { - match expr { - IrTypeExpr::Primitive(_) | IrTypeExpr::StringLiteral(_) | IrTypeExpr::StringEnum(_) => true, - IrTypeExpr::Nullable(inner) => is_multipart_text_type(inner, ir), - IrTypeExpr::Named(name) => ir.schemas.get(name).is_some_and(|schema| { - matches!(&schema.kind, IrSchemaKind::Alias(inner) if is_multipart_text_type(inner, ir)) - }), - _ => false, - } -} - fn python_param_name(name: &str) -> String { let snake = name.to_snake_case(); if snake.is_empty() { diff --git a/src/generators/rust/aioduct/sigil_emit_api.rs b/src/generators/rust/aioduct/sigil_emit_api.rs index dd47d0719..ffec087ab 100644 --- a/src/generators/rust/aioduct/sigil_emit_api.rs +++ b/src/generators/rust/aioduct/sigil_emit_api.rs @@ -4,9 +4,10 @@ use sigil_stitch::code_block::CodeBlock; use sigil_stitch::prelude::sigil_quote; use crate::generators::rust::common::emit_api::{ - BodyEncoding, MultipartPart, OpPlan, RustBackendConfig, binary_field_expr, emit_response_match, - emit_result_init, optional_binary_field_expr, optional_text_field_expr, render_to_string, - response_value_expr, rust_string_literal, text_field_expr, + BodyEncoding, MultipartPart, MultipartValueEncoding, OpPlan, RustBackendConfig, + binary_field_expr, emit_response_match, emit_result_init, optional_binary_field_expr, + optional_text_field_expr, render_to_string, response_value_expr, rust_field_name, + rust_string_literal, text_field_expr, }; /// Backend configuration for aioduct (async, with generic runtime parameter). @@ -240,11 +241,23 @@ fn emit_multipart_body( ); for part in parts { let wire_name = rust_string_literal(&part.wire_name); + let content_type = rust_string_literal(&part.content_type); + if part.value_encoding == MultipartValueEncoding::Unsupported { + if part.required { + b.add("return Err(Error::Unsupported(\"unsupported multipart part content type\"));\n", ()); + } else { + let field_name = rust_field_name(&part.wire_name); + b.begin_control_flow(&format!("if {body_var}.{field_name}.is_some()"), ()); + b.add("return Err(Error::Unsupported(\"unsupported multipart part content type\"));\n", ()); + b.end_control_flow(); + } + continue; + } if part.required { if part.is_binary { b.add( &format!( - "multipart = multipart.file({wire_name}, {wire_name}, \"application/octet-stream\", {});\n", + "multipart = multipart.file({wire_name}, {wire_name}, {content_type}, {});\n", binary_field_expr(body_var, part), ), (), @@ -252,21 +265,22 @@ fn emit_multipart_body( } else { b.add( &format!( - "multipart = multipart.text({wire_name}, {});\n", + "multipart = multipart.part(aioduct::multipart::Part::text({wire_name}, {}).mime_str({content_type}));\n", text_field_expr(body_var, part), ), (), ); } } else { + let field_name = rust_field_name(&part.wire_name); b.begin_control_flow( - &format!("if let Some(value) = &{body_var}.{}", part.field_name), + &format!("if let Some(value) = &{body_var}.{field_name}"), (), ); if part.is_binary { b.add( &format!( - "multipart = multipart.file({wire_name}, {wire_name}, \"application/octet-stream\", {});\n", + "multipart = multipart.file({wire_name}, {wire_name}, {content_type}, {});\n", optional_binary_field_expr("value"), ), (), @@ -274,7 +288,7 @@ fn emit_multipart_body( } else { b.add( &format!( - "multipart = multipart.text({wire_name}, {});\n", + "multipart = multipart.part(aioduct::multipart::Part::text({wire_name}, {}).mime_str({content_type}));\n", optional_text_field_expr("value", part), ), (), diff --git a/src/generators/rust/common/emit_api.rs b/src/generators/rust/common/emit_api.rs index 28a4a3c87..4603aab26 100644 --- a/src/generators/rust/common/emit_api.rs +++ b/src/generators/rust/common/emit_api.rs @@ -10,9 +10,10 @@ use std::collections::{BTreeMap, HashSet}; use crate::codegen::traits::file_writer::FileInfo; +use crate::generators::multipart::multipart_parts_for_request_body; +pub use crate::generators::multipart::{MultipartPart, MultipartValueEncoding}; use crate::ir::types::{ - IrObject, IrOperation, IrParameter, IrPrimitive, IrRequestBody, IrResponse, IrSchemaKind, - IrSpec, IrTypeExpr, ParameterLocation, + IrOperation, IrParameter, IrRequestBody, IrResponse, IrSpec, IrTypeExpr, ParameterLocation, }; use heck::{ToPascalCase, ToSnakeCase}; use sigil_stitch::code_block::{CodeBlock, CodeBlockBuilder}; @@ -239,21 +240,6 @@ pub enum BodyEncoding { Other(String), } -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct MultipartPart { - pub wire_name: String, - pub field_name: String, - pub is_binary: bool, - pub required: bool, - pub value_encoding: MultipartValueEncoding, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum MultipartValueEncoding { - Text, - Json, -} - pub struct TypedResponse { pub status: String, pub field_name: String, @@ -334,12 +320,12 @@ pub fn plan_body( _ => rust_type_str_qualified(&t, ir), }; let multipart_parts = if encoding == BodyEncoding::Multipart { - multipart_parts_for(&t, ir).unwrap_or_default() + multipart_parts_for_request_body(b, &media_type, ir).unwrap_or_default() } else { Vec::new() }; - let multipart_supported = - encoding != BodyEncoding::Multipart || multipart_parts_for(&t, ir).is_some(); + let multipart_supported = encoding != BodyEncoding::Multipart + || multipart_parts_for_request_body(b, &media_type, ir).is_some(); let var_name = unique_name("body", used_names); Some(BodyBinding { var_name, @@ -681,67 +667,7 @@ fn is_xml_media_type(media_type: &str) -> bool { base == "application/xml" || base == "text/xml" || base.ends_with("+xml") } -fn multipart_parts_for(t: &IrTypeExpr, ir: &IrSpec) -> Option> { - // TODO: Honor OpenAPI multipart encoding metadata once it is represented in the IR. - resolve_object(t, ir).map(|obj| multipart_parts_from_object(obj, ir)) -} - -fn multipart_parts_from_object(obj: &IrObject, ir: &IrSpec) -> Vec { - obj.properties - .iter() - .map(|(wire_name, prop)| MultipartPart { - wire_name: wire_name.clone(), - field_name: rust_field_name(wire_name), - is_binary: is_binary_type(&prop.type_expr, ir), - required: prop.required && !prop.nullable, - value_encoding: multipart_value_encoding(&prop.type_expr, ir), - }) - .collect() -} - -fn resolve_object<'a>(expr: &IrTypeExpr, ir: &'a IrSpec) -> Option<&'a IrObject> { - match expr { - IrTypeExpr::Named(name) => match ir.schemas.get(name).map(|schema| &schema.kind) { - Some(IrSchemaKind::Object(obj)) => Some(obj), - Some(IrSchemaKind::Alias(inner)) => resolve_object(inner, ir), - _ => None, - }, - IrTypeExpr::Nullable(inner) => resolve_object(inner, ir), - _ => None, - } -} - -fn is_binary_type(expr: &IrTypeExpr, ir: &IrSpec) -> bool { - match expr { - IrTypeExpr::Primitive(IrPrimitive::Binary) => true, - IrTypeExpr::Nullable(inner) => is_binary_type(inner, ir), - IrTypeExpr::Named(name) => ir.schemas.get(name).is_some_and(|schema| { - matches!(&schema.kind, IrSchemaKind::Alias(inner) if is_binary_type(inner, ir)) - }), - _ => false, - } -} - -fn multipart_value_encoding(expr: &IrTypeExpr, ir: &IrSpec) -> MultipartValueEncoding { - if is_multipart_text_type(expr, ir) { - MultipartValueEncoding::Text - } else { - MultipartValueEncoding::Json - } -} - -fn is_multipart_text_type(expr: &IrTypeExpr, ir: &IrSpec) -> bool { - match expr { - IrTypeExpr::Primitive(_) | IrTypeExpr::StringLiteral(_) | IrTypeExpr::StringEnum(_) => true, - IrTypeExpr::Nullable(inner) => is_multipart_text_type(inner, ir), - IrTypeExpr::Named(name) => ir.schemas.get(name).is_some_and(|schema| { - matches!(&schema.kind, IrSchemaKind::Alias(inner) if is_multipart_text_type(inner, ir)) - }), - _ => false, - } -} - -fn rust_field_name(wire_name: &str) -> String { +pub fn rust_field_name(wire_name: &str) -> String { escape_rust_keyword(&wire_name.to_snake_case()) } @@ -764,22 +690,27 @@ pub fn rust_string_literal(value: &str) -> String { } pub fn text_field_expr(base: &str, part: &MultipartPart) -> String { + let field_name = rust_field_name(&part.wire_name); match part.value_encoding { - MultipartValueEncoding::Text => format!("{base}.{}.to_string()", part.field_name), - MultipartValueEncoding::Json => { - format!("serde_json::to_string(&{base}.{})?", part.field_name) + MultipartValueEncoding::Text => format!("{base}.{field_name}.to_string()"), + MultipartValueEncoding::Json => format!("serde_json::to_string(&{base}.{field_name})?"), + MultipartValueEncoding::Unsupported => { + unreachable!("unsupported multipart parts are emitted before value expressions") } } } pub fn binary_field_expr(base: &str, part: &MultipartPart) -> String { - format!("{base}.{}.clone()", part.field_name) + format!("{base}.{}.clone()", rust_field_name(&part.wire_name)) } pub fn optional_text_field_expr(value: &str, part: &MultipartPart) -> String { match part.value_encoding { MultipartValueEncoding::Text => format!("{value}.to_string()"), MultipartValueEncoding::Json => format!("serde_json::to_string({value})?"), + MultipartValueEncoding::Unsupported => { + unreachable!("unsupported multipart parts are emitted before value expressions") + } } } diff --git a/src/generators/rust/reqwest/sigil_emit_api.rs b/src/generators/rust/reqwest/sigil_emit_api.rs index d93d89e61..658125800 100644 --- a/src/generators/rust/reqwest/sigil_emit_api.rs +++ b/src/generators/rust/reqwest/sigil_emit_api.rs @@ -4,9 +4,10 @@ use sigil_stitch::code_block::CodeBlock; use sigil_stitch::prelude::sigil_quote; use crate::generators::rust::common::emit_api::{ - BodyEncoding, MultipartPart, OpPlan, RustBackendConfig, binary_field_expr, emit_response_match, - emit_result_init, optional_binary_field_expr, optional_text_field_expr, render_to_string, - response_value_expr, rust_string_literal, text_field_expr, + BodyEncoding, MultipartPart, MultipartValueEncoding, OpPlan, RustBackendConfig, + binary_field_expr, emit_response_match, emit_result_init, optional_binary_field_expr, + optional_text_field_expr, render_to_string, response_value_expr, rust_field_name, + rust_string_literal, text_field_expr, }; /// Backend configuration for reqwest (async, no extra generics). @@ -236,11 +237,23 @@ fn emit_multipart_body( b.add("let mut multipart = reqwest::multipart::Form::new();\n", ()); for part in parts { let wire_name = rust_string_literal(&part.wire_name); + let content_type = rust_string_literal(&part.content_type); + if part.value_encoding == MultipartValueEncoding::Unsupported { + if part.required { + b.add("return Err(Error::Unsupported(\"unsupported multipart part content type\"));\n", ()); + } else { + let field_name = rust_field_name(&part.wire_name); + b.begin_control_flow(&format!("if {body_var}.{field_name}.is_some()"), ()); + b.add("return Err(Error::Unsupported(\"unsupported multipart part content type\"));\n", ()); + b.end_control_flow(); + } + continue; + } if part.required { if part.is_binary { b.add( &format!( - "multipart = multipart.part({wire_name}, reqwest::multipart::Part::bytes({}).file_name({wire_name}).mime_str(\"application/octet-stream\").map_err(Error::Network)?);\n", + "multipart = multipart.part({wire_name}, reqwest::multipart::Part::bytes({}).file_name({wire_name}).mime_str({content_type}).map_err(Error::Network)?);\n", binary_field_expr(body_var, part), ), (), @@ -248,21 +261,22 @@ fn emit_multipart_body( } else { b.add( &format!( - "multipart = multipart.text({wire_name}, {});\n", + "multipart = multipart.part({wire_name}, reqwest::multipart::Part::text({}).mime_str({content_type}).map_err(Error::Network)?);\n", text_field_expr(body_var, part), ), (), ); } } else { + let field_name = rust_field_name(&part.wire_name); b.begin_control_flow( - &format!("if let Some(value) = &{body_var}.{}", part.field_name), + &format!("if let Some(value) = &{body_var}.{field_name}"), (), ); if part.is_binary { b.add( &format!( - "multipart = multipart.part({wire_name}, reqwest::multipart::Part::bytes({}).file_name({wire_name}).mime_str(\"application/octet-stream\").map_err(Error::Network)?);\n", + "multipart = multipart.part({wire_name}, reqwest::multipart::Part::bytes({}).file_name({wire_name}).mime_str({content_type}).map_err(Error::Network)?);\n", optional_binary_field_expr("value"), ), (), @@ -270,7 +284,7 @@ fn emit_multipart_body( } else { b.add( &format!( - "multipart = multipart.text({wire_name}, {});\n", + "multipart = multipart.part({wire_name}, reqwest::multipart::Part::text({}).mime_str({content_type}).map_err(Error::Network)?);\n", optional_text_field_expr("value", part), ), (), diff --git a/src/generators/rust/ureq/sigil_emit_api.rs b/src/generators/rust/ureq/sigil_emit_api.rs index 6674e17c3..11b072572 100644 --- a/src/generators/rust/ureq/sigil_emit_api.rs +++ b/src/generators/rust/ureq/sigil_emit_api.rs @@ -4,9 +4,10 @@ use sigil_stitch::code_block::CodeBlock; use sigil_stitch::prelude::sigil_quote; use crate::generators::rust::common::emit_api::{ - BodyEncoding, MultipartPart, OpPlan, RustBackendConfig, binary_field_expr, emit_response_match, - emit_result_init, optional_binary_field_expr, optional_text_field_expr, render_to_string, - response_value_expr, rust_string_literal, text_field_expr, + BodyEncoding, MultipartPart, MultipartValueEncoding, OpPlan, RustBackendConfig, + binary_field_expr, emit_response_match, emit_result_init, optional_binary_field_expr, + optional_text_field_expr, render_to_string, response_value_expr, rust_field_name, + rust_string_literal, text_field_expr, }; use crate::ir::types::IrTypeExpr; @@ -314,8 +315,26 @@ fn emit_multipart_part( part: &MultipartPart, ) { let wire_name = rust_string_literal(&part.wire_name); + let content_type = rust_string_literal(&part.content_type); + if part.value_encoding == MultipartValueEncoding::Unsupported { + if part.required { + b.add( + "return Err(Error::Unsupported(\"unsupported multipart part content type\"));\n", + (), + ); + } else { + let field_name = rust_field_name(&part.wire_name); + b.begin_control_flow(&format!("if {body_var}.{field_name}.is_some()"), ()); + b.add( + "return Err(Error::Unsupported(\"unsupported multipart part content type\"));\n", + (), + ); + b.end_control_flow(); + } + return; + } if part.required { - emit_part_prefix(b, &wire_name, part.is_binary); + emit_part_prefix(b, &wire_name, part.is_binary, &content_type); if part.is_binary { b.add( &format!( @@ -335,11 +354,12 @@ fn emit_multipart_part( } b.add("multipart_body.extend_from_slice(b\"\\r\\n\");\n", ()); } else { + let field_name = rust_field_name(&part.wire_name); b.begin_control_flow( - &format!("if let Some(value) = &{body_var}.{}", part.field_name), + &format!("if let Some(value) = &{body_var}.{field_name}"), (), ); - emit_part_prefix(b, &wire_name, part.is_binary); + emit_part_prefix(b, &wire_name, part.is_binary, &content_type); if part.is_binary { b.add( &format!( @@ -366,6 +386,7 @@ fn emit_part_prefix( b: &mut sigil_stitch::code_block::CodeBlockBuilder, wire_name: &str, is_binary: bool, + content_type: &str, ) { b.add( "multipart_body.extend_from_slice(format!(\"--{}\\r\\n\", boundary).as_bytes());\n", @@ -374,14 +395,14 @@ fn emit_part_prefix( if is_binary { b.add( &format!( - "multipart_body.extend_from_slice(format!(\"Content-Disposition: form-data; name=\\\"{{}}\\\"; filename=\\\"{{}}\\\"\\r\\nContent-Type: application/octet-stream\\r\\n\\r\\n\", {wire_name}, {wire_name}).as_bytes());\n", + "multipart_body.extend_from_slice(format!(\"Content-Disposition: form-data; name=\\\"{{}}\\\"; filename=\\\"{{}}\\\"\\r\\nContent-Type: {{}}\\r\\n\\r\\n\", {wire_name}, {wire_name}, {content_type}).as_bytes());\n", ), (), ); } else { b.add( &format!( - "multipart_body.extend_from_slice(format!(\"Content-Disposition: form-data; name=\\\"{{}}\\\"\\r\\n\\r\\n\", {wire_name}).as_bytes());\n", + "multipart_body.extend_from_slice(format!(\"Content-Disposition: form-data; name=\\\"{{}}\\\"\\r\\nContent-Type: {{}}\\r\\n\\r\\n\", {wire_name}, {content_type}).as_bytes());\n", ), (), ); diff --git a/src/generators/typescript/fetch/sigil_emit_api.rs b/src/generators/typescript/fetch/sigil_emit_api.rs index 70682aa72..66e8327ea 100644 --- a/src/generators/typescript/fetch/sigil_emit_api.rs +++ b/src/generators/typescript/fetch/sigil_emit_api.rs @@ -19,9 +19,10 @@ use std::collections::{BTreeMap, BTreeSet, HashSet}; use crate::codegen::traits::file_writer::FileInfo; +use crate::generators::multipart::{MultipartValueEncoding, multipart_parts_for_request_body}; use crate::ir::types::{ - IrObject, IrOperation, IrParameter, IrPrimitive, IrRequestBody, IrResponse, IrSchemaKind, - IrSpec, IrTypeExpr, ParameterLocation as IrParameterLocation, + IrOperation, IrParameter, IrPrimitive, IrRequestBody, IrResponse, IrSpec, IrTypeExpr, + ParameterLocation as IrParameterLocation, }; use heck::{ToLowerCamelCase as _, ToPascalCase as _}; use sigil_stitch::code_block::{Arg, CodeBlock}; @@ -667,28 +668,28 @@ fn emit_request_body( if let Some(media_type) = preferred_request_media_type(body) && media_type_base(&media_type) == "multipart/form-data" { - if let Some(parts) = body - .content - .get(media_type.as_str()) - .and_then(|ty| multipart_parts_for(ty, ir, property_naming_camel_case)) + if let Some(parts) = + multipart_parts_for(body, &media_type, ir, property_naming_camel_case) { if !body.required { - cb.add( - "let requestBody: FormData | undefined = undefined;\n", - vec![], + cb.add_code( + sigil_quote!(TypeScript { + let requestBody: Blob | undefined = undefined; + }) + .expect("optional multipart request body decl builds"), ); cb.add( &format!("if ({access} !== undefined && {access} !== null) {{\n"), vec![], ); - cb.add("requestBody = new FormData();\n", vec![]); + emit_multipart_blob_setup(cb); } else { - cb.add("const requestBody = new FormData();\n", vec![]); + emit_multipart_blob_setup(cb); } for part in parts { let part_access = format!("{access}{}", ts_property_access(&part.field_name)); if part.required { - emit_form_data_append(cb, &part, &part_access, convertible); + emit_multipart_blob_part(cb, &part, &part_access, convertible); } else { cb.add( &format!( @@ -696,10 +697,11 @@ fn emit_request_body( ), vec![], ); - emit_form_data_append(cb, &part, &part_access, convertible); + emit_multipart_blob_part(cb, &part, &part_access, convertible); cb.add("}\n", vec![]); } } + emit_multipart_blob_finish(cb, body.required); if !body.required { cb.add("}\n", vec![]); } @@ -769,12 +771,77 @@ fn is_unsupported_ts_request_media_type(media_type: &str) -> bool { is_xml_media_type(media_type) || base == "application/x-www-form-urlencoded" } -fn emit_form_data_append( +fn emit_multipart_blob_setup(cb: &mut sigil_stitch::code_block::CodeBlockBuilder) { + cb.add_code( + sigil_quote!(TypeScript { + const multipartBoundary = $S("----openapi-nexus-") + Math.random().toString(16).slice(2); + const multipartChunks: Array = []; + headerParameters[$S("Content-Type")] = $S("multipart/form-data; boundary=") + multipartBoundary; + }) + .expect("multipart setup block builds"), + ); +} + +fn emit_multipart_blob_finish(cb: &mut sigil_stitch::code_block::CodeBlockBuilder, required: bool) { + let closing_boundary_tail = ts_string_literal("--\r\n"); + if required { + cb.add_code( + sigil_quote!(TypeScript { + multipartChunks.push($S("--") + multipartBoundary + $L(closing_boundary_tail)); + const requestBody = new Blob(multipartChunks); + }) + .expect("required multipart finish block builds"), + ); + } else { + cb.add_code( + sigil_quote!(TypeScript { + multipartChunks.push($S("--") + multipartBoundary + $L(closing_boundary_tail)); + requestBody = new Blob(multipartChunks); + }) + .expect("optional multipart finish block builds"), + ); + } +} + +fn emit_multipart_blob_part( cb: &mut sigil_stitch::code_block::CodeBlockBuilder, part: &MultipartPart, part_access: &str, convertible: &HashSet, ) { + if part.value_encoding == MultipartValueEncoding::Unsupported { + cb.add_code( + sigil_quote!(TypeScript { + throw new Error($S("unsupported multipart part content type")); + }) + .expect("unsupported multipart part block builds"), + ); + return; + } + let disposition = if part.is_binary { + format!( + "form-data; name=\"{}\"; filename=\"{}\"", + multipart_header_quoted(&part.wire_name), + multipart_header_quoted(&part.wire_name) + ) + } else { + format!( + "form-data; name=\"{}\"", + multipart_header_quoted(&part.wire_name) + ) + }; + let header_tail = format!( + "\r\nContent-Disposition: {}\r\nContent-Type: {}\r\n\r\n", + disposition, + multipart_header_value(&part.content_type) + ); + let header_tail_literal = ts_string_literal(&header_tail); + cb.add_code( + sigil_quote!(TypeScript { + multipartChunks.push($S("--") + multipartBoundary + $L(header_tail_literal)); + }) + .expect("multipart part header block builds"), + ); let value_expr = if part.is_binary { part_access.to_string() } else if part.value_encoding == MultipartValueEncoding::Json { @@ -784,12 +851,13 @@ fn emit_form_data_append( } else { format!("String({part_access})") }; - cb.add( - &format!( - "requestBody.append({}, {value_expr});\n", - ts_string_literal(&part.wire_name) - ), - vec![], + let crlf_literal = ts_string_literal("\r\n"); + cb.add_code( + sigil_quote!(TypeScript { + multipartChunks.push($L(value_expr)); + multipartChunks.push($L(crlf_literal)); + }) + .expect("multipart part value block builds"), ); } @@ -1440,82 +1508,36 @@ struct MultipartPart { type_expr: IrTypeExpr, is_binary: bool, required: bool, + content_type: String, value_encoding: MultipartValueEncoding, } -#[derive(Clone, Copy, PartialEq, Eq)] -enum MultipartValueEncoding { - Text, - Json, -} - fn multipart_parts_for( - t: &IrTypeExpr, + body: &IrRequestBody, + media_type: &str, ir: &IrSpec, property_naming_camel_case: bool, ) -> Option> { - // TODO: Honor OpenAPI multipart encoding metadata once it is represented in the IR. - resolve_object(t, ir).map(|obj| { - obj.properties - .iter() - .map(|(wire_name, prop)| MultipartPart { - wire_name: wire_name.clone(), + multipart_parts_for_request_body(body, media_type, ir).map(|parts| { + parts + .into_iter() + .map(|part| MultipartPart { field_name: if property_naming_camel_case { - wire_name.to_lower_camel_case() + part.wire_name.to_lower_camel_case() } else { - wire_name.clone() + part.wire_name.clone() }, - type_expr: prop.type_expr.clone(), - is_binary: is_binary_type(&prop.type_expr, ir), - required: prop.required && !prop.nullable, - value_encoding: multipart_value_encoding(&prop.type_expr, ir), + wire_name: part.wire_name, + type_expr: part.type_expr, + is_binary: part.is_binary, + required: part.required, + content_type: part.content_type, + value_encoding: part.value_encoding, }) .collect() }) } -fn resolve_object<'a>(expr: &IrTypeExpr, ir: &'a IrSpec) -> Option<&'a IrObject> { - match expr { - IrTypeExpr::Named(name) => match ir.schemas.get(name).map(|schema| &schema.kind) { - Some(IrSchemaKind::Object(obj)) => Some(obj), - Some(IrSchemaKind::Alias(inner)) => resolve_object(inner, ir), - _ => None, - }, - IrTypeExpr::Nullable(inner) => resolve_object(inner, ir), - _ => None, - } -} - -fn is_binary_type(expr: &IrTypeExpr, ir: &IrSpec) -> bool { - match expr { - IrTypeExpr::Primitive(IrPrimitive::Binary) => true, - IrTypeExpr::Nullable(inner) => is_binary_type(inner, ir), - IrTypeExpr::Named(name) => ir.schemas.get(name).is_some_and(|schema| { - matches!(&schema.kind, IrSchemaKind::Alias(inner) if is_binary_type(inner, ir)) - }), - _ => false, - } -} - -fn multipart_value_encoding(expr: &IrTypeExpr, ir: &IrSpec) -> MultipartValueEncoding { - if is_multipart_text_type(expr, ir) { - MultipartValueEncoding::Text - } else { - MultipartValueEncoding::Json - } -} - -fn is_multipart_text_type(expr: &IrTypeExpr, ir: &IrSpec) -> bool { - match expr { - IrTypeExpr::Primitive(_) | IrTypeExpr::StringLiteral(_) | IrTypeExpr::StringEnum(_) => true, - IrTypeExpr::Nullable(inner) => is_multipart_text_type(inner, ir), - IrTypeExpr::Named(name) => ir.schemas.get(name).is_some_and(|schema| { - matches!(&schema.kind, IrSchemaKind::Alias(inner) if is_multipart_text_type(inner, ir)) - }), - _ => false, - } -} - fn ts_property_access(field_name: &str) -> String { if is_js_identifier(field_name) { format!(".{field_name}") @@ -1525,7 +1547,24 @@ fn ts_property_access(field_name: &str) -> String { } fn ts_string_literal(value: &str) -> String { - format!("'{}'", value.replace('\\', "\\\\").replace('\'', "\\'")) + format!( + "'{}'", + value + .replace('\\', "\\\\") + .replace('\'', "\\'") + .replace('\r', "\\r") + .replace('\n', "\\n") + ) +} + +fn multipart_header_quoted(value: &str) -> String { + multipart_header_value(value) + .replace('\\', "\\\\") + .replace('"', "\\\"") +} + +fn multipart_header_value(value: &str) -> String { + value.replace(['\r', '\n'], "") } /// Collect unique Named type references from request bodies only @@ -1546,10 +1585,8 @@ fn collect_request_named_types( collect_convertible_named_refs(ty, convertible, &mut names); } } else if media_type_base(&media_type) == "multipart/form-data" - && let Some(parts) = body - .content - .get(media_type.as_str()) - .and_then(|ty| multipart_parts_for(ty, ir, property_naming_camel_case)) + && let Some(parts) = + multipart_parts_for(body, &media_type, ir, property_naming_camel_case) { for part in parts { if part.value_encoding == MultipartValueEncoding::Json { diff --git a/src/ir/lower/v30.rs b/src/ir/lower/v30.rs index 76050f629..4c9ab74ea 100644 --- a/src/ir/lower/v30.rs +++ b/src/ir/lower/v30.rs @@ -11,9 +11,9 @@ use super::LowerError; use crate::ir::types::{ ApiKeyLocation, IrContact, IrEnum, IrEnumValue, IrEnumValueType, IrHeader, IrInfo, IrIntersection, IrLicense, IrOAuth2Flow, IrOAuth2Flows, IrObject, IrOperation, IrParameter, - IrPrimitive, IrProperty, IrRequestBody, IrResponse, IrSchema, IrSchemaKind, - IrSecurityRequirement, IrSecurityScheme, IrServer, IrSpec, IrTaggedUnion, IrTaggedVariant, - IrTypeExpr, IrUnion, IrValidation, ParameterLocation, TaggingStyle, + IrPrimitive, IrProperty, IrRequestBody, IrRequestBodyEncoding, IrResponse, IrSchema, + IrSchemaKind, IrSecurityRequirement, IrSecurityScheme, IrServer, IrSpec, IrTaggedUnion, + IrTaggedVariant, IrTypeExpr, IrUnion, IrValidation, ParameterLocation, TaggingStyle, }; // --------------------------------------------------------------------------- @@ -917,6 +917,7 @@ impl<'a> LowerCtx<'a> { rb: &oas::RequestBody, ) -> Result { let mut content = IndexMap::new(); + let mut encoding = IndexMap::new(); for (mime, media_type) in &rb.content { if let Some(schema_ref) = &media_type.schema { let type_expr = self.lower_schema_ref_with_promotion( @@ -925,12 +926,27 @@ impl<'a> LowerCtx<'a> { )?; content.insert(mime.clone(), type_expr); } + let mut media_encoding = IndexMap::new(); + for (name, enc) in &media_type.encoding { + if enc.content_type.is_some() { + media_encoding.insert( + name.clone(), + IrRequestBodyEncoding { + content_type: enc.content_type.clone(), + }, + ); + } + } + if !media_encoding.is_empty() { + encoding.insert(mime.clone(), media_encoding); + } } Ok(IrRequestBody { required: rb.required.unwrap_or(false), description: rb.description.clone(), content, + encoding, }) } diff --git a/src/ir/lower/v31.rs b/src/ir/lower/v31.rs index 4ad65ac2b..25279e7cf 100644 --- a/src/ir/lower/v31.rs +++ b/src/ir/lower/v31.rs @@ -11,9 +11,9 @@ use super::LowerError; use crate::ir::types::{ ApiKeyLocation, IrContact, IrEnum, IrEnumValue, IrEnumValueType, IrHeader, IrInfo, IrIntersection, IrLicense, IrOAuth2Flow, IrOAuth2Flows, IrObject, IrOperation, IrParameter, - IrPrimitive, IrProperty, IrRequestBody, IrResponse, IrSchema, IrSchemaKind, - IrSecurityRequirement, IrSecurityScheme, IrServer, IrSpec, IrTaggedUnion, IrTaggedVariant, - IrTypeExpr, IrUnion, IrValidation, ParameterLocation, TaggingStyle, + IrPrimitive, IrProperty, IrRequestBody, IrRequestBodyEncoding, IrResponse, IrSchema, + IrSchemaKind, IrSecurityRequirement, IrSecurityScheme, IrServer, IrSpec, IrTaggedUnion, + IrTaggedVariant, IrTypeExpr, IrUnion, IrValidation, ParameterLocation, TaggingStyle, }; // --------------------------------------------------------------------------- @@ -977,6 +977,7 @@ impl<'a> LowerCtx<'a> { rb: &oas::RequestBody, ) -> Result { let mut content = IndexMap::new(); + let mut encoding = IndexMap::new(); for (mime, media_type) in &rb.content { if let Some(schema_ref) = &media_type.schema { let type_expr = self.lower_schema_ref_with_promotion( @@ -985,12 +986,27 @@ impl<'a> LowerCtx<'a> { )?; content.insert(mime.clone(), type_expr); } + let mut media_encoding = IndexMap::new(); + for (name, enc) in &media_type.encoding { + if enc.content_type.is_some() { + media_encoding.insert( + name.clone(), + IrRequestBodyEncoding { + content_type: enc.content_type.clone(), + }, + ); + } + } + if !media_encoding.is_empty() { + encoding.insert(mime.clone(), media_encoding); + } } Ok(IrRequestBody { required: rb.required.unwrap_or(false), description: rb.description.clone(), content, + encoding, }) } diff --git a/src/ir/lower/v32.rs b/src/ir/lower/v32.rs index 082080286..e6eb0154c 100644 --- a/src/ir/lower/v32.rs +++ b/src/ir/lower/v32.rs @@ -11,9 +11,9 @@ use super::LowerError; use crate::ir::types::{ ApiKeyLocation, IrContact, IrEnum, IrEnumValue, IrEnumValueType, IrHeader, IrInfo, IrIntersection, IrLicense, IrOAuth2Flow, IrOAuth2Flows, IrObject, IrOperation, IrParameter, - IrPrimitive, IrProperty, IrRequestBody, IrResponse, IrSchema, IrSchemaKind, - IrSecurityRequirement, IrSecurityScheme, IrServer, IrSpec, IrTaggedUnion, IrTaggedVariant, - IrTypeExpr, IrUnion, IrValidation, ParameterLocation, TaggingStyle, + IrPrimitive, IrProperty, IrRequestBody, IrRequestBodyEncoding, IrResponse, IrSchema, + IrSchemaKind, IrSecurityRequirement, IrSecurityScheme, IrServer, IrSpec, IrTaggedUnion, + IrTaggedVariant, IrTypeExpr, IrUnion, IrValidation, ParameterLocation, TaggingStyle, }; // --------------------------------------------------------------------------- @@ -976,6 +976,7 @@ impl<'a> LowerCtx<'a> { rb: &oas::RequestBody, ) -> Result { let mut content = IndexMap::new(); + let mut encoding = IndexMap::new(); for (mime, media_type) in &rb.content { if let Some(schema_ref) = &media_type.schema { let type_expr = self.lower_schema_ref_with_promotion( @@ -984,12 +985,27 @@ impl<'a> LowerCtx<'a> { )?; content.insert(mime.clone(), type_expr); } + let mut media_encoding = IndexMap::new(); + for (name, enc) in &media_type.encoding { + if enc.content_type.is_some() { + media_encoding.insert( + name.clone(), + IrRequestBodyEncoding { + content_type: enc.content_type.clone(), + }, + ); + } + } + if !media_encoding.is_empty() { + encoding.insert(mime.clone(), media_encoding); + } } Ok(IrRequestBody { required: rb.required.unwrap_or(false), description: rb.description.clone(), content, + encoding, }) } diff --git a/src/ir/types/mod.rs b/src/ir/types/mod.rs index b2fe01801..8b1af46f2 100644 --- a/src/ir/types/mod.rs +++ b/src/ir/types/mod.rs @@ -10,8 +10,8 @@ mod spec; mod type_expr; pub use operation::{ - IrHeader, IrOperation, IrParameter, IrRequestBody, IrResponse, IrSecurityRequirement, - ParameterLocation, + IrHeader, IrOperation, IrParameter, IrRequestBody, IrRequestBodyEncoding, IrResponse, + IrSecurityRequirement, ParameterLocation, }; pub use schema::{ IrEnum, IrEnumValue, IrEnumValueType, IrIntersection, IrObject, IrProperty, IrSchema, diff --git a/src/ir/types/operation.rs b/src/ir/types/operation.rs index fbb316163..84ffc6009 100644 --- a/src/ir/types/operation.rs +++ b/src/ir/types/operation.rs @@ -48,6 +48,14 @@ pub struct IrRequestBody { pub description: Option, /// Media type -> resolved schema type. pub content: IndexMap, + /// Media type -> property name -> request-body encoding metadata. + pub encoding: IndexMap>, +} + +/// Request-body encoding metadata for a single media-type property. +#[derive(Debug, Clone, Serialize)] +pub struct IrRequestBodyEncoding { + pub content_type: Option, } /// A fully resolved response. diff --git a/tests/fixtures/valid/multipart-explicit-encoding.yaml b/tests/fixtures/valid/multipart-explicit-encoding.yaml new file mode 100644 index 000000000..979788e2a --- /dev/null +++ b/tests/fixtures/valid/multipart-explicit-encoding.yaml @@ -0,0 +1,62 @@ +openapi: 3.1.0 +info: + title: Multipart Explicit Encoding + version: 1.0.0 + description: Covers explicit multipart part content types. +paths: + /uploads/encoded: + post: + tags: + - transfer + operationId: upload_encoded_asset + requestBody: + required: true + content: + multipart/form-data: + schema: + type: object + required: + - file + - purpose + - metadata + - audit + properties: + file: + type: string + format: binary + description: Binary content with an explicit content type + purpose: + type: string + description: Plain-text upload purpose + metadata: + $ref: '#/components/schemas/UploadAttributes' + audit: + $ref: '#/components/schemas/AuditAttributes' + encoding: + file: + contentType: application/pdf + metadata: + contentType: application/vnd.openapi-nexus.metadata+json + responses: + '204': + description: Uploaded +components: + schemas: + UploadAttributes: + type: object + required: + - label + properties: + label: + type: string + retention_days: + type: integer + AuditAttributes: + type: object + required: + - request_id + properties: + request_id: + type: string + reviewed: + type: boolean diff --git a/tests/golden/go/go-http/binary-transfer-media-types/apis/transfer.go.golden b/tests/golden/go/go-http/binary-transfer-media-types/apis/transfer.go.golden index 5e6d19d0d..81ead3011 100644 --- a/tests/golden/go/go-http/binary-transfer-media-types/apis/transfer.go.golden +++ b/tests/golden/go/go-http/binary-transfer-media-types/apis/transfer.go.golden @@ -13,6 +13,7 @@ import ( "io" "mime/multipart" "net/http" + "net/textproto" "strings" "example.com/sdk/models" @@ -83,23 +84,46 @@ func (a *TransferAPI) UploadAsset(ctx context.Context, body *models.UploadAssetR if body != nil { buf := &bytes.Buffer{} writer := multipart.NewWriter(buf) - partWriter, err := writer.CreateFormFile("file", "file") + { + partHeader := textproto.MIMEHeader{} + partHeader.Set("Content-Disposition", "form-data; name=\"file\"; filename=\"file\"") + partHeader.Set("Content-Type", "application/octet-stream") + partWriter, err := writer.CreatePart(partHeader) if err != nil { - return nil, fmt.Errorf("create multipart file: %w", err) + return nil, fmt.Errorf("create multipart part: %w", err) } if _, err := partWriter.Write(body.File); err != nil { return nil, fmt.Errorf("write multipart file: %w", err) } + } + { + partHeader := textproto.MIMEHeader{} + partHeader.Set("Content-Disposition", "form-data; name=\"metadata\"") + partHeader.Set("Content-Type", "application/json") + partWriter, err := writer.CreatePart(partHeader) + if err != nil { + return nil, fmt.Errorf("create multipart part: %w", err) + } partValue, err := json.Marshal(body.Metadata) if err != nil { return nil, fmt.Errorf("marshal multipart field: %w", err) } - if err := writer.WriteField("metadata", string(partValue)); err != nil { + if _, err := partWriter.Write(partValue); err != nil { return nil, fmt.Errorf("write multipart field: %w", err) } - if err := writer.WriteField("purpose", body.Purpose); err != nil { + } + { + partHeader := textproto.MIMEHeader{} + partHeader.Set("Content-Disposition", "form-data; name=\"purpose\"") + partHeader.Set("Content-Type", "text/plain") + partWriter, err := writer.CreatePart(partHeader) + if err != nil { + return nil, fmt.Errorf("create multipart part: %w", err) + } + if _, err := io.WriteString(partWriter, body.Purpose); err != nil { return nil, fmt.Errorf("write multipart field: %w", err) } + } if err := writer.Close(); err != nil { return nil, fmt.Errorf("close multipart writer: %w", err) } diff --git a/tests/golden/go/go-http/media-type-selection/apis/media.go.golden b/tests/golden/go/go-http/media-type-selection/apis/media.go.golden index 11500eff6..9a74068b9 100644 --- a/tests/golden/go/go-http/media-type-selection/apis/media.go.golden +++ b/tests/golden/go/go-http/media-type-selection/apis/media.go.golden @@ -13,6 +13,7 @@ import ( "io" "mime/multipart" "net/http" + "net/textproto" "example.com/sdk/models" "example.com/sdk/runtime" @@ -79,17 +80,31 @@ func (a *MediaAPI) SendParameterizedMultipart(ctx context.Context, body *models. if body != nil { buf := &bytes.Buffer{} writer := multipart.NewWriter(buf) - partWriter, err := writer.CreateFormFile("file", "file") + { + partHeader := textproto.MIMEHeader{} + partHeader.Set("Content-Disposition", "form-data; name=\"file\"; filename=\"file\"") + partHeader.Set("Content-Type", "application/octet-stream") + partWriter, err := writer.CreatePart(partHeader) if err != nil { - return nil, fmt.Errorf("create multipart file: %w", err) + return nil, fmt.Errorf("create multipart part: %w", err) } if _, err := partWriter.Write(body.File); err != nil { return nil, fmt.Errorf("write multipart file: %w", err) } + } if body.Note != nil { - if err := writer.WriteField("note", *body.Note); err != nil { + { + partHeader := textproto.MIMEHeader{} + partHeader.Set("Content-Disposition", "form-data; name=\"note\"") + partHeader.Set("Content-Type", "text/plain") + partWriter, err := writer.CreatePart(partHeader) + if err != nil { + return nil, fmt.Errorf("create multipart part: %w", err) + } + if _, err := io.WriteString(partWriter, *body.Note); err != nil { return nil, fmt.Errorf("write multipart field: %w", err) } + } } if err := writer.Close(); err != nil { return nil, fmt.Errorf("close multipart writer: %w", err) diff --git a/tests/golden/go/go-http/multipart-edge-cases/apis/multipart.go.golden b/tests/golden/go/go-http/multipart-edge-cases/apis/multipart.go.golden index a6cb81a39..a99b3a061 100644 --- a/tests/golden/go/go-http/multipart-edge-cases/apis/multipart.go.golden +++ b/tests/golden/go/go-http/multipart-edge-cases/apis/multipart.go.golden @@ -13,6 +13,7 @@ import ( "io" "mime/multipart" "net/http" + "net/textproto" "strconv" "example.com/sdk/models" @@ -44,37 +45,78 @@ func (a *MultipartAPI) SendOptionalParts(ctx context.Context, body *models.Optio buf := &bytes.Buffer{} writer := multipart.NewWriter(buf) if body.Attributes != nil { + { + partHeader := textproto.MIMEHeader{} + partHeader.Set("Content-Disposition", "form-data; name=\"attributes\"") + partHeader.Set("Content-Type", "application/json") + partWriter, err := writer.CreatePart(partHeader) + if err != nil { + return nil, fmt.Errorf("create multipart part: %w", err) + } partValue, err := json.Marshal(*body.Attributes) if err != nil { return nil, fmt.Errorf("marshal multipart field: %w", err) } - if err := writer.WriteField("attributes", string(partValue)); err != nil { + if _, err := partWriter.Write(partValue); err != nil { return nil, fmt.Errorf("write multipart field: %w", err) } + } } if body.Enabled != nil { - if err := writer.WriteField("enabled", strconv.FormatBool(*body.Enabled)); err != nil { + { + partHeader := textproto.MIMEHeader{} + partHeader.Set("Content-Disposition", "form-data; name=\"enabled\"") + partHeader.Set("Content-Type", "text/plain") + partWriter, err := writer.CreatePart(partHeader) + if err != nil { + return nil, fmt.Errorf("create multipart part: %w", err) + } + if _, err := io.WriteString(partWriter, strconv.FormatBool(*body.Enabled)); err != nil { return nil, fmt.Errorf("write multipart field: %w", err) } + } } if body.File != nil { - partWriter, err := writer.CreateFormFile("file", "file") + { + partHeader := textproto.MIMEHeader{} + partHeader.Set("Content-Disposition", "form-data; name=\"file\"; filename=\"file\"") + partHeader.Set("Content-Type", "application/octet-stream") + partWriter, err := writer.CreatePart(partHeader) if err != nil { - return nil, fmt.Errorf("create multipart file: %w", err) + return nil, fmt.Errorf("create multipart part: %w", err) } if _, err := partWriter.Write(*body.File); err != nil { return nil, fmt.Errorf("write multipart file: %w", err) } + } } if body.RetryCount != nil { - if err := writer.WriteField("retry_count", strconv.FormatInt(int64(*body.RetryCount), 10)); err != nil { + { + partHeader := textproto.MIMEHeader{} + partHeader.Set("Content-Disposition", "form-data; name=\"retry_count\"") + partHeader.Set("Content-Type", "text/plain") + partWriter, err := writer.CreatePart(partHeader) + if err != nil { + return nil, fmt.Errorf("create multipart part: %w", err) + } + if _, err := io.WriteString(partWriter, strconv.FormatInt(int64(*body.RetryCount), 10)); err != nil { return nil, fmt.Errorf("write multipart field: %w", err) } + } } if body.Title != nil { - if err := writer.WriteField("title", *body.Title); err != nil { + { + partHeader := textproto.MIMEHeader{} + partHeader.Set("Content-Disposition", "form-data; name=\"title\"") + partHeader.Set("Content-Type", "text/plain") + partWriter, err := writer.CreatePart(partHeader) + if err != nil { + return nil, fmt.Errorf("create multipart part: %w", err) + } + if _, err := io.WriteString(partWriter, *body.Title); err != nil { return nil, fmt.Errorf("write multipart field: %w", err) } + } } if err := writer.Close(); err != nil { return nil, fmt.Errorf("close multipart writer: %w", err) @@ -118,15 +160,42 @@ func (a *MultipartAPI) SendTextFields(ctx context.Context, body *models.TextFiel if body != nil { buf := &bytes.Buffer{} writer := multipart.NewWriter(buf) - if err := writer.WriteField("enabled", strconv.FormatBool(body.Enabled)); err != nil { + { + partHeader := textproto.MIMEHeader{} + partHeader.Set("Content-Disposition", "form-data; name=\"enabled\"") + partHeader.Set("Content-Type", "text/plain") + partWriter, err := writer.CreatePart(partHeader) + if err != nil { + return nil, fmt.Errorf("create multipart part: %w", err) + } + if _, err := io.WriteString(partWriter, strconv.FormatBool(body.Enabled)); err != nil { return nil, fmt.Errorf("write multipart field: %w", err) } - if err := writer.WriteField("note", body.Note); err != nil { + } + { + partHeader := textproto.MIMEHeader{} + partHeader.Set("Content-Disposition", "form-data; name=\"note\"") + partHeader.Set("Content-Type", "text/plain") + partWriter, err := writer.CreatePart(partHeader) + if err != nil { + return nil, fmt.Errorf("create multipart part: %w", err) + } + if _, err := io.WriteString(partWriter, body.Note); err != nil { return nil, fmt.Errorf("write multipart field: %w", err) } - if err := writer.WriteField("retry_count", strconv.FormatInt(int64(body.RetryCount), 10)); err != nil { + } + { + partHeader := textproto.MIMEHeader{} + partHeader.Set("Content-Disposition", "form-data; name=\"retry_count\"") + partHeader.Set("Content-Type", "text/plain") + partWriter, err := writer.CreatePart(partHeader) + if err != nil { + return nil, fmt.Errorf("create multipart part: %w", err) + } + if _, err := io.WriteString(partWriter, strconv.FormatInt(int64(body.RetryCount), 10)); err != nil { return nil, fmt.Errorf("write multipart field: %w", err) } + } if err := writer.Close(); err != nil { return nil, fmt.Errorf("close multipart writer: %w", err) } diff --git a/tests/golden/go/go-http/multipart-explicit-encoding/README.md.golden b/tests/golden/go/go-http/multipart-explicit-encoding/README.md.golden new file mode 100644 index 000000000..b6b911f2e --- /dev/null +++ b/tests/golden/go/go-http/multipart-explicit-encoding/README.md.golden @@ -0,0 +1,7 @@ +# Multipart Explicit Encoding + +Covers explicit multipart part content types. + +Version: `1.0.0` + +Generated by [openapi-nexus](https://github.com/adamcavendish/openapi-nexus) for `multipart-explicit-encoding`. diff --git a/tests/golden/go/go-http/multipart-explicit-encoding/apis/transfer.go.golden b/tests/golden/go/go-http/multipart-explicit-encoding/apis/transfer.go.golden new file mode 100644 index 000000000..1838e17b3 --- /dev/null +++ b/tests/golden/go/go-http/multipart-explicit-encoding/apis/transfer.go.golden @@ -0,0 +1,128 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Multipart Explicit Encoding — 1.0.0 +// Covers explicit multipart part content types. + +package apis + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "mime/multipart" + "net/http" + "net/textproto" + + "example.com/sdk/models" + "example.com/sdk/runtime" +) + +// TransferAPI groups operations under the corresponding tag. +type TransferAPI struct { + client *runtime.Client +} + +// NewTransferAPI constructs a TransferAPI bound to client. +func NewTransferAPI(client *runtime.Client) *TransferAPI { + return &TransferAPI{client: client} +} + +// UploadEncodedAssetResponse carries the response from the corresponding operation. +type UploadEncodedAssetResponse struct { + StatusCode int + Raw *http.Response +} + +// UploadEncodedAsset calls POST /uploads/encoded. +func (a *TransferAPI) UploadEncodedAsset(ctx context.Context, body *models.UploadEncodedAssetRequest) (*UploadEncodedAssetResponse, error) { + path := "/uploads/encoded" + var bodyReader io.Reader + var multipartContentType string + if body != nil { + buf := &bytes.Buffer{} + writer := multipart.NewWriter(buf) + { + partHeader := textproto.MIMEHeader{} + partHeader.Set("Content-Disposition", "form-data; name=\"audit\"") + partHeader.Set("Content-Type", "application/json") + partWriter, err := writer.CreatePart(partHeader) + if err != nil { + return nil, fmt.Errorf("create multipart part: %w", err) + } + partValue, err := json.Marshal(body.Audit) + if err != nil { + return nil, fmt.Errorf("marshal multipart field: %w", err) + } + if _, err := partWriter.Write(partValue); err != nil { + return nil, fmt.Errorf("write multipart field: %w", err) + } + } + { + partHeader := textproto.MIMEHeader{} + partHeader.Set("Content-Disposition", "form-data; name=\"file\"; filename=\"file\"") + partHeader.Set("Content-Type", "application/pdf") + partWriter, err := writer.CreatePart(partHeader) + if err != nil { + return nil, fmt.Errorf("create multipart part: %w", err) + } + if _, err := partWriter.Write(body.File); err != nil { + return nil, fmt.Errorf("write multipart file: %w", err) + } + } + { + partHeader := textproto.MIMEHeader{} + partHeader.Set("Content-Disposition", "form-data; name=\"metadata\"") + partHeader.Set("Content-Type", "application/vnd.openapi-nexus.metadata+json") + partWriter, err := writer.CreatePart(partHeader) + if err != nil { + return nil, fmt.Errorf("create multipart part: %w", err) + } + partValue, err := json.Marshal(body.Metadata) + if err != nil { + return nil, fmt.Errorf("marshal multipart field: %w", err) + } + if _, err := partWriter.Write(partValue); err != nil { + return nil, fmt.Errorf("write multipart field: %w", err) + } + } + { + partHeader := textproto.MIMEHeader{} + partHeader.Set("Content-Disposition", "form-data; name=\"purpose\"") + partHeader.Set("Content-Type", "text/plain") + partWriter, err := writer.CreatePart(partHeader) + if err != nil { + return nil, fmt.Errorf("create multipart part: %w", err) + } + if _, err := io.WriteString(partWriter, body.Purpose); err != nil { + return nil, fmt.Errorf("write multipart field: %w", err) + } + } + if err := writer.Close(); err != nil { + return nil, fmt.Errorf("close multipart writer: %w", err) + } + bodyReader = buf + multipartContentType = writer.FormDataContentType() + } + req, err := a.client.NewRequest(ctx, "POST", path, nil, bodyReader) + if err != nil { + return nil, err + } + if multipartContentType != "" { + req.Header.Set("Content-Type", multipartContentType) + } + req.Header.Set("Accept", "application/json") + httpResp, err := a.client.Do(req) + if err != nil { + return nil, err + } + defer httpResp.Body.Close() + + resp := &UploadEncodedAssetResponse{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/multipart-explicit-encoding/go.mod.golden b/tests/golden/go/go-http/multipart-explicit-encoding/go.mod.golden new file mode 100644 index 000000000..54d0d116e --- /dev/null +++ b/tests/golden/go/go-http/multipart-explicit-encoding/go.mod.golden @@ -0,0 +1,3 @@ +module example.com/sdk + +go 1.21 diff --git a/tests/golden/go/go-http/multipart-explicit-encoding/models/audit_attributes.go.golden b/tests/golden/go/go-http/multipart-explicit-encoding/models/audit_attributes.go.golden new file mode 100644 index 000000000..eb0438610 --- /dev/null +++ b/tests/golden/go/go-http/multipart-explicit-encoding/models/audit_attributes.go.golden @@ -0,0 +1,11 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Multipart Explicit Encoding — 1.0.0 +// Covers explicit multipart part content types. + +package models + +type AuditAttributes struct { + RequestId string `json:"request_id"` + Reviewed *bool `json:"reviewed,omitempty"` +} diff --git a/tests/golden/go/go-http/multipart-explicit-encoding/models/upload_attributes.go.golden b/tests/golden/go/go-http/multipart-explicit-encoding/models/upload_attributes.go.golden new file mode 100644 index 000000000..b555593a5 --- /dev/null +++ b/tests/golden/go/go-http/multipart-explicit-encoding/models/upload_attributes.go.golden @@ -0,0 +1,11 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Multipart Explicit Encoding — 1.0.0 +// Covers explicit multipart part content types. + +package models + +type UploadAttributes struct { + Label string `json:"label"` + RetentionDays *int `json:"retention_days,omitempty"` +} diff --git a/tests/golden/go/go-http/multipart-explicit-encoding/models/upload_encoded_asset_request.go.golden b/tests/golden/go/go-http/multipart-explicit-encoding/models/upload_encoded_asset_request.go.golden new file mode 100644 index 000000000..2f417e564 --- /dev/null +++ b/tests/golden/go/go-http/multipart-explicit-encoding/models/upload_encoded_asset_request.go.golden @@ -0,0 +1,15 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Multipart Explicit Encoding — 1.0.0 +// Covers explicit multipart part content types. + +package models + +type UploadEncodedAssetRequest struct { + Audit AuditAttributes `json:"audit"` + // Binary content with an explicit content type + File []byte `json:"file"` + Metadata UploadAttributes `json:"metadata"` + // Plain-text upload purpose + Purpose string `json:"purpose"` +} diff --git a/tests/golden/go/go-http/multipart-explicit-encoding/runtime/auth.go.golden b/tests/golden/go/go-http/multipart-explicit-encoding/runtime/auth.go.golden new file mode 100644 index 000000000..500752db0 --- /dev/null +++ b/tests/golden/go/go-http/multipart-explicit-encoding/runtime/auth.go.golden @@ -0,0 +1,113 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Multipart Explicit Encoding — 1.0.0 +// Covers explicit multipart part content types. + +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/multipart-explicit-encoding/runtime/client.go.golden b/tests/golden/go/go-http/multipart-explicit-encoding/runtime/client.go.golden new file mode 100644 index 000000000..b1a5bb58e --- /dev/null +++ b/tests/golden/go/go-http/multipart-explicit-encoding/runtime/client.go.golden @@ -0,0 +1,99 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Multipart Explicit Encoding — 1.0.0 +// Covers explicit multipart part content types. + +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/multipart-explicit-encoding/runtime/errors.go.golden b/tests/golden/go/go-http/multipart-explicit-encoding/runtime/errors.go.golden new file mode 100644 index 000000000..01522464c --- /dev/null +++ b/tests/golden/go/go-http/multipart-explicit-encoding/runtime/errors.go.golden @@ -0,0 +1,31 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Multipart Explicit Encoding — 1.0.0 +// Covers explicit multipart part content types. + +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/go/go-http/multipart-nested-object-parts/apis/multipart.go.golden b/tests/golden/go/go-http/multipart-nested-object-parts/apis/multipart.go.golden index 9c61b44bd..89f0d065c 100644 --- a/tests/golden/go/go-http/multipart-nested-object-parts/apis/multipart.go.golden +++ b/tests/golden/go/go-http/multipart-nested-object-parts/apis/multipart.go.golden @@ -13,6 +13,7 @@ import ( "io" "mime/multipart" "net/http" + "net/textproto" "example.com/sdk/models" "example.com/sdk/runtime" @@ -42,20 +43,34 @@ func (a *MultipartAPI) SendNestedObjectPart(ctx context.Context, body *models.Ne if body != nil { buf := &bytes.Buffer{} writer := multipart.NewWriter(buf) - partWriter, err := writer.CreateFormFile("file", "file") + { + partHeader := textproto.MIMEHeader{} + partHeader.Set("Content-Disposition", "form-data; name=\"file\"; filename=\"file\"") + partHeader.Set("Content-Type", "application/octet-stream") + partWriter, err := writer.CreatePart(partHeader) if err != nil { - return nil, fmt.Errorf("create multipart file: %w", err) + return nil, fmt.Errorf("create multipart part: %w", err) } if _, err := partWriter.Write(body.File); err != nil { return nil, fmt.Errorf("write multipart file: %w", err) } + } + { + partHeader := textproto.MIMEHeader{} + partHeader.Set("Content-Disposition", "form-data; name=\"item_config\"") + partHeader.Set("Content-Type", "application/json") + partWriter, err := writer.CreatePart(partHeader) + if err != nil { + return nil, fmt.Errorf("create multipart part: %w", err) + } partValue, err := json.Marshal(body.ItemConfig) if err != nil { return nil, fmt.Errorf("marshal multipart field: %w", err) } - if err := writer.WriteField("item_config", string(partValue)); err != nil { + if _, err := partWriter.Write(partValue); err != nil { return nil, fmt.Errorf("write multipart field: %w", err) } + } if err := writer.Close(); err != nil { return nil, fmt.Errorf("close multipart writer: %w", err) } diff --git a/tests/golden/java/java-okhttp/binary-transfer-media-types/apis/TransferApi.java.golden b/tests/golden/java/java-okhttp/binary-transfer-media-types/apis/TransferApi.java.golden index 2d0eea4b6..3b90a2451 100644 --- a/tests/golden/java/java-okhttp/binary-transfer-media-types/apis/TransferApi.java.golden +++ b/tests/golden/java/java-okhttp/binary-transfer-media-types/apis/TransferApi.java.golden @@ -117,8 +117,8 @@ public class TransferApi { Request request; MultipartBody.Builder multipartBuilder = new MultipartBody.Builder().setType(MultipartBody.FORM); multipartBuilder.addFormDataPart("file", "file", RequestBody.create(body.getFile(), MediaType.get("application/octet-stream"))); - multipartBuilder.addFormDataPart("metadata", gson.toJson(body.getMetadata())); - multipartBuilder.addFormDataPart("purpose", String.valueOf(body.getPurpose())); + multipartBuilder.addFormDataPart("metadata", null, RequestBody.create(gson.toJson(body.getMetadata()), MediaType.get("application/json"))); + multipartBuilder.addFormDataPart("purpose", null, RequestBody.create(String.valueOf(body.getPurpose()), MediaType.get("text/plain"))); RequestBody multipartBody = multipartBuilder.build(); request = client.newRequestWithBody("POST", path, null, multipartBody); Response response = client.execute(request); diff --git a/tests/golden/java/java-okhttp/media-type-selection/apis/MediaApi.java.golden b/tests/golden/java/java-okhttp/media-type-selection/apis/MediaApi.java.golden index 9cfd3b2b5..a6790b7b0 100644 --- a/tests/golden/java/java-okhttp/media-type-selection/apis/MediaApi.java.golden +++ b/tests/golden/java/java-okhttp/media-type-selection/apis/MediaApi.java.golden @@ -205,7 +205,7 @@ public class MediaApi { MultipartBody.Builder multipartBuilder = new MultipartBody.Builder().setType(MultipartBody.FORM); multipartBuilder.addFormDataPart("file", "file", RequestBody.create(body.getFile(), MediaType.get("application/octet-stream"))); if (body.getNote() != null) { - multipartBuilder.addFormDataPart("note", String.valueOf(body.getNote())); + multipartBuilder.addFormDataPart("note", null, RequestBody.create(String.valueOf(body.getNote()), MediaType.get("text/plain"))); } RequestBody multipartBody = multipartBuilder.build(); request = client.newRequestWithBody("POST", path, null, multipartBody); diff --git a/tests/golden/java/java-okhttp/multipart-edge-cases/apis/MultipartApi.java.golden b/tests/golden/java/java-okhttp/multipart-edge-cases/apis/MultipartApi.java.golden index 3415771d4..42c95e271 100644 --- a/tests/golden/java/java-okhttp/multipart-edge-cases/apis/MultipartApi.java.golden +++ b/tests/golden/java/java-okhttp/multipart-edge-cases/apis/MultipartApi.java.golden @@ -86,19 +86,19 @@ public class MultipartApi { if (body != null) { MultipartBody.Builder multipartBuilder = new MultipartBody.Builder().setType(MultipartBody.FORM); if (body.getAttributes() != null) { - multipartBuilder.addFormDataPart("attributes", gson.toJson(body.getAttributes())); + multipartBuilder.addFormDataPart("attributes", null, RequestBody.create(gson.toJson(body.getAttributes()), MediaType.get("application/json"))); } if (body.getEnabled() != null) { - multipartBuilder.addFormDataPart("enabled", String.valueOf(body.getEnabled())); + multipartBuilder.addFormDataPart("enabled", null, RequestBody.create(String.valueOf(body.getEnabled()), MediaType.get("text/plain"))); } if (body.getFile() != null) { multipartBuilder.addFormDataPart("file", "file", RequestBody.create(body.getFile(), MediaType.get("application/octet-stream"))); } if (body.getRetryCount() != null) { - multipartBuilder.addFormDataPart("retry_count", String.valueOf(body.getRetryCount())); + multipartBuilder.addFormDataPart("retry_count", null, RequestBody.create(String.valueOf(body.getRetryCount()), MediaType.get("text/plain"))); } if (body.getTitle() != null) { - multipartBuilder.addFormDataPart("title", String.valueOf(body.getTitle())); + multipartBuilder.addFormDataPart("title", null, RequestBody.create(String.valueOf(body.getTitle()), MediaType.get("text/plain"))); } multipartBody = multipartBuilder.build(); } @@ -119,9 +119,9 @@ public class MultipartApi { String path = "/multipart/text-only"; Request request; MultipartBody.Builder multipartBuilder = new MultipartBody.Builder().setType(MultipartBody.FORM); - multipartBuilder.addFormDataPart("enabled", String.valueOf(body.getEnabled())); - multipartBuilder.addFormDataPart("note", String.valueOf(body.getNote())); - multipartBuilder.addFormDataPart("retry_count", String.valueOf(body.getRetryCount())); + multipartBuilder.addFormDataPart("enabled", null, RequestBody.create(String.valueOf(body.getEnabled()), MediaType.get("text/plain"))); + multipartBuilder.addFormDataPart("note", null, RequestBody.create(String.valueOf(body.getNote()), MediaType.get("text/plain"))); + multipartBuilder.addFormDataPart("retry_count", null, RequestBody.create(String.valueOf(body.getRetryCount()), MediaType.get("text/plain"))); RequestBody multipartBody = multipartBuilder.build(); request = client.newRequestWithBody("POST", path, null, multipartBody); Response response = client.execute(request); diff --git a/tests/golden/java/java-okhttp/multipart-explicit-encoding/README.md.golden b/tests/golden/java/java-okhttp/multipart-explicit-encoding/README.md.golden new file mode 100644 index 000000000..b6b911f2e --- /dev/null +++ b/tests/golden/java/java-okhttp/multipart-explicit-encoding/README.md.golden @@ -0,0 +1,7 @@ +# Multipart Explicit Encoding + +Covers explicit multipart part content types. + +Version: `1.0.0` + +Generated by [openapi-nexus](https://github.com/adamcavendish/openapi-nexus) for `multipart-explicit-encoding`. diff --git a/tests/golden/java/java-okhttp/multipart-explicit-encoding/apis/TransferApi.java.golden b/tests/golden/java/java-okhttp/multipart-explicit-encoding/apis/TransferApi.java.golden new file mode 100644 index 000000000..8a5837cdb --- /dev/null +++ b/tests/golden/java/java-okhttp/multipart-explicit-encoding/apis/TransferApi.java.golden @@ -0,0 +1,79 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Multipart Explicit Encoding — 1.0.0 +// Covers explicit multipart part content types. + +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.MultipartBody; +import okhttp3.Request; +import okhttp3.RequestBody; +import okhttp3.Response; + +/** + * UploadEncodedAssetResponse carries the response from uploadEncodedAsset. + */ +public class UploadEncodedAssetResponse { + private final int statusCode; + private final Response raw; + + public UploadEncodedAssetResponse(int statusCode, Response raw) { + this.statusCode = statusCode; + this.raw = raw; + } + + public int getStatusCode() { + return this.statusCode; + } + + public Response getRaw() { + return this.raw; + } +} + +/** + * TransferApi groups operations under the transfer tag. + */ +public class TransferApi { + private final ApiClient client; + private final Gson gson = new Gson(); + + public TransferApi(ApiClient client) { + this.client = client; + } + + /** + * uploadEncodedAsset POST /uploads/encoded. + */ + public UploadEncodedAssetResponse uploadEncodedAsset(UploadEncodedAssetRequest body) throws IOException { + String path = "/uploads/encoded"; + Request request; + MultipartBody.Builder multipartBuilder = new MultipartBody.Builder().setType(MultipartBody.FORM); + multipartBuilder.addFormDataPart("audit", null, RequestBody.create(gson.toJson(body.getAudit()), MediaType.get("application/json"))); + multipartBuilder.addFormDataPart("file", "file", RequestBody.create(body.getFile(), MediaType.get("application/pdf"))); + multipartBuilder.addFormDataPart("metadata", null, RequestBody.create(gson.toJson(body.getMetadata()), MediaType.get("application/vnd.openapi-nexus.metadata+json"))); + multipartBuilder.addFormDataPart("purpose", null, RequestBody.create(String.valueOf(body.getPurpose()), MediaType.get("text/plain"))); + RequestBody multipartBody = multipartBuilder.build(); + request = client.newRequestWithBody("POST", path, null, multipartBody); + 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 UploadEncodedAssetResponse(response.code(), response); + } +} diff --git a/tests/golden/java/java-okhttp/multipart-explicit-encoding/build.gradle.golden b/tests/golden/java/java-okhttp/multipart-explicit-encoding/build.gradle.golden new file mode 100644 index 000000000..ec7d84dad --- /dev/null +++ b/tests/golden/java/java-okhttp/multipart-explicit-encoding/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/multipart-explicit-encoding/models/AuditAttributes.java.golden b/tests/golden/java/java-okhttp/multipart-explicit-encoding/models/AuditAttributes.java.golden new file mode 100644 index 000000000..848732d54 --- /dev/null +++ b/tests/golden/java/java-okhttp/multipart-explicit-encoding/models/AuditAttributes.java.golden @@ -0,0 +1,27 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Multipart Explicit Encoding — 1.0.0 +// Covers explicit multipart part content types. + +package com.example.sdk.models; + +import com.google.gson.annotations.SerializedName; + +public class AuditAttributes { + @SerializedName("request_id") + private String requestId; + private Boolean reviewed; + + public AuditAttributes(String requestId, Boolean reviewed) { + this.requestId = requestId; + this.reviewed = reviewed; + } + + public String getRequestId() { + return this.requestId; + } + + public Boolean getReviewed() { + return this.reviewed; + } +} diff --git a/tests/golden/java/java-okhttp/multipart-explicit-encoding/models/UploadAttributes.java.golden b/tests/golden/java/java-okhttp/multipart-explicit-encoding/models/UploadAttributes.java.golden new file mode 100644 index 000000000..b8ac82a3e --- /dev/null +++ b/tests/golden/java/java-okhttp/multipart-explicit-encoding/models/UploadAttributes.java.golden @@ -0,0 +1,27 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Multipart Explicit Encoding — 1.0.0 +// Covers explicit multipart part content types. + +package com.example.sdk.models; + +import com.google.gson.annotations.SerializedName; + +public class UploadAttributes { + private String label; + @SerializedName("retention_days") + private Integer retentionDays; + + public UploadAttributes(String label, Integer retentionDays) { + this.label = label; + this.retentionDays = retentionDays; + } + + public String getLabel() { + return this.label; + } + + public Integer getRetentionDays() { + return this.retentionDays; + } +} diff --git a/tests/golden/java/java-okhttp/multipart-explicit-encoding/models/UploadEncodedAssetRequest.java.golden b/tests/golden/java/java-okhttp/multipart-explicit-encoding/models/UploadEncodedAssetRequest.java.golden new file mode 100644 index 000000000..fc69adfb9 --- /dev/null +++ b/tests/golden/java/java-okhttp/multipart-explicit-encoding/models/UploadEncodedAssetRequest.java.golden @@ -0,0 +1,37 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Multipart Explicit Encoding — 1.0.0 +// Covers explicit multipart part content types. + +package com.example.sdk.models; + +public class UploadEncodedAssetRequest { + private AuditAttributes audit; + private byte[] file; + private UploadAttributes metadata; + private String purpose; + + public UploadEncodedAssetRequest(AuditAttributes audit, byte[] file, UploadAttributes metadata, + String purpose) { + this.audit = audit; + this.file = file; + this.metadata = metadata; + this.purpose = purpose; + } + + public AuditAttributes getAudit() { + return this.audit; + } + + public byte[] getFile() { + return this.file; + } + + public UploadAttributes getMetadata() { + return this.metadata; + } + + public String getPurpose() { + return this.purpose; + } +} diff --git a/tests/golden/java/java-okhttp/multipart-explicit-encoding/runtime/ApiClient.java.golden b/tests/golden/java/java-okhttp/multipart-explicit-encoding/runtime/ApiClient.java.golden new file mode 100644 index 000000000..a8030a1bf --- /dev/null +++ b/tests/golden/java/java-okhttp/multipart-explicit-encoding/runtime/ApiClient.java.golden @@ -0,0 +1,79 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Multipart Explicit Encoding — 1.0.0 +// Covers explicit multipart part content types. + +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/multipart-explicit-encoding/runtime/ApiException.java.golden b/tests/golden/java/java-okhttp/multipart-explicit-encoding/runtime/ApiException.java.golden new file mode 100644 index 000000000..92a3ee0ab --- /dev/null +++ b/tests/golden/java/java-okhttp/multipart-explicit-encoding/runtime/ApiException.java.golden @@ -0,0 +1,31 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Multipart Explicit Encoding — 1.0.0 +// Covers explicit multipart part content types. + +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/multipart-explicit-encoding/runtime/ApiKeyAuth.java.golden b/tests/golden/java/java-okhttp/multipart-explicit-encoding/runtime/ApiKeyAuth.java.golden new file mode 100644 index 000000000..727f22dc4 --- /dev/null +++ b/tests/golden/java/java-okhttp/multipart-explicit-encoding/runtime/ApiKeyAuth.java.golden @@ -0,0 +1,42 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Multipart Explicit Encoding — 1.0.0 +// Covers explicit multipart part content types. + +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/multipart-explicit-encoding/runtime/ApiKeyLocation.java.golden b/tests/golden/java/java-okhttp/multipart-explicit-encoding/runtime/ApiKeyLocation.java.golden new file mode 100644 index 000000000..ba7a7976d --- /dev/null +++ b/tests/golden/java/java-okhttp/multipart-explicit-encoding/runtime/ApiKeyLocation.java.golden @@ -0,0 +1,11 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Multipart Explicit Encoding — 1.0.0 +// Covers explicit multipart part content types. + +package com.example.sdk.runtime; + +public enum ApiKeyLocation { + HEADER, + QUERY +} diff --git a/tests/golden/java/java-okhttp/multipart-explicit-encoding/runtime/Authenticator.java.golden b/tests/golden/java/java-okhttp/multipart-explicit-encoding/runtime/Authenticator.java.golden new file mode 100644 index 000000000..c6a738aef --- /dev/null +++ b/tests/golden/java/java-okhttp/multipart-explicit-encoding/runtime/Authenticator.java.golden @@ -0,0 +1,12 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Multipart Explicit Encoding — 1.0.0 +// Covers explicit multipart part content types. + +package com.example.sdk.runtime; + +import okhttp3.Request; + +public interface Authenticator { + void authenticate(Request.Builder builder); +} diff --git a/tests/golden/java/java-okhttp/multipart-explicit-encoding/runtime/BearerAuth.java.golden b/tests/golden/java/java-okhttp/multipart-explicit-encoding/runtime/BearerAuth.java.golden new file mode 100644 index 000000000..930d6a540 --- /dev/null +++ b/tests/golden/java/java-okhttp/multipart-explicit-encoding/runtime/BearerAuth.java.golden @@ -0,0 +1,30 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Multipart Explicit Encoding — 1.0.0 +// Covers explicit multipart part content types. + +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/java/java-okhttp/multipart-nested-object-parts/apis/MultipartApi.java.golden b/tests/golden/java/java-okhttp/multipart-nested-object-parts/apis/MultipartApi.java.golden index 1936b2ca7..02bae9ecb 100644 --- a/tests/golden/java/java-okhttp/multipart-nested-object-parts/apis/MultipartApi.java.golden +++ b/tests/golden/java/java-okhttp/multipart-nested-object-parts/apis/MultipartApi.java.golden @@ -63,7 +63,7 @@ public class MultipartApi { Request request; MultipartBody.Builder multipartBuilder = new MultipartBody.Builder().setType(MultipartBody.FORM); multipartBuilder.addFormDataPart("file", "file", RequestBody.create(body.getFile(), MediaType.get("application/octet-stream"))); - multipartBuilder.addFormDataPart("item_config", gson.toJson(body.getItemConfig())); + multipartBuilder.addFormDataPart("item_config", null, RequestBody.create(gson.toJson(body.getItemConfig()), MediaType.get("application/json"))); RequestBody multipartBody = multipartBuilder.build(); request = client.newRequestWithBody("POST", path, null, multipartBody); Response response = client.execute(request); diff --git a/tests/golden/kotlin/kotlin-okhttp/binary-transfer-media-types/apis/TransferApi.kt.golden b/tests/golden/kotlin/kotlin-okhttp/binary-transfer-media-types/apis/TransferApi.kt.golden index 796954050..089f9cb81 100644 --- a/tests/golden/kotlin/kotlin-okhttp/binary-transfer-media-types/apis/TransferApi.kt.golden +++ b/tests/golden/kotlin/kotlin-okhttp/binary-transfer-media-types/apis/TransferApi.kt.golden @@ -61,8 +61,11 @@ class TransferApi(private val client: ApiClient) { val path = "/uploads" val multipartBuilder = MultipartBody.Builder().setType(MultipartBody.FORM) multipartBuilder.addFormDataPart("file", "file", body.file.toRequestBody("application/octet-stream".toMediaType())) - multipartBuilder.addFormDataPart("metadata", gson.toJson(body.metadata)) - multipartBuilder.addFormDataPart("purpose", body.purpose.toString()) + + multipartBuilder.addFormDataPart("metadata", null, gson.toJson(body.metadata).toRequestBody("application/json".toMediaType())) + + multipartBuilder.addFormDataPart("purpose", null, body.purpose.toString().toRequestBody("text/plain".toMediaType())) + val multipartBody = multipartBuilder.build() val request = client.newRequestWithBody("POST", path, null, multipartBody) val response = client.execute(request) diff --git a/tests/golden/kotlin/kotlin-okhttp/media-type-selection/apis/MediaApi.kt.golden b/tests/golden/kotlin/kotlin-okhttp/media-type-selection/apis/MediaApi.kt.golden index 82be6646b..9b33fc60a 100644 --- a/tests/golden/kotlin/kotlin-okhttp/media-type-selection/apis/MediaApi.kt.golden +++ b/tests/golden/kotlin/kotlin-okhttp/media-type-selection/apis/MediaApi.kt.golden @@ -84,8 +84,10 @@ class MediaApi(private val client: ApiClient) { val path = "/request/parameterized-multipart" val multipartBuilder = MultipartBody.Builder().setType(MultipartBody.FORM) multipartBuilder.addFormDataPart("file", "file", body.file.toRequestBody("application/octet-stream".toMediaType())) + if (body.note != null) { - multipartBuilder.addFormDataPart("note", body.note.toString()) + multipartBuilder.addFormDataPart("note", null, body.note.toString().toRequestBody("text/plain".toMediaType())) + } val multipartBody = multipartBuilder.build() val request = client.newRequestWithBody("POST", path, null, multipartBody) diff --git a/tests/golden/kotlin/kotlin-okhttp/multipart-edge-cases/apis/MultipartApi.kt.golden b/tests/golden/kotlin/kotlin-okhttp/multipart-edge-cases/apis/MultipartApi.kt.golden index e51414e14..c8ed5d22a 100644 --- a/tests/golden/kotlin/kotlin-okhttp/multipart-edge-cases/apis/MultipartApi.kt.golden +++ b/tests/golden/kotlin/kotlin-okhttp/multipart-edge-cases/apis/MultipartApi.kt.golden @@ -42,19 +42,24 @@ class MultipartApi(private val client: ApiClient) { if (body != null) { val multipartBuilder = MultipartBody.Builder().setType(MultipartBody.FORM) if (body.attributes != null) { - multipartBuilder.addFormDataPart("attributes", gson.toJson(body.attributes)) + multipartBuilder.addFormDataPart("attributes", null, gson.toJson(body.attributes).toRequestBody("application/json".toMediaType())) + } if (body.enabled != null) { - multipartBuilder.addFormDataPart("enabled", body.enabled.toString()) + multipartBuilder.addFormDataPart("enabled", null, body.enabled.toString().toRequestBody("text/plain".toMediaType())) + } if (body.file != null) { multipartBuilder.addFormDataPart("file", "file", body.file.toRequestBody("application/octet-stream".toMediaType())) + } if (body.retryCount != null) { - multipartBuilder.addFormDataPart("retry_count", body.retryCount.toString()) + multipartBuilder.addFormDataPart("retry_count", null, body.retryCount.toString().toRequestBody("text/plain".toMediaType())) + } if (body.title != null) { - multipartBuilder.addFormDataPart("title", body.title.toString()) + multipartBuilder.addFormDataPart("title", null, body.title.toString().toRequestBody("text/plain".toMediaType())) + } multipartBody = multipartBuilder.build() } @@ -74,9 +79,12 @@ class MultipartApi(private val client: ApiClient) { fun sendTextFields(body: TextFields): SendTextFieldsResponse { val path = "/multipart/text-only" val multipartBuilder = MultipartBody.Builder().setType(MultipartBody.FORM) - multipartBuilder.addFormDataPart("enabled", body.enabled.toString()) - multipartBuilder.addFormDataPart("note", body.note.toString()) - multipartBuilder.addFormDataPart("retry_count", body.retryCount.toString()) + multipartBuilder.addFormDataPart("enabled", null, body.enabled.toString().toRequestBody("text/plain".toMediaType())) + + multipartBuilder.addFormDataPart("note", null, body.note.toString().toRequestBody("text/plain".toMediaType())) + + multipartBuilder.addFormDataPart("retry_count", null, body.retryCount.toString().toRequestBody("text/plain".toMediaType())) + val multipartBody = multipartBuilder.build() val request = client.newRequestWithBody("POST", path, null, multipartBody) val response = client.execute(request) diff --git a/tests/golden/kotlin/kotlin-okhttp/multipart-explicit-encoding/README.md.golden b/tests/golden/kotlin/kotlin-okhttp/multipart-explicit-encoding/README.md.golden new file mode 100644 index 000000000..b6b911f2e --- /dev/null +++ b/tests/golden/kotlin/kotlin-okhttp/multipart-explicit-encoding/README.md.golden @@ -0,0 +1,7 @@ +# Multipart Explicit Encoding + +Covers explicit multipart part content types. + +Version: `1.0.0` + +Generated by [openapi-nexus](https://github.com/adamcavendish/openapi-nexus) for `multipart-explicit-encoding`. diff --git a/tests/golden/kotlin/kotlin-okhttp/multipart-explicit-encoding/apis/TransferApi.kt.golden b/tests/golden/kotlin/kotlin-okhttp/multipart-explicit-encoding/apis/TransferApi.kt.golden new file mode 100644 index 000000000..d78f87834 --- /dev/null +++ b/tests/golden/kotlin/kotlin-okhttp/multipart-explicit-encoding/apis/TransferApi.kt.golden @@ -0,0 +1,54 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Multipart Explicit Encoding — 1.0.0 +// Covers explicit multipart part content types. + +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.MultipartBody +import okhttp3.RequestBody.Companion.toRequestBody +import okhttp3.Response + +/** + * UploadEncodedAssetResponse carries the response from uploadEncodedAsset. + */ +data class UploadEncodedAssetResponse(val statusCode: Int, val raw: Response) { +} + +/** + * TransferApi groups operations under the transfer tag. + */ +class TransferApi(private val client: ApiClient) { + private val gson: Gson = Gson() + + /** + * uploadEncodedAsset POST /uploads/encoded. + */ + fun uploadEncodedAsset(body: UploadEncodedAssetRequest): UploadEncodedAssetResponse { + val path = "/uploads/encoded" + val multipartBuilder = MultipartBody.Builder().setType(MultipartBody.FORM) + multipartBuilder.addFormDataPart("audit", null, gson.toJson(body.audit).toRequestBody("application/json".toMediaType())) + + multipartBuilder.addFormDataPart("file", "file", body.file.toRequestBody("application/pdf".toMediaType())) + + multipartBuilder.addFormDataPart("metadata", null, gson.toJson(body.metadata).toRequestBody("application/vnd.openapi-nexus.metadata+json".toMediaType())) + + multipartBuilder.addFormDataPart("purpose", null, body.purpose.toString().toRequestBody("text/plain".toMediaType())) + + val multipartBody = multipartBuilder.build() + val request = client.newRequestWithBody("POST", path, null, multipartBody) + val response = client.execute(request) + + if (!response.isSuccessful) { + val errorBody = response.body?.string() ?: "" + throw ApiException(response.code, response.message, errorBody) + } + return UploadEncodedAssetResponse(statusCode = response.code, raw = response) + } +} diff --git a/tests/golden/kotlin/kotlin-okhttp/multipart-explicit-encoding/build.gradle.kts.golden b/tests/golden/kotlin/kotlin-okhttp/multipart-explicit-encoding/build.gradle.kts.golden new file mode 100644 index 000000000..5c6106455 --- /dev/null +++ b/tests/golden/kotlin/kotlin-okhttp/multipart-explicit-encoding/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/multipart-explicit-encoding/models/AuditAttributes.kt.golden b/tests/golden/kotlin/kotlin-okhttp/multipart-explicit-encoding/models/AuditAttributes.kt.golden new file mode 100644 index 000000000..eee98d6c6 --- /dev/null +++ b/tests/golden/kotlin/kotlin-okhttp/multipart-explicit-encoding/models/AuditAttributes.kt.golden @@ -0,0 +1,12 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Multipart Explicit Encoding — 1.0.0 +// Covers explicit multipart part content types. + +package com.example.sdk.models + +import com.google.gson.annotations.SerializedName + +data class AuditAttributes(@SerializedName("request_id") val requestId: String, +val reviewed: Boolean? = null) { +} diff --git a/tests/golden/kotlin/kotlin-okhttp/multipart-explicit-encoding/models/UploadAttributes.kt.golden b/tests/golden/kotlin/kotlin-okhttp/multipart-explicit-encoding/models/UploadAttributes.kt.golden new file mode 100644 index 000000000..8dc88e3a6 --- /dev/null +++ b/tests/golden/kotlin/kotlin-okhttp/multipart-explicit-encoding/models/UploadAttributes.kt.golden @@ -0,0 +1,12 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Multipart Explicit Encoding — 1.0.0 +// Covers explicit multipart part content types. + +package com.example.sdk.models + +import com.google.gson.annotations.SerializedName + +data class UploadAttributes(val label: String, +@SerializedName("retention_days") val retentionDays: Int? = null) { +} diff --git a/tests/golden/kotlin/kotlin-okhttp/multipart-explicit-encoding/models/UploadEncodedAssetRequest.kt.golden b/tests/golden/kotlin/kotlin-okhttp/multipart-explicit-encoding/models/UploadEncodedAssetRequest.kt.golden new file mode 100644 index 000000000..f328c9459 --- /dev/null +++ b/tests/golden/kotlin/kotlin-okhttp/multipart-explicit-encoding/models/UploadEncodedAssetRequest.kt.golden @@ -0,0 +1,10 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Multipart Explicit Encoding — 1.0.0 +// Covers explicit multipart part content types. + +package com.example.sdk.models + +data class UploadEncodedAssetRequest(val audit: AuditAttributes, val file: ByteArray, +val metadata: UploadAttributes, val purpose: String) { +} diff --git a/tests/golden/kotlin/kotlin-okhttp/multipart-explicit-encoding/runtime/ApiClient.kt.golden b/tests/golden/kotlin/kotlin-okhttp/multipart-explicit-encoding/runtime/ApiClient.kt.golden new file mode 100644 index 000000000..0d22392dd --- /dev/null +++ b/tests/golden/kotlin/kotlin-okhttp/multipart-explicit-encoding/runtime/ApiClient.kt.golden @@ -0,0 +1,62 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Multipart Explicit Encoding — 1.0.0 +// Covers explicit multipart part content types. + +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/multipart-explicit-encoding/runtime/ApiException.kt.golden b/tests/golden/kotlin/kotlin-okhttp/multipart-explicit-encoding/runtime/ApiException.kt.golden new file mode 100644 index 000000000..e74f4c469 --- /dev/null +++ b/tests/golden/kotlin/kotlin-okhttp/multipart-explicit-encoding/runtime/ApiException.kt.golden @@ -0,0 +1,12 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Multipart Explicit Encoding — 1.0.0 +// Covers explicit multipart part content types. + +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/multipart-explicit-encoding/runtime/Auth.kt.golden b/tests/golden/kotlin/kotlin-okhttp/multipart-explicit-encoding/runtime/Auth.kt.golden new file mode 100644 index 000000000..f5fd02dbd --- /dev/null +++ b/tests/golden/kotlin/kotlin-okhttp/multipart-explicit-encoding/runtime/Auth.kt.golden @@ -0,0 +1,71 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Multipart Explicit Encoding — 1.0.0 +// Covers explicit multipart part content types. + +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/kotlin/kotlin-okhttp/multipart-nested-object-parts/apis/MultipartApi.kt.golden b/tests/golden/kotlin/kotlin-okhttp/multipart-nested-object-parts/apis/MultipartApi.kt.golden index 3bdc940fa..fff9ca5b6 100644 --- a/tests/golden/kotlin/kotlin-okhttp/multipart-nested-object-parts/apis/MultipartApi.kt.golden +++ b/tests/golden/kotlin/kotlin-okhttp/multipart-nested-object-parts/apis/MultipartApi.kt.golden @@ -34,7 +34,9 @@ class MultipartApi(private val client: ApiClient) { val path = "/multipart/nested-object" val multipartBuilder = MultipartBody.Builder().setType(MultipartBody.FORM) multipartBuilder.addFormDataPart("file", "file", body.file.toRequestBody("application/octet-stream".toMediaType())) - multipartBuilder.addFormDataPart("item_config", gson.toJson(body.itemConfig)) + + multipartBuilder.addFormDataPart("item_config", null, gson.toJson(body.itemConfig).toRequestBody("application/json".toMediaType())) + val multipartBody = multipartBuilder.build() val request = client.newRequestWithBody("POST", path, null, multipartBody) val response = client.execute(request) diff --git a/tests/golden/python/python-httpx/binary-transfer-media-types/binary_transfer_media_types/apis/transfer_api.py.golden b/tests/golden/python/python-httpx/binary-transfer-media-types/binary_transfer_media_types/apis/transfer_api.py.golden index 3d240f985..0a39e3cac 100644 --- a/tests/golden/python/python-httpx/binary-transfer-media-types/binary_transfer_media_types/apis/transfer_api.py.golden +++ b/tests/golden/python/python-httpx/binary-transfer-media-types/binary_transfer_media_types/apis/transfer_api.py.golden @@ -28,7 +28,7 @@ class TransferApi: files: dict[str, object] = {} files["file"] = ("file", body.file, "application/octet-stream") files["metadata"] = (None, json.dumps(body.metadata.to_dict()), "application/json") - files["purpose"] = (None, str(body.purpose)) + files["purpose"] = (None, str(body.purpose), "text/plain") response = self._client.request("POST", path, files=files if files else None) if response.status_code >= 400: raise ApiError(response.status_code, response.reason_phrase, response.content) diff --git a/tests/golden/python/python-httpx/media-type-selection/media_type_selection/apis/media_api.py.golden b/tests/golden/python/python-httpx/media-type-selection/media_type_selection/apis/media_api.py.golden index 3f86321a2..63ee3c0aa 100644 --- a/tests/golden/python/python-httpx/media-type-selection/media_type_selection/apis/media_api.py.golden +++ b/tests/golden/python/python-httpx/media-type-selection/media_type_selection/apis/media_api.py.golden @@ -28,7 +28,7 @@ class MediaApi: files: dict[str, object] = {} files["file"] = ("file", body.file, "application/octet-stream") if body.note is not None: - files["note"] = (None, str(body.note)) + files["note"] = (None, str(body.note), "text/plain") response = self._client.request("POST", path, files=files if files else None) if response.status_code >= 400: diff --git a/tests/golden/python/python-httpx/multipart-edge-cases/multipart_edge_cases/apis/multipart_api.py.golden b/tests/golden/python/python-httpx/multipart-edge-cases/multipart_edge_cases/apis/multipart_api.py.golden index f7c2bca77..6340a1311 100644 --- a/tests/golden/python/python-httpx/multipart-edge-cases/multipart_edge_cases/apis/multipart_api.py.golden +++ b/tests/golden/python/python-httpx/multipart-edge-cases/multipart_edge_cases/apis/multipart_api.py.golden @@ -24,16 +24,16 @@ class MultipartApi: files["attributes"] = (None, json.dumps(body.attributes.to_dict()), "application/json") if body.enabled is not None: - files["enabled"] = (None, str(body.enabled)) + files["enabled"] = (None, str(body.enabled), "text/plain") if body.file is not None: files["file"] = ("file", body.file, "application/octet-stream") if body.retry_count is not None: - files["retry_count"] = (None, str(body.retry_count)) + files["retry_count"] = (None, str(body.retry_count), "text/plain") if body.title is not None: - files["title"] = (None, str(body.title)) + files["title"] = (None, str(body.title), "text/plain") response = self._client.request("POST", path, files=files if files else None) @@ -44,9 +44,9 @@ class MultipartApi: def send_text_fields(self, *, body: TextFields) -> None: path = "/multipart/text-only" files: dict[str, object] = {} - files["enabled"] = (None, str(body.enabled)) - files["note"] = (None, str(body.note)) - files["retry_count"] = (None, str(body.retry_count)) + files["enabled"] = (None, str(body.enabled), "text/plain") + files["note"] = (None, str(body.note), "text/plain") + files["retry_count"] = (None, str(body.retry_count), "text/plain") response = self._client.request("POST", path, files=files if files else None) if response.status_code >= 400: raise ApiError(response.status_code, response.reason_phrase, response.content) diff --git a/tests/golden/python/python-httpx/multipart-explicit-encoding/README.md.golden b/tests/golden/python/python-httpx/multipart-explicit-encoding/README.md.golden new file mode 100644 index 000000000..5270c1b8d --- /dev/null +++ b/tests/golden/python/python-httpx/multipart-explicit-encoding/README.md.golden @@ -0,0 +1,7 @@ +# Multipart Explicit Encoding + +Covers explicit multipart part content types. + +Version: `1.0.0` + +Generated by [openapi-nexus](https://github.com/adamcavendish/openapi-nexus) for `multipart_explicit_encoding`. diff --git a/tests/golden/python/python-httpx/multipart-explicit-encoding/multipart_explicit_encoding/__init__.py.golden b/tests/golden/python/python-httpx/multipart-explicit-encoding/multipart_explicit_encoding/__init__.py.golden new file mode 100644 index 000000000..94b1350f1 --- /dev/null +++ b/tests/golden/python/python-httpx/multipart-explicit-encoding/multipart_explicit_encoding/__init__.py.golden @@ -0,0 +1,10 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Multipart Explicit Encoding — 1.0.0 +# Covers explicit multipart part content types. + +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/multipart-explicit-encoding/multipart_explicit_encoding/apis/__init__.py.golden b/tests/golden/python/python-httpx/multipart-explicit-encoding/multipart_explicit_encoding/apis/__init__.py.golden new file mode 100644 index 000000000..01c26b214 --- /dev/null +++ b/tests/golden/python/python-httpx/multipart-explicit-encoding/multipart_explicit_encoding/apis/__init__.py.golden @@ -0,0 +1,6 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Multipart Explicit Encoding — 1.0.0 +# Covers explicit multipart part content types. + +from .transfer_api import TransferApi as TransferApi diff --git a/tests/golden/python/python-httpx/multipart-explicit-encoding/multipart_explicit_encoding/apis/transfer_api.py.golden b/tests/golden/python/python-httpx/multipart-explicit-encoding/multipart_explicit_encoding/apis/transfer_api.py.golden new file mode 100644 index 000000000..476fe25d5 --- /dev/null +++ b/tests/golden/python/python-httpx/multipart-explicit-encoding/multipart_explicit_encoding/apis/transfer_api.py.golden @@ -0,0 +1,28 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Multipart Explicit Encoding — 1.0.0 +# Covers explicit multipart part content types. + +from __future__ import annotations + +import json + +from ..models.upload_encoded_asset_request import UploadEncodedAssetRequest +from ..runtime.client import Client +from ..runtime.errors import ApiError + +class TransferApi: + def __init__(self, client: Client) -> None: + self._client = client + + def upload_encoded_asset(self, *, body: UploadEncodedAssetRequest) -> None: + path = "/uploads/encoded" + files: dict[str, object] = {} + files["audit"] = (None, json.dumps(body.audit.to_dict()), "application/json") + files["file"] = ("file", body.file, "application/pdf") + files["metadata"] = (None, json.dumps(body.metadata.to_dict()), "application/vnd.openapi-nexus.metadata+json") + files["purpose"] = (None, str(body.purpose), "text/plain") + response = self._client.request("POST", path, files=files if files else None) + 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/multipart-explicit-encoding/multipart_explicit_encoding/models/__init__.py.golden b/tests/golden/python/python-httpx/multipart-explicit-encoding/multipart_explicit_encoding/models/__init__.py.golden new file mode 100644 index 000000000..81e7b8856 --- /dev/null +++ b/tests/golden/python/python-httpx/multipart-explicit-encoding/multipart_explicit_encoding/models/__init__.py.golden @@ -0,0 +1,8 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Multipart Explicit Encoding — 1.0.0 +# Covers explicit multipart part content types. + +from .audit_attributes import AuditAttributes as AuditAttributes +from .upload_attributes import UploadAttributes as UploadAttributes +from .upload_encoded_asset_request import UploadEncodedAssetRequest as UploadEncodedAssetRequest diff --git a/tests/golden/python/python-httpx/multipart-explicit-encoding/multipart_explicit_encoding/models/audit_attributes.py.golden b/tests/golden/python/python-httpx/multipart-explicit-encoding/multipart_explicit_encoding/models/audit_attributes.py.golden new file mode 100644 index 000000000..d68d1e654 --- /dev/null +++ b/tests/golden/python/python-httpx/multipart-explicit-encoding/multipart_explicit_encoding/models/audit_attributes.py.golden @@ -0,0 +1,27 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Multipart Explicit Encoding — 1.0.0 +# Covers explicit multipart part content types. + +from __future__ import annotations + +from dataclasses import dataclass + +@dataclass +class AuditAttributes: + request_id: str + reviewed: bool | None = None + + def to_dict(self) -> dict[str, object]: + result: dict[str, object] = {} + result["request_id"] = self.request_id + if self.reviewed is not None: + result["reviewed"] = self.reviewed + return result + + @classmethod + def from_dict(cls, data: dict[str, object]) -> AuditAttributes: + return cls( + request_id=data["request_id"], # type: ignore[assignment] + reviewed=data.get("reviewed"), # type: ignore[assignment] + ) diff --git a/tests/golden/python/python-httpx/multipart-explicit-encoding/multipart_explicit_encoding/models/upload_attributes.py.golden b/tests/golden/python/python-httpx/multipart-explicit-encoding/multipart_explicit_encoding/models/upload_attributes.py.golden new file mode 100644 index 000000000..b5c514b73 --- /dev/null +++ b/tests/golden/python/python-httpx/multipart-explicit-encoding/multipart_explicit_encoding/models/upload_attributes.py.golden @@ -0,0 +1,27 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Multipart Explicit Encoding — 1.0.0 +# Covers explicit multipart part content types. + +from __future__ import annotations + +from dataclasses import dataclass + +@dataclass +class UploadAttributes: + label: str + retention_days: int | None = None + + def to_dict(self) -> dict[str, object]: + result: dict[str, object] = {} + result["label"] = self.label + if self.retention_days is not None: + result["retention_days"] = self.retention_days + return result + + @classmethod + def from_dict(cls, data: dict[str, object]) -> UploadAttributes: + return cls( + label=data["label"], # type: ignore[assignment] + retention_days=data.get("retention_days"), # type: ignore[assignment] + ) diff --git a/tests/golden/python/python-httpx/multipart-explicit-encoding/multipart_explicit_encoding/models/upload_encoded_asset_request.py.golden b/tests/golden/python/python-httpx/multipart-explicit-encoding/multipart_explicit_encoding/models/upload_encoded_asset_request.py.golden new file mode 100644 index 000000000..f29f19620 --- /dev/null +++ b/tests/golden/python/python-httpx/multipart-explicit-encoding/multipart_explicit_encoding/models/upload_encoded_asset_request.py.golden @@ -0,0 +1,35 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Multipart Explicit Encoding — 1.0.0 +# Covers explicit multipart part content types. + +from __future__ import annotations + +from dataclasses import dataclass + +from .audit_attributes import AuditAttributes +from .upload_attributes import UploadAttributes + +@dataclass +class UploadEncodedAssetRequest: + audit: AuditAttributes + file: bytes + metadata: UploadAttributes + purpose: str + + def to_dict(self) -> dict[str, object]: + result: dict[str, object] = {} + result["audit"] = self.audit.to_dict() + result["file"] = self.file + result["metadata"] = self.metadata.to_dict() + result["purpose"] = self.purpose + return result + + @classmethod + def from_dict(cls, data: dict[str, object]) -> UploadEncodedAssetRequest: + return cls( + audit=AuditAttributes.from_dict(data["audit"]), # type: ignore[arg-type] + file=data["file"], # type: ignore[assignment] + metadata=UploadAttributes.from_dict(data["metadata"]), # type: ignore[arg-type] + purpose=data["purpose"], # type: ignore[assignment] + ) diff --git a/tests/golden/python/python-httpx/multipart-explicit-encoding/multipart_explicit_encoding/py.typed.golden b/tests/golden/python/python-httpx/multipart-explicit-encoding/multipart_explicit_encoding/py.typed.golden new file mode 100644 index 000000000..e69de29bb diff --git a/tests/golden/python/python-httpx/multipart-explicit-encoding/multipart_explicit_encoding/runtime/__init__.py.golden b/tests/golden/python/python-httpx/multipart-explicit-encoding/multipart_explicit_encoding/runtime/__init__.py.golden new file mode 100644 index 000000000..579bfa8db --- /dev/null +++ b/tests/golden/python/python-httpx/multipart-explicit-encoding/multipart_explicit_encoding/runtime/__init__.py.golden @@ -0,0 +1,10 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Multipart Explicit Encoding — 1.0.0 +# Covers explicit multipart part content types. + +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/multipart-explicit-encoding/multipart_explicit_encoding/runtime/auth.py.golden b/tests/golden/python/python-httpx/multipart-explicit-encoding/multipart_explicit_encoding/runtime/auth.py.golden new file mode 100644 index 000000000..a822af2c0 --- /dev/null +++ b/tests/golden/python/python-httpx/multipart-explicit-encoding/multipart_explicit_encoding/runtime/auth.py.golden @@ -0,0 +1,51 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Multipart Explicit Encoding — 1.0.0 +# Covers explicit multipart part content types. + +"""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/multipart-explicit-encoding/multipart_explicit_encoding/runtime/client.py.golden b/tests/golden/python/python-httpx/multipart-explicit-encoding/multipart_explicit_encoding/runtime/client.py.golden new file mode 100644 index 000000000..2ade35069 --- /dev/null +++ b/tests/golden/python/python-httpx/multipart-explicit-encoding/multipart_explicit_encoding/runtime/client.py.golden @@ -0,0 +1,71 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Multipart Explicit Encoding — 1.0.0 +# Covers explicit multipart part content types. + +"""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/multipart-explicit-encoding/multipart_explicit_encoding/runtime/errors.py.golden b/tests/golden/python/python-httpx/multipart-explicit-encoding/multipart_explicit_encoding/runtime/errors.py.golden new file mode 100644 index 000000000..8ab65f02b --- /dev/null +++ b/tests/golden/python/python-httpx/multipart-explicit-encoding/multipart_explicit_encoding/runtime/errors.py.golden @@ -0,0 +1,18 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Multipart Explicit Encoding — 1.0.0 +# Covers explicit multipart part content types. + +"""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/multipart-explicit-encoding/pyproject.toml.golden b/tests/golden/python/python-httpx/multipart-explicit-encoding/pyproject.toml.golden new file mode 100644 index 000000000..1195e5e13 --- /dev/null +++ b/tests/golden/python/python-httpx/multipart-explicit-encoding/pyproject.toml.golden @@ -0,0 +1,10 @@ +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[project] +name = "multipart_explicit_encoding" +version = "1.0.0" +description = "Covers explicit multipart part content types." +requires-python = ">=3.12" +dependencies = ["httpx>=0.27"] diff --git a/tests/golden/python/python-requests/binary-transfer-media-types/binary_transfer_media_types/apis/transfer_api.py.golden b/tests/golden/python/python-requests/binary-transfer-media-types/binary_transfer_media_types/apis/transfer_api.py.golden index 3a5e9b8c2..6e39f0757 100644 --- a/tests/golden/python/python-requests/binary-transfer-media-types/binary_transfer_media_types/apis/transfer_api.py.golden +++ b/tests/golden/python/python-requests/binary-transfer-media-types/binary_transfer_media_types/apis/transfer_api.py.golden @@ -28,7 +28,7 @@ class TransferApi: files: dict[str, object] = {} files["file"] = ("file", body.file, "application/octet-stream") files["metadata"] = (None, json.dumps(body.metadata.to_dict()), "application/json") - files["purpose"] = (None, str(body.purpose)) + files["purpose"] = (None, str(body.purpose), "text/plain") response = self._client.request("POST", path, files=files if files else None) if response.status_code >= 400: raise ApiError(response.status_code, response.reason, response.content) diff --git a/tests/golden/python/python-requests/media-type-selection/media_type_selection/apis/media_api.py.golden b/tests/golden/python/python-requests/media-type-selection/media_type_selection/apis/media_api.py.golden index 00e36d6a9..e6f2ed0fc 100644 --- a/tests/golden/python/python-requests/media-type-selection/media_type_selection/apis/media_api.py.golden +++ b/tests/golden/python/python-requests/media-type-selection/media_type_selection/apis/media_api.py.golden @@ -28,7 +28,7 @@ class MediaApi: files: dict[str, object] = {} files["file"] = ("file", body.file, "application/octet-stream") if body.note is not None: - files["note"] = (None, str(body.note)) + files["note"] = (None, str(body.note), "text/plain") response = self._client.request("POST", path, files=files if files else None) if response.status_code >= 400: diff --git a/tests/golden/python/python-requests/multipart-edge-cases/multipart_edge_cases/apis/multipart_api.py.golden b/tests/golden/python/python-requests/multipart-edge-cases/multipart_edge_cases/apis/multipart_api.py.golden index d5250f440..5ad3222da 100644 --- a/tests/golden/python/python-requests/multipart-edge-cases/multipart_edge_cases/apis/multipart_api.py.golden +++ b/tests/golden/python/python-requests/multipart-edge-cases/multipart_edge_cases/apis/multipart_api.py.golden @@ -24,16 +24,16 @@ class MultipartApi: files["attributes"] = (None, json.dumps(body.attributes.to_dict()), "application/json") if body.enabled is not None: - files["enabled"] = (None, str(body.enabled)) + files["enabled"] = (None, str(body.enabled), "text/plain") if body.file is not None: files["file"] = ("file", body.file, "application/octet-stream") if body.retry_count is not None: - files["retry_count"] = (None, str(body.retry_count)) + files["retry_count"] = (None, str(body.retry_count), "text/plain") if body.title is not None: - files["title"] = (None, str(body.title)) + files["title"] = (None, str(body.title), "text/plain") response = self._client.request("POST", path, files=files if files else None) @@ -44,9 +44,9 @@ class MultipartApi: def send_text_fields(self, *, body: TextFields) -> None: path = "/multipart/text-only" files: dict[str, object] = {} - files["enabled"] = (None, str(body.enabled)) - files["note"] = (None, str(body.note)) - files["retry_count"] = (None, str(body.retry_count)) + files["enabled"] = (None, str(body.enabled), "text/plain") + files["note"] = (None, str(body.note), "text/plain") + files["retry_count"] = (None, str(body.retry_count), "text/plain") response = self._client.request("POST", path, files=files if files else None) if response.status_code >= 400: raise ApiError(response.status_code, response.reason, response.content) diff --git a/tests/golden/python/python-requests/multipart-explicit-encoding/README.md.golden b/tests/golden/python/python-requests/multipart-explicit-encoding/README.md.golden new file mode 100644 index 000000000..5270c1b8d --- /dev/null +++ b/tests/golden/python/python-requests/multipart-explicit-encoding/README.md.golden @@ -0,0 +1,7 @@ +# Multipart Explicit Encoding + +Covers explicit multipart part content types. + +Version: `1.0.0` + +Generated by [openapi-nexus](https://github.com/adamcavendish/openapi-nexus) for `multipart_explicit_encoding`. diff --git a/tests/golden/python/python-requests/multipart-explicit-encoding/multipart_explicit_encoding/__init__.py.golden b/tests/golden/python/python-requests/multipart-explicit-encoding/multipart_explicit_encoding/__init__.py.golden new file mode 100644 index 000000000..94b1350f1 --- /dev/null +++ b/tests/golden/python/python-requests/multipart-explicit-encoding/multipart_explicit_encoding/__init__.py.golden @@ -0,0 +1,10 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Multipart Explicit Encoding — 1.0.0 +# Covers explicit multipart part content types. + +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/multipart-explicit-encoding/multipart_explicit_encoding/apis/__init__.py.golden b/tests/golden/python/python-requests/multipart-explicit-encoding/multipart_explicit_encoding/apis/__init__.py.golden new file mode 100644 index 000000000..01c26b214 --- /dev/null +++ b/tests/golden/python/python-requests/multipart-explicit-encoding/multipart_explicit_encoding/apis/__init__.py.golden @@ -0,0 +1,6 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Multipart Explicit Encoding — 1.0.0 +# Covers explicit multipart part content types. + +from .transfer_api import TransferApi as TransferApi diff --git a/tests/golden/python/python-requests/multipart-explicit-encoding/multipart_explicit_encoding/apis/transfer_api.py.golden b/tests/golden/python/python-requests/multipart-explicit-encoding/multipart_explicit_encoding/apis/transfer_api.py.golden new file mode 100644 index 000000000..bf5d41cd7 --- /dev/null +++ b/tests/golden/python/python-requests/multipart-explicit-encoding/multipart_explicit_encoding/apis/transfer_api.py.golden @@ -0,0 +1,28 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Multipart Explicit Encoding — 1.0.0 +# Covers explicit multipart part content types. + +from __future__ import annotations + +import json + +from ..models.upload_encoded_asset_request import UploadEncodedAssetRequest +from ..runtime.client import Client +from ..runtime.errors import ApiError + +class TransferApi: + def __init__(self, client: Client) -> None: + self._client = client + + def upload_encoded_asset(self, *, body: UploadEncodedAssetRequest) -> None: + path = "/uploads/encoded" + files: dict[str, object] = {} + files["audit"] = (None, json.dumps(body.audit.to_dict()), "application/json") + files["file"] = ("file", body.file, "application/pdf") + files["metadata"] = (None, json.dumps(body.metadata.to_dict()), "application/vnd.openapi-nexus.metadata+json") + files["purpose"] = (None, str(body.purpose), "text/plain") + response = self._client.request("POST", path, files=files if files else None) + if response.status_code >= 400: + raise ApiError(response.status_code, response.reason, response.content) + return None diff --git a/tests/golden/python/python-requests/multipart-explicit-encoding/multipart_explicit_encoding/models/__init__.py.golden b/tests/golden/python/python-requests/multipart-explicit-encoding/multipart_explicit_encoding/models/__init__.py.golden new file mode 100644 index 000000000..81e7b8856 --- /dev/null +++ b/tests/golden/python/python-requests/multipart-explicit-encoding/multipart_explicit_encoding/models/__init__.py.golden @@ -0,0 +1,8 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Multipart Explicit Encoding — 1.0.0 +# Covers explicit multipart part content types. + +from .audit_attributes import AuditAttributes as AuditAttributes +from .upload_attributes import UploadAttributes as UploadAttributes +from .upload_encoded_asset_request import UploadEncodedAssetRequest as UploadEncodedAssetRequest diff --git a/tests/golden/python/python-requests/multipart-explicit-encoding/multipart_explicit_encoding/models/audit_attributes.py.golden b/tests/golden/python/python-requests/multipart-explicit-encoding/multipart_explicit_encoding/models/audit_attributes.py.golden new file mode 100644 index 000000000..d68d1e654 --- /dev/null +++ b/tests/golden/python/python-requests/multipart-explicit-encoding/multipart_explicit_encoding/models/audit_attributes.py.golden @@ -0,0 +1,27 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Multipart Explicit Encoding — 1.0.0 +# Covers explicit multipart part content types. + +from __future__ import annotations + +from dataclasses import dataclass + +@dataclass +class AuditAttributes: + request_id: str + reviewed: bool | None = None + + def to_dict(self) -> dict[str, object]: + result: dict[str, object] = {} + result["request_id"] = self.request_id + if self.reviewed is not None: + result["reviewed"] = self.reviewed + return result + + @classmethod + def from_dict(cls, data: dict[str, object]) -> AuditAttributes: + return cls( + request_id=data["request_id"], # type: ignore[assignment] + reviewed=data.get("reviewed"), # type: ignore[assignment] + ) diff --git a/tests/golden/python/python-requests/multipart-explicit-encoding/multipart_explicit_encoding/models/upload_attributes.py.golden b/tests/golden/python/python-requests/multipart-explicit-encoding/multipart_explicit_encoding/models/upload_attributes.py.golden new file mode 100644 index 000000000..b5c514b73 --- /dev/null +++ b/tests/golden/python/python-requests/multipart-explicit-encoding/multipart_explicit_encoding/models/upload_attributes.py.golden @@ -0,0 +1,27 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Multipart Explicit Encoding — 1.0.0 +# Covers explicit multipart part content types. + +from __future__ import annotations + +from dataclasses import dataclass + +@dataclass +class UploadAttributes: + label: str + retention_days: int | None = None + + def to_dict(self) -> dict[str, object]: + result: dict[str, object] = {} + result["label"] = self.label + if self.retention_days is not None: + result["retention_days"] = self.retention_days + return result + + @classmethod + def from_dict(cls, data: dict[str, object]) -> UploadAttributes: + return cls( + label=data["label"], # type: ignore[assignment] + retention_days=data.get("retention_days"), # type: ignore[assignment] + ) diff --git a/tests/golden/python/python-requests/multipart-explicit-encoding/multipart_explicit_encoding/models/upload_encoded_asset_request.py.golden b/tests/golden/python/python-requests/multipart-explicit-encoding/multipart_explicit_encoding/models/upload_encoded_asset_request.py.golden new file mode 100644 index 000000000..f29f19620 --- /dev/null +++ b/tests/golden/python/python-requests/multipart-explicit-encoding/multipart_explicit_encoding/models/upload_encoded_asset_request.py.golden @@ -0,0 +1,35 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Multipart Explicit Encoding — 1.0.0 +# Covers explicit multipart part content types. + +from __future__ import annotations + +from dataclasses import dataclass + +from .audit_attributes import AuditAttributes +from .upload_attributes import UploadAttributes + +@dataclass +class UploadEncodedAssetRequest: + audit: AuditAttributes + file: bytes + metadata: UploadAttributes + purpose: str + + def to_dict(self) -> dict[str, object]: + result: dict[str, object] = {} + result["audit"] = self.audit.to_dict() + result["file"] = self.file + result["metadata"] = self.metadata.to_dict() + result["purpose"] = self.purpose + return result + + @classmethod + def from_dict(cls, data: dict[str, object]) -> UploadEncodedAssetRequest: + return cls( + audit=AuditAttributes.from_dict(data["audit"]), # type: ignore[arg-type] + file=data["file"], # type: ignore[assignment] + metadata=UploadAttributes.from_dict(data["metadata"]), # type: ignore[arg-type] + purpose=data["purpose"], # type: ignore[assignment] + ) diff --git a/tests/golden/python/python-requests/multipart-explicit-encoding/multipart_explicit_encoding/py.typed.golden b/tests/golden/python/python-requests/multipart-explicit-encoding/multipart_explicit_encoding/py.typed.golden new file mode 100644 index 000000000..e69de29bb diff --git a/tests/golden/python/python-requests/multipart-explicit-encoding/multipart_explicit_encoding/runtime/__init__.py.golden b/tests/golden/python/python-requests/multipart-explicit-encoding/multipart_explicit_encoding/runtime/__init__.py.golden new file mode 100644 index 000000000..579bfa8db --- /dev/null +++ b/tests/golden/python/python-requests/multipart-explicit-encoding/multipart_explicit_encoding/runtime/__init__.py.golden @@ -0,0 +1,10 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Multipart Explicit Encoding — 1.0.0 +# Covers explicit multipart part content types. + +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/multipart-explicit-encoding/multipart_explicit_encoding/runtime/auth.py.golden b/tests/golden/python/python-requests/multipart-explicit-encoding/multipart_explicit_encoding/runtime/auth.py.golden new file mode 100644 index 000000000..a822af2c0 --- /dev/null +++ b/tests/golden/python/python-requests/multipart-explicit-encoding/multipart_explicit_encoding/runtime/auth.py.golden @@ -0,0 +1,51 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Multipart Explicit Encoding — 1.0.0 +# Covers explicit multipart part content types. + +"""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/multipart-explicit-encoding/multipart_explicit_encoding/runtime/client.py.golden b/tests/golden/python/python-requests/multipart-explicit-encoding/multipart_explicit_encoding/runtime/client.py.golden new file mode 100644 index 000000000..05aeaf7ac --- /dev/null +++ b/tests/golden/python/python-requests/multipart-explicit-encoding/multipart_explicit_encoding/runtime/client.py.golden @@ -0,0 +1,99 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Multipart Explicit Encoding — 1.0.0 +# Covers explicit multipart part content types. + +"""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/multipart-explicit-encoding/multipart_explicit_encoding/runtime/errors.py.golden b/tests/golden/python/python-requests/multipart-explicit-encoding/multipart_explicit_encoding/runtime/errors.py.golden new file mode 100644 index 000000000..8ab65f02b --- /dev/null +++ b/tests/golden/python/python-requests/multipart-explicit-encoding/multipart_explicit_encoding/runtime/errors.py.golden @@ -0,0 +1,18 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Multipart Explicit Encoding — 1.0.0 +# Covers explicit multipart part content types. + +"""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/multipart-explicit-encoding/pyproject.toml.golden b/tests/golden/python/python-requests/multipart-explicit-encoding/pyproject.toml.golden new file mode 100644 index 000000000..726358fec --- /dev/null +++ b/tests/golden/python/python-requests/multipart-explicit-encoding/pyproject.toml.golden @@ -0,0 +1,10 @@ +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[project] +name = "multipart_explicit_encoding" +version = "1.0.0" +description = "Covers explicit multipart part content types." +requires-python = ">=3.12" +dependencies = ["requests>=2.32"] diff --git a/tests/golden/rust/rust-aioduct/binary-transfer-media-types/src/apis/transfer.rs.golden b/tests/golden/rust/rust-aioduct/binary-transfer-media-types/src/apis/transfer.rs.golden index 1fceb102e..276e392bc 100644 --- a/tests/golden/rust/rust-aioduct/binary-transfer-media-types/src/apis/transfer.rs.golden +++ b/tests/golden/rust/rust-aioduct/binary-transfer-media-types/src/apis/transfer.rs.golden @@ -50,8 +50,8 @@ impl<'a, R: aioduct::Runtime> TransferApi<'a, R> { let mut req = self.client.post(&path)?; let mut multipart = aioduct::multipart::Multipart::new(); multipart = multipart.file("file", "file", "application/octet-stream", body.file.clone()); - multipart = multipart.text("metadata", serde_json::to_string(&body.metadata)?); - multipart = multipart.text("purpose", body.purpose.to_string()); + multipart = multipart.part(aioduct::multipart::Part::text("metadata", serde_json::to_string(&body.metadata)?).mime_str("application/json")); + multipart = multipart.part(aioduct::multipart::Part::text("purpose", body.purpose.to_string()).mime_str("text/plain")); req = req.multipart(multipart); let resp = req.send().await?; let status_code = resp.status().as_u16(); diff --git a/tests/golden/rust/rust-aioduct/media-type-selection/src/apis/media.rs.golden b/tests/golden/rust/rust-aioduct/media-type-selection/src/apis/media.rs.golden index 8025f547b..02469d7aa 100644 --- a/tests/golden/rust/rust-aioduct/media-type-selection/src/apis/media.rs.golden +++ b/tests/golden/rust/rust-aioduct/media-type-selection/src/apis/media.rs.golden @@ -43,7 +43,7 @@ impl<'a, R: aioduct::Runtime> MediaApi<'a, R> { let mut multipart = aioduct::multipart::Multipart::new(); multipart = multipart.file("file", "file", "application/octet-stream", body.file.clone()); if let Some(value) = &body.note { - multipart = multipart.text("note", value.to_string()); + multipart = multipart.part(aioduct::multipart::Part::text("note", value.to_string()).mime_str("text/plain")); } req = req.multipart(multipart); let resp = req.send().await?; 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 8ed74e421..ffce1eb8e 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 @@ -29,19 +29,19 @@ impl<'a, R: aioduct::Runtime> MultipartApi<'a, R> { let mut req = self.client.post(&path)?; let mut multipart = aioduct::multipart::Multipart::new(); if let Some(value) = &body.attributes { - multipart = multipart.text("attributes", serde_json::to_string(value)?); + 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.text("enabled", value.to_string()); + 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.text("retry_count", value.to_string()); + multipart = multipart.part(aioduct::multipart::Part::text("retry_count", value.to_string()).mime_str("text/plain")); } if let Some(value) = &body.title { - multipart = multipart.text("title", value.to_string()); + multipart = multipart.part(aioduct::multipart::Part::text("title", value.to_string()).mime_str("text/plain")); } req = req.multipart(multipart); let resp = req.send().await?; @@ -57,9 +57,9 @@ impl<'a, R: aioduct::Runtime> MultipartApi<'a, R> { let path = "/multipart/text-only".to_string(); let mut req = self.client.post(&path)?; let mut multipart = aioduct::multipart::Multipart::new(); - multipart = multipart.text("enabled", body.enabled.to_string()); - multipart = multipart.text("note", body.note.to_string()); - multipart = multipart.text("retry_count", body.retry_count.to_string()); + multipart = multipart.part(aioduct::multipart::Part::text("enabled", body.enabled.to_string()).mime_str("text/plain")); + multipart = multipart.part(aioduct::multipart::Part::text("note", body.note.to_string()).mime_str("text/plain")); + multipart = multipart.part(aioduct::multipart::Part::text("retry_count", body.retry_count.to_string()).mime_str("text/plain")); req = req.multipart(multipart); let resp = req.send().await?; let status_code = resp.status().as_u16(); diff --git a/tests/golden/rust/rust-aioduct/multipart-explicit-encoding/Cargo.toml.golden b/tests/golden/rust/rust-aioduct/multipart-explicit-encoding/Cargo.toml.golden new file mode 100644 index 000000000..e5cd3541b --- /dev/null +++ b/tests/golden/rust/rust-aioduct/multipart-explicit-encoding/Cargo.toml.golden @@ -0,0 +1,12 @@ +[package] +name = "multipart-explicit-encoding" +version = "0.1.0" +edition = "2024" +description = "Covers explicit multipart part content types." + +[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/multipart-explicit-encoding/README.md.golden b/tests/golden/rust/rust-aioduct/multipart-explicit-encoding/README.md.golden new file mode 100644 index 000000000..b6b911f2e --- /dev/null +++ b/tests/golden/rust/rust-aioduct/multipart-explicit-encoding/README.md.golden @@ -0,0 +1,7 @@ +# Multipart Explicit Encoding + +Covers explicit multipart part content types. + +Version: `1.0.0` + +Generated by [openapi-nexus](https://github.com/adamcavendish/openapi-nexus) for `multipart-explicit-encoding`. diff --git a/tests/golden/rust/rust-aioduct/multipart-explicit-encoding/src/apis/mod.rs.golden b/tests/golden/rust/rust-aioduct/multipart-explicit-encoding/src/apis/mod.rs.golden new file mode 100644 index 000000000..c68d968ed --- /dev/null +++ b/tests/golden/rust/rust-aioduct/multipart-explicit-encoding/src/apis/mod.rs.golden @@ -0,0 +1,8 @@ +// @generated +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Multipart Explicit Encoding — 1.0.0 +// Covers explicit multipart part content types. + +mod transfer; +pub use transfer::*; diff --git a/tests/golden/rust/rust-aioduct/multipart-explicit-encoding/src/apis/transfer.rs.golden b/tests/golden/rust/rust-aioduct/multipart-explicit-encoding/src/apis/transfer.rs.golden new file mode 100644 index 000000000..bdb367d6c --- /dev/null +++ b/tests/golden/rust/rust-aioduct/multipart-explicit-encoding/src/apis/transfer.rs.golden @@ -0,0 +1,46 @@ +// @generated +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Multipart Explicit Encoding — 1.0.0 +// Covers explicit multipart part content types. + +use crate::runtime::client::Client; +use crate::runtime::error::Error; + +/// API operations under the "transfer" tag. +pub struct TransferApi<'a, R: aioduct::Runtime> { + client: &'a Client, +} + +impl<'a, R: aioduct::Runtime> TransferApi<'a, R> { + /// Create a new `TransferApi` bound to the given client. + pub fn new(client: &'a Client) -> Self { + Self { + client, + } + } + + /// POST /uploads/encoded + pub async fn upload_encoded_asset( + &self, + body: &crate::models::UploadEncodedAssetRequest, + ) -> Result { + let path = "/uploads/encoded".to_string(); + let mut req = self.client.post(&path)?; + let mut multipart = aioduct::multipart::Multipart::new(); + multipart = multipart.part(aioduct::multipart::Part::text("audit", serde_json::to_string(&body.audit)?).mime_str("application/json")); + multipart = multipart.file("file", "file", "application/pdf", body.file.clone()); + multipart = multipart.part(aioduct::multipart::Part::text("metadata", serde_json::to_string(&body.metadata)?).mime_str("application/vnd.openapi-nexus.metadata+json")); + multipart = multipart.part(aioduct::multipart::Part::text("purpose", body.purpose.to_string()).mime_str("text/plain")); + req = req.multipart(multipart); + let resp = req.send().await?; + let status_code = resp.status().as_u16(); + Ok(UploadEncodedAssetResponse { status_code }) + } +} + +/// Response from `upload_encoded_asset`. +#[derive(Debug)] +pub struct UploadEncodedAssetResponse { + pub status_code: u16, +} diff --git a/tests/golden/rust/rust-aioduct/multipart-explicit-encoding/src/lib.rs.golden b/tests/golden/rust/rust-aioduct/multipart-explicit-encoding/src/lib.rs.golden new file mode 100644 index 000000000..f0e0674c0 --- /dev/null +++ b/tests/golden/rust/rust-aioduct/multipart-explicit-encoding/src/lib.rs.golden @@ -0,0 +1,11 @@ +// @generated +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Multipart Explicit Encoding — 1.0.0 +// Covers explicit multipart part content types. + +#![allow(clippy::all)] + +pub mod apis; +pub mod models; +pub mod runtime; diff --git a/tests/golden/rust/rust-aioduct/multipart-explicit-encoding/src/models/audit_attributes.rs.golden b/tests/golden/rust/rust-aioduct/multipart-explicit-encoding/src/models/audit_attributes.rs.golden new file mode 100644 index 000000000..f48cdc871 --- /dev/null +++ b/tests/golden/rust/rust-aioduct/multipart-explicit-encoding/src/models/audit_attributes.rs.golden @@ -0,0 +1,14 @@ +// @generated +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Multipart Explicit Encoding — 1.0.0 +// Covers explicit multipart part content types. + +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct AuditAttributes { + pub request_id: String, + #[serde(skip_serializing_if = "Option::is_none", default)] + pub reviewed: Option, +} diff --git a/tests/golden/rust/rust-aioduct/multipart-explicit-encoding/src/models/mod.rs.golden b/tests/golden/rust/rust-aioduct/multipart-explicit-encoding/src/models/mod.rs.golden new file mode 100644 index 000000000..b7af82c9b --- /dev/null +++ b/tests/golden/rust/rust-aioduct/multipart-explicit-encoding/src/models/mod.rs.golden @@ -0,0 +1,12 @@ +// @generated +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Multipart Explicit Encoding — 1.0.0 +// Covers explicit multipart part content types. + +mod audit_attributes; +pub use audit_attributes::*; +mod upload_attributes; +pub use upload_attributes::*; +mod upload_encoded_asset_request; +pub use upload_encoded_asset_request::*; diff --git a/tests/golden/rust/rust-aioduct/multipart-explicit-encoding/src/models/upload_attributes.rs.golden b/tests/golden/rust/rust-aioduct/multipart-explicit-encoding/src/models/upload_attributes.rs.golden new file mode 100644 index 000000000..c92552ea3 --- /dev/null +++ b/tests/golden/rust/rust-aioduct/multipart-explicit-encoding/src/models/upload_attributes.rs.golden @@ -0,0 +1,14 @@ +// @generated +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Multipart Explicit Encoding — 1.0.0 +// Covers explicit multipart part content types. + +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct UploadAttributes { + pub label: String, + #[serde(skip_serializing_if = "Option::is_none", default)] + pub retention_days: Option, +} diff --git a/tests/golden/rust/rust-aioduct/multipart-explicit-encoding/src/models/upload_encoded_asset_request.rs.golden b/tests/golden/rust/rust-aioduct/multipart-explicit-encoding/src/models/upload_encoded_asset_request.rs.golden new file mode 100644 index 000000000..c234c304f --- /dev/null +++ b/tests/golden/rust/rust-aioduct/multipart-explicit-encoding/src/models/upload_encoded_asset_request.rs.golden @@ -0,0 +1,17 @@ +// @generated +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Multipart Explicit Encoding — 1.0.0 +// Covers explicit multipart part content types. + +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct UploadEncodedAssetRequest { + pub audit: super::AuditAttributes, + /// Binary content with an explicit content type + pub file: Vec, + pub metadata: super::UploadAttributes, + /// Plain-text upload purpose + pub purpose: String, +} diff --git a/tests/golden/rust/rust-aioduct/multipart-explicit-encoding/src/runtime/auth.rs.golden b/tests/golden/rust/rust-aioduct/multipart-explicit-encoding/src/runtime/auth.rs.golden new file mode 100644 index 000000000..7b84d0270 --- /dev/null +++ b/tests/golden/rust/rust-aioduct/multipart-explicit-encoding/src/runtime/auth.rs.golden @@ -0,0 +1,180 @@ +// @generated +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Multipart Explicit Encoding — 1.0.0 +// Covers explicit multipart part content types. + +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/multipart-explicit-encoding/src/runtime/client.rs.golden b/tests/golden/rust/rust-aioduct/multipart-explicit-encoding/src/runtime/client.rs.golden new file mode 100644 index 000000000..dfdf2fad9 --- /dev/null +++ b/tests/golden/rust/rust-aioduct/multipart-explicit-encoding/src/runtime/client.rs.golden @@ -0,0 +1,112 @@ +// @generated +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Multipart Explicit Encoding — 1.0.0 +// Covers explicit multipart part content types. + +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/multipart-explicit-encoding/src/runtime/error.rs.golden b/tests/golden/rust/rust-aioduct/multipart-explicit-encoding/src/runtime/error.rs.golden new file mode 100644 index 000000000..1bf06330b --- /dev/null +++ b/tests/golden/rust/rust-aioduct/multipart-explicit-encoding/src/runtime/error.rs.golden @@ -0,0 +1,60 @@ +// @generated +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Multipart Explicit Encoding — 1.0.0 +// Covers explicit multipart part content types. + +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/multipart-explicit-encoding/src/runtime/mod.rs.golden b/tests/golden/rust/rust-aioduct/multipart-explicit-encoding/src/runtime/mod.rs.golden new file mode 100644 index 000000000..cdb1caac6 --- /dev/null +++ b/tests/golden/rust/rust-aioduct/multipart-explicit-encoding/src/runtime/mod.rs.golden @@ -0,0 +1,9 @@ +// @generated +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Multipart Explicit Encoding — 1.0.0 +// Covers explicit multipart part content types. + +pub mod auth; +pub mod client; +pub mod error; diff --git a/tests/golden/rust/rust-aioduct/multipart-nested-object-parts/src/apis/multipart.rs.golden b/tests/golden/rust/rust-aioduct/multipart-nested-object-parts/src/apis/multipart.rs.golden index f74accae3..5448aa407 100644 --- a/tests/golden/rust/rust-aioduct/multipart-nested-object-parts/src/apis/multipart.rs.golden +++ b/tests/golden/rust/rust-aioduct/multipart-nested-object-parts/src/apis/multipart.rs.golden @@ -29,7 +29,7 @@ impl<'a, R: aioduct::Runtime> MultipartApi<'a, R> { let mut req = self.client.post(&path)?; let mut multipart = aioduct::multipart::Multipart::new(); multipart = multipart.file("file", "file", "application/octet-stream", body.file.clone()); - multipart = multipart.text("item_config", serde_json::to_string(&body.item_config)?); + multipart = multipart.part(aioduct::multipart::Part::text("item_config", serde_json::to_string(&body.item_config)?).mime_str("application/json")); req = req.multipart(multipart); let resp = req.send().await?; let status_code = resp.status().as_u16(); diff --git a/tests/golden/rust/rust-reqwest/binary-transfer-media-types/src/apis/transfer.rs.golden b/tests/golden/rust/rust-reqwest/binary-transfer-media-types/src/apis/transfer.rs.golden index 8c9eaeaa1..b4a8ee252 100644 --- a/tests/golden/rust/rust-reqwest/binary-transfer-media-types/src/apis/transfer.rs.golden +++ b/tests/golden/rust/rust-reqwest/binary-transfer-media-types/src/apis/transfer.rs.golden @@ -50,8 +50,8 @@ impl<'a> TransferApi<'a> { let mut req = self.client.request(reqwest::Method::POST, &path).await?; let mut multipart = reqwest::multipart::Form::new(); multipart = multipart.part("file", reqwest::multipart::Part::bytes(body.file.clone()).file_name("file").mime_str("application/octet-stream").map_err(Error::Network)?); - multipart = multipart.text("metadata", serde_json::to_string(&body.metadata)?); - multipart = multipart.text("purpose", body.purpose.to_string()); + multipart = multipart.part("metadata", reqwest::multipart::Part::text(serde_json::to_string(&body.metadata)?).mime_str("application/json").map_err(Error::Network)?); + multipart = multipart.part("purpose", reqwest::multipart::Part::text(body.purpose.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(); diff --git a/tests/golden/rust/rust-reqwest/media-type-selection/src/apis/media.rs.golden b/tests/golden/rust/rust-reqwest/media-type-selection/src/apis/media.rs.golden index e714c812f..30513365e 100644 --- a/tests/golden/rust/rust-reqwest/media-type-selection/src/apis/media.rs.golden +++ b/tests/golden/rust/rust-reqwest/media-type-selection/src/apis/media.rs.golden @@ -43,7 +43,7 @@ impl<'a> MediaApi<'a> { let mut multipart = reqwest::multipart::Form::new(); multipart = multipart.part("file", reqwest::multipart::Part::bytes(body.file.clone()).file_name("file").mime_str("application/octet-stream").map_err(Error::Network)?); if let Some(value) = &body.note { - multipart = multipart.text("note", value.to_string()); + multipart = multipart.part("note", 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?; 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 1d182e7b7..bcc68eda9 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 @@ -29,19 +29,19 @@ impl<'a> MultipartApi<'a> { 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.text("attributes", serde_json::to_string(value)?); + 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.text("enabled", value.to_string()); + 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.text("retry_count", value.to_string()); + 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.text("title", value.to_string()); + 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?; @@ -57,9 +57,9 @@ impl<'a> MultipartApi<'a> { let path = "/multipart/text-only".to_string(); let mut req = self.client.request(reqwest::Method::POST, &path).await?; let mut multipart = reqwest::multipart::Form::new(); - multipart = multipart.text("enabled", body.enabled.to_string()); - multipart = multipart.text("note", body.note.to_string()); - multipart = multipart.text("retry_count", body.retry_count.to_string()); + multipart = multipart.part("enabled", reqwest::multipart::Part::text(body.enabled.to_string()).mime_str("text/plain").map_err(Error::Network)?); + multipart = multipart.part("note", reqwest::multipart::Part::text(body.note.to_string()).mime_str("text/plain").map_err(Error::Network)?); + multipart = multipart.part("retry_count", reqwest::multipart::Part::text(body.retry_count.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(); diff --git a/tests/golden/rust/rust-reqwest/multipart-explicit-encoding/Cargo.toml.golden b/tests/golden/rust/rust-reqwest/multipart-explicit-encoding/Cargo.toml.golden new file mode 100644 index 000000000..e0c2a19b8 --- /dev/null +++ b/tests/golden/rust/rust-reqwest/multipart-explicit-encoding/Cargo.toml.golden @@ -0,0 +1,12 @@ +[package] +name = "multipart-explicit-encoding" +version = "0.1.0" +edition = "2024" +description = "Covers explicit multipart part content types." + +[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/multipart-explicit-encoding/README.md.golden b/tests/golden/rust/rust-reqwest/multipart-explicit-encoding/README.md.golden new file mode 100644 index 000000000..b6b911f2e --- /dev/null +++ b/tests/golden/rust/rust-reqwest/multipart-explicit-encoding/README.md.golden @@ -0,0 +1,7 @@ +# Multipart Explicit Encoding + +Covers explicit multipart part content types. + +Version: `1.0.0` + +Generated by [openapi-nexus](https://github.com/adamcavendish/openapi-nexus) for `multipart-explicit-encoding`. diff --git a/tests/golden/rust/rust-reqwest/multipart-explicit-encoding/src/apis/mod.rs.golden b/tests/golden/rust/rust-reqwest/multipart-explicit-encoding/src/apis/mod.rs.golden new file mode 100644 index 000000000..c68d968ed --- /dev/null +++ b/tests/golden/rust/rust-reqwest/multipart-explicit-encoding/src/apis/mod.rs.golden @@ -0,0 +1,8 @@ +// @generated +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Multipart Explicit Encoding — 1.0.0 +// Covers explicit multipart part content types. + +mod transfer; +pub use transfer::*; diff --git a/tests/golden/rust/rust-reqwest/multipart-explicit-encoding/src/apis/transfer.rs.golden b/tests/golden/rust/rust-reqwest/multipart-explicit-encoding/src/apis/transfer.rs.golden new file mode 100644 index 000000000..6b5dbe611 --- /dev/null +++ b/tests/golden/rust/rust-reqwest/multipart-explicit-encoding/src/apis/transfer.rs.golden @@ -0,0 +1,46 @@ +// @generated +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Multipart Explicit Encoding — 1.0.0 +// Covers explicit multipart part content types. + +use crate::runtime::client::Client; +use crate::runtime::error::Error; + +/// API operations under the "transfer" tag. +pub struct TransferApi<'a> { + client: &'a Client, +} + +impl<'a> TransferApi<'a> { + /// Create a new `TransferApi` bound to the given client. + pub fn new(client: &'a Client) -> Self { + Self { + client, + } + } + + /// POST /uploads/encoded + pub async fn upload_encoded_asset( + &self, + body: &crate::models::UploadEncodedAssetRequest, + ) -> Result { + let path = "/uploads/encoded".to_string(); + let mut req = self.client.request(reqwest::Method::POST, &path).await?; + let mut multipart = reqwest::multipart::Form::new(); + multipart = multipart.part("audit", reqwest::multipart::Part::text(serde_json::to_string(&body.audit)?).mime_str("application/json").map_err(Error::Network)?); + multipart = multipart.part("file", reqwest::multipart::Part::bytes(body.file.clone()).file_name("file").mime_str("application/pdf").map_err(Error::Network)?); + multipart = multipart.part("metadata", reqwest::multipart::Part::text(serde_json::to_string(&body.metadata)?).mime_str("application/vnd.openapi-nexus.metadata+json").map_err(Error::Network)?); + multipart = multipart.part("purpose", reqwest::multipart::Part::text(body.purpose.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(UploadEncodedAssetResponse { status_code }) + } +} + +/// Response from `upload_encoded_asset`. +#[derive(Debug)] +pub struct UploadEncodedAssetResponse { + pub status_code: u16, +} diff --git a/tests/golden/rust/rust-reqwest/multipart-explicit-encoding/src/lib.rs.golden b/tests/golden/rust/rust-reqwest/multipart-explicit-encoding/src/lib.rs.golden new file mode 100644 index 000000000..f0e0674c0 --- /dev/null +++ b/tests/golden/rust/rust-reqwest/multipart-explicit-encoding/src/lib.rs.golden @@ -0,0 +1,11 @@ +// @generated +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Multipart Explicit Encoding — 1.0.0 +// Covers explicit multipart part content types. + +#![allow(clippy::all)] + +pub mod apis; +pub mod models; +pub mod runtime; diff --git a/tests/golden/rust/rust-reqwest/multipart-explicit-encoding/src/models/audit_attributes.rs.golden b/tests/golden/rust/rust-reqwest/multipart-explicit-encoding/src/models/audit_attributes.rs.golden new file mode 100644 index 000000000..f48cdc871 --- /dev/null +++ b/tests/golden/rust/rust-reqwest/multipart-explicit-encoding/src/models/audit_attributes.rs.golden @@ -0,0 +1,14 @@ +// @generated +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Multipart Explicit Encoding — 1.0.0 +// Covers explicit multipart part content types. + +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct AuditAttributes { + pub request_id: String, + #[serde(skip_serializing_if = "Option::is_none", default)] + pub reviewed: Option, +} diff --git a/tests/golden/rust/rust-reqwest/multipart-explicit-encoding/src/models/mod.rs.golden b/tests/golden/rust/rust-reqwest/multipart-explicit-encoding/src/models/mod.rs.golden new file mode 100644 index 000000000..b7af82c9b --- /dev/null +++ b/tests/golden/rust/rust-reqwest/multipart-explicit-encoding/src/models/mod.rs.golden @@ -0,0 +1,12 @@ +// @generated +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Multipart Explicit Encoding — 1.0.0 +// Covers explicit multipart part content types. + +mod audit_attributes; +pub use audit_attributes::*; +mod upload_attributes; +pub use upload_attributes::*; +mod upload_encoded_asset_request; +pub use upload_encoded_asset_request::*; diff --git a/tests/golden/rust/rust-reqwest/multipart-explicit-encoding/src/models/upload_attributes.rs.golden b/tests/golden/rust/rust-reqwest/multipart-explicit-encoding/src/models/upload_attributes.rs.golden new file mode 100644 index 000000000..c92552ea3 --- /dev/null +++ b/tests/golden/rust/rust-reqwest/multipart-explicit-encoding/src/models/upload_attributes.rs.golden @@ -0,0 +1,14 @@ +// @generated +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Multipart Explicit Encoding — 1.0.0 +// Covers explicit multipart part content types. + +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct UploadAttributes { + pub label: String, + #[serde(skip_serializing_if = "Option::is_none", default)] + pub retention_days: Option, +} diff --git a/tests/golden/rust/rust-reqwest/multipart-explicit-encoding/src/models/upload_encoded_asset_request.rs.golden b/tests/golden/rust/rust-reqwest/multipart-explicit-encoding/src/models/upload_encoded_asset_request.rs.golden new file mode 100644 index 000000000..c234c304f --- /dev/null +++ b/tests/golden/rust/rust-reqwest/multipart-explicit-encoding/src/models/upload_encoded_asset_request.rs.golden @@ -0,0 +1,17 @@ +// @generated +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Multipart Explicit Encoding — 1.0.0 +// Covers explicit multipart part content types. + +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct UploadEncodedAssetRequest { + pub audit: super::AuditAttributes, + /// Binary content with an explicit content type + pub file: Vec, + pub metadata: super::UploadAttributes, + /// Plain-text upload purpose + pub purpose: String, +} diff --git a/tests/golden/rust/rust-reqwest/multipart-explicit-encoding/src/runtime/auth.rs.golden b/tests/golden/rust/rust-reqwest/multipart-explicit-encoding/src/runtime/auth.rs.golden new file mode 100644 index 000000000..d5181845a --- /dev/null +++ b/tests/golden/rust/rust-reqwest/multipart-explicit-encoding/src/runtime/auth.rs.golden @@ -0,0 +1,189 @@ +// @generated +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Multipart Explicit Encoding — 1.0.0 +// Covers explicit multipart part content types. + +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/multipart-explicit-encoding/src/runtime/client.rs.golden b/tests/golden/rust/rust-reqwest/multipart-explicit-encoding/src/runtime/client.rs.golden new file mode 100644 index 000000000..018629411 --- /dev/null +++ b/tests/golden/rust/rust-reqwest/multipart-explicit-encoding/src/runtime/client.rs.golden @@ -0,0 +1,73 @@ +// @generated +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Multipart Explicit Encoding — 1.0.0 +// Covers explicit multipart part content types. + +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/multipart-explicit-encoding/src/runtime/error.rs.golden b/tests/golden/rust/rust-reqwest/multipart-explicit-encoding/src/runtime/error.rs.golden new file mode 100644 index 000000000..5933b72dc --- /dev/null +++ b/tests/golden/rust/rust-reqwest/multipart-explicit-encoding/src/runtime/error.rs.golden @@ -0,0 +1,65 @@ +// @generated +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Multipart Explicit Encoding — 1.0.0 +// Covers explicit multipart part content types. + +/// 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/multipart-explicit-encoding/src/runtime/mod.rs.golden b/tests/golden/rust/rust-reqwest/multipart-explicit-encoding/src/runtime/mod.rs.golden new file mode 100644 index 000000000..cdb1caac6 --- /dev/null +++ b/tests/golden/rust/rust-reqwest/multipart-explicit-encoding/src/runtime/mod.rs.golden @@ -0,0 +1,9 @@ +// @generated +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Multipart Explicit Encoding — 1.0.0 +// Covers explicit multipart part content types. + +pub mod auth; +pub mod client; +pub mod error; diff --git a/tests/golden/rust/rust-reqwest/multipart-nested-object-parts/src/apis/multipart.rs.golden b/tests/golden/rust/rust-reqwest/multipart-nested-object-parts/src/apis/multipart.rs.golden index 0740998ec..ee5c5ecdf 100644 --- a/tests/golden/rust/rust-reqwest/multipart-nested-object-parts/src/apis/multipart.rs.golden +++ b/tests/golden/rust/rust-reqwest/multipart-nested-object-parts/src/apis/multipart.rs.golden @@ -29,7 +29,7 @@ impl<'a> MultipartApi<'a> { let mut req = self.client.request(reqwest::Method::POST, &path).await?; let mut multipart = reqwest::multipart::Form::new(); multipart = multipart.part("file", reqwest::multipart::Part::bytes(body.file.clone()).file_name("file").mime_str("application/octet-stream").map_err(Error::Network)?); - multipart = multipart.text("item_config", serde_json::to_string(&body.item_config)?); + multipart = multipart.part("item_config", reqwest::multipart::Part::text(serde_json::to_string(&body.item_config)?).mime_str("application/json").map_err(Error::Network)?); req = req.multipart(multipart); let resp = self.client.send(req).await?; let status_code = resp.status().as_u16(); diff --git a/tests/golden/rust/rust-ureq/binary-transfer-media-types/src/apis/transfer.rs.golden b/tests/golden/rust/rust-ureq/binary-transfer-media-types/src/apis/transfer.rs.golden index b4badbb26..e9d6a5f4c 100644 --- a/tests/golden/rust/rust-ureq/binary-transfer-media-types/src/apis/transfer.rs.golden +++ b/tests/golden/rust/rust-ureq/binary-transfer-media-types/src/apis/transfer.rs.golden @@ -51,15 +51,15 @@ impl<'a> TransferApi<'a> { 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(); 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: application/octet-stream\r\n\r\n", "file", "file").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(&body.file.clone()); multipart_body.extend_from_slice(b"\r\n"); multipart_body.extend_from_slice(format!("--{}\r\n", boundary).as_bytes()); - multipart_body.extend_from_slice(format!("Content-Disposition: form-data; name=\"{}\"\r\n\r\n", "metadata").as_bytes()); + multipart_body.extend_from_slice(format!("Content-Disposition: form-data; name=\"{}\"\r\nContent-Type: {}\r\n\r\n", "metadata", "application/json").as_bytes()); multipart_body.extend_from_slice(serde_json::to_string(&body.metadata)?.as_bytes()); multipart_body.extend_from_slice(b"\r\n"); multipart_body.extend_from_slice(format!("--{}\r\n", boundary).as_bytes()); - multipart_body.extend_from_slice(format!("Content-Disposition: form-data; name=\"{}\"\r\n\r\n", "purpose").as_bytes()); + multipart_body.extend_from_slice(format!("Content-Disposition: form-data; name=\"{}\"\r\nContent-Type: {}\r\n\r\n", "purpose", "text/plain").as_bytes()); multipart_body.extend_from_slice(body.purpose.to_string().as_bytes()); multipart_body.extend_from_slice(b"\r\n"); multipart_body.extend_from_slice(format!("--{}--\r\n", boundary).as_bytes()); diff --git a/tests/golden/rust/rust-ureq/media-type-selection/src/apis/media.rs.golden b/tests/golden/rust/rust-ureq/media-type-selection/src/apis/media.rs.golden index 1853146fc..76d9ee4bf 100644 --- a/tests/golden/rust/rust-ureq/media-type-selection/src/apis/media.rs.golden +++ b/tests/golden/rust/rust-ureq/media-type-selection/src/apis/media.rs.golden @@ -42,12 +42,12 @@ impl<'a> MediaApi<'a> { 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(); 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: application/octet-stream\r\n\r\n", "file", "file").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(&body.file.clone()); multipart_body.extend_from_slice(b"\r\n"); if let Some(value) = &body.note { multipart_body.extend_from_slice(format!("--{}\r\n", boundary).as_bytes()); - multipart_body.extend_from_slice(format!("Content-Disposition: form-data; name=\"{}\"\r\n\r\n", "note").as_bytes()); + multipart_body.extend_from_slice(format!("Content-Disposition: form-data; name=\"{}\"\r\nContent-Type: {}\r\n\r\n", "note", "text/plain").as_bytes()); multipart_body.extend_from_slice(value.to_string().as_bytes()); multipart_body.extend_from_slice(b"\r\n"); } 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 fdbf66440..283b6ba8e 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 @@ -31,31 +31,31 @@ impl<'a> MultipartApi<'a> { 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\n\r\n", "attributes").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\n\r\n", "enabled").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: application/octet-stream\r\n\r\n", "file", "file").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\n\r\n", "retry_count").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\n\r\n", "title").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"); } @@ -76,15 +76,15 @@ impl<'a> MultipartApi<'a> { 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(); multipart_body.extend_from_slice(format!("--{}\r\n", boundary).as_bytes()); - multipart_body.extend_from_slice(format!("Content-Disposition: form-data; name=\"{}\"\r\n\r\n", "enabled").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(body.enabled.to_string().as_bytes()); multipart_body.extend_from_slice(b"\r\n"); multipart_body.extend_from_slice(format!("--{}\r\n", boundary).as_bytes()); - multipart_body.extend_from_slice(format!("Content-Disposition: form-data; name=\"{}\"\r\n\r\n", "note").as_bytes()); + multipart_body.extend_from_slice(format!("Content-Disposition: form-data; name=\"{}\"\r\nContent-Type: {}\r\n\r\n", "note", "text/plain").as_bytes()); multipart_body.extend_from_slice(body.note.to_string().as_bytes()); multipart_body.extend_from_slice(b"\r\n"); multipart_body.extend_from_slice(format!("--{}\r\n", boundary).as_bytes()); - multipart_body.extend_from_slice(format!("Content-Disposition: form-data; name=\"{}\"\r\n\r\n", "retry_count").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(body.retry_count.to_string().as_bytes()); multipart_body.extend_from_slice(b"\r\n"); multipart_body.extend_from_slice(format!("--{}--\r\n", boundary).as_bytes()); diff --git a/tests/golden/rust/rust-ureq/multipart-explicit-encoding/Cargo.toml.golden b/tests/golden/rust/rust-ureq/multipart-explicit-encoding/Cargo.toml.golden new file mode 100644 index 000000000..4b9aea450 --- /dev/null +++ b/tests/golden/rust/rust-ureq/multipart-explicit-encoding/Cargo.toml.golden @@ -0,0 +1,12 @@ +[package] +name = "multipart-explicit-encoding" +version = "0.1.0" +edition = "2024" +description = "Covers explicit multipart part content types." + +[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/multipart-explicit-encoding/README.md.golden b/tests/golden/rust/rust-ureq/multipart-explicit-encoding/README.md.golden new file mode 100644 index 000000000..b6b911f2e --- /dev/null +++ b/tests/golden/rust/rust-ureq/multipart-explicit-encoding/README.md.golden @@ -0,0 +1,7 @@ +# Multipart Explicit Encoding + +Covers explicit multipart part content types. + +Version: `1.0.0` + +Generated by [openapi-nexus](https://github.com/adamcavendish/openapi-nexus) for `multipart-explicit-encoding`. diff --git a/tests/golden/rust/rust-ureq/multipart-explicit-encoding/src/apis/mod.rs.golden b/tests/golden/rust/rust-ureq/multipart-explicit-encoding/src/apis/mod.rs.golden new file mode 100644 index 000000000..c68d968ed --- /dev/null +++ b/tests/golden/rust/rust-ureq/multipart-explicit-encoding/src/apis/mod.rs.golden @@ -0,0 +1,8 @@ +// @generated +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Multipart Explicit Encoding — 1.0.0 +// Covers explicit multipart part content types. + +mod transfer; +pub use transfer::*; diff --git a/tests/golden/rust/rust-ureq/multipart-explicit-encoding/src/apis/transfer.rs.golden b/tests/golden/rust/rust-ureq/multipart-explicit-encoding/src/apis/transfer.rs.golden new file mode 100644 index 000000000..607645b54 --- /dev/null +++ b/tests/golden/rust/rust-ureq/multipart-explicit-encoding/src/apis/transfer.rs.golden @@ -0,0 +1,60 @@ +// @generated +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Multipart Explicit Encoding — 1.0.0 +// Covers explicit multipart part content types. + +use crate::runtime::client::Client; +use crate::runtime::error::Error; + +/// API operations under the "transfer" tag. +pub struct TransferApi<'a> { + client: &'a Client, +} + +impl<'a> TransferApi<'a> { + /// Create a new `TransferApi` bound to the given client. + pub fn new(client: &'a Client) -> Self { + Self { + client, + } + } + + /// POST /uploads/encoded + pub fn upload_encoded_asset( + &self, + body: &crate::models::UploadEncodedAssetRequest, + ) -> Result { + let path = "/uploads/encoded".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(); + 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", "audit", "application/json").as_bytes()); + multipart_body.extend_from_slice(serde_json::to_string(&body.audit)?.as_bytes()); + multipart_body.extend_from_slice(b"\r\n"); + 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/pdf").as_bytes()); + multipart_body.extend_from_slice(&body.file.clone()); + multipart_body.extend_from_slice(b"\r\n"); + 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", "metadata", "application/vnd.openapi-nexus.metadata+json").as_bytes()); + multipart_body.extend_from_slice(serde_json::to_string(&body.metadata)?.as_bytes()); + multipart_body.extend_from_slice(b"\r\n"); + 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", "purpose", "text/plain").as_bytes()); + multipart_body.extend_from_slice(body.purpose.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(UploadEncodedAssetResponse { status_code }) + } +} + +/// Response from `upload_encoded_asset`. +#[derive(Debug)] +pub struct UploadEncodedAssetResponse { + pub status_code: u16, +} diff --git a/tests/golden/rust/rust-ureq/multipart-explicit-encoding/src/lib.rs.golden b/tests/golden/rust/rust-ureq/multipart-explicit-encoding/src/lib.rs.golden new file mode 100644 index 000000000..f0e0674c0 --- /dev/null +++ b/tests/golden/rust/rust-ureq/multipart-explicit-encoding/src/lib.rs.golden @@ -0,0 +1,11 @@ +// @generated +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Multipart Explicit Encoding — 1.0.0 +// Covers explicit multipart part content types. + +#![allow(clippy::all)] + +pub mod apis; +pub mod models; +pub mod runtime; diff --git a/tests/golden/rust/rust-ureq/multipart-explicit-encoding/src/models/audit_attributes.rs.golden b/tests/golden/rust/rust-ureq/multipart-explicit-encoding/src/models/audit_attributes.rs.golden new file mode 100644 index 000000000..f48cdc871 --- /dev/null +++ b/tests/golden/rust/rust-ureq/multipart-explicit-encoding/src/models/audit_attributes.rs.golden @@ -0,0 +1,14 @@ +// @generated +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Multipart Explicit Encoding — 1.0.0 +// Covers explicit multipart part content types. + +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct AuditAttributes { + pub request_id: String, + #[serde(skip_serializing_if = "Option::is_none", default)] + pub reviewed: Option, +} diff --git a/tests/golden/rust/rust-ureq/multipart-explicit-encoding/src/models/mod.rs.golden b/tests/golden/rust/rust-ureq/multipart-explicit-encoding/src/models/mod.rs.golden new file mode 100644 index 000000000..b7af82c9b --- /dev/null +++ b/tests/golden/rust/rust-ureq/multipart-explicit-encoding/src/models/mod.rs.golden @@ -0,0 +1,12 @@ +// @generated +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Multipart Explicit Encoding — 1.0.0 +// Covers explicit multipart part content types. + +mod audit_attributes; +pub use audit_attributes::*; +mod upload_attributes; +pub use upload_attributes::*; +mod upload_encoded_asset_request; +pub use upload_encoded_asset_request::*; diff --git a/tests/golden/rust/rust-ureq/multipart-explicit-encoding/src/models/upload_attributes.rs.golden b/tests/golden/rust/rust-ureq/multipart-explicit-encoding/src/models/upload_attributes.rs.golden new file mode 100644 index 000000000..c92552ea3 --- /dev/null +++ b/tests/golden/rust/rust-ureq/multipart-explicit-encoding/src/models/upload_attributes.rs.golden @@ -0,0 +1,14 @@ +// @generated +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Multipart Explicit Encoding — 1.0.0 +// Covers explicit multipart part content types. + +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct UploadAttributes { + pub label: String, + #[serde(skip_serializing_if = "Option::is_none", default)] + pub retention_days: Option, +} diff --git a/tests/golden/rust/rust-ureq/multipart-explicit-encoding/src/models/upload_encoded_asset_request.rs.golden b/tests/golden/rust/rust-ureq/multipart-explicit-encoding/src/models/upload_encoded_asset_request.rs.golden new file mode 100644 index 000000000..c234c304f --- /dev/null +++ b/tests/golden/rust/rust-ureq/multipart-explicit-encoding/src/models/upload_encoded_asset_request.rs.golden @@ -0,0 +1,17 @@ +// @generated +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Multipart Explicit Encoding — 1.0.0 +// Covers explicit multipart part content types. + +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct UploadEncodedAssetRequest { + pub audit: super::AuditAttributes, + /// Binary content with an explicit content type + pub file: Vec, + pub metadata: super::UploadAttributes, + /// Plain-text upload purpose + pub purpose: String, +} diff --git a/tests/golden/rust/rust-ureq/multipart-explicit-encoding/src/runtime/auth.rs.golden b/tests/golden/rust/rust-ureq/multipart-explicit-encoding/src/runtime/auth.rs.golden new file mode 100644 index 000000000..973e8bbb1 --- /dev/null +++ b/tests/golden/rust/rust-ureq/multipart-explicit-encoding/src/runtime/auth.rs.golden @@ -0,0 +1,169 @@ +// @generated +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Multipart Explicit Encoding — 1.0.0 +// Covers explicit multipart part content types. + +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/multipart-explicit-encoding/src/runtime/client.rs.golden b/tests/golden/rust/rust-ureq/multipart-explicit-encoding/src/runtime/client.rs.golden new file mode 100644 index 000000000..e66888186 --- /dev/null +++ b/tests/golden/rust/rust-ureq/multipart-explicit-encoding/src/runtime/client.rs.golden @@ -0,0 +1,94 @@ +// @generated +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Multipart Explicit Encoding — 1.0.0 +// Covers explicit multipart part content types. + +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/multipart-explicit-encoding/src/runtime/error.rs.golden b/tests/golden/rust/rust-ureq/multipart-explicit-encoding/src/runtime/error.rs.golden new file mode 100644 index 000000000..9c53ec9a0 --- /dev/null +++ b/tests/golden/rust/rust-ureq/multipart-explicit-encoding/src/runtime/error.rs.golden @@ -0,0 +1,64 @@ +// @generated +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Multipart Explicit Encoding — 1.0.0 +// Covers explicit multipart part content types. + +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/multipart-explicit-encoding/src/runtime/mod.rs.golden b/tests/golden/rust/rust-ureq/multipart-explicit-encoding/src/runtime/mod.rs.golden new file mode 100644 index 000000000..cdb1caac6 --- /dev/null +++ b/tests/golden/rust/rust-ureq/multipart-explicit-encoding/src/runtime/mod.rs.golden @@ -0,0 +1,9 @@ +// @generated +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Multipart Explicit Encoding — 1.0.0 +// Covers explicit multipart part content types. + +pub mod auth; +pub mod client; +pub mod error; diff --git a/tests/golden/rust/rust-ureq/multipart-nested-object-parts/src/apis/multipart.rs.golden b/tests/golden/rust/rust-ureq/multipart-nested-object-parts/src/apis/multipart.rs.golden index a93584d6c..ddfc74177 100644 --- a/tests/golden/rust/rust-ureq/multipart-nested-object-parts/src/apis/multipart.rs.golden +++ b/tests/golden/rust/rust-ureq/multipart-nested-object-parts/src/apis/multipart.rs.golden @@ -30,11 +30,11 @@ impl<'a> MultipartApi<'a> { 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(); 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: application/octet-stream\r\n\r\n", "file", "file").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(&body.file.clone()); multipart_body.extend_from_slice(b"\r\n"); multipart_body.extend_from_slice(format!("--{}\r\n", boundary).as_bytes()); - multipart_body.extend_from_slice(format!("Content-Disposition: form-data; name=\"{}\"\r\n\r\n", "item_config").as_bytes()); + multipart_body.extend_from_slice(format!("Content-Disposition: form-data; name=\"{}\"\r\nContent-Type: {}\r\n\r\n", "item_config", "application/json").as_bytes()); multipart_body.extend_from_slice(serde_json::to_string(&body.item_config)?.as_bytes()); multipart_body.extend_from_slice(b"\r\n"); multipart_body.extend_from_slice(format!("--{}--\r\n", boundary).as_bytes()); diff --git a/tests/golden/typescript/typescript-fetch/binary-transfer-media-types/apis/TransferApi.ts.golden b/tests/golden/typescript/typescript-fetch/binary-transfer-media-types/apis/TransferApi.ts.golden index 95ca27666..6ae487105 100644 --- a/tests/golden/typescript/typescript-fetch/binary-transfer-media-types/apis/TransferApi.ts.golden +++ b/tests/golden/typescript/typescript-fetch/binary-transfer-media-types/apis/TransferApi.ts.golden @@ -104,10 +104,20 @@ export class TransferApi extends BaseAPI implements TransferApiInterface { }; // Prepare request body - const requestBody = new FormData(); - requestBody.append('file', requestParameters.body.file); - requestBody.append('metadata', JSON.stringify(requestParameters.body.metadata)); - requestBody.append('purpose', String(requestParameters.body.purpose)); + const multipartBoundary = '----openapi-nexus-' + Math.random().toString(16).slice(2); + const multipartChunks: Array = []; + headerParameters['Content-Type'] = 'multipart/form-data; boundary=' + multipartBoundary; + multipartChunks.push('--' + multipartBoundary + '\r\nContent-Disposition: form-data; name="file"; filename="file"\r\nContent-Type: application/octet-stream\r\n\r\n'); + multipartChunks.push(requestParameters.body.file); + multipartChunks.push('\r\n'); + multipartChunks.push('--' + multipartBoundary + '\r\nContent-Disposition: form-data; name="metadata"\r\nContent-Type: application/json\r\n\r\n'); + multipartChunks.push(JSON.stringify(requestParameters.body.metadata)); + multipartChunks.push('\r\n'); + multipartChunks.push('--' + multipartBoundary + '\r\nContent-Disposition: form-data; name="purpose"\r\nContent-Type: text/plain\r\n\r\n'); + multipartChunks.push(String(requestParameters.body.purpose)); + multipartChunks.push('\r\n'); + multipartChunks.push('--' + multipartBoundary + '--\r\n'); + const requestBody = new Blob(multipartChunks); // Make request const response = await this.request({ path: urlPath, diff --git a/tests/golden/typescript/typescript-fetch/media-type-selection/apis/MediaApi.ts.golden b/tests/golden/typescript/typescript-fetch/media-type-selection/apis/MediaApi.ts.golden index faed93c04..e0c8fcab7 100644 --- a/tests/golden/typescript/typescript-fetch/media-type-selection/apis/MediaApi.ts.golden +++ b/tests/golden/typescript/typescript-fetch/media-type-selection/apis/MediaApi.ts.golden @@ -129,11 +129,19 @@ export class MediaApi extends BaseAPI implements MediaApiInterface { }; // Prepare request body - const requestBody = new FormData(); - requestBody.append('file', requestParameters.body.file); + const multipartBoundary = '----openapi-nexus-' + Math.random().toString(16).slice(2); + const multipartChunks: Array = []; + headerParameters['Content-Type'] = 'multipart/form-data; boundary=' + multipartBoundary; + multipartChunks.push('--' + multipartBoundary + '\r\nContent-Disposition: form-data; name="file"; filename="file"\r\nContent-Type: application/octet-stream\r\n\r\n'); + multipartChunks.push(requestParameters.body.file); + multipartChunks.push('\r\n'); if (requestParameters.body.note !== undefined && requestParameters.body.note !== null) { - requestBody.append('note', String(requestParameters.body.note)); + multipartChunks.push('--' + multipartBoundary + '\r\nContent-Disposition: form-data; name="note"\r\nContent-Type: text/plain\r\n\r\n'); + multipartChunks.push(String(requestParameters.body.note)); + multipartChunks.push('\r\n'); } + multipartChunks.push('--' + multipartBoundary + '--\r\n'); + const requestBody = new Blob(multipartChunks); // Make request const response = await this.request({ path: urlPath, diff --git a/tests/golden/typescript/typescript-fetch/multipart-edge-cases/apis/MultipartApi.ts.golden b/tests/golden/typescript/typescript-fetch/multipart-edge-cases/apis/MultipartApi.ts.golden index 38dc1ea7b..00de0eec5 100644 --- a/tests/golden/typescript/typescript-fetch/multipart-edge-cases/apis/MultipartApi.ts.golden +++ b/tests/golden/typescript/typescript-fetch/multipart-edge-cases/apis/MultipartApi.ts.golden @@ -52,24 +52,38 @@ export class MultipartApi extends BaseAPI implements MultipartApiInterface { }; // Prepare request body - let requestBody: FormData | undefined = undefined; + let requestBody: Blob | undefined = undefined; if (requestParameters.body !== undefined && requestParameters.body !== null) { - requestBody = new FormData(); + const multipartBoundary = '----openapi-nexus-' + Math.random().toString(16).slice(2); + const multipartChunks: Array = []; + headerParameters['Content-Type'] = 'multipart/form-data; boundary=' + multipartBoundary; if (requestParameters.body.attributes !== undefined && requestParameters.body.attributes !== null) { - requestBody.append('attributes', JSON.stringify(requestParameters.body.attributes)); + multipartChunks.push('--' + multipartBoundary + '\r\nContent-Disposition: form-data; name="attributes"\r\nContent-Type: application/json\r\n\r\n'); + multipartChunks.push(JSON.stringify(requestParameters.body.attributes)); + multipartChunks.push('\r\n'); } if (requestParameters.body.enabled !== undefined && requestParameters.body.enabled !== null) { - requestBody.append('enabled', String(requestParameters.body.enabled)); + multipartChunks.push('--' + multipartBoundary + '\r\nContent-Disposition: form-data; name="enabled"\r\nContent-Type: text/plain\r\n\r\n'); + multipartChunks.push(String(requestParameters.body.enabled)); + multipartChunks.push('\r\n'); } if (requestParameters.body.file !== undefined && requestParameters.body.file !== null) { - requestBody.append('file', requestParameters.body.file); + multipartChunks.push('--' + multipartBoundary + '\r\nContent-Disposition: form-data; name="file"; filename="file"\r\nContent-Type: application/octet-stream\r\n\r\n'); + multipartChunks.push(requestParameters.body.file); + multipartChunks.push('\r\n'); } if (requestParameters.body.retry_count !== undefined && requestParameters.body.retry_count !== null) { - requestBody.append('retry_count', String(requestParameters.body.retry_count)); + multipartChunks.push('--' + multipartBoundary + '\r\nContent-Disposition: form-data; name="retry_count"\r\nContent-Type: text/plain\r\n\r\n'); + multipartChunks.push(String(requestParameters.body.retry_count)); + multipartChunks.push('\r\n'); } if (requestParameters.body.title !== undefined && requestParameters.body.title !== null) { - requestBody.append('title', String(requestParameters.body.title)); + multipartChunks.push('--' + multipartBoundary + '\r\nContent-Disposition: form-data; name="title"\r\nContent-Type: text/plain\r\n\r\n'); + multipartChunks.push(String(requestParameters.body.title)); + multipartChunks.push('\r\n'); } + multipartChunks.push('--' + multipartBoundary + '--\r\n'); + requestBody = new Blob(multipartChunks); } // Make request const response = await this.request({ @@ -112,10 +126,20 @@ export class MultipartApi extends BaseAPI implements MultipartApiInterface { }; // Prepare request body - const requestBody = new FormData(); - requestBody.append('enabled', String(requestParameters.body.enabled)); - requestBody.append('note', String(requestParameters.body.note)); - requestBody.append('retry_count', String(requestParameters.body.retry_count)); + const multipartBoundary = '----openapi-nexus-' + Math.random().toString(16).slice(2); + const multipartChunks: Array = []; + headerParameters['Content-Type'] = 'multipart/form-data; boundary=' + multipartBoundary; + multipartChunks.push('--' + multipartBoundary + '\r\nContent-Disposition: form-data; name="enabled"\r\nContent-Type: text/plain\r\n\r\n'); + multipartChunks.push(String(requestParameters.body.enabled)); + multipartChunks.push('\r\n'); + multipartChunks.push('--' + multipartBoundary + '\r\nContent-Disposition: form-data; name="note"\r\nContent-Type: text/plain\r\n\r\n'); + multipartChunks.push(String(requestParameters.body.note)); + multipartChunks.push('\r\n'); + multipartChunks.push('--' + multipartBoundary + '\r\nContent-Disposition: form-data; name="retry_count"\r\nContent-Type: text/plain\r\n\r\n'); + multipartChunks.push(String(requestParameters.body.retry_count)); + multipartChunks.push('\r\n'); + multipartChunks.push('--' + multipartBoundary + '--\r\n'); + const requestBody = new Blob(multipartChunks); // Make request const response = await this.request({ path: urlPath, diff --git a/tests/golden/typescript/typescript-fetch/multipart-explicit-encoding/README.md.golden b/tests/golden/typescript/typescript-fetch/multipart-explicit-encoding/README.md.golden new file mode 100644 index 000000000..4b1bee6a7 --- /dev/null +++ b/tests/golden/typescript/typescript-fetch/multipart-explicit-encoding/README.md.golden @@ -0,0 +1,248 @@ +# multipart-explicit-encoding + +Covers explicit multipart part content types. + +**Version:** 1.0.0 + +## Overview + +This package provides a TypeScript/JavaScript client for the Multipart Explicit Encoding 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 multipart-explicit-encoding +``` + +### From local path (development) + +Add the package to your `package.json` using the `file:` protocol: + +```json +{ + "dependencies": { + "multipart-explicit-encoding": "file:../../path/to/generated/package" + } +} +``` + +Then run: + +```bash +npm install +``` + +## Quick Start + +```typescript +import { Configuration, TransferApi } from 'multipart-explicit-encoding'; + +// Create a configuration +const config = new Configuration({ + basePath: 'https://api.example.com', + headers: { + 'Authorization': 'Bearer YOUR_TOKEN' + } +}); + +// Initialize the API client +const api = new TransferApi(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 'multipart-explicit-encoding'; + +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 'multipart-explicit-encoding'; + +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 'multipart-explicit-encoding'; + +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/multipart-explicit-encoding/apis/TransferApi.ts.golden b/tests/golden/typescript/typescript-fetch/multipart-explicit-encoding/apis/TransferApi.ts.golden new file mode 100644 index 000000000..61d048ee4 --- /dev/null +++ b/tests/golden/typescript/typescript-fetch/multipart-explicit-encoding/apis/TransferApi.ts.golden @@ -0,0 +1,90 @@ +/** + * @generated by openapi-nexus. Do not edit. + * + * Multipart Explicit Encoding — 1.0.0 + * Covers explicit multipart part content types. + */ +import type { UploadEncodedAssetRequest } from '../models/UploadEncodedAssetRequest'; +import type { Configuration, HTTPQuery, InitOverrideFunction } from '../runtime/runtime'; +import { BaseAPI, DefaultConfig, RequiredError, VoidApiResponse } from '../runtime/runtime'; + +export interface ApiUploadEncodedAssetRequest { + body: UploadEncodedAssetRequest; +} + +export type UploadEncodedAssetRawResponse = + | VoidApiResponse & { status: 204 } + | VoidApiResponse & { status: number }; + + +export interface TransferApiInterface { + uploadEncodedAssetRaw: (requestParameters: ApiUploadEncodedAssetRequest, initOverrides?: RequestInit | InitOverrideFunction) => Promise; + uploadEncodedAsset: (requestParameters: ApiUploadEncodedAssetRequest, initOverrides?: RequestInit | InitOverrideFunction) => Promise; +} + +export class TransferApi extends BaseAPI implements TransferApiInterface { + /** + * Initialize the API client + */ + constructor(configuration?: Configuration) { + super(configuration ?? DefaultConfig); + } + + async uploadEncodedAssetRaw(requestParameters: ApiUploadEncodedAssetRequest, + initOverrides?: RequestInit | InitOverrideFunction): Promise { + if (requestParameters.body === undefined || requestParameters.body === null) { + throw new RequiredError( + 'body', + 'Required parameter "body" was null or undefined when calling uploadEncodedAssetRaw().' + ); + } + // Build path with path parameters + const urlPath = `/uploads/encoded`; + // Build query parameters + const queryParameters: HTTPQuery = {}; + // Build headers + const headerParameters: Record = { + }; + + // Prepare request body + const multipartBoundary = '----openapi-nexus-' + Math.random().toString(16).slice(2); + const multipartChunks: Array = []; + headerParameters['Content-Type'] = 'multipart/form-data; boundary=' + multipartBoundary; + multipartChunks.push('--' + multipartBoundary + '\r\nContent-Disposition: form-data; name="audit"\r\nContent-Type: application/json\r\n\r\n'); + multipartChunks.push(JSON.stringify(requestParameters.body.audit)); + multipartChunks.push('\r\n'); + multipartChunks.push('--' + multipartBoundary + '\r\nContent-Disposition: form-data; name="file"; filename="file"\r\nContent-Type: application/pdf\r\n\r\n'); + multipartChunks.push(requestParameters.body.file); + multipartChunks.push('\r\n'); + multipartChunks.push('--' + multipartBoundary + '\r\nContent-Disposition: form-data; name="metadata"\r\nContent-Type: application/vnd.openapi-nexus.metadata+json\r\n\r\n'); + multipartChunks.push(JSON.stringify(requestParameters.body.metadata)); + multipartChunks.push('\r\n'); + multipartChunks.push('--' + multipartBoundary + '\r\nContent-Disposition: form-data; name="purpose"\r\nContent-Type: text/plain\r\n\r\n'); + multipartChunks.push(String(requestParameters.body.purpose)); + multipartChunks.push('\r\n'); + multipartChunks.push('--' + multipartBoundary + '--\r\n'); + const requestBody = new Blob(multipartChunks); + // 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 uploadEncodedAsset(requestParameters: ApiUploadEncodedAssetRequest, + initOverrides?: RequestInit | InitOverrideFunction): Promise { + const response = await this.uploadEncodedAssetRaw(requestParameters, initOverrides); + return await response.value(); + } +} diff --git a/tests/golden/typescript/typescript-fetch/multipart-explicit-encoding/apis/index.ts.golden b/tests/golden/typescript/typescript-fetch/multipart-explicit-encoding/apis/index.ts.golden new file mode 100644 index 000000000..fd978d2d8 --- /dev/null +++ b/tests/golden/typescript/typescript-fetch/multipart-explicit-encoding/apis/index.ts.golden @@ -0,0 +1,12 @@ +/** + * @generated by openapi-nexus. Do not edit. + * + * Multipart Explicit Encoding — 1.0.0 + * Covers explicit multipart part content types. + */ +export type { + ApiUploadEncodedAssetRequest, + UploadEncodedAssetRawResponse, + TransferApiInterface, +} from './TransferApi'; +export { TransferApi } from './TransferApi'; diff --git a/tests/golden/typescript/typescript-fetch/multipart-explicit-encoding/index.ts.golden b/tests/golden/typescript/typescript-fetch/multipart-explicit-encoding/index.ts.golden new file mode 100644 index 000000000..3d84e59ac --- /dev/null +++ b/tests/golden/typescript/typescript-fetch/multipart-explicit-encoding/index.ts.golden @@ -0,0 +1,9 @@ +/** + * @generated by openapi-nexus. Do not edit. + * + * Multipart Explicit Encoding — 1.0.0 + * Covers explicit multipart part content types. + */ +export * from './runtime/runtime'; +export * from './apis'; +export * from './models'; diff --git a/tests/golden/typescript/typescript-fetch/multipart-explicit-encoding/models/AuditAttributes.ts.golden b/tests/golden/typescript/typescript-fetch/multipart-explicit-encoding/models/AuditAttributes.ts.golden new file mode 100644 index 000000000..0406dbf88 --- /dev/null +++ b/tests/golden/typescript/typescript-fetch/multipart-explicit-encoding/models/AuditAttributes.ts.golden @@ -0,0 +1,10 @@ +/** + * @generated by openapi-nexus. Do not edit. + * + * Multipart Explicit Encoding — 1.0.0 + * Covers explicit multipart part content types. + */ +export interface AuditAttributes { + readonly request_id: string; + readonly reviewed?: boolean; +} diff --git a/tests/golden/typescript/typescript-fetch/multipart-explicit-encoding/models/UploadAttributes.ts.golden b/tests/golden/typescript/typescript-fetch/multipart-explicit-encoding/models/UploadAttributes.ts.golden new file mode 100644 index 000000000..d39bc5bda --- /dev/null +++ b/tests/golden/typescript/typescript-fetch/multipart-explicit-encoding/models/UploadAttributes.ts.golden @@ -0,0 +1,10 @@ +/** + * @generated by openapi-nexus. Do not edit. + * + * Multipart Explicit Encoding — 1.0.0 + * Covers explicit multipart part content types. + */ +export interface UploadAttributes { + readonly label: string; + readonly retention_days?: number; +} diff --git a/tests/golden/typescript/typescript-fetch/multipart-explicit-encoding/models/UploadEncodedAssetRequest.ts.golden b/tests/golden/typescript/typescript-fetch/multipart-explicit-encoding/models/UploadEncodedAssetRequest.ts.golden new file mode 100644 index 000000000..382b2386c --- /dev/null +++ b/tests/golden/typescript/typescript-fetch/multipart-explicit-encoding/models/UploadEncodedAssetRequest.ts.golden @@ -0,0 +1,21 @@ +/** + * @generated by openapi-nexus. Do not edit. + * + * Multipart Explicit Encoding — 1.0.0 + * Covers explicit multipart part content types. + */ +import type { AuditAttributes } from './AuditAttributes'; +import type { UploadAttributes } from './UploadAttributes'; + +export interface UploadEncodedAssetRequest { + readonly audit: AuditAttributes; + /** + * Binary content with an explicit content type + */ + readonly file: Blob | File; + readonly metadata: UploadAttributes; + /** + * Plain-text upload purpose + */ + readonly purpose: string; +} diff --git a/tests/golden/typescript/typescript-fetch/multipart-explicit-encoding/models/index.ts.golden b/tests/golden/typescript/typescript-fetch/multipart-explicit-encoding/models/index.ts.golden new file mode 100644 index 000000000..3a9e838fb --- /dev/null +++ b/tests/golden/typescript/typescript-fetch/multipart-explicit-encoding/models/index.ts.golden @@ -0,0 +1,9 @@ +/** + * @generated by openapi-nexus. Do not edit. + * + * Multipart Explicit Encoding — 1.0.0 + * Covers explicit multipart part content types. + */ +export type { AuditAttributes } from './AuditAttributes'; +export type { UploadAttributes } from './UploadAttributes'; +export type { UploadEncodedAssetRequest } from './UploadEncodedAssetRequest'; diff --git a/tests/golden/typescript/typescript-fetch/multipart-explicit-encoding/package.json.golden b/tests/golden/typescript/typescript-fetch/multipart-explicit-encoding/package.json.golden new file mode 100644 index 000000000..d6e22243e --- /dev/null +++ b/tests/golden/typescript/typescript-fetch/multipart-explicit-encoding/package.json.golden @@ -0,0 +1,26 @@ +{ + "description": "Covers explicit multipart part content types.", + "exports": { + ".": { + "default": "./dist/index.js", + "types": "./dist/index.d.ts" + } + }, + "files": [ + "dist" + ], + "keywords": [ + "openapi", + "api-client", + "typescript", + "generated" + ], + "main": "./dist/index.js", + "name": "multipart-explicit-encoding", + "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/multipart-explicit-encoding/runtime/runtime.ts.golden b/tests/golden/typescript/typescript-fetch/multipart-explicit-encoding/runtime/runtime.ts.golden new file mode 100644 index 000000000..b5dc88774 --- /dev/null +++ b/tests/golden/typescript/typescript-fetch/multipart-explicit-encoding/runtime/runtime.ts.golden @@ -0,0 +1,461 @@ +/** + * @generated by openapi-nexus. Do not edit. + * + * Multipart Explicit Encoding — 1.0.0 + * Covers explicit multipart part content types. + */ +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/multipart-explicit-encoding/tsconfig.esm.json.golden b/tests/golden/typescript/typescript-fetch/multipart-explicit-encoding/tsconfig.esm.json.golden new file mode 100644 index 000000000..bb8350cb6 --- /dev/null +++ b/tests/golden/typescript/typescript-fetch/multipart-explicit-encoding/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/multipart-explicit-encoding/tsconfig.json.golden b/tests/golden/typescript/typescript-fetch/multipart-explicit-encoding/tsconfig.json.golden new file mode 100644 index 000000000..4b3a8cf7f --- /dev/null +++ b/tests/golden/typescript/typescript-fetch/multipart-explicit-encoding/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/typescript/typescript-fetch/multipart-nested-object-parts/apis/MultipartApi.ts.golden b/tests/golden/typescript/typescript-fetch/multipart-nested-object-parts/apis/MultipartApi.ts.golden index c24bcc9a1..345fc5eeb 100644 --- a/tests/golden/typescript/typescript-fetch/multipart-nested-object-parts/apis/MultipartApi.ts.golden +++ b/tests/golden/typescript/typescript-fetch/multipart-nested-object-parts/apis/MultipartApi.ts.golden @@ -47,9 +47,17 @@ export class MultipartApi extends BaseAPI implements MultipartApiInterface { }; // Prepare request body - const requestBody = new FormData(); - requestBody.append('file', requestParameters.body.file); - requestBody.append('item_config', JSON.stringify(requestParameters.body.item_config)); + const multipartBoundary = '----openapi-nexus-' + Math.random().toString(16).slice(2); + const multipartChunks: Array = []; + headerParameters['Content-Type'] = 'multipart/form-data; boundary=' + multipartBoundary; + multipartChunks.push('--' + multipartBoundary + '\r\nContent-Disposition: form-data; name="file"; filename="file"\r\nContent-Type: application/octet-stream\r\n\r\n'); + multipartChunks.push(requestParameters.body.file); + multipartChunks.push('\r\n'); + multipartChunks.push('--' + multipartBoundary + '\r\nContent-Disposition: form-data; name="item_config"\r\nContent-Type: application/json\r\n\r\n'); + multipartChunks.push(JSON.stringify(requestParameters.body.item_config)); + multipartChunks.push('\r\n'); + multipartChunks.push('--' + multipartBoundary + '--\r\n'); + const requestBody = new Blob(multipartChunks); // Make request const response = await this.request({ path: urlPath, diff --git a/tests/golden/typescript/typescript-fetch/ts-property-naming-camel-case-multipart-parts/apis/MultipartApi.ts.golden b/tests/golden/typescript/typescript-fetch/ts-property-naming-camel-case-multipart-parts/apis/MultipartApi.ts.golden index aa517a178..8d3ee60f9 100644 --- a/tests/golden/typescript/typescript-fetch/ts-property-naming-camel-case-multipart-parts/apis/MultipartApi.ts.golden +++ b/tests/golden/typescript/typescript-fetch/ts-property-naming-camel-case-multipart-parts/apis/MultipartApi.ts.golden @@ -48,9 +48,17 @@ export class MultipartApi extends BaseAPI implements MultipartApiInterface { }; // Prepare request body - const requestBody = new FormData(); - requestBody.append('file', requestParameters.body.file); - requestBody.append('item_config', JSON.stringify(itemConfigToJSON(requestParameters.body.itemConfig))); + const multipartBoundary = '----openapi-nexus-' + Math.random().toString(16).slice(2); + const multipartChunks: Array = []; + headerParameters['Content-Type'] = 'multipart/form-data; boundary=' + multipartBoundary; + multipartChunks.push('--' + multipartBoundary + '\r\nContent-Disposition: form-data; name="file"; filename="file"\r\nContent-Type: application/octet-stream\r\n\r\n'); + multipartChunks.push(requestParameters.body.file); + multipartChunks.push('\r\n'); + multipartChunks.push('--' + multipartBoundary + '\r\nContent-Disposition: form-data; name="item_config"\r\nContent-Type: application/json\r\n\r\n'); + multipartChunks.push(JSON.stringify(itemConfigToJSON(requestParameters.body.itemConfig))); + multipartChunks.push('\r\n'); + multipartChunks.push('--' + multipartBoundary + '--\r\n'); + const requestBody = new Blob(multipartChunks); // Make request const response = await this.request({ path: urlPath, diff --git a/tests/golden_tests_go_http.rs b/tests/golden_tests_go_http.rs index 536148a8b..5ed8808ca 100644 --- a/tests/golden_tests_go_http.rs +++ b/tests/golden_tests_go_http.rs @@ -33,6 +33,7 @@ fn get_golden_test_cases() -> HashMap<&'static str, &'static str> { ("request-body-content-types", "valid/request-body-content-types.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"), ("multipart-edge-cases", "valid/multipart-edge-cases.yaml"), ("multipart-nested-object-parts", "valid/multipart-nested-object-parts.yaml"), ("multipart-unsupported-schema", "valid/multipart-unsupported-schema.yaml"), @@ -117,6 +118,7 @@ generate_golden_tests! { test_request_body_content_types_golden: "request-body-content-types", 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", test_multipart_edge_cases_golden: "multipart-edge-cases", test_multipart_nested_object_parts_golden: "multipart-nested-object-parts", test_multipart_unsupported_schema_golden: "multipart-unsupported-schema", diff --git a/tests/golden_tests_java_okhttp.rs b/tests/golden_tests_java_okhttp.rs index caa7ffb6c..ba7ad74c3 100644 --- a/tests/golden_tests_java_okhttp.rs +++ b/tests/golden_tests_java_okhttp.rs @@ -33,6 +33,7 @@ fn get_golden_test_cases() -> HashMap<&'static str, &'static str> { ("request-body-content-types", "valid/request-body-content-types.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"), ("multipart-edge-cases", "valid/multipart-edge-cases.yaml"), ("multipart-nested-object-parts", "valid/multipart-nested-object-parts.yaml"), ("multipart-unsupported-schema", "valid/multipart-unsupported-schema.yaml"), @@ -118,6 +119,7 @@ generate_golden_tests! { test_request_body_content_types_golden: "request-body-content-types", 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", test_multipart_edge_cases_golden: "multipart-edge-cases", test_multipart_nested_object_parts_golden: "multipart-nested-object-parts", test_multipart_unsupported_schema_golden: "multipart-unsupported-schema", diff --git a/tests/golden_tests_kotlin_okhttp.rs b/tests/golden_tests_kotlin_okhttp.rs index 4c06ca017..8cfff7713 100644 --- a/tests/golden_tests_kotlin_okhttp.rs +++ b/tests/golden_tests_kotlin_okhttp.rs @@ -33,6 +33,7 @@ fn get_golden_test_cases() -> HashMap<&'static str, &'static str> { ("request-body-content-types", "valid/request-body-content-types.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"), ("multipart-edge-cases", "valid/multipart-edge-cases.yaml"), ("multipart-nested-object-parts", "valid/multipart-nested-object-parts.yaml"), ("multipart-unsupported-schema", "valid/multipart-unsupported-schema.yaml"), @@ -118,6 +119,7 @@ generate_golden_tests! { test_request_body_content_types_golden: "request-body-content-types", 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", test_multipart_edge_cases_golden: "multipart-edge-cases", test_multipart_nested_object_parts_golden: "multipart-nested-object-parts", test_multipart_unsupported_schema_golden: "multipart-unsupported-schema", diff --git a/tests/golden_tests_python_httpx.rs b/tests/golden_tests_python_httpx.rs index 397325050..a26221b7f 100644 --- a/tests/golden_tests_python_httpx.rs +++ b/tests/golden_tests_python_httpx.rs @@ -33,6 +33,7 @@ fn get_golden_test_cases() -> HashMap<&'static str, &'static str> { ("request-body-content-types", "valid/request-body-content-types.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"), ("multipart-edge-cases", "valid/multipart-edge-cases.yaml"), ("multipart-nested-object-parts", "valid/multipart-nested-object-parts.yaml"), ("multipart-unsupported-schema", "valid/multipart-unsupported-schema.yaml"), @@ -117,6 +118,7 @@ generate_golden_tests! { test_request_body_content_types_golden: "request-body-content-types", 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", test_multipart_edge_cases_golden: "multipart-edge-cases", test_multipart_nested_object_parts_golden: "multipart-nested-object-parts", test_multipart_unsupported_schema_golden: "multipart-unsupported-schema", diff --git a/tests/golden_tests_python_requests.rs b/tests/golden_tests_python_requests.rs index 3aa850c0c..feb7dcc66 100644 --- a/tests/golden_tests_python_requests.rs +++ b/tests/golden_tests_python_requests.rs @@ -33,6 +33,7 @@ fn get_golden_test_cases() -> HashMap<&'static str, &'static str> { ("request-body-content-types", "valid/request-body-content-types.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"), ("multipart-edge-cases", "valid/multipart-edge-cases.yaml"), ("multipart-nested-object-parts", "valid/multipart-nested-object-parts.yaml"), ("multipart-unsupported-schema", "valid/multipart-unsupported-schema.yaml"), @@ -117,6 +118,7 @@ generate_golden_tests! { test_request_body_content_types_golden: "request-body-content-types", 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", test_multipart_edge_cases_golden: "multipart-edge-cases", test_multipart_nested_object_parts_golden: "multipart-nested-object-parts", test_multipart_unsupported_schema_golden: "multipart-unsupported-schema", diff --git a/tests/golden_tests_rust_aioduct.rs b/tests/golden_tests_rust_aioduct.rs index 1006e4bdb..2086e36ca 100644 --- a/tests/golden_tests_rust_aioduct.rs +++ b/tests/golden_tests_rust_aioduct.rs @@ -35,6 +35,7 @@ fn get_golden_test_cases() -> HashMap<&'static str, &'static str> { ("request-body-content-types", "valid/request-body-content-types.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"), ("multipart-edge-cases", "valid/multipart-edge-cases.yaml"), ("multipart-nested-object-parts", "valid/multipart-nested-object-parts.yaml"), ("multipart-unsupported-schema", "valid/multipart-unsupported-schema.yaml"), @@ -119,6 +120,7 @@ generate_golden_tests! { test_request_body_content_types_golden: "request-body-content-types", 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", test_multipart_edge_cases_golden: "multipart-edge-cases", test_multipart_nested_object_parts_golden: "multipart-nested-object-parts", test_multipart_unsupported_schema_golden: "multipart-unsupported-schema", diff --git a/tests/golden_tests_rust_reqwest.rs b/tests/golden_tests_rust_reqwest.rs index 1b6c1d319..3228a85b7 100644 --- a/tests/golden_tests_rust_reqwest.rs +++ b/tests/golden_tests_rust_reqwest.rs @@ -35,6 +35,7 @@ fn get_golden_test_cases() -> HashMap<&'static str, &'static str> { ("request-body-content-types", "valid/request-body-content-types.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"), ("multipart-edge-cases", "valid/multipart-edge-cases.yaml"), ("multipart-nested-object-parts", "valid/multipart-nested-object-parts.yaml"), ("multipart-unsupported-schema", "valid/multipart-unsupported-schema.yaml"), @@ -119,6 +120,7 @@ generate_golden_tests! { test_request_body_content_types_golden: "request-body-content-types", 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", test_multipart_edge_cases_golden: "multipart-edge-cases", test_multipart_nested_object_parts_golden: "multipart-nested-object-parts", test_multipart_unsupported_schema_golden: "multipart-unsupported-schema", diff --git a/tests/golden_tests_rust_ureq.rs b/tests/golden_tests_rust_ureq.rs index b4c73c4ab..72d5b0a4c 100644 --- a/tests/golden_tests_rust_ureq.rs +++ b/tests/golden_tests_rust_ureq.rs @@ -35,6 +35,7 @@ fn get_golden_test_cases() -> HashMap<&'static str, &'static str> { ("request-body-content-types", "valid/request-body-content-types.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"), ("multipart-edge-cases", "valid/multipart-edge-cases.yaml"), ("multipart-nested-object-parts", "valid/multipart-nested-object-parts.yaml"), ("multipart-unsupported-schema", "valid/multipart-unsupported-schema.yaml"), @@ -119,6 +120,7 @@ generate_golden_tests! { test_request_body_content_types_golden: "request-body-content-types", 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", test_multipart_edge_cases_golden: "multipart-edge-cases", test_multipart_nested_object_parts_golden: "multipart-nested-object-parts", test_multipart_unsupported_schema_golden: "multipart-unsupported-schema", diff --git a/tests/golden_tests_typescript_fetch.rs b/tests/golden_tests_typescript_fetch.rs index a9af92752..55d32bb88 100644 --- a/tests/golden_tests_typescript_fetch.rs +++ b/tests/golden_tests_typescript_fetch.rs @@ -33,6 +33,7 @@ fn get_golden_test_cases() -> HashMap<&'static str, &'static str> { ("request-body-content-types", "valid/request-body-content-types.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"), ("multipart-edge-cases", "valid/multipart-edge-cases.yaml"), ("multipart-nested-object-parts", "valid/multipart-nested-object-parts.yaml"), ("multipart-unsupported-schema", "valid/multipart-unsupported-schema.yaml"), @@ -117,6 +118,7 @@ generate_golden_tests! { test_request_body_content_types_golden: "request-body-content-types", 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", test_multipart_edge_cases_golden: "multipart-edge-cases", test_multipart_nested_object_parts_golden: "multipart-nested-object-parts", test_multipart_unsupported_schema_golden: "multipart-unsupported-schema", diff --git a/tests/multipart_runtime_smoke.rs b/tests/multipart_runtime_smoke.rs index b753253ed..32dda8136 100644 --- a/tests/multipart_runtime_smoke.rs +++ b/tests/multipart_runtime_smoke.rs @@ -56,7 +56,7 @@ fn multipart_wire_construction_is_pinned_across_non_rust_clients() { generate_files(&PythonHttpxCodeGenerator::new(empty_config()), &fixture).unwrap(); let httpx_api = generated_file(&httpx_files, "apis/multipart_api.py"); assert!(httpx_api.contains("files: dict[str, object] = {}")); - assert!(httpx_api.contains("files[\"note\"] = (None, str(body.note))")); + assert!(httpx_api.contains("files[\"note\"] = (None, str(body.note), \"text/plain\")")); assert!(httpx_api.contains("files=files if files else None")); assert!(!httpx_api.contains("data=data")); @@ -64,21 +64,26 @@ fn multipart_wire_construction_is_pinned_across_non_rust_clients() { generate_files(&PythonRequestsCodeGenerator::new(empty_config()), &fixture).unwrap(); let requests_api = generated_file(&requests_files, "apis/multipart_api.py"); assert!(requests_api.contains("files: dict[str, object] = {}")); - assert!(requests_api.contains("files[\"note\"] = (None, str(body.note))")); + assert!(requests_api.contains("files[\"note\"] = (None, str(body.note), \"text/plain\")")); assert!(requests_api.contains("files=files if files else None")); assert!(!requests_api.contains("data=data")); let ts_files = generate_files(&TypeScriptFetchCodeGenerator::new(empty_config()), &fixture).unwrap(); let ts_api = generated_file(&ts_files, "apis/MultipartApi.ts"); - assert!(ts_api.contains("let requestBody: FormData | undefined = undefined;")); + assert!(ts_api.contains("let requestBody: Blob | undefined = undefined;")); assert!( ts_api.contains( "if (requestParameters.body !== undefined && requestParameters.body !== null)" ) ); - assert!(ts_api.contains("const requestBody = new FormData();")); - assert!(ts_api.contains("requestBody.append('note', String(requestParameters.body.note));")); + assert!(ts_api.contains("const multipartChunks: Array = [];")); + assert!( + ts_api.contains( + "Content-Disposition: form-data; name=\"note\"\\r\\nContent-Type: text/plain" + ) + ); + assert!(ts_api.contains("multipartChunks.push(String(requestParameters.body.note));")); } #[test] @@ -251,6 +256,6 @@ property_naming = "camelCase" assert!(api.contains("itemConfigToJSON")); assert!(api.contains( - "requestBody.append('item_config', JSON.stringify(itemConfigToJSON(requestParameters.body.itemConfig)));" + "multipartChunks.push(JSON.stringify(itemConfigToJSON(requestParameters.body.itemConfig)));" )); }