From c23ee7ee96b894fd5e1137586ca7fd9de229924d Mon Sep 17 00:00:00 2001 From: Adam Basfop Cavendish Date: Wed, 17 Jun 2026 03:38:40 +0800 Subject: [PATCH] fix: preserve multipart upload filenames across clients IR: - Add request-body-specific multipart input planning - Keep canonical binary schemas unchanged outside multipart request bodies - Track per-part filename fallbacks from multipart wire names Clients: - Emit UploadFile wrappers only for multipart upload request inputs - Read caller filenames across TypeScript, Go, Python, Java, Kotlin, and Rust clients - Keep text and JSON multipart parts on their existing code paths Tests: - Regenerate multipart-capable goldens across all generators - Pin dynamic filename behavior in cross-client smoke coverage --- src/generators/go/http/codegen.rs | 17 +- src/generators/go/http/runtime.rs | 14 +- .../go/http/runtime/upload_file.go.txt | 21 ++ src/generators/go/http/sigil_emit.rs | 55 ++++- src/generators/go/http/sigil_emit_api.rs | 77 ++++-- src/generators/java/okhttp/codegen.rs | 21 +- src/generators/java/okhttp/runtime.rs | 14 +- .../java/okhttp/runtime/UploadFile.java.txt | 31 +++ src/generators/java/okhttp/sigil_emit.rs | 71 ++++++ src/generators/java/okhttp/sigil_emit_api.rs | 32 ++- src/generators/kotlin/okhttp/codegen.rs | 21 +- src/generators/kotlin/okhttp/runtime.rs | 14 +- .../kotlin/okhttp/runtime/upload_file.kt.txt | 8 + src/generators/kotlin/okhttp/sigil_emit.rs | 46 ++++ .../kotlin/okhttp/sigil_emit_api.rs | 32 ++- src/generators/mod.rs | 1 + src/generators/multipart.rs | 4 +- src/generators/python/httpx/codegen.rs | 10 +- src/generators/python/httpx/emit_api.rs | 44 +++- src/generators/python/httpx/emit_models.rs | 102 +++++++- src/generators/python/httpx/project_files.rs | 23 +- src/generators/python/httpx/runtime.rs | 24 +- .../python/httpx/runtime/upload_file.py.txt | 16 ++ src/generators/python/requests/codegen.rs | 10 +- src/generators/python/requests/emit_api.rs | 44 +++- src/generators/python/requests/emit_models.rs | 102 +++++++- .../python/requests/project_files.rs | 23 +- src/generators/python/requests/runtime.rs | 24 +- .../requests/runtime/upload_file.py.txt | 16 ++ src/generators/request_inputs.rs | 228 ++++++++++++++++++ src/generators/rust/aioduct/codegen.rs | 15 +- src/generators/rust/aioduct/runtime.rs | 27 ++- .../rust/aioduct/runtime/upload_file.rs.txt | 33 +++ src/generators/rust/aioduct/sigil_emit_api.rs | 12 +- src/generators/rust/common/emit_api.rs | 50 +++- src/generators/rust/common/emit_models.rs | 42 ++++ src/generators/rust/reqwest/codegen.rs | 11 +- src/generators/rust/reqwest/runtime.rs | 23 +- .../rust/reqwest/runtime/upload_file.rs.txt | 33 +++ src/generators/rust/reqwest/sigil_emit_api.rs | 12 +- src/generators/rust/ureq/codegen.rs | 11 +- src/generators/rust/ureq/runtime.rs | 23 +- .../rust/ureq/runtime/upload_file.rs.txt | 33 +++ src/generators/rust/ureq/sigil_emit_api.rs | 32 ++- src/generators/typescript/fetch/codegen.rs | 188 ++++++++++++++- .../typescript/fetch/project_files.rs | 49 +++- .../fetch/project_files/runtime.ts.txt | 2 +- .../typescript/fetch/sigil_emit_api.rs | 138 ++++++++--- .../apis/transfer.go.golden | 8 +- ...oad_asset_multipart_request_body.go.golden | 14 ++ .../runtime/upload_file.go.golden | 26 ++ .../media-type-selection/apis/media.go.golden | 12 +- ...multipart_multipart_request_body.go.golden | 13 + .../runtime/upload_file.go.golden | 26 ++ .../apis/multipart.go.golden | 25 +- ...nal_parts_multipart_request_body.go.golden | 16 ++ ...xt_fields_multipart_request_body.go.golden | 12 + .../runtime/upload_file.go.golden | 26 ++ .../apis/transfer.go.golden | 9 +- ...ded_asset_multipart_request_body.go.golden | 15 ++ .../runtime/upload_file.go.golden | 26 ++ .../apis/multipart.go.golden | 9 +- ...ject_part_multipart_request_body.go.golden | 13 + .../runtime/upload_file.go.golden | 26 ++ .../apis/TransferApi.java.golden | 4 +- ...ploadAssetMultipartRequestBody.java.golden | 33 +++ .../runtime/UploadFile.java.golden | 36 +++ .../apis/MediaApi.java.golden | 4 +- ...dMultipartMultipartRequestBody.java.golden | 27 +++ .../runtime/UploadFile.java.golden | 36 +++ .../apis/MultipartApi.java.golden | 6 +- ...ionalPartsMultipartRequestBody.java.golden | 45 ++++ ...TextFieldsMultipartRequestBody.java.golden | 31 +++ .../runtime/UploadFile.java.golden | 36 +++ .../apis/TransferApi.java.golden | 4 +- ...codedAssetMultipartRequestBody.java.golden | 39 +++ .../runtime/UploadFile.java.golden | 36 +++ .../apis/MultipartApi.java.golden | 4 +- ...ObjectPartMultipartRequestBody.java.golden | 27 +++ .../runtime/UploadFile.java.golden | 36 +++ .../apis/TransferApi.kt.golden | 4 +- .../UploadAssetMultipartRequestBody.kt.golden | 14 ++ .../runtime/UploadFile.kt.golden | 13 + .../apis/MediaApi.kt.golden | 4 +- ...zedMultipartMultipartRequestBody.kt.golden | 13 + .../runtime/UploadFile.kt.golden | 13 + .../apis/MultipartApi.kt.golden | 6 +- ...ptionalPartsMultipartRequestBody.kt.golden | 16 ++ ...ndTextFieldsMultipartRequestBody.kt.golden | 12 + .../runtime/UploadFile.kt.golden | 13 + .../apis/TransferApi.kt.golden | 4 +- ...EncodedAssetMultipartRequestBody.kt.golden | 15 ++ .../runtime/UploadFile.kt.golden | 13 + .../apis/MultipartApi.kt.golden | 4 +- ...edObjectPartMultipartRequestBody.kt.golden | 13 + .../runtime/UploadFile.kt.golden | 13 + .../__init__.py.golden | 1 + .../apis/transfer_api.py.golden | 6 +- .../models/__init__.py.golden | 1 + ...oad_asset_multipart_request_body.py.golden | 16 ++ .../runtime/__init__.py.golden | 1 + .../runtime/upload_file.py.golden | 21 ++ .../media_type_selection/__init__.py.golden | 1 + .../apis/media_api.py.golden | 6 +- .../models/__init__.py.golden | 1 + ...multipart_multipart_request_body.py.golden | 14 ++ .../runtime/__init__.py.golden | 1 + .../runtime/upload_file.py.golden | 21 ++ .../multipart_edge_cases/__init__.py.golden | 1 + .../apis/multipart_api.py.golden | 10 +- .../models/__init__.py.golden | 2 + ...nal_parts_multipart_request_body.py.golden | 18 ++ ...xt_fields_multipart_request_body.py.golden | 14 ++ .../runtime/__init__.py.golden | 1 + .../runtime/upload_file.py.golden | 21 ++ .../__init__.py.golden | 1 + .../apis/transfer_api.py.golden | 6 +- .../models/__init__.py.golden | 1 + ...ded_asset_multipart_request_body.py.golden | 18 ++ .../runtime/__init__.py.golden | 1 + .../runtime/upload_file.py.golden | 21 ++ .../__init__.py.golden | 1 + .../apis/multipart_api.py.golden | 6 +- .../models/__init__.py.golden | 1 + ...ject_part_multipart_request_body.py.golden | 15 ++ .../runtime/__init__.py.golden | 1 + .../runtime/upload_file.py.golden | 21 ++ .../__init__.py.golden | 1 + .../apis/transfer_api.py.golden | 6 +- .../models/__init__.py.golden | 1 + ...oad_asset_multipart_request_body.py.golden | 16 ++ .../runtime/__init__.py.golden | 1 + .../runtime/upload_file.py.golden | 21 ++ .../media_type_selection/__init__.py.golden | 1 + .../apis/media_api.py.golden | 6 +- .../models/__init__.py.golden | 1 + ...multipart_multipart_request_body.py.golden | 14 ++ .../runtime/__init__.py.golden | 1 + .../runtime/upload_file.py.golden | 21 ++ .../multipart_edge_cases/__init__.py.golden | 1 + .../apis/multipart_api.py.golden | 10 +- .../models/__init__.py.golden | 2 + ...nal_parts_multipart_request_body.py.golden | 18 ++ ...xt_fields_multipart_request_body.py.golden | 14 ++ .../runtime/__init__.py.golden | 1 + .../runtime/upload_file.py.golden | 21 ++ .../__init__.py.golden | 1 + .../apis/transfer_api.py.golden | 6 +- .../models/__init__.py.golden | 1 + ...ded_asset_multipart_request_body.py.golden | 18 ++ .../runtime/__init__.py.golden | 1 + .../runtime/upload_file.py.golden | 21 ++ .../__init__.py.golden | 1 + .../apis/multipart_api.py.golden | 6 +- .../models/__init__.py.golden | 1 + ...ject_part_multipart_request_body.py.golden | 15 ++ .../runtime/__init__.py.golden | 1 + .../runtime/upload_file.py.golden | 21 ++ .../src/apis/transfer.rs.golden | 4 +- .../src/models/mod.rs.golden | 2 + ...oad_asset_multipart_request_body.rs.golden | 14 ++ .../src/runtime/mod.rs.golden | 2 + .../src/runtime/upload_file.rs.golden | 39 +++ .../src/apis/media.rs.golden | 4 +- .../src/models/mod.rs.golden | 2 + ...multipart_multipart_request_body.rs.golden | 13 + .../src/runtime/mod.rs.golden | 2 + .../src/runtime/upload_file.rs.golden | 39 +++ .../src/apis/multipart.rs.golden | 6 +- .../src/models/mod.rs.golden | 4 + ...nal_parts_multipart_request_body.rs.golden | 16 ++ ...xt_fields_multipart_request_body.rs.golden | 12 + .../src/runtime/mod.rs.golden | 2 + .../src/runtime/upload_file.rs.golden | 39 +++ .../src/apis/transfer.rs.golden | 4 +- .../src/models/mod.rs.golden | 2 + ...ded_asset_multipart_request_body.rs.golden | 15 ++ .../src/runtime/mod.rs.golden | 2 + .../src/runtime/upload_file.rs.golden | 39 +++ .../src/apis/multipart.rs.golden | 4 +- .../src/models/mod.rs.golden | 2 + ...ject_part_multipart_request_body.rs.golden | 13 + .../src/runtime/mod.rs.golden | 2 + .../src/runtime/upload_file.rs.golden | 39 +++ .../src/apis/transfer.rs.golden | 4 +- .../src/models/mod.rs.golden | 2 + ...oad_asset_multipart_request_body.rs.golden | 14 ++ .../src/runtime/mod.rs.golden | 2 + .../src/runtime/upload_file.rs.golden | 39 +++ .../src/apis/media.rs.golden | 4 +- .../src/models/mod.rs.golden | 2 + ...multipart_multipart_request_body.rs.golden | 13 + .../src/runtime/mod.rs.golden | 2 + .../src/runtime/upload_file.rs.golden | 39 +++ .../src/apis/multipart.rs.golden | 6 +- .../src/models/mod.rs.golden | 4 + ...nal_parts_multipart_request_body.rs.golden | 16 ++ ...xt_fields_multipart_request_body.rs.golden | 12 + .../src/runtime/mod.rs.golden | 2 + .../src/runtime/upload_file.rs.golden | 39 +++ .../src/apis/transfer.rs.golden | 4 +- .../src/models/mod.rs.golden | 2 + ...ded_asset_multipart_request_body.rs.golden | 15 ++ .../src/runtime/mod.rs.golden | 2 + .../src/runtime/upload_file.rs.golden | 39 +++ .../src/apis/multipart.rs.golden | 4 +- .../src/models/mod.rs.golden | 2 + ...ject_part_multipart_request_body.rs.golden | 13 + .../src/runtime/mod.rs.golden | 2 + .../src/runtime/upload_file.rs.golden | 39 +++ .../src/apis/transfer.rs.golden | 10 +- .../src/models/mod.rs.golden | 2 + ...oad_asset_multipart_request_body.rs.golden | 14 ++ .../src/runtime/mod.rs.golden | 2 + .../src/runtime/upload_file.rs.golden | 39 +++ .../src/apis/media.rs.golden | 8 +- .../src/models/mod.rs.golden | 2 + ...multipart_multipart_request_body.rs.golden | 13 + .../src/runtime/mod.rs.golden | 2 + .../src/runtime/upload_file.rs.golden | 39 +++ .../src/apis/multipart.rs.golden | 22 +- .../src/models/mod.rs.golden | 4 + ...nal_parts_multipart_request_body.rs.golden | 16 ++ ...xt_fields_multipart_request_body.rs.golden | 12 + .../src/runtime/mod.rs.golden | 2 + .../src/runtime/upload_file.rs.golden | 39 +++ .../src/apis/transfer.rs.golden | 12 +- .../src/models/mod.rs.golden | 2 + ...ded_asset_multipart_request_body.rs.golden | 15 ++ .../src/runtime/mod.rs.golden | 2 + .../src/runtime/upload_file.rs.golden | 39 +++ .../src/apis/multipart.rs.golden | 8 +- .../src/models/mod.rs.golden | 2 + ...ject_part_multipart_request_body.rs.golden | 13 + .../src/runtime/mod.rs.golden | 2 + .../src/runtime/upload_file.rs.golden | 39 +++ .../apis/TransferApi.ts.golden | 13 +- .../UploadAssetMultipartRequestBody.ts.golden | 14 ++ .../models/index.ts.golden | 1 + .../runtime/runtime.ts.golden | 33 +++ .../apis/MediaApi.ts.golden | 13 +- ...zedMultipartMultipartRequestBody.ts.golden | 12 + .../models/index.ts.golden | 1 + .../runtime/runtime.ts.golden | 33 +++ .../apis/MultipartApi.ts.golden | 17 +- ...ptionalPartsMultipartRequestBody.ts.golden | 16 ++ ...ndTextFieldsMultipartRequestBody.ts.golden | 12 + .../models/index.ts.golden | 2 + .../runtime/runtime.ts.golden | 33 +++ .../apis/TransferApi.ts.golden | 13 +- ...EncodedAssetMultipartRequestBody.ts.golden | 16 ++ .../models/index.ts.golden | 1 + .../runtime/runtime.ts.golden | 33 +++ .../apis/MultipartApi.ts.golden | 13 +- ...edObjectPartMultipartRequestBody.ts.golden | 13 + .../models/index.ts.golden | 1 + .../runtime/runtime.ts.golden | 33 +++ .../apis/TransferApi.ts.golden | 2 +- .../apis/MultipartApi.ts.golden | 13 +- ...edObjectPartMultipartRequestBody.ts.golden | 13 + .../models/index.ts.golden | 1 + .../runtime/runtime.ts.golden | 33 +++ tests/golden_tests_typescript_fetch.rs | 16 +- tests/multipart_runtime_smoke.rs | 103 +++++++- tests/sigil_api_scaffold.rs | 9 +- 265 files changed, 4370 insertions(+), 376 deletions(-) create mode 100644 src/generators/go/http/runtime/upload_file.go.txt create mode 100644 src/generators/java/okhttp/runtime/UploadFile.java.txt create mode 100644 src/generators/kotlin/okhttp/runtime/upload_file.kt.txt create mode 100644 src/generators/python/httpx/runtime/upload_file.py.txt create mode 100644 src/generators/python/requests/runtime/upload_file.py.txt create mode 100644 src/generators/request_inputs.rs create mode 100644 src/generators/rust/aioduct/runtime/upload_file.rs.txt create mode 100644 src/generators/rust/reqwest/runtime/upload_file.rs.txt create mode 100644 src/generators/rust/ureq/runtime/upload_file.rs.txt create mode 100644 tests/golden/go/go-http/binary-transfer-media-types/models/upload_asset_multipart_request_body.go.golden create mode 100644 tests/golden/go/go-http/binary-transfer-media-types/runtime/upload_file.go.golden create mode 100644 tests/golden/go/go-http/media-type-selection/models/send_parameterized_multipart_multipart_request_body.go.golden create mode 100644 tests/golden/go/go-http/media-type-selection/runtime/upload_file.go.golden create mode 100644 tests/golden/go/go-http/multipart-edge-cases/models/send_optional_parts_multipart_request_body.go.golden create mode 100644 tests/golden/go/go-http/multipart-edge-cases/models/send_text_fields_multipart_request_body.go.golden create mode 100644 tests/golden/go/go-http/multipart-edge-cases/runtime/upload_file.go.golden create mode 100644 tests/golden/go/go-http/multipart-explicit-encoding/models/upload_encoded_asset_multipart_request_body.go.golden create mode 100644 tests/golden/go/go-http/multipart-explicit-encoding/runtime/upload_file.go.golden create mode 100644 tests/golden/go/go-http/multipart-nested-object-parts/models/send_nested_object_part_multipart_request_body.go.golden create mode 100644 tests/golden/go/go-http/multipart-nested-object-parts/runtime/upload_file.go.golden create mode 100644 tests/golden/java/java-okhttp/binary-transfer-media-types/models/UploadAssetMultipartRequestBody.java.golden create mode 100644 tests/golden/java/java-okhttp/binary-transfer-media-types/runtime/UploadFile.java.golden create mode 100644 tests/golden/java/java-okhttp/media-type-selection/models/SendParameterizedMultipartMultipartRequestBody.java.golden create mode 100644 tests/golden/java/java-okhttp/media-type-selection/runtime/UploadFile.java.golden create mode 100644 tests/golden/java/java-okhttp/multipart-edge-cases/models/SendOptionalPartsMultipartRequestBody.java.golden create mode 100644 tests/golden/java/java-okhttp/multipart-edge-cases/models/SendTextFieldsMultipartRequestBody.java.golden create mode 100644 tests/golden/java/java-okhttp/multipart-edge-cases/runtime/UploadFile.java.golden create mode 100644 tests/golden/java/java-okhttp/multipart-explicit-encoding/models/UploadEncodedAssetMultipartRequestBody.java.golden create mode 100644 tests/golden/java/java-okhttp/multipart-explicit-encoding/runtime/UploadFile.java.golden create mode 100644 tests/golden/java/java-okhttp/multipart-nested-object-parts/models/SendNestedObjectPartMultipartRequestBody.java.golden create mode 100644 tests/golden/java/java-okhttp/multipart-nested-object-parts/runtime/UploadFile.java.golden create mode 100644 tests/golden/kotlin/kotlin-okhttp/binary-transfer-media-types/models/UploadAssetMultipartRequestBody.kt.golden create mode 100644 tests/golden/kotlin/kotlin-okhttp/binary-transfer-media-types/runtime/UploadFile.kt.golden create mode 100644 tests/golden/kotlin/kotlin-okhttp/media-type-selection/models/SendParameterizedMultipartMultipartRequestBody.kt.golden create mode 100644 tests/golden/kotlin/kotlin-okhttp/media-type-selection/runtime/UploadFile.kt.golden create mode 100644 tests/golden/kotlin/kotlin-okhttp/multipart-edge-cases/models/SendOptionalPartsMultipartRequestBody.kt.golden create mode 100644 tests/golden/kotlin/kotlin-okhttp/multipart-edge-cases/models/SendTextFieldsMultipartRequestBody.kt.golden create mode 100644 tests/golden/kotlin/kotlin-okhttp/multipart-edge-cases/runtime/UploadFile.kt.golden create mode 100644 tests/golden/kotlin/kotlin-okhttp/multipart-explicit-encoding/models/UploadEncodedAssetMultipartRequestBody.kt.golden create mode 100644 tests/golden/kotlin/kotlin-okhttp/multipart-explicit-encoding/runtime/UploadFile.kt.golden create mode 100644 tests/golden/kotlin/kotlin-okhttp/multipart-nested-object-parts/models/SendNestedObjectPartMultipartRequestBody.kt.golden create mode 100644 tests/golden/kotlin/kotlin-okhttp/multipart-nested-object-parts/runtime/UploadFile.kt.golden create mode 100644 tests/golden/python/python-httpx/binary-transfer-media-types/binary_transfer_media_types/models/upload_asset_multipart_request_body.py.golden create mode 100644 tests/golden/python/python-httpx/binary-transfer-media-types/binary_transfer_media_types/runtime/upload_file.py.golden create mode 100644 tests/golden/python/python-httpx/media-type-selection/media_type_selection/models/send_parameterized_multipart_multipart_request_body.py.golden create mode 100644 tests/golden/python/python-httpx/media-type-selection/media_type_selection/runtime/upload_file.py.golden create mode 100644 tests/golden/python/python-httpx/multipart-edge-cases/multipart_edge_cases/models/send_optional_parts_multipart_request_body.py.golden create mode 100644 tests/golden/python/python-httpx/multipart-edge-cases/multipart_edge_cases/models/send_text_fields_multipart_request_body.py.golden create mode 100644 tests/golden/python/python-httpx/multipart-edge-cases/multipart_edge_cases/runtime/upload_file.py.golden create mode 100644 tests/golden/python/python-httpx/multipart-explicit-encoding/multipart_explicit_encoding/models/upload_encoded_asset_multipart_request_body.py.golden create mode 100644 tests/golden/python/python-httpx/multipart-explicit-encoding/multipart_explicit_encoding/runtime/upload_file.py.golden create mode 100644 tests/golden/python/python-httpx/multipart-nested-object-parts/multipart_nested_object_parts/models/send_nested_object_part_multipart_request_body.py.golden create mode 100644 tests/golden/python/python-httpx/multipart-nested-object-parts/multipart_nested_object_parts/runtime/upload_file.py.golden create mode 100644 tests/golden/python/python-requests/binary-transfer-media-types/binary_transfer_media_types/models/upload_asset_multipart_request_body.py.golden create mode 100644 tests/golden/python/python-requests/binary-transfer-media-types/binary_transfer_media_types/runtime/upload_file.py.golden create mode 100644 tests/golden/python/python-requests/media-type-selection/media_type_selection/models/send_parameterized_multipart_multipart_request_body.py.golden create mode 100644 tests/golden/python/python-requests/media-type-selection/media_type_selection/runtime/upload_file.py.golden create mode 100644 tests/golden/python/python-requests/multipart-edge-cases/multipart_edge_cases/models/send_optional_parts_multipart_request_body.py.golden create mode 100644 tests/golden/python/python-requests/multipart-edge-cases/multipart_edge_cases/models/send_text_fields_multipart_request_body.py.golden create mode 100644 tests/golden/python/python-requests/multipart-edge-cases/multipart_edge_cases/runtime/upload_file.py.golden create mode 100644 tests/golden/python/python-requests/multipart-explicit-encoding/multipart_explicit_encoding/models/upload_encoded_asset_multipart_request_body.py.golden create mode 100644 tests/golden/python/python-requests/multipart-explicit-encoding/multipart_explicit_encoding/runtime/upload_file.py.golden create mode 100644 tests/golden/python/python-requests/multipart-nested-object-parts/multipart_nested_object_parts/models/send_nested_object_part_multipart_request_body.py.golden create mode 100644 tests/golden/python/python-requests/multipart-nested-object-parts/multipart_nested_object_parts/runtime/upload_file.py.golden create mode 100644 tests/golden/rust/rust-aioduct/binary-transfer-media-types/src/models/upload_asset_multipart_request_body.rs.golden create mode 100644 tests/golden/rust/rust-aioduct/binary-transfer-media-types/src/runtime/upload_file.rs.golden create mode 100644 tests/golden/rust/rust-aioduct/media-type-selection/src/models/send_parameterized_multipart_multipart_request_body.rs.golden create mode 100644 tests/golden/rust/rust-aioduct/media-type-selection/src/runtime/upload_file.rs.golden create mode 100644 tests/golden/rust/rust-aioduct/multipart-edge-cases/src/models/send_optional_parts_multipart_request_body.rs.golden create mode 100644 tests/golden/rust/rust-aioduct/multipart-edge-cases/src/models/send_text_fields_multipart_request_body.rs.golden create mode 100644 tests/golden/rust/rust-aioduct/multipart-edge-cases/src/runtime/upload_file.rs.golden create mode 100644 tests/golden/rust/rust-aioduct/multipart-explicit-encoding/src/models/upload_encoded_asset_multipart_request_body.rs.golden create mode 100644 tests/golden/rust/rust-aioduct/multipart-explicit-encoding/src/runtime/upload_file.rs.golden create mode 100644 tests/golden/rust/rust-aioduct/multipart-nested-object-parts/src/models/send_nested_object_part_multipart_request_body.rs.golden create mode 100644 tests/golden/rust/rust-aioduct/multipart-nested-object-parts/src/runtime/upload_file.rs.golden create mode 100644 tests/golden/rust/rust-reqwest/binary-transfer-media-types/src/models/upload_asset_multipart_request_body.rs.golden create mode 100644 tests/golden/rust/rust-reqwest/binary-transfer-media-types/src/runtime/upload_file.rs.golden create mode 100644 tests/golden/rust/rust-reqwest/media-type-selection/src/models/send_parameterized_multipart_multipart_request_body.rs.golden create mode 100644 tests/golden/rust/rust-reqwest/media-type-selection/src/runtime/upload_file.rs.golden create mode 100644 tests/golden/rust/rust-reqwest/multipart-edge-cases/src/models/send_optional_parts_multipart_request_body.rs.golden create mode 100644 tests/golden/rust/rust-reqwest/multipart-edge-cases/src/models/send_text_fields_multipart_request_body.rs.golden create mode 100644 tests/golden/rust/rust-reqwest/multipart-edge-cases/src/runtime/upload_file.rs.golden create mode 100644 tests/golden/rust/rust-reqwest/multipart-explicit-encoding/src/models/upload_encoded_asset_multipart_request_body.rs.golden create mode 100644 tests/golden/rust/rust-reqwest/multipart-explicit-encoding/src/runtime/upload_file.rs.golden create mode 100644 tests/golden/rust/rust-reqwest/multipart-nested-object-parts/src/models/send_nested_object_part_multipart_request_body.rs.golden create mode 100644 tests/golden/rust/rust-reqwest/multipart-nested-object-parts/src/runtime/upload_file.rs.golden create mode 100644 tests/golden/rust/rust-ureq/binary-transfer-media-types/src/models/upload_asset_multipart_request_body.rs.golden create mode 100644 tests/golden/rust/rust-ureq/binary-transfer-media-types/src/runtime/upload_file.rs.golden create mode 100644 tests/golden/rust/rust-ureq/media-type-selection/src/models/send_parameterized_multipart_multipart_request_body.rs.golden create mode 100644 tests/golden/rust/rust-ureq/media-type-selection/src/runtime/upload_file.rs.golden create mode 100644 tests/golden/rust/rust-ureq/multipart-edge-cases/src/models/send_optional_parts_multipart_request_body.rs.golden create mode 100644 tests/golden/rust/rust-ureq/multipart-edge-cases/src/models/send_text_fields_multipart_request_body.rs.golden create mode 100644 tests/golden/rust/rust-ureq/multipart-edge-cases/src/runtime/upload_file.rs.golden create mode 100644 tests/golden/rust/rust-ureq/multipart-explicit-encoding/src/models/upload_encoded_asset_multipart_request_body.rs.golden create mode 100644 tests/golden/rust/rust-ureq/multipart-explicit-encoding/src/runtime/upload_file.rs.golden create mode 100644 tests/golden/rust/rust-ureq/multipart-nested-object-parts/src/models/send_nested_object_part_multipart_request_body.rs.golden create mode 100644 tests/golden/rust/rust-ureq/multipart-nested-object-parts/src/runtime/upload_file.rs.golden create mode 100644 tests/golden/typescript/typescript-fetch/binary-transfer-media-types/models/UploadAssetMultipartRequestBody.ts.golden create mode 100644 tests/golden/typescript/typescript-fetch/media-type-selection/models/SendParameterizedMultipartMultipartRequestBody.ts.golden create mode 100644 tests/golden/typescript/typescript-fetch/multipart-edge-cases/models/SendOptionalPartsMultipartRequestBody.ts.golden create mode 100644 tests/golden/typescript/typescript-fetch/multipart-edge-cases/models/SendTextFieldsMultipartRequestBody.ts.golden create mode 100644 tests/golden/typescript/typescript-fetch/multipart-explicit-encoding/models/UploadEncodedAssetMultipartRequestBody.ts.golden create mode 100644 tests/golden/typescript/typescript-fetch/multipart-nested-object-parts/models/SendNestedObjectPartMultipartRequestBody.ts.golden create mode 100644 tests/golden/typescript/typescript-fetch/ts-property-naming-camel-case-multipart-parts/models/SendNestedObjectPartMultipartRequestBody.ts.golden diff --git a/src/generators/go/http/codegen.rs b/src/generators/go/http/codegen.rs index 6f5a6f616..4b1c5b810 100644 --- a/src/generators/go/http/codegen.rs +++ b/src/generators/go/http/codegen.rs @@ -16,6 +16,7 @@ use super::{sigil_emit, sigil_emit_api}; use crate::codegen::traits::code_generator::CodeGenerator; use crate::codegen::traits::file_writer::{FileInfo, FileWriter}; use crate::codegen::{GeneratorType, Language}; +use crate::generators::request_inputs::plan_multipart_request_inputs; use crate::ir::types::{IrInfo, IrSpec}; const DEFAULT_MODULE_PATH: &str = "example.com/sdk"; @@ -45,25 +46,27 @@ impl GoHttpCodeGenerator { fn generate_ir(&self, ir: &IrSpec) -> Result, Box> { let module_path = self.module_path(); let header = render_file_header(&ir.info); + let request_inputs = plan_multipart_request_inputs(ir); let mut files = Vec::new(); // Models files.extend( - sigil_emit::generate_model_files(ir, &header).map_err(|msg| { - Box::::from(format!("sigil_emit models: {msg}")) - })?, + sigil_emit::generate_model_files(ir, &module_path, &header, &request_inputs).map_err( + |msg| Box::::from(format!("sigil_emit models: {msg}")), + )?, ); // APIs files.extend( - sigil_emit_api::generate_api_files(ir, &module_path, &header).map_err(|msg| { - Box::::from(format!("sigil_emit_api: {msg}")) - })?, + sigil_emit_api::generate_api_files(ir, &module_path, &header, &request_inputs) + .map_err(|msg| { + Box::::from(format!("sigil_emit_api: {msg}")) + })?, ); // Runtime (hardcoded) - files.extend(runtime_files(&header)); + files.extend(runtime_files(&header, request_inputs.has_uploads())); // Project files: go.mod, README.md files.push(go_mod_file(&module_path)); diff --git a/src/generators/go/http/runtime.rs b/src/generators/go/http/runtime.rs index bd6cc5b8f..acc2b48b2 100644 --- a/src/generators/go/http/runtime.rs +++ b/src/generators/go/http/runtime.rs @@ -10,16 +10,24 @@ use crate::codegen::traits::file_writer::FileInfo; const CLIENT_GO: &str = include_str!("runtime/client.go.txt"); const AUTH_GO: &str = include_str!("runtime/auth.go.txt"); const ERRORS_GO: &str = include_str!("runtime/errors.go.txt"); +const UPLOAD_FILE_GO: &str = include_str!("runtime/upload_file.go.txt"); /// Returns client.go, auth.go, errors.go ready to write. /// /// The category routes these under `/runtime/` via `FileWriter`. -pub fn runtime_files(header: &str) -> Vec { - vec![ +pub fn runtime_files(header: &str, include_upload_file: bool) -> Vec { + let mut files = vec![ FileInfo::runtime("client.go".to_string(), with_header(header, CLIENT_GO)), FileInfo::runtime("auth.go".to_string(), with_header(header, AUTH_GO)), FileInfo::runtime("errors.go".to_string(), with_header(header, ERRORS_GO)), - ] + ]; + if include_upload_file { + files.push(FileInfo::runtime( + "upload_file.go".to_string(), + with_header(header, UPLOAD_FILE_GO), + )); + } + files } fn with_header(header: &str, body: &str) -> String { diff --git a/src/generators/go/http/runtime/upload_file.go.txt b/src/generators/go/http/runtime/upload_file.go.txt new file mode 100644 index 000000000..634e6dce1 --- /dev/null +++ b/src/generators/go/http/runtime/upload_file.go.txt @@ -0,0 +1,21 @@ +package runtime + +type UploadFile struct { + Data []byte + Filename string +} + +func NewUploadFile(data []byte, filename string) UploadFile { + return UploadFile{Data: data, Filename: filename} +} + +func NewUploadFileBytes(data []byte) UploadFile { + return UploadFile{Data: data} +} + +func (f UploadFile) FilenameOrDefault(defaultName string) string { + if f.Filename != "" { + return f.Filename + } + return defaultName +} diff --git a/src/generators/go/http/sigil_emit.rs b/src/generators/go/http/sigil_emit.rs index cabfaae0c..26abc9989 100644 --- a/src/generators/go/http/sigil_emit.rs +++ b/src/generators/go/http/sigil_emit.rs @@ -17,6 +17,9 @@ //! name directly. use crate::codegen::traits::file_writer::FileInfo; +use crate::generators::request_inputs::{ + RequestInputField, RequestInputFieldKind, RequestInputModel, RequestInputPlan, +}; use crate::ir::types::{ IrEnum, IrEnumValueType, IrIntersection, IrObject, IrPrimitive, IrProperty, IrSchema, IrSchemaKind, IrSpec, IrTaggedUnion, IrTypeExpr, IrUnion, TaggingStyle, @@ -36,7 +39,12 @@ const RENDER_WIDTH: usize = 100; /// Generate every model file from the IR. Each emitted file carries the /// passed-in header (e.g., the `// Code generated` banner). -pub fn generate_model_files(ir: &IrSpec, header: &str) -> Result, String> { +pub fn generate_model_files( + ir: &IrSpec, + module_path: &str, + header: &str, + request_inputs: &RequestInputPlan, +) -> Result, String> { let mut files = Vec::new(); for (name, schema) in &ir.schemas { let Some(body) = emit_model_body(schema) else { @@ -47,6 +55,9 @@ pub fn generate_model_files(ir: &IrSpec, header: &str) -> Result, }; files.push(model_file(&schema.name, header, &body)); } + for model in request_inputs.models() { + files.push(request_input_model_file(model, module_path, header)); + } Ok(files) } @@ -66,6 +77,48 @@ fn model_file(name: &str, header: &str, body: &str) -> FileInfo { FileInfo::model(filename, content) } +fn request_input_model_file( + model: &RequestInputModel, + module_path: &str, + header: &str, +) -> FileInfo { + let name = model.name.to_pascal_case(); + let stem = model.name.to_snake_case(); + let filename = if stem.ends_with("_test") { + format!("{stem}_model.go") + } else { + format!("{stem}.go") + }; + let needs_runtime = model.fields.iter().any(RequestInputField::is_upload); + let mut body = String::new(); + body.push_str(&format!("package {MODELS_PACKAGE}\n\n")); + if needs_runtime { + body.push_str(&format!("import \"{module_path}/runtime\"\n\n")); + } + body.push_str(&format!("type {name} struct {{\n")); + for field in &model.fields { + let field_name = go_field_name(&field.wire_name); + let mut field_type = request_input_go_type(field); + if !field.required && !field_type.starts_with('*') { + field_type = format!("*{field_type}"); + } + body.push_str(&format!("\t{field_name} {field_type}\n")); + } + body.push_str("}\n"); + + let mut content = String::with_capacity(header.len() + body.len()); + content.push_str(header); + content.push_str(&body); + FileInfo::model(filename, content) +} + +fn request_input_go_type(field: &RequestInputField) -> String { + match field.kind { + RequestInputFieldKind::UploadFile { .. } => "runtime.UploadFile".to_string(), + RequestInputFieldKind::SchemaValue => go_type_str(&field.type_expr), + } +} + /// Dispatch on schema kind. Returns the rendered file body (package + imports /// + declarations) but not the pre-package header comment. fn emit_model_body(schema: &IrSchema) -> Option { diff --git a/src/generators/go/http/sigil_emit_api.rs b/src/generators/go/http/sigil_emit_api.rs index d46c9667c..3f4414ffb 100644 --- a/src/generators/go/http/sigil_emit_api.rs +++ b/src/generators/go/http/sigil_emit_api.rs @@ -20,6 +20,7 @@ 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::generators::request_inputs::{RequestInputPlan, request_input_for_operation}; use crate::ir::types::{ IrOperation, IrParameter, IrPrimitive, IrRequestBody, IrResponse, IrSpec, IrTypeExpr, ParameterLocation, @@ -44,6 +45,7 @@ pub fn generate_api_files( ir: &IrSpec, module_path: &str, header: &str, + request_inputs: &RequestInputPlan, ) -> Result, String> { let by_tag = group_by_tag(&ir.operations); let mut files = Vec::with_capacity(by_tag.len()); @@ -55,7 +57,7 @@ pub fn generate_api_files( } else { format!("{stem}.go") }; - let body = emit_api_file(tag, ops, ir, module_path); + let body = emit_api_file(tag, ops, ir, module_path, request_inputs); let content = format!("{header}{body}"); files.push(FileInfo::api(filename, content)); } @@ -81,11 +83,20 @@ fn group_by_tag(operations: &[IrOperation]) -> BTreeMap String { +fn emit_api_file( + tag: &str, + ops: &[&IrOperation], + ir: &IrSpec, + module_path: &str, + request_inputs: &RequestInputPlan, +) -> String { let struct_name = format!("{}API", tag.to_pascal_case()); // Pre-plan each operation so we can build specs from the plans. - let plans: Vec = ops.iter().map(|op| plan_operation(op, ir)).collect(); + let plans: Vec = ops + .iter() + .map(|op| plan_operation(op, ir, request_inputs)) + .collect(); let filename = format!("{}.go", tag.to_snake_case()); let mut fb = FileSpec::builder(&filename) @@ -141,6 +152,9 @@ fn collect_body_imports(plans: &[OpPlan<'_>], module_path: &str) -> Vec(op: &'a IrOperation, ir: &IrSpec) -> OpPlan<'a> { +fn plan_operation<'a>( + op: &'a IrOperation, + ir: &IrSpec, + request_inputs: &RequestInputPlan, +) -> OpPlan<'a> { let op_id = sanitize_operation_id(&op.operation_id, &op.method, &op.path); let method_name = op_id.to_pascal_case(); let response_type = format!("{method_name}Response"); @@ -517,7 +535,7 @@ fn plan_operation<'a>(op: &'a IrOperation, ir: &IrSpec) -> OpPlan<'a> { let body = op .request_body .as_ref() - .and_then(|b| plan_body(b, ir, &mut used_names)); + .and_then(|b| plan_body(op, b, ir, request_inputs, &mut used_names)); let typed_responses = op.responses.iter().filter_map(plan_response).collect(); @@ -534,8 +552,10 @@ fn plan_operation<'a>(op: &'a IrOperation, ir: &IrSpec) -> OpPlan<'a> { } fn plan_body( + op: &IrOperation, b: &IrRequestBody, ir: &IrSpec, + request_inputs: &RequestInputPlan, used_names: &mut HashSet, ) -> Option { let (media_type, t) = pick_body_content(b)?; @@ -543,6 +563,9 @@ fn plan_body( let base_ty = match encoding { BodyEncoding::TextPlain => "string".to_string(), BodyEncoding::OctetStream => "[]byte".to_string(), + BodyEncoding::Multipart => request_input_for_operation(request_inputs, op, &media_type) + .map(|input| format!("models.{}", input.name.to_pascal_case())) + .unwrap_or_else(|| go_type_str(&t)), _ => go_type_str(&t), }; let go_type = @@ -876,7 +899,9 @@ fn emit_multipart_body( emit_required_multipart_part(cb, part, &value_expr); } else { cb.begin_control_flow(&format!("if {value_expr} != nil"), ()); - emit_required_multipart_part(cb, part, &format!("*{value_expr}")); + cb.add(&format!("value := *{value_expr}"), ()); + cb.add_line(); + emit_required_multipart_part(cb, part, "value"); cb.end_control_flow(); } } @@ -903,23 +928,29 @@ fn emit_required_multipart_part( cb.add_line(); cb.add("partHeader := textproto.MIMEHeader{}", ()); cb.add_line(); - let disposition = if part.is_binary { - format!( - "form-data; name={}; filename={}", - go_string_literal(&part.wire_name), - go_string_literal(&part.wire_name) - ) + if part.is_binary { + cb.add( + &format!( + "disposition := mime.FormatMediaType(\"form-data\", map[string]string{{\"name\": {}, \"filename\": {value_expr}.FilenameOrDefault({})}})", + go_string_literal(&part.wire_name), + go_string_literal(&part.wire_name) + ), + (), + ); + cb.add_line(); + cb.add("partHeader.Set(\"Content-Disposition\", disposition)", ()); + cb.add_line(); } else { - format!("form-data; name={}", go_string_literal(&part.wire_name)) - }; - cb.add( - &format!( - "partHeader.Set(\"Content-Disposition\", {})", - go_string_literal(&disposition) - ), - (), - ); - cb.add_line(); + let disposition = format!("form-data; name={}", go_string_literal(&part.wire_name)); + cb.add( + &format!( + "partHeader.Set(\"Content-Disposition\", {})", + go_string_literal(&disposition) + ), + (), + ); + cb.add_line(); + } cb.add( &format!( "partHeader.Set(\"Content-Type\", {})", @@ -939,7 +970,7 @@ fn emit_required_multipart_part( cb.end_control_flow(); if part.is_binary { cb.begin_control_flow( - &format!("if _, err := partWriter.Write({value_expr}); err != nil"), + &format!("if _, err := partWriter.Write({value_expr}.Data); err != nil"), (), ); cb.add( diff --git a/src/generators/java/okhttp/codegen.rs b/src/generators/java/okhttp/codegen.rs index 9fa77347f..629b1f819 100644 --- a/src/generators/java/okhttp/codegen.rs +++ b/src/generators/java/okhttp/codegen.rs @@ -8,6 +8,7 @@ use super::{sigil_emit, sigil_emit_api}; use crate::codegen::traits::code_generator::CodeGenerator; use crate::codegen::traits::file_writer::{FileInfo, FileWriter}; use crate::codegen::{GeneratorType, Language}; +use crate::generators::request_inputs::plan_multipart_request_inputs; use crate::ir::types::{IrInfo, IrSpec}; const DEFAULT_PACKAGE: &str = "com.example.sdk"; @@ -34,22 +35,28 @@ impl JavaOkhttpCodeGenerator { fn generate_ir(&self, ir: &IrSpec) -> Result, Box> { let package_name = self.package_name(); let header = render_file_header(&ir.info); + let request_inputs = plan_multipart_request_inputs(ir); let mut files = Vec::new(); files.extend( - sigil_emit::generate_model_files(ir, &package_name, &header).map_err(|msg| { - Box::::from(format!("sigil_emit models: {msg}")) - })?, + sigil_emit::generate_model_files(ir, &package_name, &header, &request_inputs).map_err( + |msg| Box::::from(format!("sigil_emit models: {msg}")), + )?, ); files.extend( - sigil_emit_api::generate_api_files(ir, &package_name, &header).map_err(|msg| { - Box::::from(format!("sigil_emit_api: {msg}")) - })?, + sigil_emit_api::generate_api_files(ir, &package_name, &header, &request_inputs) + .map_err(|msg| { + Box::::from(format!("sigil_emit_api: {msg}")) + })?, ); - files.extend(runtime_files(&header, &package_name)); + files.extend(runtime_files( + &header, + &package_name, + request_inputs.has_uploads(), + )); files.push(build_gradle_file(&package_name, &ir.info)); files.push(readme_file(&ir.info)); diff --git a/src/generators/java/okhttp/runtime.rs b/src/generators/java/okhttp/runtime.rs index c8c40bfe7..9f91c0fde 100644 --- a/src/generators/java/okhttp/runtime.rs +++ b/src/generators/java/okhttp/runtime.rs @@ -6,10 +6,11 @@ const AUTHENTICATOR_JAVA: &str = include_str!("runtime/Auth.java.txt"); const BEARER_AUTH_JAVA: &str = include_str!("runtime/BearerAuth.java.txt"); const API_KEY_AUTH_JAVA: &str = include_str!("runtime/ApiKeyAuth.java.txt"); const API_KEY_LOCATION_JAVA: &str = include_str!("runtime/ApiKeyLocation.java.txt"); +const UPLOAD_FILE_JAVA: &str = include_str!("runtime/UploadFile.java.txt"); -pub fn runtime_files(header: &str, package_name: &str) -> Vec { +pub fn runtime_files(header: &str, package_name: &str, include_upload_file: bool) -> Vec { let runtime_package = format!("{package_name}.runtime"); - vec![ + let mut files = vec![ FileInfo::runtime( "ApiClient.java".to_string(), repackage(header, API_CLIENT_JAVA, &runtime_package), @@ -34,7 +35,14 @@ pub fn runtime_files(header: &str, package_name: &str) -> Vec { "ApiKeyLocation.java".to_string(), repackage(header, API_KEY_LOCATION_JAVA, &runtime_package), ), - ] + ]; + if include_upload_file { + files.push(FileInfo::runtime( + "UploadFile.java".to_string(), + repackage(header, UPLOAD_FILE_JAVA, &runtime_package), + )); + } + files } fn repackage(header: &str, body: &str, runtime_package: &str) -> String { diff --git a/src/generators/java/okhttp/runtime/UploadFile.java.txt b/src/generators/java/okhttp/runtime/UploadFile.java.txt new file mode 100644 index 000000000..e543e5621 --- /dev/null +++ b/src/generators/java/okhttp/runtime/UploadFile.java.txt @@ -0,0 +1,31 @@ +package runtime; + +public final class UploadFile { + private final byte[] data; + private final String filename; + + public UploadFile(byte[] data, String filename) { + this.data = data; + this.filename = filename; + } + + public static UploadFile of(byte[] data, String filename) { + return new UploadFile(data, filename); + } + + public static UploadFile ofBytes(byte[] data) { + return new UploadFile(data, null); + } + + public byte[] getData() { + return data; + } + + public String getFilename() { + return filename; + } + + public String filenameOrDefault(String defaultName) { + return filename != null && !filename.isEmpty() ? filename : defaultName; + } +} diff --git a/src/generators/java/okhttp/sigil_emit.rs b/src/generators/java/okhttp/sigil_emit.rs index eb3b578a7..843a6ef25 100644 --- a/src/generators/java/okhttp/sigil_emit.rs +++ b/src/generators/java/okhttp/sigil_emit.rs @@ -1,6 +1,9 @@ use std::collections::HashSet; use crate::codegen::traits::file_writer::FileInfo; +use crate::generators::request_inputs::{ + RequestInputField, RequestInputFieldKind, RequestInputModel, RequestInputPlan, +}; use crate::ir::types::{ IrEnum, IrEnumValueType, IrIntersection, IrObject, IrSchema, IrSchemaKind, IrSpec, IrTaggedUnion, IrTypeExpr, IrUnion, TaggingStyle, @@ -20,6 +23,7 @@ pub fn generate_model_files( ir: &IrSpec, package_name: &str, header: &str, + request_inputs: &RequestInputPlan, ) -> Result, String> { let mut files = Vec::new(); for (_name, schema) in &ir.schemas { @@ -36,9 +40,76 @@ pub fn generate_model_files( content.push_str(&body); files.push(FileInfo::model(filename, content)); } + for model in request_inputs.models() { + files.push(request_input_model_file(model, package_name, header)); + } Ok(files) } +fn request_input_model_file( + model: &RequestInputModel, + package_name: &str, + header: &str, +) -> FileInfo { + let class_name = model.name.to_pascal_case(); + let needs_upload = model.fields.iter().any(RequestInputField::is_upload); + let mut content = String::new(); + content.push_str(header); + content.push_str(&format!("package {package_name}.models;\n\n")); + if needs_upload { + content.push_str(&format!("import {package_name}.runtime.UploadFile;\n\n")); + } + content.push_str(&format!("public final class {class_name} {{\n")); + for field in &model.fields { + content.push_str(&format!( + " private final {} {};\n", + request_input_java_type(field), + java_field_name(&field.wire_name) + )); + } + content.push('\n'); + content.push_str(&format!(" public {class_name}(")); + let params = model + .fields + .iter() + .map(|field| { + format!( + "{} {}", + request_input_java_type(field), + java_field_name(&field.wire_name) + ) + }) + .collect::>() + .join(", "); + content.push_str(¶ms); + content.push_str(") {\n"); + for field in &model.fields { + let name = java_field_name(&field.wire_name); + content.push_str(&format!(" this.{name} = {name};\n")); + } + content.push_str(" }\n\n"); + for field in &model.fields { + let field_name = java_field_name(&field.wire_name); + let getter = format!("get{}", field.wire_name.to_pascal_case()); + content.push_str(&format!( + " public {} {}() {{\n return {};\n }}\n\n", + request_input_java_type(field), + getter, + field_name + )); + } + content.push_str("}\n"); + + FileInfo::model(format!("{class_name}.java"), content) +} + +fn request_input_java_type(field: &RequestInputField) -> String { + match field.kind { + RequestInputFieldKind::UploadFile { .. } => "UploadFile".to_string(), + RequestInputFieldKind::SchemaValue => java_boxed_type_str(&field.type_expr), + } +} + fn emit_model_body(schema: &IrSchema, package_name: &str) -> Option { let file_spec = match &schema.kind { IrSchemaKind::Object(obj) => emit_object(schema, obj, package_name), diff --git a/src/generators/java/okhttp/sigil_emit_api.rs b/src/generators/java/okhttp/sigil_emit_api.rs index a73d501b7..774712be7 100644 --- a/src/generators/java/okhttp/sigil_emit_api.rs +++ b/src/generators/java/okhttp/sigil_emit_api.rs @@ -2,6 +2,7 @@ use std::collections::{BTreeMap, HashSet}; use crate::codegen::traits::file_writer::FileInfo; use crate::generators::multipart::{MultipartValueEncoding, multipart_parts_for_request_body}; +use crate::generators::request_inputs::{RequestInputPlan, request_input_for_operation}; use crate::ir::types::{ IrOperation, IrParameter, IrRequestBody, IrResponse, IrSpec, IrTypeExpr, ParameterLocation, }; @@ -20,13 +21,14 @@ pub fn generate_api_files( ir: &IrSpec, package_name: &str, header: &str, + request_inputs: &RequestInputPlan, ) -> Result, String> { let by_tag = group_by_tag(&ir.operations); let mut files = Vec::with_capacity(by_tag.len()); for (tag, ops) in &by_tag { let class_name = format!("{}Api", tag.to_pascal_case()); let filename = format!("{class_name}.java"); - let body = emit_api_file(tag, ops, ir, package_name); + let body = emit_api_file(tag, ops, ir, package_name, request_inputs); let content = format!("{header}{body}"); files.push(FileInfo::api(filename, content)); } @@ -52,9 +54,18 @@ fn group_by_tag(operations: &[IrOperation]) -> BTreeMap String { +fn emit_api_file( + tag: &str, + ops: &[&IrOperation], + ir: &IrSpec, + package_name: &str, + request_inputs: &RequestInputPlan, +) -> String { let class_name = format!("{}Api", tag.to_pascal_case()); - let plans: Vec = ops.iter().map(|op| plan_operation(op, ir)).collect(); + let plans: Vec = ops + .iter() + .map(|op| plan_operation(op, ir, request_inputs)) + .collect(); let filename = format!("{class_name}.java"); let mut fb = FileSpec::builder_with(&filename, Java::new()) @@ -607,7 +618,7 @@ fn emit_required_multipart_part( if part.is_binary { cb.add_code( sigil_quote!(Java { - multipartBuilder.addFormDataPart($S(wire_name), $S(wire_name), RequestBody.create($L(access), MediaType.get($S(content_type)))); + multipartBuilder.addFormDataPart($S(wire_name), $L(access).filenameOrDefault($S(wire_name)), RequestBody.create($L(access).getData(), MediaType.get($S(content_type)))); }) .expect("binary multipart part block builds"), ); @@ -699,7 +710,11 @@ struct TypedResponse { decoding: ResponseDecoding, } -fn plan_operation<'a>(op: &'a IrOperation, ir: &IrSpec) -> OpPlan<'a> { +fn plan_operation<'a>( + op: &'a IrOperation, + ir: &IrSpec, + request_inputs: &RequestInputPlan, +) -> OpPlan<'a> { let op_id = sanitize_operation_id(&op.operation_id, &op.method, &op.path); let method_name = op_id.to_lower_camel_case(); let response_type = format!("{}Response", op_id.to_pascal_case()); @@ -732,7 +747,7 @@ fn plan_operation<'a>(op: &'a IrOperation, ir: &IrSpec) -> OpPlan<'a> { let body = op .request_body .as_ref() - .and_then(|b| plan_body(b, ir, &mut used_names)); + .and_then(|b| plan_body(op, b, ir, request_inputs, &mut used_names)); let typed_responses = op.responses.iter().filter_map(plan_response).collect(); @@ -749,8 +764,10 @@ fn plan_operation<'a>(op: &'a IrOperation, ir: &IrSpec) -> OpPlan<'a> { } fn plan_body( + op: &IrOperation, b: &IrRequestBody, ir: &IrSpec, + request_inputs: &RequestInputPlan, used_names: &mut HashSet, ) -> Option { let (media_type, t) = pick_body_content(b)?; @@ -758,6 +775,9 @@ fn plan_body( let java_type = match encoding { BodyEncoding::TextPlain => "String".to_string(), BodyEncoding::OctetStream => "byte[]".to_string(), + BodyEncoding::Multipart => request_input_for_operation(request_inputs, op, &media_type) + .map(|input| input.name.to_pascal_case()) + .unwrap_or_else(|| java_type_str(&t)), _ => java_type_str(&t), }; let var_name = unique_name("body", used_names); diff --git a/src/generators/kotlin/okhttp/codegen.rs b/src/generators/kotlin/okhttp/codegen.rs index a1e27ae4a..d82f24e21 100644 --- a/src/generators/kotlin/okhttp/codegen.rs +++ b/src/generators/kotlin/okhttp/codegen.rs @@ -8,6 +8,7 @@ use super::{sigil_emit, sigil_emit_api}; use crate::codegen::traits::code_generator::CodeGenerator; use crate::codegen::traits::file_writer::{FileInfo, FileWriter}; use crate::codegen::{GeneratorType, Language}; +use crate::generators::request_inputs::plan_multipart_request_inputs; use crate::ir::types::{IrInfo, IrSpec}; const DEFAULT_PACKAGE: &str = "com.example.sdk"; @@ -34,22 +35,28 @@ impl KotlinOkhttpCodeGenerator { fn generate_ir(&self, ir: &IrSpec) -> Result, Box> { let package_name = self.package_name(); let header = render_file_header(&ir.info); + let request_inputs = plan_multipart_request_inputs(ir); let mut files = Vec::new(); files.extend( - sigil_emit::generate_model_files(ir, &package_name, &header).map_err(|msg| { - Box::::from(format!("sigil_emit models: {msg}")) - })?, + sigil_emit::generate_model_files(ir, &package_name, &header, &request_inputs).map_err( + |msg| Box::::from(format!("sigil_emit models: {msg}")), + )?, ); files.extend( - sigil_emit_api::generate_api_files(ir, &package_name, &header).map_err(|msg| { - Box::::from(format!("sigil_emit_api: {msg}")) - })?, + sigil_emit_api::generate_api_files(ir, &package_name, &header, &request_inputs) + .map_err(|msg| { + Box::::from(format!("sigil_emit_api: {msg}")) + })?, ); - files.extend(runtime_files(&header, &package_name)); + files.extend(runtime_files( + &header, + &package_name, + request_inputs.has_uploads(), + )); files.push(build_gradle_file(&package_name, &ir.info)); files.push(readme_file(&ir.info)); diff --git a/src/generators/kotlin/okhttp/runtime.rs b/src/generators/kotlin/okhttp/runtime.rs index 8f1adfd34..76194c04e 100644 --- a/src/generators/kotlin/okhttp/runtime.rs +++ b/src/generators/kotlin/okhttp/runtime.rs @@ -3,10 +3,11 @@ use crate::codegen::traits::file_writer::FileInfo; const API_CLIENT_KT: &str = include_str!("runtime/api_client.kt.txt"); const API_EXCEPTION_KT: &str = include_str!("runtime/api_exception.kt.txt"); const AUTH_KT: &str = include_str!("runtime/auth.kt.txt"); +const UPLOAD_FILE_KT: &str = include_str!("runtime/upload_file.kt.txt"); -pub fn runtime_files(header: &str, package_name: &str) -> Vec { +pub fn runtime_files(header: &str, package_name: &str, include_upload_file: bool) -> Vec { let runtime_package = format!("{package_name}.runtime"); - vec![ + let mut files = vec![ FileInfo::runtime( "ApiClient.kt".to_string(), repackage(header, API_CLIENT_KT, &runtime_package), @@ -19,7 +20,14 @@ pub fn runtime_files(header: &str, package_name: &str) -> Vec { "Auth.kt".to_string(), repackage(header, AUTH_KT, &runtime_package), ), - ] + ]; + if include_upload_file { + files.push(FileInfo::runtime( + "UploadFile.kt".to_string(), + repackage(header, UPLOAD_FILE_KT, &runtime_package), + )); + } + files } fn repackage(header: &str, body: &str, runtime_package: &str) -> String { diff --git a/src/generators/kotlin/okhttp/runtime/upload_file.kt.txt b/src/generators/kotlin/okhttp/runtime/upload_file.kt.txt new file mode 100644 index 000000000..a30db9e8e --- /dev/null +++ b/src/generators/kotlin/okhttp/runtime/upload_file.kt.txt @@ -0,0 +1,8 @@ +package runtime + +data class UploadFile( + val data: ByteArray, + val filename: String? = null, +) { + fun filenameOrDefault(defaultName: String): String = filename?.takeIf { it.isNotEmpty() } ?: defaultName +} diff --git a/src/generators/kotlin/okhttp/sigil_emit.rs b/src/generators/kotlin/okhttp/sigil_emit.rs index 3f361bf8a..8aaffbb3e 100644 --- a/src/generators/kotlin/okhttp/sigil_emit.rs +++ b/src/generators/kotlin/okhttp/sigil_emit.rs @@ -1,6 +1,9 @@ use std::collections::HashSet; use crate::codegen::traits::file_writer::FileInfo; +use crate::generators::request_inputs::{ + RequestInputField, RequestInputFieldKind, RequestInputModel, RequestInputPlan, +}; use crate::ir::types::{ IrEnum, IrEnumValueType, IrIntersection, IrObject, IrSchema, IrSchemaKind, IrSpec, IrTaggedUnion, IrTypeExpr, IrUnion, TaggingStyle, @@ -17,6 +20,7 @@ pub fn generate_model_files( ir: &IrSpec, package_name: &str, header: &str, + request_inputs: &RequestInputPlan, ) -> Result, String> { let mut files = Vec::new(); for (_name, schema) in &ir.schemas { @@ -33,9 +37,51 @@ pub fn generate_model_files( content.push_str(&body); files.push(FileInfo::model(filename, content)); } + for model in request_inputs.models() { + files.push(request_input_model_file(model, package_name, header)); + } Ok(files) } +fn request_input_model_file( + model: &RequestInputModel, + package_name: &str, + header: &str, +) -> FileInfo { + let class_name = model.name.to_pascal_case(); + let needs_upload = model.fields.iter().any(RequestInputField::is_upload); + let mut content = String::new(); + content.push_str(header); + content.push_str(&format!("package {package_name}.models\n\n")); + if needs_upload { + content.push_str(&format!("import {package_name}.runtime.UploadFile\n\n")); + } + content.push_str(&format!("data class {class_name}(\n")); + for field in &model.fields { + let field_name = kt_field_name(&field.wire_name); + let mut field_type = request_input_kt_type(field); + let default = if field.required { + "" + } else { + if !field_type.ends_with('?') { + field_type.push('?'); + } + " = null" + }; + content.push_str(&format!(" val {field_name}: {field_type}{default},\n")); + } + content.push_str(")\n"); + + FileInfo::model(format!("{class_name}.kt"), content) +} + +fn request_input_kt_type(field: &RequestInputField) -> String { + match field.kind { + RequestInputFieldKind::UploadFile { .. } => "UploadFile".to_string(), + RequestInputFieldKind::SchemaValue => kt_type_str(&field.type_expr), + } +} + fn emit_model_body(schema: &IrSchema, package_name: &str) -> Option { let file_spec = match &schema.kind { IrSchemaKind::Object(obj) => emit_object(schema, obj, package_name), diff --git a/src/generators/kotlin/okhttp/sigil_emit_api.rs b/src/generators/kotlin/okhttp/sigil_emit_api.rs index 958c00d62..64ba7c476 100644 --- a/src/generators/kotlin/okhttp/sigil_emit_api.rs +++ b/src/generators/kotlin/okhttp/sigil_emit_api.rs @@ -2,6 +2,7 @@ use std::collections::{BTreeMap, HashSet}; use crate::codegen::traits::file_writer::FileInfo; use crate::generators::multipart::{MultipartValueEncoding, multipart_parts_for_request_body}; +use crate::generators::request_inputs::{RequestInputPlan, request_input_for_operation}; use crate::ir::types::{ IrOperation, IrParameter, IrRequestBody, IrResponse, IrSpec, IrTypeExpr, ParameterLocation, }; @@ -20,13 +21,14 @@ pub fn generate_api_files( ir: &IrSpec, package_name: &str, header: &str, + request_inputs: &RequestInputPlan, ) -> Result, String> { let by_tag = group_by_tag(&ir.operations); let mut files = Vec::with_capacity(by_tag.len()); for (tag, ops) in &by_tag { let class_name = format!("{}Api", tag.to_pascal_case()); let filename = format!("{class_name}.kt"); - let body = emit_api_file(tag, ops, ir, package_name); + let body = emit_api_file(tag, ops, ir, package_name, request_inputs); let content = format!("{header}{body}"); files.push(FileInfo::api(filename, content)); } @@ -52,9 +54,18 @@ fn group_by_tag(operations: &[IrOperation]) -> BTreeMap String { +fn emit_api_file( + tag: &str, + ops: &[&IrOperation], + ir: &IrSpec, + package_name: &str, + request_inputs: &RequestInputPlan, +) -> String { let class_name = format!("{}Api", tag.to_pascal_case()); - let plans: Vec = ops.iter().map(|op| plan_operation(op, ir)).collect(); + let plans: Vec = ops + .iter() + .map(|op| plan_operation(op, ir, request_inputs)) + .collect(); let filename = format!("{class_name}.kt"); let mut fb = FileSpec::builder_with(&filename, Kotlin::new()) @@ -542,7 +553,7 @@ fn emit_required_multipart_part( if part.is_binary { cb.add_code( sigil_quote!(Kotlin { - multipartBuilder.addFormDataPart($S(wire_name), $S(wire_name), $L(access).toRequestBody($S(content_type).toMediaType())) + multipartBuilder.addFormDataPart($S(wire_name), $L(access).filenameOrDefault($S(wire_name)), $L(access).data.toRequestBody($S(content_type).toMediaType())) }) .expect("binary multipart part block builds"), ); @@ -635,7 +646,11 @@ struct TypedResponse { decoding: ResponseDecoding, } -fn plan_operation<'a>(op: &'a IrOperation, ir: &IrSpec) -> OpPlan<'a> { +fn plan_operation<'a>( + op: &'a IrOperation, + ir: &IrSpec, + request_inputs: &RequestInputPlan, +) -> OpPlan<'a> { let op_id = sanitize_operation_id(&op.operation_id, &op.method, &op.path); let method_name = op_id.to_lower_camel_case(); let response_type = format!("{}Response", op_id.to_pascal_case()); @@ -668,7 +683,7 @@ fn plan_operation<'a>(op: &'a IrOperation, ir: &IrSpec) -> OpPlan<'a> { let body = op .request_body .as_ref() - .and_then(|b| plan_body(b, ir, &mut used_names)); + .and_then(|b| plan_body(op, b, ir, request_inputs, &mut used_names)); let typed_responses = op.responses.iter().filter_map(plan_response).collect(); @@ -685,8 +700,10 @@ fn plan_operation<'a>(op: &'a IrOperation, ir: &IrSpec) -> OpPlan<'a> { } fn plan_body( + op: &IrOperation, b: &IrRequestBody, ir: &IrSpec, + request_inputs: &RequestInputPlan, used_names: &mut HashSet, ) -> Option { let (media_type, t) = pick_body_content(b)?; @@ -694,6 +711,9 @@ fn plan_body( let mut kt_type = match encoding { BodyEncoding::TextPlain => "String".to_string(), BodyEncoding::OctetStream => "ByteArray".to_string(), + BodyEncoding::Multipart => request_input_for_operation(request_inputs, op, &media_type) + .map(|input| input.name.to_pascal_case()) + .unwrap_or_else(|| kt_type_str(&t)), _ => kt_type_str(&t), }; if !b.required { diff --git a/src/generators/mod.rs b/src/generators/mod.rs index e1c816061..63d02cbdf 100644 --- a/src/generators/mod.rs +++ b/src/generators/mod.rs @@ -4,6 +4,7 @@ pub mod kotlin; pub mod multipart; pub mod python; pub mod registry; +pub mod request_inputs; pub mod rust; pub mod typescript; diff --git a/src/generators/multipart.rs b/src/generators/multipart.rs index a2b26976f..1bf1428b2 100644 --- a/src/generators/multipart.rs +++ b/src/generators/multipart.rs @@ -5,6 +5,7 @@ use crate::ir::types::{IrObject, IrPrimitive, IrRequestBody, IrSchemaKind, IrSpe #[derive(Debug, Clone)] pub struct MultipartPart { pub wire_name: String, + pub default_filename: String, pub type_expr: IrTypeExpr, pub is_binary: bool, pub required: bool, @@ -75,6 +76,7 @@ fn multipart_part_from_property( MultipartPart { wire_name: wire_name.to_string(), + default_filename: wire_name.to_string(), type_expr: type_expr.clone(), is_binary, required, @@ -122,7 +124,7 @@ pub fn is_json_media_type(media_type: &str) -> bool { base == "application/json" || base.ends_with("+json") } -fn media_type_base(media_type: &str) -> String { +pub fn media_type_base(media_type: &str) -> String { media_type .split(';') .next() diff --git a/src/generators/python/httpx/codegen.rs b/src/generators/python/httpx/codegen.rs index 1fc491d5f..cec33b174 100644 --- a/src/generators/python/httpx/codegen.rs +++ b/src/generators/python/httpx/codegen.rs @@ -11,6 +11,7 @@ use super::{emit_api, emit_models, project_files}; use crate::codegen::traits::code_generator::CodeGenerator; use crate::codegen::traits::file_writer::{FileInfo, FileWriter}; use crate::codegen::{GeneratorType, Language}; +use crate::generators::request_inputs::plan_multipart_request_inputs; use crate::ir::types::{IrInfo, IrSpec}; /// Python httpx code generator (synchronous). @@ -48,21 +49,22 @@ impl PythonHttpxCodeGenerator { let package_name = self.package_name(&ir.info); let _ = self.resolved_package_name.set(package_name.clone()); let header = render_file_header(&ir.info); + let request_inputs = plan_multipart_request_inputs(ir); let mut files = Vec::new(); files.extend( - emit_models::generate_model_files(ir, &header).map_err(|msg| { + emit_models::generate_model_files(ir, &header, &request_inputs).map_err(|msg| { Box::::from(format!("emit_models: {msg}")) })?, ); files.extend( - emit_api::generate_api_files(ir, &header) + emit_api::generate_api_files(ir, &header, &request_inputs) .map_err(|msg| Box::::from(format!("emit_api: {msg}")))?, ); - files.extend(runtime_files(&header)); + files.extend(runtime_files(&header, request_inputs.has_uploads())); files.extend(project_files::generate_project_files( &ir.info, @@ -70,6 +72,8 @@ impl PythonHttpxCodeGenerator { &header, &ir.schemas, &ir.operations, + request_inputs.models(), + request_inputs.has_uploads(), )); Ok(files) diff --git a/src/generators/python/httpx/emit_api.rs b/src/generators/python/httpx/emit_api.rs index 8cf7af1fe..c191bf60f 100644 --- a/src/generators/python/httpx/emit_api.rs +++ b/src/generators/python/httpx/emit_api.rs @@ -8,6 +8,7 @@ use std::collections::{BTreeMap, HashSet}; use crate::codegen::traits::file_writer::FileInfo; use crate::generators::multipart::{MultipartValueEncoding, multipart_parts_for_request_body}; +use crate::generators::request_inputs::{RequestInputPlan, request_input_for_operation}; use crate::ir::types::{ IrOperation, IrParameter, IrPrimitive, IrRequestBody, IrResponse, IrSpec, IrTypeExpr, ParameterLocation, @@ -22,13 +23,17 @@ use super::emit_models::{ }; /// Generate every API file from the IR. -pub fn generate_api_files(ir: &IrSpec, header: &str) -> Result, String> { +pub fn generate_api_files( + ir: &IrSpec, + header: &str, + request_inputs: &RequestInputPlan, +) -> Result, String> { let by_tag = group_by_tag(&ir.operations); let mut files = Vec::with_capacity(by_tag.len()); for (tag, ops) in &by_tag { let stem = tag.to_snake_case(); let filename = format!("{stem}_api.py"); - let body = emit_api_file(tag, ops, ir, header); + let body = emit_api_file(tag, ops, ir, header, request_inputs); files.push(FileInfo::api(filename, body)); } Ok(files) @@ -49,9 +54,18 @@ fn group_by_tag(operations: &[IrOperation]) -> BTreeMap String { +fn emit_api_file( + tag: &str, + ops: &[&IrOperation], + ir: &IrSpec, + header: &str, + request_inputs: &RequestInputPlan, +) -> String { let class_name = format!("{}Api", tag.to_pascal_case()); - let plans: Vec = ops.iter().map(|op| plan_operation(op, ir)).collect(); + let plans: Vec = ops + .iter() + .map(|op| plan_operation(op, ir, request_inputs)) + .collect(); let client_type = TypeName::importable("..runtime.client", "Client"); let error_type = TypeName::importable("..runtime.errors", "ApiError"); @@ -380,8 +394,8 @@ fn emit_required_multipart_part( if part.is_binary { cb.add_statement( &format!( - "files[\"{}\"] = (\"{}\", {access}, \"{}\")", - part.wire_name, part.wire_name, part.content_type + "files[\"{}\"] = ({}.filename_or_default(\"{}\"), {}.data, \"{}\")", + part.wire_name, access, part.wire_name, access, part.content_type ), (), ); @@ -571,7 +585,11 @@ struct TypedResponse { decoding: ResponseDecoding, } -fn plan_operation<'a>(op: &'a IrOperation, ir: &IrSpec) -> OpPlan<'a> { +fn plan_operation<'a>( + op: &'a IrOperation, + ir: &IrSpec, + request_inputs: &RequestInputPlan, +) -> OpPlan<'a> { let op_id = sanitize_operation_id(&op.operation_id, &op.method, &op.path); let method_name = op_id.to_snake_case(); @@ -596,7 +614,7 @@ fn plan_operation<'a>(op: &'a IrOperation, ir: &IrSpec) -> OpPlan<'a> { let body = op .request_body .as_ref() - .and_then(|b| plan_body(b, ir, &mut used_names)); + .and_then(|b| plan_body(op, b, ir, request_inputs, &mut used_names)); let typed_responses = op.responses.iter().filter_map(plan_response).collect(); @@ -612,8 +630,10 @@ fn plan_operation<'a>(op: &'a IrOperation, ir: &IrSpec) -> OpPlan<'a> { } fn plan_body( + op: &IrOperation, b: &IrRequestBody, ir: &IrSpec, + request_inputs: &RequestInputPlan, used_names: &mut HashSet, ) -> Option { let (media_type, t) = pick_body_content(b)?; @@ -626,7 +646,13 @@ fn plan_body( }; Some(BodyBinding { var_name, - type_expr: t, + type_expr: if encoding == BodyEncoding::Multipart { + request_input_for_operation(request_inputs, op, &media_type) + .map(|input| IrTypeExpr::Named(input.name.clone())) + .unwrap_or(t) + } else { + t + }, required: b.required, media_type, encoding, diff --git a/src/generators/python/httpx/emit_models.rs b/src/generators/python/httpx/emit_models.rs index 914474124..323b1c38d 100644 --- a/src/generators/python/httpx/emit_models.rs +++ b/src/generators/python/httpx/emit_models.rs @@ -5,6 +5,9 @@ //! Each schema produces one `.py` file via `FileSpec`. use crate::codegen::traits::file_writer::FileInfo; +use crate::generators::request_inputs::{ + RequestInputField, RequestInputFieldKind, RequestInputModel, RequestInputPlan, +}; use crate::ir::types::{ IrEnum, IrEnumValueType, IrIntersection, IrObject, IrPrimitive, IrProperty, IrSchema, IrSchemaKind, IrSpec, IrTaggedUnion, IrTaggedVariant, IrTypeExpr, IrUnion, TaggingStyle, @@ -15,7 +18,11 @@ use sigil_stitch::lang::python::Python; use sigil_stitch::prelude::*; /// Generate every model file from the IR. -pub fn generate_model_files(ir: &IrSpec, header: &str) -> Result, String> { +pub fn generate_model_files( + ir: &IrSpec, + header: &str, + request_inputs: &RequestInputPlan, +) -> Result, String> { let mut files = Vec::new(); for (_name, schema) in &ir.schemas { let body = emit_model_body(schema, ir).ok_or_else(|| { @@ -31,9 +38,65 @@ pub fn generate_model_files(ir: &IrSpec, header: &str) -> Result, content.push_str(&body); files.push(FileInfo::model(filename, content)); } + for model in request_inputs.models() { + files.push(request_input_model_file(model, header)); + } Ok(files) } +fn request_input_model_file(model: &RequestInputModel, header: &str) -> FileInfo { + let class_name = model.name.to_pascal_case(); + let filename = format!("{}.py", model.name.to_snake_case()); + let mut imports = std::collections::BTreeSet::new(); + let mut needs_upload = false; + for field in &model.fields { + if field.is_upload() { + needs_upload = true; + } else { + collect_request_input_imports(&field.type_expr, &mut imports); + } + } + + let mut content = String::new(); + content.push_str(header); + content.push_str("from __future__ import annotations\n\n"); + content.push_str("from dataclasses import dataclass\n"); + if needs_upload { + content.push_str("from ..runtime import UploadFile\n"); + } + for import in &imports { + content.push_str(import); + content.push('\n'); + } + content.push('\n'); + content.push_str("@dataclass\n"); + content.push_str(&format!("class {class_name}:\n")); + if model.fields.is_empty() { + content.push_str(" pass\n"); + } else { + let required = model.fields.iter().filter(|field| field.required); + let optional = model.fields.iter().filter(|field| !field.required); + for field in required.chain(optional) { + let field_name = python_field_name(&field.wire_name); + let ty = request_input_python_type(field); + if field.required { + content.push_str(&format!(" {field_name}: {ty}\n")); + } else { + content.push_str(&format!(" {field_name}: {ty} | None = None\n")); + } + } + } + + FileInfo::model(filename, content) +} + +fn request_input_python_type(field: &RequestInputField) -> String { + match field.kind { + RequestInputFieldKind::UploadFile { .. } => "UploadFile".to_string(), + RequestInputFieldKind::SchemaValue => python_type_str(&field.type_expr), + } +} + fn emit_model_body(schema: &IrSchema, ir: &IrSpec) -> Option { let file_spec = match &schema.kind { IrSchemaKind::Object(obj) => emit_object(schema, obj, ir), @@ -820,6 +883,43 @@ fn python_primitive(p: &IrPrimitive) -> &'static str { } } +fn collect_request_input_imports( + expr: &IrTypeExpr, + imports: &mut std::collections::BTreeSet, +) { + match expr { + IrTypeExpr::Named(name) => { + let py_name = name.to_pascal_case(); + let module = name.to_snake_case(); + imports.insert(format!("from .{module} import {py_name}")); + } + IrTypeExpr::Primitive(IrPrimitive::Date | IrPrimitive::DateTime) => { + imports.insert("import datetime".to_string()); + } + IrTypeExpr::Primitive(IrPrimitive::Uuid) => { + imports.insert("import uuid".to_string()); + } + IrTypeExpr::StringLiteral(_) | IrTypeExpr::StringEnum(_) => { + imports.insert("from typing import Literal".to_string()); + } + IrTypeExpr::Union(members) => { + if members.is_empty() { + imports.insert("from typing import Any".to_string()); + } + for member in members { + collect_request_input_imports(member, imports); + } + } + IrTypeExpr::Any => { + imports.insert("from typing import Any".to_string()); + } + IrTypeExpr::Array(inner) | IrTypeExpr::Map(inner) | IrTypeExpr::Nullable(inner) => { + collect_request_input_imports(inner, imports); + } + _ => {} + } +} + fn needs_typing_literal(expr: &IrTypeExpr) -> bool { match expr { IrTypeExpr::StringLiteral(_) | IrTypeExpr::StringEnum(_) => true, diff --git a/src/generators/python/httpx/project_files.rs b/src/generators/python/httpx/project_files.rs index 17a8b32c9..50c4cd56f 100644 --- a/src/generators/python/httpx/project_files.rs +++ b/src/generators/python/httpx/project_files.rs @@ -1,6 +1,7 @@ //! Project-level files: pyproject.toml, README, __init__.py barrels, py.typed. use crate::codegen::traits::file_writer::FileInfo; +use crate::generators::request_inputs::RequestInputModel; use crate::ir::types::{IrInfo, IrOperation, IrSchema}; use heck::{ToPascalCase, ToSnakeCase}; use indexmap::IndexMap; @@ -12,13 +13,15 @@ pub fn generate_project_files( header: &str, schemas: &IndexMap, operations: &[IrOperation], + request_inputs: &[RequestInputModel], + include_upload_file: bool, ) -> Vec { let files = vec![ pyproject_toml(info, package_name), readme_file(info, package_name), py_typed(package_name), - top_level_init(package_name, header), - models_init(schemas, header), + top_level_init(package_name, header, include_upload_file), + models_init(schemas, request_inputs, header), apis_init(operations, header), ]; @@ -67,7 +70,7 @@ fn py_typed(package_name: &str) -> FileInfo { FileInfo::project(format!("{package_name}/py.typed"), String::new()) } -fn top_level_init(package_name: &str, header: &str) -> FileInfo { +fn top_level_init(package_name: &str, header: &str, include_upload_file: bool) -> FileInfo { let mut content = String::new(); content.push_str(header); content.push_str("from .runtime import ApiKeyAuth as ApiKeyAuth\n"); @@ -75,10 +78,17 @@ fn top_level_init(package_name: &str, header: &str) -> FileInfo { content.push_str("from .runtime import BearerAuth as BearerAuth\n"); content.push_str("from .runtime import Client as Client\n"); content.push_str("from .runtime import ApiError as ApiError\n"); + if include_upload_file { + content.push_str("from .runtime import UploadFile as UploadFile\n"); + } FileInfo::project(format!("{package_name}/__init__.py"), content) } -fn models_init(schemas: &IndexMap, header: &str) -> FileInfo { +fn models_init( + schemas: &IndexMap, + request_inputs: &[RequestInputModel], + header: &str, +) -> FileInfo { let mut content = String::new(); content.push_str(header); @@ -88,6 +98,11 @@ fn models_init(schemas: &IndexMap, header: &str) -> FileInfo { let module = schema.name.to_snake_case(); entries.push((module, py_name)); } + for model in request_inputs { + let py_name = model.name.to_pascal_case(); + let module = model.name.to_snake_case(); + entries.push((module, py_name)); + } entries.sort_by(|a, b| a.0.cmp(&b.0)); for (module, name) in &entries { diff --git a/src/generators/python/httpx/runtime.rs b/src/generators/python/httpx/runtime.rs index bd51e4669..316030cdb 100644 --- a/src/generators/python/httpx/runtime.rs +++ b/src/generators/python/httpx/runtime.rs @@ -5,15 +5,26 @@ use crate::codegen::traits::file_writer::FileInfo; const CLIENT_PY: &str = include_str!("runtime/client.py.txt"); const AUTH_PY: &str = include_str!("runtime/auth.py.txt"); const ERRORS_PY: &str = include_str!("runtime/errors.py.txt"); +const UPLOAD_FILE_PY: &str = include_str!("runtime/upload_file.py.txt"); /// Returns runtime files ready to write. -pub fn runtime_files(header: &str) -> Vec { - vec![ +pub fn runtime_files(header: &str, include_upload_file: bool) -> Vec { + let mut files = vec![ FileInfo::runtime("client.py".to_string(), with_header(header, CLIENT_PY)), FileInfo::runtime("auth.py".to_string(), with_header(header, AUTH_PY)), FileInfo::runtime("errors.py".to_string(), with_header(header, ERRORS_PY)), - FileInfo::runtime("__init__.py".to_string(), runtime_init(header)), - ] + FileInfo::runtime( + "__init__.py".to_string(), + runtime_init(header, include_upload_file), + ), + ]; + if include_upload_file { + files.push(FileInfo::runtime( + "upload_file.py".to_string(), + with_header(header, UPLOAD_FILE_PY), + )); + } + files } fn with_header(header: &str, body: &str) -> String { @@ -23,7 +34,7 @@ fn with_header(header: &str, body: &str) -> String { out } -fn runtime_init(header: &str) -> String { +fn runtime_init(header: &str, include_upload_file: bool) -> String { let mut out = String::new(); out.push_str(header); out.push_str("from .auth import ApiKeyAuth as ApiKeyAuth\n"); @@ -31,5 +42,8 @@ fn runtime_init(header: &str) -> String { out.push_str("from .auth import BearerAuth as BearerAuth\n"); out.push_str("from .client import Client as Client\n"); out.push_str("from .errors import ApiError as ApiError\n"); + if include_upload_file { + out.push_str("from .upload_file import UploadFile as UploadFile\n"); + } out } diff --git a/src/generators/python/httpx/runtime/upload_file.py.txt b/src/generators/python/httpx/runtime/upload_file.py.txt new file mode 100644 index 000000000..7872711ee --- /dev/null +++ b/src/generators/python/httpx/runtime/upload_file.py.txt @@ -0,0 +1,16 @@ +from __future__ import annotations + +from dataclasses import dataclass + + +@dataclass(frozen=True) +class UploadFile: + data: bytes + filename: str | None = None + + @classmethod + def from_bytes(cls, data: bytes, filename: str | None = None) -> UploadFile: + return cls(data=data, filename=filename) + + def filename_or_default(self, default_name: str) -> str: + return self.filename or default_name diff --git a/src/generators/python/requests/codegen.rs b/src/generators/python/requests/codegen.rs index 2e167ca6f..552de8655 100644 --- a/src/generators/python/requests/codegen.rs +++ b/src/generators/python/requests/codegen.rs @@ -11,6 +11,7 @@ use super::{emit_api, emit_models, project_files}; use crate::codegen::traits::code_generator::CodeGenerator; use crate::codegen::traits::file_writer::{FileInfo, FileWriter}; use crate::codegen::{GeneratorType, Language}; +use crate::generators::request_inputs::plan_multipart_request_inputs; use crate::ir::types::{IrInfo, IrSpec}; /// Python requests code generator (synchronous). @@ -48,21 +49,22 @@ impl PythonRequestsCodeGenerator { let package_name = self.package_name(&ir.info); let _ = self.resolved_package_name.set(package_name.clone()); let header = render_file_header(&ir.info); + let request_inputs = plan_multipart_request_inputs(ir); let mut files = Vec::new(); files.extend( - emit_models::generate_model_files(ir, &header).map_err(|msg| { + emit_models::generate_model_files(ir, &header, &request_inputs).map_err(|msg| { Box::::from(format!("emit_models: {msg}")) })?, ); files.extend( - emit_api::generate_api_files(ir, &header) + emit_api::generate_api_files(ir, &header, &request_inputs) .map_err(|msg| Box::::from(format!("emit_api: {msg}")))?, ); - files.extend(runtime_files(&header)); + files.extend(runtime_files(&header, request_inputs.has_uploads())); files.extend(project_files::generate_project_files( &ir.info, @@ -70,6 +72,8 @@ impl PythonRequestsCodeGenerator { &header, &ir.schemas, &ir.operations, + request_inputs.models(), + request_inputs.has_uploads(), )); Ok(files) diff --git a/src/generators/python/requests/emit_api.rs b/src/generators/python/requests/emit_api.rs index ad6203695..1a7956e79 100644 --- a/src/generators/python/requests/emit_api.rs +++ b/src/generators/python/requests/emit_api.rs @@ -8,6 +8,7 @@ use std::collections::{BTreeMap, HashSet}; use crate::codegen::traits::file_writer::FileInfo; use crate::generators::multipart::{MultipartValueEncoding, multipart_parts_for_request_body}; +use crate::generators::request_inputs::{RequestInputPlan, request_input_for_operation}; use crate::ir::types::{ IrOperation, IrParameter, IrPrimitive, IrRequestBody, IrResponse, IrSpec, IrTypeExpr, ParameterLocation, @@ -22,13 +23,17 @@ use super::emit_models::{ }; /// Generate every API file from the IR. -pub fn generate_api_files(ir: &IrSpec, header: &str) -> Result, String> { +pub fn generate_api_files( + ir: &IrSpec, + header: &str, + request_inputs: &RequestInputPlan, +) -> Result, String> { let by_tag = group_by_tag(&ir.operations); let mut files = Vec::with_capacity(by_tag.len()); for (tag, ops) in &by_tag { let stem = tag.to_snake_case(); let filename = format!("{stem}_api.py"); - let body = emit_api_file(tag, ops, ir, header); + let body = emit_api_file(tag, ops, ir, header, request_inputs); files.push(FileInfo::api(filename, body)); } Ok(files) @@ -49,9 +54,18 @@ fn group_by_tag(operations: &[IrOperation]) -> BTreeMap String { +fn emit_api_file( + tag: &str, + ops: &[&IrOperation], + ir: &IrSpec, + header: &str, + request_inputs: &RequestInputPlan, +) -> String { let class_name = format!("{}Api", tag.to_pascal_case()); - let plans: Vec = ops.iter().map(|op| plan_operation(op, ir)).collect(); + let plans: Vec = ops + .iter() + .map(|op| plan_operation(op, ir, request_inputs)) + .collect(); let client_type = TypeName::importable("..runtime.client", "Client"); let error_type = TypeName::importable("..runtime.errors", "ApiError"); @@ -379,8 +393,8 @@ fn emit_required_multipart_part( if part.is_binary { cb.add_statement( &format!( - "files[\"{}\"] = (\"{}\", {access}, \"{}\")", - part.wire_name, part.wire_name, part.content_type + "files[\"{}\"] = ({}.filename_or_default(\"{}\"), {}.data, \"{}\")", + part.wire_name, access, part.wire_name, access, part.content_type ), (), ); @@ -570,7 +584,11 @@ struct TypedResponse { decoding: ResponseDecoding, } -fn plan_operation<'a>(op: &'a IrOperation, ir: &IrSpec) -> OpPlan<'a> { +fn plan_operation<'a>( + op: &'a IrOperation, + ir: &IrSpec, + request_inputs: &RequestInputPlan, +) -> OpPlan<'a> { let op_id = sanitize_operation_id(&op.operation_id, &op.method, &op.path); let method_name = op_id.to_snake_case(); @@ -595,7 +613,7 @@ fn plan_operation<'a>(op: &'a IrOperation, ir: &IrSpec) -> OpPlan<'a> { let body = op .request_body .as_ref() - .and_then(|b| plan_body(b, ir, &mut used_names)); + .and_then(|b| plan_body(op, b, ir, request_inputs, &mut used_names)); let typed_responses = op.responses.iter().filter_map(plan_response).collect(); @@ -611,8 +629,10 @@ fn plan_operation<'a>(op: &'a IrOperation, ir: &IrSpec) -> OpPlan<'a> { } fn plan_body( + op: &IrOperation, b: &IrRequestBody, ir: &IrSpec, + request_inputs: &RequestInputPlan, used_names: &mut HashSet, ) -> Option { let (media_type, t) = pick_body_content(b)?; @@ -625,7 +645,13 @@ fn plan_body( }; Some(BodyBinding { var_name, - type_expr: t, + type_expr: if encoding == BodyEncoding::Multipart { + request_input_for_operation(request_inputs, op, &media_type) + .map(|input| IrTypeExpr::Named(input.name.clone())) + .unwrap_or(t) + } else { + t + }, required: b.required, media_type, encoding, diff --git a/src/generators/python/requests/emit_models.rs b/src/generators/python/requests/emit_models.rs index 914474124..323b1c38d 100644 --- a/src/generators/python/requests/emit_models.rs +++ b/src/generators/python/requests/emit_models.rs @@ -5,6 +5,9 @@ //! Each schema produces one `.py` file via `FileSpec`. use crate::codegen::traits::file_writer::FileInfo; +use crate::generators::request_inputs::{ + RequestInputField, RequestInputFieldKind, RequestInputModel, RequestInputPlan, +}; use crate::ir::types::{ IrEnum, IrEnumValueType, IrIntersection, IrObject, IrPrimitive, IrProperty, IrSchema, IrSchemaKind, IrSpec, IrTaggedUnion, IrTaggedVariant, IrTypeExpr, IrUnion, TaggingStyle, @@ -15,7 +18,11 @@ use sigil_stitch::lang::python::Python; use sigil_stitch::prelude::*; /// Generate every model file from the IR. -pub fn generate_model_files(ir: &IrSpec, header: &str) -> Result, String> { +pub fn generate_model_files( + ir: &IrSpec, + header: &str, + request_inputs: &RequestInputPlan, +) -> Result, String> { let mut files = Vec::new(); for (_name, schema) in &ir.schemas { let body = emit_model_body(schema, ir).ok_or_else(|| { @@ -31,9 +38,65 @@ pub fn generate_model_files(ir: &IrSpec, header: &str) -> Result, content.push_str(&body); files.push(FileInfo::model(filename, content)); } + for model in request_inputs.models() { + files.push(request_input_model_file(model, header)); + } Ok(files) } +fn request_input_model_file(model: &RequestInputModel, header: &str) -> FileInfo { + let class_name = model.name.to_pascal_case(); + let filename = format!("{}.py", model.name.to_snake_case()); + let mut imports = std::collections::BTreeSet::new(); + let mut needs_upload = false; + for field in &model.fields { + if field.is_upload() { + needs_upload = true; + } else { + collect_request_input_imports(&field.type_expr, &mut imports); + } + } + + let mut content = String::new(); + content.push_str(header); + content.push_str("from __future__ import annotations\n\n"); + content.push_str("from dataclasses import dataclass\n"); + if needs_upload { + content.push_str("from ..runtime import UploadFile\n"); + } + for import in &imports { + content.push_str(import); + content.push('\n'); + } + content.push('\n'); + content.push_str("@dataclass\n"); + content.push_str(&format!("class {class_name}:\n")); + if model.fields.is_empty() { + content.push_str(" pass\n"); + } else { + let required = model.fields.iter().filter(|field| field.required); + let optional = model.fields.iter().filter(|field| !field.required); + for field in required.chain(optional) { + let field_name = python_field_name(&field.wire_name); + let ty = request_input_python_type(field); + if field.required { + content.push_str(&format!(" {field_name}: {ty}\n")); + } else { + content.push_str(&format!(" {field_name}: {ty} | None = None\n")); + } + } + } + + FileInfo::model(filename, content) +} + +fn request_input_python_type(field: &RequestInputField) -> String { + match field.kind { + RequestInputFieldKind::UploadFile { .. } => "UploadFile".to_string(), + RequestInputFieldKind::SchemaValue => python_type_str(&field.type_expr), + } +} + fn emit_model_body(schema: &IrSchema, ir: &IrSpec) -> Option { let file_spec = match &schema.kind { IrSchemaKind::Object(obj) => emit_object(schema, obj, ir), @@ -820,6 +883,43 @@ fn python_primitive(p: &IrPrimitive) -> &'static str { } } +fn collect_request_input_imports( + expr: &IrTypeExpr, + imports: &mut std::collections::BTreeSet, +) { + match expr { + IrTypeExpr::Named(name) => { + let py_name = name.to_pascal_case(); + let module = name.to_snake_case(); + imports.insert(format!("from .{module} import {py_name}")); + } + IrTypeExpr::Primitive(IrPrimitive::Date | IrPrimitive::DateTime) => { + imports.insert("import datetime".to_string()); + } + IrTypeExpr::Primitive(IrPrimitive::Uuid) => { + imports.insert("import uuid".to_string()); + } + IrTypeExpr::StringLiteral(_) | IrTypeExpr::StringEnum(_) => { + imports.insert("from typing import Literal".to_string()); + } + IrTypeExpr::Union(members) => { + if members.is_empty() { + imports.insert("from typing import Any".to_string()); + } + for member in members { + collect_request_input_imports(member, imports); + } + } + IrTypeExpr::Any => { + imports.insert("from typing import Any".to_string()); + } + IrTypeExpr::Array(inner) | IrTypeExpr::Map(inner) | IrTypeExpr::Nullable(inner) => { + collect_request_input_imports(inner, imports); + } + _ => {} + } +} + fn needs_typing_literal(expr: &IrTypeExpr) -> bool { match expr { IrTypeExpr::StringLiteral(_) | IrTypeExpr::StringEnum(_) => true, diff --git a/src/generators/python/requests/project_files.rs b/src/generators/python/requests/project_files.rs index 18d06d416..5e48d795a 100644 --- a/src/generators/python/requests/project_files.rs +++ b/src/generators/python/requests/project_files.rs @@ -1,6 +1,7 @@ //! Project-level files: pyproject.toml, README, __init__.py barrels, py.typed. use crate::codegen::traits::file_writer::FileInfo; +use crate::generators::request_inputs::RequestInputModel; use crate::ir::types::{IrInfo, IrOperation, IrSchema}; use heck::{ToPascalCase, ToSnakeCase}; use indexmap::IndexMap; @@ -12,13 +13,15 @@ pub fn generate_project_files( header: &str, schemas: &IndexMap, operations: &[IrOperation], + request_inputs: &[RequestInputModel], + include_upload_file: bool, ) -> Vec { let files = vec![ pyproject_toml(info, package_name), readme_file(info, package_name), py_typed(package_name), - top_level_init(package_name, header), - models_init(schemas, header), + top_level_init(package_name, header, include_upload_file), + models_init(schemas, request_inputs, header), apis_init(operations, header), ]; @@ -67,7 +70,7 @@ fn py_typed(package_name: &str) -> FileInfo { FileInfo::project(format!("{package_name}/py.typed"), String::new()) } -fn top_level_init(package_name: &str, header: &str) -> FileInfo { +fn top_level_init(package_name: &str, header: &str, include_upload_file: bool) -> FileInfo { let mut content = String::new(); content.push_str(header); content.push_str("from .runtime import ApiKeyAuth as ApiKeyAuth\n"); @@ -75,10 +78,17 @@ fn top_level_init(package_name: &str, header: &str) -> FileInfo { content.push_str("from .runtime import BearerAuth as BearerAuth\n"); content.push_str("from .runtime import Client as Client\n"); content.push_str("from .runtime import ApiError as ApiError\n"); + if include_upload_file { + content.push_str("from .runtime import UploadFile as UploadFile\n"); + } FileInfo::project(format!("{package_name}/__init__.py"), content) } -fn models_init(schemas: &IndexMap, header: &str) -> FileInfo { +fn models_init( + schemas: &IndexMap, + request_inputs: &[RequestInputModel], + header: &str, +) -> FileInfo { let mut content = String::new(); content.push_str(header); @@ -88,6 +98,11 @@ fn models_init(schemas: &IndexMap, header: &str) -> FileInfo { let module = schema.name.to_snake_case(); entries.push((module, py_name)); } + for model in request_inputs { + let py_name = model.name.to_pascal_case(); + let module = model.name.to_snake_case(); + entries.push((module, py_name)); + } entries.sort_by(|a, b| a.0.cmp(&b.0)); for (module, name) in &entries { diff --git a/src/generators/python/requests/runtime.rs b/src/generators/python/requests/runtime.rs index ba8547b4f..703acd75b 100644 --- a/src/generators/python/requests/runtime.rs +++ b/src/generators/python/requests/runtime.rs @@ -5,15 +5,26 @@ use crate::codegen::traits::file_writer::FileInfo; const CLIENT_PY: &str = include_str!("runtime/client.py.txt"); const AUTH_PY: &str = include_str!("runtime/auth.py.txt"); const ERRORS_PY: &str = include_str!("runtime/errors.py.txt"); +const UPLOAD_FILE_PY: &str = include_str!("runtime/upload_file.py.txt"); /// Returns runtime files ready to write. -pub fn runtime_files(header: &str) -> Vec { - vec![ +pub fn runtime_files(header: &str, include_upload_file: bool) -> Vec { + let mut files = vec![ FileInfo::runtime("client.py".to_string(), with_header(header, CLIENT_PY)), FileInfo::runtime("auth.py".to_string(), with_header(header, AUTH_PY)), FileInfo::runtime("errors.py".to_string(), with_header(header, ERRORS_PY)), - FileInfo::runtime("__init__.py".to_string(), runtime_init(header)), - ] + FileInfo::runtime( + "__init__.py".to_string(), + runtime_init(header, include_upload_file), + ), + ]; + if include_upload_file { + files.push(FileInfo::runtime( + "upload_file.py".to_string(), + with_header(header, UPLOAD_FILE_PY), + )); + } + files } fn with_header(header: &str, body: &str) -> String { @@ -23,7 +34,7 @@ fn with_header(header: &str, body: &str) -> String { out } -fn runtime_init(header: &str) -> String { +fn runtime_init(header: &str, include_upload_file: bool) -> String { let mut out = String::new(); out.push_str(header); out.push_str("from .auth import ApiKeyAuth as ApiKeyAuth\n"); @@ -31,5 +42,8 @@ fn runtime_init(header: &str) -> String { out.push_str("from .auth import BearerAuth as BearerAuth\n"); out.push_str("from .client import Client as Client\n"); out.push_str("from .errors import ApiError as ApiError\n"); + if include_upload_file { + out.push_str("from .upload_file import UploadFile as UploadFile\n"); + } out } diff --git a/src/generators/python/requests/runtime/upload_file.py.txt b/src/generators/python/requests/runtime/upload_file.py.txt new file mode 100644 index 000000000..7872711ee --- /dev/null +++ b/src/generators/python/requests/runtime/upload_file.py.txt @@ -0,0 +1,16 @@ +from __future__ import annotations + +from dataclasses import dataclass + + +@dataclass(frozen=True) +class UploadFile: + data: bytes + filename: str | None = None + + @classmethod + def from_bytes(cls, data: bytes, filename: str | None = None) -> UploadFile: + return cls(data=data, filename=filename) + + def filename_or_default(self, default_name: str) -> str: + return self.filename or default_name diff --git a/src/generators/request_inputs.rs b/src/generators/request_inputs.rs new file mode 100644 index 000000000..3e10bcf00 --- /dev/null +++ b/src/generators/request_inputs.rs @@ -0,0 +1,228 @@ +//! Synthetic request-body input models for transport-specific request shapes. +//! +//! Canonical schema models stay schema-faithful. These models exist only for +//! operation request bodies whose selected media type needs a friendlier caller +//! shape than the schema itself, currently object-shaped multipart/form-data +//! with binary upload parts. + +use std::collections::{HashMap, HashSet}; + +use heck::ToPascalCase as _; + +use crate::generators::multipart::{ + MultipartValueEncoding, media_type_base, multipart_parts_for_request_body, +}; +use crate::ir::types::{IrOperation, IrRequestBody, IrSpec, IrTypeExpr}; + +#[derive(Debug, Clone)] +pub struct RequestInputModel { + pub name: String, + pub operation_id: String, + pub media_type: String, + pub body_required: bool, + pub fields: Vec, +} + +#[derive(Debug, Clone)] +pub struct RequestInputField { + pub wire_name: String, + pub type_expr: IrTypeExpr, + pub required: bool, + pub content_type: String, + pub value_encoding: MultipartValueEncoding, + pub kind: RequestInputFieldKind, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum RequestInputFieldKind { + SchemaValue, + UploadFile { default_filename: String }, +} + +impl RequestInputField { + pub fn is_upload(&self) -> bool { + matches!(self.kind, RequestInputFieldKind::UploadFile { .. }) + } + + pub fn default_filename(&self) -> &str { + match &self.kind { + RequestInputFieldKind::UploadFile { default_filename } => default_filename, + RequestInputFieldKind::SchemaValue => &self.wire_name, + } + } +} + +#[derive(Debug, Clone)] +pub struct RequestInputPlan { + models: Vec, + by_operation_media: HashMap<(String, String), usize>, +} + +impl RequestInputPlan { + pub fn empty() -> Self { + Self { + models: Vec::new(), + by_operation_media: HashMap::new(), + } + } + + pub fn models(&self) -> &[RequestInputModel] { + &self.models + } + + pub fn get(&self, operation_id: &str, media_type: &str) -> Option<&RequestInputModel> { + let key = (operation_id.to_string(), media_type.to_string()); + self.by_operation_media + .get(&key) + .and_then(|idx| self.models.get(*idx)) + } + + pub fn has_uploads(&self) -> bool { + self.models + .iter() + .any(|model| model.fields.iter().any(RequestInputField::is_upload)) + } +} + +pub fn plan_multipart_request_inputs(ir: &IrSpec) -> RequestInputPlan { + let mut plan = RequestInputPlan::empty(); + let mut used_names: HashSet = ir + .schemas + .keys() + .map(|name| name.to_pascal_case()) + .collect(); + + for op in &ir.operations { + let Some(body) = &op.request_body else { + continue; + }; + let Some(media_type) = preferred_request_media_type(body) else { + continue; + }; + if media_type_base(&media_type) != "multipart/form-data" { + continue; + } + let Some(parts) = multipart_parts_for_request_body(body, &media_type, ir) else { + continue; + }; + + let base_name = format!( + "{}MultipartRequestBody", + sanitize_operation_id(&op.operation_id, &op.method, &op.path).to_pascal_case() + ); + let name = unique_type_name(&base_name, &mut used_names); + let fields = parts + .into_iter() + .map(|part| { + let kind = if part.is_binary { + RequestInputFieldKind::UploadFile { + default_filename: part.default_filename.clone(), + } + } else { + RequestInputFieldKind::SchemaValue + }; + RequestInputField { + wire_name: part.wire_name, + type_expr: part.type_expr, + required: part.required, + content_type: part.content_type, + value_encoding: part.value_encoding, + kind, + } + }) + .collect(); + let index = plan.models.len(); + plan.by_operation_media + .insert((op.operation_id.clone(), media_type.clone()), index); + plan.models.push(RequestInputModel { + name, + operation_id: op.operation_id.clone(), + media_type, + body_required: body.required, + fields, + }); + } + + plan +} + +pub fn request_input_for_operation<'a>( + plan: &'a RequestInputPlan, + op: &IrOperation, + media_type: &str, +) -> Option<&'a RequestInputModel> { + plan.get(&op.operation_id, media_type) +} + +pub fn preferred_request_media_type(body: &IrRequestBody) -> Option { + pick_media_type(&body.content, |media_type| { + media_type_base(media_type) == "application/json" + }) + .or_else(|| pick_media_type(&body.content, is_json_media_type)) + .or_else(|| { + pick_media_type(&body.content, |media_type| { + media_type_base(media_type) == "multipart/form-data" + }) + }) + .or_else(|| { + pick_media_type(&body.content, |media_type| { + media_type_base(media_type) == "application/x-www-form-urlencoded" + }) + }) + .or_else(|| pick_media_type(&body.content, is_xml_media_type)) + .or_else(|| { + pick_media_type(&body.content, |media_type| { + media_type_base(media_type) == "text/plain" + }) + }) + .or_else(|| { + pick_media_type(&body.content, |media_type| { + media_type_base(media_type) == "application/octet-stream" + }) + }) + .or_else(|| body.content.keys().next().cloned()) +} + +fn unique_type_name(base: &str, used: &mut HashSet) -> String { + if used.insert(base.to_string()) { + return base.to_string(); + } + for i in 2..=u32::MAX { + let candidate = format!("{base}{i}"); + if used.insert(candidate.clone()) { + return candidate; + } + } + unreachable!("request input model name collision space exhausted") +} + +fn sanitize_operation_id(op_id: &str, method: &str, path: &str) -> String { + if !op_id.is_empty() { + return op_id.to_string(); + } + let path_part: String = path + .chars() + .map(|c| if c.is_alphanumeric() { c } else { '_' }) + .collect(); + format!("{method}_{path_part}") +} + +fn pick_media_type( + content: &indexmap::IndexMap, + predicate: impl Fn(&str) -> bool, +) -> Option { + content + .keys() + .find(|media_type| predicate(media_type)) + .cloned() +} + +fn is_json_media_type(media_type: &str) -> bool { + let base = media_type_base(media_type); + base == "application/json" || base.ends_with("+json") +} + +fn is_xml_media_type(media_type: &str) -> bool { + let base = media_type_base(media_type); + base == "application/xml" || base == "text/xml" || base.ends_with("+xml") +} diff --git a/src/generators/rust/aioduct/codegen.rs b/src/generators/rust/aioduct/codegen.rs index 891acd3fb..d8cbd9573 100644 --- a/src/generators/rust/aioduct/codegen.rs +++ b/src/generators/rust/aioduct/codegen.rs @@ -9,6 +9,7 @@ use super::sigil_emit_api::{aioduct_backend_config, emit_method_body}; use crate::codegen::traits::code_generator::CodeGenerator; use crate::codegen::traits::file_writer::{FileInfo, FileWriter}; use crate::codegen::{GeneratorType, Language}; +use crate::generators::request_inputs::plan_multipart_request_inputs; use crate::generators::rust::common::{ config::RustGeneratorConfig, emit_api, emit_models, project_files, }; @@ -43,13 +44,14 @@ impl RustAioductCodeGenerator { let default_cfg = default_aioduct_features(); let aioduct_cfg: &AioductFeatureConfig = self.config.aioduct.as_ref().unwrap_or(&default_cfg); + let request_inputs = plan_multipart_request_inputs(ir); let mut files = Vec::new(); files.extend( - emit_models::generate_model_files(ir, &header, &self.config).map_err(|msg| { - Box::::from(format!("emit_models: {msg}")) - })?, + emit_models::generate_model_files(ir, &header, &self.config, &request_inputs).map_err( + |msg| Box::::from(format!("emit_models: {msg}")), + )?, ); let response_extra = self @@ -63,12 +65,17 @@ impl RustAioductCodeGenerator { &header, &backend_config, response_extra, + &request_inputs, &emit_method_body, ) .map_err(|msg| Box::::from(format!("emit_api: {msg}")))?, ); - files.extend(runtime_files(&header, aioduct_cfg)); + files.extend(runtime_files( + &header, + aioduct_cfg, + request_inputs.has_uploads(), + )); files.push(cargo_toml_file(&crate_name, ir, &self.config)); files.push(project_files::lib_rs_file(&header)); diff --git a/src/generators/rust/aioduct/runtime.rs b/src/generators/rust/aioduct/runtime.rs index 38d1322ec..c51e3a32d 100644 --- a/src/generators/rust/aioduct/runtime.rs +++ b/src/generators/rust/aioduct/runtime.rs @@ -8,16 +8,35 @@ const CLIENT_RS_TEMPLATE: &str = include_str!("runtime/client.rs.txt"); const ERROR_RS: &str = include_str!("runtime/error.rs.txt"); const AUTH_RS: &str = include_str!("runtime/auth.rs.txt"); const MOD_RS: &str = include_str!("runtime/mod.rs.txt"); +const UPLOAD_FILE_RS: &str = include_str!("runtime/upload_file.rs.txt"); /// Returns runtime files ready to write. -pub fn runtime_files(header: &str, aioduct_cfg: &AioductFeatureConfig) -> Vec { +pub fn runtime_files( + header: &str, + aioduct_cfg: &AioductFeatureConfig, + include_upload_file: bool, +) -> Vec { let client_rs = render_client_rs(aioduct_cfg); - vec![ + let mut mod_rs = MOD_RS.to_string(); + let mut files = vec![ FileInfo::runtime("client.rs".to_string(), with_header(header, &client_rs)), FileInfo::runtime("error.rs".to_string(), with_header(header, ERROR_RS)), FileInfo::runtime("auth.rs".to_string(), with_header(header, AUTH_RS)), - FileInfo::runtime("mod.rs".to_string(), with_header(header, MOD_RS)), - ] + ]; + if include_upload_file { + mod_rs.push_str( + "mod upload_file;\npub use upload_file::{multipart_header_value, UploadFile};\n", + ); + files.push(FileInfo::runtime( + "upload_file.rs".to_string(), + with_header(header, UPLOAD_FILE_RS), + )); + } + files.push(FileInfo::runtime( + "mod.rs".to_string(), + with_header(header, &mod_rs), + )); + files } fn render_client_rs(cfg: &AioductFeatureConfig) -> String { diff --git a/src/generators/rust/aioduct/runtime/upload_file.rs.txt b/src/generators/rust/aioduct/runtime/upload_file.rs.txt new file mode 100644 index 000000000..d0f91d95c --- /dev/null +++ b/src/generators/rust/aioduct/runtime/upload_file.rs.txt @@ -0,0 +1,33 @@ +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct UploadFile { + pub data: Vec, + pub filename: Option, +} + +impl UploadFile { + pub fn new(data: impl Into>, filename: impl Into) -> Self { + Self { + data: data.into(), + filename: Some(filename.into()), + } + } + + pub fn from_bytes(data: impl Into>) -> Self { + Self { + data: data.into(), + filename: None, + } + } + + pub fn filename_or_default<'a>(&'a self, default_name: &'a str) -> &'a str { + self.filename.as_deref().unwrap_or(default_name) + } +} + +pub fn multipart_header_value(value: &str) -> String { + value + .replace('\r', "") + .replace('\n', "") + .replace('\\', "\\\\") + .replace('"', "\\\"") +} diff --git a/src/generators/rust/aioduct/sigil_emit_api.rs b/src/generators/rust/aioduct/sigil_emit_api.rs index 331343574..aa47c59af 100644 --- a/src/generators/rust/aioduct/sigil_emit_api.rs +++ b/src/generators/rust/aioduct/sigil_emit_api.rs @@ -5,9 +5,9 @@ use sigil_stitch::prelude::sigil_quote; use crate::generators::rust::common::emit_api::{ 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, + binary_field_expr, binary_filename_expr, emit_response_match, emit_result_init, + optional_binary_field_expr, optional_binary_filename_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). @@ -282,7 +282,8 @@ fn emit_multipart_body( if part.is_binary { b.add( &format!( - "multipart = multipart.file({wire_name}, {wire_name}, {content_type}, {});\n", + "multipart = multipart.file({wire_name}, {}, {content_type}, {});\n", + binary_filename_expr(body_var, part), binary_field_expr(body_var, part), ), (), @@ -305,7 +306,8 @@ fn emit_multipart_body( if part.is_binary { b.add( &format!( - "multipart = multipart.file({wire_name}, {wire_name}, {content_type}, {});\n", + "multipart = multipart.file({wire_name}, {}, {content_type}, {});\n", + optional_binary_filename_expr("value", part), optional_binary_field_expr("value"), ), (), diff --git a/src/generators/rust/common/emit_api.rs b/src/generators/rust/common/emit_api.rs index 0ce6e8e07..84e9d5def 100644 --- a/src/generators/rust/common/emit_api.rs +++ b/src/generators/rust/common/emit_api.rs @@ -12,6 +12,7 @@ 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::generators::request_inputs::{RequestInputPlan, request_input_for_operation}; use crate::ir::types::{ IrOperation, IrParameter, IrRequestBody, IrResponse, IrSpec, IrTypeExpr, ParameterLocation, }; @@ -55,6 +56,7 @@ pub fn generate_api_files( header: &str, config: &RustBackendConfig, response_extra_derives: Option<&ExtraDeriveConfig>, + request_inputs: &RequestInputPlan, body_emitter: &dyn Fn(&OpPlan<'_>) -> CodeBlock, ) -> Result, String> { let by_tag = group_by_tag(&ir.operations); @@ -65,7 +67,15 @@ pub fn generate_api_files( let stem = tag.to_snake_case(); let filename = format!("{stem}.rs"); mod_entries.push(stem); - let body = emit_api_file(tag, ops, ir, config, response_extra_derives, body_emitter); + let body = emit_api_file( + tag, + ops, + ir, + config, + response_extra_derives, + request_inputs, + body_emitter, + ); let content = format!("{header}{body}"); files.push(FileInfo::api(filename, content)); } @@ -109,10 +119,14 @@ fn emit_api_file( ir: &IrSpec, config: &RustBackendConfig, response_extra_derives: Option<&ExtraDeriveConfig>, + request_inputs: &RequestInputPlan, body_emitter: &dyn Fn(&OpPlan<'_>) -> CodeBlock, ) -> String { let struct_name = format!("{}Api", tag.to_pascal_case()); - let plans: Vec = ops.iter().map(|op| plan_operation(op, ir)).collect(); + let plans: Vec = ops + .iter() + .map(|op| plan_operation(op, ir, request_inputs)) + .collect(); let stem = tag.to_snake_case(); let mut fsb = FileSpec::builder(&format!("{stem}.rs")); @@ -257,7 +271,11 @@ pub enum ResponseDecoding { Other(String), } -pub fn plan_operation<'a>(op: &'a IrOperation, ir: &'a IrSpec) -> OpPlan<'a> { +pub fn plan_operation<'a>( + op: &'a IrOperation, + ir: &'a IrSpec, + request_inputs: &RequestInputPlan, +) -> OpPlan<'a> { let op_id = sanitize_operation_id(&op.operation_id, &op.method, &op.path); let method_name = op_id.to_snake_case(); let response_type = format!("{}Response", op_id.to_pascal_case()); @@ -288,7 +306,7 @@ pub fn plan_operation<'a>(op: &'a IrOperation, ir: &'a IrSpec) -> OpPlan<'a> { let body = op .request_body .as_ref() - .and_then(|b| plan_body(b, &mut used_names, ir)); + .and_then(|b| plan_body(op, b, &mut used_names, ir, request_inputs)); let typed_responses = op .responses @@ -309,15 +327,20 @@ pub fn plan_operation<'a>(op: &'a IrOperation, ir: &'a IrSpec) -> OpPlan<'a> { } pub fn plan_body( + op: &IrOperation, b: &IrRequestBody, used_names: &mut HashSet, ir: &IrSpec, + request_inputs: &RequestInputPlan, ) -> Option { let (media_type, t) = pick_body_content(b)?; let encoding = body_encoding(&media_type); let rust_type = match encoding { BodyEncoding::OctetStream => "Vec".to_string(), BodyEncoding::TextPlain => "String".to_string(), + BodyEncoding::Multipart => request_input_for_operation(request_inputs, op, &media_type) + .map(|input| format!("crate::models::{}", input.name.to_pascal_case())) + .unwrap_or_else(|| rust_type_str_qualified(&t, ir)), _ => rust_type_str_qualified(&t, ir), }; let multipart_parts = if encoding == BodyEncoding::Multipart { @@ -708,7 +731,7 @@ pub fn text_field_expr(base: &str, part: &MultipartPart) -> String { } pub fn binary_field_expr(base: &str, part: &MultipartPart) -> String { - format!("{base}.{}.clone()", rust_field_name(&part.wire_name)) + format!("{base}.{}.data.clone()", rust_field_name(&part.wire_name)) } pub fn optional_text_field_expr(value: &str, part: &MultipartPart) -> String { @@ -722,7 +745,22 @@ pub fn optional_text_field_expr(value: &str, part: &MultipartPart) -> String { } pub fn optional_binary_field_expr(value: &str) -> String { - format!("{value}.clone()") + format!("{value}.data.clone()") +} + +pub fn binary_filename_expr(base: &str, part: &MultipartPart) -> String { + format!( + "{base}.{}.filename_or_default({}).to_string()", + rust_field_name(&part.wire_name), + rust_string_literal(&part.default_filename) + ) +} + +pub fn optional_binary_filename_expr(value: &str, part: &MultipartPart) -> String { + format!( + "{value}.filename_or_default({}).to_string()", + rust_string_literal(&part.default_filename) + ) } pub fn response_value_expr(tr: &TypedResponse, bytes_var: &str) -> String { diff --git a/src/generators/rust/common/emit_models.rs b/src/generators/rust/common/emit_models.rs index 43ae816e5..17390930a 100644 --- a/src/generators/rust/common/emit_models.rs +++ b/src/generators/rust/common/emit_models.rs @@ -13,6 +13,9 @@ use std::collections::HashSet; use crate::codegen::traits::file_writer::FileInfo; +use crate::generators::request_inputs::{ + RequestInputField, RequestInputFieldKind, RequestInputModel, RequestInputPlan, +}; use crate::ir::types::{ IrEnum, IrEnumValueType, IrIntersection, IrObject, IrPrimitive, IrSchema, IrSchemaKind, IrSpec, IrTaggedUnion, IrTypeExpr, IrUnion, TaggingStyle, @@ -30,6 +33,7 @@ pub fn generate_model_files( ir: &IrSpec, header: &str, config: &RustGeneratorConfig, + request_inputs: &RequestInputPlan, ) -> Result, String> { let mut files = Vec::new(); let mut mod_entries = Vec::new(); @@ -102,6 +106,12 @@ pub fn generate_model_files( files.push(FileInfo::model(filename, content)); } + for model in request_inputs.models() { + let stem = model.name.to_snake_case(); + mod_entries.push(stem.clone()); + files.push(request_input_model_file(model, header, &stem)); + } + // mod.rs that re-exports all model modules let mut mod_content = String::from(header); for entry in &mod_entries { @@ -112,6 +122,38 @@ pub fn generate_model_files( Ok(files) } +fn request_input_model_file(model: &RequestInputModel, header: &str, stem: &str) -> FileInfo { + let name = model.name.to_pascal_case(); + let needs_upload = model.fields.iter().any(RequestInputField::is_upload); + let mut body = String::new(); + if needs_upload { + body.push_str("use crate::runtime::UploadFile;\n\n"); + } + body.push_str("#[derive(Debug, Clone)]\n"); + body.push_str(&format!("pub struct {name} {{\n")); + for field in &model.fields { + let field_name = escape_rust_keyword(&field.wire_name.to_snake_case()); + let mut field_type = request_input_rust_type(field); + if !field.required && !field_type.starts_with("Option<") { + field_type = format!("Option<{field_type}>"); + } + body.push_str(&format!(" pub {field_name}: {field_type},\n")); + } + body.push_str("}\n"); + + let mut content = String::with_capacity(header.len() + body.len()); + content.push_str(header); + content.push_str(&body); + FileInfo::model(format!("{stem}.rs"), content) +} + +fn request_input_rust_type(field: &RequestInputField) -> String { + match field.kind { + RequestInputFieldKind::UploadFile { .. } => "UploadFile".to_string(), + RequestInputFieldKind::SchemaValue => rust_type_str_model(&field.type_expr), + } +} + fn emit_model_file( schema: &IrSchema, config: &RustGeneratorConfig, diff --git a/src/generators/rust/reqwest/codegen.rs b/src/generators/rust/reqwest/codegen.rs index 36c3989a2..7872c4f0b 100644 --- a/src/generators/rust/reqwest/codegen.rs +++ b/src/generators/rust/reqwest/codegen.rs @@ -15,6 +15,7 @@ use super::sigil_emit_api::{emit_method_body, reqwest_backend_config}; use crate::codegen::traits::code_generator::CodeGenerator; use crate::codegen::traits::file_writer::{FileInfo, FileWriter}; use crate::codegen::{GeneratorType, Language}; +use crate::generators::request_inputs::plan_multipart_request_inputs; use crate::generators::rust::common::{ config::RustGeneratorConfig, emit_api, emit_models, project_files, }; @@ -45,14 +46,15 @@ impl RustReqwestCodeGenerator { let header = project_files::render_file_header(&ir.info); let crate_name = self.crate_name(&ir.info); let backend_config = reqwest_backend_config(); + let request_inputs = plan_multipart_request_inputs(ir); let mut files = Vec::new(); // Models (shared) files.extend( - emit_models::generate_model_files(ir, &header, &self.config).map_err(|msg| { - Box::::from(format!("emit_models: {msg}")) - })?, + emit_models::generate_model_files(ir, &header, &self.config, &request_inputs).map_err( + |msg| Box::::from(format!("emit_models: {msg}")), + )?, ); // APIs (shared planning + reqwest body emitter) @@ -67,13 +69,14 @@ impl RustReqwestCodeGenerator { &header, &backend_config, response_extra, + &request_inputs, &emit_method_body, ) .map_err(|msg| Box::::from(format!("emit_api: {msg}")))?, ); // Runtime (reqwest-specific) - files.extend(runtime_files(&header)); + files.extend(runtime_files(&header, request_inputs.has_uploads())); // Project files files.push(cargo_toml_file(&crate_name, ir, &self.config)); diff --git a/src/generators/rust/reqwest/runtime.rs b/src/generators/rust/reqwest/runtime.rs index cdcb8c60d..beac9ab26 100644 --- a/src/generators/rust/reqwest/runtime.rs +++ b/src/generators/rust/reqwest/runtime.rs @@ -9,15 +9,30 @@ const CLIENT_RS: &str = include_str!("runtime/client.rs.txt"); const ERROR_RS: &str = include_str!("runtime/error.rs.txt"); const AUTH_RS: &str = include_str!("runtime/auth.rs.txt"); const MOD_RS: &str = include_str!("runtime/mod.rs.txt"); +const UPLOAD_FILE_RS: &str = include_str!("runtime/upload_file.rs.txt"); /// Returns runtime files ready to write. -pub fn runtime_files(header: &str) -> Vec { - vec![ +pub fn runtime_files(header: &str, include_upload_file: bool) -> Vec { + let mut mod_rs = MOD_RS.to_string(); + let mut files = vec![ FileInfo::runtime("client.rs".to_string(), with_header(header, CLIENT_RS)), FileInfo::runtime("error.rs".to_string(), with_header(header, ERROR_RS)), FileInfo::runtime("auth.rs".to_string(), with_header(header, AUTH_RS)), - FileInfo::runtime("mod.rs".to_string(), with_header(header, MOD_RS)), - ] + ]; + if include_upload_file { + mod_rs.push_str( + "mod upload_file;\npub use upload_file::{multipart_header_value, UploadFile};\n", + ); + files.push(FileInfo::runtime( + "upload_file.rs".to_string(), + with_header(header, UPLOAD_FILE_RS), + )); + } + files.push(FileInfo::runtime( + "mod.rs".to_string(), + with_header(header, &mod_rs), + )); + files } fn with_header(header: &str, body: &str) -> String { diff --git a/src/generators/rust/reqwest/runtime/upload_file.rs.txt b/src/generators/rust/reqwest/runtime/upload_file.rs.txt new file mode 100644 index 000000000..d0f91d95c --- /dev/null +++ b/src/generators/rust/reqwest/runtime/upload_file.rs.txt @@ -0,0 +1,33 @@ +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct UploadFile { + pub data: Vec, + pub filename: Option, +} + +impl UploadFile { + pub fn new(data: impl Into>, filename: impl Into) -> Self { + Self { + data: data.into(), + filename: Some(filename.into()), + } + } + + pub fn from_bytes(data: impl Into>) -> Self { + Self { + data: data.into(), + filename: None, + } + } + + pub fn filename_or_default<'a>(&'a self, default_name: &'a str) -> &'a str { + self.filename.as_deref().unwrap_or(default_name) + } +} + +pub fn multipart_header_value(value: &str) -> String { + value + .replace('\r', "") + .replace('\n', "") + .replace('\\', "\\\\") + .replace('"', "\\\"") +} diff --git a/src/generators/rust/reqwest/sigil_emit_api.rs b/src/generators/rust/reqwest/sigil_emit_api.rs index 92ff93010..cea23563c 100644 --- a/src/generators/rust/reqwest/sigil_emit_api.rs +++ b/src/generators/rust/reqwest/sigil_emit_api.rs @@ -5,9 +5,9 @@ use sigil_stitch::prelude::sigil_quote; use crate::generators::rust::common::emit_api::{ 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, + binary_field_expr, binary_filename_expr, emit_response_match, emit_result_init, + optional_binary_field_expr, optional_binary_filename_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). @@ -278,8 +278,9 @@ fn emit_multipart_body( if part.is_binary { b.add( &format!( - "multipart = multipart.part({wire_name}, reqwest::multipart::Part::bytes({}).file_name({wire_name}).mime_str({content_type}).map_err(Error::Network)?);\n", + "multipart = multipart.part({wire_name}, reqwest::multipart::Part::bytes({}).file_name({}).mime_str({content_type}).map_err(Error::Network)?);\n", binary_field_expr(body_var, part), + binary_filename_expr(body_var, part), ), (), ); @@ -301,8 +302,9 @@ fn emit_multipart_body( if part.is_binary { b.add( &format!( - "multipart = multipart.part({wire_name}, reqwest::multipart::Part::bytes({}).file_name({wire_name}).mime_str({content_type}).map_err(Error::Network)?);\n", + "multipart = multipart.part({wire_name}, reqwest::multipart::Part::bytes({}).file_name({}).mime_str({content_type}).map_err(Error::Network)?);\n", optional_binary_field_expr("value"), + optional_binary_filename_expr("value", part), ), (), ); diff --git a/src/generators/rust/ureq/codegen.rs b/src/generators/rust/ureq/codegen.rs index 2e37a1726..d9ddd6ef6 100644 --- a/src/generators/rust/ureq/codegen.rs +++ b/src/generators/rust/ureq/codegen.rs @@ -9,6 +9,7 @@ use super::sigil_emit_api::{emit_method_body, ureq_backend_config}; use crate::codegen::traits::code_generator::CodeGenerator; use crate::codegen::traits::file_writer::{FileInfo, FileWriter}; use crate::codegen::{GeneratorType, Language}; +use crate::generators::request_inputs::plan_multipart_request_inputs; use crate::generators::rust::common::{ config::RustGeneratorConfig, emit_api, emit_models, project_files, }; @@ -38,13 +39,14 @@ impl RustUreqCodeGenerator { let header = project_files::render_file_header(&ir.info); let crate_name = self.crate_name(&ir.info); let backend_config = ureq_backend_config(); + let request_inputs = plan_multipart_request_inputs(ir); let mut files = Vec::new(); files.extend( - emit_models::generate_model_files(ir, &header, &self.config).map_err(|msg| { - Box::::from(format!("emit_models: {msg}")) - })?, + emit_models::generate_model_files(ir, &header, &self.config, &request_inputs).map_err( + |msg| Box::::from(format!("emit_models: {msg}")), + )?, ); let response_extra = self @@ -58,12 +60,13 @@ impl RustUreqCodeGenerator { &header, &backend_config, response_extra, + &request_inputs, &emit_method_body, ) .map_err(|msg| Box::::from(format!("emit_api: {msg}")))?, ); - files.extend(runtime_files(&header)); + files.extend(runtime_files(&header, request_inputs.has_uploads())); files.push(cargo_toml_file(&crate_name, &ir.info, &self.config)); files.push(project_files::lib_rs_file(&header)); diff --git a/src/generators/rust/ureq/runtime.rs b/src/generators/rust/ureq/runtime.rs index 4fa95c4e2..89e8d02e9 100644 --- a/src/generators/rust/ureq/runtime.rs +++ b/src/generators/rust/ureq/runtime.rs @@ -7,13 +7,28 @@ const CLIENT_RS: &str = include_str!("runtime/client.rs.txt"); const ERROR_RS: &str = include_str!("runtime/error.rs.txt"); const AUTH_RS: &str = include_str!("runtime/auth.rs.txt"); const MOD_RS: &str = include_str!("runtime/mod.rs.txt"); +const UPLOAD_FILE_RS: &str = include_str!("runtime/upload_file.rs.txt"); /// Returns runtime files ready to write. -pub fn runtime_files(header: &str) -> Vec { - vec![ +pub fn runtime_files(header: &str, include_upload_file: bool) -> Vec { + let mut mod_rs = MOD_RS.to_string(); + let mut files = vec![ FileInfo::runtime("client.rs".to_string(), with_header(header, CLIENT_RS)), FileInfo::runtime("error.rs".to_string(), with_header(header, ERROR_RS)), FileInfo::runtime("auth.rs".to_string(), with_header(header, AUTH_RS)), - FileInfo::runtime("mod.rs".to_string(), with_header(header, MOD_RS)), - ] + ]; + if include_upload_file { + mod_rs.push_str( + "mod upload_file;\npub use upload_file::{multipart_header_value, UploadFile};\n", + ); + files.push(FileInfo::runtime( + "upload_file.rs".to_string(), + with_header(header, UPLOAD_FILE_RS), + )); + } + files.push(FileInfo::runtime( + "mod.rs".to_string(), + with_header(header, &mod_rs), + )); + files } diff --git a/src/generators/rust/ureq/runtime/upload_file.rs.txt b/src/generators/rust/ureq/runtime/upload_file.rs.txt new file mode 100644 index 000000000..d0f91d95c --- /dev/null +++ b/src/generators/rust/ureq/runtime/upload_file.rs.txt @@ -0,0 +1,33 @@ +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct UploadFile { + pub data: Vec, + pub filename: Option, +} + +impl UploadFile { + pub fn new(data: impl Into>, filename: impl Into) -> Self { + Self { + data: data.into(), + filename: Some(filename.into()), + } + } + + pub fn from_bytes(data: impl Into>) -> Self { + Self { + data: data.into(), + filename: None, + } + } + + pub fn filename_or_default<'a>(&'a self, default_name: &'a str) -> &'a str { + self.filename.as_deref().unwrap_or(default_name) + } +} + +pub fn multipart_header_value(value: &str) -> String { + value + .replace('\r', "") + .replace('\n', "") + .replace('\\', "\\\\") + .replace('"', "\\\"") +} diff --git a/src/generators/rust/ureq/sigil_emit_api.rs b/src/generators/rust/ureq/sigil_emit_api.rs index d55985bbd..7f6c8c685 100644 --- a/src/generators/rust/ureq/sigil_emit_api.rs +++ b/src/generators/rust/ureq/sigil_emit_api.rs @@ -5,9 +5,9 @@ use sigil_stitch::prelude::sigil_quote; use crate::generators::rust::common::emit_api::{ 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, + binary_field_expr, binary_filename_expr, emit_response_match, emit_result_init, + optional_binary_field_expr, optional_binary_filename_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; @@ -370,7 +370,14 @@ fn emit_multipart_part( return; } if part.required { - emit_part_prefix(b, &wire_name, part.is_binary, &content_type); + let filename_expr = part.is_binary.then(|| binary_filename_expr(body_var, part)); + emit_part_prefix( + b, + &wire_name, + part.is_binary, + &content_type, + filename_expr.as_deref(), + ); if part.is_binary { b.add( &format!( @@ -395,7 +402,16 @@ fn emit_multipart_part( &format!("if let Some(value) = &{body_var}.{field_name}"), (), ); - emit_part_prefix(b, &wire_name, part.is_binary, &content_type); + let filename_expr = part + .is_binary + .then(|| optional_binary_filename_expr("value", part)); + emit_part_prefix( + b, + &wire_name, + part.is_binary, + &content_type, + filename_expr.as_deref(), + ); if part.is_binary { b.add( &format!( @@ -423,22 +439,24 @@ fn emit_part_prefix( wire_name: &str, is_binary: bool, content_type: &str, + filename_expr: Option<&str>, ) { b.add( "multipart_body.extend_from_slice(format!(\"--{}\\r\\n\", boundary).as_bytes());\n", (), ); if is_binary { + let filename_expr = filename_expr.expect("binary multipart part filename"); b.add( &format!( - "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", + "multipart_body.extend_from_slice(format!(\"Content-Disposition: form-data; name=\\\"{{}}\\\"; filename=\\\"{{}}\\\"\\r\\nContent-Type: {{}}\\r\\n\\r\\n\", crate::runtime::multipart_header_value({wire_name}), crate::runtime::multipart_header_value(&{filename_expr}), {content_type}).as_bytes());\n", ), (), ); } else { b.add( &format!( - "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", + "multipart_body.extend_from_slice(format!(\"Content-Disposition: form-data; name=\\\"{{}}\\\"\\r\\nContent-Type: {{}}\\r\\n\\r\\n\", crate::runtime::multipart_header_value({wire_name}), {content_type}).as_bytes());\n", ), (), ); diff --git a/src/generators/typescript/fetch/codegen.rs b/src/generators/typescript/fetch/codegen.rs index 51abf8a43..a8c120604 100644 --- a/src/generators/typescript/fetch/codegen.rs +++ b/src/generators/typescript/fetch/codegen.rs @@ -13,7 +13,11 @@ use crate::codegen::NamingConvention; use crate::codegen::traits::code_generator::CodeGenerator; use crate::codegen::traits::file_writer::{FileInfo, FileWriter}; use crate::codegen::{GeneratorType, Language}; -use crate::ir::types::IrSpec; +use crate::generators::request_inputs::{ + RequestInputField, RequestInputFieldKind, RequestInputModel, RequestInputPlan, + plan_multipart_request_inputs, +}; +use crate::ir::types::{IrPrimitive, IrSpec, IrTypeExpr}; /// TypeScript Fetch code generator #[derive(Debug, Clone)] @@ -51,6 +55,7 @@ impl TypeScriptFetchCodeGenerator { pub fn generate_models_from_ir( &self, ir: &crate::ir::types::IrSpec, + request_inputs: &RequestInputPlan, ) -> Result, Box> { let ts = TypeScript::new().with_indent(&self.config.indent); let flags = super::sigil_emit::EmitFlags { @@ -63,7 +68,7 @@ impl TypeScriptFetchCodeGenerator { Box::::from(format!("sigil_emit model generation: {msg}")) })?; - if !ir.schemas.is_empty() { + if !ir.schemas.is_empty() || !request_inputs.models().is_empty() { let mut names: Vec = ir.schemas.keys().cloned().collect(); names.sort(); let mut exports: Vec = Vec::new(); @@ -134,30 +139,99 @@ impl TypeScriptFetchCodeGenerator { } } } + for model in request_inputs.models() { + let type_name = model.name.to_pascal_case(); + let filename = self.generate_filename(&model.name); + let stem = filename.trim_end_matches(".ts"); + exports.push(format!("export type {{ {type_name} }} from './{stem}';")); + } files.push(render_index_file(&ir.info, "models/index.ts", &exports)); } + for model in request_inputs.models() { + files.push(self.request_input_model_file(ir, model)); + } + Ok(files) } + fn request_input_model_file(&self, ir: &IrSpec, model: &RequestInputModel) -> FileInfo { + let header = super::project_files::render_file_header(&ir.info); + let mut imports: std::collections::BTreeMap> = + std::collections::BTreeMap::new(); + let mut needs_upload = false; + for field in &model.fields { + if field.is_upload() { + needs_upload = true; + } else { + collect_ts_model_imports(&field.type_expr, &mut imports); + } + } + + let mut content = String::new(); + content.push_str(&header); + if needs_upload { + content.push_str("import type { UploadFileInput } from '../runtime/runtime';\n"); + } + for (schema_name, type_names) in imports { + let filename = self.generate_filename(&schema_name); + let stem = filename.trim_end_matches(".ts"); + let names = type_names.into_iter().collect::>().join(", "); + content.push_str(&format!("import type {{ {names} }} from './{stem}';\n")); + } + if needs_upload || !model.fields.is_empty() { + content.push('\n'); + } + + content.push_str(&format!( + "export interface {} {{\n", + model.name.to_pascal_case() + )); + for field in &model.fields { + let field_name = + if self.config.property_naming == super::config::PropertyNaming::CamelCase { + field.wire_name.to_lower_camel_case() + } else { + field.wire_name.clone() + }; + let ty = request_input_field_ts_type(field); + let optional = if field.required { "" } else { "?" }; + content.push_str(&format!( + " {}{}: {};\n", + ts_property_name(&field_name), + optional, + ty + )); + } + content.push_str("}\n"); + + FileInfo::model(self.generate_filename(&model.name), content) + } + /// Generate ALL files from the IR spec. fn generate_ir(&self, ir: &IrSpec) -> Result, Box> { let mut files = Vec::new(); + let request_inputs = plan_multipart_request_inputs(ir); - files.extend(self.generate_apis_from_ir(ir)?); - files.extend(self.generate_models_from_ir(ir)?); + files.extend(self.generate_apis_from_ir(ir, &request_inputs)?); + files.extend(self.generate_models_from_ir(ir, &request_inputs)?); let base_path = ir .servers .first() .map(|s| s.url.clone()) .unwrap_or_else(|| "http://localhost".to_string()); - files.push(render_runtime_file(&ir.info, &base_path)); + files.push(render_runtime_file( + &ir.info, + &base_path, + request_inputs.has_uploads(), + )); files.push(render_readme_file(ir)); let has_apis = !ir.operations.is_empty(); - let has_models = ir.schemas.values().any(|s| s.is_component); + let has_models = + ir.schemas.values().any(|s| s.is_component) || !request_inputs.models().is_empty(); files.extend(self.generate_project_files_from_ir(ir, has_apis, has_models)); Ok(files) @@ -170,11 +244,13 @@ impl TypeScriptFetchCodeGenerator { fn generate_apis_from_ir( &self, ir: &crate::ir::types::IrSpec, + request_inputs: &RequestInputPlan, ) -> Result, Box> { let ts = TypeScript::new().with_indent(&self.config.indent); let mut files = super::sigil_emit_api::generate_api_files( ir, self.config.property_naming == super::config::PropertyNaming::CamelCase, + request_inputs, &ts, ) .map_err(|msg| { @@ -400,6 +476,106 @@ export default defineConfig({ } } +fn request_input_field_ts_type(field: &RequestInputField) -> String { + match field.kind { + RequestInputFieldKind::UploadFile { .. } => "UploadFileInput".to_string(), + RequestInputFieldKind::SchemaValue => ts_type_string(&field.type_expr), + } +} + +fn ts_type_string(expr: &IrTypeExpr) -> String { + match expr { + IrTypeExpr::Named(name) => name.to_pascal_case(), + IrTypeExpr::Primitive(p) => match p { + IrPrimitive::Binary => "Blob | File".to_string(), + IrPrimitive::String + | IrPrimitive::Date + | IrPrimitive::DateTime + | IrPrimitive::Uuid + | IrPrimitive::StringWithFormat(_) => "string".to_string(), + IrPrimitive::Integer + | IrPrimitive::IntegerWithFormat(_) + | IrPrimitive::Number + | IrPrimitive::NumberWithFormat(_) => "number".to_string(), + IrPrimitive::Boolean => "boolean".to_string(), + }, + IrTypeExpr::Array(inner) => format!("readonly {}[]", ts_type_string_nested(inner)), + IrTypeExpr::Nullable(inner) => { + format!("{} | null", parenthesize_union(&ts_type_string(inner))) + } + IrTypeExpr::StringLiteral(value) => { + format!("'{}'", value.replace('\\', "\\\\").replace('\'', "\\'")) + } + IrTypeExpr::StringEnum(values) => values + .iter() + .map(|value| format!("'{}'", value.replace('\\', "\\\\").replace('\'', "\\'"))) + .collect::>() + .join(" | "), + IrTypeExpr::Map(inner) => format!("Record", ts_type_string(inner)), + IrTypeExpr::Union(members) => members + .iter() + .map(ts_type_string) + .collect::>() + .join(" | "), + IrTypeExpr::Any => "unknown".to_string(), + } +} + +fn ts_type_string_nested(expr: &IrTypeExpr) -> String { + match expr { + IrTypeExpr::Array(inner) => format!("{}[]", ts_type_string_nested(inner)), + other => parenthesize_union(&ts_type_string(other)), + } +} + +fn parenthesize_union(ty: &str) -> String { + if ty.contains(" | ") { + format!("({ty})") + } else { + ty.to_string() + } +} + +fn collect_ts_model_imports( + expr: &IrTypeExpr, + imports: &mut std::collections::BTreeMap>, +) { + match expr { + IrTypeExpr::Named(name) => { + imports + .entry(name.clone()) + .or_default() + .insert(name.to_pascal_case()); + } + IrTypeExpr::Array(inner) | IrTypeExpr::Nullable(inner) | IrTypeExpr::Map(inner) => { + collect_ts_model_imports(inner, imports); + } + IrTypeExpr::Union(members) => { + for member in members { + collect_ts_model_imports(member, imports); + } + } + _ => {} + } +} + +fn ts_property_name(name: &str) -> String { + if is_js_identifier(name) { + name.to_string() + } else { + format!("'{}'", name.replace('\\', "\\\\").replace('\'', "\\'")) + } +} + +fn is_js_identifier(name: &str) -> bool { + let mut chars = name.chars(); + match chars.next() { + Some(c) if c == '_' || c == '$' || c.is_ascii_alphabetic() => {} + _ => return false, + } + chars.all(|c| c == '_' || c == '$' || c.is_ascii_alphanumeric()) +} + impl CodeGenerator for TypeScriptFetchCodeGenerator { fn language(&self) -> Language { Language::TypeScript diff --git a/src/generators/typescript/fetch/project_files.rs b/src/generators/typescript/fetch/project_files.rs index aa907206c..221f53588 100644 --- a/src/generators/typescript/fetch/project_files.rs +++ b/src/generators/typescript/fetch/project_files.rs @@ -8,6 +8,39 @@ use crate::ir::types::{IrInfo, IrSpec}; const RUNTIME_TS_BODY: &str = include_str!("project_files/runtime.ts.txt"); const README_MD_BODY: &str = include_str!("project_files/README.md.txt"); +const UPLOAD_FILE_HELPERS: &str = r#"export interface UploadFile { + data: Blob; + filename?: string; +} + +export type UploadFileInput = Blob | File | UploadFile; + +export function uploadFileData(value: UploadFileInput): Blob { + return isUploadFile(value) ? value.data : value; +} + +export function uploadFileFilename(value: UploadFileInput, fallback: string): string { + if (isFile(value) && value.name) { + return value.name; + } + if (isUploadFile(value) && value.filename) { + return value.filename; + } + return fallback; +} + +export function multipartHeaderValue(value: string): string { + return value.replace(/[\r\n]/g, '').replace(/\\/g, '\\\\').replace(/"/g, '\\"'); +} + +function isUploadFile(value: UploadFileInput): value is UploadFile { + return typeof value === 'object' && value !== null && 'data' in value; +} + +function isFile(value: UploadFileInput): value is File { + return typeof File !== 'undefined' && value instanceof File; +} +"#; /// Render the `@generated` file header used by all generated TypeScript files. /// Terminates with a trailing newline so callers can concatenate body content @@ -40,10 +73,20 @@ pub fn render_index_file(info: &IrInfo, output_path: &str, exports: &[String]) - FileInfo::project(output_path.to_string(), content) } -/// Render `runtime/runtime.ts` — fully static except for the base URL. -pub fn render_runtime_file(info: &IrInfo, base_path: &str) -> FileInfo { +/// Render `runtime/runtime.ts` — static except for the base URL and conditional +/// upload helpers used by multipart request input models. +pub fn render_runtime_file(info: &IrInfo, base_path: &str, include_upload_file: bool) -> FileInfo { let mut content = render_file_header(info); - content.push_str(&RUNTIME_TS_BODY.replace("{{BASE_PATH}}", base_path)); + let upload_helpers = if include_upload_file { + format!("\n{}\n", UPLOAD_FILE_HELPERS.trim_end()) + } else { + String::new() + }; + content.push_str( + &RUNTIME_TS_BODY + .replace("{{BASE_PATH}}", base_path) + .replace("{{UPLOAD_FILE_HELPERS}}", &upload_helpers), + ); FileInfo::runtime("runtime.ts".to_string(), content) } diff --git a/src/generators/typescript/fetch/project_files/runtime.ts.txt b/src/generators/typescript/fetch/project_files/runtime.ts.txt index 548b67f21..6ab25fe84 100644 --- a/src/generators/typescript/fetch/project_files/runtime.ts.txt +++ b/src/generators/typescript/fetch/project_files/runtime.ts.txt @@ -78,7 +78,7 @@ export class Configuration { } export const DefaultConfig = new Configuration(); - +{{UPLOAD_FILE_HELPERS}} /** * This is the base class for all generated API classes. */ diff --git a/src/generators/typescript/fetch/sigil_emit_api.rs b/src/generators/typescript/fetch/sigil_emit_api.rs index 66e8327ea..d1dde54de 100644 --- a/src/generators/typescript/fetch/sigil_emit_api.rs +++ b/src/generators/typescript/fetch/sigil_emit_api.rs @@ -20,6 +20,7 @@ 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::generators::request_inputs::{RequestInputPlan, request_input_for_operation}; use crate::ir::types::{ IrOperation, IrParameter, IrPrimitive, IrRequestBody, IrResponse, IrSpec, IrTypeExpr, ParameterLocation as IrParameterLocation, @@ -44,6 +45,7 @@ const RUNTIME_MOD: &str = "../runtime/runtime"; pub fn generate_api_files( ir: &IrSpec, property_naming_camel_case: bool, + request_inputs: &RequestInputPlan, ts: &TypeScript, ) -> Result, String> { let header = super::project_files::render_file_header(&ir.info); @@ -56,7 +58,15 @@ pub fn generate_api_files( let mut files = Vec::with_capacity(by_tag.len()); for (tag, ops) in &by_tag { - let file_spec = emit_api_file(tag, ops, ir, property_naming_camel_case, &convertible, ts)?; + let file_spec = emit_api_file( + tag, + ops, + ir, + property_naming_camel_case, + request_inputs, + &convertible, + ts, + )?; let body = file_spec .render(100) .map_err(|e| format!("sigil_emit_api: render {tag}: {e}"))?; @@ -132,6 +142,7 @@ fn emit_api_file( ops: &[&IrOperation], ir: &IrSpec, property_naming_camel_case: bool, + request_inputs: &RequestInputPlan, convertible: &HashSet, ts: &TypeScript, ) -> Result { @@ -142,7 +153,7 @@ fn emit_api_file( // Request interfaces — one per op that has at least one parameter / body. for op in ops { - if let Some(req_iface) = build_request_interface(op) { + if let Some(req_iface) = build_request_interface(op, request_inputs) { fb = fb.add_type(req_iface); } } @@ -274,7 +285,10 @@ fn raw_response_members(op: &IrOperation) -> Vec { // Request interfaces // ============================================================================ -fn build_request_interface(op: &IrOperation) -> Option { +fn build_request_interface( + op: &IrOperation, + request_inputs: &RequestInputPlan, +) -> Option { let has_params = !op.parameters.is_empty() || op.request_body.is_some(); if !has_params { return None; @@ -294,7 +308,12 @@ fn build_request_interface(op: &IrOperation) -> Option { tb = tb.add_field(build_param_field(param, &resolved_param(&names, param))); } if let Some(rb) = &op.request_body { - tb = tb.add_field(build_body_field(rb, &resolved_body(&names))); + tb = tb.add_field(build_body_field( + op, + rb, + &resolved_body(&names), + request_inputs, + )); } tb.build().ok() @@ -345,10 +364,23 @@ fn preferred_request_media_type(rb: &IrRequestBody) -> Option { .or_else(|| pick_first_media_type(&rb.content)) } -fn build_body_field(rb: &IrRequestBody, name: &str) -> FieldSpec { +fn build_body_field( + op: &IrOperation, + rb: &IrRequestBody, + name: &str, + request_inputs: &RequestInputPlan, +) -> FieldSpec { let ty = preferred_request_media_type(rb) - .and_then(|mt| rb.content.get(mt.as_str())) - .map(type_expr_to_typename) + .and_then(|mt| { + if media_type_base(&mt) == "multipart/form-data" { + request_input_for_operation(request_inputs, op, &mt).map(|input| { + let ts_name = input.name.to_pascal_case(); + TypeName::importable_type(&format!("../models/{ts_name}"), &ts_name) + }) + } else { + rb.content.get(mt.as_str()).map(type_expr_to_typename) + } + }) .unwrap_or_else(|| TypeName::primitive("unknown")); let mut fb = FieldSpec::builder(name, ty); if !rb.required { @@ -818,47 +850,79 @@ fn emit_multipart_blob_part( ); return; } - let disposition = if part.is_binary { - format!( - "form-data; name=\"{}\"; filename=\"{}\"", - multipart_header_quoted(&part.wire_name), + if part.is_binary { + cb.add("{\n", vec![]); + let header_prefix = format!( + "\r\nContent-Disposition: form-data; name=\"{}\"; filename=\"", multipart_header_quoted(&part.wire_name) - ) + ); + let header_suffix = format!( + "\"\r\nContent-Type: {}\r\n\r\n", + multipart_header_value(&part.content_type) + ); + cb.add( + &format!( + "const multipartFilename = %T({part_access}, {});\n", + ts_string_literal(&part.wire_name) + ), + vec![Arg::TypeName(rt_value("uploadFileFilename"))], + ); + cb.add( + &format!( + "multipartChunks.push('--' + multipartBoundary + {} + %T(multipartFilename) + {});\n", + ts_string_literal(&header_prefix), + ts_string_literal(&header_suffix) + ), + vec![Arg::TypeName(rt_value("multipartHeaderValue"))], + ); } else { - format!( + let disposition = 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 { - let json_value = multipart_part_to_json_expr(part, part_access, convertible) - .unwrap_or_else(|| part_access.to_string()); - format!("JSON.stringify({json_value})") + ); + 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"), + ); + } + if part.is_binary { + cb.add( + &format!("multipartChunks.push(%T({part_access}));\n"), + vec![Arg::TypeName(rt_value("uploadFileData"))], + ); } else { - format!("String({part_access})") - }; + let value_expr = if part.value_encoding == MultipartValueEncoding::Json { + let json_value = multipart_part_to_json_expr(part, part_access, convertible) + .unwrap_or_else(|| part_access.to_string()); + format!("JSON.stringify({json_value})") + } else { + format!("String({part_access})") + }; + cb.add_code( + sigil_quote!(TypeScript { + multipartChunks.push($L(value_expr)); + }) + .expect("multipart part value block builds"), + ); + } 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"), + .expect("multipart part trailing crlf block builds"), ); + if part.is_binary { + cb.add("}\n", vec![]); + } } fn emit_make_request( 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 81ead3011..4c748fd3e 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 @@ -11,6 +11,7 @@ import ( "encoding/json" "fmt" "io" + "mime" "mime/multipart" "net/http" "net/textproto" @@ -77,7 +78,7 @@ type UploadAssetResponse struct { } // UploadAsset calls POST /uploads. -func (a *TransferAPI) UploadAsset(ctx context.Context, body *models.UploadAssetRequest) (*UploadAssetResponse, error) { +func (a *TransferAPI) UploadAsset(ctx context.Context, body *models.UploadAssetMultipartRequestBody) (*UploadAssetResponse, error) { path := "/uploads" var bodyReader io.Reader var multipartContentType string @@ -86,13 +87,14 @@ func (a *TransferAPI) UploadAsset(ctx context.Context, body *models.UploadAssetR writer := multipart.NewWriter(buf) { partHeader := textproto.MIMEHeader{} - partHeader.Set("Content-Disposition", "form-data; name=\"file\"; filename=\"file\"") + disposition := mime.FormatMediaType("form-data", map[string]string{"name": "file", "filename": body.File.FilenameOrDefault("file")}) + partHeader.Set("Content-Disposition", disposition) partHeader.Set("Content-Type", "application/octet-stream") 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 { + if _, err := partWriter.Write(body.File.Data); err != nil { return nil, fmt.Errorf("write multipart file: %w", err) } } diff --git a/tests/golden/go/go-http/binary-transfer-media-types/models/upload_asset_multipart_request_body.go.golden b/tests/golden/go/go-http/binary-transfer-media-types/models/upload_asset_multipart_request_body.go.golden new file mode 100644 index 000000000..9f8574025 --- /dev/null +++ b/tests/golden/go/go-http/binary-transfer-media-types/models/upload_asset_multipart_request_body.go.golden @@ -0,0 +1,14 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Binary Transfer Media Types — 1.0.0 +// Covers multipart upload and octet-stream download. + +package models + +import "example.com/sdk/runtime" + +type UploadAssetMultipartRequestBody struct { + File runtime.UploadFile + Metadata UploadAttributes + Purpose string +} diff --git a/tests/golden/go/go-http/binary-transfer-media-types/runtime/upload_file.go.golden b/tests/golden/go/go-http/binary-transfer-media-types/runtime/upload_file.go.golden new file mode 100644 index 000000000..8e4227fd8 --- /dev/null +++ b/tests/golden/go/go-http/binary-transfer-media-types/runtime/upload_file.go.golden @@ -0,0 +1,26 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Binary Transfer Media Types — 1.0.0 +// Covers multipart upload and octet-stream download. + +package runtime + +type UploadFile struct { + Data []byte + Filename string +} + +func NewUploadFile(data []byte, filename string) UploadFile { + return UploadFile{Data: data, Filename: filename} +} + +func NewUploadFileBytes(data []byte) UploadFile { + return UploadFile{Data: data} +} + +func (f UploadFile) FilenameOrDefault(defaultName string) string { + if f.Filename != "" { + return f.Filename + } + return defaultName +} 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 9a74068b9..fc553d146 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 @@ -11,6 +11,7 @@ import ( "encoding/json" "fmt" "io" + "mime" "mime/multipart" "net/http" "net/textproto" @@ -73,7 +74,8 @@ type SendParameterizedMultipartResponse struct { } // SendParameterizedMultipart calls POST /request/parameterized-multipart. -func (a *MediaAPI) SendParameterizedMultipart(ctx context.Context, body *models.FileEnvelope) (*SendParameterizedMultipartResponse, error) { +func (a *MediaAPI) SendParameterizedMultipart(ctx context.Context, +body *models.SendParameterizedMultipartMultipartRequestBody) (*SendParameterizedMultipartResponse, error) { path := "/request/parameterized-multipart" var bodyReader io.Reader var multipartContentType string @@ -82,17 +84,19 @@ func (a *MediaAPI) SendParameterizedMultipart(ctx context.Context, body *models. writer := multipart.NewWriter(buf) { partHeader := textproto.MIMEHeader{} - partHeader.Set("Content-Disposition", "form-data; name=\"file\"; filename=\"file\"") + disposition := mime.FormatMediaType("form-data", map[string]string{"name": "file", "filename": body.File.FilenameOrDefault("file")}) + partHeader.Set("Content-Disposition", disposition) partHeader.Set("Content-Type", "application/octet-stream") 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 { + if _, err := partWriter.Write(body.File.Data); err != nil { return nil, fmt.Errorf("write multipart file: %w", err) } } if body.Note != nil { + value := *body.Note { partHeader := textproto.MIMEHeader{} partHeader.Set("Content-Disposition", "form-data; name=\"note\"") @@ -101,7 +105,7 @@ func (a *MediaAPI) SendParameterizedMultipart(ctx context.Context, body *models. if err != nil { return nil, fmt.Errorf("create multipart part: %w", err) } - if _, err := io.WriteString(partWriter, *body.Note); err != nil { + if _, err := io.WriteString(partWriter, value); err != nil { return nil, fmt.Errorf("write multipart field: %w", err) } } diff --git a/tests/golden/go/go-http/media-type-selection/models/send_parameterized_multipart_multipart_request_body.go.golden b/tests/golden/go/go-http/media-type-selection/models/send_parameterized_multipart_multipart_request_body.go.golden new file mode 100644 index 000000000..ada44d6dc --- /dev/null +++ b/tests/golden/go/go-http/media-type-selection/models/send_parameterized_multipart_multipart_request_body.go.golden @@ -0,0 +1,13 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Media Type Selection — 1.0.0 +// Covers normalized media-type selection for requests and responses. + +package models + +import "example.com/sdk/runtime" + +type SendParameterizedMultipartMultipartRequestBody struct { + File runtime.UploadFile + Note *string +} diff --git a/tests/golden/go/go-http/media-type-selection/runtime/upload_file.go.golden b/tests/golden/go/go-http/media-type-selection/runtime/upload_file.go.golden new file mode 100644 index 000000000..b548cc8de --- /dev/null +++ b/tests/golden/go/go-http/media-type-selection/runtime/upload_file.go.golden @@ -0,0 +1,26 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Media Type Selection — 1.0.0 +// Covers normalized media-type selection for requests and responses. + +package runtime + +type UploadFile struct { + Data []byte + Filename string +} + +func NewUploadFile(data []byte, filename string) UploadFile { + return UploadFile{Data: data, Filename: filename} +} + +func NewUploadFileBytes(data []byte) UploadFile { + return UploadFile{Data: data} +} + +func (f UploadFile) FilenameOrDefault(defaultName string) string { + if f.Filename != "" { + return f.Filename + } + return defaultName +} 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 a99b3a061..8ccc79d9a 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 @@ -11,6 +11,7 @@ import ( "encoding/json" "fmt" "io" + "mime" "mime/multipart" "net/http" "net/textproto" @@ -37,7 +38,8 @@ type SendOptionalPartsResponse struct { } // SendOptionalParts calls POST /multipart/optional. -func (a *MultipartAPI) SendOptionalParts(ctx context.Context, body *models.OptionalUpload) (*SendOptionalPartsResponse, error) { +func (a *MultipartAPI) SendOptionalParts(ctx context.Context, +body *models.SendOptionalPartsMultipartRequestBody) (*SendOptionalPartsResponse, error) { path := "/multipart/optional" var bodyReader io.Reader var multipartContentType string @@ -45,6 +47,7 @@ func (a *MultipartAPI) SendOptionalParts(ctx context.Context, body *models.Optio buf := &bytes.Buffer{} writer := multipart.NewWriter(buf) if body.Attributes != nil { + value := *body.Attributes { partHeader := textproto.MIMEHeader{} partHeader.Set("Content-Disposition", "form-data; name=\"attributes\"") @@ -53,7 +56,7 @@ func (a *MultipartAPI) SendOptionalParts(ctx context.Context, body *models.Optio if err != nil { return nil, fmt.Errorf("create multipart part: %w", err) } - partValue, err := json.Marshal(*body.Attributes) + partValue, err := json.Marshal(value) if err != nil { return nil, fmt.Errorf("marshal multipart field: %w", err) } @@ -63,6 +66,7 @@ func (a *MultipartAPI) SendOptionalParts(ctx context.Context, body *models.Optio } } if body.Enabled != nil { + value := *body.Enabled { partHeader := textproto.MIMEHeader{} partHeader.Set("Content-Disposition", "form-data; name=\"enabled\"") @@ -71,26 +75,29 @@ func (a *MultipartAPI) SendOptionalParts(ctx context.Context, body *models.Optio if err != nil { return nil, fmt.Errorf("create multipart part: %w", err) } - if _, err := io.WriteString(partWriter, strconv.FormatBool(*body.Enabled)); err != nil { + if _, err := io.WriteString(partWriter, strconv.FormatBool(value)); err != nil { return nil, fmt.Errorf("write multipart field: %w", err) } } } if body.File != nil { + value := *body.File { partHeader := textproto.MIMEHeader{} - partHeader.Set("Content-Disposition", "form-data; name=\"file\"; filename=\"file\"") + disposition := mime.FormatMediaType("form-data", map[string]string{"name": "file", "filename": value.FilenameOrDefault("file")}) + partHeader.Set("Content-Disposition", disposition) partHeader.Set("Content-Type", "application/octet-stream") 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 { + if _, err := partWriter.Write(value.Data); err != nil { return nil, fmt.Errorf("write multipart file: %w", err) } } } if body.RetryCount != nil { + value := *body.RetryCount { partHeader := textproto.MIMEHeader{} partHeader.Set("Content-Disposition", "form-data; name=\"retry_count\"") @@ -99,12 +106,13 @@ func (a *MultipartAPI) SendOptionalParts(ctx context.Context, body *models.Optio 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 { + if _, err := io.WriteString(partWriter, strconv.FormatInt(int64(value), 10)); err != nil { return nil, fmt.Errorf("write multipart field: %w", err) } } } if body.Title != nil { + value := *body.Title { partHeader := textproto.MIMEHeader{} partHeader.Set("Content-Disposition", "form-data; name=\"title\"") @@ -113,7 +121,7 @@ func (a *MultipartAPI) SendOptionalParts(ctx context.Context, body *models.Optio if err != nil { return nil, fmt.Errorf("create multipart part: %w", err) } - if _, err := io.WriteString(partWriter, *body.Title); err != nil { + if _, err := io.WriteString(partWriter, value); err != nil { return nil, fmt.Errorf("write multipart field: %w", err) } } @@ -153,7 +161,8 @@ type SendTextFieldsResponse struct { } // SendTextFields calls POST /multipart/text-only. -func (a *MultipartAPI) SendTextFields(ctx context.Context, body *models.TextFields) (*SendTextFieldsResponse, error) { +func (a *MultipartAPI) SendTextFields(ctx context.Context, +body *models.SendTextFieldsMultipartRequestBody) (*SendTextFieldsResponse, error) { path := "/multipart/text-only" var bodyReader io.Reader var multipartContentType string diff --git a/tests/golden/go/go-http/multipart-edge-cases/models/send_optional_parts_multipart_request_body.go.golden b/tests/golden/go/go-http/multipart-edge-cases/models/send_optional_parts_multipart_request_body.go.golden new file mode 100644 index 000000000..64fea74c9 --- /dev/null +++ b/tests/golden/go/go-http/multipart-edge-cases/models/send_optional_parts_multipart_request_body.go.golden @@ -0,0 +1,16 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Multipart Edge Cases — 1.0.0 +// Covers optional multipart bodies, optional parts, and text-only multipart fields. + +package models + +import "example.com/sdk/runtime" + +type SendOptionalPartsMultipartRequestBody struct { + Attributes *Attributes + Enabled *bool + File *runtime.UploadFile + RetryCount *int + Title *string +} diff --git a/tests/golden/go/go-http/multipart-edge-cases/models/send_text_fields_multipart_request_body.go.golden b/tests/golden/go/go-http/multipart-edge-cases/models/send_text_fields_multipart_request_body.go.golden new file mode 100644 index 000000000..18657b2d9 --- /dev/null +++ b/tests/golden/go/go-http/multipart-edge-cases/models/send_text_fields_multipart_request_body.go.golden @@ -0,0 +1,12 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Multipart Edge Cases — 1.0.0 +// Covers optional multipart bodies, optional parts, and text-only multipart fields. + +package models + +type SendTextFieldsMultipartRequestBody struct { + Enabled bool + Note string + RetryCount int +} diff --git a/tests/golden/go/go-http/multipart-edge-cases/runtime/upload_file.go.golden b/tests/golden/go/go-http/multipart-edge-cases/runtime/upload_file.go.golden new file mode 100644 index 000000000..e4ab49350 --- /dev/null +++ b/tests/golden/go/go-http/multipart-edge-cases/runtime/upload_file.go.golden @@ -0,0 +1,26 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Multipart Edge Cases — 1.0.0 +// Covers optional multipart bodies, optional parts, and text-only multipart fields. + +package runtime + +type UploadFile struct { + Data []byte + Filename string +} + +func NewUploadFile(data []byte, filename string) UploadFile { + return UploadFile{Data: data, Filename: filename} +} + +func NewUploadFileBytes(data []byte) UploadFile { + return UploadFile{Data: data} +} + +func (f UploadFile) FilenameOrDefault(defaultName string) string { + if f.Filename != "" { + return f.Filename + } + return defaultName +} 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 index 1838e17b3..3d01076d9 100644 --- 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 @@ -11,6 +11,7 @@ import ( "encoding/json" "fmt" "io" + "mime" "mime/multipart" "net/http" "net/textproto" @@ -36,7 +37,8 @@ type UploadEncodedAssetResponse struct { } // UploadEncodedAsset calls POST /uploads/encoded. -func (a *TransferAPI) UploadEncodedAsset(ctx context.Context, body *models.UploadEncodedAssetRequest) (*UploadEncodedAssetResponse, error) { +func (a *TransferAPI) UploadEncodedAsset(ctx context.Context, +body *models.UploadEncodedAssetMultipartRequestBody) (*UploadEncodedAssetResponse, error) { path := "/uploads/encoded" var bodyReader io.Reader var multipartContentType string @@ -61,13 +63,14 @@ func (a *TransferAPI) UploadEncodedAsset(ctx context.Context, body *models.Uploa } { partHeader := textproto.MIMEHeader{} - partHeader.Set("Content-Disposition", "form-data; name=\"file\"; filename=\"file\"") + disposition := mime.FormatMediaType("form-data", map[string]string{"name": "file", "filename": body.File.FilenameOrDefault("file")}) + partHeader.Set("Content-Disposition", disposition) 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 { + if _, err := partWriter.Write(body.File.Data); err != nil { return nil, fmt.Errorf("write multipart file: %w", err) } } diff --git a/tests/golden/go/go-http/multipart-explicit-encoding/models/upload_encoded_asset_multipart_request_body.go.golden b/tests/golden/go/go-http/multipart-explicit-encoding/models/upload_encoded_asset_multipart_request_body.go.golden new file mode 100644 index 000000000..4cb86378c --- /dev/null +++ b/tests/golden/go/go-http/multipart-explicit-encoding/models/upload_encoded_asset_multipart_request_body.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 + +import "example.com/sdk/runtime" + +type UploadEncodedAssetMultipartRequestBody struct { + Audit AuditAttributes + File runtime.UploadFile + Metadata UploadAttributes + Purpose string +} diff --git a/tests/golden/go/go-http/multipart-explicit-encoding/runtime/upload_file.go.golden b/tests/golden/go/go-http/multipart-explicit-encoding/runtime/upload_file.go.golden new file mode 100644 index 000000000..2e8abf304 --- /dev/null +++ b/tests/golden/go/go-http/multipart-explicit-encoding/runtime/upload_file.go.golden @@ -0,0 +1,26 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Multipart Explicit Encoding — 1.0.0 +// Covers explicit multipart part content types. + +package runtime + +type UploadFile struct { + Data []byte + Filename string +} + +func NewUploadFile(data []byte, filename string) UploadFile { + return UploadFile{Data: data, Filename: filename} +} + +func NewUploadFileBytes(data []byte) UploadFile { + return UploadFile{Data: data} +} + +func (f UploadFile) FilenameOrDefault(defaultName string) string { + if f.Filename != "" { + return f.Filename + } + return defaultName +} 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 89f0d065c..735453c41 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 @@ -11,6 +11,7 @@ import ( "encoding/json" "fmt" "io" + "mime" "mime/multipart" "net/http" "net/textproto" @@ -36,7 +37,8 @@ type SendNestedObjectPartResponse struct { } // SendNestedObjectPart calls POST /multipart/nested-object. -func (a *MultipartAPI) SendNestedObjectPart(ctx context.Context, body *models.NestedUpload) (*SendNestedObjectPartResponse, error) { +func (a *MultipartAPI) SendNestedObjectPart(ctx context.Context, +body *models.SendNestedObjectPartMultipartRequestBody) (*SendNestedObjectPartResponse, error) { path := "/multipart/nested-object" var bodyReader io.Reader var multipartContentType string @@ -45,13 +47,14 @@ func (a *MultipartAPI) SendNestedObjectPart(ctx context.Context, body *models.Ne writer := multipart.NewWriter(buf) { partHeader := textproto.MIMEHeader{} - partHeader.Set("Content-Disposition", "form-data; name=\"file\"; filename=\"file\"") + disposition := mime.FormatMediaType("form-data", map[string]string{"name": "file", "filename": body.File.FilenameOrDefault("file")}) + partHeader.Set("Content-Disposition", disposition) partHeader.Set("Content-Type", "application/octet-stream") 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 { + if _, err := partWriter.Write(body.File.Data); err != nil { return nil, fmt.Errorf("write multipart file: %w", err) } } diff --git a/tests/golden/go/go-http/multipart-nested-object-parts/models/send_nested_object_part_multipart_request_body.go.golden b/tests/golden/go/go-http/multipart-nested-object-parts/models/send_nested_object_part_multipart_request_body.go.golden new file mode 100644 index 000000000..1df856fc4 --- /dev/null +++ b/tests/golden/go/go-http/multipart-nested-object-parts/models/send_nested_object_part_multipart_request_body.go.golden @@ -0,0 +1,13 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Multipart Nested Object Parts — 1.0.0 +// Covers multipart object parts whose wire names differ from ergonomic names. + +package models + +import "example.com/sdk/runtime" + +type SendNestedObjectPartMultipartRequestBody struct { + File runtime.UploadFile + ItemConfig ItemConfig +} diff --git a/tests/golden/go/go-http/multipart-nested-object-parts/runtime/upload_file.go.golden b/tests/golden/go/go-http/multipart-nested-object-parts/runtime/upload_file.go.golden new file mode 100644 index 000000000..98b38065e --- /dev/null +++ b/tests/golden/go/go-http/multipart-nested-object-parts/runtime/upload_file.go.golden @@ -0,0 +1,26 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Multipart Nested Object Parts — 1.0.0 +// Covers multipart object parts whose wire names differ from ergonomic names. + +package runtime + +type UploadFile struct { + Data []byte + Filename string +} + +func NewUploadFile(data []byte, filename string) UploadFile { + return UploadFile{Data: data, Filename: filename} +} + +func NewUploadFileBytes(data []byte) UploadFile { + return UploadFile{Data: data} +} + +func (f UploadFile) FilenameOrDefault(defaultName string) string { + if f.Filename != "" { + return f.Filename + } + return defaultName +} 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 3b90a2451..cfad6f9c0 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 @@ -112,11 +112,11 @@ public class TransferApi { /** * uploadAsset POST /uploads. */ - public UploadAssetResponse uploadAsset(UploadAssetRequest body) throws IOException { + public UploadAssetResponse uploadAsset(UploadAssetMultipartRequestBody body) throws IOException { String path = "/uploads"; 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("file", body.getFile().filenameOrDefault("file"), RequestBody.create(body.getFile().getData(), MediaType.get("application/octet-stream"))); 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(); diff --git a/tests/golden/java/java-okhttp/binary-transfer-media-types/models/UploadAssetMultipartRequestBody.java.golden b/tests/golden/java/java-okhttp/binary-transfer-media-types/models/UploadAssetMultipartRequestBody.java.golden new file mode 100644 index 000000000..fcf67c7bb --- /dev/null +++ b/tests/golden/java/java-okhttp/binary-transfer-media-types/models/UploadAssetMultipartRequestBody.java.golden @@ -0,0 +1,33 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Binary Transfer Media Types — 1.0.0 +// Covers multipart upload and octet-stream download. + +package com.example.sdk.models; + +import com.example.sdk.runtime.UploadFile; + +public final class UploadAssetMultipartRequestBody { + private final UploadFile file; + private final UploadAttributes metadata; + private final String purpose; + + public UploadAssetMultipartRequestBody(UploadFile file, UploadAttributes metadata, String purpose) { + this.file = file; + this.metadata = metadata; + this.purpose = purpose; + } + + public UploadFile getFile() { + return file; + } + + public UploadAttributes getMetadata() { + return metadata; + } + + public String getPurpose() { + return purpose; + } + +} diff --git a/tests/golden/java/java-okhttp/binary-transfer-media-types/runtime/UploadFile.java.golden b/tests/golden/java/java-okhttp/binary-transfer-media-types/runtime/UploadFile.java.golden new file mode 100644 index 000000000..561539789 --- /dev/null +++ b/tests/golden/java/java-okhttp/binary-transfer-media-types/runtime/UploadFile.java.golden @@ -0,0 +1,36 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Binary Transfer Media Types — 1.0.0 +// Covers multipart upload and octet-stream download. + +package com.example.sdk.runtime; + +public final class UploadFile { + private final byte[] data; + private final String filename; + + public UploadFile(byte[] data, String filename) { + this.data = data; + this.filename = filename; + } + + public static UploadFile of(byte[] data, String filename) { + return new UploadFile(data, filename); + } + + public static UploadFile ofBytes(byte[] data) { + return new UploadFile(data, null); + } + + public byte[] getData() { + return data; + } + + public String getFilename() { + return filename; + } + + public String filenameOrDefault(String defaultName) { + return filename != null && !filename.isEmpty() ? filename : defaultName; + } +} 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 a6790b7b0..8f376ea03 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 @@ -199,11 +199,11 @@ public class MediaApi { /** * sendParameterizedMultipart POST /request/parameterized-multipart. */ - public SendParameterizedMultipartResponse sendParameterizedMultipart(FileEnvelope body) throws IOException { + public SendParameterizedMultipartResponse sendParameterizedMultipart(SendParameterizedMultipartMultipartRequestBody body) throws IOException { String path = "/request/parameterized-multipart"; 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("file", body.getFile().filenameOrDefault("file"), RequestBody.create(body.getFile().getData(), MediaType.get("application/octet-stream"))); if (body.getNote() != null) { multipartBuilder.addFormDataPart("note", null, RequestBody.create(String.valueOf(body.getNote()), MediaType.get("text/plain"))); } diff --git a/tests/golden/java/java-okhttp/media-type-selection/models/SendParameterizedMultipartMultipartRequestBody.java.golden b/tests/golden/java/java-okhttp/media-type-selection/models/SendParameterizedMultipartMultipartRequestBody.java.golden new file mode 100644 index 000000000..766e02b35 --- /dev/null +++ b/tests/golden/java/java-okhttp/media-type-selection/models/SendParameterizedMultipartMultipartRequestBody.java.golden @@ -0,0 +1,27 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Media Type Selection — 1.0.0 +// Covers normalized media-type selection for requests and responses. + +package com.example.sdk.models; + +import com.example.sdk.runtime.UploadFile; + +public final class SendParameterizedMultipartMultipartRequestBody { + private final UploadFile file; + private final String note; + + public SendParameterizedMultipartMultipartRequestBody(UploadFile file, String note) { + this.file = file; + this.note = note; + } + + public UploadFile getFile() { + return file; + } + + public String getNote() { + return note; + } + +} diff --git a/tests/golden/java/java-okhttp/media-type-selection/runtime/UploadFile.java.golden b/tests/golden/java/java-okhttp/media-type-selection/runtime/UploadFile.java.golden new file mode 100644 index 000000000..c44ee34f3 --- /dev/null +++ b/tests/golden/java/java-okhttp/media-type-selection/runtime/UploadFile.java.golden @@ -0,0 +1,36 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Media Type Selection — 1.0.0 +// Covers normalized media-type selection for requests and responses. + +package com.example.sdk.runtime; + +public final class UploadFile { + private final byte[] data; + private final String filename; + + public UploadFile(byte[] data, String filename) { + this.data = data; + this.filename = filename; + } + + public static UploadFile of(byte[] data, String filename) { + return new UploadFile(data, filename); + } + + public static UploadFile ofBytes(byte[] data) { + return new UploadFile(data, null); + } + + public byte[] getData() { + return data; + } + + public String getFilename() { + return filename; + } + + public String filenameOrDefault(String defaultName) { + return filename != null && !filename.isEmpty() ? filename : defaultName; + } +} 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 42c95e271..4bfe2c50d 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 @@ -79,7 +79,7 @@ public class MultipartApi { /** * sendOptionalParts POST /multipart/optional. */ - public SendOptionalPartsResponse sendOptionalParts(OptionalUpload body) throws IOException { + public SendOptionalPartsResponse sendOptionalParts(SendOptionalPartsMultipartRequestBody body) throws IOException { String path = "/multipart/optional"; Request request; RequestBody multipartBody = RequestBody.create(new byte[0], null); @@ -92,7 +92,7 @@ public class MultipartApi { 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"))); + multipartBuilder.addFormDataPart("file", body.getFile().filenameOrDefault("file"), RequestBody.create(body.getFile().getData(), MediaType.get("application/octet-stream"))); } if (body.getRetryCount() != null) { multipartBuilder.addFormDataPart("retry_count", null, RequestBody.create(String.valueOf(body.getRetryCount()), MediaType.get("text/plain"))); @@ -115,7 +115,7 @@ public class MultipartApi { /** * sendTextFields POST /multipart/text-only. */ - public SendTextFieldsResponse sendTextFields(TextFields body) throws IOException { + public SendTextFieldsResponse sendTextFields(SendTextFieldsMultipartRequestBody body) throws IOException { String path = "/multipart/text-only"; Request request; MultipartBody.Builder multipartBuilder = new MultipartBody.Builder().setType(MultipartBody.FORM); diff --git a/tests/golden/java/java-okhttp/multipart-edge-cases/models/SendOptionalPartsMultipartRequestBody.java.golden b/tests/golden/java/java-okhttp/multipart-edge-cases/models/SendOptionalPartsMultipartRequestBody.java.golden new file mode 100644 index 000000000..2da830a5b --- /dev/null +++ b/tests/golden/java/java-okhttp/multipart-edge-cases/models/SendOptionalPartsMultipartRequestBody.java.golden @@ -0,0 +1,45 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Multipart Edge Cases — 1.0.0 +// Covers optional multipart bodies, optional parts, and text-only multipart fields. + +package com.example.sdk.models; + +import com.example.sdk.runtime.UploadFile; + +public final class SendOptionalPartsMultipartRequestBody { + private final Attributes attributes; + private final Boolean enabled; + private final UploadFile file; + private final Integer retryCount; + private final String title; + + public SendOptionalPartsMultipartRequestBody(Attributes attributes, Boolean enabled, UploadFile file, Integer retryCount, String title) { + this.attributes = attributes; + this.enabled = enabled; + this.file = file; + this.retryCount = retryCount; + this.title = title; + } + + public Attributes getAttributes() { + return attributes; + } + + public Boolean getEnabled() { + return enabled; + } + + public UploadFile getFile() { + return file; + } + + public Integer getRetryCount() { + return retryCount; + } + + public String getTitle() { + return title; + } + +} diff --git a/tests/golden/java/java-okhttp/multipart-edge-cases/models/SendTextFieldsMultipartRequestBody.java.golden b/tests/golden/java/java-okhttp/multipart-edge-cases/models/SendTextFieldsMultipartRequestBody.java.golden new file mode 100644 index 000000000..efaec402b --- /dev/null +++ b/tests/golden/java/java-okhttp/multipart-edge-cases/models/SendTextFieldsMultipartRequestBody.java.golden @@ -0,0 +1,31 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Multipart Edge Cases — 1.0.0 +// Covers optional multipart bodies, optional parts, and text-only multipart fields. + +package com.example.sdk.models; + +public final class SendTextFieldsMultipartRequestBody { + private final Boolean enabled; + private final String note; + private final Integer retryCount; + + public SendTextFieldsMultipartRequestBody(Boolean enabled, String note, Integer retryCount) { + this.enabled = enabled; + this.note = note; + this.retryCount = retryCount; + } + + public Boolean getEnabled() { + return enabled; + } + + public String getNote() { + return note; + } + + public Integer getRetryCount() { + return retryCount; + } + +} diff --git a/tests/golden/java/java-okhttp/multipart-edge-cases/runtime/UploadFile.java.golden b/tests/golden/java/java-okhttp/multipart-edge-cases/runtime/UploadFile.java.golden new file mode 100644 index 000000000..57613d9bf --- /dev/null +++ b/tests/golden/java/java-okhttp/multipart-edge-cases/runtime/UploadFile.java.golden @@ -0,0 +1,36 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Multipart Edge Cases — 1.0.0 +// Covers optional multipart bodies, optional parts, and text-only multipart fields. + +package com.example.sdk.runtime; + +public final class UploadFile { + private final byte[] data; + private final String filename; + + public UploadFile(byte[] data, String filename) { + this.data = data; + this.filename = filename; + } + + public static UploadFile of(byte[] data, String filename) { + return new UploadFile(data, filename); + } + + public static UploadFile ofBytes(byte[] data) { + return new UploadFile(data, null); + } + + public byte[] getData() { + return data; + } + + public String getFilename() { + return filename; + } + + public String filenameOrDefault(String defaultName) { + return filename != null && !filename.isEmpty() ? filename : defaultName; + } +} 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 index 8a5837cdb..722e6e2ca 100644 --- 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 @@ -58,12 +58,12 @@ public class TransferApi { /** * uploadEncodedAsset POST /uploads/encoded. */ - public UploadEncodedAssetResponse uploadEncodedAsset(UploadEncodedAssetRequest body) throws IOException { + public UploadEncodedAssetResponse uploadEncodedAsset(UploadEncodedAssetMultipartRequestBody 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("file", body.getFile().filenameOrDefault("file"), RequestBody.create(body.getFile().getData(), 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(); diff --git a/tests/golden/java/java-okhttp/multipart-explicit-encoding/models/UploadEncodedAssetMultipartRequestBody.java.golden b/tests/golden/java/java-okhttp/multipart-explicit-encoding/models/UploadEncodedAssetMultipartRequestBody.java.golden new file mode 100644 index 000000000..d0b66a0b5 --- /dev/null +++ b/tests/golden/java/java-okhttp/multipart-explicit-encoding/models/UploadEncodedAssetMultipartRequestBody.java.golden @@ -0,0 +1,39 @@ +// 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.example.sdk.runtime.UploadFile; + +public final class UploadEncodedAssetMultipartRequestBody { + private final AuditAttributes audit; + private final UploadFile file; + private final UploadAttributes metadata; + private final String purpose; + + public UploadEncodedAssetMultipartRequestBody(AuditAttributes audit, UploadFile file, UploadAttributes metadata, String purpose) { + this.audit = audit; + this.file = file; + this.metadata = metadata; + this.purpose = purpose; + } + + public AuditAttributes getAudit() { + return audit; + } + + public UploadFile getFile() { + return file; + } + + public UploadAttributes getMetadata() { + return metadata; + } + + public String getPurpose() { + return purpose; + } + +} diff --git a/tests/golden/java/java-okhttp/multipart-explicit-encoding/runtime/UploadFile.java.golden b/tests/golden/java/java-okhttp/multipart-explicit-encoding/runtime/UploadFile.java.golden new file mode 100644 index 000000000..a65a466ee --- /dev/null +++ b/tests/golden/java/java-okhttp/multipart-explicit-encoding/runtime/UploadFile.java.golden @@ -0,0 +1,36 @@ +// 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 final class UploadFile { + private final byte[] data; + private final String filename; + + public UploadFile(byte[] data, String filename) { + this.data = data; + this.filename = filename; + } + + public static UploadFile of(byte[] data, String filename) { + return new UploadFile(data, filename); + } + + public static UploadFile ofBytes(byte[] data) { + return new UploadFile(data, null); + } + + public byte[] getData() { + return data; + } + + public String getFilename() { + return filename; + } + + public String filenameOrDefault(String defaultName) { + return filename != null && !filename.isEmpty() ? filename : defaultName; + } +} 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 02bae9ecb..4d55659e8 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 @@ -58,11 +58,11 @@ public class MultipartApi { /** * sendNestedObjectPart POST /multipart/nested-object. */ - public SendNestedObjectPartResponse sendNestedObjectPart(NestedUpload body) throws IOException { + public SendNestedObjectPartResponse sendNestedObjectPart(SendNestedObjectPartMultipartRequestBody body) throws IOException { String path = "/multipart/nested-object"; 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("file", body.getFile().filenameOrDefault("file"), RequestBody.create(body.getFile().getData(), MediaType.get("application/octet-stream"))); 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); diff --git a/tests/golden/java/java-okhttp/multipart-nested-object-parts/models/SendNestedObjectPartMultipartRequestBody.java.golden b/tests/golden/java/java-okhttp/multipart-nested-object-parts/models/SendNestedObjectPartMultipartRequestBody.java.golden new file mode 100644 index 000000000..3345a2084 --- /dev/null +++ b/tests/golden/java/java-okhttp/multipart-nested-object-parts/models/SendNestedObjectPartMultipartRequestBody.java.golden @@ -0,0 +1,27 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Multipart Nested Object Parts — 1.0.0 +// Covers multipart object parts whose wire names differ from ergonomic names. + +package com.example.sdk.models; + +import com.example.sdk.runtime.UploadFile; + +public final class SendNestedObjectPartMultipartRequestBody { + private final UploadFile file; + private final ItemConfig itemConfig; + + public SendNestedObjectPartMultipartRequestBody(UploadFile file, ItemConfig itemConfig) { + this.file = file; + this.itemConfig = itemConfig; + } + + public UploadFile getFile() { + return file; + } + + public ItemConfig getItemConfig() { + return itemConfig; + } + +} diff --git a/tests/golden/java/java-okhttp/multipart-nested-object-parts/runtime/UploadFile.java.golden b/tests/golden/java/java-okhttp/multipart-nested-object-parts/runtime/UploadFile.java.golden new file mode 100644 index 000000000..fd6615f7f --- /dev/null +++ b/tests/golden/java/java-okhttp/multipart-nested-object-parts/runtime/UploadFile.java.golden @@ -0,0 +1,36 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Multipart Nested Object Parts — 1.0.0 +// Covers multipart object parts whose wire names differ from ergonomic names. + +package com.example.sdk.runtime; + +public final class UploadFile { + private final byte[] data; + private final String filename; + + public UploadFile(byte[] data, String filename) { + this.data = data; + this.filename = filename; + } + + public static UploadFile of(byte[] data, String filename) { + return new UploadFile(data, filename); + } + + public static UploadFile ofBytes(byte[] data) { + return new UploadFile(data, null); + } + + public byte[] getData() { + return data; + } + + public String getFilename() { + return filename; + } + + public String filenameOrDefault(String defaultName) { + return filename != null && !filename.isEmpty() ? filename : defaultName; + } +} 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 089f9cb81..232217632 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 @@ -57,10 +57,10 @@ class TransferApi(private val client: ApiClient) { /** * uploadAsset POST /uploads. */ - fun uploadAsset(body: UploadAssetRequest): UploadAssetResponse { + fun uploadAsset(body: UploadAssetMultipartRequestBody): UploadAssetResponse { val path = "/uploads" val multipartBuilder = MultipartBody.Builder().setType(MultipartBody.FORM) - multipartBuilder.addFormDataPart("file", "file", body.file.toRequestBody("application/octet-stream".toMediaType())) + multipartBuilder.addFormDataPart("file", body.file.filenameOrDefault("file"), body.file.data.toRequestBody("application/octet-stream".toMediaType())) multipartBuilder.addFormDataPart("metadata", null, gson.toJson(body.metadata).toRequestBody("application/json".toMediaType())) diff --git a/tests/golden/kotlin/kotlin-okhttp/binary-transfer-media-types/models/UploadAssetMultipartRequestBody.kt.golden b/tests/golden/kotlin/kotlin-okhttp/binary-transfer-media-types/models/UploadAssetMultipartRequestBody.kt.golden new file mode 100644 index 000000000..01a32c828 --- /dev/null +++ b/tests/golden/kotlin/kotlin-okhttp/binary-transfer-media-types/models/UploadAssetMultipartRequestBody.kt.golden @@ -0,0 +1,14 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Binary Transfer Media Types — 1.0.0 +// Covers multipart upload and octet-stream download. + +package com.example.sdk.models + +import com.example.sdk.runtime.UploadFile + +data class UploadAssetMultipartRequestBody( + val file: UploadFile, + val metadata: UploadAttributes, + val purpose: String, +) diff --git a/tests/golden/kotlin/kotlin-okhttp/binary-transfer-media-types/runtime/UploadFile.kt.golden b/tests/golden/kotlin/kotlin-okhttp/binary-transfer-media-types/runtime/UploadFile.kt.golden new file mode 100644 index 000000000..9220b70d9 --- /dev/null +++ b/tests/golden/kotlin/kotlin-okhttp/binary-transfer-media-types/runtime/UploadFile.kt.golden @@ -0,0 +1,13 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Binary Transfer Media Types — 1.0.0 +// Covers multipart upload and octet-stream download. + +package com.example.sdk.runtime + +data class UploadFile( + val data: ByteArray, + val filename: String? = null, +) { + fun filenameOrDefault(defaultName: String): String = filename?.takeIf { it.isNotEmpty() } ?: defaultName +} 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 9b33fc60a..9f8acef5d 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 @@ -80,10 +80,10 @@ class MediaApi(private val client: ApiClient) { /** * sendParameterizedMultipart POST /request/parameterized-multipart. */ - fun sendParameterizedMultipart(body: FileEnvelope): SendParameterizedMultipartResponse { + fun sendParameterizedMultipart(body: SendParameterizedMultipartMultipartRequestBody): SendParameterizedMultipartResponse { val path = "/request/parameterized-multipart" val multipartBuilder = MultipartBody.Builder().setType(MultipartBody.FORM) - multipartBuilder.addFormDataPart("file", "file", body.file.toRequestBody("application/octet-stream".toMediaType())) + multipartBuilder.addFormDataPart("file", body.file.filenameOrDefault("file"), body.file.data.toRequestBody("application/octet-stream".toMediaType())) if (body.note != null) { multipartBuilder.addFormDataPart("note", null, body.note.toString().toRequestBody("text/plain".toMediaType())) diff --git a/tests/golden/kotlin/kotlin-okhttp/media-type-selection/models/SendParameterizedMultipartMultipartRequestBody.kt.golden b/tests/golden/kotlin/kotlin-okhttp/media-type-selection/models/SendParameterizedMultipartMultipartRequestBody.kt.golden new file mode 100644 index 000000000..c2f500ba0 --- /dev/null +++ b/tests/golden/kotlin/kotlin-okhttp/media-type-selection/models/SendParameterizedMultipartMultipartRequestBody.kt.golden @@ -0,0 +1,13 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Media Type Selection — 1.0.0 +// Covers normalized media-type selection for requests and responses. + +package com.example.sdk.models + +import com.example.sdk.runtime.UploadFile + +data class SendParameterizedMultipartMultipartRequestBody( + val file: UploadFile, + val note: String? = null, +) diff --git a/tests/golden/kotlin/kotlin-okhttp/media-type-selection/runtime/UploadFile.kt.golden b/tests/golden/kotlin/kotlin-okhttp/media-type-selection/runtime/UploadFile.kt.golden new file mode 100644 index 000000000..bf88df8bd --- /dev/null +++ b/tests/golden/kotlin/kotlin-okhttp/media-type-selection/runtime/UploadFile.kt.golden @@ -0,0 +1,13 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Media Type Selection — 1.0.0 +// Covers normalized media-type selection for requests and responses. + +package com.example.sdk.runtime + +data class UploadFile( + val data: ByteArray, + val filename: String? = null, +) { + fun filenameOrDefault(defaultName: String): String = filename?.takeIf { it.isNotEmpty() } ?: defaultName +} 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 c8ed5d22a..ff250429a 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 @@ -36,7 +36,7 @@ class MultipartApi(private val client: ApiClient) { /** * sendOptionalParts POST /multipart/optional. */ - fun sendOptionalParts(body: OptionalUpload?): SendOptionalPartsResponse { + fun sendOptionalParts(body: SendOptionalPartsMultipartRequestBody?): SendOptionalPartsResponse { val path = "/multipart/optional" var multipartBody = ByteArray(0).toRequestBody(null) if (body != null) { @@ -50,7 +50,7 @@ class MultipartApi(private val client: ApiClient) { } if (body.file != null) { - multipartBuilder.addFormDataPart("file", "file", body.file.toRequestBody("application/octet-stream".toMediaType())) + multipartBuilder.addFormDataPart("file", body.file.filenameOrDefault("file"), body.file.data.toRequestBody("application/octet-stream".toMediaType())) } if (body.retryCount != null) { @@ -76,7 +76,7 @@ class MultipartApi(private val client: ApiClient) { /** * sendTextFields POST /multipart/text-only. */ - fun sendTextFields(body: TextFields): SendTextFieldsResponse { + fun sendTextFields(body: SendTextFieldsMultipartRequestBody): SendTextFieldsResponse { val path = "/multipart/text-only" val multipartBuilder = MultipartBody.Builder().setType(MultipartBody.FORM) multipartBuilder.addFormDataPart("enabled", null, body.enabled.toString().toRequestBody("text/plain".toMediaType())) diff --git a/tests/golden/kotlin/kotlin-okhttp/multipart-edge-cases/models/SendOptionalPartsMultipartRequestBody.kt.golden b/tests/golden/kotlin/kotlin-okhttp/multipart-edge-cases/models/SendOptionalPartsMultipartRequestBody.kt.golden new file mode 100644 index 000000000..17f163aa7 --- /dev/null +++ b/tests/golden/kotlin/kotlin-okhttp/multipart-edge-cases/models/SendOptionalPartsMultipartRequestBody.kt.golden @@ -0,0 +1,16 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Multipart Edge Cases — 1.0.0 +// Covers optional multipart bodies, optional parts, and text-only multipart fields. + +package com.example.sdk.models + +import com.example.sdk.runtime.UploadFile + +data class SendOptionalPartsMultipartRequestBody( + val attributes: Attributes? = null, + val enabled: Boolean? = null, + val file: UploadFile? = null, + val retryCount: Int? = null, + val title: String? = null, +) diff --git a/tests/golden/kotlin/kotlin-okhttp/multipart-edge-cases/models/SendTextFieldsMultipartRequestBody.kt.golden b/tests/golden/kotlin/kotlin-okhttp/multipart-edge-cases/models/SendTextFieldsMultipartRequestBody.kt.golden new file mode 100644 index 000000000..2098cca3b --- /dev/null +++ b/tests/golden/kotlin/kotlin-okhttp/multipart-edge-cases/models/SendTextFieldsMultipartRequestBody.kt.golden @@ -0,0 +1,12 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Multipart Edge Cases — 1.0.0 +// Covers optional multipart bodies, optional parts, and text-only multipart fields. + +package com.example.sdk.models + +data class SendTextFieldsMultipartRequestBody( + val enabled: Boolean, + val note: String, + val retryCount: Int, +) diff --git a/tests/golden/kotlin/kotlin-okhttp/multipart-edge-cases/runtime/UploadFile.kt.golden b/tests/golden/kotlin/kotlin-okhttp/multipart-edge-cases/runtime/UploadFile.kt.golden new file mode 100644 index 000000000..7ea92de44 --- /dev/null +++ b/tests/golden/kotlin/kotlin-okhttp/multipart-edge-cases/runtime/UploadFile.kt.golden @@ -0,0 +1,13 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Multipart Edge Cases — 1.0.0 +// Covers optional multipart bodies, optional parts, and text-only multipart fields. + +package com.example.sdk.runtime + +data class UploadFile( + val data: ByteArray, + val filename: String? = null, +) { + fun filenameOrDefault(defaultName: String): String = filename?.takeIf { it.isNotEmpty() } ?: defaultName +} 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 index d78f87834..6e07f3d92 100644 --- 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 @@ -30,12 +30,12 @@ class TransferApi(private val client: ApiClient) { /** * uploadEncodedAsset POST /uploads/encoded. */ - fun uploadEncodedAsset(body: UploadEncodedAssetRequest): UploadEncodedAssetResponse { + fun uploadEncodedAsset(body: UploadEncodedAssetMultipartRequestBody): 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("file", body.file.filenameOrDefault("file"), body.file.data.toRequestBody("application/pdf".toMediaType())) multipartBuilder.addFormDataPart("metadata", null, gson.toJson(body.metadata).toRequestBody("application/vnd.openapi-nexus.metadata+json".toMediaType())) diff --git a/tests/golden/kotlin/kotlin-okhttp/multipart-explicit-encoding/models/UploadEncodedAssetMultipartRequestBody.kt.golden b/tests/golden/kotlin/kotlin-okhttp/multipart-explicit-encoding/models/UploadEncodedAssetMultipartRequestBody.kt.golden new file mode 100644 index 000000000..1a4355bcd --- /dev/null +++ b/tests/golden/kotlin/kotlin-okhttp/multipart-explicit-encoding/models/UploadEncodedAssetMultipartRequestBody.kt.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 com.example.sdk.models + +import com.example.sdk.runtime.UploadFile + +data class UploadEncodedAssetMultipartRequestBody( + val audit: AuditAttributes, + val file: UploadFile, + val metadata: UploadAttributes, + val purpose: String, +) diff --git a/tests/golden/kotlin/kotlin-okhttp/multipart-explicit-encoding/runtime/UploadFile.kt.golden b/tests/golden/kotlin/kotlin-okhttp/multipart-explicit-encoding/runtime/UploadFile.kt.golden new file mode 100644 index 000000000..d28c57370 --- /dev/null +++ b/tests/golden/kotlin/kotlin-okhttp/multipart-explicit-encoding/runtime/UploadFile.kt.golden @@ -0,0 +1,13 @@ +// 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 + +data class UploadFile( + val data: ByteArray, + val filename: String? = null, +) { + fun filenameOrDefault(defaultName: String): String = filename?.takeIf { it.isNotEmpty() } ?: defaultName +} 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 fff9ca5b6..b879bd619 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 @@ -30,10 +30,10 @@ class MultipartApi(private val client: ApiClient) { /** * sendNestedObjectPart POST /multipart/nested-object. */ - fun sendNestedObjectPart(body: NestedUpload): SendNestedObjectPartResponse { + fun sendNestedObjectPart(body: SendNestedObjectPartMultipartRequestBody): SendNestedObjectPartResponse { 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("file", body.file.filenameOrDefault("file"), body.file.data.toRequestBody("application/octet-stream".toMediaType())) multipartBuilder.addFormDataPart("item_config", null, gson.toJson(body.itemConfig).toRequestBody("application/json".toMediaType())) diff --git a/tests/golden/kotlin/kotlin-okhttp/multipart-nested-object-parts/models/SendNestedObjectPartMultipartRequestBody.kt.golden b/tests/golden/kotlin/kotlin-okhttp/multipart-nested-object-parts/models/SendNestedObjectPartMultipartRequestBody.kt.golden new file mode 100644 index 000000000..f99866d38 --- /dev/null +++ b/tests/golden/kotlin/kotlin-okhttp/multipart-nested-object-parts/models/SendNestedObjectPartMultipartRequestBody.kt.golden @@ -0,0 +1,13 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Multipart Nested Object Parts — 1.0.0 +// Covers multipart object parts whose wire names differ from ergonomic names. + +package com.example.sdk.models + +import com.example.sdk.runtime.UploadFile + +data class SendNestedObjectPartMultipartRequestBody( + val file: UploadFile, + val itemConfig: ItemConfig, +) diff --git a/tests/golden/kotlin/kotlin-okhttp/multipart-nested-object-parts/runtime/UploadFile.kt.golden b/tests/golden/kotlin/kotlin-okhttp/multipart-nested-object-parts/runtime/UploadFile.kt.golden new file mode 100644 index 000000000..738eee69e --- /dev/null +++ b/tests/golden/kotlin/kotlin-okhttp/multipart-nested-object-parts/runtime/UploadFile.kt.golden @@ -0,0 +1,13 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Multipart Nested Object Parts — 1.0.0 +// Covers multipart object parts whose wire names differ from ergonomic names. + +package com.example.sdk.runtime + +data class UploadFile( + val data: ByteArray, + val filename: String? = null, +) { + fun filenameOrDefault(defaultName: String): String = filename?.takeIf { it.isNotEmpty() } ?: defaultName +} diff --git a/tests/golden/python/python-httpx/binary-transfer-media-types/binary_transfer_media_types/__init__.py.golden b/tests/golden/python/python-httpx/binary-transfer-media-types/binary_transfer_media_types/__init__.py.golden index 831124ba5..78b22cff3 100644 --- a/tests/golden/python/python-httpx/binary-transfer-media-types/binary_transfer_media_types/__init__.py.golden +++ b/tests/golden/python/python-httpx/binary-transfer-media-types/binary_transfer_media_types/__init__.py.golden @@ -8,3 +8,4 @@ from .runtime import Authenticator as Authenticator from .runtime import BearerAuth as BearerAuth from .runtime import Client as Client from .runtime import ApiError as ApiError +from .runtime import UploadFile as UploadFile 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 0a39e3cac..ff301391a 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 @@ -7,7 +7,7 @@ from __future__ import annotations import json -from ..models.upload_asset_request import UploadAssetRequest +from ..models.upload_asset_multipart_request_body import UploadAssetMultipartRequestBody from ..models.upload_metadata import UploadMetadata from ..runtime.client import Client from ..runtime.errors import ApiError @@ -23,10 +23,10 @@ class TransferApi: raise ApiError(response.status_code, response.reason_phrase, response.content) return response.content - def upload_asset(self, *, body: UploadAssetRequest) -> UploadMetadata: + def upload_asset(self, *, body: UploadAssetMultipartRequestBody) -> UploadMetadata: path = "/uploads" files: dict[str, object] = {} - files["file"] = ("file", body.file, "application/octet-stream") + files["file"] = (body.file.filename_or_default("file"), body.file.data, "application/octet-stream") files["metadata"] = (None, json.dumps(body.metadata.to_dict()), "application/json") files["purpose"] = (None, str(body.purpose), "text/plain") response = self._client.request("POST", path, files=files if files else None) diff --git a/tests/golden/python/python-httpx/binary-transfer-media-types/binary_transfer_media_types/models/__init__.py.golden b/tests/golden/python/python-httpx/binary-transfer-media-types/binary_transfer_media_types/models/__init__.py.golden index f9db79fc3..2037658ba 100644 --- a/tests/golden/python/python-httpx/binary-transfer-media-types/binary_transfer_media_types/models/__init__.py.golden +++ b/tests/golden/python/python-httpx/binary-transfer-media-types/binary_transfer_media_types/models/__init__.py.golden @@ -3,6 +3,7 @@ # Binary Transfer Media Types — 1.0.0 # Covers multipart upload and octet-stream download. +from .upload_asset_multipart_request_body import UploadAssetMultipartRequestBody as UploadAssetMultipartRequestBody from .upload_asset_request import UploadAssetRequest as UploadAssetRequest from .upload_attributes import UploadAttributes as UploadAttributes from .upload_metadata import UploadMetadata as UploadMetadata diff --git a/tests/golden/python/python-httpx/binary-transfer-media-types/binary_transfer_media_types/models/upload_asset_multipart_request_body.py.golden b/tests/golden/python/python-httpx/binary-transfer-media-types/binary_transfer_media_types/models/upload_asset_multipart_request_body.py.golden new file mode 100644 index 000000000..7eb53bc9b --- /dev/null +++ b/tests/golden/python/python-httpx/binary-transfer-media-types/binary_transfer_media_types/models/upload_asset_multipart_request_body.py.golden @@ -0,0 +1,16 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Binary Transfer Media Types — 1.0.0 +# Covers multipart upload and octet-stream download. + +from __future__ import annotations + +from dataclasses import dataclass +from ..runtime import UploadFile +from .upload_attributes import UploadAttributes + +@dataclass +class UploadAssetMultipartRequestBody: + file: UploadFile + metadata: UploadAttributes + purpose: str diff --git a/tests/golden/python/python-httpx/binary-transfer-media-types/binary_transfer_media_types/runtime/__init__.py.golden b/tests/golden/python/python-httpx/binary-transfer-media-types/binary_transfer_media_types/runtime/__init__.py.golden index e86edb27b..d4db5869f 100644 --- a/tests/golden/python/python-httpx/binary-transfer-media-types/binary_transfer_media_types/runtime/__init__.py.golden +++ b/tests/golden/python/python-httpx/binary-transfer-media-types/binary_transfer_media_types/runtime/__init__.py.golden @@ -8,3 +8,4 @@ from .auth import Authenticator as Authenticator from .auth import BearerAuth as BearerAuth from .client import Client as Client from .errors import ApiError as ApiError +from .upload_file import UploadFile as UploadFile diff --git a/tests/golden/python/python-httpx/binary-transfer-media-types/binary_transfer_media_types/runtime/upload_file.py.golden b/tests/golden/python/python-httpx/binary-transfer-media-types/binary_transfer_media_types/runtime/upload_file.py.golden new file mode 100644 index 000000000..0442a5060 --- /dev/null +++ b/tests/golden/python/python-httpx/binary-transfer-media-types/binary_transfer_media_types/runtime/upload_file.py.golden @@ -0,0 +1,21 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Binary Transfer Media Types — 1.0.0 +# Covers multipart upload and octet-stream download. + +from __future__ import annotations + +from dataclasses import dataclass + + +@dataclass(frozen=True) +class UploadFile: + data: bytes + filename: str | None = None + + @classmethod + def from_bytes(cls, data: bytes, filename: str | None = None) -> UploadFile: + return cls(data=data, filename=filename) + + def filename_or_default(self, default_name: str) -> str: + return self.filename or default_name diff --git a/tests/golden/python/python-httpx/media-type-selection/media_type_selection/__init__.py.golden b/tests/golden/python/python-httpx/media-type-selection/media_type_selection/__init__.py.golden index 35a708cb1..5facd7420 100644 --- a/tests/golden/python/python-httpx/media-type-selection/media_type_selection/__init__.py.golden +++ b/tests/golden/python/python-httpx/media-type-selection/media_type_selection/__init__.py.golden @@ -8,3 +8,4 @@ from .runtime import Authenticator as Authenticator from .runtime import BearerAuth as BearerAuth from .runtime import Client as Client from .runtime import ApiError as ApiError +from .runtime import UploadFile as UploadFile 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 63ee3c0aa..aa62ce567 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 @@ -5,8 +5,8 @@ from __future__ import annotations -from ..models.file_envelope import FileEnvelope from ..models.payload import Payload +from ..models.send_parameterized_multipart_multipart_request_body import SendParameterizedMultipartMultipartRequestBody from ..runtime.client import Client from ..runtime.errors import ApiError @@ -23,10 +23,10 @@ class MediaApi: raise ApiError(response.status_code, response.reason_phrase, response.content) return None - def send_parameterized_multipart(self, *, body: FileEnvelope) -> None: + def send_parameterized_multipart(self, *, body: SendParameterizedMultipartMultipartRequestBody) -> None: path = "/request/parameterized-multipart" files: dict[str, object] = {} - files["file"] = ("file", body.file, "application/octet-stream") + files["file"] = (body.file.filename_or_default("file"), body.file.data, "application/octet-stream") if body.note is not None: files["note"] = (None, str(body.note), "text/plain") diff --git a/tests/golden/python/python-httpx/media-type-selection/media_type_selection/models/__init__.py.golden b/tests/golden/python/python-httpx/media-type-selection/media_type_selection/models/__init__.py.golden index b37a095eb..f196127c6 100644 --- a/tests/golden/python/python-httpx/media-type-selection/media_type_selection/models/__init__.py.golden +++ b/tests/golden/python/python-httpx/media-type-selection/media_type_selection/models/__init__.py.golden @@ -5,3 +5,4 @@ from .file_envelope import FileEnvelope as FileEnvelope from .payload import Payload as Payload +from .send_parameterized_multipart_multipart_request_body import SendParameterizedMultipartMultipartRequestBody as SendParameterizedMultipartMultipartRequestBody diff --git a/tests/golden/python/python-httpx/media-type-selection/media_type_selection/models/send_parameterized_multipart_multipart_request_body.py.golden b/tests/golden/python/python-httpx/media-type-selection/media_type_selection/models/send_parameterized_multipart_multipart_request_body.py.golden new file mode 100644 index 000000000..5cd19682b --- /dev/null +++ b/tests/golden/python/python-httpx/media-type-selection/media_type_selection/models/send_parameterized_multipart_multipart_request_body.py.golden @@ -0,0 +1,14 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Media Type Selection — 1.0.0 +# Covers normalized media-type selection for requests and responses. + +from __future__ import annotations + +from dataclasses import dataclass +from ..runtime import UploadFile + +@dataclass +class SendParameterizedMultipartMultipartRequestBody: + file: UploadFile + note: str | None = None diff --git a/tests/golden/python/python-httpx/media-type-selection/media_type_selection/runtime/__init__.py.golden b/tests/golden/python/python-httpx/media-type-selection/media_type_selection/runtime/__init__.py.golden index e4f6e6349..f9b731b69 100644 --- a/tests/golden/python/python-httpx/media-type-selection/media_type_selection/runtime/__init__.py.golden +++ b/tests/golden/python/python-httpx/media-type-selection/media_type_selection/runtime/__init__.py.golden @@ -8,3 +8,4 @@ from .auth import Authenticator as Authenticator from .auth import BearerAuth as BearerAuth from .client import Client as Client from .errors import ApiError as ApiError +from .upload_file import UploadFile as UploadFile diff --git a/tests/golden/python/python-httpx/media-type-selection/media_type_selection/runtime/upload_file.py.golden b/tests/golden/python/python-httpx/media-type-selection/media_type_selection/runtime/upload_file.py.golden new file mode 100644 index 000000000..d4260f100 --- /dev/null +++ b/tests/golden/python/python-httpx/media-type-selection/media_type_selection/runtime/upload_file.py.golden @@ -0,0 +1,21 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Media Type Selection — 1.0.0 +# Covers normalized media-type selection for requests and responses. + +from __future__ import annotations + +from dataclasses import dataclass + + +@dataclass(frozen=True) +class UploadFile: + data: bytes + filename: str | None = None + + @classmethod + def from_bytes(cls, data: bytes, filename: str | None = None) -> UploadFile: + return cls(data=data, filename=filename) + + def filename_or_default(self, default_name: str) -> str: + return self.filename or default_name diff --git a/tests/golden/python/python-httpx/multipart-edge-cases/multipart_edge_cases/__init__.py.golden b/tests/golden/python/python-httpx/multipart-edge-cases/multipart_edge_cases/__init__.py.golden index 7fb6a7a96..eb795ddd0 100644 --- a/tests/golden/python/python-httpx/multipart-edge-cases/multipart_edge_cases/__init__.py.golden +++ b/tests/golden/python/python-httpx/multipart-edge-cases/multipart_edge_cases/__init__.py.golden @@ -8,3 +8,4 @@ from .runtime import Authenticator as Authenticator from .runtime import BearerAuth as BearerAuth from .runtime import Client as Client from .runtime import ApiError as ApiError +from .runtime import UploadFile as UploadFile 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 6340a1311..7533a57bf 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 @@ -7,8 +7,8 @@ from __future__ import annotations import json -from ..models.optional_upload import OptionalUpload -from ..models.text_fields import TextFields +from ..models.send_optional_parts_multipart_request_body import SendOptionalPartsMultipartRequestBody +from ..models.send_text_fields_multipart_request_body import SendTextFieldsMultipartRequestBody from ..runtime.client import Client from ..runtime.errors import ApiError @@ -16,7 +16,7 @@ class MultipartApi: def __init__(self, client: Client) -> None: self._client = client - def send_optional_parts(self, *, body: OptionalUpload | None = None) -> None: + def send_optional_parts(self, *, body: SendOptionalPartsMultipartRequestBody | None = None) -> None: path = "/multipart/optional" files: dict[str, object] = {} if body is not None: @@ -27,7 +27,7 @@ class MultipartApi: files["enabled"] = (None, str(body.enabled), "text/plain") if body.file is not None: - files["file"] = ("file", body.file, "application/octet-stream") + files["file"] = (body.file.filename_or_default("file"), body.file.data, "application/octet-stream") if body.retry_count is not None: files["retry_count"] = (None, str(body.retry_count), "text/plain") @@ -41,7 +41,7 @@ class MultipartApi: raise ApiError(response.status_code, response.reason_phrase, response.content) return None - def send_text_fields(self, *, body: TextFields) -> None: + def send_text_fields(self, *, body: SendTextFieldsMultipartRequestBody) -> None: path = "/multipart/text-only" files: dict[str, object] = {} files["enabled"] = (None, str(body.enabled), "text/plain") diff --git a/tests/golden/python/python-httpx/multipart-edge-cases/multipart_edge_cases/models/__init__.py.golden b/tests/golden/python/python-httpx/multipart-edge-cases/multipart_edge_cases/models/__init__.py.golden index 71bfeea0a..cc307d188 100644 --- a/tests/golden/python/python-httpx/multipart-edge-cases/multipart_edge_cases/models/__init__.py.golden +++ b/tests/golden/python/python-httpx/multipart-edge-cases/multipart_edge_cases/models/__init__.py.golden @@ -5,4 +5,6 @@ from .attributes import Attributes as Attributes from .optional_upload import OptionalUpload as OptionalUpload +from .send_optional_parts_multipart_request_body import SendOptionalPartsMultipartRequestBody as SendOptionalPartsMultipartRequestBody +from .send_text_fields_multipart_request_body import SendTextFieldsMultipartRequestBody as SendTextFieldsMultipartRequestBody from .text_fields import TextFields as TextFields diff --git a/tests/golden/python/python-httpx/multipart-edge-cases/multipart_edge_cases/models/send_optional_parts_multipart_request_body.py.golden b/tests/golden/python/python-httpx/multipart-edge-cases/multipart_edge_cases/models/send_optional_parts_multipart_request_body.py.golden new file mode 100644 index 000000000..b65eb4410 --- /dev/null +++ b/tests/golden/python/python-httpx/multipart-edge-cases/multipart_edge_cases/models/send_optional_parts_multipart_request_body.py.golden @@ -0,0 +1,18 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Multipart Edge Cases — 1.0.0 +# Covers optional multipart bodies, optional parts, and text-only multipart fields. + +from __future__ import annotations + +from dataclasses import dataclass +from ..runtime import UploadFile +from .attributes import Attributes + +@dataclass +class SendOptionalPartsMultipartRequestBody: + attributes: Attributes | None = None + enabled: bool | None = None + file: UploadFile | None = None + retry_count: int | None = None + title: str | None = None diff --git a/tests/golden/python/python-httpx/multipart-edge-cases/multipart_edge_cases/models/send_text_fields_multipart_request_body.py.golden b/tests/golden/python/python-httpx/multipart-edge-cases/multipart_edge_cases/models/send_text_fields_multipart_request_body.py.golden new file mode 100644 index 000000000..088b5347a --- /dev/null +++ b/tests/golden/python/python-httpx/multipart-edge-cases/multipart_edge_cases/models/send_text_fields_multipart_request_body.py.golden @@ -0,0 +1,14 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Multipart Edge Cases — 1.0.0 +# Covers optional multipart bodies, optional parts, and text-only multipart fields. + +from __future__ import annotations + +from dataclasses import dataclass + +@dataclass +class SendTextFieldsMultipartRequestBody: + enabled: bool + note: str + retry_count: int diff --git a/tests/golden/python/python-httpx/multipart-edge-cases/multipart_edge_cases/runtime/__init__.py.golden b/tests/golden/python/python-httpx/multipart-edge-cases/multipart_edge_cases/runtime/__init__.py.golden index 7226802fc..d25d1b65e 100644 --- a/tests/golden/python/python-httpx/multipart-edge-cases/multipart_edge_cases/runtime/__init__.py.golden +++ b/tests/golden/python/python-httpx/multipart-edge-cases/multipart_edge_cases/runtime/__init__.py.golden @@ -8,3 +8,4 @@ from .auth import Authenticator as Authenticator from .auth import BearerAuth as BearerAuth from .client import Client as Client from .errors import ApiError as ApiError +from .upload_file import UploadFile as UploadFile diff --git a/tests/golden/python/python-httpx/multipart-edge-cases/multipart_edge_cases/runtime/upload_file.py.golden b/tests/golden/python/python-httpx/multipart-edge-cases/multipart_edge_cases/runtime/upload_file.py.golden new file mode 100644 index 000000000..cd5c1f6f0 --- /dev/null +++ b/tests/golden/python/python-httpx/multipart-edge-cases/multipart_edge_cases/runtime/upload_file.py.golden @@ -0,0 +1,21 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Multipart Edge Cases — 1.0.0 +# Covers optional multipart bodies, optional parts, and text-only multipart fields. + +from __future__ import annotations + +from dataclasses import dataclass + + +@dataclass(frozen=True) +class UploadFile: + data: bytes + filename: str | None = None + + @classmethod + def from_bytes(cls, data: bytes, filename: str | None = None) -> UploadFile: + return cls(data=data, filename=filename) + + def filename_or_default(self, default_name: str) -> str: + return self.filename or default_name 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 index 94b1350f1..4acc621cd 100644 --- 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 @@ -8,3 +8,4 @@ from .runtime import Authenticator as Authenticator from .runtime import BearerAuth as BearerAuth from .runtime import Client as Client from .runtime import ApiError as ApiError +from .runtime import UploadFile as UploadFile 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 index 476fe25d5..e31445387 100644 --- 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 @@ -7,7 +7,7 @@ from __future__ import annotations import json -from ..models.upload_encoded_asset_request import UploadEncodedAssetRequest +from ..models.upload_encoded_asset_multipart_request_body import UploadEncodedAssetMultipartRequestBody from ..runtime.client import Client from ..runtime.errors import ApiError @@ -15,11 +15,11 @@ class TransferApi: def __init__(self, client: Client) -> None: self._client = client - def upload_encoded_asset(self, *, body: UploadEncodedAssetRequest) -> None: + def upload_encoded_asset(self, *, body: UploadEncodedAssetMultipartRequestBody) -> 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["file"] = (body.file.filename_or_default("file"), body.file.data, "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) 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 index 81e7b8856..6d8418805 100644 --- 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 @@ -5,4 +5,5 @@ from .audit_attributes import AuditAttributes as AuditAttributes from .upload_attributes import UploadAttributes as UploadAttributes +from .upload_encoded_asset_multipart_request_body import UploadEncodedAssetMultipartRequestBody as UploadEncodedAssetMultipartRequestBody from .upload_encoded_asset_request import UploadEncodedAssetRequest as UploadEncodedAssetRequest diff --git a/tests/golden/python/python-httpx/multipart-explicit-encoding/multipart_explicit_encoding/models/upload_encoded_asset_multipart_request_body.py.golden b/tests/golden/python/python-httpx/multipart-explicit-encoding/multipart_explicit_encoding/models/upload_encoded_asset_multipart_request_body.py.golden new file mode 100644 index 000000000..be589df82 --- /dev/null +++ b/tests/golden/python/python-httpx/multipart-explicit-encoding/multipart_explicit_encoding/models/upload_encoded_asset_multipart_request_body.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. + +from __future__ import annotations + +from dataclasses import dataclass +from ..runtime import UploadFile +from .audit_attributes import AuditAttributes +from .upload_attributes import UploadAttributes + +@dataclass +class UploadEncodedAssetMultipartRequestBody: + audit: AuditAttributes + file: UploadFile + metadata: UploadAttributes + purpose: str 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 index 579bfa8db..0bf0ce984 100644 --- 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 @@ -8,3 +8,4 @@ from .auth import Authenticator as Authenticator from .auth import BearerAuth as BearerAuth from .client import Client as Client from .errors import ApiError as ApiError +from .upload_file import UploadFile as UploadFile diff --git a/tests/golden/python/python-httpx/multipart-explicit-encoding/multipart_explicit_encoding/runtime/upload_file.py.golden b/tests/golden/python/python-httpx/multipart-explicit-encoding/multipart_explicit_encoding/runtime/upload_file.py.golden new file mode 100644 index 000000000..b758ec358 --- /dev/null +++ b/tests/golden/python/python-httpx/multipart-explicit-encoding/multipart_explicit_encoding/runtime/upload_file.py.golden @@ -0,0 +1,21 @@ +# 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(frozen=True) +class UploadFile: + data: bytes + filename: str | None = None + + @classmethod + def from_bytes(cls, data: bytes, filename: str | None = None) -> UploadFile: + return cls(data=data, filename=filename) + + def filename_or_default(self, default_name: str) -> str: + return self.filename or default_name diff --git a/tests/golden/python/python-httpx/multipart-nested-object-parts/multipart_nested_object_parts/__init__.py.golden b/tests/golden/python/python-httpx/multipart-nested-object-parts/multipart_nested_object_parts/__init__.py.golden index 054b30587..9d52a0512 100644 --- a/tests/golden/python/python-httpx/multipart-nested-object-parts/multipart_nested_object_parts/__init__.py.golden +++ b/tests/golden/python/python-httpx/multipart-nested-object-parts/multipart_nested_object_parts/__init__.py.golden @@ -8,3 +8,4 @@ from .runtime import Authenticator as Authenticator from .runtime import BearerAuth as BearerAuth from .runtime import Client as Client from .runtime import ApiError as ApiError +from .runtime import UploadFile as UploadFile diff --git a/tests/golden/python/python-httpx/multipart-nested-object-parts/multipart_nested_object_parts/apis/multipart_api.py.golden b/tests/golden/python/python-httpx/multipart-nested-object-parts/multipart_nested_object_parts/apis/multipart_api.py.golden index 9f001468b..d29a571f6 100644 --- a/tests/golden/python/python-httpx/multipart-nested-object-parts/multipart_nested_object_parts/apis/multipart_api.py.golden +++ b/tests/golden/python/python-httpx/multipart-nested-object-parts/multipart_nested_object_parts/apis/multipart_api.py.golden @@ -7,7 +7,7 @@ from __future__ import annotations import json -from ..models.nested_upload import NestedUpload +from ..models.send_nested_object_part_multipart_request_body import SendNestedObjectPartMultipartRequestBody from ..runtime.client import Client from ..runtime.errors import ApiError @@ -15,10 +15,10 @@ class MultipartApi: def __init__(self, client: Client) -> None: self._client = client - def send_nested_object_part(self, *, body: NestedUpload) -> None: + def send_nested_object_part(self, *, body: SendNestedObjectPartMultipartRequestBody) -> None: path = "/multipart/nested-object" files: dict[str, object] = {} - files["file"] = ("file", body.file, "application/octet-stream") + files["file"] = (body.file.filename_or_default("file"), body.file.data, "application/octet-stream") files["item_config"] = (None, json.dumps(body.item_config.to_dict()), "application/json") 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-nested-object-parts/multipart_nested_object_parts/models/__init__.py.golden b/tests/golden/python/python-httpx/multipart-nested-object-parts/multipart_nested_object_parts/models/__init__.py.golden index 2d68dd31f..d862fb169 100644 --- a/tests/golden/python/python-httpx/multipart-nested-object-parts/multipart_nested_object_parts/models/__init__.py.golden +++ b/tests/golden/python/python-httpx/multipart-nested-object-parts/multipart_nested_object_parts/models/__init__.py.golden @@ -5,3 +5,4 @@ from .item_config import ItemConfig as ItemConfig from .nested_upload import NestedUpload as NestedUpload +from .send_nested_object_part_multipart_request_body import SendNestedObjectPartMultipartRequestBody as SendNestedObjectPartMultipartRequestBody diff --git a/tests/golden/python/python-httpx/multipart-nested-object-parts/multipart_nested_object_parts/models/send_nested_object_part_multipart_request_body.py.golden b/tests/golden/python/python-httpx/multipart-nested-object-parts/multipart_nested_object_parts/models/send_nested_object_part_multipart_request_body.py.golden new file mode 100644 index 000000000..633109766 --- /dev/null +++ b/tests/golden/python/python-httpx/multipart-nested-object-parts/multipart_nested_object_parts/models/send_nested_object_part_multipart_request_body.py.golden @@ -0,0 +1,15 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Multipart Nested Object Parts — 1.0.0 +# Covers multipart object parts whose wire names differ from ergonomic names. + +from __future__ import annotations + +from dataclasses import dataclass +from ..runtime import UploadFile +from .item_config import ItemConfig + +@dataclass +class SendNestedObjectPartMultipartRequestBody: + file: UploadFile + item_config: ItemConfig diff --git a/tests/golden/python/python-httpx/multipart-nested-object-parts/multipart_nested_object_parts/runtime/__init__.py.golden b/tests/golden/python/python-httpx/multipart-nested-object-parts/multipart_nested_object_parts/runtime/__init__.py.golden index 977ff9b16..ace71435a 100644 --- a/tests/golden/python/python-httpx/multipart-nested-object-parts/multipart_nested_object_parts/runtime/__init__.py.golden +++ b/tests/golden/python/python-httpx/multipart-nested-object-parts/multipart_nested_object_parts/runtime/__init__.py.golden @@ -8,3 +8,4 @@ from .auth import Authenticator as Authenticator from .auth import BearerAuth as BearerAuth from .client import Client as Client from .errors import ApiError as ApiError +from .upload_file import UploadFile as UploadFile diff --git a/tests/golden/python/python-httpx/multipart-nested-object-parts/multipart_nested_object_parts/runtime/upload_file.py.golden b/tests/golden/python/python-httpx/multipart-nested-object-parts/multipart_nested_object_parts/runtime/upload_file.py.golden new file mode 100644 index 000000000..15322f524 --- /dev/null +++ b/tests/golden/python/python-httpx/multipart-nested-object-parts/multipart_nested_object_parts/runtime/upload_file.py.golden @@ -0,0 +1,21 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Multipart Nested Object Parts — 1.0.0 +# Covers multipart object parts whose wire names differ from ergonomic names. + +from __future__ import annotations + +from dataclasses import dataclass + + +@dataclass(frozen=True) +class UploadFile: + data: bytes + filename: str | None = None + + @classmethod + def from_bytes(cls, data: bytes, filename: str | None = None) -> UploadFile: + return cls(data=data, filename=filename) + + def filename_or_default(self, default_name: str) -> str: + return self.filename or default_name diff --git a/tests/golden/python/python-requests/binary-transfer-media-types/binary_transfer_media_types/__init__.py.golden b/tests/golden/python/python-requests/binary-transfer-media-types/binary_transfer_media_types/__init__.py.golden index 831124ba5..78b22cff3 100644 --- a/tests/golden/python/python-requests/binary-transfer-media-types/binary_transfer_media_types/__init__.py.golden +++ b/tests/golden/python/python-requests/binary-transfer-media-types/binary_transfer_media_types/__init__.py.golden @@ -8,3 +8,4 @@ from .runtime import Authenticator as Authenticator from .runtime import BearerAuth as BearerAuth from .runtime import Client as Client from .runtime import ApiError as ApiError +from .runtime import UploadFile as UploadFile 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 6e39f0757..8159e658d 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 @@ -7,7 +7,7 @@ from __future__ import annotations import json -from ..models.upload_asset_request import UploadAssetRequest +from ..models.upload_asset_multipart_request_body import UploadAssetMultipartRequestBody from ..models.upload_metadata import UploadMetadata from ..runtime.client import Client from ..runtime.errors import ApiError @@ -23,10 +23,10 @@ class TransferApi: raise ApiError(response.status_code, response.reason, response.content) return response.content - def upload_asset(self, *, body: UploadAssetRequest) -> UploadMetadata: + def upload_asset(self, *, body: UploadAssetMultipartRequestBody) -> UploadMetadata: path = "/uploads" files: dict[str, object] = {} - files["file"] = ("file", body.file, "application/octet-stream") + files["file"] = (body.file.filename_or_default("file"), body.file.data, "application/octet-stream") files["metadata"] = (None, json.dumps(body.metadata.to_dict()), "application/json") files["purpose"] = (None, str(body.purpose), "text/plain") response = self._client.request("POST", path, files=files if files else None) diff --git a/tests/golden/python/python-requests/binary-transfer-media-types/binary_transfer_media_types/models/__init__.py.golden b/tests/golden/python/python-requests/binary-transfer-media-types/binary_transfer_media_types/models/__init__.py.golden index f9db79fc3..2037658ba 100644 --- a/tests/golden/python/python-requests/binary-transfer-media-types/binary_transfer_media_types/models/__init__.py.golden +++ b/tests/golden/python/python-requests/binary-transfer-media-types/binary_transfer_media_types/models/__init__.py.golden @@ -3,6 +3,7 @@ # Binary Transfer Media Types — 1.0.0 # Covers multipart upload and octet-stream download. +from .upload_asset_multipart_request_body import UploadAssetMultipartRequestBody as UploadAssetMultipartRequestBody from .upload_asset_request import UploadAssetRequest as UploadAssetRequest from .upload_attributes import UploadAttributes as UploadAttributes from .upload_metadata import UploadMetadata as UploadMetadata diff --git a/tests/golden/python/python-requests/binary-transfer-media-types/binary_transfer_media_types/models/upload_asset_multipart_request_body.py.golden b/tests/golden/python/python-requests/binary-transfer-media-types/binary_transfer_media_types/models/upload_asset_multipart_request_body.py.golden new file mode 100644 index 000000000..7eb53bc9b --- /dev/null +++ b/tests/golden/python/python-requests/binary-transfer-media-types/binary_transfer_media_types/models/upload_asset_multipart_request_body.py.golden @@ -0,0 +1,16 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Binary Transfer Media Types — 1.0.0 +# Covers multipart upload and octet-stream download. + +from __future__ import annotations + +from dataclasses import dataclass +from ..runtime import UploadFile +from .upload_attributes import UploadAttributes + +@dataclass +class UploadAssetMultipartRequestBody: + file: UploadFile + metadata: UploadAttributes + purpose: str diff --git a/tests/golden/python/python-requests/binary-transfer-media-types/binary_transfer_media_types/runtime/__init__.py.golden b/tests/golden/python/python-requests/binary-transfer-media-types/binary_transfer_media_types/runtime/__init__.py.golden index e86edb27b..d4db5869f 100644 --- a/tests/golden/python/python-requests/binary-transfer-media-types/binary_transfer_media_types/runtime/__init__.py.golden +++ b/tests/golden/python/python-requests/binary-transfer-media-types/binary_transfer_media_types/runtime/__init__.py.golden @@ -8,3 +8,4 @@ from .auth import Authenticator as Authenticator from .auth import BearerAuth as BearerAuth from .client import Client as Client from .errors import ApiError as ApiError +from .upload_file import UploadFile as UploadFile diff --git a/tests/golden/python/python-requests/binary-transfer-media-types/binary_transfer_media_types/runtime/upload_file.py.golden b/tests/golden/python/python-requests/binary-transfer-media-types/binary_transfer_media_types/runtime/upload_file.py.golden new file mode 100644 index 000000000..0442a5060 --- /dev/null +++ b/tests/golden/python/python-requests/binary-transfer-media-types/binary_transfer_media_types/runtime/upload_file.py.golden @@ -0,0 +1,21 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Binary Transfer Media Types — 1.0.0 +# Covers multipart upload and octet-stream download. + +from __future__ import annotations + +from dataclasses import dataclass + + +@dataclass(frozen=True) +class UploadFile: + data: bytes + filename: str | None = None + + @classmethod + def from_bytes(cls, data: bytes, filename: str | None = None) -> UploadFile: + return cls(data=data, filename=filename) + + def filename_or_default(self, default_name: str) -> str: + return self.filename or default_name diff --git a/tests/golden/python/python-requests/media-type-selection/media_type_selection/__init__.py.golden b/tests/golden/python/python-requests/media-type-selection/media_type_selection/__init__.py.golden index 35a708cb1..5facd7420 100644 --- a/tests/golden/python/python-requests/media-type-selection/media_type_selection/__init__.py.golden +++ b/tests/golden/python/python-requests/media-type-selection/media_type_selection/__init__.py.golden @@ -8,3 +8,4 @@ from .runtime import Authenticator as Authenticator from .runtime import BearerAuth as BearerAuth from .runtime import Client as Client from .runtime import ApiError as ApiError +from .runtime import UploadFile as UploadFile 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 e6f2ed0fc..731fe1e7a 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 @@ -5,8 +5,8 @@ from __future__ import annotations -from ..models.file_envelope import FileEnvelope from ..models.payload import Payload +from ..models.send_parameterized_multipart_multipart_request_body import SendParameterizedMultipartMultipartRequestBody from ..runtime.client import Client from ..runtime.errors import ApiError @@ -23,10 +23,10 @@ class MediaApi: raise ApiError(response.status_code, response.reason, response.content) return None - def send_parameterized_multipart(self, *, body: FileEnvelope) -> None: + def send_parameterized_multipart(self, *, body: SendParameterizedMultipartMultipartRequestBody) -> None: path = "/request/parameterized-multipart" files: dict[str, object] = {} - files["file"] = ("file", body.file, "application/octet-stream") + files["file"] = (body.file.filename_or_default("file"), body.file.data, "application/octet-stream") if body.note is not None: files["note"] = (None, str(body.note), "text/plain") diff --git a/tests/golden/python/python-requests/media-type-selection/media_type_selection/models/__init__.py.golden b/tests/golden/python/python-requests/media-type-selection/media_type_selection/models/__init__.py.golden index b37a095eb..f196127c6 100644 --- a/tests/golden/python/python-requests/media-type-selection/media_type_selection/models/__init__.py.golden +++ b/tests/golden/python/python-requests/media-type-selection/media_type_selection/models/__init__.py.golden @@ -5,3 +5,4 @@ from .file_envelope import FileEnvelope as FileEnvelope from .payload import Payload as Payload +from .send_parameterized_multipart_multipart_request_body import SendParameterizedMultipartMultipartRequestBody as SendParameterizedMultipartMultipartRequestBody diff --git a/tests/golden/python/python-requests/media-type-selection/media_type_selection/models/send_parameterized_multipart_multipart_request_body.py.golden b/tests/golden/python/python-requests/media-type-selection/media_type_selection/models/send_parameterized_multipart_multipart_request_body.py.golden new file mode 100644 index 000000000..5cd19682b --- /dev/null +++ b/tests/golden/python/python-requests/media-type-selection/media_type_selection/models/send_parameterized_multipart_multipart_request_body.py.golden @@ -0,0 +1,14 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Media Type Selection — 1.0.0 +# Covers normalized media-type selection for requests and responses. + +from __future__ import annotations + +from dataclasses import dataclass +from ..runtime import UploadFile + +@dataclass +class SendParameterizedMultipartMultipartRequestBody: + file: UploadFile + note: str | None = None diff --git a/tests/golden/python/python-requests/media-type-selection/media_type_selection/runtime/__init__.py.golden b/tests/golden/python/python-requests/media-type-selection/media_type_selection/runtime/__init__.py.golden index e4f6e6349..f9b731b69 100644 --- a/tests/golden/python/python-requests/media-type-selection/media_type_selection/runtime/__init__.py.golden +++ b/tests/golden/python/python-requests/media-type-selection/media_type_selection/runtime/__init__.py.golden @@ -8,3 +8,4 @@ from .auth import Authenticator as Authenticator from .auth import BearerAuth as BearerAuth from .client import Client as Client from .errors import ApiError as ApiError +from .upload_file import UploadFile as UploadFile diff --git a/tests/golden/python/python-requests/media-type-selection/media_type_selection/runtime/upload_file.py.golden b/tests/golden/python/python-requests/media-type-selection/media_type_selection/runtime/upload_file.py.golden new file mode 100644 index 000000000..d4260f100 --- /dev/null +++ b/tests/golden/python/python-requests/media-type-selection/media_type_selection/runtime/upload_file.py.golden @@ -0,0 +1,21 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Media Type Selection — 1.0.0 +# Covers normalized media-type selection for requests and responses. + +from __future__ import annotations + +from dataclasses import dataclass + + +@dataclass(frozen=True) +class UploadFile: + data: bytes + filename: str | None = None + + @classmethod + def from_bytes(cls, data: bytes, filename: str | None = None) -> UploadFile: + return cls(data=data, filename=filename) + + def filename_or_default(self, default_name: str) -> str: + return self.filename or default_name diff --git a/tests/golden/python/python-requests/multipart-edge-cases/multipart_edge_cases/__init__.py.golden b/tests/golden/python/python-requests/multipart-edge-cases/multipart_edge_cases/__init__.py.golden index 7fb6a7a96..eb795ddd0 100644 --- a/tests/golden/python/python-requests/multipart-edge-cases/multipart_edge_cases/__init__.py.golden +++ b/tests/golden/python/python-requests/multipart-edge-cases/multipart_edge_cases/__init__.py.golden @@ -8,3 +8,4 @@ from .runtime import Authenticator as Authenticator from .runtime import BearerAuth as BearerAuth from .runtime import Client as Client from .runtime import ApiError as ApiError +from .runtime import UploadFile as UploadFile 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 5ad3222da..5467af461 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 @@ -7,8 +7,8 @@ from __future__ import annotations import json -from ..models.optional_upload import OptionalUpload -from ..models.text_fields import TextFields +from ..models.send_optional_parts_multipart_request_body import SendOptionalPartsMultipartRequestBody +from ..models.send_text_fields_multipart_request_body import SendTextFieldsMultipartRequestBody from ..runtime.client import Client from ..runtime.errors import ApiError @@ -16,7 +16,7 @@ class MultipartApi: def __init__(self, client: Client) -> None: self._client = client - def send_optional_parts(self, *, body: OptionalUpload | None = None) -> None: + def send_optional_parts(self, *, body: SendOptionalPartsMultipartRequestBody | None = None) -> None: path = "/multipart/optional" files: dict[str, object] = {} if body is not None: @@ -27,7 +27,7 @@ class MultipartApi: files["enabled"] = (None, str(body.enabled), "text/plain") if body.file is not None: - files["file"] = ("file", body.file, "application/octet-stream") + files["file"] = (body.file.filename_or_default("file"), body.file.data, "application/octet-stream") if body.retry_count is not None: files["retry_count"] = (None, str(body.retry_count), "text/plain") @@ -41,7 +41,7 @@ class MultipartApi: raise ApiError(response.status_code, response.reason, response.content) return None - def send_text_fields(self, *, body: TextFields) -> None: + def send_text_fields(self, *, body: SendTextFieldsMultipartRequestBody) -> None: path = "/multipart/text-only" files: dict[str, object] = {} files["enabled"] = (None, str(body.enabled), "text/plain") diff --git a/tests/golden/python/python-requests/multipart-edge-cases/multipart_edge_cases/models/__init__.py.golden b/tests/golden/python/python-requests/multipart-edge-cases/multipart_edge_cases/models/__init__.py.golden index 71bfeea0a..cc307d188 100644 --- a/tests/golden/python/python-requests/multipart-edge-cases/multipart_edge_cases/models/__init__.py.golden +++ b/tests/golden/python/python-requests/multipart-edge-cases/multipart_edge_cases/models/__init__.py.golden @@ -5,4 +5,6 @@ from .attributes import Attributes as Attributes from .optional_upload import OptionalUpload as OptionalUpload +from .send_optional_parts_multipart_request_body import SendOptionalPartsMultipartRequestBody as SendOptionalPartsMultipartRequestBody +from .send_text_fields_multipart_request_body import SendTextFieldsMultipartRequestBody as SendTextFieldsMultipartRequestBody from .text_fields import TextFields as TextFields diff --git a/tests/golden/python/python-requests/multipart-edge-cases/multipart_edge_cases/models/send_optional_parts_multipart_request_body.py.golden b/tests/golden/python/python-requests/multipart-edge-cases/multipart_edge_cases/models/send_optional_parts_multipart_request_body.py.golden new file mode 100644 index 000000000..b65eb4410 --- /dev/null +++ b/tests/golden/python/python-requests/multipart-edge-cases/multipart_edge_cases/models/send_optional_parts_multipart_request_body.py.golden @@ -0,0 +1,18 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Multipart Edge Cases — 1.0.0 +# Covers optional multipart bodies, optional parts, and text-only multipart fields. + +from __future__ import annotations + +from dataclasses import dataclass +from ..runtime import UploadFile +from .attributes import Attributes + +@dataclass +class SendOptionalPartsMultipartRequestBody: + attributes: Attributes | None = None + enabled: bool | None = None + file: UploadFile | None = None + retry_count: int | None = None + title: str | None = None diff --git a/tests/golden/python/python-requests/multipart-edge-cases/multipart_edge_cases/models/send_text_fields_multipart_request_body.py.golden b/tests/golden/python/python-requests/multipart-edge-cases/multipart_edge_cases/models/send_text_fields_multipart_request_body.py.golden new file mode 100644 index 000000000..088b5347a --- /dev/null +++ b/tests/golden/python/python-requests/multipart-edge-cases/multipart_edge_cases/models/send_text_fields_multipart_request_body.py.golden @@ -0,0 +1,14 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Multipart Edge Cases — 1.0.0 +# Covers optional multipart bodies, optional parts, and text-only multipart fields. + +from __future__ import annotations + +from dataclasses import dataclass + +@dataclass +class SendTextFieldsMultipartRequestBody: + enabled: bool + note: str + retry_count: int diff --git a/tests/golden/python/python-requests/multipart-edge-cases/multipart_edge_cases/runtime/__init__.py.golden b/tests/golden/python/python-requests/multipart-edge-cases/multipart_edge_cases/runtime/__init__.py.golden index 7226802fc..d25d1b65e 100644 --- a/tests/golden/python/python-requests/multipart-edge-cases/multipart_edge_cases/runtime/__init__.py.golden +++ b/tests/golden/python/python-requests/multipart-edge-cases/multipart_edge_cases/runtime/__init__.py.golden @@ -8,3 +8,4 @@ from .auth import Authenticator as Authenticator from .auth import BearerAuth as BearerAuth from .client import Client as Client from .errors import ApiError as ApiError +from .upload_file import UploadFile as UploadFile diff --git a/tests/golden/python/python-requests/multipart-edge-cases/multipart_edge_cases/runtime/upload_file.py.golden b/tests/golden/python/python-requests/multipart-edge-cases/multipart_edge_cases/runtime/upload_file.py.golden new file mode 100644 index 000000000..cd5c1f6f0 --- /dev/null +++ b/tests/golden/python/python-requests/multipart-edge-cases/multipart_edge_cases/runtime/upload_file.py.golden @@ -0,0 +1,21 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Multipart Edge Cases — 1.0.0 +# Covers optional multipart bodies, optional parts, and text-only multipart fields. + +from __future__ import annotations + +from dataclasses import dataclass + + +@dataclass(frozen=True) +class UploadFile: + data: bytes + filename: str | None = None + + @classmethod + def from_bytes(cls, data: bytes, filename: str | None = None) -> UploadFile: + return cls(data=data, filename=filename) + + def filename_or_default(self, default_name: str) -> str: + return self.filename or default_name 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 index 94b1350f1..4acc621cd 100644 --- 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 @@ -8,3 +8,4 @@ from .runtime import Authenticator as Authenticator from .runtime import BearerAuth as BearerAuth from .runtime import Client as Client from .runtime import ApiError as ApiError +from .runtime import UploadFile as UploadFile 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 index bf5d41cd7..7ac8838b7 100644 --- 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 @@ -7,7 +7,7 @@ from __future__ import annotations import json -from ..models.upload_encoded_asset_request import UploadEncodedAssetRequest +from ..models.upload_encoded_asset_multipart_request_body import UploadEncodedAssetMultipartRequestBody from ..runtime.client import Client from ..runtime.errors import ApiError @@ -15,11 +15,11 @@ class TransferApi: def __init__(self, client: Client) -> None: self._client = client - def upload_encoded_asset(self, *, body: UploadEncodedAssetRequest) -> None: + def upload_encoded_asset(self, *, body: UploadEncodedAssetMultipartRequestBody) -> 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["file"] = (body.file.filename_or_default("file"), body.file.data, "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) 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 index 81e7b8856..6d8418805 100644 --- 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 @@ -5,4 +5,5 @@ from .audit_attributes import AuditAttributes as AuditAttributes from .upload_attributes import UploadAttributes as UploadAttributes +from .upload_encoded_asset_multipart_request_body import UploadEncodedAssetMultipartRequestBody as UploadEncodedAssetMultipartRequestBody from .upload_encoded_asset_request import UploadEncodedAssetRequest as UploadEncodedAssetRequest diff --git a/tests/golden/python/python-requests/multipart-explicit-encoding/multipart_explicit_encoding/models/upload_encoded_asset_multipart_request_body.py.golden b/tests/golden/python/python-requests/multipart-explicit-encoding/multipart_explicit_encoding/models/upload_encoded_asset_multipart_request_body.py.golden new file mode 100644 index 000000000..be589df82 --- /dev/null +++ b/tests/golden/python/python-requests/multipart-explicit-encoding/multipart_explicit_encoding/models/upload_encoded_asset_multipart_request_body.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. + +from __future__ import annotations + +from dataclasses import dataclass +from ..runtime import UploadFile +from .audit_attributes import AuditAttributes +from .upload_attributes import UploadAttributes + +@dataclass +class UploadEncodedAssetMultipartRequestBody: + audit: AuditAttributes + file: UploadFile + metadata: UploadAttributes + purpose: str 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 index 579bfa8db..0bf0ce984 100644 --- 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 @@ -8,3 +8,4 @@ from .auth import Authenticator as Authenticator from .auth import BearerAuth as BearerAuth from .client import Client as Client from .errors import ApiError as ApiError +from .upload_file import UploadFile as UploadFile diff --git a/tests/golden/python/python-requests/multipart-explicit-encoding/multipart_explicit_encoding/runtime/upload_file.py.golden b/tests/golden/python/python-requests/multipart-explicit-encoding/multipart_explicit_encoding/runtime/upload_file.py.golden new file mode 100644 index 000000000..b758ec358 --- /dev/null +++ b/tests/golden/python/python-requests/multipart-explicit-encoding/multipart_explicit_encoding/runtime/upload_file.py.golden @@ -0,0 +1,21 @@ +# 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(frozen=True) +class UploadFile: + data: bytes + filename: str | None = None + + @classmethod + def from_bytes(cls, data: bytes, filename: str | None = None) -> UploadFile: + return cls(data=data, filename=filename) + + def filename_or_default(self, default_name: str) -> str: + return self.filename or default_name diff --git a/tests/golden/python/python-requests/multipart-nested-object-parts/multipart_nested_object_parts/__init__.py.golden b/tests/golden/python/python-requests/multipart-nested-object-parts/multipart_nested_object_parts/__init__.py.golden index 054b30587..9d52a0512 100644 --- a/tests/golden/python/python-requests/multipart-nested-object-parts/multipart_nested_object_parts/__init__.py.golden +++ b/tests/golden/python/python-requests/multipart-nested-object-parts/multipart_nested_object_parts/__init__.py.golden @@ -8,3 +8,4 @@ from .runtime import Authenticator as Authenticator from .runtime import BearerAuth as BearerAuth from .runtime import Client as Client from .runtime import ApiError as ApiError +from .runtime import UploadFile as UploadFile diff --git a/tests/golden/python/python-requests/multipart-nested-object-parts/multipart_nested_object_parts/apis/multipart_api.py.golden b/tests/golden/python/python-requests/multipart-nested-object-parts/multipart_nested_object_parts/apis/multipart_api.py.golden index a94f9c628..1391d63ae 100644 --- a/tests/golden/python/python-requests/multipart-nested-object-parts/multipart_nested_object_parts/apis/multipart_api.py.golden +++ b/tests/golden/python/python-requests/multipart-nested-object-parts/multipart_nested_object_parts/apis/multipart_api.py.golden @@ -7,7 +7,7 @@ from __future__ import annotations import json -from ..models.nested_upload import NestedUpload +from ..models.send_nested_object_part_multipart_request_body import SendNestedObjectPartMultipartRequestBody from ..runtime.client import Client from ..runtime.errors import ApiError @@ -15,10 +15,10 @@ class MultipartApi: def __init__(self, client: Client) -> None: self._client = client - def send_nested_object_part(self, *, body: NestedUpload) -> None: + def send_nested_object_part(self, *, body: SendNestedObjectPartMultipartRequestBody) -> None: path = "/multipart/nested-object" files: dict[str, object] = {} - files["file"] = ("file", body.file, "application/octet-stream") + files["file"] = (body.file.filename_or_default("file"), body.file.data, "application/octet-stream") files["item_config"] = (None, json.dumps(body.item_config.to_dict()), "application/json") 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-nested-object-parts/multipart_nested_object_parts/models/__init__.py.golden b/tests/golden/python/python-requests/multipart-nested-object-parts/multipart_nested_object_parts/models/__init__.py.golden index 2d68dd31f..d862fb169 100644 --- a/tests/golden/python/python-requests/multipart-nested-object-parts/multipart_nested_object_parts/models/__init__.py.golden +++ b/tests/golden/python/python-requests/multipart-nested-object-parts/multipart_nested_object_parts/models/__init__.py.golden @@ -5,3 +5,4 @@ from .item_config import ItemConfig as ItemConfig from .nested_upload import NestedUpload as NestedUpload +from .send_nested_object_part_multipart_request_body import SendNestedObjectPartMultipartRequestBody as SendNestedObjectPartMultipartRequestBody diff --git a/tests/golden/python/python-requests/multipart-nested-object-parts/multipart_nested_object_parts/models/send_nested_object_part_multipart_request_body.py.golden b/tests/golden/python/python-requests/multipart-nested-object-parts/multipart_nested_object_parts/models/send_nested_object_part_multipart_request_body.py.golden new file mode 100644 index 000000000..633109766 --- /dev/null +++ b/tests/golden/python/python-requests/multipart-nested-object-parts/multipart_nested_object_parts/models/send_nested_object_part_multipart_request_body.py.golden @@ -0,0 +1,15 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Multipart Nested Object Parts — 1.0.0 +# Covers multipart object parts whose wire names differ from ergonomic names. + +from __future__ import annotations + +from dataclasses import dataclass +from ..runtime import UploadFile +from .item_config import ItemConfig + +@dataclass +class SendNestedObjectPartMultipartRequestBody: + file: UploadFile + item_config: ItemConfig diff --git a/tests/golden/python/python-requests/multipart-nested-object-parts/multipart_nested_object_parts/runtime/__init__.py.golden b/tests/golden/python/python-requests/multipart-nested-object-parts/multipart_nested_object_parts/runtime/__init__.py.golden index 977ff9b16..ace71435a 100644 --- a/tests/golden/python/python-requests/multipart-nested-object-parts/multipart_nested_object_parts/runtime/__init__.py.golden +++ b/tests/golden/python/python-requests/multipart-nested-object-parts/multipart_nested_object_parts/runtime/__init__.py.golden @@ -8,3 +8,4 @@ from .auth import Authenticator as Authenticator from .auth import BearerAuth as BearerAuth from .client import Client as Client from .errors import ApiError as ApiError +from .upload_file import UploadFile as UploadFile diff --git a/tests/golden/python/python-requests/multipart-nested-object-parts/multipart_nested_object_parts/runtime/upload_file.py.golden b/tests/golden/python/python-requests/multipart-nested-object-parts/multipart_nested_object_parts/runtime/upload_file.py.golden new file mode 100644 index 000000000..15322f524 --- /dev/null +++ b/tests/golden/python/python-requests/multipart-nested-object-parts/multipart_nested_object_parts/runtime/upload_file.py.golden @@ -0,0 +1,21 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Multipart Nested Object Parts — 1.0.0 +# Covers multipart object parts whose wire names differ from ergonomic names. + +from __future__ import annotations + +from dataclasses import dataclass + + +@dataclass(frozen=True) +class UploadFile: + data: bytes + filename: str | None = None + + @classmethod + def from_bytes(cls, data: bytes, filename: str | None = None) -> UploadFile: + return cls(data=data, filename=filename) + + def filename_or_default(self, default_name: str) -> str: + return self.filename or default_name 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 276e392bc..38c937eb7 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 @@ -44,12 +44,12 @@ impl<'a, R: aioduct::Runtime> TransferApi<'a, R> { /// POST /uploads pub async fn upload_asset( &self, - body: &crate::models::UploadAssetRequest, + body: &crate::models::UploadAssetMultipartRequestBody, ) -> Result { let path = "/uploads".to_string(); 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.file("file", body.file.filename_or_default("file").to_string(), "application/octet-stream", body.file.data.clone()); 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); diff --git a/tests/golden/rust/rust-aioduct/binary-transfer-media-types/src/models/mod.rs.golden b/tests/golden/rust/rust-aioduct/binary-transfer-media-types/src/models/mod.rs.golden index 117bf91a3..60875d3ba 100644 --- a/tests/golden/rust/rust-aioduct/binary-transfer-media-types/src/models/mod.rs.golden +++ b/tests/golden/rust/rust-aioduct/binary-transfer-media-types/src/models/mod.rs.golden @@ -10,3 +10,5 @@ mod upload_metadata; pub use upload_metadata::*; mod upload_asset_request; pub use upload_asset_request::*; +mod upload_asset_multipart_request_body; +pub use upload_asset_multipart_request_body::*; diff --git a/tests/golden/rust/rust-aioduct/binary-transfer-media-types/src/models/upload_asset_multipart_request_body.rs.golden b/tests/golden/rust/rust-aioduct/binary-transfer-media-types/src/models/upload_asset_multipart_request_body.rs.golden new file mode 100644 index 000000000..e0e849f92 --- /dev/null +++ b/tests/golden/rust/rust-aioduct/binary-transfer-media-types/src/models/upload_asset_multipart_request_body.rs.golden @@ -0,0 +1,14 @@ +// @generated +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Binary Transfer Media Types — 1.0.0 +// Covers multipart upload and octet-stream download. + +use crate::runtime::UploadFile; + +#[derive(Debug, Clone)] +pub struct UploadAssetMultipartRequestBody { + pub file: UploadFile, + pub metadata: super::UploadAttributes, + pub purpose: String, +} diff --git a/tests/golden/rust/rust-aioduct/binary-transfer-media-types/src/runtime/mod.rs.golden b/tests/golden/rust/rust-aioduct/binary-transfer-media-types/src/runtime/mod.rs.golden index b8766221d..5d68d3229 100644 --- a/tests/golden/rust/rust-aioduct/binary-transfer-media-types/src/runtime/mod.rs.golden +++ b/tests/golden/rust/rust-aioduct/binary-transfer-media-types/src/runtime/mod.rs.golden @@ -7,3 +7,5 @@ pub mod auth; pub mod client; pub mod error; +mod upload_file; +pub use upload_file::{multipart_header_value, UploadFile}; diff --git a/tests/golden/rust/rust-aioduct/binary-transfer-media-types/src/runtime/upload_file.rs.golden b/tests/golden/rust/rust-aioduct/binary-transfer-media-types/src/runtime/upload_file.rs.golden new file mode 100644 index 000000000..8fb5b330d --- /dev/null +++ b/tests/golden/rust/rust-aioduct/binary-transfer-media-types/src/runtime/upload_file.rs.golden @@ -0,0 +1,39 @@ +// @generated +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Binary Transfer Media Types — 1.0.0 +// Covers multipart upload and octet-stream download. + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct UploadFile { + pub data: Vec, + pub filename: Option, +} + +impl UploadFile { + pub fn new(data: impl Into>, filename: impl Into) -> Self { + Self { + data: data.into(), + filename: Some(filename.into()), + } + } + + pub fn from_bytes(data: impl Into>) -> Self { + Self { + data: data.into(), + filename: None, + } + } + + pub fn filename_or_default<'a>(&'a self, default_name: &'a str) -> &'a str { + self.filename.as_deref().unwrap_or(default_name) + } +} + +pub fn multipart_header_value(value: &str) -> String { + value + .replace('\r', "") + .replace('\n', "") + .replace('\\', "\\\\") + .replace('"', "\\\"") +} 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 02469d7aa..7be5f0eca 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 @@ -36,12 +36,12 @@ impl<'a, R: aioduct::Runtime> MediaApi<'a, R> { /// POST /request/parameterized-multipart pub async fn send_parameterized_multipart( &self, - body: &crate::models::FileEnvelope, + body: &crate::models::SendParameterizedMultipartMultipartRequestBody, ) -> Result { let path = "/request/parameterized-multipart".to_string(); 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.file("file", body.file.filename_or_default("file").to_string(), "application/octet-stream", body.file.data.clone()); if let Some(value) = &body.note { multipart = multipart.part(aioduct::multipart::Part::text("note", value.to_string()).mime_str("text/plain")); } diff --git a/tests/golden/rust/rust-aioduct/media-type-selection/src/models/mod.rs.golden b/tests/golden/rust/rust-aioduct/media-type-selection/src/models/mod.rs.golden index f9bb2e882..c18add594 100644 --- a/tests/golden/rust/rust-aioduct/media-type-selection/src/models/mod.rs.golden +++ b/tests/golden/rust/rust-aioduct/media-type-selection/src/models/mod.rs.golden @@ -8,3 +8,5 @@ mod file_envelope; pub use file_envelope::*; mod payload; pub use payload::*; +mod send_parameterized_multipart_multipart_request_body; +pub use send_parameterized_multipart_multipart_request_body::*; diff --git a/tests/golden/rust/rust-aioduct/media-type-selection/src/models/send_parameterized_multipart_multipart_request_body.rs.golden b/tests/golden/rust/rust-aioduct/media-type-selection/src/models/send_parameterized_multipart_multipart_request_body.rs.golden new file mode 100644 index 000000000..db0b45ab9 --- /dev/null +++ b/tests/golden/rust/rust-aioduct/media-type-selection/src/models/send_parameterized_multipart_multipart_request_body.rs.golden @@ -0,0 +1,13 @@ +// @generated +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Media Type Selection — 1.0.0 +// Covers normalized media-type selection for requests and responses. + +use crate::runtime::UploadFile; + +#[derive(Debug, Clone)] +pub struct SendParameterizedMultipartMultipartRequestBody { + pub file: UploadFile, + pub note: Option, +} diff --git a/tests/golden/rust/rust-aioduct/media-type-selection/src/runtime/mod.rs.golden b/tests/golden/rust/rust-aioduct/media-type-selection/src/runtime/mod.rs.golden index 31db0c2b8..2e077722d 100644 --- a/tests/golden/rust/rust-aioduct/media-type-selection/src/runtime/mod.rs.golden +++ b/tests/golden/rust/rust-aioduct/media-type-selection/src/runtime/mod.rs.golden @@ -7,3 +7,5 @@ pub mod auth; pub mod client; pub mod error; +mod upload_file; +pub use upload_file::{multipart_header_value, UploadFile}; diff --git a/tests/golden/rust/rust-aioduct/media-type-selection/src/runtime/upload_file.rs.golden b/tests/golden/rust/rust-aioduct/media-type-selection/src/runtime/upload_file.rs.golden new file mode 100644 index 000000000..078d5f15e --- /dev/null +++ b/tests/golden/rust/rust-aioduct/media-type-selection/src/runtime/upload_file.rs.golden @@ -0,0 +1,39 @@ +// @generated +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Media Type Selection — 1.0.0 +// Covers normalized media-type selection for requests and responses. + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct UploadFile { + pub data: Vec, + pub filename: Option, +} + +impl UploadFile { + pub fn new(data: impl Into>, filename: impl Into) -> Self { + Self { + data: data.into(), + filename: Some(filename.into()), + } + } + + pub fn from_bytes(data: impl Into>) -> Self { + Self { + data: data.into(), + filename: None, + } + } + + pub fn filename_or_default<'a>(&'a self, default_name: &'a str) -> &'a str { + self.filename.as_deref().unwrap_or(default_name) + } +} + +pub fn multipart_header_value(value: &str) -> String { + value + .replace('\r', "") + .replace('\n', "") + .replace('\\', "\\\\") + .replace('"', "\\\"") +} 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 627b47ad0..bfeb4e8a1 100644 --- a/tests/golden/rust/rust-aioduct/multipart-edge-cases/src/apis/multipart.rs.golden +++ b/tests/golden/rust/rust-aioduct/multipart-edge-cases/src/apis/multipart.rs.golden @@ -23,7 +23,7 @@ impl<'a, R: aioduct::Runtime> MultipartApi<'a, R> { /// POST /multipart/optional pub async fn send_optional_parts( &self, - body: Option<&crate::models::OptionalUpload>, + body: Option<&crate::models::SendOptionalPartsMultipartRequestBody>, ) -> Result { let path = "/multipart/optional".to_string(); let mut req = self.client.post(&path)?; @@ -36,7 +36,7 @@ impl<'a, R: aioduct::Runtime> MultipartApi<'a, R> { 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()); + multipart = multipart.file("file", value.filename_or_default("file").to_string(), "application/octet-stream", value.data.clone()); } if let Some(value) = &body.retry_count { multipart = multipart.part(aioduct::multipart::Part::text("retry_count", value.to_string()).mime_str("text/plain")); @@ -54,7 +54,7 @@ impl<'a, R: aioduct::Runtime> MultipartApi<'a, R> { /// POST /multipart/text-only pub async fn send_text_fields( &self, - body: &crate::models::TextFields, + body: &crate::models::SendTextFieldsMultipartRequestBody, ) -> Result { let path = "/multipart/text-only".to_string(); let mut req = self.client.post(&path)?; diff --git a/tests/golden/rust/rust-aioduct/multipart-edge-cases/src/models/mod.rs.golden b/tests/golden/rust/rust-aioduct/multipart-edge-cases/src/models/mod.rs.golden index 97c6c9a6b..9a57d0a2a 100644 --- a/tests/golden/rust/rust-aioduct/multipart-edge-cases/src/models/mod.rs.golden +++ b/tests/golden/rust/rust-aioduct/multipart-edge-cases/src/models/mod.rs.golden @@ -10,3 +10,7 @@ mod optional_upload; pub use optional_upload::*; mod text_fields; pub use text_fields::*; +mod send_optional_parts_multipart_request_body; +pub use send_optional_parts_multipart_request_body::*; +mod send_text_fields_multipart_request_body; +pub use send_text_fields_multipart_request_body::*; diff --git a/tests/golden/rust/rust-aioduct/multipart-edge-cases/src/models/send_optional_parts_multipart_request_body.rs.golden b/tests/golden/rust/rust-aioduct/multipart-edge-cases/src/models/send_optional_parts_multipart_request_body.rs.golden new file mode 100644 index 000000000..16f36eae6 --- /dev/null +++ b/tests/golden/rust/rust-aioduct/multipart-edge-cases/src/models/send_optional_parts_multipart_request_body.rs.golden @@ -0,0 +1,16 @@ +// @generated +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Multipart Edge Cases — 1.0.0 +// Covers optional multipart bodies, optional parts, and text-only multipart fields. + +use crate::runtime::UploadFile; + +#[derive(Debug, Clone)] +pub struct SendOptionalPartsMultipartRequestBody { + pub attributes: Option, + pub enabled: Option, + pub file: Option, + pub retry_count: Option, + pub title: Option, +} diff --git a/tests/golden/rust/rust-aioduct/multipart-edge-cases/src/models/send_text_fields_multipart_request_body.rs.golden b/tests/golden/rust/rust-aioduct/multipart-edge-cases/src/models/send_text_fields_multipart_request_body.rs.golden new file mode 100644 index 000000000..3afc2b599 --- /dev/null +++ b/tests/golden/rust/rust-aioduct/multipart-edge-cases/src/models/send_text_fields_multipart_request_body.rs.golden @@ -0,0 +1,12 @@ +// @generated +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Multipart Edge Cases — 1.0.0 +// Covers optional multipart bodies, optional parts, and text-only multipart fields. + +#[derive(Debug, Clone)] +pub struct SendTextFieldsMultipartRequestBody { + pub enabled: bool, + pub note: String, + pub retry_count: i64, +} diff --git a/tests/golden/rust/rust-aioduct/multipart-edge-cases/src/runtime/mod.rs.golden b/tests/golden/rust/rust-aioduct/multipart-edge-cases/src/runtime/mod.rs.golden index ef7165a41..e4e8d7a7d 100644 --- a/tests/golden/rust/rust-aioduct/multipart-edge-cases/src/runtime/mod.rs.golden +++ b/tests/golden/rust/rust-aioduct/multipart-edge-cases/src/runtime/mod.rs.golden @@ -7,3 +7,5 @@ pub mod auth; pub mod client; pub mod error; +mod upload_file; +pub use upload_file::{multipart_header_value, UploadFile}; diff --git a/tests/golden/rust/rust-aioduct/multipart-edge-cases/src/runtime/upload_file.rs.golden b/tests/golden/rust/rust-aioduct/multipart-edge-cases/src/runtime/upload_file.rs.golden new file mode 100644 index 000000000..7447e35ee --- /dev/null +++ b/tests/golden/rust/rust-aioduct/multipart-edge-cases/src/runtime/upload_file.rs.golden @@ -0,0 +1,39 @@ +// @generated +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Multipart Edge Cases — 1.0.0 +// Covers optional multipart bodies, optional parts, and text-only multipart fields. + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct UploadFile { + pub data: Vec, + pub filename: Option, +} + +impl UploadFile { + pub fn new(data: impl Into>, filename: impl Into) -> Self { + Self { + data: data.into(), + filename: Some(filename.into()), + } + } + + pub fn from_bytes(data: impl Into>) -> Self { + Self { + data: data.into(), + filename: None, + } + } + + pub fn filename_or_default<'a>(&'a self, default_name: &'a str) -> &'a str { + self.filename.as_deref().unwrap_or(default_name) + } +} + +pub fn multipart_header_value(value: &str) -> String { + value + .replace('\r', "") + .replace('\n', "") + .replace('\\', "\\\\") + .replace('"', "\\\"") +} 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 index bdb367d6c..a579174e9 100644 --- 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 @@ -23,13 +23,13 @@ impl<'a, R: aioduct::Runtime> TransferApi<'a, R> { /// POST /uploads/encoded pub async fn upload_encoded_asset( &self, - body: &crate::models::UploadEncodedAssetRequest, + body: &crate::models::UploadEncodedAssetMultipartRequestBody, ) -> 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.file("file", body.file.filename_or_default("file").to_string(), "application/pdf", body.file.data.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); 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 index b7af82c9b..a994e2c64 100644 --- 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 @@ -10,3 +10,5 @@ mod upload_attributes; pub use upload_attributes::*; mod upload_encoded_asset_request; pub use upload_encoded_asset_request::*; +mod upload_encoded_asset_multipart_request_body; +pub use upload_encoded_asset_multipart_request_body::*; diff --git a/tests/golden/rust/rust-aioduct/multipart-explicit-encoding/src/models/upload_encoded_asset_multipart_request_body.rs.golden b/tests/golden/rust/rust-aioduct/multipart-explicit-encoding/src/models/upload_encoded_asset_multipart_request_body.rs.golden new file mode 100644 index 000000000..440bc15b9 --- /dev/null +++ b/tests/golden/rust/rust-aioduct/multipart-explicit-encoding/src/models/upload_encoded_asset_multipart_request_body.rs.golden @@ -0,0 +1,15 @@ +// @generated +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Multipart Explicit Encoding — 1.0.0 +// Covers explicit multipart part content types. + +use crate::runtime::UploadFile; + +#[derive(Debug, Clone)] +pub struct UploadEncodedAssetMultipartRequestBody { + pub audit: super::AuditAttributes, + pub file: UploadFile, + pub metadata: super::UploadAttributes, + pub purpose: String, +} 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 index cdb1caac6..87bd15390 100644 --- 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 @@ -7,3 +7,5 @@ pub mod auth; pub mod client; pub mod error; +mod upload_file; +pub use upload_file::{multipart_header_value, UploadFile}; diff --git a/tests/golden/rust/rust-aioduct/multipart-explicit-encoding/src/runtime/upload_file.rs.golden b/tests/golden/rust/rust-aioduct/multipart-explicit-encoding/src/runtime/upload_file.rs.golden new file mode 100644 index 000000000..18a3267b4 --- /dev/null +++ b/tests/golden/rust/rust-aioduct/multipart-explicit-encoding/src/runtime/upload_file.rs.golden @@ -0,0 +1,39 @@ +// @generated +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Multipart Explicit Encoding — 1.0.0 +// Covers explicit multipart part content types. + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct UploadFile { + pub data: Vec, + pub filename: Option, +} + +impl UploadFile { + pub fn new(data: impl Into>, filename: impl Into) -> Self { + Self { + data: data.into(), + filename: Some(filename.into()), + } + } + + pub fn from_bytes(data: impl Into>) -> Self { + Self { + data: data.into(), + filename: None, + } + } + + pub fn filename_or_default<'a>(&'a self, default_name: &'a str) -> &'a str { + self.filename.as_deref().unwrap_or(default_name) + } +} + +pub fn multipart_header_value(value: &str) -> String { + value + .replace('\r', "") + .replace('\n', "") + .replace('\\', "\\\\") + .replace('"', "\\\"") +} 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 5448aa407..1f20ce4e5 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 @@ -23,12 +23,12 @@ impl<'a, R: aioduct::Runtime> MultipartApi<'a, R> { /// POST /multipart/nested-object pub async fn send_nested_object_part( &self, - body: &crate::models::NestedUpload, + body: &crate::models::SendNestedObjectPartMultipartRequestBody, ) -> Result { let path = "/multipart/nested-object".to_string(); 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.file("file", body.file.filename_or_default("file").to_string(), "application/octet-stream", body.file.data.clone()); 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?; diff --git a/tests/golden/rust/rust-aioduct/multipart-nested-object-parts/src/models/mod.rs.golden b/tests/golden/rust/rust-aioduct/multipart-nested-object-parts/src/models/mod.rs.golden index 2f9637c82..2873ea810 100644 --- a/tests/golden/rust/rust-aioduct/multipart-nested-object-parts/src/models/mod.rs.golden +++ b/tests/golden/rust/rust-aioduct/multipart-nested-object-parts/src/models/mod.rs.golden @@ -8,3 +8,5 @@ mod item_config; pub use item_config::*; mod nested_upload; pub use nested_upload::*; +mod send_nested_object_part_multipart_request_body; +pub use send_nested_object_part_multipart_request_body::*; diff --git a/tests/golden/rust/rust-aioduct/multipart-nested-object-parts/src/models/send_nested_object_part_multipart_request_body.rs.golden b/tests/golden/rust/rust-aioduct/multipart-nested-object-parts/src/models/send_nested_object_part_multipart_request_body.rs.golden new file mode 100644 index 000000000..306bf40cf --- /dev/null +++ b/tests/golden/rust/rust-aioduct/multipart-nested-object-parts/src/models/send_nested_object_part_multipart_request_body.rs.golden @@ -0,0 +1,13 @@ +// @generated +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Multipart Nested Object Parts — 1.0.0 +// Covers multipart object parts whose wire names differ from ergonomic names. + +use crate::runtime::UploadFile; + +#[derive(Debug, Clone)] +pub struct SendNestedObjectPartMultipartRequestBody { + pub file: UploadFile, + pub item_config: super::ItemConfig, +} diff --git a/tests/golden/rust/rust-aioduct/multipart-nested-object-parts/src/runtime/mod.rs.golden b/tests/golden/rust/rust-aioduct/multipart-nested-object-parts/src/runtime/mod.rs.golden index 61150544a..906c7a59e 100644 --- a/tests/golden/rust/rust-aioduct/multipart-nested-object-parts/src/runtime/mod.rs.golden +++ b/tests/golden/rust/rust-aioduct/multipart-nested-object-parts/src/runtime/mod.rs.golden @@ -7,3 +7,5 @@ pub mod auth; pub mod client; pub mod error; +mod upload_file; +pub use upload_file::{multipart_header_value, UploadFile}; diff --git a/tests/golden/rust/rust-aioduct/multipart-nested-object-parts/src/runtime/upload_file.rs.golden b/tests/golden/rust/rust-aioduct/multipart-nested-object-parts/src/runtime/upload_file.rs.golden new file mode 100644 index 000000000..b5c6ba9da --- /dev/null +++ b/tests/golden/rust/rust-aioduct/multipart-nested-object-parts/src/runtime/upload_file.rs.golden @@ -0,0 +1,39 @@ +// @generated +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Multipart Nested Object Parts — 1.0.0 +// Covers multipart object parts whose wire names differ from ergonomic names. + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct UploadFile { + pub data: Vec, + pub filename: Option, +} + +impl UploadFile { + pub fn new(data: impl Into>, filename: impl Into) -> Self { + Self { + data: data.into(), + filename: Some(filename.into()), + } + } + + pub fn from_bytes(data: impl Into>) -> Self { + Self { + data: data.into(), + filename: None, + } + } + + pub fn filename_or_default<'a>(&'a self, default_name: &'a str) -> &'a str { + self.filename.as_deref().unwrap_or(default_name) + } +} + +pub fn multipart_header_value(value: &str) -> String { + value + .replace('\r', "") + .replace('\n', "") + .replace('\\', "\\\\") + .replace('"', "\\\"") +} 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 b4a8ee252..baa5e48b1 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 @@ -44,12 +44,12 @@ impl<'a> TransferApi<'a> { /// POST /uploads pub async fn upload_asset( &self, - body: &crate::models::UploadAssetRequest, + body: &crate::models::UploadAssetMultipartRequestBody, ) -> Result { let path = "/uploads".to_string(); 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.part("file", reqwest::multipart::Part::bytes(body.file.data.clone()).file_name(body.file.filename_or_default("file").to_string()).mime_str("application/octet-stream").map_err(Error::Network)?); 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); diff --git a/tests/golden/rust/rust-reqwest/binary-transfer-media-types/src/models/mod.rs.golden b/tests/golden/rust/rust-reqwest/binary-transfer-media-types/src/models/mod.rs.golden index 117bf91a3..60875d3ba 100644 --- a/tests/golden/rust/rust-reqwest/binary-transfer-media-types/src/models/mod.rs.golden +++ b/tests/golden/rust/rust-reqwest/binary-transfer-media-types/src/models/mod.rs.golden @@ -10,3 +10,5 @@ mod upload_metadata; pub use upload_metadata::*; mod upload_asset_request; pub use upload_asset_request::*; +mod upload_asset_multipart_request_body; +pub use upload_asset_multipart_request_body::*; diff --git a/tests/golden/rust/rust-reqwest/binary-transfer-media-types/src/models/upload_asset_multipart_request_body.rs.golden b/tests/golden/rust/rust-reqwest/binary-transfer-media-types/src/models/upload_asset_multipart_request_body.rs.golden new file mode 100644 index 000000000..e0e849f92 --- /dev/null +++ b/tests/golden/rust/rust-reqwest/binary-transfer-media-types/src/models/upload_asset_multipart_request_body.rs.golden @@ -0,0 +1,14 @@ +// @generated +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Binary Transfer Media Types — 1.0.0 +// Covers multipart upload and octet-stream download. + +use crate::runtime::UploadFile; + +#[derive(Debug, Clone)] +pub struct UploadAssetMultipartRequestBody { + pub file: UploadFile, + pub metadata: super::UploadAttributes, + pub purpose: String, +} diff --git a/tests/golden/rust/rust-reqwest/binary-transfer-media-types/src/runtime/mod.rs.golden b/tests/golden/rust/rust-reqwest/binary-transfer-media-types/src/runtime/mod.rs.golden index b8766221d..5d68d3229 100644 --- a/tests/golden/rust/rust-reqwest/binary-transfer-media-types/src/runtime/mod.rs.golden +++ b/tests/golden/rust/rust-reqwest/binary-transfer-media-types/src/runtime/mod.rs.golden @@ -7,3 +7,5 @@ pub mod auth; pub mod client; pub mod error; +mod upload_file; +pub use upload_file::{multipart_header_value, UploadFile}; diff --git a/tests/golden/rust/rust-reqwest/binary-transfer-media-types/src/runtime/upload_file.rs.golden b/tests/golden/rust/rust-reqwest/binary-transfer-media-types/src/runtime/upload_file.rs.golden new file mode 100644 index 000000000..8fb5b330d --- /dev/null +++ b/tests/golden/rust/rust-reqwest/binary-transfer-media-types/src/runtime/upload_file.rs.golden @@ -0,0 +1,39 @@ +// @generated +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Binary Transfer Media Types — 1.0.0 +// Covers multipart upload and octet-stream download. + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct UploadFile { + pub data: Vec, + pub filename: Option, +} + +impl UploadFile { + pub fn new(data: impl Into>, filename: impl Into) -> Self { + Self { + data: data.into(), + filename: Some(filename.into()), + } + } + + pub fn from_bytes(data: impl Into>) -> Self { + Self { + data: data.into(), + filename: None, + } + } + + pub fn filename_or_default<'a>(&'a self, default_name: &'a str) -> &'a str { + self.filename.as_deref().unwrap_or(default_name) + } +} + +pub fn multipart_header_value(value: &str) -> String { + value + .replace('\r', "") + .replace('\n', "") + .replace('\\', "\\\\") + .replace('"', "\\\"") +} 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 30513365e..87bb6a380 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 @@ -36,12 +36,12 @@ impl<'a> MediaApi<'a> { /// POST /request/parameterized-multipart pub async fn send_parameterized_multipart( &self, - body: &crate::models::FileEnvelope, + body: &crate::models::SendParameterizedMultipartMultipartRequestBody, ) -> Result { let path = "/request/parameterized-multipart".to_string(); 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.part("file", reqwest::multipart::Part::bytes(body.file.data.clone()).file_name(body.file.filename_or_default("file").to_string()).mime_str("application/octet-stream").map_err(Error::Network)?); if let Some(value) = &body.note { multipart = multipart.part("note", reqwest::multipart::Part::text(value.to_string()).mime_str("text/plain").map_err(Error::Network)?); } diff --git a/tests/golden/rust/rust-reqwest/media-type-selection/src/models/mod.rs.golden b/tests/golden/rust/rust-reqwest/media-type-selection/src/models/mod.rs.golden index f9bb2e882..c18add594 100644 --- a/tests/golden/rust/rust-reqwest/media-type-selection/src/models/mod.rs.golden +++ b/tests/golden/rust/rust-reqwest/media-type-selection/src/models/mod.rs.golden @@ -8,3 +8,5 @@ mod file_envelope; pub use file_envelope::*; mod payload; pub use payload::*; +mod send_parameterized_multipart_multipart_request_body; +pub use send_parameterized_multipart_multipart_request_body::*; diff --git a/tests/golden/rust/rust-reqwest/media-type-selection/src/models/send_parameterized_multipart_multipart_request_body.rs.golden b/tests/golden/rust/rust-reqwest/media-type-selection/src/models/send_parameterized_multipart_multipart_request_body.rs.golden new file mode 100644 index 000000000..db0b45ab9 --- /dev/null +++ b/tests/golden/rust/rust-reqwest/media-type-selection/src/models/send_parameterized_multipart_multipart_request_body.rs.golden @@ -0,0 +1,13 @@ +// @generated +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Media Type Selection — 1.0.0 +// Covers normalized media-type selection for requests and responses. + +use crate::runtime::UploadFile; + +#[derive(Debug, Clone)] +pub struct SendParameterizedMultipartMultipartRequestBody { + pub file: UploadFile, + pub note: Option, +} diff --git a/tests/golden/rust/rust-reqwest/media-type-selection/src/runtime/mod.rs.golden b/tests/golden/rust/rust-reqwest/media-type-selection/src/runtime/mod.rs.golden index 31db0c2b8..2e077722d 100644 --- a/tests/golden/rust/rust-reqwest/media-type-selection/src/runtime/mod.rs.golden +++ b/tests/golden/rust/rust-reqwest/media-type-selection/src/runtime/mod.rs.golden @@ -7,3 +7,5 @@ pub mod auth; pub mod client; pub mod error; +mod upload_file; +pub use upload_file::{multipart_header_value, UploadFile}; diff --git a/tests/golden/rust/rust-reqwest/media-type-selection/src/runtime/upload_file.rs.golden b/tests/golden/rust/rust-reqwest/media-type-selection/src/runtime/upload_file.rs.golden new file mode 100644 index 000000000..078d5f15e --- /dev/null +++ b/tests/golden/rust/rust-reqwest/media-type-selection/src/runtime/upload_file.rs.golden @@ -0,0 +1,39 @@ +// @generated +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Media Type Selection — 1.0.0 +// Covers normalized media-type selection for requests and responses. + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct UploadFile { + pub data: Vec, + pub filename: Option, +} + +impl UploadFile { + pub fn new(data: impl Into>, filename: impl Into) -> Self { + Self { + data: data.into(), + filename: Some(filename.into()), + } + } + + pub fn from_bytes(data: impl Into>) -> Self { + Self { + data: data.into(), + filename: None, + } + } + + pub fn filename_or_default<'a>(&'a self, default_name: &'a str) -> &'a str { + self.filename.as_deref().unwrap_or(default_name) + } +} + +pub fn multipart_header_value(value: &str) -> String { + value + .replace('\r', "") + .replace('\n', "") + .replace('\\', "\\\\") + .replace('"', "\\\"") +} 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 68197c4bf..5c30c97b2 100644 --- a/tests/golden/rust/rust-reqwest/multipart-edge-cases/src/apis/multipart.rs.golden +++ b/tests/golden/rust/rust-reqwest/multipart-edge-cases/src/apis/multipart.rs.golden @@ -23,7 +23,7 @@ impl<'a> MultipartApi<'a> { /// POST /multipart/optional pub async fn send_optional_parts( &self, - body: Option<&crate::models::OptionalUpload>, + body: Option<&crate::models::SendOptionalPartsMultipartRequestBody>, ) -> Result { let path = "/multipart/optional".to_string(); let mut req = self.client.request(reqwest::Method::POST, &path).await?; @@ -36,7 +36,7 @@ impl<'a> MultipartApi<'a> { 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)?); + multipart = multipart.part("file", reqwest::multipart::Part::bytes(value.data.clone()).file_name(value.filename_or_default("file").to_string()).mime_str("application/octet-stream").map_err(Error::Network)?); } if let Some(value) = &body.retry_count { multipart = multipart.part("retry_count", reqwest::multipart::Part::text(value.to_string()).mime_str("text/plain").map_err(Error::Network)?); @@ -54,7 +54,7 @@ impl<'a> MultipartApi<'a> { /// POST /multipart/text-only pub async fn send_text_fields( &self, - body: &crate::models::TextFields, + body: &crate::models::SendTextFieldsMultipartRequestBody, ) -> Result { let path = "/multipart/text-only".to_string(); let mut req = self.client.request(reqwest::Method::POST, &path).await?; diff --git a/tests/golden/rust/rust-reqwest/multipart-edge-cases/src/models/mod.rs.golden b/tests/golden/rust/rust-reqwest/multipart-edge-cases/src/models/mod.rs.golden index 97c6c9a6b..9a57d0a2a 100644 --- a/tests/golden/rust/rust-reqwest/multipart-edge-cases/src/models/mod.rs.golden +++ b/tests/golden/rust/rust-reqwest/multipart-edge-cases/src/models/mod.rs.golden @@ -10,3 +10,7 @@ mod optional_upload; pub use optional_upload::*; mod text_fields; pub use text_fields::*; +mod send_optional_parts_multipart_request_body; +pub use send_optional_parts_multipart_request_body::*; +mod send_text_fields_multipart_request_body; +pub use send_text_fields_multipart_request_body::*; diff --git a/tests/golden/rust/rust-reqwest/multipart-edge-cases/src/models/send_optional_parts_multipart_request_body.rs.golden b/tests/golden/rust/rust-reqwest/multipart-edge-cases/src/models/send_optional_parts_multipart_request_body.rs.golden new file mode 100644 index 000000000..16f36eae6 --- /dev/null +++ b/tests/golden/rust/rust-reqwest/multipart-edge-cases/src/models/send_optional_parts_multipart_request_body.rs.golden @@ -0,0 +1,16 @@ +// @generated +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Multipart Edge Cases — 1.0.0 +// Covers optional multipart bodies, optional parts, and text-only multipart fields. + +use crate::runtime::UploadFile; + +#[derive(Debug, Clone)] +pub struct SendOptionalPartsMultipartRequestBody { + pub attributes: Option, + pub enabled: Option, + pub file: Option, + pub retry_count: Option, + pub title: Option, +} diff --git a/tests/golden/rust/rust-reqwest/multipart-edge-cases/src/models/send_text_fields_multipart_request_body.rs.golden b/tests/golden/rust/rust-reqwest/multipart-edge-cases/src/models/send_text_fields_multipart_request_body.rs.golden new file mode 100644 index 000000000..3afc2b599 --- /dev/null +++ b/tests/golden/rust/rust-reqwest/multipart-edge-cases/src/models/send_text_fields_multipart_request_body.rs.golden @@ -0,0 +1,12 @@ +// @generated +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Multipart Edge Cases — 1.0.0 +// Covers optional multipart bodies, optional parts, and text-only multipart fields. + +#[derive(Debug, Clone)] +pub struct SendTextFieldsMultipartRequestBody { + pub enabled: bool, + pub note: String, + pub retry_count: i64, +} diff --git a/tests/golden/rust/rust-reqwest/multipart-edge-cases/src/runtime/mod.rs.golden b/tests/golden/rust/rust-reqwest/multipart-edge-cases/src/runtime/mod.rs.golden index ef7165a41..e4e8d7a7d 100644 --- a/tests/golden/rust/rust-reqwest/multipart-edge-cases/src/runtime/mod.rs.golden +++ b/tests/golden/rust/rust-reqwest/multipart-edge-cases/src/runtime/mod.rs.golden @@ -7,3 +7,5 @@ pub mod auth; pub mod client; pub mod error; +mod upload_file; +pub use upload_file::{multipart_header_value, UploadFile}; diff --git a/tests/golden/rust/rust-reqwest/multipart-edge-cases/src/runtime/upload_file.rs.golden b/tests/golden/rust/rust-reqwest/multipart-edge-cases/src/runtime/upload_file.rs.golden new file mode 100644 index 000000000..7447e35ee --- /dev/null +++ b/tests/golden/rust/rust-reqwest/multipart-edge-cases/src/runtime/upload_file.rs.golden @@ -0,0 +1,39 @@ +// @generated +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Multipart Edge Cases — 1.0.0 +// Covers optional multipart bodies, optional parts, and text-only multipart fields. + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct UploadFile { + pub data: Vec, + pub filename: Option, +} + +impl UploadFile { + pub fn new(data: impl Into>, filename: impl Into) -> Self { + Self { + data: data.into(), + filename: Some(filename.into()), + } + } + + pub fn from_bytes(data: impl Into>) -> Self { + Self { + data: data.into(), + filename: None, + } + } + + pub fn filename_or_default<'a>(&'a self, default_name: &'a str) -> &'a str { + self.filename.as_deref().unwrap_or(default_name) + } +} + +pub fn multipart_header_value(value: &str) -> String { + value + .replace('\r', "") + .replace('\n', "") + .replace('\\', "\\\\") + .replace('"', "\\\"") +} 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 index 6b5dbe611..b8ee33c17 100644 --- 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 @@ -23,13 +23,13 @@ impl<'a> TransferApi<'a> { /// POST /uploads/encoded pub async fn upload_encoded_asset( &self, - body: &crate::models::UploadEncodedAssetRequest, + body: &crate::models::UploadEncodedAssetMultipartRequestBody, ) -> 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("file", reqwest::multipart::Part::bytes(body.file.data.clone()).file_name(body.file.filename_or_default("file").to_string()).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); 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 index b7af82c9b..a994e2c64 100644 --- 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 @@ -10,3 +10,5 @@ mod upload_attributes; pub use upload_attributes::*; mod upload_encoded_asset_request; pub use upload_encoded_asset_request::*; +mod upload_encoded_asset_multipart_request_body; +pub use upload_encoded_asset_multipart_request_body::*; diff --git a/tests/golden/rust/rust-reqwest/multipart-explicit-encoding/src/models/upload_encoded_asset_multipart_request_body.rs.golden b/tests/golden/rust/rust-reqwest/multipart-explicit-encoding/src/models/upload_encoded_asset_multipart_request_body.rs.golden new file mode 100644 index 000000000..440bc15b9 --- /dev/null +++ b/tests/golden/rust/rust-reqwest/multipart-explicit-encoding/src/models/upload_encoded_asset_multipart_request_body.rs.golden @@ -0,0 +1,15 @@ +// @generated +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Multipart Explicit Encoding — 1.0.0 +// Covers explicit multipart part content types. + +use crate::runtime::UploadFile; + +#[derive(Debug, Clone)] +pub struct UploadEncodedAssetMultipartRequestBody { + pub audit: super::AuditAttributes, + pub file: UploadFile, + pub metadata: super::UploadAttributes, + pub purpose: String, +} 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 index cdb1caac6..87bd15390 100644 --- 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 @@ -7,3 +7,5 @@ pub mod auth; pub mod client; pub mod error; +mod upload_file; +pub use upload_file::{multipart_header_value, UploadFile}; diff --git a/tests/golden/rust/rust-reqwest/multipart-explicit-encoding/src/runtime/upload_file.rs.golden b/tests/golden/rust/rust-reqwest/multipart-explicit-encoding/src/runtime/upload_file.rs.golden new file mode 100644 index 000000000..18a3267b4 --- /dev/null +++ b/tests/golden/rust/rust-reqwest/multipart-explicit-encoding/src/runtime/upload_file.rs.golden @@ -0,0 +1,39 @@ +// @generated +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Multipart Explicit Encoding — 1.0.0 +// Covers explicit multipart part content types. + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct UploadFile { + pub data: Vec, + pub filename: Option, +} + +impl UploadFile { + pub fn new(data: impl Into>, filename: impl Into) -> Self { + Self { + data: data.into(), + filename: Some(filename.into()), + } + } + + pub fn from_bytes(data: impl Into>) -> Self { + Self { + data: data.into(), + filename: None, + } + } + + pub fn filename_or_default<'a>(&'a self, default_name: &'a str) -> &'a str { + self.filename.as_deref().unwrap_or(default_name) + } +} + +pub fn multipart_header_value(value: &str) -> String { + value + .replace('\r', "") + .replace('\n', "") + .replace('\\', "\\\\") + .replace('"', "\\\"") +} 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 ee5c5ecdf..d44bd6ccf 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 @@ -23,12 +23,12 @@ impl<'a> MultipartApi<'a> { /// POST /multipart/nested-object pub async fn send_nested_object_part( &self, - body: &crate::models::NestedUpload, + body: &crate::models::SendNestedObjectPartMultipartRequestBody, ) -> Result { let path = "/multipart/nested-object".to_string(); 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.part("file", reqwest::multipart::Part::bytes(body.file.data.clone()).file_name(body.file.filename_or_default("file").to_string()).mime_str("application/octet-stream").map_err(Error::Network)?); 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?; diff --git a/tests/golden/rust/rust-reqwest/multipart-nested-object-parts/src/models/mod.rs.golden b/tests/golden/rust/rust-reqwest/multipart-nested-object-parts/src/models/mod.rs.golden index 2f9637c82..2873ea810 100644 --- a/tests/golden/rust/rust-reqwest/multipart-nested-object-parts/src/models/mod.rs.golden +++ b/tests/golden/rust/rust-reqwest/multipart-nested-object-parts/src/models/mod.rs.golden @@ -8,3 +8,5 @@ mod item_config; pub use item_config::*; mod nested_upload; pub use nested_upload::*; +mod send_nested_object_part_multipart_request_body; +pub use send_nested_object_part_multipart_request_body::*; diff --git a/tests/golden/rust/rust-reqwest/multipart-nested-object-parts/src/models/send_nested_object_part_multipart_request_body.rs.golden b/tests/golden/rust/rust-reqwest/multipart-nested-object-parts/src/models/send_nested_object_part_multipart_request_body.rs.golden new file mode 100644 index 000000000..306bf40cf --- /dev/null +++ b/tests/golden/rust/rust-reqwest/multipart-nested-object-parts/src/models/send_nested_object_part_multipart_request_body.rs.golden @@ -0,0 +1,13 @@ +// @generated +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Multipart Nested Object Parts — 1.0.0 +// Covers multipart object parts whose wire names differ from ergonomic names. + +use crate::runtime::UploadFile; + +#[derive(Debug, Clone)] +pub struct SendNestedObjectPartMultipartRequestBody { + pub file: UploadFile, + pub item_config: super::ItemConfig, +} diff --git a/tests/golden/rust/rust-reqwest/multipart-nested-object-parts/src/runtime/mod.rs.golden b/tests/golden/rust/rust-reqwest/multipart-nested-object-parts/src/runtime/mod.rs.golden index 61150544a..906c7a59e 100644 --- a/tests/golden/rust/rust-reqwest/multipart-nested-object-parts/src/runtime/mod.rs.golden +++ b/tests/golden/rust/rust-reqwest/multipart-nested-object-parts/src/runtime/mod.rs.golden @@ -7,3 +7,5 @@ pub mod auth; pub mod client; pub mod error; +mod upload_file; +pub use upload_file::{multipart_header_value, UploadFile}; diff --git a/tests/golden/rust/rust-reqwest/multipart-nested-object-parts/src/runtime/upload_file.rs.golden b/tests/golden/rust/rust-reqwest/multipart-nested-object-parts/src/runtime/upload_file.rs.golden new file mode 100644 index 000000000..b5c6ba9da --- /dev/null +++ b/tests/golden/rust/rust-reqwest/multipart-nested-object-parts/src/runtime/upload_file.rs.golden @@ -0,0 +1,39 @@ +// @generated +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Multipart Nested Object Parts — 1.0.0 +// Covers multipart object parts whose wire names differ from ergonomic names. + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct UploadFile { + pub data: Vec, + pub filename: Option, +} + +impl UploadFile { + pub fn new(data: impl Into>, filename: impl Into) -> Self { + Self { + data: data.into(), + filename: Some(filename.into()), + } + } + + pub fn from_bytes(data: impl Into>) -> Self { + Self { + data: data.into(), + filename: None, + } + } + + pub fn filename_or_default<'a>(&'a self, default_name: &'a str) -> &'a str { + self.filename.as_deref().unwrap_or(default_name) + } +} + +pub fn multipart_header_value(value: &str) -> String { + value + .replace('\r', "") + .replace('\n', "") + .replace('\\', "\\\\") + .replace('"', "\\\"") +} 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 e9d6a5f4c..bac34e7e6 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 @@ -44,22 +44,22 @@ impl<'a> TransferApi<'a> { /// POST /uploads pub fn upload_asset( &self, - body: &crate::models::UploadAssetRequest, + body: &crate::models::UploadAssetMultipartRequestBody, ) -> Result { let path = "/uploads".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=\"{}\"; 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(format!("Content-Disposition: form-data; name=\"{}\"; filename=\"{}\"\r\nContent-Type: {}\r\n\r\n", crate::runtime::multipart_header_value("file"), crate::runtime::multipart_header_value(&body.file.filename_or_default("file").to_string()), "application/octet-stream").as_bytes()); + multipart_body.extend_from_slice(&body.file.data.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/json").as_bytes()); + multipart_body.extend_from_slice(format!("Content-Disposition: form-data; name=\"{}\"\r\nContent-Type: {}\r\n\r\n", crate::runtime::multipart_header_value("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\nContent-Type: {}\r\n\r\n", "purpose", "text/plain").as_bytes()); + multipart_body.extend_from_slice(format!("Content-Disposition: form-data; name=\"{}\"\r\nContent-Type: {}\r\n\r\n", crate::runtime::multipart_header_value("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/binary-transfer-media-types/src/models/mod.rs.golden b/tests/golden/rust/rust-ureq/binary-transfer-media-types/src/models/mod.rs.golden index 117bf91a3..60875d3ba 100644 --- a/tests/golden/rust/rust-ureq/binary-transfer-media-types/src/models/mod.rs.golden +++ b/tests/golden/rust/rust-ureq/binary-transfer-media-types/src/models/mod.rs.golden @@ -10,3 +10,5 @@ mod upload_metadata; pub use upload_metadata::*; mod upload_asset_request; pub use upload_asset_request::*; +mod upload_asset_multipart_request_body; +pub use upload_asset_multipart_request_body::*; diff --git a/tests/golden/rust/rust-ureq/binary-transfer-media-types/src/models/upload_asset_multipart_request_body.rs.golden b/tests/golden/rust/rust-ureq/binary-transfer-media-types/src/models/upload_asset_multipart_request_body.rs.golden new file mode 100644 index 000000000..e0e849f92 --- /dev/null +++ b/tests/golden/rust/rust-ureq/binary-transfer-media-types/src/models/upload_asset_multipart_request_body.rs.golden @@ -0,0 +1,14 @@ +// @generated +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Binary Transfer Media Types — 1.0.0 +// Covers multipart upload and octet-stream download. + +use crate::runtime::UploadFile; + +#[derive(Debug, Clone)] +pub struct UploadAssetMultipartRequestBody { + pub file: UploadFile, + pub metadata: super::UploadAttributes, + pub purpose: String, +} diff --git a/tests/golden/rust/rust-ureq/binary-transfer-media-types/src/runtime/mod.rs.golden b/tests/golden/rust/rust-ureq/binary-transfer-media-types/src/runtime/mod.rs.golden index b8766221d..5d68d3229 100644 --- a/tests/golden/rust/rust-ureq/binary-transfer-media-types/src/runtime/mod.rs.golden +++ b/tests/golden/rust/rust-ureq/binary-transfer-media-types/src/runtime/mod.rs.golden @@ -7,3 +7,5 @@ pub mod auth; pub mod client; pub mod error; +mod upload_file; +pub use upload_file::{multipart_header_value, UploadFile}; diff --git a/tests/golden/rust/rust-ureq/binary-transfer-media-types/src/runtime/upload_file.rs.golden b/tests/golden/rust/rust-ureq/binary-transfer-media-types/src/runtime/upload_file.rs.golden new file mode 100644 index 000000000..8fb5b330d --- /dev/null +++ b/tests/golden/rust/rust-ureq/binary-transfer-media-types/src/runtime/upload_file.rs.golden @@ -0,0 +1,39 @@ +// @generated +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Binary Transfer Media Types — 1.0.0 +// Covers multipart upload and octet-stream download. + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct UploadFile { + pub data: Vec, + pub filename: Option, +} + +impl UploadFile { + pub fn new(data: impl Into>, filename: impl Into) -> Self { + Self { + data: data.into(), + filename: Some(filename.into()), + } + } + + pub fn from_bytes(data: impl Into>) -> Self { + Self { + data: data.into(), + filename: None, + } + } + + pub fn filename_or_default<'a>(&'a self, default_name: &'a str) -> &'a str { + self.filename.as_deref().unwrap_or(default_name) + } +} + +pub fn multipart_header_value(value: &str) -> String { + value + .replace('\r', "") + .replace('\n', "") + .replace('\\', "\\\\") + .replace('"', "\\\"") +} 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 76d9ee4bf..60bec0a59 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 @@ -35,19 +35,19 @@ impl<'a> MediaApi<'a> { /// POST /request/parameterized-multipart pub fn send_parameterized_multipart( &self, - body: &crate::models::FileEnvelope, + body: &crate::models::SendParameterizedMultipartMultipartRequestBody, ) -> Result { let path = "/request/parameterized-multipart".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=\"{}\"; 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(format!("Content-Disposition: form-data; name=\"{}\"; filename=\"{}\"\r\nContent-Type: {}\r\n\r\n", crate::runtime::multipart_header_value("file"), crate::runtime::multipart_header_value(&body.file.filename_or_default("file").to_string()), "application/octet-stream").as_bytes()); + multipart_body.extend_from_slice(&body.file.data.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\nContent-Type: {}\r\n\r\n", "note", "text/plain").as_bytes()); + multipart_body.extend_from_slice(format!("Content-Disposition: form-data; name=\"{}\"\r\nContent-Type: {}\r\n\r\n", crate::runtime::multipart_header_value("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/media-type-selection/src/models/mod.rs.golden b/tests/golden/rust/rust-ureq/media-type-selection/src/models/mod.rs.golden index f9bb2e882..c18add594 100644 --- a/tests/golden/rust/rust-ureq/media-type-selection/src/models/mod.rs.golden +++ b/tests/golden/rust/rust-ureq/media-type-selection/src/models/mod.rs.golden @@ -8,3 +8,5 @@ mod file_envelope; pub use file_envelope::*; mod payload; pub use payload::*; +mod send_parameterized_multipart_multipart_request_body; +pub use send_parameterized_multipart_multipart_request_body::*; diff --git a/tests/golden/rust/rust-ureq/media-type-selection/src/models/send_parameterized_multipart_multipart_request_body.rs.golden b/tests/golden/rust/rust-ureq/media-type-selection/src/models/send_parameterized_multipart_multipart_request_body.rs.golden new file mode 100644 index 000000000..db0b45ab9 --- /dev/null +++ b/tests/golden/rust/rust-ureq/media-type-selection/src/models/send_parameterized_multipart_multipart_request_body.rs.golden @@ -0,0 +1,13 @@ +// @generated +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Media Type Selection — 1.0.0 +// Covers normalized media-type selection for requests and responses. + +use crate::runtime::UploadFile; + +#[derive(Debug, Clone)] +pub struct SendParameterizedMultipartMultipartRequestBody { + pub file: UploadFile, + pub note: Option, +} diff --git a/tests/golden/rust/rust-ureq/media-type-selection/src/runtime/mod.rs.golden b/tests/golden/rust/rust-ureq/media-type-selection/src/runtime/mod.rs.golden index 31db0c2b8..2e077722d 100644 --- a/tests/golden/rust/rust-ureq/media-type-selection/src/runtime/mod.rs.golden +++ b/tests/golden/rust/rust-ureq/media-type-selection/src/runtime/mod.rs.golden @@ -7,3 +7,5 @@ pub mod auth; pub mod client; pub mod error; +mod upload_file; +pub use upload_file::{multipart_header_value, UploadFile}; diff --git a/tests/golden/rust/rust-ureq/media-type-selection/src/runtime/upload_file.rs.golden b/tests/golden/rust/rust-ureq/media-type-selection/src/runtime/upload_file.rs.golden new file mode 100644 index 000000000..078d5f15e --- /dev/null +++ b/tests/golden/rust/rust-ureq/media-type-selection/src/runtime/upload_file.rs.golden @@ -0,0 +1,39 @@ +// @generated +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Media Type Selection — 1.0.0 +// Covers normalized media-type selection for requests and responses. + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct UploadFile { + pub data: Vec, + pub filename: Option, +} + +impl UploadFile { + pub fn new(data: impl Into>, filename: impl Into) -> Self { + Self { + data: data.into(), + filename: Some(filename.into()), + } + } + + pub fn from_bytes(data: impl Into>) -> Self { + Self { + data: data.into(), + filename: None, + } + } + + pub fn filename_or_default<'a>(&'a self, default_name: &'a str) -> &'a str { + self.filename.as_deref().unwrap_or(default_name) + } +} + +pub fn multipart_header_value(value: &str) -> String { + value + .replace('\r', "") + .replace('\n', "") + .replace('\\', "\\\\") + .replace('"', "\\\"") +} 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 325e6c28a..e61ce318e 100644 --- a/tests/golden/rust/rust-ureq/multipart-edge-cases/src/apis/multipart.rs.golden +++ b/tests/golden/rust/rust-ureq/multipart-edge-cases/src/apis/multipart.rs.golden @@ -23,7 +23,7 @@ impl<'a> MultipartApi<'a> { /// POST /multipart/optional pub fn send_optional_parts( &self, - body: Option<&crate::models::OptionalUpload>, + body: Option<&crate::models::SendOptionalPartsMultipartRequestBody>, ) -> Result { let path = "/multipart/optional".to_string(); let mut req = self.client.post(&path); @@ -33,31 +33,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\nContent-Type: {}\r\n\r\n", "attributes", "application/json").as_bytes()); + multipart_body.extend_from_slice(format!("Content-Disposition: form-data; name=\"{}\"\r\nContent-Type: {}\r\n\r\n", crate::runtime::multipart_header_value("attributes"), "application/json").as_bytes()); multipart_body.extend_from_slice(serde_json::to_string(value)?.as_bytes()); multipart_body.extend_from_slice(b"\r\n"); } if let Some(value) = &body.enabled { multipart_body.extend_from_slice(format!("--{}\r\n", boundary).as_bytes()); - multipart_body.extend_from_slice(format!("Content-Disposition: form-data; name=\"{}\"\r\nContent-Type: {}\r\n\r\n", "enabled", "text/plain").as_bytes()); + multipart_body.extend_from_slice(format!("Content-Disposition: form-data; name=\"{}\"\r\nContent-Type: {}\r\n\r\n", crate::runtime::multipart_header_value("enabled"), "text/plain").as_bytes()); multipart_body.extend_from_slice(value.to_string().as_bytes()); multipart_body.extend_from_slice(b"\r\n"); } if let Some(value) = &body.file { multipart_body.extend_from_slice(format!("--{}\r\n", boundary).as_bytes()); - multipart_body.extend_from_slice(format!("Content-Disposition: form-data; name=\"{}\"; filename=\"{}\"\r\nContent-Type: {}\r\n\r\n", "file", "file", "application/octet-stream").as_bytes()); - multipart_body.extend_from_slice(&value.clone()); + multipart_body.extend_from_slice(format!("Content-Disposition: form-data; name=\"{}\"; filename=\"{}\"\r\nContent-Type: {}\r\n\r\n", crate::runtime::multipart_header_value("file"), crate::runtime::multipart_header_value(&value.filename_or_default("file").to_string()), "application/octet-stream").as_bytes()); + multipart_body.extend_from_slice(&value.data.clone()); multipart_body.extend_from_slice(b"\r\n"); } if let Some(value) = &body.retry_count { multipart_body.extend_from_slice(format!("--{}\r\n", boundary).as_bytes()); - multipart_body.extend_from_slice(format!("Content-Disposition: form-data; name=\"{}\"\r\nContent-Type: {}\r\n\r\n", "retry_count", "text/plain").as_bytes()); + multipart_body.extend_from_slice(format!("Content-Disposition: form-data; name=\"{}\"\r\nContent-Type: {}\r\n\r\n", crate::runtime::multipart_header_value("retry_count"), "text/plain").as_bytes()); multipart_body.extend_from_slice(value.to_string().as_bytes()); multipart_body.extend_from_slice(b"\r\n"); } if let Some(value) = &body.title { multipart_body.extend_from_slice(format!("--{}\r\n", boundary).as_bytes()); - multipart_body.extend_from_slice(format!("Content-Disposition: form-data; name=\"{}\"\r\nContent-Type: {}\r\n\r\n", "title", "text/plain").as_bytes()); + multipart_body.extend_from_slice(format!("Content-Disposition: form-data; name=\"{}\"\r\nContent-Type: {}\r\n\r\n", crate::runtime::multipart_header_value("title"), "text/plain").as_bytes()); multipart_body.extend_from_slice(value.to_string().as_bytes()); multipart_body.extend_from_slice(b"\r\n"); } @@ -75,22 +75,22 @@ impl<'a> MultipartApi<'a> { /// POST /multipart/text-only pub fn send_text_fields( &self, - body: &crate::models::TextFields, + body: &crate::models::SendTextFieldsMultipartRequestBody, ) -> Result { let path = "/multipart/text-only".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", "enabled", "text/plain").as_bytes()); + multipart_body.extend_from_slice(format!("Content-Disposition: form-data; name=\"{}\"\r\nContent-Type: {}\r\n\r\n", crate::runtime::multipart_header_value("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\nContent-Type: {}\r\n\r\n", "note", "text/plain").as_bytes()); + multipart_body.extend_from_slice(format!("Content-Disposition: form-data; name=\"{}\"\r\nContent-Type: {}\r\n\r\n", crate::runtime::multipart_header_value("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\nContent-Type: {}\r\n\r\n", "retry_count", "text/plain").as_bytes()); + multipart_body.extend_from_slice(format!("Content-Disposition: form-data; name=\"{}\"\r\nContent-Type: {}\r\n\r\n", crate::runtime::multipart_header_value("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-edge-cases/src/models/mod.rs.golden b/tests/golden/rust/rust-ureq/multipart-edge-cases/src/models/mod.rs.golden index 97c6c9a6b..9a57d0a2a 100644 --- a/tests/golden/rust/rust-ureq/multipart-edge-cases/src/models/mod.rs.golden +++ b/tests/golden/rust/rust-ureq/multipart-edge-cases/src/models/mod.rs.golden @@ -10,3 +10,7 @@ mod optional_upload; pub use optional_upload::*; mod text_fields; pub use text_fields::*; +mod send_optional_parts_multipart_request_body; +pub use send_optional_parts_multipart_request_body::*; +mod send_text_fields_multipart_request_body; +pub use send_text_fields_multipart_request_body::*; diff --git a/tests/golden/rust/rust-ureq/multipart-edge-cases/src/models/send_optional_parts_multipart_request_body.rs.golden b/tests/golden/rust/rust-ureq/multipart-edge-cases/src/models/send_optional_parts_multipart_request_body.rs.golden new file mode 100644 index 000000000..16f36eae6 --- /dev/null +++ b/tests/golden/rust/rust-ureq/multipart-edge-cases/src/models/send_optional_parts_multipart_request_body.rs.golden @@ -0,0 +1,16 @@ +// @generated +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Multipart Edge Cases — 1.0.0 +// Covers optional multipart bodies, optional parts, and text-only multipart fields. + +use crate::runtime::UploadFile; + +#[derive(Debug, Clone)] +pub struct SendOptionalPartsMultipartRequestBody { + pub attributes: Option, + pub enabled: Option, + pub file: Option, + pub retry_count: Option, + pub title: Option, +} diff --git a/tests/golden/rust/rust-ureq/multipart-edge-cases/src/models/send_text_fields_multipart_request_body.rs.golden b/tests/golden/rust/rust-ureq/multipart-edge-cases/src/models/send_text_fields_multipart_request_body.rs.golden new file mode 100644 index 000000000..3afc2b599 --- /dev/null +++ b/tests/golden/rust/rust-ureq/multipart-edge-cases/src/models/send_text_fields_multipart_request_body.rs.golden @@ -0,0 +1,12 @@ +// @generated +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Multipart Edge Cases — 1.0.0 +// Covers optional multipart bodies, optional parts, and text-only multipart fields. + +#[derive(Debug, Clone)] +pub struct SendTextFieldsMultipartRequestBody { + pub enabled: bool, + pub note: String, + pub retry_count: i64, +} diff --git a/tests/golden/rust/rust-ureq/multipart-edge-cases/src/runtime/mod.rs.golden b/tests/golden/rust/rust-ureq/multipart-edge-cases/src/runtime/mod.rs.golden index ef7165a41..e4e8d7a7d 100644 --- a/tests/golden/rust/rust-ureq/multipart-edge-cases/src/runtime/mod.rs.golden +++ b/tests/golden/rust/rust-ureq/multipart-edge-cases/src/runtime/mod.rs.golden @@ -7,3 +7,5 @@ pub mod auth; pub mod client; pub mod error; +mod upload_file; +pub use upload_file::{multipart_header_value, UploadFile}; diff --git a/tests/golden/rust/rust-ureq/multipart-edge-cases/src/runtime/upload_file.rs.golden b/tests/golden/rust/rust-ureq/multipart-edge-cases/src/runtime/upload_file.rs.golden new file mode 100644 index 000000000..7447e35ee --- /dev/null +++ b/tests/golden/rust/rust-ureq/multipart-edge-cases/src/runtime/upload_file.rs.golden @@ -0,0 +1,39 @@ +// @generated +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Multipart Edge Cases — 1.0.0 +// Covers optional multipart bodies, optional parts, and text-only multipart fields. + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct UploadFile { + pub data: Vec, + pub filename: Option, +} + +impl UploadFile { + pub fn new(data: impl Into>, filename: impl Into) -> Self { + Self { + data: data.into(), + filename: Some(filename.into()), + } + } + + pub fn from_bytes(data: impl Into>) -> Self { + Self { + data: data.into(), + filename: None, + } + } + + pub fn filename_or_default<'a>(&'a self, default_name: &'a str) -> &'a str { + self.filename.as_deref().unwrap_or(default_name) + } +} + +pub fn multipart_header_value(value: &str) -> String { + value + .replace('\r', "") + .replace('\n', "") + .replace('\\', "\\\\") + .replace('"', "\\\"") +} 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 index 607645b54..429283ab6 100644 --- 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 @@ -23,26 +23,26 @@ impl<'a> TransferApi<'a> { /// POST /uploads/encoded pub fn upload_encoded_asset( &self, - body: &crate::models::UploadEncodedAssetRequest, + body: &crate::models::UploadEncodedAssetMultipartRequestBody, ) -> 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(format!("Content-Disposition: form-data; name=\"{}\"\r\nContent-Type: {}\r\n\r\n", crate::runtime::multipart_header_value("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(format!("Content-Disposition: form-data; name=\"{}\"; filename=\"{}\"\r\nContent-Type: {}\r\n\r\n", crate::runtime::multipart_header_value("file"), crate::runtime::multipart_header_value(&body.file.filename_or_default("file").to_string()), "application/pdf").as_bytes()); + multipart_body.extend_from_slice(&body.file.data.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(format!("Content-Disposition: form-data; name=\"{}\"\r\nContent-Type: {}\r\n\r\n", crate::runtime::multipart_header_value("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(format!("Content-Disposition: form-data; name=\"{}\"\r\nContent-Type: {}\r\n\r\n", crate::runtime::multipart_header_value("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/multipart-explicit-encoding/src/models/mod.rs.golden b/tests/golden/rust/rust-ureq/multipart-explicit-encoding/src/models/mod.rs.golden index b7af82c9b..a994e2c64 100644 --- 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 @@ -10,3 +10,5 @@ mod upload_attributes; pub use upload_attributes::*; mod upload_encoded_asset_request; pub use upload_encoded_asset_request::*; +mod upload_encoded_asset_multipart_request_body; +pub use upload_encoded_asset_multipart_request_body::*; diff --git a/tests/golden/rust/rust-ureq/multipart-explicit-encoding/src/models/upload_encoded_asset_multipart_request_body.rs.golden b/tests/golden/rust/rust-ureq/multipart-explicit-encoding/src/models/upload_encoded_asset_multipart_request_body.rs.golden new file mode 100644 index 000000000..440bc15b9 --- /dev/null +++ b/tests/golden/rust/rust-ureq/multipart-explicit-encoding/src/models/upload_encoded_asset_multipart_request_body.rs.golden @@ -0,0 +1,15 @@ +// @generated +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Multipart Explicit Encoding — 1.0.0 +// Covers explicit multipart part content types. + +use crate::runtime::UploadFile; + +#[derive(Debug, Clone)] +pub struct UploadEncodedAssetMultipartRequestBody { + pub audit: super::AuditAttributes, + pub file: UploadFile, + pub metadata: super::UploadAttributes, + pub purpose: String, +} 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 index cdb1caac6..87bd15390 100644 --- 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 @@ -7,3 +7,5 @@ pub mod auth; pub mod client; pub mod error; +mod upload_file; +pub use upload_file::{multipart_header_value, UploadFile}; diff --git a/tests/golden/rust/rust-ureq/multipart-explicit-encoding/src/runtime/upload_file.rs.golden b/tests/golden/rust/rust-ureq/multipart-explicit-encoding/src/runtime/upload_file.rs.golden new file mode 100644 index 000000000..18a3267b4 --- /dev/null +++ b/tests/golden/rust/rust-ureq/multipart-explicit-encoding/src/runtime/upload_file.rs.golden @@ -0,0 +1,39 @@ +// @generated +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Multipart Explicit Encoding — 1.0.0 +// Covers explicit multipart part content types. + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct UploadFile { + pub data: Vec, + pub filename: Option, +} + +impl UploadFile { + pub fn new(data: impl Into>, filename: impl Into) -> Self { + Self { + data: data.into(), + filename: Some(filename.into()), + } + } + + pub fn from_bytes(data: impl Into>) -> Self { + Self { + data: data.into(), + filename: None, + } + } + + pub fn filename_or_default<'a>(&'a self, default_name: &'a str) -> &'a str { + self.filename.as_deref().unwrap_or(default_name) + } +} + +pub fn multipart_header_value(value: &str) -> String { + value + .replace('\r', "") + .replace('\n', "") + .replace('\\', "\\\\") + .replace('"', "\\\"") +} 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 ddfc74177..0348aa705 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 @@ -23,18 +23,18 @@ impl<'a> MultipartApi<'a> { /// POST /multipart/nested-object pub fn send_nested_object_part( &self, - body: &crate::models::NestedUpload, + body: &crate::models::SendNestedObjectPartMultipartRequestBody, ) -> Result { let path = "/multipart/nested-object".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=\"{}\"; 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(format!("Content-Disposition: form-data; name=\"{}\"; filename=\"{}\"\r\nContent-Type: {}\r\n\r\n", crate::runtime::multipart_header_value("file"), crate::runtime::multipart_header_value(&body.file.filename_or_default("file").to_string()), "application/octet-stream").as_bytes()); + multipart_body.extend_from_slice(&body.file.data.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", "item_config", "application/json").as_bytes()); + multipart_body.extend_from_slice(format!("Content-Disposition: form-data; name=\"{}\"\r\nContent-Type: {}\r\n\r\n", crate::runtime::multipart_header_value("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/rust/rust-ureq/multipart-nested-object-parts/src/models/mod.rs.golden b/tests/golden/rust/rust-ureq/multipart-nested-object-parts/src/models/mod.rs.golden index 2f9637c82..2873ea810 100644 --- a/tests/golden/rust/rust-ureq/multipart-nested-object-parts/src/models/mod.rs.golden +++ b/tests/golden/rust/rust-ureq/multipart-nested-object-parts/src/models/mod.rs.golden @@ -8,3 +8,5 @@ mod item_config; pub use item_config::*; mod nested_upload; pub use nested_upload::*; +mod send_nested_object_part_multipart_request_body; +pub use send_nested_object_part_multipart_request_body::*; diff --git a/tests/golden/rust/rust-ureq/multipart-nested-object-parts/src/models/send_nested_object_part_multipart_request_body.rs.golden b/tests/golden/rust/rust-ureq/multipart-nested-object-parts/src/models/send_nested_object_part_multipart_request_body.rs.golden new file mode 100644 index 000000000..306bf40cf --- /dev/null +++ b/tests/golden/rust/rust-ureq/multipart-nested-object-parts/src/models/send_nested_object_part_multipart_request_body.rs.golden @@ -0,0 +1,13 @@ +// @generated +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Multipart Nested Object Parts — 1.0.0 +// Covers multipart object parts whose wire names differ from ergonomic names. + +use crate::runtime::UploadFile; + +#[derive(Debug, Clone)] +pub struct SendNestedObjectPartMultipartRequestBody { + pub file: UploadFile, + pub item_config: super::ItemConfig, +} diff --git a/tests/golden/rust/rust-ureq/multipart-nested-object-parts/src/runtime/mod.rs.golden b/tests/golden/rust/rust-ureq/multipart-nested-object-parts/src/runtime/mod.rs.golden index 61150544a..906c7a59e 100644 --- a/tests/golden/rust/rust-ureq/multipart-nested-object-parts/src/runtime/mod.rs.golden +++ b/tests/golden/rust/rust-ureq/multipart-nested-object-parts/src/runtime/mod.rs.golden @@ -7,3 +7,5 @@ pub mod auth; pub mod client; pub mod error; +mod upload_file; +pub use upload_file::{multipart_header_value, UploadFile}; diff --git a/tests/golden/rust/rust-ureq/multipart-nested-object-parts/src/runtime/upload_file.rs.golden b/tests/golden/rust/rust-ureq/multipart-nested-object-parts/src/runtime/upload_file.rs.golden new file mode 100644 index 000000000..b5c6ba9da --- /dev/null +++ b/tests/golden/rust/rust-ureq/multipart-nested-object-parts/src/runtime/upload_file.rs.golden @@ -0,0 +1,39 @@ +// @generated +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Multipart Nested Object Parts — 1.0.0 +// Covers multipart object parts whose wire names differ from ergonomic names. + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct UploadFile { + pub data: Vec, + pub filename: Option, +} + +impl UploadFile { + pub fn new(data: impl Into>, filename: impl Into) -> Self { + Self { + data: data.into(), + filename: Some(filename.into()), + } + } + + pub fn from_bytes(data: impl Into>) -> Self { + Self { + data: data.into(), + filename: None, + } + } + + pub fn filename_or_default<'a>(&'a self, default_name: &'a str) -> &'a str { + self.filename.as_deref().unwrap_or(default_name) + } +} + +pub fn multipart_header_value(value: &str) -> String { + value + .replace('\r', "") + .replace('\n', "") + .replace('\\', "\\\\") + .replace('"', "\\\"") +} 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 6ae487105..fa2dc0250 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 @@ -4,17 +4,17 @@ * Binary Transfer Media Types — 1.0.0 * Covers multipart upload and octet-stream download. */ -import type { UploadAssetRequest } from '../models/UploadAssetRequest'; +import type { UploadAssetMultipartRequestBody } from '../models/UploadAssetMultipartRequestBody'; import type { UploadMetadata } from '../models/UploadMetadata'; import type { Configuration, HTTPQuery, InitOverrideFunction } from '../runtime/runtime'; -import { BaseAPI, BlobApiResponse, DefaultConfig, JSONApiResponse, RequiredError, VoidApiResponse } from '../runtime/runtime'; +import { BaseAPI, BlobApiResponse, DefaultConfig, JSONApiResponse, RequiredError, VoidApiResponse, multipartHeaderValue, uploadFileData, uploadFileFilename } from '../runtime/runtime'; export interface ApiDownloadAssetRequest { assetId: string; } export interface ApiUploadAssetRequest { - body: UploadAssetRequest; + body: UploadAssetMultipartRequestBody; } export type DownloadAssetRawResponse = @@ -107,9 +107,12 @@ export class TransferApi extends BaseAPI implements TransferApiInterface { 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); + { + const multipartFilename = uploadFileFilename(requestParameters.body.file, 'file'); + multipartChunks.push('--' + multipartBoundary + '\r\nContent-Disposition: form-data; name="file"; filename="' + multipartHeaderValue(multipartFilename) + '"\r\nContent-Type: application/octet-stream\r\n\r\n'); + multipartChunks.push(uploadFileData(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'); diff --git a/tests/golden/typescript/typescript-fetch/binary-transfer-media-types/models/UploadAssetMultipartRequestBody.ts.golden b/tests/golden/typescript/typescript-fetch/binary-transfer-media-types/models/UploadAssetMultipartRequestBody.ts.golden new file mode 100644 index 000000000..8a2c25650 --- /dev/null +++ b/tests/golden/typescript/typescript-fetch/binary-transfer-media-types/models/UploadAssetMultipartRequestBody.ts.golden @@ -0,0 +1,14 @@ +/** + * @generated by openapi-nexus. Do not edit. + * + * Binary Transfer Media Types — 1.0.0 + * Covers multipart upload and octet-stream download. + */ +import type { UploadFileInput } from '../runtime/runtime'; +import type { UploadAttributes } from './UploadAttributes'; + +export interface UploadAssetMultipartRequestBody { + file: UploadFileInput; + metadata: UploadAttributes; + purpose: string; +} diff --git a/tests/golden/typescript/typescript-fetch/binary-transfer-media-types/models/index.ts.golden b/tests/golden/typescript/typescript-fetch/binary-transfer-media-types/models/index.ts.golden index fc17edafc..717f4bf8d 100644 --- a/tests/golden/typescript/typescript-fetch/binary-transfer-media-types/models/index.ts.golden +++ b/tests/golden/typescript/typescript-fetch/binary-transfer-media-types/models/index.ts.golden @@ -7,3 +7,4 @@ export type { UploadAttributes } from './UploadAttributes'; export type { UploadMetadata } from './UploadMetadata'; export type { UploadAssetRequest } from './UploadAssetRequest'; +export type { UploadAssetMultipartRequestBody } from './UploadAssetMultipartRequestBody'; diff --git a/tests/golden/typescript/typescript-fetch/binary-transfer-media-types/runtime/runtime.ts.golden b/tests/golden/typescript/typescript-fetch/binary-transfer-media-types/runtime/runtime.ts.golden index d1de0494a..e1a47408c 100644 --- a/tests/golden/typescript/typescript-fetch/binary-transfer-media-types/runtime/runtime.ts.golden +++ b/tests/golden/typescript/typescript-fetch/binary-transfer-media-types/runtime/runtime.ts.golden @@ -85,6 +85,39 @@ export class Configuration { export const DefaultConfig = new Configuration(); +export interface UploadFile { + data: Blob; + filename?: string; +} + +export type UploadFileInput = Blob | File | UploadFile; + +export function uploadFileData(value: UploadFileInput): Blob { + return isUploadFile(value) ? value.data : value; +} + +export function uploadFileFilename(value: UploadFileInput, fallback: string): string { + if (isFile(value) && value.name) { + return value.name; + } + if (isUploadFile(value) && value.filename) { + return value.filename; + } + return fallback; +} + +export function multipartHeaderValue(value: string): string { + return value.replace(/[\r\n]/g, '').replace(/\\/g, '\\\\').replace(/"/g, '\\"'); +} + +function isUploadFile(value: UploadFileInput): value is UploadFile { + return typeof value === 'object' && value !== null && 'data' in value; +} + +function isFile(value: UploadFileInput): value is File { + return typeof File !== 'undefined' && value instanceof File; +} + /** * This is the base class for all generated API classes. */ 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 e0c8fcab7..c18a64fe0 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 @@ -4,17 +4,17 @@ * Media Type Selection — 1.0.0 * Covers normalized media-type selection for requests and responses. */ -import type { FileEnvelope } from '../models/FileEnvelope'; import type { Payload } from '../models/Payload'; +import type { SendParameterizedMultipartMultipartRequestBody } from '../models/SendParameterizedMultipartMultipartRequestBody'; import type { Configuration, HTTPQuery, InitOverrideFunction } from '../runtime/runtime'; -import { BaseAPI, BlobApiResponse, DefaultConfig, JSONApiResponse, RequiredError, TextApiResponse, VoidApiResponse } from '../runtime/runtime'; +import { BaseAPI, BlobApiResponse, DefaultConfig, JSONApiResponse, RequiredError, TextApiResponse, VoidApiResponse, multipartHeaderValue, uploadFileData, uploadFileFilename } from '../runtime/runtime'; export interface ApiSendJsonPreferredRequest { body: Payload; } export interface ApiSendParameterizedMultipartRequest { - body: FileEnvelope; + body: SendParameterizedMultipartMultipartRequestBody; } export interface ApiSendVendorJsonRequest { @@ -132,9 +132,12 @@ export class MediaApi extends BaseAPI implements MediaApiInterface { 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); + { + const multipartFilename = uploadFileFilename(requestParameters.body.file, 'file'); + multipartChunks.push('--' + multipartBoundary + '\r\nContent-Disposition: form-data; name="file"; filename="' + multipartHeaderValue(multipartFilename) + '"\r\nContent-Type: application/octet-stream\r\n\r\n'); + multipartChunks.push(uploadFileData(requestParameters.body.file)); multipartChunks.push('\r\n'); + } if (requestParameters.body.note !== undefined && requestParameters.body.note !== null) { 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)); diff --git a/tests/golden/typescript/typescript-fetch/media-type-selection/models/SendParameterizedMultipartMultipartRequestBody.ts.golden b/tests/golden/typescript/typescript-fetch/media-type-selection/models/SendParameterizedMultipartMultipartRequestBody.ts.golden new file mode 100644 index 000000000..5a6cd44e2 --- /dev/null +++ b/tests/golden/typescript/typescript-fetch/media-type-selection/models/SendParameterizedMultipartMultipartRequestBody.ts.golden @@ -0,0 +1,12 @@ +/** + * @generated by openapi-nexus. Do not edit. + * + * Media Type Selection — 1.0.0 + * Covers normalized media-type selection for requests and responses. + */ +import type { UploadFileInput } from '../runtime/runtime'; + +export interface SendParameterizedMultipartMultipartRequestBody { + file: UploadFileInput; + note?: string; +} diff --git a/tests/golden/typescript/typescript-fetch/media-type-selection/models/index.ts.golden b/tests/golden/typescript/typescript-fetch/media-type-selection/models/index.ts.golden index e2ee19b27..a4b3b2484 100644 --- a/tests/golden/typescript/typescript-fetch/media-type-selection/models/index.ts.golden +++ b/tests/golden/typescript/typescript-fetch/media-type-selection/models/index.ts.golden @@ -6,3 +6,4 @@ */ export type { FileEnvelope } from './FileEnvelope'; export type { Payload } from './Payload'; +export type { SendParameterizedMultipartMultipartRequestBody } from './SendParameterizedMultipartMultipartRequestBody'; diff --git a/tests/golden/typescript/typescript-fetch/media-type-selection/runtime/runtime.ts.golden b/tests/golden/typescript/typescript-fetch/media-type-selection/runtime/runtime.ts.golden index 0343f0502..c51ce860b 100644 --- a/tests/golden/typescript/typescript-fetch/media-type-selection/runtime/runtime.ts.golden +++ b/tests/golden/typescript/typescript-fetch/media-type-selection/runtime/runtime.ts.golden @@ -85,6 +85,39 @@ export class Configuration { export const DefaultConfig = new Configuration(); +export interface UploadFile { + data: Blob; + filename?: string; +} + +export type UploadFileInput = Blob | File | UploadFile; + +export function uploadFileData(value: UploadFileInput): Blob { + return isUploadFile(value) ? value.data : value; +} + +export function uploadFileFilename(value: UploadFileInput, fallback: string): string { + if (isFile(value) && value.name) { + return value.name; + } + if (isUploadFile(value) && value.filename) { + return value.filename; + } + return fallback; +} + +export function multipartHeaderValue(value: string): string { + return value.replace(/[\r\n]/g, '').replace(/\\/g, '\\\\').replace(/"/g, '\\"'); +} + +function isUploadFile(value: UploadFileInput): value is UploadFile { + return typeof value === 'object' && value !== null && 'data' in value; +} + +function isFile(value: UploadFileInput): value is File { + return typeof File !== 'undefined' && value instanceof File; +} + /** * This is the base class for all generated API classes. */ 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 00de0eec5..c5771ec41 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 @@ -4,17 +4,17 @@ * Multipart Edge Cases — 1.0.0 * Covers optional multipart bodies, optional parts, and text-only multipart fields. */ -import type { OptionalUpload } from '../models/OptionalUpload'; -import type { TextFields } from '../models/TextFields'; +import type { SendOptionalPartsMultipartRequestBody } from '../models/SendOptionalPartsMultipartRequestBody'; +import type { SendTextFieldsMultipartRequestBody } from '../models/SendTextFieldsMultipartRequestBody'; import type { Configuration, HTTPQuery, InitOverrideFunction } from '../runtime/runtime'; -import { BaseAPI, DefaultConfig, RequiredError, VoidApiResponse } from '../runtime/runtime'; +import { BaseAPI, DefaultConfig, RequiredError, VoidApiResponse, multipartHeaderValue, uploadFileData, uploadFileFilename } from '../runtime/runtime'; export interface ApiSendOptionalPartsRequest { - body?: OptionalUpload; + body?: SendOptionalPartsMultipartRequestBody; } export interface ApiSendTextFieldsRequest { - body: TextFields; + body: SendTextFieldsMultipartRequestBody; } export type SendOptionalPartsRawResponse = @@ -68,10 +68,13 @@ export class MultipartApi extends BaseAPI implements MultipartApiInterface { multipartChunks.push('\r\n'); } if (requestParameters.body.file !== undefined && requestParameters.body.file !== null) { - 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); + { + const multipartFilename = uploadFileFilename(requestParameters.body.file, 'file'); + multipartChunks.push('--' + multipartBoundary + '\r\nContent-Disposition: form-data; name="file"; filename="' + multipartHeaderValue(multipartFilename) + '"\r\nContent-Type: application/octet-stream\r\n\r\n'); + multipartChunks.push(uploadFileData(requestParameters.body.file)); multipartChunks.push('\r\n'); } + } if (requestParameters.body.retry_count !== undefined && requestParameters.body.retry_count !== null) { 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)); diff --git a/tests/golden/typescript/typescript-fetch/multipart-edge-cases/models/SendOptionalPartsMultipartRequestBody.ts.golden b/tests/golden/typescript/typescript-fetch/multipart-edge-cases/models/SendOptionalPartsMultipartRequestBody.ts.golden new file mode 100644 index 000000000..70f326f23 --- /dev/null +++ b/tests/golden/typescript/typescript-fetch/multipart-edge-cases/models/SendOptionalPartsMultipartRequestBody.ts.golden @@ -0,0 +1,16 @@ +/** + * @generated by openapi-nexus. Do not edit. + * + * Multipart Edge Cases — 1.0.0 + * Covers optional multipart bodies, optional parts, and text-only multipart fields. + */ +import type { UploadFileInput } from '../runtime/runtime'; +import type { Attributes } from './Attributes'; + +export interface SendOptionalPartsMultipartRequestBody { + attributes?: Attributes; + enabled?: boolean; + file?: UploadFileInput; + retry_count?: number; + title?: string; +} diff --git a/tests/golden/typescript/typescript-fetch/multipart-edge-cases/models/SendTextFieldsMultipartRequestBody.ts.golden b/tests/golden/typescript/typescript-fetch/multipart-edge-cases/models/SendTextFieldsMultipartRequestBody.ts.golden new file mode 100644 index 000000000..9df1a2466 --- /dev/null +++ b/tests/golden/typescript/typescript-fetch/multipart-edge-cases/models/SendTextFieldsMultipartRequestBody.ts.golden @@ -0,0 +1,12 @@ +/** + * @generated by openapi-nexus. Do not edit. + * + * Multipart Edge Cases — 1.0.0 + * Covers optional multipart bodies, optional parts, and text-only multipart fields. + */ + +export interface SendTextFieldsMultipartRequestBody { + enabled: boolean; + note: string; + retry_count: number; +} diff --git a/tests/golden/typescript/typescript-fetch/multipart-edge-cases/models/index.ts.golden b/tests/golden/typescript/typescript-fetch/multipart-edge-cases/models/index.ts.golden index db8b8692e..9f8fea93b 100644 --- a/tests/golden/typescript/typescript-fetch/multipart-edge-cases/models/index.ts.golden +++ b/tests/golden/typescript/typescript-fetch/multipart-edge-cases/models/index.ts.golden @@ -7,3 +7,5 @@ export type { Attributes } from './Attributes'; export type { OptionalUpload } from './OptionalUpload'; export type { TextFields } from './TextFields'; +export type { SendOptionalPartsMultipartRequestBody } from './SendOptionalPartsMultipartRequestBody'; +export type { SendTextFieldsMultipartRequestBody } from './SendTextFieldsMultipartRequestBody'; diff --git a/tests/golden/typescript/typescript-fetch/multipart-edge-cases/runtime/runtime.ts.golden b/tests/golden/typescript/typescript-fetch/multipart-edge-cases/runtime/runtime.ts.golden index 365f9872a..27245db24 100644 --- a/tests/golden/typescript/typescript-fetch/multipart-edge-cases/runtime/runtime.ts.golden +++ b/tests/golden/typescript/typescript-fetch/multipart-edge-cases/runtime/runtime.ts.golden @@ -85,6 +85,39 @@ export class Configuration { export const DefaultConfig = new Configuration(); +export interface UploadFile { + data: Blob; + filename?: string; +} + +export type UploadFileInput = Blob | File | UploadFile; + +export function uploadFileData(value: UploadFileInput): Blob { + return isUploadFile(value) ? value.data : value; +} + +export function uploadFileFilename(value: UploadFileInput, fallback: string): string { + if (isFile(value) && value.name) { + return value.name; + } + if (isUploadFile(value) && value.filename) { + return value.filename; + } + return fallback; +} + +export function multipartHeaderValue(value: string): string { + return value.replace(/[\r\n]/g, '').replace(/\\/g, '\\\\').replace(/"/g, '\\"'); +} + +function isUploadFile(value: UploadFileInput): value is UploadFile { + return typeof value === 'object' && value !== null && 'data' in value; +} + +function isFile(value: UploadFileInput): value is File { + return typeof File !== 'undefined' && value instanceof File; +} + /** * This is the base class for all generated API classes. */ 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 index 61d048ee4..3a023ac8c 100644 --- 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 @@ -4,12 +4,12 @@ * Multipart Explicit Encoding — 1.0.0 * Covers explicit multipart part content types. */ -import type { UploadEncodedAssetRequest } from '../models/UploadEncodedAssetRequest'; +import type { UploadEncodedAssetMultipartRequestBody } from '../models/UploadEncodedAssetMultipartRequestBody'; import type { Configuration, HTTPQuery, InitOverrideFunction } from '../runtime/runtime'; -import { BaseAPI, DefaultConfig, RequiredError, VoidApiResponse } from '../runtime/runtime'; +import { BaseAPI, DefaultConfig, RequiredError, VoidApiResponse, multipartHeaderValue, uploadFileData, uploadFileFilename } from '../runtime/runtime'; export interface ApiUploadEncodedAssetRequest { - body: UploadEncodedAssetRequest; + body: UploadEncodedAssetMultipartRequestBody; } export type UploadEncodedAssetRawResponse = @@ -53,9 +53,12 @@ export class TransferApi extends BaseAPI implements TransferApiInterface { 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); + { + const multipartFilename = uploadFileFilename(requestParameters.body.file, 'file'); + multipartChunks.push('--' + multipartBoundary + '\r\nContent-Disposition: form-data; name="file"; filename="' + multipartHeaderValue(multipartFilename) + '"\r\nContent-Type: application/pdf\r\n\r\n'); + multipartChunks.push(uploadFileData(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'); diff --git a/tests/golden/typescript/typescript-fetch/multipart-explicit-encoding/models/UploadEncodedAssetMultipartRequestBody.ts.golden b/tests/golden/typescript/typescript-fetch/multipart-explicit-encoding/models/UploadEncodedAssetMultipartRequestBody.ts.golden new file mode 100644 index 000000000..ab90ff73c --- /dev/null +++ b/tests/golden/typescript/typescript-fetch/multipart-explicit-encoding/models/UploadEncodedAssetMultipartRequestBody.ts.golden @@ -0,0 +1,16 @@ +/** + * @generated by openapi-nexus. Do not edit. + * + * Multipart Explicit Encoding — 1.0.0 + * Covers explicit multipart part content types. + */ +import type { UploadFileInput } from '../runtime/runtime'; +import type { AuditAttributes } from './AuditAttributes'; +import type { UploadAttributes } from './UploadAttributes'; + +export interface UploadEncodedAssetMultipartRequestBody { + audit: AuditAttributes; + file: UploadFileInput; + metadata: UploadAttributes; + 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 index 3a9e838fb..98833dde0 100644 --- 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 @@ -7,3 +7,4 @@ export type { AuditAttributes } from './AuditAttributes'; export type { UploadAttributes } from './UploadAttributes'; export type { UploadEncodedAssetRequest } from './UploadEncodedAssetRequest'; +export type { UploadEncodedAssetMultipartRequestBody } from './UploadEncodedAssetMultipartRequestBody'; 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 index b5dc88774..311e2bbcf 100644 --- 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 @@ -85,6 +85,39 @@ export class Configuration { export const DefaultConfig = new Configuration(); +export interface UploadFile { + data: Blob; + filename?: string; +} + +export type UploadFileInput = Blob | File | UploadFile; + +export function uploadFileData(value: UploadFileInput): Blob { + return isUploadFile(value) ? value.data : value; +} + +export function uploadFileFilename(value: UploadFileInput, fallback: string): string { + if (isFile(value) && value.name) { + return value.name; + } + if (isUploadFile(value) && value.filename) { + return value.filename; + } + return fallback; +} + +export function multipartHeaderValue(value: string): string { + return value.replace(/[\r\n]/g, '').replace(/\\/g, '\\\\').replace(/"/g, '\\"'); +} + +function isUploadFile(value: UploadFileInput): value is UploadFile { + return typeof value === 'object' && value !== null && 'data' in value; +} + +function isFile(value: UploadFileInput): value is File { + return typeof File !== 'undefined' && value instanceof File; +} + /** * This is the base class for all generated API classes. */ 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 345fc5eeb..7ae4e013d 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 @@ -4,12 +4,12 @@ * Multipart Nested Object Parts — 1.0.0 * Covers multipart object parts whose wire names differ from ergonomic names. */ -import type { NestedUpload } from '../models/NestedUpload'; +import type { SendNestedObjectPartMultipartRequestBody } from '../models/SendNestedObjectPartMultipartRequestBody'; import type { Configuration, HTTPQuery, InitOverrideFunction } from '../runtime/runtime'; -import { BaseAPI, DefaultConfig, RequiredError, VoidApiResponse } from '../runtime/runtime'; +import { BaseAPI, DefaultConfig, RequiredError, VoidApiResponse, multipartHeaderValue, uploadFileData, uploadFileFilename } from '../runtime/runtime'; export interface ApiSendNestedObjectPartRequest { - body: NestedUpload; + body: SendNestedObjectPartMultipartRequestBody; } export type SendNestedObjectPartRawResponse = @@ -50,9 +50,12 @@ export class MultipartApi extends BaseAPI implements MultipartApiInterface { 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); + { + const multipartFilename = uploadFileFilename(requestParameters.body.file, 'file'); + multipartChunks.push('--' + multipartBoundary + '\r\nContent-Disposition: form-data; name="file"; filename="' + multipartHeaderValue(multipartFilename) + '"\r\nContent-Type: application/octet-stream\r\n\r\n'); + multipartChunks.push(uploadFileData(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'); diff --git a/tests/golden/typescript/typescript-fetch/multipart-nested-object-parts/models/SendNestedObjectPartMultipartRequestBody.ts.golden b/tests/golden/typescript/typescript-fetch/multipart-nested-object-parts/models/SendNestedObjectPartMultipartRequestBody.ts.golden new file mode 100644 index 000000000..9bd8d29dc --- /dev/null +++ b/tests/golden/typescript/typescript-fetch/multipart-nested-object-parts/models/SendNestedObjectPartMultipartRequestBody.ts.golden @@ -0,0 +1,13 @@ +/** + * @generated by openapi-nexus. Do not edit. + * + * Multipart Nested Object Parts — 1.0.0 + * Covers multipart object parts whose wire names differ from ergonomic names. + */ +import type { UploadFileInput } from '../runtime/runtime'; +import type { ItemConfig } from './ItemConfig'; + +export interface SendNestedObjectPartMultipartRequestBody { + file: UploadFileInput; + item_config: ItemConfig; +} diff --git a/tests/golden/typescript/typescript-fetch/multipart-nested-object-parts/models/index.ts.golden b/tests/golden/typescript/typescript-fetch/multipart-nested-object-parts/models/index.ts.golden index 5c9c11b59..8d5d5b0fb 100644 --- a/tests/golden/typescript/typescript-fetch/multipart-nested-object-parts/models/index.ts.golden +++ b/tests/golden/typescript/typescript-fetch/multipart-nested-object-parts/models/index.ts.golden @@ -6,3 +6,4 @@ */ export type { ItemConfig } from './ItemConfig'; export type { NestedUpload } from './NestedUpload'; +export type { SendNestedObjectPartMultipartRequestBody } from './SendNestedObjectPartMultipartRequestBody'; diff --git a/tests/golden/typescript/typescript-fetch/multipart-nested-object-parts/runtime/runtime.ts.golden b/tests/golden/typescript/typescript-fetch/multipart-nested-object-parts/runtime/runtime.ts.golden index be8df9abe..437168e05 100644 --- a/tests/golden/typescript/typescript-fetch/multipart-nested-object-parts/runtime/runtime.ts.golden +++ b/tests/golden/typescript/typescript-fetch/multipart-nested-object-parts/runtime/runtime.ts.golden @@ -85,6 +85,39 @@ export class Configuration { export const DefaultConfig = new Configuration(); +export interface UploadFile { + data: Blob; + filename?: string; +} + +export type UploadFileInput = Blob | File | UploadFile; + +export function uploadFileData(value: UploadFileInput): Blob { + return isUploadFile(value) ? value.data : value; +} + +export function uploadFileFilename(value: UploadFileInput, fallback: string): string { + if (isFile(value) && value.name) { + return value.name; + } + if (isUploadFile(value) && value.filename) { + return value.filename; + } + return fallback; +} + +export function multipartHeaderValue(value: string): string { + return value.replace(/[\r\n]/g, '').replace(/\\/g, '\\\\').replace(/"/g, '\\"'); +} + +function isUploadFile(value: UploadFileInput): value is UploadFile { + return typeof value === 'object' && value !== null && 'data' in value; +} + +function isFile(value: UploadFileInput): value is File { + return typeof File !== 'undefined' && value instanceof File; +} + /** * This is the base class for all generated API classes. */ diff --git a/tests/golden/typescript/typescript-fetch/multipart-unsupported-schema/apis/TransferApi.ts.golden b/tests/golden/typescript/typescript-fetch/multipart-unsupported-schema/apis/TransferApi.ts.golden index 12472b94f..a0af2d8d3 100644 --- a/tests/golden/typescript/typescript-fetch/multipart-unsupported-schema/apis/TransferApi.ts.golden +++ b/tests/golden/typescript/typescript-fetch/multipart-unsupported-schema/apis/TransferApi.ts.golden @@ -8,7 +8,7 @@ import type { Configuration, HTTPQuery, InitOverrideFunction } from '../runtime/ import { BaseAPI, DefaultConfig, RequiredError, VoidApiResponse } from '../runtime/runtime'; export interface ApiUploadRawMultipartRequest { - body: Blob | File; + body: unknown; } export type UploadRawMultipartRawResponse = 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 8d3ee60f9..8a2b33fde 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 @@ -5,12 +5,12 @@ * Covers multipart object parts whose wire names differ from ergonomic names. */ import { itemConfigToJSON } from '../models/ItemConfig'; -import type { NestedUpload } from '../models/NestedUpload'; +import type { SendNestedObjectPartMultipartRequestBody } from '../models/SendNestedObjectPartMultipartRequestBody'; import type { Configuration, HTTPQuery, InitOverrideFunction } from '../runtime/runtime'; -import { BaseAPI, DefaultConfig, RequiredError, VoidApiResponse } from '../runtime/runtime'; +import { BaseAPI, DefaultConfig, RequiredError, VoidApiResponse, multipartHeaderValue, uploadFileData, uploadFileFilename } from '../runtime/runtime'; export interface ApiSendNestedObjectPartRequest { - body: NestedUpload; + body: SendNestedObjectPartMultipartRequestBody; } export type SendNestedObjectPartRawResponse = @@ -51,9 +51,12 @@ export class MultipartApi extends BaseAPI implements MultipartApiInterface { 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); + { + const multipartFilename = uploadFileFilename(requestParameters.body.file, 'file'); + multipartChunks.push('--' + multipartBoundary + '\r\nContent-Disposition: form-data; name="file"; filename="' + multipartHeaderValue(multipartFilename) + '"\r\nContent-Type: application/octet-stream\r\n\r\n'); + multipartChunks.push(uploadFileData(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'); diff --git a/tests/golden/typescript/typescript-fetch/ts-property-naming-camel-case-multipart-parts/models/SendNestedObjectPartMultipartRequestBody.ts.golden b/tests/golden/typescript/typescript-fetch/ts-property-naming-camel-case-multipart-parts/models/SendNestedObjectPartMultipartRequestBody.ts.golden new file mode 100644 index 000000000..24f239092 --- /dev/null +++ b/tests/golden/typescript/typescript-fetch/ts-property-naming-camel-case-multipart-parts/models/SendNestedObjectPartMultipartRequestBody.ts.golden @@ -0,0 +1,13 @@ +/** + * @generated by openapi-nexus. Do not edit. + * + * Multipart Nested Object Parts — 1.0.0 + * Covers multipart object parts whose wire names differ from ergonomic names. + */ +import type { UploadFileInput } from '../runtime/runtime'; +import type { ItemConfig } from './ItemConfig'; + +export interface SendNestedObjectPartMultipartRequestBody { + file: UploadFileInput; + itemConfig: ItemConfig; +} diff --git a/tests/golden/typescript/typescript-fetch/ts-property-naming-camel-case-multipart-parts/models/index.ts.golden b/tests/golden/typescript/typescript-fetch/ts-property-naming-camel-case-multipart-parts/models/index.ts.golden index 0720dc467..f672a0038 100644 --- a/tests/golden/typescript/typescript-fetch/ts-property-naming-camel-case-multipart-parts/models/index.ts.golden +++ b/tests/golden/typescript/typescript-fetch/ts-property-naming-camel-case-multipart-parts/models/index.ts.golden @@ -8,3 +8,4 @@ export type { ItemConfig, ItemConfig$Wire } from './ItemConfig'; export { itemConfigFromJSON, itemConfigToJSON } from './ItemConfig'; export type { NestedUpload, NestedUpload$Wire } from './NestedUpload'; export { nestedUploadFromJSON, nestedUploadToJSON } from './NestedUpload'; +export type { SendNestedObjectPartMultipartRequestBody } from './SendNestedObjectPartMultipartRequestBody'; diff --git a/tests/golden/typescript/typescript-fetch/ts-property-naming-camel-case-multipart-parts/runtime/runtime.ts.golden b/tests/golden/typescript/typescript-fetch/ts-property-naming-camel-case-multipart-parts/runtime/runtime.ts.golden index be8df9abe..437168e05 100644 --- a/tests/golden/typescript/typescript-fetch/ts-property-naming-camel-case-multipart-parts/runtime/runtime.ts.golden +++ b/tests/golden/typescript/typescript-fetch/ts-property-naming-camel-case-multipart-parts/runtime/runtime.ts.golden @@ -85,6 +85,39 @@ export class Configuration { export const DefaultConfig = new Configuration(); +export interface UploadFile { + data: Blob; + filename?: string; +} + +export type UploadFileInput = Blob | File | UploadFile; + +export function uploadFileData(value: UploadFileInput): Blob { + return isUploadFile(value) ? value.data : value; +} + +export function uploadFileFilename(value: UploadFileInput, fallback: string): string { + if (isFile(value) && value.name) { + return value.name; + } + if (isUploadFile(value) && value.filename) { + return value.filename; + } + return fallback; +} + +export function multipartHeaderValue(value: string): string { + return value.replace(/[\r\n]/g, '').replace(/\\/g, '\\\\').replace(/"/g, '\\"'); +} + +function isUploadFile(value: UploadFileInput): value is UploadFile { + return typeof value === 'object' && value !== null && 'data' in value; +} + +function isFile(value: UploadFileInput): value is File { + return typeof File !== 'undefined' && value instanceof File; +} + /** * This is the base class for all generated API classes. */ diff --git a/tests/golden_tests_typescript_fetch.rs b/tests/golden_tests_typescript_fetch.rs index 3ac1c3b6b..f62bc1057 100644 --- a/tests/golden_tests_typescript_fetch.rs +++ b/tests/golden_tests_typescript_fetch.rs @@ -9,6 +9,7 @@ use std::path::Path; use tracing_test::traced_test; +use openapi_nexus::generators::request_inputs::RequestInputPlan; use openapi_nexus::generators::typescript::fetch::TypeScriptFetchCodeGenerator; use openapi_nexus::test_utils::{run_golden_test, test_cases_from_slice}; @@ -442,7 +443,10 @@ components: let ir = openapi_nexus::ir::lower::lower(parsed).unwrap(); let generator = TypeScriptFetchCodeGenerator::new(toml::value::Table::new()); - let files = generator.generate_models_from_ir(&ir).unwrap(); + let request_inputs = RequestInputPlan::empty(); + let files = generator + .generate_models_from_ir(&ir, &request_inputs) + .unwrap(); assert!( files.len() >= 3, @@ -507,7 +511,10 @@ components: let ir = openapi_nexus::ir::lower::lower(parsed).unwrap(); let generator = TypeScriptFetchCodeGenerator::new(toml::value::Table::new()); - let files = generator.generate_models_from_ir(&ir).unwrap(); + let request_inputs = RequestInputPlan::empty(); + let files = generator + .generate_models_from_ir(&ir, &request_inputs) + .unwrap(); let profile_file = files .iter() @@ -542,7 +549,10 @@ components: let ir = openapi_nexus::ir::lower::lower(parsed).unwrap(); let generator = TypeScriptFetchCodeGenerator::new(toml::value::Table::new()); - let files = generator.generate_models_from_ir(&ir).unwrap(); + let request_inputs = RequestInputPlan::empty(); + let files = generator + .generate_models_from_ir(&ir, &request_inputs) + .unwrap(); assert!( files.len() >= 3, diff --git a/tests/multipart_runtime_smoke.rs b/tests/multipart_runtime_smoke.rs index 32dda8136..283268a99 100644 --- a/tests/multipart_runtime_smoke.rs +++ b/tests/multipart_runtime_smoke.rs @@ -5,6 +5,9 @@ use openapi_nexus::generators::java::okhttp::JavaOkhttpCodeGenerator; use openapi_nexus::generators::kotlin::okhttp::KotlinOkhttpCodeGenerator; use openapi_nexus::generators::python::httpx::PythonHttpxCodeGenerator; use openapi_nexus::generators::python::requests::PythonRequestsCodeGenerator; +use openapi_nexus::generators::rust::aioduct::RustAioductCodeGenerator; +use openapi_nexus::generators::rust::reqwest::RustReqwestCodeGenerator; +use openapi_nexus::generators::rust::ureq::RustUreqCodeGenerator; use openapi_nexus::generators::typescript::fetch::TypeScriptFetchCodeGenerator; use openapi_nexus::test_utils::{generate_files, read_fixture}; @@ -26,51 +29,97 @@ fn generated_file<'a>(files: &'a HashMap, suffix: &str) -> &'a s } #[test] -fn multipart_wire_construction_is_pinned_across_non_rust_clients() { +fn multipart_wire_construction_is_pinned_across_clients() { let fixture = read_fixture("valid/multipart-edge-cases.yaml"); let go_files = generate_files(&GoHttpCodeGenerator::new(empty_config()), &fixture).unwrap(); let go_api = generated_file(&go_files, "apis/multipart.go"); + let go_input = generated_file( + &go_files, + "models/send_optional_parts_multipart_request_body.go", + ); assert!(go_api.contains("multipart.NewWriter(buf)")); - assert!(go_api.contains("strconv.FormatInt(int64(*body.RetryCount), 10)")); - assert!(go_api.contains("strconv.FormatBool(*body.Enabled)")); + assert!(go_api.contains("mime.FormatMediaType(\"form-data\"")); + assert!(go_api.contains("value.FilenameOrDefault(\"file\")")); + assert!(go_api.contains("partWriter.Write(value.Data)")); + assert!(go_api.contains("strconv.FormatInt(int64(value), 10)")); + assert!(go_api.contains("strconv.FormatBool(value)")); + assert!(go_input.contains("File *runtime.UploadFile")); + assert!(!go_api.contains("filename=\"file\"")); let java_files = generate_files(&JavaOkhttpCodeGenerator::new(empty_config()), &fixture).unwrap(); let java_api = generated_file(&java_files, "apis/MultipartApi.java"); + let java_input = generated_file( + &java_files, + "models/SendOptionalPartsMultipartRequestBody.java", + ); assert!(java_api.contains("client.newRequestWithBody(\"POST\", path, null, multipartBody)")); assert!(java_api.contains("RequestBody multipartBody = RequestBody.create(new byte[0], null)")); assert!(java_api.contains("if (body != null)")); assert!(java_api.contains("if (body.getFile() != null)")); + assert!(java_api.contains("body.getFile().filenameOrDefault(\"file\")")); + assert!(java_api.contains("body.getFile().getData()")); + assert!(java_input.contains("private final UploadFile file;")); + assert!(!java_api.contains("addFormDataPart(\"file\", \"file\"")); let kotlin_files = generate_files(&KotlinOkhttpCodeGenerator::new(empty_config()), &fixture).unwrap(); let kotlin_api = generated_file(&kotlin_files, "apis/MultipartApi.kt"); + let kotlin_input = generated_file( + &kotlin_files, + "models/SendOptionalPartsMultipartRequestBody.kt", + ); assert!(kotlin_api.contains("client.newRequestWithBody(\"POST\", path, null, multipartBody)")); - assert!(kotlin_api.contains("body: OptionalUpload?")); + assert!(kotlin_api.contains("body: SendOptionalPartsMultipartRequestBody?")); assert!(kotlin_api.contains("var multipartBody = ByteArray(0).toRequestBody(null)")); assert!(kotlin_api.contains("if (body != null)")); assert!(kotlin_api.contains("if (body.file != null)")); + assert!(kotlin_api.contains("body.file.filenameOrDefault(\"file\")")); + assert!(kotlin_api.contains("body.file.data.toRequestBody")); + assert!(kotlin_input.contains("val file: UploadFile? = null")); + assert!(!kotlin_api.contains("addFormDataPart(\"file\", \"file\"")); let httpx_files = generate_files(&PythonHttpxCodeGenerator::new(empty_config()), &fixture).unwrap(); let httpx_api = generated_file(&httpx_files, "apis/multipart_api.py"); + let httpx_input = generated_file( + &httpx_files, + "models/send_optional_parts_multipart_request_body.py", + ); assert!(httpx_api.contains("files: dict[str, object] = {}")); assert!(httpx_api.contains("files[\"note\"] = (None, str(body.note), \"text/plain\")")); + assert!( + httpx_api + .contains("files[\"file\"] = (body.file.filename_or_default(\"file\"), body.file.data") + ); assert!(httpx_api.contains("files=files if files else None")); + assert!(httpx_input.contains("file: UploadFile | None = None")); assert!(!httpx_api.contains("data=data")); + assert!(!httpx_api.contains("files[\"file\"] = (\"file\"")); let requests_files = generate_files(&PythonRequestsCodeGenerator::new(empty_config()), &fixture).unwrap(); let requests_api = generated_file(&requests_files, "apis/multipart_api.py"); + let requests_input = generated_file( + &requests_files, + "models/send_optional_parts_multipart_request_body.py", + ); assert!(requests_api.contains("files: dict[str, object] = {}")); assert!(requests_api.contains("files[\"note\"] = (None, str(body.note), \"text/plain\")")); + assert!( + requests_api + .contains("files[\"file\"] = (body.file.filename_or_default(\"file\"), body.file.data") + ); assert!(requests_api.contains("files=files if files else None")); + assert!(requests_input.contains("file: UploadFile | None = None")); assert!(!requests_api.contains("data=data")); + assert!(!requests_api.contains("files[\"file\"] = (\"file\"")); let ts_files = generate_files(&TypeScriptFetchCodeGenerator::new(empty_config()), &fixture).unwrap(); let ts_api = generated_file(&ts_files, "apis/MultipartApi.ts"); + let ts_input = generated_file(&ts_files, "models/SendOptionalPartsMultipartRequestBody.ts"); assert!(ts_api.contains("let requestBody: Blob | undefined = undefined;")); assert!( ts_api.contains( @@ -83,7 +132,53 @@ fn multipart_wire_construction_is_pinned_across_non_rust_clients() { "Content-Disposition: form-data; name=\"note\"\\r\\nContent-Type: text/plain" ) ); + assert!(ts_api.contains("uploadFileFilename(requestParameters.body.file, 'file')")); + assert!(ts_api.contains("uploadFileData(requestParameters.body.file)")); + assert!(ts_input.contains("file?: UploadFileInput;")); assert!(ts_api.contains("multipartChunks.push(String(requestParameters.body.note));")); + assert!(!ts_api.contains("filename=\"file\"")); + + let reqwest_files = + generate_files(&RustReqwestCodeGenerator::new(empty_config()), &fixture).unwrap(); + let reqwest_api = generated_file(&reqwest_files, "apis/multipart.rs"); + let reqwest_input = generated_file( + &reqwest_files, + "models/send_optional_parts_multipart_request_body.rs", + ); + assert!( + reqwest_api.contains("body: Option<&crate::models::SendOptionalPartsMultipartRequestBody>") + ); + assert!(reqwest_api.contains(".file_name(value.filename_or_default(\"file\").to_string())")); + assert!(reqwest_api.contains("reqwest::multipart::Part::bytes(value.data.clone())")); + assert!(reqwest_input.contains("pub file: Option")); + assert!(!reqwest_api.contains(".file_name(\"file\")")); + + let ureq_files = generate_files(&RustUreqCodeGenerator::new(empty_config()), &fixture).unwrap(); + let ureq_api = generated_file(&ureq_files, "apis/multipart.rs"); + let ureq_input = generated_file( + &ureq_files, + "models/send_optional_parts_multipart_request_body.rs", + ); + assert!(ureq_api.contains( + "crate::runtime::multipart_header_value(&value.filename_or_default(\"file\").to_string())" + )); + assert!(ureq_api.contains("multipart_body.extend_from_slice(&value.data.clone());")); + assert!(ureq_input.contains("pub file: Option")); + assert!(!ureq_api.contains("filename=\\\"file\\\"")); + + let aioduct_files = + generate_files(&RustAioductCodeGenerator::new(empty_config()), &fixture).unwrap(); + let aioduct_api = generated_file(&aioduct_files, "apis/multipart.rs"); + let aioduct_input = generated_file( + &aioduct_files, + "models/send_optional_parts_multipart_request_body.rs", + ); + assert!( + aioduct_api.contains( + "multipart.file(\"file\", value.filename_or_default(\"file\").to_string(), \"application/octet-stream\", value.data.clone())" + ) + ); + assert!(aioduct_input.contains("pub file: Option")); } #[test] diff --git a/tests/sigil_api_scaffold.rs b/tests/sigil_api_scaffold.rs index 1872fb204..971146e60 100644 --- a/tests/sigil_api_scaffold.rs +++ b/tests/sigil_api_scaffold.rs @@ -6,6 +6,7 @@ use std::fs; use std::path::Path; +use openapi_nexus::generators::request_inputs::RequestInputPlan; use openapi_nexus::generators::typescript::fetch::sigil_emit_api::generate_api_files; use sigil_stitch::lang::typescript::TypeScript; @@ -30,7 +31,9 @@ fn petstore_api_scaffold_emits_one_file_per_tag() { let parsed = openapi_nexus::parser::parse_content_yaml(&yaml).unwrap(); let ir = openapi_nexus::ir::lower::lower(parsed).unwrap(); - let files = generate_api_files(&ir, false, &TypeScript::new()).expect("scaffold renders"); + let request_inputs = RequestInputPlan::empty(); + let files = generate_api_files(&ir, false, &request_inputs, &TypeScript::new()) + .expect("scaffold renders"); assert!( !files.is_empty(), "expected at least one API file for petstore" @@ -74,7 +77,9 @@ fn minimal_api_scaffold_has_no_request_interface_when_op_has_no_params() { let parsed = openapi_nexus::parser::parse_content_yaml(&yaml).unwrap(); let ir = openapi_nexus::ir::lower::lower(parsed).unwrap(); - let files = generate_api_files(&ir, false, &TypeScript::new()).expect("scaffold renders"); + let request_inputs = RequestInputPlan::empty(); + let files = generate_api_files(&ir, false, &request_inputs, &TypeScript::new()) + .expect("scaffold renders"); let default_api = files .iter() .find(|f| f.filename.ends_with("DefaultApi.ts"))