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}