1use thiserror::Error;
2
3#[derive(Error, Debug, Clone)]
5pub enum ClientError {
6 #[error("HTTP error: {0}")]
8 Http(String),
9
10 #[error("JSON parse error: {0}")]
12 JsonParse(String),
13
14 #[error("Authentication error: {0}")]
16 Auth(String),
17
18 #[error("Token expired")]
20 TokenExpired,
21
22 #[error("Snapshot not found: {id}")]
24 SnapshotNotFound { id: String },
25
26 #[error("Article not found: {name}")]
28 ArticleNotFound { name: String },
29
30 #[error("Rate limited")]
32 RateLimited {
33 retry_after: Option<u64>,
35 },
36
37 #[error("Invalid configuration: {0}")]
39 Config(String),
40
41 #[error("Network error: {0}")]
43 Network(String),
44
45 #[error("IO error: {0}")]
47 Io(String),
48
49 #[error("Streaming error: {0}")]
51 Stream(String),
52}
53
54impl From<reqwest::Error> for ClientError {
55 fn from(e: reqwest::Error) -> Self {
56 if e.is_timeout() {
57 ClientError::Network(format!("Request timeout: {e}"))
58 } else if e.is_connect() {
59 ClientError::Network(format!("Connection error: {e}"))
60 } else {
61 ClientError::Http(e.to_string())
62 }
63 }
64}
65
66impl From<serde_json::Error> for ClientError {
67 fn from(e: serde_json::Error) -> Self {
68 ClientError::JsonParse(e.to_string())
69 }
70}
71
72impl From<std::io::Error> for ClientError {
73 fn from(e: std::io::Error) -> Self {
74 ClientError::Io(e.to_string())
75 }
76}
77
78#[cfg(test)]
79mod tests {
80 use super::*;
81
82 #[test]
83 fn test_error_display() {
84 let err = ClientError::Http("connection refused".to_string());
85 assert_eq!(err.to_string(), "HTTP error: connection refused");
86
87 let err = ClientError::Auth("invalid credentials".to_string());
88 assert_eq!(err.to_string(), "Authentication error: invalid credentials");
89
90 let err = ClientError::TokenExpired;
91 assert_eq!(err.to_string(), "Token expired");
92
93 let err = ClientError::SnapshotNotFound {
94 id: "snap-123".to_string(),
95 };
96 assert_eq!(err.to_string(), "Snapshot not found: snap-123");
97
98 let err = ClientError::ArticleNotFound {
99 name: "Test Article".to_string(),
100 };
101 assert_eq!(err.to_string(), "Article not found: Test Article");
102
103 let err = ClientError::RateLimited {
104 retry_after: Some(60),
105 };
106 assert_eq!(err.to_string(), "Rate limited");
107
108 let err = ClientError::Config("invalid timeout".to_string());
109 assert_eq!(err.to_string(), "Invalid configuration: invalid timeout");
110
111 let err = ClientError::Network("dns failed".to_string());
112 assert_eq!(err.to_string(), "Network error: dns failed");
113
114 let err = ClientError::Io("file not found".to_string());
115 assert_eq!(err.to_string(), "IO error: file not found");
116
117 let err = ClientError::Stream("unexpected EOF".to_string());
118 assert_eq!(err.to_string(), "Streaming error: unexpected EOF");
119 }
120
121 #[test]
122 fn test_error_clone() {
123 let err = ClientError::Auth("test".to_string());
124 let cloned = err.clone();
125 assert_eq!(err.to_string(), cloned.to_string());
126 }
127
128 #[test]
129 fn test_error_debug() {
130 let err = ClientError::Http("test".to_string());
131 let debug = format!("{:?}", err);
132 assert!(debug.contains("Http"));
133 assert!(debug.contains("test"));
134 }
135
136 #[test]
137 fn test_from_io_error() {
138 let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
139 let client_err: ClientError = io_err.into();
140 assert!(matches!(client_err, ClientError::Io(_)));
141 assert!(client_err.to_string().contains("file not found"));
142 }
143
144 #[test]
145 fn test_rate_limited_with_retry_after() {
146 let err = ClientError::RateLimited {
147 retry_after: Some(60),
148 };
149 assert!(matches!(
150 err,
151 ClientError::RateLimited {
152 retry_after: Some(60)
153 }
154 ));
155 }
156
157 #[test]
158 fn test_rate_limited_without_retry_after() {
159 let err = ClientError::RateLimited { retry_after: None };
160 assert!(matches!(
161 err,
162 ClientError::RateLimited { retry_after: None }
163 ));
164 }
165}