Skip to main content

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 = match () {
260                    () if req_err.is_connect() => Some(Cow::Borrowed(constants::ERR_CONNECTION)),
261                    () if req_err.is_timeout() => Some(Cow::Borrowed(constants::ERR_TIMEOUT)),
262                    () if req_err.is_status() => {
263                        req_err.status().and_then(|status| match status.as_u16() {
264                            401 => Some(Cow::Borrowed(constants::ERR_API_CREDENTIALS)),
265                            403 => Some(Cow::Borrowed(constants::ERR_PERMISSION_DENIED)),
266                            404 => Some(Cow::Borrowed(constants::ERR_ENDPOINT_NOT_FOUND)),
267                            429 => Some(Cow::Borrowed(constants::ERR_RATE_LIMITED)),
268                            500..=599 => Some(Cow::Borrowed(constants::ERR_SERVER_ERROR)),
269                            _ => None,
270                        })
271                    }
272                    () => None,
273                };
274                ("Network", req_err.to_string(), context, None)
275            }
276            Self::Yaml(yaml_err) => (
277                "YAMLParsing",
278                yaml_err.to_string(),
279                Some(Cow::Borrowed(constants::ERR_YAML_SYNTAX)),
280                None,
281            ),
282            Self::Json(json_err) => (
283                "JSONParsing",
284                json_err.to_string(),
285                Some(Cow::Borrowed(constants::ERR_JSON_SYNTAX)),
286                None,
287            ),
288            Self::Toml(toml_err) => (
289                "TOMLParsing",
290                toml_err.to_string(),
291                Some(Cow::Borrowed(constants::ERR_TOML_SYNTAX)),
292                None,
293            ),
294            Self::Internal {
295                kind,
296                message,
297                context: ctx,
298            } => {
299                let context = ctx.as_ref().and_then(|c| c.suggestion.clone());
300                let details = ctx.as_ref().and_then(|c| c.details.clone());
301                (kind.as_str(), message.to_string(), context, details)
302            }
303            Self::Anyhow(anyhow_err) => ("Unknown", anyhow_err.to_string(), None, None),
304        };
305
306        JsonError {
307            error_type: Cow::Borrowed(error_type),
308            message,
309            context,
310            details,
311        }
312    }
313}
314
315impl Error {
316    /// Create a specification not found error
317    pub fn spec_not_found(name: impl Into<String>) -> Self {
318        let name = name.into();
319        Self::Internal {
320            kind: ErrorKind::Specification,
321            message: Cow::Owned(format!("API specification '{name}' not found")),
322            context: Some(
323                ErrorContext::with_detail("spec_name", &name)
324                    .and_suggestion(constants::MSG_USE_CONFIG_LIST),
325            ),
326        }
327    }
328
329    /// Create a specification already exists error
330    pub fn spec_already_exists(name: impl Into<String>) -> Self {
331        let name = name.into();
332        Self::Internal {
333            kind: ErrorKind::Specification,
334            message: Cow::Owned(format!(
335                "API specification '{name}' already exists. Use --force to overwrite"
336            )),
337            context: Some(ErrorContext::with_detail("spec_name", &name)),
338        }
339    }
340
341    /// Create a cached spec not found error
342    pub fn cached_spec_not_found(name: impl Into<String>) -> Self {
343        let name = name.into();
344        Self::Internal {
345            kind: ErrorKind::Specification,
346            message: Cow::Owned(format!(
347                "No cached spec found for '{name}'. Run 'aperture config add {name}' first"
348            )),
349            context: Some(ErrorContext::with_detail("spec_name", &name)),
350        }
351    }
352
353    /// Create a cached spec corrupted error
354    pub fn cached_spec_corrupted(name: impl Into<String>, reason: impl Into<String>) -> Self {
355        let name = name.into();
356        let reason = reason.into();
357        Self::Internal {
358            kind: ErrorKind::Specification,
359            message: Cow::Owned(format!(
360                "Failed to deserialize cached spec '{name}': {reason}. The cache may be corrupted"
361            )),
362            context: Some(ErrorContext::new(
363                Some(json!({ "spec_name": name, "corruption_reason": reason })),
364                Some(Cow::Borrowed(
365                    "Try removing and re-adding the specification.",
366                )),
367            )),
368        }
369    }
370
371    /// Create a cache version mismatch error
372    pub fn cache_version_mismatch(name: impl Into<String>, found: u32, expected: u32) -> Self {
373        let name = name.into();
374        Self::Internal {
375            kind: ErrorKind::Specification,
376            message: Cow::Owned(format!(
377                "Cache format version mismatch for '{name}': found v{found}, expected v{expected}"
378            )),
379            context: Some(ErrorContext::new(
380                Some(
381                    json!({ "spec_name": name, "found_version": found, "expected_version": expected }),
382                ),
383                Some(Cow::Borrowed(
384                    "Run 'aperture config reinit' to regenerate the cache.",
385                )),
386            )),
387        }
388    }
389
390    /// Create a secret not set error
391    pub fn secret_not_set(scheme_name: impl Into<String>, env_var: impl Into<String>) -> Self {
392        let scheme_name = scheme_name.into();
393        let env_var = env_var.into();
394        let suggestion = crate::suggestions::suggest_auth_fix(&scheme_name, Some(&env_var));
395        Self::Internal {
396            kind: ErrorKind::Authentication,
397            message: Cow::Owned(format!(
398                "Environment variable '{env_var}' required for authentication '{scheme_name}' is not set"
399            )),
400            context: Some(ErrorContext::new(
401                Some(json!({ "scheme_name": scheme_name, "env_var": env_var })),
402                Some(Cow::Owned(suggestion)),
403            )),
404        }
405    }
406
407    /// Create an unsupported auth scheme error
408    pub fn unsupported_auth_scheme(scheme: impl Into<String>) -> Self {
409        let scheme = scheme.into();
410        Self::Internal {
411            kind: ErrorKind::Authentication,
412            message: Cow::Owned(format!("Unsupported HTTP authentication scheme: {scheme}")),
413            context: Some(ErrorContext::new(
414                Some(json!({ "scheme": scheme })),
415                Some(Cow::Borrowed(
416                    "Only 'bearer' and 'basic' schemes are supported.",
417                )),
418            )),
419        }
420    }
421
422    /// Create an unsupported security scheme error
423    pub fn unsupported_security_scheme(scheme_type: impl Into<String>) -> Self {
424        let scheme_type = scheme_type.into();
425        Self::Internal {
426            kind: ErrorKind::Authentication,
427            message: Cow::Owned(format!("Unsupported security scheme type: {scheme_type}")),
428            context: Some(ErrorContext::new(
429                Some(json!({ "scheme_type": scheme_type })),
430                Some(Cow::Borrowed(
431                    "Only 'apiKey' and 'http' security schemes are supported.",
432                )),
433            )),
434        }
435    }
436
437    /// Create a generic validation error
438    pub fn validation_error(message: impl Into<String>) -> Self {
439        let message = message.into();
440        Self::Internal {
441            kind: ErrorKind::Validation,
442            message: Cow::Owned(format!("Validation error: {message}")),
443            context: None,
444        }
445    }
446
447    /// Create an invalid configuration error
448    pub fn invalid_config(reason: impl Into<String>) -> Self {
449        let reason = reason.into();
450        Self::Internal {
451            kind: ErrorKind::Validation,
452            message: Cow::Owned(format!("Invalid configuration: {reason}")),
453            context: Some(
454                ErrorContext::with_detail("reason", &reason)
455                    .and_suggestion("Check the configuration file syntax and structure."),
456            ),
457        }
458    }
459
460    /// Create an invalid JSON body error
461    pub fn invalid_json_body(reason: impl Into<String>) -> Self {
462        let reason = reason.into();
463        Self::Internal {
464            kind: ErrorKind::Validation,
465            message: Cow::Owned(format!("Invalid JSON body: {reason}")),
466            context: Some(ErrorContext::new(
467                Some(json!({ "reason": reason })),
468                Some(Cow::Borrowed(
469                    "Check that the JSON body is properly formatted.",
470                )),
471            )),
472        }
473    }
474
475    /// Create an invalid path error
476    pub fn invalid_path(path: impl Into<String>, reason: impl Into<String>) -> Self {
477        let path = path.into();
478        let reason = reason.into();
479        Self::Internal {
480            kind: ErrorKind::Validation,
481            message: Cow::Owned(format!("Invalid path '{path}': {reason}")),
482            context: Some(ErrorContext::new(
483                Some(json!({ "path": path, "reason": reason })),
484                Some(Cow::Borrowed("Check the path format and ensure it exists.")),
485            )),
486        }
487    }
488
489    /// Create a request failed error
490    pub fn request_failed(status: reqwest::StatusCode, reason: impl Into<String>) -> Self {
491        let reason = reason.into();
492        Self::Internal {
493            kind: ErrorKind::HttpRequest,
494            message: Cow::Owned(format!("Request failed with status {status}: {reason}")),
495            context: Some(ErrorContext::new(
496                Some(json!({ "status_code": status.as_u16(), "reason": reason })),
497                Some(Cow::Borrowed(
498                    "Check the API endpoint, parameters, and authentication.",
499                )),
500            )),
501        }
502    }
503
504    /// Create a response read error
505    pub fn response_read_error(reason: impl Into<String>) -> Self {
506        let reason = reason.into();
507        Self::Internal {
508            kind: ErrorKind::HttpRequest,
509            message: Cow::Owned(format!("Failed to read response: {reason}")),
510            context: Some(ErrorContext::new(
511                Some(json!({ "reason": reason })),
512                Some(Cow::Borrowed(
513                    "Check network connectivity and server status.",
514                )),
515            )),
516        }
517    }
518
519    /// Create an invalid HTTP method error
520    pub fn invalid_http_method(method: impl Into<String>) -> Self {
521        let method = method.into();
522        Self::Internal {
523            kind: ErrorKind::HttpRequest,
524            message: Cow::Owned(format!("Invalid HTTP method: {method}")),
525            context: Some(ErrorContext::new(
526                Some(json!({ "method": method })),
527                Some(Cow::Borrowed(
528                    "Valid HTTP methods are: GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS.",
529                )),
530            )),
531        }
532    }
533
534    // ---- Header Errors ----
535
536    /// Create an invalid header name error
537    pub fn invalid_header_name(name: impl Into<String>, reason: impl Into<String>) -> Self {
538        let name = name.into();
539        let reason = reason.into();
540        Self::Internal {
541            kind: ErrorKind::Headers,
542            message: Cow::Owned(format!("Invalid header name '{name}': {reason}")),
543            context: Some(ErrorContext::new(
544                Some(json!({ "header_name": name, "reason": reason })),
545                Some(Cow::Borrowed(
546                    "Header names must contain only valid HTTP header characters.",
547                )),
548            )),
549        }
550    }
551
552    /// Create an invalid header value error
553    pub fn invalid_header_value(name: impl Into<String>, reason: impl Into<String>) -> Self {
554        let name = name.into();
555        let reason = reason.into();
556        Self::Internal {
557            kind: ErrorKind::Headers,
558            message: Cow::Owned(format!("Invalid header value for '{name}': {reason}")),
559            context: Some(ErrorContext::new(
560                Some(json!({ "header_name": name, "reason": reason })),
561                Some(Cow::Borrowed(
562                    "Header values must contain only valid HTTP header characters.",
563                )),
564            )),
565        }
566    }
567
568    /// Create an invalid header format error
569    pub fn invalid_header_format(header: impl Into<String>) -> Self {
570        let header = header.into();
571        Self::Internal {
572            kind: ErrorKind::Headers,
573            message: Cow::Owned(format!(
574                "Invalid header format '{header}'. Expected 'Name: Value'"
575            )),
576            context: Some(ErrorContext::new(
577                Some(json!({ "header": header })),
578                Some(Cow::Borrowed("Headers must be in 'Name: Value' format.")),
579            )),
580        }
581    }
582
583    /// Create an empty header name error
584    #[must_use]
585    pub const fn empty_header_name() -> Self {
586        Self::Internal {
587            kind: ErrorKind::Headers,
588            message: Cow::Borrowed("Header name cannot be empty"),
589            context: Some(ErrorContext::with_suggestion(Cow::Borrowed(
590                "Provide a valid header name before the colon.",
591            ))),
592        }
593    }
594
595    // ---- Interactive Errors ----
596
597    /// Create an interactive input too long error
598    #[must_use]
599    pub fn interactive_input_too_long(max_length: usize) -> Self {
600        Self::Internal {
601            kind: ErrorKind::Interactive,
602            message: Cow::Owned(format!("Input too long (maximum {max_length} characters)")),
603            context: Some(
604                ErrorContext::with_detail("max_length", max_length)
605                    .and_suggestion("Please provide a shorter input."),
606            ),
607        }
608    }
609
610    /// Create an interactive invalid characters error
611    pub fn interactive_invalid_characters(
612        invalid_chars: impl Into<String>,
613        suggestion: impl Into<String>,
614    ) -> Self {
615        let invalid_chars = invalid_chars.into();
616        Self::Internal {
617            kind: ErrorKind::Interactive,
618            message: Cow::Owned(format!("Invalid characters found: {invalid_chars}")),
619            context: Some(ErrorContext::new(
620                Some(json!({ "invalid_characters": invalid_chars })),
621                Some(Cow::Owned(suggestion.into())),
622            )),
623        }
624    }
625
626    /// Create an interactive timeout error
627    #[must_use]
628    pub const fn interactive_timeout() -> Self {
629        Self::Internal {
630            kind: ErrorKind::Interactive,
631            message: Cow::Borrowed("Input timeout - no response received"),
632            context: Some(ErrorContext::with_suggestion(Cow::Borrowed(
633                "Please respond within the timeout period.",
634            ))),
635        }
636    }
637
638    /// Create an interactive retries exhausted error
639    pub fn interactive_retries_exhausted(
640        max_retries: usize,
641        last_error: impl Into<String>,
642        suggestions: &[String],
643    ) -> Self {
644        let last_error = last_error.into();
645        Self::Internal {
646            kind: ErrorKind::Interactive,
647            message: Cow::Owned(format!(
648                "Maximum retry attempts ({max_retries}) exceeded: {last_error}"
649            )),
650            context: Some(ErrorContext::new(
651                Some(
652                    json!({ "max_attempts": max_retries, "last_error": last_error, "suggestions": suggestions }),
653                ),
654                Some(Cow::Owned(format!(
655                    "Suggestions: {}",
656                    suggestions.join("; ")
657                ))),
658            )),
659        }
660    }
661
662    // ---- Server Variable Errors ----
663
664    /// Create a missing server variable error
665    pub fn missing_server_variable(name: impl Into<String>) -> Self {
666        let name = name.into();
667        Self::Internal {
668            kind: ErrorKind::ServerVariable,
669            message: Cow::Owned(format!("Required server variable '{name}' is not provided")),
670            context: Some(
671                ErrorContext::with_detail("variable_name", &name).and_suggestion(format!(
672                    "Provide the variable with --server-var {name}=<value>"
673                )),
674            ),
675        }
676    }
677
678    /// Create an unknown server variable error
679    pub fn unknown_server_variable(name: impl Into<String>, available: &[String]) -> Self {
680        let name = name.into();
681        let available_list = available.join(", ");
682        Self::Internal {
683            kind: ErrorKind::ServerVariable,
684            message: Cow::Owned(format!(
685                "Unknown server variable '{name}'. Available variables: {available_list}"
686            )),
687            context: Some(ErrorContext::new(
688                Some(json!({ "variable_name": name, "available_variables": available })),
689                Some(Cow::Owned(format!("Use one of: {available_list}"))),
690            )),
691        }
692    }
693
694    /// Create an unresolved template variable error
695    pub fn unresolved_template_variable(name: impl Into<String>, url: impl Into<String>) -> Self {
696        let name = name.into();
697        let url = url.into();
698        Self::Internal {
699            kind: ErrorKind::ServerVariable,
700            message: Cow::Owned(format!(
701                "Unresolved template variable '{name}' in URL '{url}'"
702            )),
703            context: Some(ErrorContext::new(
704                Some(json!({ "variable_name": name, "template_url": url })),
705                Some(Cow::Borrowed(
706                    "Ensure all template variables are provided with --server-var",
707                )),
708            )),
709        }
710    }
711
712    /// Create an invalid environment variable name error with suggestion
713    pub fn invalid_environment_variable_name(
714        name: impl Into<String>,
715        reason: impl Into<String>,
716        suggestion: impl Into<String>,
717    ) -> Self {
718        let name = name.into();
719        let reason = reason.into();
720        Self::Internal {
721            kind: ErrorKind::Interactive,
722            message: Cow::Owned(format!(
723                "Invalid environment variable name '{name}': {reason}"
724            )),
725            context: Some(
726                ErrorContext::with_name_reason("variable_name", &name, &reason)
727                    .and_suggestion(suggestion),
728            ),
729        }
730    }
731
732    /// Create an invalid server variable format error
733    pub fn invalid_server_var_format(arg: impl Into<String>, reason: impl Into<String>) -> Self {
734        let arg = arg.into();
735        let reason = reason.into();
736        Self::Internal {
737            kind: ErrorKind::ServerVariable,
738            message: Cow::Owned(format!(
739                "Invalid server variable format in '{arg}': {reason}"
740            )),
741            context: Some(ErrorContext::new(
742                Some(json!({ "argument": arg, "reason": reason })),
743                Some(Cow::Borrowed(
744                    "Server variables must be in 'key=value' format.",
745                )),
746            )),
747        }
748    }
749
750    /// Create an invalid server variable value error
751    pub fn invalid_server_var_value(
752        name: impl Into<String>,
753        value: impl Into<String>,
754        allowed_values: &[String],
755    ) -> Self {
756        let name = name.into();
757        let value = value.into();
758        Self::Internal {
759            kind: ErrorKind::ServerVariable,
760            message: Cow::Owned(format!(
761                "Invalid value '{value}' for server variable '{name}'"
762            )),
763            context: Some(ErrorContext::new(
764                Some(
765                    json!({ "variable_name": name, "provided_value": value, "allowed_values": allowed_values }),
766                ),
767                Some(Cow::Owned(format!(
768                    "Allowed values: {}",
769                    allowed_values.join(", ")
770                ))),
771            )),
772        }
773    }
774
775    // ---- Runtime Errors ----
776
777    /// Create an operation not found error
778    pub fn operation_not_found(operation: impl Into<String>) -> Self {
779        let operation = operation.into();
780        Self::Internal {
781            kind: ErrorKind::Runtime,
782            message: Cow::Owned(format!("Operation '{operation}' not found")),
783            context: Some(ErrorContext::new(
784                Some(json!({ "operation": operation })),
785                Some(Cow::Borrowed(
786                    "Check available operations with --help or --describe-json",
787                )),
788            )),
789        }
790    }
791
792    /// Create an operation not found error with suggestions
793    pub fn operation_not_found_with_suggestions(
794        operation: impl Into<String>,
795        suggestions: &[String],
796    ) -> Self {
797        let operation = operation.into();
798        let suggestion_text = if suggestions.is_empty() {
799            "Check available operations with --help or --describe-json".to_string()
800        } else {
801            format!("Did you mean one of these?\n{}", suggestions.join("\n"))
802        };
803
804        Self::Internal {
805            kind: ErrorKind::Validation,
806            message: Cow::Owned(format!("Operation '{operation}' not found")),
807            context: Some(ErrorContext::new(
808                Some(json!({
809                    "operation": operation,
810                    "suggestions": suggestions
811                })),
812                Some(Cow::Owned(suggestion_text)),
813            )),
814        }
815    }
816
817    /// Create a network request failed error
818    pub fn network_request_failed(reason: impl Into<String>) -> Self {
819        let reason = reason.into();
820        Self::Internal {
821            kind: ErrorKind::Network,
822            message: Cow::Owned(format!("Network request failed: {reason}")),
823            context: Some(
824                ErrorContext::with_detail("reason", &reason)
825                    .and_suggestion("Check network connectivity and URL validity"),
826            ),
827        }
828    }
829
830    /// Create a serialization error
831    pub fn serialization_error(reason: impl Into<String>) -> Self {
832        let reason = reason.into();
833        Self::Internal {
834            kind: ErrorKind::Validation,
835            message: Cow::Owned(format!("Serialization failed: {reason}")),
836            context: Some(
837                ErrorContext::with_detail("reason", &reason)
838                    .and_suggestion("Check data structure validity"),
839            ),
840        }
841    }
842
843    /// Create a home directory not found error
844    #[must_use]
845    pub fn home_directory_not_found() -> Self {
846        Self::Internal {
847            kind: ErrorKind::Runtime,
848            message: Cow::Borrowed("Home directory not found"),
849            context: Some(ErrorContext::new(
850                Some(serde_json::json!({})),
851                Some(Cow::Borrowed("Ensure HOME environment variable is set")),
852            )),
853        }
854    }
855
856    /// Create an invalid command error
857    pub fn invalid_command(context: impl Into<String>, reason: impl Into<String>) -> Self {
858        let context = context.into();
859        let reason = reason.into();
860        Self::Internal {
861            kind: ErrorKind::Validation,
862            message: Cow::Owned(format!("Invalid command for '{context}': {reason}")),
863            context: Some(
864                ErrorContext::with_name_reason("context", &context, &reason)
865                    .and_suggestion("Check available commands with --help or --describe-json"),
866            ),
867        }
868    }
869
870    /// Create an HTTP error with context
871    pub fn http_error_with_context(
872        status: u16,
873        body: impl Into<String>,
874        api_name: impl Into<String>,
875        operation_id: Option<impl Into<String>>,
876        security_schemes: &[String],
877    ) -> Self {
878        let body = body.into();
879        let api_name = api_name.into();
880        let operation_id = operation_id.map(std::convert::Into::into);
881
882        // Include important parts of response body in message for backward compatibility
883        let message = if body.len() <= 200 && !body.is_empty() {
884            format!("HTTP {status} error for '{api_name}': {body}")
885        } else {
886            format!("HTTP {status} error for '{api_name}'")
887        };
888
889        Self::Internal {
890            kind: ErrorKind::HttpRequest,
891            message: Cow::Owned(message),
892            context: Some(ErrorContext::new(
893                Some(json!({
894                    "status": status,
895                    "response_body": body,
896                    "api_name": api_name,
897                    "operation_id": operation_id,
898                    "security_schemes": security_schemes
899                })),
900                Some(Cow::Borrowed(
901                    "Check the API endpoint, parameters, and authentication.",
902                )),
903            )),
904        }
905    }
906
907    /// Create a JQ filter error
908    pub fn jq_filter_error(filter: impl Into<String>, reason: impl Into<String>) -> Self {
909        let filter = filter.into();
910        let reason = reason.into();
911        Self::Internal {
912            kind: ErrorKind::Validation,
913            message: Cow::Owned(format!("JQ filter error in '{filter}': {reason}")),
914            context: Some(
915                ErrorContext::with_name_reason("filter", &filter, &reason)
916                    .and_suggestion("Check JQ filter syntax and data structure compatibility"),
917            ),
918        }
919    }
920
921    /// Create a transient network error
922    pub fn transient_network_error(reason: impl Into<String>, retryable: bool) -> Self {
923        let reason = reason.into();
924        Self::Internal {
925            kind: ErrorKind::Network,
926            message: Cow::Owned(format!("Transient network error: {reason}")),
927            context: Some(ErrorContext::new(
928                Some(serde_json::json!({
929                    "reason": reason,
930                    "retryable": retryable
931                })),
932                Some(Cow::Borrowed(if retryable {
933                    "This error may be temporary and could succeed on retry"
934                } else {
935                    "This error is not retryable"
936                })),
937            )),
938        }
939    }
940
941    /// Create a retry limit exceeded error
942    pub fn retry_limit_exceeded(max_attempts: u32, last_error: impl Into<String>) -> Self {
943        let last_error = last_error.into();
944        Self::Internal {
945            kind: ErrorKind::Network,
946            message: Cow::Owned(format!(
947                "Retry limit exceeded after {max_attempts} attempts: {last_error}"
948            )),
949            context: Some(ErrorContext::new(
950                Some(serde_json::json!({
951                    "max_attempts": max_attempts,
952                    "last_error": last_error
953                })),
954                Some(Cow::Borrowed(
955                    "Consider checking network connectivity or increasing retry limits",
956                )),
957            )),
958        }
959    }
960
961    /// Create a retry limit exceeded error with detailed retry information
962    #[allow(clippy::too_many_arguments)]
963    pub fn retry_limit_exceeded_detailed(
964        max_attempts: u32,
965        attempts_made: u32,
966        last_error: impl Into<String>,
967        initial_delay_ms: u64,
968        max_delay_ms: u64,
969        last_status_code: Option<u16>,
970        operation_id: impl Into<String>,
971    ) -> Self {
972        let last_error = last_error.into();
973        let operation_id = operation_id.into();
974        Self::Internal {
975            kind: ErrorKind::Network,
976            message: Cow::Owned(format!(
977                "Retry limit exceeded after {attempts_made}/{max_attempts} attempts for {operation_id}: {last_error}"
978            )),
979            context: Some(ErrorContext::new(
980                Some(serde_json::json!({
981                    "retry_info": {
982                        "max_attempts": max_attempts,
983                        "attempts_made": attempts_made,
984                        "initial_delay_ms": initial_delay_ms,
985                        "max_delay_ms": max_delay_ms,
986                        "last_status_code": last_status_code,
987                        "operation_id": operation_id
988                    },
989                    "last_error": last_error
990                })),
991                Some(Cow::Borrowed(
992                    "Consider checking network connectivity, API availability, or increasing retry limits",
993                )),
994            )),
995        }
996    }
997
998    /// Create a request timeout error
999    #[must_use]
1000    pub fn request_timeout(timeout_seconds: u64) -> Self {
1001        Self::Internal {
1002            kind: ErrorKind::Network,
1003            message: Cow::Owned(format!("Request timed out after {timeout_seconds} seconds")),
1004            context: Some(ErrorContext::new(
1005                Some(serde_json::json!({
1006                    "timeout_seconds": timeout_seconds
1007                })),
1008                Some(Cow::Borrowed(
1009                    "Consider increasing the timeout or checking network connectivity",
1010                )),
1011            )),
1012        }
1013    }
1014
1015    /// Create a missing path parameter error
1016    pub fn missing_path_parameter(name: impl Into<String>) -> Self {
1017        let name = name.into();
1018        Self::Internal {
1019            kind: ErrorKind::Validation,
1020            message: Cow::Owned(format!("Missing required path parameter: {name}")),
1021            context: Some(
1022                ErrorContext::with_detail("parameter_name", &name)
1023                    .and_suggestion("Provide a value for this required path parameter"),
1024            ),
1025        }
1026    }
1027
1028    /// Create a general I/O error
1029    pub fn io_error(message: impl Into<String>) -> Self {
1030        let message = message.into();
1031        Self::Internal {
1032            kind: ErrorKind::Runtime,
1033            message: Cow::Owned(message),
1034            context: None,
1035        }
1036    }
1037
1038    /// Create an invalid idempotency key error
1039    #[must_use]
1040    pub const fn invalid_idempotency_key() -> Self {
1041        Self::Internal {
1042            kind: ErrorKind::Headers,
1043            message: Cow::Borrowed("Invalid idempotency key format"),
1044            context: Some(ErrorContext::new(
1045                None,
1046                Some(Cow::Borrowed(
1047                    "Ensure the idempotency key contains only valid header characters",
1048                )),
1049            )),
1050        }
1051    }
1052
1053    /// Create an editor not set error
1054    #[must_use]
1055    pub const fn editor_not_set() -> Self {
1056        Self::Internal {
1057            kind: ErrorKind::Interactive,
1058            message: Cow::Borrowed("EDITOR environment variable not set"),
1059            context: Some(ErrorContext::new(
1060                None,
1061                Some(Cow::Borrowed(
1062                    "Set your preferred editor: export EDITOR=vim",
1063                )),
1064            )),
1065        }
1066    }
1067
1068    /// Create an editor failed error
1069    pub fn editor_failed(name: impl Into<String>) -> Self {
1070        let name = name.into();
1071        Self::Internal {
1072            kind: ErrorKind::Interactive,
1073            message: Cow::Owned(format!("Editor '{name}' failed to complete")),
1074            context: Some(ErrorContext::new(
1075                Some(serde_json::json!({ "editor": name })),
1076                Some(Cow::Borrowed(
1077                    "Check if the editor is properly installed and configured",
1078                )),
1079            )),
1080        }
1081    }
1082
1083    // ---- Settings Errors ----
1084
1085    /// Create an unknown setting key error
1086    pub fn unknown_setting_key(key: impl Into<String>) -> Self {
1087        let key = key.into();
1088        Self::Internal {
1089            kind: ErrorKind::Validation,
1090            message: Cow::Owned(format!("Unknown setting key: '{key}'")),
1091            context: Some(ErrorContext::new(
1092                Some(json!({ "key": key })),
1093                Some(Cow::Borrowed(
1094                    "Run 'aperture config settings' to see available settings.",
1095                )),
1096            )),
1097        }
1098    }
1099
1100    /// Create an invalid setting value error
1101    pub fn invalid_setting_value(
1102        key: crate::config::settings::SettingKey,
1103        value: impl Into<String>,
1104    ) -> Self {
1105        let value = value.into();
1106        let expected_type = key.type_name();
1107        Self::Internal {
1108            kind: ErrorKind::Validation,
1109            message: Cow::Owned(format!(
1110                "Invalid value for '{key}': expected {expected_type}, got '{value}'"
1111            )),
1112            context: Some(ErrorContext::new(
1113                Some(json!({
1114                    "key": key.as_str(),
1115                    "value": value,
1116                    "expected_type": expected_type
1117                })),
1118                Some(Cow::Owned(format!(
1119                    "Provide a valid {expected_type} value for this setting."
1120                ))),
1121            )),
1122        }
1123    }
1124
1125    /// Create a setting value out of range error
1126    pub fn setting_value_out_of_range(
1127        key: crate::config::settings::SettingKey,
1128        value: impl Into<String>,
1129        reason: &str,
1130    ) -> Self {
1131        let value = value.into();
1132        Self::Internal {
1133            kind: ErrorKind::Validation,
1134            message: Cow::Owned(format!(
1135                "Value '{value}' out of range for '{key}': {reason}"
1136            )),
1137            context: Some(ErrorContext::new(
1138                Some(json!({
1139                    "key": key.as_str(),
1140                    "value": value,
1141                    "reason": reason
1142                })),
1143                Some(Cow::Owned(format!(
1144                    "Provide a value within the valid range: {reason}"
1145                ))),
1146            )),
1147        }
1148    }
1149}