1use std::fmt;
4
5use reqwest::StatusCode;
6use serde::Deserialize;
7
8#[derive(Debug)]
10pub enum Error {
11 Http(reqwest::Error),
13 Api(ApiError),
15 RateLimit(RateLimitError),
17 Serialization(serde_json::Error),
19}
20
21#[derive(Debug, Clone)]
23pub struct ApiError {
24 pub status_code: StatusCode,
26 pub message: String,
28 pub documentation_url: String,
30 pub request_id: String,
32}
33
34#[derive(Debug, Clone)]
36pub struct RateLimitError {
37 pub api_error: ApiError,
39 pub limit: i64,
41 pub remaining: i64,
43 pub reset: i64,
45 pub retry_after: i64,
47}
48
49#[derive(Deserialize)]
50struct ApiErrorResponse {
51 #[serde(default)]
52 message: String,
53 #[serde(default)]
54 documentation_url: String,
55}
56
57impl fmt::Display for Error {
58 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
59 match self {
60 Error::Http(e) => write!(f, "HTTP error: {e}"),
61 Error::Api(e) => write!(f, "{e}"),
62 Error::RateLimit(e) => write!(f, "{e}"),
63 Error::Serialization(e) => write!(f, "serialization error: {e}"),
64 }
65 }
66}
67
68impl std::error::Error for Error {
69 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
70 match self {
71 Error::Http(e) => Some(e),
72 Error::Serialization(e) => Some(e),
73 _ => None,
74 }
75 }
76}
77
78impl fmt::Display for ApiError {
79 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
80 if self.message.is_empty() {
81 write!(f, "GitVerse API error (status {})", self.status_code)
82 } else {
83 write!(
84 f,
85 "GitVerse API error (status {}): {}",
86 self.status_code, self.message
87 )
88 }
89 }
90}
91
92impl fmt::Display for RateLimitError {
93 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
94 write!(
95 f,
96 "rate limit exceeded: retry after {} seconds",
97 self.retry_after
98 )
99 }
100}
101
102impl From<reqwest::Error> for Error {
103 fn from(err: reqwest::Error) -> Self {
104 Error::Http(err)
105 }
106}
107
108impl From<serde_json::Error> for Error {
109 fn from(err: serde_json::Error) -> Self {
110 Error::Serialization(err)
111 }
112}
113
114pub fn is_not_found(err: &Error) -> bool {
116 matches!(err, Error::Api(e) if e.status_code == StatusCode::NOT_FOUND)
117}
118
119pub fn is_rate_limit_error(err: &Error) -> bool {
121 matches!(err, Error::RateLimit(_))
122}
123
124pub fn is_unauthorized(err: &Error) -> bool {
126 matches!(err, Error::Api(e) if e.status_code == StatusCode::UNAUTHORIZED)
127}
128
129pub fn is_forbidden(err: &Error) -> bool {
131 matches!(err, Error::Api(e) if e.status_code == StatusCode::FORBIDDEN)
132}
133
134pub(crate) async fn parse_api_error(resp: reqwest::Response) -> Error {
135 let status_code = resp.status();
136 let headers = resp.headers().clone();
137
138 let request_id = headers
139 .get("X-Request-Id")
140 .and_then(|v| v.to_str().ok())
141 .unwrap_or("")
142 .to_string();
143
144 let body = match resp.text().await {
145 Ok(text) => text,
146 Err(e) => format!("(failed to read response body: {e})"),
147 };
148
149 let (message, documentation_url) = match serde_json::from_str::<ApiErrorResponse>(&body) {
150 Ok(err_resp) => (err_resp.message, err_resp.documentation_url),
151 Err(_) => (body, String::new()),
152 };
153
154 let api_error = ApiError {
155 status_code,
156 message,
157 documentation_url,
158 request_id,
159 };
160
161 if status_code == StatusCode::TOO_MANY_REQUESTS {
162 let limit = headers
163 .get("GitVerse-RateLimit-Limit")
164 .and_then(|v| v.to_str().ok())
165 .and_then(|v| v.parse().ok())
166 .unwrap_or(0);
167
168 let remaining = headers
169 .get("GitVerse-RateLimit-User-Remaining")
170 .or_else(|| headers.get("GitVerse-RateLimit-Remaining"))
171 .and_then(|v| v.to_str().ok())
172 .and_then(|v| v.parse().ok())
173 .unwrap_or(0);
174
175 let reset = headers
176 .get("Gitverse-Ratelimit-Reset")
177 .and_then(|v| v.to_str().ok())
178 .and_then(|v| v.parse().ok())
179 .unwrap_or(0);
180
181 let retry_after = headers
182 .get("GitVerse-RateLimit-Retry-After")
183 .and_then(|v| v.to_str().ok())
184 .and_then(|v| v.parse().ok())
185 .unwrap_or(60);
186
187 return Error::RateLimit(RateLimitError {
188 api_error,
189 limit,
190 remaining,
191 reset,
192 retry_after,
193 });
194 }
195
196 Error::Api(api_error)
197}