waddling-errors 0.7.3

Structured, secure-by-default diagnostic codes for distributed systems with no_std and role-based documentation
Documentation
//! Diagnostic code structure

use crate::Severity;
use crate::traits::{ComponentId, PrimaryId};
use core::fmt;

#[cfg(feature = "std")]
use std::{format, string::String};

#[cfg(not(feature = "std"))]
use alloc::{format, string::String};

/// Waddling diagnostic code: `SEVERITY.COMPONENT.PRIMARY.SEQUENCE`
///
/// Format: `E.CRYPTO.SALT.001`
///
/// This type is generic over component and primary types that implement
/// the `ComponentId` and `PrimaryId` traits. This allows full type safety
/// while maintaining extensibility.
///
/// # Examples
///
/// Define your own component and primary enums:
/// ```rust
/// use waddling_errors::{Code, ComponentId, PrimaryId};
///
/// #[derive(Debug, Clone, Copy)]
/// enum Component { Crypto }
/// impl ComponentId for Component {
///     fn as_str(&self) -> &'static str { "CRYPTO" }
/// }
///
/// #[derive(Debug, Clone, Copy)]
/// enum Primary { Salt }
/// impl PrimaryId for Primary {
///     fn as_str(&self) -> &'static str { "SALT" }
/// }
///
/// const ERR: Code<Component, Primary> = Code::error(Component::Crypto, Primary::Salt, 1);
/// assert_eq!(ERR.code(), "E.CRYPTO.SALT.001");
/// ```
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct Code<C: ComponentId, P: PrimaryId> {
    severity: Severity,
    component: C,
    primary: P,
    sequence: u16,
}

impl<C: ComponentId, P: PrimaryId> Code<C, P> {
    /// Create a new code with explicit severity
    ///
    /// # Panics
    ///
    /// Panics if sequence > 999
    pub const fn new(severity: Severity, component: C, primary: P, sequence: u16) -> Self {
        assert!(sequence <= 999, "Sequence must be <= 999");

        Self {
            severity,
            component,
            primary,
            sequence,
        }
    }

    /// Create an error code (E)
    pub const fn error(component: C, primary: P, sequence: u16) -> Self {
        Self::new(Severity::Error, component, primary, sequence)
    }

    /// Create a warning code (W)
    pub const fn warning(component: C, primary: P, sequence: u16) -> Self {
        Self::new(Severity::Warning, component, primary, sequence)
    }

    /// Create a critical code (C)
    pub const fn critical(component: C, primary: P, sequence: u16) -> Self {
        Self::new(Severity::Critical, component, primary, sequence)
    }

    /// Create a blocked code (B)
    pub const fn blocked(component: C, primary: P, sequence: u16) -> Self {
        Self::new(Severity::Blocked, component, primary, sequence)
    }

    /// Create a help code (H)
    pub const fn help(component: C, primary: P, sequence: u16) -> Self {
        Self::new(Severity::Help, component, primary, sequence)
    }

    /// Create a success code (S)
    pub const fn success(component: C, primary: P, sequence: u16) -> Self {
        Self::new(Severity::Success, component, primary, sequence)
    }

    /// Create a completed code (K)
    pub const fn completed(component: C, primary: P, sequence: u16) -> Self {
        Self::new(Severity::Completed, component, primary, sequence)
    }

    /// Create an info code (I)
    pub const fn info(component: C, primary: P, sequence: u16) -> Self {
        Self::new(Severity::Info, component, primary, sequence)
    }

    /// Create a trace code (T)
    pub const fn trace(component: C, primary: P, sequence: u16) -> Self {
        Self::new(Severity::Trace, component, primary, sequence)
    }

    /// Get the full error code (e.g., "E.CRYPTO.SALT.001")
    pub fn code(&self) -> String {
        format!(
            "{}.{}.{}.{:03}",
            self.severity.as_char(),
            self.component.as_str(),
            self.primary.as_str(),
            self.sequence
        )
    }

    /// Write error code to formatter without allocating
    ///
    /// Use in performance-critical paths to avoid String allocation.
    pub fn write_code(&self, f: &mut impl fmt::Write) -> fmt::Result {
        write!(
            f,
            "{}.{}.{}.{:03}",
            self.severity.as_char(),
            self.component.as_str(),
            self.primary.as_str(),
            self.sequence
        )
    }

    /// Get severity
    pub const fn severity(&self) -> Severity {
        self.severity
    }

    /// Get component
    pub const fn component(&self) -> C {
        self.component
    }

    /// Get component as string
    pub fn component_str(&self) -> &'static str {
        self.component.as_str()
    }

    /// Get primary category
    pub const fn primary(&self) -> P {
        self.primary
    }

    /// Get primary as string
    pub fn primary_str(&self) -> &'static str {
        self.primary.as_str()
    }

    /// Get sequence number
    pub const fn sequence(&self) -> u16 {
        self.sequence
    }

    /// Get 5-character base62 hash using global configuration
    ///
    /// Uses global hash configuration from:
    /// 1. Environment variables (`WADDLING_HASH_ALGORITHM`, `WADDLING_HASH_SEED`)
    /// 2. Cargo.toml metadata (`[package.metadata.waddling-errors]`)
    /// 3. Default (xxHash64 + "wdp-v1" seed) - WDP conformant
    ///
    /// Returns alphanumeric hash safe for all logging systems.
    /// Provides 916M combinations for collision resistance.
    ///
    /// # Examples
    ///
    /// ```
    /// use waddling_errors::{Code, Severity};
    /// # use waddling_errors::ComponentId;
    /// # use waddling_errors::PrimaryId;
    /// # #[derive(Debug, Copy, Clone)]
    /// # enum Component { Auth }
    /// # impl ComponentId for Component {
    /// #     fn as_str(&self) -> &'static str { "AUTH" }
    /// # }
    /// # #[derive(Debug, Copy, Clone)]
    /// # enum Primary { Token }
    /// # impl PrimaryId for Primary {
    /// #     fn as_str(&self) -> &'static str { "TOKEN" }
    /// # }
    ///
    /// let code = Code::error(Component::Auth, Primary::Token, 1);
    /// # #[cfg(feature = "runtime-hash")]
    /// let hash = code.hash();
    /// # #[cfg(feature = "runtime-hash")]
    /// assert_eq!(hash.len(), 5);
    /// ```
    #[cfg(all(feature = "runtime-hash", feature = "std"))]
    pub fn hash(&self) -> String {
        // Use WDP-conformant hashing which normalizes to uppercase
        // This ensures Code::hash() produces the same hash as:
        // - diag! macro compile-time hashes
        // - compute_wdp_hash() runtime hashes
        // - Cross-language WDP implementations
        waddling_errors_hash::compute_wdp_hash(&self.code())
    }

    /// Get hash with custom configuration
    ///
    /// Allows overriding the global configuration for specific use cases
    /// like multi-tenant systems or per-diagnostic custom algorithms.
    ///
    /// **Note:** This function does NOT normalize input. If you need WDP-conformant
    /// hashing with case-insensitivity, use [`hash()`](Self::hash) instead.
    ///
    /// # Examples
    ///
    /// ```
    /// use waddling_errors::{Code, Severity};
    /// use waddling_errors_hash::HashConfig;
    /// # use waddling_errors::ComponentId;
    /// # use waddling_errors::PrimaryId;
    /// # #[derive(Debug, Copy, Clone)]
    /// # enum Component { Auth }
    /// # impl ComponentId for Component {
    /// #     fn as_str(&self) -> &'static str { "AUTH" }
    /// # }
    /// # #[derive(Debug, Copy, Clone)]
    /// # enum Primary { Token }
    /// # impl PrimaryId for Primary {
    /// #     fn as_str(&self) -> &'static str { "TOKEN" }
    /// # }
    ///
    /// let code = Code::error(Component::Auth, Primary::Token, 1);
    ///
    /// // Use a custom seed for this specific hash
    /// # #[cfg(feature = "runtime-hash")]
    /// let config = HashConfig::with_seed(12345);
    /// # #[cfg(feature = "runtime-hash")]
    /// let hash = code.hash_with_config(&config);
    /// # #[cfg(feature = "runtime-hash")]
    /// assert_eq!(hash.len(), 5);
    /// ```
    #[cfg(feature = "runtime-hash")]
    pub fn hash_with_config(&self, config: &waddling_errors_hash::HashConfig) -> String {
        use waddling_errors_hash::compute_hash_with_config;
        compute_hash_with_config(&self.code(), config)
    }
}

impl<C: ComponentId, P: PrimaryId> fmt::Display for Code<C, P> {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(
            f,
            "{}.{}.{}.{:03}",
            self.severity.as_char(),
            self.component.as_str(),
            self.primary.as_str(),
            self.sequence
        )
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::traits::{ComponentId, PrimaryId};

    #[cfg(feature = "std")]
    use std::string::ToString;

    #[cfg(not(feature = "std"))]
    use alloc::string::ToString;

    #[derive(Debug, Copy, Clone)]
    enum TestComponent {
        Crypto,
        Pattern,
        Io,
        LongComponent,
    }

    impl ComponentId for TestComponent {
        fn as_str(&self) -> &'static str {
            match self {
                TestComponent::Crypto => "CRYPTO",
                TestComponent::Pattern => "PATTERN",
                TestComponent::Io => "IO",
                TestComponent::LongComponent => "LONGCOMPONNT",
            }
        }
    }

    #[derive(Debug, Copy, Clone)]
    enum TestPrimary {
        Salt,
        Parse,
        Fs,
        LongPrimary,
    }

    impl PrimaryId for TestPrimary {
        fn as_str(&self) -> &'static str {
            match self {
                TestPrimary::Salt => "SALT",
                TestPrimary::Parse => "PARSE",
                TestPrimary::Fs => "FS",
                TestPrimary::LongPrimary => "LONGPRIMARY1",
            }
        }
    }

    #[test]
    fn test_error_code_format() {
        const CODE: Code<TestComponent, TestPrimary> =
            Code::new(Severity::Error, TestComponent::Crypto, TestPrimary::Salt, 1);
        assert_eq!(CODE.code(), "E.CRYPTO.SALT.001");
        assert_eq!(CODE.severity(), Severity::Error);
        assert_eq!(CODE.component_str(), "CRYPTO");
        assert_eq!(CODE.primary_str(), "SALT");
        assert_eq!(CODE.sequence(), 1);
    }

    #[test]
    fn test_error_code_display() {
        const CODE: Code<TestComponent, TestPrimary> = Code::new(
            Severity::Warning,
            TestComponent::Pattern,
            TestPrimary::Parse,
            5,
        );
        assert_eq!(CODE.to_string(), "W.PATTERN.PARSE.005");
    }

    #[cfg(feature = "runtime-hash")]
    #[test]
    fn test_error_code_hash() {
        const CODE: Code<TestComponent, TestPrimary> =
            Code::new(Severity::Error, TestComponent::Crypto, TestPrimary::Salt, 1);
        let hash1 = CODE.hash();
        let hash2 = CODE.hash();
        assert_eq!(hash1, hash2); // Deterministic
        assert_eq!(hash1.len(), 5);
        assert!(hash1.chars().all(|c| c.is_ascii_alphanumeric()));
    }

    #[test]
    fn test_length_validation() {
        const MIN: Code<TestComponent, TestPrimary> =
            Code::new(Severity::Error, TestComponent::Io, TestPrimary::Fs, 1);
        const MAX: Code<TestComponent, TestPrimary> = Code::new(
            Severity::Error,
            TestComponent::LongComponent,
            TestPrimary::LongPrimary,
            1,
        );
        assert_eq!(MIN.component_str(), "IO");
        assert_eq!(MAX.component_str(), "LONGCOMPONNT");
    }
}