Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ public class RustServerCodegen extends AbstractRustCodegen implements CodegenCon
protected String externCrateName;
protected Map<String, Map<String, String>> pathSetMap = new HashMap();
protected Map<String, Map<String, String>> callbacksPathSetMap = new HashMap();
protected Set<String> globalOperationIds = new HashSet<>();

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

String underscoredOperationId = underscore(op.operationId);
op.vendorExtensions.put("x-operation-id", underscoredOperationId);
op.vendorExtensions.put("x-uppercase-operation-id", underscoredOperationId.toUpperCase(Locale.ROOT));
// Deduplicate x-operation-id across all tag groups. All operations are merged into a single
// mod.rs, so handle_<x-operation-id>() functions must be globally unique, not just per-tag.
String uniqueOperationId = underscoredOperationId;
int opIdCounter = 0;
while (globalOperationIds.contains(uniqueOperationId)) {
uniqueOperationId = underscoredOperationId + "_" + opIdCounter;
opIdCounter++;
}
globalOperationIds.add(uniqueOperationId);
if (!uniqueOperationId.equals(underscoredOperationId)) {
LOGGER.warn("generated unique x-operation-id `{}` for operationId `{}`", uniqueOperationId, op.operationId);
}
op.vendorExtensions.put("x-operation-id", uniqueOperationId);
op.vendorExtensions.put("x-uppercase-operation-id", uniqueOperationId.toUpperCase(Locale.ROOT));
String vendorExtensionPath = op.path.replace("{", ":").replace("}", "");
op.vendorExtensions.put("x-path", vendorExtensionPath);
op.vendorExtensions.put("x-path-id", pathId);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,24 @@ use crate::{{{operationId}}}Response;
{{/callbacks}}
{{>server-service-footer}}

{{! Per-operation handler functions — extracted from the match arms in run() to
reduce per-function compilation-unit size and avoid rustc OOM on large APIs. }}
{{#apiInfo}}
{{#apis}}
{{#operations}}
{{#operation}}
{{#callbacks}}
{{#urls}}
{{#requests}}
{{>server-operation-handler}}

{{/requests}}
{{/urls}}
{{/callbacks}}
{{/operation}}
{{/operations}}
{{/apis}}
{{/apiInfo}}
/// Request parser for `Api`.
pub struct ApiRequestParser;
impl<T> RequestParser<T> for ApiRequestParser {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,18 @@ pub mod callbacks;
{{/pathSet}}
{{>server-service-footer}}

{{! Per-operation handler functions — extracted from the match arms in run() to
reduce per-function compilation-unit size and avoid rustc OOM on large APIs. }}
{{#apiInfo}}
{{#apis}}
{{#operations}}
{{#operation}}
{{>server-operation-handler}}

{{/operation}}
{{/operations}}
{{/apis}}
{{/apiInfo}}
/// Request parser for `Api`.
pub struct ApiRequestParser;
impl<T> RequestParser<T> for ApiRequestParser {
Expand Down

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import java.util.regex.Pattern;

/**
* Tests for RustServerCodegen.
Expand Down Expand Up @@ -60,6 +61,35 @@ public void testIntegerParameterTypeFitting() throws IOException {
target.toFile().deleteOnExit();
}

/**
* Test that two operations whose operationIds normalize to the same snake_case string
* (e.g., "fooBar" and "foo_bar" both become "foo_bar") receive distinct x-operation-id
* values. All operations are emitted as handle_<x-operation-id>() free functions in a
* single mod.rs, so duplicates would cause a Rust compile error.
*/
@Test
public void testDuplicateOperationIdDeduplication() throws IOException {
Path target = Files.createTempDirectory("test");
final CodegenConfigurator configurator = new CodegenConfigurator()
.setGeneratorName("rust-server")
.setInputSpec("src/test/resources/3_0/rust-server/duplicate-operation-id.yaml")
.setSkipOverwrite(false)
.setOutputDir(target.toAbsolutePath().toString().replace("\\", "/"));
List<File> files = new DefaultGenerator().opts(configurator.toClientOptInput()).generate();
files.forEach(File::deleteOnExit);

Path serverModPath = Path.of(target.toString(), "/src/server/mod.rs");
TestUtils.assertFileExists(serverModPath);

// Both operations produce snake_case "foo_bar". The second should be renamed to
// "foo_bar_0" so there are two distinct handle_*() functions, not a duplicate.
TestUtils.assertFileContains(serverModPath, "handle_foo_bar(");
TestUtils.assertFileContains(serverModPath, "handle_foo_bar_0(");

// Clean up
target.toFile().deleteOnExit();
}

/**
* Test that required query params without examples disable the client example.
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@

# Test that two operations in different tags with the same operationId (after snake_case
# normalization) produce distinct x-operation-id values, avoiding a Rust compile error
# from duplicate handle_<x-operation-id>() functions in the same mod.rs.
openapi: 3.1.1
info:
title: Duplicate OperationId Test
description: Spec with two operations that normalize to the same snake_case operation ID
version: 1.0.0
servers:
- url: http://localhost:8080
paths:
/foo/bar:
get:
operationId: fooBar
summary: First operation
tags:
- TagA
responses:
"200":
description: OK
/foo/baz:
get:
operationId: foo_bar
summary: Second operation - normalizes to same snake_case as fooBar
tags:
- TagB
responses:
"200":
description: OK
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,53 @@ impl<T, C, ReqBody> hyper::service::Service<(Request<ReqBody>, C)> for Service<T

// MultipartRelatedRequestPost - POST /multipart_related_request
hyper::Method::POST if path.matched(paths::ID_MULTIPART_RELATED_REQUEST) => {
handle_multipart_related_request_post(api_impl, uri, headers, body, context, validation, multipart_form_size_limit).await
},

// MultipartRequestPost - POST /multipart_request
hyper::Method::POST if path.matched(paths::ID_MULTIPART_REQUEST) => {
handle_multipart_request_post(api_impl, uri, headers, body, context, validation, multipart_form_size_limit).await
},

// MultipleIdenticalMimeTypesPost - POST /multiple-identical-mime-types
hyper::Method::POST if path.matched(paths::ID_MULTIPLE_IDENTICAL_MIME_TYPES) => {
handle_multiple_identical_mime_types_post(api_impl, uri, headers, body, context, validation, multipart_form_size_limit).await
},

_ if path.matched(paths::ID_MULTIPART_RELATED_REQUEST) => method_not_allowed(),
_ if path.matched(paths::ID_MULTIPART_REQUEST) => method_not_allowed(),
_ if path.matched(paths::ID_MULTIPLE_IDENTICAL_MIME_TYPES) => method_not_allowed(),
_ => Ok(Response::builder().status(StatusCode::NOT_FOUND)
.body(BoxBody::new(http_body_util::Empty::new()))
.expect("Unable to create Not Found response"))
}
}
Box::pin(run(
self.api_impl.clone(),
req,
self.validation,
self.multipart_form_size_limit
))
}
}

#[allow(unused_variables)]
async fn handle_multipart_related_request_post<T, C, ReqBody>(
mut api_impl: T,
uri: hyper::Uri,
headers: HeaderMap,
body: ReqBody,
context: C,
validation: bool,
multipart_form_size_limit: Option<u64>,
) -> Result<Response<BoxBody<Bytes, Infallible>>, crate::ServiceError>
where
T: Api<C> + Clone + Send + 'static,
C: Has<XSpanIdString> + Send + Sync + 'static,
ReqBody: Body + Send + 'static,
ReqBody::Error: Into<Box<dyn Error + Send + Sync>> + Send,
ReqBody::Data: Send,
{
// Handle body parameters (note that non-required body parameters will ignore garbage
// values, rather than causing a 400 response). Produce warning header and logs for
// any unused fields.
Expand Down Expand Up @@ -383,10 +430,25 @@ impl<T, C, ReqBody> hyper::service::Service<(Request<ReqBody>, C)> for Service<T
.body(body_from_string(format!("Unable to read body: {}", e.into())))
.expect("Unable to create Bad Request response due to unable to read body")),
}
},
}

// MultipartRequestPost - POST /multipart_request
hyper::Method::POST if path.matched(paths::ID_MULTIPART_REQUEST) => {
#[allow(unused_variables)]
async fn handle_multipart_request_post<T, C, ReqBody>(
mut api_impl: T,
uri: hyper::Uri,
headers: HeaderMap,
body: ReqBody,
context: C,
validation: bool,
multipart_form_size_limit: Option<u64>,
) -> Result<Response<BoxBody<Bytes, Infallible>>, crate::ServiceError>
where
T: Api<C> + Clone + Send + 'static,
C: Has<XSpanIdString> + Send + Sync + 'static,
ReqBody: Body + Send + 'static,
ReqBody::Error: Into<Box<dyn Error + Send + Sync>> + Send,
ReqBody::Data: Send,
{
// Handle body parameters (note that non-required body parameters will ignore garbage
// values, rather than causing a 400 response). Produce warning header and logs for
// any unused fields.
Expand Down Expand Up @@ -571,10 +633,25 @@ impl<T, C, ReqBody> hyper::service::Service<(Request<ReqBody>, C)> for Service<T
.body(body_from_string(format!("Unable to read body: {}", e.into())))
.expect("Unable to create Bad Request response due to unable to read body")),
}
},
}

// MultipleIdenticalMimeTypesPost - POST /multiple-identical-mime-types
hyper::Method::POST if path.matched(paths::ID_MULTIPLE_IDENTICAL_MIME_TYPES) => {
#[allow(unused_variables)]
async fn handle_multiple_identical_mime_types_post<T, C, ReqBody>(
mut api_impl: T,
uri: hyper::Uri,
headers: HeaderMap,
body: ReqBody,
context: C,
validation: bool,
multipart_form_size_limit: Option<u64>,
) -> Result<Response<BoxBody<Bytes, Infallible>>, crate::ServiceError>
where
T: Api<C> + Clone + Send + 'static,
C: Has<XSpanIdString> + Send + Sync + 'static,
ReqBody: Body + Send + 'static,
ReqBody::Error: Into<Box<dyn Error + Send + Sync>> + Send,
ReqBody::Data: Send,
{
// Handle body parameters (note that non-required body parameters will ignore garbage
// values, rather than causing a 400 response). Produce warning header and logs for
// any unused fields.
Expand Down Expand Up @@ -677,23 +754,6 @@ impl<T, C, ReqBody> hyper::service::Service<(Request<ReqBody>, C)> for Service<T
.body(body_from_string(format!("Unable to read body: {}", e.into())))
.expect("Unable to create Bad Request response due to unable to read body")),
}
},

_ if path.matched(paths::ID_MULTIPART_RELATED_REQUEST) => method_not_allowed(),
_ if path.matched(paths::ID_MULTIPART_REQUEST) => method_not_allowed(),
_ if path.matched(paths::ID_MULTIPLE_IDENTICAL_MIME_TYPES) => method_not_allowed(),
_ => Ok(Response::builder().status(StatusCode::NOT_FOUND)
.body(BoxBody::new(http_body_util::Empty::new()))
.expect("Unable to create Not Found response"))
}
}
Box::pin(run(
self.api_impl.clone(),
req,
self.validation,
self.multipart_form_size_limit
))
}
}

/// Request parser for `Api`.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,39 @@ impl<T, C, ReqBody> hyper::service::Service<(Request<ReqBody>, C)> for Service<T

// OpGet - GET /op
hyper::Method::GET if path.matched(paths::ID_OP) => {
handle_op_get(api_impl, uri, headers, body, context, validation).await
},

_ if path.matched(paths::ID_OP) => method_not_allowed(),
_ => Ok(Response::builder().status(StatusCode::NOT_FOUND)
.body(BoxBody::new(http_body_util::Empty::new()))
.expect("Unable to create Not Found response"))
}
}
Box::pin(run(
self.api_impl.clone(),
req,
self.validation
))
}
}

#[allow(unused_variables)]
async fn handle_op_get<T, C, ReqBody>(
mut api_impl: T,
uri: hyper::Uri,
headers: HeaderMap,
body: ReqBody,
context: C,
validation: bool,
) -> Result<Response<BoxBody<Bytes, Infallible>>, crate::ServiceError>
where
T: Api<C> + Clone + Send + 'static,
C: Has<XSpanIdString> + Send + Sync + 'static,
ReqBody: Body + Send + 'static,
ReqBody::Error: Into<Box<dyn Error + Send + Sync>> + Send,
ReqBody::Data: Send,
{
// Handle body parameters (note that non-required body parameters will ignore garbage
// values, rather than causing a 400 response). Produce warning header and logs for
// any unused fields.
Expand Down Expand Up @@ -292,20 +325,6 @@ impl<T, C, ReqBody> hyper::service::Service<(Request<ReqBody>, C)> for Service<T
.body(body_from_string(format!("Unable to read body: {}", e.into())))
.expect("Unable to create Bad Request response due to unable to read body")),
}
},

_ if path.matched(paths::ID_OP) => method_not_allowed(),
_ => Ok(Response::builder().status(StatusCode::NOT_FOUND)
.body(BoxBody::new(http_body_util::Empty::new()))
.expect("Unable to create Not Found response"))
}
}
Box::pin(run(
self.api_impl.clone(),
req,
self.validation
))
}
}

/// Request parser for `Api`.
Expand Down
Loading
Loading