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

[Core, Rust Server] anyOf / oneOf support for Rust Server #6690

Merged
merged 15 commits into from
Jan 23, 2021
Merged
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
2 changes: 1 addition & 1 deletion docs/generators/rust-server.md
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,7 @@ These options may be applied as additional-properties (cli) or configOptions (pl
| Name | Supported | Defined By |
| ---- | --------- | ---------- |
|Simple|✓|OAS2,OAS3
|Composite||OAS2,OAS3
|Composite||OAS2,OAS3
|Polymorphism|✗|OAS2,OAS3
|Union|✗|OAS3

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -378,38 +378,34 @@ private void flattenComposedChildren(OpenAPI openAPI, String key, List<Schema> c
ListIterator<Schema> listIterator = children.listIterator();
while (listIterator.hasNext()) {
Schema component = listIterator.next();
if (component instanceof ObjectSchema || // for inline schema with type:object
(component != null && component.getProperties() != null &&
!component.getProperties().isEmpty())) { // for inline schema without type:object
Schema op = component;
if (op.get$ref() == null && op.getProperties() != null && op.getProperties().size() > 0) {
// If a `title` attribute is defined in the inline schema, codegen uses it to name the
// inline schema. Otherwise, we'll use the default naming such as InlineObject1, etc.
// We know that this is not the best way to name the model.
//
// Such naming strategy may result in issues. If the value of the 'title' attribute
// happens to match a schema defined elsewhere in the specification, 'innerModelName'
// will be the same as that other schema.
//
// To have complete control of the model naming, one can define the model separately
// instead of inline.
String innerModelName = resolveModelName(op.getTitle(), key);
Schema innerModel = modelFromProperty(openAPI, op, innerModelName);
String existing = matchGenerated(innerModel);
if (existing == null) {
openAPI.getComponents().addSchemas(innerModelName, innerModel);
addGenerated(innerModelName, innerModel);
Schema schema = new Schema().$ref(innerModelName);
schema.setRequired(op.getRequired());
listIterator.set(schema);
} else {
Schema schema = new Schema().$ref(existing);
schema.setRequired(op.getRequired());
listIterator.set(schema);
}
if ((component != null) &&
(component.get$ref() == null) &&
((component.getProperties() != null && !component.getProperties().isEmpty()) ||
(component.getEnum() != null && !component.getEnum().isEmpty()))) {
// If a `title` attribute is defined in the inline schema, codegen uses it to name the
// inline schema. Otherwise, we'll use the default naming such as InlineObject1, etc.
// We know that this is not the best way to name the model.
//
// Such naming strategy may result in issues. If the value of the 'title' attribute
// happens to match a schema defined elsewhere in the specification, 'innerModelName'
// will be the same as that other schema.
//
// To have complete control of the model naming, one can define the model separately
// instead of inline.
String innerModelName = resolveModelName(component.getTitle(), key);
Schema innerModel = modelFromProperty(openAPI, component, innerModelName);
String existing = matchGenerated(innerModel);
if (existing == null) {
openAPI.getComponents().addSchemas(innerModelName, innerModel);
addGenerated(innerModelName, innerModel);
Schema schema = new Schema().$ref(innerModelName);
schema.setRequired(component.getRequired());
listIterator.set(schema);
} else {
Schema schema = new Schema().$ref(existing);
schema.setRequired(component.getRequired());
listIterator.set(schema);
}
} else {
// likely a reference to schema (not inline schema)
}
}
}
Expand Down Expand Up @@ -540,7 +536,7 @@ private void addGenerated(String name, Schema model) {
*/
private String sanitizeName(final String name) {
return name
.replaceAll("^[0-9]", "_") // e.g. 12object => _2object
.replaceAll("^[0-9]", "_$0") // e.g. 12object => _12object
.replaceAll("[^A-Za-z0-9]", "_"); // e.g. io.schema.User name => io_schema_User_name
}

Expand Down Expand Up @@ -671,6 +667,8 @@ private Schema modelFromProperty(OpenAPI openAPI, Schema object, String path) {
model.setXml(xml);
model.setRequired(object.getRequired());
model.setNullable(object.getNullable());
model.setEnum(object.getEnum());
model.setType(object.getType());
model.setDiscriminator(object.getDiscriminator());
model.setWriteOnly(object.getWriteOnly());
model.setUniqueItems(object.getUniqueItems());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import io.swagger.v3.oas.models.Operation;
import io.swagger.v3.oas.models.info.Info;
import io.swagger.v3.oas.models.media.ArraySchema;
import io.swagger.v3.oas.models.media.ComposedSchema;
import io.swagger.v3.oas.models.media.FileSchema;
import io.swagger.v3.oas.models.media.Schema;
import io.swagger.v3.oas.models.media.XML;
Expand Down Expand Up @@ -96,14 +97,11 @@ public RustServerCodegen() {
SecurityFeature.OAuth2_Implicit
))
.excludeGlobalFeatures(
GlobalFeature.XMLStructureDefinitions,
GlobalFeature.LinkObjects,
GlobalFeature.ParameterStyling
)
.excludeSchemaSupportFeatures(
SchemaSupportFeature.Polymorphism,
SchemaSupportFeature.Union,
SchemaSupportFeature.Composite
SchemaSupportFeature.Polymorphism
)
.excludeParameterFeatures(
ParameterFeature.Cookie
Expand Down Expand Up @@ -400,7 +398,7 @@ public String toModelName(String name) {
}

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

@Override
public CodegenModel fromModel(String name, Schema model) {
LOGGER.trace("Creating model from schema: {}", model);

Map<String, Schema> allDefinitions = ModelUtils.getSchemas(this.openAPI);
CodegenModel mdl = super.fromModel(name, model);

mdl.vendorExtensions.put("x-upper-case-name", name.toUpperCase(Locale.ROOT));
if (!StringUtils.isEmpty(model.get$ref())) {
Schema schema = allDefinitions.get(ModelUtils.getSimpleRef(model.get$ref()));
Expand Down Expand Up @@ -1233,6 +1234,8 @@ public CodegenModel fromModel(String name, Schema model) {
} else {
mdl.arrayModelType = toModelName(mdl.arrayModelType);
}
} else if ((mdl.anyOf.size() > 0) || (mdl.oneOf.size() > 0)) {
mdl.dataType = getSchemaType(model);
}

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

LOGGER.trace("Created model: {}", mdl);

return mdl;
}

Expand Down Expand Up @@ -1403,6 +1408,28 @@ else if (ModelUtils.isBooleanSchema(p)) {
return defaultValue;
}

@Override
public String toOneOfName(List<String> names, ComposedSchema composedSchema) {
List<Schema> schemas = ModelUtils.getInterfaces(composedSchema);

List<String> types = new ArrayList<>();
for (Schema s : schemas) {
types.add(getTypeDeclaration(s));
}
return "swagger::OneOf" + types.size() + "<" + String.join(",", types) + ">";
}

@Override
public String toAnyOfName(List<String> names, ComposedSchema composedSchema) {
List<Schema> schemas = ModelUtils.getInterfaces(composedSchema);

List<String> types = new ArrayList<>();
for (Schema s : schemas) {
types.add(getTypeDeclaration(s));
}
return "swagger::AnyOf" + types.size() + "<" + String.join(",", types) + ">";
}

@Override
public void postProcessModelProperty(CodegenModel model, CodegenProperty property) {
super.postProcessModelProperty(model, property);
Expand Down Expand Up @@ -1529,6 +1556,8 @@ public Map<String, Object> postProcessModels(Map<String, Object> objs) {
Map<String, Object> mo = (Map<String, Object>) _mo;
CodegenModel cm = (CodegenModel) mo.get("model");

LOGGER.trace("Post processing model: {}", cm);

if (cm.dataType != null && cm.dataType.equals("object")) {
// Object isn't a sensible default. Instead, we set it to
// 'null'. This ensures that we treat this model as a struct
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,26 +4,38 @@ use crate::models;
#[cfg(any(feature = "client", feature = "server"))]
use crate::header;
{{! Don't "use" structs here - they can conflict with the names of models, and mean that the code won't compile }}

{{#models}}{{#model}}
{{#description}}/// {{{description}}}
{{/description}}{{#isEnum}}/// Enumeration of values.
{{#models}}
{{#model}}

{{#description}}
/// {{{description}}}
{{/description}}
{{#isEnum}}
/// Enumeration of values.
/// Since this enum's variants do not hold data, we can easily define them them as `#[repr(C)]`
/// which helps with FFI.
#[allow(non_camel_case_types)]
#[repr(C)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, serde::Serialize, serde::Deserialize)]
#[cfg_attr(feature = "conversion", derive(frunk_enum_derive::LabelledGenericEnum))]{{#xmlName}}
#[serde(rename = "{{{xmlName}}}")]{{/xmlName}}
pub enum {{{classname}}} { {{#allowableValues}}{{#enumVars}}
pub enum {{{classname}}} {
{{#allowableValues}}
{{#enumVars}}
#[serde(rename = {{{value}}})]
{{{name}}},{{/enumVars}}{{/allowableValues}}
{{{name}}},
{{/enumVars}}
{{/allowableValues}}
}

impl std::fmt::Display for {{{classname}}} {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match *self { {{#allowableValues}}{{#enumVars}}
{{{classname}}}::{{{name}}} => write!(f, "{}", {{{value}}}),{{/enumVars}}{{/allowableValues}}
match *self {
{{#allowableValues}}
{{#enumVars}}
{{{classname}}}::{{{name}}} => write!(f, "{}", {{{value}}}),
{{/enumVars}}
{{/allowableValues}}
}
}
}
Expand Down Expand Up @@ -62,8 +74,8 @@ impl std::convert::From<{{{dataType}}}> for {{{classname}}} {
{{{classname}}}(x)
}
}

{{#vendorExtensions.x-is-string}}

impl std::string::ToString for {{{classname}}} {
fn to_string(&self) -> String {
self.0.to_string()
Expand Down Expand Up @@ -121,45 +133,8 @@ impl ::std::str::FromStr for {{{classname}}} {
{{/additionalPropertiesType}}
{{/dataType}}
{{^dataType}}
// Methods for converting between header::IntoHeaderValue<{{{classname}}}> and hyper::header::HeaderValue

#[cfg(any(feature = "client", feature = "server"))]
impl std::convert::TryFrom<header::IntoHeaderValue<{{{classname}}}>> for hyper::header::HeaderValue {
type Error = String;

fn try_from(hdr_value: header::IntoHeaderValue<{{{classname}}}>) -> std::result::Result<Self, Self::Error> {
let hdr_value = hdr_value.to_string();
match hyper::header::HeaderValue::from_str(&hdr_value) {
std::result::Result::Ok(value) => std::result::Result::Ok(value),
std::result::Result::Err(e) => std::result::Result::Err(
format!("Invalid header value for {{classname}} - value: {} is invalid {}",
hdr_value, e))
}
}
}

#[cfg(any(feature = "client", feature = "server"))]
impl std::convert::TryFrom<hyper::header::HeaderValue> for header::IntoHeaderValue<{{{classname}}}> {
type Error = String;

fn try_from(hdr_value: hyper::header::HeaderValue) -> std::result::Result<Self, Self::Error> {
match hdr_value.to_str() {
std::result::Result::Ok(value) => {
match <{{{classname}}} as std::str::FromStr>::from_str(value) {
std::result::Result::Ok(value) => std::result::Result::Ok(header::IntoHeaderValue(value)),
std::result::Result::Err(err) => std::result::Result::Err(
format!("Unable to convert header value '{}' into {{classname}} - {}",
value, err))
}
},
std::result::Result::Err(e) => std::result::Result::Err(
format!("Unable to convert header: {:?} to string: {}",
hdr_value, e))
}
}
}

{{#arrayModelType}}{{#vendorExtensions}}{{#x-item-xml-name}}// Utility function for wrapping list elements when serializing xml
{{#arrayModelType}}
{{#vendorExtensions}}{{#x-item-xml-name}}// Utility function for wrapping list elements when serializing xml
#[allow(non_snake_case)]
fn wrap_in_{{{x-item-xml-name}}}<S>(item: &Vec<{{{arrayModelType}}}>, serializer: S) -> std::result::Result<S::Ok, S::Error>
where
Expand Down Expand Up @@ -265,7 +240,9 @@ impl std::str::FromStr for {{{classname}}} {
}
}

{{/arrayModelType}}{{^arrayModelType}}{{! general struct}}
{{/arrayModelType}}
{{^arrayModelType}}
{{! general struct}}
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
#[cfg_attr(feature = "conversion", derive(frunk::LabelledGeneric))]
{{#xmlName}}
Expand Down Expand Up @@ -417,7 +394,7 @@ impl std::str::FromStr for {{{classname}}} {
"{{{baseName}}}" => return std::result::Result::Err("Parsing a nullable type in this style is not supported in {{{classname}}}".to_string()),
{{/isNullable}}
{{^isNullable}}
"{{{baseName}}}" => intermediate_rep.{{{name}}}.push({{{dataType}}}::from_str(val).map_err(|x| format!("{}", x))?),
"{{{baseName}}}" => intermediate_rep.{{{name}}}.push(<{{{dataType}}} as std::str::FromStr>::from_str(val).map_err(|x| format!("{}", x))?),
{{/isNullable}}
{{/isContainer}}
{{/isByteArray}}
Expand All @@ -444,22 +421,60 @@ impl std::str::FromStr for {{{classname}}} {
})
}
}

{{/arrayModelType}}

// Methods for converting between header::IntoHeaderValue<{{{classname}}}> and hyper::header::HeaderValue

#[cfg(any(feature = "client", feature = "server"))]
impl std::convert::TryFrom<header::IntoHeaderValue<{{{classname}}}>> for hyper::header::HeaderValue {
type Error = String;

fn try_from(hdr_value: header::IntoHeaderValue<{{{classname}}}>) -> std::result::Result<Self, Self::Error> {
let hdr_value = hdr_value.to_string();
match hyper::header::HeaderValue::from_str(&hdr_value) {
std::result::Result::Ok(value) => std::result::Result::Ok(value),
std::result::Result::Err(e) => std::result::Result::Err(
format!("Invalid header value for {{classname}} - value: {} is invalid {}",
hdr_value, e))
}
}
}

#[cfg(any(feature = "client", feature = "server"))]
impl std::convert::TryFrom<hyper::header::HeaderValue> for header::IntoHeaderValue<{{{classname}}}> {
type Error = String;

fn try_from(hdr_value: hyper::header::HeaderValue) -> std::result::Result<Self, Self::Error> {
match hdr_value.to_str() {
std::result::Result::Ok(value) => {
match <{{{classname}}} as std::str::FromStr>::from_str(value) {
std::result::Result::Ok(value) => std::result::Result::Ok(header::IntoHeaderValue(value)),
std::result::Result::Err(err) => std::result::Result::Err(
format!("Unable to convert header value '{}' into {{classname}} - {}",
value, err))
}
},
std::result::Result::Err(e) => std::result::Result::Err(
format!("Unable to convert header: {:?} to string: {}",
hdr_value, e))
}
}
}

{{/dataType}}
{{/isEnum}}

{{#usesXml}}
{{#usesXmlNamespaces}}
{{#xmlNamespace}}

impl {{{classname}}} {
/// Associated constant for this model's XML namespace.
#[allow(dead_code)]
pub const NAMESPACE: &'static str = "{{{xmlNamespace}}}";
}

{{/xmlNamespace}}
{{/usesXmlNamespaces}}

impl {{{classname}}} {
/// Helper function to allow us to convert this model to an XML string.
/// Will panic if serialisation fails.
Expand All @@ -478,4 +493,4 @@ impl {{{classname}}} {
}
{{/usesXml}}
{{/model}}
{{/models}}
{{/models}}
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ pub enum {{{operationId}}}Response {
{{^required}}
Option<
{{/required}}
{{{datatype}}}
{{{dataType}}}
{{^required}}
>
{{/required}}
Expand Down
Loading