Skip to main content

deep_time/
an_err.rs

1//! `AnErr<K, const N: usize = 31>` is a `Copy` error type consisting of
2//! an error kind and a bounded human-readable reason string (`N` bytes).
3//! Additional context can be appended to the reason.
4//!
5//! ## Defining error kinds
6//!
7//! ```rust
8//! use deep_time::{AnErr, an_err};
9//!
10//! #[derive(Debug, Clone, Copy, PartialEq, Eq)]
11//! #[repr(u8)]
12//! pub enum MyKind {
13//!     NotFound,
14//!     InvalidInput,
15//!     Timeout,
16//! }
17//!
18//! pub type MyError = AnErr<MyKind, 63>;
19//! ```
20//!
21//! ## Construction and context
22//!
23//! Use the [`an_err!`] macro to create errors and add context:
24//!
25//! ```rust
26//! use deep_time::{AnErr, an_err};
27//!
28//! #[derive(Debug, Clone, Copy, PartialEq, Eq)]
29//! enum MyKind {
30//!     NotFound,
31//!     InvalidInput,
32//! }
33//!
34//! let err: AnErr<MyKind> = an_err!(MyKind::NotFound);
35//!
36//! let max = 100u32;
37//! let err: AnErr<MyKind> = an_err!(MyKind::InvalidInput, "expected value in range [0, {}]", max);
38//!
39//! let user_id = 42u64;
40//! let detailed: AnErr<MyKind> = an_err!("while processing user {}", user_id => err);
41//! ```
42//!
43//! The following methods are also available:
44//!
45//! - [`AnErr::new`]
46//! - [`AnErr::with_fmt`]
47//! - [`AnErr::with_reason`]
48//! - [`AnErr::context`]
49//! - [`AnErr::context_fmt`]
50//!
51//! When context is added, the new text is appended to the existing reason.
52//! The total length is silently truncated to `REASON_LEN` bytes if necessary.
53//!
54//! ## Display
55//!
56//! `AnErr` implements `Display` in the form `KindName` or `KindName: reason text`.
57//!
58//! ## Wire format (`wire` feature)
59//!
60//! With the `wire` feature enabled, the following methods become available:
61//!
62//! - [`AnErr::wire_size`]
63//! - [`AnErr::to_wire_bytes`]
64//! - [`AnErr::from_wire_bytes`]
65//!
66//! [`AnErr`]: AnErr
67//! [`an_err!`]: an_err
68
69use crate::LiteStr;
70use core::fmt;
71use core::fmt::Write;
72
73/// A compact, `Copy`, zero-allocation error type consisting of a single
74/// error kind and a human-readable reason string.
75///
76/// When context is added via `context`, `context_fmt`, or the `=>` form of
77/// `an_err!`, the new reason text is appended to the existing reason.
78///
79/// The total is silently truncated to `REASON_LEN`
80/// bytes if necessary.
81#[derive(Clone, Copy, PartialEq, Eq)]
82#[must_use = "this error should be handled or converted to a different type e.g. `pub type DtErr = AnErr<MyKind, 31>;`"]
83pub struct AnErr<K, const REASON_LEN: usize = 31>
84where
85    K: Copy + Clone + core::fmt::Debug + PartialEq + Eq,
86{
87    /// The error kind.
88    pub kind: K,
89
90    /// Accumulated reason string (controlled by `REASON_LEN`).
91    /// Can be empty.
92    pub reason: LiteStr<REASON_LEN>,
93}
94
95impl<K, const REASON_LEN: usize> AnErr<K, REASON_LEN>
96where
97    K: Copy + Clone + core::fmt::Debug + PartialEq + Eq,
98{
99    /// Creates a new error with the given kind and empty reason.
100    #[inline(always)]
101    pub fn new(kind: K) -> Self {
102        Self {
103            kind,
104            reason: LiteStr::default(),
105        }
106    }
107
108    /// Creates a new error with the given kind and reason.
109    #[inline(always)]
110    pub fn with_reason(kind: K, reason: LiteStr<REASON_LEN>) -> Self {
111        Self { kind, reason }
112    }
113
114    /// Creates a new error with the given kind and a formatted reason.
115    ///
116    /// The formatted text is truncated if it exceeds `REASON_LEN` bytes.
117    #[inline]
118    pub fn with_fmt(kind: K, args: core::fmt::Arguments<'_>) -> Self {
119        let mut reason = LiteStr::<REASON_LEN>::default();
120        let _ = write!(&mut reason, "{}", args);
121        Self { kind, reason }
122    }
123
124    /// Appends context by appending the given reason text to the accumulated
125    /// reason. Truncates if the total would exceed `REASON_LEN` bytes.
126    #[inline(always)]
127    pub fn context(&mut self, new_reason: LiteStr<REASON_LEN>) {
128        self.append_reason(new_reason);
129    }
130
131    /// Appends context using a formatted reason string.
132    #[inline]
133    pub fn context_fmt(&mut self, args: core::fmt::Arguments<'_>) {
134        let mut new_reason = LiteStr::<REASON_LEN>::default();
135        let _ = write!(&mut new_reason, "{}", args);
136        self.append_reason(new_reason);
137    }
138
139    #[inline(always)]
140    fn append_reason(&mut self, new_reason: LiteStr<REASON_LEN>) {
141        let _ = write!(&mut self.reason, "{}", new_reason.as_str());
142    }
143
144    /// Returns the current error kind.
145    #[inline(always)]
146    pub fn kind(&self) -> K {
147        self.kind
148    }
149
150    /// Returns the accumulated reason.
151    #[inline(always)]
152    pub fn reason(&self) -> &LiteStr<REASON_LEN> {
153        &self.reason
154    }
155}
156
157impl<K, const REASON_LEN: usize> From<K> for AnErr<K, REASON_LEN>
158where
159    K: Copy + Clone + core::fmt::Debug + PartialEq + Eq,
160{
161    #[inline]
162    fn from(kind: K) -> Self {
163        Self::new(kind)
164    }
165}
166
167impl<K, const REASON_LEN: usize> core::fmt::Display for AnErr<K, REASON_LEN>
168where
169    K: Copy + Clone + core::fmt::Debug + PartialEq + Eq,
170{
171    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
172        write!(f, "{:?}", self.kind)?;
173
174        if !self.reason.as_bytes().is_empty() {
175            write!(f, ": {}", self.reason.as_str())?;
176        }
177
178        Ok(())
179    }
180}
181
182impl<K, const REASON_LEN: usize> fmt::Debug for AnErr<K, REASON_LEN>
183where
184    K: Copy + Clone + fmt::Debug + PartialEq + Eq,
185{
186    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
187        fmt::Display::fmt(self, f)
188    }
189}
190
191impl<K, const REASON_LEN: usize> core::error::Error for AnErr<K, REASON_LEN> where
192    K: Copy + Clone + core::fmt::Debug + PartialEq + Eq
193{
194}
195
196/// Ergonomic constructor and chaining macro for [`AnErr`].
197///
198/// ## Forms
199///
200/// | Form                                           | Equivalent to                                      |
201/// |------------------------------------------------|----------------------------------------------------|
202/// | `an_err!(Kind)`                                | `AnErr::new(Kind)`                                 |
203/// | `an_err!(Kind, "reason")`                      | `AnErr::with_fmt(Kind, ...)`                       |
204/// | `an_err!(Kind, "reason {}", arg, ...)`         | `AnErr::with_fmt(Kind, ...)`                       |
205/// | `an_err!("reason" => inner)`                   | `inner.context(...)` (appends to reason only)      |
206/// | `an_err!("reason {}", arg => inner)`           | `inner.context_fmt(...)` (appends to reason only)  |
207#[macro_export]
208macro_rules! an_err {
209    ($kind:expr) => {
210        $crate::AnErr::new($kind)
211    };
212
213    ($fmt:literal $(, $arg:expr)* => $inner:expr $(,)?) => {{
214        let mut e = $inner;
215        e.context_fmt(format_args!($fmt $(, $arg)*));
216        e
217    }};
218
219    ($kind:expr, $fmt:literal $(, $arg:expr)* $(,)?) => {
220        $crate::AnErr::with_fmt($kind, format_args!($fmt $(, $arg)*))
221    };
222}
223
224#[cfg(feature = "wire")]
225impl<K, const REASON_LEN: usize> AnErr<K, REASON_LEN>
226where
227    K: Copy + Clone + core::fmt::Debug + PartialEq + Eq,
228{
229    /// Serialize this error to a fixed-size byte buffer for transmission.
230    ///
231    /// The provided buffer must be at least `Self::wire_size()` bytes long.
232    /// Returns the number of bytes written.
233    pub fn to_wire_bytes(
234        &self,
235        kind_to_u16: impl Fn(K) -> u16,
236        buf: &mut [u8],
237    ) -> Result<usize, ()> {
238        let needed = Self::wire_size();
239        if buf.len() < needed {
240            return Err(());
241        }
242
243        let mut offset = 0;
244        buf[offset] = 1; // version
245        offset += 1;
246
247        let kind_val = kind_to_u16(self.kind);
248        buf[offset..offset + 2].copy_from_slice(&kind_val.to_le_bytes());
249        offset += 2;
250
251        buf[offset..offset + REASON_LEN].copy_from_slice(&self.reason.bytes);
252
253        Ok(needed)
254    }
255
256    /// Returns the exact size (in bytes) of the wire representation.
257    pub const fn wire_size() -> usize {
258        1 + 2 + REASON_LEN
259    }
260
261    /// Deserialize from a wire buffer directly into an `AnErr`.
262    ///
263    /// Requires a closure that maps the stored `u16` back to your concrete `K`.
264    /// Returns `None` on corruption, wrong size, unknown version, or mapping failure.
265    pub fn from_wire_bytes(bytes: &[u8], u16_to_kind: impl Fn(u16) -> Option<K>) -> Option<Self> {
266        if bytes.len() != Self::wire_size() {
267            return None;
268        }
269
270        let mut offset = 0;
271        if bytes[offset] != 1 {
272            return None;
273        }
274        offset += 1;
275
276        let kind_bytes = <[u8; 2]>::try_from(&bytes[offset..offset + 2]).ok()?;
277        let kind_u16 = u16::from_le_bytes(kind_bytes);
278        let kind = u16_to_kind(kind_u16)?;
279
280        offset += 2;
281
282        let reason_bytes = &bytes[offset..offset + REASON_LEN];
283        let reason = LiteStr::from_bytes(reason_bytes);
284
285        Some(Self { kind, reason })
286    }
287}
288
289#[cfg(feature = "wire")]
290#[cfg(test)]
291mod tests {
292    use super::*;
293
294    #[derive(Debug, Clone, Copy, PartialEq, Eq)]
295    #[repr(u8)]
296    enum TestKind {
297        Foo,
298    }
299
300    #[test]
301    fn test_wire_roundtrip_with_append() {
302        let err: AnErr<TestKind, 15> = an_err!("bar" => an_err!(TestKind::Foo, "foo"));
303
304        let size = AnErr::<TestKind, 15>::wire_size();
305        let mut buf = [0u8; 32];
306
307        let written = err.to_wire_bytes(|k| k as u16, &mut buf).unwrap();
308        assert_eq!(written, size);
309
310        let decoded = AnErr::<TestKind, 15>::from_wire_bytes(&buf[..written], |v| {
311            if v == 0 { Some(TestKind::Foo) } else { None }
312        })
313        .unwrap();
314
315        assert_eq!(decoded.kind(), TestKind::Foo);
316        assert_eq!(decoded.reason.as_str(), "foobar");
317    }
318}