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}