1use thiserror::Error;
4
5#[derive(Error, Debug)]
6pub enum GrrError {
7 #[error("Authentication error: {0}")]
8 Auth(Box<dyn std::error::Error + Send + Sync>),
9
10 #[error("HTTP request error: {0}")]
11 Http(#[from] reqwest::Error),
12
13 #[error("JSON serialization error: {0}")]
14 Json(#[from] serde_json::Error),
15
16 #[error("IO error: {0}")]
17 Io(#[from] std::io::Error),
18
19 #[error("URL parse error: {0}")]
20 Url(#[from] url::ParseError),
21
22 #[error("Configuration error: {0}")]
23 Config(String),
24
25 #[error("Rate limited: retry after {retry_after_secs}s")]
26 RateLimited { retry_after_secs: u64 },
27
28 #[error("Internal error: {0}")]
29 Internal(String),
30
31 #[error("Timeout: {0}")]
32 Timeout(String),
33
34 #[error(transparent)]
35 Anyhow(#[from] anyhow::Error),
36}
37
38pub type Result<T, E = GrrError> = std::result::Result<T, E>;
39
40pub(crate) fn json_error_detail(body: &serde_json::Value) -> String {
41 let reason = body
42 .get("error")
43 .and_then(|value| {
44 value
45 .as_str()
46 .or_else(|| value.get("message").and_then(serde_json::Value::as_str))
47 })
48 .or_else(|| body.get("message").and_then(serde_json::Value::as_str))
49 .unwrap_or("unknown error");
50 let description = body
51 .get("error_description")
52 .and_then(serde_json::Value::as_str)
53 .or_else(|| body.get("description").and_then(serde_json::Value::as_str))
54 .unwrap_or("");
55 if description.is_empty() || description == reason {
56 reason.to_string()
57 } else {
58 format!("{reason}: {description}")
59 }
60}
61
62#[cfg(test)]
63mod tests {
64 use super::*;
65 use serde_json::json;
66
67 #[test]
68 fn extracts_oauth_error_details() {
69 let value = json!({"error":"invalid_grant", "error_description":"expired"});
70 assert_eq!(json_error_detail(&value), "invalid_grant: expired");
71 }
72}