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