Skip to main content

local_store/
errors.rs

1//! Error types for local store operations.
2
3use std::fmt;
4
5use thiserror::Error;
6
7use crate::format_convert::FormatConvertError;
8
9/// File I/O operation kind.
10///
11/// Identifies the specific type of I/O operation that failed.
12#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13pub enum IoOperationKind {
14    /// Reading from a file
15    Read,
16    /// Writing to a file
17    Write,
18    /// Creating a new file
19    Create,
20    /// Deleting a file
21    Delete,
22    /// Renaming/moving a file
23    Rename,
24    /// Creating a directory
25    CreateDir,
26    /// Reading directory contents
27    ReadDir,
28    /// Syncing file contents to disk
29    Sync,
30}
31
32impl fmt::Display for IoOperationKind {
33    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
34        match self {
35            Self::Read => write!(f, "read"),
36            Self::Write => write!(f, "write"),
37            Self::Create => write!(f, "create"),
38            Self::Delete => write!(f, "delete"),
39            Self::Rename => write!(f, "rename"),
40            Self::CreateDir => write!(f, "create directory"),
41            Self::ReadDir => write!(f, "read directory"),
42            Self::Sync => write!(f, "sync"),
43        }
44    }
45}
46
47/// Format I/O error message with operation, path, context, and error details.
48fn format_io_error(
49    operation: &IoOperationKind,
50    path: &str,
51    context: &Option<String>,
52    error: &str,
53) -> String {
54    if let Some(ctx) = context {
55        format!("Failed to {} {} at '{}': {}", operation, ctx, path, error)
56    } else {
57        format!("Failed to {} file at '{}': {}", operation, path, error)
58    }
59}
60
61/// Error types for path and store operations.
62#[derive(Error, Debug)]
63#[non_exhaustive]
64pub enum StoreError {
65    /// File I/O error with detailed operation context.
66    ///
67    /// Provides specific information about which I/O operation failed,
68    /// along with optional context (e.g., "temporary file", "after 3 retries").
69    #[error("{}", format_io_error(.operation, .path, .context, .error))]
70    IoError {
71        /// The I/O operation that failed.
72        operation: IoOperationKind,
73        /// The file path where the error occurred.
74        path: String,
75        /// Additional context (e.g., "temporary file", "after 3 retries").
76        context: Option<String>,
77        /// The underlying I/O error message.
78        error: String,
79    },
80
81    /// Failed to find home directory.
82    #[error("Cannot determine home directory")]
83    HomeDirNotFound,
84
85    /// Failed to encode or decode a filename for the given entity ID.
86    ///
87    /// Raised when a filename encoding strategy (Direct/UrlEncode/Base64) cannot
88    /// encode the ID on write, or cannot decode the stored filename on read.
89    ///
90    /// # Arguments
91    ///
92    /// * `id` - The entity ID that could not be encoded/decoded.
93    /// * `reason` - A human-readable explanation of the failure.
94    #[error("Failed to encode filename for ID '{id}': {reason}")]
95    FilenameEncoding {
96        /// The entity ID involved in the encoding failure.
97        id: String,
98        /// Human-readable reason for the failure.
99        reason: String,
100    },
101
102    /// Format conversion failed (e.g. JSON → TOML serialization error).
103    ///
104    /// Wraps a [`FormatConvertError`] produced by `local_store::format_convert`.
105    #[error("format conversion: {0}")]
106    FormatConvert(#[from] FormatConvertError),
107}
108
109#[cfg(test)]
110mod tests {
111    use super::*;
112
113    #[test]
114    fn test_store_error_io_error_display_without_context() {
115        let err = StoreError::IoError {
116            operation: IoOperationKind::Read,
117            path: "/path/to/file.toml".to_string(),
118            context: None,
119            error: "Permission denied".to_string(),
120        };
121        let display = format!("{}", err);
122        assert!(display.contains("Failed to read"));
123        assert!(display.contains("/path/to/file.toml"));
124        assert!(display.contains("Permission denied"));
125    }
126
127    #[test]
128    fn test_store_error_io_error_display_with_context() {
129        let err = StoreError::IoError {
130            operation: IoOperationKind::Write,
131            path: "/path/to/tmp.toml".to_string(),
132            context: Some("temporary file".to_string()),
133            error: "Disk full".to_string(),
134        };
135        let display = format!("{}", err);
136        assert!(display.contains("Failed to write"));
137        assert!(display.contains("temporary file"));
138        assert!(display.contains("/path/to/tmp.toml"));
139        assert!(display.contains("Disk full"));
140    }
141
142    #[test]
143    fn test_store_error_home_dir_not_found_display() {
144        let err = StoreError::HomeDirNotFound;
145        let display = format!("{}", err);
146        assert!(display.contains("Cannot determine home directory"));
147    }
148
149    #[test]
150    fn test_store_error_is_std_error() {
151        let err = StoreError::HomeDirNotFound;
152        let _: &dyn std::error::Error = &err;
153    }
154
155    #[test]
156    fn test_io_operation_kind_display() {
157        assert_eq!(IoOperationKind::Read.to_string(), "read");
158        assert_eq!(IoOperationKind::Write.to_string(), "write");
159        assert_eq!(IoOperationKind::Create.to_string(), "create");
160        assert_eq!(IoOperationKind::Delete.to_string(), "delete");
161        assert_eq!(IoOperationKind::Rename.to_string(), "rename");
162        assert_eq!(IoOperationKind::CreateDir.to_string(), "create directory");
163        assert_eq!(IoOperationKind::ReadDir.to_string(), "read directory");
164        assert_eq!(IoOperationKind::Sync.to_string(), "sync");
165    }
166
167    #[test]
168    fn test_store_error_filename_encoding_display() {
169        let err = StoreError::FilenameEncoding {
170            id: "my/id".to_string(),
171            reason: "ID contains invalid characters for Direct encoding".to_string(),
172        };
173        let display = format!("{}", err);
174        assert!(display.contains("my/id"), "display should contain id");
175        assert!(
176            display.contains("invalid characters"),
177            "display should contain reason"
178        );
179    }
180
181    #[test]
182    fn test_store_error_io_error_rename_with_retries() {
183        let err = StoreError::IoError {
184            operation: IoOperationKind::Rename,
185            path: "/path/to/file.toml".to_string(),
186            context: Some("after 3 retries".to_string()),
187            error: "Resource temporarily unavailable".to_string(),
188        };
189        let display = format!("{}", err);
190        assert!(display.contains("Failed to rename"));
191        assert!(display.contains("after 3 retries"));
192        assert!(display.contains("/path/to/file.toml"));
193        assert!(display.contains("Resource temporarily unavailable"));
194    }
195}