Skip to main content

atlassian_cli_api/
error.rs

1use thiserror::Error;
2
3#[derive(Error, Debug)]
4pub enum ApiError {
5    #[error("HTTP request failed: {0}")]
6    RequestFailed(#[from] reqwest::Error),
7
8    #[error("Rate limit exceeded. Retry after {retry_after} seconds")]
9    RateLimitExceeded { retry_after: u64 },
10
11    #[error("Authentication failed: {message}")]
12    AuthenticationFailed { message: String },
13
14    #[error("Access forbidden: {message}")]
15    Forbidden { message: String },
16
17    #[error("Resource not found: {resource}")]
18    NotFound { resource: String },
19
20    #[error("Invalid request: {message}")]
21    BadRequest { message: String },
22
23    #[error("Server error: {status} - {message}")]
24    ServerError { status: u16, message: String },
25
26    #[error("Invalid URL: {0}")]
27    InvalidUrl(#[from] url::ParseError),
28
29    #[error("JSON serialization error: {0}")]
30    JsonError(#[from] serde_json::Error),
31
32    #[error("Request timeout after {attempts} attempts")]
33    Timeout { attempts: usize },
34
35    #[error("Invalid response format: {0}")]
36    InvalidResponse(String),
37}
38
39impl ApiError {
40    pub fn is_retryable(&self) -> bool {
41        match self {
42            ApiError::RateLimitExceeded { .. } => true,
43            ApiError::ServerError { status, .. } if *status >= 500 => true,
44            ApiError::Timeout { .. } => true,
45            _ => false,
46        }
47    }
48
49    pub fn suggestion(&self) -> Option<&str> {
50        match self {
51            ApiError::AuthenticationFailed { .. } | ApiError::Forbidden { .. } => {
52                Some("Verify tokens with: atlassian-cli auth list\nTest auth with: atlassian-cli auth test [--bitbucket]")
53            }
54            ApiError::RateLimitExceeded { .. } => {
55                Some("Consider reducing request frequency or use bulk operations")
56            }
57            ApiError::NotFound { .. } => Some("Check if the resource ID is correct"),
58            ApiError::BadRequest { message } => {
59                if message.contains("Version number must be 1") {
60                    Some("This is a draft page. Use 'confluence page publish' to publish for the first time")
61                } else if message.to_lowercase().contains("version") {
62                    Some("Version conflict detected. The content may have been modified. Fetch latest and retry")
63                } else {
64                    Some("Review the request parameters")
65                }
66            }
67            ApiError::Timeout { .. } => Some("Check your network connection or try again later"),
68            _ => None,
69        }
70    }
71}
72
73pub type Result<T> = std::result::Result<T, ApiError>;
74
75#[cfg(test)]
76mod tests {
77    use super::*;
78
79    #[test]
80    fn forbidden_has_suggestion() {
81        let err = ApiError::Forbidden {
82            message: "no access".to_string(),
83        };
84        assert!(err.suggestion().is_some());
85        assert!(err.suggestion().unwrap().contains("auth test"));
86    }
87
88    #[test]
89    fn forbidden_is_not_retryable() {
90        let err = ApiError::Forbidden {
91            message: "no access".to_string(),
92        };
93        assert!(!err.is_retryable());
94    }
95
96    #[test]
97    fn authentication_failed_has_suggestion() {
98        let err = ApiError::AuthenticationFailed {
99            message: "expired".to_string(),
100        };
101        assert!(err.suggestion().is_some());
102        assert!(err.suggestion().unwrap().contains("auth test"));
103    }
104}