Skip to main content

ddd_toolkit_core/domain/
error.rs

1use std::{error::Error, fmt::Display};
2
3/// A validation failure for a domain value, naming the type that rejected
4/// the value and why.
5///
6/// `type_name` is a plain string, not tied to `Self` by the compiler: it is
7/// whatever the caller of [`ValidationError::new`] passes in. Derive-macro
8/// generated code always passes the exact type name it was expanded for, so
9/// it is guaranteed accurate there. A hand-written `impl` (as in this
10/// crate's own test fixtures) must keep the `type_name` argument in sync
11/// with the type by hand - copy-pasting an impl from one type to another is
12/// the most likely way for it to drift.
13///
14/// # Examples
15///
16/// ```
17/// use ddd_toolkit_core::domain::ValidationError;
18///
19/// let error = ValidationError::new("Email", "missing '@'");
20///
21/// assert_eq!(error.type_name, "Email");
22/// assert_eq!(error.reason, "missing '@'");
23/// assert_eq!(error.to_string(), "invalid Email: missing '@'");
24/// ```
25#[derive(Debug, Clone, PartialEq, Eq)]
26pub struct ValidationError {
27    /// The name of the type that rejected the value.
28    pub type_name: &'static str,
29    /// A human-readable explanation of why the value was rejected.
30    pub reason: String,
31}
32
33impl ValidationError {
34    /// Creates a new `ValidationError` for the type named `type_name`.
35    pub fn new(type_name: &'static str, reason: impl Into<String>) -> Self {
36        Self {
37            type_name,
38            reason: reason.into(),
39        }
40    }
41}
42
43impl Display for ValidationError {
44    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
45        write!(f, "invalid {}: {}", self.type_name, self.reason)
46    }
47}
48
49impl Error for ValidationError {}
50
51#[cfg(test)]
52mod test {
53    use crate::domain::ValidationError;
54
55    #[test]
56    fn new_sets_type_name_field() {
57        let ve = ValidationError::new("foo", "bar");
58        assert_eq!(ve.type_name, "foo")
59    }
60
61    #[test]
62    fn new_sets_reason_field() {
63        let ve = ValidationError::new("foo", "bar");
64        assert_eq!(ve.reason, "bar")
65    }
66
67    #[test]
68    fn new_accepts_str_reason() {
69        ValidationError::new("foo", "bar");
70    }
71
72    #[test]
73    fn new_accepts_string_reason() {
74        ValidationError::new("foo", "bar".to_string());
75    }
76
77    #[test]
78    fn display_formats_invalid_type_and_reason() {
79        let ve = ValidationError::new("foo", "bar");
80        assert_eq!(format!("{ve}"), "invalid foo: bar")
81    }
82
83    #[test]
84    fn clone_produces_equal_value() {
85        let ve = ValidationError::new("foo", "bar");
86        let ve_clone = ve.clone();
87
88        assert_eq!(ve, ve_clone);
89        assert_eq!(ve.type_name, ve_clone.type_name);
90        assert_eq!(ve.reason, ve_clone.reason)
91    }
92
93    #[test]
94    fn eq_returns_true_for_same_fields() {
95        let ve = ValidationError::new("foo", "bar");
96        let ve_2 = ValidationError::new("foo", "bar");
97
98        assert_eq!(ve, ve_2);
99        assert_eq!(ve.type_name, ve_2.type_name);
100        assert_eq!(ve.reason, ve_2.reason)
101    }
102
103    #[test]
104    fn eq_returns_false_for_different_type_name() {
105        let ve = ValidationError::new("foo", "bar");
106        let ve_2 = ValidationError::new("bar", "bar");
107
108        assert_ne!(ve, ve_2);
109        assert_ne!(ve.type_name, ve_2.type_name);
110        assert_eq!(ve.reason, ve_2.reason)
111    }
112
113    #[test]
114    fn eq_returns_false_for_different_reason() {
115        let ve = ValidationError::new("foo", "bar");
116        let ve_2 = ValidationError::new("foo", "foo");
117
118        assert_ne!(ve, ve_2);
119        assert_eq!(ve.type_name, ve_2.type_name);
120        assert_ne!(ve.reason, ve_2.reason)
121    }
122
123    #[test]
124    fn implements_std_error_trait() {
125        let ve = ValidationError::new("foo", "bar");
126        let err: &dyn std::error::Error = &ve;
127        assert!(err.source().is_none())
128    }
129
130    #[test]
131    fn new_accepts_empty_reason() {
132        let ve = ValidationError::new("foo", "");
133        assert!(ve.reason.is_empty())
134    }
135
136    #[test]
137    fn display_formats_empty_reason() {
138        let ve: ValidationError = ValidationError::new("foo", "");
139        assert_eq!(format!("{}", ve), "invalid foo: ")
140    }
141}