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