Skip to content

Commit 6ae6757

Browse files
author
Thomas Ville
committed
[rust] support discriminators on existing fields
1 parent 4f9f14a commit 6ae6757

File tree

94 files changed

+2898
-8
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

94 files changed

+2898
-8
lines changed
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
generatorName: rust
2+
outputDir: samples/client/others/rust/hyper/discriminator-reserved-keyword
3+
library: hyper
4+
inputSpec: modules/openapi-generator/src/test/resources/3_0/rust/discriminator-reserved-keyword.yaml
5+
templateDir: modules/openapi-generator/src/main/resources/rust
6+
additionalProperties:
7+
supportAsync: false
8+
packageName: discriminator-reserved-keyword-hyper
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
generatorName: rust
2+
outputDir: samples/client/others/rust/reqwest/discriminator-reserved-keyword
3+
library: reqwest
4+
inputSpec: modules/openapi-generator/src/test/resources/3_0/rust/discriminator-reserved-keyword.yaml
5+
templateDir: modules/openapi-generator/src/main/resources/rust
6+
additionalProperties:
7+
supportAsync: false
8+
packageName: discriminator-reserved-keyword-reqwest

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

Lines changed: 77 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -318,6 +318,20 @@ public CodegenModel fromModel(String name, Schema model) {
318318
return mdl;
319319
}
320320

321+
@Override
322+
public Map<String, ModelsMap> postProcessAllModels(Map<String, ModelsMap> objs) {
323+
// Collect all models into a single list
324+
List<ModelMap> allModels = new ArrayList<>();
325+
for (ModelsMap models : objs.values()) {
326+
allModels.addAll(models.getModels());
327+
}
328+
329+
// Process oneOf discriminators across all models
330+
postProcessOneOfModels(allModels);
331+
332+
return super.postProcessAllModels(objs);
333+
}
334+
321335
@Override
322336
public ModelsMap postProcessModels(ModelsMap objs) {
323337
for (ModelMap model : objs.getModels()) {
@@ -666,6 +680,66 @@ public void postProcessModelProperty(CodegenModel model, CodegenProperty propert
666680
}
667681
}
668682

683+
private void postProcessOneOfModels(List<ModelMap> allModels) {
684+
final HashMap<String, List<String>> oneOfMapDiscriminator = new HashMap<>();
685+
686+
for (ModelMap mo : allModels) {
687+
final CodegenModel cm = mo.getModel();
688+
689+
final CodegenComposedSchemas cs = cm.getComposedSchemas();
690+
691+
if (cs != null) {
692+
final List<CodegenProperty> csOneOf = cs.getOneOf();
693+
694+
if (csOneOf != null) {
695+
for (CodegenProperty model : csOneOf) {
696+
// Generate a valid name for the enum variant.
697+
// Mainly needed for primitive types.
698+
String[] modelParts = model.dataType.replace("<", "Of").replace(">", "").split("::");
699+
model.datatypeWithEnum = StringUtils.camelize(modelParts[modelParts.length - 1]);
700+
701+
// Primitive type is not properly set, this overrides it to guarantee adequate model generation.
702+
if (!model.getDataType().matches(String.format(Locale.ROOT, ".*::%s", model.getDatatypeWithEnum()))) {
703+
model.isPrimitiveType = true;
704+
}
705+
}
706+
707+
cs.setOneOf(csOneOf);
708+
cm.setComposedSchemas(cs);
709+
}
710+
}
711+
712+
if (cm.discriminator != null) {
713+
for (String model : cm.oneOf) {
714+
List<String> discriminators = oneOfMapDiscriminator.getOrDefault(model, new ArrayList<>());
715+
discriminators.add(cm.discriminator.getPropertyBaseName());
716+
oneOfMapDiscriminator.put(model, discriminators);
717+
}
718+
}
719+
}
720+
721+
for (ModelMap mo : allModels) {
722+
final CodegenModel cm = mo.getModel();
723+
724+
for (CodegenProperty var : cm.vars) {
725+
var.isDiscriminator = false;
726+
}
727+
728+
final List<String> discriminatorsForModel = oneOfMapDiscriminator.get(cm.getSchemaName());
729+
730+
if (discriminatorsForModel != null) {
731+
for (String discriminator : discriminatorsForModel) {
732+
for (CodegenProperty var : cm.vars) {
733+
if (var.baseName.equals(discriminator)) {
734+
var.isDiscriminator = true;
735+
break;
736+
}
737+
}
738+
}
739+
}
740+
}
741+
}
742+
669743
@Override
670744
public void postProcessParameter(CodegenParameter parameter) {
671745
super.postProcessParameter(parameter);
@@ -867,10 +941,10 @@ public String toDefaultValue(Schema p) {
867941
@Override
868942
protected ImmutableMap.Builder<String, Lambda> addMustacheLambdas() {
869943
return super.addMustacheLambdas()
870-
// Convert variable names to lifetime names.
871-
// Generally they are the same, but `#` is not valid in lifetime names.
944+
// Convert raw identifiers to a safe name that can be used in other contexts
945+
// like lifetimes or function names.
872946
// Rust uses `r#` prefix for variables that are also keywords.
873-
.put("lifetimeName", new ReplaceAllLambda("^r#", "r_"));
947+
.put("escapeRawIdentifier", new ReplaceAllLambda("^r#", "r_"));
874948
}
875949

876950
public static <K, V> Map<V, List<K>> invertMap(Map<K, V> map) {

modules/openapi-generator/src/main/resources/rust/model.mustache

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,11 @@ pub struct {{{classname}}} {
131131
{{#isByteArray}}
132132
{{#vendorExtensions.isMandatory}}#[serde_as(as = "serde_with::base64::Base64")]{{/vendorExtensions.isMandatory}}{{^vendorExtensions.isMandatory}}#[serde_as(as = "{{^serdeAsDoubleOption}}Option{{/serdeAsDoubleOption}}{{#serdeAsDoubleOption}}super::DoubleOption{{/serdeAsDoubleOption}}<serde_with::base64::Base64>")]{{/vendorExtensions.isMandatory}}
133133
{{/isByteArray}}
134+
{{#isDiscriminator}}
135+
{{#required}}
136+
#[serde(default = "{{{classname}}}::_default_for_{{#lambda.escapeRawIdentifier}}{{{name}}}{{/lambda.escapeRawIdentifier}}")]
137+
{{/required}}
138+
{{/isDiscriminator}}
134139
#[serde(rename = "{{{baseName}}}"{{^required}}{{#isNullable}}, default{{^isByteArray}}, with = "::serde_with::rust::double_option"{{/isByteArray}}{{/isNullable}}{{/required}}{{^required}}, skip_serializing_if = "Option::is_none"{{/required}}{{#required}}{{#isNullable}}, deserialize_with = "Option::deserialize"{{/isNullable}}{{/required}})]
135140
pub {{{name}}}: {{!
136141
### Option Start
@@ -172,6 +177,18 @@ impl {{{classname}}} {
172177
}
173178
}
174179
}
180+
{{#vars}}
181+
{{#isDiscriminator}}
182+
{{#required}}
183+
184+
impl {{{classname}}} {
185+
fn _default_for_{{#lambda.escapeRawIdentifier}}{{{name}}}{{/lambda.escapeRawIdentifier}}() -> String {
186+
String::from("{{#defaultValue}}{{{.}}}{{/defaultValue}}{{^defaultValue}}{{classname}}{{/defaultValue}}")
187+
}
188+
}
189+
{{/required}}
190+
{{/isDiscriminator}}
191+
{{/vars}}
175192
{{/oneOf.isEmpty}}
176193
{{^oneOf.isEmpty}}
177194
{{! TODO: add other vars that are not part of the oneOf}}

modules/openapi-generator/src/main/resources/rust/reqwest-trait/api.mustache

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -39,13 +39,13 @@ pub trait {{{classname}}}: Send + Sync {
3939
{{^vendorExtensions.x-group-parameters}}
4040
async fn {{{operationId}}}{{!
4141
### Lifetimes
42-
}}<{{#allParams}}'{{#lambda.lifetimeName}}{{{paramName}}}{{/lambda.lifetimeName}}{{^-last}}, {{/-last}}{{/allParams}}>{{!
42+
}}<{{#allParams}}'{{#lambda.escapeRawIdentifier}}{{{paramName}}}{{/lambda.escapeRawIdentifier}}{{^-last}}, {{/-last}}{{/allParams}}>{{!
4343
### Function parameter names
4444
}}(&self, {{#allParams}}{{{paramName}}}: {{!
4545
### Option Start
4646
}}{{^required}}Option<{{/required}}{{#required}}{{#isNullable}}Option<{{/isNullable}}{{/required}}{{!
4747
### &str and Vec<&str>
48-
}}{{#isString}}{{#isArray}}Vec<{{/isArray}}{{^isUuid}}&'{{#lambda.lifetimeName}}{{{paramName}}}{{/lambda.lifetimeName}} str{{/isUuid}}{{#isArray}}>{{/isArray}}{{/isString}}{{!
48+
}}{{#isString}}{{#isArray}}Vec<{{/isArray}}{{^isUuid}}&'{{#lambda.escapeRawIdentifier}}{{{paramName}}}{{/lambda.escapeRawIdentifier}} str{{/isUuid}}{{#isArray}}>{{/isArray}}{{/isString}}{{!
4949
### UUIDs
5050
}}{{#isUuid}}{{#isArray}}Vec<{{/isArray}}&str{{#isArray}}>{{/isArray}}{{/isUuid}}{{!
5151
### Models and primative types
@@ -147,13 +147,13 @@ impl {{classname}} for {{classname}}Client {
147147
{{^vendorExtensions.x-group-parameters}}
148148
async fn {{{operationId}}}{{!
149149
### Lifetimes
150-
}}<{{#allParams}}'{{#lambda.lifetimeName}}{{{paramName}}}{{/lambda.lifetimeName}}{{^-last}}, {{/-last}}{{/allParams}}>{{!
150+
}}<{{#allParams}}'{{#lambda.escapeRawIdentifier}}{{{paramName}}}{{/lambda.escapeRawIdentifier}}{{^-last}}, {{/-last}}{{/allParams}}>{{!
151151
### Function parameter names
152152
}}(&self, {{#allParams}}{{{paramName}}}: {{!
153153
### Option Start
154154
}}{{^required}}Option<{{/required}}{{#required}}{{#isNullable}}Option<{{/isNullable}}{{/required}}{{!
155155
### &str and Vec<&str>
156-
}}{{#isString}}{{#isArray}}Vec<{{/isArray}}{{^isUuid}}&'{{#lambda.lifetimeName}}{{{paramName}}}{{/lambda.lifetimeName}} str{{/isUuid}}{{#isArray}}>{{/isArray}}{{/isString}}{{!
156+
}}{{#isString}}{{#isArray}}Vec<{{/isArray}}{{^isUuid}}&'{{#lambda.escapeRawIdentifier}}{{{paramName}}}{{/lambda.escapeRawIdentifier}} str{{/isUuid}}{{#isArray}}>{{/isArray}}{{/isString}}{{!
157157
### UUIDs
158158
}}{{#isUuid}}{{#isArray}}Vec<{{/isArray}}&str{{#isArray}}>{{/isArray}}{{/isUuid}}{{!
159159
### Models and primative types

modules/openapi-generator/src/test/resources/3_0/oneOfDiscriminator.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -297,4 +297,4 @@ components:
297297
- $ref: '#/components/schemas/DiscOptionalTypeCorrect'
298298
- $ref: '#/components/schemas/FruitType'
299299
discriminator:
300-
propertyName: fruitType
300+
propertyName: fruitType
Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
openapi: 3.0.1
2+
info:
3+
title: Rust Discriminator Reserved Keyword Test
4+
description: >
5+
This tests discriminator fields with Rust reserved keywords to ensure
6+
proper escaping and function name generation
7+
version: 1.0.0
8+
servers:
9+
- url: "http://localhost:8080"
10+
paths:
11+
/vehicle:
12+
post:
13+
summary: Create a vehicle
14+
operationId: createVehicle
15+
requestBody:
16+
required: true
17+
content:
18+
application/json:
19+
schema:
20+
$ref: '#/components/schemas/Vehicle'
21+
responses:
22+
'201':
23+
description: Vehicle created
24+
content:
25+
application/json:
26+
schema:
27+
$ref: '#/components/schemas/Vehicle'
28+
/shape:
29+
post:
30+
summary: Create a shape
31+
operationId: createShape
32+
requestBody:
33+
required: true
34+
content:
35+
application/json:
36+
schema:
37+
$ref: '#/components/schemas/Shape'
38+
responses:
39+
'201':
40+
description: Shape created
41+
content:
42+
application/json:
43+
schema:
44+
$ref: '#/components/schemas/Shape'
45+
46+
components:
47+
schemas:
48+
# Test case 1: Required discriminator with reserved keyword "type"
49+
Vehicle:
50+
type: object
51+
oneOf:
52+
- $ref: '#/components/schemas/Car'
53+
- $ref: '#/components/schemas/Truck'
54+
discriminator:
55+
propertyName: type
56+
mapping:
57+
car: '#/components/schemas/Car'
58+
truck: '#/components/schemas/Truck'
59+
60+
Car:
61+
type: object
62+
required:
63+
- type
64+
- numDoors
65+
properties:
66+
type:
67+
type: string
68+
description: Vehicle type discriminator
69+
default: Vehicle
70+
numDoors:
71+
type: integer
72+
description: Number of doors
73+
74+
Truck:
75+
type: object
76+
required:
77+
- type
78+
- cargoCapacity
79+
properties:
80+
type:
81+
type: string
82+
description: Vehicle type discriminator
83+
cargoCapacity:
84+
type: number
85+
description: Cargo capacity in tons
86+
87+
# Test case 2: Optional discriminator with reserved keyword "type"
88+
Shape:
89+
type: object
90+
oneOf:
91+
- $ref: '#/components/schemas/Circle'
92+
- $ref: '#/components/schemas/Square'
93+
discriminator:
94+
propertyName: type
95+
96+
Circle:
97+
type: object
98+
properties:
99+
type:
100+
type: string
101+
description: Shape type discriminator
102+
radius:
103+
type: number
104+
description: Circle radius
105+
106+
Square:
107+
type: object
108+
properties:
109+
type:
110+
type: string
111+
description: Shape type discriminator
112+
size:
113+
type: number
114+
description: Square size
115+
116+
# Test case 3: Discriminator with other reserved keywords
117+
Message:
118+
type: object
119+
oneOf:
120+
- $ref: '#/components/schemas/TextMessage'
121+
- $ref: '#/components/schemas/ImageMessage'
122+
discriminator:
123+
propertyName: match
124+
mapping:
125+
text: '#/components/schemas/TextMessage'
126+
image: '#/components/schemas/ImageMessage'
127+
128+
TextMessage:
129+
type: object
130+
required:
131+
- match
132+
- content
133+
properties:
134+
match:
135+
type: string
136+
description: Message type discriminator (reserved keyword)
137+
content:
138+
type: string
139+
description: Text content
140+
141+
ImageMessage:
142+
type: object
143+
required:
144+
- match
145+
- url
146+
properties:
147+
match:
148+
type: string
149+
description: Message type discriminator (reserved keyword)
150+
url:
151+
type: string
152+
description: Image URL
153+
154+
# Test case 4: Reserved keyword in nested oneOf
155+
Container:
156+
type: object
157+
oneOf:
158+
- $ref: '#/components/schemas/BoxContainer'
159+
- $ref: '#/components/schemas/BagContainer'
160+
discriminator:
161+
propertyName: return
162+
163+
BoxContainer:
164+
type: object
165+
properties:
166+
return:
167+
type: string
168+
description: Container type (reserved keyword)
169+
dimensions:
170+
type: string
171+
description: Box dimensions
172+
173+
BagContainer:
174+
type: object
175+
properties:
176+
return:
177+
type: string
178+
description: Container type (reserved keyword)
179+
material:
180+
type: string
181+
description: Bag material

0 commit comments

Comments
 (0)