Skip to main content

elizaos_plugin_github/
error.rs

1#![allow(missing_docs)]
2
3use std::fmt;
4use thiserror::Error;
5
6pub type Result<T> = std::result::Result<T, GitHubError>;
7
8#[derive(Debug, Error)]
9pub enum GitHubError {
10    #[error("GitHub client not initialized - call start() first")]
11    ClientNotInitialized,
12
13    #[error("Configuration error: {0}")]
14    ConfigError(String),
15
16    #[error("Missing required setting: {0}")]
17    MissingSetting(String),
18
19    #[error("Invalid argument: {0}")]
20    InvalidArgument(String),
21
22    #[error("Repository not found: {owner}/{repo}")]
23    RepositoryNotFound { owner: String, repo: String },
24
25    #[error("Branch not found: {branch} in {owner}/{repo}")]
26    BranchNotFound {
27        branch: String,
28        owner: String,
29        repo: String,
30    },
31
32    #[error("File not found: {path} in {owner}/{repo}")]
33    FileNotFound {
34        path: String,
35        owner: String,
36        repo: String,
37    },
38
39    #[error("Issue #{issue_number} not found in {owner}/{repo}")]
40    IssueNotFound {
41        issue_number: u64,
42        owner: String,
43        repo: String,
44    },
45
46    #[error("Pull request #{pull_number} not found in {owner}/{repo}")]
47    PullRequestNotFound {
48        pull_number: u64,
49        owner: String,
50        repo: String,
51    },
52
53    #[error("Permission denied: {0}")]
54    PermissionDenied(String),
55
56    #[error("Rate limited by GitHub API, retry after {retry_after_ms}ms")]
57    RateLimited {
58        retry_after_ms: u64,
59        remaining: u32,
60        reset_at: chrono::DateTime<chrono::Utc>,
61    },
62
63    #[error("Secondary rate limit hit, retry after {retry_after_ms}ms")]
64    SecondaryRateLimit { retry_after_ms: u64 },
65
66    #[error("Merge conflict in pull request #{pull_number} in {owner}/{repo}")]
67    MergeConflict {
68        pull_number: u64,
69        owner: String,
70        repo: String,
71    },
72
73    #[error("Branch already exists: {branch} in {owner}/{repo}")]
74    BranchExists {
75        branch: String,
76        owner: String,
77        repo: String,
78    },
79
80    #[error("Validation failed for {field}: {reason}")]
81    ValidationFailed { field: String, reason: String },
82
83    #[error("GitHub API error ({status}): {message}")]
84    ApiError {
85        status: u16,
86        message: String,
87        code: Option<String>,
88        documentation_url: Option<String>,
89    },
90
91    #[error("Network error: {0}")]
92    NetworkError(String),
93
94    #[error("Operation timed out after {timeout_ms}ms: {operation}")]
95    Timeout { timeout_ms: u64, operation: String },
96
97    #[error("Git operation failed ({operation}): {reason}")]
98    GitOperation { operation: String, reason: String },
99
100    #[error("Webhook verification failed: {0}")]
101    WebhookVerification(String),
102
103    #[error("Serialization error: {0}")]
104    SerializationError(String),
105
106    #[error("Internal error: {0}")]
107    Internal(String),
108
109    #[cfg(feature = "native")]
110    #[error("GitHub API error: {0}")]
111    OctocrabError(#[from] octocrab::Error),
112}
113
114impl GitHubError {
115    pub fn is_retryable(&self) -> bool {
116        matches!(
117            self,
118            GitHubError::RateLimited { .. }
119                | GitHubError::SecondaryRateLimit { .. }
120                | GitHubError::Timeout { .. }
121                | GitHubError::NetworkError(_)
122        )
123    }
124
125    pub fn retry_after_ms(&self) -> Option<u64> {
126        match self {
127            GitHubError::RateLimited { retry_after_ms, .. } => Some(*retry_after_ms),
128            GitHubError::SecondaryRateLimit { retry_after_ms } => Some(*retry_after_ms),
129            GitHubError::Timeout { timeout_ms, .. } => Some(*timeout_ms / 2),
130            _ => None,
131        }
132    }
133}
134
135impl From<serde_json::Error> for GitHubError {
136    fn from(err: serde_json::Error) -> Self {
137        GitHubError::SerializationError(err.to_string())
138    }
139}
140
141impl From<std::io::Error> for GitHubError {
142    fn from(err: std::io::Error) -> Self {
143        GitHubError::Internal(format!("I/O error: {}", err))
144    }
145}
146
147impl From<reqwest::Error> for GitHubError {
148    fn from(err: reqwest::Error) -> Self {
149        if err.is_timeout() {
150            GitHubError::Timeout {
151                timeout_ms: 30000,
152                operation: "HTTP request".to_string(),
153            }
154        } else {
155            GitHubError::NetworkError(err.to_string())
156        }
157    }
158}
159
160#[derive(Debug)]
161pub struct ErrorContext<E: fmt::Display> {
162    error: E,
163    context: String,
164}
165
166impl<E: fmt::Display> fmt::Display for ErrorContext<E> {
167    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
168        write!(f, "{}: {}", self.context, self.error)
169    }
170}
171
172impl<E: fmt::Display + fmt::Debug> std::error::Error for ErrorContext<E> {}
173
174pub trait WithContext<T, E: fmt::Display> {
175    fn with_context<F: FnOnce() -> String>(self, f: F) -> std::result::Result<T, ErrorContext<E>>;
176}
177
178impl<T, E: fmt::Display> WithContext<T, E> for std::result::Result<T, E> {
179    fn with_context<F: FnOnce() -> String>(self, f: F) -> std::result::Result<T, ErrorContext<E>> {
180        self.map_err(|e| ErrorContext {
181            error: e,
182            context: f(),
183        })
184    }
185}
186
187#[cfg(test)]
188mod tests {
189    use super::*;
190
191    #[test]
192    fn test_error_display() {
193        let err = GitHubError::MissingSetting("GITHUB_API_TOKEN".to_string());
194        assert!(err.to_string().contains("GITHUB_API_TOKEN"));
195    }
196
197    #[test]
198    fn test_error_retryable() {
199        assert!(GitHubError::RateLimited {
200            retry_after_ms: 1000,
201            remaining: 0,
202            reset_at: chrono::Utc::now(),
203        }
204        .is_retryable());
205        assert!(GitHubError::Timeout {
206            timeout_ms: 5000,
207            operation: "test".to_string(),
208        }
209        .is_retryable());
210        assert!(!GitHubError::ClientNotInitialized.is_retryable());
211    }
212
213    #[test]
214    fn test_retry_after() {
215        let err = GitHubError::RateLimited {
216            retry_after_ms: 1000,
217            remaining: 0,
218            reset_at: chrono::Utc::now(),
219        };
220        assert_eq!(err.retry_after_ms(), Some(1000));
221
222        let err = GitHubError::ClientNotInitialized;
223        assert_eq!(err.retry_after_ms(), None);
224    }
225}