# waddling-errors-macros
**Procedural macros for ergonomic error code definitions**
[](https://crates.io/crates/waddling-errors-macros)
[](https://docs.rs/waddling-errors-macros)
[](../LICENSE)
---
## Overview
This crate provides procedural macros for [waddling-errors](../waddling-errors), reducing boilerplate by ~70% compared to the manual trait-based approach.
**Manual approach:**
```rust
// 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:**
```rust
component! { Auth { value: "AUTH", docs: "Authentication" } }
diag! { E.AUTH.TOKEN.MISSING: { message: "Token missing" } }
```
---
## Installation
```toml
[dependencies]
waddling-errors = "0.7"
waddling-errors-macros = "0.7"
# For documentation generation
[features]
doc-gen = ["waddling-errors-macros/doc-gen", "waddling-errors-macros/metadata"]
```
---
## Quick Start
> ⚠️ **Critical:** You **must** call `setup!` at your crate root before `diag!` will work. This is easy to miss!
### Step 1: Setup at Crate Root (Required)
```rust
// src/lib.rs or src/main.rs - MUST be at crate root level
use waddling_errors_macros::setup;
// Configure where your definitions live
setup! {
components = crate::components,
primaries = crate::primaries,
sequences = crate::sequences,
}
// If using default paths, you can use empty setup:
// setup! {}
```
### Step 2: Define Building Blocks
```rust
// src/components.rs
use waddling_errors_macros::component;
component! {
Auth {
value: "AUTH",
docs: "Authentication and authorization",
tags: ["security", "user-management"],
},
}
```
```rust
// src/primaries.rs
use waddling_errors_macros::primary;
primary! {
Token {
value: "TOKEN",
docs: "Token-related errors",
related: ["Session"],
},
}
```
### Step 3: Define Diagnostics (Anywhere)
```rust
// src/errors.rs (or any module - setup! makes this work)
use waddling_errors_macros::diag;
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
```
### Common Mistake
```rust
// ❌ WRONG: Missing setup! - diag! won't find your definitions
use waddling_errors_macros::diag;
diag! { E.AUTH.TOKEN.001: { message: "Error" } } // Compile error!
// ✅ CORRECT: Add setup! at crate root first
// In lib.rs: setup! { components = crate::components, ... }
```
---
## Macros
### `component!` - Define Components
```rust
component! {
ComponentName {
value: "COMPONENT_VALUE", // Required: UPPER_SNAKE_CASE
docs: "Component description", // Optional
tags: ["tag1", "tag2"], // Optional
related: ["OtherComponent"], // Optional
},
}
```
### `primary!` - Define Primary Categories
```rust
primary! {
CategoryName {
value: "CATEGORY_VALUE", // Required: UPPER_SNAKE_CASE
docs: "Category description", // Optional
tags: ["tag1", "tag2"], // Optional
related: ["OtherCategory"], // Optional
},
}
```
### `sequence!` - Define Sequences
```rust
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
```rust
diag! {
// Optional: Auto-register for doc generation
<json, html, catalog>,
SEVERITY.COMPONENT.PRIMARY.SEQUENCE: {
message: "Error for {{pii/email}} at {{timestamp}}",
fields: [timestamp], // Non-PII fields → {{field}} in message
pii: [email], // PII fields → {{pii/field}} in message
// 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",
},
}
```
**Field Placeholders:**
- `{{field}}` - Non-PII field, sent in `f` object in wire protocol
- `{{pii/field}}` - PII field, sent in `pii.data` object for access-controlled handling
### `component_location!` - Mark Component Locations
Mark files as containing code for a component (for documentation):
```rust
use waddling_errors_macros::component_location;
// Simple: mark file as Auth component (default: internal role)
component_location!(Auth);
// With explicit role
component_location!(Auth, role = public);
component_location!(Database, role = developer);
// Multiple components per file
component_location!(Auth, role = public);
component_location!(Api, role = developer);
```
Auto-registers with `ctor` - no manual registration needed!
### `#[in_component]` - Attribute for Modules
Mark a module as belonging to a component:
```rust
use waddling_errors_macros::in_component;
#[in_component(Auth)]
mod auth_impl {
// Implementation code
}
#[in_component(Auth, role = public)]
mod auth_example {
// Example code for public documentation
}
```
---
## Visibility Markers
Control where metadata appears using visibility markers:
| `'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
```rust
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:
| `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:
```rust
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:**
```rust
// 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:
```rust
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:
```rust
#[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](VALIDATION.md) for complete guide.
---
## Auto-Registration
Automatically register diagnostics for documentation generation:
```rust
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:
```toml
[dependencies]
waddling-errors-macros = { version = "0.7", features = ["auto-register"] }
```
## Documentation Generation Workflow
Complete end-to-end workflow for generating documentation:
### Step 1: Define Errors with Auto-Registration
```rust
// src/errors/mod.rs
use waddling_errors_macros::{component, primary, diag, setup};
// Configure paths (in lib.rs or main.rs)
setup! {
components = crate::errors,
primaries = crate::errors,
}
component! { Auth { value: "AUTH" } }
primary! { Token { value: "TOKEN" } }
diag! {
<json, html>, // Auto-register for these formats
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 hints: ["Check token refresh logic", "Verify server time sync"],
'CR 'Int hints: ["Query auth_tokens table for debug info"],
'R role: "Public",
'R tags: ["auth", "security"],
},
}
```
### Step 2: Create Separate Doc Generation Binary
```rust
// src/bin/doc_gen.rs
use my_app::errors::*; // Imports all errors → triggers auto-registration
use waddling_errors::doc_generator::{DocRegistry, HtmlRenderer, JsonRenderer};
use waddling_errors::registry;
fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut doc_registry = DocRegistry::new(
env!("CARGO_PKG_NAME"),
env!("CARGO_PKG_VERSION")
);
// Transfer auto-registered diagnostics from global registry
registry::register_all_with_doc_gen(&mut doc_registry);
let (diag_count, comp_count, prim_count, seq_count, _) = registry::statistics();
println!("📊 Registered: {} diagnostics, {} components", diag_count, comp_count);
// Generate documentation for all roles
doc_registry.render_all_roles(
vec![Box::new(HtmlRenderer::new()), Box::new(JsonRenderer)],
"target/docs"
)?;
println!("✅ Documentation generated:");
println!(" 📘 target/docs/{}-pub.html", env!("CARGO_PKG_NAME"));
println!(" 👨💻 target/docs/{}-dev.html", env!("CARGO_PKG_NAME"));
println!(" 🔒 target/docs/{}-int.html", env!("CARGO_PKG_NAME"));
Ok(())
}
```
### Step 3: Configure Cargo.toml
```toml
# In your Cargo.toml
[dependencies]
waddling-errors = { version = "0.7", features = ["metadata"] }
waddling-errors-macros = { version = "0.7", features = ["metadata", "auto-register"] }
[dev-dependencies]
waddling-errors = { version = "0.7", features = ["doc-gen", "auto-register"] }
[[bin]]
name = "doc-gen"
path = "src/bin/doc_gen.rs"
required-features = ["waddling-errors/doc-gen", "waddling-errors-macros/auto-register"]
```
### Step 4: Generate Documentation
```bash
# Generate docs
cargo run --bin doc-gen --features waddling-errors/doc-gen,waddling-errors-macros/auto-register
# Or create a cargo alias in .cargo/config.toml:
# [alias]
# docs = "run --bin doc-gen --features waddling-errors/doc-gen,waddling-errors-macros/auto-register"
cargo docs
```
### Why This Pattern?
**✅ Advantages:**
- **Clean main binary**: No doc-gen dependencies, no error imports
- **Zero boilerplate**: `<json, html>` in `diag!` handles registration
- **Separate concerns**: Doc generation isolated from app logic
- **Production-ready**: Main binary has zero doc-gen overhead
- **Idiomatic Rust**: Same pattern as criterion benchmarks, cargo-bench
**Production binary** (`cargo build --release`):
- No serde, serde_json
- No HTML/JSON renderers
- No doc generation code
- Only lightweight error structs with WDP hashes
**Doc-gen binary** (`cargo run --bin doc-gen`):
- Only runs during development/CI
- Never ships to production
- Loads errors → triggers auto-registration → generates docs
---
## Generated Constants
The `diag!` macro generates up to three constants:
```rust
// ✅ 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:**
```rust
// 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:
```bash
# 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/](examples/) directory for all examples.
---
## Manual vs Macro Approach
| **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](../waddling-errors/examples/complete_system) for manual approach comparison.
---
## Features
| `metadata` | Enable compile-time documentation metadata | ❌ |
| `doc-gen` | Enable documentation generation (includes metadata) | ❌ |
| `auto-register` | Automatic diagnostic registration | ❌ |
| `hash` | Base62 hash code generation | ❌ |
---
## Documentation
- **[Examples](examples/README.md)** - Comprehensive examples with explanations
- **[Validation Guide](VALIDATION.md)** - Compile-time validation modes
- **[Component Location Roles](../docs/COMPONENT_LOCATION_ROLES.md)** - File path security
- **[In-Component Attribute](../docs/IN_COMPONENT_ROLE_SUPPORT.md)** - Component tracking
- **[API Documentation](https://docs.rs/waddling-errors-macros)** - Full API reference
---
## 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](LICENSE-MIT) and [LICENSE-APACHE](LICENSE-APACHE).