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 #[error("Unauthenticated: {0}")]
65 Unauthenticated(String),
66
67 #[error("Unauthorized: {0}")]
68 Unauthorized(String),
69}
70
71impl CamelError {
72 pub fn classify(&self) -> &'static str {
73 #[allow(unreachable_patterns)]
74 match self {
75 Self::ComponentNotFound(_) => "component",
76 Self::EndpointCreationFailed(_) | Self::InvalidUri(_) => "endpoint",
77 Self::ProcessorError(_) | Self::ProcessorErrorWithSource(_, _) => "processor",
78 Self::TypeConversionFailed(_) | Self::AlreadyConsumed => "type_conversion",
79 Self::Io(_) => "io",
80 Self::RouteError(_) => "route",
81 Self::CircuitOpen(_) => "circuit_open",
82 Self::HttpOperationFailed { .. } => "http",
83 Self::Config(_) => "config",
84 Self::DeadLetterChannelFailed(_) => "dead_letter",
85 Self::Stopped => "stopped",
86 Self::StreamLimitExceeded(_) => "stream",
87 Self::ChannelClosed => "channel",
88 Self::Unauthenticated(_) => "unauthenticated",
89 Self::Unauthorized(_) => "unauthorized",
90 _ => "unknown",
91 }
92 }
93}
94
95impl From<std::io::Error> for CamelError {
96 fn from(err: std::io::Error) -> Self {
97 CamelError::Io(err.to_string())
98 }
99}
100
101impl From<crate::template::TemplateError> for CamelError {
102 fn from(err: crate::template::TemplateError) -> Self {
103 CamelError::Config(err.to_string())
104 }
105}
106
107#[cfg(test)]
108mod tests {
109 use super::*;
110
111 fn all_error_samples() -> Vec<CamelError> {
112 vec![
113 CamelError::ComponentNotFound("x".to_string()),
114 CamelError::EndpointCreationFailed("x".to_string()),
115 CamelError::ProcessorError("x".to_string()),
116 CamelError::ProcessorErrorWithSource(
117 "x".to_string(),
118 Arc::new(std::io::Error::new(std::io::ErrorKind::Other, "inner")),
119 ),
120 CamelError::TypeConversionFailed("x".to_string()),
121 CamelError::InvalidUri("x".to_string()),
122 CamelError::ChannelClosed,
123 CamelError::RouteError("x".to_string()),
124 CamelError::Io("x".to_string()),
125 CamelError::DeadLetterChannelFailed("x".to_string()),
126 CamelError::CircuitOpen("x".to_string()),
127 CamelError::HttpOperationFailed {
128 method: "GET".to_string(),
129 url: "https://example.com".to_string(),
130 status_code: 500,
131 status_text: "Internal Server Error".to_string(),
132 response_body: Some("error".to_string()),
133 },
134 CamelError::Stopped,
135 CamelError::Config("x".to_string()),
136 CamelError::AlreadyConsumed,
137 CamelError::StreamLimitExceeded(42),
138 CamelError::Unauthenticated("token expired".to_string()),
139 CamelError::Unauthorized("missing admin role".to_string()),
140 ]
141 }
142
143 #[test]
144 fn test_http_operation_failed_display() {
145 let err = CamelError::HttpOperationFailed {
146 method: "GET".to_string(),
147 url: "https://example.com/test".to_string(),
148 status_code: 404,
149 status_text: "Not Found".to_string(),
150 response_body: Some("page not found".to_string()),
151 };
152 let msg = format!("{err}");
153 assert!(msg.contains("404"));
154 assert!(msg.contains("Not Found"));
155 }
156
157 #[test]
158 fn test_http_operation_failed_clone() {
159 let err = CamelError::HttpOperationFailed {
160 method: "POST".to_string(),
161 url: "https://api.example.com/users".to_string(),
162 status_code: 500,
163 status_text: "Internal Server Error".to_string(),
164 response_body: None,
165 };
166 let cloned = err.clone();
167 assert!(matches!(
168 cloned,
169 CamelError::HttpOperationFailed {
170 status_code: 500,
171 ..
172 }
173 ));
174 }
175
176 #[test]
177 fn test_stopped_variant_exists_and_is_clone() {
178 let err = CamelError::Stopped;
179 let cloned = err.clone();
180 assert!(matches!(cloned, CamelError::Stopped));
181 assert_eq!(format!("{err}"), "Exchange stopped by Stop EIP");
182 }
183
184 #[test]
185 fn test_classify_maps_all_variants() {
186 assert_eq!(
187 CamelError::ComponentNotFound("x".to_string()).classify(),
188 "component"
189 );
190 assert_eq!(
191 CamelError::EndpointCreationFailed("x".to_string()).classify(),
192 "endpoint"
193 );
194 assert_eq!(
195 CamelError::ProcessorError("x".to_string()).classify(),
196 "processor"
197 );
198 assert_eq!(
199 CamelError::TypeConversionFailed("x".to_string()).classify(),
200 "type_conversion"
201 );
202 assert_eq!(
203 CamelError::InvalidUri("x".to_string()).classify(),
204 "endpoint"
205 );
206 assert_eq!(CamelError::ChannelClosed.classify(), "channel");
207 assert_eq!(CamelError::RouteError("x".to_string()).classify(), "route");
208 assert_eq!(CamelError::Io("x".to_string()).classify(), "io");
209 assert_eq!(
210 CamelError::DeadLetterChannelFailed("x".to_string()).classify(),
211 "dead_letter"
212 );
213 assert_eq!(
214 CamelError::CircuitOpen("x".to_string()).classify(),
215 "circuit_open"
216 );
217 assert_eq!(
218 CamelError::HttpOperationFailed {
219 method: "GET".to_string(),
220 url: "https://example.com".to_string(),
221 status_code: 500,
222 status_text: "Internal Server Error".to_string(),
223 response_body: None,
224 }
225 .classify(),
226 "http"
227 );
228 assert_eq!(CamelError::Stopped.classify(), "stopped");
229 assert_eq!(CamelError::Config("x".to_string()).classify(), "config");
230 assert_eq!(CamelError::AlreadyConsumed.classify(), "type_conversion");
231 assert_eq!(CamelError::StreamLimitExceeded(42).classify(), "stream");
232 }
233
234 #[test]
235 fn test_classify_output_is_ascii_and_short() {
236 for error in all_error_samples() {
237 let class = error.classify();
238 assert!(class.is_ascii());
239 assert!(class.len() <= 15, "class too long: {class}");
240 }
241 }
242
243 #[test]
244 fn test_auth_variants_classify() {
245 assert_eq!(
246 CamelError::Unauthenticated("x".to_string()).classify(),
247 "unauthenticated"
248 );
249 assert_eq!(
250 CamelError::Unauthorized("x".to_string()).classify(),
251 "unauthorized"
252 );
253 }
254
255 #[test]
256 fn test_auth_variants_are_clone() {
257 let err = CamelError::Unauthenticated("test".to_string());
258 let cloned = err.clone();
259 assert!(matches!(cloned, CamelError::Unauthenticated(_)));
260
261 let err2 = CamelError::Unauthorized("test".to_string());
262 let cloned2 = err2.clone();
263 assert!(matches!(cloned2, CamelError::Unauthorized(_)));
264 }
265}