duckduckgo-core 0.1.6

DuckDuckGo search client library for duckduckgo-cli
Documentation
use thiserror::Error;

#[derive(Debug, Error)]
pub enum Error {
    #[error("Usage error. ({0}) Invalid input. → Check the command syntax.")]
    Usage(String),
    #[error(
        "Cannot reach DuckDuckGo. ({0}) Network request failed. → Check connectivity, set --proxy, or increase --timeout."
    )]
    Network(String),
    #[error(
        "Unexpected DuckDuckGo response. ({0}) Remote response was not usable. → Retry later or file an issue if this persists."
    )]
    Remote(String),
    #[error(
        "Unexpected DuckDuckGo HTML structure. ({0}) Selectors may have changed. → Update duckduckgo-cli or file an issue with -vv output and the query used."
    )]
    Parse(String),
    #[error(
        "Search blocked by DuckDuckGo. ({0}) Anti-bot rate limit exceeded. → Wait 60s, route through --proxy, or reduce concurrency."
    )]
    Blocked(String),
    #[error(
        "Local I/O error. ({0}) Could not read or write local files. → Check permissions and available disk space."
    )]
    Io(String),
}

pub type Result<T> = std::result::Result<T, Error>;

impl Error {
    #[must_use]
    pub fn exit_code(&self) -> i32 {
        match self {
            Self::Usage(_) => 2,
            Self::Network(_) | Self::Remote(_) => 3,
            Self::Parse(_) => 4,
            Self::Blocked(_) => 5,
            Self::Io(_) => 6,
        }
    }
}

impl From<std::io::Error> for Error {
    fn from(value: std::io::Error) -> Self {
        Self::Io(value.to_string())
    }
}

#[cfg(test)]
mod tests {
    use super::Error;

    #[test]
    fn exit_codes_match_spec() {
        assert_eq!(Error::Usage("x".to_owned()).exit_code(), 2);
        assert_eq!(Error::Network("x".to_owned()).exit_code(), 3);
        assert_eq!(Error::Remote("x".to_owned()).exit_code(), 3);
        assert_eq!(Error::Parse("x".to_owned()).exit_code(), 4);
        assert_eq!(Error::Blocked("x".to_owned()).exit_code(), 5);
        assert_eq!(Error::Io("x".to_owned()).exit_code(), 6);
    }

    #[test]
    fn display_includes_context_and_guidance() {
        let message = Error::Blocked("http_202".to_owned()).to_string();
        assert!(message.contains("Search blocked"));
        assert!(message.contains("http_202"));
        assert!(message.contains("Wait 60s"));
    }
}