Skip to content

Commit 1cafc16

Browse files
authored
fix: apply integer type fitting for Rust params (#22853)
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.
1 parent 2682130 commit 1cafc16

10 files changed

Lines changed: 226 additions & 57 deletions

File tree

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

Lines changed: 60 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -610,6 +610,10 @@ public CodegenOperation fromOperation(String path, String httpMethod, Operation
610610
processParam(param, op);
611611
}
612612

613+
for (CodegenParameter param : op.pathParams) {
614+
processParam(param, op);
615+
}
616+
613617
// We keep track of the 'default' model type for this API. If there are
614618
// *any* XML responses, then we set the default to XML, otherwise we
615619
// let the default be JSON. It would be odd for an API to want to use
@@ -1459,6 +1463,45 @@ public String toAllOfName(List<String> names, Schema composedSchema) {
14591463
return null;
14601464
}
14611465

1466+
/**
1467+
* Determine the appropriate Rust integer type based on format and min/max constraints.
1468+
* Returns the fitted data type, or null if the baseType is not an integer.
1469+
*
1470+
* @param dataFormat The data format (e.g., "int32", "int64", "uint32", "uint64")
1471+
* @param minimum The minimum value constraint
1472+
* @param maximum The maximum value constraint
1473+
* @param exclusiveMinimum Whether the minimum is exclusive
1474+
* @param exclusiveMaximum Whether the maximum is exclusive
1475+
* @return The fitted Rust integer type.
1476+
*/
1477+
private String applyIntegerTypeFitting(String dataFormat,
1478+
String minimum, String maximum,
1479+
boolean exclusiveMinimum, boolean exclusiveMaximum) {
1480+
BigInteger min = Optional.ofNullable(minimum).filter(s -> !s.isEmpty()).map(BigInteger::new).orElse(null);
1481+
BigInteger max = Optional.ofNullable(maximum).filter(s -> !s.isEmpty()).map(BigInteger::new).orElse(null);
1482+
1483+
boolean unsigned = canFitIntoUnsigned(min, exclusiveMinimum);
1484+
1485+
if (Strings.isNullOrEmpty(dataFormat)) {
1486+
return bestFittingIntegerType(min, exclusiveMinimum, max, exclusiveMaximum, true);
1487+
} else {
1488+
switch (dataFormat) {
1489+
// custom integer formats (legacy)
1490+
case "uint32":
1491+
return "u32";
1492+
case "uint64":
1493+
return "u64";
1494+
case "int32":
1495+
return unsigned ? "u32" : "i32";
1496+
case "int64":
1497+
return unsigned ? "u64" : "i64";
1498+
default:
1499+
LOGGER.warn("The integer format '{}' is not recognized and will be ignored.", dataFormat);
1500+
return bestFittingIntegerType(min, exclusiveMinimum, max, exclusiveMaximum, true);
1501+
}
1502+
}
1503+
}
1504+
14621505
@Override
14631506
public void postProcessModelProperty(CodegenModel model, CodegenProperty property) {
14641507
super.postProcessModelProperty(model, property);
@@ -1492,41 +1535,12 @@ public void postProcessModelProperty(CodegenModel model, CodegenProperty propert
14921535
// Integer type fitting
14931536
if (Objects.equals(property.baseType, "integer")) {
14941537

1495-
BigInteger minimum = Optional.ofNullable(property.getMinimum()).map(BigInteger::new).orElse(null);
1496-
BigInteger maximum = Optional.ofNullable(property.getMaximum()).map(BigInteger::new).orElse(null);
1497-
1498-
boolean unsigned = canFitIntoUnsigned(minimum, property.getExclusiveMinimum());
1499-
1500-
if (Strings.isNullOrEmpty(property.dataFormat)) {
1501-
property.dataType = bestFittingIntegerType(minimum,
1502-
property.getExclusiveMinimum(),
1503-
maximum,
1504-
property.getExclusiveMaximum(),
1505-
true);
1506-
} else {
1507-
switch (property.dataFormat) {
1508-
// custom integer formats (legacy)
1509-
case "uint32":
1510-
property.dataType = "u32";
1511-
break;
1512-
case "uint64":
1513-
property.dataType = "u64";
1514-
break;
1515-
case "int32":
1516-
property.dataType = unsigned ? "u32" : "i32";
1517-
break;
1518-
case "int64":
1519-
property.dataType = unsigned ? "u64" : "i64";
1520-
break;
1521-
default:
1522-
LOGGER.warn("The integer format '{}' is not recognized and will be ignored.", property.dataFormat);
1523-
property.dataType = bestFittingIntegerType(minimum,
1524-
property.getExclusiveMinimum(),
1525-
maximum,
1526-
property.getExclusiveMaximum(),
1527-
true);
1528-
}
1529-
}
1538+
property.dataType = applyIntegerTypeFitting(
1539+
property.dataFormat,
1540+
property.getMinimum(),
1541+
property.getMaximum(),
1542+
property.getExclusiveMinimum(),
1543+
property.getExclusiveMaximum());
15301544
}
15311545

15321546
property.name = underscore(property.name);
@@ -1580,6 +1594,17 @@ public ModelsMap postProcessModels(ModelsMap objs) {
15801594
private void processParam(CodegenParameter param, CodegenOperation op) {
15811595
String example = null;
15821596

1597+
// If a parameter is an integer, fit it into the right type.
1598+
// Note: For CodegenParameter, baseType may be null, so we check isInteger/isLong/isShort flags instead.
1599+
if (param.isInteger || param.isLong || param.isShort) {
1600+
param.dataType = applyIntegerTypeFitting(
1601+
param.dataFormat,
1602+
param.minimum,
1603+
param.maximum,
1604+
param.exclusiveMinimum,
1605+
param.exclusiveMaximum);
1606+
}
1607+
15831608
// If a parameter uses UUIDs, we need to import the UUID package.
15841609
if (uuidType.equals(param.dataType)) {
15851610
additionalProperties.put("apiUsesUuid", true);
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
package org.openapitools.codegen.rust;
2+
3+
import org.openapitools.codegen.DefaultGenerator;
4+
import org.openapitools.codegen.TestUtils;
5+
import org.openapitools.codegen.config.CodegenConfigurator;
6+
import org.testng.annotations.Test;
7+
8+
import java.io.File;
9+
import java.io.IOException;
10+
import java.nio.file.Files;
11+
import java.nio.file.Path;
12+
import java.util.List;
13+
14+
/**
15+
* Tests for RustServerCodegen.
16+
*/
17+
public class RustServerCodegenTest {
18+
19+
/**
20+
* Test that integer parameters with minimum/maximum constraints are assigned appropriate Rust types.
21+
* This tests that integer parameter type fitting logic is applied to CodegenParameter objects.
22+
*/
23+
@Test
24+
public void testIntegerParameterTypeFitting() throws IOException {
25+
Path target = Files.createTempDirectory("test");
26+
final CodegenConfigurator configurator = new CodegenConfigurator()
27+
.setGeneratorName("rust-server")
28+
.setInputSpec("src/test/resources/3_0/rust-server/integer-params.yaml")
29+
.setSkipOverwrite(false)
30+
.setOutputDir(target.toAbsolutePath().toString().replace("\\", "/"));
31+
List<File> files = new DefaultGenerator().opts(configurator.toClientOptInput()).generate();
32+
files.forEach(File::deleteOnExit);
33+
34+
Path libPath = Path.of(target.toString(), "/src/lib.rs");
35+
TestUtils.assertFileExists(libPath);
36+
37+
// Verify that parameters with known min/max ranges get appropriate types
38+
// age: 0-150 should fit in u8
39+
TestUtils.assertFileContains(libPath, "age: u8");
40+
41+
// temperature: -50 to 50 should fit in i8
42+
TestUtils.assertFileContains(libPath, "temperature: i8");
43+
44+
// count: 0-65535 should fit in u16
45+
TestUtils.assertFileContains(libPath, "count: u16");
46+
47+
// offset: -32768 to 32767 should fit in i16
48+
TestUtils.assertFileContains(libPath, "offset: i16");
49+
50+
// large_unsigned: 0-4294967295 should be u32
51+
TestUtils.assertFileContains(libPath, "large_unsigned: u32");
52+
53+
// Verify integer with int32 format and minimum >= 0 becomes u32
54+
TestUtils.assertFileContains(libPath, "positive_int32: u32");
55+
56+
// Verify integer with int64 format and minimum >= 0 becomes u64
57+
TestUtils.assertFileContains(libPath, "positive_int64: u64");
58+
59+
// Clean up
60+
target.toFile().deleteOnExit();
61+
}
62+
}
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
2+
# Test that integer parameters are generated into the right
3+
# primitives in Rust code.
4+
openapi: 3.1.1
5+
info:
6+
title: Integer Parameter Type Fitting Test
7+
description: Test spec to verify that integer parameters with minimum/maximum constraints get appropriate Rust types
8+
version: 1.0.0
9+
servers:
10+
- url: http://localhost:8080
11+
paths:
12+
/test/integers:
13+
get:
14+
operationId: testIntegerParameters
15+
summary: Test integer parameter type fitting
16+
parameters:
17+
# age: 0-150 should fit in u8 (unsigned, 8-bit)
18+
- name: age
19+
in: query
20+
required: true
21+
schema:
22+
type: integer
23+
minimum: 0
24+
maximum: 150
25+
# temperature: -50 to 50 should fit in i8 (signed, 8-bit)
26+
- name: temperature
27+
in: query
28+
required: true
29+
schema:
30+
type: integer
31+
minimum: -50
32+
maximum: 50
33+
# count: 0-65535 should fit in u16 (unsigned, 16-bit)
34+
- name: count
35+
in: query
36+
required: true
37+
schema:
38+
type: integer
39+
minimum: 0
40+
maximum: 65535
41+
# offset: -32768 to 32767 should fit in i16 (signed, 16-bit)
42+
- name: offset
43+
in: query
44+
required: true
45+
schema:
46+
type: integer
47+
minimum: -32768
48+
maximum: 32767
49+
# large_unsigned: 0-4294967295 should be u32
50+
- name: large_unsigned
51+
in: query
52+
required: true
53+
schema:
54+
type: integer
55+
minimum: 0
56+
maximum: 4294967295
57+
# positive_int32: format int32 with min >= 0 should become u32
58+
- name: positive_int32
59+
in: query
60+
required: true
61+
schema:
62+
type: integer
63+
format: int32
64+
minimum: 0
65+
# positive_int64: format int64 with min >= 0 should become u64
66+
- name: positive_int64
67+
in: query
68+
required: true
69+
schema:
70+
type: integer
71+
format: int64
72+
minimum: 0
73+
responses:
74+
'200':
75+
description: OK
76+
content:
77+
application/json:
78+
schema:
79+
type: object
80+
properties:
81+
success:
82+
type: boolean

samples/server/petstore/rust-server/output/petstore-with-fake-endpoints-models-for-testing/bin/cli.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -154,9 +154,9 @@ enum Operation {
154154
#[clap(value_parser = parse_json::<swagger::ByteArray>)]
155155
byte: swagger::ByteArray,
156156
/// None
157-
integer: Option<i32>,
157+
integer: Option<u32>,
158158
/// None
159-
int32: Option<i32>,
159+
int32: Option<u32>,
160160
/// None
161161
int64: Option<i64>,
162162
/// None
@@ -292,7 +292,7 @@ enum Operation {
292292
/// Find purchase order by ID
293293
GetOrderById {
294294
/// ID of pet that needs to be fetched
295-
order_id: i64,
295+
order_id: u64,
296296
},
297297
/// Create user
298298
CreateUser {

samples/server/petstore/rust-server/output/petstore-with-fake-endpoints-models-for-testing/docs/fake_api.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -278,8 +278,8 @@ Name | Type | Description | Notes
278278
**double** | **f64**| None |
279279
**pattern_without_delimiter** | **String**| None |
280280
**byte** | **swagger::ByteArray**| None |
281-
**integer** | **i32**| None |
282-
**int32** | **i32**| None |
281+
**integer** | **u32**| None |
282+
**int32** | **u32**| None |
283283
**int64** | **i64**| None |
284284
**float** | **f32**| None |
285285
**string** | **String**| None |

samples/server/petstore/rust-server/output/petstore-with-fake-endpoints-models-for-testing/docs/store_api.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ For valid response try integer IDs with value <= 5 or > 10. Other values will ge
9696

9797
Name | Type | Description | Notes
9898
------------- | ------------- | ------------- | -------------
99-
**order_id** | **i64**| ID of pet that needs to be fetched |
99+
**order_id** | **u64**| ID of pet that needs to be fetched |
100100

101101
### Return type
102102

samples/server/petstore/rust-server/output/petstore-with-fake-endpoints-models-for-testing/examples/server/server.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -269,8 +269,8 @@ impl<C> Api<C> for Server<C> where C: Has<XSpanIdString> + Send + Sync
269269
double: f64,
270270
pattern_without_delimiter: String,
271271
byte: swagger::ByteArray,
272-
integer: Option<i32>,
273-
int32: Option<i32>,
272+
integer: Option<u32>,
273+
int32: Option<u32>,
274274
int64: Option<i64>,
275275
float: Option<f32>,
276276
string: Option<String>,
@@ -458,7 +458,7 @@ impl<C> Api<C> for Server<C> where C: Has<XSpanIdString> + Send + Sync
458458
/// Find purchase order by ID
459459
async fn get_order_by_id(
460460
&self,
461-
order_id: i64,
461+
order_id: u64,
462462
context: &C) -> Result<GetOrderByIdResponse, ApiError>
463463
{
464464
info!("get_order_by_id({}) - X-Span-ID: {:?}", order_id, context.get().0.clone());

samples/server/petstore/rust-server/output/petstore-with-fake-endpoints-models-for-testing/src/client/mod.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1241,8 +1241,8 @@ impl<S, C, B> Api<C> for Client<S, C> where
12411241
param_double: f64,
12421242
param_pattern_without_delimiter: String,
12431243
param_byte: swagger::ByteArray,
1244-
param_integer: Option<i32>,
1245-
param_int32: Option<i32>,
1244+
param_integer: Option<u32>,
1245+
param_int32: Option<u32>,
12461246
param_int64: Option<i64>,
12471247
param_float: Option<f32>,
12481248
param_string: Option<String>,
@@ -3065,7 +3065,7 @@ impl<S, C, B> Api<C> for Client<S, C> where
30653065
#[allow(clippy::vec_init_then_push)]
30663066
async fn get_order_by_id(
30673067
&self,
3068-
param_order_id: i64,
3068+
param_order_id: u64,
30693069
context: &C) -> Result<GetOrderByIdResponse, ApiError>
30703070
{
30713071
let mut client_service = self.client_service.clone();

0 commit comments

Comments
 (0)