1use crate::{AppError, ErrorCode};
2
3impl From<std::io::Error> for AppError {
6 fn from(e: std::io::Error) -> Self {
7 use std::io::ErrorKind;
8 let code = match e.kind() {
14 ErrorKind::NotFound => ErrorCode::NotFound,
15 ErrorKind::PermissionDenied => ErrorCode::Forbidden,
16 ErrorKind::AlreadyExists => ErrorCode::AlreadyExists,
17 ErrorKind::TimedOut => ErrorCode::Timeout,
18 ErrorKind::ConnectionRefused
19 | ErrorKind::ConnectionReset
20 | ErrorKind::ConnectionAborted
21 | ErrorKind::NotConnected
22 | ErrorKind::BrokenPipe => ErrorCode::ConnectionFailed,
23 ErrorKind::InvalidInput | ErrorKind::InvalidData => ErrorCode::InvalidInput,
24 _ => return AppError::internal(e),
25 };
26 let message = e.to_string();
27 AppError::new(code, message).with_cause(e)
28 }
29}
30
31impl From<serde_json::Error> for AppError {
34 fn from(e: serde_json::Error) -> Self {
35 let message = e.to_string();
36 AppError::new(ErrorCode::InvalidFormat, message).with_cause(e)
37 }
38}
39
40impl From<std::fmt::Error> for AppError {
43 fn from(e: std::fmt::Error) -> Self {
44 AppError::internal(e)
45 }
46}
47
48impl From<std::str::Utf8Error> for AppError {
51 fn from(e: std::str::Utf8Error) -> Self {
52 let message = e.to_string();
53 AppError::new(ErrorCode::InvalidInput, message).with_cause(e)
54 }
55}
56
57impl From<&AppError> for http::StatusCode {
60 fn from(e: &AppError) -> Self {
61 e.http_status()
62 }
63}
64
65#[cfg(test)]
66mod tests {
67 use super::*;
68
69 #[test]
70 fn io_not_found_maps_to_not_found() {
71 let io = std::io::Error::new(std::io::ErrorKind::NotFound, "missing");
72 let err = AppError::from(io);
73 assert_eq!(err.code(), ErrorCode::NotFound);
74 assert_eq!(err.http_status(), http::StatusCode::NOT_FOUND);
75 assert!(err.cause().is_some());
76 }
77
78 #[test]
79 fn io_permission_denied_maps_to_forbidden() {
80 let io = std::io::Error::new(std::io::ErrorKind::PermissionDenied, "denied");
81 let err = AppError::from(io);
82 assert_eq!(err.code(), ErrorCode::Forbidden);
83 }
84
85 #[test]
86 fn io_timed_out_maps_to_timeout_and_is_retryable() {
87 let io = std::io::Error::new(std::io::ErrorKind::TimedOut, "slow");
88 let err = AppError::from(io);
89 assert_eq!(err.code(), ErrorCode::Timeout);
90 assert!(err.is_retryable());
91 }
92
93 #[test]
94 fn io_connection_refused_maps_to_connection_failed() {
95 let io = std::io::Error::new(std::io::ErrorKind::ConnectionRefused, "refused");
96 let err = AppError::from(io);
97 assert_eq!(err.code(), ErrorCode::ConnectionFailed);
98 }
99
100 #[test]
101 fn io_other_maps_to_internal_with_generic_message() {
102 let io = std::io::Error::other("weird secret detail");
103 let err = AppError::from(io);
104 assert_eq!(err.code(), ErrorCode::Internal);
105 assert_eq!(err.message(), "internal server error");
106 assert!(err.cause().is_some());
107 }
108
109 #[test]
110 fn serde_json_error_maps_to_invalid_format_with_cause() {
111 let e = serde_json::from_str::<i32>("not json").unwrap_err();
112 let err = AppError::from(e);
113 assert_eq!(err.code(), ErrorCode::InvalidFormat);
114 assert!(err.cause().is_some());
115 }
116}