waddling_errors/
traits.rs

1//! Trait-based extensibility for error code components and primaries
2//!
3//! This module provides a trait-based system that allows users to define their own
4//! component and primary types without requiring changes to waddling-errors.
5//!
6//! # Design Philosophy
7//!
8//! - **Level 1**: Minimal traits (`ComponentId`, `PrimaryId`) - just `.as_str()`
9//! - **Level 2**: Optional documentation traits for basic metadata
10//! - **Level 3**: Full error metadata for rich documentation generation
11//!
12//! Users choose what level fits their needs - no forcing!
13
14use core::fmt;
15
16#[cfg(feature = "std")]
17use std::string::String;
18
19#[cfg(not(feature = "std"))]
20use alloc::string::String;
21
22// ============================================================================
23// LEVEL 1: MINIMAL (Required)
24// ============================================================================
25
26/// Trait for component identifiers
27///
28/// This is the minimal trait needed for error codes. Just implement `.as_str()`.
29///
30/// # Examples
31///
32/// ```rust
33/// use waddling_errors::ComponentId;
34///
35/// #[derive(Debug, Clone, Copy)]
36/// enum Component {
37///     Parser,
38///     Network,
39/// }
40///
41/// impl ComponentId for Component {
42///     fn as_str(&self) -> &'static str {
43///         match self {
44///             Component::Parser => "PARSER",
45///             Component::Network => "NETWORK",
46///         }
47///     }
48/// }
49/// ```
50pub trait ComponentId: Copy + fmt::Debug {
51    /// Get the component name (e.g., "PARSER", "CRYPTO")
52    ///
53    /// Must be uppercase, 2-12 characters.
54    fn as_str(&self) -> &'static str;
55}
56
57/// Trait for primary category identifiers
58///
59/// This is the minimal trait needed for error codes. Just implement `.as_str()`.
60///
61/// # Examples
62///
63/// ```rust
64/// use waddling_errors::PrimaryId;
65///
66/// #[derive(Debug, Clone, Copy)]
67/// enum Primary {
68///     Syntax,
69///     Type,
70/// }
71///
72/// impl PrimaryId for Primary {
73///     fn as_str(&self) -> &'static str {
74///         match self {
75///             Primary::Syntax => "SYNTAX",
76///             Primary::Type => "TYPE",
77///         }
78///     }
79/// }
80/// ```
81pub trait PrimaryId: Copy + fmt::Debug {
82    /// Get the primary category name (e.g., "SYNTAX", "TYPE")
83    ///
84    /// Must be uppercase, 2-12 characters.
85    fn as_str(&self) -> &'static str;
86}
87
88// ============================================================================
89// LEVEL 2: DOCUMENTED (Optional - for basic doc generation)
90// ============================================================================
91
92/// Extended trait for components with documentation metadata
93///
94/// Implement this trait if you want your components to appear in generated documentation.
95/// All methods have default implementations, so you only provide what you need.
96///
97/// # Examples
98///
99/// ```rust
100/// use waddling_errors::{ComponentId, ComponentIdDocumented};
101///
102/// #[derive(Debug, Clone, Copy)]
103/// enum Component {
104///     Parser,
105/// }
106///
107/// impl ComponentId for Component {
108///     fn as_str(&self) -> &'static str {
109///         match self {
110///             Component::Parser => "PARSER",
111///         }
112///     }
113/// }
114///
115/// impl ComponentIdDocumented for Component {
116///     fn description(&self) -> Option<&'static str> {
117///         Some(match self {
118///             Component::Parser => "Syntax parsing and tokenization",
119///         })
120///     }
121///
122///     fn tags(&self) -> &'static [&'static str] {
123///         match self {
124///             Component::Parser => &["frontend", "syntax"],
125///         }
126///     }
127/// }
128/// ```
129pub trait ComponentIdDocumented: ComponentId {
130    /// Human-readable description of this component
131    ///
132    /// Example: "Parsing phase errors and syntax validation"
133    fn description(&self) -> Option<&'static str> {
134        None
135    }
136
137    /// Example error codes from this component
138    ///
139    /// Example: `&["E.PARSER.SYNTAX.001", "W.PARSER.STYLE.010"]`
140    fn examples(&self) -> &'static [&'static str] {
141        &[]
142    }
143
144    /// Categories/tags for organization
145    ///
146    /// Example: `&["frontend", "compile-time"]`
147    fn tags(&self) -> &'static [&'static str] {
148        &[]
149    }
150}
151
152/// Extended trait for primaries with documentation metadata
153///
154/// Implement this trait if you want your primary categories to appear in
155/// generated documentation with rich metadata.
156///
157/// # Examples
158///
159/// ```rust
160/// use waddling_errors::{PrimaryId, PrimaryIdDocumented};
161///
162/// #[derive(Debug, Clone, Copy)]
163/// enum Primary {
164///     Syntax,
165/// }
166///
167/// impl PrimaryId for Primary {
168///     fn as_str(&self) -> &'static str {
169///         match self {
170///             Primary::Syntax => "SYNTAX",
171///         }
172///     }
173/// }
174///
175/// impl PrimaryIdDocumented for Primary {
176///     fn description(&self) -> Option<&'static str> {
177///         Some("Syntax-level parsing errors")
178///     }
179/// }
180/// ```
181pub trait PrimaryIdDocumented: PrimaryId {
182    /// Human-readable description of this primary category
183    fn description(&self) -> Option<&'static str> {
184        None
185    }
186
187    /// Example error codes using this primary
188    fn examples(&self) -> &'static [&'static str] {
189        &[]
190    }
191
192    /// Related primary categories
193    ///
194    /// Example: SYNTAX relates to PARSE, TOKENIZE
195    fn related(&self) -> &'static [&'static str] {
196        &[]
197    }
198}
199
200// ============================================================================
201// LEVEL 3: FULL METADATA (Optional - for rich documentation)
202// ============================================================================
203
204/// Role visibility for documentation generation
205///
206/// Controls who sees this error in generated documentation.
207#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
208#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
209pub enum Role {
210    /// Visible to all users (library consumers, end users)
211    Public,
212    /// Visible only to team members (internal documentation)
213    Internal,
214    /// Visible only to library/compiler developers
215    Developer,
216}
217
218impl Role {
219    /// Returns the privilege level for hierarchical comparisons.
220    ///
221    /// Lower numbers are more restrictive:
222    /// - Public: 0 (most restrictive)
223    /// - Developer: 1
224    /// - Internal: 2 (least restrictive, sees everything)
225    pub fn privilege_level(&self) -> u8 {
226        match self {
227            Role::Public => 0,
228            Role::Developer => 1,
229            Role::Internal => 2,
230        }
231    }
232
233    /// Check if this role can view content marked with the given role.
234    ///
235    /// Visibility is hierarchical:
236    /// - Public can only see Public content
237    /// - Developer can see Public and Developer content
238    /// - Internal can see all content
239    /// - Content with None (unspecified) is visible to everyone
240    ///
241    /// # Examples
242    ///
243    /// ```
244    /// use waddling_errors::traits::Role;
245    ///
246    /// assert!(Role::Public.can_view(Some(Role::Public)));
247    /// assert!(!Role::Public.can_view(Some(Role::Internal)));
248    /// assert!(Role::Internal.can_view(Some(Role::Public)));
249    /// assert!(Role::Developer.can_view(None)); // Unspecified visible to all
250    /// ```
251    pub fn can_view(&self, content_role: Option<Role>) -> bool {
252        match content_role {
253            None => true, // Unspecified content is visible to all roles
254            Some(role) => self.privilege_level() >= role.privilege_level(),
255        }
256    }
257}
258
259/// Field-level visibility marker for hints, descriptions, and metadata.
260///
261/// Allows marking individual hints or descriptions with different visibility levels.
262/// For example, a hint might be marked as Internal-only while the error itself is Public.
263///
264/// # Examples
265///
266/// ```rust
267/// use waddling_errors::FieldMeta;
268///
269/// let public_hint = FieldMeta::public("Check the API documentation");
270/// let internal_hint = FieldMeta::internal("Check Redis connection on host-01");
271/// ```
272#[derive(Debug, Clone, PartialEq, Eq)]
273#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
274pub struct FieldMeta {
275    /// The actual text content
276    pub content: String,
277    /// Visibility level for this field
278    pub visibility: Role,
279}
280
281impl FieldMeta {
282    /// Create a public field (visible to everyone)
283    pub fn public(content: impl Into<String>) -> Self {
284        Self {
285            content: content.into(),
286            visibility: Role::Public,
287        }
288    }
289
290    /// Create a developer field (visible to developers)
291    pub fn developer(content: impl Into<String>) -> Self {
292        Self {
293            content: content.into(),
294            visibility: Role::Developer,
295        }
296    }
297
298    /// Create an internal field (visible to internal team only)
299    pub fn internal(content: impl Into<String>) -> Self {
300        Self {
301            content: content.into(),
302            visibility: Role::Internal,
303        }
304    }
305
306    /// Check if this field should be visible at the given role level
307    pub fn visible_at(&self, role: Role) -> bool {
308        match (role, self.visibility) {
309            // Public can see only Public
310            (Role::Public, Role::Public) => true,
311            (Role::Public, _) => false,
312            // Developer can see Public and Developer
313            (Role::Developer, Role::Public | Role::Developer) => true,
314            (Role::Developer, Role::Internal) => false,
315            // Internal can see everything
316            (Role::Internal, _) => true,
317        }
318    }
319}
320
321/// Full error metadata for advanced documentation generation
322///
323/// Implement this trait on specific error types to export complete
324/// error definitions to JSON/docs with all metadata.
325///
326/// # Examples
327///
328/// ```rust
329/// use waddling_errors::{ErrorMetadata, Role};
330///
331/// struct ParserSyntax001;
332///
333/// impl ErrorMetadata for ParserSyntax001 {
334///     fn code(&self) -> &'static str {
335///         "E.PARSER.SYNTAX.001"
336///     }
337///
338///     fn description(&self) -> Option<&'static str> {
339///         Some("Expected semicolon at end of statement")
340///     }
341///
342///     fn hints(&self) -> &'static [&'static str] {
343///         &["Add a semicolon after the statement"]
344///     }
345///
346///     fn role(&self) -> Role {
347///         Role::Public
348///     }
349/// }
350/// ```
351pub trait ErrorMetadata {
352    /// Get the full error code string (e.g., "E.PARSER.SYNTAX.001")
353    fn code(&self) -> &'static str;
354
355    /// Human-readable description
356    fn description(&self) -> Option<&'static str> {
357        None
358    }
359
360    /// Helpful hints for fixing the error
361    fn hints(&self) -> &'static [&'static str] {
362        &[]
363    }
364
365    /// Documentation visibility role
366    fn role(&self) -> Role {
367        Role::Public
368    }
369
370    /// URL to detailed documentation (if available)
371    fn docs_url(&self) -> Option<&'static str> {
372        None
373    }
374
375    /// Example usage/messages
376    fn examples(&self) -> &'static [&'static str] {
377        &[]
378    }
379
380    /// Related error codes
381    fn related_codes(&self) -> &'static [&'static str] {
382        &[]
383    }
384
385    /// Version when this error was introduced
386    fn since_version(&self) -> Option<&'static str> {
387        None
388    }
389}