tgltrk 0.1.2

Unofficial Toggl Track CLI — manage timers, entries, projects, clients, and tags from the command line
use std::fmt;

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

#[derive(Debug)]
pub enum AppError {
    Api(String),
    HttpStatus { status: u16, body: String },
    Auth(String),
    Keyring(String),
    Cache(String),
    Io(String),
    NotFound(String),
    InvalidInput(String),
}

impl fmt::Display for AppError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            AppError::Api(msg) => write!(f, "API error: {msg}"),
            AppError::HttpStatus { status, body } => {
                write!(f, "HTTP {status}: {body}")
            }
            AppError::Auth(msg) => write!(f, "Authentication error: {msg}"),
            AppError::Keyring(msg) => write!(f, "Keyring error: {msg}"),
            AppError::Cache(msg) => write!(f, "Cache error: {msg}"),
            AppError::Io(msg) => write!(f, "IO error: {msg}"),
            AppError::NotFound(msg) => write!(f, "Not found: {msg}"),
            AppError::InvalidInput(msg) => write!(f, "Invalid input: {msg}"),
        }
    }
}

impl std::error::Error for AppError {}

impl From<reqwest::Error> for AppError {
    fn from(e: reqwest::Error) -> Self {
        AppError::Api(e.to_string())
    }
}

impl From<serde_json::Error> for AppError {
    fn from(e: serde_json::Error) -> Self {
        AppError::Api(format!("JSON parse error: {e}"))
    }
}

impl From<keyring::Error> for AppError {
    fn from(e: keyring::Error) -> Self {
        AppError::Keyring(e.to_string())
    }
}

impl From<crate::cache::CacheError> for AppError {
    fn from(e: crate::cache::CacheError) -> Self {
        AppError::Cache(e.to_string())
    }
}

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

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

    #[test]
    fn display_api_error() {
        let e = AppError::Api("msg".to_string());
        assert_eq!(e.to_string(), "API error: msg");
    }

    #[test]
    fn display_http_status() {
        let e = AppError::HttpStatus {
            status: 404,
            body: "not found".to_string(),
        };
        assert_eq!(e.to_string(), "HTTP 404: not found");
    }

    #[test]
    fn display_auth_error() {
        let e = AppError::Auth("msg".to_string());
        assert_eq!(e.to_string(), "Authentication error: msg");
    }

    #[test]
    fn display_keyring_error() {
        let e = AppError::Keyring("msg".to_string());
        assert_eq!(e.to_string(), "Keyring error: msg");
    }

    #[test]
    fn display_cache_error() {
        let e = AppError::Cache("msg".to_string());
        assert_eq!(e.to_string(), "Cache error: msg");
    }

    #[test]
    fn display_not_found() {
        let e = AppError::NotFound("msg".to_string());
        assert_eq!(e.to_string(), "Not found: msg");
    }

    #[test]
    fn display_invalid_input() {
        let e = AppError::InvalidInput("msg".to_string());
        assert_eq!(e.to_string(), "Invalid input: msg");
    }

    #[test]
    fn from_serde_json_error() {
        let json_err = serde_json::from_str::<String>("not json").unwrap_err();
        let app_err = AppError::from(json_err);
        match &app_err {
            AppError::Api(msg) => assert!(msg.contains("JSON parse error:"), "got: {msg}"),
            other => panic!("expected Api, got: {other:?}"),
        }
    }

    #[test]
    fn from_keyring_error() {
        let kr_err = keyring::Error::NoEntry;
        let app_err = AppError::from(kr_err);
        match &app_err {
            AppError::Keyring(_) => {}
            other => panic!("expected Keyring, got: {other:?}"),
        }
    }

    #[test]
    fn from_cache_error() {
        use crate::cache::CacheError;
        let cache_err = CacheError::InvalidKey("bad".to_string());
        let app_err = AppError::from(cache_err);
        match &app_err {
            AppError::Cache(msg) => assert!(msg.contains("Invalid cache key"), "got: {msg}"),
            other => panic!("expected Cache, got: {other:?}"),
        }
    }

    #[test]
    fn error_trait_impl() {
        let e = AppError::Api("test".to_string());
        let _: &dyn std::error::Error = &e;
    }
}