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`]: a structured error w/ 32-bit identifier
6//! - [`FrozenResult`]: a result alias using [`FrozenError`]
7//!
8//! ## Id
9//!
10//! Each [`FrozenError`] uses a 32-bit identifier encoded in following format,
11//!
12//! `| module:8 | domain:8 | reason:16 |`
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(1, 2, ErrCode::new(0x0033, "io"), "read failed"))
23//! }
24//!
25//! let err = read_file().unwrap_err();
26//!
27//! assert_eq!(err.id, 0x0102_0033);
28//! assert!(err.context.contains("[io]"));
29//! ```
30
31/// Custom result type w/ [`FrozenError`] as error type
32pub type FrozenResult<T> = Result<T, FrozenError>;
33
34/// Utility for error propagation used across [`frozen_core`]
35#[derive(Debug, Clone)]
36pub struct FrozenError {
37    /// Encoded 32-bit unique identifier for [`FrozenError`]
38    pub id: u32,
39
40    /// Error context for the [`FrozenError`]
41    pub context: String,
42}
43
44impl FrozenError {
45    /// Construct a new [`FrozenError`]
46    ///
47    /// ## Example
48    ///
49    /// ```
50    /// use frozen_core::error::{FrozenError, ErrCode};
51    ///
52    /// let err = FrozenError::new(1, 2, ErrCode::new(0x0033, "io"), "failed to read file");
53    ///
54    /// assert_eq!(err.id, 0x0102_0033);
55    /// assert!(err.context.contains("[io]"));
56    /// assert!(err.context.contains("failed to read file"));
57    /// ```
58    #[inline(always)]
59    pub fn new(module: u8, domain: u8, code: ErrCode, errmsg: &str) -> Self {
60        Self { id: error_id(module, domain, code.reason), context: format!("[{}] {}", code.detail, errmsg) }
61    }
62
63    /// Construct a new [`FrozenError`] from raw [`Error`] object
64    ///
65    /// ## Example
66    ///
67    /// ```
68    /// use frozen_core::error::{FrozenError, ErrCode};
69    ///
70    /// let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file missing");
71    /// let err = FrozenError::new_raw(1, 2, ErrCode::new(0x0033, "io"), io_err);
72    ///
73    /// assert_eq!(err.id, 0x0102_0033);
74    /// assert!(err.context.contains("[io]"));
75    /// assert!(err.context.contains("file missing"));
76    /// ```
77    #[inline(always)]
78    pub fn new_raw<E: std::fmt::Display>(module: u8, domain: u8, code: ErrCode, err: E) -> Self {
79        Self { id: error_id(module, domain, code.reason), context: format!("[{}] {}", code.detail, err) }
80    }
81
82    /// Compare two errors by their encoded id's
83    ///
84    /// ## Example
85    ///
86    /// ```
87    /// use frozen_core::error::{FrozenError, ErrCode};
88    ///
89    /// let err1 = FrozenError::new(0, 0, ErrCode::new(0x30, "test"), "something failed");
90    /// let err2 = FrozenError::new(0, 0, ErrCode::new(0x30, "test"), "another message");
91    ///
92    /// assert!(err1.compare_id(&err2));
93    /// ```
94    #[inline(always)]
95    pub fn compare_id(&self, err: &FrozenError) -> bool {
96        self.id == err.id
97    }
98}
99
100/// Static error descriptor used to construct [`FrozenError`]
101///
102/// ## Example
103///
104/// ```
105/// use frozen_core::error::ErrCode;
106///
107/// const LOCK_ERR: ErrCode = frozen_core::error::ErrCode::new(0x300, "lock error");
108///
109/// assert_eq!(LOCK_ERR.reason, 0x300);
110/// assert_eq!(LOCK_ERR.detail, "lock error");
111/// ```
112#[derive(Debug, Clone)]
113pub struct ErrCode {
114    /// 16-bit reason code encoded into [`FrozenError::id`]
115    pub reason: u16,
116
117    /// Short subsystem label included in the formatted error context
118    pub detail: &'static str,
119}
120
121impl ErrCode {
122    /// Create a new [`ErrCode`]
123    ///
124    /// *NOTE:* This function is `const`, allowing error codes to be defined as compile-time constants
125    ///
126    /// ## Example
127    ///
128    /// ```
129    /// use frozen_core::error::ErrCode;
130    ///
131    /// const LOCK_ERR: ErrCode = frozen_core::error::ErrCode::new(0x300, "lock error");
132    ///
133    /// assert_eq!(LOCK_ERR.reason, 0x300);
134    /// assert_eq!(LOCK_ERR.detail, "lock error");
135    /// ```
136    #[inline]
137    pub const fn new(reason: u16, detail: &'static str) -> Self {
138        Self { reason, detail }
139    }
140}
141
142#[inline]
143const fn error_id(module: u8, domain: u8, reason: u16) -> u32 {
144    ((module as u32) << 0x18) | ((domain as u32) << 0x10) | (reason as u32)
145}
146
147#[cfg(test)]
148mod tests {
149    use super::*;
150
151    #[inline]
152    const fn from_error_id(eid: u32) -> (u8, u8, u16) {
153        let module = ((eid >> 24) & 0xff) as u8;
154        let domain = ((eid >> 16) & 0xff) as u8;
155        let reason = (eid & 0xffff) as u16;
156
157        (module, domain, reason)
158    }
159
160    #[test]
161    fn ok_context_exact_format() {
162        let err = FrozenError::new(1, 2, ErrCode::new(3, "io"), "failure");
163        assert_eq!(err.context, "[io] failure");
164    }
165
166    #[test]
167    fn ok_empty_message() {
168        let err = FrozenError::new(1, 2, ErrCode::new(3, "io"), "");
169        assert_eq!(err.context, "[io] ");
170    }
171
172    #[test]
173    fn ok_empty_detail() {
174        let err = FrozenError::new(1, 2, ErrCode::new(3, ""), "failure");
175        assert_eq!(err.context, "[] failure");
176    }
177
178    #[test]
179    fn ok_error_id_roundtrip_basic() {
180        let err = FrozenError::new(0x01, 0x02, ErrCode::new(0x0033, "io"), "read failed");
181        let (m, d, r) = from_error_id(err.id);
182
183        assert_eq!(err.id, 0x0102_0033);
184        assert_eq!((m, d, r), (0x01, 0x02, 0x0033));
185    }
186
187    #[test]
188    fn ok_new_and_new_raw_same_id() {
189        let e1 = FrozenError::new(1, 2, ErrCode::new(3, "io"), "fail");
190
191        let io_err = std::io::Error::new(std::io::ErrorKind::Other, "fail");
192        let e2 = FrozenError::new_raw(1, 2, ErrCode::new(3, "io"), io_err);
193
194        assert_eq!(e1.id, e2.id);
195    }
196
197    #[test]
198    fn ok_error_id_roundtrip_edges() {
199        let err = FrozenError::new(0xff, 0x00, ErrCode::new(0xffff, "edge"), "max values");
200        let (m, d, r) = from_error_id(err.id);
201
202        assert_eq!(err.id, 0xff00_ffff);
203        assert_eq!((m, d, r), (0xff, 0x00, 0xffff));
204    }
205
206    #[test]
207    fn ok_error_id_reason_only_changes_low_bits() {
208        let e1 = FrozenError::new(1, 2, ErrCode::new(1, "x"), "a");
209        let e2 = FrozenError::new(1, 2, ErrCode::new(2, "x"), "a");
210
211        assert_eq!(e1.id & 0xffff_0000, e2.id & 0xffff_0000);
212        assert_ne!(e1.id & 0x0000_ffff, e2.id & 0x0000_ffff);
213    }
214
215    #[test]
216    fn ok_context_formatting() {
217        let err = FrozenError::new(1, 1, ErrCode::new(1, "io"), "disk failure");
218        assert_eq!(err.context, "[io] disk failure");
219    }
220
221    #[test]
222    fn ok_new_raw_uses_display() {
223        let io_err = std::io::Error::new(std::io::ErrorKind::PermissionDenied, "access denied");
224        let err = FrozenError::new_raw(1, 2, ErrCode::new(3, "io"), io_err);
225
226        assert!(err.context.contains("[io]"));
227        assert!(err.context.contains("access denied"));
228    }
229
230    #[test]
231    fn ok_compare_same_id_different_context() {
232        let e1 = FrozenError::new(1, 2, ErrCode::new(3, "io"), "a");
233        let e2 = FrozenError::new(1, 2, ErrCode::new(3, "io"), "b");
234
235        assert!(e1.compare_id(&e2));
236    }
237
238    #[test]
239    fn ok_compare_different_id() {
240        let e1 = FrozenError::new(1, 2, ErrCode::new(3, "io"), "a");
241        let e2 = FrozenError::new(1, 2, ErrCode::new(4, "io"), "a");
242
243        assert!(!e1.compare_id(&e2));
244    }
245}