1use thiserror::Error;
2
3#[derive(Error, Debug)]
11pub enum NanonisError {
12 #[error("IO error: {context}")]
26 Io {
27 #[source]
28 source: std::io::Error,
29 context: String,
30 },
31
32 #[error("Timeout{}", if .0.is_empty() { String::new() } else { format!(": {}", .0) })]
44 Timeout(String),
45
46 #[error("Protocol error: {0}")]
62 Protocol(String),
63
64 #[error("Server error: {message} (code: {code})")]
80 Server { code: i32, message: String },
81}
82
83impl NanonisError {
84 pub fn is_server_error(&self) -> bool {
86 matches!(self, NanonisError::Server { .. })
87 }
88
89 pub fn error_code(&self) -> Option<i32> {
91 match self {
92 NanonisError::Server { code, .. } => Some(*code),
93 _ => None,
94 }
95 }
96
97 pub fn is_timeout(&self) -> bool {
99 matches!(self, NanonisError::Timeout(_))
100 }
101
102 pub fn is_io(&self) -> bool {
104 matches!(self, NanonisError::Io { .. })
105 }
106
107 pub fn is_protocol(&self) -> bool {
109 matches!(self, NanonisError::Protocol(_))
110 }
111}
112
113impl NanonisError {
114 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
129impl 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
136impl 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}