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}