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