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 using global configuration
168    ///
169    /// Uses global hash configuration from:
170    /// 1. Environment variables (`WADDLING_HASH_ALGORITHM`, `WADDLING_HASH_SEED`)
171    /// 2. Cargo.toml metadata (`[package.metadata.waddling-errors]`)
172    /// 3. Default (xxHash64 + "wdp-v1" seed) - WDP conformant
173    ///
174    /// Returns alphanumeric hash safe for all logging systems.
175    /// Provides 916M combinations for collision resistance.
176    ///
177    /// # Examples
178    ///
179    /// ```
180    /// use waddling_errors::{Code, Severity};
181    /// # use waddling_errors::ComponentId;
182    /// # use waddling_errors::PrimaryId;
183    /// # #[derive(Debug, Copy, Clone)]
184    /// # enum Component { Auth }
185    /// # impl ComponentId for Component {
186    /// #     fn as_str(&self) -> &'static str { "AUTH" }
187    /// # }
188    /// # #[derive(Debug, Copy, Clone)]
189    /// # enum Primary { Token }
190    /// # impl PrimaryId for Primary {
191    /// #     fn as_str(&self) -> &'static str { "TOKEN" }
192    /// # }
193    ///
194    /// let code = Code::error(Component::Auth, Primary::Token, 1);
195    /// # #[cfg(feature = "runtime-hash")]
196    /// let hash = code.hash();
197    /// # #[cfg(feature = "runtime-hash")]
198    /// assert_eq!(hash.len(), 5);
199    /// ```
200    #[cfg(all(feature = "runtime-hash", feature = "std"))]
201    pub fn hash(&self) -> String {
202        use waddling_errors_hash::{compute_hash_with_config, load_global_config};
203        let config = load_global_config();
204        compute_hash_with_config(&self.code(), &config)
205    }
206
207    /// Get hash with custom configuration
208    ///
209    /// Allows overriding the global configuration for specific use cases
210    /// like multi-tenant systems or per-diagnostic custom algorithms.
211    ///
212    /// # Examples
213    ///
214    /// ```
215    /// use waddling_errors::{Code, Severity};
216    /// use waddling_errors_hash::HashConfig;
217    /// # use waddling_errors::ComponentId;
218    /// # use waddling_errors::PrimaryId;
219    /// # #[derive(Debug, Copy, Clone)]
220    /// # enum Component { Auth }
221    /// # impl ComponentId for Component {
222    /// #     fn as_str(&self) -> &'static str { "AUTH" }
223    /// # }
224    /// # #[derive(Debug, Copy, Clone)]
225    /// # enum Primary { Token }
226    /// # impl PrimaryId for Primary {
227    /// #     fn as_str(&self) -> &'static str { "TOKEN" }
228    /// # }
229    ///
230    /// let code = Code::error(Component::Auth, Primary::Token, 1);
231    ///
232    /// // Use a custom seed for this specific hash
233    /// # #[cfg(feature = "runtime-hash")]
234    /// let config = HashConfig::with_seed(12345);
235    /// # #[cfg(feature = "runtime-hash")]
236    /// let hash = code.hash_with_config(&config);
237    /// # #[cfg(feature = "runtime-hash")]
238    /// assert_eq!(hash.len(), 5);
239    /// ```
240    #[cfg(feature = "runtime-hash")]
241    pub fn hash_with_config(&self, config: &waddling_errors_hash::HashConfig) -> String {
242        use waddling_errors_hash::compute_hash_with_config;
243        compute_hash_with_config(&self.code(), config)
244    }
245}
246
247impl<C: ComponentId, P: PrimaryId> fmt::Display for Code<C, P> {
248    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
249        write!(
250            f,
251            "{}.{}.{}.{:03}",
252            self.severity.as_char(),
253            self.component.as_str(),
254            self.primary.as_str(),
255            self.sequence
256        )
257    }
258}
259
260#[cfg(test)]
261mod tests {
262    use super::*;
263    use crate::traits::{ComponentId, PrimaryId};
264
265    #[cfg(feature = "std")]
266    use std::string::ToString;
267
268    #[cfg(not(feature = "std"))]
269    use alloc::string::ToString;
270
271    #[derive(Debug, Copy, Clone)]
272    enum TestComponent {
273        Crypto,
274        Pattern,
275        Io,
276        LongComponent,
277    }
278
279    impl ComponentId for TestComponent {
280        fn as_str(&self) -> &'static str {
281            match self {
282                TestComponent::Crypto => "CRYPTO",
283                TestComponent::Pattern => "PATTERN",
284                TestComponent::Io => "IO",
285                TestComponent::LongComponent => "LONGCOMPONNT",
286            }
287        }
288    }
289
290    #[derive(Debug, Copy, Clone)]
291    enum TestPrimary {
292        Salt,
293        Parse,
294        Fs,
295        LongPrimary,
296    }
297
298    impl PrimaryId for TestPrimary {
299        fn as_str(&self) -> &'static str {
300            match self {
301                TestPrimary::Salt => "SALT",
302                TestPrimary::Parse => "PARSE",
303                TestPrimary::Fs => "FS",
304                TestPrimary::LongPrimary => "LONGPRIMARY1",
305            }
306        }
307    }
308
309    #[test]
310    fn test_error_code_format() {
311        const CODE: Code<TestComponent, TestPrimary> =
312            Code::new(Severity::Error, TestComponent::Crypto, TestPrimary::Salt, 1);
313        assert_eq!(CODE.code(), "E.CRYPTO.SALT.001");
314        assert_eq!(CODE.severity(), Severity::Error);
315        assert_eq!(CODE.component_str(), "CRYPTO");
316        assert_eq!(CODE.primary_str(), "SALT");
317        assert_eq!(CODE.sequence(), 1);
318    }
319
320    #[test]
321    fn test_error_code_display() {
322        const CODE: Code<TestComponent, TestPrimary> = Code::new(
323            Severity::Warning,
324            TestComponent::Pattern,
325            TestPrimary::Parse,
326            5,
327        );
328        assert_eq!(CODE.to_string(), "W.PATTERN.PARSE.005");
329    }
330
331    #[cfg(feature = "runtime-hash")]
332    #[test]
333    fn test_error_code_hash() {
334        const CODE: Code<TestComponent, TestPrimary> =
335            Code::new(Severity::Error, TestComponent::Crypto, TestPrimary::Salt, 1);
336        let hash1 = CODE.hash();
337        let hash2 = CODE.hash();
338        assert_eq!(hash1, hash2); // Deterministic
339        assert_eq!(hash1.len(), 5);
340        assert!(hash1.chars().all(|c| c.is_ascii_alphanumeric()));
341    }
342
343    #[test]
344    fn test_length_validation() {
345        const MIN: Code<TestComponent, TestPrimary> =
346            Code::new(Severity::Error, TestComponent::Io, TestPrimary::Fs, 1);
347        const MAX: Code<TestComponent, TestPrimary> = Code::new(
348            Severity::Error,
349            TestComponent::LongComponent,
350            TestPrimary::LongPrimary,
351            1,
352        );
353        assert_eq!(MIN.component_str(), "IO");
354        assert_eq!(MAX.component_str(), "LONGCOMPONNT");
355    }
356}