1use std::sync::Arc;
2use thiserror::Error;
3
4#[derive(Debug, Clone, Error)]
6#[non_exhaustive]
7pub enum CamelError {
8 #[error("Component not found: {0}")]
9 ComponentNotFound(String),
10
11 #[error("Endpoint creation failed: {0}")]
12 EndpointCreationFailed(String),
13
14 #[error("Processor error: {0}")]
15 ProcessorError(String),
16
17 #[error("Processor error: {0}")]
20 ProcessorErrorWithSource(String, #[source] Arc<dyn std::error::Error + Send + Sync>),
21
22 #[error("Type conversion failed: {0}")]
23 TypeConversionFailed(String),
24
25 #[error("Invalid URI: {0}")]
26 InvalidUri(String),
27
28 #[error("Channel closed")]
29 ChannelClosed,
30
31 #[error("Route error: {0}")]
32 RouteError(String),
33
34 #[error("IO error: {0}")]
35 Io(String),
36
37 #[error("Dead letter channel failed: {0}")]
38 DeadLetterChannelFailed(String),
39
40 #[error("Circuit breaker open: {0}")]
41 CircuitOpen(String),
42
43 #[error("HTTP {method} {url} failed: {status_code} {status_text}")]
44 HttpOperationFailed {
45 method: String,
46 url: String,
47 status_code: u16,
48 status_text: String,
49 response_body: Option<String>,
50 },
51
52 #[error("Consumer stopping: semaphore closed during poll_ready")]
56 ConsumerStopping,
57
58 #[error("Configuration error: {0}")]
59 Config(String),
60
61 #[error("Body stream has already been consumed")]
62 AlreadyConsumed,
63
64 #[error("Stream size exceeded limit: {0}")]
65 StreamLimitExceeded(usize),
66
67 #[error("Unauthenticated: {0}")]
68 Unauthenticated(String),
69
70 #[error("Unauthorized: {0}")]
71 Unauthorized(String),
72}
73
74impl CamelError {
75 pub fn classify(&self) -> &'static str {
76 #[allow(unreachable_patterns)]
77 match self {
78 Self::ComponentNotFound(_) => "component",
79 Self::EndpointCreationFailed(_) | Self::InvalidUri(_) => "endpoint",
80 Self::ProcessorError(_) | Self::ProcessorErrorWithSource(_, _) => "processor",
81 Self::TypeConversionFailed(_) | Self::AlreadyConsumed => "type_conversion",
82 Self::Io(_) => "io",
83 Self::RouteError(_) => "route",
84 Self::CircuitOpen(_) => "circuit_open",
85 Self::HttpOperationFailed { .. } => "http",
86 Self::Config(_) => "config",
87 Self::DeadLetterChannelFailed(_) => "dead_letter",
88 Self::ConsumerStopping => "consumer_stop",
89 Self::StreamLimitExceeded(_) => "stream",
90 Self::ChannelClosed => "channel",
91 Self::Unauthenticated(_) => "unauthenticated",
92 Self::Unauthorized(_) => "unauthorized",
93 _ => "unknown",
94 }
95 }
96
97 pub fn variant_name(&self) -> &'static str {
106 match self {
107 Self::ComponentNotFound(_) => "ComponentNotFound",
108 Self::EndpointCreationFailed(_) => "EndpointCreationFailed",
109 Self::ProcessorError(_) => "ProcessorError",
110 Self::ProcessorErrorWithSource(_, _) => "ProcessorError",
111 Self::TypeConversionFailed(_) => "TypeConversionFailed",
112 Self::InvalidUri(_) => "InvalidUri",
113 Self::ChannelClosed => "ChannelClosed",
114 Self::RouteError(_) => "RouteError",
115 Self::Io(_) => "Io",
116 Self::DeadLetterChannelFailed(_) => "DeadLetterChannelFailed",
117 Self::CircuitOpen(_) => "CircuitOpen",
118 Self::HttpOperationFailed { .. } => "HttpOperationFailed",
119 Self::ConsumerStopping => "ConsumerStopping",
120 Self::Config(_) => "Config",
121 Self::AlreadyConsumed => "AlreadyConsumed",
122 Self::StreamLimitExceeded(_) => "StreamLimitExceeded",
123 Self::Unauthenticated(_) => "Unauthenticated",
124 Self::Unauthorized(_) => "Unauthorized",
125 }
126 }
127}
128
129impl From<std::io::Error> for CamelError {
130 fn from(err: std::io::Error) -> Self {
131 CamelError::Io(err.to_string())
132 }
133}
134
135impl From<crate::template::TemplateError> for CamelError {
136 fn from(err: crate::template::TemplateError) -> Self {
137 CamelError::Config(err.to_string())
138 }
139}
140
141#[cfg(test)]
142mod tests {
143 use super::*;
144
145 fn all_error_samples() -> Vec<CamelError> {
146 vec![
147 CamelError::ComponentNotFound("x".to_string()),
148 CamelError::EndpointCreationFailed("x".to_string()),
149 CamelError::ProcessorError("x".to_string()),
150 CamelError::ProcessorErrorWithSource(
151 "x".to_string(),
152 Arc::new(std::io::Error::new(std::io::ErrorKind::Other, "inner")),
153 ),
154 CamelError::TypeConversionFailed("x".to_string()),
155 CamelError::InvalidUri("x".to_string()),
156 CamelError::ChannelClosed,
157 CamelError::RouteError("x".to_string()),
158 CamelError::Io("x".to_string()),
159 CamelError::DeadLetterChannelFailed("x".to_string()),
160 CamelError::CircuitOpen("x".to_string()),
161 CamelError::HttpOperationFailed {
162 method: "GET".to_string(),
163 url: "https://example.com".to_string(),
164 status_code: 500,
165 status_text: "Internal Server Error".to_string(),
166 response_body: Some("error".to_string()),
167 },
168 CamelError::ConsumerStopping,
169 CamelError::Config("x".to_string()),
170 CamelError::AlreadyConsumed,
171 CamelError::StreamLimitExceeded(42),
172 CamelError::Unauthenticated("token expired".to_string()),
173 CamelError::Unauthorized("missing admin role".to_string()),
174 ]
175 }
176
177 #[test]
178 fn test_http_operation_failed_display() {
179 let err = CamelError::HttpOperationFailed {
180 method: "GET".to_string(),
181 url: "https://example.com/test".to_string(),
182 status_code: 404,
183 status_text: "Not Found".to_string(),
184 response_body: Some("page not found".to_string()),
185 };
186 let msg = format!("{err}");
187 assert!(msg.contains("404"));
188 assert!(msg.contains("Not Found"));
189 }
190
191 #[test]
192 fn test_http_operation_failed_clone() {
193 let err = CamelError::HttpOperationFailed {
194 method: "POST".to_string(),
195 url: "https://api.example.com/users".to_string(),
196 status_code: 500,
197 status_text: "Internal Server Error".to_string(),
198 response_body: None,
199 };
200 let cloned = err.clone();
201 assert!(matches!(
202 cloned,
203 CamelError::HttpOperationFailed {
204 status_code: 500,
205 ..
206 }
207 ));
208 }
209
210 #[test]
211 fn test_classify_maps_all_variants() {
212 assert_eq!(
213 CamelError::ComponentNotFound("x".to_string()).classify(),
214 "component"
215 );
216 assert_eq!(
217 CamelError::EndpointCreationFailed("x".to_string()).classify(),
218 "endpoint"
219 );
220 assert_eq!(
221 CamelError::ProcessorError("x".to_string()).classify(),
222 "processor"
223 );
224 assert_eq!(
225 CamelError::TypeConversionFailed("x".to_string()).classify(),
226 "type_conversion"
227 );
228 assert_eq!(
229 CamelError::InvalidUri("x".to_string()).classify(),
230 "endpoint"
231 );
232 assert_eq!(CamelError::ChannelClosed.classify(), "channel");
233 assert_eq!(CamelError::RouteError("x".to_string()).classify(), "route");
234 assert_eq!(CamelError::Io("x".to_string()).classify(), "io");
235 assert_eq!(
236 CamelError::DeadLetterChannelFailed("x".to_string()).classify(),
237 "dead_letter"
238 );
239 assert_eq!(
240 CamelError::CircuitOpen("x".to_string()).classify(),
241 "circuit_open"
242 );
243 assert_eq!(
244 CamelError::HttpOperationFailed {
245 method: "GET".to_string(),
246 url: "https://example.com".to_string(),
247 status_code: 500,
248 status_text: "Internal Server Error".to_string(),
249 response_body: None,
250 }
251 .classify(),
252 "http"
253 );
254 assert_eq!(CamelError::Config("x".to_string()).classify(), "config");
255 assert_eq!(CamelError::AlreadyConsumed.classify(), "type_conversion");
256 assert_eq!(CamelError::StreamLimitExceeded(42).classify(), "stream");
257 }
258
259 #[test]
260 fn test_classify_output_is_ascii_and_short() {
261 for error in all_error_samples() {
262 let class = error.classify();
263 assert!(class.is_ascii());
264 assert!(class.len() <= 15, "class too long: {class}");
265 }
266 }
267
268 #[test]
269 fn test_auth_variants_classify() {
270 assert_eq!(
271 CamelError::Unauthenticated("x".to_string()).classify(),
272 "unauthenticated"
273 );
274 assert_eq!(
275 CamelError::Unauthorized("x".to_string()).classify(),
276 "unauthorized"
277 );
278 }
279
280 #[test]
281 fn test_auth_variants_are_clone() {
282 let err = CamelError::Unauthenticated("test".to_string());
283 let cloned = err.clone();
284 assert!(matches!(cloned, CamelError::Unauthenticated(_)));
285
286 let err2 = CamelError::Unauthorized("test".to_string());
287 let cloned2 = err2.clone();
288 assert!(matches!(cloned2, CamelError::Unauthorized(_)));
289 }
290}
291
292#[cfg(test)]
293mod variant_name_tests {
294 use super::CamelError;
295 use std::sync::Arc;
296
297 #[test]
302 fn variant_name_covers_all_variants() {
303 let cases: Vec<(CamelError, &str)> = vec![
304 (
305 CamelError::ComponentNotFound("x".into()),
306 "ComponentNotFound",
307 ),
308 (
309 CamelError::EndpointCreationFailed("x".into()),
310 "EndpointCreationFailed",
311 ),
312 (CamelError::ProcessorError("x".into()), "ProcessorError"),
313 (
314 CamelError::ProcessorErrorWithSource(
315 "x".into(),
316 Arc::new(std::io::Error::new(std::io::ErrorKind::Other, "y")),
317 ),
318 "ProcessorError", ),
320 (
321 CamelError::TypeConversionFailed("x".into()),
322 "TypeConversionFailed",
323 ),
324 (CamelError::InvalidUri("x".into()), "InvalidUri"),
325 (CamelError::ChannelClosed, "ChannelClosed"),
326 (CamelError::RouteError("x".into()), "RouteError"),
327 (CamelError::Io("x".into()), "Io"),
328 (
329 CamelError::DeadLetterChannelFailed("x".into()),
330 "DeadLetterChannelFailed",
331 ),
332 (CamelError::CircuitOpen("x".into()), "CircuitOpen"),
333 (
334 CamelError::HttpOperationFailed {
335 method: "GET".into(),
336 url: "https://example.com".into(),
337 status_code: 500,
338 status_text: "Internal Server Error".into(),
339 response_body: None,
340 },
341 "HttpOperationFailed",
342 ),
343 (CamelError::Config("x".into()), "Config"),
344 (CamelError::AlreadyConsumed, "AlreadyConsumed"),
345 (CamelError::StreamLimitExceeded(42), "StreamLimitExceeded"),
346 (CamelError::Unauthenticated("x".into()), "Unauthenticated"),
347 (CamelError::Unauthorized("x".into()), "Unauthorized"),
348 ];
349
350 for (err, expected) in cases {
351 assert_eq!(
352 err.variant_name(),
353 expected,
354 "variant_name mismatch for {:?}",
355 err
356 );
357 }
358 }
359}