1use std::fmt;
4
5pub type Result<T> = std::result::Result<T, Error>;
7
8#[derive(Debug)]
10pub enum Error {
11 InvalidUrl {
13 url: String,
15 reason: String,
17 },
18
19 ConnectionFailed {
21 host: String,
23 source: std::io::Error,
25 },
26
27 ConnectionTimeout {
29 host: String,
31 },
32
33 TooManyConnections {
35 host: String,
37 max: usize,
39 },
40
41 Timeout {
43 duration: std::time::Duration,
45 },
46
47 InvalidMethod {
49 method: String,
51 },
52
53 InvalidHeader {
55 name: String,
57 value: String,
59 },
60
61 StatusError {
63 status: u16,
65 body: String,
67 },
68
69 BodyReadError {
71 source: std::io::Error,
73 },
74
75 JsonError {
77 source: String,
79 },
80
81 InvalidConfig {
83 message: String,
85 },
86
87 AuthError {
89 message: String,
91 },
92
93 Internal {
95 message: String,
97 },
98}
99
100impl fmt::Display for Error {
101 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
102 match self {
103 Error::InvalidUrl { url, reason } => {
104 write!(f, "Invalid URL '{}': {}", url, reason)
105 }
106 Error::ConnectionFailed { host, source } => {
107 write!(f, "Failed to connect to {}: {}", host, source)
108 }
109 Error::ConnectionTimeout { host } => {
110 write!(f, "Connection to {} timed out", host)
111 }
112 Error::TooManyConnections { host, max } => {
113 write!(f, "Too many connections to {} (max: {})", host, max)
114 }
115 Error::Timeout { duration } => {
116 write!(f, "Request timed out after {:?}", duration)
117 }
118 Error::InvalidMethod { method } => {
119 write!(f, "Invalid HTTP method: {}", method)
120 }
121 Error::InvalidHeader { name, value } => {
122 write!(f, "Invalid header '{}': {}", name, value)
123 }
124 Error::StatusError { status, body } => {
125 write!(f, "HTTP error {}: {}", status, body)
126 }
127 Error::BodyReadError { source } => {
128 write!(f, "Failed to read response body: {}", source)
129 }
130 Error::JsonError { source } => {
131 write!(f, "JSON error: {}", source)
132 }
133 Error::InvalidConfig { message } => {
134 write!(f, "Invalid configuration: {}", message)
135 }
136 Error::AuthError { message } => {
137 write!(f, "Authentication error: {}", message)
138 }
139 Error::Internal { message } => {
140 write!(f, "Internal error: {}", message)
141 }
142 }
143 }
144}
145
146impl std::error::Error for Error {
147 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
148 match self {
149 Error::ConnectionFailed { source, .. } => Some(source),
150 Error::BodyReadError { source } => Some(source),
151 _ => None,
152 }
153 }
154}
155
156impl From<std::io::Error> for Error {
157 fn from(err: std::io::Error) -> Self {
158 Error::BodyReadError { source: err }
159 }
160}
161
162#[cfg(test)]
163mod tests {
164 use super::*;
165
166 #[test]
167 fn test_error_display() {
168 let err = Error::InvalidUrl {
169 url: "not-a-url".to_string(),
170 reason: "missing scheme".to_string(),
171 };
172 assert!(err.to_string().contains("Invalid URL"));
173 assert!(err.to_string().contains("not-a-url"));
174 }
175
176 #[test]
177 fn test_error_from_io() {
178 let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
179 let err: Error = io_err.into();
180 assert!(matches!(err, Error::BodyReadError { .. }));
181 }
182
183 #[test]
184 fn test_timeout_error() {
185 let err = Error::Timeout {
186 duration: std::time::Duration::from_secs(30),
187 };
188 assert!(err.to_string().contains("timed out"));
189 assert!(err.to_string().contains("30s"));
190 }
191}