Skip to main content

nanonis_rs/
error.rs

1use thiserror::Error;
2
3/// Error types for Nanonis communication.
4///
5/// This enum represents the four categories of errors that can occur:
6/// - [`Io`](NanonisError::Io) - Network and I/O errors
7/// - [`Timeout`](NanonisError::Timeout) - Connection or operation timeouts
8/// - [`Protocol`](NanonisError::Protocol) - Binary protocol parsing/validation errors
9/// - [`Server`](NanonisError::Server) - Errors returned by the Nanonis server
10#[derive(Error, Debug)]
11pub enum NanonisError {
12    /// IO error with context describing what operation failed.
13    ///
14    /// # Example
15    /// ```
16    /// use nanonis_rs::NanonisError;
17    ///
18    /// let io_err = std::io::Error::new(std::io::ErrorKind::ConnectionRefused, "refused");
19    /// let err = NanonisError::Io {
20    ///     source: io_err,
21    ///     context: "connecting to server".to_string(),
22    /// };
23    /// assert!(err.to_string().contains("connecting to server"));
24    /// ```
25    #[error("IO error: {context}")]
26    Io {
27        #[source]
28        source: std::io::Error,
29        context: String,
30    },
31
32    /// Connection or operation timeout.
33    ///
34    /// Contains an optional description of what timed out.
35    ///
36    /// # Example
37    /// ```
38    /// use nanonis_rs::NanonisError;
39    ///
40    /// let err = NanonisError::Timeout("waiting for scan to complete".to_string());
41    /// assert!(err.to_string().contains("scan"));
42    /// ```
43    #[error("Timeout{}", if .0.is_empty() { String::new() } else { format!(": {}", .0) })]
44    Timeout(String),
45
46    /// Protocol error during parsing, validation, or type conversion.
47    ///
48    /// This covers all errors related to the binary protocol:
49    /// - Unexpected response formats
50    /// - Type mismatches in received data
51    /// - Invalid command parameters
52    /// - Serialization failures
53    ///
54    /// # Example
55    /// ```
56    /// use nanonis_rs::NanonisError;
57    ///
58    /// let err = NanonisError::Protocol("Expected f32, got i32".to_string());
59    /// assert!(err.to_string().contains("Expected f32"));
60    /// ```
61    #[error("Protocol error: {0}")]
62    Protocol(String),
63
64    /// Error returned by the Nanonis server.
65    ///
66    /// The server returns an error code and message when a command fails.
67    ///
68    /// # Example
69    /// ```
70    /// use nanonis_rs::NanonisError;
71    ///
72    /// let err = NanonisError::Server {
73    ///     code: -1,
74    ///     message: "Invalid parameter".to_string(),
75    /// };
76    /// assert!(err.is_server_error());
77    /// assert_eq!(err.error_code(), Some(-1));
78    /// ```
79    #[error("Server error: {message} (code: {code})")]
80    Server { code: i32, message: String },
81}
82
83impl NanonisError {
84    /// Check if this is a server-side error.
85    pub fn is_server_error(&self) -> bool {
86        matches!(self, NanonisError::Server { .. })
87    }
88
89    /// Get error code if this is a server error.
90    pub fn error_code(&self) -> Option<i32> {
91        match self {
92            NanonisError::Server { code, .. } => Some(*code),
93            _ => None,
94        }
95    }
96
97    /// Check if this is a timeout error.
98    pub fn is_timeout(&self) -> bool {
99        matches!(self, NanonisError::Timeout(_))
100    }
101
102    /// Check if this is an I/O error.
103    pub fn is_io(&self) -> bool {
104        matches!(self, NanonisError::Io { .. })
105    }
106
107    /// Check if this is a protocol error.
108    pub fn is_protocol(&self) -> bool {
109        matches!(self, NanonisError::Protocol(_))
110    }
111}
112
113impl NanonisError {
114    /// Create the appropriate error variant from an `io::Error`, classifying
115    /// timeouts as [`NanonisError::Timeout`] rather than [`NanonisError::Io`].
116    pub(crate) fn from_io(error: std::io::Error, context: impl Into<String>) -> Self {
117        match error.kind() {
118            std::io::ErrorKind::TimedOut | std::io::ErrorKind::WouldBlock => {
119                NanonisError::Timeout(context.into())
120            }
121            _ => NanonisError::Io {
122                source: error,
123                context: context.into(),
124            },
125        }
126    }
127}
128
129// Allow conversion from std::io::Error
130impl From<std::io::Error> for NanonisError {
131    fn from(error: std::io::Error) -> Self {
132        Self::from_io(error, "IO operation failed")
133    }
134}
135
136// Allow conversion from serde_json::Error
137impl From<serde_json::Error> for NanonisError {
138    fn from(error: serde_json::Error) -> Self {
139        NanonisError::Protocol(format!("JSON serialization error: {error}"))
140    }
141}
142
143#[cfg(test)]
144mod tests {
145    use super::*;
146
147    fn make_io_error() -> NanonisError {
148        NanonisError::Io {
149            source: std::io::Error::new(std::io::ErrorKind::ConnectionRefused, "refused"),
150            context: "connecting".to_string(),
151        }
152    }
153
154    #[test]
155    fn predicates() {
156        let io = make_io_error();
157        assert!(io.is_io());
158        assert!(!io.is_timeout());
159        assert!(!io.is_protocol());
160        assert!(!io.is_server_error());
161
162        let timeout = NanonisError::Timeout("scan".into());
163        assert!(timeout.is_timeout());
164        assert!(!timeout.is_io());
165
166        let proto = NanonisError::Protocol("bad".into());
167        assert!(proto.is_protocol());
168        assert!(!proto.is_server_error());
169
170        let server = NanonisError::Server {
171            code: -1,
172            message: "fail".into(),
173        };
174        assert!(server.is_server_error());
175        assert!(!server.is_protocol());
176    }
177
178    #[test]
179    fn error_code() {
180        assert_eq!(
181            NanonisError::Server {
182                code: 42,
183                message: "".into()
184            }
185            .error_code(),
186            Some(42)
187        );
188        assert_eq!(
189            NanonisError::Server {
190                code: -1,
191                message: "".into()
192            }
193            .error_code(),
194            Some(-1)
195        );
196        assert_eq!(NanonisError::Protocol("x".into()).error_code(), None);
197        assert_eq!(NanonisError::Timeout("".into()).error_code(), None);
198        assert_eq!(make_io_error().error_code(), None);
199    }
200
201    #[test]
202    fn display_formats() {
203        assert!(
204            NanonisError::Timeout("scan".into())
205                .to_string()
206                .contains("scan")
207        );
208        assert_eq!(NanonisError::Timeout("".into()).to_string(), "Timeout");
209        assert!(
210            NanonisError::Protocol("bad parse".into())
211                .to_string()
212                .contains("bad parse")
213        );
214        let s = NanonisError::Server {
215            code: -1,
216            message: "Invalid".into(),
217        }
218        .to_string();
219        assert!(s.contains("Invalid") && s.contains("-1"));
220    }
221
222    #[test]
223    fn from_io_error() {
224        let io_err = std::io::Error::new(std::io::ErrorKind::BrokenPipe, "pipe");
225        let err: NanonisError = io_err.into();
226        assert!(err.is_io());
227        assert!(err.to_string().contains("IO operation failed"));
228    }
229
230    #[test]
231    fn from_io_classifies_timeouts() {
232        let timed_out = std::io::Error::new(std::io::ErrorKind::TimedOut, "timed out");
233        let err = NanonisError::from_io(timed_out, "reading response");
234        assert!(err.is_timeout());
235        assert!(err.to_string().contains("reading response"));
236
237        let would_block = std::io::Error::new(std::io::ErrorKind::WouldBlock, "blocked");
238        let err = NanonisError::from_io(would_block, "writing command");
239        assert!(err.is_timeout());
240    }
241
242    #[test]
243    fn from_io_preserves_non_timeouts() {
244        let broken = std::io::Error::new(std::io::ErrorKind::BrokenPipe, "pipe");
245        let err = NanonisError::from_io(broken, "sending data");
246        assert!(err.is_io());
247        assert!(!err.is_timeout());
248    }
249
250    #[test]
251    fn from_trait_classifies_timeouts() {
252        let timed_out = std::io::Error::new(std::io::ErrorKind::TimedOut, "timed out");
253        let err: NanonisError = timed_out.into();
254        assert!(
255            err.is_timeout(),
256            "From<io::Error> should classify TimedOut as Timeout"
257        );
258    }
259}