aperture_cli/
error.rs

1//! Error handling module for Aperture CLI
2//!
3//! This module provides a consolidated error handling system that categorizes
4//! all application errors into 9 distinct kinds. The design follows these principles:
5//!
6//! 1. **Error Consolidation**: All errors are mapped to one of 9 `ErrorKind` categories
7//! 2. **Structured Context**: Each error can include structured JSON details and suggestions
8//! 3. **Builder Pattern**: `ErrorContext` provides fluent builder methods for error construction
9//! 4. **JSON Support**: All errors can be serialized to JSON for programmatic consumption
10
11use crate::constants;
12use serde::{Deserialize, Serialize};
13use serde_json::json;
14use std::borrow::Cow;
15use thiserror::Error;
16
17#[derive(Error, Debug)]
18pub enum Error {
19    // Keep essential external errors that can't be consolidated
20    #[error("I/O error: {0}")]
21    Io(#[from] std::io::Error),
22    #[error("Network error: {0}")]
23    Network(#[from] reqwest::Error),
24    #[error("YAML parsing error: {0}")]
25    Yaml(#[from] serde_yaml::Error),
26    #[error("JSON parsing error: {0}")]
27    Json(#[from] serde_json::Error),
28    #[error("TOML parsing error: {0}")]
29    Toml(#[from] toml::de::Error),
30
31    // Consolidated error variant using new infrastructure
32    #[error("{kind}: {message}")]
33    Internal {
34        kind: ErrorKind,
35        message: Cow<'static, str>,
36        context: Option<ErrorContext>,
37    },
38
39    #[error(transparent)]
40    Anyhow(#[from] anyhow::Error),
41}
42
43/// Error categories for consolidated error handling
44///
45/// This enum represents the 8 primary error categories used throughout
46/// the application. All internal errors are mapped to one of these categories
47/// to provide consistent error handling and reporting.
48#[derive(Debug, Clone, PartialEq, Eq)]
49pub enum ErrorKind {
50    /// Specification-related errors (not found, already exists, cache issues)
51    Specification,
52    /// Authentication and authorization errors
53    Authentication,
54    /// Input validation and configuration errors
55    Validation,
56    /// Network connectivity and transport errors (connection, DNS, timeouts)
57    Network,
58    /// HTTP request/response errors (status codes, API errors)
59    HttpRequest,
60    /// Header processing errors
61    Headers,
62    /// Interactive input errors
63    Interactive,
64    /// Server variable resolution errors
65    ServerVariable,
66    /// Runtime operation errors
67    Runtime,
68}
69
70/// Additional context for consolidated errors
71#[derive(Debug, Clone)]
72pub struct ErrorContext {
73    /// Structured details for programmatic access
74    pub details: Option<serde_json::Value>,
75    /// Human-readable suggestion for resolving the error
76    pub suggestion: Option<Cow<'static, str>>,
77}
78
79impl ErrorContext {
80    /// Create a new error context with details and suggestion
81    #[must_use]
82    pub const fn new(
83        details: Option<serde_json::Value>,
84        suggestion: Option<Cow<'static, str>>,
85    ) -> Self {
86        Self {
87            details,
88            suggestion,
89        }
90    }
91
92    /// Create error context with only details
93    #[must_use]
94    pub const fn with_details(details: serde_json::Value) -> Self {
95        Self {
96            details: Some(details),
97            suggestion: None,
98        }
99    }
100
101    /// Create error context with only suggestion
102    #[must_use]
103    pub const fn with_suggestion(suggestion: Cow<'static, str>) -> Self {
104        Self {
105            details: None,
106            suggestion: Some(suggestion),
107        }
108    }
109
110    /// Builder method to add a single detail field
111    #[must_use]
112    pub fn with_detail(key: &str, value: impl serde::Serialize) -> Self {
113        Self {
114            details: Some(json!({ key: value })),
115            suggestion: None,
116        }
117    }
118
119    /// Builder method to add name and reason details
120    #[must_use]
121    pub fn with_name_reason(name_field: &str, name: &str, reason: &str) -> Self {
122        Self {
123            details: Some(json!({ name_field: name, "reason": reason })),
124            suggestion: None,
125        }
126    }
127
128    /// Add suggestion to existing context
129    #[must_use]
130    pub fn and_suggestion(mut self, suggestion: impl Into<String>) -> Self {
131        self.suggestion = Some(Cow::Owned(suggestion.into()));
132        self
133    }
134}
135
136impl ErrorKind {
137    /// Get the string identifier for this error kind
138    #[must_use]
139    pub const fn as_str(&self) -> &'static str {
140        match self {
141            Self::Specification => "Specification",
142            Self::Authentication => "Authentication",
143            Self::Validation => "Validation",
144            Self::Network => "Network",
145            Self::HttpRequest => "HttpError",
146            Self::Headers => "Headers",
147            Self::Interactive => "Interactive",
148            Self::ServerVariable => "ServerVariable",
149            Self::Runtime => "Runtime",
150        }
151    }
152}
153
154impl std::fmt::Display for ErrorKind {
155    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
156        write!(f, "{}", self.as_str())
157    }
158}
159
160/// JSON representation of an error for structured output
161#[derive(Debug, Serialize, Deserialize)]
162pub struct JsonError {
163    pub error_type: Cow<'static, str>,
164    pub message: String,
165    pub context: Option<Cow<'static, str>>,
166    #[serde(skip_serializing_if = "Option::is_none")]
167    pub details: Option<serde_json::Value>,
168}
169
170impl Error {
171    /// Add context to an error for better user messaging
172    #[must_use]
173    pub fn with_context(self, context: &str) -> Self {
174        match self {
175            Self::Network(e) => Self::invalid_config(format!("{context}: {e}")),
176            Self::Io(e) => Self::invalid_config(format!("{context}: {e}")),
177            Self::Internal {
178                kind,
179                message,
180                context: ctx,
181            } => Self::Internal {
182                kind,
183                message: Cow::Owned(format!("{context}: {message}")),
184                context: ctx,
185            },
186            _ => self,
187        }
188    }
189
190    /// Add operation context to an error for better debugging
191    #[must_use]
192    pub fn with_operation_context(self, operation: &str, api: &str) -> Self {
193        match self {
194            Self::Internal {
195                kind,
196                message,
197                context,
198            } => Self::Internal {
199                kind,
200                message: Cow::Owned(format!("Operation '{operation}' on API '{api}': {message}")),
201                context,
202            },
203            Self::Network(e) => {
204                Self::invalid_config(format!("Operation '{operation}' on API '{api}': {e}"))
205            }
206            _ => self,
207        }
208    }
209
210    /// Add suggestions to error messages for better user guidance
211    #[must_use]
212    pub fn with_suggestion(self, suggestion: &str) -> Self {
213        match self {
214            Self::Internal {
215                kind,
216                message,
217                context,
218            } => Self::Internal {
219                kind,
220                message,
221                context: context.map_or_else(
222                    || {
223                        Some(ErrorContext::with_suggestion(Cow::Owned(
224                            suggestion.to_string(),
225                        )))
226                    },
227                    |mut ctx| {
228                        ctx.suggestion = Some(Cow::Owned(suggestion.to_string()));
229                        Some(ctx)
230                    },
231                ),
232            },
233            _ => self,
234        }
235    }
236
237    /// Convert error to JSON representation for structured output
238    #[must_use]
239    pub fn to_json(&self) -> JsonError {
240        let (error_type, message, context, details): (
241            &str,
242            String,
243            Option<Cow<'static, str>>,
244            Option<serde_json::Value>,
245        ) = match self {
246            Self::Io(io_err) => {
247                let context = match io_err.kind() {
248                    std::io::ErrorKind::NotFound => {
249                        Some(Cow::Borrowed(constants::ERR_FILE_NOT_FOUND))
250                    }
251                    std::io::ErrorKind::PermissionDenied => {
252                        Some(Cow::Borrowed(constants::ERR_PERMISSION))
253                    }
254                    _ => None,
255                };
256                ("FileSystem", io_err.to_string(), context, None)
257            }
258            Self::Network(req_err) => {
259                let context = if req_err.is_connect() {
260                    Some(Cow::Borrowed(constants::ERR_CONNECTION))
261                } else if req_err.is_timeout() {
262                    Some(Cow::Borrowed(constants::ERR_TIMEOUT))
263                } else if req_err.is_status() {
264                    req_err.status().and_then(|status| match status.as_u16() {
265                        401 => Some(Cow::Borrowed(constants::ERR_API_CREDENTIALS)),
266                        403 => Some(Cow::Borrowed(constants::ERR_PERMISSION_DENIED)),
267                        404 => Some(Cow::Borrowed(constants::ERR_ENDPOINT_NOT_FOUND)),
268                        429 => Some(Cow::Borrowed(constants::ERR_RATE_LIMITED)),
269                        500..=599 => Some(Cow::Borrowed(constants::ERR_SERVER_ERROR)),
270                        _ => None,
271                    })
272                } else {
273                    None
274                };
275                ("Network", req_err.to_string(), context, None)
276            }
277            Self::Yaml(yaml_err) => (
278                "YAMLParsing",
279                yaml_err.to_string(),
280                Some(Cow::Borrowed(constants::ERR_YAML_SYNTAX)),
281                None,
282            ),
283            Self::Json(json_err) => (
284                "JSONParsing",
285                json_err.to_string(),
286                Some(Cow::Borrowed(constants::ERR_JSON_SYNTAX)),
287                None,
288            ),
289            Self::Toml(toml_err) => (
290                "TOMLParsing",
291                toml_err.to_string(),
292                Some(Cow::Borrowed(constants::ERR_TOML_SYNTAX)),
293                None,
294            ),
295            Self::Internal {
296                kind,
297                message,
298                context: ctx,
299            } => {
300                let context = ctx.as_ref().and_then(|c| c.suggestion.clone());
301                let details = ctx.as_ref().and_then(|c| c.details.clone());
302                (kind.as_str(), message.to_string(), context, details)
303            }
304            Self::Anyhow(anyhow_err) => ("Unknown", anyhow_err.to_string(), None, None),
305        };
306
307        JsonError {
308            error_type: Cow::Borrowed(error_type),
309            message,
310            context,
311            details,
312        }
313    }
314}
315
316impl Error {
317    /// Create a specification not found error
318    pub fn spec_not_found(name: impl Into<String>) -> Self {
319        let name = name.into();
320        Self::Internal {
321            kind: ErrorKind::Specification,
322            message: Cow::Owned(format!("API specification '{name}' not found")),
323            context: Some(
324                ErrorContext::with_detail("spec_name", &name)
325                    .and_suggestion(constants::MSG_USE_CONFIG_LIST),
326            ),
327        }
328    }
329
330    /// Create a specification already exists error
331    pub fn spec_already_exists(name: impl Into<String>) -> Self {
332        let name = name.into();
333        Self::Internal {
334            kind: ErrorKind::Specification,
335            message: Cow::Owned(format!(
336                "API specification '{name}' already exists. Use --force to overwrite"
337            )),
338            context: Some(ErrorContext::with_detail("spec_name", &name)),
339        }
340    }
341
342    /// Create a cached spec not found error
343    pub fn cached_spec_not_found(name: impl Into<String>) -> Self {
344        let name = name.into();
345        Self::Internal {
346            kind: ErrorKind::Specification,
347            message: Cow::Owned(format!(
348                "No cached spec found for '{name}'. Run 'aperture config add {name}' first"
349            )),
350            context: Some(ErrorContext::with_detail("spec_name", &name)),
351        }
352    }
353
354    /// Create a cached spec corrupted error
355    pub fn cached_spec_corrupted(name: impl Into<String>, reason: impl Into<String>) -> Self {
356        use serde_json::json;
357        let name = name.into();
358        let reason = reason.into();
359        Self::Internal {
360            kind: ErrorKind::Specification,
361            message: Cow::Owned(format!(
362                "Failed to deserialize cached spec '{name}': {reason}. The cache may be corrupted"
363            )),
364            context: Some(ErrorContext::new(
365                Some(json!({ "spec_name": name, "corruption_reason": reason })),
366                Some(Cow::Borrowed(
367                    "Try removing and re-adding the specification.",
368                )),
369            )),
370        }
371    }
372
373    /// Create a cache version mismatch error
374    pub fn cache_version_mismatch(name: impl Into<String>, found: u32, expected: u32) -> Self {
375        use serde_json::json;
376        let name = name.into();
377        Self::Internal {
378            kind: ErrorKind::Specification,
379            message: Cow::Owned(format!(
380                "Cache format version mismatch for '{name}': found v{found}, expected v{expected}"
381            )),
382            context: Some(ErrorContext::new(
383                Some(
384                    json!({ "spec_name": name, "found_version": found, "expected_version": expected }),
385                ),
386                Some(Cow::Borrowed(
387                    "Run 'aperture config reinit' to regenerate the cache.",
388                )),
389            )),
390        }
391    }
392
393    /// Create a secret not set error
394    pub fn secret_not_set(scheme_name: impl Into<String>, env_var: impl Into<String>) -> Self {
395        use serde_json::json;
396        let scheme_name = scheme_name.into();
397        let env_var = env_var.into();
398        Self::Internal {
399            kind: ErrorKind::Authentication,
400            message: Cow::Owned(format!(
401                "Environment variable '{env_var}' required for authentication '{scheme_name}' is not set"
402            )),
403            context: Some(ErrorContext::new(
404                Some(json!({ "scheme_name": scheme_name, "env_var": env_var })),
405                Some(Cow::Owned(format!("Set the environment variable: export {env_var}=<your-secret>"))),
406            )),
407        }
408    }
409
410    /// Create an unsupported auth scheme error
411    pub fn unsupported_auth_scheme(scheme: impl Into<String>) -> Self {
412        use serde_json::json;
413        let scheme = scheme.into();
414        Self::Internal {
415            kind: ErrorKind::Authentication,
416            message: Cow::Owned(format!("Unsupported HTTP authentication scheme: {scheme}")),
417            context: Some(ErrorContext::new(
418                Some(json!({ "scheme": scheme })),
419                Some(Cow::Borrowed(
420                    "Only 'bearer' and 'basic' schemes are supported.",
421                )),
422            )),
423        }
424    }
425
426    /// Create an unsupported security scheme error
427    pub fn unsupported_security_scheme(scheme_type: impl Into<String>) -> Self {
428        use serde_json::json;
429        let scheme_type = scheme_type.into();
430        Self::Internal {
431            kind: ErrorKind::Authentication,
432            message: Cow::Owned(format!("Unsupported security scheme type: {scheme_type}")),
433            context: Some(ErrorContext::new(
434                Some(json!({ "scheme_type": scheme_type })),
435                Some(Cow::Borrowed(
436                    "Only 'apiKey' and 'http' security schemes are supported.",
437                )),
438            )),
439        }
440    }
441
442    /// Create a generic validation error
443    pub fn validation_error(message: impl Into<String>) -> Self {
444        let message = message.into();
445        Self::Internal {
446            kind: ErrorKind::Validation,
447            message: Cow::Owned(format!("Validation error: {message}")),
448            context: None,
449        }
450    }
451
452    /// Create an invalid configuration error
453    pub fn invalid_config(reason: impl Into<String>) -> Self {
454        let reason = reason.into();
455        Self::Internal {
456            kind: ErrorKind::Validation,
457            message: Cow::Owned(format!("Invalid configuration: {reason}")),
458            context: Some(
459                ErrorContext::with_detail("reason", &reason)
460                    .and_suggestion("Check the configuration file syntax and structure."),
461            ),
462        }
463    }
464
465    /// Create an invalid JSON body error
466    pub fn invalid_json_body(reason: impl Into<String>) -> Self {
467        use serde_json::json;
468        let reason = reason.into();
469        Self::Internal {
470            kind: ErrorKind::Validation,
471            message: Cow::Owned(format!("Invalid JSON body: {reason}")),
472            context: Some(ErrorContext::new(
473                Some(json!({ "reason": reason })),
474                Some(Cow::Borrowed(
475                    "Check that the JSON body is properly formatted.",
476                )),
477            )),
478        }
479    }
480
481    /// Create an invalid path error
482    pub fn invalid_path(path: impl Into<String>, reason: impl Into<String>) -> Self {
483        use serde_json::json;
484        let path = path.into();
485        let reason = reason.into();
486        Self::Internal {
487            kind: ErrorKind::Validation,
488            message: Cow::Owned(format!("Invalid path '{path}': {reason}")),
489            context: Some(ErrorContext::new(
490                Some(json!({ "path": path, "reason": reason })),
491                Some(Cow::Borrowed("Check the path format and ensure it exists.")),
492            )),
493        }
494    }
495
496    /// Create a request failed error
497    pub fn request_failed(status: reqwest::StatusCode, reason: impl Into<String>) -> Self {
498        use serde_json::json;
499        let reason = reason.into();
500        Self::Internal {
501            kind: ErrorKind::HttpRequest,
502            message: Cow::Owned(format!("Request failed with status {status}: {reason}")),
503            context: Some(ErrorContext::new(
504                Some(json!({ "status_code": status.as_u16(), "reason": reason })),
505                Some(Cow::Borrowed(
506                    "Check the API endpoint, parameters, and authentication.",
507                )),
508            )),
509        }
510    }
511
512    /// Create a response read error
513    pub fn response_read_error(reason: impl Into<String>) -> Self {
514        use serde_json::json;
515        let reason = reason.into();
516        Self::Internal {
517            kind: ErrorKind::HttpRequest,
518            message: Cow::Owned(format!("Failed to read response: {reason}")),
519            context: Some(ErrorContext::new(
520                Some(json!({ "reason": reason })),
521                Some(Cow::Borrowed(
522                    "Check network connectivity and server status.",
523                )),
524            )),
525        }
526    }
527
528    /// Create an invalid HTTP method error
529    pub fn invalid_http_method(method: impl Into<String>) -> Self {
530        use serde_json::json;
531        let method = method.into();
532        Self::Internal {
533            kind: ErrorKind::HttpRequest,
534            message: Cow::Owned(format!("Invalid HTTP method: {method}")),
535            context: Some(ErrorContext::new(
536                Some(json!({ "method": method })),
537                Some(Cow::Borrowed(
538                    "Valid HTTP methods are: GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS.",
539                )),
540            )),
541        }
542    }
543
544    // ---- Header Errors ----
545
546    /// Create an invalid header name error
547    pub fn invalid_header_name(name: impl Into<String>, reason: impl Into<String>) -> Self {
548        use serde_json::json;
549        let name = name.into();
550        let reason = reason.into();
551        Self::Internal {
552            kind: ErrorKind::Headers,
553            message: Cow::Owned(format!("Invalid header name '{name}': {reason}")),
554            context: Some(ErrorContext::new(
555                Some(json!({ "header_name": name, "reason": reason })),
556                Some(Cow::Borrowed(
557                    "Header names must contain only valid HTTP header characters.",
558                )),
559            )),
560        }
561    }
562
563    /// Create an invalid header value error
564    pub fn invalid_header_value(name: impl Into<String>, reason: impl Into<String>) -> Self {
565        use serde_json::json;
566        let name = name.into();
567        let reason = reason.into();
568        Self::Internal {
569            kind: ErrorKind::Headers,
570            message: Cow::Owned(format!("Invalid header value for '{name}': {reason}")),
571            context: Some(ErrorContext::new(
572                Some(json!({ "header_name": name, "reason": reason })),
573                Some(Cow::Borrowed(
574                    "Header values must contain only valid HTTP header characters.",
575                )),
576            )),
577        }
578    }
579
580    /// Create an invalid header format error
581    pub fn invalid_header_format(header: impl Into<String>) -> Self {
582        use serde_json::json;
583        let header = header.into();
584        Self::Internal {
585            kind: ErrorKind::Headers,
586            message: Cow::Owned(format!(
587                "Invalid header format '{header}'. Expected 'Name: Value'"
588            )),
589            context: Some(ErrorContext::new(
590                Some(json!({ "header": header })),
591                Some(Cow::Borrowed("Headers must be in 'Name: Value' format.")),
592            )),
593        }
594    }
595
596    /// Create an empty header name error
597    #[must_use]
598    pub const fn empty_header_name() -> Self {
599        Self::Internal {
600            kind: ErrorKind::Headers,
601            message: Cow::Borrowed("Header name cannot be empty"),
602            context: Some(ErrorContext::with_suggestion(Cow::Borrowed(
603                "Provide a valid header name before the colon.",
604            ))),
605        }
606    }
607
608    // ---- Interactive Errors ----
609
610    /// Create an interactive input too long error
611    #[must_use]
612    pub fn interactive_input_too_long(max_length: usize) -> Self {
613        Self::Internal {
614            kind: ErrorKind::Interactive,
615            message: Cow::Owned(format!("Input too long (maximum {max_length} characters)")),
616            context: Some(
617                ErrorContext::with_detail("max_length", max_length)
618                    .and_suggestion("Please provide a shorter input."),
619            ),
620        }
621    }
622
623    /// Create an interactive invalid characters error
624    pub fn interactive_invalid_characters(
625        invalid_chars: impl Into<String>,
626        suggestion: impl Into<String>,
627    ) -> Self {
628        use serde_json::json;
629        let invalid_chars = invalid_chars.into();
630        Self::Internal {
631            kind: ErrorKind::Interactive,
632            message: Cow::Owned(format!("Invalid characters found: {invalid_chars}")),
633            context: Some(ErrorContext::new(
634                Some(json!({ "invalid_characters": invalid_chars })),
635                Some(Cow::Owned(suggestion.into())),
636            )),
637        }
638    }
639
640    /// Create an interactive timeout error
641    #[must_use]
642    pub const fn interactive_timeout() -> Self {
643        Self::Internal {
644            kind: ErrorKind::Interactive,
645            message: Cow::Borrowed("Input timeout - no response received"),
646            context: Some(ErrorContext::with_suggestion(Cow::Borrowed(
647                "Please respond within the timeout period.",
648            ))),
649        }
650    }
651
652    /// Create an interactive retries exhausted error
653    pub fn interactive_retries_exhausted(
654        max_retries: usize,
655        last_error: impl Into<String>,
656        suggestions: &[String],
657    ) -> Self {
658        use serde_json::json;
659        let last_error = last_error.into();
660        Self::Internal {
661            kind: ErrorKind::Interactive,
662            message: Cow::Owned(format!(
663                "Maximum retry attempts ({max_retries}) exceeded: {last_error}"
664            )),
665            context: Some(ErrorContext::new(
666                Some(
667                    json!({ "max_attempts": max_retries, "last_error": last_error, "suggestions": suggestions }),
668                ),
669                Some(Cow::Owned(format!(
670                    "Suggestions: {}",
671                    suggestions.join("; ")
672                ))),
673            )),
674        }
675    }
676
677    // ---- Server Variable Errors ----
678
679    /// Create a missing server variable error
680    pub fn missing_server_variable(name: impl Into<String>) -> Self {
681        let name = name.into();
682        Self::Internal {
683            kind: ErrorKind::ServerVariable,
684            message: Cow::Owned(format!("Required server variable '{name}' is not provided")),
685            context: Some(
686                ErrorContext::with_detail("variable_name", &name).and_suggestion(format!(
687                    "Provide the variable with --server-var {name}=<value>"
688                )),
689            ),
690        }
691    }
692
693    /// Create an unknown server variable error
694    pub fn unknown_server_variable(name: impl Into<String>, available: &[String]) -> Self {
695        use serde_json::json;
696        let name = name.into();
697        let available_list = available.join(", ");
698        Self::Internal {
699            kind: ErrorKind::ServerVariable,
700            message: Cow::Owned(format!(
701                "Unknown server variable '{name}'. Available variables: {available_list}"
702            )),
703            context: Some(ErrorContext::new(
704                Some(json!({ "variable_name": name, "available_variables": available })),
705                Some(Cow::Owned(format!("Use one of: {available_list}"))),
706            )),
707        }
708    }
709
710    /// Create an unresolved template variable error
711    pub fn unresolved_template_variable(name: impl Into<String>, url: impl Into<String>) -> Self {
712        use serde_json::json;
713        let name = name.into();
714        let url = url.into();
715        Self::Internal {
716            kind: ErrorKind::ServerVariable,
717            message: Cow::Owned(format!(
718                "Unresolved template variable '{name}' in URL '{url}'"
719            )),
720            context: Some(ErrorContext::new(
721                Some(json!({ "variable_name": name, "template_url": url })),
722                Some(Cow::Borrowed(
723                    "Ensure all template variables are provided with --server-var",
724                )),
725            )),
726        }
727    }
728
729    /// Create an invalid environment variable name error with suggestion
730    pub fn invalid_environment_variable_name(
731        name: impl Into<String>,
732        reason: impl Into<String>,
733        suggestion: impl Into<String>,
734    ) -> Self {
735        let name = name.into();
736        let reason = reason.into();
737        Self::Internal {
738            kind: ErrorKind::Interactive,
739            message: Cow::Owned(format!(
740                "Invalid environment variable name '{name}': {reason}"
741            )),
742            context: Some(
743                ErrorContext::with_name_reason("variable_name", &name, &reason)
744                    .and_suggestion(suggestion),
745            ),
746        }
747    }
748
749    /// Create an invalid server variable format error
750    pub fn invalid_server_var_format(arg: impl Into<String>, reason: impl Into<String>) -> Self {
751        use serde_json::json;
752        let arg = arg.into();
753        let reason = reason.into();
754        Self::Internal {
755            kind: ErrorKind::ServerVariable,
756            message: Cow::Owned(format!(
757                "Invalid server variable format in '{arg}': {reason}"
758            )),
759            context: Some(ErrorContext::new(
760                Some(json!({ "argument": arg, "reason": reason })),
761                Some(Cow::Borrowed(
762                    "Server variables must be in 'key=value' format.",
763                )),
764            )),
765        }
766    }
767
768    /// Create an invalid server variable value error
769    pub fn invalid_server_var_value(
770        name: impl Into<String>,
771        value: impl Into<String>,
772        allowed_values: &[String],
773    ) -> Self {
774        use serde_json::json;
775        let name = name.into();
776        let value = value.into();
777        Self::Internal {
778            kind: ErrorKind::ServerVariable,
779            message: Cow::Owned(format!(
780                "Invalid value '{value}' for server variable '{name}'"
781            )),
782            context: Some(ErrorContext::new(
783                Some(
784                    json!({ "variable_name": name, "provided_value": value, "allowed_values": allowed_values }),
785                ),
786                Some(Cow::Owned(format!(
787                    "Allowed values: {}",
788                    allowed_values.join(", ")
789                ))),
790            )),
791        }
792    }
793
794    // ---- Runtime Errors ----
795
796    /// Create an operation not found error
797    pub fn operation_not_found(operation: impl Into<String>) -> Self {
798        use serde_json::json;
799        let operation = operation.into();
800        Self::Internal {
801            kind: ErrorKind::Runtime,
802            message: Cow::Owned(format!("Operation '{operation}' not found")),
803            context: Some(ErrorContext::new(
804                Some(json!({ "operation": operation })),
805                Some(Cow::Borrowed(
806                    "Check available operations with --help or --describe-json",
807                )),
808            )),
809        }
810    }
811
812    /// Create a network request failed error
813    pub fn network_request_failed(reason: impl Into<String>) -> Self {
814        let reason = reason.into();
815        Self::Internal {
816            kind: ErrorKind::Network,
817            message: Cow::Owned(format!("Network request failed: {reason}")),
818            context: Some(
819                ErrorContext::with_detail("reason", &reason)
820                    .and_suggestion("Check network connectivity and URL validity"),
821            ),
822        }
823    }
824
825    /// Create a serialization error
826    pub fn serialization_error(reason: impl Into<String>) -> Self {
827        let reason = reason.into();
828        Self::Internal {
829            kind: ErrorKind::Validation,
830            message: Cow::Owned(format!("Serialization failed: {reason}")),
831            context: Some(
832                ErrorContext::with_detail("reason", &reason)
833                    .and_suggestion("Check data structure validity"),
834            ),
835        }
836    }
837
838    /// Create a home directory not found error
839    #[must_use]
840    pub fn home_directory_not_found() -> Self {
841        Self::Internal {
842            kind: ErrorKind::Runtime,
843            message: Cow::Borrowed("Home directory not found"),
844            context: Some(ErrorContext::new(
845                Some(serde_json::json!({})),
846                Some(Cow::Borrowed("Ensure HOME environment variable is set")),
847            )),
848        }
849    }
850
851    /// Create an invalid command error
852    pub fn invalid_command(context: impl Into<String>, reason: impl Into<String>) -> Self {
853        let context = context.into();
854        let reason = reason.into();
855        Self::Internal {
856            kind: ErrorKind::Validation,
857            message: Cow::Owned(format!("Invalid command for '{context}': {reason}")),
858            context: Some(
859                ErrorContext::with_name_reason("context", &context, &reason)
860                    .and_suggestion("Check available commands with --help or --describe-json"),
861            ),
862        }
863    }
864
865    /// Create an HTTP error with context
866    pub fn http_error_with_context(
867        status: u16,
868        body: impl Into<String>,
869        api_name: impl Into<String>,
870        operation_id: Option<impl Into<String>>,
871        security_schemes: &[String],
872    ) -> Self {
873        use serde_json::json;
874        let body = body.into();
875        let api_name = api_name.into();
876        let operation_id = operation_id.map(std::convert::Into::into);
877
878        // Include important parts of response body in message for backward compatibility
879        let message = if body.len() <= 200 && !body.is_empty() {
880            format!("HTTP {status} error for '{api_name}': {body}")
881        } else {
882            format!("HTTP {status} error for '{api_name}'")
883        };
884
885        Self::Internal {
886            kind: ErrorKind::HttpRequest,
887            message: Cow::Owned(message),
888            context: Some(ErrorContext::new(
889                Some(json!({
890                    "status": status,
891                    "response_body": body,
892                    "api_name": api_name,
893                    "operation_id": operation_id,
894                    "security_schemes": security_schemes
895                })),
896                Some(Cow::Borrowed(
897                    "Check the API endpoint, parameters, and authentication.",
898                )),
899            )),
900        }
901    }
902
903    /// Create a JQ filter error
904    pub fn jq_filter_error(filter: impl Into<String>, reason: impl Into<String>) -> Self {
905        let filter = filter.into();
906        let reason = reason.into();
907        Self::Internal {
908            kind: ErrorKind::Validation,
909            message: Cow::Owned(format!("JQ filter error in '{filter}': {reason}")),
910            context: Some(
911                ErrorContext::with_name_reason("filter", &filter, &reason)
912                    .and_suggestion("Check JQ filter syntax and data structure compatibility"),
913            ),
914        }
915    }
916
917    /// Create a transient network error
918    pub fn transient_network_error(reason: impl Into<String>, retryable: bool) -> Self {
919        let reason = reason.into();
920        Self::Internal {
921            kind: ErrorKind::Network,
922            message: Cow::Owned(format!("Transient network error: {reason}")),
923            context: Some(ErrorContext::new(
924                Some(serde_json::json!({
925                    "reason": reason,
926                    "retryable": retryable
927                })),
928                Some(Cow::Borrowed(if retryable {
929                    "This error may be temporary and could succeed on retry"
930                } else {
931                    "This error is not retryable"
932                })),
933            )),
934        }
935    }
936
937    /// Create a retry limit exceeded error
938    pub fn retry_limit_exceeded(max_attempts: u32, last_error: impl Into<String>) -> Self {
939        let last_error = last_error.into();
940        Self::Internal {
941            kind: ErrorKind::Network,
942            message: Cow::Owned(format!(
943                "Retry limit exceeded after {max_attempts} attempts: {last_error}"
944            )),
945            context: Some(ErrorContext::new(
946                Some(serde_json::json!({
947                    "max_attempts": max_attempts,
948                    "last_error": last_error
949                })),
950                Some(Cow::Borrowed(
951                    "Consider checking network connectivity or increasing retry limits",
952                )),
953            )),
954        }
955    }
956
957    /// Create a request timeout error
958    #[must_use]
959    pub fn request_timeout(timeout_seconds: u64) -> Self {
960        Self::Internal {
961            kind: ErrorKind::Network,
962            message: Cow::Owned(format!("Request timed out after {timeout_seconds} seconds")),
963            context: Some(ErrorContext::new(
964                Some(serde_json::json!({
965                    "timeout_seconds": timeout_seconds
966                })),
967                Some(Cow::Borrowed(
968                    "Consider increasing the timeout or checking network connectivity",
969                )),
970            )),
971        }
972    }
973
974    /// Create a missing path parameter error
975    pub fn missing_path_parameter(name: impl Into<String>) -> Self {
976        let name = name.into();
977        Self::Internal {
978            kind: ErrorKind::Validation,
979            message: Cow::Owned(format!("Missing required path parameter: {name}")),
980            context: Some(
981                ErrorContext::with_detail("parameter_name", &name)
982                    .and_suggestion("Provide a value for this required path parameter"),
983            ),
984        }
985    }
986
987    /// Create a general I/O error
988    pub fn io_error(message: impl Into<String>) -> Self {
989        let message = message.into();
990        Self::Internal {
991            kind: ErrorKind::Runtime,
992            message: Cow::Owned(message),
993            context: None,
994        }
995    }
996
997    /// Create an invalid idempotency key error
998    #[must_use]
999    pub const fn invalid_idempotency_key() -> Self {
1000        Self::Internal {
1001            kind: ErrorKind::Headers,
1002            message: Cow::Borrowed("Invalid idempotency key format"),
1003            context: Some(ErrorContext::new(
1004                None,
1005                Some(Cow::Borrowed(
1006                    "Ensure the idempotency key contains only valid header characters",
1007                )),
1008            )),
1009        }
1010    }
1011
1012    /// Create an editor not set error
1013    #[must_use]
1014    pub const fn editor_not_set() -> Self {
1015        Self::Internal {
1016            kind: ErrorKind::Interactive,
1017            message: Cow::Borrowed("EDITOR environment variable not set"),
1018            context: Some(ErrorContext::new(
1019                None,
1020                Some(Cow::Borrowed(
1021                    "Set your preferred editor: export EDITOR=vim",
1022                )),
1023            )),
1024        }
1025    }
1026
1027    /// Create an editor failed error
1028    pub fn editor_failed(name: impl Into<String>) -> Self {
1029        let name = name.into();
1030        Self::Internal {
1031            kind: ErrorKind::Interactive,
1032            message: Cow::Owned(format!("Editor '{name}' failed to complete")),
1033            context: Some(ErrorContext::new(
1034                Some(serde_json::json!({ "editor": name })),
1035                Some(Cow::Borrowed(
1036                    "Check if the editor is properly installed and configured",
1037                )),
1038            )),
1039        }
1040    }
1041}