waddling_errors/
code.rs

1//! Diagnostic code structure
2
3use crate::Severity;
4use core::fmt;
5
6#[cfg(feature = "std")]
7use std::{format, string::String};
8
9#[cfg(not(feature = "std"))]
10use alloc::{format, string::String};
11
12/// Waddling diagnostic code: `SEVERITY.COMPONENT.PRIMARY.SEQUENCE`
13///
14/// Format: `E.CRYPTO.SALT.001`
15///
16/// # Examples
17///
18/// ```rust
19/// use waddling_errors::{Code, Severity};
20///
21/// const ERR: Code = Code::error("CRYPTO", "SALT", 1);
22/// assert_eq!(ERR.code(), "E.CRYPTO.SALT.001");
23/// ```
24#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
25#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
26pub struct Code {
27    severity: Severity,
28    component: &'static str,
29    primary: &'static str,
30    sequence: u16,
31}
32
33impl Code {
34    const MAX_LEN: usize = 12;
35    const MIN_LEN: usize = 2;
36
37    /// Create a new code with explicit severity
38    ///
39    /// # Panics
40    ///
41    /// Panics if sequence > 999, or component/primary length not in 2-12 range
42    pub const fn new(
43        severity: Severity,
44        component: &'static str,
45        primary: &'static str,
46        sequence: u16,
47    ) -> Self {
48        assert!(sequence <= 999, "Sequence must be <= 999");
49
50        let component_len = component.len();
51        assert!(
52            component_len >= Self::MIN_LEN && component_len <= Self::MAX_LEN,
53            "Component must be 2-12 characters"
54        );
55
56        let primary_len = primary.len();
57        assert!(
58            primary_len >= Self::MIN_LEN && primary_len <= Self::MAX_LEN,
59            "Primary must be 2-12 characters"
60        );
61
62        Self {
63            severity,
64            component,
65            primary,
66            sequence,
67        }
68    }
69
70    /// Create an error code (E)
71    pub const fn error(component: &'static str, primary: &'static str, sequence: u16) -> Self {
72        Self::new(Severity::Error, component, primary, sequence)
73    }
74
75    /// Create a warning code (W)
76    pub const fn warning(component: &'static str, primary: &'static str, sequence: u16) -> Self {
77        Self::new(Severity::Warning, component, primary, sequence)
78    }
79
80    /// Create a critical code (C)
81    pub const fn critical(component: &'static str, primary: &'static str, sequence: u16) -> Self {
82        Self::new(Severity::Critical, component, primary, sequence)
83    }
84
85    /// Create a blocked code (B)
86    pub const fn blocked(component: &'static str, primary: &'static str, sequence: u16) -> Self {
87        Self::new(Severity::Blocked, component, primary, sequence)
88    }
89
90    /// Create a help code (H)
91    pub const fn help(component: &'static str, primary: &'static str, sequence: u16) -> Self {
92        Self::new(Severity::Help, component, primary, sequence)
93    }
94
95    /// Create a success code (S)
96    pub const fn success(component: &'static str, primary: &'static str, sequence: u16) -> Self {
97        Self::new(Severity::Success, component, primary, sequence)
98    }
99
100    /// Create a completed code (K)
101    pub const fn completed(component: &'static str, primary: &'static str, sequence: u16) -> Self {
102        Self::new(Severity::Completed, component, primary, sequence)
103    }
104
105    /// Create an info code (I)
106    pub const fn info(component: &'static str, primary: &'static str, sequence: u16) -> Self {
107        Self::new(Severity::Info, component, primary, sequence)
108    }
109
110    /// Create a trace code (T)
111    pub const fn trace(component: &'static str, primary: &'static str, sequence: u16) -> Self {
112        Self::new(Severity::Trace, component, primary, sequence)
113    }
114
115    /// Get the full error code (e.g., "E.CRYPTO.SALT.001")
116    pub fn code(&self) -> String {
117        format!(
118            "{}.{}.{}.{:03}",
119            self.severity.as_char(),
120            self.component,
121            self.primary,
122            self.sequence
123        )
124    }
125
126    /// Write error code to formatter without allocating
127    ///
128    /// Use in performance-critical paths to avoid String allocation.
129    pub fn write_code(&self, f: &mut impl fmt::Write) -> fmt::Result {
130        write!(
131            f,
132            "{}.{}.{}.{:03}",
133            self.severity.as_char(),
134            self.component,
135            self.primary,
136            self.sequence
137        )
138    }
139
140    /// Get severity
141    pub const fn severity(&self) -> Severity {
142        self.severity
143    }
144
145    /// Get component
146    pub const fn component(&self) -> &'static str {
147        self.component
148    }
149
150    /// Get primary category
151    pub const fn primary(&self) -> &'static str {
152        self.primary
153    }
154
155    /// Get sequence number
156    pub const fn sequence(&self) -> u16 {
157        self.sequence
158    }
159
160    /// Get 5-character base62 hash (requires "hash" feature)
161    ///
162    /// Returns alphanumeric hash safe for all logging systems.
163    /// Provides 916M combinations for collision resistance.
164    #[cfg(feature = "hash")]
165    pub fn hash(&self) -> String {
166        crate::hash::compute_hash(&self.code())
167    }
168}
169
170impl fmt::Display for Code {
171    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
172        write!(
173            f,
174            "{}.{}.{}.{:03}",
175            self.severity.as_char(),
176            self.component,
177            self.primary,
178            self.sequence
179        )
180    }
181}
182
183#[cfg(test)]
184mod tests {
185    use super::*;
186
187    #[cfg(feature = "std")]
188    use std::string::ToString;
189
190    #[cfg(not(feature = "std"))]
191    use alloc::string::ToString;
192
193    #[test]
194    fn test_error_code_format() {
195        const CODE: Code = Code::new(Severity::Error, "CRYPTO", "SALT", 1);
196        assert_eq!(CODE.code(), "E.CRYPTO.SALT.001");
197        assert_eq!(CODE.severity(), Severity::Error);
198        assert_eq!(CODE.component(), "CRYPTO");
199        assert_eq!(CODE.primary(), "SALT");
200        assert_eq!(CODE.sequence(), 1);
201    }
202
203    #[test]
204    fn test_error_code_display() {
205        const CODE: Code = Code::new(Severity::Warning, "PATTERN", "PARSE", 5);
206        assert_eq!(CODE.to_string(), "W.PATTERN.PARSE.005");
207    }
208
209    #[cfg(feature = "hash")]
210    #[test]
211    fn test_error_code_hash() {
212        const CODE: Code = Code::new(Severity::Error, "CRYPTO", "SALT", 1);
213        let hash1 = CODE.hash();
214        let hash2 = CODE.hash();
215        assert_eq!(hash1, hash2); // Deterministic
216        assert_eq!(hash1.len(), 5);
217        assert!(hash1.chars().all(|c| c.is_ascii_alphanumeric()));
218    }
219
220    #[test]
221    fn test_length_validation() {
222        const MIN: Code = Code::new(Severity::Error, "IO", "FS", 1);
223        const MAX: Code = Code::new(Severity::Error, "LONGCOMPONNT", "LONGPRIMARY1", 1);
224        assert_eq!(MIN.component(), "IO");
225        assert_eq!(MAX.component(), "LONGCOMPONNT");
226    }
227}