Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Rust-Axum][Breaking Change] Improve the oneOf model generator #20336

Open
wants to merge 8 commits into
base: master
Choose a base branch
from
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
11 changes: 11 additions & 0 deletions bin/configs/manual/rust-axum-oneof-v3.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
generatorName: rust-axum
outputDir: samples/server/petstore/rust-axum/output/rust-axum-oneof
inputSpec: modules/openapi-generator/src/test/resources/3_0/rust-axum/rust-axum-oneof.yaml
templateDir: modules/openapi-generator/src/main/resources/rust-axum
generateAliasAsModel: true
additionalProperties:
hideGenerationTimestamp: "true"
packageName: rust-axum-oneof
globalProperties:
skipFormModel: "false"
enablePostProcessFile: true
Original file line number Diff line number Diff line change
Expand Up @@ -1083,8 +1083,8 @@ public String toString() {
sb.append(", items='").append(items).append('\'');
sb.append(", additionalProperties='").append(additionalProperties).append('\'');
sb.append(", isModel='").append(isModel).append('\'');
sb.append(", isNull='").append(isNull);
sb.append(", hasValidation='").append(hasValidation);
sb.append(", isNull='").append(isNull).append('\'');
sb.append(", hasValidation='").append(hasValidation).append('\'');
Comment on lines +1086 to +1087
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was annoying to me, it broke string formatting, which made debugging slightly harder.

sb.append(", getAdditionalPropertiesIsAnyType=").append(getAdditionalPropertiesIsAnyType());
sb.append(", getHasDiscriminatorWithNonEmptyMapping=").append(hasDiscriminatorWithNonEmptyMapping);
sb.append(", getIsAnyType=").append(getIsAnyType());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -594,8 +594,105 @@ public CodegenOperation fromOperation(String path, String httpMethod, Operation
return op;
}

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

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

CodegenComposedSchemas cs = cm.getComposedSchemas();
if (cs != null) {
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 = 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.getPropertyName());
oneOfMapDiscriminator.put(model, discriminators);
}
}
}

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

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

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

if (discriminatorsForModel != null) {
for (String discriminator : discriminatorsForModel) {
boolean hasDiscriminatorDefined = false;

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

// If the discriminator field is not a defined attribute in the variant structure, create it.
if (!hasDiscriminatorDefined) {
CodegenProperty property = new CodegenProperty();

// Static attributes
// Only strings are supported by serde for tag field types, so it's the only one we'll deal with
property.openApiType = "string";
property.complexType = "string";
property.dataType = "String";
property.datatypeWithEnum = "String";
property.baseType = "string";
property.required = true;
property.isPrimitiveType = true;
property.isString = true;
property.isDiscriminator = true;

// Attributes based on the discriminator value
property.baseName = discriminator;
property.name = discriminator;
property.nameInCamelCase = camelize(discriminator);
property.nameInPascalCase = property.nameInCamelCase.substring(0, 1).toUpperCase(Locale.ROOT) + property.nameInCamelCase.substring(1);
property.nameInSnakeCase = underscore(discriminator).toUpperCase(Locale.ROOT);
property.getter = String.format(Locale.ROOT, "get%s", property.nameInPascalCase);
property.setter = String.format(Locale.ROOT, "set%s", property.nameInPascalCase);
property.defaultValueWithParam = String.format(Locale.ROOT, " = data.%s;", property.name);

// Attributes based on the model name
property.defaultValue = String.format(Locale.ROOT, "r#\"%s\"#.to_string()", cm.getSchemaName());
property.jsonSchema = String.format(Locale.ROOT, "{ \"default\":\"%s\"; \"type\":\"string\" }", cm.getSchemaName());

cm.vars.add(property);
}
}
}
}
}

@Override
public OperationsMap postProcessOperationsWithModels(final OperationsMap operationsMap, List<ModelMap> allModels) {
postProcessOneOfModels(allModels);

final OperationMap operations = operationsMap.getOperations();
operations.put("classnamePascalCase", camelize(operations.getClassname()));

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -573,21 +573,70 @@ impl PartialEq for {{{classname}}} {
self.0.get() == other.0.get()
}
}

{{/anyOf.size}}
{{#oneOf.size}}
/// One of:
{{#oneOf}}
/// - {{{.}}}
{{/oneOf}}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct {{{classname}}}(Box<serde_json::value::RawValue>);
{{#discriminator}}
#[derive(Debug, Clone, PartialEq, serde::Deserialize)]
#[serde(tag = "{{{propertyBaseName}}}")]
{{/discriminator}}
{{^discriminator}}
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
#[serde(untagged)]
{{/discriminator}}
#[allow(non_camel_case_types)]
pub enum {{{classname}}} {
{{#composedSchemas}}
{{#oneOf}}
{{{datatypeWithEnum}}}(Box<{{{dataType}}}>),
{{/oneOf}}
{{/composedSchemas}}
}

impl validator::Validate for {{{classname}}}
{
fn validate(&self) -> std::result::Result<(), validator::ValidationErrors> {
std::result::Result::Ok(())
match self {
{{#composedSchemas}}
{{#oneOf}}
{{#isPrimitiveType}}
Self::{{{datatypeWithEnum}}}(_) => std::result::Result::Ok(()),
{{/isPrimitiveType}}
{{^isPrimitiveType}}
Self::{{{datatypeWithEnum}}}(x) => x.validate(),
{{/isPrimitiveType}}
{{/oneOf}}
{{/composedSchemas}}
}
}
}

{{#discriminator}}
impl serde::Serialize for {{{classname}}} {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where S: serde::Serializer {
match self {
{{#composedSchemas}}
{{#oneOf}}
Self::{{{datatypeWithEnum}}}(x) => x.serialize(serializer),
{{/oneOf}}
{{/composedSchemas}}
}
}
}
{{/discriminator}}



{{#composedSchemas}}
{{#oneOf}}
impl From<{{{dataType}}}> for {{{classname}}} {
fn from(value: {{{dataType}}}) -> Self {
Self::{{{datatypeWithEnum}}}(Box::new(value))
}
}
{{/oneOf}}
{{/composedSchemas}}

/// Converts Query Parameters representation (style=form, explode=false) to a {{{classname}}} value
/// as specified in https://swagger.io/docs/specification/serialization/
Expand All @@ -600,11 +649,6 @@ impl std::str::FromStr for {{{classname}}} {
}
}

impl PartialEq for {{{classname}}} {
fn eq(&self, other: &Self) -> bool {
self.0.get() == other.0.get()
}
}
{{/oneOf.size}}
{{^anyOf.size}}
{{^oneOf.size}}
Expand All @@ -613,11 +657,15 @@ impl PartialEq for {{{classname}}} {
pub struct {{{classname}}} {
{{#vars}}
{{#description}}
/// {{{.}}}
/// {{{.}}}
{{/description}}
{{#isEnum}}
/// Note: inline enums are not fully supported by openapi-generator
/// Note: inline enums are not fully supported by openapi-generator
{{/isEnum}}
{{#isDiscriminator}}
#[serde(default = "{{{classname}}}::__name_for_{{{name}}}")]
#[serde(serialize_with = "{{{classname}}}::__serialize_{{{name}}}")]
{{/isDiscriminator}}
#[serde(rename = "{{{baseName}}}")]
{{#hasValidation}}
#[validate(
Expand Down Expand Up @@ -685,6 +733,25 @@ pub struct {{{classname}}} {
{{/vars}}
}


{{#vars}}
{{#isDiscriminator}}
impl {{{classname}}} {
fn __name_for_{{{name}}}() -> String {
String::from("{{{classname}}}")
}

fn __serialize_{{{name}}}<S>(_: &String, s: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
s.serialize_str(&Self::__name_for_{{{name}}}())
}
}
{{/isDiscriminator}}
{{/vars}}


{{#vars}}
{{#hasValidation}}
{{#pattern}}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
openapi: "3.0.4"
info:
title: "test"
version: "0.0.1"
paths:
"/":
post:
operationId: foo
requestBody:
required: true
content:
"application/json":
schema:
"$ref": "#/components/schemas/Message"
responses:
"200":
description: Re-serialize and echo the request data
content:
"application/json":
schema:
"$ref": "#/components/schemas/Message"
components:
schemas:
Message:
type: object
additionalProperties: false
discriminator:
propertyName: op
oneOf:
- "$ref": "#/components/schemas/Hello"
- "$ref": "#/components/schemas/Greeting"
- "$ref": "#/components/schemas/Goodbye"
title: Message
Hello:
type: object
additionalProperties: false
title: Hello
properties:
op:
type: string
enum:
- Hello
default: Hello
d:
type: object
properties:
welcome_message:
type: string
required:
- welcome_message
required:
- op
- d
Greeting:
type: object
additionalProperties: false
title: Greeting
properties:
d:
type: object
properties:
greet_message:
type: string
required:
- greet_message
required:
- d
Goodbye:
type: object
additionalProperties: false
title: Goodbye
properties:
op:
type: string
enum:
- Goodbye
d:
type: object
properties:
goodbye_message:
type: string
required:
- goodbye_message
required:
- op
- d
Loading
Loading