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(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: String,
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),
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>(module: u8, domain: u8, code: ErrCode, err: E) -> Self {
99        Self { domain, module, reason: code.reason, context: format!("[{}] {}", code.detail, err) }
100    }
101
102    /// Compare two errors by their encoded id's
103    ///
104    /// ## Example
105    ///
106    /// ```
107    /// use frozen_core::error::{FrozenError, ErrCode};
108    ///
109    /// let err1 = FrozenError::new(0x1A, 0x2A, ErrCode::new(0x3A, "test"), "something failed");
110    /// let err2 = FrozenError::new(0x1A, 0x2A, ErrCode::new(0x3A, "test"), "another message");
111    ///
112    /// assert!(err1.is_equal(&err2));
113    /// ```
114    #[inline(always)]
115    pub fn is_equal(&self, err: &FrozenError) -> bool {
116        self == err
117    }
118}
119
120impl std::fmt::Debug for FrozenError {
121    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
122        f.debug_struct("FrozenError")
123            .field("Module", &self.module)
124            .field("Domain", &self.domain)
125            .field("Reason", &self.reason)
126            .finish()
127    }
128}
129
130impl PartialEq for FrozenError {
131    fn eq(&self, other: &Self) -> bool {
132        (self.module == other.module)
133            && (self.domain == other.domain)
134            && (self.reason == other.reason)
135    }
136}
137
138/// Static error descriptor used to construct [`FrozenError`]
139///
140/// ## Example
141///
142/// ```
143/// use frozen_core::error::ErrCode;
144///
145/// const LOCK_ERR: ErrCode = frozen_core::error::ErrCode::new(0xFF, "lock error");
146///
147/// assert_eq!(LOCK_ERR.reason, 0xFF);
148/// assert_eq!(LOCK_ERR.detail, "lock error");
149/// ```
150#[derive(Debug, Clone)]
151pub struct ErrCode {
152    /// 8-bit reason code encoded into [`FrozenError::id`]
153    pub reason: u8,
154
155    /// Short subsystem label included in the formatted error context
156    pub detail: &'static str,
157}
158
159impl ErrCode {
160    /// Create a new [`ErrCode`]
161    ///
162    /// *NOTE:* This function is `const`, allowing error codes to be defined as compile-time constants
163    ///
164    /// ## Example
165    ///
166    /// ```
167    /// use frozen_core::error::ErrCode;
168    ///
169    /// const LOCK_ERR: ErrCode = frozen_core::error::ErrCode::new(0xFF, "lock error");
170    ///
171    /// assert_eq!(LOCK_ERR.reason, 0xFF);
172    /// assert_eq!(LOCK_ERR.detail, "lock error");
173    /// ```
174    #[inline]
175    pub const fn new(reason: u8, detail: &'static str) -> Self {
176        Self { reason, detail }
177    }
178}
179
180#[cfg(test)]
181mod tests {
182    use super::*;
183
184    #[test]
185    fn ok_context_exact_format() {
186        let err = FrozenError::new(1, 2, ErrCode::new(3, "io"), "failure");
187        assert_eq!(err.context, "[io] failure");
188    }
189
190    #[test]
191    fn ok_empty_message() {
192        let err = FrozenError::new(1, 2, ErrCode::new(3, "io"), "");
193        assert_eq!(err.context, "[io] ");
194    }
195
196    #[test]
197    fn ok_empty_detail() {
198        let err = FrozenError::new(1, 2, ErrCode::new(3, ""), "failure");
199        assert_eq!(err.context, "[] failure");
200    }
201
202    #[test]
203    fn ok_new_and_new_raw_same_id() {
204        let e1 = FrozenError::new(1, 2, ErrCode::new(3, "io"), "fail");
205
206        let io_err = std::io::Error::new(std::io::ErrorKind::Other, "fail");
207        let e2 = FrozenError::new_raw(1, 2, ErrCode::new(3, "io"), io_err);
208
209        assert_eq!(e1, e2);
210    }
211
212    #[test]
213    fn ok_context_formatting() {
214        let err = FrozenError::new(1, 1, ErrCode::new(1, "io"), "disk failure");
215        assert_eq!(err.context, "[io] disk failure");
216    }
217
218    #[test]
219    fn ok_new_raw_uses_display() {
220        let io_err = std::io::Error::new(std::io::ErrorKind::PermissionDenied, "access denied");
221        let err = FrozenError::new_raw(1, 2, ErrCode::new(3, "io"), io_err);
222
223        assert!(err.context.contains("[io]"));
224        assert!(err.context.contains("access denied"));
225    }
226
227    #[test]
228    fn ok_compare_same_id_different_context() {
229        let e1 = FrozenError::new(1, 2, ErrCode::new(3, "io"), "a");
230        let e2 = FrozenError::new(1, 2, ErrCode::new(3, "io"), "b");
231
232        assert_eq!(e1, e2);
233    }
234
235    #[test]
236    fn ok_compare_different_id() {
237        let e1 = FrozenError::new(1, 2, ErrCode::new(3, "io"), "a");
238        let e2 = FrozenError::new(1, 2, ErrCode::new(4, "io"), "a");
239
240        assert_ne!(e1, e2);
241    }
242}