1use anyhow::Result;
6use colored::Colorize;
7
8use super::styling::{Styling, Theme};
9
10#[derive(Debug, Clone)]
12pub struct StructuredError {
13 message: String,
15 provider: Option<String>,
17 model: Option<String>,
19 underlying: Option<String>,
21 context: Vec<(String, String)>,
23 hints: Vec<String>,
25 exit_code: i32,
27}
28
29impl StructuredError {
30 pub fn new(message: &str) -> Self {
32 Self {
33 message: message.to_string(),
34 provider: None,
35 model: None,
36 underlying: None,
37 context: Vec::new(),
38 hints: Vec::new(),
39 exit_code: 1,
40 }
41 }
42
43 pub fn with_provider(mut self, provider: &str) -> Self {
45 self.provider = Some(provider.to_string());
46 self
47 }
48
49 pub fn with_model(mut self, model: &str) -> Self {
51 self.model = Some(model.to_string());
52 self
53 }
54
55 pub fn with_underlying(mut self, error: &str) -> Self {
57 self.underlying = Some(error.to_string());
58 self
59 }
60
61 #[allow(dead_code)]
63 pub fn with_context(mut self, key: &str, value: &str) -> Self {
64 self.context.push((key.to_string(), value.to_string()));
65 self
66 }
67
68 #[allow(dead_code)]
70 pub fn with_hint(mut self, hint: &str) -> Self {
71 self.hints.push(hint.to_string());
72 self
73 }
74
75 pub fn with_hints<T: IntoIterator<Item = String>>(mut self, hints: T) -> Self {
77 self.hints.extend(hints);
78 self
79 }
80
81 pub fn with_exit_code(mut self, code: i32) -> Self {
83 self.exit_code = code;
84 self
85 }
86
87 pub fn exit_code(&self) -> i32 {
89 self.exit_code
90 }
91
92 pub fn display(&self, _theme: &Theme) -> String {
94 let mut output = String::new();
95
96 output.push_str(&format!(
98 "{} {}\n",
99 Styling::error("X"),
100 Styling::header(&self.message)
101 ));
102
103 output.push_str(&Styling::divider(50));
105 output.push('\n');
106
107 if let Some(ref provider) = self.provider {
109 output.push_str(&format!("{}: {}\n", "Provider".dimmed(), provider));
110 }
111 if let Some(ref model) = self.model {
112 output.push_str(&format!("{}: {}\n", "Model".dimmed(), model));
113 }
114 if let Some(ref underlying) = self.underlying {
115 output.push_str(&format!("{}: {}\n", "Error".dimmed(), underlying));
116 }
117
118 for (key, value) in &self.context {
120 output.push_str(&format!("{}: {}\n", key.dimmed(), value));
121 }
122
123 if !self.hints.is_empty() {
125 output.push('\n');
126 output.push_str("Suggestions:\n");
127 for hint in &self.hints {
128 output.push_str(&format!(" - {}\n", hint));
129 }
130 }
131
132 output
133 }
134
135 #[allow(dead_code)]
137 pub fn to_json(&self) -> String {
138 use serde_json::json;
139
140 let hints_array: Vec<String> = self.hints.clone();
141 let context_obj: serde_json::Map<String, serde_json::Value> = self
142 .context
143 .iter()
144 .map(|(k, v)| (k.clone(), serde_json::Value::String(v.clone())))
145 .collect();
146
147 let obj = json!({
148 "error": self.message,
149 "provider": self.provider,
150 "model": self.model,
151 "underlying": self.underlying,
152 "context": context_obj,
153 "hints": hints_array,
154 "exit_code": self.exit_code,
155 });
156
157 serde_json::to_string_pretty(&obj).unwrap_or_else(|_| "{}".to_string())
158 }
159
160 #[allow(dead_code)]
162 pub fn to_markdown(&self) -> String {
163 let mut output = String::new();
164
165 output.push_str("## Error\n\n");
166 output.push_str(&format!("**{}**\n\n", self.message));
167
168 if let Some(ref provider) = self.provider {
169 output.push_str(&format!("- **Provider:** {}\n", provider));
170 }
171 if let Some(ref model) = self.model {
172 output.push_str(&format!("- **Model:** {}\n", model));
173 }
174 if let Some(ref underlying) = self.underlying {
175 output.push_str(&format!("- **Error:** {}\n", underlying));
176 }
177
178 if !self.hints.is_empty() {
179 output.push_str("\n## Suggestions\n\n");
180 for hint in &self.hints {
181 output.push_str(&format!("- {}\n", hint));
182 }
183 }
184
185 output
186 }
187}
188
189#[allow(dead_code)]
190pub trait ToStructured {
192 fn to_structured(&self) -> StructuredError;
193}
194
195impl ToStructured for anyhow::Error {
196 fn to_structured(&self) -> StructuredError {
197 StructuredError::new(&self.to_string())
198 }
199}
200
201#[allow(dead_code)]
203pub mod patterns {
204 use super::*;
205
206 pub fn rate_limit(provider: &str, model: &str) -> StructuredError {
208 StructuredError::new("API rate limit exceeded")
209 .with_provider(provider)
210 .with_model(model)
211 .with_hints(vec![
212 "Wait a few seconds and try again".to_string(),
213 "Use a lighter/faster model".to_string(),
214 "Check the provider's rate limits".to_string(),
215 ])
216 }
217
218 pub fn auth(provider: &str) -> StructuredError {
220 StructuredError::new("Authentication failed")
221 .with_provider(provider)
222 .with_exit_code(401)
223 .with_hints(vec![
224 "Run 'rco auth login' to authenticate".to_string(),
225 "Check your API key is valid".to_string(),
226 "Ensure your account has access to the model".to_string(),
227 ])
228 }
229
230 pub fn invalid_api_key(provider: &str) -> StructuredError {
232 StructuredError::new("Invalid API key")
233 .with_provider(provider)
234 .with_exit_code(401)
235 .with_hints(vec![
236 "Check your API key is correct".to_string(),
237 "Run 'rco auth login' to re-authenticate".to_string(),
238 "Verify your API key has the right permissions".to_string(),
239 ])
240 }
241
242 pub fn no_changes() -> StructuredError {
244 StructuredError::new("No changes to commit")
245 .with_exit_code(0)
246 .with_hints(vec![
247 "Stage some changes with 'git add'".to_string(),
248 "Use 'git add -A' to stage all changes".to_string(),
249 "Check for untracked files".to_string(),
250 ])
251 }
252
253 pub fn not_git_repo() -> StructuredError {
255 StructuredError::new("Not a git repository")
256 .with_exit_code(128)
257 .with_hints(vec![
258 "Initialize a git repository with 'git init'".to_string(),
259 "Navigate to a git repository".to_string(),
260 "Clone a repository first".to_string(),
261 ])
262 }
263
264 pub fn provider_not_found(provider: &str) -> StructuredError {
266 StructuredError::new(&format!("Provider not found: {}", provider))
267 .with_exit_code(1)
268 .with_hints(vec![
269 "Check the provider name is correct".to_string(),
270 "Run 'rco config describe' to see available providers".to_string(),
271 "Supported providers: openai, anthropic, ollama, gemini, and more".to_string(),
272 ])
273 }
274
275 pub fn model_not_found(model: &str, provider: &str) -> StructuredError {
277 StructuredError::new(&format!("Model not found: {}", model))
278 .with_provider(provider)
279 .with_exit_code(1)
280 .with_hints(vec![
281 "Check the model name is correct".to_string(),
282 "Run 'rco model --list' to see available models".to_string(),
283 "Try using the default model for this provider".to_string(),
284 ])
285 }
286
287 pub fn network(error: &str) -> StructuredError {
289 StructuredError::new("Network error")
290 .with_underlying(error)
291 .with_hints(vec![
292 "Check your internet connection".to_string(),
293 "Verify the API endpoint is accessible".to_string(),
294 "Check for firewall or proxy issues".to_string(),
295 "Try again later".to_string(),
296 ])
297 }
298
299 pub fn timeout(provider: &str) -> StructuredError {
301 StructuredError::new("Request timed out")
302 .with_provider(provider)
303 .with_hints(vec![
304 "Try again - it may be a temporary issue".to_string(),
305 "Use a smaller/faster model".to_string(),
306 "Check the provider's status page".to_string(),
307 ])
308 }
309}
310
311#[allow(dead_code)]
312pub fn print_error(error: &StructuredError, theme: &Theme) {
314 match theme.use_colors {
315 true => {
316 eprintln!("{}", error.display(theme));
317 }
318 false => {
319 eprintln!("Error: {}", error.message);
320 if let Some(ref provider) = error.provider {
321 eprintln!("Provider: {}", provider);
322 }
323 if let Some(ref model) = error.model {
324 eprintln!("Model: {}", model);
325 }
326 if let Some(ref underlying) = error.underlying {
327 eprintln!("Error: {}", underlying);
328 }
329 if !error.hints.is_empty() {
330 eprintln!("Suggestions:");
331 for hint in &error.hints {
332 eprintln!(" - {}", hint);
333 }
334 }
335 }
336 }
337}
338
339#[allow(dead_code)]
340pub fn exit_with_error(error: &StructuredError) -> ! {
342 let theme = Theme::new();
343 print_error(error, &theme);
344 std::process::exit(error.exit_code());
345}
346
347#[allow(dead_code)]
348pub fn context<T, E: std::error::Error + Send + Sync>(
350 result: Result<T, E>,
351 message: &str,
352) -> Result<T, Box<StructuredError>> {
353 result.map_err(|e| Box::new(StructuredError::new(message).with_underlying(&e.to_string())))
354}
355
356#[cfg(test)]
357mod tests {
358 use super::*;
359
360 #[test]
361 fn test_structured_error_new() {
362 let error = StructuredError::new("Test error");
363 assert_eq!(error.message, "Test error");
364 assert!(error.provider.is_none());
365 assert!(error.hints.is_empty());
366 assert_eq!(error.exit_code(), 1);
367 }
368
369 #[test]
370 fn test_structured_error_with_chain() {
371 let error = StructuredError::new("Main error")
372 .with_provider("TestProvider")
373 .with_model("TestModel")
374 .with_underlying("Underlying error")
375 .with_hint("Hint 1")
376 .with_hint("Hint 2")
377 .with_exit_code(42);
378
379 assert_eq!(error.message, "Main error");
380 assert_eq!(error.provider, Some("TestProvider".to_string()));
381 assert_eq!(error.model, Some("TestModel".to_string()));
382 assert_eq!(error.underlying, Some("Underlying error".to_string()));
383 assert_eq!(error.hints.len(), 2);
384 assert_eq!(error.exit_code(), 42);
385 }
386
387 #[test]
388 fn test_error_patterns_rate_limit() {
389 let error = patterns::rate_limit("Anthropic", "claude-3-5-haiku");
390 assert!(error.message.contains("rate limit"));
391 assert_eq!(error.provider, Some("Anthropic".to_string()));
392 assert_eq!(error.model, Some("claude-3-5-haiku".to_string()));
393 assert!(!error.hints.is_empty());
394 }
395
396 #[test]
397 fn test_error_patterns_auth() {
398 let error = patterns::auth("OpenAI");
399 assert!(error.message.contains("Authentication"));
400 assert_eq!(error.exit_code(), 401);
401 }
402
403 #[test]
404 fn test_error_patterns_no_changes() {
405 let error = patterns::no_changes();
406 assert!(error.message.contains("No changes"));
407 assert_eq!(error.exit_code(), 0);
408 }
409
410 #[test]
411 fn test_error_to_json() {
412 let error = StructuredError::new("Test").with_hint("Hint 1");
413 let json = error.to_json();
414 assert!(json.contains("Test"));
415 assert!(json.contains("Hint 1"));
416 }
417
418 #[test]
419 fn test_error_to_markdown() {
420 let error = StructuredError::new("Test Error")
421 .with_provider("TestProvider")
422 .with_hint("Try again");
423 let md = error.to_markdown();
424 assert!(md.contains("## Error"));
425 assert!(md.contains("Test Error"));
426 assert!(md.contains("Provider"));
427 assert!(md.contains("## Suggestions"));
428 }
429
430 #[test]
431 fn test_structured_error_display() {
432 let theme = Theme::new();
433 let error = StructuredError::new("Test error").with_hint("Test hint");
434 let display = error.display(&theme);
435 assert!(display.contains("Test error"));
436 assert!(display.contains("Suggestions"));
437 assert!(display.contains("Test hint"));
438 }
439}