domain-key 0.5.2

High-performance, domain-driven, type-safe key system for Rust
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
//! Error types and error handling for domain-key
//!
//! This module provides comprehensive error handling for key validation and creation.
//! All errors are designed to provide detailed information for debugging while maintaining
//! performance in the happy path.

use core::fmt;
use thiserror::Error;

#[cfg(not(feature = "std"))]
use alloc::format;
#[cfg(not(feature = "std"))]
use alloc::string::String;

use core::fmt::Write;

// ============================================================================
// CORE ERROR TYPES
// ============================================================================

/// Comprehensive error type for key parsing and validation failures
///
/// This enum covers all possible validation failures that can occur during
/// key creation, providing detailed information for debugging and user feedback.
///
/// # Error Categories
///
/// - **Length Errors**: Empty keys or keys exceeding maximum length
/// - **Character Errors**: Invalid characters at specific positions
/// - **Structure Errors**: Invalid patterns like consecutive special characters
/// - **Domain Errors**: Domain-specific validation failures
/// - **Custom Errors**: Application-specific validation failures
///
/// # Examples
///
/// ```rust
/// use domain_key::{KeyParseError, ErrorCategory};
///
/// // Handle different error types
/// match KeyParseError::Empty {
///     err => {
///         println!("Error: {}", err);
///         println!("Code: {}", err.code());
///         println!("Category: {:?}", err.category());
///     }
/// }
/// ```
#[derive(Debug, Error, PartialEq, Eq, Clone)]
#[non_exhaustive]
pub enum KeyParseError {
    /// Key cannot be empty or contain only whitespace
    ///
    /// This error occurs when attempting to create a key from an empty string
    /// or a string containing only whitespace characters.
    #[error("Key cannot be empty or whitespace")]
    Empty,

    /// Key contains a character that is not allowed at the specified position
    ///
    /// Each domain defines which characters are allowed. This error provides
    /// the specific character, its position, and optionally what was expected.
    #[error("Invalid character '{character}' at position {position}")]
    InvalidCharacter {
        /// The invalid character that was found
        character: char,
        /// Position where the invalid character was found (0-based)
        position: usize,
        /// Optional description of what characters are expected
        expected: Option<&'static str>,
    },

    /// Key exceeds the maximum allowed length for the domain
    ///
    /// Each domain can specify a maximum length. This error provides both
    /// the limit and the actual length that was attempted.
    #[error("Key is too long (max {max_length} characters, got {actual_length})")]
    TooLong {
        /// The maximum allowed length for this domain
        max_length: usize,
        /// The actual length of the key that was attempted
        actual_length: usize,
    },

    /// Key is shorter than the minimum allowed length for the domain
    ///
    /// Each domain can specify a minimum length. This error provides both
    /// the required minimum and the actual length that was attempted.
    #[error("Key is too short (min {min_length} characters, got {actual_length})")]
    TooShort {
        /// The minimum allowed length for this domain
        min_length: usize,
        /// The actual length of the key that was attempted
        actual_length: usize,
    },

    /// Key has invalid structure (consecutive special chars, invalid start/end)
    ///
    /// This covers structural issues like:
    /// - Starting or ending with special characters
    /// - Consecutive special characters
    /// - Invalid character sequences
    #[error("Key has invalid structure: {reason}")]
    InvalidStructure {
        /// Description of the structural issue
        reason: &'static str,
    },

    /// Domain-specific validation error
    ///
    /// This error is returned when domain-specific validation rules fail.
    /// It includes the domain name and a descriptive message.
    #[error("Domain '{domain}' validation failed: {message}")]
    DomainValidation {
        /// The domain name where validation failed
        domain: &'static str,
        /// The error message describing what validation failed
        message: String,
    },

    /// Custom error for specific use cases
    ///
    /// Applications can define custom validation errors with numeric codes
    /// for structured error handling.
    #[error("Custom validation error (code: {code}): {message}")]
    Custom {
        /// Custom error code for programmatic handling
        code: u32,
        /// The custom error message
        message: String,
    },
}

impl KeyParseError {
    /// Create a domain validation error with domain name
    ///
    /// This is the preferred way to create domain validation errors.
    ///
    /// # Examples
    ///
    /// ```rust
    /// use domain_key::KeyParseError;
    ///
    /// let error = KeyParseError::domain_error("my_domain", "Custom validation failed");
    /// // Verify it's the correct error type
    /// match error {
    ///     KeyParseError::DomainValidation { domain, message } => {
    ///         assert_eq!(domain, "my_domain");
    ///         assert!(message.contains("Custom validation failed"));
    ///     },
    ///     _ => panic!("Expected domain validation error"),
    /// }
    /// ```
    pub fn domain_error(domain: &'static str, message: impl Into<String>) -> Self {
        Self::DomainValidation {
            domain,
            message: message.into(),
        }
    }

    /// Create a domain validation error without specifying domain (for internal use)
    pub fn domain_error_generic(message: impl Into<String>) -> Self {
        Self::DomainValidation {
            domain: "unknown",
            message: message.into(),
        }
    }

    /// Create a domain validation error with source error information
    ///
    /// The source error's `Display` representation is appended to `message`,
    /// separated by `": "`.  This is a **flattened** representation — the
    /// resulting `KeyParseError` does **not** implement `std::error::Error::source()`
    /// chaining back to the original error.  In other words, `error.source()`
    /// will return `None`, and tools such as `anyhow` / `tracing` that walk the
    /// error chain will not see the wrapped cause.
    ///
    /// If you need a proper causal chain, wrap the original error in your own
    /// error type (e.g. via `anyhow::Error` or a `thiserror` wrapper) before
    /// converting it to a `KeyParseError`.
    ///
    /// # Limitation
    ///
    /// Properly storing a `Box<dyn Error + Send + Sync>` inside `KeyParseError`
    /// variants would require either a new variant or a breaking field change.
    /// Until that refactor is done, the full error context is preserved only in
    /// the formatted message string.
    #[cfg(feature = "std")]
    pub fn domain_error_with_source(
        domain: &'static str,
        message: impl Into<String>,
        source: &(dyn std::error::Error + Send + Sync),
    ) -> Self {
        let full_message = format!("{}: {}", message.into(), source);
        Self::DomainValidation {
            domain,
            message: full_message,
        }
    }

    /// Create a custom validation error
    ///
    /// Custom errors allow applications to define their own error codes
    /// for structured error handling.
    ///
    /// # Examples
    ///
    /// ```rust
    /// use domain_key::KeyParseError;
    ///
    /// let error = KeyParseError::custom(1001, "Business rule violation");
    /// assert_eq!(error.code(), 1001);
    /// ```
    pub fn custom(code: u32, message: impl Into<String>) -> Self {
        Self::Custom {
            code,
            message: message.into(),
        }
    }

    /// Create a custom validation error with source error information
    ///
    /// The source error's `Display` representation is appended to `message`,
    /// separated by `": "`.  This is a **flattened** representation — the
    /// resulting `KeyParseError` does **not** implement `std::error::Error::source()`
    /// chaining back to the original error.  In other words, `error.source()`
    /// will return `None`, and tools such as `anyhow` / `tracing` that walk the
    /// error chain will not see the wrapped cause.
    ///
    /// If you need a proper causal chain, wrap the original error in your own
    /// error type (e.g. via `anyhow::Error` or a `thiserror` wrapper) before
    /// converting it to a `KeyParseError`.
    ///
    /// # Limitation
    ///
    /// Properly storing a `Box<dyn Error + Send + Sync>` inside `KeyParseError`
    /// variants would require either a new variant or a breaking field change.
    /// Until that refactor is done, the full error context is preserved only in
    /// the formatted message string.
    #[cfg(feature = "std")]
    pub fn custom_with_source(
        code: u32,
        message: impl Into<String>,
        source: &(dyn std::error::Error + Send + Sync),
    ) -> Self {
        let full_message = format!("{}: {}", message.into(), source);
        Self::Custom {
            code,
            message: full_message,
        }
    }

    /// Get the error code for machine processing
    ///
    /// Returns a numeric code that can be used for programmatic error handling.
    /// This is useful for APIs that need to return structured error responses.
    ///
    /// # Error Codes
    ///
    /// - `1001`: Empty key
    /// - `1002`: Invalid character
    /// - `1003`: Key too long
    /// - `1004`: Invalid structure
    /// - `2000`: Domain validation (base code)
    /// - Custom codes: As specified in `Custom` errors
    ///
    /// # Examples
    ///
    /// ```rust
    /// use domain_key::KeyParseError;
    ///
    /// assert_eq!(KeyParseError::Empty.code(), 1001);
    /// assert_eq!(KeyParseError::custom(42, "test").code(), 42);
    /// ```
    #[must_use]
    pub const fn code(&self) -> u32 {
        match self {
            Self::Empty => 1001,
            Self::InvalidCharacter { .. } => 1002,
            Self::TooLong { .. } => 1003,
            Self::InvalidStructure { .. } => 1004,
            Self::TooShort { .. } => 1005,
            Self::DomainValidation { .. } => 2000,
            Self::Custom { code, .. } => *code,
        }
    }

    /// Get the error category for classification
    ///
    /// Returns the general category of this error for higher-level error handling.
    /// This allows applications to handle broad categories of errors uniformly.
    ///
    /// # Examples
    ///
    /// ```rust
    /// use domain_key::{KeyParseError, ErrorCategory};
    ///
    /// match KeyParseError::Empty.category() {
    ///     ErrorCategory::Length => println!("Length-related error"),
    ///     ErrorCategory::Character => println!("Character-related error"),
    ///     _ => println!("Other error type"),
    /// }
    /// ```
    #[must_use]
    pub const fn category(&self) -> ErrorCategory {
        match self {
            Self::Empty | Self::TooLong { .. } | Self::TooShort { .. } => ErrorCategory::Length,
            Self::InvalidCharacter { .. } => ErrorCategory::Character,
            Self::InvalidStructure { .. } => ErrorCategory::Structure,
            Self::DomainValidation { .. } => ErrorCategory::Domain,
            Self::Custom { code, .. } => match code {
                1002 => ErrorCategory::Character,
                1003 => ErrorCategory::Length,
                1004 => ErrorCategory::Structure,
                _ => ErrorCategory::Custom,
            },
        }
    }

    /// Get a human-readable description of what went wrong
    ///
    /// This provides additional context beyond the basic error message,
    /// useful for user-facing error messages or debugging.
    #[must_use]
    pub fn description(&self) -> &'static str {
        match self {
            Self::Empty => "Key cannot be empty or contain only whitespace characters",
            Self::InvalidCharacter { .. } => {
                "Key contains characters that are not allowed by the domain"
            }
            Self::TooLong { .. } => "Key exceeds the maximum length allowed by the domain",
            Self::TooShort { .. } => {
                "Key is shorter than the minimum length required by the domain"
            }
            Self::InvalidStructure { .. } => "Key has invalid structure or formatting",
            Self::DomainValidation { .. } => "Key fails domain-specific validation rules",
            Self::Custom { .. } => "Key fails custom validation rules",
        }
    }

    /// Get suggested actions for fixing this error
    ///
    /// Returns helpful suggestions for how to fix the validation error.
    #[must_use]
    pub fn suggestions(&self) -> &'static [&'static str] {
        match self {
            Self::Empty => &[
                "Provide a non-empty key",
                "Remove leading/trailing whitespace",
            ],
            Self::InvalidCharacter { .. } => &[
                "Use only allowed characters (check domain rules)",
                "Remove or replace invalid characters",
            ],
            Self::TooLong { .. } => &[
                "Shorten the key to fit within length limits",
                "Consider using abbreviated forms",
            ],
            Self::TooShort { .. } => &["Lengthen the key to meet the minimum length requirement"],
            Self::InvalidStructure { .. } => &[
                "Avoid consecutive special characters",
                "Don't start or end with special characters",
                "Follow the expected key format",
            ],
            Self::DomainValidation { .. } => &[
                "Check domain-specific validation rules",
                "Refer to domain documentation",
            ],
            Self::Custom { .. } => &[
                "Check application-specific validation rules",
                "Contact system administrator if needed",
            ],
        }
    }

    /// Check if this error is recoverable through user action
    ///
    /// Returns `true` if the user can potentially fix this error by modifying
    /// their input, `false` if it represents a programming error or system issue.
    #[must_use]
    pub const fn is_recoverable(&self) -> bool {
        match self {
            Self::Empty
            | Self::InvalidCharacter { .. }
            | Self::TooLong { .. }
            | Self::TooShort { .. }
            | Self::InvalidStructure { .. }
            | Self::DomainValidation { .. } => true,
            Self::Custom { .. } => false, // Depends on the specific custom error
        }
    }
}

// ============================================================================
// ERROR CATEGORIES
// ============================================================================

/// Error category for classification of validation errors
///
/// These categories allow applications to handle broad types of validation
/// errors uniformly, regardless of the specific error details.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum ErrorCategory {
    /// Length-related errors (empty, too long)
    Length,
    /// Character-related errors (invalid characters)
    Character,
    /// Structure-related errors (invalid format, consecutive special chars)
    Structure,
    /// Domain-specific validation errors
    Domain,
    /// Custom validation errors
    Custom,
}

impl ErrorCategory {
    /// Get a human-readable name for this category
    #[must_use]
    pub const fn name(self) -> &'static str {
        match self {
            Self::Length => "Length",
            Self::Character => "Character",
            Self::Structure => "Structure",
            Self::Domain => "Domain",
            Self::Custom => "Custom",
        }
    }

    /// Get a description of what this category represents
    #[must_use]
    pub const fn description(self) -> &'static str {
        match self {
            Self::Length => "Errors related to key length (empty, too long, etc.)",
            Self::Character => "Errors related to invalid characters in the key",
            Self::Structure => "Errors related to key structure and formatting",
            Self::Domain => "Errors from domain-specific validation rules",
            Self::Custom => "Custom application-specific validation errors",
        }
    }
}

impl fmt::Display for ErrorCategory {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{}", self.name())
    }
}

// ============================================================================
// ID PARSE ERROR
// ============================================================================

/// Error type for numeric ID parsing failures
///
/// This error is returned when a string cannot be parsed as a valid `Id<D>`.
///
/// # Examples
///
/// ```rust
/// use domain_key::IdParseError;
///
/// let result = "not_a_number".parse::<u64>();
/// assert!(result.is_err());
/// ```
#[derive(Debug, Error, PartialEq, Eq, Clone)]
#[non_exhaustive]
pub enum IdParseError {
    /// The value is zero, which is not a valid identifier
    #[error("ID cannot be zero")]
    Zero,

    /// The string could not be parsed as a valid non-zero u64 number
    #[error("Invalid numeric ID: {0}")]
    InvalidNumber(#[from] core::num::ParseIntError),
}

impl IdParseError {
    /// Returns a user-friendly error message suitable for display.
    #[must_use]
    pub fn user_message(&self) -> &'static str {
        match self {
            Self::Zero => "Identifier cannot be zero",
            Self::InvalidNumber(_) => "Value must be a positive integer",
        }
    }
}

// ============================================================================
// UUID PARSE ERROR
// ============================================================================

/// Error type for UUID parsing failures
///
/// This error is returned when a string cannot be parsed as a valid `Uuid<D>`.
/// Only available when the `uuid` feature is enabled.
///
/// Note: `UuidParseError` does not implement `PartialEq` because
/// `uuid::Error` does not implement it.
#[cfg(feature = "uuid")]
#[derive(Debug, Error, Clone)]
#[non_exhaustive]
pub enum UuidParseError {
    /// The string could not be parsed as a valid UUID
    #[error("Invalid UUID: {0}")]
    InvalidUuid(#[from] ::uuid::Error),
}

// ============================================================================
// ULID PARSE ERROR
// ============================================================================

/// Error type for prefixed ULID parsing failures
///
/// Returned when a string cannot be parsed as [`crate::Ulid<D>`](crate::Ulid): wrong
/// `{PREFIX}_` prefix or invalid Crockford Base32 body.
///
/// Only available when the `ulid` feature is enabled.
#[cfg(feature = "ulid")]
#[derive(Debug, Error, PartialEq, Eq, Clone)]
#[non_exhaustive]
pub enum UlidParseError {
    /// The string does not start with `"{PREFIX}_"` for this domain
    #[error("invalid ULID prefix: expected `{expected_prefix}_...`")]
    WrongPrefix {
        /// Expected [`UlidDomain::PREFIX`](crate::UlidDomain::PREFIX) value
        expected_prefix: &'static str,
    },
    /// The ULID body (after the prefix) is not valid Crockford Base32
    #[error("invalid ULID: {0}")]
    InvalidUlid(::ulid::DecodeError),
}

// ============================================================================
// ERROR UTILITIES
// ============================================================================

/// Builder for creating detailed validation errors
///
/// This builder provides a fluent interface for creating complex validation
/// errors with additional context and suggestions.
#[derive(Debug)]
pub struct ErrorBuilder {
    category: ErrorCategory,
    code: Option<u32>,
    domain: Option<&'static str>,
    message: String,
    context: Option<String>,
}

impl ErrorBuilder {
    /// Create a new error builder for the given category
    #[must_use]
    pub fn new(category: ErrorCategory) -> Self {
        Self {
            category,
            code: None,
            domain: None,
            message: String::new(),
            context: None,
        }
    }

    /// Set the error message
    #[must_use]
    pub fn message(mut self, message: impl Into<String>) -> Self {
        self.message = message.into();
        self
    }

    /// Set a custom error code (used when category is `Custom`)
    #[must_use]
    pub fn code(mut self, code: u32) -> Self {
        self.code = Some(code);
        self
    }

    /// Set the domain name (used when category is `Domain`)
    #[must_use]
    pub fn domain(mut self, domain: &'static str) -> Self {
        self.domain = Some(domain);
        self
    }

    /// Set additional context information
    #[must_use]
    pub fn context(mut self, context: impl Into<String>) -> Self {
        self.context = Some(context.into());
        self
    }

    /// Build the final error
    #[must_use]
    pub fn build(self) -> KeyParseError {
        let message = if let Some(context) = self.context {
            format!("{} (Context: {})", self.message, context)
        } else {
            self.message
        };

        match self.category {
            ErrorCategory::Custom => KeyParseError::custom(self.code.unwrap_or(0), message),
            ErrorCategory::Domain => {
                KeyParseError::domain_error(self.domain.unwrap_or("unknown"), message)
            }
            // Use the reserved structural codes so that category() round-trips
            // correctly back to the originally-specified category.
            // 1004 → ErrorCategory::Structure, 1003 → ErrorCategory::Length,
            // 1002 → ErrorCategory::Character  (mirroring code() for each variant)
            ErrorCategory::Structure => KeyParseError::custom(1004, message),
            ErrorCategory::Length => KeyParseError::custom(1003, message),
            ErrorCategory::Character => KeyParseError::custom(1002, message),
        }
    }
}

// ============================================================================
// ERROR FORMATTING UTILITIES
// ============================================================================

/// Format an error for display to end users
///
/// This function provides a user-friendly representation of validation errors,
/// including suggestions for how to fix them.
#[must_use]
pub fn format_user_error(error: &KeyParseError) -> String {
    let mut output = format!("Error: {error}");

    let suggestions = error.suggestions();
    if !suggestions.is_empty() {
        output.push_str("\n\nSuggestions:");
        for suggestion in suggestions {
            write!(output, "\n  - {suggestion}").unwrap();
        }
    }

    output
}

/// Format an error for logging or debugging
///
/// This function provides a detailed representation suitable for logs,
/// including error codes and categories.
#[must_use]
pub fn format_debug_error(error: &KeyParseError) -> String {
    format!(
        "[{}:{}] {} (Category: {})",
        error.code(),
        error.category().name(),
        error,
        error.description()
    )
}

// ============================================================================
// TESTS
// ============================================================================

#[cfg(test)]
mod tests {
    use super::*;

    #[cfg(not(feature = "std"))]
    use alloc::string::ToString;

    #[test]
    fn each_variant_has_unique_error_code() {
        assert_eq!(KeyParseError::Empty.code(), 1001);
        assert_eq!(
            KeyParseError::InvalidCharacter {
                character: 'x',
                position: 0,
                expected: None
            }
            .code(),
            1002
        );
        assert_eq!(
            KeyParseError::TooLong {
                max_length: 10,
                actual_length: 20
            }
            .code(),
            1003
        );
        assert_eq!(
            KeyParseError::InvalidStructure { reason: "test" }.code(),
            1004
        );
        assert_eq!(
            KeyParseError::TooShort {
                min_length: 5,
                actual_length: 2
            }
            .code(),
            1005
        );
        assert_eq!(
            KeyParseError::DomainValidation {
                domain: "test",
                message: "msg".to_string()
            }
            .code(),
            2000
        );
        assert_eq!(
            KeyParseError::Custom {
                code: 42,
                message: "msg".to_string()
            }
            .code(),
            42
        );
    }

    #[test]
    fn variants_map_to_correct_category() {
        assert_eq!(KeyParseError::Empty.category(), ErrorCategory::Length);
        assert_eq!(
            KeyParseError::InvalidCharacter {
                character: 'x',
                position: 0,
                expected: None
            }
            .category(),
            ErrorCategory::Character
        );
        assert_eq!(
            KeyParseError::TooLong {
                max_length: 10,
                actual_length: 20
            }
            .category(),
            ErrorCategory::Length
        );
        assert_eq!(
            KeyParseError::InvalidStructure { reason: "test" }.category(),
            ErrorCategory::Structure
        );
        assert_eq!(
            KeyParseError::TooShort {
                min_length: 5,
                actual_length: 2
            }
            .category(),
            ErrorCategory::Length
        );
        assert_eq!(
            KeyParseError::DomainValidation {
                domain: "test",
                message: "msg".to_string()
            }
            .category(),
            ErrorCategory::Domain
        );
        assert_eq!(
            KeyParseError::Custom {
                code: 42,
                message: "msg".to_string()
            }
            .category(),
            ErrorCategory::Custom
        );
    }

    #[test]
    fn empty_error_provides_recovery_suggestions() {
        let error = KeyParseError::Empty;
        let suggestions = error.suggestions();
        assert!(!suggestions.is_empty());
        assert!(suggestions.iter().any(|s| s.contains("non-empty")));
    }

    #[test]
    fn builder_produces_custom_error_with_code_and_context() {
        let error = ErrorBuilder::new(ErrorCategory::Custom)
            .message("Test error")
            .code(1234)
            .context("In test function")
            .build();

        assert_eq!(error.code(), 1234);
        assert_eq!(error.category(), ErrorCategory::Custom);
    }

    #[test]
    fn variants_carry_correct_payloads() {
        let error1 = KeyParseError::InvalidCharacter {
            character: '!',
            position: 5,
            expected: Some("alphanumeric"),
        };
        assert!(matches!(
            error1,
            KeyParseError::InvalidCharacter {
                character: '!',
                position: 5,
                ..
            }
        ));

        let error2 = KeyParseError::TooLong {
            max_length: 32,
            actual_length: 64,
        };
        assert!(matches!(
            error2,
            KeyParseError::TooLong {
                max_length: 32,
                actual_length: 64
            }
        ));

        let error3 = KeyParseError::InvalidStructure {
            reason: "consecutive underscores",
        };
        assert!(matches!(
            error3,
            KeyParseError::InvalidStructure {
                reason: "consecutive underscores"
            }
        ));

        let error4 = KeyParseError::domain_error("test", "Invalid format");
        assert!(matches!(
            error4,
            KeyParseError::DomainValidation { domain: "test", .. }
        ));
    }

    #[test]
    fn user_and_debug_formats_include_expected_sections() {
        let error = KeyParseError::Empty;
        let user_format = format_user_error(&error);
        let debug_format = format_debug_error(&error);

        assert!(user_format.contains("Error:"));
        assert!(user_format.contains("Suggestions:"));
        assert!(debug_format.contains("1001"));
        assert!(debug_format.contains("Length"));
    }

    #[test]
    fn recoverable_errors_distinguished_from_non_recoverable() {
        assert!(KeyParseError::Empty.is_recoverable());
        assert!(KeyParseError::InvalidCharacter {
            character: 'x',
            position: 0,
            expected: None
        }
        .is_recoverable());
        assert!(KeyParseError::TooShort {
            min_length: 5,
            actual_length: 2
        }
        .is_recoverable());
        assert!(!KeyParseError::Custom {
            code: 42,
            message: "msg".to_string()
        }
        .is_recoverable());
    }

    #[test]
    fn category_display_and_description_are_populated() {
        assert_eq!(ErrorCategory::Length.to_string(), "Length");
        assert_eq!(ErrorCategory::Character.name(), "Character");
        assert!(ErrorCategory::Domain
            .description()
            .contains("domain-specific"));
    }
}