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 call")]
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("Auth provider unavailable: {0}")]
154 AuthProviderUnavailable(String),
155
156 #[error("Validation failed: {0}")]
157 ValidationError(String),
158
159 #[error("Template reload failed: {0}")]
160 TemplateReload(String),
161
162 #[error("Endpoint URI error: {0}")]
166 EndpointUri(EndpointUriError),
167}
168
169impl CamelError {
170 pub fn classify(&self) -> &'static str {
171 #[allow(unreachable_patterns)]
172 match self {
173 Self::ComponentNotFound(_) => "component",
174 Self::EndpointCreationFailed(_) | Self::InvalidUri(_) | Self::EndpointUri(_) => {
175 "endpoint"
176 }
177 Self::ProcessorError(_)
178 | Self::ProcessorErrorWithSource(_, _)
179 | Self::AuthProviderUnavailable(_) => "processor",
180 Self::TypeConversionFailed(_) | Self::AlreadyConsumed => "type_conversion",
181 Self::Io(_) => "io",
182 Self::RouteError(_) => "route",
183 Self::CircuitOpen(_) => "circuit_open",
184 Self::HttpOperationFailed { .. } => "http",
185 Self::Config(_) | Self::ConfigValidation(_) => "config",
186 Self::DeadLetterChannelFailed(_) => "dead_letter",
187 Self::ConsumerStopping => "consumer_stop",
188 Self::StreamLimitExceeded(_) => "stream",
189 Self::ChannelClosed => "channel",
190 Self::Unauthenticated(_) => "unauthenticated",
191 Self::Unauthorized(_) => "unauthorized",
192 Self::ValidationError(_) => "validation",
193 Self::TemplateReload(_) => "template",
194 _ => "unknown",
195 }
196 }
197
198 pub fn variant_name(&self) -> &'static str {
208 match self {
209 Self::ComponentNotFound(_) => "ComponentNotFound",
210 Self::EndpointCreationFailed(_) => "EndpointCreationFailed",
211 Self::ProcessorError(_) => "ProcessorError",
212 Self::ProcessorErrorWithSource(_, _) => "ProcessorError",
213 Self::AuthProviderUnavailable(_) => "ProcessorError",
214 Self::TypeConversionFailed(_) => "TypeConversionFailed",
215 Self::InvalidUri(_) => "InvalidUri",
216 Self::ChannelClosed => "ChannelClosed",
217 Self::RouteError(_) => "RouteError",
218 Self::Io(_) => "Io",
219 Self::DeadLetterChannelFailed(_) => "DeadLetterChannelFailed",
220 Self::CircuitOpen(_) => "CircuitOpen",
221 Self::HttpOperationFailed { .. } => "HttpOperationFailed",
222 Self::ConsumerStopping => "ConsumerStopping",
223 Self::Config(_) => "Config",
224 Self::ConfigValidation(_) => "ConfigValidation",
225 Self::AlreadyConsumed => "AlreadyConsumed",
226 Self::StreamLimitExceeded(_) => "StreamLimitExceeded",
227 Self::Unauthenticated(_) => "Unauthenticated",
228 Self::Unauthorized(_) => "Unauthorized",
229 Self::ValidationError(_) => "ValidationError",
230 Self::TemplateReload(_) => "TemplateReload",
231 Self::EndpointUri(_) => "EndpointUri",
232 }
233 }
234}
235
236impl From<std::io::Error> for CamelError {
237 fn from(err: std::io::Error) -> Self {
238 CamelError::Io(err.to_string())
239 }
240}
241
242impl From<crate::template::TemplateError> for CamelError {
243 fn from(err: crate::template::TemplateError) -> Self {
244 CamelError::Config(err.to_string())
245 }
246}
247
248impl From<ConfigValidationError> for CamelError {
249 fn from(e: ConfigValidationError) -> Self {
250 CamelError::ConfigValidation(e)
251 }
252}
253
254impl From<EndpointUriError> for CamelError {
255 fn from(e: EndpointUriError) -> Self {
256 CamelError::EndpointUri(e)
257 }
258}
259
260#[cfg(test)]
261mod tests {
262 use super::*;
263
264 fn all_error_samples() -> Vec<CamelError> {
265 vec![
266 CamelError::ComponentNotFound("x".to_string()),
267 CamelError::EndpointCreationFailed("x".to_string()),
268 CamelError::ProcessorError("x".to_string()),
269 CamelError::ProcessorErrorWithSource(
270 "x".to_string(),
271 Arc::new(std::io::Error::other("inner")),
272 ),
273 CamelError::TypeConversionFailed("x".to_string()),
274 CamelError::InvalidUri("x".to_string()),
275 CamelError::ChannelClosed,
276 CamelError::RouteError("x".to_string()),
277 CamelError::Io("x".to_string()),
278 CamelError::DeadLetterChannelFailed("x".to_string()),
279 CamelError::CircuitOpen("x".to_string()),
280 CamelError::HttpOperationFailed {
281 method: "GET".to_string(),
282 url: "https://example.com".to_string(),
283 status_code: 500,
284 status_text: "Internal Server Error".to_string(),
285 response_body: Some("error".to_string()),
286 },
287 CamelError::ConsumerStopping,
288 CamelError::Config("x".to_string()),
289 CamelError::ConfigValidation(ConfigValidationError::ThrottlerMaxRequestsZero),
290 CamelError::AlreadyConsumed,
291 CamelError::StreamLimitExceeded(42),
292 CamelError::Unauthenticated("token expired".to_string()),
293 CamelError::Unauthorized("missing admin role".to_string()),
294 CamelError::AuthProviderUnavailable("jwks down".to_string()),
295 CamelError::ValidationError("body does not match schema".to_string()),
296 CamelError::TemplateReload("reload failed".to_string()),
297 CamelError::EndpointUri(EndpointUriError::MissingScheme),
298 ]
299 }
300
301 #[test]
302 fn test_http_operation_failed_display() {
303 let err = CamelError::HttpOperationFailed {
304 method: "GET".to_string(),
305 url: "https://example.com/test".to_string(),
306 status_code: 404,
307 status_text: "Not Found".to_string(),
308 response_body: Some("page not found".to_string()),
309 };
310 let msg = format!("{err}");
311 assert!(msg.contains("404"));
312 assert!(msg.contains("Not Found"));
313 }
314
315 #[test]
316 fn test_http_operation_failed_clone() {
317 let err = CamelError::HttpOperationFailed {
318 method: "POST".to_string(),
319 url: "https://api.example.com/users".to_string(),
320 status_code: 500,
321 status_text: "Internal Server Error".to_string(),
322 response_body: None,
323 };
324 let cloned = err.clone();
325 assert!(matches!(
326 cloned,
327 CamelError::HttpOperationFailed {
328 status_code: 500,
329 ..
330 }
331 ));
332 }
333
334 #[test]
335 fn test_classify_maps_all_variants() {
336 assert_eq!(
337 CamelError::ComponentNotFound("x".to_string()).classify(),
338 "component"
339 );
340 assert_eq!(
341 CamelError::EndpointCreationFailed("x".to_string()).classify(),
342 "endpoint"
343 );
344 assert_eq!(
345 CamelError::ProcessorError("x".to_string()).classify(),
346 "processor"
347 );
348 assert_eq!(
349 CamelError::TypeConversionFailed("x".to_string()).classify(),
350 "type_conversion"
351 );
352 assert_eq!(
353 CamelError::InvalidUri("x".to_string()).classify(),
354 "endpoint"
355 );
356 assert_eq!(CamelError::ChannelClosed.classify(), "channel");
357 assert_eq!(CamelError::RouteError("x".to_string()).classify(), "route");
358 assert_eq!(CamelError::Io("x".to_string()).classify(), "io");
359 assert_eq!(
360 CamelError::DeadLetterChannelFailed("x".to_string()).classify(),
361 "dead_letter"
362 );
363 assert_eq!(
364 CamelError::CircuitOpen("x".to_string()).classify(),
365 "circuit_open"
366 );
367 assert_eq!(
368 CamelError::HttpOperationFailed {
369 method: "GET".to_string(),
370 url: "https://example.com".to_string(),
371 status_code: 500,
372 status_text: "Internal Server Error".to_string(),
373 response_body: None,
374 }
375 .classify(),
376 "http"
377 );
378 assert_eq!(CamelError::Config("x".to_string()).classify(), "config");
379 assert_eq!(
380 CamelError::ConfigValidation(ConfigValidationError::ThrottlerMaxRequestsZero)
381 .classify(),
382 "config"
383 );
384 assert_eq!(CamelError::AlreadyConsumed.classify(), "type_conversion");
385 assert_eq!(CamelError::StreamLimitExceeded(42).classify(), "stream");
386 assert_eq!(
387 CamelError::ValidationError("bad".to_string()).classify(),
388 "validation"
389 );
390 }
391
392 #[test]
393 fn test_classify_output_is_ascii_and_short() {
394 for error in all_error_samples() {
395 let class = error.classify();
396 assert!(class.is_ascii());
397 assert!(class.len() <= 15, "class too long: {class}");
398 }
399 }
400
401 #[test]
402 fn test_auth_variants_classify() {
403 assert_eq!(
404 CamelError::Unauthenticated("x".to_string()).classify(),
405 "unauthenticated"
406 );
407 assert_eq!(
408 CamelError::Unauthorized("x".to_string()).classify(),
409 "unauthorized"
410 );
411 }
412
413 #[test]
414 fn test_validation_error_classify() {
415 assert_eq!(
416 CamelError::ValidationError("bad".to_string()).classify(),
417 "validation"
418 );
419 }
420
421 #[test]
422 fn template_reload_classifies_as_template() {
423 let err = CamelError::TemplateReload("boom".into());
424 assert_eq!(err.classify(), "template");
425 }
426
427 #[test]
428 fn template_reload_variant_name() {
429 let err = CamelError::TemplateReload("boom".into());
430 assert_eq!(err.variant_name(), "TemplateReload");
431 }
432
433 #[test]
434 fn test_auth_variants_are_clone() {
435 let err = CamelError::Unauthenticated("test".to_string());
436 let cloned = err.clone();
437 assert!(matches!(cloned, CamelError::Unauthenticated(_)));
438
439 let err2 = CamelError::Unauthorized("test".to_string());
440 let cloned2 = err2.clone();
441 assert!(matches!(cloned2, CamelError::Unauthorized(_)));
442 }
443
444 #[test]
445 fn auth_provider_unavailable_display_carries_detail() {
446 let err = CamelError::AuthProviderUnavailable("conn refused".into());
447 let msg = err.to_string();
448 assert!(msg.contains("conn refused"));
449 assert!(
450 msg.starts_with("Auth provider unavailable"),
451 "display should start with 'Auth provider unavailable', got: {msg}"
452 );
453 }
454}
455
456#[cfg(test)]
457mod variant_name_tests {
458 use super::{CamelError, ConfigValidationError, EndpointUriError};
459 use std::sync::Arc;
460
461 #[test]
466 fn variant_name_covers_all_variants() {
467 let cases: Vec<(CamelError, &str)> = vec![
468 (
469 CamelError::ComponentNotFound("x".into()),
470 "ComponentNotFound",
471 ),
472 (
473 CamelError::EndpointCreationFailed("x".into()),
474 "EndpointCreationFailed",
475 ),
476 (CamelError::ProcessorError("x".into()), "ProcessorError"),
477 (
478 CamelError::ProcessorErrorWithSource(
479 "x".into(),
480 Arc::new(std::io::Error::other("y")),
481 ),
482 "ProcessorError", ),
484 (
485 CamelError::TypeConversionFailed("x".into()),
486 "TypeConversionFailed",
487 ),
488 (CamelError::InvalidUri("x".into()), "InvalidUri"),
489 (CamelError::ChannelClosed, "ChannelClosed"),
490 (CamelError::RouteError("x".into()), "RouteError"),
491 (CamelError::Io("x".into()), "Io"),
492 (
493 CamelError::DeadLetterChannelFailed("x".into()),
494 "DeadLetterChannelFailed",
495 ),
496 (CamelError::CircuitOpen("x".into()), "CircuitOpen"),
497 (
498 CamelError::HttpOperationFailed {
499 method: "GET".into(),
500 url: "https://example.com".into(),
501 status_code: 500,
502 status_text: "Internal Server Error".into(),
503 response_body: None,
504 },
505 "HttpOperationFailed",
506 ),
507 (CamelError::Config("x".into()), "Config"),
508 (
509 CamelError::ConfigValidation(ConfigValidationError::ThrottlerMaxRequestsZero),
510 "ConfigValidation",
511 ),
512 (CamelError::AlreadyConsumed, "AlreadyConsumed"),
513 (CamelError::StreamLimitExceeded(42), "StreamLimitExceeded"),
514 (CamelError::Unauthenticated("x".into()), "Unauthenticated"),
515 (CamelError::Unauthorized("x".into()), "Unauthorized"),
516 (CamelError::ValidationError("bad".into()), "ValidationError"),
517 (CamelError::TemplateReload("x".into()), "TemplateReload"),
518 (
519 CamelError::EndpointUri(EndpointUriError::MissingScheme),
520 "EndpointUri",
521 ),
522 (
523 CamelError::AuthProviderUnavailable("x".into()),
524 "ProcessorError",
525 ),
526 ];
527
528 for (err, expected) in cases {
529 assert_eq!(
530 err.variant_name(),
531 expected,
532 "variant_name mismatch for {:?}",
533 err
534 );
535 }
536 }
537
538 #[test]
539 fn auth_provider_unavailable_classifies_as_processor() {
540 let err = CamelError::AuthProviderUnavailable("jwks down".into());
541 assert_eq!(err.classify(), "processor");
542 }
543
544 #[test]
545 fn auth_provider_unavailable_variant_name_aliases_processor_error() {
546 let err = CamelError::AuthProviderUnavailable("jwks down".into());
547 assert_eq!(err.variant_name(), "ProcessorError");
548 }
549}