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("Exchange stopped by Stop EIP")]
53 Stopped,
54
55 #[error("Consumer stopping: semaphore closed during poll_ready")]
59 ConsumerStopping,
60
61 #[error("Configuration error: {0}")]
62 Config(String),
63
64 #[error("Body stream has already been consumed")]
65 AlreadyConsumed,
66
67 #[error("Stream size exceeded limit: {0}")]
68 StreamLimitExceeded(usize),
69
70 #[error("Unauthenticated: {0}")]
71 Unauthenticated(String),
72
73 #[error("Unauthorized: {0}")]
74 Unauthorized(String),
75}
76
77impl CamelError {
78 pub fn classify(&self) -> &'static str {
79 #[allow(unreachable_patterns)]
80 match self {
81 Self::ComponentNotFound(_) => "component",
82 Self::EndpointCreationFailed(_) | Self::InvalidUri(_) => "endpoint",
83 Self::ProcessorError(_) | Self::ProcessorErrorWithSource(_, _) => "processor",
84 Self::TypeConversionFailed(_) | Self::AlreadyConsumed => "type_conversion",
85 Self::Io(_) => "io",
86 Self::RouteError(_) => "route",
87 Self::CircuitOpen(_) => "circuit_open",
88 Self::HttpOperationFailed { .. } => "http",
89 Self::Config(_) => "config",
90 Self::DeadLetterChannelFailed(_) => "dead_letter",
91 Self::Stopped => "stopped",
92 Self::ConsumerStopping => "consumer_stop",
93 Self::StreamLimitExceeded(_) => "stream",
94 Self::ChannelClosed => "channel",
95 Self::Unauthenticated(_) => "unauthenticated",
96 Self::Unauthorized(_) => "unauthorized",
97 _ => "unknown",
98 }
99 }
100
101 pub fn variant_name(&self) -> &'static str {
110 match self {
111 Self::ComponentNotFound(_) => "ComponentNotFound",
112 Self::EndpointCreationFailed(_) => "EndpointCreationFailed",
113 Self::ProcessorError(_) => "ProcessorError",
114 Self::ProcessorErrorWithSource(_, _) => "ProcessorError",
115 Self::TypeConversionFailed(_) => "TypeConversionFailed",
116 Self::InvalidUri(_) => "InvalidUri",
117 Self::ChannelClosed => "ChannelClosed",
118 Self::RouteError(_) => "RouteError",
119 Self::Io(_) => "Io",
120 Self::DeadLetterChannelFailed(_) => "DeadLetterChannelFailed",
121 Self::CircuitOpen(_) => "CircuitOpen",
122 Self::HttpOperationFailed { .. } => "HttpOperationFailed",
123 Self::Stopped => "Stopped",
124 Self::ConsumerStopping => "ConsumerStopping",
125 Self::Config(_) => "Config",
126 Self::AlreadyConsumed => "AlreadyConsumed",
127 Self::StreamLimitExceeded(_) => "StreamLimitExceeded",
128 Self::Unauthenticated(_) => "Unauthenticated",
129 Self::Unauthorized(_) => "Unauthorized",
130 }
131 }
132}
133
134impl From<std::io::Error> for CamelError {
135 fn from(err: std::io::Error) -> Self {
136 CamelError::Io(err.to_string())
137 }
138}
139
140impl From<crate::template::TemplateError> for CamelError {
141 fn from(err: crate::template::TemplateError) -> Self {
142 CamelError::Config(err.to_string())
143 }
144}
145
146#[cfg(test)]
147mod tests {
148 use super::*;
149
150 fn all_error_samples() -> Vec<CamelError> {
151 vec![
152 CamelError::ComponentNotFound("x".to_string()),
153 CamelError::EndpointCreationFailed("x".to_string()),
154 CamelError::ProcessorError("x".to_string()),
155 CamelError::ProcessorErrorWithSource(
156 "x".to_string(),
157 Arc::new(std::io::Error::new(std::io::ErrorKind::Other, "inner")),
158 ),
159 CamelError::TypeConversionFailed("x".to_string()),
160 CamelError::InvalidUri("x".to_string()),
161 CamelError::ChannelClosed,
162 CamelError::RouteError("x".to_string()),
163 CamelError::Io("x".to_string()),
164 CamelError::DeadLetterChannelFailed("x".to_string()),
165 CamelError::CircuitOpen("x".to_string()),
166 CamelError::HttpOperationFailed {
167 method: "GET".to_string(),
168 url: "https://example.com".to_string(),
169 status_code: 500,
170 status_text: "Internal Server Error".to_string(),
171 response_body: Some("error".to_string()),
172 },
173 CamelError::Stopped,
174 CamelError::ConsumerStopping,
175 CamelError::Config("x".to_string()),
176 CamelError::AlreadyConsumed,
177 CamelError::StreamLimitExceeded(42),
178 CamelError::Unauthenticated("token expired".to_string()),
179 CamelError::Unauthorized("missing admin role".to_string()),
180 ]
181 }
182
183 #[test]
184 fn test_http_operation_failed_display() {
185 let err = CamelError::HttpOperationFailed {
186 method: "GET".to_string(),
187 url: "https://example.com/test".to_string(),
188 status_code: 404,
189 status_text: "Not Found".to_string(),
190 response_body: Some("page not found".to_string()),
191 };
192 let msg = format!("{err}");
193 assert!(msg.contains("404"));
194 assert!(msg.contains("Not Found"));
195 }
196
197 #[test]
198 fn test_http_operation_failed_clone() {
199 let err = CamelError::HttpOperationFailed {
200 method: "POST".to_string(),
201 url: "https://api.example.com/users".to_string(),
202 status_code: 500,
203 status_text: "Internal Server Error".to_string(),
204 response_body: None,
205 };
206 let cloned = err.clone();
207 assert!(matches!(
208 cloned,
209 CamelError::HttpOperationFailed {
210 status_code: 500,
211 ..
212 }
213 ));
214 }
215
216 #[test]
217 fn test_stopped_variant_exists_and_is_clone() {
218 let err = CamelError::Stopped;
219 let cloned = err.clone();
220 assert!(matches!(cloned, CamelError::Stopped));
221 assert_eq!(format!("{err}"), "Exchange stopped by Stop EIP");
222 }
223
224 #[test]
225 fn test_classify_maps_all_variants() {
226 assert_eq!(
227 CamelError::ComponentNotFound("x".to_string()).classify(),
228 "component"
229 );
230 assert_eq!(
231 CamelError::EndpointCreationFailed("x".to_string()).classify(),
232 "endpoint"
233 );
234 assert_eq!(
235 CamelError::ProcessorError("x".to_string()).classify(),
236 "processor"
237 );
238 assert_eq!(
239 CamelError::TypeConversionFailed("x".to_string()).classify(),
240 "type_conversion"
241 );
242 assert_eq!(
243 CamelError::InvalidUri("x".to_string()).classify(),
244 "endpoint"
245 );
246 assert_eq!(CamelError::ChannelClosed.classify(), "channel");
247 assert_eq!(CamelError::RouteError("x".to_string()).classify(), "route");
248 assert_eq!(CamelError::Io("x".to_string()).classify(), "io");
249 assert_eq!(
250 CamelError::DeadLetterChannelFailed("x".to_string()).classify(),
251 "dead_letter"
252 );
253 assert_eq!(
254 CamelError::CircuitOpen("x".to_string()).classify(),
255 "circuit_open"
256 );
257 assert_eq!(
258 CamelError::HttpOperationFailed {
259 method: "GET".to_string(),
260 url: "https://example.com".to_string(),
261 status_code: 500,
262 status_text: "Internal Server Error".to_string(),
263 response_body: None,
264 }
265 .classify(),
266 "http"
267 );
268 assert_eq!(CamelError::Stopped.classify(), "stopped");
269 assert_eq!(CamelError::Config("x".to_string()).classify(), "config");
270 assert_eq!(CamelError::AlreadyConsumed.classify(), "type_conversion");
271 assert_eq!(CamelError::StreamLimitExceeded(42).classify(), "stream");
272 }
273
274 #[test]
275 fn test_classify_output_is_ascii_and_short() {
276 for error in all_error_samples() {
277 let class = error.classify();
278 assert!(class.is_ascii());
279 assert!(class.len() <= 15, "class too long: {class}");
280 }
281 }
282
283 #[test]
284 fn test_auth_variants_classify() {
285 assert_eq!(
286 CamelError::Unauthenticated("x".to_string()).classify(),
287 "unauthenticated"
288 );
289 assert_eq!(
290 CamelError::Unauthorized("x".to_string()).classify(),
291 "unauthorized"
292 );
293 }
294
295 #[test]
296 fn test_auth_variants_are_clone() {
297 let err = CamelError::Unauthenticated("test".to_string());
298 let cloned = err.clone();
299 assert!(matches!(cloned, CamelError::Unauthenticated(_)));
300
301 let err2 = CamelError::Unauthorized("test".to_string());
302 let cloned2 = err2.clone();
303 assert!(matches!(cloned2, CamelError::Unauthorized(_)));
304 }
305}
306
307#[cfg(test)]
308mod variant_name_tests {
309 use super::CamelError;
310 use std::sync::Arc;
311
312 #[test]
317 fn variant_name_covers_all_variants() {
318 let cases: Vec<(CamelError, &str)> = vec![
319 (
320 CamelError::ComponentNotFound("x".into()),
321 "ComponentNotFound",
322 ),
323 (
324 CamelError::EndpointCreationFailed("x".into()),
325 "EndpointCreationFailed",
326 ),
327 (CamelError::ProcessorError("x".into()), "ProcessorError"),
328 (
329 CamelError::ProcessorErrorWithSource(
330 "x".into(),
331 Arc::new(std::io::Error::new(std::io::ErrorKind::Other, "y")),
332 ),
333 "ProcessorError", ),
335 (
336 CamelError::TypeConversionFailed("x".into()),
337 "TypeConversionFailed",
338 ),
339 (CamelError::InvalidUri("x".into()), "InvalidUri"),
340 (CamelError::ChannelClosed, "ChannelClosed"),
341 (CamelError::RouteError("x".into()), "RouteError"),
342 (CamelError::Io("x".into()), "Io"),
343 (
344 CamelError::DeadLetterChannelFailed("x".into()),
345 "DeadLetterChannelFailed",
346 ),
347 (CamelError::CircuitOpen("x".into()), "CircuitOpen"),
348 (
349 CamelError::HttpOperationFailed {
350 method: "GET".into(),
351 url: "https://example.com".into(),
352 status_code: 500,
353 status_text: "Internal Server Error".into(),
354 response_body: None,
355 },
356 "HttpOperationFailed",
357 ),
358 (CamelError::Stopped, "Stopped"),
359 (CamelError::Config("x".into()), "Config"),
360 (CamelError::AlreadyConsumed, "AlreadyConsumed"),
361 (CamelError::StreamLimitExceeded(42), "StreamLimitExceeded"),
362 (CamelError::Unauthenticated("x".into()), "Unauthenticated"),
363 (CamelError::Unauthorized("x".into()), "Unauthorized"),
364 ];
365
366 for (err, expected) in cases {
367 assert_eq!(
368 err.variant_name(),
369 expected,
370 "variant_name mismatch for {:?}",
371 err
372 );
373 }
374 }
375}