Skip to content

Commit

Permalink
[Rust][Client][Reqwest] Better http error handling (#6481)
Browse files Browse the repository at this point in the history
* Stronger typing for http errors with Rust client. (#5609).

* Error structure can be parametrized (but is still hardcoded with `serde_json::Value` in generated code).

* Each API method has is own enum of functionnal errors.

* Fix the missing "Debug" derivation for API error enums.

* Generate models for error deserialization.

* Handle several 2xx success models.

* Expose new API objects, required to use the API (params, success, error structs/enums).

Co-authored-by: William Cheng <wing328hk@gmail.com>
  • Loading branch information
bcourtine and wing328 committed Jun 13, 2020
1 parent eb26fe5 commit 8e2bf99
Show file tree
Hide file tree
Showing 20 changed files with 1,589 additions and 278 deletions.
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
{{>partial_header}}
#[allow(unused_imports)]
use std::rc::Rc;
use std::borrow::Borrow;
{{^supportAsync}}use std::borrow::Borrow;{{/supportAsync}}
use std::option::Option;

use reqwest;

use crate::apis::ResponseContent;
use super::{Error, configuration};

{{^supportAsync}}
Expand Down Expand Up @@ -44,15 +45,59 @@ pub struct {{{operationIdCamelCase}}}Params {
{{/operation}}
{{/operations}}

{{#operations}}
{{#operation}}
/// struct for typed successes of method `{{operationId}}`
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum {{{operationIdCamelCase}}}Success {
{{#responses}}
{{#is2xx}}
Status{{code}}({{#isEnum}}{{{enumName}}}{{/isEnum}}{{^isEnum}}{{{dataType}}}{{/isEnum}}),
{{/is2xx}}
{{#is3xx}}
Status{{code}}({{#isEnum}}{{{enumName}}}{{/isEnum}}{{^isEnum}}{{{dataType}}}{{/isEnum}}),
{{/is3xx}}
{{/responses}}
UnknownList(Vec<serde_json::Value>),
UnknownValue(serde_json::Value),
}

{{/operation}}
{{/operations}}
{{#operations}}
{{#operation}}
/// struct for typed errors of method `{{operationId}}`
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum {{{operationIdCamelCase}}}Error {
{{#responses}}
{{#is4xx}}
Status{{code}}({{#isEnum}}{{{enumName}}}{{/isEnum}}{{^isEnum}}{{{dataType}}}{{/isEnum}}),
{{/is4xx}}
{{#is5xx}}
Status{{code}}({{#isEnum}}{{{enumName}}}{{/isEnum}}{{^isEnum}}{{{dataType}}}{{/isEnum}}),
{{/is5xx}}
{{#isDefault}}
DefaultResponse({{#isEnum}}{{{enumName}}}{{/isEnum}}{{^isEnum}}{{{dataType}}}{{/isEnum}}),
{{/isDefault}}
{{/responses}}
UnknownList(Vec<serde_json::Value>),
UnknownValue(serde_json::Value),
}

{{/operation}}
{{/operations}}

{{^supportAsync}}
pub trait {{{classname}}} {
{{#operations}}
{{#operation}}
{{#vendorExtensions.x-group-parameters}}
fn {{{operationId}}}(&self{{#allParams}}{{#-first}}, params: {{{operationIdCamelCase}}}Params{{/-first}}{{/allParams}}) -> Result<{{^returnType}}(){{/returnType}}{{#returnType}}{{{returnType}}}{{/returnType}}, Error>;
fn {{{operationId}}}(&self{{#allParams}}{{#-first}}, params: {{{operationIdCamelCase}}}Params{{/-first}}{{/allParams}}) -> Result<ResponseContent<{{{operationIdCamelCase}}}Success>, Error<{{{operationIdCamelCase}}}Error>>;
{{/vendorExtensions.x-group-parameters}}
{{^vendorExtensions.x-group-parameters}}
fn {{{operationId}}}(&self, {{#allParams}}{{{paramName}}}: {{^required}}Option<{{/required}}{{#required}}{{#isNullable}}Option<{{/isNullable}}{{/required}}{{#isString}}&str{{/isString}}{{#isUuid}}&str{{/isUuid}}{{^isString}}{{^isUuid}}{{^isPrimitiveType}}{{^isContainer}}crate::models::{{/isContainer}}{{/isPrimitiveType}}{{{dataType}}}{{/isUuid}}{{/isString}}{{^required}}>{{/required}}{{#required}}{{#isNullable}}>{{/isNullable}}{{/required}}{{#hasMore}}, {{/hasMore}}{{/allParams}}) -> Result<{{^returnType}}(){{/returnType}}{{#returnType}}{{{returnType}}}{{/returnType}}, Error>;
fn {{{operationId}}}(&self, {{#allParams}}{{{paramName}}}: {{^required}}Option<{{/required}}{{#required}}{{#isNullable}}Option<{{/isNullable}}{{/required}}{{#isString}}&str{{/isString}}{{#isUuid}}&str{{/isUuid}}{{^isString}}{{^isUuid}}{{^isPrimitiveType}}{{^isContainer}}crate::models::{{/isContainer}}{{/isPrimitiveType}}{{{dataType}}}{{/isUuid}}{{/isString}}{{^required}}>{{/required}}{{#required}}{{#isNullable}}>{{/isNullable}}{{/required}}{{#hasMore}}, {{/hasMore}}{{/allParams}}) -> Result<ResponseContent<{{{operationIdCamelCase}}}Success>, Error<{{{operationIdCamelCase}}}Error>>;
{{/vendorExtensions.x-group-parameters}}
{{/operation}}
{{/operations}}
Expand All @@ -63,15 +108,15 @@ impl {{{classname}}} for {{{classname}}}Client {
{{#operations}}
{{#operation}}
{{#vendorExtensions.x-group-parameters}}
{{#supportAsync}}pub async {{/supportAsync}}fn {{{operationId}}}({{^supportAsync}}&self{{/supportAsync}}{{#supportAsync}}configuration: &configuration::Configuration{{/supportAsync}}{{#allParams}}{{#-first}}, params: {{{operationIdCamelCase}}}Params{{/-first}}{{/allParams}}) -> Result<{{^returnType}}(){{/returnType}}{{#returnType}}{{{returnType}}}{{/returnType}}, Error> {
{{#supportAsync}}pub async {{/supportAsync}}fn {{{operationId}}}({{^supportAsync}}&self{{/supportAsync}}{{#supportAsync}}configuration: &configuration::Configuration{{/supportAsync}}{{#allParams}}{{#-first}}, params: {{{operationIdCamelCase}}}Params{{/-first}}{{/allParams}}) -> Result<ResponseContent<{{{operationIdCamelCase}}}Success>, Error<{{{operationIdCamelCase}}}Error>> {
// unbox the parameters
{{#allParams}}
let {{paramName}} = params.{{paramName}};
{{/allParams}}

{{/vendorExtensions.x-group-parameters}}
{{^vendorExtensions.x-group-parameters}}
{{#supportAsync}}pub async {{/supportAsync}}fn {{{operationId}}}({{^supportAsync}}&self{{/supportAsync}}{{#supportAsync}}configuration: &configuration::Configuration{{/supportAsync}}, {{#allParams}}{{{paramName}}}: {{^required}}Option<{{/required}}{{#required}}{{#isNullable}}Option<{{/isNullable}}{{/required}}{{#isString}}&str{{/isString}}{{#isUuid}}&str{{/isUuid}}{{^isString}}{{^isUuid}}{{^isPrimitiveType}}{{^isContainer}}crate::models::{{/isContainer}}{{/isPrimitiveType}}{{{dataType}}}{{/isUuid}}{{/isString}}{{^required}}>{{/required}}{{#required}}{{#isNullable}}>{{/isNullable}}{{/required}}{{#hasMore}}, {{/hasMore}}{{/allParams}}) -> Result<{{^returnType}}(){{/returnType}}{{#returnType}}{{{returnType}}}{{/returnType}}, Error> {
{{#supportAsync}}pub async {{/supportAsync}}fn {{{operationId}}}({{^supportAsync}}&self{{/supportAsync}}{{#supportAsync}}configuration: &configuration::Configuration{{/supportAsync}}, {{#allParams}}{{{paramName}}}: {{^required}}Option<{{/required}}{{#required}}{{#isNullable}}Option<{{/isNullable}}{{/required}}{{#isString}}&str{{/isString}}{{#isUuid}}&str{{/isUuid}}{{^isString}}{{^isUuid}}{{^isPrimitiveType}}{{^isContainer}}crate::models::{{/isContainer}}{{/isPrimitiveType}}{{{dataType}}}{{/isUuid}}{{/isString}}{{^required}}>{{/required}}{{#required}}{{#isNullable}}>{{/isNullable}}{{/required}}{{#hasMore}}, {{/hasMore}}{{/allParams}}) -> Result<ResponseContent<{{{operationIdCamelCase}}}Success>, Error<{{{operationIdCamelCase}}}Error>> {
{{/vendorExtensions.x-group-parameters}}
{{^supportAsync}}
let configuration: &configuration::Configuration = self.configuration.borrow();
Expand Down Expand Up @@ -263,18 +308,20 @@ impl {{{classname}}} for {{{classname}}}Client {
{{/hasBodyParam}}

let req = req_builder.build()?;
{{^returnType}}
client.execute(req){{#supportAsync}}.await{{/supportAsync}}?.error_for_status()?;
Ok(())
{{/returnType}}
{{#returnType}}
{{#supportAsync}}
Ok(client.execute(req).await?.error_for_status()?.json::<{{{.}}}>().await?)
{{/supportAsync}}
{{^supportAsync}}
Ok(client.execute(req)?.error_for_status()?.json()?)
{{/supportAsync}}
{{/returnType}}
let {{^supportAsync}}mut {{/supportAsync}}resp = client.execute(req){{#supportAsync}}.await{{/supportAsync}}?;

let status = resp.status();
let content = resp.text(){{#supportAsync}}.await{{/supportAsync}}?;

if status.is_success() {
let entity: Option<{{{operationIdCamelCase}}}Success> = serde_json::from_str(&content).ok();
let result = ResponseContent { status, content, entity };
Ok(result)
} else {
let entity: Option<{{{operationIdCamelCase}}}Error> = serde_json::from_str(&content).ok();
let error = ResponseContent { status, content, entity };
Err(Error::ResponseError(error))
}
}

{{/operation}}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,26 +1,34 @@
use reqwest;
use serde_json;

#[derive(Debug, Clone)]
pub struct ResponseContent<T> {
pub status: reqwest::StatusCode,
pub content: String,
pub entity: Option<T>,
}

#[derive(Debug)]
pub enum Error {
pub enum Error<T> {
Reqwest(reqwest::Error),
Serde(serde_json::Error),
Io(std::io::Error),
ResponseError(ResponseContent<T>),
}

impl From<reqwest::Error> for Error {
impl <T> From<reqwest::Error> for Error<T> {
fn from(e: reqwest::Error) -> Self {
Error::Reqwest(e)
}
}

impl From<serde_json::Error> for Error {
impl <T> From<serde_json::Error> for Error<T> {
fn from(e: serde_json::Error) -> Self {
Error::Serde(e)
}
}

impl From<std::io::Error> for Error {
impl <T> From<std::io::Error> for Error<T> {
fn from(e: std::io::Error) -> Self {
Error::Io(e)
}
Expand All @@ -32,26 +40,7 @@ pub fn urlencode<T: AsRef<str>>(s: T) -> String {

{{#apiInfo}}
{{#apis}}
mod {{{classFilename}}};
{{#operations}}
{{#operation}}
{{^supportAsync}}
{{#-first}}
pub use self::{{{classFilename}}}::{ {{{classname}}}, {{{classname}}}Client };
{{/-first}}
{{/supportAsync}}
{{#supportAsync}}
pub use self::{{{classFilename}}}::{ {{{operationId}}} };
{{/supportAsync}}
{{#vendorExtensions.x-group-parameters}}
{{#allParams}}
{{#-first}}
pub use self::{{{classFilename}}}::{{{operationIdCamelCase}}}Params as {{{classname}}}{{{operationIdCamelCase}}}Params;
{{/-first}}
{{/allParams}}
{{/vendorExtensions.x-group-parameters}}
{{/operation}}
{{/operations}}
pub mod {{{classFilename}}};
{{/apis}}
{{/apiInfo}}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ pub struct APIClient {
{{#operations}}
{{#operation}}
{{#-last}}
{{{classFilename}}}: Box<dyn crate::apis::{{{classname}}}>,
{{{classFilename}}}: Box<dyn crate::apis::{{{classFilename}}}::{{{classname}}}>,
{{/-last}}
{{/operation}}
{{/operations}}
Expand All @@ -26,7 +26,7 @@ impl APIClient {
{{#operations}}
{{#operation}}
{{#-last}}
{{{classFilename}}}: Box::new(crate::apis::{{{classname}}}Client::new(rc.clone())),
{{{classFilename}}}: Box::new(crate::apis::{{{classFilename}}}::{{{classname}}}Client::new(rc.clone())),
{{/-last}}
{{/operation}}
{{/operations}}
Expand All @@ -40,7 +40,7 @@ impl APIClient {
{{#operations}}
{{#operation}}
{{#-last}}
pub fn {{{classFilename}}}(&self) -> &dyn crate::apis::{{{classname}}}{
pub fn {{{classFilename}}}(&self) -> &dyn crate::apis::{{{classFilename}}}::{{{classname}}}{
self.{{{classFilename}}}.as_ref()
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,19 @@ use std::rc::Rc;
use super::configuration::Configuration;

pub struct APIClient {
default_api: Box<dyn crate::apis::DefaultApi>,
default_api: Box<dyn crate::apis::default_api::DefaultApi>,
}

impl APIClient {
pub fn new(configuration: Configuration) -> APIClient {
let rc = Rc::new(configuration);

APIClient {
default_api: Box::new(crate::apis::DefaultApiClient::new(rc.clone())),
default_api: Box::new(crate::apis::default_api::DefaultApiClient::new(rc.clone())),
}
}

pub fn default_api(&self) -> &dyn crate::apis::DefaultApi{
pub fn default_api(&self) -> &dyn crate::apis::default_api::DefaultApi{
self.default_api.as_ref()
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ use std::option::Option;

use reqwest;

use crate::apis::ResponseContent;
use super::{Error, configuration};

pub struct DefaultApiClient {
Expand All @@ -30,12 +31,31 @@ impl DefaultApiClient {
}


/// struct for typed successes of method `fileresponsetest`
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum FileresponsetestSuccess {
Status200(std::path::PathBuf),
UnknownList(Vec<serde_json::Value>),
UnknownValue(serde_json::Value),
}

/// struct for typed errors of method `fileresponsetest`
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum FileresponsetestError {
DefaultResponse(std::path::PathBuf),
UnknownList(Vec<serde_json::Value>),
UnknownValue(serde_json::Value),
}


pub trait DefaultApi {
fn fileresponsetest(&self, ) -> Result<std::path::PathBuf, Error>;
fn fileresponsetest(&self, ) -> Result<ResponseContent<FileresponsetestSuccess>, Error<FileresponsetestError>>;
}

impl DefaultApi for DefaultApiClient {
fn fileresponsetest(&self, ) -> Result<std::path::PathBuf, Error> {
fn fileresponsetest(&self, ) -> Result<ResponseContent<FileresponsetestSuccess>, Error<FileresponsetestError>> {
let configuration: &configuration::Configuration = self.configuration.borrow();
let client = &configuration.client;

Expand All @@ -47,7 +67,20 @@ impl DefaultApi for DefaultApiClient {
}

let req = req_builder.build()?;
Ok(client.execute(req)?.error_for_status()?.json()?)
let mut resp = client.execute(req)?;

let status = resp.status();
let content = resp.text()?;

if status.is_success() {
let entity: Option<FileresponsetestSuccess> = serde_json::from_str(&content).ok();
let result = ResponseContent { status, content, entity };
Ok(result)
} else {
let entity: Option<FileresponsetestError> = serde_json::from_str(&content).ok();
let error = ResponseContent { status, content, entity };
Err(Error::ResponseError(error))
}
}

}
Original file line number Diff line number Diff line change
@@ -1,26 +1,34 @@
use reqwest;
use serde_json;

#[derive(Debug, Clone)]
pub struct ResponseContent<T> {
pub status: reqwest::StatusCode,
pub content: String,
pub entity: Option<T>,
}

#[derive(Debug)]
pub enum Error {
pub enum Error<T> {
Reqwest(reqwest::Error),
Serde(serde_json::Error),
Io(std::io::Error),
ResponseError(ResponseContent<T>),
}

impl From<reqwest::Error> for Error {
impl <T> From<reqwest::Error> for Error<T> {
fn from(e: reqwest::Error) -> Self {
Error::Reqwest(e)
}
}

impl From<serde_json::Error> for Error {
impl <T> From<serde_json::Error> for Error<T> {
fn from(e: serde_json::Error) -> Self {
Error::Serde(e)
}
}

impl From<std::io::Error> for Error {
impl <T> From<std::io::Error> for Error<T> {
fn from(e: std::io::Error) -> Self {
Error::Io(e)
}
Expand All @@ -30,8 +38,7 @@ pub fn urlencode<T: AsRef<str>>(s: T) -> String {
::url::form_urlencoded::byte_serialize(s.as_ref().as_bytes()).collect()
}

mod default_api;
pub use self::default_api::{ DefaultApi, DefaultApiClient };
pub mod default_api;

pub mod client;
pub mod configuration;
Loading

0 comments on commit 8e2bf99

Please sign in to comment.