Skip to main content

xcom_rs/
errors.rs

1use crate::{
2    output::{print_envelope, OutputFormat},
3    protocol::{Envelope, ErrorCode, ErrorDetails, ExitCode},
4};
5use std::collections::HashMap;
6
7/// Helper struct for emitting error responses with consistent formatting and metadata
8pub struct ErrorResponder;
9
10impl ErrorResponder {
11    /// Emit an error response and exit with the appropriate exit code
12    ///
13    /// This centralizes error response generation by:
14    /// - Creating an error envelope with optional metadata
15    /// - Printing the response in the specified format
16    /// - Exiting with the appropriate exit code
17    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    /// Create metadata map from trace_id if present
33    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    /// Create a simple error with code and message
42    pub fn error(code: ErrorCode, message: impl Into<String>) -> ErrorDetails {
43        ErrorDetails::new(code, message)
44    }
45
46    /// Create an error with retry_after_ms
47    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    /// Create an error with additional details
56    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    /// Create an auth required error with next steps
65    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    // Note: Cannot test emit() as it calls std::process::exit
137    // Integration tests should cover the end-to-end behavior
138}