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