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