1use std::fmt;
2use std::sync::Arc;
3use thiserror::Error;
4
5#[derive(Debug, Clone, PartialEq, Eq, Error)]
11#[non_exhaustive]
12pub enum ConfigValidationError {
13 #[error(
14 "aggregator config requires at least one completion bound (size, timeout, predicate, or interval)"
15 )]
16 AggregatorMissingCompletionBound,
17
18 #[error("aggregator requires at least one of max_buckets, completionTimeout, or bucket_ttl")]
23 AggregatorMissingMemoryBound,
24
25 #[error(
32 "aggregator Timeout completion requires bucket_ttl (memory-release bound for the timeout-task cap fallback)"
33 )]
34 AggregatorTimeoutRequiresTtl,
35
36 #[error("throttler max_requests must be > 0")]
37 ThrottlerMaxRequestsZero,
38
39 #[error("loop step must specify either 'count' or 'while', not both")]
40 LoopConflictingCountAndWhile,
41
42 #[error("on_exceptions clause cannot set both steps and handled_by (delegation is exclusive)")]
43 OnExceptionStepsHandledByConflict,
44
45 #[error("loop step must specify either 'count' or 'while'")]
46 LoopMissingCountOrWhile,
47
48 #[error("SQL use_message_body_for_sql requires allow_dynamic_query=true")]
49 SqlDynamicQueryWithoutAllowDynamic,
50}
51
52#[derive(Debug, Clone, PartialEq, Eq, Error)]
58#[non_exhaustive]
59pub enum EndpointUriError {
60 #[error(
62 "endpoint URI parameter `{key}` duplicates a key already present in the base URI query"
63 )]
64 DuplicateKey { key: String },
65
66 #[error("endpoint URI is missing a scheme (expected `scheme:path`)")]
68 MissingScheme,
69
70 #[error("endpoint URI query contains a pair with an empty key")]
72 EmptyQueryKey,
73
74 #[error("endpoint URI parameter key `{key}` is empty or contains a reserved character")]
76 InvalidParamKey { key: String },
77}
78
79#[derive(Debug)]
131pub struct OpaqueErrorSource(Arc<dyn std::error::Error + Send + Sync>);
132
133impl OpaqueErrorSource {
134 pub fn new(source: Arc<dyn std::error::Error + Send + Sync>) -> Self {
136 Self(source)
137 }
138
139 fn clone_handle(&self) -> Self {
143 Self(Arc::clone(&self.0))
144 }
145}
146
147impl fmt::Display for OpaqueErrorSource {
148 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
149 self.0.fmt(f)
150 }
151}
152
153impl std::error::Error for OpaqueErrorSource {
154 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
155 Some(self.0.as_ref())
158 }
159}
160
161#[derive(Debug, Error)]
163#[non_exhaustive]
164pub enum CamelError {
165 #[error("Component not found: {0}")]
166 ComponentNotFound(String),
167
168 #[error("Endpoint creation failed: {0}")]
169 EndpointCreationFailed(String),
170
171 #[error("Endpoint creation failed: {0}")]
174 EndpointCreationFailedWithSource(String, #[source] OpaqueErrorSource),
175
176 #[error("Processor error: {0}")]
177 ProcessorError(String),
178
179 #[error("Processor error: {0}")]
182 ProcessorErrorWithSource(String, #[source] Arc<dyn std::error::Error + Send + Sync>),
183
184 #[error("Type conversion failed: {0}")]
185 TypeConversionFailed(String),
186
187 #[error("Invalid URI: {0}")]
188 InvalidUri(String),
189
190 #[error("Channel closed")]
191 ChannelClosed,
192
193 #[error("Route error: {0}")]
194 RouteError(String),
195
196 #[error("IO error: {0}")]
197 Io(String),
198
199 #[error("Dead letter channel failed: {0}")]
200 DeadLetterChannelFailed(String),
201
202 #[error("Circuit breaker open: {0}")]
203 CircuitOpen(String),
204
205 #[error("HTTP {method} {url} failed: {status_code} {status_text}")]
206 HttpOperationFailed {
207 method: String,
208 url: String,
209 status_code: u16,
210 status_text: String,
211 response_body: Option<String>,
212 },
213
214 #[error("Consumer stopping: semaphore closed during call")]
218 ConsumerStopping,
219
220 #[error("Configuration error: {0}")]
221 Config(String),
222
223 #[error("Configuration validation error: {0}")]
227 ConfigValidation(ConfigValidationError),
228
229 #[error("Body stream has already been consumed")]
230 AlreadyConsumed,
231
232 #[error("Stream size exceeded limit: {0}")]
233 StreamLimitExceeded(usize),
234
235 #[error("Unauthenticated: {0}")]
236 Unauthenticated(String),
237
238 #[error("Unauthorized: {0}")]
239 Unauthorized(String),
240
241 #[error("Auth provider unavailable: {0}")]
245 AuthProviderUnavailable(String),
246
247 #[error("Validation failed: {0}")]
248 ValidationError(String),
249
250 #[error("Template reload failed: {0}")]
251 TemplateReload(String),
252
253 #[error("Endpoint URI error: {0}")]
257 EndpointUri(EndpointUriError),
258
259 #[error("Unsupported media type: consumed {consumed}, declared {declared}")]
262 UnsupportedMediaType { consumed: String, declared: String },
263
264 #[error("Not acceptable: accept {accept}, produced {produced}")]
267 NotAcceptable { accept: String, produced: String },
268}
269
270impl Clone for CamelError {
276 fn clone(&self) -> Self {
277 match self {
278 Self::ComponentNotFound(msg) => Self::ComponentNotFound(msg.clone()),
279 Self::EndpointCreationFailed(msg) => Self::EndpointCreationFailed(msg.clone()),
280 Self::EndpointCreationFailedWithSource(msg, source) => {
281 Self::EndpointCreationFailedWithSource(msg.clone(), source.clone_handle())
282 }
283 Self::ProcessorError(msg) => Self::ProcessorError(msg.clone()),
284 Self::ProcessorErrorWithSource(msg, source) => {
285 Self::ProcessorErrorWithSource(msg.clone(), Arc::clone(source))
286 }
287 Self::TypeConversionFailed(msg) => Self::TypeConversionFailed(msg.clone()),
288 Self::InvalidUri(msg) => Self::InvalidUri(msg.clone()),
289 Self::ChannelClosed => Self::ChannelClosed,
290 Self::RouteError(msg) => Self::RouteError(msg.clone()),
291 Self::Io(msg) => Self::Io(msg.clone()),
292 Self::DeadLetterChannelFailed(msg) => Self::DeadLetterChannelFailed(msg.clone()),
293 Self::CircuitOpen(msg) => Self::CircuitOpen(msg.clone()),
294 Self::HttpOperationFailed {
295 method,
296 url,
297 status_code,
298 status_text,
299 response_body,
300 } => Self::HttpOperationFailed {
301 method: method.clone(),
302 url: url.clone(),
303 status_code: *status_code,
304 status_text: status_text.clone(),
305 response_body: response_body.clone(),
306 },
307 Self::ConsumerStopping => Self::ConsumerStopping,
308 Self::Config(msg) => Self::Config(msg.clone()),
309 Self::ConfigValidation(e) => Self::ConfigValidation(e.clone()),
310 Self::AlreadyConsumed => Self::AlreadyConsumed,
311 Self::StreamLimitExceeded(limit) => Self::StreamLimitExceeded(*limit),
312 Self::Unauthenticated(msg) => Self::Unauthenticated(msg.clone()),
313 Self::Unauthorized(msg) => Self::Unauthorized(msg.clone()),
314 Self::AuthProviderUnavailable(msg) => Self::AuthProviderUnavailable(msg.clone()),
315 Self::ValidationError(msg) => Self::ValidationError(msg.clone()),
316 Self::TemplateReload(msg) => Self::TemplateReload(msg.clone()),
317 Self::EndpointUri(e) => Self::EndpointUri(e.clone()),
318 Self::UnsupportedMediaType { consumed, declared } => Self::UnsupportedMediaType {
319 consumed: consumed.clone(),
320 declared: declared.clone(),
321 },
322 Self::NotAcceptable { accept, produced } => Self::NotAcceptable {
323 accept: accept.clone(),
324 produced: produced.clone(),
325 },
326 }
327 }
328}
329
330pub const CIRCUIT_OPEN: &str = "circuit_open";
336
337impl CamelError {
338 pub fn classify(&self) -> &'static str {
339 #[allow(unreachable_patterns)]
340 match self {
341 Self::ComponentNotFound(_) => "component",
342 Self::EndpointCreationFailed(_)
343 | Self::EndpointCreationFailedWithSource(_, _)
344 | Self::InvalidUri(_)
345 | Self::EndpointUri(_) => "endpoint",
346 Self::ProcessorError(_)
347 | Self::ProcessorErrorWithSource(_, _)
348 | Self::AuthProviderUnavailable(_) => "processor",
349 Self::TypeConversionFailed(_) | Self::AlreadyConsumed => "type_conversion",
350 Self::Io(_) => "io",
351 Self::RouteError(_) => "route",
352 Self::CircuitOpen(_) => CIRCUIT_OPEN,
353 Self::HttpOperationFailed { .. } => "http",
354 Self::Config(_) | Self::ConfigValidation(_) => "config",
355 Self::DeadLetterChannelFailed(_) => "dead_letter",
356 Self::ConsumerStopping => "consumer_stop",
357 Self::StreamLimitExceeded(_) => "stream",
358 Self::ChannelClosed => "channel",
359 Self::Unauthenticated(_) => "unauthenticated",
360 Self::Unauthorized(_) => "unauthorized",
361 Self::ValidationError(_) => "validation",
362 Self::TemplateReload(_) => "template",
363 Self::UnsupportedMediaType { .. } => "unsupported_media_type",
364 Self::NotAcceptable { .. } => "not_acceptable",
365 _ => "unknown",
366 }
367 }
368
369 pub fn variant_name(&self) -> &'static str {
381 match self {
382 Self::ComponentNotFound(_) => "ComponentNotFound",
383 Self::EndpointCreationFailed(_) => "EndpointCreationFailed",
384 Self::EndpointCreationFailedWithSource(_, _) => "EndpointCreationFailed",
385 Self::ProcessorError(_) => "ProcessorError",
386 Self::ProcessorErrorWithSource(_, _) => "ProcessorError",
387 Self::AuthProviderUnavailable(_) => "ProcessorError",
388 Self::TypeConversionFailed(_) => "TypeConversionFailed",
389 Self::InvalidUri(_) => "InvalidUri",
390 Self::ChannelClosed => "ChannelClosed",
391 Self::RouteError(_) => "RouteError",
392 Self::Io(_) => "Io",
393 Self::DeadLetterChannelFailed(_) => "DeadLetterChannelFailed",
394 Self::CircuitOpen(_) => "CircuitOpen",
395 Self::HttpOperationFailed { .. } => "HttpOperationFailed",
396 Self::ConsumerStopping => "ConsumerStopping",
397 Self::Config(_) => "Config",
398 Self::ConfigValidation(_) => "ConfigValidation",
399 Self::AlreadyConsumed => "AlreadyConsumed",
400 Self::StreamLimitExceeded(_) => "StreamLimitExceeded",
401 Self::Unauthenticated(_) => "Unauthenticated",
402 Self::Unauthorized(_) => "Unauthorized",
403 Self::ValidationError(_) => "ValidationError",
404 Self::TemplateReload(_) => "TemplateReload",
405 Self::EndpointUri(_) => "EndpointUri",
406 Self::UnsupportedMediaType { .. } => "UnsupportedMediaType",
407 Self::NotAcceptable { .. } => "NotAcceptable",
408 }
409 }
410}
411
412impl From<std::io::Error> for CamelError {
413 fn from(err: std::io::Error) -> Self {
414 CamelError::Io(err.to_string())
415 }
416}
417
418impl From<crate::template::TemplateError> for CamelError {
419 fn from(err: crate::template::TemplateError) -> Self {
420 CamelError::Config(err.to_string())
421 }
422}
423
424impl From<ConfigValidationError> for CamelError {
425 fn from(e: ConfigValidationError) -> Self {
426 CamelError::ConfigValidation(e)
427 }
428}
429
430impl From<EndpointUriError> for CamelError {
431 fn from(e: EndpointUriError) -> Self {
432 CamelError::EndpointUri(e)
433 }
434}
435
436#[cfg(test)]
437mod tests {
438 use super::*;
439 use std::error::Error as _;
442
443 #[derive(Debug)]
445 struct SampleSource;
446
447 impl fmt::Display for SampleSource {
448 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
449 f.write_str("sample source")
450 }
451 }
452
453 impl std::error::Error for SampleSource {}
454
455 fn all_error_samples() -> Vec<CamelError> {
456 vec![
457 CamelError::ComponentNotFound("x".to_string()),
458 CamelError::EndpointCreationFailed("x".to_string()),
459 CamelError::EndpointCreationFailedWithSource(
460 "x".to_string(),
461 OpaqueErrorSource::new(Arc::new(SampleSource)),
462 ),
463 CamelError::ProcessorError("x".to_string()),
464 CamelError::ProcessorErrorWithSource(
465 "x".to_string(),
466 Arc::new(std::io::Error::other("inner")),
467 ),
468 CamelError::TypeConversionFailed("x".to_string()),
469 CamelError::InvalidUri("x".to_string()),
470 CamelError::ChannelClosed,
471 CamelError::RouteError("x".to_string()),
472 CamelError::Io("x".to_string()),
473 CamelError::DeadLetterChannelFailed("x".to_string()),
474 CamelError::CircuitOpen("x".to_string()),
475 CamelError::HttpOperationFailed {
476 method: "GET".to_string(),
477 url: "https://example.com".to_string(),
478 status_code: 500,
479 status_text: "Internal Server Error".to_string(),
480 response_body: Some("error".to_string()),
481 },
482 CamelError::ConsumerStopping,
483 CamelError::Config("x".to_string()),
484 CamelError::ConfigValidation(ConfigValidationError::ThrottlerMaxRequestsZero),
485 CamelError::AlreadyConsumed,
486 CamelError::StreamLimitExceeded(42),
487 CamelError::Unauthenticated("token expired".to_string()),
488 CamelError::Unauthorized("missing admin role".to_string()),
489 CamelError::AuthProviderUnavailable("jwks down".to_string()),
490 CamelError::ValidationError("body does not match schema".to_string()),
491 CamelError::TemplateReload("reload failed".to_string()),
492 CamelError::EndpointUri(EndpointUriError::MissingScheme),
493 CamelError::UnsupportedMediaType {
494 consumed: "text/plain".to_string(),
495 declared: "application/json".to_string(),
496 },
497 CamelError::NotAcceptable {
498 accept: "application/xml".to_string(),
499 produced: "application/json".to_string(),
500 },
501 ]
502 }
503
504 #[test]
505 fn test_http_operation_failed_display() {
506 let err = CamelError::HttpOperationFailed {
507 method: "GET".to_string(),
508 url: "https://example.com/test".to_string(),
509 status_code: 404,
510 status_text: "Not Found".to_string(),
511 response_body: Some("page not found".to_string()),
512 };
513 let msg = format!("{err}");
514 assert!(msg.contains("404"));
515 assert!(msg.contains("Not Found"));
516 }
517
518 #[test]
519 fn test_http_operation_failed_clone() {
520 let err = CamelError::HttpOperationFailed {
521 method: "POST".to_string(),
522 url: "https://api.example.com/users".to_string(),
523 status_code: 500,
524 status_text: "Internal Server Error".to_string(),
525 response_body: None,
526 };
527 let cloned = err.clone();
528 assert!(matches!(
529 cloned,
530 CamelError::HttpOperationFailed {
531 status_code: 500,
532 ..
533 }
534 ));
535 }
536
537 #[test]
538 fn test_classify_maps_all_variants() {
539 assert_eq!(
540 CamelError::ComponentNotFound("x".to_string()).classify(),
541 "component"
542 );
543 assert_eq!(
544 CamelError::EndpointCreationFailed("x".to_string()).classify(),
545 "endpoint"
546 );
547 assert_eq!(
548 CamelError::ProcessorError("x".to_string()).classify(),
549 "processor"
550 );
551 assert_eq!(
552 CamelError::TypeConversionFailed("x".to_string()).classify(),
553 "type_conversion"
554 );
555 assert_eq!(
556 CamelError::InvalidUri("x".to_string()).classify(),
557 "endpoint"
558 );
559 assert_eq!(CamelError::ChannelClosed.classify(), "channel");
560 assert_eq!(CamelError::RouteError("x".to_string()).classify(), "route");
561 assert_eq!(CamelError::Io("x".to_string()).classify(), "io");
562 assert_eq!(
563 CamelError::DeadLetterChannelFailed("x".to_string()).classify(),
564 "dead_letter"
565 );
566 assert_eq!(
567 CamelError::CircuitOpen("x".to_string()).classify(),
568 "circuit_open"
569 );
570 assert_eq!(
571 CamelError::HttpOperationFailed {
572 method: "GET".to_string(),
573 url: "https://example.com".to_string(),
574 status_code: 500,
575 status_text: "Internal Server Error".to_string(),
576 response_body: None,
577 }
578 .classify(),
579 "http"
580 );
581 assert_eq!(CamelError::Config("x".to_string()).classify(), "config");
582 assert_eq!(
583 CamelError::ConfigValidation(ConfigValidationError::ThrottlerMaxRequestsZero)
584 .classify(),
585 "config"
586 );
587 assert_eq!(CamelError::AlreadyConsumed.classify(), "type_conversion");
588 assert_eq!(CamelError::StreamLimitExceeded(42).classify(), "stream");
589 assert_eq!(
590 CamelError::ValidationError("bad".to_string()).classify(),
591 "validation"
592 );
593 }
594
595 #[test]
596 fn test_classify_output_is_ascii_and_short() {
597 for error in all_error_samples() {
598 let class = error.classify();
599 assert!(class.is_ascii());
600 assert!(class.len() <= 22, "class too long: {class}");
602 }
603 }
604
605 #[test]
606 fn test_auth_variants_classify() {
607 assert_eq!(
608 CamelError::Unauthenticated("x".to_string()).classify(),
609 "unauthenticated"
610 );
611 assert_eq!(
612 CamelError::Unauthorized("x".to_string()).classify(),
613 "unauthorized"
614 );
615 }
616
617 #[test]
618 fn test_validation_error_classify() {
619 assert_eq!(
620 CamelError::ValidationError("bad".to_string()).classify(),
621 "validation"
622 );
623 }
624
625 #[test]
626 fn template_reload_classifies_as_template() {
627 let err = CamelError::TemplateReload("boom".into());
628 assert_eq!(err.classify(), "template");
629 }
630
631 #[test]
632 fn template_reload_variant_name() {
633 let err = CamelError::TemplateReload("boom".into());
634 assert_eq!(err.variant_name(), "TemplateReload");
635 }
636
637 #[test]
638 fn test_auth_variants_are_clone() {
639 let err = CamelError::Unauthenticated("test".to_string());
640 let cloned = err.clone();
641 assert!(matches!(cloned, CamelError::Unauthenticated(_)));
642
643 let err2 = CamelError::Unauthorized("test".to_string());
644 let cloned2 = err2.clone();
645 assert!(matches!(cloned2, CamelError::Unauthorized(_)));
646 }
647
648 #[test]
649 fn classification_unchanged_for_callers() {
650 assert_eq!(
655 CamelError::CircuitOpen("breaker open".into()).classify(),
656 "circuit_open"
657 );
658 }
659
660 #[test]
661 fn auth_provider_unavailable_display_carries_detail() {
662 let err = CamelError::AuthProviderUnavailable("conn refused".into());
663 let msg = err.to_string();
664 assert!(msg.contains("conn refused"));
665 assert!(
666 msg.starts_with("Auth provider unavailable"),
667 "display should start with 'Auth provider unavailable', got: {msg}"
668 );
669 }
670
671 #[test]
672 fn classify_negotiation_errors() {
673 let unsupported = CamelError::UnsupportedMediaType {
674 consumed: "text/plain".into(),
675 declared: "application/json".into(),
676 };
677 let not_acceptable = CamelError::NotAcceptable {
678 accept: "application/xml".into(),
679 produced: "application/json".into(),
680 };
681 assert_eq!(unsupported.classify(), "unsupported_media_type");
682 assert_eq!(not_acceptable.classify(), "not_acceptable");
683 }
684
685 #[test]
686 fn variant_names_negotiation_errors() {
687 let unsupported = CamelError::UnsupportedMediaType {
688 consumed: "text/plain".into(),
689 declared: "application/json".into(),
690 };
691 let not_acceptable = CamelError::NotAcceptable {
692 accept: "application/xml".into(),
693 produced: "application/json".into(),
694 };
695 assert_eq!(unsupported.variant_name(), "UnsupportedMediaType");
696 assert_eq!(not_acceptable.variant_name(), "NotAcceptable");
697 }
698
699 #[test]
700 fn display_negotiation_errors() {
701 let unsupported = CamelError::UnsupportedMediaType {
702 consumed: "text/plain".into(),
703 declared: "application/json".into(),
704 };
705 let not_acceptable = CamelError::NotAcceptable {
706 accept: "application/xml".into(),
707 produced: "application/json".into(),
708 };
709 let unsupported_msg = unsupported.to_string();
710 assert!(unsupported_msg.contains("text/plain"));
711 assert!(unsupported_msg.contains("application/json"));
712 let not_acceptable_msg = not_acceptable.to_string();
713 assert!(not_acceptable_msg.contains("application/xml"));
714 assert!(not_acceptable_msg.contains("application/json"));
715 }
716
717 #[test]
718 fn config_validation_error_on_exception_conflict_display() {
719 let err = ConfigValidationError::OnExceptionStepsHandledByConflict;
720 let msg = format!("{err}");
721 assert!(msg.contains("steps"));
722 assert!(msg.contains("handled_by"));
723 assert!(msg.contains("exclusive"));
724 }
725
726 #[test]
727 fn opaque_error_source_exposes_only_pointee() {
728 let src = OpaqueErrorSource::new(Arc::new(SampleSource));
729 let pointee = src.source().unwrap();
730 assert!(pointee.downcast_ref::<SampleSource>().is_some());
731 }
732
733 #[test]
734 fn endpoint_creation_failed_with_source_aliases_to_plain() {
735 let e = CamelError::EndpointCreationFailedWithSource(
736 "d".to_string(),
737 OpaqueErrorSource::new(Arc::new(SampleSource)),
738 );
739 assert_eq!(e.variant_name(), "EndpointCreationFailed");
740 assert_eq!(e.classify(), "endpoint");
741 assert_eq!(e.to_string(), "Endpoint creation failed: d");
742 }
743
744 #[test]
745 fn clone_preserves_variant_identity_for_all_error_samples() {
746 for e in all_error_samples() {
747 let c = e.clone();
748 assert_eq!(c.variant_name(), e.variant_name());
749 assert_eq!(c.classify(), e.classify());
750 assert_eq!(c.to_string(), e.to_string());
751 }
752 }
753
754 #[test]
755 fn camel_error_clone_preserves_source_provenance() {
756 let e = CamelError::EndpointCreationFailedWithSource(
757 "d".to_string(),
758 OpaqueErrorSource::new(Arc::new(SampleSource)),
759 );
760 let c = e.clone();
761 let wrapper = c.source().unwrap();
765 let pointee = wrapper.source().unwrap();
766 assert!(pointee.downcast_ref::<SampleSource>().is_some());
767 }
768}
769
770#[cfg(test)]
771mod variant_name_tests {
772 use super::{CamelError, ConfigValidationError, EndpointUriError, OpaqueErrorSource};
773 use std::sync::Arc;
774
775 #[test]
786 fn variant_name_covers_all_variants() {
787 let cases: Vec<(CamelError, &str)> = vec![
788 (
789 CamelError::ComponentNotFound("x".into()),
790 "ComponentNotFound",
791 ),
792 (
793 CamelError::EndpointCreationFailed("x".into()),
794 "EndpointCreationFailed",
795 ),
796 (
797 CamelError::EndpointCreationFailedWithSource(
798 "x".into(),
799 OpaqueErrorSource::new(Arc::new(std::io::Error::other("y"))),
800 ),
801 "EndpointCreationFailed", ),
803 (CamelError::ProcessorError("x".into()), "ProcessorError"),
804 (
805 CamelError::ProcessorErrorWithSource(
806 "x".into(),
807 Arc::new(std::io::Error::other("y")),
808 ),
809 "ProcessorError", ),
811 (
812 CamelError::TypeConversionFailed("x".into()),
813 "TypeConversionFailed",
814 ),
815 (CamelError::InvalidUri("x".into()), "InvalidUri"),
816 (CamelError::ChannelClosed, "ChannelClosed"),
817 (CamelError::RouteError("x".into()), "RouteError"),
818 (CamelError::Io("x".into()), "Io"),
819 (
820 CamelError::DeadLetterChannelFailed("x".into()),
821 "DeadLetterChannelFailed",
822 ),
823 (CamelError::CircuitOpen("x".into()), "CircuitOpen"),
824 (
825 CamelError::HttpOperationFailed {
826 method: "GET".into(),
827 url: "https://example.com".into(),
828 status_code: 500,
829 status_text: "Internal Server Error".into(),
830 response_body: None,
831 },
832 "HttpOperationFailed",
833 ),
834 (CamelError::ConsumerStopping, "ConsumerStopping"),
835 (CamelError::Config("x".into()), "Config"),
836 (
837 CamelError::ConfigValidation(ConfigValidationError::ThrottlerMaxRequestsZero),
838 "ConfigValidation",
839 ),
840 (CamelError::AlreadyConsumed, "AlreadyConsumed"),
841 (CamelError::StreamLimitExceeded(42), "StreamLimitExceeded"),
842 (CamelError::Unauthenticated("x".into()), "Unauthenticated"),
843 (CamelError::Unauthorized("x".into()), "Unauthorized"),
844 (CamelError::ValidationError("bad".into()), "ValidationError"),
845 (CamelError::TemplateReload("x".into()), "TemplateReload"),
846 (
847 CamelError::EndpointUri(EndpointUriError::MissingScheme),
848 "EndpointUri",
849 ),
850 (
851 CamelError::UnsupportedMediaType {
852 consumed: "text/plain".into(),
853 declared: "application/json".into(),
854 },
855 "UnsupportedMediaType",
856 ),
857 (
858 CamelError::NotAcceptable {
859 accept: "application/xml".into(),
860 produced: "application/json".into(),
861 },
862 "NotAcceptable",
863 ),
864 (
865 CamelError::AuthProviderUnavailable("x".into()),
866 "ProcessorError",
867 ),
868 ];
869
870 assert_eq!(
871 cases.len(),
872 26,
873 "variant_name_covers_all_variants must cover every CamelError variant; \
874 extend this table and the camel-dsl classification guard"
875 );
876
877 for (err, expected) in cases {
878 assert_eq!(
879 err.variant_name(),
880 expected,
881 "variant_name mismatch for {:?}",
882 err
883 );
884 }
885 }
886
887 #[test]
888 fn auth_provider_unavailable_classifies_as_processor() {
889 let err = CamelError::AuthProviderUnavailable("jwks down".into());
890 assert_eq!(err.classify(), "processor");
891 }
892
893 #[test]
894 fn auth_provider_unavailable_variant_name_aliases_processor_error() {
895 let err = CamelError::AuthProviderUnavailable("jwks down".into());
896 assert_eq!(err.variant_name(), "ProcessorError");
897 }
898}