error_envelope/
error.rs

1use crate::Code;
2use serde::{Serialize, Serializer};
3use std::fmt;
4use std::time::Duration;
5
6/// Structured error envelope for HTTP APIs.
7#[derive(Debug, Clone)]
8pub struct Error {
9    pub code: Code,
10    pub message: String,
11    pub details: Option<serde_json::Value>,
12    pub trace_id: Option<String>,
13    pub retryable: bool,
14
15    // Not serialized directly
16    pub status: u16,
17    pub retry_after: Option<Duration>,
18
19    // Cause is not clonable, so we store it as a string
20    cause_message: Option<String>,
21}
22
23impl Error {
24    /// Creates a new error with the given code, status, and message.
25    pub fn new(code: Code, status: u16, message: impl Into<String>) -> Self {
26        let message = message.into();
27        let message = if message.is_empty() {
28            code.default_message().to_string()
29        } else {
30            message
31        };
32
33        let status = if status == 0 {
34            code.default_status()
35        } else {
36            status
37        };
38
39        Self {
40            code,
41            message,
42            details: None,
43            trace_id: None,
44            retryable: code.is_retryable_default(),
45            status,
46            retry_after: None,
47            cause_message: None,
48        }
49    }
50
51    /// Creates a new error with a formatted message.
52    ///
53    /// This is a semantic alias for `new()` that signals the message
54    /// is typically constructed with `format!()`.
55    ///
56    /// # Example
57    /// ```
58    /// use error_envelope::{Error, Code};
59    /// let user_id = 123;
60    /// let err = Error::newf(Code::NotFound, 404, format!("user {} not found", user_id));
61    /// ```
62    pub fn newf(code: Code, status: u16, message: impl Into<String>) -> Self {
63        Self::new(code, status, message)
64    }
65
66    /// Creates a new error that wraps an underlying cause.
67    pub fn wrap(
68        code: Code,
69        status: u16,
70        message: impl Into<String>,
71        cause: impl std::error::Error,
72    ) -> Self {
73        let mut err = Self::new(code, status, message);
74        err.cause_message = Some(cause.to_string());
75        err
76    }
77
78    /// Adds structured details to the error.
79    pub fn with_details(mut self, details: serde_json::Value) -> Self {
80        self.details = Some(details);
81        self
82    }
83
84    /// Adds a trace ID for distributed tracing.
85    pub fn with_trace_id(mut self, trace_id: impl Into<String>) -> Self {
86        self.trace_id = Some(trace_id.into());
87        self
88    }
89
90    /// Sets whether the error is retryable.
91    pub fn with_retryable(mut self, retryable: bool) -> Self {
92        self.retryable = retryable;
93        self
94    }
95
96    /// Overrides the HTTP status code.
97    pub fn with_status(mut self, status: u16) -> Self {
98        if status != 0 {
99            self.status = status;
100        }
101        self
102    }
103
104    /// Sets the retry-after duration for rate-limited responses.
105    pub fn with_retry_after(mut self, duration: Duration) -> Self {
106        self.retry_after = Some(duration);
107        self
108    }
109
110    /// Returns the cause message if available.
111    pub fn cause(&self) -> Option<&str> {
112        self.cause_message.as_deref()
113    }
114
115    /// Returns the HTTP status code.
116    pub fn status(&self) -> u16 {
117        self.status
118    }
119}
120
121impl fmt::Display for Error {
122    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
123        if let Some(ref cause) = self.cause_message {
124            write!(f, "{:?}: {} ({})", self.code, self.message, cause)
125        } else {
126            write!(f, "{:?}: {}", self.code, self.message)
127        }
128    }
129}
130
131impl std::error::Error for Error {
132    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
133        // Since we only store the cause message, we can't return the original error
134        None
135    }
136}
137
138// Custom serialization to include retry_after as human-readable duration
139impl Serialize for Error {
140    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
141    where
142        S: Serializer,
143    {
144        use serde::ser::SerializeStruct;
145
146        // Count actual fields that will be serialized
147        let mut field_count = 3; // code, message, retryable (always present)
148        if self.details.is_some() {
149            field_count += 1;
150        }
151        if self.trace_id.is_some() {
152            field_count += 1;
153        }
154        if self.retry_after.is_some() {
155            field_count += 1;
156        }
157
158        let mut state = serializer.serialize_struct("Error", field_count)?;
159
160        state.serialize_field("code", &self.code)?;
161        state.serialize_field("message", &self.message)?;
162
163        if self.details.is_some() {
164            state.serialize_field("details", &self.details)?;
165        }
166
167        if self.trace_id.is_some() {
168            state.serialize_field("trace_id", &self.trace_id)?;
169        }
170
171        state.serialize_field("retryable", &self.retryable)?;
172
173        if let Some(ref duration) = self.retry_after {
174            let secs = duration.as_secs();
175            let formatted = if secs < 60 {
176                format!("{}s", secs)
177            } else {
178                format!("{}m{}s", secs / 60, secs % 60)
179            };
180            state.serialize_field("retry_after", &formatted)?;
181        }
182
183        state.end()
184    }
185}