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 success code (S)
91    pub const fn success(component: &'static str, primary: &'static str, sequence: u16) -> Self {
92        Self::new(Severity::Success, component, primary, sequence)
93    }
94
95    /// Create a completed code (K)
96    pub const fn completed(component: &'static str, primary: &'static str, sequence: u16) -> Self {
97        Self::new(Severity::Completed, component, primary, sequence)
98    }
99
100    /// Create an info code (I)
101    pub const fn info(component: &'static str, primary: &'static str, sequence: u16) -> Self {
102        Self::new(Severity::Info, component, primary, sequence)
103    }
104
105    /// Create a trace code (T)
106    pub const fn trace(component: &'static str, primary: &'static str, sequence: u16) -> Self {
107        Self::new(Severity::Trace, component, primary, sequence)
108    }
109
110    /// Get the full error code (e.g., "E.CRYPTO.SALT.001")
111    pub fn code(&self) -> String {
112        format!(
113            "{}.{}.{}.{:03}",
114            self.severity.as_char(),
115            self.component,
116            self.primary,
117            self.sequence
118        )
119    }
120
121    /// Write error code to formatter without allocating
122    ///
123    /// Use in performance-critical paths to avoid String allocation.
124    pub fn write_code(&self, f: &mut impl fmt::Write) -> fmt::Result {
125        write!(
126            f,
127            "{}.{}.{}.{:03}",
128            self.severity.as_char(),
129            self.component,
130            self.primary,
131            self.sequence
132        )
133    }
134
135    /// Get severity
136    pub const fn severity(&self) -> Severity {
137        self.severity
138    }
139
140    /// Get component
141    pub const fn component(&self) -> &'static str {
142        self.component
143    }
144
145    /// Get primary category
146    pub const fn primary(&self) -> &'static str {
147        self.primary
148    }
149
150    /// Get sequence number
151    pub const fn sequence(&self) -> u16 {
152        self.sequence
153    }
154
155    /// Get 5-character base62 hash (requires "hash" feature)
156    ///
157    /// Returns alphanumeric hash safe for all logging systems.
158    /// Provides 916M combinations for collision resistance.
159    #[cfg(feature = "hash")]
160    pub fn hash(&self) -> String {
161        crate::hash::compute_hash(&self.code())
162    }
163}
164
165impl fmt::Display for Code {
166    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
167        write!(
168            f,
169            "{}.{}.{}.{:03}",
170            self.severity.as_char(),
171            self.component,
172            self.primary,
173            self.sequence
174        )
175    }
176}
177
178#[cfg(test)]
179mod tests {
180    use super::*;
181
182    #[cfg(feature = "std")]
183    use std::string::ToString;
184
185    #[cfg(not(feature = "std"))]
186    use alloc::string::ToString;
187
188    #[test]
189    fn test_error_code_format() {
190        const CODE: Code = Code::new(Severity::Error, "CRYPTO", "SALT", 1);
191        assert_eq!(CODE.code(), "E.CRYPTO.SALT.001");
192        assert_eq!(CODE.severity(), Severity::Error);
193        assert_eq!(CODE.component(), "CRYPTO");
194        assert_eq!(CODE.primary(), "SALT");
195        assert_eq!(CODE.sequence(), 1);
196    }
197
198    #[test]
199    fn test_error_code_display() {
200        const CODE: Code = Code::new(Severity::Warning, "PATTERN", "PARSE", 5);
201        assert_eq!(CODE.to_string(), "W.PATTERN.PARSE.005");
202    }
203
204    #[cfg(feature = "hash")]
205    #[test]
206    fn test_error_code_hash() {
207        const CODE: Code = Code::new(Severity::Error, "CRYPTO", "SALT", 1);
208        let hash1 = CODE.hash();
209        let hash2 = CODE.hash();
210        assert_eq!(hash1, hash2); // Deterministic
211        assert_eq!(hash1.len(), 5);
212        assert!(hash1.chars().all(|c| c.is_ascii_alphanumeric()));
213    }
214
215    #[test]
216    fn test_length_validation() {
217        const MIN: Code = Code::new(Severity::Error, "IO", "FS", 1);
218        const MAX: Code = Code::new(Severity::Error, "LONGCOMPONNT", "LONGPRIMARY1", 1);
219        assert_eq!(MIN.component(), "IO");
220        assert_eq!(MAX.component(), "LONGCOMPONNT");
221    }
222}