1use std::time::Duration;
2
3use crate::resource::{ErrorResponse, OAuth2ErrorResponse};
4use reqwest::StatusCode;
5use thiserror::Error;
6
7pub type Result<T> = std::result::Result<T, Error>;
11
12#[derive(Debug, Error)]
14#[error(transparent)]
15pub struct Error {
16 inner: Box<ErrorKind>,
17}
18
19#[derive(Debug, Error)]
20enum ErrorKind {
21 #[error("Request error: {0}")]
23 RequestError(#[source] reqwest::Error),
24 #[error("Unexpected response: {reason}")]
25 UnexpectedResponse { reason: &'static str },
26 #[error("Api error with {status}: ({}) {}", .response.code, .response.message)]
27 ErrorResponse {
28 status: StatusCode,
29 response: ErrorResponse,
30 retry_after: Option<u32>,
31 },
32 #[error("OAuth2 error with {status}: ({}) {}", .response.error, .response.error_description)]
33 OAuth2Error {
34 status: StatusCode,
35 response: OAuth2ErrorResponse,
36 retry_after: Option<u32>,
37 },
38}
39
40impl Error {
41 pub(crate) fn from_error_response(
42 status: StatusCode,
43 response: ErrorResponse,
44 retry_after: Option<u32>,
45 ) -> Self {
46 Self {
47 inner: Box::new(ErrorKind::ErrorResponse {
48 status,
49 response,
50 retry_after,
51 }),
52 }
53 }
54
55 pub(crate) fn unexpected_response(reason: &'static str) -> Self {
56 Self {
57 inner: Box::new(ErrorKind::UnexpectedResponse { reason }),
58 }
59 }
60
61 pub(crate) fn from_oauth2_error_response(
62 status: StatusCode,
63 response: OAuth2ErrorResponse,
64 retry_after: Option<u32>,
65 ) -> Self {
66 Self {
67 inner: Box::new(ErrorKind::OAuth2Error {
68 status,
69 response,
70 retry_after,
71 }),
72 }
73 }
74
75 #[must_use]
77 pub fn error_response(&self) -> Option<&ErrorResponse> {
78 match &*self.inner {
79 ErrorKind::ErrorResponse { response, .. } => Some(response),
80 _ => None,
81 }
82 }
83
84 #[must_use]
86 pub fn oauth2_error_response(&self) -> Option<&OAuth2ErrorResponse> {
87 match &*self.inner {
88 ErrorKind::OAuth2Error { response, .. } => Some(response),
89 _ => None,
90 }
91 }
92
93 #[must_use]
95 pub fn status_code(&self) -> Option<StatusCode> {
96 match &*self.inner {
97 ErrorKind::RequestError(source) => source.status(),
98 ErrorKind::UnexpectedResponse { .. } => None,
99 ErrorKind::ErrorResponse { status, .. } | ErrorKind::OAuth2Error { status, .. } => {
100 Some(*status)
101 }
102 }
103 }
104
105 #[must_use]
110 pub fn retry_after(&self) -> Option<Duration> {
111 match &*self.inner {
112 ErrorKind::ErrorResponse { retry_after, .. }
113 | ErrorKind::OAuth2Error { retry_after, .. } => {
114 Some(Duration::from_secs((*retry_after)?.into()))
115 }
116 _ => None,
117 }
118 }
119}
120
121impl From<reqwest::Error> for Error {
122 fn from(source: reqwest::Error) -> Self {
123 Self {
124 inner: Box::new(ErrorKind::RequestError(source)),
125 }
126 }
127}
128
129#[cfg(test)]
130mod tests {
131 use std::error::Error as _;
132
133 use super::*;
134
135 #[test]
136 fn error_source() {
137 let err = reqwest::blocking::get("urn:urn").unwrap_err();
138 let original_err_fmt = err.to_string();
139 let source_err_fmt = Error::from(err).source().unwrap().to_string();
140 assert_eq!(source_err_fmt, original_err_fmt);
141 }
142}