Skip to main content

structfs_core_store/
error.rs

1//! Error types for the Core layer.
2
3use crate::format::Format;
4use crate::path::{Path, PathError};
5
6/// Stable semantic categories for value conversion and codec failures.
7#[derive(Debug, Clone, Copy, PartialEq, Eq)]
8#[non_exhaustive]
9pub enum CodecErrorKind {
10    UnsupportedProfile,
11    UnsupportedVersion,
12    Syntax,
13    InvalidUnicode,
14    InvalidNode,
15    InvalidBase64,
16    DuplicateKey,
17    OutOfRange,
18    UnsupportedValue,
19    TypeMismatch,
20    AmbiguousOption,
21    Noncanonical,
22    ResourceLimit,
23    Io,
24}
25
26impl CodecErrorKind {
27    /// Stable machine-readable name from the v1 specification.
28    pub const fn as_str(self) -> &'static str {
29        match self {
30            Self::UnsupportedProfile => "unsupported_profile",
31            Self::UnsupportedVersion => "unsupported_version",
32            Self::Syntax => "syntax",
33            Self::InvalidUnicode => "invalid_unicode",
34            Self::InvalidNode => "invalid_node",
35            Self::InvalidBase64 => "invalid_base64",
36            Self::DuplicateKey => "duplicate_key",
37            Self::OutOfRange => "out_of_range",
38            Self::UnsupportedValue => "unsupported_value",
39            Self::TypeMismatch => "type_mismatch",
40            Self::AmbiguousOption => "ambiguous_option",
41            Self::Noncanonical => "noncanonical",
42            Self::ResourceLimit => "resource_limit",
43            Self::Io => "io",
44        }
45    }
46}
47
48/// Whether a codec error occurred during encoding or decoding.
49#[derive(Debug, Clone, Copy, PartialEq, Eq)]
50pub enum CodecOperation {
51    Encode,
52    Decode,
53}
54
55impl std::fmt::Display for CodecOperation {
56    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
57        match self {
58            CodecOperation::Encode => write!(f, "encode"),
59            CodecOperation::Decode => write!(f, "decode"),
60        }
61    }
62}
63
64/// Errors at the Core layer.
65///
66/// These include semantic errors (invalid paths, codec failures) in addition
67/// to the transport errors from the LL layer.
68///
69/// The typed variants (`NotFound`, `PermissionDenied`, `Conflict`,
70/// `Overloaded`, `DeadlineExceeded`, `ResourceLimit`) exist so that stores
71/// and transports can map errors structurally instead of string-matching
72/// messages. Prefer them over `Store` when one fits.
73#[derive(Debug)]
74#[non_exhaustive]
75pub enum Error {
76    /// Path validation error.
77    Path(PathError),
78
79    /// No route found for path (in overlay/mount stores).
80    NoRoute { path: Path },
81
82    /// Codec error during encode/decode.
83    Codec {
84        kind: CodecErrorKind,
85        operation: CodecOperation,
86        format: Format,
87        message: String,
88    },
89
90    /// Format not supported by codec.
91    UnsupportedFormat(Format),
92
93    /// Error from the LL layer.
94    Ll(structfs_ll_store::LLError),
95
96    /// I/O error (filesystem, network).
97    Io(std::io::Error),
98
99    /// Store-specific error with context.
100    Store {
101        store: &'static str,
102        operation: &'static str,
103        message: String,
104    },
105
106    /// An operation referenced a path that does not exist.
107    ///
108    /// Note: `Reader::read` signals a missing path with `Ok(None)`; this
109    /// variant is for operations that *require* the path to exist (writes
110    /// to handles, deletes, remote calls).
111    NotFound { path: Path },
112
113    /// The caller is not allowed to perform this operation.
114    PermissionDenied { message: String },
115
116    /// The operation conflicts with concurrent or existing state
117    /// (stale version, handle already exists, etc.).
118    Conflict { message: String },
119
120    /// The store is temporarily unable to accept the operation.
121    Overloaded { message: String },
122
123    /// The operation did not complete within its deadline.
124    DeadlineExceeded { message: String },
125
126    /// A resource limit was exceeded (size, depth, count, quota).
127    ResourceLimit { message: String },
128
129    /// The operation was cancelled — typically a parked read whose handle
130    /// was released or whose store is shutting down.
131    Cancelled { message: String },
132}
133
134impl Error {
135    /// Create a store-specific error.
136    pub fn store(store: &'static str, operation: &'static str, message: impl Into<String>) -> Self {
137        Error::Store {
138            store,
139            operation,
140            message: message.into(),
141        }
142    }
143
144    /// Create a codec decode error.
145    pub fn decode(format: Format, message: impl Into<String>) -> Self {
146        Error::Codec {
147            kind: CodecErrorKind::Syntax,
148            operation: CodecOperation::Decode,
149            format,
150            message: message.into(),
151        }
152    }
153
154    /// Create a codec encode error.
155    pub fn encode(format: Format, message: impl Into<String>) -> Self {
156        Error::Codec {
157            kind: CodecErrorKind::UnsupportedValue,
158            operation: CodecOperation::Encode,
159            format,
160            message: message.into(),
161        }
162    }
163
164    /// Create a not-found error for a path.
165    pub fn not_found(path: Path) -> Self {
166        Error::NotFound { path }
167    }
168
169    /// Create a permission-denied error.
170    pub fn permission_denied(message: impl Into<String>) -> Self {
171        Error::PermissionDenied {
172            message: message.into(),
173        }
174    }
175
176    /// Create a conflict error.
177    pub fn conflict(message: impl Into<String>) -> Self {
178        Error::Conflict {
179            message: message.into(),
180        }
181    }
182
183    /// Create an overloaded error.
184    pub fn overloaded(message: impl Into<String>) -> Self {
185        Error::Overloaded {
186            message: message.into(),
187        }
188    }
189
190    /// Create a deadline-exceeded error.
191    pub fn deadline_exceeded(message: impl Into<String>) -> Self {
192        Error::DeadlineExceeded {
193            message: message.into(),
194        }
195    }
196
197    /// Create a resource-limit error.
198    pub fn resource_limit(message: impl Into<String>) -> Self {
199        Error::ResourceLimit {
200            message: message.into(),
201        }
202    }
203
204    /// Create a cancelled error.
205    pub fn cancelled(message: impl Into<String>) -> Self {
206        Error::Cancelled {
207            message: message.into(),
208        }
209    }
210
211    /// Whether this error is a cancellation.
212    pub fn is_cancelled(&self) -> bool {
213        matches!(self, Error::Cancelled { .. })
214    }
215
216    /// Whether this error is a not-found error.
217    pub fn is_not_found(&self) -> bool {
218        matches!(self, Error::NotFound { .. })
219    }
220}
221
222impl std::fmt::Display for Error {
223    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
224        match self {
225            Error::Path(e) => write!(f, "path error: {}", e),
226            Error::NoRoute { path } => write!(f, "no route to {}", path),
227            Error::Codec {
228                operation,
229                format,
230                message,
231                ..
232            } => {
233                write!(f, "{} failed for format {}: {}", operation, format, message)
234            }
235            Error::UnsupportedFormat(format) => {
236                write!(f, "unsupported format: {}", format)
237            }
238            Error::Ll(e) => write!(f, "low-level error: {}", e),
239            Error::Io(e) => write!(f, "I/O error: {}", e),
240            Error::Store {
241                store,
242                operation,
243                message,
244            } => write!(f, "{}::{}: {}", store, operation, message),
245            Error::NotFound { path } => write!(f, "not found: {}", path),
246            Error::PermissionDenied { message } => write!(f, "permission denied: {}", message),
247            Error::Conflict { message } => write!(f, "conflict: {}", message),
248            Error::Overloaded { message } => write!(f, "overloaded: {}", message),
249            Error::DeadlineExceeded { message } => write!(f, "deadline exceeded: {}", message),
250            Error::ResourceLimit { message } => write!(f, "resource limit: {}", message),
251            Error::Cancelled { message } => write!(f, "cancelled: {}", message),
252        }
253    }
254}
255
256impl std::error::Error for Error {
257    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
258        match self {
259            Error::Path(e) => Some(e),
260            Error::Ll(e) => Some(e),
261            Error::Io(e) => Some(e),
262            _ => None,
263        }
264    }
265}
266
267impl From<PathError> for Error {
268    fn from(e: PathError) -> Self {
269        Error::Path(e)
270    }
271}
272
273impl From<structfs_ll_store::LLError> for Error {
274    fn from(e: structfs_ll_store::LLError) -> Self {
275        Error::Ll(e)
276    }
277}
278
279impl From<std::io::Error> for Error {
280    fn from(e: std::io::Error) -> Self {
281        Error::Io(e)
282    }
283}
284
285#[cfg(test)]
286mod tests {
287    use super::*;
288    use std::error::Error as StdError;
289
290    #[test]
291    fn error_display() {
292        let e = Error::NoRoute {
293            path: Path::parse("foo/bar").unwrap(),
294        };
295        assert_eq!(e.to_string(), "no route to foo/bar");
296
297        let e = Error::UnsupportedFormat(Format::PROTOBUF);
298        assert!(format!("{}", e).contains("protobuf"));
299    }
300
301    #[test]
302    fn path_error_display() {
303        let e = Error::Path(PathError::InvalidComponent {
304            component: "bad".to_string(),
305            position: 1,
306            message: "invalid".to_string(),
307        });
308        assert!(format!("{}", e).contains("path error"));
309    }
310
311    #[test]
312    fn codec_decode_error_display() {
313        let e = Error::decode(Format::JSON, "unexpected token");
314        let display = format!("{}", e);
315        assert!(display.contains("decode"));
316        assert!(display.contains("json"));
317        assert!(display.contains("unexpected token"));
318    }
319
320    #[test]
321    fn codec_encode_error_display() {
322        let e = Error::encode(Format::CBOR, "serialization failed");
323        let display = format!("{}", e);
324        assert!(display.contains("encode"));
325        assert!(display.contains("cbor"));
326        assert!(display.contains("serialization failed"));
327    }
328
329    #[test]
330    fn ll_error_display() {
331        let ll_err = structfs_ll_store::LLError::NotSupported;
332        let e = Error::Ll(ll_err);
333        let display = format!("{}", e);
334        assert!(display.contains("low-level error"));
335    }
336
337    #[test]
338    fn store_error_display() {
339        let e = Error::store("http_broker", "read", "Request 42 not found");
340        assert_eq!(e.to_string(), "http_broker::read: Request 42 not found");
341    }
342
343    #[test]
344    fn io_error_display() {
345        let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
346        let e = Error::Io(io_err);
347        let display = format!("{}", e);
348        assert!(display.contains("I/O error"));
349        assert!(display.contains("file not found"));
350    }
351
352    #[test]
353    fn path_error_source() {
354        let e = Error::Path(PathError::InvalidPath {
355            message: "test".to_string(),
356        });
357        assert!(StdError::source(&e).is_some());
358    }
359
360    #[test]
361    fn ll_error_source() {
362        let ll_err = structfs_ll_store::LLError::NotSupported;
363        let e = Error::Ll(ll_err);
364        assert!(StdError::source(&e).is_some());
365    }
366
367    #[test]
368    fn io_error_source() {
369        let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
370        let e = Error::Io(io_err);
371        assert!(StdError::source(&e).is_some());
372    }
373
374    #[test]
375    fn store_error_source_is_none() {
376        let e = Error::store("test", "op", "message");
377        assert!(StdError::source(&e).is_none());
378    }
379
380    #[test]
381    fn path_error_conversion() {
382        let path_err = PathError::InvalidPath {
383            message: "test".to_string(),
384        };
385        let e: Error = path_err.into();
386        assert!(matches!(e, Error::Path(_)));
387    }
388
389    #[test]
390    fn ll_error_conversion() {
391        let ll_err = structfs_ll_store::LLError::ResourceExhausted;
392        let e: Error = ll_err.into();
393        assert!(matches!(e, Error::Ll(_)));
394    }
395
396    #[test]
397    fn io_error_conversion() {
398        let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
399        let e: Error = io_err.into();
400        assert!(matches!(e, Error::Io(_)));
401    }
402
403    #[test]
404    fn typed_variant_display() {
405        let e = Error::not_found(Path::parse("users/123").unwrap());
406        assert_eq!(e.to_string(), "not found: users/123");
407        assert!(e.is_not_found());
408
409        assert_eq!(
410            Error::permission_denied("read-only mount").to_string(),
411            "permission denied: read-only mount"
412        );
413        assert_eq!(
414            Error::conflict("stale version").to_string(),
415            "conflict: stale version"
416        );
417        assert_eq!(
418            Error::overloaded("queue full").to_string(),
419            "overloaded: queue full"
420        );
421        assert_eq!(
422            Error::deadline_exceeded("10s elapsed").to_string(),
423            "deadline exceeded: 10s elapsed"
424        );
425        assert_eq!(
426            Error::resource_limit("frame too large").to_string(),
427            "resource limit: frame too large"
428        );
429    }
430
431    #[test]
432    fn typed_variants_have_no_source() {
433        assert!(StdError::source(&Error::conflict("x")).is_none());
434        assert!(!Error::conflict("x").is_not_found());
435    }
436
437    #[test]
438    fn codec_operation_display() {
439        assert_eq!(CodecOperation::Encode.to_string(), "encode");
440        assert_eq!(CodecOperation::Decode.to_string(), "decode");
441    }
442
443    #[test]
444    fn codec_error_with_operation() {
445        let e = Error::Codec {
446            kind: CodecErrorKind::Syntax,
447            operation: CodecOperation::Decode,
448            format: Format::JSON,
449            message: "test".to_string(),
450        };
451        assert!(matches!(
452            e,
453            Error::Codec {
454                operation: CodecOperation::Decode,
455                ..
456            }
457        ));
458    }
459}