mermaid_cli/models/
error.rs

1/// Comprehensive error types for the model system
2///
3/// Replaces scattered anyhow::Error usage with structured, actionable errors
4/// that enable proper recovery, retry logic, and user-friendly messages.
5
6use std::fmt;
7
8/// Top-level error type for all model operations
9#[derive(Debug)]
10pub enum ModelError {
11    /// Backend-specific error (connection, API, etc)
12    Backend(BackendError),
13
14    /// Configuration error (invalid settings, missing keys, etc)
15    Config(ConfigError),
16
17    /// Model not found or unavailable
18    ModelNotFound { model: String, searched: Vec<String> },
19
20    /// Request timeout
21    Timeout { operation: String, duration_secs: u64 },
22
23    /// Rate limit exceeded
24    RateLimit { retry_after: Option<u64> },
25
26    /// Invalid request (malformed input, bad parameters)
27    InvalidRequest(String),
28
29    /// Response parsing error
30    ParseError { message: String, raw: Option<String> },
31
32    /// Stream error (connection dropped, incomplete response)
33    StreamError(String),
34
35    /// Authentication error
36    Authentication(String),
37}
38
39impl fmt::Display for ModelError {
40    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
41        match self {
42            ModelError::Backend(e) => write!(f, "Backend error: {}", e),
43            ModelError::Config(e) => write!(f, "Configuration error: {}", e),
44            ModelError::ModelNotFound { model, searched } => {
45                write!(f, "Model '{}' not found. Searched: {}", model, searched.join(", "))
46            }
47            ModelError::Timeout { operation, duration_secs } => {
48                write!(f, "Operation '{}' timed out after {} seconds", operation, duration_secs)
49            }
50            ModelError::RateLimit { retry_after } => {
51                if let Some(secs) = retry_after {
52                    write!(f, "Rate limit exceeded. Retry after {} seconds", secs)
53                } else {
54                    write!(f, "Rate limit exceeded")
55                }
56            }
57            ModelError::InvalidRequest(msg) => write!(f, "Invalid request: {}", msg),
58            ModelError::ParseError { message, raw } => {
59                if let Some(r) = raw {
60                    write!(f, "Parse error: {} (raw: {})", message, r)
61                } else {
62                    write!(f, "Parse error: {}", message)
63                }
64            }
65            ModelError::StreamError(msg) => write!(f, "Stream error: {}", msg),
66            ModelError::Authentication(msg) => write!(f, "Authentication error: {}", msg),
67        }
68    }
69}
70
71impl std::error::Error for ModelError {}
72
73/// Backend-specific errors
74#[derive(Debug)]
75pub enum BackendError {
76    /// Connection failed (network, DNS, etc)
77    ConnectionFailed { backend: String, url: String, reason: String },
78
79    /// Backend not available (not running, health check failed)
80    NotAvailable { backend: String, reason: String },
81
82    /// HTTP error from backend
83    HttpError { status: u16, message: String },
84
85    /// Backend returned unexpected response format
86    UnexpectedResponse { backend: String, message: String },
87
88    /// Provider-specific error
89    ProviderError { provider: String, code: Option<String>, message: String },
90}
91
92impl fmt::Display for BackendError {
93    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
94        match self {
95            BackendError::ConnectionFailed { backend, url, reason } => {
96                write!(f, "Failed to connect to {} at {}: {}", backend, url, reason)
97            }
98            BackendError::NotAvailable { backend, reason } => {
99                write!(f, "Backend '{}' not available: {}", backend, reason)
100            }
101            BackendError::HttpError { status, message } => {
102                write!(f, "HTTP error {}: {}", status, message)
103            }
104            BackendError::UnexpectedResponse { backend, message } => {
105                write!(f, "Unexpected response from {}: {}", backend, message)
106            }
107            BackendError::ProviderError { provider, code, message } => {
108                if let Some(c) = code {
109                    write!(f, "{} error {}: {}", provider, c, message)
110                } else {
111                    write!(f, "{} error: {}", provider, message)
112                }
113            }
114        }
115    }
116}
117
118impl std::error::Error for BackendError {}
119
120/// Configuration errors
121#[derive(Debug)]
122pub enum ConfigError {
123    /// Missing required configuration
124    MissingRequired(String),
125
126    /// Invalid value for configuration
127    InvalidValue { field: String, value: String, reason: String },
128
129    /// File operation error (read, parse, etc)
130    FileError { path: String, reason: String },
131}
132
133impl fmt::Display for ConfigError {
134    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
135        match self {
136            ConfigError::MissingRequired(field) => {
137                write!(f, "Missing required configuration: {}", field)
138            }
139            ConfigError::InvalidValue { field, value, reason } => {
140                write!(f, "Invalid value for '{}': '{}' ({})", field, value, reason)
141            }
142            ConfigError::FileError { path, reason } => {
143                write!(f, "Error reading config file '{}': {}", path, reason)
144            }
145        }
146    }
147}
148
149impl std::error::Error for ConfigError {}
150
151/// Result type alias for model operations
152pub type Result<T> = std::result::Result<T, ModelError>;
153
154/// Conversion from anyhow::Error (for gradual migration)
155impl From<anyhow::Error> for ModelError {
156    fn from(err: anyhow::Error) -> Self {
157        ModelError::InvalidRequest(err.to_string())
158    }
159}
160
161/// Conversion from reqwest::Error
162impl From<reqwest::Error> for ModelError {
163    fn from(err: reqwest::Error) -> Self {
164        if err.is_timeout() {
165            ModelError::Timeout {
166                operation: "HTTP request".to_string(),
167                duration_secs: 120,
168            }
169        } else if err.is_connect() {
170            ModelError::Backend(BackendError::ConnectionFailed {
171                backend: "unknown".to_string(),
172                url: err.url().map(|u| u.to_string()).unwrap_or_else(|| "unknown".to_string()),
173                reason: err.to_string(),
174            })
175        } else if err.is_status() {
176            let status = err.status().map(|s| s.as_u16()).unwrap_or(500);
177            ModelError::Backend(BackendError::HttpError {
178                status,
179                message: err.to_string(),
180            })
181        } else {
182            ModelError::Backend(BackendError::UnexpectedResponse {
183                backend: "unknown".to_string(),
184                message: err.to_string(),
185            })
186        }
187    }
188}
189
190/// Conversion from serde_json::Error
191impl From<serde_json::Error> for ModelError {
192    fn from(err: serde_json::Error) -> Self {
193        ModelError::ParseError {
194            message: err.to_string(),
195            raw: None,
196        }
197    }
198}