Skip to main content

rskit_errors/
convert.rs

1use crate::{AppError, ErrorCode};
2
3// ── std::io::Error ──────────────────────────────────────────────────────────
4
5impl From<std::io::Error> for AppError {
6    fn from(e: std::io::Error) -> Self {
7        use std::io::ErrorKind;
8        // Map common kinds to their semantic code so HTTP status and retry
9        // hints stay meaningful (e.g. a missing file is 404, not 500). The
10        // `io::Error` message is safe to surface for these classified kinds,
11        // but unclassified errors fall through to `internal` so their detail
12        // is preserved only as a (non-serialized) cause.
13        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
31// ── serde_json::Error ───────────────────────────────────────────────────────
32
33impl 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
40// ── std::fmt::Error ─────────────────────────────────────────────────────────
41
42impl From<std::fmt::Error> for AppError {
43    fn from(e: std::fmt::Error) -> Self {
44        AppError::internal(e)
45    }
46}
47
48// ── std::str::Utf8Error ─────────────────────────────────────────────────────
49
50impl 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
57// ── http::StatusCode ────────────────────────────────────────────────────────
58
59impl 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}