hubcaps_ex/
errors.rs

1//! Client errors
2use crate::jwt::errors::Error as JWTError;
3use http::StatusCode;
4use reqwest::Error as ReqwestError;
5use serde::Deserialize;
6use serde_json::error::Error as SerdeError;
7use std::error::Error as StdError;
8use std::fmt;
9use std::io::Error as IoError;
10use std::result::Result as StdResult;
11use std::time::Duration;
12use url::ParseError;
13
14/// A standard result type capturing common errors for all GitHub operations
15pub type Result<T> = StdResult<T, Error>;
16
17#[derive(Debug)]
18pub enum Error {
19    /// Client side error returned for faulty requests
20    Fault {
21        code: StatusCode,
22        error: ClientError,
23    },
24    /// Error kind returned when a credential's rate limit has been exhausted. Wait for the reset duration before issuing more requests
25    RateLimit { reset: Duration },
26    /// Serialization related errors
27    Codec(SerdeError),
28    /// HTTP client errors
29    Reqwest(ReqwestError),
30    /// Url format errors
31    Url(ParseError),
32    /// Network errors
33    IO(IoError),
34    /// JWT validation errors
35    JWT(JWTError),
36}
37
38impl From<SerdeError> for Error {
39    fn from(err: SerdeError) -> Self {
40        Error::Codec(err)
41    }
42}
43
44impl From<ReqwestError> for Error {
45    fn from(err: ReqwestError) -> Self {
46        Error::Reqwest(err)
47    }
48}
49
50impl From<ParseError> for Error {
51    fn from(err: ParseError) -> Self {
52        Error::Url(err)
53    }
54}
55
56impl From<IoError> for Error {
57    fn from(err: IoError) -> Self {
58        Error::IO(err)
59    }
60}
61
62impl From<JWTError> for Error {
63    fn from(err: JWTError) -> Self {
64        Error::JWT(err)
65    }
66}
67
68impl StdError for Error {
69    fn source(&self) -> Option<&(dyn StdError + 'static)> {
70        match self {
71            Error::Codec(err) => Some(err),
72            Error::Reqwest(err) => Some(err),
73            Error::Url(err) => Some(err),
74            Error::IO(err) => Some(err),
75            Error::JWT(err) => Some(err),
76            _ => None,
77        }
78    }
79}
80
81impl fmt::Display for Error {
82    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
83        match self {
84            Error::Fault { code, error } => write!(f, "{}: {}", code, error.message),
85            Error::RateLimit { reset } => write!(
86                f,
87                "Rate limit exhausted. Will reset in {} seconds",
88                reset.as_secs()
89            ),
90            Error::Codec(err) => write!(f, "{}", err),
91            Error::Reqwest(err) => write!(f, "{}", err),
92            Error::Url(err) => write!(f, "{}", err),
93            Error::IO(err) => write!(f, "{}", err),
94            Error::JWT(err) => write!(f, "{}", err),
95        }
96    }
97}
98
99// representations
100
101#[derive(Debug, Deserialize, PartialEq)]
102pub struct FieldErr {
103    pub resource: String,
104    pub field: Option<String>,
105    pub code: String,
106    pub message: Option<String>,
107    pub documentation_url: Option<String>,
108}
109
110#[derive(Debug, Deserialize, PartialEq)]
111pub struct ClientError {
112    pub message: String,
113    pub errors: Option<Vec<FieldErr>>,
114    pub documentation_url: Option<String>,
115}
116
117#[cfg(test)]
118mod tests {
119    use super::{ClientError, FieldErr};
120
121    #[test]
122    fn deserialize_client_field_errors() {
123        for (json, expect) in vec![
124            // see https://github.com/softprops/hubcaps/issues/31
125            (
126                r#"{"message": "Validation Failed","errors":
127                [{
128                    "resource": "Release",
129                    "code": "custom",
130                    "message": "Published releases must have a valid tag"
131                }]}"#,
132                ClientError {
133                    message: "Validation Failed".to_owned(),
134                    errors: Some(vec![FieldErr {
135                        resource: "Release".to_owned(),
136                        code: "custom".to_owned(),
137                        field: None,
138                        message: Some(
139                            "Published releases \
140                             must have a valid tag"
141                                .to_owned(),
142                        ),
143                        documentation_url: None,
144                    }]),
145                    documentation_url: None,
146                },
147            ),
148        ] {
149            assert_eq!(serde_json::from_str::<ClientError>(json).unwrap(), expect);
150        }
151    }
152
153    #[test]
154    fn deserialize_client_top_level_documentation_url() {
155        let json = serde_json::json!({
156            "message": "Not Found",
157            "documentation_url": "https://developer.github.com/v3/activity/watching/#set-a-repository-subscription"
158        });
159        let expect = ClientError {
160            message: String::from("Not Found"),
161            errors: None,
162            documentation_url: Some(String::from(
163                "https://developer.github.com/v3/activity/watching/#set-a-repository-subscription",
164            )),
165        };
166        assert_eq!(serde_json::from_value::<ClientError>(json).unwrap(), expect)
167    }
168}