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