Skip to main content

wme_client/
error.rs

1use thiserror::Error;
2
3/// Errors that can occur in the client.
4#[derive(Error, Debug, Clone)]
5pub enum ClientError {
6    /// HTTP request failed.
7    #[error("HTTP error: {0}")]
8    Http(String),
9
10    /// JSON parsing error.
11    #[error("JSON parse error: {0}")]
12    JsonParse(String),
13
14    /// Authentication error.
15    #[error("Authentication error: {0}")]
16    Auth(String),
17
18    /// Token expired.
19    #[error("Token expired")]
20    TokenExpired,
21
22    /// Snapshot not found.
23    #[error("Snapshot not found: {id}")]
24    SnapshotNotFound { id: String },
25
26    /// Article not found.
27    #[error("Article not found: {name}")]
28    ArticleNotFound { name: String },
29
30    /// Rate limited.
31    #[error("Rate limited")]
32    RateLimited {
33        /// Seconds to wait before retry
34        retry_after: Option<u64>,
35    },
36
37    /// Invalid configuration.
38    #[error("Invalid configuration: {0}")]
39    Config(String),
40
41    /// Network error.
42    #[error("Network error: {0}")]
43    Network(String),
44
45    /// IO error.
46    #[error("IO error: {0}")]
47    Io(String),
48
49    /// Streaming error.
50    #[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}