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