Skip to content

Commit 16342ee

Browse files
authored
feat(rust-server): refactor server module templates to avoid a monolithic run function. (#23432)
* feat(rust-server): refactor server module templates to avoid a monolithic run function. * Add guard against duplicate operation function name
1 parent 599b93d commit 16342ee

File tree

15 files changed

+3331
-874
lines changed

15 files changed

+3331
-874
lines changed

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

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ public class RustServerCodegen extends AbstractRustCodegen implements CodegenCon
7070
protected String externCrateName;
7171
protected Map<String, Map<String, String>> pathSetMap = new HashMap();
7272
protected Map<String, Map<String, String>> callbacksPathSetMap = new HashMap();
73+
protected Set<String> globalOperationIds = new HashSet<>();
7374

7475
private static final String uuidType = "uuid::Uuid";
7576
private static final String bytesType = "swagger::ByteArray";
@@ -583,8 +584,20 @@ public CodegenOperation fromOperation(String path, String httpMethod, Operation
583584
}
584585

585586
String underscoredOperationId = underscore(op.operationId);
586-
op.vendorExtensions.put("x-operation-id", underscoredOperationId);
587-
op.vendorExtensions.put("x-uppercase-operation-id", underscoredOperationId.toUpperCase(Locale.ROOT));
587+
// Deduplicate x-operation-id across all tag groups. All operations are merged into a single
588+
// mod.rs, so handle_<x-operation-id>() functions must be globally unique, not just per-tag.
589+
String uniqueOperationId = underscoredOperationId;
590+
int opIdCounter = 0;
591+
while (globalOperationIds.contains(uniqueOperationId)) {
592+
uniqueOperationId = underscoredOperationId + "_" + opIdCounter;
593+
opIdCounter++;
594+
}
595+
globalOperationIds.add(uniqueOperationId);
596+
if (!uniqueOperationId.equals(underscoredOperationId)) {
597+
LOGGER.warn("generated unique x-operation-id `{}` for operationId `{}`", uniqueOperationId, op.operationId);
598+
}
599+
op.vendorExtensions.put("x-operation-id", uniqueOperationId);
600+
op.vendorExtensions.put("x-uppercase-operation-id", uniqueOperationId.toUpperCase(Locale.ROOT));
588601
String vendorExtensionPath = op.path.replace("{", ":").replace("}", "");
589602
op.vendorExtensions.put("x-path", vendorExtensionPath);
590603
op.vendorExtensions.put("x-path-id", pathId);

modules/openapi-generator/src/main/resources/rust-server/client-callbacks.mustache

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,24 @@ use crate::{{{operationId}}}Response;
4949
{{/callbacks}}
5050
{{>server-service-footer}}
5151

52+
{{! Per-operation handler functions — extracted from the match arms in run() to
53+
reduce per-function compilation-unit size and avoid rustc OOM on large APIs. }}
54+
{{#apiInfo}}
55+
{{#apis}}
56+
{{#operations}}
57+
{{#operation}}
58+
{{#callbacks}}
59+
{{#urls}}
60+
{{#requests}}
61+
{{>server-operation-handler}}
62+
63+
{{/requests}}
64+
{{/urls}}
65+
{{/callbacks}}
66+
{{/operation}}
67+
{{/operations}}
68+
{{/apis}}
69+
{{/apiInfo}}
5270
/// Request parser for `Api`.
5371
pub struct ApiRequestParser;
5472
impl<T> RequestParser<T> for ApiRequestParser {

modules/openapi-generator/src/main/resources/rust-server/server-mod.mustache

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,18 @@ pub mod callbacks;
3030
{{/pathSet}}
3131
{{>server-service-footer}}
3232

33+
{{! Per-operation handler functions — extracted from the match arms in run() to
34+
reduce per-function compilation-unit size and avoid rustc OOM on large APIs. }}
35+
{{#apiInfo}}
36+
{{#apis}}
37+
{{#operations}}
38+
{{#operation}}
39+
{{>server-operation-handler}}
40+
41+
{{/operation}}
42+
{{/operations}}
43+
{{/apis}}
44+
{{/apiInfo}}
3345
/// Request parser for `Api`.
3446
pub struct ApiRequestParser;
3547
impl<T> RequestParser<T> for ApiRequestParser {

modules/openapi-generator/src/main/resources/rust-server/server-operation-handler.mustache

Lines changed: 356 additions & 0 deletions
Large diffs are not rendered by default.

modules/openapi-generator/src/main/resources/rust-server/server-operation.mustache

Lines changed: 1 addition & 321 deletions
Large diffs are not rendered by default.

modules/openapi-generator/src/test/java/org/openapitools/codegen/rust/RustServerCodegenTest.java

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import java.nio.file.Files;
1111
import java.nio.file.Path;
1212
import java.util.List;
13+
import java.util.regex.Pattern;
1314

1415
/**
1516
* Tests for RustServerCodegen.
@@ -60,6 +61,35 @@ public void testIntegerParameterTypeFitting() throws IOException {
6061
target.toFile().deleteOnExit();
6162
}
6263

64+
/**
65+
* Test that two operations whose operationIds normalize to the same snake_case string
66+
* (e.g., "fooBar" and "foo_bar" both become "foo_bar") receive distinct x-operation-id
67+
* values. All operations are emitted as handle_<x-operation-id>() free functions in a
68+
* single mod.rs, so duplicates would cause a Rust compile error.
69+
*/
70+
@Test
71+
public void testDuplicateOperationIdDeduplication() throws IOException {
72+
Path target = Files.createTempDirectory("test");
73+
final CodegenConfigurator configurator = new CodegenConfigurator()
74+
.setGeneratorName("rust-server")
75+
.setInputSpec("src/test/resources/3_0/rust-server/duplicate-operation-id.yaml")
76+
.setSkipOverwrite(false)
77+
.setOutputDir(target.toAbsolutePath().toString().replace("\\", "/"));
78+
List<File> files = new DefaultGenerator().opts(configurator.toClientOptInput()).generate();
79+
files.forEach(File::deleteOnExit);
80+
81+
Path serverModPath = Path.of(target.toString(), "/src/server/mod.rs");
82+
TestUtils.assertFileExists(serverModPath);
83+
84+
// Both operations produce snake_case "foo_bar". The second should be renamed to
85+
// "foo_bar_0" so there are two distinct handle_*() functions, not a duplicate.
86+
TestUtils.assertFileContains(serverModPath, "handle_foo_bar(");
87+
TestUtils.assertFileContains(serverModPath, "handle_foo_bar_0(");
88+
89+
// Clean up
90+
target.toFile().deleteOnExit();
91+
}
92+
6393
/**
6494
* Test that required query params without examples disable the client example.
6595
*/
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
2+
# Test that two operations in different tags with the same operationId (after snake_case
3+
# normalization) produce distinct x-operation-id values, avoiding a Rust compile error
4+
# from duplicate handle_<x-operation-id>() functions in the same mod.rs.
5+
openapi: 3.1.1
6+
info:
7+
title: Duplicate OperationId Test
8+
description: Spec with two operations that normalize to the same snake_case operation ID
9+
version: 1.0.0
10+
servers:
11+
- url: http://localhost:8080
12+
paths:
13+
/foo/bar:
14+
get:
15+
operationId: fooBar
16+
summary: First operation
17+
tags:
18+
- TagA
19+
responses:
20+
"200":
21+
description: OK
22+
/foo/baz:
23+
get:
24+
operationId: foo_bar
25+
summary: Second operation - normalizes to same snake_case as fooBar
26+
tags:
27+
- TagB
28+
responses:
29+
"200":
30+
description: OK

samples/server/petstore/rust-server/output/multipart-v3/src/server/mod.rs

Lines changed: 83 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -256,6 +256,53 @@ impl<T, C, ReqBody> hyper::service::Service<(Request<ReqBody>, C)> for Service<T
256256

257257
// MultipartRelatedRequestPost - POST /multipart_related_request
258258
hyper::Method::POST if path.matched(paths::ID_MULTIPART_RELATED_REQUEST) => {
259+
handle_multipart_related_request_post(api_impl, uri, headers, body, context, validation, multipart_form_size_limit).await
260+
},
261+
262+
// MultipartRequestPost - POST /multipart_request
263+
hyper::Method::POST if path.matched(paths::ID_MULTIPART_REQUEST) => {
264+
handle_multipart_request_post(api_impl, uri, headers, body, context, validation, multipart_form_size_limit).await
265+
},
266+
267+
// MultipleIdenticalMimeTypesPost - POST /multiple-identical-mime-types
268+
hyper::Method::POST if path.matched(paths::ID_MULTIPLE_IDENTICAL_MIME_TYPES) => {
269+
handle_multiple_identical_mime_types_post(api_impl, uri, headers, body, context, validation, multipart_form_size_limit).await
270+
},
271+
272+
_ if path.matched(paths::ID_MULTIPART_RELATED_REQUEST) => method_not_allowed(),
273+
_ if path.matched(paths::ID_MULTIPART_REQUEST) => method_not_allowed(),
274+
_ if path.matched(paths::ID_MULTIPLE_IDENTICAL_MIME_TYPES) => method_not_allowed(),
275+
_ => Ok(Response::builder().status(StatusCode::NOT_FOUND)
276+
.body(BoxBody::new(http_body_util::Empty::new()))
277+
.expect("Unable to create Not Found response"))
278+
}
279+
}
280+
Box::pin(run(
281+
self.api_impl.clone(),
282+
req,
283+
self.validation,
284+
self.multipart_form_size_limit
285+
))
286+
}
287+
}
288+
289+
#[allow(unused_variables)]
290+
async fn handle_multipart_related_request_post<T, C, ReqBody>(
291+
mut api_impl: T,
292+
uri: hyper::Uri,
293+
headers: HeaderMap,
294+
body: ReqBody,
295+
context: C,
296+
validation: bool,
297+
multipart_form_size_limit: Option<u64>,
298+
) -> Result<Response<BoxBody<Bytes, Infallible>>, crate::ServiceError>
299+
where
300+
T: Api<C> + Clone + Send + 'static,
301+
C: Has<XSpanIdString> + Send + Sync + 'static,
302+
ReqBody: Body + Send + 'static,
303+
ReqBody::Error: Into<Box<dyn Error + Send + Sync>> + Send,
304+
ReqBody::Data: Send,
305+
{
259306
// Handle body parameters (note that non-required body parameters will ignore garbage
260307
// values, rather than causing a 400 response). Produce warning header and logs for
261308
// any unused fields.
@@ -383,10 +430,25 @@ impl<T, C, ReqBody> hyper::service::Service<(Request<ReqBody>, C)> for Service<T
383430
.body(body_from_string(format!("Unable to read body: {}", e.into())))
384431
.expect("Unable to create Bad Request response due to unable to read body")),
385432
}
386-
},
433+
}
387434

388-
// MultipartRequestPost - POST /multipart_request
389-
hyper::Method::POST if path.matched(paths::ID_MULTIPART_REQUEST) => {
435+
#[allow(unused_variables)]
436+
async fn handle_multipart_request_post<T, C, ReqBody>(
437+
mut api_impl: T,
438+
uri: hyper::Uri,
439+
headers: HeaderMap,
440+
body: ReqBody,
441+
context: C,
442+
validation: bool,
443+
multipart_form_size_limit: Option<u64>,
444+
) -> Result<Response<BoxBody<Bytes, Infallible>>, crate::ServiceError>
445+
where
446+
T: Api<C> + Clone + Send + 'static,
447+
C: Has<XSpanIdString> + Send + Sync + 'static,
448+
ReqBody: Body + Send + 'static,
449+
ReqBody::Error: Into<Box<dyn Error + Send + Sync>> + Send,
450+
ReqBody::Data: Send,
451+
{
390452
// Handle body parameters (note that non-required body parameters will ignore garbage
391453
// values, rather than causing a 400 response). Produce warning header and logs for
392454
// any unused fields.
@@ -571,10 +633,25 @@ impl<T, C, ReqBody> hyper::service::Service<(Request<ReqBody>, C)> for Service<T
571633
.body(body_from_string(format!("Unable to read body: {}", e.into())))
572634
.expect("Unable to create Bad Request response due to unable to read body")),
573635
}
574-
},
636+
}
575637

576-
// MultipleIdenticalMimeTypesPost - POST /multiple-identical-mime-types
577-
hyper::Method::POST if path.matched(paths::ID_MULTIPLE_IDENTICAL_MIME_TYPES) => {
638+
#[allow(unused_variables)]
639+
async fn handle_multiple_identical_mime_types_post<T, C, ReqBody>(
640+
mut api_impl: T,
641+
uri: hyper::Uri,
642+
headers: HeaderMap,
643+
body: ReqBody,
644+
context: C,
645+
validation: bool,
646+
multipart_form_size_limit: Option<u64>,
647+
) -> Result<Response<BoxBody<Bytes, Infallible>>, crate::ServiceError>
648+
where
649+
T: Api<C> + Clone + Send + 'static,
650+
C: Has<XSpanIdString> + Send + Sync + 'static,
651+
ReqBody: Body + Send + 'static,
652+
ReqBody::Error: Into<Box<dyn Error + Send + Sync>> + Send,
653+
ReqBody::Data: Send,
654+
{
578655
// Handle body parameters (note that non-required body parameters will ignore garbage
579656
// values, rather than causing a 400 response). Produce warning header and logs for
580657
// any unused fields.
@@ -677,23 +754,6 @@ impl<T, C, ReqBody> hyper::service::Service<(Request<ReqBody>, C)> for Service<T
677754
.body(body_from_string(format!("Unable to read body: {}", e.into())))
678755
.expect("Unable to create Bad Request response due to unable to read body")),
679756
}
680-
},
681-
682-
_ if path.matched(paths::ID_MULTIPART_RELATED_REQUEST) => method_not_allowed(),
683-
_ if path.matched(paths::ID_MULTIPART_REQUEST) => method_not_allowed(),
684-
_ if path.matched(paths::ID_MULTIPLE_IDENTICAL_MIME_TYPES) => method_not_allowed(),
685-
_ => Ok(Response::builder().status(StatusCode::NOT_FOUND)
686-
.body(BoxBody::new(http_body_util::Empty::new()))
687-
.expect("Unable to create Not Found response"))
688-
}
689-
}
690-
Box::pin(run(
691-
self.api_impl.clone(),
692-
req,
693-
self.validation,
694-
self.multipart_form_size_limit
695-
))
696-
}
697757
}
698758

699759
/// Request parser for `Api`.

samples/server/petstore/rust-server/output/no-example-v3/src/server/mod.rs

Lines changed: 33 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,39 @@ impl<T, C, ReqBody> hyper::service::Service<(Request<ReqBody>, C)> for Service<T
219219

220220
// OpGet - GET /op
221221
hyper::Method::GET if path.matched(paths::ID_OP) => {
222+
handle_op_get(api_impl, uri, headers, body, context, validation).await
223+
},
224+
225+
_ if path.matched(paths::ID_OP) => method_not_allowed(),
226+
_ => Ok(Response::builder().status(StatusCode::NOT_FOUND)
227+
.body(BoxBody::new(http_body_util::Empty::new()))
228+
.expect("Unable to create Not Found response"))
229+
}
230+
}
231+
Box::pin(run(
232+
self.api_impl.clone(),
233+
req,
234+
self.validation
235+
))
236+
}
237+
}
238+
239+
#[allow(unused_variables)]
240+
async fn handle_op_get<T, C, ReqBody>(
241+
mut api_impl: T,
242+
uri: hyper::Uri,
243+
headers: HeaderMap,
244+
body: ReqBody,
245+
context: C,
246+
validation: bool,
247+
) -> Result<Response<BoxBody<Bytes, Infallible>>, crate::ServiceError>
248+
where
249+
T: Api<C> + Clone + Send + 'static,
250+
C: Has<XSpanIdString> + Send + Sync + 'static,
251+
ReqBody: Body + Send + 'static,
252+
ReqBody::Error: Into<Box<dyn Error + Send + Sync>> + Send,
253+
ReqBody::Data: Send,
254+
{
222255
// Handle body parameters (note that non-required body parameters will ignore garbage
223256
// values, rather than causing a 400 response). Produce warning header and logs for
224257
// any unused fields.
@@ -292,20 +325,6 @@ impl<T, C, ReqBody> hyper::service::Service<(Request<ReqBody>, C)> for Service<T
292325
.body(body_from_string(format!("Unable to read body: {}", e.into())))
293326
.expect("Unable to create Bad Request response due to unable to read body")),
294327
}
295-
},
296-
297-
_ if path.matched(paths::ID_OP) => method_not_allowed(),
298-
_ => Ok(Response::builder().status(StatusCode::NOT_FOUND)
299-
.body(BoxBody::new(http_body_util::Empty::new()))
300-
.expect("Unable to create Not Found response"))
301-
}
302-
}
303-
Box::pin(run(
304-
self.api_impl.clone(),
305-
req,
306-
self.validation
307-
))
308-
}
309328
}
310329

311330
/// Request parser for `Api`.

0 commit comments

Comments
 (0)