[Rust Server] Support RFC 7807 (#5407)

* Support RFC 7807 - Problem Details for HTTP APIs
* Add test for RFC 7807
* Update samples
This commit is contained in:
Richard Whitehouse
2020-02-26 18:18:01 +00:00
committed by GitHub
parent 1b3094be87
commit ca9376fdbb
14 changed files with 350 additions and 38 deletions

View File

@@ -72,6 +72,10 @@ public class RustServerCodegen extends DefaultCodegen implements CodegenConfig {
private static final String plainTextMimeType = "text/plain";
private static final String jsonMimeType = "application/json";
// RFC 7807 Support
private static final String problemJsonMimeType = "application/problem+json";
private static final String problemXmlMimeType = "application/problem+xml";
public RustServerCodegen() {
super();
@@ -524,11 +528,13 @@ public class RustServerCodegen extends DefaultCodegen implements CodegenConfig {
}
private boolean isMimetypeXml(String mimetype) {
return mimetype.toLowerCase(Locale.ROOT).startsWith(xmlMimeType);
return mimetype.toLowerCase(Locale.ROOT).startsWith(xmlMimeType) ||
mimetype.toLowerCase(Locale.ROOT).startsWith(problemXmlMimeType);
}
private boolean isMimetypeJson(String mimetype) {
return mimetype.toLowerCase(Locale.ROOT).startsWith(jsonMimeType);
return mimetype.toLowerCase(Locale.ROOT).startsWith(jsonMimeType) ||
mimetype.toLowerCase(Locale.ROOT).startsWith(problemJsonMimeType);
}
private boolean isMimetypeWwwFormUrlEncoded(String mimetype) {

View File

@@ -246,6 +246,27 @@ paths:
responses:
'200':
description: Success
/rfc7807:
get:
responses:
'204':
description: OK
content:
application/json:
schema:
$ref: '#/components/schemas/ObjectWithArrayOfObjects'
'404':
description: NotFound
content:
application/problem+json:
schema:
$ref: '#/components/schemas/ObjectWithArrayOfObjects'
'406':
description: NotAcceptable
content:
application/problem+xml:
schema:
$ref: '#/components/schemas/ObjectWithArrayOfObjects'
components:
securitySchemes:

View File

@@ -68,6 +68,7 @@ cargo run --example client ParamgetGet
cargo run --example client ReadonlyAuthSchemeGet
cargo run --example client RequiredOctetStreamPut
cargo run --example client ResponsesWithHeadersGet
cargo run --example client Rfc7807Get
cargo run --example client UntypedPropertyGet
cargo run --example client UuidGet
cargo run --example client XmlExtraPost
@@ -115,6 +116,7 @@ Method | HTTP request | Description
[****](docs/default_api.md#) | **GET** /readonly_auth_scheme |
[****](docs/default_api.md#) | **PUT** /required_octet_stream |
[****](docs/default_api.md#) | **GET** /responses_with_headers |
[****](docs/default_api.md#) | **GET** /rfc7807 |
[****](docs/default_api.md#) | **GET** /untyped_property |
[****](docs/default_api.md#) | **GET** /uuid |
[****](docs/default_api.md#) | **POST** /xml_extra |

View File

@@ -241,6 +241,27 @@ paths:
responses:
"200":
description: Success
/rfc7807:
get:
responses:
"204":
content:
application/json:
schema:
$ref: '#/components/schemas/ObjectWithArrayOfObjects'
description: OK
"404":
content:
application/problem+json:
schema:
$ref: '#/components/schemas/ObjectWithArrayOfObjects'
description: NotFound
"406":
content:
application/problem+xml:
schema:
$ref: '#/components/schemas/ObjectWithArrayOfObjects'
description: NotAcceptable
components:
schemas:
EnumWithStarObject:
@@ -313,6 +334,10 @@ components:
name: snake_another_xml_object
namespace: http://foo.bar
ObjectWithArrayOfObjects:
example:
objectArray:
- null
- null
properties:
objectArray:
items:

View File

@@ -1,13 +0,0 @@
# ObjUntypedProps
## Properties
Name | Type | Description | Notes
------------ | ------------- | ------------- | -------------
**req_untyped** | [***serde_json::Value**](object.md) | |
**req_untyped_nullable** | [***serde_json::Value**](object.md) | |
**not_req_untyped** | [***serde_json::Value**](object.md) | | [optional] [default to None]
**not_req_untyped_nullable** | [***serde_json::Value**](object.md) | | [optional] [default to None]
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)

View File

@@ -1,9 +0,0 @@
# OptionalObjectField
## Properties
Name | Type | Description | Notes
------------ | ------------- | ------------- | -------------
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)

View File

@@ -1,9 +0,0 @@
# RequiredObjectField
## Properties
Name | Type | Description | Notes
------------ | ------------- | ------------- | -------------
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)

View File

@@ -11,6 +11,7 @@ Method | HTTP request | Description
****](default_api.md#) | **GET** /readonly_auth_scheme |
****](default_api.md#) | **PUT** /required_octet_stream |
****](default_api.md#) | **GET** /responses_with_headers |
****](default_api.md#) | **GET** /rfc7807 |
****](default_api.md#) | **GET** /untyped_property |
****](default_api.md#) | **GET** /uuid |
****](default_api.md#) | **POST** /xml_extra |
@@ -192,6 +193,28 @@ No authorization required
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
# ****
> models::ObjectWithArrayOfObjects ()
### Required Parameters
This endpoint does not need any parameter.
### Return type
[**models::ObjectWithArrayOfObjects**](ObjectWithArrayOfObjects.md)
### Authorization
No authorization required
### HTTP request headers
- **Content-Type**: Not defined
- **Accept**: application/json, application/problem+json, application/problem+xml,
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
# ****
> (optional)

View File

@@ -21,6 +21,7 @@ use openapi_v3::{Api, ApiNoContext, Client, ContextWrapperExt,
ReadonlyAuthSchemeGetResponse,
RequiredOctetStreamPutResponse,
ResponsesWithHeadersGetResponse,
Rfc7807GetResponse,
UntypedPropertyGetResponse,
UuidGetResponse,
XmlExtraPostResponse,
@@ -51,6 +52,8 @@ fn main() {
"ResponsesWithHeadersGet",
"Rfc7807Get",
"UntypedPropertyGet",
"UuidGet",
@@ -163,6 +166,13 @@ fn main() {
println!("{:?} (X-Span-ID: {:?})", result, (client.context() as &dyn Has<XSpanIdString>).get().clone());
},
Some("Rfc7807Get") => {
let mut rt = tokio::runtime::Runtime::new().unwrap();
let result = rt.block_on(client.rfc7807_get(
));
println!("{:?} (X-Span-ID: {:?})", result, (client.context() as &dyn Has<XSpanIdString>).get().clone());
},
Some("UntypedPropertyGet") => {
let mut rt = tokio::runtime::Runtime::new().unwrap();
let result = rt.block_on(client.untyped_property_get(

View File

@@ -24,6 +24,7 @@ use openapi_v3::{Api, ApiError,
ReadonlyAuthSchemeGetResponse,
RequiredOctetStreamPutResponse,
ResponsesWithHeadersGetResponse,
Rfc7807GetResponse,
UntypedPropertyGetResponse,
UuidGetResponse,
XmlExtraPostResponse,
@@ -97,6 +98,13 @@ impl<C> Api<C> for Server<C> where C: Has<XSpanIdString>{
}
fn rfc7807_get(&self, context: &C) -> Box<Future<Item=Rfc7807GetResponse, Error=ApiError> + Send> {
let context = context.clone();
println!("rfc7807_get() - X-Span-ID: {:?}", context.get().0.clone());
Box::new(futures::failed("Generic failure".into()))
}
fn untyped_property_get(&self, object_untyped_props: Option<models::ObjectUntypedProps>, context: &C) -> Box<Future<Item=UntypedPropertyGetResponse, Error=ApiError> + Send> {
let context = context.clone();
println!("untyped_property_get({:?}) - X-Span-ID: {:?}", object_untyped_props, context.get().0.clone());

View File

@@ -36,6 +36,7 @@ use {Api,
ReadonlyAuthSchemeGetResponse,
RequiredOctetStreamPutResponse,
ResponsesWithHeadersGetResponse,
Rfc7807GetResponse,
UntypedPropertyGetResponse,
UuidGetResponse,
XmlExtraPostResponse,
@@ -944,6 +945,131 @@ impl<C, F> Api<C> for Client<F> where
}
fn rfc7807_get(&self, context: &C) -> Box<dyn Future<Item=Rfc7807GetResponse, Error=ApiError> + Send> {
let mut uri = format!(
"{}/rfc7807",
self.base_path
);
// Query parameters
let mut query_string = url::form_urlencoded::Serializer::new("".to_owned());
let query_string_str = query_string.finish();
if !query_string_str.is_empty() {
uri += "?";
uri += &query_string_str;
}
let uri = match Uri::from_str(&uri) {
Ok(uri) => uri,
Err(err) => return Box::new(future::err(ApiError(format!("Unable to build URI: {}", err)))),
};
let mut request = match hyper::Request::builder()
.method("GET")
.uri(uri)
.body(Body::empty()) {
Ok(req) => req,
Err(e) => return Box::new(future::err(ApiError(format!("Unable to create request: {}", e))))
};
let header = HeaderValue::from_str((context as &dyn Has<XSpanIdString>).get().0.clone().to_string().as_str());
request.headers_mut().insert(HeaderName::from_static("x-span-id"), match header {
Ok(h) => h,
Err(e) => return Box::new(future::err(ApiError(format!("Unable to create X-Span ID header value: {}", e))))
});
Box::new(self.client_service.request(request)
.map_err(|e| ApiError(format!("No response received: {}", e)))
.and_then(|mut response| {
match response.status().as_u16() {
204 => {
let body = response.into_body();
Box::new(
body
.concat2()
.map_err(|e| ApiError(format!("Failed to read response: {}", e)))
.and_then(|body|
str::from_utf8(&body)
.map_err(|e| ApiError(format!("Response was not valid UTF8: {}", e)))
.and_then(|body|
serde_json::from_str::<models::ObjectWithArrayOfObjects>(body)
.map_err(|e| e.into())
)
)
.map(move |body| {
Rfc7807GetResponse::OK
(body)
})
) as Box<dyn Future<Item=_, Error=_> + Send>
},
404 => {
let body = response.into_body();
Box::new(
body
.concat2()
.map_err(|e| ApiError(format!("Failed to read response: {}", e)))
.and_then(|body|
str::from_utf8(&body)
.map_err(|e| ApiError(format!("Response was not valid UTF8: {}", e)))
.and_then(|body|
serde_json::from_str::<models::ObjectWithArrayOfObjects>(body)
.map_err(|e| e.into())
)
)
.map(move |body| {
Rfc7807GetResponse::NotFound
(body)
})
) as Box<dyn Future<Item=_, Error=_> + Send>
},
406 => {
let body = response.into_body();
Box::new(
body
.concat2()
.map_err(|e| ApiError(format!("Failed to read response: {}", e)))
.and_then(|body|
str::from_utf8(&body)
.map_err(|e| ApiError(format!("Response was not valid UTF8: {}", e)))
.and_then(|body|
// ToDo: this will move to swagger-rs and become a standard From conversion trait
// once https://github.com/RReverser/serde-xml-rs/pull/45 is accepted upstream
serde_xml_rs::from_str::<models::ObjectWithArrayOfObjects>(body)
.map_err(|e| ApiError(format!("Response body did not match the schema: {}", e)))
)
)
.map(move |body| {
Rfc7807GetResponse::NotAcceptable
(body)
})
) as Box<dyn Future<Item=_, Error=_> + Send>
},
code => {
let headers = response.headers().clone();
Box::new(response.into_body()
.take(100)
.concat2()
.then(move |body|
future::err(ApiError(format!("Unexpected response code {}:\n{:?}\n\n{}",
code,
headers,
match body {
Ok(ref body) => match str::from_utf8(body) {
Ok(body) => Cow::from(body),
Err(e) => Cow::from(format!("<Body was not UTF8: {:?}>", e)),
},
Err(e) => Cow::from(format!("<Failed to read body: {}>", e)),
})))
)
) as Box<dyn Future<Item=_, Error=_> + Send>
}
}
}))
}
fn untyped_property_get(&self, param_object_untyped_props: Option<models::ObjectUntypedProps>, context: &C) -> Box<dyn Future<Item=UntypedPropertyGetResponse, Error=ApiError> + Send> {
let mut uri = format!(
"{}/untyped_property",

View File

@@ -154,6 +154,21 @@ pub enum ResponsesWithHeadersGetResponse {
}
}
#[derive(Debug, PartialEq)]
pub enum Rfc7807GetResponse {
/// OK
OK
( models::ObjectWithArrayOfObjects )
,
/// NotFound
NotFound
( models::ObjectWithArrayOfObjects )
,
/// NotAcceptable
NotAcceptable
( models::ObjectWithArrayOfObjects )
}
#[derive(Debug, PartialEq)]
pub enum UntypedPropertyGetResponse {
/// Check that untyped properties works
@@ -238,6 +253,9 @@ pub trait Api<C> {
fn responses_with_headers_get(&self, context: &C) -> Box<dyn Future<Item=ResponsesWithHeadersGetResponse, Error=ApiError> + Send>;
fn rfc7807_get(&self, context: &C) -> Box<dyn Future<Item=Rfc7807GetResponse, Error=ApiError> + Send>;
fn untyped_property_get(&self, object_untyped_props: Option<models::ObjectUntypedProps>, context: &C) -> Box<dyn Future<Item=UntypedPropertyGetResponse, Error=ApiError> + Send>;
@@ -285,6 +303,9 @@ pub trait ApiNoContext {
fn responses_with_headers_get(&self) -> Box<dyn Future<Item=ResponsesWithHeadersGetResponse, Error=ApiError> + Send>;
fn rfc7807_get(&self) -> Box<dyn Future<Item=Rfc7807GetResponse, Error=ApiError> + Send>;
fn untyped_property_get(&self, object_untyped_props: Option<models::ObjectUntypedProps>) -> Box<dyn Future<Item=UntypedPropertyGetResponse, Error=ApiError> + Send>;
@@ -357,6 +378,11 @@ impl<'a, T: Api<C>, C> ApiNoContext for ContextWrapper<'a, T, C> {
}
fn rfc7807_get(&self) -> Box<dyn Future<Item=Rfc7807GetResponse, Error=ApiError> + Send> {
self.api().rfc7807_get(&self.context())
}
fn untyped_property_get(&self, object_untyped_props: Option<models::ObjectUntypedProps>) -> Box<dyn Future<Item=UntypedPropertyGetResponse, Error=ApiError> + Send> {
self.api().untyped_property_get(object_untyped_props, &self.context())
}

View File

@@ -30,6 +30,15 @@ pub mod responses {
/// Create &str objects for the response content types for ResponsesWithHeadersGet
pub static RESPONSES_WITH_HEADERS_GET_SUCCESS: &str = "application/json";
/// Create &str objects for the response content types for Rfc7807Get
pub static RFC7807_GET_OK: &str = "application/json";
/// Create &str objects for the response content types for Rfc7807Get
pub static RFC7807_GET_NOT_FOUND: &str = "application/problem+json";
/// Create &str objects for the response content types for Rfc7807Get
pub static RFC7807_GET_NOT_ACCEPTABLE: &str = "application/problem+xml";
/// Create &str objects for the response content types for UuidGet
pub static UUID_GET_DUPLICATE_RESPONSE_LONG_TEXT: &str = "application/json";

View File

@@ -28,6 +28,7 @@ use {Api,
ReadonlyAuthSchemeGetResponse,
RequiredOctetStreamPutResponse,
ResponsesWithHeadersGetResponse,
Rfc7807GetResponse,
UntypedPropertyGetResponse,
UuidGetResponse,
XmlExtraPostResponse,
@@ -55,6 +56,7 @@ mod paths {
r"^/readonly_auth_scheme$",
r"^/required_octet_stream$",
r"^/responses_with_headers$",
r"^/rfc7807$",
r"^/untyped_property$",
r"^/uuid$",
r"^/xml$",
@@ -70,11 +72,12 @@ mod paths {
pub static ID_READONLY_AUTH_SCHEME: usize = 4;
pub static ID_REQUIRED_OCTET_STREAM: usize = 5;
pub static ID_RESPONSES_WITH_HEADERS: usize = 6;
pub static ID_UNTYPED_PROPERTY: usize = 7;
pub static ID_UUID: usize = 8;
pub static ID_XML: usize = 9;
pub static ID_XML_EXTRA: usize = 10;
pub static ID_XML_OTHER: usize = 11;
pub static ID_RFC7807: usize = 7;
pub static ID_UNTYPED_PROPERTY: usize = 8;
pub static ID_UUID: usize = 9;
pub static ID_XML: usize = 10;
pub static ID_XML_EXTRA: usize = 11;
pub static ID_XML_OTHER: usize = 12;
}
pub struct MakeService<T, RC> {
@@ -700,6 +703,87 @@ where
}) as Self::Future
},
// Rfc7807Get - GET /rfc7807
&hyper::Method::GET if path.matched(paths::ID_RFC7807) => {
Box::new({
{{
Box::new(
api_impl.rfc7807_get(
&context
).then(move |result| {
let mut response = Response::new(Body::empty());
response.headers_mut().insert(
HeaderName::from_static("x-span-id"),
HeaderValue::from_str((&context as &dyn Has<XSpanIdString>).get().0.clone().to_string().as_str())
.expect("Unable to create X-Span-ID header value"));
match result {
Ok(rsp) => match rsp {
Rfc7807GetResponse::OK
(body)
=> {
*response.status_mut() = StatusCode::from_u16(204).expect("Unable to turn 204 into a StatusCode");
response.headers_mut().insert(
CONTENT_TYPE,
HeaderValue::from_str(mimetypes::responses::RFC7807_GET_OK)
.expect("Unable to create Content-Type header for RFC7807_GET_OK"));
let body = serde_json::to_string(&body).expect("impossible to fail to serialize");
*response.body_mut() = Body::from(body);
},
Rfc7807GetResponse::NotFound
(body)
=> {
*response.status_mut() = StatusCode::from_u16(404).expect("Unable to turn 404 into a StatusCode");
response.headers_mut().insert(
CONTENT_TYPE,
HeaderValue::from_str(mimetypes::responses::RFC7807_GET_NOT_FOUND)
.expect("Unable to create Content-Type header for RFC7807_GET_NOT_FOUND"));
let body = serde_json::to_string(&body).expect("impossible to fail to serialize");
*response.body_mut() = Body::from(body);
},
Rfc7807GetResponse::NotAcceptable
(body)
=> {
*response.status_mut() = StatusCode::from_u16(406).expect("Unable to turn 406 into a StatusCode");
response.headers_mut().insert(
CONTENT_TYPE,
HeaderValue::from_str(mimetypes::responses::RFC7807_GET_NOT_ACCEPTABLE)
.expect("Unable to create Content-Type header for RFC7807_GET_NOT_ACCEPTABLE"));
let body = serde_xml_rs::to_string(&body).expect("impossible to fail to serialize");
*response.body_mut() = Body::from(body);
},
},
Err(_) => {
// Application code returned an error. This should not happen, as the implementation should
// return a valid response.
*response.status_mut() = StatusCode::INTERNAL_SERVER_ERROR;
*response.body_mut() = Body::from("An internal error occurred");
},
}
future::ok(response)
}
))
}}
}) as Self::Future
},
// UntypedPropertyGet - GET /untyped_property
&hyper::Method::GET if path.matched(paths::ID_UNTYPED_PROPERTY) => {
// Body parameters (note that non-required body parameters will ignore garbage
@@ -1263,6 +1347,9 @@ impl<T> RequestParser<T> for ApiRequestParser {
// ResponsesWithHeadersGet - GET /responses_with_headers
&hyper::Method::GET if path.matched(paths::ID_RESPONSES_WITH_HEADERS) => Ok("ResponsesWithHeadersGet"),
// Rfc7807Get - GET /rfc7807
&hyper::Method::GET if path.matched(paths::ID_RFC7807) => Ok("Rfc7807Get"),
// UntypedPropertyGet - GET /untyped_property
&hyper::Method::GET if path.matched(paths::ID_UNTYPED_PROPERTY) => Ok("UntypedPropertyGet"),