Skip to main content

domain_key/
error.rs

1//! Error types and error handling for domain-key
2//!
3//! This module provides comprehensive error handling for key validation and creation.
4//! All errors are designed to provide detailed information for debugging while maintaining
5//! performance in the happy path.
6
7use core::fmt;
8use thiserror::Error;
9
10#[cfg(not(feature = "std"))]
11use alloc::format;
12#[cfg(not(feature = "std"))]
13use alloc::string::String;
14#[cfg(not(feature = "std"))]
15use alloc::vec;
16#[cfg(not(feature = "std"))]
17use alloc::vec::Vec;
18
19use core::fmt::Write;
20
21// ============================================================================
22// CORE ERROR TYPES
23// ============================================================================
24
25/// Comprehensive error type for key parsing and validation failures
26///
27/// This enum covers all possible validation failures that can occur during
28/// key creation, providing detailed information for debugging and user feedback.
29///
30/// # Error Categories
31///
32/// - **Length Errors**: Empty keys or keys exceeding maximum length
33/// - **Character Errors**: Invalid characters at specific positions
34/// - **Structure Errors**: Invalid patterns like consecutive special characters
35/// - **Domain Errors**: Domain-specific validation failures
36/// - **Custom Errors**: Application-specific validation failures
37///
38/// # Examples
39///
40/// ```rust
41/// use domain_key::{KeyParseError, ErrorCategory};
42///
43/// // Handle different error types
44/// match KeyParseError::Empty {
45///     err => {
46///         println!("Error: {}", err);
47///         println!("Code: {}", err.code());
48///         println!("Category: {:?}", err.category());
49///     }
50/// }
51/// ```
52#[derive(Debug, Error, PartialEq, Eq, Clone)]
53#[non_exhaustive]
54pub enum KeyParseError {
55    /// Key cannot be empty or contain only whitespace
56    ///
57    /// This error occurs when attempting to create a key from an empty string
58    /// or a string containing only whitespace characters.
59    #[error("Key cannot be empty or whitespace")]
60    Empty,
61
62    /// Key contains a character that is not allowed at the specified position
63    ///
64    /// Each domain defines which characters are allowed. This error provides
65    /// the specific character, its position, and optionally what was expected.
66    #[error("Invalid character '{character}' at position {position}")]
67    InvalidCharacter {
68        /// The invalid character that was found
69        character: char,
70        /// Position where the invalid character was found (0-based)
71        position: usize,
72        /// Optional description of what characters are expected
73        expected: Option<&'static str>,
74    },
75
76    /// Key exceeds the maximum allowed length for the domain
77    ///
78    /// Each domain can specify a maximum length. This error provides both
79    /// the limit and the actual length that was attempted.
80    #[error("Key is too long (max {max_length} characters, got {actual_length})")]
81    TooLong {
82        /// The maximum allowed length for this domain
83        max_length: usize,
84        /// The actual length of the key that was attempted
85        actual_length: usize,
86    },
87
88    /// Key is shorter than the minimum allowed length for the domain
89    ///
90    /// Each domain can specify a minimum length. This error provides both
91    /// the required minimum and the actual length that was attempted.
92    #[error("Key is too short (min {min_length} characters, got {actual_length})")]
93    TooShort {
94        /// The minimum allowed length for this domain
95        min_length: usize,
96        /// The actual length of the key that was attempted
97        actual_length: usize,
98    },
99
100    /// Key has invalid structure (consecutive special chars, invalid start/end)
101    ///
102    /// This covers structural issues like:
103    /// - Starting or ending with special characters
104    /// - Consecutive special characters
105    /// - Invalid character sequences
106    #[error("Key has invalid structure: {reason}")]
107    InvalidStructure {
108        /// Description of the structural issue
109        reason: &'static str,
110    },
111
112    /// Domain-specific validation error
113    ///
114    /// This error is returned when domain-specific validation rules fail.
115    /// It includes the domain name and a descriptive message.
116    #[error("Domain '{domain}' validation failed: {message}")]
117    DomainValidation {
118        /// The domain name where validation failed
119        domain: &'static str,
120        /// The error message describing what validation failed
121        message: String,
122    },
123
124    /// Custom error for specific use cases
125    ///
126    /// Applications can define custom validation errors with numeric codes
127    /// for structured error handling.
128    #[error("Custom validation error (code: {code}): {message}")]
129    Custom {
130        /// Custom error code for programmatic handling
131        code: u32,
132        /// The custom error message
133        message: String,
134    },
135}
136
137impl KeyParseError {
138    /// Create a domain validation error with domain name
139    ///
140    /// This is the preferred way to create domain validation errors.
141    ///
142    /// # Examples
143    ///
144    /// ```rust
145    /// use domain_key::KeyParseError;
146    ///
147    /// let error = KeyParseError::domain_error("my_domain", "Custom validation failed");
148    /// // Verify it's the correct error type
149    /// match error {
150    ///     KeyParseError::DomainValidation { domain, message } => {
151    ///         assert_eq!(domain, "my_domain");
152    ///         assert!(message.contains("Custom validation failed"));
153    ///     },
154    ///     _ => panic!("Expected domain validation error"),
155    /// }
156    /// ```
157    pub fn domain_error(domain: &'static str, message: impl Into<String>) -> Self {
158        Self::DomainValidation {
159            domain,
160            message: message.into(),
161        }
162    }
163
164    /// Create a domain validation error without specifying domain (for internal use)
165    pub fn domain_error_generic(message: impl Into<String>) -> Self {
166        Self::DomainValidation {
167            domain: "unknown",
168            message: message.into(),
169        }
170    }
171
172    /// Create a domain validation error with source error information
173    ///
174    /// The source error's `Display` representation is appended to `message`,
175    /// separated by `": "`.  This is a **flattened** representation — the
176    /// resulting `KeyParseError` does **not** implement `std::error::Error::source()`
177    /// chaining back to the original error.  In other words, `error.source()`
178    /// will return `None`, and tools such as `anyhow` / `tracing` that walk the
179    /// error chain will not see the wrapped cause.
180    ///
181    /// If you need a proper causal chain, wrap the original error in your own
182    /// error type (e.g. via `anyhow::Error` or a `thiserror` wrapper) before
183    /// converting it to a `KeyParseError`.
184    ///
185    /// # Limitation
186    ///
187    /// Properly storing a `Box<dyn Error + Send + Sync>` inside `KeyParseError`
188    /// variants would require either a new variant or a breaking field change.
189    /// Until that refactor is done, the full error context is preserved only in
190    /// the formatted message string.
191    #[cfg(feature = "std")]
192    pub fn domain_error_with_source(
193        domain: &'static str,
194        message: impl Into<String>,
195        source: &(dyn std::error::Error + Send + Sync),
196    ) -> Self {
197        let full_message = format!("{}: {}", message.into(), source);
198        Self::DomainValidation {
199            domain,
200            message: full_message,
201        }
202    }
203
204    /// Create a custom validation error
205    ///
206    /// Custom errors allow applications to define their own error codes
207    /// for structured error handling.
208    ///
209    /// # Examples
210    ///
211    /// ```rust
212    /// use domain_key::KeyParseError;
213    ///
214    /// let error = KeyParseError::custom(1001, "Business rule violation");
215    /// assert_eq!(error.code(), 1001);
216    /// ```
217    pub fn custom(code: u32, message: impl Into<String>) -> Self {
218        Self::Custom {
219            code,
220            message: message.into(),
221        }
222    }
223
224    /// Create a custom validation error with source error information
225    ///
226    /// The source error's `Display` representation is appended to `message`,
227    /// separated by `": "`.  This is a **flattened** representation — the
228    /// resulting `KeyParseError` does **not** implement `std::error::Error::source()`
229    /// chaining back to the original error.  In other words, `error.source()`
230    /// will return `None`, and tools such as `anyhow` / `tracing` that walk the
231    /// error chain will not see the wrapped cause.
232    ///
233    /// If you need a proper causal chain, wrap the original error in your own
234    /// error type (e.g. via `anyhow::Error` or a `thiserror` wrapper) before
235    /// converting it to a `KeyParseError`.
236    ///
237    /// # Limitation
238    ///
239    /// Properly storing a `Box<dyn Error + Send + Sync>` inside `KeyParseError`
240    /// variants would require either a new variant or a breaking field change.
241    /// Until that refactor is done, the full error context is preserved only in
242    /// the formatted message string.
243    #[cfg(feature = "std")]
244    pub fn custom_with_source(
245        code: u32,
246        message: impl Into<String>,
247        source: &(dyn std::error::Error + Send + Sync),
248    ) -> Self {
249        let full_message = format!("{}: {}", message.into(), source);
250        Self::Custom {
251            code,
252            message: full_message,
253        }
254    }
255
256    /// Get the error code for machine processing
257    ///
258    /// Returns a numeric code that can be used for programmatic error handling.
259    /// This is useful for APIs that need to return structured error responses.
260    ///
261    /// # Error Codes
262    ///
263    /// - `1001`: Empty key
264    /// - `1002`: Invalid character
265    /// - `1003`: Key too long
266    /// - `1004`: Invalid structure
267    /// - `2000`: Domain validation (base code)
268    /// - Custom codes: As specified in `Custom` errors
269    ///
270    /// # Examples
271    ///
272    /// ```rust
273    /// use domain_key::KeyParseError;
274    ///
275    /// assert_eq!(KeyParseError::Empty.code(), 1001);
276    /// assert_eq!(KeyParseError::custom(42, "test").code(), 42);
277    /// ```
278    #[must_use]
279    pub const fn code(&self) -> u32 {
280        match self {
281            Self::Empty => 1001,
282            Self::InvalidCharacter { .. } => 1002,
283            Self::TooLong { .. } => 1003,
284            Self::InvalidStructure { .. } => 1004,
285            Self::TooShort { .. } => 1005,
286            Self::DomainValidation { .. } => 2000,
287            Self::Custom { code, .. } => *code,
288        }
289    }
290
291    /// Get the error category for classification
292    ///
293    /// Returns the general category of this error for higher-level error handling.
294    /// This allows applications to handle broad categories of errors uniformly.
295    ///
296    /// # Examples
297    ///
298    /// ```rust
299    /// use domain_key::{KeyParseError, ErrorCategory};
300    ///
301    /// match KeyParseError::Empty.category() {
302    ///     ErrorCategory::Length => println!("Length-related error"),
303    ///     ErrorCategory::Character => println!("Character-related error"),
304    ///     _ => println!("Other error type"),
305    /// }
306    /// ```
307    #[must_use]
308    pub const fn category(&self) -> ErrorCategory {
309        match self {
310            Self::Empty | Self::TooLong { .. } | Self::TooShort { .. } => ErrorCategory::Length,
311            Self::InvalidCharacter { .. } => ErrorCategory::Character,
312            Self::InvalidStructure { .. } => ErrorCategory::Structure,
313            Self::DomainValidation { .. } => ErrorCategory::Domain,
314            Self::Custom { code, .. } => match code {
315                1002 => ErrorCategory::Character,
316                1003 => ErrorCategory::Length,
317                1004 => ErrorCategory::Structure,
318                _ => ErrorCategory::Custom,
319            },
320        }
321    }
322
323    /// Get a human-readable description of what went wrong
324    ///
325    /// This provides additional context beyond the basic error message,
326    /// useful for user-facing error messages or debugging.
327    #[must_use]
328    pub fn description(&self) -> &'static str {
329        match self {
330            Self::Empty => "Key cannot be empty or contain only whitespace characters",
331            Self::InvalidCharacter { .. } => {
332                "Key contains characters that are not allowed by the domain"
333            }
334            Self::TooLong { .. } => "Key exceeds the maximum length allowed by the domain",
335            Self::TooShort { .. } => {
336                "Key is shorter than the minimum length required by the domain"
337            }
338            Self::InvalidStructure { .. } => "Key has invalid structure or formatting",
339            Self::DomainValidation { .. } => "Key fails domain-specific validation rules",
340            Self::Custom { .. } => "Key fails custom validation rules",
341        }
342    }
343
344    /// Get suggested actions for fixing this error
345    ///
346    /// Returns helpful suggestions for how to fix the validation error.
347    #[must_use]
348    pub fn suggestions(&self) -> &'static [&'static str] {
349        match self {
350            Self::Empty => &[
351                "Provide a non-empty key",
352                "Remove leading/trailing whitespace",
353            ],
354            Self::InvalidCharacter { .. } => &[
355                "Use only allowed characters (check domain rules)",
356                "Remove or replace invalid characters",
357            ],
358            Self::TooLong { .. } => &[
359                "Shorten the key to fit within length limits",
360                "Consider using abbreviated forms",
361            ],
362            Self::TooShort { .. } => &["Lengthen the key to meet the minimum length requirement"],
363            Self::InvalidStructure { .. } => &[
364                "Avoid consecutive special characters",
365                "Don't start or end with special characters",
366                "Follow the expected key format",
367            ],
368            Self::DomainValidation { .. } => &[
369                "Check domain-specific validation rules",
370                "Refer to domain documentation",
371            ],
372            Self::Custom { .. } => &[
373                "Check application-specific validation rules",
374                "Contact system administrator if needed",
375            ],
376        }
377    }
378
379    /// Check if this error is recoverable through user action
380    ///
381    /// Returns `true` if the user can potentially fix this error by modifying
382    /// their input, `false` if it represents a programming error or system issue.
383    #[must_use]
384    pub const fn is_recoverable(&self) -> bool {
385        match self {
386            Self::Empty
387            | Self::InvalidCharacter { .. }
388            | Self::TooLong { .. }
389            | Self::TooShort { .. }
390            | Self::InvalidStructure { .. }
391            | Self::DomainValidation { .. } => true,
392            Self::Custom { .. } => false, // Depends on the specific custom error
393        }
394    }
395}
396
397// ============================================================================
398// ERROR CATEGORIES
399// ============================================================================
400
401/// Error category for classification of validation errors
402///
403/// These categories allow applications to handle broad types of validation
404/// errors uniformly, regardless of the specific error details.
405#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
406#[non_exhaustive]
407pub enum ErrorCategory {
408    /// Length-related errors (empty, too long)
409    Length,
410    /// Character-related errors (invalid characters)
411    Character,
412    /// Structure-related errors (invalid format, consecutive special chars)
413    Structure,
414    /// Domain-specific validation errors
415    Domain,
416    /// Custom validation errors
417    Custom,
418}
419
420impl ErrorCategory {
421    /// Get a human-readable name for this category
422    #[must_use]
423    pub const fn name(self) -> &'static str {
424        match self {
425            Self::Length => "Length",
426            Self::Character => "Character",
427            Self::Structure => "Structure",
428            Self::Domain => "Domain",
429            Self::Custom => "Custom",
430        }
431    }
432
433    /// Get a description of what this category represents
434    #[must_use]
435    pub const fn description(self) -> &'static str {
436        match self {
437            Self::Length => "Errors related to key length (empty, too long, etc.)",
438            Self::Character => "Errors related to invalid characters in the key",
439            Self::Structure => "Errors related to key structure and formatting",
440            Self::Domain => "Errors from domain-specific validation rules",
441            Self::Custom => "Custom application-specific validation errors",
442        }
443    }
444}
445
446impl fmt::Display for ErrorCategory {
447    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
448        write!(f, "{}", self.name())
449    }
450}
451
452// ============================================================================
453// ID PARSE ERROR
454// ============================================================================
455
456/// Error type for numeric ID parsing failures
457///
458/// This error is returned when a string cannot be parsed as a valid `Id<D>`.
459///
460/// # Examples
461///
462/// ```rust
463/// use domain_key::IdParseError;
464///
465/// let result = "not_a_number".parse::<u64>();
466/// assert!(result.is_err());
467/// ```
468#[derive(Debug, Error, PartialEq, Eq, Clone)]
469#[non_exhaustive]
470pub enum IdParseError {
471    /// The value is zero, which is not a valid identifier
472    #[error("ID cannot be zero")]
473    Zero,
474
475    /// The string could not be parsed as a valid non-zero u64 number
476    #[error("Invalid numeric ID: {0}")]
477    InvalidNumber(#[from] core::num::ParseIntError),
478}
479
480impl IdParseError {
481    /// Returns a user-friendly error message suitable for display.
482    #[must_use]
483    pub fn user_message(&self) -> &'static str {
484        match self {
485            Self::Zero => "Identifier cannot be zero",
486            Self::InvalidNumber(_) => "Value must be a positive integer",
487        }
488    }
489}
490
491// ============================================================================
492// UUID PARSE ERROR
493// ============================================================================
494
495/// Error type for UUID parsing failures
496///
497/// This error is returned when a string cannot be parsed as a valid `Uuid<D>`.
498/// Only available when the `uuid` feature is enabled.
499///
500/// Note: `UuidParseError` does not implement `PartialEq` because
501/// `uuid::Error` does not implement it.
502#[cfg(feature = "uuid")]
503#[derive(Debug, Error, Clone)]
504#[non_exhaustive]
505pub enum UuidParseError {
506    /// The string could not be parsed as a valid UUID
507    #[error("Invalid UUID: {0}")]
508    InvalidUuid(#[from] ::uuid::Error),
509}
510
511// ============================================================================
512// ERROR UTILITIES
513// ============================================================================
514
515/// Builder for creating detailed validation errors
516///
517/// This builder provides a fluent interface for creating complex validation
518/// errors with additional context and suggestions.
519#[derive(Debug)]
520pub struct ErrorBuilder {
521    category: ErrorCategory,
522    code: Option<u32>,
523    domain: Option<&'static str>,
524    message: String,
525    context: Option<String>,
526}
527
528impl ErrorBuilder {
529    /// Create a new error builder for the given category
530    #[must_use]
531    pub fn new(category: ErrorCategory) -> Self {
532        Self {
533            category,
534            code: None,
535            domain: None,
536            message: String::new(),
537            context: None,
538        }
539    }
540
541    /// Set the error message
542    #[must_use]
543    pub fn message(mut self, message: impl Into<String>) -> Self {
544        self.message = message.into();
545        self
546    }
547
548    /// Set a custom error code (used when category is `Custom`)
549    #[must_use]
550    pub fn code(mut self, code: u32) -> Self {
551        self.code = Some(code);
552        self
553    }
554
555    /// Set the domain name (used when category is `Domain`)
556    #[must_use]
557    pub fn domain(mut self, domain: &'static str) -> Self {
558        self.domain = Some(domain);
559        self
560    }
561
562    /// Set additional context information
563    #[must_use]
564    pub fn context(mut self, context: impl Into<String>) -> Self {
565        self.context = Some(context.into());
566        self
567    }
568
569    /// Build the final error
570    #[must_use]
571    pub fn build(self) -> KeyParseError {
572        let message = if let Some(context) = self.context {
573            format!("{} (Context: {})", self.message, context)
574        } else {
575            self.message
576        };
577
578        match self.category {
579            ErrorCategory::Custom => KeyParseError::custom(self.code.unwrap_or(0), message),
580            ErrorCategory::Domain => {
581                KeyParseError::domain_error(self.domain.unwrap_or("unknown"), message)
582            }
583            // Use the reserved structural codes so that category() round-trips
584            // correctly back to the originally-specified category.
585            // 1004 → ErrorCategory::Structure, 1003 → ErrorCategory::Length,
586            // 1002 → ErrorCategory::Character  (mirroring code() for each variant)
587            ErrorCategory::Structure => KeyParseError::custom(1004, message),
588            ErrorCategory::Length => KeyParseError::custom(1003, message),
589            ErrorCategory::Character => KeyParseError::custom(1002, message),
590        }
591    }
592}
593
594// ============================================================================
595// ERROR FORMATTING UTILITIES
596// ============================================================================
597
598/// Format an error for display to end users
599///
600/// This function provides a user-friendly representation of validation errors,
601/// including suggestions for how to fix them.
602#[must_use]
603pub fn format_user_error(error: &KeyParseError) -> String {
604    let mut output = format!("Error: {error}");
605
606    let suggestions = error.suggestions();
607    if !suggestions.is_empty() {
608        output.push_str("\n\nSuggestions:");
609        for suggestion in suggestions {
610            write!(output, "\n  - {suggestion}").unwrap();
611        }
612    }
613
614    output
615}
616
617/// Format an error for logging or debugging
618///
619/// This function provides a detailed representation suitable for logs,
620/// including error codes and categories.
621#[must_use]
622pub fn format_debug_error(error: &KeyParseError) -> String {
623    format!(
624        "[{}:{}] {} (Category: {})",
625        error.code(),
626        error.category().name(),
627        error,
628        error.description()
629    )
630}
631
632// ============================================================================
633// TESTS
634// ============================================================================
635
636#[cfg(test)]
637mod tests {
638    use super::*;
639
640    #[cfg(not(feature = "std"))]
641    use alloc::string::ToString;
642
643    #[test]
644    fn each_variant_has_unique_error_code() {
645        assert_eq!(KeyParseError::Empty.code(), 1001);
646        assert_eq!(
647            KeyParseError::InvalidCharacter {
648                character: 'x',
649                position: 0,
650                expected: None
651            }
652            .code(),
653            1002
654        );
655        assert_eq!(
656            KeyParseError::TooLong {
657                max_length: 10,
658                actual_length: 20
659            }
660            .code(),
661            1003
662        );
663        assert_eq!(
664            KeyParseError::InvalidStructure { reason: "test" }.code(),
665            1004
666        );
667        assert_eq!(
668            KeyParseError::TooShort {
669                min_length: 5,
670                actual_length: 2
671            }
672            .code(),
673            1005
674        );
675        assert_eq!(
676            KeyParseError::DomainValidation {
677                domain: "test",
678                message: "msg".to_string()
679            }
680            .code(),
681            2000
682        );
683        assert_eq!(
684            KeyParseError::Custom {
685                code: 42,
686                message: "msg".to_string()
687            }
688            .code(),
689            42
690        );
691    }
692
693    #[test]
694    fn variants_map_to_correct_category() {
695        assert_eq!(KeyParseError::Empty.category(), ErrorCategory::Length);
696        assert_eq!(
697            KeyParseError::InvalidCharacter {
698                character: 'x',
699                position: 0,
700                expected: None
701            }
702            .category(),
703            ErrorCategory::Character
704        );
705        assert_eq!(
706            KeyParseError::TooLong {
707                max_length: 10,
708                actual_length: 20
709            }
710            .category(),
711            ErrorCategory::Length
712        );
713        assert_eq!(
714            KeyParseError::InvalidStructure { reason: "test" }.category(),
715            ErrorCategory::Structure
716        );
717        assert_eq!(
718            KeyParseError::TooShort {
719                min_length: 5,
720                actual_length: 2
721            }
722            .category(),
723            ErrorCategory::Length
724        );
725        assert_eq!(
726            KeyParseError::DomainValidation {
727                domain: "test",
728                message: "msg".to_string()
729            }
730            .category(),
731            ErrorCategory::Domain
732        );
733        assert_eq!(
734            KeyParseError::Custom {
735                code: 42,
736                message: "msg".to_string()
737            }
738            .category(),
739            ErrorCategory::Custom
740        );
741    }
742
743    #[test]
744    fn empty_error_provides_recovery_suggestions() {
745        let error = KeyParseError::Empty;
746        let suggestions = error.suggestions();
747        assert!(!suggestions.is_empty());
748        assert!(suggestions.iter().any(|s| s.contains("non-empty")));
749    }
750
751    #[test]
752    fn builder_produces_custom_error_with_code_and_context() {
753        let error = ErrorBuilder::new(ErrorCategory::Custom)
754            .message("Test error")
755            .code(1234)
756            .context("In test function")
757            .build();
758
759        assert_eq!(error.code(), 1234);
760        assert_eq!(error.category(), ErrorCategory::Custom);
761    }
762
763    #[test]
764    fn variants_carry_correct_payloads() {
765        let error1 = KeyParseError::InvalidCharacter {
766            character: '!',
767            position: 5,
768            expected: Some("alphanumeric"),
769        };
770        assert!(matches!(
771            error1,
772            KeyParseError::InvalidCharacter {
773                character: '!',
774                position: 5,
775                ..
776            }
777        ));
778
779        let error2 = KeyParseError::TooLong {
780            max_length: 32,
781            actual_length: 64,
782        };
783        assert!(matches!(
784            error2,
785            KeyParseError::TooLong {
786                max_length: 32,
787                actual_length: 64
788            }
789        ));
790
791        let error3 = KeyParseError::InvalidStructure {
792            reason: "consecutive underscores",
793        };
794        assert!(matches!(
795            error3,
796            KeyParseError::InvalidStructure {
797                reason: "consecutive underscores"
798            }
799        ));
800
801        let error4 = KeyParseError::domain_error("test", "Invalid format");
802        assert!(matches!(
803            error4,
804            KeyParseError::DomainValidation { domain: "test", .. }
805        ));
806    }
807
808    #[test]
809    fn user_and_debug_formats_include_expected_sections() {
810        let error = KeyParseError::Empty;
811        let user_format = format_user_error(&error);
812        let debug_format = format_debug_error(&error);
813
814        assert!(user_format.contains("Error:"));
815        assert!(user_format.contains("Suggestions:"));
816        assert!(debug_format.contains("1001"));
817        assert!(debug_format.contains("Length"));
818    }
819
820    #[test]
821    fn recoverable_errors_distinguished_from_non_recoverable() {
822        assert!(KeyParseError::Empty.is_recoverable());
823        assert!(KeyParseError::InvalidCharacter {
824            character: 'x',
825            position: 0,
826            expected: None
827        }
828        .is_recoverable());
829        assert!(KeyParseError::TooShort {
830            min_length: 5,
831            actual_length: 2
832        }
833        .is_recoverable());
834        assert!(!KeyParseError::Custom {
835            code: 42,
836            message: "msg".to_string()
837        }
838        .is_recoverable());
839    }
840
841    #[test]
842    fn category_display_and_description_are_populated() {
843        assert_eq!(ErrorCategory::Length.to_string(), "Length");
844        assert_eq!(ErrorCategory::Character.name(), "Character");
845        assert!(ErrorCategory::Domain
846            .description()
847            .contains("domain-specific"));
848    }
849}