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}