waddling-errors-macros 0.5.0

Procedural macros for waddling-errors
Documentation

waddling-errors-macros

Procedural macros for ergonomic error code definitions

Crates.io Documentation License


Overview

This crate provides procedural macros for waddling-errors, reducing boilerplate by ~70% compared to the manual trait-based approach.

Manual approach:

// Define enum
#[derive(Debug, Copy, Clone)]
enum Component { Auth }

// Implement trait
impl ComponentId for Component {
    fn as_str(&self) -> &'static str {
        match self { Component::Auth => "AUTH" }
    }
}

// Create constant
const ERR: Code<Component, Primary> = Code::error(Component::Auth, Primary::Token, 1);

Macro approach:

component! { Auth { value: "AUTH", docs: "Authentication" } }
diag! { E.AUTH.TOKEN.MISSING: { message: "Token missing" } }

Installation

[dependencies]

waddling-errors = "0.5"

waddling-errors-macros = "0.5"



# For documentation generation

[features]

doc-gen = ["waddling-errors-macros/doc-gen", "waddling-errors-macros/metadata"]


Quick Start

use waddling_errors_macros::{component, primary, diag};

// Define components
component! {
    Auth {
        value: "AUTH",
        docs: "Authentication and authorization",
        tags: ["security", "user-management"],
    },
}

// Define primary categories
primary! {
    Token {
        value: "TOKEN",
        docs: "Token-related errors",
        related: ["Session"],
    },
}

// Define diagnostics
diag! {
    E.AUTH.TOKEN.EXPIRED: {
        message: "JWT token expired at {timestamp}",
        fields: [timestamp],
        
        'CR 'Pub description: "Your session has expired. Please log in again.",
        'CR 'Dev description: "Token TTL exceeded. Check refresh logic.",
        'CR 'Int description: "Query auth_tokens table for debug info.",
        
        'R role: "Public",
        'R tags: ["auth", "session"],
    },
}

// Use the generated constant
let error = E_AUTH_TOKEN_EXPIRED;
println!("{}", error.runtime.full_code());  // E.AUTH.TOKEN.EXPIRED

Macros

component! - Define Components

component! {
    ComponentName {
        value: "COMPONENT_VALUE",              // Required: UPPER_SNAKE_CASE
        docs: "Component description",         // Optional
        tags: ["tag1", "tag2"],               // Optional
        related: ["OtherComponent"],          // Optional
    },
}

primary! - Define Primary Categories

primary! {
    CategoryName {
        value: "CATEGORY_VALUE",              // Required: UPPER_SNAKE_CASE
        docs: "Category description",         // Optional
        tags: ["tag1", "tag2"],              // Optional
        related: ["OtherCategory"],          // Optional
    },
}

sequence! - Define Sequences

pub mod sequences {
    use waddling_errors_macros::sequence;
    
    sequence! {
        MISSING(1) {
            description: "Required item not provided",
            typical_severity: "Error",
            hints: ["Check required parameters"],
        },
    }
}

diag! - Define Diagnostics

diag! {
    // Optional: Auto-register for doc generation
    <json, html, catalog>,
    
    SEVERITY.COMPONENT.PRIMARY.SEQUENCE: {
        message: "Error message with {field}",
        fields: [field],
        
        // Role-based descriptions
        'CR 'Pub description: "User-facing description",
        'CR 'Dev description: "Developer description",
        'CR 'Int description: "Internal team description",
        
        // Metadata
        'R role: "Public",
        'R tags: ["tag1", "tag2"],
        'C docs_url: "https://docs.example.com/errors",
    },
}

Visibility Markers

Control where metadata appears using visibility markers:

Marker Context Use Case
'C Compile-time only Documentation, code snippets, URLs
'R Runtime only Role, tags, runtime categorization
'CR or 'RC Both contexts Descriptions, hints, messages

Why this matters:

  • Smaller binaries - Documentation doesn't bloat production code
  • Flexible content - Different hints for docs vs runtime
  • Security - Keep sensitive info out of binaries

Example

diag! {
    E.AUTH.SECRET.ROTATION_FAILED: {
        message: "Secret rotation failed",
        
        // Documentation: verbose explanation
        'C description: "Key rotation failed during HSM communication. \
                        Check network connectivity to key management service...",
        
        // Runtime: concise message
        'R description: "Key rotation failed",
        
        // Both: end-user hint
        'CR hints: ["Contact security team if issue persists"],
        
        // Documentation only
        'C code_snippet: {
            wrong: "rotate_key()",
            correct: "rotate_key().with_retry(3)",
        },
        
        // Runtime only
        'R role: "Internal",
    },
}

Field Defaults

When no marker is specified:

Field Default Rationale
description, hints 'CR Useful in both contexts
role, tags, related_codes 'R Runtime behavior
code_snippet, docs_url, introduced, deprecated 'C Documentation only

Role-Based Documentation

Support three documentation roles with gated fields:

diag! {
    E.AUTH.TOKEN.EXPIRED: {
        message: "Token expired",
        
        // Public: sanitized, safe for end users
        'CR 'Pub description: "Your session has expired.",
        'CR 'Pub hints: ["Click login button"],
        
        // Developer: debugging context
        'CR 'Dev description: "JWT token TTL exceeded (3600s default).",
        'CR 'Dev hints: ["Check token refresh logic", "Verify server time sync"],
        
        // Internal: full transparency
        'CR 'Int description: "Token expired. Redis key: auth:token:{user_id}",
        'CR 'Int hints: ["Query auth_tokens table", "Check token_refresh_log"],
        
        'R role: "Public",  // Minimum role to see this error
    },
}

Generated constants access role-specific fields:

// Runtime access
let hints_pub = error.hints_for_role(Role::Public);
let hints_dev = error.hints_for_role(Role::Developer);
let hints_int = error.hints_for_role(Role::Internal);

Component Location Tracking

Mark where components are implemented with role-based filtering:

use waddling_errors_macros::in_component;

// Public documentation example (visible to all)
#[in_component(Auth, role = public)]
mod auth_example {
    // Example code
}

// Internal implementation (default: internal role)
#[in_component(Auth)]
mod auth_internals {
    // Internal implementation
}

// Developer debugging utilities
#[in_component(Auth, role = developer)]
mod auth_debug {
    // Debug utilities
}

Security: File paths are filtered by role in generated documentation!


Compile-Time Validation

Enable strict validation to catch errors at compile time:

#[validate_strict]
component! {
    Auth {
        value: "AUTH",  // Must be UPPER_SNAKE_CASE
        docs: "Authentication",
    }
}

#[validate_strict]
diag! {
    E.AUTH.TOKEN.INVALID: {
        message: "Invalid token",
        // Compiler checks:
        // - Component "AUTH" exists
        // - Primary "TOKEN" exists  
        // - Sequence value is valid
        // - Severity is valid
    }
}

Use #[validate_relaxed] for prototyping.

See VALIDATION.md for complete guide.


Auto-Registration

Automatically register diagnostics for documentation generation:

diag! {
    <json, html, catalog>,  // Auto-register with these renderers
    
    E.AUTH.TOKEN.EXPIRED: {
        message: "Token expired",
        'CR 'Pub description: "Session expired",
    },
}

Enable with auto-register feature:

[dependencies]

waddling-errors-macros = { version = "0.5", features = ["auto-register"] }


Generated Constants

The diag! macro generates up to three constants:

// ✅ Always generated (no feature flags needed)
pub const E_AUTH_TOKEN_EXPIRED: DiagnosticRuntime = /* ... */;

// 📚 Only with 'metadata' feature
#[cfg(feature = "metadata")]
pub const E_AUTH_TOKEN_EXPIRED_DOCS: DiagnosticDocs = /* ... */;

#[cfg(feature = "metadata")]
pub const E_AUTH_TOKEN_EXPIRED_COMPLETE: DiagnosticComplete = /* ... */;

Usage:

// Production: lightweight runtime
let error = E_AUTH_TOKEN_EXPIRED;
println!("Code: {}", error.runtime.full_code());
println!("Message: {}", error.runtime.message);

// Documentation generation: full metadata
#[cfg(feature = "metadata")]
fn generate_docs(registry: &mut DocRegistry) {
    let complete = E_AUTH_TOKEN_EXPIRED_COMPLETE;
    registry.register_diagnostic(&complete)?;
}

Examples

Run examples to see macros in action:

# Complete system with macros (~70% less code than manual)

cargo run --example complete_system --features "metadata,doc-gen,auto-register"


# Browser-server catalog for IoT/mobile

cargo run --example browser_server_catalog --features "metadata,hash"


# Component location security

cargo run --example component_location_security --features "metadata,doc-gen"


# Compile-time validation

cargo run --example strict_validation_demo --features "metadata"


# Custom XML renderer

cargo run --example custom_xml_renderer --features "metadata,doc-gen,auto-register"


# WASM/no_std

cargo run --example no_std_wasm --features "metadata"

See examples/ directory for all examples.


Manual vs Macro Approach

Aspect Manual Macros
Boilerplate High (~100 lines per component) Low (~10 lines)
Type safety Manual trait impl Automatic
Validation Runtime only Compile-time + runtime
Role gating Manual filtering Automatic
Auto-registration Manual calls Automatic (with feature)
Learning curve Steep (traits + generics) Gentle (declarative)
Flexibility Full control Sufficient for most use cases

Use manual approach when:

  • You need maximum flexibility
  • You're integrating with existing trait-based code
  • You want to avoid proc macros

Use macro approach when:

  • You want less boilerplate (~70% reduction)
  • You want compile-time validation
  • You're starting a new project
  • You want automatic doc generation

See waddling-errors/examples/complete_system for manual approach comparison.


Features

Feature Description Default
metadata Enable compile-time documentation metadata
doc-gen Enable documentation generation (includes metadata)
auto-register Automatic diagnostic registration
hash Base62 hash code generation

Documentation


When to Use Macros

✅ Perfect For:

  • New projects starting fresh
  • Teams wanting less boilerplate
  • Projects needing compile-time validation
  • Documentation-heavy error systems
  • Multi-role security requirements

❌ Consider Manual Approach:

  • Maximum flexibility needed
  • Existing trait-based codebase
  • Proc macro avoidance required
  • Learning trait system is goal

License

Dual-licensed under MIT or Apache-2.0. See LICENSE-MIT and LICENSE-APACHE.