1use 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 #[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 #[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#[derive(Debug, Clone, PartialEq, Eq)]
49pub enum ErrorKind {
50 Specification,
52 Authentication,
54 Validation,
56 Network,
58 HttpRequest,
60 Headers,
62 Interactive,
64 ServerVariable,
66 Runtime,
68}
69
70#[derive(Debug, Clone)]
72pub struct ErrorContext {
73 pub details: Option<serde_json::Value>,
75 pub suggestion: Option<Cow<'static, str>>,
77}
78
79impl ErrorContext {
80 #[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 #[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 #[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 #[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 #[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 #[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 #[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#[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 #[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 #[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 #[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 #[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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 #[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 #[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 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 #[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 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 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 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 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 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 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 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 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 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 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 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 #[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 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 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 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 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 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 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 #[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 #[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 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 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 #[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 #[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 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 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 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 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}