Skip to main content

wx_uploader/
error.rs

1//! Error types for the wx-uploader library
2//!
3//! This module provides comprehensive error handling using `thiserror` for
4//! library errors and integrates with `anyhow` for application-level error handling.
5
6use std::path::PathBuf;
7use thiserror::Error;
8
9/// Result type alias for the wx-uploader library
10pub type Result<T> = std::result::Result<T, Error>;
11
12/// Main error type for the wx-uploader library
13#[derive(Error, Debug)]
14pub enum Error {
15    /// I/O operation failed
16    #[error("I/O error: {0}")]
17    Io(#[from] std::io::Error),
18
19    /// WeChat API error
20    #[error("WeChat API error: {message}")]
21    WeChat { message: String },
22
23    /// HTTP request failed
24    #[error("HTTP request failed: {0}")]
25    Http(#[from] reqwest::Error),
26
27    /// YAML parsing or serialization error
28    #[error("YAML error: {0}")]
29    Yaml(#[from] serde_yaml::Error),
30
31    /// JSON parsing or serialization error
32    #[error("JSON error: {0}")]
33    Json(#[from] serde_json::Error),
34
35    /// Regular expression error
36    #[error("Regex error: {0}")]
37    Regex(#[from] regex::Error),
38
39    /// File not found
40    #[error("File not found: {path}")]
41    FileNotFound { path: PathBuf },
42
43    /// Invalid file format
44    #[error("Invalid file format for {path}: {reason}")]
45    InvalidFormat { path: PathBuf, reason: String },
46
47    /// Environment variable missing
48    #[error("Missing required environment variable: {var}")]
49    MissingEnvVar { var: String },
50
51    /// OpenAI API error
52    #[error("OpenAI API error: {message}")]
53    OpenAI { message: String },
54
55    /// Gemini API error
56    #[error("Gemini API error: {message}")]
57    Gemini { message: String },
58
59    /// Cover image error
60    #[error("Cover image error for {path}: {reason}")]
61    CoverImage { path: PathBuf, reason: String },
62
63    /// Markdown parsing error
64    #[error("Markdown parsing error for {path}: {reason}")]
65    MarkdownParse { path: PathBuf, reason: String },
66
67    /// Configuration error
68    #[error("Configuration error: {message}")]
69    Config { message: String },
70
71    /// Generic error with context
72    #[error("Operation failed: {message}")]
73    Generic { message: String },
74}
75
76impl Error {
77    /// Creates a new file not found error
78    pub fn file_not_found(path: impl Into<PathBuf>) -> Self {
79        Self::FileNotFound { path: path.into() }
80    }
81
82    /// Creates a new invalid format error
83    pub fn invalid_format(path: impl Into<PathBuf>, reason: impl Into<String>) -> Self {
84        Self::InvalidFormat {
85            path: path.into(),
86            reason: reason.into(),
87        }
88    }
89
90    /// Creates a new missing environment variable error
91    pub fn missing_env_var(var: impl Into<String>) -> Self {
92        Self::MissingEnvVar { var: var.into() }
93    }
94
95    /// Creates a new OpenAI API error
96    pub fn openai(message: impl Into<String>) -> Self {
97        Self::OpenAI {
98            message: message.into(),
99        }
100    }
101
102    /// Creates a new Gemini API error
103    pub fn gemini(message: impl Into<String>) -> Self {
104        Self::Gemini {
105            message: message.into(),
106        }
107    }
108
109    /// Creates a new cover image error
110    pub fn cover_image(path: impl Into<PathBuf>, reason: impl Into<String>) -> Self {
111        Self::CoverImage {
112            path: path.into(),
113            reason: reason.into(),
114        }
115    }
116
117    /// Creates a new markdown parsing error
118    pub fn markdown_parse(path: impl Into<PathBuf>, reason: impl Into<String>) -> Self {
119        Self::MarkdownParse {
120            path: path.into(),
121            reason: reason.into(),
122        }
123    }
124
125    /// Creates a new configuration error
126    pub fn config(message: impl Into<String>) -> Self {
127        Self::Config {
128            message: message.into(),
129        }
130    }
131
132    /// Creates a generic error with context
133    pub fn generic(message: impl Into<String>) -> Self {
134        Self::Generic {
135            message: message.into(),
136        }
137    }
138
139    /// Creates a new WeChat API error
140    pub fn wechat(message: impl Into<String>) -> Self {
141        Self::WeChat {
142            message: message.into(),
143        }
144    }
145}
146
147/// Conversion from anyhow::Error for compatibility
148impl From<anyhow::Error> for Error {
149    fn from(err: anyhow::Error) -> Self {
150        Self::Generic {
151            message: err.to_string(),
152        }
153    }
154}
155
156// Note: We don't implement From<wechat_pub_rs::Error> because the exact error type
157// varies between versions. Instead, we handle WeChat errors manually in the wechat module.
158
159#[cfg(test)]
160mod tests {
161    use super::*;
162    use std::path::Path;
163
164    #[test]
165    fn test_error_creation() {
166        let path = Path::new("test.md");
167
168        let file_not_found = Error::file_not_found(path);
169        assert!(matches!(file_not_found, Error::FileNotFound { .. }));
170        assert!(file_not_found.to_string().contains("test.md"));
171
172        let invalid_format = Error::invalid_format(path, "malformed YAML");
173        assert!(matches!(invalid_format, Error::InvalidFormat { .. }));
174        assert!(invalid_format.to_string().contains("malformed YAML"));
175
176        let missing_env = Error::missing_env_var("WECHAT_APP_ID");
177        assert!(matches!(missing_env, Error::MissingEnvVar { .. }));
178        assert!(missing_env.to_string().contains("WECHAT_APP_ID"));
179
180        let openai_error = Error::openai("API rate limit exceeded");
181        assert!(matches!(openai_error, Error::OpenAI { .. }));
182        assert!(openai_error.to_string().contains("rate limit"));
183
184        let cover_error = Error::cover_image(path, "download failed");
185        assert!(matches!(cover_error, Error::CoverImage { .. }));
186        assert!(cover_error.to_string().contains("download failed"));
187
188        let markdown_error = Error::markdown_parse(path, "invalid frontmatter");
189        assert!(matches!(markdown_error, Error::MarkdownParse { .. }));
190        assert!(markdown_error.to_string().contains("invalid frontmatter"));
191
192        let config_error = Error::config("invalid configuration");
193        assert!(matches!(config_error, Error::Config { .. }));
194        assert!(config_error.to_string().contains("invalid configuration"));
195
196        let generic_error = Error::generic("something went wrong");
197        assert!(matches!(generic_error, Error::Generic { .. }));
198        assert!(generic_error.to_string().contains("something went wrong"));
199    }
200
201    #[test]
202    fn test_anyhow_conversion() {
203        let anyhow_error = anyhow::anyhow!("test error message");
204        let our_error: Error = anyhow_error.into();
205
206        assert!(matches!(our_error, Error::Generic { .. }));
207        assert!(our_error.to_string().contains("test error message"));
208    }
209
210    #[test]
211    fn test_error_chain() {
212        use std::io;
213
214        let io_error = io::Error::new(io::ErrorKind::NotFound, "file not found");
215        let our_error: Error = io_error.into();
216
217        assert!(matches!(our_error, Error::Io(_)));
218        assert!(our_error.to_string().contains("I/O error"));
219    }
220}