Skip to main content

ddd_toolkit_core/domain/
vo.rs

1use std::fmt::Debug;
2
3/// A domain object with no identity, whose equality is purely structural:
4/// two value objects are the same if their data is the same.
5///
6/// # Examples
7///
8/// ```
9/// use ddd_toolkit_core::domain::ValueObject;
10///
11/// #[derive(Debug, Clone, PartialEq)]
12/// struct Money(u32);
13///
14/// impl ValueObject for Money {}
15///
16/// assert_eq!(Money(500), Money(500));
17/// assert_ne!(Money(500), Money(100));
18/// ```
19pub trait ValueObject: Clone + PartialEq + Debug + Send + Sync {}
20
21/// A type that wraps a single inner value.
22///
23/// Always implemented alongside [`Wrapped`]; see [`Wrapped`]'s docs for a
24/// worked example.
25pub trait WrappedInner {
26    /// The wrapped type.
27    type Inner;
28}
29
30/// A [`ValueObject`] newtype around a single [`WrappedInner::Inner`] value,
31/// only constructible through validation (`TryFrom<Self::Inner>`).
32///
33/// Note this invariant only holds if the wrapped field is private - see
34/// `#[derive(ValueObject)]` in `ddd-toolkit-macro`, which enforces that at
35/// derive time.
36///
37/// # Examples
38///
39/// ```
40/// use ddd_toolkit_core::domain::{ValidationError, ValueObject, Wrapped, WrappedInner};
41///
42/// #[derive(Debug, Clone, PartialEq)]
43/// struct NonEmptyString(String);
44///
45/// impl ValueObject for NonEmptyString {}
46///
47/// impl WrappedInner for NonEmptyString {
48///     type Inner = String;
49/// }
50///
51/// impl Wrapped for NonEmptyString {
52///     fn as_inner(&self) -> &Self::Inner {
53///         &self.0
54///     }
55///
56///     fn into_inner(self) -> Self::Inner {
57///         self.0
58///     }
59/// }
60///
61/// impl TryFrom<String> for NonEmptyString {
62///     type Error = ValidationError;
63///
64///     fn try_from(value: String) -> Result<Self, Self::Error> {
65///         if value.trim().is_empty() {
66///             return Err(ValidationError::new("NonEmptyString", "empty"));
67///         }
68///         Ok(Self(value))
69///     }
70/// }
71///
72/// let name: NonEmptyString = "alice".to_string().try_into()?;
73/// assert_eq!(name.as_inner(), "alice");
74/// assert_eq!(name.into_inner(), "alice".to_string());
75///
76/// assert!(NonEmptyString::try_from(String::new()).is_err());
77/// # Ok::<(), ValidationError>(())
78/// ```
79pub trait Wrapped:
80    ValueObject
81    + WrappedInner
82    + TryFrom<<Self as WrappedInner>::Inner, Error: std::error::Error + Send + Sync + 'static>
83{
84    /// Borrows the wrapped value.
85    fn as_inner(&self) -> &Self::Inner;
86    /// Consumes `self`, returning the wrapped value.
87    fn into_inner(self) -> Self::Inner;
88}
89
90#[cfg(test)]
91mod test {
92    use super::*;
93    use crate::domain::ValidationError;
94
95    #[derive(Clone, PartialEq, Debug)]
96    struct FooValue(String);
97
98    impl ValueObject for FooValue {}
99
100    impl WrappedInner for FooValue {
101        type Inner = String;
102    }
103
104    impl Wrapped for FooValue {
105        fn as_inner(&self) -> &Self::Inner {
106            &self.0
107        }
108
109        fn into_inner(self) -> Self::Inner {
110            self.0
111        }
112    }
113
114    impl TryFrom<String> for FooValue {
115        type Error = ValidationError;
116
117        fn try_from(value: String) -> Result<Self, Self::Error> {
118            if value.trim().is_empty() {
119                return Err(ValidationError::new("FooValue", "empty"));
120            }
121            Ok(Self(value))
122        }
123    }
124
125    #[test]
126    fn try_from_non_empty_string_succeeds() {
127        assert!(FooValue::try_from("value".to_string()).is_ok())
128    }
129
130    #[test]
131    fn as_inner_returns_reference_to_original_value() {
132        let foo = FooValue("value".to_string());
133        assert_eq!(foo.as_inner(), "value")
134    }
135
136    #[test]
137    fn into_inner_returns_owned_original_value() {
138        let foo = FooValue("value".to_string());
139        assert_eq!(foo.into_inner(), "value".to_string())
140    }
141
142    #[test]
143    fn round_trip_preserves_value() {
144        let foo = FooValue::try_from("value".to_string()).unwrap();
145        assert_eq!(foo.into_inner(), "value".to_string())
146    }
147
148    #[test]
149    fn clone_produces_equal_value() {
150        let foo = FooValue("value".to_string());
151        let bar = foo.clone();
152
153        assert_eq!(foo, bar);
154        assert_eq!(foo.into_inner(), bar.into_inner());
155    }
156
157    #[test]
158    fn eq_returns_true_for_same_inner_value() {
159        let foo = FooValue("foo".to_string());
160        let bar = FooValue("foo".to_string());
161
162        assert_eq!(foo, bar);
163        assert_eq!(foo.into_inner(), bar.into_inner());
164    }
165
166    #[test]
167    fn eq_returns_false_for_different_inner_value() {
168        let foo = FooValue("foo".to_string());
169        let bar = FooValue("bar".to_string());
170
171        assert_ne!(foo, bar);
172        assert_ne!(foo.into_inner(), bar.into_inner());
173    }
174
175    #[test]
176    fn try_from_empty_string_returns_err() {
177        assert!(FooValue::try_from("".to_string()).is_err());
178        assert!(FooValue::try_from("  ".to_string()).is_err());
179    }
180
181    #[test]
182    fn try_from_empty_string_error_has_expected_type_name() {
183        let err = FooValue::try_from("".to_string()).unwrap_err();
184        assert_eq!(err.type_name, "FooValue")
185    }
186
187    #[test]
188    fn try_from_empty_string_error_has_expected_reason() {
189        let err = FooValue::try_from("".to_string()).unwrap_err();
190        assert_eq!(err.reason, "empty")
191    }
192
193    #[test]
194    fn try_from_empty_string_error_display_message() {
195        let err = FooValue::try_from("".to_string()).unwrap_err();
196        assert_eq!(format!("{err}"), "invalid FooValue: empty")
197    }
198}