1use std::fmt;
4
5use thiserror::Error;
6
7use crate::format_convert::FormatConvertError;
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13#[non_exhaustive]
14pub enum IoOperationKind {
15 Read,
17 Write,
19 Create,
21 Delete,
23 Rename,
25 CreateDir,
27 ReadDir,
29 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
48fn 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#[derive(Error, Debug)]
64#[non_exhaustive]
65pub enum StoreError {
66 #[error("{}", format_io_error(.operation, .path, .context, .error))]
71 IoError {
72 operation: IoOperationKind,
74 path: String,
76 context: Option<String>,
78 error: String,
80 },
81
82 #[error("Cannot determine home directory")]
84 HomeDirNotFound,
85
86 #[error("Failed to encode filename for ID '{id}': {reason}")]
96 FilenameEncoding {
97 id: String,
99 reason: String,
101 },
102
103 #[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}