Skip to main content

frozen_core/
error.rs

1//! Utilities for error propagation used across `[frozen_core]`
2//!
3//! This module provides,
4//!
5//! - [`FrozenError`] which is a structured error w/ 24-bit identifier
6//! - [`FrozenResult`] which is a result alias using [`FrozenError`]
7//!
8//! ## Id
9//!
10//! Each [`FrozenError`] uses a 24-bit identifier encoded in the following format,
11//!
12//! `| module:8 | domain:8 | reason:8 |`
13//!
14//! This id packs important context which aids in the debugging process
15//!
16//! ## Example
17//!
18//! ```
19//! use frozen_core::error::{FrozenError, FrozenResult, ErrCode};
20//!
21//! fn read_file() -> FrozenResult<()> {
22//!     Err(FrozenError::new(0x10, 0x20, ErrCode::new(0x30, "io"), "read failed"))
23//! }
24//!
25//! let err = read_file().unwrap_err();
26//!
27//! assert_eq!(err.module, 0x10);
28//! assert_eq!(err.domain, 0x20);
29//! assert_eq!(err.reason, 0x30);
30//!
31//! assert!(err.context.contains("[io]"));
32//! ```
33
34/// Custom result type w/ [`FrozenError`] as error type
35pub type FrozenResult<T> = Result<T, FrozenError>;
36
37/// Utility for error propagation used across [`frozen_core`]
38#[derive(Clone)]
39pub struct FrozenError {
40    /// 8-bit unique identifier to identify the _module_ of [`FrozenError`] object
41    pub module: u8,
42
43    /// 8-bit unique identifier to identify the _domain_ of [`FrozenError`] object
44    pub domain: u8,
45
46    /// 8-bit unique identifier to identify the _reason_ of [`FrozenError`] object
47    pub reason: u8,
48
49    /// Error context for the [`FrozenError`]
50    pub context: Box<str>,
51}
52
53impl FrozenError {
54    /// Construct a new [`FrozenError`]
55    ///
56    /// ## Example
57    ///
58    /// ```
59    /// use frozen_core::error::{FrozenError, ErrCode};
60    ///
61    /// let err = FrozenError::new(0x10, 0x20, ErrCode::new(0x30, "io"), "failed to read file");
62    ///
63    /// assert_eq!(err.module, 0x10);
64    /// assert_eq!(err.domain, 0x20);
65    /// assert_eq!(err.reason, 0x30);
66    ///
67    /// assert!(err.context.contains("[io]"));
68    /// assert!(err.context.contains("failed to read file"));
69    /// ```
70    #[inline(always)]
71    pub fn new(module: u8, domain: u8, code: ErrCode, errmsg: &str) -> Self {
72        Self {
73            module,
74            domain,
75            reason: code.reason,
76            context: format!("[{}] {}", code.detail, errmsg).into_boxed_str(),
77        }
78    }
79
80    /// Construct a new [`FrozenError`] from raw [`Error`] object
81    ///
82    /// ## Example
83    ///
84    /// ```
85    /// use frozen_core::error::{FrozenError, ErrCode};
86    ///
87    /// let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file missing");
88    /// let err = FrozenError::new_raw(0x14, 0x24, ErrCode::new(0x34, "io"), io_err);
89    ///
90    /// assert_eq!(err.module, 0x14);
91    /// assert_eq!(err.domain, 0x24);
92    /// assert_eq!(err.reason, 0x34);
93    ///
94    /// assert!(err.context.contains("[io]"));
95    /// assert!(err.context.contains("file missing"));
96    /// ```
97    #[inline(always)]
98    pub fn new_raw<E: std::fmt::Display>(
99        module: u8,
100        domain: u8,
101        code: ErrCode,
102        err: E,
103    ) -> Self {
104        Self {
105            domain,
106            module,
107            reason: code.reason,
108            context: format!("[{}] {}", code.detail, err).into_boxed_str(),
109        }
110    }
111}
112
113impl std::fmt::Debug for FrozenError {
114    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
115        write!(
116            f,
117            "FrozenError {{ module: {:#04x}, domain: {:#04x}, reason: {:#04x}, context: {:?} }}",
118            self.module, self.domain, self.reason, self.context
119        )
120    }
121}
122
123impl std::fmt::Display for FrozenError {
124    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
125        write!(
126            f,
127            "[{:#04x}:{:#04x}:{:#04x}] {}",
128            self.module, self.domain, self.reason, self.context
129        )
130    }
131}
132
133impl std::error::Error for FrozenError {}
134
135impl PartialEq for FrozenError {
136    fn eq(&self, other: &Self) -> bool {
137        (self.module == other.module)
138            && (self.domain == other.domain)
139            && (self.reason == other.reason)
140    }
141}
142
143/// Static error descriptor used to construct [`FrozenError`]
144///
145/// ## Example
146///
147/// ```
148/// use frozen_core::error::ErrCode;
149///
150/// const LOCK_ERR: ErrCode = frozen_core::error::ErrCode::new(0xFF, "lock error");
151///
152/// assert_eq!(LOCK_ERR.reason, 0xFF);
153/// assert_eq!(LOCK_ERR.detail, "lock error");
154/// ```
155#[derive(Debug, Clone)]
156pub struct ErrCode {
157    /// 8-bit reason code encoded into [`FrozenError::id`]
158    pub reason: u8,
159
160    /// Short subsystem label included in the formatted error context
161    pub detail: &'static str,
162}
163
164impl ErrCode {
165    /// Create a new [`ErrCode`]
166    ///
167    /// *NOTE:* This function is `const`, allowing error codes to be defined as compile-time constants
168    ///
169    /// ## Example
170    ///
171    /// ```
172    /// use frozen_core::error::ErrCode;
173    ///
174    /// const LOCK_ERR: ErrCode = frozen_core::error::ErrCode::new(0xFF, "lock error");
175    ///
176    /// assert_eq!(LOCK_ERR.reason, 0xFF);
177    /// assert_eq!(LOCK_ERR.detail, "lock error");
178    /// ```
179    #[inline]
180    pub const fn new(reason: u8, detail: &'static str) -> Self {
181        Self { reason, detail }
182    }
183}
184
185#[cfg(test)]
186mod tests {
187    use super::*;
188
189    #[test]
190    fn ok_context_exact_format() {
191        let err = FrozenError::new(1, 2, ErrCode::new(3, "io"), "failure");
192        assert_eq!(err.context, "[io] failure".into());
193    }
194
195    #[test]
196    fn ok_empty_message() {
197        let err = FrozenError::new(1, 2, ErrCode::new(3, "io"), "");
198        assert_eq!(err.context, "[io] ".into());
199    }
200
201    #[test]
202    fn ok_empty_detail() {
203        let err = FrozenError::new(1, 2, ErrCode::new(3, ""), "failure");
204        assert_eq!(err.context, "[] failure".into());
205    }
206
207    #[test]
208    fn ok_new_and_new_raw_same_id() {
209        let e1 = FrozenError::new(1, 2, ErrCode::new(3, "io"), "fail");
210
211        let io_err = std::io::Error::new(std::io::ErrorKind::Other, "fail");
212        let e2 = FrozenError::new_raw(1, 2, ErrCode::new(3, "io"), io_err);
213
214        assert_eq!(e1, e2);
215    }
216
217    #[test]
218    fn ok_context_formatting() {
219        let err = FrozenError::new(1, 1, ErrCode::new(1, "io"), "disk failure");
220        assert_eq!(err.context, "[io] disk failure".into());
221    }
222
223    #[test]
224    fn ok_new_raw_uses_display() {
225        let io_err = std::io::Error::new(
226            std::io::ErrorKind::PermissionDenied,
227            "access denied",
228        );
229        let err = FrozenError::new_raw(1, 2, ErrCode::new(3, "io"), io_err);
230
231        assert!(err.context.contains("[io]"));
232        assert!(err.context.contains("access denied"));
233    }
234
235    #[test]
236    fn ok_compare_same_id_different_context() {
237        let e1 = FrozenError::new(1, 2, ErrCode::new(3, "io"), "a");
238        let e2 = FrozenError::new(1, 2, ErrCode::new(3, "io"), "b");
239
240        assert_eq!(e1, e2);
241    }
242
243    #[test]
244    fn ok_compare_different_id() {
245        let e1 = FrozenError::new(1, 2, ErrCode::new(3, "io"), "a");
246        let e2 = FrozenError::new(1, 2, ErrCode::new(4, "io"), "a");
247
248        assert_ne!(e1, e2);
249    }
250
251    #[test]
252    fn ok_display_format() {
253        let err = FrozenError::new(
254            0x01,
255            0x11,
256            ErrCode::new(0x1C, "lock"),
257            "file locked",
258        );
259        assert_eq!(format!("{err}"), "[0x01:0x11:0x1c] [lock] file locked");
260    }
261
262    #[test]
263    fn ok_debug_format_includes_context() {
264        let err = FrozenError::new(
265            0x01,
266            0x11,
267            ErrCode::new(0x1C, "lock"),
268            "file locked",
269        );
270        let dbg = format!("{err:?}");
271        assert!(dbg.contains("0x01"));
272        assert!(dbg.contains("0x11"));
273        assert!(dbg.contains("0x1c"));
274        assert!(dbg.contains("[lock] file locked"));
275    }
276
277    #[test]
278    fn ok_is_std_error() {
279        // NOTE: FrozenError must satisfy `std::error::Error` so it can be used with `?` into Box<dyn Error>
280        // and ecosystem tooling
281        fn assert_is_error<E: std::error::Error>(_: &E) {}
282        let err = FrozenError::new(1, 2, ErrCode::new(3, "io"), "fail");
283        assert_is_error(&err);
284    }
285}