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(Clone, Copy, Debug, PartialEq, Eq)]
86#[non_exhaustive]
87pub enum ExpressionErrorClass {
88 Runtime,
90 Arithmetic,
92 TypeMismatch,
94 FunctionNotFound,
96 Limit,
98 Timeout,
100 Conversion,
102 Parse,
104}
105
106impl ExpressionErrorClass {
107 pub fn as_str(&self) -> &'static str {
109 match self {
110 Self::Runtime => "runtime",
111 Self::Arithmetic => "arithmetic",
112 Self::TypeMismatch => "type-mismatch",
113 Self::FunctionNotFound => "function-not-found",
114 Self::Limit => "limit",
115 Self::Timeout => "timeout",
116 Self::Conversion => "conversion",
117 Self::Parse => "parse",
118 }
119 }
120}
121
122impl fmt::Display for ExpressionErrorClass {
123 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
124 f.write_str(self.as_str())
125 }
126}
127
128#[derive(Clone, Copy, Debug, PartialEq, Eq)]
131pub struct ErrorPosition {
132 pub line: u32,
134 pub column: u32,
136}
137
138impl fmt::Display for ErrorPosition {
139 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
140 write!(f, "{}:{}", self.line, self.column)
141 }
142}
143
144#[derive(Clone, Debug, PartialEq, Eq)]
149pub struct ConversionDetail {
150 pub source_type: String,
152 pub target: String,
154}
155
156fn expression_position_suffix(position: &Option<ErrorPosition>) -> String {
158 match position {
159 Some(pos) => format!(" at {pos}"),
160 None => String::new(),
161 }
162}
163
164fn expression_conversion_suffix(conversion: &Option<ConversionDetail>) -> String {
167 match conversion {
168 Some(detail) => format!(
169 " while converting {} to {}",
170 detail.source_type, detail.target
171 ),
172 None => String::new(),
173 }
174}
175
176#[derive(Debug)]
228pub struct OpaqueErrorSource(Arc<dyn std::error::Error + Send + Sync>);
229
230impl OpaqueErrorSource {
231 pub fn new(source: Arc<dyn std::error::Error + Send + Sync>) -> Self {
233 Self(source)
234 }
235
236 fn clone_handle(&self) -> Self {
240 Self(Arc::clone(&self.0))
241 }
242}
243
244impl fmt::Display for OpaqueErrorSource {
245 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
246 self.0.fmt(f)
247 }
248}
249
250impl std::error::Error for OpaqueErrorSource {
251 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
252 Some(self.0.as_ref())
255 }
256}
257
258#[derive(Debug, Error)]
260#[non_exhaustive]
261pub enum CamelError {
262 #[error("Component not found: {0}")]
263 ComponentNotFound(String),
264
265 #[error("Endpoint creation failed: {0}")]
266 EndpointCreationFailed(String),
267
268 #[error("Endpoint creation failed: {0}")]
271 EndpointCreationFailedWithSource(String, #[source] OpaqueErrorSource),
272
273 #[error("Processor error: {0}")]
274 ProcessorError(String),
275
276 #[error("Processor error: {0}")]
279 ProcessorErrorWithSource(String, #[source] Arc<dyn std::error::Error + Send + Sync>),
280
281 #[error("Type conversion failed: {0}")]
282 TypeConversionFailed(String),
283
284 #[error("Invalid URI: {0}")]
285 InvalidUri(String),
286
287 #[error("Channel closed")]
288 ChannelClosed,
289
290 #[error("Route error: {0}")]
291 RouteError(String),
292
293 #[error("IO error: {0}")]
294 Io(String),
295
296 #[error("Dead letter channel failed: {0}")]
297 DeadLetterChannelFailed(String),
298
299 #[error("Circuit breaker open: {0}")]
300 CircuitOpen(String),
301
302 #[error("HTTP {method} {url} failed: {status_code} {status_text}")]
303 HttpOperationFailed {
304 method: String,
305 url: String,
306 status_code: u16,
307 status_text: String,
308 response_body: Option<String>,
309 },
310
311 #[error("Consumer stopping: semaphore closed during call")]
315 ConsumerStopping,
316
317 #[error("Configuration error: {0}")]
318 Config(String),
319
320 #[error("Configuration validation error: {0}")]
324 ConfigValidation(ConfigValidationError),
325
326 #[error("Body stream has already been consumed")]
327 AlreadyConsumed,
328
329 #[error("Stream size exceeded limit: {0}")]
330 StreamLimitExceeded(usize),
331
332 #[error("Unauthenticated: {0}")]
333 Unauthenticated(String),
334
335 #[error("Unauthorized: {0}")]
336 Unauthorized(String),
337
338 #[error("Auth provider unavailable: {0}")]
342 AuthProviderUnavailable(String),
343
344 #[error("Validation failed: {0}")]
345 ValidationError(String),
346
347 #[error("Template reload failed: {0}")]
348 TemplateReload(String),
349
350 #[error("Endpoint URI error: {0}")]
354 EndpointUri(EndpointUriError),
355
356 #[error("Unsupported media type: consumed {consumed}, declared {declared}")]
359 UnsupportedMediaType { consumed: String, declared: String },
360
361 #[error("Not acceptable: accept {accept}, produced {produced}")]
364 NotAcceptable { accept: String, produced: String },
365
366 #[error(
370 "expression failed: {class} in {language} `{verb}` step `{step_id}` (route `{route_id}`){}{}",
371 expression_position_suffix(.position),
372 expression_conversion_suffix(.conversion),
373 )]
374 ExpressionFailed {
375 language: String,
377 route_id: String,
379 step_id: String,
381 verb: String,
383 class: ExpressionErrorClass,
385 position: Option<ErrorPosition>,
387 conversion: Option<ConversionDetail>,
389 #[source]
393 cause: Option<Box<CamelError>>,
394 },
395}
396
397impl Clone for CamelError {
403 fn clone(&self) -> Self {
404 match self {
405 Self::ComponentNotFound(msg) => Self::ComponentNotFound(msg.clone()),
406 Self::EndpointCreationFailed(msg) => Self::EndpointCreationFailed(msg.clone()),
407 Self::EndpointCreationFailedWithSource(msg, source) => {
408 Self::EndpointCreationFailedWithSource(msg.clone(), source.clone_handle())
409 }
410 Self::ProcessorError(msg) => Self::ProcessorError(msg.clone()),
411 Self::ProcessorErrorWithSource(msg, source) => {
412 Self::ProcessorErrorWithSource(msg.clone(), Arc::clone(source))
413 }
414 Self::TypeConversionFailed(msg) => Self::TypeConversionFailed(msg.clone()),
415 Self::InvalidUri(msg) => Self::InvalidUri(msg.clone()),
416 Self::ChannelClosed => Self::ChannelClosed,
417 Self::RouteError(msg) => Self::RouteError(msg.clone()),
418 Self::Io(msg) => Self::Io(msg.clone()),
419 Self::DeadLetterChannelFailed(msg) => Self::DeadLetterChannelFailed(msg.clone()),
420 Self::CircuitOpen(msg) => Self::CircuitOpen(msg.clone()),
421 Self::HttpOperationFailed {
422 method,
423 url,
424 status_code,
425 status_text,
426 response_body,
427 } => Self::HttpOperationFailed {
428 method: method.clone(),
429 url: url.clone(),
430 status_code: *status_code,
431 status_text: status_text.clone(),
432 response_body: response_body.clone(),
433 },
434 Self::ConsumerStopping => Self::ConsumerStopping,
435 Self::Config(msg) => Self::Config(msg.clone()),
436 Self::ConfigValidation(e) => Self::ConfigValidation(e.clone()),
437 Self::AlreadyConsumed => Self::AlreadyConsumed,
438 Self::StreamLimitExceeded(limit) => Self::StreamLimitExceeded(*limit),
439 Self::Unauthenticated(msg) => Self::Unauthenticated(msg.clone()),
440 Self::Unauthorized(msg) => Self::Unauthorized(msg.clone()),
441 Self::AuthProviderUnavailable(msg) => Self::AuthProviderUnavailable(msg.clone()),
442 Self::ValidationError(msg) => Self::ValidationError(msg.clone()),
443 Self::TemplateReload(msg) => Self::TemplateReload(msg.clone()),
444 Self::EndpointUri(e) => Self::EndpointUri(e.clone()),
445 Self::UnsupportedMediaType { consumed, declared } => Self::UnsupportedMediaType {
446 consumed: consumed.clone(),
447 declared: declared.clone(),
448 },
449 Self::NotAcceptable { accept, produced } => Self::NotAcceptable {
450 accept: accept.clone(),
451 produced: produced.clone(),
452 },
453 Self::ExpressionFailed {
454 language,
455 route_id,
456 step_id,
457 verb,
458 class,
459 position,
460 conversion,
461 cause,
462 } => Self::ExpressionFailed {
463 language: language.clone(),
464 route_id: route_id.clone(),
465 step_id: step_id.clone(),
466 verb: verb.clone(),
467 class: *class,
468 position: *position,
469 conversion: conversion.clone(),
470 cause: cause.clone(),
471 },
472 }
473 }
474}
475
476pub const CIRCUIT_OPEN: &str = "circuit_open";
482
483impl CamelError {
484 pub fn classify(&self) -> &'static str {
485 #[allow(unreachable_patterns)]
486 match self {
487 Self::ComponentNotFound(_) => "component",
488 Self::EndpointCreationFailed(_)
489 | Self::EndpointCreationFailedWithSource(_, _)
490 | Self::InvalidUri(_)
491 | Self::EndpointUri(_) => "endpoint",
492 Self::ProcessorError(_)
493 | Self::ProcessorErrorWithSource(_, _)
494 | Self::AuthProviderUnavailable(_) => "processor",
495 Self::TypeConversionFailed(_) | Self::AlreadyConsumed => "type_conversion",
496 Self::Io(_) => "io",
497 Self::RouteError(_) => "route",
498 Self::CircuitOpen(_) => CIRCUIT_OPEN,
499 Self::HttpOperationFailed { .. } => "http",
500 Self::Config(_) | Self::ConfigValidation(_) => "config",
501 Self::DeadLetterChannelFailed(_) => "dead_letter",
502 Self::ConsumerStopping => "consumer_stop",
503 Self::StreamLimitExceeded(_) => "stream",
504 Self::ChannelClosed => "channel",
505 Self::Unauthenticated(_) => "unauthenticated",
506 Self::Unauthorized(_) => "unauthorized",
507 Self::ValidationError(_) => "validation",
508 Self::TemplateReload(_) => "template",
509 Self::UnsupportedMediaType { .. } => "unsupported_media_type",
510 Self::NotAcceptable { .. } => "not_acceptable",
511 _ => "unknown",
512 }
513 }
514
515 pub fn variant_name(&self) -> &'static str {
527 match self {
528 Self::ComponentNotFound(_) => "ComponentNotFound",
529 Self::EndpointCreationFailed(_) => "EndpointCreationFailed",
530 Self::EndpointCreationFailedWithSource(_, _) => "EndpointCreationFailed",
531 Self::ProcessorError(_) => "ProcessorError",
532 Self::ProcessorErrorWithSource(_, _) => "ProcessorError",
533 Self::AuthProviderUnavailable(_) => "ProcessorError",
534 Self::TypeConversionFailed(_) => "TypeConversionFailed",
535 Self::InvalidUri(_) => "InvalidUri",
536 Self::ChannelClosed => "ChannelClosed",
537 Self::RouteError(_) => "RouteError",
538 Self::Io(_) => "Io",
539 Self::DeadLetterChannelFailed(_) => "DeadLetterChannelFailed",
540 Self::CircuitOpen(_) => "CircuitOpen",
541 Self::HttpOperationFailed { .. } => "HttpOperationFailed",
542 Self::ConsumerStopping => "ConsumerStopping",
543 Self::Config(_) => "Config",
544 Self::ConfigValidation(_) => "ConfigValidation",
545 Self::AlreadyConsumed => "AlreadyConsumed",
546 Self::StreamLimitExceeded(_) => "StreamLimitExceeded",
547 Self::Unauthenticated(_) => "Unauthenticated",
548 Self::Unauthorized(_) => "Unauthorized",
549 Self::ValidationError(_) => "ValidationError",
550 Self::TemplateReload(_) => "TemplateReload",
551 Self::EndpointUri(_) => "EndpointUri",
552 Self::UnsupportedMediaType { .. } => "UnsupportedMediaType",
553 Self::NotAcceptable { .. } => "NotAcceptable",
554 Self::ExpressionFailed { .. } => "ExpressionFailed",
555 }
556 }
557}
558
559impl From<std::io::Error> for CamelError {
560 fn from(err: std::io::Error) -> Self {
561 CamelError::Io(err.to_string())
562 }
563}
564
565impl From<crate::template::TemplateError> for CamelError {
566 fn from(err: crate::template::TemplateError) -> Self {
567 CamelError::Config(err.to_string())
568 }
569}
570
571impl From<ConfigValidationError> for CamelError {
572 fn from(e: ConfigValidationError) -> Self {
573 CamelError::ConfigValidation(e)
574 }
575}
576
577impl From<EndpointUriError> for CamelError {
578 fn from(e: EndpointUriError) -> Self {
579 CamelError::EndpointUri(e)
580 }
581}
582
583#[cfg(test)]
584mod tests {
585 use super::*;
586 use std::error::Error as _;
589
590 #[derive(Debug)]
592 struct SampleSource;
593
594 impl fmt::Display for SampleSource {
595 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
596 f.write_str("sample source")
597 }
598 }
599
600 impl std::error::Error for SampleSource {}
601
602 fn all_error_samples() -> Vec<CamelError> {
603 vec![
604 CamelError::ComponentNotFound("x".to_string()),
605 CamelError::EndpointCreationFailed("x".to_string()),
606 CamelError::EndpointCreationFailedWithSource(
607 "x".to_string(),
608 OpaqueErrorSource::new(Arc::new(SampleSource)),
609 ),
610 CamelError::ProcessorError("x".to_string()),
611 CamelError::ProcessorErrorWithSource(
612 "x".to_string(),
613 Arc::new(std::io::Error::other("inner")),
614 ),
615 CamelError::TypeConversionFailed("x".to_string()),
616 CamelError::InvalidUri("x".to_string()),
617 CamelError::ChannelClosed,
618 CamelError::RouteError("x".to_string()),
619 CamelError::Io("x".to_string()),
620 CamelError::DeadLetterChannelFailed("x".to_string()),
621 CamelError::CircuitOpen("x".to_string()),
622 CamelError::HttpOperationFailed {
623 method: "GET".to_string(),
624 url: "https://example.com".to_string(),
625 status_code: 500,
626 status_text: "Internal Server Error".to_string(),
627 response_body: Some("error".to_string()),
628 },
629 CamelError::ConsumerStopping,
630 CamelError::Config("x".to_string()),
631 CamelError::ConfigValidation(ConfigValidationError::ThrottlerMaxRequestsZero),
632 CamelError::AlreadyConsumed,
633 CamelError::StreamLimitExceeded(42),
634 CamelError::Unauthenticated("token expired".to_string()),
635 CamelError::Unauthorized("missing admin role".to_string()),
636 CamelError::AuthProviderUnavailable("jwks down".to_string()),
637 CamelError::ValidationError("body does not match schema".to_string()),
638 CamelError::TemplateReload("reload failed".to_string()),
639 CamelError::EndpointUri(EndpointUriError::MissingScheme),
640 CamelError::UnsupportedMediaType {
641 consumed: "text/plain".to_string(),
642 declared: "application/json".to_string(),
643 },
644 CamelError::NotAcceptable {
645 accept: "application/xml".to_string(),
646 produced: "application/json".to_string(),
647 },
648 ]
649 }
650
651 #[test]
652 fn test_http_operation_failed_display() {
653 let err = CamelError::HttpOperationFailed {
654 method: "GET".to_string(),
655 url: "https://example.com/test".to_string(),
656 status_code: 404,
657 status_text: "Not Found".to_string(),
658 response_body: Some("page not found".to_string()),
659 };
660 let msg = format!("{err}");
661 assert!(msg.contains("404"));
662 assert!(msg.contains("Not Found"));
663 }
664
665 #[test]
666 fn test_http_operation_failed_clone() {
667 let err = CamelError::HttpOperationFailed {
668 method: "POST".to_string(),
669 url: "https://api.example.com/users".to_string(),
670 status_code: 500,
671 status_text: "Internal Server Error".to_string(),
672 response_body: None,
673 };
674 let cloned = err.clone();
675 assert!(matches!(
676 cloned,
677 CamelError::HttpOperationFailed {
678 status_code: 500,
679 ..
680 }
681 ));
682 }
683
684 #[test]
685 fn test_classify_maps_all_variants() {
686 assert_eq!(
687 CamelError::ComponentNotFound("x".to_string()).classify(),
688 "component"
689 );
690 assert_eq!(
691 CamelError::EndpointCreationFailed("x".to_string()).classify(),
692 "endpoint"
693 );
694 assert_eq!(
695 CamelError::ProcessorError("x".to_string()).classify(),
696 "processor"
697 );
698 assert_eq!(
699 CamelError::TypeConversionFailed("x".to_string()).classify(),
700 "type_conversion"
701 );
702 assert_eq!(
703 CamelError::InvalidUri("x".to_string()).classify(),
704 "endpoint"
705 );
706 assert_eq!(CamelError::ChannelClosed.classify(), "channel");
707 assert_eq!(CamelError::RouteError("x".to_string()).classify(), "route");
708 assert_eq!(CamelError::Io("x".to_string()).classify(), "io");
709 assert_eq!(
710 CamelError::DeadLetterChannelFailed("x".to_string()).classify(),
711 "dead_letter"
712 );
713 assert_eq!(
714 CamelError::CircuitOpen("x".to_string()).classify(),
715 "circuit_open"
716 );
717 assert_eq!(
718 CamelError::HttpOperationFailed {
719 method: "GET".to_string(),
720 url: "https://example.com".to_string(),
721 status_code: 500,
722 status_text: "Internal Server Error".to_string(),
723 response_body: None,
724 }
725 .classify(),
726 "http"
727 );
728 assert_eq!(CamelError::Config("x".to_string()).classify(), "config");
729 assert_eq!(
730 CamelError::ConfigValidation(ConfigValidationError::ThrottlerMaxRequestsZero)
731 .classify(),
732 "config"
733 );
734 assert_eq!(CamelError::AlreadyConsumed.classify(), "type_conversion");
735 assert_eq!(CamelError::StreamLimitExceeded(42).classify(), "stream");
736 assert_eq!(
737 CamelError::ValidationError("bad".to_string()).classify(),
738 "validation"
739 );
740 }
741
742 #[test]
743 fn test_classify_output_is_ascii_and_short() {
744 for error in all_error_samples() {
745 let class = error.classify();
746 assert!(class.is_ascii());
747 assert!(class.len() <= 22, "class too long: {class}");
749 }
750 }
751
752 #[test]
753 fn test_auth_variants_classify() {
754 assert_eq!(
755 CamelError::Unauthenticated("x".to_string()).classify(),
756 "unauthenticated"
757 );
758 assert_eq!(
759 CamelError::Unauthorized("x".to_string()).classify(),
760 "unauthorized"
761 );
762 }
763
764 #[test]
765 fn test_validation_error_classify() {
766 assert_eq!(
767 CamelError::ValidationError("bad".to_string()).classify(),
768 "validation"
769 );
770 }
771
772 #[test]
773 fn template_reload_classifies_as_template() {
774 let err = CamelError::TemplateReload("boom".into());
775 assert_eq!(err.classify(), "template");
776 }
777
778 #[test]
779 fn template_reload_variant_name() {
780 let err = CamelError::TemplateReload("boom".into());
781 assert_eq!(err.variant_name(), "TemplateReload");
782 }
783
784 #[test]
785 fn test_auth_variants_are_clone() {
786 let err = CamelError::Unauthenticated("test".to_string());
787 let cloned = err.clone();
788 assert!(matches!(cloned, CamelError::Unauthenticated(_)));
789
790 let err2 = CamelError::Unauthorized("test".to_string());
791 let cloned2 = err2.clone();
792 assert!(matches!(cloned2, CamelError::Unauthorized(_)));
793 }
794
795 #[test]
796 fn classification_unchanged_for_callers() {
797 assert_eq!(
802 CamelError::CircuitOpen("breaker open".into()).classify(),
803 "circuit_open"
804 );
805 }
806
807 #[test]
808 fn auth_provider_unavailable_display_carries_detail() {
809 let err = CamelError::AuthProviderUnavailable("conn refused".into());
810 let msg = err.to_string();
811 assert!(msg.contains("conn refused"));
812 assert!(
813 msg.starts_with("Auth provider unavailable"),
814 "display should start with 'Auth provider unavailable', got: {msg}"
815 );
816 }
817
818 #[test]
819 fn classify_negotiation_errors() {
820 let unsupported = CamelError::UnsupportedMediaType {
821 consumed: "text/plain".into(),
822 declared: "application/json".into(),
823 };
824 let not_acceptable = CamelError::NotAcceptable {
825 accept: "application/xml".into(),
826 produced: "application/json".into(),
827 };
828 assert_eq!(unsupported.classify(), "unsupported_media_type");
829 assert_eq!(not_acceptable.classify(), "not_acceptable");
830 }
831
832 #[test]
833 fn variant_names_negotiation_errors() {
834 let unsupported = CamelError::UnsupportedMediaType {
835 consumed: "text/plain".into(),
836 declared: "application/json".into(),
837 };
838 let not_acceptable = CamelError::NotAcceptable {
839 accept: "application/xml".into(),
840 produced: "application/json".into(),
841 };
842 assert_eq!(unsupported.variant_name(), "UnsupportedMediaType");
843 assert_eq!(not_acceptable.variant_name(), "NotAcceptable");
844 }
845
846 #[test]
847 fn display_negotiation_errors() {
848 let unsupported = CamelError::UnsupportedMediaType {
849 consumed: "text/plain".into(),
850 declared: "application/json".into(),
851 };
852 let not_acceptable = CamelError::NotAcceptable {
853 accept: "application/xml".into(),
854 produced: "application/json".into(),
855 };
856 let unsupported_msg = unsupported.to_string();
857 assert!(unsupported_msg.contains("text/plain"));
858 assert!(unsupported_msg.contains("application/json"));
859 let not_acceptable_msg = not_acceptable.to_string();
860 assert!(not_acceptable_msg.contains("application/xml"));
861 assert!(not_acceptable_msg.contains("application/json"));
862 }
863
864 #[test]
865 fn expression_failed_display_has_no_exchange_data() {
866 let err = CamelError::ExpressionFailed {
867 language: "rhai".to_string(),
868 route_id: "r1".to_string(),
869 step_id: "set_property#0".to_string(),
870 verb: "set_property".to_string(),
871 class: ExpressionErrorClass::Arithmetic,
872 position: Some(ErrorPosition { line: 3, column: 8 }),
873 conversion: None,
874 cause: None,
875 };
876 let msg = err.to_string();
877 assert!(msg.contains("arithmetic"), "missing class: {msg}");
878 assert!(msg.contains("rhai"), "missing language: {msg}");
879 assert!(msg.contains("set_property"), "missing verb/step: {msg}");
880 assert!(msg.contains("3:8"), "missing position: {msg}");
881 assert_eq!(
883 msg,
884 "expression failed: arithmetic in rhai `set_property` step \
885 `set_property#0` (route `r1`) at 3:8"
886 );
887 }
888
889 #[test]
890 fn expression_failed_variant_name() {
891 let err = CamelError::ExpressionFailed {
892 language: "rhai".to_string(),
893 route_id: "r1".to_string(),
894 step_id: "set_property#0".to_string(),
895 verb: "set_property".to_string(),
896 class: ExpressionErrorClass::Runtime,
897 position: None,
898 conversion: None,
899 cause: None,
900 };
901 assert_eq!(err.variant_name(), "ExpressionFailed");
902 }
903
904 #[test]
905 fn config_validation_error_on_exception_conflict_display() {
906 let err = ConfigValidationError::OnExceptionStepsHandledByConflict;
907 let msg = format!("{err}");
908 assert!(msg.contains("steps"));
909 assert!(msg.contains("handled_by"));
910 assert!(msg.contains("exclusive"));
911 }
912
913 #[test]
914 fn opaque_error_source_exposes_only_pointee() {
915 let src = OpaqueErrorSource::new(Arc::new(SampleSource));
916 let pointee = src.source().unwrap();
917 assert!(pointee.downcast_ref::<SampleSource>().is_some());
918 }
919
920 #[test]
921 fn endpoint_creation_failed_with_source_aliases_to_plain() {
922 let e = CamelError::EndpointCreationFailedWithSource(
923 "d".to_string(),
924 OpaqueErrorSource::new(Arc::new(SampleSource)),
925 );
926 assert_eq!(e.variant_name(), "EndpointCreationFailed");
927 assert_eq!(e.classify(), "endpoint");
928 assert_eq!(e.to_string(), "Endpoint creation failed: d");
929 }
930
931 #[test]
932 fn clone_preserves_variant_identity_for_all_error_samples() {
933 for e in all_error_samples() {
934 let c = e.clone();
935 assert_eq!(c.variant_name(), e.variant_name());
936 assert_eq!(c.classify(), e.classify());
937 assert_eq!(c.to_string(), e.to_string());
938 }
939 }
940
941 #[test]
942 fn camel_error_clone_preserves_source_provenance() {
943 let e = CamelError::EndpointCreationFailedWithSource(
944 "d".to_string(),
945 OpaqueErrorSource::new(Arc::new(SampleSource)),
946 );
947 let c = e.clone();
948 let wrapper = c.source().unwrap();
952 let pointee = wrapper.source().unwrap();
953 assert!(pointee.downcast_ref::<SampleSource>().is_some());
954 }
955}
956
957#[cfg(test)]
958mod variant_name_tests {
959 use super::{
960 CamelError, ConfigValidationError, EndpointUriError, ExpressionErrorClass,
961 OpaqueErrorSource,
962 };
963 use std::sync::Arc;
964
965 #[test]
976 fn variant_name_covers_all_variants() {
977 let cases: Vec<(CamelError, &str)> = vec![
978 (
979 CamelError::ComponentNotFound("x".into()),
980 "ComponentNotFound",
981 ),
982 (
983 CamelError::EndpointCreationFailed("x".into()),
984 "EndpointCreationFailed",
985 ),
986 (
987 CamelError::EndpointCreationFailedWithSource(
988 "x".into(),
989 OpaqueErrorSource::new(Arc::new(std::io::Error::other("y"))),
990 ),
991 "EndpointCreationFailed", ),
993 (CamelError::ProcessorError("x".into()), "ProcessorError"),
994 (
995 CamelError::ProcessorErrorWithSource(
996 "x".into(),
997 Arc::new(std::io::Error::other("y")),
998 ),
999 "ProcessorError", ),
1001 (
1002 CamelError::TypeConversionFailed("x".into()),
1003 "TypeConversionFailed",
1004 ),
1005 (CamelError::InvalidUri("x".into()), "InvalidUri"),
1006 (CamelError::ChannelClosed, "ChannelClosed"),
1007 (CamelError::RouteError("x".into()), "RouteError"),
1008 (CamelError::Io("x".into()), "Io"),
1009 (
1010 CamelError::DeadLetterChannelFailed("x".into()),
1011 "DeadLetterChannelFailed",
1012 ),
1013 (CamelError::CircuitOpen("x".into()), "CircuitOpen"),
1014 (
1015 CamelError::HttpOperationFailed {
1016 method: "GET".into(),
1017 url: "https://example.com".into(),
1018 status_code: 500,
1019 status_text: "Internal Server Error".into(),
1020 response_body: None,
1021 },
1022 "HttpOperationFailed",
1023 ),
1024 (CamelError::ConsumerStopping, "ConsumerStopping"),
1025 (CamelError::Config("x".into()), "Config"),
1026 (
1027 CamelError::ConfigValidation(ConfigValidationError::ThrottlerMaxRequestsZero),
1028 "ConfigValidation",
1029 ),
1030 (CamelError::AlreadyConsumed, "AlreadyConsumed"),
1031 (CamelError::StreamLimitExceeded(42), "StreamLimitExceeded"),
1032 (CamelError::Unauthenticated("x".into()), "Unauthenticated"),
1033 (CamelError::Unauthorized("x".into()), "Unauthorized"),
1034 (CamelError::ValidationError("bad".into()), "ValidationError"),
1035 (CamelError::TemplateReload("x".into()), "TemplateReload"),
1036 (
1037 CamelError::EndpointUri(EndpointUriError::MissingScheme),
1038 "EndpointUri",
1039 ),
1040 (
1041 CamelError::UnsupportedMediaType {
1042 consumed: "text/plain".into(),
1043 declared: "application/json".into(),
1044 },
1045 "UnsupportedMediaType",
1046 ),
1047 (
1048 CamelError::NotAcceptable {
1049 accept: "application/xml".into(),
1050 produced: "application/json".into(),
1051 },
1052 "NotAcceptable",
1053 ),
1054 (
1055 CamelError::ExpressionFailed {
1056 language: "rhai".into(),
1057 route_id: "r1".into(),
1058 step_id: "set_property#0".into(),
1059 verb: "set_property".into(),
1060 class: ExpressionErrorClass::Runtime,
1061 position: None,
1062 conversion: None,
1063 cause: None,
1064 },
1065 "ExpressionFailed",
1066 ),
1067 (
1068 CamelError::AuthProviderUnavailable("x".into()),
1069 "ProcessorError",
1070 ),
1071 ];
1072
1073 assert_eq!(
1074 cases.len(),
1075 27,
1076 "variant_name_covers_all_variants must cover every CamelError variant; \
1077 extend this table and the camel-dsl classification guard"
1078 );
1079
1080 for (err, expected) in cases {
1081 assert_eq!(
1082 err.variant_name(),
1083 expected,
1084 "variant_name mismatch for {:?}",
1085 err
1086 );
1087 }
1088 }
1089
1090 #[test]
1091 fn auth_provider_unavailable_classifies_as_processor() {
1092 let err = CamelError::AuthProviderUnavailable("jwks down".into());
1093 assert_eq!(err.classify(), "processor");
1094 }
1095
1096 #[test]
1097 fn auth_provider_unavailable_variant_name_aliases_processor_error() {
1098 let err = CamelError::AuthProviderUnavailable("jwks down".into());
1099 assert_eq!(err.variant_name(), "ProcessorError");
1100 }
1101}