1use std::sync::Arc;
2use thiserror::Error;
3
4#[derive(Debug, Clone, PartialEq, Eq, Error)]
10#[non_exhaustive]
11pub enum ConfigValidationError {
12 #[error(
13 "aggregator config requires at least one completion bound (size, timeout, predicate, or interval)"
14 )]
15 AggregatorMissingCompletionBound,
16
17 #[error("aggregator requires at least one of max_buckets, completionTimeout, or bucket_ttl")]
22 AggregatorMissingMemoryBound,
23
24 #[error(
31 "aggregator Timeout completion requires bucket_ttl (memory-release bound for the timeout-task cap fallback)"
32 )]
33 AggregatorTimeoutRequiresTtl,
34
35 #[error("throttler max_requests must be > 0")]
36 ThrottlerMaxRequestsZero,
37
38 #[error("loop step must specify either 'count' or 'while', not both")]
39 LoopConflictingCountAndWhile,
40
41 #[error("loop step must specify either 'count' or 'while'")]
42 LoopMissingCountOrWhile,
43
44 #[error("SQL use_message_body_for_sql requires allow_dynamic_query=true")]
45 SqlDynamicQueryWithoutAllowDynamic,
46}
47
48#[derive(Debug, Clone, Error)]
50#[non_exhaustive]
51pub enum CamelError {
52 #[error("Component not found: {0}")]
53 ComponentNotFound(String),
54
55 #[error("Endpoint creation failed: {0}")]
56 EndpointCreationFailed(String),
57
58 #[error("Processor error: {0}")]
59 ProcessorError(String),
60
61 #[error("Processor error: {0}")]
64 ProcessorErrorWithSource(String, #[source] Arc<dyn std::error::Error + Send + Sync>),
65
66 #[error("Type conversion failed: {0}")]
67 TypeConversionFailed(String),
68
69 #[error("Invalid URI: {0}")]
70 InvalidUri(String),
71
72 #[error("Channel closed")]
73 ChannelClosed,
74
75 #[error("Route error: {0}")]
76 RouteError(String),
77
78 #[error("IO error: {0}")]
79 Io(String),
80
81 #[error("Dead letter channel failed: {0}")]
82 DeadLetterChannelFailed(String),
83
84 #[error("Circuit breaker open: {0}")]
85 CircuitOpen(String),
86
87 #[error("HTTP {method} {url} failed: {status_code} {status_text}")]
88 HttpOperationFailed {
89 method: String,
90 url: String,
91 status_code: u16,
92 status_text: String,
93 response_body: Option<String>,
94 },
95
96 #[error("Consumer stopping: semaphore closed during poll_ready")]
100 ConsumerStopping,
101
102 #[error("Configuration error: {0}")]
103 Config(String),
104
105 #[error("Configuration validation error: {0}")]
109 ConfigValidation(ConfigValidationError),
110
111 #[error("Body stream has already been consumed")]
112 AlreadyConsumed,
113
114 #[error("Stream size exceeded limit: {0}")]
115 StreamLimitExceeded(usize),
116
117 #[error("Unauthenticated: {0}")]
118 Unauthenticated(String),
119
120 #[error("Unauthorized: {0}")]
121 Unauthorized(String),
122
123 #[error("Validation failed: {0}")]
124 ValidationError(String),
125
126 #[error("Template reload failed: {0}")]
127 TemplateReload(String),
128}
129
130impl CamelError {
131 pub fn classify(&self) -> &'static str {
132 #[allow(unreachable_patterns)]
133 match self {
134 Self::ComponentNotFound(_) => "component",
135 Self::EndpointCreationFailed(_) | Self::InvalidUri(_) => "endpoint",
136 Self::ProcessorError(_) | Self::ProcessorErrorWithSource(_, _) => "processor",
137 Self::TypeConversionFailed(_) | Self::AlreadyConsumed => "type_conversion",
138 Self::Io(_) => "io",
139 Self::RouteError(_) => "route",
140 Self::CircuitOpen(_) => "circuit_open",
141 Self::HttpOperationFailed { .. } => "http",
142 Self::Config(_) | Self::ConfigValidation(_) => "config",
143 Self::DeadLetterChannelFailed(_) => "dead_letter",
144 Self::ConsumerStopping => "consumer_stop",
145 Self::StreamLimitExceeded(_) => "stream",
146 Self::ChannelClosed => "channel",
147 Self::Unauthenticated(_) => "unauthenticated",
148 Self::Unauthorized(_) => "unauthorized",
149 Self::ValidationError(_) => "validation",
150 Self::TemplateReload(_) => "template",
151 _ => "unknown",
152 }
153 }
154
155 pub fn variant_name(&self) -> &'static str {
164 match self {
165 Self::ComponentNotFound(_) => "ComponentNotFound",
166 Self::EndpointCreationFailed(_) => "EndpointCreationFailed",
167 Self::ProcessorError(_) => "ProcessorError",
168 Self::ProcessorErrorWithSource(_, _) => "ProcessorError",
169 Self::TypeConversionFailed(_) => "TypeConversionFailed",
170 Self::InvalidUri(_) => "InvalidUri",
171 Self::ChannelClosed => "ChannelClosed",
172 Self::RouteError(_) => "RouteError",
173 Self::Io(_) => "Io",
174 Self::DeadLetterChannelFailed(_) => "DeadLetterChannelFailed",
175 Self::CircuitOpen(_) => "CircuitOpen",
176 Self::HttpOperationFailed { .. } => "HttpOperationFailed",
177 Self::ConsumerStopping => "ConsumerStopping",
178 Self::Config(_) => "Config",
179 Self::ConfigValidation(_) => "ConfigValidation",
180 Self::AlreadyConsumed => "AlreadyConsumed",
181 Self::StreamLimitExceeded(_) => "StreamLimitExceeded",
182 Self::Unauthenticated(_) => "Unauthenticated",
183 Self::Unauthorized(_) => "Unauthorized",
184 Self::ValidationError(_) => "ValidationError",
185 Self::TemplateReload(_) => "TemplateReload",
186 }
187 }
188}
189
190impl From<std::io::Error> for CamelError {
191 fn from(err: std::io::Error) -> Self {
192 CamelError::Io(err.to_string())
193 }
194}
195
196impl From<crate::template::TemplateError> for CamelError {
197 fn from(err: crate::template::TemplateError) -> Self {
198 CamelError::Config(err.to_string())
199 }
200}
201
202impl From<ConfigValidationError> for CamelError {
203 fn from(e: ConfigValidationError) -> Self {
204 CamelError::ConfigValidation(e)
205 }
206}
207
208#[cfg(test)]
209mod tests {
210 use super::*;
211
212 fn all_error_samples() -> Vec<CamelError> {
213 vec![
214 CamelError::ComponentNotFound("x".to_string()),
215 CamelError::EndpointCreationFailed("x".to_string()),
216 CamelError::ProcessorError("x".to_string()),
217 CamelError::ProcessorErrorWithSource(
218 "x".to_string(),
219 Arc::new(std::io::Error::other("inner")),
220 ),
221 CamelError::TypeConversionFailed("x".to_string()),
222 CamelError::InvalidUri("x".to_string()),
223 CamelError::ChannelClosed,
224 CamelError::RouteError("x".to_string()),
225 CamelError::Io("x".to_string()),
226 CamelError::DeadLetterChannelFailed("x".to_string()),
227 CamelError::CircuitOpen("x".to_string()),
228 CamelError::HttpOperationFailed {
229 method: "GET".to_string(),
230 url: "https://example.com".to_string(),
231 status_code: 500,
232 status_text: "Internal Server Error".to_string(),
233 response_body: Some("error".to_string()),
234 },
235 CamelError::ConsumerStopping,
236 CamelError::Config("x".to_string()),
237 CamelError::ConfigValidation(ConfigValidationError::ThrottlerMaxRequestsZero),
238 CamelError::AlreadyConsumed,
239 CamelError::StreamLimitExceeded(42),
240 CamelError::Unauthenticated("token expired".to_string()),
241 CamelError::Unauthorized("missing admin role".to_string()),
242 CamelError::ValidationError("body does not match schema".to_string()),
243 CamelError::TemplateReload("reload failed".to_string()),
244 ]
245 }
246
247 #[test]
248 fn test_http_operation_failed_display() {
249 let err = CamelError::HttpOperationFailed {
250 method: "GET".to_string(),
251 url: "https://example.com/test".to_string(),
252 status_code: 404,
253 status_text: "Not Found".to_string(),
254 response_body: Some("page not found".to_string()),
255 };
256 let msg = format!("{err}");
257 assert!(msg.contains("404"));
258 assert!(msg.contains("Not Found"));
259 }
260
261 #[test]
262 fn test_http_operation_failed_clone() {
263 let err = CamelError::HttpOperationFailed {
264 method: "POST".to_string(),
265 url: "https://api.example.com/users".to_string(),
266 status_code: 500,
267 status_text: "Internal Server Error".to_string(),
268 response_body: None,
269 };
270 let cloned = err.clone();
271 assert!(matches!(
272 cloned,
273 CamelError::HttpOperationFailed {
274 status_code: 500,
275 ..
276 }
277 ));
278 }
279
280 #[test]
281 fn test_classify_maps_all_variants() {
282 assert_eq!(
283 CamelError::ComponentNotFound("x".to_string()).classify(),
284 "component"
285 );
286 assert_eq!(
287 CamelError::EndpointCreationFailed("x".to_string()).classify(),
288 "endpoint"
289 );
290 assert_eq!(
291 CamelError::ProcessorError("x".to_string()).classify(),
292 "processor"
293 );
294 assert_eq!(
295 CamelError::TypeConversionFailed("x".to_string()).classify(),
296 "type_conversion"
297 );
298 assert_eq!(
299 CamelError::InvalidUri("x".to_string()).classify(),
300 "endpoint"
301 );
302 assert_eq!(CamelError::ChannelClosed.classify(), "channel");
303 assert_eq!(CamelError::RouteError("x".to_string()).classify(), "route");
304 assert_eq!(CamelError::Io("x".to_string()).classify(), "io");
305 assert_eq!(
306 CamelError::DeadLetterChannelFailed("x".to_string()).classify(),
307 "dead_letter"
308 );
309 assert_eq!(
310 CamelError::CircuitOpen("x".to_string()).classify(),
311 "circuit_open"
312 );
313 assert_eq!(
314 CamelError::HttpOperationFailed {
315 method: "GET".to_string(),
316 url: "https://example.com".to_string(),
317 status_code: 500,
318 status_text: "Internal Server Error".to_string(),
319 response_body: None,
320 }
321 .classify(),
322 "http"
323 );
324 assert_eq!(CamelError::Config("x".to_string()).classify(), "config");
325 assert_eq!(
326 CamelError::ConfigValidation(ConfigValidationError::ThrottlerMaxRequestsZero)
327 .classify(),
328 "config"
329 );
330 assert_eq!(CamelError::AlreadyConsumed.classify(), "type_conversion");
331 assert_eq!(CamelError::StreamLimitExceeded(42).classify(), "stream");
332 assert_eq!(
333 CamelError::ValidationError("bad".to_string()).classify(),
334 "validation"
335 );
336 }
337
338 #[test]
339 fn test_classify_output_is_ascii_and_short() {
340 for error in all_error_samples() {
341 let class = error.classify();
342 assert!(class.is_ascii());
343 assert!(class.len() <= 15, "class too long: {class}");
344 }
345 }
346
347 #[test]
348 fn test_auth_variants_classify() {
349 assert_eq!(
350 CamelError::Unauthenticated("x".to_string()).classify(),
351 "unauthenticated"
352 );
353 assert_eq!(
354 CamelError::Unauthorized("x".to_string()).classify(),
355 "unauthorized"
356 );
357 }
358
359 #[test]
360 fn test_validation_error_classify() {
361 assert_eq!(
362 CamelError::ValidationError("bad".to_string()).classify(),
363 "validation"
364 );
365 }
366
367 #[test]
368 fn template_reload_classifies_as_template() {
369 let err = CamelError::TemplateReload("boom".into());
370 assert_eq!(err.classify(), "template");
371 }
372
373 #[test]
374 fn template_reload_variant_name() {
375 let err = CamelError::TemplateReload("boom".into());
376 assert_eq!(err.variant_name(), "TemplateReload");
377 }
378
379 #[test]
380 fn test_auth_variants_are_clone() {
381 let err = CamelError::Unauthenticated("test".to_string());
382 let cloned = err.clone();
383 assert!(matches!(cloned, CamelError::Unauthenticated(_)));
384
385 let err2 = CamelError::Unauthorized("test".to_string());
386 let cloned2 = err2.clone();
387 assert!(matches!(cloned2, CamelError::Unauthorized(_)));
388 }
389}
390
391#[cfg(test)]
392mod variant_name_tests {
393 use super::{CamelError, ConfigValidationError};
394 use std::sync::Arc;
395
396 #[test]
401 fn variant_name_covers_all_variants() {
402 let cases: Vec<(CamelError, &str)> = vec![
403 (
404 CamelError::ComponentNotFound("x".into()),
405 "ComponentNotFound",
406 ),
407 (
408 CamelError::EndpointCreationFailed("x".into()),
409 "EndpointCreationFailed",
410 ),
411 (CamelError::ProcessorError("x".into()), "ProcessorError"),
412 (
413 CamelError::ProcessorErrorWithSource(
414 "x".into(),
415 Arc::new(std::io::Error::other("y")),
416 ),
417 "ProcessorError", ),
419 (
420 CamelError::TypeConversionFailed("x".into()),
421 "TypeConversionFailed",
422 ),
423 (CamelError::InvalidUri("x".into()), "InvalidUri"),
424 (CamelError::ChannelClosed, "ChannelClosed"),
425 (CamelError::RouteError("x".into()), "RouteError"),
426 (CamelError::Io("x".into()), "Io"),
427 (
428 CamelError::DeadLetterChannelFailed("x".into()),
429 "DeadLetterChannelFailed",
430 ),
431 (CamelError::CircuitOpen("x".into()), "CircuitOpen"),
432 (
433 CamelError::HttpOperationFailed {
434 method: "GET".into(),
435 url: "https://example.com".into(),
436 status_code: 500,
437 status_text: "Internal Server Error".into(),
438 response_body: None,
439 },
440 "HttpOperationFailed",
441 ),
442 (CamelError::Config("x".into()), "Config"),
443 (
444 CamelError::ConfigValidation(ConfigValidationError::ThrottlerMaxRequestsZero),
445 "ConfigValidation",
446 ),
447 (CamelError::AlreadyConsumed, "AlreadyConsumed"),
448 (CamelError::StreamLimitExceeded(42), "StreamLimitExceeded"),
449 (CamelError::Unauthenticated("x".into()), "Unauthenticated"),
450 (CamelError::Unauthorized("x".into()), "Unauthorized"),
451 (CamelError::ValidationError("bad".into()), "ValidationError"),
452 (CamelError::TemplateReload("x".into()), "TemplateReload"),
453 ];
454
455 for (err, expected) in cases {
456 assert_eq!(
457 err.variant_name(),
458 expected,
459 "variant_name mismatch for {:?}",
460 err
461 );
462 }
463 }
464}