cratestack_client_rust/
error.rs1use cratestack_core::{CoolError, CoolErrorResponse};
2use reqwest::StatusCode;
3
4pub type HeaderPair<'a> = (&'a str, &'a str);
5pub type QueryPair<'a> = (&'a str, &'a str);
6
7#[derive(Debug)]
10pub struct TransportError {
11 inner: Box<reqwest::Error>,
12}
13
14impl TransportError {
15 pub fn reqwest_error(&self) -> &reqwest::Error {
22 &self.inner
23 }
24
25 pub fn into_source(self) -> reqwest::Error {
27 *self.inner
28 }
29}
30
31impl std::fmt::Display for TransportError {
32 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
33 write!(f, "{}", self.inner)
34 }
35}
36
37impl std::error::Error for TransportError {
38 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
39 Some(self.inner.as_ref())
40 }
41}
42
43impl From<reqwest::Error> for TransportError {
44 fn from(e: reqwest::Error) -> Self {
45 TransportError { inner: Box::new(e) }
46 }
47}
48
49#[derive(Debug, thiserror::Error)]
50#[non_exhaustive]
51pub enum ClientError {
52 #[error("transport error: {0}")]
53 Transport(#[source] TransportError),
54 #[error("codec error: {0}")]
55 Codec(#[from] CoolError),
56 #[error("state error: {0}")]
57 State(String),
58 #[error("invalid response: {0}")]
59 InvalidResponse(String),
60 #[error("bad input: {0}")]
61 BadInput(String),
62 #[error("remote call failed with status {status}: {message}")]
63 Remote {
64 status: StatusCode,
65 error: Option<CoolErrorResponse>,
66 message: String,
67 },
68}
69
70impl From<reqwest::Error> for ClientError {
71 fn from(e: reqwest::Error) -> Self {
72 ClientError::Transport(TransportError::from(e))
73 }
74}
75
76#[cfg(test)]
77mod tests {
78 use super::*;
79
80 async fn synthesize_reqwest_error() -> reqwest::Error {
86 let _ = rustls::crypto::ring::default_provider().install_default();
90
91 reqwest::Client::new()
92 .get("http://127.0.0.1:1")
93 .send()
94 .await
95 .expect_err("connecting to a closed loopback port should fail")
96 }
97
98 #[tokio::test]
99 async fn transport_error_display_and_accessor_forward_to_reqwest() {
100 let reqwest_err = synthesize_reqwest_error().await;
101 let expected_message = reqwest_err.to_string();
102
103 let transport = TransportError::from(reqwest_err);
104
105 assert_eq!(transport.to_string(), expected_message);
106 assert_eq!(transport.reqwest_error().to_string(), expected_message);
107 }
108
109 #[tokio::test]
110 async fn transport_error_into_source_round_trips_the_reqwest_error() {
111 let reqwest_err = synthesize_reqwest_error().await;
112 let expected_message = reqwest_err.to_string();
113
114 let recovered = TransportError::from(reqwest_err).into_source();
115
116 assert_eq!(recovered.to_string(), expected_message);
117 }
118
119 #[tokio::test]
120 async fn client_error_transport_chains_via_std_error_source() {
121 let reqwest_err = synthesize_reqwest_error().await;
122 let expected_message = reqwest_err.to_string();
123
124 let client_err = ClientError::from(reqwest_err);
125
126 assert!(matches!(client_err, ClientError::Transport(_)));
128 assert!(client_err.to_string().contains(&expected_message));
130
131 let source = std::error::Error::source(&client_err)
137 .expect("ClientError::Transport should chain to the reqwest::Error via source()");
138 assert_eq!(source.to_string(), expected_message);
139 }
140
141 #[test]
142 fn client_error_variants() {
143 let transport_err = ClientError::State("state error".to_string());
144 assert!(matches!(transport_err, ClientError::State(_)));
145
146 let invalid_resp = ClientError::InvalidResponse("invalid".to_string());
147 assert!(matches!(invalid_resp, ClientError::InvalidResponse(_)));
148
149 let bad_input = ClientError::BadInput("bad".to_string());
150 assert!(matches!(bad_input, ClientError::BadInput(_)));
151 }
152
153 #[test]
154 fn client_error_displays_correctly() {
155 let err = ClientError::State("test error".to_string());
156 assert_eq!(err.to_string(), "state error: test error");
157
158 let err = ClientError::InvalidResponse("invalid".to_string());
159 assert_eq!(err.to_string(), "invalid response: invalid");
160 }
161}