Skip to main content

camel_api/
error.rs

1use thiserror::Error;
2
3/// Core error type for the Camel framework.
4#[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}