Skip to main content

deep_time/
an_err.rs

1//! `AnErr<K, const N: usize = 31>` is an 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!`](crate::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//! [`Display`] writes the kind's [`Debug`] output (`{kind:?}`), then `: ` and the
57//! reason when it is non-empty — e.g. `YearOutOfRange` or `YearOutOfRange: year=10000`.
58//! If the reason fills its buffer, ` (reason may be truncated)` is appended.
59//!
60//! ## Wire format (`wire` feature)
61//!
62//! With the `wire` feature enabled, the following methods become available:
63//!
64//! - [`wire_size`](crate::AnErr::wire_size)
65//! - [`to_wire_bytes`](crate::AnErr::to_wire_bytes)
66//! - [`from_wire_bytes`](crate::AnErr::from_wire_bytes)
67//!
68//! [`AnErr`]: AnErr
69
70use crate::BufStr;
71use core::fmt;
72use core::fmt::Write;
73
74/// A compact, zero-allocation error type consisting of a single
75/// error kind and a human-readable reason string.
76///
77/// When context is added via `context`, `context_fmt`, or the `=>` form of
78/// `an_err!`, the new reason text is appended to the existing reason.
79///
80/// The total is silently truncated to `REASON_LEN`
81/// bytes if necessary.
82#[derive(Clone, PartialEq, Eq)]
83#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
84#[must_use = "this error should be handled or converted to a different type e.g. `pub type DtErr = AnErr<MyKind, 31>;`"]
85pub struct AnErr<K, const REASON_LEN: usize = 31>
86where
87    K: Copy + Clone + core::fmt::Debug + PartialEq + Eq,
88{
89    /// The error kind.
90    pub kind: K,
91
92    /// Accumulated reason string (controlled by `REASON_LEN`).
93    /// Can be empty.
94    pub reason: BufStr<REASON_LEN>,
95}
96
97impl<K, const REASON_LEN: usize> AnErr<K, REASON_LEN>
98where
99    K: Copy + Clone + core::fmt::Debug + PartialEq + Eq,
100{
101    /// Creates a new error with the given kind and empty reason.
102    #[inline(always)]
103    pub fn new(kind: K) -> Self {
104        Self {
105            kind,
106            reason: BufStr::default(),
107        }
108    }
109
110    /// Creates a new error with the given kind and reason.
111    #[inline(always)]
112    pub fn with_reason(kind: K, reason: BufStr<REASON_LEN>) -> Self {
113        Self { kind, reason }
114    }
115
116    /// Creates a new error with the given kind and a formatted reason.
117    ///
118    /// The formatted text is truncated if it exceeds `REASON_LEN` bytes.
119    #[inline]
120    pub fn with_fmt(kind: K, args: core::fmt::Arguments<'_>) -> Self {
121        let mut reason = BufStr::<REASON_LEN>::default();
122        let _ = write!(&mut reason, "{}", args);
123        Self { kind, reason }
124    }
125
126    /// Appends context by appending the given reason text to the accumulated
127    /// reason. Truncates if the total would exceed `REASON_LEN` bytes.
128    #[inline(always)]
129    pub fn context(&mut self, new_reason: BufStr<REASON_LEN>) {
130        self.append_reason(new_reason);
131    }
132
133    /// Appends context using a formatted reason string.
134    #[inline]
135    pub fn context_fmt(&mut self, args: core::fmt::Arguments<'_>) {
136        let mut new_reason = BufStr::<REASON_LEN>::default();
137        let _ = write!(&mut new_reason, "{}", args);
138        self.append_reason(new_reason);
139    }
140
141    #[inline(always)]
142    fn append_reason(&mut self, new_reason: BufStr<REASON_LEN>) {
143        let _ = write!(&mut self.reason, "{}", new_reason.as_str());
144    }
145
146    /// Returns the current error kind.
147    #[inline(always)]
148    pub fn kind(&self) -> K {
149        self.kind
150    }
151
152    /// Returns the accumulated reason.
153    #[inline(always)]
154    pub fn reason(&self) -> &BufStr<REASON_LEN> {
155        &self.reason
156    }
157}
158
159impl<K, const REASON_LEN: usize> From<K> for AnErr<K, REASON_LEN>
160where
161    K: Copy + Clone + core::fmt::Debug + PartialEq + Eq,
162{
163    #[inline]
164    fn from(kind: K) -> Self {
165        Self::new(kind)
166    }
167}
168
169impl<K, const REASON_LEN: usize> core::fmt::Display for AnErr<K, REASON_LEN>
170where
171    K: Copy + Clone + core::fmt::Debug + PartialEq + Eq,
172{
173    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
174        write!(f, "{:?}", self.kind)?;
175
176        if !self.reason.as_bytes().is_empty() {
177            write!(f, ": {}", self.reason.as_str())?;
178            if self.reason.as_bytes().len() == REASON_LEN {
179                write!(f, " (reason may be truncated)")?;
180            }
181        }
182
183        Ok(())
184    }
185}
186
187impl<K, const REASON_LEN: usize> fmt::Debug for AnErr<K, REASON_LEN>
188where
189    K: Copy + Clone + fmt::Debug + PartialEq + Eq,
190{
191    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
192        fmt::Display::fmt(self, f)
193    }
194}
195
196impl<K, const REASON_LEN: usize> core::error::Error for AnErr<K, REASON_LEN> where
197    K: Copy + Clone + core::fmt::Debug + PartialEq + Eq
198{
199}
200
201/// Ergonomic constructor and chaining macro for [`AnErr`].
202///
203/// ## Forms
204///
205/// | Form                                           | Equivalent to                                      |
206/// |------------------------------------------------|----------------------------------------------------|
207/// | `an_err!(Kind)`                                | `AnErr::new(Kind)`                                 |
208/// | `an_err!(Kind, "reason")`                      | `AnErr::with_fmt(Kind, ...)`                       |
209/// | `an_err!(Kind, "reason {}", arg, ...)`         | `AnErr::with_fmt(Kind, ...)`                       |
210/// | `an_err!("reason" => inner)`                   | `inner.context(...)` (appends to reason only)      |
211/// | `an_err!("reason {}", arg => inner)`           | `inner.context_fmt(...)` (appends to reason only)  |
212#[macro_export]
213macro_rules! an_err {
214    ($kind:expr) => {
215        $crate::AnErr::new($kind)
216    };
217
218    ($fmt:literal $(, $arg:expr)* => $inner:expr $(,)?) => {{
219        let mut e = $inner;
220        e.context_fmt(format_args!($fmt $(, $arg)*));
221        e
222    }};
223
224    ($kind:expr, $fmt:literal $(, $arg:expr)* $(,)?) => {
225        $crate::AnErr::with_fmt($kind, format_args!($fmt $(, $arg)*))
226    };
227}
228
229#[cfg(feature = "defmt")]
230impl<K, const REASON_LEN: usize> defmt::Format for AnErr<K, REASON_LEN>
231where
232    K: defmt::Format + Copy + Clone + core::fmt::Debug + PartialEq + Eq,
233{
234    fn format(&self, f: defmt::Formatter) {
235        if self.reason.as_bytes().is_empty() {
236            defmt::write!(f, "{}", self.kind);
237        } else if self.reason.as_bytes().len() == REASON_LEN {
238            defmt::write!(
239                f,
240                "{}: {} (reason may be truncated)",
241                self.kind,
242                self.reason.as_str()
243            );
244        } else {
245            defmt::write!(f, "{}: {}", self.kind, self.reason.as_str());
246        }
247    }
248}
249
250#[cfg(feature = "wire")]
251impl<K, const REASON_LEN: usize> AnErr<K, REASON_LEN>
252where
253    K: Copy + Clone + core::fmt::Debug + PartialEq + Eq,
254{
255    /// Serialize this error to a fixed-size byte buffer for transmission.
256    ///
257    /// The provided buffer must be at least `Self::wire_size()` bytes long.
258    /// Returns the number of bytes written.
259    pub fn to_wire_bytes(
260        &self,
261        kind_to_u16: impl Fn(K) -> u16,
262        buf: &mut [u8],
263    ) -> Result<usize, ()> {
264        let needed = Self::wire_size();
265        if buf.len() < needed {
266            return Err(());
267        }
268
269        let mut offset = 0;
270        buf[offset] = 1; // version
271        offset += 1;
272
273        let kind_val = kind_to_u16(self.kind);
274        buf[offset..offset + 2].copy_from_slice(&kind_val.to_le_bytes());
275        offset += 2;
276
277        buf[offset..offset + REASON_LEN].copy_from_slice(&self.reason.bytes);
278
279        Ok(needed)
280    }
281
282    /// Returns the exact size (in bytes) of the wire representation.
283    pub const fn wire_size() -> usize {
284        1 + 2 + REASON_LEN
285    }
286
287    /// Deserialize from a wire buffer directly into an `AnErr`.
288    ///
289    /// Requires a closure that maps the stored `u16` back to your concrete `K`.
290    /// Returns `None` on corruption, wrong size, unknown version, or mapping failure.
291    pub fn from_wire_bytes(bytes: &[u8], u16_to_kind: impl Fn(u16) -> Option<K>) -> Option<Self> {
292        if bytes.len() != Self::wire_size() {
293            return None;
294        }
295
296        let mut offset = 0;
297        if bytes[offset] != 1 {
298            return None;
299        }
300        offset += 1;
301
302        let kind_bytes = <[u8; 2]>::try_from(&bytes[offset..offset + 2]).ok()?;
303        let kind_u16 = u16::from_le_bytes(kind_bytes);
304        let kind = u16_to_kind(kind_u16)?;
305
306        offset += 2;
307
308        let reason_bytes = &bytes[offset..offset + REASON_LEN];
309        let reason = BufStr::from_bytes(reason_bytes);
310
311        Some(Self { kind, reason })
312    }
313}
314
315#[cfg(feature = "wire")]
316#[cfg(test)]
317mod tests {
318    use super::*;
319
320    #[derive(Debug, Clone, Copy, PartialEq, Eq)]
321    #[repr(u8)]
322    enum TestKind {
323        Foo,
324    }
325
326    #[test]
327    fn test_wire_roundtrip_with_append() {
328        let err: AnErr<TestKind, 15> = an_err!("bar" => an_err!(TestKind::Foo, "foo"));
329
330        let size = AnErr::<TestKind, 15>::wire_size();
331        let mut buf = [0u8; 32];
332
333        let written = err.to_wire_bytes(|k| k as u16, &mut buf).unwrap();
334        assert_eq!(written, size);
335
336        let decoded = AnErr::<TestKind, 15>::from_wire_bytes(&buf[..written], |v| {
337            if v == 0 { Some(TestKind::Foo) } else { None }
338        })
339        .unwrap();
340
341        assert_eq!(decoded.kind(), TestKind::Foo);
342        assert_eq!(decoded.reason.as_str(), "foobar");
343    }
344}