Skip to main content

soar_dl/
error.rs

1use miette::Diagnostic;
2use thiserror::Error;
3
4#[derive(Error, Diagnostic, Debug)]
5pub enum DownloadError {
6    #[error("Invalid URL: {url}")]
7    #[diagnostic(code(soar_dl::invalid_url))]
8    InvalidUrl {
9        url: String,
10        #[source]
11        source: url::ParseError,
12    },
13
14    #[error(transparent)]
15    #[diagnostic(code(soar_dl::extract_error))]
16    ExtractError(#[from] compak::error::ArchiveError),
17
18    #[error(transparent)]
19    #[diagnostic(
20        code(soar_dl::network),
21        help("Check your internet connection or try again later")
22    )]
23    Network(#[from] Box<ureq::Error>),
24
25    #[error("HTTP {status}: {url}")]
26    #[diagnostic(code(soar_dl::http_error))]
27    HttpError { status: u16, url: String },
28
29    #[error(transparent)]
30    #[diagnostic(code(soar_dl::io))]
31    Io(#[from] std::io::Error),
32
33    #[error("No matching assets found")]
34    #[diagnostic(
35        code(soar_dl::no_match),
36        help("Available assets:\n{}", .available.join("\n"))
37    )]
38    NoMatch { available: Vec<String> },
39
40    #[error("Layer not found")]
41    #[diagnostic(code(soar_dl::layer_not_found))]
42    LayerNotFound,
43
44    #[error("Invalid response from server")]
45    #[diagnostic(code(soar_dl::invalid_response))]
46    InvalidResponse,
47
48    #[error("File name could not be determined")]
49    #[diagnostic(
50        code(soar_dl::no_filename),
51        help("Try specifying an output path explicitly")
52    )]
53    NoFilename,
54
55    #[error("Resume metadata mismatch")]
56    #[diagnostic(code(soar_dl::resume_mismatch))]
57    ResumeMismatch,
58
59    #[error("Multiple download errors occurred")]
60    #[diagnostic(code(soar_dl::multiple_errors))]
61    Multiple { errors: Vec<String> },
62}
63
64pub type Result<T> = miette::Result<T>;
65
66impl From<ureq::Error> for DownloadError {
67    /// Converts a `ureq::Error` into a `DownloadError::Network` variant.
68    ///
69    /// # Examples
70    ///
71    /// ```no_run
72    /// use soar_dl::error::DownloadError;
73    ///
74    /// // Given a `ureq::Error` `e`, convert it into a `DownloadError`
75    /// let e: ureq::Error = /* obtained from a ureq request */ unimplemented!();
76    /// let err: DownloadError = DownloadError::from(e);
77    /// match err {
78    ///     DownloadError::Network(_) => (),
79    ///     _ => panic!("expected DownloadError::Network"),
80    /// }
81    /// ```
82    fn from(e: ureq::Error) -> Self {
83        Self::Network(Box::new(e))
84    }
85}
86
87#[cfg(test)]
88mod tests {
89    use super::*;
90
91    #[test]
92    fn test_download_error_invalid_url() {
93        let err = DownloadError::InvalidUrl {
94            url: "invalid".to_string(),
95            source: url::ParseError::RelativeUrlWithoutBase,
96        };
97        let msg = format!("{}", err);
98        assert!(msg.contains("Invalid URL"));
99        assert!(msg.contains("invalid"));
100    }
101
102    #[test]
103    fn test_download_error_http_error() {
104        let err = DownloadError::HttpError {
105            status: 404,
106            url: "https://example.com/notfound".to_string(),
107        };
108        let msg = format!("{}", err);
109        assert!(msg.contains("HTTP 404"));
110        assert!(msg.contains("https://example.com/notfound"));
111    }
112
113    #[test]
114    fn test_download_error_no_match() {
115        let err = DownloadError::NoMatch {
116            available: vec!["file1.zip".to_string(), "file2.tar.gz".to_string()],
117        };
118        let msg = format!("{}", err);
119        assert!(msg.contains("No matching assets found"));
120    }
121
122    #[test]
123    fn test_download_error_layer_not_found() {
124        let err = DownloadError::LayerNotFound;
125        let msg = format!("{}", err);
126        assert_eq!(msg, "Layer not found");
127    }
128
129    #[test]
130    fn test_download_error_invalid_response() {
131        let err = DownloadError::InvalidResponse;
132        let msg = format!("{}", err);
133        assert_eq!(msg, "Invalid response from server");
134    }
135
136    #[test]
137    fn test_download_error_no_filename() {
138        let err = DownloadError::NoFilename;
139        let msg = format!("{}", err);
140        assert_eq!(msg, "File name could not be determined");
141    }
142
143    #[test]
144    fn test_download_error_resume_mismatch() {
145        let err = DownloadError::ResumeMismatch;
146        let msg = format!("{}", err);
147        assert_eq!(msg, "Resume metadata mismatch");
148    }
149
150    #[test]
151    fn test_download_error_multiple() {
152        let err = DownloadError::Multiple {
153            errors: vec!["Error 1".to_string(), "Error 2".to_string()],
154        };
155        let msg = format!("{}", err);
156        assert_eq!(msg, "Multiple download errors occurred");
157    }
158
159    #[test]
160    fn test_download_error_io() {
161        let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
162        let err = DownloadError::Io(io_err);
163        let msg = format!("{}", err);
164        assert!(msg.contains("file not found"));
165    }
166
167    #[test]
168    fn test_download_error_debug() {
169        let err = DownloadError::LayerNotFound;
170        let debug = format!("{:?}", err);
171        assert!(debug.contains("LayerNotFound"));
172    }
173
174    #[test]
175    fn test_from_ureq_error() {
176        let ureq_err = ureq::Error::ConnectionFailed;
177        let download_err: DownloadError = ureq_err.into();
178
179        match download_err {
180            DownloadError::Network(_) => (),
181            _ => panic!("Expected Network error variant"),
182        }
183    }
184
185    #[test]
186    fn test_error_source_chain() {
187        let err = DownloadError::InvalidUrl {
188            url: "invalid".to_string(),
189            source: url::ParseError::RelativeUrlWithoutBase,
190        };
191
192        // Check that we can get the source
193        assert!(std::error::Error::source(&err).is_some());
194    }
195}