Skip to main content

adk_rust_mcp_common/
error.rs

1//! Error types for the common library.
2//!
3//! This module provides a unified error hierarchy using `thiserror` for consistent
4//! error handling across all MCP GenMedia servers.
5//!
6//! # Error Categories
7//!
8//! - `ConfigError`: Missing or invalid configuration
9//! - `GcsError`: Google Cloud Storage operations
10//! - `AuthError`: Authentication failures
11//! - `Error::Api`: Google Cloud API errors (includes endpoint and status)
12//! - `Error::Validation`: Input validation failures
13//! - `Error::Io`: File system operations
14//! - `Error::Ffmpeg`: FFmpeg/FFprobe execution errors
15//! - `Error::Timeout`: Long-running operation timeouts
16
17use thiserror::Error;
18
19/// Unified error type for the common library.
20///
21/// This enum provides a single error type that can represent all error conditions
22/// across the MCP GenMedia servers, enabling consistent error handling and reporting.
23#[derive(Debug, Error)]
24pub enum Error {
25    /// Configuration errors (missing env vars, invalid values)
26    #[error(transparent)]
27    Config(#[from] ConfigError),
28
29    /// GCS operation errors (upload, download, invalid URIs)
30    #[error(transparent)]
31    Gcs(#[from] GcsError),
32
33    /// Authentication errors (ADC not configured, token refresh failures)
34    #[error(transparent)]
35    Auth(#[from] AuthError),
36
37    /// API errors with endpoint and HTTP status context
38    ///
39    /// Includes the API endpoint that failed, HTTP status code, and error message
40    /// for debugging and user feedback.
41    #[error("API error for {endpoint} (HTTP {status_code}): {message}")]
42    Api {
43        /// The API endpoint that was called
44        endpoint: String,
45        /// HTTP status code returned by the API
46        status_code: u16,
47        /// Error message from the API or describing the failure
48        message: String,
49    },
50
51    /// Input validation errors
52    #[error("Validation error: {0}")]
53    Validation(String),
54
55    /// File system I/O errors
56    #[error(transparent)]
57    Io(#[from] std::io::Error),
58
59    /// FFmpeg/FFprobe execution errors
60    #[error("FFmpeg error: {0}")]
61    Ffmpeg(String),
62
63    /// Operation timeout errors
64    #[error("Operation timed out after {0} seconds")]
65    Timeout(u64),
66}
67
68impl Error {
69    /// Create a new API error with endpoint, status code, and message.
70    ///
71    /// # Arguments
72    ///
73    /// * `endpoint` - The API endpoint that was called
74    /// * `status_code` - The HTTP status code returned
75    /// * `message` - A description of the error
76    ///
77    /// # Example
78    ///
79    /// ```
80    /// use adk_rust_mcp_common::error::Error;
81    ///
82    /// let err = Error::api(
83    ///     "https://api.example.com/v1/generate",
84    ///     500,
85    ///     "Internal server error"
86    /// );
87    /// assert!(err.to_string().contains("api.example.com"));
88    /// assert!(err.to_string().contains("500"));
89    /// ```
90    pub fn api(endpoint: impl Into<String>, status_code: u16, message: impl Into<String>) -> Self {
91        Error::Api {
92            endpoint: endpoint.into(),
93            status_code,
94            message: message.into(),
95        }
96    }
97
98    /// Create a new validation error.
99    ///
100    /// # Example
101    ///
102    /// ```
103    /// use adk_rust_mcp_common::error::Error;
104    ///
105    /// let err = Error::validation("prompt cannot be empty");
106    /// assert!(err.to_string().contains("prompt cannot be empty"));
107    /// ```
108    pub fn validation(message: impl Into<String>) -> Self {
109        Error::Validation(message.into())
110    }
111
112    /// Create a new FFmpeg error.
113    ///
114    /// # Example
115    ///
116    /// ```
117    /// use adk_rust_mcp_common::error::Error;
118    ///
119    /// let err = Error::ffmpeg("Invalid input format");
120    /// assert!(err.to_string().contains("Invalid input format"));
121    /// ```
122    pub fn ffmpeg(message: impl Into<String>) -> Self {
123        Error::Ffmpeg(message.into())
124    }
125
126    /// Create a new timeout error.
127    ///
128    /// # Example
129    ///
130    /// ```
131    /// use adk_rust_mcp_common::error::Error;
132    ///
133    /// let err = Error::timeout(300);
134    /// assert!(err.to_string().contains("300 seconds"));
135    /// ```
136    pub fn timeout(seconds: u64) -> Self {
137        Error::Timeout(seconds)
138    }
139}
140
141/// Configuration errors.
142///
143/// These errors occur when loading or validating configuration from
144/// environment variables or configuration files.
145#[derive(Debug, Error)]
146pub enum ConfigError {
147    /// A required environment variable is not set
148    #[error("Required environment variable {0} is not set")]
149    MissingEnvVar(String),
150
151    /// An environment variable has an invalid value
152    #[error("Invalid value for {0}: {1}")]
153    InvalidValue(String, String),
154}
155
156impl ConfigError {
157    /// Create a new missing environment variable error.
158    pub fn missing_env_var(name: impl Into<String>) -> Self {
159        ConfigError::MissingEnvVar(name.into())
160    }
161
162    /// Create a new invalid value error.
163    pub fn invalid_value(name: impl Into<String>, reason: impl Into<String>) -> Self {
164        ConfigError::InvalidValue(name.into(), reason.into())
165    }
166}
167
168/// GCS operation type for error context.
169#[derive(Debug, Clone, Copy, PartialEq, Eq)]
170pub enum GcsOperation {
171    /// Upload operation
172    Upload,
173    /// Download operation
174    Download,
175    /// Check existence operation
176    Exists,
177    /// Delete operation
178    Delete,
179}
180
181impl std::fmt::Display for GcsOperation {
182    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
183        match self {
184            GcsOperation::Upload => write!(f, "upload"),
185            GcsOperation::Download => write!(f, "download"),
186            GcsOperation::Exists => write!(f, "exists"),
187            GcsOperation::Delete => write!(f, "delete"),
188        }
189    }
190}
191
192/// GCS operation errors.
193///
194/// These errors occur during Google Cloud Storage operations such as
195/// uploading, downloading, or checking object existence.
196#[derive(Debug, Error)]
197pub enum GcsError {
198    /// The GCS URI format is invalid
199    #[error("Invalid GCS URI: {0}")]
200    InvalidUri(String),
201
202    /// A GCS operation failed with context about the URI and operation type
203    #[error("GCS {operation} failed for {uri}: {message}")]
204    OperationFailed {
205        /// The GCS URI that was being accessed
206        uri: String,
207        /// The type of operation that failed
208        operation: GcsOperation,
209        /// Error message describing the failure
210        message: String,
211    },
212
213    /// Authentication error during GCS operation
214    #[error("GCS authentication error: {0}")]
215    AuthError(String),
216}
217
218impl GcsError {
219    /// Create a new invalid URI error.
220    pub fn invalid_uri(uri: impl Into<String>) -> Self {
221        GcsError::InvalidUri(uri.into())
222    }
223
224    /// Create a new operation failed error with full context.
225    ///
226    /// # Arguments
227    ///
228    /// * `uri` - The GCS URI that was being accessed
229    /// * `operation` - The type of operation that failed
230    /// * `message` - A description of the failure
231    ///
232    /// # Example
233    ///
234    /// ```
235    /// use adk_rust_mcp_common::error::{GcsError, GcsOperation};
236    ///
237    /// let err = GcsError::operation_failed(
238    ///     "gs://my-bucket/path/to/file.txt",
239    ///     GcsOperation::Upload,
240    ///     "Permission denied"
241    /// );
242    /// assert!(err.to_string().contains("gs://my-bucket"));
243    /// assert!(err.to_string().contains("upload"));
244    /// ```
245    pub fn operation_failed(
246        uri: impl Into<String>,
247        operation: GcsOperation,
248        message: impl Into<String>,
249    ) -> Self {
250        GcsError::OperationFailed {
251            uri: uri.into(),
252            operation,
253            message: message.into(),
254        }
255    }
256
257    /// Create a new authentication error.
258    pub fn auth_error(message: impl Into<String>) -> Self {
259        GcsError::AuthError(message.into())
260    }
261}
262
263/// Authentication errors.
264///
265/// These errors occur during authentication with Google Cloud services
266/// using Application Default Credentials (ADC).
267#[derive(Debug, Error)]
268pub enum AuthError {
269    /// ADC is not configured
270    #[error("ADC not configured. Run 'gcloud auth application-default login' or set GOOGLE_APPLICATION_CREDENTIALS")]
271    NotConfigured,
272
273    /// Token refresh failed
274    #[error("Token refresh failed: {0}")]
275    RefreshFailed(String),
276}
277
278impl AuthError {
279    /// Create a new token refresh failed error.
280    pub fn refresh_failed(message: impl Into<String>) -> Self {
281        AuthError::RefreshFailed(message.into())
282    }
283}
284
285/// Result type alias using the unified Error type.
286pub type Result<T> = std::result::Result<T, Error>;
287
288#[cfg(test)]
289mod tests {
290    use super::*;
291
292    #[test]
293    fn test_api_error_includes_endpoint_and_status() {
294        let err = Error::api("https://vertex.googleapis.com/v1/generate", 500, "Internal error");
295        let msg = err.to_string();
296        assert!(msg.contains("vertex.googleapis.com"), "Should contain endpoint");
297        assert!(msg.contains("500"), "Should contain status code");
298        assert!(msg.contains("Internal error"), "Should contain message");
299    }
300
301    #[test]
302    fn test_gcs_error_includes_uri_and_operation() {
303        let err = GcsError::operation_failed(
304            "gs://my-bucket/path/file.txt",
305            GcsOperation::Upload,
306            "Access denied",
307        );
308        let msg = err.to_string();
309        assert!(msg.contains("gs://my-bucket"), "Should contain URI");
310        assert!(msg.contains("upload"), "Should contain operation type");
311        assert!(msg.contains("Access denied"), "Should contain message");
312    }
313
314    #[test]
315    fn test_config_error_includes_var_name() {
316        let err = ConfigError::missing_env_var("PROJECT_ID");
317        let msg = err.to_string();
318        assert!(msg.contains("PROJECT_ID"), "Should contain variable name");
319    }
320
321    #[test]
322    fn test_error_from_config_error() {
323        let config_err = ConfigError::missing_env_var("TEST_VAR");
324        let err: Error = config_err.into();
325        assert!(matches!(err, Error::Config(_)));
326    }
327
328    #[test]
329    fn test_error_from_gcs_error() {
330        let gcs_err = GcsError::invalid_uri("invalid://uri");
331        let err: Error = gcs_err.into();
332        assert!(matches!(err, Error::Gcs(_)));
333    }
334
335    #[test]
336    fn test_error_from_auth_error() {
337        let auth_err = AuthError::NotConfigured;
338        let err: Error = auth_err.into();
339        assert!(matches!(err, Error::Auth(_)));
340    }
341
342    #[test]
343    fn test_error_from_io_error() {
344        let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
345        let err: Error = io_err.into();
346        assert!(matches!(err, Error::Io(_)));
347    }
348
349    #[test]
350    fn test_timeout_error() {
351        let err = Error::timeout(300);
352        let msg = err.to_string();
353        assert!(msg.contains("300"), "Should contain timeout duration");
354        assert!(msg.contains("seconds"), "Should mention seconds");
355    }
356
357    #[test]
358    fn test_ffmpeg_error() {
359        let err = Error::ffmpeg("Invalid codec");
360        let msg = err.to_string();
361        assert!(msg.contains("FFmpeg"), "Should mention FFmpeg");
362        assert!(msg.contains("Invalid codec"), "Should contain message");
363    }
364
365    #[test]
366    fn test_validation_error() {
367        let err = Error::validation("prompt too long");
368        let msg = err.to_string();
369        assert!(msg.contains("Validation"), "Should mention validation");
370        assert!(msg.contains("prompt too long"), "Should contain message");
371    }
372
373    #[test]
374    fn test_gcs_operation_display() {
375        assert_eq!(GcsOperation::Upload.to_string(), "upload");
376        assert_eq!(GcsOperation::Download.to_string(), "download");
377        assert_eq!(GcsOperation::Exists.to_string(), "exists");
378        assert_eq!(GcsOperation::Delete.to_string(), "delete");
379    }
380}