1use 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
14pub type Result<T> = StdResult<T, Error>;
16
17#[derive(Debug)]
18pub enum Error {
19 Fault {
21 code: StatusCode,
22 error: ClientError,
23 },
24 RateLimit { reset: Duration },
26 Codec(SerdeError),
28 Reqwest(ReqwestError),
30 Url(ParseError),
32 IO(IoError),
34 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#[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 (
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}