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-server] Restore newtype and XML support #1705

Closed
wants to merge 17 commits into from
Closed
Original file line number Diff line number Diff line change
Expand Up @@ -17,19 +17,24 @@

package org.openapitools.codegen.languages;

import io.swagger.v3.core.util.Json;
import io.swagger.v3.oas.models.OpenAPI;
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.FileSchema;
import io.swagger.v3.oas.models.media.Schema;
import io.swagger.v3.oas.models.media.StringSchema;
import io.swagger.v3.oas.models.media.XML;
import io.swagger.v3.oas.models.parameters.Parameter;
import io.swagger.v3.oas.models.parameters.RequestBody;
import org.apache.commons.lang3.StringUtils;
import org.openapitools.codegen.CliOption;
import org.openapitools.codegen.CodegenConfig;
import org.openapitools.codegen.CodegenConstants;
import org.openapitools.codegen.CodegenModel;
import org.openapitools.codegen.CodegenModelType;
import org.openapitools.codegen.CodegenModelFactory;
import org.openapitools.codegen.CodegenOperation;
import org.openapitools.codegen.CodegenParameter;
import org.openapitools.codegen.CodegenProperty;
Expand Down Expand Up @@ -733,6 +738,39 @@ public boolean isDataTypeFile(final String dataType) {
return dataType != null && dataType.equals(typeMapping.get("File").toString());
}

// This is a really terrible hack. We're working around the fact that the
// base version of `fromRequestBody` checks to see whether the body is a
// ref. If so, it unwraps the reference and replaces it with its inner
// type. This causes problems in rust-server, as it means that we use inner
// types in the API, rather than the correct outer type.
//
// Thus, we grab the inner schema beforehand, and then tinker afterwards to
// restore things to sensible values.
@Override
public CodegenParameter fromRequestBody(RequestBody body, Map<String, Schema> schemas, Set<String> imports, String bodyParameterName) {
Schema original_schema = ModelUtils.getSchemaFromRequestBody(body);
CodegenParameter codegenParameter = super.fromRequestBody(body, schemas, imports, bodyParameterName);

if (StringUtils.isNotBlank(original_schema.get$ref())) {
// Undo the mess `super.fromRequestBody` made - re-wrap the inner
// type.
codegenParameter.dataType = getTypeDeclaration(original_schema);
codegenParameter.isPrimitiveType = false;
codegenParameter.isListContainer = false;
codegenParameter.isString = false;

// This is a model, so should only have an example if explicitly
// defined.
if (codegenParameter.vendorExtensions != null && codegenParameter.vendorExtensions.containsKey("x-example")) {
codegenParameter.example = Json.pretty(codegenParameter.vendorExtensions.get("x-example"));
} else {
codegenParameter.example = null;
}
}

return codegenParameter;
}

@Override
public String getTypeDeclaration(Schema p) {
if (ModelUtils.isArraySchema(p)) {
Expand Down Expand Up @@ -770,39 +808,6 @@ public String getTypeDeclaration(Schema p) {
return super.getTypeDeclaration(p);
}

@Override
public CodegenParameter fromParameter(Parameter param, Set<String> imports) {
CodegenParameter parameter = super.fromParameter(param, imports);
if (!parameter.isString && !parameter.isNumeric && !parameter.isByteArray &&
!parameter.isBinary && !parameter.isFile && !parameter.isBoolean &&
!parameter.isDate && !parameter.isDateTime && !parameter.isUuid &&
!parameter.isListContainer && !parameter.isMapContainer &&
!languageSpecificPrimitives.contains(parameter.dataType)) {

String name = "models::" + getTypeDeclaration(parameter.dataType);
parameter.dataType = name;
parameter.baseType = name;
}

return parameter;
}

@Override
public void postProcessParameter(CodegenParameter parameter) {
// If this parameter is not a primitive type, prefix it with "models::"
// to ensure it's namespaced correctly in the Rust code.
if (!parameter.isString && !parameter.isNumeric && !parameter.isByteArray &&
!parameter.isBinary && !parameter.isFile && !parameter.isBoolean &&
!parameter.isDate && !parameter.isDateTime && !parameter.isUuid &&
!parameter.isListContainer && !parameter.isMapContainer &&
!languageSpecificPrimitives.contains(parameter.dataType)) {

String name = "models::" + getTypeDeclaration(parameter.dataType);
parameter.dataType = name;
parameter.baseType = name;
}
}

@Override
public String toInstantiationType(Schema p) {
if (ModelUtils.isArraySchema(p)) {
Expand All @@ -827,17 +832,34 @@ public CodegenModel fromModel(String name, Schema model, Map<String, Schema> all
}
if (ModelUtils.isArraySchema(model)) {
ArraySchema am = (ArraySchema) model;
String xmlName = null;

// Detect XML list where the inner item is defined directly.
if ((am.getItems() != null) &&
(am.getItems().getXml() != null)) {
xmlName = am.getItems().getXml().getName();
}

// If this model's items require wrapping in xml, squirrel
// away the xml name so we can insert it into the relevant model fields.
String xmlName = am.getItems().getXml().getName();
if (xmlName != null) {
mdl.vendorExtensions.put("itemXmlName", xmlName);
modelXmlNames.put("models::" + mdl.classname, xmlName);
// Detect XML list where the inner item is a reference.
if (am.getXml() != null && am.getXml().getWrapped() &&
am.getItems() != null &&
!StringUtils.isEmpty(am.getItems().get$ref())) {
Schema inner_schema = allDefinitions.get(
ModelUtils.getSimpleRef(am.getItems().get$ref()));

if (inner_schema.getXml() != null &&
inner_schema.getXml().getName() != null) {
xmlName = inner_schema.getXml().getName();
}
}

// If this model's items require wrapping in xml, squirrel away the
// xml name so we can insert it into the relevant model fields.
if (xmlName != null) {
mdl.vendorExtensions.put("itemXmlName", xmlName);
modelXmlNames.put("models::" + mdl.classname, xmlName);
}

mdl.arrayModelType = toModelName(mdl.arrayModelType);
}

Expand Down Expand Up @@ -1067,23 +1089,6 @@ public Map<String, Object> postProcessModels(Map<String, Object> objs) {
return super.postProcessModelsEnum(objs);
}

private boolean paramHasXmlNamespace(CodegenParameter param, Map<String, Schema> definitions) {
Object refName = param.vendorExtensions.get("refName");

if ((refName != null) && (refName instanceof String)) {
String name = (String) refName;
Schema model = definitions.get(ModelUtils.getSimpleRef(name));

if (model != null) {
XML xml = model.getXml();
if ((xml != null) && (xml.getNamespace() != null)) {
return true;
}
}
}
return false;
}

private void processParam(CodegenParameter param, CodegenOperation op) {
String example = null;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -273,23 +273,31 @@ impl<F, C> Api<C> for Client<F> where
request.headers_mut().set(ContentType(mimetypes::requests::{{#vendorExtensions}}{{{uppercase_operation_id}}}{{/vendorExtensions}}.clone()));
request.set_body(body.into_bytes());{{/-last}}{{/formParams}}{{/vendorExtensions}}{{#bodyParam}}{{#-first}}
// Body parameter
{{/-first}}{{#vendorExtensions}}{{#required}}{{#consumesPlainText}} let body = param_{{{paramName}}};{{/consumesPlainText}}{{#consumesXml}}
{{^has_namespace}} let body = serde_xml_rs::to_string(&param_{{{paramName}}}).expect("impossible to fail to serialize");{{/has_namespace}}{{#has_namespace}}
let mut namespaces = BTreeMap::new();
// An empty string is used to indicate a global namespace in xmltree.
namespaces.insert("".to_string(), models::namespaces::{{{uppercase_data_type}}}.clone());
let body = serde_xml_rs::to_string_with_namespaces(&param_{{{paramName}}}, namespaces).expect("impossible to fail to serialize");{{/has_namespace}}{{/consumesXml}}{{#consumesJson}}
let body = serde_json::to_string(&param_{{{paramName}}}).expect("impossible to fail to serialize");{{/consumesJson}}
{{/required}}{{^required}}{{#consumesPlainText}} let body = param_{{{paramName}}};
{{/consumesPlainText}}{{^consumesPlainText}} let body = param_{{{paramName}}}.map(|ref body| {
{{#consumesXml}}
{{^has_namespace}} serde_xml_rs::to_string(body).expect("impossible to fail to serialize"){{/has_namespace}}{{#has_namespace}}
let mut namespaces = BTreeMap::new();
// An empty string is used to indicate a global namespace in xmltree.
namespaces.insert("".to_string(), models::namespaces::{{{uppercase_data_type}}}.clone());
serde_xml_rs::to_string_with_namespaces(body, namespaces).expect("impossible to fail to serialize"){{/has_namespace}}{{/consumesXml}}{{#consumesJson}}
serde_json::to_string(body).expect("impossible to fail to serialize"){{/consumesJson}}
});{{/consumesPlainText}}{{/required}}{{/vendorExtensions}}{{/bodyParam}}
{{/-first}}
{{#vendorExtensions}}
{{#consumesPlainText}}
let body = param_{{{paramName}}};
{{/consumesPlainText}}
{{#required}}
{{#consumesXml}}
let body = param_{{{paramName}}}.to_xml();
{{/consumesXml}}
{{#consumesJson}}
let body = serde_json::to_string(&param_{{{paramName}}}).expect("impossible to fail to serialize");
{{/consumesJson}}
{{/required}}
{{^required}}
let body = param_{{{paramName}}}.map(|ref body| {
{{#consumesXml}}
body.to_xml()
{{/consumesXml}}
{{#consumesJson}}
serde_json::to_string(body).expect("impossible to fail to serialize")
{{/consumesJson}}
});
{{/required}}
{{/vendorExtensions}}
{{/bodyParam}}

{{#bodyParam}}{{^required}}if let Some(body) = body {
{{/required}} request.set_body(body.into_bytes());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@ use std::marker::PhantomData;
use hyper;
use {{{externCrateName}}};
use swagger::{Has, XSpanIdString};
{{#hasAuthMethods}}
use swagger::auth::Authorization;
{{/hasAuthMethods}}

pub struct NewService<C>{
marker: PhantomData<C>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,17 @@
extern crate chrono;
extern crate uuid;

{{#usesXml}}use serde_xml_rs;{{/usesXml}}
{{#usesXml}}
use serde_xml_rs;
{{/usesXml}}
use serde::ser::Serializer;

{{#usesXml}}
use std::collections::{HashMap, BTreeMap};
{{/usesXml}}
{{^usesXml}}
use std::collections::HashMap;
{{/usesXml}}
use models;
use swagger;

Expand Down Expand Up @@ -78,7 +85,7 @@ where
}

{{/itemXmlName}}{{/vendorExtensions}}{{! vec}}#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct {{{classname}}}(Vec<{{{arrayModelType}}}>);
pub struct {{{classname}}}({{#vendorExtensions}}{{#itemXmlName}}#[serde(serialize_with = "wrap_in_{{{itemXmlName}}}")]{{/itemXmlName}}{{/vendorExtensions}}Vec<{{{arrayModelType}}}>);

impl ::std::convert::From<Vec<{{{arrayModelType}}}>> for {{{classname}}} {
fn from(x: Vec<{{{arrayModelType}}}>) -> Self {
Expand Down Expand Up @@ -164,12 +171,42 @@ impl {{{classname}}} {
}
}
}
{{/arrayModelType}}{{/dataType}}{{/isEnum}}{{/model}}{{/models}}{{#usesXmlNamespaces}}
{{/arrayModelType}}
{{/dataType}}
{{/isEnum}}

{{#usesXml}}
impl {{{classname}}} {
/// Helper function to allow us to convert this model to an XML string.
/// Will panic if serialisation fails.
#[allow(dead_code, non_snake_case)]
pub(crate) fn to_xml(&self) -> String {
{{#xmlNamespace}}
let mut namespaces = BTreeMap::new();
// An empty string is used to indicate a global namespace in xmltree.
namespaces.insert("".to_string(), models::namespaces::{{#vendorExtensions}}{{{upperCaseName}}}{{/vendorExtensions}}.clone());
serde_xml_rs::to_string_with_namespaces(&self, namespaces).expect("impossible to fail to serialize")
{{/xmlNamespace}}
{{^xmlNamespace}}
serde_xml_rs::to_string(&self).expect("impossible to fail to serialize")
{{/xmlNamespace}}
}
}
{{/usesXml}}
{{/model}}
{{/models}}

{{#usesXmlNamespaces}}
//XML namespaces
pub mod namespaces {
lazy_static!{
{{#models}}{{#model}}{{#xmlNamespace}}pub static ref {{#vendorExtensions}}{{{upperCaseName}}}{{/vendorExtensions}}: String = "{{{xmlNamespace}}}".to_string();
{{/xmlNamespace}}{{/model}}{{/models}}
{{#models}}
{{#model}}
{{#xmlNamespace}}
pub static ref {{#vendorExtensions}}{{{upperCaseName}}}{{/vendorExtensions}}: String = "{{{xmlNamespace}}}".to_string();
{{/xmlNamespace}}
{{/model}}
{{/models}}
}
}
{{/usesXmlNamespaces}}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
openapi: 3.0.1
info:
title: My title
description: API under test
version: 1.0.7
paths:
/xml:
post:
requestBody:
content:
application/xml:
schema:
$ref: '#/components/schemas/XmlArray'
responses:
201:
description: OK
content:
application/xml:
schema:
$ref: '#/components/schemas/XmlObject'
components:
schemas:
XmlArray:
type: array
xml:
wrapped: true
items:
xml:
name: another
$ref: '#/components/schemas/XmlInner'
XmlInner:
type: string
xml:
name: another
XmlObject:
description: An XML object
type: object
properties:
inner:
type: string
xml:
name: an_xml_object
namespace: foo.bar

Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ paths:
description: Success
schema:
type: object

# Requests with arbitrary JSON currently fail.
# post:
# summary: Send an arbitrary JSON blob
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
[build]
rustflags = [
"-W", "missing_docs", # detects missing documentation for public members

"-W", "trivial_casts", # detects trivial casts which could be removed

"-W", "trivial_numeric_casts", # detects trivial casts of numeric types which could be removed

"-W", "unsafe_code", # usage of `unsafe` code

"-W", "unused_qualifications", # detects unnecessarily qualified names

"-W", "unused_extern_crates", # extern crates that are never used

"-W", "unused_import_braces", # unnecessary braces around an imported item

"-D", "warnings", # all warnings should be denied
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
target
Cargo.lock
Loading