Skip to main content

devboy_core/
error.rs

1//! Error types for devboy-tools.
2//!
3//! This module provides a unified error handling system that works across
4//! all providers and components.
5
6use thiserror::Error;
7
8/// Main error type for devboy operations.
9#[derive(Error, Debug)]
10pub enum Error {
11    // =========================================================================
12    // HTTP / Network Errors
13    // =========================================================================
14    /// HTTP request failed
15    #[error("HTTP error: {0}")]
16    Http(String),
17
18    /// Network connectivity error
19    #[error("Network error: {0}")]
20    Network(String),
21
22    #[error("Request timeout")]
23    Timeout,
24
25    // =========================================================================
26    // Authentication / Authorization Errors
27    // =========================================================================
28    /// 401 Unauthorized - invalid or missing credentials
29    #[error("Unauthorized: {0}")]
30    Unauthorized(String),
31
32    /// 403 Forbidden - valid credentials but insufficient permissions
33    #[error("Forbidden: {0}")]
34    Forbidden(String),
35
36    // =========================================================================
37    // API Errors
38    // =========================================================================
39    /// API returned an error response
40    #[error("API error ({status}): {message}")]
41    Api {
42        /// HTTP status code
43        status: u16,
44        /// Error message from API
45        message: String,
46    },
47
48    /// Resource not found (404)
49    #[error("Not found: {0}")]
50    NotFound(String),
51
52    /// Rate limit exceeded (429)
53    #[error("Rate limit exceeded: retry after {retry_after:?}s")]
54    RateLimited {
55        /// Seconds to wait before retry
56        retry_after: Option<u64>,
57    },
58
59    /// Server error (5xx)
60    #[error("Server error ({status}): {message}")]
61    ServerError {
62        /// HTTP status code
63        status: u16,
64        /// Error message
65        message: String,
66    },
67
68    // =========================================================================
69    // Data Errors
70    // =========================================================================
71    /// Serialization/deserialization failed
72    #[error("Serialization error: {0}")]
73    Serialization(#[from] serde_json::Error),
74
75    /// Invalid data format or content
76    #[error("Invalid data: {0}")]
77    InvalidData(String),
78
79    // =========================================================================
80    // Configuration Errors
81    // =========================================================================
82    /// Configuration error
83    #[error("Configuration error: {0}")]
84    Config(String),
85
86    /// Missing required configuration
87    #[error("Missing configuration: {0}")]
88    MissingConfig(String),
89
90    // =========================================================================
91    // Storage Errors
92    // =========================================================================
93    /// Storage/keychain error
94    #[error("Storage error: {0}")]
95    Storage(String),
96
97    /// Credential not found in keychain
98    #[error("Credential not found: {provider}/{key}")]
99    CredentialNotFound { provider: String, key: String },
100
101    // =========================================================================
102    // Provider Errors
103    // =========================================================================
104    /// Provider not found or not configured
105    #[error("Provider not found: {0}")]
106    ProviderNotFound(String),
107
108    /// Provider not supported for this operation
109    #[error("Provider '{provider}' does not support: {operation}")]
110    ProviderUnsupported {
111        provider: String,
112        /// Unsupported operation
113        operation: String,
114    },
115
116    // =========================================================================
117    // Generic Errors
118    // =========================================================================
119    /// IO error
120    #[error("IO error: {0}")]
121    Io(#[from] std::io::Error),
122
123    /// Generic error wrapper
124    #[error("{0}")]
125    Other(#[from] anyhow::Error),
126}
127
128impl Error {
129    /// Create an API error from HTTP status and message.
130    pub fn from_status(status: u16, message: impl Into<String>) -> Self {
131        let message = message.into();
132        match status {
133            401 => Error::Unauthorized(message),
134            403 => Error::Forbidden(message),
135            404 => Error::NotFound(message),
136            429 => Error::RateLimited { retry_after: None },
137            500..=599 => Error::ServerError { status, message },
138            _ => Error::Api { status, message },
139        }
140    }
141
142    /// Check if this is a retryable error.
143    pub fn is_retryable(&self) -> bool {
144        matches!(
145            self,
146            Error::Timeout
147                | Error::Network(_)
148                | Error::RateLimited { .. }
149                | Error::ServerError { .. }
150        )
151    }
152
153    /// Check if this is an authentication error.
154    pub fn is_auth_error(&self) -> bool {
155        matches!(self, Error::Unauthorized(_) | Error::Forbidden(_))
156    }
157}
158
159/// Result type alias for devboy operations.
160pub type Result<T> = std::result::Result<T, Error>;
161
162#[cfg(test)]
163mod tests {
164    use super::*;
165
166    #[test]
167    fn test_from_status() {
168        assert!(matches!(
169            Error::from_status(401, "test"),
170            Error::Unauthorized(_)
171        ));
172        assert!(matches!(
173            Error::from_status(403, "test"),
174            Error::Forbidden(_)
175        ));
176        assert!(matches!(
177            Error::from_status(404, "test"),
178            Error::NotFound(_)
179        ));
180        assert!(matches!(
181            Error::from_status(429, "test"),
182            Error::RateLimited { .. }
183        ));
184        assert!(matches!(
185            Error::from_status(500, "test"),
186            Error::ServerError { .. }
187        ));
188        assert!(matches!(Error::from_status(400, "test"), Error::Api { .. }));
189    }
190
191    #[test]
192    fn test_is_retryable() {
193        assert!(Error::Timeout.is_retryable());
194        assert!(Error::Network("test".into()).is_retryable());
195        assert!(Error::RateLimited { retry_after: None }.is_retryable());
196        assert!(
197            Error::ServerError {
198                status: 500,
199                message: "test".into()
200            }
201            .is_retryable()
202        );
203        assert!(!Error::Unauthorized("test".into()).is_retryable());
204        assert!(!Error::NotFound("test".into()).is_retryable());
205    }
206
207    #[test]
208    fn test_is_auth_error() {
209        assert!(Error::Unauthorized("test".into()).is_auth_error());
210        assert!(Error::Forbidden("test".into()).is_auth_error());
211        assert!(!Error::NotFound("test".into()).is_auth_error());
212    }
213}