Skip to content
Open
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
8 changes: 8 additions & 0 deletions bin/configs/rust-hyper-discriminator-reserved-keyword.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
generatorName: rust
outputDir: samples/client/others/rust/hyper/discriminator-reserved-keyword
library: hyper
inputSpec: modules/openapi-generator/src/test/resources/3_0/rust/discriminator-reserved-keyword.yaml
templateDir: modules/openapi-generator/src/main/resources/rust
additionalProperties:
supportAsync: false
packageName: discriminator-reserved-keyword-hyper
8 changes: 8 additions & 0 deletions bin/configs/rust-reqwest-discriminator-reserved-keyword.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
generatorName: rust
outputDir: samples/client/others/rust/reqwest/discriminator-reserved-keyword
library: reqwest
inputSpec: modules/openapi-generator/src/test/resources/3_0/rust/discriminator-reserved-keyword.yaml
templateDir: modules/openapi-generator/src/main/resources/rust
additionalProperties:
supportAsync: false
packageName: discriminator-reserved-keyword-reqwest
Original file line number Diff line number Diff line change
Expand Up @@ -318,6 +318,20 @@ public CodegenModel fromModel(String name, Schema model) {
return mdl;
}

@Override
public Map<String, ModelsMap> postProcessAllModels(Map<String, ModelsMap> objs) {
// Collect all models into a single list
List<ModelMap> allModels = new ArrayList<>();
for (ModelsMap models : objs.values()) {
allModels.addAll(models.getModels());
}

// Process oneOf discriminators across all models
postProcessOneOfModels(allModels);

return super.postProcessAllModels(objs);
}

@Override
public ModelsMap postProcessModels(ModelsMap objs) {
for (ModelMap model : objs.getModels()) {
Expand Down Expand Up @@ -666,6 +680,66 @@ public void postProcessModelProperty(CodegenModel model, CodegenProperty propert
}
}

private void postProcessOneOfModels(List<ModelMap> allModels) {
final HashMap<String, List<String>> oneOfMapDiscriminator = new HashMap<>();

for (ModelMap mo : allModels) {
final CodegenModel cm = mo.getModel();

final CodegenComposedSchemas cs = cm.getComposedSchemas();

if (cs != null) {
final List<CodegenProperty> csOneOf = cs.getOneOf();

if (csOneOf != null) {
for (CodegenProperty model : csOneOf) {
// Generate a valid name for the enum variant.
// Mainly needed for primitive types.
String[] modelParts = model.dataType.replace("<", "Of").replace(">", "").split("::");
model.datatypeWithEnum = StringUtils.camelize(modelParts[modelParts.length - 1]);

// Primitive type is not properly set, this overrides it to guarantee adequate model generation.
if (!model.getDataType().matches(String.format(Locale.ROOT, ".*::%s", model.getDatatypeWithEnum()))) {
model.isPrimitiveType = true;
}
}

cs.setOneOf(csOneOf);
cm.setComposedSchemas(cs);
}
}

if (cm.discriminator != null) {
for (String model : cm.oneOf) {
List<String> discriminators = oneOfMapDiscriminator.getOrDefault(model, new ArrayList<>());
discriminators.add(cm.discriminator.getPropertyBaseName());
oneOfMapDiscriminator.put(model, discriminators);
}
}
}

for (ModelMap mo : allModels) {
final CodegenModel cm = mo.getModel();

for (CodegenProperty var : cm.vars) {
var.isDiscriminator = false;
}

final List<String> discriminatorsForModel = oneOfMapDiscriminator.get(cm.getSchemaName());

if (discriminatorsForModel != null) {
for (String discriminator : discriminatorsForModel) {
for (CodegenProperty var : cm.vars) {
if (var.baseName.equals(discriminator)) {
var.isDiscriminator = true;
break;
}
}
}
}
}
}

@Override
public void postProcessParameter(CodegenParameter parameter) {
super.postProcessParameter(parameter);
Expand Down Expand Up @@ -867,10 +941,10 @@ public String toDefaultValue(Schema p) {
@Override
protected ImmutableMap.Builder<String, Lambda> addMustacheLambdas() {
return super.addMustacheLambdas()
// Convert variable names to lifetime names.
// Generally they are the same, but `#` is not valid in lifetime names.
// Convert raw identifiers to a safe name that can be used in other contexts
// like lifetimes or function names.
// Rust uses `r#` prefix for variables that are also keywords.
.put("lifetimeName", new ReplaceAllLambda("^r#", "r_"));
.put("escapeRawIdentifier", new ReplaceAllLambda("^r#", "r_"));
}

public static <K, V> Map<V, List<K>> invertMap(Map<K, V> map) {
Expand Down
17 changes: 17 additions & 0 deletions modules/openapi-generator/src/main/resources/rust/model.mustache
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,11 @@ pub struct {{{classname}}} {
{{#isByteArray}}
{{#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}}
{{/isByteArray}}
{{#isDiscriminator}}
{{#required}}
#[serde(default = "{{{classname}}}::_default_for_{{#lambda.escapeRawIdentifier}}{{{name}}}{{/lambda.escapeRawIdentifier}}")]
{{/required}}
{{/isDiscriminator}}
#[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}})]
pub {{{name}}}: {{!
### Option Start
Expand Down Expand Up @@ -172,6 +177,18 @@ impl {{{classname}}} {
}
}
}
{{#vars}}
{{#isDiscriminator}}
{{#required}}

impl {{{classname}}} {
fn _default_for_{{#lambda.escapeRawIdentifier}}{{{name}}}{{/lambda.escapeRawIdentifier}}() -> String {
String::from("{{#defaultValue}}{{{.}}}{{/defaultValue}}{{^defaultValue}}{{classname}}{{/defaultValue}}")
}
}
{{/required}}
{{/isDiscriminator}}
{{/vars}}
{{/oneOf.isEmpty}}
{{^oneOf.isEmpty}}
{{! TODO: add other vars that are not part of the oneOf}}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,13 +39,13 @@ pub trait {{{classname}}}: Send + Sync {
{{^vendorExtensions.x-group-parameters}}
async fn {{{operationId}}}{{!
### Lifetimes
}}<{{#allParams}}'{{#lambda.lifetimeName}}{{{paramName}}}{{/lambda.lifetimeName}}{{^-last}}, {{/-last}}{{/allParams}}>{{!
}}<{{#allParams}}'{{#lambda.escapeRawIdentifier}}{{{paramName}}}{{/lambda.escapeRawIdentifier}}{{^-last}}, {{/-last}}{{/allParams}}>{{!
### Function parameter names
}}(&self, {{#allParams}}{{{paramName}}}: {{!
### Option Start
}}{{^required}}Option<{{/required}}{{#required}}{{#isNullable}}Option<{{/isNullable}}{{/required}}{{!
### &str and Vec<&str>
}}{{#isString}}{{#isArray}}Vec<{{/isArray}}{{^isUuid}}&'{{#lambda.lifetimeName}}{{{paramName}}}{{/lambda.lifetimeName}} str{{/isUuid}}{{#isArray}}>{{/isArray}}{{/isString}}{{!
}}{{#isString}}{{#isArray}}Vec<{{/isArray}}{{^isUuid}}&'{{#lambda.escapeRawIdentifier}}{{{paramName}}}{{/lambda.escapeRawIdentifier}} str{{/isUuid}}{{#isArray}}>{{/isArray}}{{/isString}}{{!
### UUIDs
}}{{#isUuid}}{{#isArray}}Vec<{{/isArray}}&str{{#isArray}}>{{/isArray}}{{/isUuid}}{{!
### Models and primative types
Expand Down Expand Up @@ -147,13 +147,13 @@ impl {{classname}} for {{classname}}Client {
{{^vendorExtensions.x-group-parameters}}
async fn {{{operationId}}}{{!
### Lifetimes
}}<{{#allParams}}'{{#lambda.lifetimeName}}{{{paramName}}}{{/lambda.lifetimeName}}{{^-last}}, {{/-last}}{{/allParams}}>{{!
}}<{{#allParams}}'{{#lambda.escapeRawIdentifier}}{{{paramName}}}{{/lambda.escapeRawIdentifier}}{{^-last}}, {{/-last}}{{/allParams}}>{{!
### Function parameter names
}}(&self, {{#allParams}}{{{paramName}}}: {{!
### Option Start
}}{{^required}}Option<{{/required}}{{#required}}{{#isNullable}}Option<{{/isNullable}}{{/required}}{{!
### &str and Vec<&str>
}}{{#isString}}{{#isArray}}Vec<{{/isArray}}{{^isUuid}}&'{{#lambda.lifetimeName}}{{{paramName}}}{{/lambda.lifetimeName}} str{{/isUuid}}{{#isArray}}>{{/isArray}}{{/isString}}{{!
}}{{#isString}}{{#isArray}}Vec<{{/isArray}}{{^isUuid}}&'{{#lambda.escapeRawIdentifier}}{{{paramName}}}{{/lambda.escapeRawIdentifier}} str{{/isUuid}}{{#isArray}}>{{/isArray}}{{/isString}}{{!
### UUIDs
}}{{#isUuid}}{{#isArray}}Vec<{{/isArray}}&str{{#isArray}}>{{/isArray}}{{/isUuid}}{{!
### Models and primative types
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -297,4 +297,4 @@ components:
- $ref: '#/components/schemas/DiscOptionalTypeCorrect'
- $ref: '#/components/schemas/FruitType'
discriminator:
propertyName: fruitType
propertyName: fruitType
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
openapi: 3.0.1
info:
title: Rust Discriminator Reserved Keyword Test
description: >
This tests discriminator fields with Rust reserved keywords to ensure
proper escaping and function name generation
version: 1.0.0
servers:
- url: "http://localhost:8080"
paths:
/vehicle:
post:
summary: Create a vehicle
operationId: createVehicle
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/Vehicle'
responses:
'201':
description: Vehicle created
content:
application/json:
schema:
$ref: '#/components/schemas/Vehicle'
/shape:
post:
summary: Create a shape
operationId: createShape
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/Shape'
responses:
'201':
description: Shape created
content:
application/json:
schema:
$ref: '#/components/schemas/Shape'

components:
schemas:
# Test case 1: Required discriminator with reserved keyword "type"
Vehicle:
type: object
oneOf:
- $ref: '#/components/schemas/Car'
- $ref: '#/components/schemas/Truck'
discriminator:
propertyName: type
mapping:
car: '#/components/schemas/Car'
truck: '#/components/schemas/Truck'

Car:
type: object
required:
- type
- numDoors
properties:
type:
type: string
description: Vehicle type discriminator
default: Vehicle
numDoors:
type: integer
description: Number of doors

Truck:
type: object
required:
- type
- cargoCapacity
properties:
type:
type: string
description: Vehicle type discriminator
cargoCapacity:
type: number
description: Cargo capacity in tons

# Test case 2: Optional discriminator with reserved keyword "type"
Shape:
type: object
oneOf:
- $ref: '#/components/schemas/Circle'
- $ref: '#/components/schemas/Square'
discriminator:
propertyName: type

Circle:
type: object
properties:
type:
type: string
description: Shape type discriminator
radius:
type: number
description: Circle radius

Square:
type: object
properties:
type:
type: string
description: Shape type discriminator
size:
type: number
description: Square size

# Test case 3: Discriminator with other reserved keywords
Message:
type: object
oneOf:
- $ref: '#/components/schemas/TextMessage'
- $ref: '#/components/schemas/ImageMessage'
discriminator:
propertyName: match
mapping:
text: '#/components/schemas/TextMessage'
image: '#/components/schemas/ImageMessage'

TextMessage:
type: object
required:
- match
- content
properties:
match:
type: string
description: Message type discriminator (reserved keyword)
content:
type: string
description: Text content

ImageMessage:
type: object
required:
- match
- url
properties:
match:
type: string
description: Message type discriminator (reserved keyword)
url:
type: string
description: Image URL

# Test case 4: Reserved keyword in nested oneOf
Container:
type: object
oneOf:
- $ref: '#/components/schemas/BoxContainer'
- $ref: '#/components/schemas/BagContainer'
discriminator:
propertyName: return

BoxContainer:
type: object
properties:
return:
type: string
description: Container type (reserved keyword)
dimensions:
type: string
description: Box dimensions

BagContainer:
type: object
properties:
return:
type: string
description: Container type (reserved keyword)
material:
type: string
description: Bag material
Loading
Loading