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