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        let name = name.into();
357        let reason = reason.into();
358        Self::Internal {
359            kind: ErrorKind::Specification,
360            message: Cow::Owned(format!(
361                "Failed to deserialize cached spec '{name}': {reason}. The cache may be corrupted"
362            )),
363            context: Some(ErrorContext::new(
364                Some(json!({ "spec_name": name, "corruption_reason": reason })),
365                Some(Cow::Borrowed(
366                    "Try removing and re-adding the specification.",
367                )),
368            )),
369        }
370    }
371
372    /// Create a cache version mismatch error
373    pub fn cache_version_mismatch(name: impl Into<String>, found: u32, expected: u32) -> Self {
374        let name = name.into();
375        Self::Internal {
376            kind: ErrorKind::Specification,
377            message: Cow::Owned(format!(
378                "Cache format version mismatch for '{name}': found v{found}, expected v{expected}"
379            )),
380            context: Some(ErrorContext::new(
381                Some(
382                    json!({ "spec_name": name, "found_version": found, "expected_version": expected }),
383                ),
384                Some(Cow::Borrowed(
385                    "Run 'aperture config reinit' to regenerate the cache.",
386                )),
387            )),
388        }
389    }
390
391    /// Create a secret not set error
392    pub fn secret_not_set(scheme_name: impl Into<String>, env_var: impl Into<String>) -> Self {
393        let scheme_name = scheme_name.into();
394        let env_var = env_var.into();
395        let suggestion = crate::suggestions::suggest_auth_fix(&scheme_name, Some(&env_var));
396        Self::Internal {
397            kind: ErrorKind::Authentication,
398            message: Cow::Owned(format!(
399                "Environment variable '{env_var}' required for authentication '{scheme_name}' is not set"
400            )),
401            context: Some(ErrorContext::new(
402                Some(json!({ "scheme_name": scheme_name, "env_var": env_var })),
403                Some(Cow::Owned(suggestion)),
404            )),
405        }
406    }
407
408    /// Create an unsupported auth scheme error
409    pub fn unsupported_auth_scheme(scheme: impl Into<String>) -> Self {
410        let scheme = scheme.into();
411        Self::Internal {
412            kind: ErrorKind::Authentication,
413            message: Cow::Owned(format!("Unsupported HTTP authentication scheme: {scheme}")),
414            context: Some(ErrorContext::new(
415                Some(json!({ "scheme": scheme })),
416                Some(Cow::Borrowed(
417                    "Only 'bearer' and 'basic' schemes are supported.",
418                )),
419            )),
420        }
421    }
422
423    /// Create an unsupported security scheme error
424    pub fn unsupported_security_scheme(scheme_type: impl Into<String>) -> Self {
425        let scheme_type = scheme_type.into();
426        Self::Internal {
427            kind: ErrorKind::Authentication,
428            message: Cow::Owned(format!("Unsupported security scheme type: {scheme_type}")),
429            context: Some(ErrorContext::new(
430                Some(json!({ "scheme_type": scheme_type })),
431                Some(Cow::Borrowed(
432                    "Only 'apiKey' and 'http' security schemes are supported.",
433                )),
434            )),
435        }
436    }
437
438    /// Create a generic validation error
439    pub fn validation_error(message: impl Into<String>) -> Self {
440        let message = message.into();
441        Self::Internal {
442            kind: ErrorKind::Validation,
443            message: Cow::Owned(format!("Validation error: {message}")),
444            context: None,
445        }
446    }
447
448    /// Create an invalid configuration error
449    pub fn invalid_config(reason: impl Into<String>) -> Self {
450        let reason = reason.into();
451        Self::Internal {
452            kind: ErrorKind::Validation,
453            message: Cow::Owned(format!("Invalid configuration: {reason}")),
454            context: Some(
455                ErrorContext::with_detail("reason", &reason)
456                    .and_suggestion("Check the configuration file syntax and structure."),
457            ),
458        }
459    }
460
461    /// Create an invalid JSON body error
462    pub fn invalid_json_body(reason: impl Into<String>) -> Self {
463        let reason = reason.into();
464        Self::Internal {
465            kind: ErrorKind::Validation,
466            message: Cow::Owned(format!("Invalid JSON body: {reason}")),
467            context: Some(ErrorContext::new(
468                Some(json!({ "reason": reason })),
469                Some(Cow::Borrowed(
470                    "Check that the JSON body is properly formatted.",
471                )),
472            )),
473        }
474    }
475
476    /// Create an invalid path error
477    pub fn invalid_path(path: impl Into<String>, reason: impl Into<String>) -> Self {
478        let path = path.into();
479        let reason = reason.into();
480        Self::Internal {
481            kind: ErrorKind::Validation,
482            message: Cow::Owned(format!("Invalid path '{path}': {reason}")),
483            context: Some(ErrorContext::new(
484                Some(json!({ "path": path, "reason": reason })),
485                Some(Cow::Borrowed("Check the path format and ensure it exists.")),
486            )),
487        }
488    }
489
490    /// Create a request failed error
491    pub fn request_failed(status: reqwest::StatusCode, reason: impl Into<String>) -> Self {
492        let reason = reason.into();
493        Self::Internal {
494            kind: ErrorKind::HttpRequest,
495            message: Cow::Owned(format!("Request failed with status {status}: {reason}")),
496            context: Some(ErrorContext::new(
497                Some(json!({ "status_code": status.as_u16(), "reason": reason })),
498                Some(Cow::Borrowed(
499                    "Check the API endpoint, parameters, and authentication.",
500                )),
501            )),
502        }
503    }
504
505    /// Create a response read error
506    pub fn response_read_error(reason: impl Into<String>) -> Self {
507        let reason = reason.into();
508        Self::Internal {
509            kind: ErrorKind::HttpRequest,
510            message: Cow::Owned(format!("Failed to read response: {reason}")),
511            context: Some(ErrorContext::new(
512                Some(json!({ "reason": reason })),
513                Some(Cow::Borrowed(
514                    "Check network connectivity and server status.",
515                )),
516            )),
517        }
518    }
519
520    /// Create an invalid HTTP method error
521    pub fn invalid_http_method(method: impl Into<String>) -> Self {
522        let method = method.into();
523        Self::Internal {
524            kind: ErrorKind::HttpRequest,
525            message: Cow::Owned(format!("Invalid HTTP method: {method}")),
526            context: Some(ErrorContext::new(
527                Some(json!({ "method": method })),
528                Some(Cow::Borrowed(
529                    "Valid HTTP methods are: GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS.",
530                )),
531            )),
532        }
533    }
534
535    // ---- Header Errors ----
536
537    /// Create an invalid header name error
538    pub fn invalid_header_name(name: impl Into<String>, reason: impl Into<String>) -> Self {
539        let name = name.into();
540        let reason = reason.into();
541        Self::Internal {
542            kind: ErrorKind::Headers,
543            message: Cow::Owned(format!("Invalid header name '{name}': {reason}")),
544            context: Some(ErrorContext::new(
545                Some(json!({ "header_name": name, "reason": reason })),
546                Some(Cow::Borrowed(
547                    "Header names must contain only valid HTTP header characters.",
548                )),
549            )),
550        }
551    }
552
553    /// Create an invalid header value error
554    pub fn invalid_header_value(name: impl Into<String>, reason: impl Into<String>) -> Self {
555        let name = name.into();
556        let reason = reason.into();
557        Self::Internal {
558            kind: ErrorKind::Headers,
559            message: Cow::Owned(format!("Invalid header value for '{name}': {reason}")),
560            context: Some(ErrorContext::new(
561                Some(json!({ "header_name": name, "reason": reason })),
562                Some(Cow::Borrowed(
563                    "Header values must contain only valid HTTP header characters.",
564                )),
565            )),
566        }
567    }
568
569    /// Create an invalid header format error
570    pub fn invalid_header_format(header: impl Into<String>) -> Self {
571        let header = header.into();
572        Self::Internal {
573            kind: ErrorKind::Headers,
574            message: Cow::Owned(format!(
575                "Invalid header format '{header}'. Expected 'Name: Value'"
576            )),
577            context: Some(ErrorContext::new(
578                Some(json!({ "header": header })),
579                Some(Cow::Borrowed("Headers must be in 'Name: Value' format.")),
580            )),
581        }
582    }
583
584    /// Create an empty header name error
585    #[must_use]
586    pub const fn empty_header_name() -> Self {
587        Self::Internal {
588            kind: ErrorKind::Headers,
589            message: Cow::Borrowed("Header name cannot be empty"),
590            context: Some(ErrorContext::with_suggestion(Cow::Borrowed(
591                "Provide a valid header name before the colon.",
592            ))),
593        }
594    }
595
596    // ---- Interactive Errors ----
597
598    /// Create an interactive input too long error
599    #[must_use]
600    pub fn interactive_input_too_long(max_length: usize) -> Self {
601        Self::Internal {
602            kind: ErrorKind::Interactive,
603            message: Cow::Owned(format!("Input too long (maximum {max_length} characters)")),
604            context: Some(
605                ErrorContext::with_detail("max_length", max_length)
606                    .and_suggestion("Please provide a shorter input."),
607            ),
608        }
609    }
610
611    /// Create an interactive invalid characters error
612    pub fn interactive_invalid_characters(
613        invalid_chars: impl Into<String>,
614        suggestion: impl Into<String>,
615    ) -> Self {
616        let invalid_chars = invalid_chars.into();
617        Self::Internal {
618            kind: ErrorKind::Interactive,
619            message: Cow::Owned(format!("Invalid characters found: {invalid_chars}")),
620            context: Some(ErrorContext::new(
621                Some(json!({ "invalid_characters": invalid_chars })),
622                Some(Cow::Owned(suggestion.into())),
623            )),
624        }
625    }
626
627    /// Create an interactive timeout error
628    #[must_use]
629    pub const fn interactive_timeout() -> Self {
630        Self::Internal {
631            kind: ErrorKind::Interactive,
632            message: Cow::Borrowed("Input timeout - no response received"),
633            context: Some(ErrorContext::with_suggestion(Cow::Borrowed(
634                "Please respond within the timeout period.",
635            ))),
636        }
637    }
638
639    /// Create an interactive retries exhausted error
640    pub fn interactive_retries_exhausted(
641        max_retries: usize,
642        last_error: impl Into<String>,
643        suggestions: &[String],
644    ) -> Self {
645        let last_error = last_error.into();
646        Self::Internal {
647            kind: ErrorKind::Interactive,
648            message: Cow::Owned(format!(
649                "Maximum retry attempts ({max_retries}) exceeded: {last_error}"
650            )),
651            context: Some(ErrorContext::new(
652                Some(
653                    json!({ "max_attempts": max_retries, "last_error": last_error, "suggestions": suggestions }),
654                ),
655                Some(Cow::Owned(format!(
656                    "Suggestions: {}",
657                    suggestions.join("; ")
658                ))),
659            )),
660        }
661    }
662
663    // ---- Server Variable Errors ----
664
665    /// Create a missing server variable error
666    pub fn missing_server_variable(name: impl Into<String>) -> Self {
667        let name = name.into();
668        Self::Internal {
669            kind: ErrorKind::ServerVariable,
670            message: Cow::Owned(format!("Required server variable '{name}' is not provided")),
671            context: Some(
672                ErrorContext::with_detail("variable_name", &name).and_suggestion(format!(
673                    "Provide the variable with --server-var {name}=<value>"
674                )),
675            ),
676        }
677    }
678
679    /// Create an unknown server variable error
680    pub fn unknown_server_variable(name: impl Into<String>, available: &[String]) -> Self {
681        let name = name.into();
682        let available_list = available.join(", ");
683        Self::Internal {
684            kind: ErrorKind::ServerVariable,
685            message: Cow::Owned(format!(
686                "Unknown server variable '{name}'. Available variables: {available_list}"
687            )),
688            context: Some(ErrorContext::new(
689                Some(json!({ "variable_name": name, "available_variables": available })),
690                Some(Cow::Owned(format!("Use one of: {available_list}"))),
691            )),
692        }
693    }
694
695    /// Create an unresolved template variable error
696    pub fn unresolved_template_variable(name: impl Into<String>, url: impl Into<String>) -> Self {
697        let name = name.into();
698        let url = url.into();
699        Self::Internal {
700            kind: ErrorKind::ServerVariable,
701            message: Cow::Owned(format!(
702                "Unresolved template variable '{name}' in URL '{url}'"
703            )),
704            context: Some(ErrorContext::new(
705                Some(json!({ "variable_name": name, "template_url": url })),
706                Some(Cow::Borrowed(
707                    "Ensure all template variables are provided with --server-var",
708                )),
709            )),
710        }
711    }
712
713    /// Create an invalid environment variable name error with suggestion
714    pub fn invalid_environment_variable_name(
715        name: impl Into<String>,
716        reason: impl Into<String>,
717        suggestion: impl Into<String>,
718    ) -> Self {
719        let name = name.into();
720        let reason = reason.into();
721        Self::Internal {
722            kind: ErrorKind::Interactive,
723            message: Cow::Owned(format!(
724                "Invalid environment variable name '{name}': {reason}"
725            )),
726            context: Some(
727                ErrorContext::with_name_reason("variable_name", &name, &reason)
728                    .and_suggestion(suggestion),
729            ),
730        }
731    }
732
733    /// Create an invalid server variable format error
734    pub fn invalid_server_var_format(arg: impl Into<String>, reason: impl Into<String>) -> Self {
735        let arg = arg.into();
736        let reason = reason.into();
737        Self::Internal {
738            kind: ErrorKind::ServerVariable,
739            message: Cow::Owned(format!(
740                "Invalid server variable format in '{arg}': {reason}"
741            )),
742            context: Some(ErrorContext::new(
743                Some(json!({ "argument": arg, "reason": reason })),
744                Some(Cow::Borrowed(
745                    "Server variables must be in 'key=value' format.",
746                )),
747            )),
748        }
749    }
750
751    /// Create an invalid server variable value error
752    pub fn invalid_server_var_value(
753        name: impl Into<String>,
754        value: impl Into<String>,
755        allowed_values: &[String],
756    ) -> Self {
757        let name = name.into();
758        let value = value.into();
759        Self::Internal {
760            kind: ErrorKind::ServerVariable,
761            message: Cow::Owned(format!(
762                "Invalid value '{value}' for server variable '{name}'"
763            )),
764            context: Some(ErrorContext::new(
765                Some(
766                    json!({ "variable_name": name, "provided_value": value, "allowed_values": allowed_values }),
767                ),
768                Some(Cow::Owned(format!(
769                    "Allowed values: {}",
770                    allowed_values.join(", ")
771                ))),
772            )),
773        }
774    }
775
776    // ---- Runtime Errors ----
777
778    /// Create an operation not found error
779    pub fn operation_not_found(operation: impl Into<String>) -> Self {
780        let operation = operation.into();
781        Self::Internal {
782            kind: ErrorKind::Runtime,
783            message: Cow::Owned(format!("Operation '{operation}' not found")),
784            context: Some(ErrorContext::new(
785                Some(json!({ "operation": operation })),
786                Some(Cow::Borrowed(
787                    "Check available operations with --help or --describe-json",
788                )),
789            )),
790        }
791    }
792
793    /// Create an operation not found error with suggestions
794    pub fn operation_not_found_with_suggestions(
795        operation: impl Into<String>,
796        suggestions: &[String],
797    ) -> Self {
798        let operation = operation.into();
799        let suggestion_text = if suggestions.is_empty() {
800            "Check available operations with --help or --describe-json".to_string()
801        } else {
802            format!("Did you mean one of these?\n{}", suggestions.join("\n"))
803        };
804
805        Self::Internal {
806            kind: ErrorKind::Validation,
807            message: Cow::Owned(format!("Operation '{operation}' not found")),
808            context: Some(ErrorContext::new(
809                Some(json!({
810                    "operation": operation,
811                    "suggestions": suggestions
812                })),
813                Some(Cow::Owned(suggestion_text)),
814            )),
815        }
816    }
817
818    /// Create a network request failed error
819    pub fn network_request_failed(reason: impl Into<String>) -> Self {
820        let reason = reason.into();
821        Self::Internal {
822            kind: ErrorKind::Network,
823            message: Cow::Owned(format!("Network request failed: {reason}")),
824            context: Some(
825                ErrorContext::with_detail("reason", &reason)
826                    .and_suggestion("Check network connectivity and URL validity"),
827            ),
828        }
829    }
830
831    /// Create a serialization error
832    pub fn serialization_error(reason: impl Into<String>) -> Self {
833        let reason = reason.into();
834        Self::Internal {
835            kind: ErrorKind::Validation,
836            message: Cow::Owned(format!("Serialization failed: {reason}")),
837            context: Some(
838                ErrorContext::with_detail("reason", &reason)
839                    .and_suggestion("Check data structure validity"),
840            ),
841        }
842    }
843
844    /// Create a home directory not found error
845    #[must_use]
846    pub fn home_directory_not_found() -> Self {
847        Self::Internal {
848            kind: ErrorKind::Runtime,
849            message: Cow::Borrowed("Home directory not found"),
850            context: Some(ErrorContext::new(
851                Some(serde_json::json!({})),
852                Some(Cow::Borrowed("Ensure HOME environment variable is set")),
853            )),
854        }
855    }
856
857    /// Create an invalid command error
858    pub fn invalid_command(context: impl Into<String>, reason: impl Into<String>) -> Self {
859        let context = context.into();
860        let reason = reason.into();
861        Self::Internal {
862            kind: ErrorKind::Validation,
863            message: Cow::Owned(format!("Invalid command for '{context}': {reason}")),
864            context: Some(
865                ErrorContext::with_name_reason("context", &context, &reason)
866                    .and_suggestion("Check available commands with --help or --describe-json"),
867            ),
868        }
869    }
870
871    /// Create an HTTP error with context
872    pub fn http_error_with_context(
873        status: u16,
874        body: impl Into<String>,
875        api_name: impl Into<String>,
876        operation_id: Option<impl Into<String>>,
877        security_schemes: &[String],
878    ) -> Self {
879        let body = body.into();
880        let api_name = api_name.into();
881        let operation_id = operation_id.map(std::convert::Into::into);
882
883        // Include important parts of response body in message for backward compatibility
884        let message = if body.len() <= 200 && !body.is_empty() {
885            format!("HTTP {status} error for '{api_name}': {body}")
886        } else {
887            format!("HTTP {status} error for '{api_name}'")
888        };
889
890        Self::Internal {
891            kind: ErrorKind::HttpRequest,
892            message: Cow::Owned(message),
893            context: Some(ErrorContext::new(
894                Some(json!({
895                    "status": status,
896                    "response_body": body,
897                    "api_name": api_name,
898                    "operation_id": operation_id,
899                    "security_schemes": security_schemes
900                })),
901                Some(Cow::Borrowed(
902                    "Check the API endpoint, parameters, and authentication.",
903                )),
904            )),
905        }
906    }
907
908    /// Create a JQ filter error
909    pub fn jq_filter_error(filter: impl Into<String>, reason: impl Into<String>) -> Self {
910        let filter = filter.into();
911        let reason = reason.into();
912        Self::Internal {
913            kind: ErrorKind::Validation,
914            message: Cow::Owned(format!("JQ filter error in '{filter}': {reason}")),
915            context: Some(
916                ErrorContext::with_name_reason("filter", &filter, &reason)
917                    .and_suggestion("Check JQ filter syntax and data structure compatibility"),
918            ),
919        }
920    }
921
922    /// Create a transient network error
923    pub fn transient_network_error(reason: impl Into<String>, retryable: bool) -> Self {
924        let reason = reason.into();
925        Self::Internal {
926            kind: ErrorKind::Network,
927            message: Cow::Owned(format!("Transient network error: {reason}")),
928            context: Some(ErrorContext::new(
929                Some(serde_json::json!({
930                    "reason": reason,
931                    "retryable": retryable
932                })),
933                Some(Cow::Borrowed(if retryable {
934                    "This error may be temporary and could succeed on retry"
935                } else {
936                    "This error is not retryable"
937                })),
938            )),
939        }
940    }
941
942    /// Create a retry limit exceeded error
943    pub fn retry_limit_exceeded(max_attempts: u32, last_error: impl Into<String>) -> Self {
944        let last_error = last_error.into();
945        Self::Internal {
946            kind: ErrorKind::Network,
947            message: Cow::Owned(format!(
948                "Retry limit exceeded after {max_attempts} attempts: {last_error}"
949            )),
950            context: Some(ErrorContext::new(
951                Some(serde_json::json!({
952                    "max_attempts": max_attempts,
953                    "last_error": last_error
954                })),
955                Some(Cow::Borrowed(
956                    "Consider checking network connectivity or increasing retry limits",
957                )),
958            )),
959        }
960    }
961
962    /// Create a request timeout error
963    #[must_use]
964    pub fn request_timeout(timeout_seconds: u64) -> Self {
965        Self::Internal {
966            kind: ErrorKind::Network,
967            message: Cow::Owned(format!("Request timed out after {timeout_seconds} seconds")),
968            context: Some(ErrorContext::new(
969                Some(serde_json::json!({
970                    "timeout_seconds": timeout_seconds
971                })),
972                Some(Cow::Borrowed(
973                    "Consider increasing the timeout or checking network connectivity",
974                )),
975            )),
976        }
977    }
978
979    /// Create a missing path parameter error
980    pub fn missing_path_parameter(name: impl Into<String>) -> Self {
981        let name = name.into();
982        Self::Internal {
983            kind: ErrorKind::Validation,
984            message: Cow::Owned(format!("Missing required path parameter: {name}")),
985            context: Some(
986                ErrorContext::with_detail("parameter_name", &name)
987                    .and_suggestion("Provide a value for this required path parameter"),
988            ),
989        }
990    }
991
992    /// Create a general I/O error
993    pub fn io_error(message: impl Into<String>) -> Self {
994        let message = message.into();
995        Self::Internal {
996            kind: ErrorKind::Runtime,
997            message: Cow::Owned(message),
998            context: None,
999        }
1000    }
1001
1002    /// Create an invalid idempotency key error
1003    #[must_use]
1004    pub const fn invalid_idempotency_key() -> Self {
1005        Self::Internal {
1006            kind: ErrorKind::Headers,
1007            message: Cow::Borrowed("Invalid idempotency key format"),
1008            context: Some(ErrorContext::new(
1009                None,
1010                Some(Cow::Borrowed(
1011                    "Ensure the idempotency key contains only valid header characters",
1012                )),
1013            )),
1014        }
1015    }
1016
1017    /// Create an editor not set error
1018    #[must_use]
1019    pub const fn editor_not_set() -> Self {
1020        Self::Internal {
1021            kind: ErrorKind::Interactive,
1022            message: Cow::Borrowed("EDITOR environment variable not set"),
1023            context: Some(ErrorContext::new(
1024                None,
1025                Some(Cow::Borrowed(
1026                    "Set your preferred editor: export EDITOR=vim",
1027                )),
1028            )),
1029        }
1030    }
1031
1032    /// Create an editor failed error
1033    pub fn editor_failed(name: impl Into<String>) -> Self {
1034        let name = name.into();
1035        Self::Internal {
1036            kind: ErrorKind::Interactive,
1037            message: Cow::Owned(format!("Editor '{name}' failed to complete")),
1038            context: Some(ErrorContext::new(
1039                Some(serde_json::json!({ "editor": name })),
1040                Some(Cow::Borrowed(
1041                    "Check if the editor is properly installed and configured",
1042                )),
1043            )),
1044        }
1045    }
1046}