waddling_errors/
code.rs

1//! Diagnostic code structure
2
3use crate::Severity;
4use crate::traits::{ComponentId, PrimaryId};
5use core::fmt;
6
7#[cfg(feature = "std")]
8use std::{format, string::String};
9
10#[cfg(not(feature = "std"))]
11use alloc::{format, string::String};
12
13/// Waddling diagnostic code: `SEVERITY.COMPONENT.PRIMARY.SEQUENCE`
14///
15/// Format: `E.CRYPTO.SALT.001`
16///
17/// This type is generic over component and primary types that implement
18/// the `ComponentId` and `PrimaryId` traits. This allows full type safety
19/// while maintaining extensibility.
20///
21/// # Examples
22///
23/// Define your own component and primary enums:
24/// ```rust
25/// use waddling_errors::{Code, ComponentId, PrimaryId};
26///
27/// #[derive(Debug, Clone, Copy)]
28/// enum Component { Crypto }
29/// impl ComponentId for Component {
30///     fn as_str(&self) -> &'static str { "CRYPTO" }
31/// }
32///
33/// #[derive(Debug, Clone, Copy)]
34/// enum Primary { Salt }
35/// impl PrimaryId for Primary {
36///     fn as_str(&self) -> &'static str { "SALT" }
37/// }
38///
39/// const ERR: Code<Component, Primary> = Code::error(Component::Crypto, Primary::Salt, 1);
40/// assert_eq!(ERR.code(), "E.CRYPTO.SALT.001");
41/// ```
42#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
43pub struct Code<C: ComponentId, P: PrimaryId> {
44    severity: Severity,
45    component: C,
46    primary: P,
47    sequence: u16,
48}
49
50impl<C: ComponentId, P: PrimaryId> Code<C, P> {
51    /// Create a new code with explicit severity
52    ///
53    /// # Panics
54    ///
55    /// Panics if sequence > 999
56    pub const fn new(severity: Severity, component: C, primary: P, sequence: u16) -> Self {
57        assert!(sequence <= 999, "Sequence must be <= 999");
58
59        Self {
60            severity,
61            component,
62            primary,
63            sequence,
64        }
65    }
66
67    /// Create an error code (E)
68    pub const fn error(component: C, primary: P, sequence: u16) -> Self {
69        Self::new(Severity::Error, component, primary, sequence)
70    }
71
72    /// Create a warning code (W)
73    pub const fn warning(component: C, primary: P, sequence: u16) -> Self {
74        Self::new(Severity::Warning, component, primary, sequence)
75    }
76
77    /// Create a critical code (C)
78    pub const fn critical(component: C, primary: P, sequence: u16) -> Self {
79        Self::new(Severity::Critical, component, primary, sequence)
80    }
81
82    /// Create a blocked code (B)
83    pub const fn blocked(component: C, primary: P, sequence: u16) -> Self {
84        Self::new(Severity::Blocked, component, primary, sequence)
85    }
86
87    /// Create a help code (H)
88    pub const fn help(component: C, primary: P, sequence: u16) -> Self {
89        Self::new(Severity::Help, component, primary, sequence)
90    }
91
92    /// Create a success code (S)
93    pub const fn success(component: C, primary: P, sequence: u16) -> Self {
94        Self::new(Severity::Success, component, primary, sequence)
95    }
96
97    /// Create a completed code (K)
98    pub const fn completed(component: C, primary: P, sequence: u16) -> Self {
99        Self::new(Severity::Completed, component, primary, sequence)
100    }
101
102    /// Create an info code (I)
103    pub const fn info(component: C, primary: P, sequence: u16) -> Self {
104        Self::new(Severity::Info, component, primary, sequence)
105    }
106
107    /// Create a trace code (T)
108    pub const fn trace(component: C, primary: P, sequence: u16) -> Self {
109        Self::new(Severity::Trace, component, primary, sequence)
110    }
111
112    /// Get the full error code (e.g., "E.CRYPTO.SALT.001")
113    pub fn code(&self) -> String {
114        format!(
115            "{}.{}.{}.{:03}",
116            self.severity.as_char(),
117            self.component.as_str(),
118            self.primary.as_str(),
119            self.sequence
120        )
121    }
122
123    /// Write error code to formatter without allocating
124    ///
125    /// Use in performance-critical paths to avoid String allocation.
126    pub fn write_code(&self, f: &mut impl fmt::Write) -> fmt::Result {
127        write!(
128            f,
129            "{}.{}.{}.{:03}",
130            self.severity.as_char(),
131            self.component.as_str(),
132            self.primary.as_str(),
133            self.sequence
134        )
135    }
136
137    /// Get severity
138    pub const fn severity(&self) -> Severity {
139        self.severity
140    }
141
142    /// Get component
143    pub const fn component(&self) -> C {
144        self.component
145    }
146
147    /// Get component as string
148    pub fn component_str(&self) -> &'static str {
149        self.component.as_str()
150    }
151
152    /// Get primary category
153    pub const fn primary(&self) -> P {
154        self.primary
155    }
156
157    /// Get primary as string
158    pub fn primary_str(&self) -> &'static str {
159        self.primary.as_str()
160    }
161
162    /// Get sequence number
163    pub const fn sequence(&self) -> u16 {
164        self.sequence
165    }
166
167    /// Get 5-character base62 hash (requires "hash" feature)
168    ///
169    /// Returns alphanumeric hash safe for all logging systems.
170    /// Provides 916M combinations for collision resistance.
171    #[cfg(feature = "hash")]
172    pub fn hash(&self) -> String {
173        crate::hash::compute_hash(&self.code())
174    }
175}
176
177impl<C: ComponentId, P: PrimaryId> fmt::Display for Code<C, P> {
178    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
179        write!(
180            f,
181            "{}.{}.{}.{:03}",
182            self.severity.as_char(),
183            self.component.as_str(),
184            self.primary.as_str(),
185            self.sequence
186        )
187    }
188}
189
190#[cfg(test)]
191mod tests {
192    use super::*;
193    use crate::traits::{ComponentId, PrimaryId};
194
195    #[cfg(feature = "std")]
196    use std::string::ToString;
197
198    #[cfg(not(feature = "std"))]
199    use alloc::string::ToString;
200
201    #[derive(Debug, Copy, Clone)]
202    enum TestComponent {
203        Crypto,
204        Pattern,
205        Io,
206        LongComponent,
207    }
208
209    impl ComponentId for TestComponent {
210        fn as_str(&self) -> &'static str {
211            match self {
212                TestComponent::Crypto => "CRYPTO",
213                TestComponent::Pattern => "PATTERN",
214                TestComponent::Io => "IO",
215                TestComponent::LongComponent => "LONGCOMPONNT",
216            }
217        }
218    }
219
220    #[derive(Debug, Copy, Clone)]
221    enum TestPrimary {
222        Salt,
223        Parse,
224        Fs,
225        LongPrimary,
226    }
227
228    impl PrimaryId for TestPrimary {
229        fn as_str(&self) -> &'static str {
230            match self {
231                TestPrimary::Salt => "SALT",
232                TestPrimary::Parse => "PARSE",
233                TestPrimary::Fs => "FS",
234                TestPrimary::LongPrimary => "LONGPRIMARY1",
235            }
236        }
237    }
238
239    #[test]
240    fn test_error_code_format() {
241        const CODE: Code<TestComponent, TestPrimary> =
242            Code::new(Severity::Error, TestComponent::Crypto, TestPrimary::Salt, 1);
243        assert_eq!(CODE.code(), "E.CRYPTO.SALT.001");
244        assert_eq!(CODE.severity(), Severity::Error);
245        assert_eq!(CODE.component_str(), "CRYPTO");
246        assert_eq!(CODE.primary_str(), "SALT");
247        assert_eq!(CODE.sequence(), 1);
248    }
249
250    #[test]
251    fn test_error_code_display() {
252        const CODE: Code<TestComponent, TestPrimary> = Code::new(
253            Severity::Warning,
254            TestComponent::Pattern,
255            TestPrimary::Parse,
256            5,
257        );
258        assert_eq!(CODE.to_string(), "W.PATTERN.PARSE.005");
259    }
260
261    #[cfg(feature = "hash")]
262    #[test]
263    fn test_error_code_hash() {
264        const CODE: Code<TestComponent, TestPrimary> =
265            Code::new(Severity::Error, TestComponent::Crypto, TestPrimary::Salt, 1);
266        let hash1 = CODE.hash();
267        let hash2 = CODE.hash();
268        assert_eq!(hash1, hash2); // Deterministic
269        assert_eq!(hash1.len(), 5);
270        assert!(hash1.chars().all(|c| c.is_ascii_alphanumeric()));
271    }
272
273    #[test]
274    fn test_length_validation() {
275        const MIN: Code<TestComponent, TestPrimary> =
276            Code::new(Severity::Error, TestComponent::Io, TestPrimary::Fs, 1);
277        const MAX: Code<TestComponent, TestPrimary> = Code::new(
278            Severity::Error,
279            TestComponent::LongComponent,
280            TestPrimary::LongPrimary,
281            1,
282        );
283        assert_eq!(MIN.component_str(), "IO");
284        assert_eq!(MAX.component_str(), "LONGCOMPONNT");
285    }
286}