waddling-errors 0.7.3

Structured, secure-by-default diagnostic codes for distributed systems with no_std and role-based documentation
Documentation
//! Trait-based extensibility for error code components and primaries
//!
//! This module provides a trait-based system that allows users to define their own
//! component and primary types without requiring changes to waddling-errors.
//!
//! # Design Philosophy
//!
//! - **Level 1**: Minimal traits (`ComponentId`, `PrimaryId`) - just `.as_str()`
//! - **Level 2**: Optional documentation traits for basic metadata
//! - **Level 3**: Full error metadata for rich documentation generation
//!
//! Users choose what level fits their needs - no forcing!

use core::fmt;

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

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

// ============================================================================
// LEVEL 1: MINIMAL (Required)
// ============================================================================

/// Trait for component identifiers
///
/// This is the minimal trait needed for error codes. Just implement `.as_str()`.
///
/// # Examples
///
/// ```rust
/// use waddling_errors::ComponentId;
///
/// #[derive(Debug, Clone, Copy)]
/// enum Component {
///     Parser,
///     Network,
/// }
///
/// impl ComponentId for Component {
///     fn as_str(&self) -> &'static str {
///         match self {
///             Component::Parser => "PARSER",
///             Component::Network => "NETWORK",
///         }
///     }
/// }
/// ```
pub trait ComponentId: Copy + fmt::Debug {
    /// Get the component name (e.g., "PARSER", "CRYPTO")
    ///
    /// Must be uppercase, 2-12 characters.
    fn as_str(&self) -> &'static str;
}

/// Trait for primary category identifiers
///
/// This is the minimal trait needed for error codes. Just implement `.as_str()`.
///
/// # Examples
///
/// ```rust
/// use waddling_errors::PrimaryId;
///
/// #[derive(Debug, Clone, Copy)]
/// enum Primary {
///     Syntax,
///     Type,
/// }
///
/// impl PrimaryId for Primary {
///     fn as_str(&self) -> &'static str {
///         match self {
///             Primary::Syntax => "SYNTAX",
///             Primary::Type => "TYPE",
///         }
///     }
/// }
/// ```
pub trait PrimaryId: Copy + fmt::Debug {
    /// Get the primary category name (e.g., "SYNTAX", "TYPE")
    ///
    /// Must be uppercase, 2-12 characters.
    fn as_str(&self) -> &'static str;
}

// ============================================================================
// LEVEL 2: DOCUMENTED (Optional - for basic doc generation)
// ============================================================================

/// Extended trait for components with documentation metadata
///
/// Implement this trait if you want your components to appear in generated documentation.
/// All methods have default implementations, so you only provide what you need.
///
/// # Examples
///
/// ```rust
/// use waddling_errors::{ComponentId, ComponentIdDocumented};
///
/// #[derive(Debug, Clone, Copy)]
/// enum Component {
///     Parser,
/// }
///
/// impl ComponentId for Component {
///     fn as_str(&self) -> &'static str {
///         match self {
///             Component::Parser => "PARSER",
///         }
///     }
/// }
///
/// impl ComponentIdDocumented for Component {
///     fn description(&self) -> Option<&'static str> {
///         Some(match self {
///             Component::Parser => "Syntax parsing and tokenization",
///         })
///     }
///
///     fn tags(&self) -> &'static [&'static str] {
///         match self {
///             Component::Parser => &["frontend", "syntax"],
///         }
///     }
/// }
/// ```
pub trait ComponentIdDocumented: ComponentId {
    /// Human-readable description of this component
    ///
    /// Example: "Parsing phase errors and syntax validation"
    fn description(&self) -> Option<&'static str> {
        None
    }

    /// Example error codes from this component
    ///
    /// Example: `&["E.PARSER.SYNTAX.001", "W.PARSER.STYLE.010"]`
    fn examples(&self) -> &'static [&'static str] {
        &[]
    }

    /// Categories/tags for organization
    ///
    /// Example: `&["frontend", "compile-time"]`
    fn tags(&self) -> &'static [&'static str] {
        &[]
    }
}

/// Extended trait for primaries with documentation metadata
///
/// Implement this trait if you want your primary categories to appear in
/// generated documentation with rich metadata.
///
/// # Examples
///
/// ```rust
/// use waddling_errors::{PrimaryId, PrimaryIdDocumented};
///
/// #[derive(Debug, Clone, Copy)]
/// enum Primary {
///     Syntax,
/// }
///
/// impl PrimaryId for Primary {
///     fn as_str(&self) -> &'static str {
///         match self {
///             Primary::Syntax => "SYNTAX",
///         }
///     }
/// }
///
/// impl PrimaryIdDocumented for Primary {
///     fn description(&self) -> Option<&'static str> {
///         Some("Syntax-level parsing errors")
///     }
/// }
/// ```
pub trait PrimaryIdDocumented: PrimaryId {
    /// Human-readable description of this primary category
    fn description(&self) -> Option<&'static str> {
        None
    }

    /// Example error codes using this primary
    fn examples(&self) -> &'static [&'static str] {
        &[]
    }

    /// Related primary categories
    ///
    /// Example: SYNTAX relates to PARSE, TOKENIZE
    fn related(&self) -> &'static [&'static str] {
        &[]
    }
}

// ============================================================================
// LEVEL 3: FULL METADATA (Optional - for rich documentation)
// ============================================================================

/// Role visibility for documentation generation
///
/// Controls who sees this error in generated documentation.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum Role {
    /// Visible to all users (library consumers, end users)
    Public,
    /// Visible only to team members (internal documentation)
    Internal,
    /// Visible only to library/compiler developers
    Developer,
}

impl Role {
    /// Returns the privilege level for hierarchical comparisons.
    ///
    /// Lower numbers are more restrictive:
    /// - Public: 0 (most restrictive)
    /// - Developer: 1
    /// - Internal: 2 (least restrictive, sees everything)
    pub fn privilege_level(&self) -> u8 {
        match self {
            Role::Public => 0,
            Role::Developer => 1,
            Role::Internal => 2,
        }
    }

    /// Check if this role can view content marked with the given role.
    ///
    /// Visibility is hierarchical:
    /// - Public can only see Public content
    /// - Developer can see Public and Developer content
    /// - Internal can see all content
    /// - Content with None (unspecified) is visible to everyone
    ///
    /// # Examples
    ///
    /// ```
    /// use waddling_errors::traits::Role;
    ///
    /// assert!(Role::Public.can_view(Some(Role::Public)));
    /// assert!(!Role::Public.can_view(Some(Role::Internal)));
    /// assert!(Role::Internal.can_view(Some(Role::Public)));
    /// assert!(Role::Developer.can_view(None)); // Unspecified visible to all
    /// ```
    pub fn can_view(&self, content_role: Option<Role>) -> bool {
        match content_role {
            None => true, // Unspecified content is visible to all roles
            Some(role) => self.privilege_level() >= role.privilege_level(),
        }
    }
}

/// Field-level visibility marker for hints, descriptions, and metadata.
///
/// Allows marking individual hints or descriptions with different visibility levels.
/// For example, a hint might be marked as Internal-only while the error itself is Public.
///
/// # Examples
///
/// ```rust
/// use waddling_errors::FieldMeta;
///
/// let public_hint = FieldMeta::public("Check the API documentation");
/// let internal_hint = FieldMeta::internal("Check Redis connection on host-01");
/// ```
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct FieldMeta {
    /// The actual text content
    pub content: String,
    /// Visibility level for this field
    pub visibility: Role,
}

impl FieldMeta {
    /// Create a public field (visible to everyone)
    pub fn public(content: impl Into<String>) -> Self {
        Self {
            content: content.into(),
            visibility: Role::Public,
        }
    }

    /// Create a developer field (visible to developers)
    pub fn developer(content: impl Into<String>) -> Self {
        Self {
            content: content.into(),
            visibility: Role::Developer,
        }
    }

    /// Create an internal field (visible to internal team only)
    pub fn internal(content: impl Into<String>) -> Self {
        Self {
            content: content.into(),
            visibility: Role::Internal,
        }
    }

    /// Check if this field should be visible at the given role level
    pub fn visible_at(&self, role: Role) -> bool {
        match (role, self.visibility) {
            // Public can see only Public
            (Role::Public, Role::Public) => true,
            (Role::Public, _) => false,
            // Developer can see Public and Developer
            (Role::Developer, Role::Public | Role::Developer) => true,
            (Role::Developer, Role::Internal) => false,
            // Internal can see everything
            (Role::Internal, _) => true,
        }
    }
}

/// Full error metadata for advanced documentation generation
///
/// Implement this trait on specific error types to export complete
/// error definitions to JSON/docs with all metadata.
///
/// # Examples
///
/// ```rust
/// use waddling_errors::{ErrorMetadata, Role};
///
/// struct ParserSyntax001;
///
/// impl ErrorMetadata for ParserSyntax001 {
///     fn code(&self) -> &'static str {
///         "E.PARSER.SYNTAX.001"
///     }
///
///     fn description(&self) -> Option<&'static str> {
///         Some("Expected semicolon at end of statement")
///     }
///
///     fn hints(&self) -> &'static [&'static str] {
///         &["Add a semicolon after the statement"]
///     }
///
///     fn role(&self) -> Role {
///         Role::Public
///     }
/// }
/// ```
pub trait ErrorMetadata {
    /// Get the full error code string (e.g., "E.PARSER.SYNTAX.001")
    fn code(&self) -> &'static str;

    /// Human-readable description
    fn description(&self) -> Option<&'static str> {
        None
    }

    /// Helpful hints for fixing the error
    fn hints(&self) -> &'static [&'static str] {
        &[]
    }

    /// Documentation visibility role
    fn role(&self) -> Role {
        Role::Public
    }

    /// URL to detailed documentation (if available)
    fn docs_url(&self) -> Option<&'static str> {
        None
    }

    /// Example usage/messages
    fn examples(&self) -> &'static [&'static str] {
        &[]
    }

    /// Related error codes
    fn related_codes(&self) -> &'static [&'static str] {
        &[]
    }

    /// Version when this error was introduced
    fn since_version(&self) -> Option<&'static str> {
        None
    }
}