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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 #[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 #[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 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 #[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 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 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 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 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 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 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 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 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 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 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 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 #[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 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 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 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 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 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 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 #[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 #[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 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 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 #[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 #[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 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 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 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 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 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
1185 #[must_use]
1189 pub fn batch_cycle_detected(cycle: &[String]) -> Self {
1190 let cycle_str = cycle.join(" → ");
1191 Self::Internal {
1192 kind: ErrorKind::Validation,
1193 message: Cow::Owned(format!(
1194 "Dependency cycle detected in batch operations: {cycle_str}"
1195 )),
1196 context: Some(ErrorContext::new(
1197 Some(json!({ "cycle": cycle })),
1198 Some(Cow::Borrowed(
1199 "Remove circular dependencies between batch operations.",
1200 )),
1201 )),
1202 }
1203 }
1204
1205 pub fn batch_missing_dependency(
1207 operation_id: impl Into<String>,
1208 missing_dep: impl Into<String>,
1209 ) -> Self {
1210 let operation_id = operation_id.into();
1211 let missing_dep = missing_dep.into();
1212 Self::Internal {
1213 kind: ErrorKind::Validation,
1214 message: Cow::Owned(format!(
1215 "Operation '{operation_id}' depends on '{missing_dep}' which does not exist"
1216 )),
1217 context: Some(ErrorContext::new(
1218 Some(json!({ "operation_id": operation_id, "missing_dependency": missing_dep })),
1219 Some(Cow::Borrowed(
1220 "Check that the depends_on references match existing operation ids.",
1221 )),
1222 )),
1223 }
1224 }
1225
1226 pub fn batch_undefined_variable(
1228 operation_id: impl Into<String>,
1229 variable: impl Into<String>,
1230 ) -> Self {
1231 let operation_id = operation_id.into();
1232 let variable = variable.into();
1233 Self::Internal {
1234 kind: ErrorKind::Validation,
1235 message: Cow::Owned(format!(
1236 "Operation '{operation_id}' references undefined variable '{{{{{variable}}}}}'"
1237 )),
1238 context: Some(ErrorContext::new(
1239 Some(json!({ "operation_id": operation_id, "variable": variable })),
1240 Some(Cow::Borrowed(
1241 "Ensure the variable is captured by a preceding operation.",
1242 )),
1243 )),
1244 }
1245 }
1246
1247 pub fn batch_capture_failed(
1249 operation_id: impl Into<String>,
1250 variable: impl Into<String>,
1251 reason: impl Into<String>,
1252 ) -> Self {
1253 let operation_id = operation_id.into();
1254 let variable = variable.into();
1255 let reason = reason.into();
1256 Self::Internal {
1257 kind: ErrorKind::Validation,
1258 message: Cow::Owned(format!(
1259 "Failed to capture variable '{variable}' from operation '{operation_id}': {reason}"
1260 )),
1261 context: Some(ErrorContext::new(
1262 Some(
1263 json!({ "operation_id": operation_id, "variable": variable, "reason": reason }),
1264 ),
1265 Some(Cow::Borrowed(
1266 "Check the JQ query and ensure the response contains the expected data.",
1267 )),
1268 )),
1269 }
1270 }
1271
1272 pub fn batch_missing_id(context: impl Into<String>) -> Self {
1274 let context = context.into();
1275 Self::Internal {
1276 kind: ErrorKind::Validation,
1277 message: Cow::Owned(format!(
1278 "Batch operation requires an id: {context}"
1279 )),
1280 context: Some(ErrorContext::new(
1281 Some(json!({ "context": context })),
1282 Some(Cow::Borrowed(
1283 "Add an 'id' field to operations that use capture, capture_append, or depends_on.",
1284 )),
1285 )),
1286 }
1287 }
1288}
1289
1290#[cfg(test)]
1291mod tests {
1292 use super::*;
1293 use std::time::Duration;
1294 use wiremock::matchers::{method, path};
1295 use wiremock::{Mock, MockServer, ResponseTemplate};
1296
1297 #[test]
1300 fn test_to_json_specification_kind() {
1301 let err = Error::spec_not_found("my-api");
1302 let j = err.to_json();
1303 assert_eq!(j.error_type, "Specification");
1304 assert!(j.message.contains("my-api"));
1305 assert!(j.context.is_some(), "spec_not_found carries a suggestion");
1306 assert!(j.details.is_some());
1307 }
1308
1309 #[test]
1310 fn test_to_json_specification_cache_stale() {
1311 let err = Error::cache_stale("stale-api");
1312 let j = err.to_json();
1313 assert_eq!(j.error_type, "Specification");
1314 assert!(j.message.contains("stale-api"));
1315 assert!(j.context.is_some());
1316 }
1317
1318 #[test]
1319 fn test_to_json_authentication_secret_not_set() {
1320 let err = Error::secret_not_set("api-key", "MY_API_KEY");
1321 let j = err.to_json();
1322 assert_eq!(j.error_type, "Authentication");
1323 assert!(j.message.contains("MY_API_KEY"));
1324 assert!(j.context.is_some(), "secret_not_set carries a suggestion");
1325 assert!(j.details.is_some());
1326 }
1327
1328 #[test]
1329 fn test_to_json_authentication_unsupported_scheme() {
1330 let err = Error::unsupported_auth_scheme("digest");
1331 let j = err.to_json();
1332 assert_eq!(j.error_type, "Authentication");
1333 assert!(j.message.contains("digest"));
1334 assert!(j.context.is_some());
1335 }
1336
1337 #[test]
1338 fn test_to_json_validation_kind() {
1339 let err = Error::invalid_config("bad value");
1340 let j = err.to_json();
1341 assert_eq!(j.error_type, "Validation");
1342 assert!(j.message.contains("bad value"));
1343 assert!(j.context.is_some());
1344 }
1345
1346 #[test]
1347 fn test_to_json_network_internal_kind() {
1348 let err = Error::retry_limit_exceeded(3, "connection refused");
1350 let j = err.to_json();
1351 assert_eq!(j.error_type, "Network");
1352 assert!(
1353 j.context.is_some(),
1354 "retry_limit_exceeded carries a suggestion"
1355 );
1356 }
1357
1358 #[test]
1359 fn test_to_json_http_request_kind() {
1360 let err = Error::request_failed(reqwest::StatusCode::UNPROCESSABLE_ENTITY, "bad body");
1361 let j = err.to_json();
1362 assert_eq!(j.error_type, "HttpError");
1363 assert!(j.message.contains("422") || j.message.contains("Unprocessable"));
1364 assert!(j.message.contains("bad body"));
1365 assert!(j.context.is_some());
1366 assert!(j.details.is_some());
1367 }
1368
1369 #[test]
1370 fn test_to_json_headers_invalid_header_name() {
1371 let err = Error::invalid_header_name("X-Bad\0Header", "contains NUL");
1372 let j = err.to_json();
1373 assert_eq!(j.error_type, "Headers");
1374 assert!(j.context.is_some(), "header errors carry suggestions");
1375 assert!(j.details.is_some());
1376 }
1377
1378 #[test]
1379 fn test_to_json_headers_empty_header_name() {
1380 let err = Error::empty_header_name();
1381 let j = err.to_json();
1382 assert_eq!(j.error_type, "Headers");
1383 assert!(j.context.is_some());
1384 }
1385
1386 #[test]
1387 fn test_to_json_headers_invalid_idempotency_key() {
1388 let err = Error::invalid_idempotency_key();
1389 let j = err.to_json();
1390 assert_eq!(j.error_type, "Headers");
1391 assert!(j.context.is_some());
1392 }
1393
1394 #[test]
1395 fn test_to_json_interactive_timeout() {
1396 let err = Error::interactive_timeout();
1397 let j = err.to_json();
1398 assert_eq!(j.error_type, "Interactive");
1399 assert!(j.context.is_some(), "interactive errors carry suggestions");
1400 }
1401
1402 #[test]
1403 fn test_to_json_interactive_input_too_long() {
1404 let err = Error::interactive_input_too_long(256);
1405 let j = err.to_json();
1406 assert_eq!(j.error_type, "Interactive");
1407 assert!(j.context.is_some());
1408 assert!(j.details.is_some());
1409 }
1410
1411 #[test]
1412 fn test_to_json_interactive_editor_not_set() {
1413 let err = Error::editor_not_set();
1414 let j = err.to_json();
1415 assert_eq!(j.error_type, "Interactive");
1416 assert!(j.context.is_some());
1417 }
1418
1419 #[test]
1420 fn test_to_json_server_variable_missing() {
1421 let err = Error::missing_server_variable("region");
1422 let j = err.to_json();
1423 assert_eq!(j.error_type, "ServerVariable");
1424 assert!(j.message.contains("region"));
1425 assert!(
1426 j.context.is_some(),
1427 "server variable errors carry suggestions"
1428 );
1429 assert!(j.details.is_some());
1430 }
1431
1432 #[test]
1433 fn test_to_json_server_variable_unresolved_template() {
1434 let err = Error::unresolved_template_variable("env", "https://api.{env}.example.com");
1435 let j = err.to_json();
1436 assert_eq!(j.error_type, "ServerVariable");
1437 assert!(j.context.is_some());
1438 }
1439
1440 #[test]
1441 fn test_to_json_runtime_kind() {
1442 let err = Error::operation_not_found("unknown-op");
1443 let j = err.to_json();
1444 assert_eq!(j.error_type, "Runtime");
1445 assert!(j.message.contains("unknown-op"));
1446 assert!(j.context.is_some());
1447 }
1448
1449 #[test]
1452 fn test_to_json_io_not_found() {
1453 let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "no such file");
1454 let err = Error::Io(io_err);
1455 let j = err.to_json();
1456 assert_eq!(j.error_type, "FileSystem");
1457 assert!(j.context.is_some(), "NotFound carries a suggestion");
1458 }
1459
1460 #[test]
1461 fn test_to_json_io_permission_denied() {
1462 let io_err = std::io::Error::new(std::io::ErrorKind::PermissionDenied, "access denied");
1463 let err = Error::Io(io_err);
1464 let j = err.to_json();
1465 assert_eq!(j.error_type, "FileSystem");
1466 assert!(j.context.is_some(), "PermissionDenied carries a suggestion");
1467 }
1468
1469 #[test]
1470 fn test_to_json_io_other_kind() {
1471 let io_err = std::io::Error::new(std::io::ErrorKind::BrokenPipe, "broken pipe");
1472 let err = Error::Io(io_err);
1473 let j = err.to_json();
1474 assert_eq!(j.error_type, "FileSystem");
1475 assert!(j.context.is_none(), "generic IO kind carries no suggestion");
1476 }
1477
1478 #[test]
1479 fn test_to_json_yaml_error() {
1480 let yaml_err = serde_yaml::from_str::<serde_yaml::Value>("key: - value").unwrap_err();
1481 let err = Error::Yaml(yaml_err);
1482 let j = err.to_json();
1483 assert_eq!(j.error_type, "YAMLParsing");
1484 assert!(j.context.is_some());
1485 }
1486
1487 #[test]
1488 fn test_to_json_json_error() {
1489 let json_err = serde_json::from_str::<serde_json::Value>("{bad").unwrap_err();
1490 let err = Error::Json(json_err);
1491 let j = err.to_json();
1492 assert_eq!(j.error_type, "JSONParsing");
1493 assert!(j.context.is_some());
1494 }
1495
1496 #[test]
1497 fn test_to_json_toml_error() {
1498 let toml_err = toml::from_str::<toml::Value>("key = ").unwrap_err();
1499 let err = Error::Toml(toml_err);
1500 let j = err.to_json();
1501 assert_eq!(j.error_type, "TOMLParsing");
1502 assert!(j.context.is_some());
1503 }
1504
1505 #[test]
1506 fn test_to_json_anyhow_error() {
1507 let err = Error::Anyhow(anyhow::anyhow!("unexpected failure"));
1508 let j = err.to_json();
1509 assert_eq!(j.error_type, "Unknown");
1510 assert!(j.message.contains("unexpected failure"));
1511 assert!(j.context.is_none(), "anyhow errors carry no suggestion");
1512 }
1513
1514 async fn status_req_error(status: u16) -> reqwest::Error {
1519 let server = MockServer::start().await;
1520 Mock::given(method("GET"))
1521 .and(path("/err"))
1522 .respond_with(ResponseTemplate::new(status))
1523 .mount(&server)
1524 .await;
1525 reqwest::Client::new()
1526 .get(format!("{}/err", server.uri()))
1527 .send()
1528 .await
1529 .expect("mock server must respond")
1530 .error_for_status()
1531 .expect_err("status >= 400 must produce an error")
1532 }
1533
1534 #[tokio::test]
1535 async fn test_to_json_network_connect_error() {
1536 let req_err = reqwest::Client::new()
1537 .get("http://127.0.0.1:1/")
1538 .send()
1539 .await
1540 .expect_err("port 1 must refuse connections");
1541 assert!(req_err.is_connect());
1542 let j = Error::Network(req_err).to_json();
1543 assert_eq!(j.error_type, "Network");
1544 assert!(
1545 j.context.is_some(),
1546 "connect error carries ERR_CONNECTION hint"
1547 );
1548 }
1549
1550 #[tokio::test]
1551 async fn test_to_json_network_timeout_error() {
1552 let server = MockServer::start().await;
1553 Mock::given(method("GET"))
1554 .and(path("/slow"))
1555 .respond_with(ResponseTemplate::new(200).set_delay(Duration::from_secs(10)))
1556 .mount(&server)
1557 .await;
1558 let req_err = reqwest::Client::builder()
1559 .timeout(Duration::from_millis(1))
1560 .build()
1561 .unwrap()
1562 .get(format!("{}/slow", server.uri()))
1563 .send()
1564 .await
1565 .expect_err("must time out");
1566 assert!(req_err.is_timeout());
1567 let j = Error::Network(req_err).to_json();
1568 assert_eq!(j.error_type, "Network");
1569 assert!(
1570 j.context.is_some(),
1571 "timeout error carries ERR_TIMEOUT hint"
1572 );
1573 }
1574
1575 #[tokio::test]
1576 async fn test_to_json_network_401() {
1577 let req_err = status_req_error(401).await;
1578 let j = Error::Network(req_err).to_json();
1579 assert_eq!(j.error_type, "Network");
1580 assert!(j.context.is_some());
1581 }
1582
1583 #[tokio::test]
1584 async fn test_to_json_network_403() {
1585 let req_err = status_req_error(403).await;
1586 let j = Error::Network(req_err).to_json();
1587 assert_eq!(j.error_type, "Network");
1588 assert!(j.context.is_some());
1589 }
1590
1591 #[tokio::test]
1592 async fn test_to_json_network_404() {
1593 let req_err = status_req_error(404).await;
1594 let j = Error::Network(req_err).to_json();
1595 assert_eq!(j.error_type, "Network");
1596 assert!(j.context.is_some());
1597 }
1598
1599 #[tokio::test]
1600 async fn test_to_json_network_429() {
1601 let req_err = status_req_error(429).await;
1602 let j = Error::Network(req_err).to_json();
1603 assert_eq!(j.error_type, "Network");
1604 assert!(j.context.is_some());
1605 }
1606
1607 #[tokio::test]
1608 async fn test_to_json_network_500() {
1609 let req_err = status_req_error(500).await;
1610 let j = Error::Network(req_err).to_json();
1611 assert_eq!(j.error_type, "Network");
1612 assert!(j.context.is_some());
1613 }
1614
1615 #[tokio::test]
1619 async fn test_to_json_network_status_fallback_no_context() {
1620 let req_err = status_req_error(418).await;
1621 let j = Error::Network(req_err).to_json();
1622 assert_eq!(j.error_type, "Network");
1623 assert!(
1624 j.context.is_none(),
1625 "unrecognised status code must produce no suggestion"
1626 );
1627 }
1628}