Skip to content

Commit aa729b4

Browse files
committed
[Rust] Enum Query Parameter Serialization Fixes
Adds tests to ensure this won't regress again. Also fixes some other compile errors with Box<> and file uploads.
1 parent eb65e93 commit aa729b4

58 files changed

Lines changed: 2739 additions & 114 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
generatorName: rust
2+
outputDir: samples/client/others/rust/reqwest/multipart-async
3+
library: reqwest
4+
inputSpec: modules/openapi-generator/src/test/resources/3_0/rust/multipart-file-upload.yaml
5+
templateDir: modules/openapi-generator/src/main/resources/rust
6+
additionalProperties:
7+
supportAsync: true
8+
useSingleRequestParameter: true
9+
packageName: multipart-upload-reqwest-async

modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/RustClientCodegen.java

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -741,7 +741,7 @@ public OperationsMap postProcessOperationsWithModels(OperationsMap objs, List<Mo
741741
}
742742

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

754+
// Also check form params for file uploads (multipart)
755+
if (!useAsyncFileStream) {
756+
for (var param: operation.formParams) {
757+
if (param.isFile && supportAsync) {
758+
useAsyncFileStream = true;
759+
additionalProperties.put("useAsyncFileStream", Boolean.TRUE);
760+
operation.vendorExtensions.put("useAsyncFileStream", Boolean.TRUE);
761+
break;
762+
}
763+
}
764+
}
765+
754766
// http method verb conversion, depending on client library (e.g. Hyper: PUT => Put, Reqwest: PUT => put)
755767
if (HYPER_LIBRARY.equals(getLibrary())) {
756768
operation.httpMethod = StringUtils.camelize(operation.httpMethod.toLowerCase(Locale.ROOT));

modules/openapi-generator/src/main/resources/rust/model.mustache

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -167,7 +167,7 @@ impl {{{classname}}} {
167167
}}{{^-last}}, {{/-last}}{{/requiredVars}}) -> {{{classname}}} {
168168
{{{classname}}} {
169169
{{#vars}}
170-
{{{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}},
170+
{{{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}},
171171
{{/vars}}
172172
}
173173
}

modules/openapi-generator/src/main/resources/rust/reqwest/api.mustache

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,7 @@ pub {{#supportAsync}}async {{/supportAsync}}fn {{{operationId}}}(configuration:
204204
{{^isObject}}
205205
{{^isModel}}
206206
{{^isEnum}}
207+
{{^isEnumRef}}
207208
{{#isPrimitiveType}}
208209
if let Some(ref param_value) = {{{vendorExtensions.x-rust-param-identifier}}} {
209210
req_builder = req_builder.query(&[("{{{baseName}}}", &param_value.to_string())]);
@@ -214,7 +215,18 @@ pub {{#supportAsync}}async {{/supportAsync}}fn {{{operationId}}}(configuration:
214215
req_builder = req_builder.query(&[("{{{baseName}}}", &serde_json::to_string(param_value)?)]);
215216
};
216217
{{/isPrimitiveType}}
218+
{{/isEnumRef}}
217219
{{/isEnum}}
220+
{{#isEnum}}
221+
if let Some(ref param_value) = {{{vendorExtensions.x-rust-param-identifier}}} {
222+
req_builder = req_builder.query(&[("{{{baseName}}}", &param_value.to_string())]);
223+
};
224+
{{/isEnum}}
225+
{{#isEnumRef}}
226+
if let Some(ref param_value) = {{{vendorExtensions.x-rust-param-identifier}}} {
227+
req_builder = req_builder.query(&[("{{{baseName}}}", &param_value.to_string())]);
228+
};
229+
{{/isEnumRef}}
218230
{{/isModel}}
219231
{{/isObject}}
220232
{{/isNullable}}
@@ -255,17 +267,22 @@ pub {{#supportAsync}}async {{/supportAsync}}fn {{{operationId}}}(configuration:
255267
req_builder = req_builder.query(&[("{{{baseName}}}", &serde_json::to_string(param_value)?)]);
256268
{{/isModel}}
257269
{{#isEnum}}
258-
req_builder = req_builder.query(&[("{{{baseName}}}", &serde_json::to_string(param_value)?)]);
270+
req_builder = req_builder.query(&[("{{{baseName}}}", &param_value.to_string())]);
259271
{{/isEnum}}
272+
{{#isEnumRef}}
273+
req_builder = req_builder.query(&[("{{{baseName}}}", &param_value.to_string())]);
274+
{{/isEnumRef}}
260275
{{^isObject}}
261276
{{^isModel}}
262277
{{^isEnum}}
278+
{{^isEnumRef}}
263279
{{#isPrimitiveType}}
264280
req_builder = req_builder.query(&[("{{{baseName}}}", &param_value.to_string())]);
265281
{{/isPrimitiveType}}
266282
{{^isPrimitiveType}}
267283
req_builder = req_builder.query(&[("{{{baseName}}}", &serde_json::to_string(param_value)?)]);
268284
{{/isPrimitiveType}}
285+
{{/isEnumRef}}
269286
{{/isEnum}}
270287
{{/isModel}}
271288
{{/isObject}}
@@ -405,11 +422,17 @@ pub {{#supportAsync}}async {{/supportAsync}}fn {{{operationId}}}(configuration:
405422
{{#supportAsync}}
406423
{{^required}}
407424
if let Some(ref param_value) = {{{vendorExtensions.x-rust-param-identifier}}} {
408-
multipart_form = multipart_form.file("{{{baseName}}}", param_value.as_os_str()).await?;
425+
let file_bytes = tokio::fs::read(param_value).await?;
426+
let file_name = param_value.file_name().map(|n| n.to_string_lossy().to_string()).unwrap_or_default();
427+
let file_part = reqwest::multipart::Part::bytes(file_bytes).file_name(file_name);
428+
multipart_form = multipart_form.part("{{{baseName}}}", file_part);
409429
}
410430
{{/required}}
411431
{{#required}}
412-
multipart_form = multipart_form.file("{{{baseName}}}", {{{vendorExtensions.x-rust-param-identifier}}}.as_os_str()).await?;
432+
let file_bytes = tokio::fs::read(&{{{vendorExtensions.x-rust-param-identifier}}}).await?;
433+
let file_name = {{{vendorExtensions.x-rust-param-identifier}}}.file_name().map(|n| n.to_string_lossy().to_string()).unwrap_or_default();
434+
let file_part = reqwest::multipart::Part::bytes(file_bytes).file_name(file_name);
435+
multipart_form = multipart_form.part("{{{baseName}}}", file_part);
413436
{{/required}}
414437
{{/supportAsync}}
415438
{{/isFile}}
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
openapi: 3.0.3
2+
info:
3+
title: Multipart File Upload Test
4+
description: Regression test for async multipart file uploads with tokio::fs
5+
version: 1.0.0
6+
servers:
7+
- url: http://localhost:8080
8+
paths:
9+
/upload/single:
10+
post:
11+
operationId: uploadSingleFile
12+
summary: Upload a single file (required parameter)
13+
description: Tests async multipart file upload with required file parameter
14+
requestBody:
15+
required: true
16+
content:
17+
multipart/form-data:
18+
schema:
19+
type: object
20+
required:
21+
- file
22+
- description
23+
properties:
24+
description:
25+
type: string
26+
description: File description metadata
27+
file:
28+
type: string
29+
format: binary
30+
description: File to upload
31+
responses:
32+
'200':
33+
description: Upload successful
34+
content:
35+
application/json:
36+
schema:
37+
$ref: '#/components/schemas/UploadResponse'
38+
'400':
39+
description: Bad request
40+
41+
/upload/optional:
42+
post:
43+
operationId: uploadOptionalFile
44+
summary: Upload an optional file
45+
description: Tests async multipart file upload with optional file parameter
46+
requestBody:
47+
content:
48+
multipart/form-data:
49+
schema:
50+
type: object
51+
properties:
52+
metadata:
53+
type: string
54+
description: Optional metadata string
55+
file:
56+
type: string
57+
format: binary
58+
description: Optional file to upload
59+
responses:
60+
'200':
61+
description: Upload successful
62+
content:
63+
application/json:
64+
schema:
65+
$ref: '#/components/schemas/UploadResponse'
66+
67+
/upload/multiple-fields:
68+
post:
69+
operationId: uploadMultipleFields
70+
summary: Upload with multiple form fields
71+
description: Tests async multipart with multiple files and text fields
72+
requestBody:
73+
content:
74+
multipart/form-data:
75+
schema:
76+
type: object
77+
required:
78+
- primaryFile
79+
properties:
80+
title:
81+
type: string
82+
description: Upload title
83+
tags:
84+
type: array
85+
items:
86+
type: string
87+
description: Tags for the upload
88+
primaryFile:
89+
type: string
90+
format: binary
91+
description: Primary file (required)
92+
thumbnail:
93+
type: string
94+
format: binary
95+
description: Optional thumbnail file
96+
responses:
97+
'200':
98+
description: Upload successful
99+
content:
100+
application/json:
101+
schema:
102+
$ref: '#/components/schemas/UploadResponse'
103+
'400':
104+
description: Bad request
105+
106+
components:
107+
schemas:
108+
UploadResponse:
109+
type: object
110+
required:
111+
- success
112+
- fileCount
113+
properties:
114+
success:
115+
type: boolean
116+
description: Whether the upload was successful
117+
fileCount:
118+
type: integer
119+
format: int32
120+
description: Number of files uploaded
121+
message:
122+
type: string
123+
description: Optional message about the upload

modules/openapi-generator/src/test/resources/3_0/rust/petstore.yaml

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -719,6 +719,50 @@ paths:
719719
application/json:
720720
schema:
721721
type: string
722+
'/tests/inlineEnumBoxing':
723+
post:
724+
tags:
725+
- testing
726+
summary: 'Test for inline enum fields not being boxed in model constructors'
727+
description: 'Regression test to ensure inline enum fields are not wrapped in Box::new() in model constructors'
728+
requestBody:
729+
required: true
730+
content:
731+
application/json:
732+
schema:
733+
$ref: '#/components/schemas/ModelWithInlineEnum'
734+
responses:
735+
'200':
736+
description: successful operation
737+
content:
738+
application/json:
739+
schema:
740+
$ref: '#/components/schemas/ModelWithInlineEnum'
741+
get:
742+
tags:
743+
- testing
744+
summary: 'Get model with inline enums'
745+
description: 'Tests inline enum query parameters'
746+
parameters:
747+
- name: status
748+
in: query
749+
description: Filter by status (inline enum)
750+
required: false
751+
schema:
752+
type: string
753+
enum:
754+
- draft
755+
- published
756+
- archived
757+
responses:
758+
'200':
759+
description: successful operation
760+
content:
761+
application/json:
762+
schema:
763+
type: array
764+
items:
765+
$ref: '#/components/schemas/ModelWithInlineEnum'
722766
externalDocs:
723767
description: Find out more about Swagger
724768
url: 'http://swagger.io'
@@ -1128,6 +1172,38 @@ components:
11281172
oneOf:
11291173
- $ref: '#/components/schemas/Order'
11301174
- $ref: '#/components/schemas/Order'
1175+
ModelWithInlineEnum:
1176+
type: object
1177+
required:
1178+
- status
1179+
properties:
1180+
id:
1181+
type: integer
1182+
format: int64
1183+
description: Model ID
1184+
status:
1185+
type: string
1186+
description: Status with inline enum (tests inline enum not being boxed in constructor)
1187+
enum:
1188+
- draft
1189+
- published
1190+
- archived
1191+
priority:
1192+
type: string
1193+
description: Priority level (optional inline enum)
1194+
enum:
1195+
- low
1196+
- medium
1197+
- high
1198+
- critical
1199+
metadata:
1200+
type: object
1201+
description: Optional metadata object
1202+
properties:
1203+
tags:
1204+
type: array
1205+
items:
1206+
type: string
11311207
Page:
11321208
type: object
11331209
properties:

0 commit comments

Comments
 (0)