Skip to main content

rust_ynab/ynab/
errors.rs

1use serde::{Deserialize, Serialize};
2
3#[derive(Deserialize)]
4pub(crate) struct ErrorResponse {
5    pub(crate) error: ApiError,
6}
7
8#[derive(Debug, Deserialize, Serialize)]
9pub struct ApiError {
10    pub id: String,
11    pub name: String,
12    pub detail: String,
13}
14
15impl std::fmt::Display for ApiError {
16    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
17        write!(f, "({}) {} - {}", self.id, self.name, self.detail)
18    }
19}
20
21#[derive(Debug, thiserror::Error)]
22pub enum Error {
23    #[error("bad request: {0}")]
24    BadRequest(ApiError),
25    #[error("internal server error: {0}")]
26    InternalServerError(ApiError),
27    #[error("unauthorized: {0}")]
28    Unauthorized(ApiError),
29    #[error("rate limited: {0}")]
30    RateLimited(ApiError),
31    #[error("not found: {0}")]
32    NotFound(ApiError),
33    #[error("forbidden: {0}")]
34    Forbidden(ApiError),
35    #[error("conflict: {0}")]
36    Conflict(ApiError),
37    #[error("service unavailable: {0}")]
38    ServiceUnavailable(ApiError),
39    #[error("unknown error: {0}")]
40    UnknownError(ApiError),
41    #[error(transparent)]
42    Reqwest(#[from] reqwest::Error),
43    #[error(transparent)]
44    ParseError(#[from] url::ParseError),
45    #[error("invalid rate limit configuration: {0}")]
46    InvalidRateLimit(String),
47}
48
49impl Error {
50    pub fn new_api_error(status: reqwest::StatusCode, api_error: ApiError) -> Self {
51        match status {
52            reqwest::StatusCode::BAD_REQUEST => Error::BadRequest(api_error),
53            reqwest::StatusCode::INTERNAL_SERVER_ERROR => Error::InternalServerError(api_error),
54            reqwest::StatusCode::UNAUTHORIZED => Error::Unauthorized(api_error),
55            reqwest::StatusCode::TOO_MANY_REQUESTS => Error::RateLimited(api_error),
56            reqwest::StatusCode::NOT_FOUND => Error::NotFound(api_error),
57            reqwest::StatusCode::FORBIDDEN => Error::Forbidden(api_error),
58            reqwest::StatusCode::CONFLICT => Error::Conflict(api_error),
59            reqwest::StatusCode::SERVICE_UNAVAILABLE => Error::ServiceUnavailable(api_error),
60            _ => Error::UnknownError(api_error),
61        }
62    }
63}
64
65#[cfg(test)]
66mod tests {
67    use super::*;
68    use reqwest::StatusCode;
69
70    fn api_error() -> ApiError {
71        ApiError {
72            id: "test".into(),
73            name: "test_error".into(),
74            detail: "something went
75  wrong"
76                .into(),
77        }
78    }
79
80    #[test]
81    fn maps_status_codes_to_error_variants() {
82        let cases: &[(StatusCode, fn(&Error) -> bool)] = &[
83            (StatusCode::BAD_REQUEST, |e| {
84                matches!(e, Error::BadRequest(_))
85            }),
86            (StatusCode::UNAUTHORIZED, |e| {
87                matches!(e, Error::Unauthorized(_))
88            }),
89            (StatusCode::FORBIDDEN, |e| matches!(e, Error::Forbidden(_))),
90            (StatusCode::NOT_FOUND, |e| matches!(e, Error::NotFound(_))),
91            (StatusCode::CONFLICT, |e| matches!(e, Error::Conflict(_))),
92            (StatusCode::TOO_MANY_REQUESTS, |e| {
93                matches!(e, Error::RateLimited(_))
94            }),
95            (StatusCode::INTERNAL_SERVER_ERROR, |e| {
96                matches!(e, Error::InternalServerError(_))
97            }),
98            (StatusCode::SERVICE_UNAVAILABLE, |e| {
99                matches!(e, Error::ServiceUnavailable(_))
100            }),
101            (StatusCode::IM_A_TEAPOT, |e| {
102                matches!(e, Error::UnknownError(_))
103            }),
104        ];
105
106        for (status, check) in cases {
107            let err = Error::new_api_error(*status, api_error());
108            assert!(check(&err), "wrong variant for status {status}");
109        }
110    }
111}