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