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}