lmrc_gitlab/error.rs
1//! Error types for the GitLab client library.
2//!
3//! This module provides a comprehensive error type [`GitLabError`] that covers
4//! all possible error scenarios when interacting with the GitLab API.
5//!
6//! # Examples
7//!
8//! ```no_run
9//! use lmrc_gitlab::{GitLabClient, GitLabError};
10//!
11//! async fn example() -> Result<(), GitLabError> {
12//! let client = GitLabClient::new("https://gitlab.com", "invalid-token")?;
13//! // This will return GitLabError::Authentication if token is invalid
14//! Ok(())
15//! }
16//! ```
17
18use std::fmt;
19
20/// Result type alias using [`GitLabError`] as the error type.
21pub type Result<T> = std::result::Result<T, GitLabError>;
22
23/// The main error type for all GitLab client operations.
24///
25/// This enum covers all possible error scenarios including authentication,
26/// API errors, network issues, and data validation problems.
27#[derive(Debug, thiserror::Error)]
28pub enum GitLabError {
29 /// Authentication failed due to invalid or missing credentials.
30 ///
31 /// # Examples
32 ///
33 /// ```no_run
34 /// # use lmrc_gitlab::GitLabError;
35 /// let error = GitLabError::Authentication("Invalid token".to_string());
36 /// assert_eq!(error.to_string(), "Authentication failed: Invalid token");
37 /// ```
38 #[error("Authentication failed: {0}")]
39 Authentication(String),
40
41 /// GitLab API returned an error response.
42 ///
43 /// This includes HTTP error codes, API-specific errors, and malformed responses.
44 #[error("GitLab API error: {0}")]
45 Api(String),
46
47 /// HTTP request failed due to network or connection issues.
48 #[error("HTTP request failed: {0}")]
49 Http(#[from] reqwest::Error),
50
51 /// Requested resource was not found (HTTP 404).
52 ///
53 /// # Examples
54 ///
55 /// ```
56 /// # use lmrc_gitlab::GitLabError;
57 /// let error = GitLabError::NotFound {
58 /// resource: "pipeline".to_string(),
59 /// id: "12345".to_string(),
60 /// };
61 /// ```
62 #[error("Resource not found: {resource} with id {id}")]
63 NotFound {
64 /// The type of resource (e.g., "pipeline", "job", "project")
65 resource: String,
66 /// The identifier for the resource
67 id: String,
68 },
69
70 /// Invalid configuration provided to the client.
71 ///
72 /// This includes invalid URLs, missing required fields, or incompatible settings.
73 #[error("Invalid configuration: {0}")]
74 Config(String),
75
76 /// Failed to serialize or deserialize data.
77 #[error("Serialization error: {0}")]
78 Serialization(#[from] serde_json::Error),
79
80 /// Rate limit exceeded for API requests.
81 ///
82 /// GitLab enforces rate limits on API calls. When exceeded, this error is returned.
83 #[error("Rate limit exceeded. Retry after {retry_after:?} seconds")]
84 RateLimit {
85 /// Number of seconds to wait before retrying (if provided by GitLab)
86 retry_after: Option<u64>,
87 },
88
89 /// Operation is not permitted due to insufficient permissions.
90 #[error("Permission denied: {0}")]
91 PermissionDenied(String),
92
93 /// Invalid input or parameters provided.
94 ///
95 /// # Examples
96 ///
97 /// ```
98 /// # use lmrc_gitlab::GitLabError;
99 /// let error = GitLabError::InvalidInput {
100 /// field: "status".to_string(),
101 /// message: "Must be one of: success, failed, running".to_string(),
102 /// };
103 /// ```
104 #[error("Invalid input for field '{field}': {message}")]
105 InvalidInput {
106 /// The field name that has invalid input
107 field: String,
108 /// Description of why the input is invalid
109 message: String,
110 },
111
112 /// Operation timed out.
113 ///
114 /// This can occur when waiting for a pipeline to complete, polling for status, etc.
115 #[error("Operation timed out after {seconds} seconds")]
116 Timeout {
117 /// Number of seconds elapsed before timeout
118 seconds: u64,
119 },
120
121 /// The underlying GitLab API returned a status that indicates a conflict (HTTP 409).
122 ///
123 /// Common scenarios include attempting to create a resource that already exists.
124 #[error("Conflict: {0}")]
125 Conflict(String),
126
127 /// The GitLab server is unavailable or returned an error (HTTP 5xx).
128 #[error("GitLab server error: {0}")]
129 ServerError(String),
130
131 /// An unexpected or unknown error occurred.
132 ///
133 /// This is used as a catch-all for errors that don't fit other categories.
134 #[error("Unexpected error: {0}")]
135 Unexpected(String),
136}
137
138impl GitLabError {
139 /// Creates an authentication error.
140 ///
141 /// # Examples
142 ///
143 /// ```
144 /// # use lmrc_gitlab::GitLabError;
145 /// let error = GitLabError::authentication("Token expired");
146 /// ```
147 pub fn authentication<S: Into<String>>(msg: S) -> Self {
148 Self::Authentication(msg.into())
149 }
150
151 /// Creates an API error.
152 ///
153 /// # Examples
154 ///
155 /// ```
156 /// # use lmrc_gitlab::GitLabError;
157 /// let error = GitLabError::api("Invalid project path");
158 /// ```
159 pub fn api<S: Into<String>>(msg: S) -> Self {
160 Self::Api(msg.into())
161 }
162
163 /// Creates a not found error.
164 ///
165 /// # Examples
166 ///
167 /// ```
168 /// # use lmrc_gitlab::GitLabError;
169 /// let error = GitLabError::not_found("pipeline", 12345);
170 /// ```
171 pub fn not_found<S: Into<String>, I: fmt::Display>(resource: S, id: I) -> Self {
172 Self::NotFound {
173 resource: resource.into(),
174 id: id.to_string(),
175 }
176 }
177
178 /// Creates a configuration error.
179 ///
180 /// # Examples
181 ///
182 /// ```
183 /// # use lmrc_gitlab::GitLabError;
184 /// let error = GitLabError::config("Missing API token");
185 /// ```
186 pub fn config<S: Into<String>>(msg: S) -> Self {
187 Self::Config(msg.into())
188 }
189
190 /// Creates a rate limit error.
191 ///
192 /// # Examples
193 ///
194 /// ```
195 /// # use lmrc_gitlab::GitLabError;
196 /// let error = GitLabError::rate_limit(Some(60));
197 /// ```
198 pub fn rate_limit(retry_after: Option<u64>) -> Self {
199 Self::RateLimit { retry_after }
200 }
201
202 /// Creates a permission denied error.
203 ///
204 /// # Examples
205 ///
206 /// ```
207 /// # use lmrc_gitlab::GitLabError;
208 /// let error = GitLabError::permission_denied("Cannot delete protected branch");
209 /// ```
210 pub fn permission_denied<S: Into<String>>(msg: S) -> Self {
211 Self::PermissionDenied(msg.into())
212 }
213
214 /// Creates an invalid input error.
215 ///
216 /// # Examples
217 ///
218 /// ```
219 /// # use lmrc_gitlab::GitLabError;
220 /// let error = GitLabError::invalid_input("ref", "Branch name cannot be empty");
221 /// ```
222 pub fn invalid_input<S: Into<String>, M: Into<String>>(field: S, message: M) -> Self {
223 Self::InvalidInput {
224 field: field.into(),
225 message: message.into(),
226 }
227 }
228
229 /// Creates a timeout error.
230 ///
231 /// # Examples
232 ///
233 /// ```
234 /// # use lmrc_gitlab::GitLabError;
235 /// let error = GitLabError::timeout(300);
236 /// ```
237 pub fn timeout(seconds: u64) -> Self {
238 Self::Timeout { seconds }
239 }
240
241 /// Creates a conflict error.
242 ///
243 /// # Examples
244 ///
245 /// ```
246 /// # use lmrc_gitlab::GitLabError;
247 /// let error = GitLabError::conflict("Pipeline already exists");
248 /// ```
249 pub fn conflict<S: Into<String>>(msg: S) -> Self {
250 Self::Conflict(msg.into())
251 }
252
253 /// Creates a server error.
254 ///
255 /// # Examples
256 ///
257 /// ```
258 /// # use lmrc_gitlab::GitLabError;
259 /// let error = GitLabError::server_error("Internal server error");
260 /// ```
261 pub fn server_error<S: Into<String>>(msg: S) -> Self {
262 Self::ServerError(msg.into())
263 }
264
265 /// Creates an unexpected error.
266 ///
267 /// # Examples
268 ///
269 /// ```
270 /// # use lmrc_gitlab::GitLabError;
271 /// let error = GitLabError::unexpected("Unknown error occurred");
272 /// ```
273 pub fn unexpected<S: Into<String>>(msg: S) -> Self {
274 Self::Unexpected(msg.into())
275 }
276
277 /// Returns `true` if this error is retryable.
278 ///
279 /// Retryable errors include network issues, rate limits, and server errors.
280 ///
281 /// # Examples
282 ///
283 /// ```
284 /// # use lmrc_gitlab::GitLabError;
285 /// let error = GitLabError::rate_limit(Some(60));
286 /// assert!(error.is_retryable());
287 ///
288 /// let error = GitLabError::not_found("pipeline", 123);
289 /// assert!(!error.is_retryable());
290 /// ```
291 pub fn is_retryable(&self) -> bool {
292 matches!(
293 self,
294 Self::RateLimit { .. } | Self::ServerError(_) | Self::Timeout { .. }
295 )
296 }
297
298 /// Returns `true` if this is a client error (4xx status codes).
299 ///
300 /// Client errors indicate problems with the request that cannot be
301 /// resolved by retrying.
302 pub fn is_client_error(&self) -> bool {
303 matches!(
304 self,
305 Self::Authentication(_)
306 | Self::NotFound { .. }
307 | Self::PermissionDenied(_)
308 | Self::InvalidInput { .. }
309 | Self::Conflict(_)
310 )
311 }
312
313 /// Returns `true` if this is a server error (5xx status codes).
314 pub fn is_server_error(&self) -> bool {
315 matches!(self, Self::ServerError(_))
316 }
317}
318
319#[cfg(test)]
320mod tests {
321 use super::*;
322
323 #[test]
324 fn test_error_display() {
325 let error = GitLabError::authentication("Invalid token");
326 assert_eq!(error.to_string(), "Authentication failed: Invalid token");
327
328 let error = GitLabError::not_found("pipeline", 12345);
329 assert_eq!(
330 error.to_string(),
331 "Resource not found: pipeline with id 12345"
332 );
333 }
334
335 #[test]
336 fn test_is_retryable() {
337 assert!(GitLabError::rate_limit(Some(60)).is_retryable());
338 assert!(GitLabError::server_error("Error").is_retryable());
339 assert!(GitLabError::timeout(300).is_retryable());
340
341 assert!(!GitLabError::not_found("job", 123).is_retryable());
342 assert!(!GitLabError::authentication("Invalid").is_retryable());
343 }
344
345 #[test]
346 fn test_is_client_error() {
347 assert!(GitLabError::authentication("Invalid").is_client_error());
348 assert!(GitLabError::not_found("job", 123).is_client_error());
349 assert!(GitLabError::permission_denied("Denied").is_client_error());
350
351 assert!(!GitLabError::server_error("Error").is_client_error());
352 assert!(!GitLabError::rate_limit(None).is_client_error());
353 }
354
355 #[test]
356 fn test_is_server_error() {
357 assert!(GitLabError::server_error("Error").is_server_error());
358
359 assert!(!GitLabError::not_found("job", 123).is_server_error());
360 assert!(!GitLabError::authentication("Invalid").is_server_error());
361 }
362}