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}