Skip to content

Commit a8fe886

Browse files
committed
fix: apply integer type fitting in lib.rs
We already have logic in postProcessModelProperty to fit integer parameters into the correct Rust primitives. However, this doesn't apply to CodegenParameters 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 CodegenParameter and model post-processing.
1 parent 6f211a2 commit a8fe886

9 files changed

Lines changed: 221 additions & 56 deletions

File tree

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

Lines changed: 56 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1459,6 +1459,45 @@ public String toAllOfName(List<String> names, Schema composedSchema) {
14591459
return null;
14601460
}
14611461

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

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-
}
1534+
property.dataType = applyIntegerTypeFitting(
1535+
property.dataFormat,
1536+
property.getMinimum(),
1537+
property.getMaximum(),
1538+
property.getExclusiveMinimum(),
1539+
property.getExclusiveMaximum());
15301540
}
15311541

15321542
property.name = underscore(property.name);
@@ -1580,6 +1590,17 @@ public ModelsMap postProcessModels(ModelsMap objs) {
15801590
private void processParam(CodegenParameter param, CodegenOperation op) {
15811591
String example = null;
15821592

1593+
// If a parameter is an integer, fit it into the right type.
1594+
// Note: For CodegenParameter, baseType may be null, so we check isInteger/isLong/isShort flags instead.
1595+
if (param.isInteger || param.isLong || param.isShort) {
1596+
param.dataType = applyIntegerTypeFitting(
1597+
param.dataFormat,
1598+
param.minimum,
1599+
param.maximum,
1600+
param.exclusiveMinimum,
1601+
param.exclusiveMaximum);
1602+
}
1603+
15831604
// If a parameter uses UUIDs, we need to import the UUID package.
15841605
if (uuidType.equals(param.dataType)) {
15851606
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
@@ -1208,8 +1208,8 @@ impl<S, C, B> Api<C> for Client<S, C> where
12081208
param_double: f64,
12091209
param_pattern_without_delimiter: String,
12101210
param_byte: swagger::ByteArray,
1211-
param_integer: Option<i32>,
1212-
param_int32: Option<i32>,
1211+
param_integer: Option<u32>,
1212+
param_int32: Option<u32>,
12131213
param_int64: Option<i64>,
12141214
param_float: Option<f32>,
12151215
param_string: Option<String>,
@@ -3032,7 +3032,7 @@ impl<S, C, B> Api<C> for Client<S, C> where
30323032
#[allow(clippy::vec_init_then_push)]
30333033
async fn get_order_by_id(
30343034
&self,
3035-
param_order_id: i64,
3035+
param_order_id: u64,
30363036
context: &C) -> Result<GetOrderByIdResponse, ApiError>
30373037
{
30383038
let mut client_service = self.client_service.clone();

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

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -380,8 +380,8 @@ pub trait Api<C: Send + Sync> {
380380
double: f64,
381381
pattern_without_delimiter: String,
382382
byte: swagger::ByteArray,
383-
integer: Option<i32>,
384-
int32: Option<i32>,
383+
integer: Option<u32>,
384+
int32: Option<u32>,
385385
int64: Option<i64>,
386386
float: Option<f32>,
387387
string: Option<String>,
@@ -501,7 +501,7 @@ pub trait Api<C: Send + Sync> {
501501
/// Find purchase order by ID
502502
async fn get_order_by_id(
503503
&self,
504-
order_id: i64,
504+
order_id: u64,
505505
context: &C) -> Result<GetOrderByIdResponse, ApiError>;
506506

507507
/// Create user
@@ -620,8 +620,8 @@ pub trait ApiNoContext<C: Send + Sync> {
620620
double: f64,
621621
pattern_without_delimiter: String,
622622
byte: swagger::ByteArray,
623-
integer: Option<i32>,
624-
int32: Option<i32>,
623+
integer: Option<u32>,
624+
int32: Option<u32>,
625625
int64: Option<i64>,
626626
float: Option<f32>,
627627
string: Option<String>,
@@ -741,7 +741,7 @@ pub trait ApiNoContext<C: Send + Sync> {
741741
/// Find purchase order by ID
742742
async fn get_order_by_id(
743743
&self,
744-
order_id: i64,
744+
order_id: u64,
745745
) -> Result<GetOrderByIdResponse, ApiError>;
746746

747747
/// Create user
@@ -903,8 +903,8 @@ impl<T: Api<C> + Send + Sync, C: Clone + Send + Sync> ApiNoContext<C> for Contex
903903
double: f64,
904904
pattern_without_delimiter: String,
905905
byte: swagger::ByteArray,
906-
integer: Option<i32>,
907-
int32: Option<i32>,
906+
integer: Option<u32>,
907+
int32: Option<u32>,
908908
int64: Option<i64>,
909909
float: Option<f32>,
910910
string: Option<String>,
@@ -1092,7 +1092,7 @@ impl<T: Api<C> + Send + Sync, C: Clone + Send + Sync> ApiNoContext<C> for Contex
10921092
/// Find purchase order by ID
10931093
async fn get_order_by_id(
10941094
&self,
1095-
order_id: i64,
1095+
order_id: u64,
10961096
) -> Result<GetOrderByIdResponse, ApiError>
10971097
{
10981098
let context = self.context().clone();

0 commit comments

Comments
 (0)