waddling_errors_macros/lib.rs
1//! Procedural macros for waddling-errors
2//!
3//! This crate provides convenient macros for defining error code components,
4//! primaries, and sequences with rich metadata support.
5//!
6//! # Macros
7//!
8//! - `component!` - Define component enums with automatic ComponentId impl
9//! - `primary!` - Define primary enums with automatic PrimaryId impl
10//! - `sequence!` - Define sequence constants with metadata
11//!
12//! # Examples
13//!
14//! ```ignore
15//! use waddling_errors_macros::{component, primary, sequence};
16//!
17//! component! {
18//! Auth,
19//! Database {
20//! docs: "Database operations",
21//! tags: ["backend"],
22//! },
23//! }
24//!
25//! primary! {
26//! Token,
27//! Connection {
28//! docs: "Connection errors",
29//! },
30//! }
31//!
32//! sequence! {
33//! MISSING(001) {
34//! description: "Item not found",
35//! hints: ["Check if item exists"],
36//! },
37//! }
38//! ```
39
40use proc_macro::TokenStream;
41
42mod component;
43mod component_location;
44mod diag;
45mod doc_gen_attr;
46mod doc_gen_macro;
47mod in_component;
48mod primary;
49mod sequence;
50mod setup;
51
52/// Define component enums with automatic ComponentId trait implementation.
53///
54/// # WDP Naming Convention
55///
56/// Per WDP specification, component names SHOULD use **PascalCase**:
57/// - ✅ `Auth`, `Database`, `ApiGateway`, `TokenValidator`
58/// - ❌ `AUTH`, `DATABASE`, `API_GATEWAY` (non-standard)
59///
60/// Error codes follow the format: `E.Component.Primary.SEQUENCE`
61/// - Example: `E.Auth.Token.EXPIRED` not `E.AUTH.TOKEN.EXPIRED`
62///
63/// # Metadata Fields
64///
65/// Supports all metadata fields in any order:
66/// - `description` - Documentation string
67/// - `introduced` - Version introduced
68/// - `deprecated` - Deprecation notice
69/// - `examples` - Array of example error codes
70/// - `tags` - Array of category tags
71///
72/// # Examples
73///
74/// ```ignore
75/// component! {
76/// Auth, // Simple: "Auth"
77/// Database { // With metadata
78/// description: "Database operations",
79/// introduced: "1.0.0",
80/// tags: ["backend", "storage"],
81/// },
82/// }
83///
84/// // Generated code implements ComponentIdDocumented trait:
85/// let desc = Component::Database.description(); // Trait method
86/// let tags = Component::Database.tags();
87/// ```
88#[proc_macro]
89pub fn component(input: TokenStream) -> TokenStream {
90 component::expand(input)
91}
92
93/// Define primary enums with automatic PrimaryId trait implementation.
94///
95/// # WDP Naming Convention
96///
97/// Per WDP specification, primary names SHOULD use **PascalCase**:
98/// - ✅ `Token`, `Connection`, `Validation`, `RateLimit`
99/// - ❌ `TOKEN`, `CONNECTION`, `VALIDATION` (non-standard)
100///
101/// Error codes follow the format: `E.Component.Primary.SEQUENCE`
102/// - Example: `E.Database.Connection.TIMEOUT` not `E.DATABASE.CONNECTION.TIMEOUT`
103///
104/// # Metadata Fields
105///
106/// Supports all metadata fields in any order:
107/// - `description` - Documentation string
108/// - `introduced` - Version introduced
109/// - `deprecated` - Deprecation notice
110/// - `examples` - Array of example error codes
111/// - `related` - Array of related primary codes
112///
113/// # Examples
114///
115/// ```ignore
116/// primary! {
117/// Token, // Simple: "Token"
118/// Connection { // With metadata
119/// description: "Connection errors",
120/// related: ["Timeout", "Pool"],
121/// },
122/// }
123///
124/// // Generated code implements PrimaryIdDocumented trait:
125/// let desc = Primary::Connection.description(); // Trait method
126/// let related = Primary::Connection.related();
127/// ```
128#[proc_macro]
129pub fn primary(input: TokenStream) -> TokenStream {
130 primary::expand(input)
131}
132
133/// Define sequence constants with rich metadata.
134///
135/// Supports all 5 metadata fields in any order:
136/// - `description` - Human-readable description
137/// - `typical_severity` - Typical severity level
138/// - `hints` - Array of helpful hints
139/// - `related` - Array of related sequences
140/// - `introduced` - Version introduced
141///
142/// # Examples
143///
144/// ```ignore
145/// sequence! {
146/// MISSING(001), // Simple
147/// CUSTOM(042) { // With metadata
148/// description: "Custom logic error",
149/// hints: ["Check business rules", "Verify input"],
150/// related: ["043", "044"],
151/// introduced: "1.2.0",
152/// },
153/// }
154/// ```
155#[proc_macro]
156pub fn sequence(input: TokenStream) -> TokenStream {
157 sequence::expand(input)
158}
159
160/// Define complete diagnostic metadata with full 4-part error codes.
161///
162/// **This macro generates THREE constants**:
163/// 1. `E_COMPONENT_PRIMARY_SEQUENCE` - Runtime metadata (always present)
164/// 2. `E_COMPONENT_PRIMARY_SEQUENCE_DOCS` - Compile-time docs (with `metadata` feature)
165/// 3. `E_COMPONENT_PRIMARY_SEQUENCE_COMPLETE` - Combined (with `metadata` feature)
166///
167/// # 4-Part Code Format
168///
169/// `Severity.Component.Primary.Sequence`
170///
171/// # Visibility Markers
172///
173/// Fields can have visibility markers to control where they're included:
174/// - `'C` - Compile-time only (documentation generation, not in binary)
175/// - `'R` - Runtime only (in production binary, not in docs)
176/// - `'RC` or `'CR` - Both (default for most fields)
177///
178/// **Mnemonic**: `'CR` = Compile + Runtime (both contexts)
179///
180/// # Supported Fields (in any order)
181///
182/// **Core fields (always present):**
183/// - `message` (required) - Message template with `{{field}}` placeholders
184/// - `fields` (optional) - Array of non-PII field names used in message (referenced as `{{field}}`)
185/// - `pii` (optional) - Array of PII field names (referenced as `{{pii/field}}` in message)
186/// - `severity` (optional) - Override the severity from the code
187///
188/// **Fields with visibility markers:**
189/// - `description ['C|'R|'RC|'CR]` - Human-readable description
190/// - `hints ['C|'R|'RC|'CR]` - Array of helpful hints
191/// - `role ['R|'RC|'CR]` - Role-based visibility
192/// - `tags ['R]` - Categorization tags (runtime only)
193/// - `related_codes ['R]` - Related error codes (runtime only)
194/// - `code_snippet ['C]` - Code examples (compile-time only)
195/// - `docs_url ['C]` - Documentation URL (compile-time only)
196/// - `introduced ['C]` - Version introduced (compile-time only)
197/// - `deprecated ['C]` - Deprecation notice (compile-time only)
198///
199/// # Examples
200///
201/// **Basic usage (defaults to 'RC - both contexts):**
202/// ```ignore
203/// diag! {
204/// Error.Auth.Token.MISSING: {
205/// message: "Token '{{token}}' not found",
206/// fields: [token],
207/// description: "API token missing", // Available in both
208/// hints: ["Check your credentials"], // Available in both
209/// },
210/// }
211/// ```
212///
213/// **With PII fields:**
214/// ```ignore
215/// diag! {
216/// Error.Auth.Login.FAILED: {
217/// message: "Login failed for {{pii/email}} at {{timestamp}}",
218/// fields: [timestamp], // Non-PII: goes to `f` in wire protocol
219/// pii: [email], // PII: goes to `pii.data` in wire protocol
220/// description: "Authentication failed",
221/// },
222/// }
223/// ```
224///
225/// **Advanced usage with visibility markers:**
226/// ```ignore
227/// diag! {
228/// Error.Auth.Token.MISSING: {
229/// message: "Token '{{token}}' not found",
230/// fields: [token],
231///
232/// // Verbose docs for compile-time
233/// description 'C: "Detailed explanation for documentation...",
234///
235/// // Short message for runtime
236/// description 'R: "Token missing",
237///
238/// // Different hints for different audiences
239/// hints 'C: ["For developers: Check auth flow", "See RFC 6750"],
240/// hints 'R: ["Include Authorization header", "Verify API key"],
241/// hints 'CR: ["Contact support if needed"], // 'RC also works
242///
243/// // Runtime categorization
244/// role 'R: "Public",
245/// tags: ["auth", "security"],
246///
247/// // Compile-time documentation
248/// code_snippet: {
249/// wrong: "fetch(url)",
250/// correct: "fetch(url, { headers: { 'Authorization': 'Bearer <token>' } })",
251/// },
252/// docs_url: "https://docs.example.com/auth",
253/// introduced: "1.0.0",
254/// },
255/// }
256/// ```
257///
258/// # Generated Code
259///
260/// This generates THREE constants:
261///
262/// ```ignore
263/// // Always generated (no feature flags)
264/// pub const E_AUTH_TOKEN_MISSING: DiagnosticRuntime = /* runtime metadata */;
265///
266/// // Only with metadata feature
267/// #[cfg(feature = "metadata")]
268/// pub const E_AUTH_TOKEN_MISSING_DOCS: DiagnosticDocs = /* compile-time docs */;
269///
270/// #[cfg(feature = "metadata")]
271/// pub const E_AUTH_TOKEN_MISSING_COMPLETE: DiagnosticComplete = /* both */;
272/// ```
273///
274/// Frameworks can use the runtime constant for error handling, and the complete
275/// constant for documentation generation.
276#[proc_macro]
277pub fn diag(input: TokenStream) -> TokenStream {
278 diag::expand(input)
279}
280
281/// Attribute macro for automatic documentation generation
282///
283/// Wraps `diag!` and generates registration code for specified formats.
284///
285/// # Examples
286///
287/// ```ignore
288/// #[doc_gen("json", "html")]
289/// diag! {
290/// E.Auth.Token.MISSING: {
291/// message: "Token not found",
292/// // ...
293/// },
294/// }
295/// ```
296///
297/// This generates a `__doc_gen_registry` module with format tracking.
298#[proc_macro_attribute]
299pub fn doc_gen(attr: TokenStream, item: TokenStream) -> TokenStream {
300 doc_gen_attr::expand(attr, item)
301}
302
303/// Generates documentation registration function for a diagnostic.
304///
305/// This macro creates a registration function that can be called to register
306/// a diagnostic with a DocRegistry for documentation generation.
307///
308/// # Example
309/// ```rust,ignore
310/// // Define diagnostic
311/// diag! {
312/// E.Auth.Token.EXPIRED: {
313/// message: "Token expired",
314/// // ...
315/// },
316/// }
317///
318/// // Generate registration code
319/// doc_gen_register! {
320/// E.Auth.Token.EXPIRED => ["json", "html"]
321/// }
322///
323/// // Later, use the generated function:
324/// let mut registry = DocRegistry::new("myapp", "1.0.0");
325/// register_e_auth_token_expired_complete_for_doc_gen(&mut registry);
326/// ```
327#[proc_macro]
328pub fn doc_gen_register(input: TokenStream) -> TokenStream {
329 doc_gen_macro::expand(input)
330}
331
332/// Mark modules or files as belonging to a specific component.
333///
334/// This attribute enables:
335/// - **Discovery**: Find all files in a component via grep or IDE search
336/// - **Navigation**: IDE integration for "jump to component" features
337/// - **Documentation**: Auto-group errors by component in generated docs
338/// - **Organization**: Track scattered components across different folders
339///
340/// # Syntax
341///
342/// ```ignore
343/// #[in_component(ComponentName)]
344/// ```
345///
346/// # Examples
347///
348/// **Marking a folder** (via mod.rs):
349/// ```ignore
350/// // src/auth/mod.rs
351/// #[in_component(Auth)]
352///
353/// mod token;
354/// mod session;
355/// ```
356///
357/// **Marking scattered files**:
358/// ```ignore
359/// // src/api/middleware.rs
360/// #[in_component(Auth)] // Auth logic here too!
361///
362/// use waddling_errors::diag;
363/// // ... Auth-related errors ...
364/// ```
365///
366/// **Finding all Auth components**:
367/// ```bash
368/// grep -r "#\[in_component(Auth)\]" src/
369/// ```
370///
371/// # Metadata Generated
372///
373/// The macro creates a hidden module with:
374/// - Component name constant
375/// - Module path (for IDE integration)
376/// - File path (for tooling)
377/// - Marker trait (for discovery)
378#[proc_macro_attribute]
379pub fn in_component(attr: TokenStream, item: TokenStream) -> TokenStream {
380 in_component::expand(attr, item)
381}
382
383/// Configure module paths for waddling-errors definitions.
384///
385/// This macro sets up path aliases that `diag!` uses to resolve component,
386/// primary, and sequence definitions. This enables flexible project structure
387/// where definitions can be placed in any module.
388///
389/// **Must be called once at the crate root (lib.rs or main.rs).**
390///
391/// # Syntax
392///
393/// ```ignore
394/// waddling_errors::setup! {
395/// components = crate::my_components,
396/// primaries = crate::error_primaries,
397/// sequences = crate::my_sequences,
398/// }
399/// ```
400///
401/// # What it Generates
402///
403/// ```ignore
404/// pub(crate) mod __wd_paths {
405/// pub use crate::my_components as components;
406/// pub use crate::error_primaries as primaries;
407/// pub use crate::my_sequences as sequences;
408/// }
409/// ```
410///
411/// # Why Use This
412///
413/// Without `setup!`, `diag!` expects definitions at fixed paths:
414/// - `crate::components::*`
415/// - `crate::primaries::*`
416/// - `crate::sequences::*`
417///
418/// With `setup!`, you can place definitions anywhere and `diag!` will find them.
419///
420/// # Example Project Structure
421///
422/// ```ignore
423/// // src/lib.rs
424/// waddling_errors::setup! {
425/// components = crate::errors::components,
426/// primaries = crate::errors::primaries,
427/// sequences = crate::errors::sequences,
428/// }
429///
430/// mod errors {
431/// pub mod components { /* component! definitions */ }
432/// pub mod primaries { /* primary! definitions */ }
433/// pub mod sequences { /* sequence! definitions */ }
434/// }
435///
436/// mod api {
437/// // diag! here works - finds definitions via __wd_paths
438/// diag! { E.Api.Auth.001: { ... } }
439/// }
440/// ```
441///
442/// # Partial Configuration
443///
444/// You can specify only some paths - others will use defaults:
445///
446/// ```ignore
447/// waddling_errors::setup! {
448/// sequences = crate::my_sequences,
449/// // components defaults to crate::components
450/// // primaries defaults to crate::primaries
451/// }
452/// ```
453///
454/// # No Collision Between Crates
455///
456/// Each crate has its own `__wd_paths` module (via `crate::`), so a library
457/// using waddling-errors won't collide with an application using it too.
458#[proc_macro]
459pub fn setup(input: TokenStream) -> TokenStream {
460 setup::expand(input)
461}
462/// Register a component location without attaching to a module.
463///
464/// This is a standalone macro that registers where a component's code lives,
465/// without requiring an attribute on a module. It's more flexible than
466/// `#[in_component]` and supports:
467///
468/// - **Multiple components per file** - Register several component locations
469/// - **File-level usage** - No need to wrap code in a module
470/// - **Folder markers** - Use in `mod.rs` to mark entire folders
471/// - **Role-based security** - Control visibility in documentation
472///
473/// # Syntax
474///
475/// ```ignore
476/// component_location!(ComponentName); // Default: Internal (secure)
477/// component_location!(ComponentName, role = public); // Public: visible to everyone
478/// component_location!(ComponentName, role = developer); // Developer: devs + internal
479/// component_location!(ComponentName, role = internal); // Internal: team only (explicit)
480/// ```
481///
482/// # Examples
483///
484/// **Single component location:**
485/// ```ignore
486/// // src/auth/jwt.rs
487/// use waddling_errors_macros::component_location;
488///
489/// component_location!(Auth); // Defaults to internal role
490///
491/// // ... your JWT code and diag! definitions ...
492/// ```
493///
494/// **Multiple components in same file:**
495/// ```ignore
496/// // src/shared/crypto.rs - shared between Auth and Crypto components
497/// component_location!(Auth, role = internal);
498/// component_location!(Crypto, role = developer);
499///
500/// // ... shared cryptographic utilities ...
501/// ```
502///
503/// **Public documentation example:**
504/// ```ignore
505/// // examples/auth_usage.rs
506/// component_location!(Auth, role = public);
507///
508/// // ... example code for public documentation ...
509/// ```
510///
511/// **Folder marker (in mod.rs):**
512/// ```ignore
513/// // src/database/mod.rs
514/// component_location!(Database);
515///
516/// pub mod connection;
517/// pub mod queries;
518/// pub mod migrations;
519/// ```
520///
521/// # Generated Code
522///
523/// For `component_location!(Auth, role = public)` the macro generates:
524///
525/// ```ignore
526/// pub mod __component_loc_auth {
527/// pub const COMPONENT: &str = "Auth"; // Preserves case to match component!
528/// pub const FILE: &str = "src/myfile.rs"; // file!() path
529/// pub const MODULE_PATH: &str = "mymod"; // module_path!()
530/// pub const ROLE: Option<Role> = Some(Role::Public);
531///
532/// #[cfg(feature = "metadata")]
533/// pub fn register(registry: &mut DocRegistry) {
534/// registry.register_component_location_with_role(COMPONENT, FILE, ROLE);
535/// }
536/// }
537/// ```
538///
539/// # Accessing Generated Constants
540///
541/// Access constants via the generated marker module `__component_loc_<name>`:
542///
543/// ```ignore
544/// component_location!(Auth, role = public);
545/// component_location!(Crypto, role = developer);
546///
547/// fn main() {
548/// // Access constants (preserves original case to match component! registration)
549/// assert_eq!(__component_loc_auth::COMPONENT, "Auth");
550/// assert_eq!(__component_loc_crypto::COMPONENT, "Crypto");
551///
552/// // Check file paths
553/// println!("Auth location: {}", __component_loc_auth::FILE);
554/// }
555/// ```
556///
557/// # Registration
558///
559/// With `auto-register` feature, locations are registered automatically via `ctor`.
560/// Manual registration is also supported:
561///
562/// ```ignore
563/// let mut registry = DocRegistry::new("myapp", "1.0.0");
564/// __component_loc_auth::register(&mut registry);
565/// __component_loc_crypto::register(&mut registry);
566/// ```
567///
568/// # Why Use This Over `#[in_component]`?
569///
570/// | Feature | `#[in_component]` | `component_location!` |
571/// |---------|------------------|----------------------|
572/// | Attach to module | ✅ Required | ❌ Not needed |
573/// | Multiple per file | ❌ Awkward | ✅ Easy |
574/// | File-level use | ❌ Needs wrapper | ✅ Direct |
575/// | Non-module items | ⚠️ Creates sibling mod | ✅ Clean |
576///
577/// Use `component_location!` when you want to mark locations without
578/// restructuring your code around modules.
579#[proc_macro]
580pub fn component_location(input: TokenStream) -> TokenStream {
581 component_location::expand(input)
582}