1use std::path::PathBuf;
7use thiserror::Error;
8
9pub type Result<T> = std::result::Result<T, Error>;
11
12#[derive(Error, Debug)]
14pub enum Error {
15 #[error("I/O error: {0}")]
17 Io(#[from] std::io::Error),
18
19 #[error("WeChat API error: {message}")]
21 WeChat { message: String },
22
23 #[error("HTTP request failed: {0}")]
25 Http(#[from] reqwest::Error),
26
27 #[error("YAML error: {0}")]
29 Yaml(#[from] serde_yaml::Error),
30
31 #[error("JSON error: {0}")]
33 Json(#[from] serde_json::Error),
34
35 #[error("Regex error: {0}")]
37 Regex(#[from] regex::Error),
38
39 #[error("File not found: {path}")]
41 FileNotFound { path: PathBuf },
42
43 #[error("Invalid file format for {path}: {reason}")]
45 InvalidFormat { path: PathBuf, reason: String },
46
47 #[error("Missing required environment variable: {var}")]
49 MissingEnvVar { var: String },
50
51 #[error("OpenAI API error: {message}")]
53 OpenAI { message: String },
54
55 #[error("Gemini API error: {message}")]
57 Gemini { message: String },
58
59 #[error("Cover image error for {path}: {reason}")]
61 CoverImage { path: PathBuf, reason: String },
62
63 #[error("Markdown parsing error for {path}: {reason}")]
65 MarkdownParse { path: PathBuf, reason: String },
66
67 #[error("Configuration error: {message}")]
69 Config { message: String },
70
71 #[error("Operation failed: {message}")]
73 Generic { message: String },
74}
75
76impl Error {
77 pub fn file_not_found(path: impl Into<PathBuf>) -> Self {
79 Self::FileNotFound { path: path.into() }
80 }
81
82 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 pub fn missing_env_var(var: impl Into<String>) -> Self {
92 Self::MissingEnvVar { var: var.into() }
93 }
94
95 pub fn openai(message: impl Into<String>) -> Self {
97 Self::OpenAI {
98 message: message.into(),
99 }
100 }
101
102 pub fn gemini(message: impl Into<String>) -> Self {
104 Self::Gemini {
105 message: message.into(),
106 }
107 }
108
109 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 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 pub fn config(message: impl Into<String>) -> Self {
127 Self::Config {
128 message: message.into(),
129 }
130 }
131
132 pub fn generic(message: impl Into<String>) -> Self {
134 Self::Generic {
135 message: message.into(),
136 }
137 }
138
139 pub fn wechat(message: impl Into<String>) -> Self {
141 Self::WeChat {
142 message: message.into(),
143 }
144 }
145}
146
147impl 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#[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}