use std::path::PathBuf;
use thiserror::Error;
pub type Result<T> = std::result::Result<T, Error>;
#[derive(Error, Debug)]
pub enum Error {
#[error("I/O error: {0}")]
Io(#[from] std::io::Error),
#[error("WeChat API error: {message}")]
WeChat { message: String },
#[error("HTTP request failed: {0}")]
Http(#[from] reqwest::Error),
#[error("YAML error: {0}")]
Yaml(#[from] serde_yaml::Error),
#[error("JSON error: {0}")]
Json(#[from] serde_json::Error),
#[error("Regex error: {0}")]
Regex(#[from] regex::Error),
#[error("File not found: {path}")]
FileNotFound { path: PathBuf },
#[error("Invalid file format for {path}: {reason}")]
InvalidFormat { path: PathBuf, reason: String },
#[error("Missing required environment variable: {var}")]
MissingEnvVar { var: String },
#[error("OpenAI API error: {message}")]
OpenAI { message: String },
#[error("Gemini API error: {message}")]
Gemini { message: String },
#[error("Cover image error for {path}: {reason}")]
CoverImage { path: PathBuf, reason: String },
#[error("Markdown parsing error for {path}: {reason}")]
MarkdownParse { path: PathBuf, reason: String },
#[error("Configuration error: {message}")]
Config { message: String },
#[error("Operation failed: {message}")]
Generic { message: String },
}
impl Error {
pub fn file_not_found(path: impl Into<PathBuf>) -> Self {
Self::FileNotFound { path: path.into() }
}
pub fn invalid_format(path: impl Into<PathBuf>, reason: impl Into<String>) -> Self {
Self::InvalidFormat {
path: path.into(),
reason: reason.into(),
}
}
pub fn missing_env_var(var: impl Into<String>) -> Self {
Self::MissingEnvVar { var: var.into() }
}
pub fn openai(message: impl Into<String>) -> Self {
Self::OpenAI {
message: message.into(),
}
}
pub fn gemini(message: impl Into<String>) -> Self {
Self::Gemini {
message: message.into(),
}
}
pub fn cover_image(path: impl Into<PathBuf>, reason: impl Into<String>) -> Self {
Self::CoverImage {
path: path.into(),
reason: reason.into(),
}
}
pub fn markdown_parse(path: impl Into<PathBuf>, reason: impl Into<String>) -> Self {
Self::MarkdownParse {
path: path.into(),
reason: reason.into(),
}
}
pub fn config(message: impl Into<String>) -> Self {
Self::Config {
message: message.into(),
}
}
pub fn generic(message: impl Into<String>) -> Self {
Self::Generic {
message: message.into(),
}
}
pub fn wechat(message: impl Into<String>) -> Self {
Self::WeChat {
message: message.into(),
}
}
}
impl From<anyhow::Error> for Error {
fn from(err: anyhow::Error) -> Self {
Self::Generic {
message: err.to_string(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::Path;
#[test]
fn test_error_creation() {
let path = Path::new("test.md");
let file_not_found = Error::file_not_found(path);
assert!(matches!(file_not_found, Error::FileNotFound { .. }));
assert!(file_not_found.to_string().contains("test.md"));
let invalid_format = Error::invalid_format(path, "malformed YAML");
assert!(matches!(invalid_format, Error::InvalidFormat { .. }));
assert!(invalid_format.to_string().contains("malformed YAML"));
let missing_env = Error::missing_env_var("WECHAT_APP_ID");
assert!(matches!(missing_env, Error::MissingEnvVar { .. }));
assert!(missing_env.to_string().contains("WECHAT_APP_ID"));
let openai_error = Error::openai("API rate limit exceeded");
assert!(matches!(openai_error, Error::OpenAI { .. }));
assert!(openai_error.to_string().contains("rate limit"));
let cover_error = Error::cover_image(path, "download failed");
assert!(matches!(cover_error, Error::CoverImage { .. }));
assert!(cover_error.to_string().contains("download failed"));
let markdown_error = Error::markdown_parse(path, "invalid frontmatter");
assert!(matches!(markdown_error, Error::MarkdownParse { .. }));
assert!(markdown_error.to_string().contains("invalid frontmatter"));
let config_error = Error::config("invalid configuration");
assert!(matches!(config_error, Error::Config { .. }));
assert!(config_error.to_string().contains("invalid configuration"));
let generic_error = Error::generic("something went wrong");
assert!(matches!(generic_error, Error::Generic { .. }));
assert!(generic_error.to_string().contains("something went wrong"));
}
#[test]
fn test_anyhow_conversion() {
let anyhow_error = anyhow::anyhow!("test error message");
let our_error: Error = anyhow_error.into();
assert!(matches!(our_error, Error::Generic { .. }));
assert!(our_error.to_string().contains("test error message"));
}
#[test]
fn test_error_chain() {
use std::io;
let io_error = io::Error::new(io::ErrorKind::NotFound, "file not found");
let our_error: Error = io_error.into();
assert!(matches!(our_error, Error::Io(_)));
assert!(our_error.to_string().contains("I/O error"));
}
}