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.
5pub trait ValueObject: Clone + PartialEq + Debug + Send + Sync {}
6
7/// A type that wraps a single inner value.
8pub trait WrappedInner {
9    /// The wrapped type.
10    type Inner;
11}
12
13/// A [`ValueObject`] newtype around a single [`WrappedInner::Inner`] value,
14/// only constructible through validation (`TryFrom<Self::Inner>`).
15///
16/// Note this invariant only holds if the wrapped field is private - see
17/// `#[derive(ValueObject)]` in `ddd-toolkit-macro`, which enforces that at
18/// derive time.
19pub trait Wrapped:
20    ValueObject
21    + WrappedInner
22    + TryFrom<<Self as WrappedInner>::Inner, Error: std::error::Error + Send + Sync + 'static>
23{
24    /// Borrows the wrapped value.
25    fn as_inner(&self) -> &Self::Inner;
26    /// Consumes `self`, returning the wrapped value.
27    fn into_inner(self) -> Self::Inner;
28}
29
30#[cfg(test)]
31mod test {
32    use super::*;
33    use crate::domain::ValidationError;
34
35    #[derive(Clone, PartialEq, Debug)]
36    struct FooValue(String);
37
38    impl ValueObject for FooValue {}
39
40    impl WrappedInner for FooValue {
41        type Inner = String;
42    }
43
44    impl Wrapped for FooValue {
45        fn as_inner(&self) -> &Self::Inner {
46            &self.0
47        }
48
49        fn into_inner(self) -> Self::Inner {
50            self.0
51        }
52    }
53
54    impl TryFrom<String> for FooValue {
55        type Error = ValidationError;
56
57        fn try_from(value: String) -> Result<Self, Self::Error> {
58            if value.trim().is_empty() {
59                return Err(ValidationError::new("FooValue", "empty"));
60            }
61            Ok(Self(value))
62        }
63    }
64
65    #[test]
66    fn try_from_non_empty_string_succeeds() {
67        assert!(FooValue::try_from("value".to_string()).is_ok())
68    }
69
70    #[test]
71    fn as_inner_returns_reference_to_original_value() {
72        let foo = FooValue("value".to_string());
73        assert_eq!(foo.as_inner(), "value")
74    }
75
76    #[test]
77    fn into_inner_returns_owned_original_value() {
78        let foo = FooValue("value".to_string());
79        assert_eq!(foo.into_inner(), "value".to_string())
80    }
81
82    #[test]
83    fn round_trip_preserves_value() {
84        let foo = FooValue::try_from("value".to_string()).unwrap();
85        assert_eq!(foo.into_inner(), "value".to_string())
86    }
87
88    #[test]
89    fn clone_produces_equal_value() {
90        let foo = FooValue("value".to_string());
91        let bar = foo.clone();
92
93        assert_eq!(foo, bar);
94        assert_eq!(foo.into_inner(), bar.into_inner());
95    }
96
97    #[test]
98    fn eq_returns_true_for_same_inner_value() {
99        let foo = FooValue("foo".to_string());
100        let bar = FooValue("foo".to_string());
101
102        assert_eq!(foo, bar);
103        assert_eq!(foo.into_inner(), bar.into_inner());
104    }
105
106    #[test]
107    fn eq_returns_false_for_different_inner_value() {
108        let foo = FooValue("foo".to_string());
109        let bar = FooValue("bar".to_string());
110
111        assert_ne!(foo, bar);
112        assert_ne!(foo.into_inner(), bar.into_inner());
113    }
114
115    #[test]
116    fn try_from_empty_string_returns_err() {
117        assert!(FooValue::try_from("".to_string()).is_err());
118        assert!(FooValue::try_from("  ".to_string()).is_err());
119    }
120
121    #[test]
122    fn try_from_empty_string_error_has_expected_type_name() {
123        let err = FooValue::try_from("".to_string()).unwrap_err();
124        assert_eq!(err.type_name, "FooValue")
125    }
126
127    #[test]
128    fn try_from_empty_string_error_has_expected_reason() {
129        let err = FooValue::try_from("".to_string()).unwrap_err();
130        assert_eq!(err.reason, "empty")
131    }
132
133    #[test]
134    fn try_from_empty_string_error_display_message() {
135        let err = FooValue::try_from("".to_string()).unwrap_err();
136        assert_eq!(format!("{err}"), "invalid FooValue: empty")
137    }
138}