waddling_errors/
lib.rs

1//! Ultra-minimal diagnostic code system for the Waddling ecosystem
2//!
3//! This crate provides a standardized 4-part diagnostic code format:
4//!
5//! **Format**: `SEVERITY.COMPONENT.PRIMARY.SEQUENCE` → `E.CRYPTO.SALT.001`
6//!
7//! # Quick Start
8//!
9//! ```rust
10//! use waddling_errors::prelude::*;
11//!
12//! // Ergonomic convenience functions
13//! const ERR_SALT: Code = error("CRYPTO", "SALT", 1);
14//!
15//! assert_eq!(ERR_SALT.code(), "E.CRYPTO.SALT.001");
16//! ```
17//!
18//! # Alternative API Styles
19//!
20//! ```rust
21//! use waddling_errors::{Code, Severity};
22//!
23//! // Method style
24//! const ERR: Code = Code::error("CRYPTO", "SALT", 1);
25//!
26//! // Explicit severity
27//! const WARN: Code = Code::new(Severity::Warning, "CRYPTO", "WEAK", 1);
28//! ```
29//!
30//! # Sequence Conventions
31//!
32//! The Waddling ecosystem uses **semantic sequence numbers** for common error patterns:
33//!
34//! | Sequence | Meaning       | Example                                  |
35//! |----------|---------------|------------------------------------------|
36//! | 001      | MISSING       | Required item not provided               |
37//! | 002      | MISMATCH      | Values don't match expected type/length  |
38//! | 003      | INVALID       | Format/validation failed                 |
39//! | 021      | NOTFOUND      | Resource not found                       |
40//! | 025      | CORRUPTED     | Data corrupted/integrity check failed    |
41//! | 031-897  | (project)     | Domain-specific sequences                |
42//! | 999      | COMPLETE      | Full completion                          |
43//!
44//! **Benefits**: `.001` always means "missing" across ALL Waddling projects,
45//! enabling instant recognition and cross-project consistency.
46//!
47//! ```rust
48//! use waddling_errors::prelude::*;
49//!
50//! // Following conventions (recommended)
51//! const ERR_MISSING_SALT: Code = error("CRYPTO", "SALT", 1);  // .001 = MISSING
52//!
53//! const ERR_LENGTH_MISMATCH: Code = error("CRYPTO", "LENGTH", 2);  // .002 = MISMATCH
54//!
55//! // Domain-specific (031-897 range for project logic)
56//! const ERR_HMAC_COMPUTE: Code = error("CRYPTO", "HMAC", 31);
57//! ```
58//!
59//! These conventions are **SHOULD** guidelines (RFC 2119), not requirements.
60//! See repository docs/SEQUENCE-CONVENTIONS.md for complete list and enforcement strategies.
61//!
62//! # Error Registry Pattern
63//!
64//! For larger projects, create a central registry:
65//!
66//! ```rust
67//! // errors.rs - Your project's error registry
68//! pub mod errors {
69//!     use waddling_errors::prelude::*;
70//!     
71//!     // Crypto errors (E.CRYPTO.*)
72//!     pub const SALT_MISSING: Code = error("CRYPTO", "SALT", 1);
73//!     pub const KEY_LENGTH: Code = error("CRYPTO", "LENGTH", 2);
74//!     pub const HMAC_INVALID: Code = error("CRYPTO", "HMAC", 3);
75//!     
76//!     // Parser warnings (W.PARSE.*)
77//!     pub const DEPRECATED_SYNTAX: Code = warning("PARSE", "DEPR", 1);
78//! }
79//! ```
80
81#![no_std]
82#![forbid(unsafe_code)]
83
84#[cfg(feature = "std")]
85extern crate std;
86
87#[cfg(not(feature = "std"))]
88extern crate alloc;
89
90#[cfg(feature = "std")]
91use std::{format, string::String};
92
93#[cfg(not(feature = "std"))]
94use alloc::{format, string::String};
95
96use core::fmt;
97
98// ============================================================================
99// Top-level convenience functions
100// ============================================================================
101
102/// Create an error code (E)
103pub const fn error(component: &'static str, primary: &'static str, sequence: u16) -> Code {
104    Code::error(component, primary, sequence)
105}
106
107/// Create a warning code (W)
108pub const fn warning(component: &'static str, primary: &'static str, sequence: u16) -> Code {
109    Code::warning(component, primary, sequence)
110}
111
112/// Create a critical code (C)
113pub const fn critical(component: &'static str, primary: &'static str, sequence: u16) -> Code {
114    Code::critical(component, primary, sequence)
115}
116
117/// Create a blocked code (B)
118pub const fn blocked(component: &'static str, primary: &'static str, sequence: u16) -> Code {
119    Code::blocked(component, primary, sequence)
120}
121
122/// Create a success code (S)
123pub const fn success(component: &'static str, primary: &'static str, sequence: u16) -> Code {
124    Code::success(component, primary, sequence)
125}
126
127/// Create a completed code (K)
128pub const fn completed(component: &'static str, primary: &'static str, sequence: u16) -> Code {
129    Code::completed(component, primary, sequence)
130}
131
132/// Create an info code (I)
133pub const fn info(component: &'static str, primary: &'static str, sequence: u16) -> Code {
134    Code::info(component, primary, sequence)
135}
136
137/// Create a trace code (T)
138pub const fn trace(component: &'static str, primary: &'static str, sequence: u16) -> Code {
139    Code::trace(component, primary, sequence)
140}
141
142// ============================================================================
143// Prelude module
144// ============================================================================
145
146/// Prelude module for convenient imports
147pub mod prelude {
148    pub use crate::{Code, Severity};
149    pub use crate::{blocked, completed, critical, error, info, success, trace, warning};
150}
151
152// ============================================================================
153// Backward compatibility
154// ============================================================================
155
156/// Backward compatibility alias for `Code`
157pub type ErrorCode = Code;
158
159/// Backward compatibility alias for `Code`
160pub type DiagnosticCode = Code;
161
162// ============================================================================
163// Severity (for 4-part format)
164// ============================================================================
165
166/// Diagnostic message severity level (single-character prefix for 4-part codes)
167///
168/// Represents the severity/type of a diagnostic code. Each severity gets a
169/// single-character prefix in the 4-part format: `SEVERITY.COMPONENT.PRIMARY.SEQUENCE`
170///
171/// # Available Severities
172///
173/// - **Error (E)** - Operation failed
174/// - **Warning (W)** - Potential issue
175/// - **Critical (C)** - Severe issue
176/// - **Blocked (B)** - Execution blocked
177/// - **Success (S)** - Operation succeeded
178/// - **Completed (K)** - Task/phase finished
179/// - **Info (I)** - Informational events
180/// - **Trace (T)** - Execution traces, probes
181///
182/// # Examples
183///
184/// ```rust
185/// use waddling_errors::{Code, Severity};
186///
187/// const ERR: Code = Code::new(Severity::Error, "io", "FILE", 1);
188/// const WARN: Code = Code::new(Severity::Warning, "api", "DEPR", 1);
189///
190/// assert_eq!(ERR.severity().as_char(), 'E');
191/// assert_eq!(WARN.severity().as_char(), 'W');
192/// ```
193#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
194#[repr(u8)]
195pub enum Severity {
196    /// **Error (E)** - Operation failed
197    ///
198    /// Example: invalid input, parse error, type mismatch, auth failure
199    Error = b'E',
200
201    /// **Warning (W)** - Potential issue or caveat
202    ///
203    /// Example: deprecated API, performance concern, edge case
204    Warning = b'W',
205
206    /// **Critical (C)** - Severe issue requiring attention
207    ///
208    /// Example: data corruption, security breach, checksum fail
209    Critical = b'C',
210
211    /// **Blocked (B)** - Execution blocked/waiting
212    ///
213    /// Example: deadlock, I/O wait, network unavailable, resource locked
214    Blocked = b'B',
215
216    /// **Success (S)** - Operation succeeded
217    ///
218    /// Example: compilation successful, tests passed, deployment complete
219    Success = b'S',
220
221    /// **Completed (K)** - Task or phase completed
222    ///
223    /// Example: parsing complete, type-checking done, optimization finished
224    Completed = b'K',
225
226    /// **Info (I)** - General informational events
227    ///
228    /// Example: server started, config loaded, milestone reached, status updates
229    Info = b'I',
230
231    /// **Trace (T)** - Execution traces and instrumentation
232    ///
233    /// Example: probe data, function entry/exit, timing measurements, thread events
234    Trace = b'T',
235}
236
237impl Severity {
238    /// Get the single-character code (E, W, C, B, S, K, I, T)
239    pub const fn as_char(self) -> char {
240        self as u8 as char
241    }
242
243    /// Get the severity level as a number (for ordering/filtering)
244    ///
245    /// Higher numbers = more severe/important (Trace=0 ... Error=7)
246    ///
247    /// Note: Success/Completed are treated as low-priority (similar to Info/Trace)
248    /// since they're positive outcomes that don't require immediate action.
249    pub const fn level(self) -> u8 {
250        match self {
251            Severity::Trace => 0,
252            Severity::Info => 1,
253            Severity::Completed => 2,
254            Severity::Success => 3,
255            Severity::Warning => 4,
256            Severity::Critical => 5,
257            Severity::Blocked => 6,
258            Severity::Error => 7,
259        }
260    }
261
262    /// Check if this severity represents a **positive outcome** (success/completion)
263    ///
264    /// Returns `true` for Success and Completed severities.
265    pub const fn is_positive(self) -> bool {
266        matches!(self, Severity::Success | Severity::Completed)
267    }
268}
269
270impl fmt::Display for Severity {
271    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
272        write!(f, "{}", self.as_char())
273    }
274}
275
276// ============================================================================
277// Code (4-part: SEVERITY.COMPONENT.PRIMARY.SEQUENCE)
278// ============================================================================
279
280/// Waddling diagnostic code: `SEVERITY.COMPONENT.PRIMARY.SEQUENCE`
281///
282/// A lightweight, type-safe diagnostic code for the Waddling ecosystem.
283/// Supports all severity levels (errors, warnings, success, info, traces).
284///
285/// # Format
286///
287/// `SEVERITY.COMPONENT.PRIMARY.SEQUENCE` → `E.CRYPTO.SALT.001`
288///
289/// Where SEVERITY is:
290/// - `E` = Error (operation failed)
291/// - `W` = Warning (potential issue)
292/// - `C` = Critical (severe issue)
293/// - `B` = Blocked (execution blocked)
294/// - `S` = Success (operation succeeded)
295/// - `K` = Completed (task finished)
296/// - `I` = Info (informational events)
297/// - `T` = Trace (execution traces, probes)
298///
299/// # Examples
300///
301/// ```rust
302/// use waddling_errors::{Code, Severity};
303///
304/// // Using constructor methods (recommended)
305/// const ERR_SALT: Code = Code::error("CRYPTO", "SALT", 1);
306/// const WARN_DEPR: Code = Code::warning("API", "DEPR", 1);
307/// const SUCCESS: Code = Code::success("BUILD", "DONE", 999);
308///
309/// assert_eq!(ERR_SALT.code(), "E.CRYPTO.SALT.001");
310/// assert_eq!(WARN_DEPR.code(), "W.API.DEPR.001");
311/// assert_eq!(SUCCESS.code(), "S.BUILD.DONE.999");
312/// ```
313///
314/// # Using the prelude for ergonomic imports
315///
316/// ```rust
317/// use waddling_errors::prelude::*;
318///
319/// const ERR_PARSE: Code = error("parser", "DOM", 1);
320/// const WARN_API: Code = warning("api", "DEPR", 10);
321/// ```
322#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
323pub struct Code {
324    severity: Severity,
325    component: &'static str,
326    primary: &'static str,
327    sequence: u16,
328}
329
330impl Code {
331    /// Maximum length for component and primary strings
332    const MAX_LEN: usize = 12;
333
334    /// Minimum length for component and primary strings
335    const MIN_LEN: usize = 2;
336
337    /// Create a new code with explicit severity
338    ///
339    /// # Panics
340    ///
341    /// Panics if:
342    /// - sequence > 999
343    /// - component length < 2 or > 12
344    /// - primary length < 2 or > 12
345    pub const fn new(
346        severity: Severity,
347        component: &'static str,
348        primary: &'static str,
349        sequence: u16,
350    ) -> Self {
351        assert!(sequence <= 999, "Sequence must be <= 999");
352
353        // Validate component length
354        let component_len = component.len();
355        assert!(
356            component_len >= Self::MIN_LEN && component_len <= Self::MAX_LEN,
357            "Component must be 2-12 characters"
358        );
359
360        // Validate primary length
361        let primary_len = primary.len();
362        assert!(
363            primary_len >= Self::MIN_LEN && primary_len <= Self::MAX_LEN,
364            "Primary must be 2-12 characters"
365        );
366
367        Self {
368            severity,
369            component,
370            primary,
371            sequence,
372        }
373    }
374
375    /// Create an error code (E)
376    ///
377    /// # Examples
378    ///
379    /// ```rust
380    /// use waddling_errors::Code;
381    ///
382    /// const ERR: Code = Code::error("CRYPTO", "SALT", 1);
383    /// assert_eq!(ERR.code(), "E.CRYPTO.SALT.001");
384    /// ```
385    pub const fn error(component: &'static str, primary: &'static str, sequence: u16) -> Self {
386        Self::new(Severity::Error, component, primary, sequence)
387    }
388
389    /// Create a warning code (W)
390    pub const fn warning(component: &'static str, primary: &'static str, sequence: u16) -> Self {
391        Self::new(Severity::Warning, component, primary, sequence)
392    }
393
394    /// Create a critical code (C)
395    pub const fn critical(component: &'static str, primary: &'static str, sequence: u16) -> Self {
396        Self::new(Severity::Critical, component, primary, sequence)
397    }
398
399    /// Create a blocked code (B)
400    pub const fn blocked(component: &'static str, primary: &'static str, sequence: u16) -> Self {
401        Self::new(Severity::Blocked, component, primary, sequence)
402    }
403
404    /// Create a success code (S)
405    pub const fn success(component: &'static str, primary: &'static str, sequence: u16) -> Self {
406        Self::new(Severity::Success, component, primary, sequence)
407    }
408
409    /// Create a completed code (K)
410    pub const fn completed(component: &'static str, primary: &'static str, sequence: u16) -> Self {
411        Self::new(Severity::Completed, component, primary, sequence)
412    }
413
414    /// Create an info code (I)
415    pub const fn info(component: &'static str, primary: &'static str, sequence: u16) -> Self {
416        Self::new(Severity::Info, component, primary, sequence)
417    }
418
419    /// Create a trace code (T)
420    pub const fn trace(component: &'static str, primary: &'static str, sequence: u16) -> Self {
421        Self::new(Severity::Trace, component, primary, sequence)
422    }
423
424    /// Get the full error code (e.g., "E.CRYPTO.SALT.001")
425    pub fn code(&self) -> String {
426        format!(
427            "{}.{}.{}.{:03}",
428            self.severity.as_char(),
429            self.component,
430            self.primary,
431            self.sequence
432        )
433    }
434
435    /// Write error code to formatter without allocating (e.g., "E.CRYPTO.SALT.001")
436    ///
437    /// Use this in performance-critical paths to avoid String allocation.
438    ///
439    /// # Examples
440    ///
441    /// ```rust
442    /// use waddling_errors::{ErrorCode, Severity};
443    /// use std::fmt::Write;
444    ///
445    /// let code = ErrorCode::new(Severity::Error, "CRYPTO", "SALT", 1);
446    /// let mut buf = String::new();
447    /// code.write_code(&mut buf).unwrap();
448    /// assert_eq!(buf, "E.CRYPTO.SALT.001");
449    /// ```
450    pub fn write_code(&self, f: &mut impl fmt::Write) -> fmt::Result {
451        write!(
452            f,
453            "{}.{}.{}.{:03}",
454            self.severity.as_char(),
455            self.component,
456            self.primary,
457            self.sequence
458        )
459    }
460
461    /// Get severity (e.g., `Severity::Error`)
462    pub const fn severity(&self) -> Severity {
463        self.severity
464    }
465
466    /// Get component (e.g., "CRYPTO")
467    pub const fn component(&self) -> &'static str {
468        self.component
469    }
470
471    /// Get primary category (e.g., "SALT")
472    pub const fn primary(&self) -> &'static str {
473        self.primary
474    }
475
476    /// Get sequence number (e.g., 1)
477    pub const fn sequence(&self) -> u16 {
478        self.sequence
479    }
480
481    /// Get 5-character base62 hash (requires "hash" feature)
482    ///
483    /// Returns alphanumeric hash (0-9, A-Z, a-z only) safe for all logging systems.
484    /// Provides excellent collision resistance: 916M combinations.
485    #[cfg(feature = "hash")]
486    pub fn hash(&self) -> String {
487        crate::hash::compute_hash(&self.code())
488    }
489}
490
491impl fmt::Display for Code {
492    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
493        write!(
494            f,
495            "{}.{}.{}.{:03}",
496            self.severity.as_char(),
497            self.component,
498            self.primary,
499            self.sequence
500        )
501    }
502}
503
504// ============================================================================
505// Optional ahash hashing
506// ============================================================================
507
508#[cfg(feature = "hash")]
509mod hash {
510    use ahash::RandomState;
511    use core::hash::{BuildHasher, Hash, Hasher};
512
513    #[cfg(feature = "std")]
514    use std::string::String;
515
516    #[cfg(not(feature = "std"))]
517    use alloc::string::String;
518
519    /// Compute a 5-character hash from input string using base62 encoding
520    ///
521    /// Uses ahash with "Waddling" salt and encodes as base62 (alphanumeric only).
522    ///
523    /// **Collision resistance:**
524    /// - Hex (base16, 4 chars): 65,536 combinations
525    /// - Base62 (5 chars): 916,132,832 combinations (~14,000x better!)
526    ///
527    /// Base62 uses only safe alphanumeric: 0-9, A-Z, a-z (no special chars that might break logging/parsing)
528    pub(crate) fn compute_hash(input: &str) -> String {
529        // Create a deterministic hasher with "Waddling" salt
530        const SALT: &str = "Waddling";
531        let state = RandomState::with_seeds(
532            0x576164646c696e67, // "Waddling" as u64 (partial)
533            0x0000000000000000,
534            0x0000000000000000,
535            0x0000000000000000,
536        );
537
538        let mut hasher = state.build_hasher();
539        SALT.hash(&mut hasher);
540        input.hash(&mut hasher);
541        let result = hasher.finish();
542
543        // Take first 5 bytes for excellent distribution
544        let bytes = [
545            (result >> 56) as u8,
546            (result >> 48) as u8,
547            (result >> 40) as u8,
548            (result >> 32) as u8,
549            (result >> 24) as u8,
550        ];
551
552        // Convert to base62 encoding (alphanumeric only - safe for all logging systems)
553        // 0-9, A-Z, a-z (62 chars total)
554        const BASE62_CHARS: &[u8] =
555            b"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
556        const BASE: u64 = 62;
557
558        // Convert bytes to u64 for base62 conversion
559        let mut num = 0u64;
560        for &byte in &bytes {
561            num = (num << 8) | (byte as u64);
562        }
563
564        // Convert to base62 (5 chars)
565        let mut result_chars = [0u8; 5];
566        let mut n = num;
567
568        for i in (0..5).rev() {
569            result_chars[i] = BASE62_CHARS[(n % BASE) as usize];
570            n /= BASE;
571        }
572
573        // Convert to String (all chars are valid ASCII alphanumeric)
574        String::from_utf8(result_chars.to_vec()).expect("base62 encoding produces valid UTF-8")
575    }
576}
577
578// ============================================================================
579// Tests
580// ============================================================================
581
582#[cfg(test)]
583mod tests {
584    use super::*;
585
586    #[cfg(feature = "std")]
587    use std::string::ToString;
588
589    #[cfg(not(feature = "std"))]
590    use alloc::string::ToString;
591
592    #[test]
593    fn test_severity_ordering() {
594        assert!(Severity::Trace.level() < Severity::Error.level());
595        assert!(Severity::Warning.level() < Severity::Critical.level());
596    }
597
598    #[test]
599    fn test_severity_positive() {
600        // Positive outcomes
601        assert!(Severity::Success.is_positive());
602        assert!(Severity::Completed.is_positive());
603
604        // Not positive
605        assert!(!Severity::Error.is_positive());
606        assert!(!Severity::Warning.is_positive());
607        assert!(!Severity::Critical.is_positive());
608        assert!(!Severity::Blocked.is_positive());
609        assert!(!Severity::Info.is_positive());
610        assert!(!Severity::Trace.is_positive());
611    }
612
613    #[test]
614    fn test_error_code_format() {
615        const CODE: Code = Code::new(Severity::Error, "CRYPTO", "SALT", 1);
616        assert_eq!(CODE.code(), "E.CRYPTO.SALT.001");
617        assert_eq!(CODE.severity(), Severity::Error);
618        assert_eq!(CODE.component(), "CRYPTO");
619        assert_eq!(CODE.primary(), "SALT");
620        assert_eq!(CODE.sequence(), 1);
621    }
622
623    #[test]
624    fn test_error_code_display() {
625        const CODE: Code = Code::new(Severity::Warning, "PATTERN", "PARSE", 5);
626        assert_eq!(CODE.to_string(), "W.PATTERN.PARSE.005");
627    }
628
629    #[cfg(feature = "hash")]
630    #[test]
631    fn test_error_code_hash() {
632        const CODE: Code = Code::new(Severity::Error, "CRYPTO", "SALT", 1);
633        let hash1 = CODE.hash();
634        let hash2 = CODE.hash();
635        assert_eq!(hash1, hash2); // Deterministic
636        assert_eq!(hash1.len(), 5); // 5 base62 chars
637
638        // Verify only alphanumeric chars (safe for logging)
639        assert!(hash1.chars().all(|c| c.is_ascii_alphanumeric()));
640    }
641
642    #[test]
643    fn test_length_validation() {
644        // Valid lengths (2-12 chars)
645        const MIN: Code = Code::new(Severity::Error, "IO", "FS", 1);
646        const MAX: Code = Code::new(Severity::Error, "LONGCOMPONNT", "LONGPRIMARY1", 1);
647        assert_eq!(MIN.component(), "IO");
648        assert_eq!(MAX.component(), "LONGCOMPONNT");
649
650        // These will fail at compile-time, not runtime, so we document them:
651        // const TOO_SHORT_COMPONENT: Code = Code::new(Severity::Error, "X", "VALID", 1); // Compile error!
652        // const TOO_LONG_COMPONENT: Code = Code::new(Severity::Error, "THISISSOTOOLONG", "VALID", 1); // Compile error!
653        // const TOO_SHORT_PRIMARY: Code = Code::new(Severity::Error, "VALID", "Y", 1); // Compile error!
654        // const TOO_LONG_PRIMARY: Code = Code::new(Severity::Error, "VALID", "WAYTOOLONGPRIM", 1); // Compile error!
655    }
656}