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 has invalid structure (consecutive special chars, invalid start/end)
89 ///
90 /// This covers structural issues like:
91 /// - Starting or ending with special characters
92 /// - Consecutive special characters
93 /// - Invalid character sequences
94 #[error("Key has invalid structure: {reason}")]
95 InvalidStructure {
96 /// Description of the structural issue
97 reason: &'static str,
98 },
99
100 /// Domain-specific validation error
101 ///
102 /// This error is returned when domain-specific validation rules fail.
103 /// It includes the domain name and a descriptive message.
104 #[error("Domain '{domain}' validation failed: {message}")]
105 DomainValidation {
106 /// The domain name where validation failed
107 domain: &'static str,
108 /// The error message describing what validation failed
109 message: String,
110 },
111
112 /// Custom error for specific use cases
113 ///
114 /// Applications can define custom validation errors with numeric codes
115 /// for structured error handling.
116 #[error("Custom validation error (code: {code}): {message}")]
117 Custom {
118 /// Custom error code for programmatic handling
119 code: u32,
120 /// The custom error message
121 message: String,
122 },
123}
124
125impl KeyParseError {
126 /// Create a domain validation error with domain name
127 ///
128 /// This is the preferred way to create domain validation errors.
129 ///
130 /// # Examples
131 ///
132 /// ```rust
133 /// use domain_key::KeyParseError;
134 ///
135 /// let error = KeyParseError::domain_error("my_domain", "Custom validation failed");
136 /// // Verify it's the correct error type
137 /// match error {
138 /// KeyParseError::DomainValidation { domain, message } => {
139 /// assert_eq!(domain, "my_domain");
140 /// assert!(message.contains("Custom validation failed"));
141 /// },
142 /// _ => panic!("Expected domain validation error"),
143 /// }
144 /// ```
145 pub fn domain_error(domain: &'static str, message: impl Into<String>) -> Self {
146 Self::DomainValidation {
147 domain,
148 message: message.into(),
149 }
150 }
151
152 /// Create a domain validation error without specifying domain (for internal use)
153 pub fn domain_error_generic(message: impl Into<String>) -> Self {
154 Self::DomainValidation {
155 domain: "unknown",
156 message: message.into(),
157 }
158 }
159
160 /// Create a domain validation error with source error information
161 #[cfg(feature = "std")]
162 pub fn domain_error_with_source(
163 domain: &'static str,
164 message: impl Into<String>,
165 source: &(dyn std::error::Error + Send + Sync),
166 ) -> Self {
167 let full_message = format!("{}: {}", message.into(), source);
168 Self::DomainValidation {
169 domain,
170 message: full_message,
171 }
172 }
173
174 /// Create a custom validation error
175 ///
176 /// Custom errors allow applications to define their own error codes
177 /// for structured error handling.
178 ///
179 /// # Examples
180 ///
181 /// ```rust
182 /// use domain_key::KeyParseError;
183 ///
184 /// let error = KeyParseError::custom(1001, "Business rule violation");
185 /// assert_eq!(error.code(), 1001);
186 /// ```
187 pub fn custom(code: u32, message: impl Into<String>) -> Self {
188 Self::Custom {
189 code,
190 message: message.into(),
191 }
192 }
193
194 /// Create a custom validation error with source error information
195 #[cfg(feature = "std")]
196 pub fn custom_with_source(
197 code: u32,
198 message: impl Into<String>,
199 source: &(dyn std::error::Error + Send + Sync),
200 ) -> Self {
201 let full_message = format!("{}: {}", message.into(), source);
202 Self::Custom {
203 code,
204 message: full_message,
205 }
206 }
207
208 /// Get the error code for machine processing
209 ///
210 /// Returns a numeric code that can be used for programmatic error handling.
211 /// This is useful for APIs that need to return structured error responses.
212 ///
213 /// # Error Codes
214 ///
215 /// - `1001`: Empty key
216 /// - `1002`: Invalid character
217 /// - `1003`: Key too long
218 /// - `1004`: Invalid structure
219 /// - `2000`: Domain validation (base code)
220 /// - Custom codes: As specified in `Custom` errors
221 ///
222 /// # Examples
223 ///
224 /// ```rust
225 /// use domain_key::KeyParseError;
226 ///
227 /// assert_eq!(KeyParseError::Empty.code(), 1001);
228 /// assert_eq!(KeyParseError::custom(42, "test").code(), 42);
229 /// ```
230 #[must_use]
231 pub const fn code(&self) -> u32 {
232 match self {
233 Self::Empty => 1001,
234 Self::InvalidCharacter { .. } => 1002,
235 Self::TooLong { .. } => 1003,
236 Self::InvalidStructure { .. } => 1004,
237 Self::DomainValidation { .. } => 2000,
238 Self::Custom { code, .. } => *code,
239 }
240 }
241
242 /// Get the error category for classification
243 ///
244 /// Returns the general category of this error for higher-level error handling.
245 /// This allows applications to handle broad categories of errors uniformly.
246 ///
247 /// # Examples
248 ///
249 /// ```rust
250 /// use domain_key::{KeyParseError, ErrorCategory};
251 ///
252 /// match KeyParseError::Empty.category() {
253 /// ErrorCategory::Length => println!("Length-related error"),
254 /// ErrorCategory::Character => println!("Character-related error"),
255 /// _ => println!("Other error type"),
256 /// }
257 /// ```
258 #[must_use]
259 pub const fn category(&self) -> ErrorCategory {
260 match self {
261 Self::Empty | Self::TooLong { .. } => ErrorCategory::Length,
262 Self::InvalidCharacter { .. } => ErrorCategory::Character,
263 Self::InvalidStructure { .. } => ErrorCategory::Structure,
264 Self::DomainValidation { .. } => ErrorCategory::Domain,
265 Self::Custom { .. } => ErrorCategory::Custom,
266 }
267 }
268
269 /// Get a human-readable description of what went wrong
270 ///
271 /// This provides additional context beyond the basic error message,
272 /// useful for user-facing error messages or debugging.
273 #[must_use]
274 pub fn description(&self) -> &'static str {
275 match self {
276 Self::Empty => "Key cannot be empty or contain only whitespace characters",
277 Self::InvalidCharacter { .. } => {
278 "Key contains characters that are not allowed by the domain"
279 }
280 Self::TooLong { .. } => "Key exceeds the maximum length allowed by the domain",
281 Self::InvalidStructure { .. } => "Key has invalid structure or formatting",
282 Self::DomainValidation { .. } => "Key fails domain-specific validation rules",
283 Self::Custom { .. } => "Key fails custom validation rules",
284 }
285 }
286
287 /// Get suggested actions for fixing this error
288 ///
289 /// Returns helpful suggestions for how to fix the validation error.
290 #[must_use]
291 pub fn suggestions(&self) -> Vec<&'static str> {
292 match self {
293 Self::Empty => vec![
294 "Provide a non-empty key",
295 "Remove leading/trailing whitespace",
296 ],
297 Self::InvalidCharacter { .. } => vec![
298 "Use only allowed characters (check domain rules)",
299 "Remove or replace invalid characters",
300 ],
301 Self::TooLong { .. } => vec![
302 "Shorten the key to fit within length limits",
303 "Consider using abbreviated forms",
304 ],
305 Self::InvalidStructure { .. } => vec![
306 "Avoid consecutive special characters",
307 "Don't start or end with special characters",
308 "Follow the expected key format",
309 ],
310 Self::DomainValidation { .. } => vec![
311 "Check domain-specific validation rules",
312 "Refer to domain documentation",
313 ],
314 Self::Custom { .. } => vec![
315 "Check application-specific validation rules",
316 "Contact system administrator if needed",
317 ],
318 }
319 }
320
321 /// Check if this error is recoverable through user action
322 ///
323 /// Returns `true` if the user can potentially fix this error by modifying
324 /// their input, `false` if it represents a programming error or system issue.
325 #[must_use]
326 pub const fn is_recoverable(&self) -> bool {
327 match self {
328 Self::Empty
329 | Self::InvalidCharacter { .. }
330 | Self::TooLong { .. }
331 | Self::InvalidStructure { .. }
332 | Self::DomainValidation { .. } => true,
333 Self::Custom { .. } => false, // Depends on the specific custom error
334 }
335 }
336}
337
338// ============================================================================
339// ERROR CATEGORIES
340// ============================================================================
341
342/// Error category for classification of validation errors
343///
344/// These categories allow applications to handle broad types of validation
345/// errors uniformly, regardless of the specific error details.
346#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
347#[non_exhaustive]
348pub enum ErrorCategory {
349 /// Length-related errors (empty, too long)
350 Length,
351 /// Character-related errors (invalid characters)
352 Character,
353 /// Structure-related errors (invalid format, consecutive special chars)
354 Structure,
355 /// Domain-specific validation errors
356 Domain,
357 /// Custom validation errors
358 Custom,
359}
360
361impl ErrorCategory {
362 /// Get a human-readable name for this category
363 #[must_use]
364 pub const fn name(self) -> &'static str {
365 match self {
366 Self::Length => "Length",
367 Self::Character => "Character",
368 Self::Structure => "Structure",
369 Self::Domain => "Domain",
370 Self::Custom => "Custom",
371 }
372 }
373
374 /// Get a description of what this category represents
375 #[must_use]
376 pub const fn description(self) -> &'static str {
377 match self {
378 Self::Length => "Errors related to key length (empty, too long, etc.)",
379 Self::Character => "Errors related to invalid characters in the key",
380 Self::Structure => "Errors related to key structure and formatting",
381 Self::Domain => "Errors from domain-specific validation rules",
382 Self::Custom => "Custom application-specific validation errors",
383 }
384 }
385}
386
387impl fmt::Display for ErrorCategory {
388 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
389 write!(f, "{}", self.name())
390 }
391}
392
393// ============================================================================
394// ID PARSE ERROR
395// ============================================================================
396
397/// Error type for numeric ID parsing failures
398///
399/// This error is returned when a string cannot be parsed as a valid `Id<D>`.
400///
401/// # Examples
402///
403/// ```rust
404/// use domain_key::IdParseError;
405///
406/// let result = "not_a_number".parse::<u64>();
407/// assert!(result.is_err());
408/// ```
409#[derive(Debug, Error, PartialEq, Eq, Clone)]
410#[non_exhaustive]
411pub enum IdParseError {
412 /// The value is zero, which is not a valid identifier
413 #[error("ID cannot be zero")]
414 Zero,
415
416 /// The string could not be parsed as a valid non-zero u64 number
417 #[error("Invalid numeric ID: {0}")]
418 InvalidNumber(#[from] core::num::ParseIntError),
419}
420
421impl IdParseError {
422 /// Returns a user-friendly error message suitable for display.
423 #[must_use]
424 pub fn user_message(&self) -> &'static str {
425 match self {
426 Self::Zero => "Identifier cannot be zero",
427 Self::InvalidNumber(_) => "Value must be a positive integer",
428 }
429 }
430}
431
432// ============================================================================
433// UUID PARSE ERROR
434// ============================================================================
435
436/// Error type for UUID parsing failures
437///
438/// This error is returned when a string cannot be parsed as a valid `Uuid<D>`.
439/// Only available when the `uuid` feature is enabled.
440///
441/// Note: `UuidParseError` does not implement `PartialEq` because
442/// `uuid::Error` does not implement it.
443#[cfg(feature = "uuid")]
444#[derive(Debug, Error, Clone)]
445#[non_exhaustive]
446pub enum UuidParseError {
447 /// The string could not be parsed as a valid UUID
448 #[error("Invalid UUID: {0}")]
449 InvalidUuid(#[from] ::uuid::Error),
450}
451
452// ============================================================================
453// ERROR UTILITIES
454// ============================================================================
455
456/// Builder for creating detailed validation errors
457///
458/// This builder provides a fluent interface for creating complex validation
459/// errors with additional context and suggestions.
460#[derive(Debug)]
461pub struct ErrorBuilder {
462 category: ErrorCategory,
463 code: Option<u32>,
464 domain: Option<&'static str>,
465 message: String,
466 context: Option<String>,
467}
468
469impl ErrorBuilder {
470 /// Create a new error builder for the given category
471 #[must_use]
472 pub fn new(category: ErrorCategory) -> Self {
473 Self {
474 category,
475 code: None,
476 domain: None,
477 message: String::new(),
478 context: None,
479 }
480 }
481
482 /// Set the error message
483 #[must_use]
484 pub fn message(mut self, message: impl Into<String>) -> Self {
485 self.message = message.into();
486 self
487 }
488
489 /// Set a custom error code (used when category is `Custom`)
490 #[must_use]
491 pub fn code(mut self, code: u32) -> Self {
492 self.code = Some(code);
493 self
494 }
495
496 /// Set the domain name (used when category is `Domain`)
497 #[must_use]
498 pub fn domain(mut self, domain: &'static str) -> Self {
499 self.domain = Some(domain);
500 self
501 }
502
503 /// Set additional context information
504 #[must_use]
505 pub fn context(mut self, context: impl Into<String>) -> Self {
506 self.context = Some(context.into());
507 self
508 }
509
510 /// Build the final error
511 #[must_use]
512 pub fn build(self) -> KeyParseError {
513 let message = if let Some(context) = self.context {
514 format!("{} (Context: {})", self.message, context)
515 } else {
516 self.message
517 };
518
519 match self.category {
520 ErrorCategory::Custom => KeyParseError::custom(self.code.unwrap_or(0), message),
521 ErrorCategory::Domain => {
522 KeyParseError::domain_error(self.domain.unwrap_or("unknown"), message)
523 }
524 ErrorCategory::Structure => {
525 // Structure errors use static str, so we fall back to domain error
526 // to preserve the dynamic message
527 KeyParseError::domain_error(self.domain.unwrap_or("unknown"), message)
528 }
529 ErrorCategory::Length | ErrorCategory::Character => {
530 // These categories have specific variants; fall back to domain error
531 // for builder-created errors with dynamic messages
532 KeyParseError::domain_error(self.domain.unwrap_or("unknown"), message)
533 }
534 }
535 }
536}
537
538// ============================================================================
539// ERROR FORMATTING UTILITIES
540// ============================================================================
541
542/// Format an error for display to end users
543///
544/// This function provides a user-friendly representation of validation errors,
545/// including suggestions for how to fix them.
546#[must_use]
547pub fn format_user_error(error: &KeyParseError) -> String {
548 let mut output = format!("Error: {error}");
549
550 let suggestions = error.suggestions();
551 if !suggestions.is_empty() {
552 output.push_str("\n\nSuggestions:");
553 for suggestion in suggestions {
554 write!(output, "\n - {suggestion}").unwrap();
555 }
556 }
557
558 output
559}
560
561/// Format an error for logging or debugging
562///
563/// This function provides a detailed representation suitable for logs,
564/// including error codes and categories.
565#[must_use]
566pub fn format_debug_error(error: &KeyParseError) -> String {
567 format!(
568 "[{}:{}] {} (Category: {})",
569 error.code(),
570 error.category().name(),
571 error,
572 error.description()
573 )
574}
575
576// ============================================================================
577// TESTS
578// ============================================================================
579
580#[cfg(test)]
581mod tests {
582 use super::*;
583
584 #[cfg(not(feature = "std"))]
585 use alloc::string::ToString;
586
587 #[test]
588 fn each_variant_has_unique_error_code() {
589 assert_eq!(KeyParseError::Empty.code(), 1001);
590 assert_eq!(
591 KeyParseError::InvalidCharacter {
592 character: 'x',
593 position: 0,
594 expected: None
595 }
596 .code(),
597 1002
598 );
599 assert_eq!(
600 KeyParseError::TooLong {
601 max_length: 10,
602 actual_length: 20
603 }
604 .code(),
605 1003
606 );
607 assert_eq!(
608 KeyParseError::InvalidStructure { reason: "test" }.code(),
609 1004
610 );
611 assert_eq!(
612 KeyParseError::DomainValidation {
613 domain: "test",
614 message: "msg".to_string()
615 }
616 .code(),
617 2000
618 );
619 assert_eq!(
620 KeyParseError::Custom {
621 code: 42,
622 message: "msg".to_string()
623 }
624 .code(),
625 42
626 );
627 }
628
629 #[test]
630 fn variants_map_to_correct_category() {
631 assert_eq!(KeyParseError::Empty.category(), ErrorCategory::Length);
632 assert_eq!(
633 KeyParseError::InvalidCharacter {
634 character: 'x',
635 position: 0,
636 expected: None
637 }
638 .category(),
639 ErrorCategory::Character
640 );
641 assert_eq!(
642 KeyParseError::TooLong {
643 max_length: 10,
644 actual_length: 20
645 }
646 .category(),
647 ErrorCategory::Length
648 );
649 assert_eq!(
650 KeyParseError::InvalidStructure { reason: "test" }.category(),
651 ErrorCategory::Structure
652 );
653 assert_eq!(
654 KeyParseError::DomainValidation {
655 domain: "test",
656 message: "msg".to_string()
657 }
658 .category(),
659 ErrorCategory::Domain
660 );
661 assert_eq!(
662 KeyParseError::Custom {
663 code: 42,
664 message: "msg".to_string()
665 }
666 .category(),
667 ErrorCategory::Custom
668 );
669 }
670
671 #[test]
672 fn empty_error_provides_recovery_suggestions() {
673 let error = KeyParseError::Empty;
674 let suggestions = error.suggestions();
675 assert!(!suggestions.is_empty());
676 assert!(suggestions.iter().any(|s| s.contains("non-empty")));
677 }
678
679 #[test]
680 fn builder_produces_custom_error_with_code_and_context() {
681 let error = ErrorBuilder::new(ErrorCategory::Custom)
682 .message("Test error")
683 .code(1234)
684 .context("In test function")
685 .build();
686
687 assert_eq!(error.code(), 1234);
688 assert_eq!(error.category(), ErrorCategory::Custom);
689 }
690
691 #[test]
692 fn variants_carry_correct_payloads() {
693 let error1 = KeyParseError::InvalidCharacter {
694 character: '!',
695 position: 5,
696 expected: Some("alphanumeric"),
697 };
698 assert!(matches!(
699 error1,
700 KeyParseError::InvalidCharacter {
701 character: '!',
702 position: 5,
703 ..
704 }
705 ));
706
707 let error2 = KeyParseError::TooLong {
708 max_length: 32,
709 actual_length: 64,
710 };
711 assert!(matches!(
712 error2,
713 KeyParseError::TooLong {
714 max_length: 32,
715 actual_length: 64
716 }
717 ));
718
719 let error3 = KeyParseError::InvalidStructure {
720 reason: "consecutive underscores",
721 };
722 assert!(matches!(
723 error3,
724 KeyParseError::InvalidStructure {
725 reason: "consecutive underscores"
726 }
727 ));
728
729 let error4 = KeyParseError::domain_error("test", "Invalid format");
730 assert!(matches!(
731 error4,
732 KeyParseError::DomainValidation { domain: "test", .. }
733 ));
734 }
735
736 #[test]
737 fn user_and_debug_formats_include_expected_sections() {
738 let error = KeyParseError::Empty;
739 let user_format = format_user_error(&error);
740 let debug_format = format_debug_error(&error);
741
742 assert!(user_format.contains("Error:"));
743 assert!(user_format.contains("Suggestions:"));
744 assert!(debug_format.contains("1001"));
745 assert!(debug_format.contains("Length"));
746 }
747
748 #[test]
749 fn recoverable_errors_distinguished_from_non_recoverable() {
750 assert!(KeyParseError::Empty.is_recoverable());
751 assert!(KeyParseError::InvalidCharacter {
752 character: 'x',
753 position: 0,
754 expected: None
755 }
756 .is_recoverable());
757 assert!(!KeyParseError::Custom {
758 code: 42,
759 message: "msg".to_string()
760 }
761 .is_recoverable());
762 }
763
764 #[test]
765 fn category_display_and_description_are_populated() {
766 assert_eq!(ErrorCategory::Length.to_string(), "Length");
767 assert_eq!(ErrorCategory::Character.name(), "Character");
768 assert!(ErrorCategory::Domain
769 .description()
770 .contains("domain-specific"));
771 }
772}