Skip to content

Commit 5f2ca61

Browse files
authored
[Core, Rust Server] anyOf / oneOf support for Rust Server (#6690)
* [Core] Inline Model Resolution of Enums Enums need to be named types, so handle them as part of inline model resolution * [Rust Server] Handle models starting with a number correctly * [Rust Server] Additional trace * [Rust Server] Add support for oneOf/anyOf * [Rust Server] Update supported features * [Rust Server] General template tidy up * [Rust Server] Implement IntoHeaderValue for wrapped data types * [Rust Server] Convert from string correctly * [Rust Server] Test for anyOf/oneOf * Update samples * Update docs
1 parent 0068932 commit 5f2ca61

28 files changed

Lines changed: 2547 additions & 1485 deletions

File tree

docs/generators/rust-server.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -186,7 +186,7 @@ These options may be applied as additional-properties (cli) or configOptions (pl
186186
| Name | Supported | Defined By |
187187
| ---- | --------- | ---------- |
188188
|Simple|✓|OAS2,OAS3
189-
|Composite||OAS2,OAS3
189+
|Composite||OAS2,OAS3
190190
|Polymorphism|✗|OAS2,OAS3
191191
|Union|✗|OAS3
192192

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

Lines changed: 30 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -378,38 +378,34 @@ private void flattenComposedChildren(OpenAPI openAPI, String key, List<Schema> c
378378
ListIterator<Schema> listIterator = children.listIterator();
379379
while (listIterator.hasNext()) {
380380
Schema component = listIterator.next();
381-
if (component instanceof ObjectSchema || // for inline schema with type:object
382-
(component != null && component.getProperties() != null &&
383-
!component.getProperties().isEmpty())) { // for inline schema without type:object
384-
Schema op = component;
385-
if (op.get$ref() == null && op.getProperties() != null && op.getProperties().size() > 0) {
386-
// If a `title` attribute is defined in the inline schema, codegen uses it to name the
387-
// inline schema. Otherwise, we'll use the default naming such as InlineObject1, etc.
388-
// We know that this is not the best way to name the model.
389-
//
390-
// Such naming strategy may result in issues. If the value of the 'title' attribute
391-
// happens to match a schema defined elsewhere in the specification, 'innerModelName'
392-
// will be the same as that other schema.
393-
//
394-
// To have complete control of the model naming, one can define the model separately
395-
// instead of inline.
396-
String innerModelName = resolveModelName(op.getTitle(), key);
397-
Schema innerModel = modelFromProperty(openAPI, op, innerModelName);
398-
String existing = matchGenerated(innerModel);
399-
if (existing == null) {
400-
openAPI.getComponents().addSchemas(innerModelName, innerModel);
401-
addGenerated(innerModelName, innerModel);
402-
Schema schema = new Schema().$ref(innerModelName);
403-
schema.setRequired(op.getRequired());
404-
listIterator.set(schema);
405-
} else {
406-
Schema schema = new Schema().$ref(existing);
407-
schema.setRequired(op.getRequired());
408-
listIterator.set(schema);
409-
}
381+
if ((component != null) &&
382+
(component.get$ref() == null) &&
383+
((component.getProperties() != null && !component.getProperties().isEmpty()) ||
384+
(component.getEnum() != null && !component.getEnum().isEmpty()))) {
385+
// If a `title` attribute is defined in the inline schema, codegen uses it to name the
386+
// inline schema. Otherwise, we'll use the default naming such as InlineObject1, etc.
387+
// We know that this is not the best way to name the model.
388+
//
389+
// Such naming strategy may result in issues. If the value of the 'title' attribute
390+
// happens to match a schema defined elsewhere in the specification, 'innerModelName'
391+
// will be the same as that other schema.
392+
//
393+
// To have complete control of the model naming, one can define the model separately
394+
// instead of inline.
395+
String innerModelName = resolveModelName(component.getTitle(), key);
396+
Schema innerModel = modelFromProperty(openAPI, component, innerModelName);
397+
String existing = matchGenerated(innerModel);
398+
if (existing == null) {
399+
openAPI.getComponents().addSchemas(innerModelName, innerModel);
400+
addGenerated(innerModelName, innerModel);
401+
Schema schema = new Schema().$ref(innerModelName);
402+
schema.setRequired(component.getRequired());
403+
listIterator.set(schema);
404+
} else {
405+
Schema schema = new Schema().$ref(existing);
406+
schema.setRequired(component.getRequired());
407+
listIterator.set(schema);
410408
}
411-
} else {
412-
// likely a reference to schema (not inline schema)
413409
}
414410
}
415411
}
@@ -540,7 +536,7 @@ private void addGenerated(String name, Schema model) {
540536
*/
541537
private String sanitizeName(final String name) {
542538
return name
543-
.replaceAll("^[0-9]", "_") // e.g. 12object => _2object
539+
.replaceAll("^[0-9]", "_$0") // e.g. 12object => _12object
544540
.replaceAll("[^A-Za-z0-9]", "_"); // e.g. io.schema.User name => io_schema_User_name
545541
}
546542

@@ -671,6 +667,8 @@ private Schema modelFromProperty(OpenAPI openAPI, Schema object, String path) {
671667
model.setXml(xml);
672668
model.setRequired(object.getRequired());
673669
model.setNullable(object.getNullable());
670+
model.setEnum(object.getEnum());
671+
model.setType(object.getType());
674672
model.setDiscriminator(object.getDiscriminator());
675673
model.setWriteOnly(object.getWriteOnly());
676674
model.setUniqueItems(object.getUniqueItems());

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

Lines changed: 34 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import io.swagger.v3.oas.models.Operation;
2323
import io.swagger.v3.oas.models.info.Info;
2424
import io.swagger.v3.oas.models.media.ArraySchema;
25+
import io.swagger.v3.oas.models.media.ComposedSchema;
2526
import io.swagger.v3.oas.models.media.FileSchema;
2627
import io.swagger.v3.oas.models.media.Schema;
2728
import io.swagger.v3.oas.models.media.XML;
@@ -96,14 +97,11 @@ public RustServerCodegen() {
9697
SecurityFeature.OAuth2_Implicit
9798
))
9899
.excludeGlobalFeatures(
99-
GlobalFeature.XMLStructureDefinitions,
100100
GlobalFeature.LinkObjects,
101101
GlobalFeature.ParameterStyling
102102
)
103103
.excludeSchemaSupportFeatures(
104-
SchemaSupportFeature.Polymorphism,
105-
SchemaSupportFeature.Union,
106-
SchemaSupportFeature.Composite
104+
SchemaSupportFeature.Polymorphism
107105
)
108106
.excludeParameterFeatures(
109107
ParameterFeature.Cookie
@@ -400,7 +398,7 @@ public String toModelName(String name) {
400398
}
401399

402400
// model name starts with number
403-
else if (name.matches("^\\d.*")) {
401+
else if (camelizedName.matches("^\\d.*")) {
404402
// e.g. 200Response => Model200Response (after camelize)
405403
camelizedName = "Model" + camelizedName;
406404
LOGGER.warn(name + " (model name starts with number) cannot be used as model name. Renamed to " + camelizedName);
@@ -1191,8 +1189,11 @@ public String toInstantiationType(Schema p) {
11911189

11921190
@Override
11931191
public CodegenModel fromModel(String name, Schema model) {
1192+
LOGGER.trace("Creating model from schema: {}", model);
1193+
11941194
Map<String, Schema> allDefinitions = ModelUtils.getSchemas(this.openAPI);
11951195
CodegenModel mdl = super.fromModel(name, model);
1196+
11961197
mdl.vendorExtensions.put("x-upper-case-name", name.toUpperCase(Locale.ROOT));
11971198
if (!StringUtils.isEmpty(model.get$ref())) {
11981199
Schema schema = allDefinitions.get(ModelUtils.getSimpleRef(model.get$ref()));
@@ -1233,6 +1234,8 @@ public CodegenModel fromModel(String name, Schema model) {
12331234
} else {
12341235
mdl.arrayModelType = toModelName(mdl.arrayModelType);
12351236
}
1237+
} else if ((mdl.anyOf.size() > 0) || (mdl.oneOf.size() > 0)) {
1238+
mdl.dataType = getSchemaType(model);
12361239
}
12371240

12381241
if (mdl.xmlNamespace != null) {
@@ -1245,6 +1248,8 @@ public CodegenModel fromModel(String name, Schema model) {
12451248
mdl.additionalPropertiesType = getTypeDeclaration(additionalProperties);
12461249
}
12471250

1251+
LOGGER.trace("Created model: {}", mdl);
1252+
12481253
return mdl;
12491254
}
12501255

@@ -1403,6 +1408,28 @@ else if (ModelUtils.isBooleanSchema(p)) {
14031408
return defaultValue;
14041409
}
14051410

1411+
@Override
1412+
public String toOneOfName(List<String> names, ComposedSchema composedSchema) {
1413+
List<Schema> schemas = ModelUtils.getInterfaces(composedSchema);
1414+
1415+
List<String> types = new ArrayList<>();
1416+
for (Schema s : schemas) {
1417+
types.add(getTypeDeclaration(s));
1418+
}
1419+
return "swagger::OneOf" + types.size() + "<" + String.join(",", types) + ">";
1420+
}
1421+
1422+
@Override
1423+
public String toAnyOfName(List<String> names, ComposedSchema composedSchema) {
1424+
List<Schema> schemas = ModelUtils.getInterfaces(composedSchema);
1425+
1426+
List<String> types = new ArrayList<>();
1427+
for (Schema s : schemas) {
1428+
types.add(getTypeDeclaration(s));
1429+
}
1430+
return "swagger::AnyOf" + types.size() + "<" + String.join(",", types) + ">";
1431+
}
1432+
14061433
@Override
14071434
public void postProcessModelProperty(CodegenModel model, CodegenProperty property) {
14081435
super.postProcessModelProperty(model, property);
@@ -1529,6 +1556,8 @@ public Map<String, Object> postProcessModels(Map<String, Object> objs) {
15291556
Map<String, Object> mo = (Map<String, Object>) _mo;
15301557
CodegenModel cm = (CodegenModel) mo.get("model");
15311558

1559+
LOGGER.trace("Post processing model: {}", cm);
1560+
15321561
if (cm.dataType != null && cm.dataType.equals("object")) {
15331562
// Object isn't a sensible default. Instead, we set it to
15341563
// 'null'. This ensures that we treat this model as a struct

modules/openapi-generator/src/main/resources/rust-server/models.mustache

Lines changed: 69 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -4,26 +4,38 @@ use crate::models;
44
#[cfg(any(feature = "client", feature = "server"))]
55
use crate::header;
66
{{! Don't "use" structs here - they can conflict with the names of models, and mean that the code won't compile }}
7-
8-
{{#models}}{{#model}}
9-
{{#description}}/// {{{description}}}
10-
{{/description}}{{#isEnum}}/// Enumeration of values.
7+
{{#models}}
8+
{{#model}}
9+
10+
{{#description}}
11+
/// {{{description}}}
12+
{{/description}}
13+
{{#isEnum}}
14+
/// Enumeration of values.
1115
/// Since this enum's variants do not hold data, we can easily define them them as `#[repr(C)]`
1216
/// which helps with FFI.
1317
#[allow(non_camel_case_types)]
1418
#[repr(C)]
1519
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, serde::Serialize, serde::Deserialize)]
1620
#[cfg_attr(feature = "conversion", derive(frunk_enum_derive::LabelledGenericEnum))]{{#xmlName}}
1721
#[serde(rename = "{{{xmlName}}}")]{{/xmlName}}
18-
pub enum {{{classname}}} { {{#allowableValues}}{{#enumVars}}
22+
pub enum {{{classname}}} {
23+
{{#allowableValues}}
24+
{{#enumVars}}
1925
#[serde(rename = {{{value}}})]
20-
{{{name}}},{{/enumVars}}{{/allowableValues}}
26+
{{{name}}},
27+
{{/enumVars}}
28+
{{/allowableValues}}
2129
}
2230

2331
impl std::fmt::Display for {{{classname}}} {
2432
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
25-
match *self { {{#allowableValues}}{{#enumVars}}
26-
{{{classname}}}::{{{name}}} => write!(f, "{}", {{{value}}}),{{/enumVars}}{{/allowableValues}}
33+
match *self {
34+
{{#allowableValues}}
35+
{{#enumVars}}
36+
{{{classname}}}::{{{name}}} => write!(f, "{}", {{{value}}}),
37+
{{/enumVars}}
38+
{{/allowableValues}}
2739
}
2840
}
2941
}
@@ -62,8 +74,8 @@ impl std::convert::From<{{{dataType}}}> for {{{classname}}} {
6274
{{{classname}}}(x)
6375
}
6476
}
65-
6677
{{#vendorExtensions.x-is-string}}
78+
6779
impl std::string::ToString for {{{classname}}} {
6880
fn to_string(&self) -> String {
6981
self.0.to_string()
@@ -121,45 +133,8 @@ impl ::std::str::FromStr for {{{classname}}} {
121133
{{/additionalPropertiesType}}
122134
{{/dataType}}
123135
{{^dataType}}
124-
// Methods for converting between header::IntoHeaderValue<{{{classname}}}> and hyper::header::HeaderValue
125-
126-
#[cfg(any(feature = "client", feature = "server"))]
127-
impl std::convert::TryFrom<header::IntoHeaderValue<{{{classname}}}>> for hyper::header::HeaderValue {
128-
type Error = String;
129-
130-
fn try_from(hdr_value: header::IntoHeaderValue<{{{classname}}}>) -> std::result::Result<Self, Self::Error> {
131-
let hdr_value = hdr_value.to_string();
132-
match hyper::header::HeaderValue::from_str(&hdr_value) {
133-
std::result::Result::Ok(value) => std::result::Result::Ok(value),
134-
std::result::Result::Err(e) => std::result::Result::Err(
135-
format!("Invalid header value for {{classname}} - value: {} is invalid {}",
136-
hdr_value, e))
137-
}
138-
}
139-
}
140-
141-
#[cfg(any(feature = "client", feature = "server"))]
142-
impl std::convert::TryFrom<hyper::header::HeaderValue> for header::IntoHeaderValue<{{{classname}}}> {
143-
type Error = String;
144-
145-
fn try_from(hdr_value: hyper::header::HeaderValue) -> std::result::Result<Self, Self::Error> {
146-
match hdr_value.to_str() {
147-
std::result::Result::Ok(value) => {
148-
match <{{{classname}}} as std::str::FromStr>::from_str(value) {
149-
std::result::Result::Ok(value) => std::result::Result::Ok(header::IntoHeaderValue(value)),
150-
std::result::Result::Err(err) => std::result::Result::Err(
151-
format!("Unable to convert header value '{}' into {{classname}} - {}",
152-
value, err))
153-
}
154-
},
155-
std::result::Result::Err(e) => std::result::Result::Err(
156-
format!("Unable to convert header: {:?} to string: {}",
157-
hdr_value, e))
158-
}
159-
}
160-
}
161-
162-
{{#arrayModelType}}{{#vendorExtensions}}{{#x-item-xml-name}}// Utility function for wrapping list elements when serializing xml
136+
{{#arrayModelType}}
137+
{{#vendorExtensions}}{{#x-item-xml-name}}// Utility function for wrapping list elements when serializing xml
163138
#[allow(non_snake_case)]
164139
fn wrap_in_{{{x-item-xml-name}}}<S>(item: &Vec<{{{arrayModelType}}}>, serializer: S) -> std::result::Result<S::Ok, S::Error>
165140
where
@@ -265,7 +240,9 @@ impl std::str::FromStr for {{{classname}}} {
265240
}
266241
}
267242

268-
{{/arrayModelType}}{{^arrayModelType}}{{! general struct}}
243+
{{/arrayModelType}}
244+
{{^arrayModelType}}
245+
{{! general struct}}
269246
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
270247
#[cfg_attr(feature = "conversion", derive(frunk::LabelledGeneric))]
271248
{{#xmlName}}
@@ -417,7 +394,7 @@ impl std::str::FromStr for {{{classname}}} {
417394
"{{{baseName}}}" => return std::result::Result::Err("Parsing a nullable type in this style is not supported in {{{classname}}}".to_string()),
418395
{{/isNullable}}
419396
{{^isNullable}}
420-
"{{{baseName}}}" => intermediate_rep.{{{name}}}.push({{{dataType}}}::from_str(val).map_err(|x| format!("{}", x))?),
397+
"{{{baseName}}}" => intermediate_rep.{{{name}}}.push(<{{{dataType}}} as std::str::FromStr>::from_str(val).map_err(|x| format!("{}", x))?),
421398
{{/isNullable}}
422399
{{/isContainer}}
423400
{{/isByteArray}}
@@ -444,22 +421,60 @@ impl std::str::FromStr for {{{classname}}} {
444421
})
445422
}
446423
}
447-
448424
{{/arrayModelType}}
425+
426+
// Methods for converting between header::IntoHeaderValue<{{{classname}}}> and hyper::header::HeaderValue
427+
428+
#[cfg(any(feature = "client", feature = "server"))]
429+
impl std::convert::TryFrom<header::IntoHeaderValue<{{{classname}}}>> for hyper::header::HeaderValue {
430+
type Error = String;
431+
432+
fn try_from(hdr_value: header::IntoHeaderValue<{{{classname}}}>) -> std::result::Result<Self, Self::Error> {
433+
let hdr_value = hdr_value.to_string();
434+
match hyper::header::HeaderValue::from_str(&hdr_value) {
435+
std::result::Result::Ok(value) => std::result::Result::Ok(value),
436+
std::result::Result::Err(e) => std::result::Result::Err(
437+
format!("Invalid header value for {{classname}} - value: {} is invalid {}",
438+
hdr_value, e))
439+
}
440+
}
441+
}
442+
443+
#[cfg(any(feature = "client", feature = "server"))]
444+
impl std::convert::TryFrom<hyper::header::HeaderValue> for header::IntoHeaderValue<{{{classname}}}> {
445+
type Error = String;
446+
447+
fn try_from(hdr_value: hyper::header::HeaderValue) -> std::result::Result<Self, Self::Error> {
448+
match hdr_value.to_str() {
449+
std::result::Result::Ok(value) => {
450+
match <{{{classname}}} as std::str::FromStr>::from_str(value) {
451+
std::result::Result::Ok(value) => std::result::Result::Ok(header::IntoHeaderValue(value)),
452+
std::result::Result::Err(err) => std::result::Result::Err(
453+
format!("Unable to convert header value '{}' into {{classname}} - {}",
454+
value, err))
455+
}
456+
},
457+
std::result::Result::Err(e) => std::result::Result::Err(
458+
format!("Unable to convert header: {:?} to string: {}",
459+
hdr_value, e))
460+
}
461+
}
462+
}
463+
449464
{{/dataType}}
450465
{{/isEnum}}
451-
452466
{{#usesXml}}
453467
{{#usesXmlNamespaces}}
454468
{{#xmlNamespace}}
469+
455470
impl {{{classname}}} {
456471
/// Associated constant for this model's XML namespace.
457472
#[allow(dead_code)]
458473
pub const NAMESPACE: &'static str = "{{{xmlNamespace}}}";
459474
}
460-
461475
{{/xmlNamespace}}
462476
{{/usesXmlNamespaces}}
477+
463478
impl {{{classname}}} {
464479
/// Helper function to allow us to convert this model to an XML string.
465480
/// Will panic if serialisation fails.
@@ -478,4 +493,4 @@ impl {{{classname}}} {
478493
}
479494
{{/usesXml}}
480495
{{/model}}
481-
{{/models}}
496+
{{/models}}

modules/openapi-generator/src/main/resources/rust-server/response.mustache

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ pub enum {{{operationId}}}Response {
4242
{{^required}}
4343
Option<
4444
{{/required}}
45-
{{{datatype}}}
45+
{{{dataType}}}
4646
{{^required}}
4747
>
4848
{{/required}}

0 commit comments

Comments
 (0)