Skip to main content

duckduckgo_core/
error.rs

1use thiserror::Error;
2
3#[derive(Debug, Error)]
4pub enum Error {
5    #[error("Usage error. ({0}) Invalid input. → Check the command syntax.")]
6    Usage(String),
7    #[error(
8        "Cannot reach DuckDuckGo. ({0}) Network request failed. → Check connectivity, set --proxy, or increase --timeout."
9    )]
10    Network(String),
11    #[error(
12        "Unexpected DuckDuckGo response. ({0}) Remote response was not usable. → Retry later or file an issue if this persists."
13    )]
14    Remote(String),
15    #[error(
16        "Unexpected DuckDuckGo HTML structure. ({0}) Selectors may have changed. → Update duckduckgo-cli or file an issue with -vv output and the query used."
17    )]
18    Parse(String),
19    #[error(
20        "Search blocked by DuckDuckGo. ({0}) Anti-bot rate limit exceeded. → Wait 60s, route through --proxy, or reduce concurrency."
21    )]
22    Blocked(String),
23    #[error(
24        "Local I/O error. ({0}) Could not read or write local files. → Check permissions and available disk space."
25    )]
26    Io(String),
27}
28
29pub type Result<T> = std::result::Result<T, Error>;
30
31impl Error {
32    #[must_use]
33    pub fn exit_code(&self) -> i32 {
34        match self {
35            Self::Usage(_) => 2,
36            Self::Network(_) | Self::Remote(_) => 3,
37            Self::Parse(_) => 4,
38            Self::Blocked(_) => 5,
39            Self::Io(_) => 6,
40        }
41    }
42}
43
44impl From<std::io::Error> for Error {
45    fn from(value: std::io::Error) -> Self {
46        Self::Io(value.to_string())
47    }
48}
49
50#[cfg(test)]
51mod tests {
52    use super::Error;
53
54    #[test]
55    fn exit_codes_match_spec() {
56        assert_eq!(Error::Usage("x".to_owned()).exit_code(), 2);
57        assert_eq!(Error::Network("x".to_owned()).exit_code(), 3);
58        assert_eq!(Error::Remote("x".to_owned()).exit_code(), 3);
59        assert_eq!(Error::Parse("x".to_owned()).exit_code(), 4);
60        assert_eq!(Error::Blocked("x".to_owned()).exit_code(), 5);
61        assert_eq!(Error::Io("x".to_owned()).exit_code(), 6);
62    }
63
64    #[test]
65    fn display_includes_context_and_guidance() {
66        let message = Error::Blocked("http_202".to_owned()).to_string();
67        assert!(message.contains("Search blocked"));
68        assert!(message.contains("http_202"));
69        assert!(message.contains("Wait 60s"));
70    }
71}