use std::fmt;
pub type Result<T> = std::result::Result<T, WeChatError>;
#[derive(Debug, Clone, thiserror::Error)]
pub enum WeChatError {
#[error("Network request failed: {message}")]
Network { message: String },
#[error("Request timeout")]
Timeout,
#[error("Invalid access token")]
InvalidToken,
#[error("Invalid application credentials")]
InvalidCredentials,
#[error("File not found: {path}")]
FileNotFound { path: String },
#[error("Failed to read file: {path}, reason: {reason}")]
FileRead { path: String, reason: String },
#[error("Markdown parsing failed: {reason}")]
MarkdownParse { reason: String },
#[error("Image upload failed: {path}, reason: {reason}")]
ImageUpload { path: String, reason: String },
#[error("Theme not found: {theme}")]
ThemeNotFound { theme: String },
#[error("Theme rendering failed: {theme}, reason: {reason}")]
ThemeRender { theme: String, reason: String },
#[error("WeChat API error [{code}]: {message}")]
WeChatApi { code: i32, message: String },
#[error("Configuration error: {message}")]
Config { message: String },
#[error("JSON processing failed: {message}")]
Json { message: String },
#[error("I/O error: {message}")]
Io { message: String },
#[error("Internal error: {message}")]
Internal { message: String },
}
impl WeChatError {
pub fn is_retryable(&self) -> bool {
match self {
WeChatError::Network { .. } | WeChatError::Timeout => true,
WeChatError::InvalidToken => true,
WeChatError::ImageUpload { .. } => true,
WeChatError::WeChatApi { code, .. } => match code {
40001 | 40014 | 42001 | 42007 => true,
45009 | 45011 => true,
-1 | 50001 | 50002 => true,
_ => false,
},
_ => false,
}
}
pub fn severity(&self) -> ErrorSeverity {
match self {
WeChatError::Network { .. }
| WeChatError::Timeout
| WeChatError::ImageUpload { .. } => ErrorSeverity::Warning,
WeChatError::InvalidToken | WeChatError::InvalidCredentials => ErrorSeverity::Error,
WeChatError::FileNotFound { .. }
| WeChatError::FileRead { .. }
| WeChatError::MarkdownParse { .. }
| WeChatError::ThemeNotFound { .. }
| WeChatError::Config { .. } => ErrorSeverity::Error,
WeChatError::WeChatApi { code, .. } => match code {
40013 | 48001 => ErrorSeverity::Critical,
_ => ErrorSeverity::Error,
},
WeChatError::ThemeRender { .. }
| WeChatError::Json { .. }
| WeChatError::Io { .. }
| WeChatError::Internal { .. } => ErrorSeverity::Error,
}
}
pub fn from_api_response(code: i32, message: impl Into<String>) -> Self {
WeChatError::WeChatApi {
code,
message: message.into(),
}
}
pub fn file_error(path: impl Into<String>, reason: impl Into<String>) -> Self {
WeChatError::FileRead {
path: path.into(),
reason: reason.into(),
}
}
pub fn config_error(message: impl Into<String>) -> Self {
WeChatError::Config {
message: message.into(),
}
}
pub fn retry_delay(&self) -> std::time::Duration {
use std::time::Duration;
match self {
WeChatError::Network { .. } | WeChatError::Timeout => Duration::from_secs(1),
WeChatError::InvalidToken => Duration::from_millis(100),
WeChatError::ImageUpload { .. } => Duration::from_millis(500),
WeChatError::WeChatApi { code, .. } => match code {
45009 | 45011 => Duration::from_secs(10),
-1 | 50001 | 50002 => Duration::from_secs(2),
40001 | 40014 | 42001 | 42007 => Duration::from_millis(200),
_ => Duration::from_secs(1),
},
_ => Duration::ZERO,
}
}
pub fn max_retries(&self) -> u32 {
match self {
WeChatError::Network { .. } | WeChatError::Timeout => 5,
WeChatError::InvalidToken => 2,
WeChatError::ImageUpload { .. } => 3,
WeChatError::WeChatApi { code, .. } => match code {
45009 | 45011 => 10,
-1 | 50001 | 50002 => 3,
40001 | 40014 | 42001 | 42007 => 2,
_ => 0,
},
_ => 0,
}
}
pub fn is_temporary(&self) -> bool {
match self {
WeChatError::Network { .. } | WeChatError::Timeout => true,
WeChatError::WeChatApi { code, .. } => match code {
-1 | 50001 | 50002 => true,
45009 | 45011 => true,
_ => false,
},
_ => false,
}
}
pub fn recovery_suggestion(&self) -> Option<&'static str> {
match self {
WeChatError::InvalidToken => Some("Try refreshing the access token"),
WeChatError::InvalidCredentials => Some("Check your app_id and app_secret"),
WeChatError::FileNotFound { .. } => Some("Check if the file path is correct"),
WeChatError::ImageUpload { .. } => Some("Check file size and format"),
WeChatError::ThemeNotFound { .. } => Some("Use a valid theme name or 'default'"),
WeChatError::WeChatApi { code, .. } => match code {
40001 => Some("Access token expired, refresh and retry"),
40003 => Some("Check your openid parameter"),
45009 => Some("Rate limit exceeded, wait and retry"),
48001 => Some("API unauthorized, check permissions"),
_ => Some("Check WeChat API documentation for error code"),
},
_ => None,
}
}
}
impl From<reqwest::Error> for WeChatError {
fn from(error: reqwest::Error) -> Self {
WeChatError::Network {
message: error.to_string(),
}
}
}
impl From<serde_json::Error> for WeChatError {
fn from(error: serde_json::Error) -> Self {
WeChatError::Json {
message: error.to_string(),
}
}
}
impl From<std::io::Error> for WeChatError {
fn from(error: std::io::Error) -> Self {
WeChatError::Io {
message: error.to_string(),
}
}
}
impl From<anyhow::Error> for WeChatError {
fn from(error: anyhow::Error) -> Self {
WeChatError::Internal {
message: error.to_string(),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ErrorSeverity {
Warning,
Error,
Critical,
}
impl fmt::Display for ErrorSeverity {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ErrorSeverity::Warning => write!(f, "WARNING"),
ErrorSeverity::Error => write!(f, "ERROR"),
ErrorSeverity::Critical => write!(f, "CRITICAL"),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_error_retryability() {
let timeout_err = WeChatError::Timeout;
assert!(timeout_err.is_retryable());
let file_err = WeChatError::FileNotFound {
path: "test.md".to_string(),
};
assert!(!file_err.is_retryable());
let token_err = WeChatError::from_api_response(40001, "invalid credential");
assert!(token_err.is_retryable());
let param_err = WeChatError::from_api_response(40003, "invalid openid");
assert!(!param_err.is_retryable());
}
#[test]
fn test_error_severity() {
let network_err = WeChatError::Timeout;
assert_eq!(network_err.severity(), ErrorSeverity::Warning);
let config_err = WeChatError::config_error("missing app_id");
assert_eq!(config_err.severity(), ErrorSeverity::Error);
let critical_api_err = WeChatError::from_api_response(40013, "invalid appid");
assert_eq!(critical_api_err.severity(), ErrorSeverity::Critical);
}
#[test]
fn test_error_creation_helpers() {
let file_err = WeChatError::file_error("/path/to/file.md", "permission denied");
match file_err {
WeChatError::FileRead { path, reason } => {
assert_eq!(path, "/path/to/file.md");
assert_eq!(reason, "permission denied");
}
_ => panic!("Expected FileRead error"),
}
let config_err = WeChatError::config_error("invalid configuration");
match config_err {
WeChatError::Config { message } => {
assert_eq!(message, "invalid configuration");
}
_ => panic!("Expected Config error"),
}
}
#[test]
fn test_retry_delay() {
let network_err = WeChatError::Network {
message: "connection failed".to_string(),
};
assert_eq!(network_err.retry_delay(), std::time::Duration::from_secs(1));
let rate_limit_err = WeChatError::from_api_response(45009, "rate limit exceeded");
assert_eq!(
rate_limit_err.retry_delay(),
std::time::Duration::from_secs(10)
);
let token_err = WeChatError::from_api_response(40001, "invalid credential");
assert_eq!(
token_err.retry_delay(),
std::time::Duration::from_millis(200)
);
}
#[test]
fn test_max_retries() {
let network_err = WeChatError::Network {
message: "connection failed".to_string(),
};
assert_eq!(network_err.max_retries(), 5);
let rate_limit_err = WeChatError::from_api_response(45009, "rate limit exceeded");
assert_eq!(rate_limit_err.max_retries(), 10);
let config_err = WeChatError::config_error("invalid config");
assert_eq!(config_err.max_retries(), 0);
}
#[test]
fn test_is_temporary() {
let network_err = WeChatError::Network {
message: "connection failed".to_string(),
};
assert!(network_err.is_temporary());
let server_err = WeChatError::from_api_response(50001, "server error");
assert!(server_err.is_temporary());
let config_err = WeChatError::config_error("invalid config");
assert!(!config_err.is_temporary());
}
#[test]
fn test_recovery_suggestion() {
let token_err = WeChatError::InvalidToken;
assert_eq!(
token_err.recovery_suggestion(),
Some("Try refreshing the access token")
);
let file_err = WeChatError::FileNotFound {
path: "test.md".to_string(),
};
assert_eq!(
file_err.recovery_suggestion(),
Some("Check if the file path is correct")
);
let api_err = WeChatError::from_api_response(40001, "invalid credential");
assert_eq!(
api_err.recovery_suggestion(),
Some("Access token expired, refresh and retry")
);
}
}