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