1use thiserror::Error;
8
9#[derive(Error, Debug)]
11pub enum RsGuardError {
12 #[error("GitHub API error: {status} - {message}")]
14 GitHubApi {
15 status: u16,
17 message: String,
19 },
20
21 #[error("LLM API error ({provider}): {status} - {message}")]
23 LlmApi {
24 provider: String,
26 status: u16,
28 message: String,
30 },
31
32 #[error("Failed to parse verdict: {0}")]
34 VerdictParse(
35 String,
37 ),
38
39 #[error("Configuration error: {0}")]
41 Config(
42 String,
44 ),
45
46 #[error("I/O error: {0}")]
48 Io(#[from] std::io::Error),
49
50 #[error(
52 "Diff too large: {size_bytes} bytes ({line_count} lines). Maximum is 100KB or 1500 lines."
53 )]
54 DiffTooLarge {
55 size_bytes: usize,
57 line_count: usize,
59 },
60
61 #[error("No diff content found")]
63 EmptyDiff,
64
65 #[error("Invalid diff content: response does not appear to be a diff")]
67 InvalidDiffContent,
68
69 #[error("Permission denied for review state {state}: {message}")]
71 PermissionDenied {
72 state: String,
74 message: String,
76 },
77}
78
79impl RsGuardError {
80 pub fn is_retryable(&self) -> bool {
86 matches!(
87 self,
88 RsGuardError::GitHubApi {
89 status: 0 | 429 | 502 | 503 | 504,
90 ..
91 } | RsGuardError::LlmApi {
92 status: 0 | 429 | 502 | 503 | 504,
93 ..
94 }
95 )
96 }
97
98 pub fn is_permission_denied(&self) -> bool {
100 match self {
101 RsGuardError::GitHubApi { status: 403, .. } => true,
102 RsGuardError::GitHubApi {
103 status: 422,
104 message,
105 } => message.to_lowercase().contains("not permitted"),
106 RsGuardError::PermissionDenied { .. } => true,
107 _ => false,
108 }
109 }
110}
111
112#[cfg(test)]
113mod tests {
114 use super::*;
115
116 #[test]
117 fn test_is_retryable_github_429() {
118 let err = RsGuardError::GitHubApi {
119 status: 429,
120 message: "rate limited".to_string(),
121 };
122 assert!(err.is_retryable());
123 }
124
125 #[test]
126 fn test_is_retryable_github_502() {
127 let err = RsGuardError::GitHubApi {
128 status: 502,
129 message: "bad gateway".to_string(),
130 };
131 assert!(err.is_retryable());
132 }
133
134 #[test]
135 fn test_is_retryable_github_503() {
136 let err = RsGuardError::GitHubApi {
137 status: 503,
138 message: "service unavailable".to_string(),
139 };
140 assert!(err.is_retryable());
141 }
142
143 #[test]
144 fn test_is_retryable_github_504() {
145 let err = RsGuardError::GitHubApi {
146 status: 504,
147 message: "gateway timeout".to_string(),
148 };
149 assert!(err.is_retryable());
150 }
151
152 #[test]
153 fn test_is_retryable_github_0() {
154 let err = RsGuardError::GitHubApi {
155 status: 0,
156 message: "connection error".to_string(),
157 };
158 assert!(err.is_retryable());
159 }
160
161 #[test]
162 fn test_is_retryable_github_404_not_retryable() {
163 let err = RsGuardError::GitHubApi {
164 status: 404,
165 message: "not found".to_string(),
166 };
167 assert!(!err.is_retryable());
168 }
169
170 #[test]
171 fn test_is_retryable_github_403_not_retryable() {
172 let err = RsGuardError::GitHubApi {
173 status: 403,
174 message: "forbidden".to_string(),
175 };
176 assert!(!err.is_retryable());
177 }
178
179 #[test]
180 fn test_is_retryable_llm_429() {
181 let err = RsGuardError::LlmApi {
182 provider: "deepseek".to_string(),
183 status: 429,
184 message: "rate limited".to_string(),
185 };
186 assert!(err.is_retryable());
187 }
188
189 #[test]
190 fn test_is_retryable_llm_0() {
191 let err = RsGuardError::LlmApi {
192 provider: "deepseek".to_string(),
193 status: 0,
194 message: "connection error".to_string(),
195 };
196 assert!(err.is_retryable());
197 }
198
199 #[test]
200 fn test_is_retryable_config_not_retryable() {
201 let err = RsGuardError::Config("bad config".to_string());
202 assert!(!err.is_retryable());
203 }
204
205 #[test]
206 fn test_is_permission_denied_403() {
207 let err = RsGuardError::GitHubApi {
208 status: 403,
209 message: "forbidden".to_string(),
210 };
211 assert!(err.is_permission_denied());
212 }
213
214 #[test]
215 fn test_is_permission_denied_422_not_permitted() {
216 let err = RsGuardError::GitHubApi {
217 status: 422,
218 message: "Review not permitted for this user".to_string(),
219 };
220 assert!(err.is_permission_denied());
221 }
222
223 #[test]
224 fn test_is_permission_denied_422_case_insensitive() {
225 let err = RsGuardError::GitHubApi {
226 status: 422,
227 message: "NOT PERMITTED".to_string(),
228 };
229 assert!(err.is_permission_denied());
230 }
231
232 #[test]
233 fn test_is_permission_denied_422_other_message() {
234 let err = RsGuardError::GitHubApi {
235 status: 422,
236 message: "Validation failed".to_string(),
237 };
238 assert!(!err.is_permission_denied());
239 }
240
241 #[test]
242 fn test_is_permission_denied_explicit_variant() {
243 let err = RsGuardError::PermissionDenied {
244 state: "APPROVE".to_string(),
245 message: "not allowed".to_string(),
246 };
247 assert!(err.is_permission_denied());
248 }
249
250 #[test]
251 fn test_is_permission_denied_404_not_denied() {
252 let err = RsGuardError::GitHubApi {
253 status: 404,
254 message: "not found".to_string(),
255 };
256 assert!(!err.is_permission_denied());
257 }
258}