gglib_core/download/
errors.rs1use serde::{Deserialize, Serialize};
8use thiserror::Error;
9
10#[derive(Clone, Debug, Error, Serialize, Deserialize, PartialEq, Eq)]
15pub enum DownloadError {
16 #[error("I/O error ({kind}): {message}")]
18 Io {
19 kind: String,
21 message: String,
23 },
24
25 #[error("Network error: {message}")]
27 Network {
28 message: String,
30 #[serde(skip_serializing_if = "Option::is_none")]
32 status_code: Option<u16>,
33 },
34
35 #[error("Not found: {message}")]
37 NotFound {
38 message: String,
40 },
41
42 #[error("Invalid quantization: {value}")]
44 InvalidQuantization {
45 value: String,
47 },
48
49 #[error("Resolution failed: {message}")]
51 ResolutionFailed {
52 message: String,
54 },
55
56 #[error("Queue full: maximum {max_size} downloads allowed")]
58 QueueFull {
59 max_size: u32,
61 },
62
63 #[error("Already queued: {id}")]
65 AlreadyQueued {
66 id: String,
68 },
69
70 #[error("Not in queue: {id}")]
72 NotInQueue {
73 id: String,
75 },
76
77 #[error("Download cancelled")]
79 Cancelled,
80
81 #[error("Download interrupted at {bytes_downloaded} bytes")]
83 Interrupted {
84 bytes_downloaded: u64,
86 },
87
88 #[error("Integrity check failed: expected {expected}, got {actual}")]
90 IntegrityFailed {
91 expected: String,
93 actual: String,
95 },
96
97 #[error("{message}")]
99 Other {
100 message: String,
102 },
103}
104
105impl DownloadError {
106 pub fn io(kind: impl Into<String>, message: impl Into<String>) -> Self {
108 Self::Io {
109 kind: kind.into(),
110 message: message.into(),
111 }
112 }
113
114 #[must_use]
118 pub fn from_io_error(err: &std::io::Error) -> Self {
119 let kind = err.kind();
120 Self::Io {
121 kind: format!("{kind:?}"),
122 message: err.to_string(),
123 }
124 }
125
126 pub fn network(message: impl Into<String>) -> Self {
128 Self::Network {
129 message: message.into(),
130 status_code: None,
131 }
132 }
133
134 pub fn network_with_status(message: impl Into<String>, status_code: u16) -> Self {
136 Self::Network {
137 message: message.into(),
138 status_code: Some(status_code),
139 }
140 }
141
142 pub fn not_found(message: impl Into<String>) -> Self {
144 Self::NotFound {
145 message: message.into(),
146 }
147 }
148
149 pub fn invalid_quantization(value: impl Into<String>) -> Self {
151 Self::InvalidQuantization {
152 value: value.into(),
153 }
154 }
155
156 pub fn resolution_failed(message: impl Into<String>) -> Self {
158 Self::ResolutionFailed {
159 message: message.into(),
160 }
161 }
162
163 #[must_use]
165 pub const fn queue_full(max_size: u32) -> Self {
166 Self::QueueFull { max_size }
167 }
168
169 pub fn already_queued(id: impl Into<String>) -> Self {
171 Self::AlreadyQueued { id: id.into() }
172 }
173
174 pub fn not_in_queue(id: impl Into<String>) -> Self {
176 Self::NotInQueue { id: id.into() }
177 }
178
179 pub fn integrity_failed(expected: impl Into<String>, actual: impl Into<String>) -> Self {
181 Self::IntegrityFailed {
182 expected: expected.into(),
183 actual: actual.into(),
184 }
185 }
186
187 pub fn other(message: impl Into<String>) -> Self {
189 Self::Other {
190 message: message.into(),
191 }
192 }
193
194 #[must_use]
196 pub const fn is_recoverable(&self) -> bool {
197 matches!(
198 self,
199 Self::Network { .. } | Self::Interrupted { .. } | Self::Io { .. }
200 )
201 }
202
203 #[must_use]
205 pub const fn is_cancelled(&self) -> bool {
206 matches!(self, Self::Cancelled)
207 }
208
209 #[must_use]
211 pub fn user_message(&self) -> String {
212 match self {
213 Self::Io { message, .. } => format!("File operation failed: {message}"),
214 Self::Network {
215 message,
216 status_code: Some(code),
217 } => {
218 format!("Network error (HTTP {code}): {message}")
219 }
220 Self::Network { message, .. } => format!("Network error: {message}"),
221 Self::NotFound { message } => format!("Not found: {message}"),
222 Self::InvalidQuantization { value } => {
223 format!("Invalid quantization '{value}'. Use values like `Q4_K_M`, `Q5_K_S`, etc.")
224 }
225 Self::ResolutionFailed { message } => format!("Could not resolve file: {message}"),
226 Self::QueueFull { max_size } => {
227 format!(
228 "Download queue is full (max {max_size} items). Wait for a download to complete."
229 )
230 }
231 Self::AlreadyQueued { id } => {
232 format!("Download '{id}' is already in the queue.")
233 }
234 Self::NotInQueue { id } => {
235 format!("Download '{id}' is not in the queue.")
236 }
237 Self::Cancelled => "Download was cancelled.".to_string(),
238 Self::Interrupted { bytes_downloaded } => {
239 format!("Download interrupted after {bytes_downloaded} bytes. You can resume it.")
240 }
241 Self::IntegrityFailed { .. } => {
242 "File integrity check failed. The download may be corrupted.".to_string()
243 }
244 Self::Other { message } => message.clone(),
245 }
246 }
247}
248
249pub type DownloadResult<T> = Result<T, DownloadError>;
251
252#[cfg(test)]
253mod tests {
254 use super::*;
255
256 #[test]
257 fn test_io_error_from_std() {
258 let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
259 let err = DownloadError::from_io_error(&io_err);
260
261 match err {
262 DownloadError::Io { kind, message } => {
263 assert_eq!(kind, "NotFound");
264 assert!(message.contains("file not found"));
265 }
266 _ => panic!("Expected Io variant"),
267 }
268 }
269
270 #[test]
271 fn test_error_serialization() {
272 let err = DownloadError::network_with_status("timeout", 408);
273 let json = serde_json::to_string(&err).unwrap();
274 assert!(json.contains("408"));
275 assert!(json.contains("timeout"));
276
277 let parsed: DownloadError = serde_json::from_str(&json).unwrap();
278 assert_eq!(parsed, err);
279 }
280
281 #[test]
282 fn test_is_recoverable() {
283 assert!(DownloadError::network("timeout").is_recoverable());
284 assert!(
285 DownloadError::Interrupted {
286 bytes_downloaded: 100
287 }
288 .is_recoverable()
289 );
290 assert!(!DownloadError::Cancelled.is_recoverable());
291 assert!(!DownloadError::invalid_quantization("bad").is_recoverable());
292 }
293
294 #[test]
295 fn test_user_messages() {
296 let err = DownloadError::queue_full(5);
297 assert!(err.user_message().contains('5'));
298 assert!(err.user_message().contains("full"));
299 }
300}