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 WDP-conformant hashing which normalizes to uppercase
203        // This ensures Code::hash() produces the same hash as:
204        // - diag! macro compile-time hashes
205        // - compute_wdp_hash() runtime hashes
206        // - Cross-language WDP implementations
207        waddling_errors_hash::compute_wdp_hash(&self.code())
208    }
209
210    /// Get hash with custom configuration
211    ///
212    /// Allows overriding the global configuration for specific use cases
213    /// like multi-tenant systems or per-diagnostic custom algorithms.
214    ///
215    /// **Note:** This function does NOT normalize input. If you need WDP-conformant
216    /// hashing with case-insensitivity, use [`hash()`](Self::hash) instead.
217    ///
218    /// # Examples
219    ///
220    /// ```
221    /// use waddling_errors::{Code, Severity};
222    /// use waddling_errors_hash::HashConfig;
223    /// # use waddling_errors::ComponentId;
224    /// # use waddling_errors::PrimaryId;
225    /// # #[derive(Debug, Copy, Clone)]
226    /// # enum Component { Auth }
227    /// # impl ComponentId for Component {
228    /// #     fn as_str(&self) -> &'static str { "AUTH" }
229    /// # }
230    /// # #[derive(Debug, Copy, Clone)]
231    /// # enum Primary { Token }
232    /// # impl PrimaryId for Primary {
233    /// #     fn as_str(&self) -> &'static str { "TOKEN" }
234    /// # }
235    ///
236    /// let code = Code::error(Component::Auth, Primary::Token, 1);
237    ///
238    /// // Use a custom seed for this specific hash
239    /// # #[cfg(feature = "runtime-hash")]
240    /// let config = HashConfig::with_seed(12345);
241    /// # #[cfg(feature = "runtime-hash")]
242    /// let hash = code.hash_with_config(&config);
243    /// # #[cfg(feature = "runtime-hash")]
244    /// assert_eq!(hash.len(), 5);
245    /// ```
246    #[cfg(feature = "runtime-hash")]
247    pub fn hash_with_config(&self, config: &waddling_errors_hash::HashConfig) -> String {
248        use waddling_errors_hash::compute_hash_with_config;
249        compute_hash_with_config(&self.code(), config)
250    }
251}
252
253impl<C: ComponentId, P: PrimaryId> fmt::Display for Code<C, P> {
254    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
255        write!(
256            f,
257            "{}.{}.{}.{:03}",
258            self.severity.as_char(),
259            self.component.as_str(),
260            self.primary.as_str(),
261            self.sequence
262        )
263    }
264}
265
266#[cfg(test)]
267mod tests {
268    use super::*;
269    use crate::traits::{ComponentId, PrimaryId};
270
271    #[cfg(feature = "std")]
272    use std::string::ToString;
273
274    #[cfg(not(feature = "std"))]
275    use alloc::string::ToString;
276
277    #[derive(Debug, Copy, Clone)]
278    enum TestComponent {
279        Crypto,
280        Pattern,
281        Io,
282        LongComponent,
283    }
284
285    impl ComponentId for TestComponent {
286        fn as_str(&self) -> &'static str {
287            match self {
288                TestComponent::Crypto => "CRYPTO",
289                TestComponent::Pattern => "PATTERN",
290                TestComponent::Io => "IO",
291                TestComponent::LongComponent => "LONGCOMPONNT",
292            }
293        }
294    }
295
296    #[derive(Debug, Copy, Clone)]
297    enum TestPrimary {
298        Salt,
299        Parse,
300        Fs,
301        LongPrimary,
302    }
303
304    impl PrimaryId for TestPrimary {
305        fn as_str(&self) -> &'static str {
306            match self {
307                TestPrimary::Salt => "SALT",
308                TestPrimary::Parse => "PARSE",
309                TestPrimary::Fs => "FS",
310                TestPrimary::LongPrimary => "LONGPRIMARY1",
311            }
312        }
313    }
314
315    #[test]
316    fn test_error_code_format() {
317        const CODE: Code<TestComponent, TestPrimary> =
318            Code::new(Severity::Error, TestComponent::Crypto, TestPrimary::Salt, 1);
319        assert_eq!(CODE.code(), "E.CRYPTO.SALT.001");
320        assert_eq!(CODE.severity(), Severity::Error);
321        assert_eq!(CODE.component_str(), "CRYPTO");
322        assert_eq!(CODE.primary_str(), "SALT");
323        assert_eq!(CODE.sequence(), 1);
324    }
325
326    #[test]
327    fn test_error_code_display() {
328        const CODE: Code<TestComponent, TestPrimary> = Code::new(
329            Severity::Warning,
330            TestComponent::Pattern,
331            TestPrimary::Parse,
332            5,
333        );
334        assert_eq!(CODE.to_string(), "W.PATTERN.PARSE.005");
335    }
336
337    #[cfg(feature = "runtime-hash")]
338    #[test]
339    fn test_error_code_hash() {
340        const CODE: Code<TestComponent, TestPrimary> =
341            Code::new(Severity::Error, TestComponent::Crypto, TestPrimary::Salt, 1);
342        let hash1 = CODE.hash();
343        let hash2 = CODE.hash();
344        assert_eq!(hash1, hash2); // Deterministic
345        assert_eq!(hash1.len(), 5);
346        assert!(hash1.chars().all(|c| c.is_ascii_alphanumeric()));
347    }
348
349    #[test]
350    fn test_length_validation() {
351        const MIN: Code<TestComponent, TestPrimary> =
352            Code::new(Severity::Error, TestComponent::Io, TestPrimary::Fs, 1);
353        const MAX: Code<TestComponent, TestPrimary> = Code::new(
354            Severity::Error,
355            TestComponent::LongComponent,
356            TestPrimary::LongPrimary,
357            1,
358        );
359        assert_eq!(MIN.component_str(), "IO");
360        assert_eq!(MAX.component_str(), "LONGCOMPONNT");
361    }
362}