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 #[error("Validation failed: {0}")]
74 ValidationError(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::ConsumerStopping => "consumer_stop",
92 Self::StreamLimitExceeded(_) => "stream",
93 Self::ChannelClosed => "channel",
94 Self::Unauthenticated(_) => "unauthenticated",
95 Self::Unauthorized(_) => "unauthorized",
96 Self::ValidationError(_) => "validation",
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::ConsumerStopping => "ConsumerStopping",
124 Self::Config(_) => "Config",
125 Self::AlreadyConsumed => "AlreadyConsumed",
126 Self::StreamLimitExceeded(_) => "StreamLimitExceeded",
127 Self::Unauthenticated(_) => "Unauthenticated",
128 Self::Unauthorized(_) => "Unauthorized",
129 Self::ValidationError(_) => "ValidationError",
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::ConsumerStopping,
174 CamelError::Config("x".to_string()),
175 CamelError::AlreadyConsumed,
176 CamelError::StreamLimitExceeded(42),
177 CamelError::Unauthenticated("token expired".to_string()),
178 CamelError::Unauthorized("missing admin role".to_string()),
179 CamelError::ValidationError("body does not match schema".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_classify_maps_all_variants() {
218 assert_eq!(
219 CamelError::ComponentNotFound("x".to_string()).classify(),
220 "component"
221 );
222 assert_eq!(
223 CamelError::EndpointCreationFailed("x".to_string()).classify(),
224 "endpoint"
225 );
226 assert_eq!(
227 CamelError::ProcessorError("x".to_string()).classify(),
228 "processor"
229 );
230 assert_eq!(
231 CamelError::TypeConversionFailed("x".to_string()).classify(),
232 "type_conversion"
233 );
234 assert_eq!(
235 CamelError::InvalidUri("x".to_string()).classify(),
236 "endpoint"
237 );
238 assert_eq!(CamelError::ChannelClosed.classify(), "channel");
239 assert_eq!(CamelError::RouteError("x".to_string()).classify(), "route");
240 assert_eq!(CamelError::Io("x".to_string()).classify(), "io");
241 assert_eq!(
242 CamelError::DeadLetterChannelFailed("x".to_string()).classify(),
243 "dead_letter"
244 );
245 assert_eq!(
246 CamelError::CircuitOpen("x".to_string()).classify(),
247 "circuit_open"
248 );
249 assert_eq!(
250 CamelError::HttpOperationFailed {
251 method: "GET".to_string(),
252 url: "https://example.com".to_string(),
253 status_code: 500,
254 status_text: "Internal Server Error".to_string(),
255 response_body: None,
256 }
257 .classify(),
258 "http"
259 );
260 assert_eq!(CamelError::Config("x".to_string()).classify(), "config");
261 assert_eq!(CamelError::AlreadyConsumed.classify(), "type_conversion");
262 assert_eq!(CamelError::StreamLimitExceeded(42).classify(), "stream");
263 assert_eq!(
264 CamelError::ValidationError("bad".to_string()).classify(),
265 "validation"
266 );
267 }
268
269 #[test]
270 fn test_classify_output_is_ascii_and_short() {
271 for error in all_error_samples() {
272 let class = error.classify();
273 assert!(class.is_ascii());
274 assert!(class.len() <= 15, "class too long: {class}");
275 }
276 }
277
278 #[test]
279 fn test_auth_variants_classify() {
280 assert_eq!(
281 CamelError::Unauthenticated("x".to_string()).classify(),
282 "unauthenticated"
283 );
284 assert_eq!(
285 CamelError::Unauthorized("x".to_string()).classify(),
286 "unauthorized"
287 );
288 }
289
290 #[test]
291 fn test_validation_error_classify() {
292 assert_eq!(
293 CamelError::ValidationError("bad".to_string()).classify(),
294 "validation"
295 );
296 }
297
298 #[test]
299 fn test_auth_variants_are_clone() {
300 let err = CamelError::Unauthenticated("test".to_string());
301 let cloned = err.clone();
302 assert!(matches!(cloned, CamelError::Unauthenticated(_)));
303
304 let err2 = CamelError::Unauthorized("test".to_string());
305 let cloned2 = err2.clone();
306 assert!(matches!(cloned2, CamelError::Unauthorized(_)));
307 }
308}
309
310#[cfg(test)]
311mod variant_name_tests {
312 use super::CamelError;
313 use std::sync::Arc;
314
315 #[test]
320 fn variant_name_covers_all_variants() {
321 let cases: Vec<(CamelError, &str)> = vec![
322 (
323 CamelError::ComponentNotFound("x".into()),
324 "ComponentNotFound",
325 ),
326 (
327 CamelError::EndpointCreationFailed("x".into()),
328 "EndpointCreationFailed",
329 ),
330 (CamelError::ProcessorError("x".into()), "ProcessorError"),
331 (
332 CamelError::ProcessorErrorWithSource(
333 "x".into(),
334 Arc::new(std::io::Error::new(std::io::ErrorKind::Other, "y")),
335 ),
336 "ProcessorError", ),
338 (
339 CamelError::TypeConversionFailed("x".into()),
340 "TypeConversionFailed",
341 ),
342 (CamelError::InvalidUri("x".into()), "InvalidUri"),
343 (CamelError::ChannelClosed, "ChannelClosed"),
344 (CamelError::RouteError("x".into()), "RouteError"),
345 (CamelError::Io("x".into()), "Io"),
346 (
347 CamelError::DeadLetterChannelFailed("x".into()),
348 "DeadLetterChannelFailed",
349 ),
350 (CamelError::CircuitOpen("x".into()), "CircuitOpen"),
351 (
352 CamelError::HttpOperationFailed {
353 method: "GET".into(),
354 url: "https://example.com".into(),
355 status_code: 500,
356 status_text: "Internal Server Error".into(),
357 response_body: None,
358 },
359 "HttpOperationFailed",
360 ),
361 (CamelError::Config("x".into()), "Config"),
362 (CamelError::AlreadyConsumed, "AlreadyConsumed"),
363 (CamelError::StreamLimitExceeded(42), "StreamLimitExceeded"),
364 (CamelError::Unauthenticated("x".into()), "Unauthenticated"),
365 (CamelError::Unauthorized("x".into()), "Unauthorized"),
366 (CamelError::ValidationError("bad".into()), "ValidationError"),
367 ];
368
369 for (err, expected) in cases {
370 assert_eq!(
371 err.variant_name(),
372 expected,
373 "variant_name mismatch for {:?}",
374 err
375 );
376 }
377 }
378}