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, PartialEq, Eq, Error)]
54#[non_exhaustive]
55pub enum EndpointUriError {
56 #[error(
58 "endpoint URI parameter `{key}` duplicates a key already present in the base URI query"
59 )]
60 DuplicateKey { key: String },
61
62 #[error("endpoint URI is missing a scheme (expected `scheme:path`)")]
64 MissingScheme,
65
66 #[error("endpoint URI query contains a pair with an empty key")]
68 EmptyQueryKey,
69
70 #[error("endpoint URI parameter key `{key}` is empty or contains a reserved character")]
72 InvalidParamKey { key: String },
73}
74
75#[derive(Debug, Clone, Error)]
77#[non_exhaustive]
78pub enum CamelError {
79 #[error("Component not found: {0}")]
80 ComponentNotFound(String),
81
82 #[error("Endpoint creation failed: {0}")]
83 EndpointCreationFailed(String),
84
85 #[error("Processor error: {0}")]
86 ProcessorError(String),
87
88 #[error("Processor error: {0}")]
91 ProcessorErrorWithSource(String, #[source] Arc<dyn std::error::Error + Send + Sync>),
92
93 #[error("Type conversion failed: {0}")]
94 TypeConversionFailed(String),
95
96 #[error("Invalid URI: {0}")]
97 InvalidUri(String),
98
99 #[error("Channel closed")]
100 ChannelClosed,
101
102 #[error("Route error: {0}")]
103 RouteError(String),
104
105 #[error("IO error: {0}")]
106 Io(String),
107
108 #[error("Dead letter channel failed: {0}")]
109 DeadLetterChannelFailed(String),
110
111 #[error("Circuit breaker open: {0}")]
112 CircuitOpen(String),
113
114 #[error("HTTP {method} {url} failed: {status_code} {status_text}")]
115 HttpOperationFailed {
116 method: String,
117 url: String,
118 status_code: u16,
119 status_text: String,
120 response_body: Option<String>,
121 },
122
123 #[error("Consumer stopping: semaphore closed during poll_ready")]
127 ConsumerStopping,
128
129 #[error("Configuration error: {0}")]
130 Config(String),
131
132 #[error("Configuration validation error: {0}")]
136 ConfigValidation(ConfigValidationError),
137
138 #[error("Body stream has already been consumed")]
139 AlreadyConsumed,
140
141 #[error("Stream size exceeded limit: {0}")]
142 StreamLimitExceeded(usize),
143
144 #[error("Unauthenticated: {0}")]
145 Unauthenticated(String),
146
147 #[error("Unauthorized: {0}")]
148 Unauthorized(String),
149
150 #[error("Validation failed: {0}")]
151 ValidationError(String),
152
153 #[error("Template reload failed: {0}")]
154 TemplateReload(String),
155
156 #[error("Endpoint URI error: {0}")]
160 EndpointUri(EndpointUriError),
161}
162
163impl CamelError {
164 pub fn classify(&self) -> &'static str {
165 #[allow(unreachable_patterns)]
166 match self {
167 Self::ComponentNotFound(_) => "component",
168 Self::EndpointCreationFailed(_) | Self::InvalidUri(_) | Self::EndpointUri(_) => {
169 "endpoint"
170 }
171 Self::ProcessorError(_) | Self::ProcessorErrorWithSource(_, _) => "processor",
172 Self::TypeConversionFailed(_) | Self::AlreadyConsumed => "type_conversion",
173 Self::Io(_) => "io",
174 Self::RouteError(_) => "route",
175 Self::CircuitOpen(_) => "circuit_open",
176 Self::HttpOperationFailed { .. } => "http",
177 Self::Config(_) | Self::ConfigValidation(_) => "config",
178 Self::DeadLetterChannelFailed(_) => "dead_letter",
179 Self::ConsumerStopping => "consumer_stop",
180 Self::StreamLimitExceeded(_) => "stream",
181 Self::ChannelClosed => "channel",
182 Self::Unauthenticated(_) => "unauthenticated",
183 Self::Unauthorized(_) => "unauthorized",
184 Self::ValidationError(_) => "validation",
185 Self::TemplateReload(_) => "template",
186 _ => "unknown",
187 }
188 }
189
190 pub fn variant_name(&self) -> &'static str {
199 match self {
200 Self::ComponentNotFound(_) => "ComponentNotFound",
201 Self::EndpointCreationFailed(_) => "EndpointCreationFailed",
202 Self::ProcessorError(_) => "ProcessorError",
203 Self::ProcessorErrorWithSource(_, _) => "ProcessorError",
204 Self::TypeConversionFailed(_) => "TypeConversionFailed",
205 Self::InvalidUri(_) => "InvalidUri",
206 Self::ChannelClosed => "ChannelClosed",
207 Self::RouteError(_) => "RouteError",
208 Self::Io(_) => "Io",
209 Self::DeadLetterChannelFailed(_) => "DeadLetterChannelFailed",
210 Self::CircuitOpen(_) => "CircuitOpen",
211 Self::HttpOperationFailed { .. } => "HttpOperationFailed",
212 Self::ConsumerStopping => "ConsumerStopping",
213 Self::Config(_) => "Config",
214 Self::ConfigValidation(_) => "ConfigValidation",
215 Self::AlreadyConsumed => "AlreadyConsumed",
216 Self::StreamLimitExceeded(_) => "StreamLimitExceeded",
217 Self::Unauthenticated(_) => "Unauthenticated",
218 Self::Unauthorized(_) => "Unauthorized",
219 Self::ValidationError(_) => "ValidationError",
220 Self::TemplateReload(_) => "TemplateReload",
221 Self::EndpointUri(_) => "EndpointUri",
222 }
223 }
224}
225
226impl From<std::io::Error> for CamelError {
227 fn from(err: std::io::Error) -> Self {
228 CamelError::Io(err.to_string())
229 }
230}
231
232impl From<crate::template::TemplateError> for CamelError {
233 fn from(err: crate::template::TemplateError) -> Self {
234 CamelError::Config(err.to_string())
235 }
236}
237
238impl From<ConfigValidationError> for CamelError {
239 fn from(e: ConfigValidationError) -> Self {
240 CamelError::ConfigValidation(e)
241 }
242}
243
244impl From<EndpointUriError> for CamelError {
245 fn from(e: EndpointUriError) -> Self {
246 CamelError::EndpointUri(e)
247 }
248}
249
250#[cfg(test)]
251mod tests {
252 use super::*;
253
254 fn all_error_samples() -> Vec<CamelError> {
255 vec![
256 CamelError::ComponentNotFound("x".to_string()),
257 CamelError::EndpointCreationFailed("x".to_string()),
258 CamelError::ProcessorError("x".to_string()),
259 CamelError::ProcessorErrorWithSource(
260 "x".to_string(),
261 Arc::new(std::io::Error::other("inner")),
262 ),
263 CamelError::TypeConversionFailed("x".to_string()),
264 CamelError::InvalidUri("x".to_string()),
265 CamelError::ChannelClosed,
266 CamelError::RouteError("x".to_string()),
267 CamelError::Io("x".to_string()),
268 CamelError::DeadLetterChannelFailed("x".to_string()),
269 CamelError::CircuitOpen("x".to_string()),
270 CamelError::HttpOperationFailed {
271 method: "GET".to_string(),
272 url: "https://example.com".to_string(),
273 status_code: 500,
274 status_text: "Internal Server Error".to_string(),
275 response_body: Some("error".to_string()),
276 },
277 CamelError::ConsumerStopping,
278 CamelError::Config("x".to_string()),
279 CamelError::ConfigValidation(ConfigValidationError::ThrottlerMaxRequestsZero),
280 CamelError::AlreadyConsumed,
281 CamelError::StreamLimitExceeded(42),
282 CamelError::Unauthenticated("token expired".to_string()),
283 CamelError::Unauthorized("missing admin role".to_string()),
284 CamelError::ValidationError("body does not match schema".to_string()),
285 CamelError::TemplateReload("reload failed".to_string()),
286 CamelError::EndpointUri(EndpointUriError::MissingScheme),
287 ]
288 }
289
290 #[test]
291 fn test_http_operation_failed_display() {
292 let err = CamelError::HttpOperationFailed {
293 method: "GET".to_string(),
294 url: "https://example.com/test".to_string(),
295 status_code: 404,
296 status_text: "Not Found".to_string(),
297 response_body: Some("page not found".to_string()),
298 };
299 let msg = format!("{err}");
300 assert!(msg.contains("404"));
301 assert!(msg.contains("Not Found"));
302 }
303
304 #[test]
305 fn test_http_operation_failed_clone() {
306 let err = CamelError::HttpOperationFailed {
307 method: "POST".to_string(),
308 url: "https://api.example.com/users".to_string(),
309 status_code: 500,
310 status_text: "Internal Server Error".to_string(),
311 response_body: None,
312 };
313 let cloned = err.clone();
314 assert!(matches!(
315 cloned,
316 CamelError::HttpOperationFailed {
317 status_code: 500,
318 ..
319 }
320 ));
321 }
322
323 #[test]
324 fn test_classify_maps_all_variants() {
325 assert_eq!(
326 CamelError::ComponentNotFound("x".to_string()).classify(),
327 "component"
328 );
329 assert_eq!(
330 CamelError::EndpointCreationFailed("x".to_string()).classify(),
331 "endpoint"
332 );
333 assert_eq!(
334 CamelError::ProcessorError("x".to_string()).classify(),
335 "processor"
336 );
337 assert_eq!(
338 CamelError::TypeConversionFailed("x".to_string()).classify(),
339 "type_conversion"
340 );
341 assert_eq!(
342 CamelError::InvalidUri("x".to_string()).classify(),
343 "endpoint"
344 );
345 assert_eq!(CamelError::ChannelClosed.classify(), "channel");
346 assert_eq!(CamelError::RouteError("x".to_string()).classify(), "route");
347 assert_eq!(CamelError::Io("x".to_string()).classify(), "io");
348 assert_eq!(
349 CamelError::DeadLetterChannelFailed("x".to_string()).classify(),
350 "dead_letter"
351 );
352 assert_eq!(
353 CamelError::CircuitOpen("x".to_string()).classify(),
354 "circuit_open"
355 );
356 assert_eq!(
357 CamelError::HttpOperationFailed {
358 method: "GET".to_string(),
359 url: "https://example.com".to_string(),
360 status_code: 500,
361 status_text: "Internal Server Error".to_string(),
362 response_body: None,
363 }
364 .classify(),
365 "http"
366 );
367 assert_eq!(CamelError::Config("x".to_string()).classify(), "config");
368 assert_eq!(
369 CamelError::ConfigValidation(ConfigValidationError::ThrottlerMaxRequestsZero)
370 .classify(),
371 "config"
372 );
373 assert_eq!(CamelError::AlreadyConsumed.classify(), "type_conversion");
374 assert_eq!(CamelError::StreamLimitExceeded(42).classify(), "stream");
375 assert_eq!(
376 CamelError::ValidationError("bad".to_string()).classify(),
377 "validation"
378 );
379 }
380
381 #[test]
382 fn test_classify_output_is_ascii_and_short() {
383 for error in all_error_samples() {
384 let class = error.classify();
385 assert!(class.is_ascii());
386 assert!(class.len() <= 15, "class too long: {class}");
387 }
388 }
389
390 #[test]
391 fn test_auth_variants_classify() {
392 assert_eq!(
393 CamelError::Unauthenticated("x".to_string()).classify(),
394 "unauthenticated"
395 );
396 assert_eq!(
397 CamelError::Unauthorized("x".to_string()).classify(),
398 "unauthorized"
399 );
400 }
401
402 #[test]
403 fn test_validation_error_classify() {
404 assert_eq!(
405 CamelError::ValidationError("bad".to_string()).classify(),
406 "validation"
407 );
408 }
409
410 #[test]
411 fn template_reload_classifies_as_template() {
412 let err = CamelError::TemplateReload("boom".into());
413 assert_eq!(err.classify(), "template");
414 }
415
416 #[test]
417 fn template_reload_variant_name() {
418 let err = CamelError::TemplateReload("boom".into());
419 assert_eq!(err.variant_name(), "TemplateReload");
420 }
421
422 #[test]
423 fn test_auth_variants_are_clone() {
424 let err = CamelError::Unauthenticated("test".to_string());
425 let cloned = err.clone();
426 assert!(matches!(cloned, CamelError::Unauthenticated(_)));
427
428 let err2 = CamelError::Unauthorized("test".to_string());
429 let cloned2 = err2.clone();
430 assert!(matches!(cloned2, CamelError::Unauthorized(_)));
431 }
432}
433
434#[cfg(test)]
435mod variant_name_tests {
436 use super::{CamelError, ConfigValidationError, EndpointUriError};
437 use std::sync::Arc;
438
439 #[test]
444 fn variant_name_covers_all_variants() {
445 let cases: Vec<(CamelError, &str)> = vec![
446 (
447 CamelError::ComponentNotFound("x".into()),
448 "ComponentNotFound",
449 ),
450 (
451 CamelError::EndpointCreationFailed("x".into()),
452 "EndpointCreationFailed",
453 ),
454 (CamelError::ProcessorError("x".into()), "ProcessorError"),
455 (
456 CamelError::ProcessorErrorWithSource(
457 "x".into(),
458 Arc::new(std::io::Error::other("y")),
459 ),
460 "ProcessorError", ),
462 (
463 CamelError::TypeConversionFailed("x".into()),
464 "TypeConversionFailed",
465 ),
466 (CamelError::InvalidUri("x".into()), "InvalidUri"),
467 (CamelError::ChannelClosed, "ChannelClosed"),
468 (CamelError::RouteError("x".into()), "RouteError"),
469 (CamelError::Io("x".into()), "Io"),
470 (
471 CamelError::DeadLetterChannelFailed("x".into()),
472 "DeadLetterChannelFailed",
473 ),
474 (CamelError::CircuitOpen("x".into()), "CircuitOpen"),
475 (
476 CamelError::HttpOperationFailed {
477 method: "GET".into(),
478 url: "https://example.com".into(),
479 status_code: 500,
480 status_text: "Internal Server Error".into(),
481 response_body: None,
482 },
483 "HttpOperationFailed",
484 ),
485 (CamelError::Config("x".into()), "Config"),
486 (
487 CamelError::ConfigValidation(ConfigValidationError::ThrottlerMaxRequestsZero),
488 "ConfigValidation",
489 ),
490 (CamelError::AlreadyConsumed, "AlreadyConsumed"),
491 (CamelError::StreamLimitExceeded(42), "StreamLimitExceeded"),
492 (CamelError::Unauthenticated("x".into()), "Unauthenticated"),
493 (CamelError::Unauthorized("x".into()), "Unauthorized"),
494 (CamelError::ValidationError("bad".into()), "ValidationError"),
495 (CamelError::TemplateReload("x".into()), "TemplateReload"),
496 (
497 CamelError::EndpointUri(EndpointUriError::MissingScheme),
498 "EndpointUri",
499 ),
500 ];
501
502 for (err, expected) in cases {
503 assert_eq!(
504 err.variant_name(),
505 expected,
506 "variant_name mismatch for {:?}",
507 err
508 );
509 }
510 }
511}