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