1use crate::{
2 output::{print_envelope, OutputFormat},
3 protocol::{Envelope, ErrorCode, ErrorDetails, ExitCode},
4};
5use std::collections::HashMap;
6
7pub struct ErrorResponder;
9
10impl ErrorResponder {
11 pub fn emit(
18 error: ErrorDetails,
19 output_format: OutputFormat,
20 meta: Option<HashMap<String, serde_json::Value>>,
21 exit_code: ExitCode,
22 ) -> ! {
23 let envelope = if let Some(meta) = meta {
24 Envelope::<()>::error_with_meta("error", error, meta)
25 } else {
26 Envelope::<()>::error("error", error)
27 };
28 let _ = print_envelope(&envelope, output_format);
29 std::process::exit(exit_code.into());
30 }
31
32 pub fn create_meta(trace_id: Option<&String>) -> Option<HashMap<String, serde_json::Value>> {
34 trace_id.map(|trace_id| {
35 let mut m = HashMap::new();
36 m.insert("traceId".to_string(), serde_json::json!(trace_id));
37 m
38 })
39 }
40
41 pub fn error(code: ErrorCode, message: impl Into<String>) -> ErrorDetails {
43 ErrorDetails::new(code, message)
44 }
45
46 pub fn error_with_retry(
48 code: ErrorCode,
49 message: impl Into<String>,
50 retry_after_ms: u64,
51 ) -> ErrorDetails {
52 ErrorDetails::with_retry_after(code, message, retry_after_ms)
53 }
54
55 pub fn error_with_details(
57 code: ErrorCode,
58 message: impl Into<String>,
59 details: HashMap<String, serde_json::Value>,
60 ) -> ErrorDetails {
61 ErrorDetails::with_details(code, message, details)
62 }
63
64 pub fn auth_required_error(
66 message: impl Into<String>,
67 next_steps: Vec<String>,
68 ) -> ErrorDetails {
69 ErrorDetails::auth_required(message, next_steps)
70 }
71}
72
73#[cfg(test)]
74mod tests {
75 use super::*;
76
77 #[test]
78 fn test_create_meta_with_trace_id() {
79 let trace_id = "test-trace-123".to_string();
80 let meta = ErrorResponder::create_meta(Some(&trace_id));
81 assert!(meta.is_some());
82 let meta = meta.unwrap();
83 assert_eq!(meta.get("traceId").unwrap(), "test-trace-123");
84 }
85
86 #[test]
87 fn test_create_meta_without_trace_id() {
88 let meta = ErrorResponder::create_meta(None);
89 assert!(meta.is_none());
90 }
91
92 #[test]
93 fn test_error_builder() {
94 let error = ErrorResponder::error(ErrorCode::InvalidArgument, "test message");
95 assert_eq!(error.code, ErrorCode::InvalidArgument);
96 assert_eq!(error.message, "test message");
97 assert!(!error.is_retryable);
98 assert!(error.retry_after_ms.is_none());
99 assert!(error.details.is_none());
100 }
101
102 #[test]
103 fn test_error_with_retry_builder() {
104 let error =
105 ErrorResponder::error_with_retry(ErrorCode::RateLimitExceeded, "rate limited", 5000);
106 assert_eq!(error.code, ErrorCode::RateLimitExceeded);
107 assert_eq!(error.message, "rate limited");
108 assert!(error.is_retryable);
109 assert_eq!(error.retry_after_ms, Some(5000));
110 assert!(error.details.is_none());
111 }
112
113 #[test]
114 fn test_error_with_details_builder() {
115 let mut details = HashMap::new();
116 details.insert("key".to_string(), serde_json::json!("value"));
117 let error =
118 ErrorResponder::error_with_details(ErrorCode::InternalError, "error", details.clone());
119 assert_eq!(error.code, ErrorCode::InternalError);
120 assert_eq!(error.message, "error");
121 assert!(error.details.is_some());
122 assert_eq!(error.details.unwrap().get("key").unwrap(), "value");
123 }
124
125 #[test]
126 fn test_auth_required_error_builder() {
127 let next_steps = vec!["Run auth login".to_string()];
128 let error = ErrorResponder::auth_required_error("auth needed", next_steps.clone());
129 assert_eq!(error.code, ErrorCode::AuthRequired);
130 assert_eq!(error.message, "auth needed");
131 assert!(error.details.is_some());
132 let details = error.details.unwrap();
133 assert!(details.contains_key("nextSteps"));
134 }
135
136 }