textlog 0.1.5

macOS clipboard + OCR daemon exposed to Claude Code as an MCP server
use thiserror::Error;

#[derive(Error, Debug)]
pub enum Error {
    #[error("Configuration file not found: {0}")]
    ConfigNotFound(String),

    #[error("Failed to parse configuration: {0}")]
    ConfigParse(#[from] toml::de::Error),

    #[error("Failed to serialize configuration: {0}")]
    ConfigSerialize(#[from] toml::ser::Error),

    #[error("I/O error: {0}")]
    Io(#[from] std::io::Error),

    #[error("Clipboard access failed: {0}")]
    ClipboardAccess(String),

    #[error("Apple Vision OCR failed: {0}")]
    Ocr(String),

    #[error("SQLite error: {0}")]
    Sqlite(#[from] rusqlite::Error),

    #[error("Storage data error: {0}")]
    Storage(String),

    #[error("MCP protocol error: {0}")]
    Mcp(String),

    #[error("Notification dispatch failed: {0}")]
    Notification(String),

    #[error("launchctl operation failed: {0}")]
    Launchctl(String),

    #[error("tl doctor: {0}")]
    Doctor(String),

    #[error("Privacy filter compilation failed: {0}")]
    FilterCompile(#[from] regex::Error),
}

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

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

    #[test]
    fn config_not_found_shows_path() {
        let err = Error::ConfigNotFound("config.toml".to_string());
        assert!(err.to_string().contains("config.toml"));
    }

    #[test]
    fn clipboard_access_shows_detail() {
        let err = Error::ClipboardAccess("permission denied".to_string());
        assert!(err.to_string().contains("permission denied"));
    }

    #[test]
    fn mcp_protocol_shows_detail() {
        let err = Error::Mcp("unknown tool foo".to_string());
        assert!(err.to_string().contains("unknown tool foo"));
    }

    #[test]
    fn ocr_error_shows_detail() {
        let err = Error::Ocr("vision request failed".to_string());
        assert!(err.to_string().contains("vision request failed"));
    }

    #[test]
    fn io_error_auto_converts() {
        fn fails() -> Result<()> {
            Err(std::io::Error::new(std::io::ErrorKind::NotFound, "gone"))?;
            Ok(())
        }
        let err = fails().unwrap_err();
        assert!(matches!(err, Error::Io(_)));
    }

    #[test]
    fn toml_parse_error_auto_converts() {
        let bad_toml = "not = valid = toml";
        let err: Error = toml::from_str::<toml::Value>(bad_toml).unwrap_err().into();
        assert!(matches!(err, Error::ConfigParse(_)));
    }

    #[test]
    fn regex_error_auto_converts() {
        let pattern = String::from("(") + "unclosed";
        let err: Error = regex::Regex::new(&pattern).unwrap_err().into();
        assert!(matches!(err, Error::FilterCompile(_)));
    }

    #[test]
    fn notification_shows_detail() {
        let err = Error::Notification("UNAuthorization denied".to_string());
        assert!(err.to_string().contains("UNAuthorization denied"));
    }

    #[test]
    fn launchctl_shows_detail() {
        let err = Error::Launchctl("bootstrap failed: EEXIST".to_string());
        assert!(err.to_string().contains("bootstrap failed"));
    }

    #[test]
    fn storage_shows_detail() {
        let err = Error::Storage("hex parse: bad length".to_string());
        assert!(err.to_string().contains("hex parse"));
    }

    #[test]
    fn sqlite_error_auto_converts() {
        let conn_err = rusqlite::Connection::open("/nonexistent/path/does/not/exist.db");
        let err: Error = conn_err.unwrap_err().into();
        assert!(matches!(err, Error::Sqlite(_)));
    }
}