Skip to main content

structfs_ll_store/
error.rs

1//! Error types for the LL layer.
2//!
3//! Errors at this level are transport-focused. No semantic errors like
4//! "invalid path format" or "type mismatch" - those belong in higher layers.
5
6use bytes::Bytes;
7
8/// Errors at the LL (low-level) layer.
9///
10/// These are transport and system-level errors only. Semantic errors
11/// (invalid paths, type mismatches, codec failures) belong in higher layers.
12#[derive(Debug)]
13pub enum LLError {
14    /// Generic I/O or transport failure.
15    ///
16    /// Use this for network errors, file I/O errors, IPC failures, etc.
17    Transport(Box<dyn std::error::Error + Send + Sync>),
18
19    /// The operation is not supported by this store.
20    ///
21    /// For example, writing to a read-only store.
22    NotSupported,
23
24    /// Resource limit exceeded.
25    ///
26    /// Memory exhaustion, too many open handles, etc.
27    ResourceExhausted,
28
29    /// Protocol-specific error with a numeric code.
30    ///
31    /// The code and detail are opaque to the LL layer. Higher layers
32    /// or the transport protocol define their meaning.
33    Protocol {
34        /// Protocol-specific error code.
35        code: u32,
36        /// Optional detail bytes (error message, structured error, etc.)
37        detail: Bytes,
38    },
39}
40
41impl std::fmt::Display for LLError {
42    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
43        match self {
44            LLError::Transport(e) => write!(f, "transport error: {}", e),
45            LLError::NotSupported => write!(f, "operation not supported"),
46            LLError::ResourceExhausted => write!(f, "resource exhausted"),
47            LLError::Protocol { code, detail } => {
48                if detail.is_empty() {
49                    write!(f, "protocol error: code {}", code)
50                } else {
51                    // Try to display detail as UTF-8, fall back to hex
52                    match std::str::from_utf8(detail) {
53                        Ok(s) => write!(f, "protocol error: code {} - {}", code, s),
54                        Err(_) => write!(f, "protocol error: code {} - {:?}", code, detail),
55                    }
56                }
57            }
58        }
59    }
60}
61
62impl std::error::Error for LLError {
63    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
64        match self {
65            LLError::Transport(e) => Some(e.as_ref()),
66            _ => None,
67        }
68    }
69}
70
71impl From<std::io::Error> for LLError {
72    fn from(e: std::io::Error) -> Self {
73        LLError::Transport(Box::new(e))
74    }
75}
76
77#[cfg(test)]
78mod tests {
79    use super::*;
80    use std::error::Error as StdError;
81
82    #[test]
83    fn error_display_works() {
84        let e = LLError::NotSupported;
85        assert_eq!(format!("{}", e), "operation not supported");
86
87        let e = LLError::Protocol {
88            code: 42,
89            detail: Bytes::from_static(b"something went wrong"),
90        };
91        assert!(format!("{}", e).contains("42"));
92        assert!(format!("{}", e).contains("something went wrong"));
93    }
94
95    #[test]
96    fn resource_exhausted_display() {
97        let e = LLError::ResourceExhausted;
98        assert_eq!(format!("{}", e), "resource exhausted");
99    }
100
101    #[test]
102    fn protocol_empty_detail_display() {
103        let e = LLError::Protocol {
104            code: 100,
105            detail: Bytes::new(),
106        };
107        assert_eq!(format!("{}", e), "protocol error: code 100");
108    }
109
110    #[test]
111    fn protocol_non_utf8_detail_display() {
112        let e = LLError::Protocol {
113            code: 200,
114            detail: Bytes::from_static(&[0xFF, 0xFE, 0x00]),
115        };
116        let display = format!("{}", e);
117        assert!(display.contains("200"));
118        // Should fall back to debug format
119        assert!(display.contains("protocol error"));
120    }
121
122    #[test]
123    fn transport_error_display() {
124        let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
125        let e = LLError::Transport(Box::new(io_err));
126        let display = format!("{}", e);
127        assert!(display.contains("transport error"));
128        assert!(display.contains("file not found"));
129    }
130
131    #[test]
132    fn transport_error_source() {
133        let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
134        let e = LLError::Transport(Box::new(io_err));
135        assert!(StdError::source(&e).is_some());
136    }
137
138    #[test]
139    fn non_transport_error_source_is_none() {
140        let e = LLError::NotSupported;
141        assert!(StdError::source(&e).is_none());
142
143        let e = LLError::ResourceExhausted;
144        assert!(StdError::source(&e).is_none());
145
146        let e = LLError::Protocol {
147            code: 1,
148            detail: Bytes::new(),
149        };
150        assert!(StdError::source(&e).is_none());
151    }
152
153    #[test]
154    fn io_error_converts() {
155        let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
156        let ll_err: LLError = io_err.into();
157        assert!(matches!(ll_err, LLError::Transport(_)));
158    }
159}