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 pub fn network(message: impl Into<String>) -> Self {
116 Self::Network {
117 message: message.into(),
118 status_code: None,
119 }
120 }
121
122 pub fn network_with_status(message: impl Into<String>, status_code: u16) -> Self {
124 Self::Network {
125 message: message.into(),
126 status_code: Some(status_code),
127 }
128 }
129
130 pub fn not_found(message: impl Into<String>) -> Self {
132 Self::NotFound {
133 message: message.into(),
134 }
135 }
136
137 pub fn resolution_failed(message: impl Into<String>) -> Self {
139 Self::ResolutionFailed {
140 message: message.into(),
141 }
142 }
143
144 #[must_use]
146 pub const fn queue_full(max_size: u32) -> Self {
147 Self::QueueFull { max_size }
148 }
149
150 pub fn already_queued(id: impl Into<String>) -> Self {
152 Self::AlreadyQueued { id: id.into() }
153 }
154
155 pub fn not_in_queue(id: impl Into<String>) -> Self {
157 Self::NotInQueue { id: id.into() }
158 }
159
160 pub fn integrity_failed(expected: impl Into<String>, actual: impl Into<String>) -> Self {
162 Self::IntegrityFailed {
163 expected: expected.into(),
164 actual: actual.into(),
165 }
166 }
167
168 pub fn other(message: impl Into<String>) -> Self {
170 Self::Other {
171 message: message.into(),
172 }
173 }
174
175 #[must_use]
177 pub const fn is_recoverable(&self) -> bool {
178 matches!(
179 self,
180 Self::Network { .. } | Self::Interrupted { .. } | Self::Io { .. }
181 )
182 }
183
184 #[must_use]
186 pub const fn is_cancelled(&self) -> bool {
187 matches!(self, Self::Cancelled)
188 }
189
190 #[must_use]
192 pub fn user_message(&self) -> String {
193 match self {
194 Self::Io { message, .. } => format!("File operation failed: {message}"),
195 Self::Network {
196 message,
197 status_code: Some(code),
198 } => {
199 format!("Network error (HTTP {code}): {message}")
200 }
201 Self::Network { message, .. } => format!("Network error: {message}"),
202 Self::NotFound { message } => format!("Not found: {message}"),
203 Self::InvalidQuantization { value } => {
204 format!("Invalid quantization '{value}'. Use values like `Q4_K_M`, `Q5_K_S`, etc.")
205 }
206 Self::ResolutionFailed { message } => format!("Could not resolve file: {message}"),
207 Self::QueueFull { max_size } => {
208 format!(
209 "Download queue is full (max {max_size} items). Wait for a download to complete."
210 )
211 }
212 Self::AlreadyQueued { id } => {
213 format!("Download '{id}' is already in the queue.")
214 }
215 Self::NotInQueue { id } => {
216 format!("Download '{id}' is not in the queue.")
217 }
218 Self::Cancelled => "Download was cancelled.".to_string(),
219 Self::Interrupted { bytes_downloaded } => {
220 format!("Download interrupted after {bytes_downloaded} bytes. You can resume it.")
221 }
222 Self::IntegrityFailed { .. } => {
223 "File integrity check failed. The download may be corrupted.".to_string()
224 }
225 Self::Other { message } => message.clone(),
226 }
227 }
228}
229
230#[cfg(test)]
231mod tests {
232 use super::*;
233
234 #[test]
235 fn test_error_serialization() {
236 let err = DownloadError::network_with_status("timeout", 408);
237 let json = serde_json::to_string(&err).unwrap();
238 assert!(json.contains("408"));
239 assert!(json.contains("timeout"));
240
241 let parsed: DownloadError = serde_json::from_str(&json).unwrap();
242 assert_eq!(parsed, err);
243 }
244
245 #[test]
246 fn test_is_recoverable() {
247 assert!(DownloadError::network("timeout").is_recoverable());
248 assert!(
249 DownloadError::Interrupted {
250 bytes_downloaded: 100
251 }
252 .is_recoverable()
253 );
254 assert!(!DownloadError::Cancelled.is_recoverable());
255 assert!(
256 !DownloadError::InvalidQuantization {
257 value: "bad".to_string(),
258 }
259 .is_recoverable()
260 );
261 }
262
263 #[test]
264 fn test_user_messages() {
265 let err = DownloadError::queue_full(5);
266 assert!(err.user_message().contains('5'));
267 assert!(err.user_message().contains("full"));
268 }
269}