Skip to content
Closed
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-reqwest-discriminated-union.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
generatorName: rust
outputDir: samples/client/others/rust/reqwest/discriminated-union
library: reqwest
inputSpec: modules/openapi-generator/src/test/resources/3_0/rust/discriminated-union.yaml
templateDir: modules/openapi-generator/src/main/resources/rust
additionalProperties:
supportAsync: false
packageName: discriminated-union
Original file line number Diff line number Diff line change
Expand Up @@ -333,6 +333,42 @@ public CodegenModel fromModel(String name, Schema model) {
return mdl;
}

@Override
public Map<String, ModelsMap> postProcessAllModels(Map<String, ModelsMap> objs) {
objs = super.postProcessAllModels(objs);

for (ModelsMap modelsMap : objs.values()) {
for (ModelMap modelMap : modelsMap.getModels()) {
CodegenModel cm = modelMap.getModel();
CodegenDiscriminator discriminator = cm.discriminator;
if (discriminator == null || discriminator.getMappedModels() == null) {
continue;
}
// a mapping that names the base itself leaves no struct to wrap, and an enum-typed tag would be
// written twice (the child's copy keeps its default): keep the inline variants
if (discriminator.getMappedModels().stream().anyMatch(m -> cm.name.equals(m.getSchemaName()))
|| cm.allVars.stream().anyMatch(v -> discriminator.getPropertyBaseName().equals(v.baseName) && v.getIsEnumOrRef())) {
discriminator.getVendorExtensions().put("x-rust-inline-variants", true);
continue;
}
// a mapped child's discriminator doubles as the union's serde tag, which is consumed before the
// child deserializes: mark it so the template defaults it and skips it while unset
for (CodegenDiscriminator.MappedModel mapped : discriminator.getMappedModels()) {
if (mapped.getModel() == null) {
continue;
}
for (CodegenProperty var : mapped.getModel().vars) {
if (discriminator.getPropertyBaseName().equals(var.baseName)) {
var.isDiscriminator = true;
}
}
}
}
}

return objs;
}

@Override
public ModelsMap postProcessModels(ModelsMap objs) {
for (ModelMap model : objs.getModels()) {
Expand Down
13 changes: 10 additions & 3 deletions modules/openapi-generator/src/main/resources/rust/model.mustache
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,11 @@ pub enum {{{classname}}} {
{{^oneOf}}
{{#mappedModels}}
#[serde(rename="{{mappingName}}")]
{{^vendorExtensions.x-rust-inline-variants}}
{{!-- newtype variant: the child's own fields survive; named by modelName (unique per mapping), wraps the real classname --}}
{{{modelName}}}({{^avoidBoxedModels}}Box<{{/avoidBoxedModels}}models::{{#model}}{{{classname}}}{{/model}}{{^model}}{{{modelName}}}{{/model}}{{^avoidBoxedModels}}>{{/avoidBoxedModels}}),
{{/vendorExtensions.x-rust-inline-variants}}
{{#vendorExtensions.x-rust-inline-variants}}
{{{modelName}}} {
{{#vars}}
{{#description}}
Expand All @@ -93,6 +98,7 @@ pub enum {{{classname}}} {
{{{name}}}: {{#required}}{{#isNullable}}Option<{{/isNullable}}{{/required}}{{^required}}Option<{{/required}}{{#isEnum}}{{{enumName}}}{{/isEnum}}{{^isEnum}}{{#isModel}}{{^avoidBoxedModels}}Box<{{/avoidBoxedModels}}{{{dataType}}}{{^avoidBoxedModels}}>{{/avoidBoxedModels}}{{/isModel}}{{^isModel}}{{{dataType}}}{{/isModel}}{{/isEnum}}{{#required}}{{#isNullable}}>{{/isNullable}}{{/required}}{{^required}}>{{/required}},
{{/vars}}
},
{{/vendorExtensions.x-rust-inline-variants}}
{{/mappedModels}}
{{/oneOf}}
{{^oneOf.isEmpty}}
Expand All @@ -110,11 +116,11 @@ pub enum {{{classname}}} {

impl Default for {{classname}} {
fn default() -> Self {
{{^oneOf}}{{#mappedModels}}{{#-first}}Self::{{modelName}} {
{{^oneOf}}{{#mappedModels}}{{#-first}}{{^vendorExtensions.x-rust-inline-variants}}Self::{{modelName}}(Default::default()){{/vendorExtensions.x-rust-inline-variants}}{{#vendorExtensions.x-rust-inline-variants}}Self::{{modelName}} {
{{#vars}}
{{{name}}}: Default::default(),
{{/vars}}
}{{/-first}}{{/mappedModels}}
}{{/vendorExtensions.x-rust-inline-variants}}{{/-first}}{{/mappedModels}}
{{/oneOf}}{{^oneOf.isEmpty}}{{#composedSchemas.oneOf}}{{#-first}}Self::{{{name}}}(Default::default()){{/-first}}{{/composedSchemas.oneOf}}{{/oneOf.isEmpty}}
}
}
Expand All @@ -133,7 +139,8 @@ 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}}
#[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}})]
{{!-- a mapped child's discriminator is the union's consumed serde tag: default it, skip it while unset --}}
#[serde(rename = "{{{baseName}}}"{{#isDiscriminator}}{{#required}}, default{{#isNullable}}, skip_serializing_if = "Option::is_none"{{/isNullable}}{{^isNullable}}{{#isString}}{{^isEnum}}, skip_serializing_if = "String::is_empty"{{/isEnum}}{{/isString}}{{/isNullable}}{{/required}}{{/isDiscriminator}}{{^required}}{{#isNullable}}, default{{^isByteArray}}, with = "::serde_with::rust::double_option"{{/isByteArray}}{{/isNullable}}{{/required}}{{^required}}, skip_serializing_if = "Option::is_none"{{/required}}{{#required}}{{#isNullable}}{{^isDiscriminator}}, deserialize_with = "Option::deserialize"{{/isDiscriminator}}{{/isNullable}}{{/required}})]
pub {{{name}}}: {{!
### Option Start
}}{{#isNullable}}Option<{{/isNullable}}{{^required}}Option<{{/required}}{{!
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -298,6 +298,49 @@ public void testMultipleArrayTypesEnum() throws IOException {
TestUtils.assertFileContains(outputPath, enumSpec);
}

@Test
public void testDiscriminatedUnionKeepsChildFields() throws IOException {
Path target = Files.createTempDirectory("test");
target.toFile().deleteOnExit();
final CodegenConfigurator configurator = new CodegenConfigurator()
.setGeneratorName("rust")
.setInputSpec("src/test/resources/3_0/rust/discriminated-union.yaml")
.setSkipOverwrite(false)
.setOutputDir(target.toAbsolutePath().toString().replace("\\", "/"));
new DefaultGenerator().opts(configurator.toClientOptInput()).generate();

// newtype variants, so the child's own fields survive
Path unionPath = Path.of(target.toString(), "/src/models/api_error.rs");
TestUtils.assertFileContains(unionPath, "#[serde(tag = \"type\")]");
TestUtils.assertFileContains(unionPath, "ObjectExists(Box<models::ObjectExists>),");
TestUtils.assertFileContains(unionPath, "ValidationError(Box<models::ValidationError>),");
TestUtils.assertFileContains(unionPath, "Self::ObjectExists(Default::default())");
TestUtils.assertFileNotContains(unionPath, "ObjectExists {");

// the consumed tag key defaults and is skipped while unset; standalone field and new() unchanged
Path childPath = Path.of(target.toString(), "/src/models/object_exists.rs");
TestUtils.assertFileContains(childPath,
"#[serde(rename = \"type\", default, skip_serializing_if = \"String::is_empty\")]",
"pub identifier: String,",
"pub fn new(r#type: String, message: String, identifier: String) -> ObjectExists {");
TestUtils.assertFileContains(Path.of(target.toString(), "/src/models/alpha.rs"),
"#[serde(rename = \"kind\", default, skip_serializing_if = \"Option::is_none\")]");
// an enum-typed tag would be written twice, the child's copy keeping its default: inline variants, children untouched
TestUtils.assertFileContains(Path.of(target.toString(), "/src/models/pet.rs"),
"#[serde(rename=\"Cat\")]\n Cat {", "#[serde(rename=\"Dog\")]\n Dog {");
TestUtils.assertFileNotContains(Path.of(target.toString(), "/src/models/pet.rs"), "Box<models::Cat>");
TestUtils.assertFileContains(Path.of(target.toString(), "/src/models/cat.rs"), "#[serde(rename = \"petType\")]\n pub pet_type: PetType,");
TestUtils.assertFileContains(Path.of(target.toString(), "/src/models/shape.rs"), "#[serde(rename=\"circle\")]\n Circle {");
TestUtils.assertFileContains(Path.of(target.toString(), "/src/models/circle.rs"), "#[serde(rename = \"kind\")]\n pub kind: models::ShapeKind,");

// a mapping that names the base itself leaves no struct to wrap (E0275): inline variants, children untouched
TestUtils.assertFileContains(Path.of(target.toString(), "/src/models/creature.rs"),
"#[serde(rename=\"Creature\")]\n Creature {", "#[serde(rename=\"Bird\")]\n Bird {");
TestUtils.assertFileNotContains(Path.of(target.toString(), "/src/models/creature.rs"), "Box<models::Creature>");
TestUtils.assertFileContains(Path.of(target.toString(), "/src/models/creature.rs"), "#[serde(tag = \"kind\")]");
TestUtils.assertFileContains(Path.of(target.toString(), "/src/models/bird.rs"), "#[serde(rename = \"kind\")]\n pub kind: String,");
}

@Test
public void testIntegerPropertyEnum() throws IOException {
Path target = Files.createTempDirectory("test");
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
openapi: 3.0.3
info:
title: discriminated union
version: 1.0.0
paths:
/errors:
get:
operationId: getError
responses:
'200':
description: ok
content:
application/json:
schema:
$ref: '#/components/schemas/ApiError'
components:
schemas:
ApiError:
type: object
required:
- type
- message
properties:
type:
type: string
message:
type: string
discriminator:
propertyName: type
mapping:
ObjectExists: '#/components/schemas/ObjectExists'
ValidationError: '#/components/schemas/ValidationError'
ObjectExists:
allOf:
- $ref: '#/components/schemas/ApiError'
- type: object
required:
- identifier
properties:
identifier:
type: string
objectType:
type: string
ValidationError:
allOf:
- $ref: '#/components/schemas/ApiError'
- type: object
properties:
field:
type: string
# nullable discriminator: String::is_empty would not compile against it
NullableTagged:
type: object
required:
- kind
properties:
kind:
type: string
nullable: true
label:
type: string
discriminator:
propertyName: kind
mapping:
Alpha: '#/components/schemas/Alpha'
Alpha:
allOf:
- $ref: '#/components/schemas/NullableTagged'
- type: object
properties:
alphaOnly:
type: string
# enum-typed discriminators, inline and $ref: the union keeps the inline variants
Pet:
type: object
required:
- petType
properties:
petType:
type: string
enum: [Cat, Dog]
discriminator:
propertyName: petType
mapping:
Cat: '#/components/schemas/Cat'
Dog: '#/components/schemas/Dog'
Cat:
allOf:
- $ref: '#/components/schemas/Pet'
- type: object
properties:
lives:
type: integer
Dog:
allOf:
- $ref: '#/components/schemas/Pet'
- type: object
properties:
bark:
type: boolean
ShapeKind:
type: string
enum: [circle]
Shape:
type: object
required:
- kind
properties:
kind:
$ref: '#/components/schemas/ShapeKind'
discriminator:
propertyName: kind
mapping:
circle: '#/components/schemas/Circle'
Circle:
allOf:
- $ref: '#/components/schemas/Shape'
- type: object
properties:
radius:
type: number
# the mapping names the base itself: no struct to wrap, so the union keeps the inline variants
Creature:
type: object
required:
- kind
properties:
kind:
type: string
name:
type: string
discriminator:
propertyName: kind
mapping:
Creature: '#/components/schemas/Creature'
Bird: '#/components/schemas/Bird'
Bird:
allOf:
- $ref: '#/components/schemas/Creature'
- type: object
properties:
wingspan:
type: number
2 changes: 1 addition & 1 deletion samples/client/others/rust/hyper/oneOf/src/models/bar.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ pub struct Bar {
#[serde(rename = "@baseType", skip_serializing_if = "Option::is_none")]
pub at_base_type: Option<String>,
/// When sub-classing, this defines the sub-class Extensible name
#[serde(rename = "@type")]
#[serde(rename = "@type", default, skip_serializing_if = "String::is_empty")]
pub at_type: String,
#[serde(rename = "barPropA", skip_serializing_if = "Option::is_none")]
pub bar_prop_a: Option<String>,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ pub struct BarCreate {
#[serde(rename = "@baseType", skip_serializing_if = "Option::is_none")]
pub at_base_type: Option<String>,
/// When sub-classing, this defines the sub-class Extensible name
#[serde(rename = "@type")]
#[serde(rename = "@type", default, skip_serializing_if = "String::is_empty")]
pub at_type: String,
#[serde(rename = "barPropA", skip_serializing_if = "Option::is_none")]
pub bar_prop_a: Option<String>,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ pub struct BarRef {
#[serde(rename = "@baseType", skip_serializing_if = "Option::is_none")]
pub at_base_type: Option<String>,
/// When sub-classing, this defines the sub-class Extensible name
#[serde(rename = "@type")]
#[serde(rename = "@type", default, skip_serializing_if = "String::is_empty")]
pub at_type: String,
/// Name of the related entity.
#[serde(rename = "name", skip_serializing_if = "Option::is_none")]
Expand Down
Loading
Loading