From cbc71d7c38c62aeb0720fc6954f5fb4bd4e9eb30 Mon Sep 17 00:00:00 2001 From: George Holderness Date: Fri, 30 Jan 2026 16:04:59 +0000 Subject: [PATCH] fix: apply integer type fitting for Rust params We already have logic in postProcessModelProperty to fit integer parameters into the correct Rust primitives. However, this doesn't apply to other kinds of parameters so integer-typed parameters which end up in function calls for Api traits in lib.rs are always i32, even when this is improper. This commit refactors integer type fitting so that we can run it on both processParam and model post-processing. --- .../codegen/languages/RustServerCodegen.java | 95 ++++++++++++------- .../codegen/rust/RustServerCodegenTest.java | 62 ++++++++++++ .../3_0/rust-server/integer-params.yaml | 82 ++++++++++++++++ .../bin/cli.rs | 6 +- .../docs/fake_api.md | 4 +- .../docs/store_api.md | 2 +- .../examples/server/server.rs | 6 +- .../src/client/mod.rs | 6 +- .../src/lib.rs | 18 ++-- .../src/server/mod.rs | 2 +- 10 files changed, 226 insertions(+), 57 deletions(-) create mode 100644 modules/openapi-generator/src/test/java/org/openapitools/codegen/rust/RustServerCodegenTest.java create mode 100644 modules/openapi-generator/src/test/resources/3_0/rust-server/integer-params.yaml diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/RustServerCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/RustServerCodegen.java index 39a45900a91a..e54439b40fa0 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/RustServerCodegen.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/RustServerCodegen.java @@ -610,6 +610,10 @@ public CodegenOperation fromOperation(String path, String httpMethod, Operation processParam(param, op); } + for (CodegenParameter param : op.pathParams) { + processParam(param, op); + } + // We keep track of the 'default' model type for this API. If there are // *any* XML responses, then we set the default to XML, otherwise we // let the default be JSON. It would be odd for an API to want to use @@ -1459,6 +1463,45 @@ public String toAllOfName(List names, Schema composedSchema) { return null; } + /** + * Determine the appropriate Rust integer type based on format and min/max constraints. + * Returns the fitted data type, or null if the baseType is not an integer. + * + * @param dataFormat The data format (e.g., "int32", "int64", "uint32", "uint64") + * @param minimum The minimum value constraint + * @param maximum The maximum value constraint + * @param exclusiveMinimum Whether the minimum is exclusive + * @param exclusiveMaximum Whether the maximum is exclusive + * @return The fitted Rust integer type. + */ + private String applyIntegerTypeFitting(String dataFormat, + String minimum, String maximum, + boolean exclusiveMinimum, boolean exclusiveMaximum) { + BigInteger min = Optional.ofNullable(minimum).filter(s -> !s.isEmpty()).map(BigInteger::new).orElse(null); + BigInteger max = Optional.ofNullable(maximum).filter(s -> !s.isEmpty()).map(BigInteger::new).orElse(null); + + boolean unsigned = canFitIntoUnsigned(min, exclusiveMinimum); + + if (Strings.isNullOrEmpty(dataFormat)) { + return bestFittingIntegerType(min, exclusiveMinimum, max, exclusiveMaximum, true); + } else { + switch (dataFormat) { + // custom integer formats (legacy) + case "uint32": + return "u32"; + case "uint64": + return "u64"; + case "int32": + return unsigned ? "u32" : "i32"; + case "int64": + return unsigned ? "u64" : "i64"; + default: + LOGGER.warn("The integer format '{}' is not recognized and will be ignored.", dataFormat); + return bestFittingIntegerType(min, exclusiveMinimum, max, exclusiveMaximum, true); + } + } + } + @Override public void postProcessModelProperty(CodegenModel model, CodegenProperty property) { super.postProcessModelProperty(model, property); @@ -1492,41 +1535,12 @@ public void postProcessModelProperty(CodegenModel model, CodegenProperty propert // Integer type fitting if (Objects.equals(property.baseType, "integer")) { - BigInteger minimum = Optional.ofNullable(property.getMinimum()).map(BigInteger::new).orElse(null); - BigInteger maximum = Optional.ofNullable(property.getMaximum()).map(BigInteger::new).orElse(null); - - boolean unsigned = canFitIntoUnsigned(minimum, property.getExclusiveMinimum()); - - if (Strings.isNullOrEmpty(property.dataFormat)) { - property.dataType = bestFittingIntegerType(minimum, - property.getExclusiveMinimum(), - maximum, - property.getExclusiveMaximum(), - true); - } else { - switch (property.dataFormat) { - // custom integer formats (legacy) - case "uint32": - property.dataType = "u32"; - break; - case "uint64": - property.dataType = "u64"; - break; - case "int32": - property.dataType = unsigned ? "u32" : "i32"; - break; - case "int64": - property.dataType = unsigned ? "u64" : "i64"; - break; - default: - LOGGER.warn("The integer format '{}' is not recognized and will be ignored.", property.dataFormat); - property.dataType = bestFittingIntegerType(minimum, - property.getExclusiveMinimum(), - maximum, - property.getExclusiveMaximum(), - true); - } - } + property.dataType = applyIntegerTypeFitting( + property.dataFormat, + property.getMinimum(), + property.getMaximum(), + property.getExclusiveMinimum(), + property.getExclusiveMaximum()); } property.name = underscore(property.name); @@ -1580,6 +1594,17 @@ public ModelsMap postProcessModels(ModelsMap objs) { private void processParam(CodegenParameter param, CodegenOperation op) { String example = null; + // If a parameter is an integer, fit it into the right type. + // Note: For CodegenParameter, baseType may be null, so we check isInteger/isLong/isShort flags instead. + if (param.isInteger || param.isLong || param.isShort) { + param.dataType = applyIntegerTypeFitting( + param.dataFormat, + param.minimum, + param.maximum, + param.exclusiveMinimum, + param.exclusiveMaximum); + } + // If a parameter uses UUIDs, we need to import the UUID package. if (uuidType.equals(param.dataType)) { additionalProperties.put("apiUsesUuid", true); diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/rust/RustServerCodegenTest.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/rust/RustServerCodegenTest.java new file mode 100644 index 000000000000..6b730cce6f18 --- /dev/null +++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/rust/RustServerCodegenTest.java @@ -0,0 +1,62 @@ +package org.openapitools.codegen.rust; + +import org.openapitools.codegen.DefaultGenerator; +import org.openapitools.codegen.TestUtils; +import org.openapitools.codegen.config.CodegenConfigurator; +import org.testng.annotations.Test; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; + +/** + * Tests for RustServerCodegen. + */ +public class RustServerCodegenTest { + + /** + * Test that integer parameters with minimum/maximum constraints are assigned appropriate Rust types. + * This tests that integer parameter type fitting logic is applied to CodegenParameter objects. + */ + @Test + public void testIntegerParameterTypeFitting() throws IOException { + Path target = Files.createTempDirectory("test"); + final CodegenConfigurator configurator = new CodegenConfigurator() + .setGeneratorName("rust-server") + .setInputSpec("src/test/resources/3_0/rust-server/integer-params.yaml") + .setSkipOverwrite(false) + .setOutputDir(target.toAbsolutePath().toString().replace("\\", "/")); + List files = new DefaultGenerator().opts(configurator.toClientOptInput()).generate(); + files.forEach(File::deleteOnExit); + + Path libPath = Path.of(target.toString(), "/src/lib.rs"); + TestUtils.assertFileExists(libPath); + + // Verify that parameters with known min/max ranges get appropriate types + // age: 0-150 should fit in u8 + TestUtils.assertFileContains(libPath, "age: u8"); + + // temperature: -50 to 50 should fit in i8 + TestUtils.assertFileContains(libPath, "temperature: i8"); + + // count: 0-65535 should fit in u16 + TestUtils.assertFileContains(libPath, "count: u16"); + + // offset: -32768 to 32767 should fit in i16 + TestUtils.assertFileContains(libPath, "offset: i16"); + + // large_unsigned: 0-4294967295 should be u32 + TestUtils.assertFileContains(libPath, "large_unsigned: u32"); + + // Verify integer with int32 format and minimum >= 0 becomes u32 + TestUtils.assertFileContains(libPath, "positive_int32: u32"); + + // Verify integer with int64 format and minimum >= 0 becomes u64 + TestUtils.assertFileContains(libPath, "positive_int64: u64"); + + // Clean up + target.toFile().deleteOnExit(); + } +} diff --git a/modules/openapi-generator/src/test/resources/3_0/rust-server/integer-params.yaml b/modules/openapi-generator/src/test/resources/3_0/rust-server/integer-params.yaml new file mode 100644 index 000000000000..5869a005e910 --- /dev/null +++ b/modules/openapi-generator/src/test/resources/3_0/rust-server/integer-params.yaml @@ -0,0 +1,82 @@ + +# Test that integer parameters are generated into the right +# primitives in Rust code. +openapi: 3.1.1 +info: + title: Integer Parameter Type Fitting Test + description: Test spec to verify that integer parameters with minimum/maximum constraints get appropriate Rust types + version: 1.0.0 +servers: + - url: http://localhost:8080 +paths: + /test/integers: + get: + operationId: testIntegerParameters + summary: Test integer parameter type fitting + parameters: + # age: 0-150 should fit in u8 (unsigned, 8-bit) + - name: age + in: query + required: true + schema: + type: integer + minimum: 0 + maximum: 150 + # temperature: -50 to 50 should fit in i8 (signed, 8-bit) + - name: temperature + in: query + required: true + schema: + type: integer + minimum: -50 + maximum: 50 + # count: 0-65535 should fit in u16 (unsigned, 16-bit) + - name: count + in: query + required: true + schema: + type: integer + minimum: 0 + maximum: 65535 + # offset: -32768 to 32767 should fit in i16 (signed, 16-bit) + - name: offset + in: query + required: true + schema: + type: integer + minimum: -32768 + maximum: 32767 + # large_unsigned: 0-4294967295 should be u32 + - name: large_unsigned + in: query + required: true + schema: + type: integer + minimum: 0 + maximum: 4294967295 + # positive_int32: format int32 with min >= 0 should become u32 + - name: positive_int32 + in: query + required: true + schema: + type: integer + format: int32 + minimum: 0 + # positive_int64: format int64 with min >= 0 should become u64 + - name: positive_int64 + in: query + required: true + schema: + type: integer + format: int64 + minimum: 0 + responses: + '200': + description: OK + content: + application/json: + schema: + type: object + properties: + success: + type: boolean diff --git a/samples/server/petstore/rust-server/output/petstore-with-fake-endpoints-models-for-testing/bin/cli.rs b/samples/server/petstore/rust-server/output/petstore-with-fake-endpoints-models-for-testing/bin/cli.rs index 5cc7d996d9a2..4bb45def50f0 100644 --- a/samples/server/petstore/rust-server/output/petstore-with-fake-endpoints-models-for-testing/bin/cli.rs +++ b/samples/server/petstore/rust-server/output/petstore-with-fake-endpoints-models-for-testing/bin/cli.rs @@ -154,9 +154,9 @@ enum Operation { #[clap(value_parser = parse_json::)] byte: swagger::ByteArray, /// None - integer: Option, + integer: Option, /// None - int32: Option, + int32: Option, /// None int64: Option, /// None @@ -292,7 +292,7 @@ enum Operation { /// Find purchase order by ID GetOrderById { /// ID of pet that needs to be fetched - order_id: i64, + order_id: u64, }, /// Create user CreateUser { diff --git a/samples/server/petstore/rust-server/output/petstore-with-fake-endpoints-models-for-testing/docs/fake_api.md b/samples/server/petstore/rust-server/output/petstore-with-fake-endpoints-models-for-testing/docs/fake_api.md index 27c59de54b24..9b690e7b1ff3 100644 --- a/samples/server/petstore/rust-server/output/petstore-with-fake-endpoints-models-for-testing/docs/fake_api.md +++ b/samples/server/petstore/rust-server/output/petstore-with-fake-endpoints-models-for-testing/docs/fake_api.md @@ -278,8 +278,8 @@ Name | Type | Description | Notes **double** | **f64**| None | **pattern_without_delimiter** | **String**| None | **byte** | **swagger::ByteArray**| None | - **integer** | **i32**| None | - **int32** | **i32**| None | + **integer** | **u32**| None | + **int32** | **u32**| None | **int64** | **i64**| None | **float** | **f32**| None | **string** | **String**| None | diff --git a/samples/server/petstore/rust-server/output/petstore-with-fake-endpoints-models-for-testing/docs/store_api.md b/samples/server/petstore/rust-server/output/petstore-with-fake-endpoints-models-for-testing/docs/store_api.md index f8e633abd83e..2e92b2ee355a 100644 --- a/samples/server/petstore/rust-server/output/petstore-with-fake-endpoints-models-for-testing/docs/store_api.md +++ b/samples/server/petstore/rust-server/output/petstore-with-fake-endpoints-models-for-testing/docs/store_api.md @@ -96,7 +96,7 @@ For valid response try integer IDs with value <= 5 or > 10. Other values will ge Name | Type | Description | Notes ------------- | ------------- | ------------- | ------------- - **order_id** | **i64**| ID of pet that needs to be fetched | + **order_id** | **u64**| ID of pet that needs to be fetched | ### Return type diff --git a/samples/server/petstore/rust-server/output/petstore-with-fake-endpoints-models-for-testing/examples/server/server.rs b/samples/server/petstore/rust-server/output/petstore-with-fake-endpoints-models-for-testing/examples/server/server.rs index 87df5af2b446..98ed4ce9894a 100644 --- a/samples/server/petstore/rust-server/output/petstore-with-fake-endpoints-models-for-testing/examples/server/server.rs +++ b/samples/server/petstore/rust-server/output/petstore-with-fake-endpoints-models-for-testing/examples/server/server.rs @@ -269,8 +269,8 @@ impl Api for Server where C: Has + Send + Sync double: f64, pattern_without_delimiter: String, byte: swagger::ByteArray, - integer: Option, - int32: Option, + integer: Option, + int32: Option, int64: Option, float: Option, string: Option, @@ -458,7 +458,7 @@ impl Api for Server where C: Has + Send + Sync /// Find purchase order by ID async fn get_order_by_id( &self, - order_id: i64, + order_id: u64, context: &C) -> Result { info!("get_order_by_id({}) - X-Span-ID: {:?}", order_id, context.get().0.clone()); diff --git a/samples/server/petstore/rust-server/output/petstore-with-fake-endpoints-models-for-testing/src/client/mod.rs b/samples/server/petstore/rust-server/output/petstore-with-fake-endpoints-models-for-testing/src/client/mod.rs index 93a264aefb97..e2d780db6582 100644 --- a/samples/server/petstore/rust-server/output/petstore-with-fake-endpoints-models-for-testing/src/client/mod.rs +++ b/samples/server/petstore/rust-server/output/petstore-with-fake-endpoints-models-for-testing/src/client/mod.rs @@ -1208,8 +1208,8 @@ impl Api for Client where param_double: f64, param_pattern_without_delimiter: String, param_byte: swagger::ByteArray, - param_integer: Option, - param_int32: Option, + param_integer: Option, + param_int32: Option, param_int64: Option, param_float: Option, param_string: Option, @@ -3032,7 +3032,7 @@ impl Api for Client where #[allow(clippy::vec_init_then_push)] async fn get_order_by_id( &self, - param_order_id: i64, + param_order_id: u64, context: &C) -> Result { let mut client_service = self.client_service.clone(); diff --git a/samples/server/petstore/rust-server/output/petstore-with-fake-endpoints-models-for-testing/src/lib.rs b/samples/server/petstore/rust-server/output/petstore-with-fake-endpoints-models-for-testing/src/lib.rs index aa34695336e7..3c409b899326 100644 --- a/samples/server/petstore/rust-server/output/petstore-with-fake-endpoints-models-for-testing/src/lib.rs +++ b/samples/server/petstore/rust-server/output/petstore-with-fake-endpoints-models-for-testing/src/lib.rs @@ -380,8 +380,8 @@ pub trait Api { double: f64, pattern_without_delimiter: String, byte: swagger::ByteArray, - integer: Option, - int32: Option, + integer: Option, + int32: Option, int64: Option, float: Option, string: Option, @@ -501,7 +501,7 @@ pub trait Api { /// Find purchase order by ID async fn get_order_by_id( &self, - order_id: i64, + order_id: u64, context: &C) -> Result; /// Create user @@ -620,8 +620,8 @@ pub trait ApiNoContext { double: f64, pattern_without_delimiter: String, byte: swagger::ByteArray, - integer: Option, - int32: Option, + integer: Option, + int32: Option, int64: Option, float: Option, string: Option, @@ -741,7 +741,7 @@ pub trait ApiNoContext { /// Find purchase order by ID async fn get_order_by_id( &self, - order_id: i64, + order_id: u64, ) -> Result; /// Create user @@ -903,8 +903,8 @@ impl + Send + Sync, C: Clone + Send + Sync> ApiNoContext for Contex double: f64, pattern_without_delimiter: String, byte: swagger::ByteArray, - integer: Option, - int32: Option, + integer: Option, + int32: Option, int64: Option, float: Option, string: Option, @@ -1092,7 +1092,7 @@ impl + Send + Sync, C: Clone + Send + Sync> ApiNoContext for Contex /// Find purchase order by ID async fn get_order_by_id( &self, - order_id: i64, + order_id: u64, ) -> Result { let context = self.context().clone(); diff --git a/samples/server/petstore/rust-server/output/petstore-with-fake-endpoints-models-for-testing/src/server/mod.rs b/samples/server/petstore/rust-server/output/petstore-with-fake-endpoints-models-for-testing/src/server/mod.rs index 57e02efadc1f..83e6ca4f40a6 100644 --- a/samples/server/petstore/rust-server/output/petstore-with-fake-endpoints-models-for-testing/src/server/mod.rs +++ b/samples/server/petstore/rust-server/output/petstore-with-fake-endpoints-models-for-testing/src/server/mod.rs @@ -2606,7 +2606,7 @@ impl hyper::service::Service<(Request, C)> for Service match param_order_id.parse::() { + Ok(param_order_id) => match param_order_id.parse::() { Ok(param_order_id) => param_order_id, Err(e) => return Ok(Response::builder() .status(StatusCode::BAD_REQUEST)