Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions bin/configs/rust-reqwest-multipart-async.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
generatorName: rust
outputDir: samples/client/others/rust/reqwest/multipart-async
library: reqwest
inputSpec: modules/openapi-generator/src/test/resources/3_0/rust/multipart-file-upload.yaml
templateDir: modules/openapi-generator/src/main/resources/rust
additionalProperties:
supportAsync: true
useSingleRequestParameter: true
packageName: multipart-upload-reqwest-async
Original file line number Diff line number Diff line change
Expand Up @@ -741,7 +741,7 @@ public OperationsMap postProcessOperationsWithModels(OperationsMap objs, List<Mo
}

// If we use a file body parameter, we need to include the imports and crates for it
// But they should be added only once per file
// But they should be added only once per file
for (var param: operation.bodyParams) {
if (param.isFile && supportAsync && !useAsyncFileStream) {
useAsyncFileStream = true;
Expand All @@ -751,6 +751,18 @@ public OperationsMap postProcessOperationsWithModels(OperationsMap objs, List<Mo
}
}

// Also check form params for file uploads (multipart)
if (!useAsyncFileStream) {
for (var param: operation.formParams) {
if (param.isFile && supportAsync) {
useAsyncFileStream = true;
additionalProperties.put("useAsyncFileStream", Boolean.TRUE);
operation.vendorExtensions.put("useAsyncFileStream", Boolean.TRUE);
break;
}
}
}

// http method verb conversion, depending on client library (e.g. Hyper: PUT => Put, Reqwest: PUT => put)
if (HYPER_LIBRARY.equals(getLibrary())) {
operation.httpMethod = StringUtils.camelize(operation.httpMethod.toLowerCase(Locale.ROOT));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,7 @@ impl {{{classname}}} {
}}{{^-last}}, {{/-last}}{{/requiredVars}}) -> {{{classname}}} {
{{{classname}}} {
{{#vars}}
{{{name}}}{{^required}}: None{{/required}}{{#required}}{{#isModel}}{{^avoidBoxedModels}}: {{^isNullable}}Box::new({{{name}}}){{/isNullable}}{{#isNullable}}if let Some(x) = {{{name}}} {Some(Box::new(x))} else {None}{{/isNullable}}{{/avoidBoxedModels}}{{/isModel}}{{/required}},
{{{name}}}{{^required}}: None{{/required}}{{#required}}{{^isEnum}}{{#isModel}}{{^avoidBoxedModels}}: {{^isNullable}}Box::new({{{name}}}){{/isNullable}}{{#isNullable}}if let Some(x) = {{{name}}} {Some(Box::new(x))} else {None}{{/isNullable}}{{/avoidBoxedModels}}{{/isModel}}{{/isEnum}}{{/required}},
{{/vars}}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,7 @@ pub {{#supportAsync}}async {{/supportAsync}}fn {{{operationId}}}(configuration:
{{^isObject}}
{{^isModel}}
{{^isEnum}}
{{^isEnumRef}}
{{#isPrimitiveType}}
if let Some(ref param_value) = {{{vendorExtensions.x-rust-param-identifier}}} {
req_builder = req_builder.query(&[("{{{baseName}}}", &param_value.to_string())]);
Expand All @@ -214,7 +215,18 @@ pub {{#supportAsync}}async {{/supportAsync}}fn {{{operationId}}}(configuration:
req_builder = req_builder.query(&[("{{{baseName}}}", &serde_json::to_string(param_value)?)]);
};
{{/isPrimitiveType}}
{{/isEnumRef}}
{{/isEnum}}
{{#isEnum}}
if let Some(ref param_value) = {{{vendorExtensions.x-rust-param-identifier}}} {
req_builder = req_builder.query(&[("{{{baseName}}}", &param_value.to_string())]);
};
{{/isEnum}}
{{#isEnumRef}}
if let Some(ref param_value) = {{{vendorExtensions.x-rust-param-identifier}}} {
req_builder = req_builder.query(&[("{{{baseName}}}", &param_value.to_string())]);
};
{{/isEnumRef}}
{{/isModel}}
{{/isObject}}
{{/isNullable}}
Expand Down Expand Up @@ -255,17 +267,22 @@ pub {{#supportAsync}}async {{/supportAsync}}fn {{{operationId}}}(configuration:
req_builder = req_builder.query(&[("{{{baseName}}}", &serde_json::to_string(param_value)?)]);
{{/isModel}}
{{#isEnum}}
req_builder = req_builder.query(&[("{{{baseName}}}", &serde_json::to_string(param_value)?)]);
req_builder = req_builder.query(&[("{{{baseName}}}", &param_value.to_string())]);
{{/isEnum}}
{{#isEnumRef}}
req_builder = req_builder.query(&[("{{{baseName}}}", &param_value.to_string())]);
{{/isEnumRef}}
{{^isObject}}
{{^isModel}}
{{^isEnum}}
{{^isEnumRef}}
{{#isPrimitiveType}}
req_builder = req_builder.query(&[("{{{baseName}}}", &param_value.to_string())]);
{{/isPrimitiveType}}
{{^isPrimitiveType}}
req_builder = req_builder.query(&[("{{{baseName}}}", &serde_json::to_string(param_value)?)]);
{{/isPrimitiveType}}
{{/isEnumRef}}
{{/isEnum}}
{{/isModel}}
{{/isObject}}
Expand Down Expand Up @@ -405,11 +422,17 @@ pub {{#supportAsync}}async {{/supportAsync}}fn {{{operationId}}}(configuration:
{{#supportAsync}}
{{^required}}
if let Some(ref param_value) = {{{vendorExtensions.x-rust-param-identifier}}} {
multipart_form = multipart_form.file("{{{baseName}}}", param_value.as_os_str()).await?;
let file_bytes = tokio::fs::read(param_value).await?;
let file_name = param_value.file_name().map(|n| n.to_string_lossy().to_string()).unwrap_or_default();
let file_part = reqwest::multipart::Part::bytes(file_bytes).file_name(file_name);
multipart_form = multipart_form.part("{{{baseName}}}", file_part);
}
{{/required}}
{{#required}}
multipart_form = multipart_form.file("{{{baseName}}}", {{{vendorExtensions.x-rust-param-identifier}}}.as_os_str()).await?;
let file_bytes = tokio::fs::read(&{{{vendorExtensions.x-rust-param-identifier}}}).await?;
let file_name = {{{vendorExtensions.x-rust-param-identifier}}}.file_name().map(|n| n.to_string_lossy().to_string()).unwrap_or_default();
let file_part = reqwest::multipart::Part::bytes(file_bytes).file_name(file_name);
multipart_form = multipart_form.part("{{{baseName}}}", file_part);
{{/required}}
{{/supportAsync}}
{{/isFile}}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
openapi: 3.0.3
info:
title: Multipart File Upload Test
description: Regression test for async multipart file uploads with tokio::fs
version: 1.0.0
servers:
- url: http://localhost:8080
paths:
/upload/single:
post:
operationId: uploadSingleFile
summary: Upload a single file (required parameter)
description: Tests async multipart file upload with required file parameter
requestBody:
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot Jan 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: requestBody missing required: true, making required multipart file optional

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At modules/openapi-generator/src/test/resources/3_0/rust/multipart-file-upload.yaml, line 14:

<comment>requestBody missing `required: true`, making required multipart file optional</comment>

<file context>
@@ -0,0 +1,123 @@
+      operationId: uploadSingleFile
+      summary: Upload a single file (required parameter)
+      description: Tests async multipart file upload with required file parameter
+      requestBody:
+        required: true
+        content:
</file context>
Fix with Cubic

required: true
content:
multipart/form-data:
schema:
type: object
required:
- file
- description
properties:
description:
type: string
description: File description metadata
file:
type: string
format: binary
description: File to upload
responses:
'200':
description: Upload successful
content:
application/json:
schema:
$ref: '#/components/schemas/UploadResponse'
'400':
description: Bad request

/upload/optional:
post:
operationId: uploadOptionalFile
summary: Upload an optional file
description: Tests async multipart file upload with optional file parameter
requestBody:
content:
multipart/form-data:
schema:
type: object
properties:
metadata:
type: string
description: Optional metadata string
file:
type: string
format: binary
description: Optional file to upload
responses:
'200':
description: Upload successful
content:
application/json:
schema:
$ref: '#/components/schemas/UploadResponse'

/upload/multiple-fields:
post:
operationId: uploadMultipleFields
summary: Upload with multiple form fields
description: Tests async multipart with multiple files and text fields
requestBody:
content:
multipart/form-data:
schema:
type: object
required:
- primaryFile
properties:
title:
type: string
description: Upload title
tags:
type: array
items:
type: string
description: Tags for the upload
primaryFile:
type: string
format: binary
description: Primary file (required)
thumbnail:
type: string
format: binary
description: Optional thumbnail file
responses:
'200':
description: Upload successful
content:
application/json:
schema:
$ref: '#/components/schemas/UploadResponse'
'400':
description: Bad request

components:
schemas:
UploadResponse:
type: object
required:
- success
- fileCount
properties:
success:
type: boolean
description: Whether the upload was successful
fileCount:
type: integer
format: int32
description: Number of files uploaded
message:
type: string
description: Optional message about the upload
Original file line number Diff line number Diff line change
Expand Up @@ -719,6 +719,50 @@ paths:
application/json:
schema:
type: string
'/tests/inlineEnumBoxing':
post:
tags:
- testing
summary: 'Test for inline enum fields not being boxed in model constructors'
description: 'Regression test to ensure inline enum fields are not wrapped in Box::new() in model constructors'
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/ModelWithInlineEnum'
responses:
'200':
description: successful operation
content:
application/json:
schema:
$ref: '#/components/schemas/ModelWithInlineEnum'
get:
tags:
- testing
summary: 'Get model with inline enums'
description: 'Tests inline enum query parameters'
parameters:
- name: status
in: query
description: Filter by status (inline enum)
required: false
schema:
type: string
enum:
- draft
- published
- archived
responses:
'200':
description: successful operation
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/ModelWithInlineEnum'
externalDocs:
description: Find out more about Swagger
url: 'http://swagger.io'
Expand Down Expand Up @@ -1128,6 +1172,38 @@ components:
oneOf:
- $ref: '#/components/schemas/Order'
- $ref: '#/components/schemas/Order'
ModelWithInlineEnum:
type: object
required:
- status
properties:
id:
type: integer
format: int64
description: Model ID
status:
type: string
description: Status with inline enum (tests inline enum not being boxed in constructor)
enum:
- draft
- published
- archived
priority:
type: string
description: Priority level (optional inline enum)
enum:
- low
- medium
- high
- critical
metadata:
type: object
description: Optional metadata object
properties:
tags:
type: array
items:
type: string
Page:
type: object
properties:
Expand Down
Loading
Loading