Skip to main content

domain_key/
error.rs

1//! Error types and error handling for domain-key
2//!
3//! This module provides comprehensive error handling for key validation and creation.
4//! All errors are designed to provide detailed information for debugging while maintaining
5//! performance in the happy path.
6
7use core::fmt;
8use thiserror::Error;
9
10#[cfg(not(feature = "std"))]
11use alloc::format;
12#[cfg(not(feature = "std"))]
13use alloc::string::String;
14#[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}