Skip to main content

every_other_token/
error.rs

1//! Crate-level error type for Every-Other-Token.
2//!
3//! All internal modules should return `EotError` (or a type convertible to it)
4//! rather than `Box<dyn std::error::Error>`.  The top-level `main` converts to
5//! `Box<dyn std::error::Error>` at the boundary so callers see a clean message.
6
7use thiserror::Error;
8
9/// Top-level error enum.  Each variant maps to a distinct failure domain so
10/// that callers can match on the kind of error rather than string-comparing.
11#[derive(Debug, Error)]
12pub enum EotError {
13    /// An expected API key environment variable was not set.
14    #[error("API key not set: {0}")]
15    ApiKeyMissing(String),
16
17    /// The user supplied a transform name that could not be parsed.
18    #[error("invalid transform '{0}': {1}")]
19    InvalidTransform(String, String),
20
21    /// A provider returned a non-success HTTP status.
22    #[error("provider HTTP {status} from {url}")]
23    ProviderHttp { status: u16, url: String },
24
25    /// The provider response body could not be parsed.
26    #[error("provider JSON parse error: {0}")]
27    ProviderJson(String),
28
29    /// An underlying HTTP client error (connection refused, timeout, etc.).
30    #[error("HTTP client error: {0}")]
31    Http(#[from] reqwest::Error),
32
33    /// A JSON serialization / deserialization error.
34    #[error("JSON error: {0}")]
35    Json(#[from] serde_json::Error),
36
37    /// A standard I/O error (file not found, permission denied, etc.).
38    #[error("I/O error: {0}")]
39    Io(#[from] std::io::Error),
40
41    /// SQLite database error from the experiment store.
42    #[error("database error: {0}")]
43    Database(#[from] rusqlite::Error),
44
45    /// An arbitrary error from external code that has not yet been migrated to
46    /// `EotError`.  Used as a migration shim — new code should not add these.
47    #[error("{0}")]
48    Other(String),
49}
50
51impl From<Box<dyn std::error::Error + Send + Sync>> for EotError {
52    fn from(e: Box<dyn std::error::Error + Send + Sync>) -> Self {
53        EotError::Other(e.to_string())
54    }
55}
56
57impl From<Box<dyn std::error::Error>> for EotError {
58    fn from(e: Box<dyn std::error::Error>) -> Self {
59        EotError::Other(e.to_string())
60    }
61}
62
63impl From<String> for EotError {
64    fn from(s: String) -> Self {
65        EotError::Other(s)
66    }
67}
68
69impl From<&str> for EotError {
70    fn from(s: &str) -> Self {
71        EotError::Other(s.to_string())
72    }
73}
74
75#[cfg(test)]
76mod tests {
77    use super::*;
78
79    #[test]
80    fn test_api_key_missing_message() {
81        let e = EotError::ApiKeyMissing("OPENAI_API_KEY".to_string());
82        assert!(e.to_string().contains("OPENAI_API_KEY"));
83    }
84
85    #[test]
86    fn test_invalid_transform_message() {
87        let e = EotError::InvalidTransform("foo".to_string(), "unknown".to_string());
88        assert!(e.to_string().contains("foo"));
89        assert!(e.to_string().contains("unknown"));
90    }
91
92    #[test]
93    fn test_provider_http_message() {
94        let e = EotError::ProviderHttp {
95            status: 429,
96            url: "https://api.openai.com".to_string(),
97        };
98        let msg = e.to_string();
99        assert!(msg.contains("429"));
100        assert!(msg.contains("openai.com"));
101    }
102
103    #[test]
104    fn test_other_wraps_string() {
105        let e: EotError = EotError::from("something went wrong");
106        assert_eq!(e.to_string(), "something went wrong");
107    }
108
109    #[test]
110    fn test_from_string() {
111        let e: EotError = "my error".to_string().into();
112        assert_eq!(e.to_string(), "my error");
113    }
114
115    #[test]
116    fn test_io_error_wraps() {
117        let io = std::io::Error::new(std::io::ErrorKind::NotFound, "file missing");
118        let e = EotError::Io(io);
119        assert!(e.to_string().contains("file missing"));
120    }
121
122    #[test]
123    fn test_provider_json_message() {
124        let e = EotError::ProviderJson("unexpected field".to_string());
125        assert!(e.to_string().contains("unexpected field"));
126    }
127
128    #[test]
129    fn test_json_error_wraps() {
130        let json_err = serde_json::from_str::<serde_json::Value>("{{invalid").unwrap_err();
131        let e = EotError::Json(json_err);
132        assert!(e.to_string().contains("JSON error"));
133    }
134
135    #[test]
136    fn test_debug_format() {
137        let e = EotError::ApiKeyMissing("TEST_KEY".to_string());
138        let debug = format!("{:?}", e);
139        assert!(debug.contains("ApiKeyMissing"));
140    }
141}