1use std::sync::Arc;
2use thiserror::Error;
3
4#[derive(Debug, Clone, Error)]
6#[non_exhaustive]
7pub enum CamelError {
8 #[error("Component not found: {0}")]
9 ComponentNotFound(String),
10
11 #[error("Endpoint creation failed: {0}")]
12 EndpointCreationFailed(String),
13
14 #[error("Processor error: {0}")]
15 ProcessorError(String),
16
17 #[error("Processor error: {0}")]
20 ProcessorErrorWithSource(String, #[source] Arc<dyn std::error::Error + Send + Sync>),
21
22 #[error("Type conversion failed: {0}")]
23 TypeConversionFailed(String),
24
25 #[error("Invalid URI: {0}")]
26 InvalidUri(String),
27
28 #[error("Channel closed")]
29 ChannelClosed,
30
31 #[error("Route error: {0}")]
32 RouteError(String),
33
34 #[error("IO error: {0}")]
35 Io(String),
36
37 #[error("Dead letter channel failed: {0}")]
38 DeadLetterChannelFailed(String),
39
40 #[error("Circuit breaker open: {0}")]
41 CircuitOpen(String),
42
43 #[error("HTTP {method} {url} failed: {status_code} {status_text}")]
44 HttpOperationFailed {
45 method: String,
46 url: String,
47 status_code: u16,
48 status_text: String,
49 response_body: Option<String>,
50 },
51
52 #[error("Exchange stopped by Stop EIP")]
53 Stopped,
54
55 #[error("Configuration error: {0}")]
56 Config(String),
57
58 #[error("Body stream has already been consumed")]
59 AlreadyConsumed,
60
61 #[error("Stream size exceeded limit: {0}")]
62 StreamLimitExceeded(usize),
63}
64
65impl CamelError {
66 pub fn classify(&self) -> &'static str {
67 #[allow(unreachable_patterns)]
68 match self {
69 Self::ComponentNotFound(_) => "component",
70 Self::EndpointCreationFailed(_) | Self::InvalidUri(_) => "endpoint",
71 Self::ProcessorError(_) | Self::ProcessorErrorWithSource(_, _) => "processor",
72 Self::TypeConversionFailed(_) | Self::AlreadyConsumed => "type_conversion",
73 Self::Io(_) => "io",
74 Self::RouteError(_) => "route",
75 Self::CircuitOpen(_) => "circuit_open",
76 Self::HttpOperationFailed { .. } => "http",
77 Self::Config(_) => "config",
78 Self::DeadLetterChannelFailed(_) => "dead_letter",
79 Self::Stopped => "stopped",
80 Self::StreamLimitExceeded(_) => "stream",
81 Self::ChannelClosed => "channel",
82 _ => "unknown",
83 }
84 }
85}
86
87impl From<std::io::Error> for CamelError {
88 fn from(err: std::io::Error) -> Self {
89 CamelError::Io(err.to_string())
90 }
91}
92
93#[cfg(test)]
94mod tests {
95 use super::*;
96
97 fn all_error_samples() -> Vec<CamelError> {
98 vec![
99 CamelError::ComponentNotFound("x".to_string()),
100 CamelError::EndpointCreationFailed("x".to_string()),
101 CamelError::ProcessorError("x".to_string()),
102 CamelError::ProcessorErrorWithSource(
103 "x".to_string(),
104 Arc::new(std::io::Error::new(std::io::ErrorKind::Other, "inner")),
105 ),
106 CamelError::TypeConversionFailed("x".to_string()),
107 CamelError::InvalidUri("x".to_string()),
108 CamelError::ChannelClosed,
109 CamelError::RouteError("x".to_string()),
110 CamelError::Io("x".to_string()),
111 CamelError::DeadLetterChannelFailed("x".to_string()),
112 CamelError::CircuitOpen("x".to_string()),
113 CamelError::HttpOperationFailed {
114 method: "GET".to_string(),
115 url: "https://example.com".to_string(),
116 status_code: 500,
117 status_text: "Internal Server Error".to_string(),
118 response_body: Some("error".to_string()),
119 },
120 CamelError::Stopped,
121 CamelError::Config("x".to_string()),
122 CamelError::AlreadyConsumed,
123 CamelError::StreamLimitExceeded(42),
124 ]
125 }
126
127 #[test]
128 fn test_http_operation_failed_display() {
129 let err = CamelError::HttpOperationFailed {
130 method: "GET".to_string(),
131 url: "https://example.com/test".to_string(),
132 status_code: 404,
133 status_text: "Not Found".to_string(),
134 response_body: Some("page not found".to_string()),
135 };
136 let msg = format!("{err}");
137 assert!(msg.contains("404"));
138 assert!(msg.contains("Not Found"));
139 }
140
141 #[test]
142 fn test_http_operation_failed_clone() {
143 let err = CamelError::HttpOperationFailed {
144 method: "POST".to_string(),
145 url: "https://api.example.com/users".to_string(),
146 status_code: 500,
147 status_text: "Internal Server Error".to_string(),
148 response_body: None,
149 };
150 let cloned = err.clone();
151 assert!(matches!(
152 cloned,
153 CamelError::HttpOperationFailed {
154 status_code: 500,
155 ..
156 }
157 ));
158 }
159
160 #[test]
161 fn test_stopped_variant_exists_and_is_clone() {
162 let err = CamelError::Stopped;
163 let cloned = err.clone();
164 assert!(matches!(cloned, CamelError::Stopped));
165 assert_eq!(format!("{err}"), "Exchange stopped by Stop EIP");
166 }
167
168 #[test]
169 fn test_classify_maps_all_variants() {
170 assert_eq!(
171 CamelError::ComponentNotFound("x".to_string()).classify(),
172 "component"
173 );
174 assert_eq!(
175 CamelError::EndpointCreationFailed("x".to_string()).classify(),
176 "endpoint"
177 );
178 assert_eq!(
179 CamelError::ProcessorError("x".to_string()).classify(),
180 "processor"
181 );
182 assert_eq!(
183 CamelError::TypeConversionFailed("x".to_string()).classify(),
184 "type_conversion"
185 );
186 assert_eq!(
187 CamelError::InvalidUri("x".to_string()).classify(),
188 "endpoint"
189 );
190 assert_eq!(CamelError::ChannelClosed.classify(), "channel");
191 assert_eq!(CamelError::RouteError("x".to_string()).classify(), "route");
192 assert_eq!(CamelError::Io("x".to_string()).classify(), "io");
193 assert_eq!(
194 CamelError::DeadLetterChannelFailed("x".to_string()).classify(),
195 "dead_letter"
196 );
197 assert_eq!(
198 CamelError::CircuitOpen("x".to_string()).classify(),
199 "circuit_open"
200 );
201 assert_eq!(
202 CamelError::HttpOperationFailed {
203 method: "GET".to_string(),
204 url: "https://example.com".to_string(),
205 status_code: 500,
206 status_text: "Internal Server Error".to_string(),
207 response_body: None,
208 }
209 .classify(),
210 "http"
211 );
212 assert_eq!(CamelError::Stopped.classify(), "stopped");
213 assert_eq!(CamelError::Config("x".to_string()).classify(), "config");
214 assert_eq!(CamelError::AlreadyConsumed.classify(), "type_conversion");
215 assert_eq!(CamelError::StreamLimitExceeded(42).classify(), "stream");
216 }
217
218 #[test]
219 fn test_classify_output_is_ascii_and_short() {
220 for error in all_error_samples() {
221 let class = error.classify();
222 assert!(class.is_ascii());
223 assert!(class.len() <= 15, "class too long: {class}");
224 }
225 }
226}