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/// With the `serde` feature enabled, this derives `Serialize` only, not
15/// `Deserialize`: `type_name` is `&'static str`, and there is no sound way
16/// to produce a `&'static str` from arbitrary deserialized input without
17/// leaking memory. Serializing is the common need anyway (e.g. reporting a
18/// validation failure in an API error body); reconstructing one from JSON
19/// is not.
20///
21/// # Examples
22///
23/// ```
24/// use ddd_toolkit_core::domain::ValidationError;
25///
26/// let error = ValidationError::new("Email", "missing '@'");
27///
28/// assert_eq!(error.type_name, "Email");
29/// assert_eq!(error.reason, "missing '@'");
30/// assert_eq!(error.to_string(), "invalid Email: missing '@'");
31/// ```
32#[derive(Debug, Clone, PartialEq, Eq)]
33#[cfg_attr(feature = "serde", derive(serde::Serialize))]
34pub struct ValidationError {
35    /// The name of the type that rejected the value.
36    pub type_name: &'static str,
37    /// A human-readable explanation of why the value was rejected.
38    pub reason: String,
39}
40
41impl ValidationError {
42    /// Creates a new `ValidationError` for the type named `type_name`.
43    pub fn new(type_name: &'static str, reason: impl Into<String>) -> Self {
44        Self {
45            type_name,
46            reason: reason.into(),
47        }
48    }
49}
50
51impl Display for ValidationError {
52    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
53        write!(f, "invalid {}: {}", self.type_name, self.reason)
54    }
55}
56
57impl Error for ValidationError {}
58
59#[cfg(test)]
60mod test {
61    use crate::domain::ValidationError;
62
63    #[test]
64    fn new_sets_type_name_field() {
65        let ve = ValidationError::new("foo", "bar");
66        assert_eq!(ve.type_name, "foo")
67    }
68
69    #[test]
70    fn new_sets_reason_field() {
71        let ve = ValidationError::new("foo", "bar");
72        assert_eq!(ve.reason, "bar")
73    }
74
75    #[test]
76    fn new_accepts_str_reason() {
77        ValidationError::new("foo", "bar");
78    }
79
80    #[test]
81    fn new_accepts_string_reason() {
82        ValidationError::new("foo", "bar".to_string());
83    }
84
85    #[test]
86    fn display_formats_invalid_type_and_reason() {
87        let ve = ValidationError::new("foo", "bar");
88        assert_eq!(format!("{ve}"), "invalid foo: bar")
89    }
90
91    #[test]
92    fn clone_produces_equal_value() {
93        let ve = ValidationError::new("foo", "bar");
94        let ve_clone = ve.clone();
95
96        assert_eq!(ve, ve_clone);
97        assert_eq!(ve.type_name, ve_clone.type_name);
98        assert_eq!(ve.reason, ve_clone.reason)
99    }
100
101    #[test]
102    fn eq_returns_true_for_same_fields() {
103        let ve = ValidationError::new("foo", "bar");
104        let ve_2 = ValidationError::new("foo", "bar");
105
106        assert_eq!(ve, ve_2);
107        assert_eq!(ve.type_name, ve_2.type_name);
108        assert_eq!(ve.reason, ve_2.reason)
109    }
110
111    #[test]
112    fn eq_returns_false_for_different_type_name() {
113        let ve = ValidationError::new("foo", "bar");
114        let ve_2 = ValidationError::new("bar", "bar");
115
116        assert_ne!(ve, ve_2);
117        assert_ne!(ve.type_name, ve_2.type_name);
118        assert_eq!(ve.reason, ve_2.reason)
119    }
120
121    #[test]
122    fn eq_returns_false_for_different_reason() {
123        let ve = ValidationError::new("foo", "bar");
124        let ve_2 = ValidationError::new("foo", "foo");
125
126        assert_ne!(ve, ve_2);
127        assert_eq!(ve.type_name, ve_2.type_name);
128        assert_ne!(ve.reason, ve_2.reason)
129    }
130
131    #[test]
132    fn implements_std_error_trait() {
133        let ve = ValidationError::new("foo", "bar");
134        let err: &dyn std::error::Error = &ve;
135        assert!(err.source().is_none())
136    }
137
138    #[test]
139    fn new_accepts_empty_reason() {
140        let ve = ValidationError::new("foo", "");
141        assert!(ve.reason.is_empty())
142    }
143
144    #[test]
145    fn display_formats_empty_reason() {
146        let ve: ValidationError = ValidationError::new("foo", "");
147        assert_eq!(format!("{}", ve), "invalid foo: ")
148    }
149
150    #[cfg(feature = "serde")]
151    #[test]
152    fn serializes_to_json_object_with_type_name_and_reason_fields() {
153        let ve = ValidationError::new("Email", "missing '@'");
154        let json = serde_json::to_value(&ve).unwrap();
155        assert_eq!(
156            json,
157            serde_json::json!({"type_name": "Email", "reason": "missing '@'"})
158        );
159    }
160}