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}