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