Skip to main content

backbone_core/
value_object.rs

1//! DDD Value Object Pattern
2//!
3//! Provides traits for implementing Value Objects in Domain-Driven Design.
4//! Value Objects are immutable objects that are defined by their attributes
5//! rather than by identity.
6//!
7//! # Example
8//!
9//! ```ignore
10//! use backbone_core::ValueObject;
11//!
12//! #[derive(Clone, Debug, PartialEq, Eq, Hash)]
13//! pub struct EmailAddress(String);
14//!
15//! impl ValueObject for EmailAddress {
16//!     type Error = EmailError;
17//!
18//!     fn validate(&self) -> Result<(), Self::Error> {
19//!         if self.0.contains('@') && self.0.len() > 3 {
20//!             Ok(())
21//!         } else {
22//!             Err(EmailError::InvalidFormat)
23//!         }
24//!     }
25//! }
26//!
27//! impl EmailAddress {
28//!     pub fn new(email: impl Into<String>) -> Result<Self, EmailError> {
29//!         Self(email.into()).new_validated()
30//!     }
31//! }
32//! ```
33
34use std::fmt::Debug;
35use std::hash::Hash;
36
37/// DDD Value Object trait.
38///
39/// Value Objects are characterized by:
40/// - Immutability (once created, they don't change)
41/// - Equality based on attributes, not identity
42/// - Self-validation
43///
44/// # Required Bounds
45///
46/// - `Clone`: Value objects should be copyable
47/// - `Eq + Hash`: Equality based on attributes
48/// - `Debug`: For debugging and logging
49/// - `Send + Sync`: Thread safety for async contexts
50pub trait ValueObject: Clone + Eq + Hash + Debug + Send + Sync {
51    /// Error type for validation failures.
52    type Error: std::error::Error + Send + Sync;
53
54    /// Validate the value object.
55    ///
56    /// Returns `Ok(())` if the value object is valid,
57    /// or an error describing the validation failure.
58    fn validate(&self) -> Result<(), Self::Error>;
59
60    /// Create a validated instance.
61    ///
62    /// This is a convenience method that validates after construction.
63    fn new_validated(self) -> Result<Self, Self::Error>
64    where
65        Self: Sized,
66    {
67        self.validate()?;
68        Ok(self)
69    }
70
71    /// Check if this value object is valid without returning an error.
72    fn is_valid(&self) -> bool {
73        self.validate().is_ok()
74    }
75}
76
77/// Extension trait for optional value objects.
78pub trait OptionalValueObject<T: ValueObject> {
79    /// Validate the inner value if present.
80    fn validate_if_present(&self) -> Result<(), T::Error>;
81}
82
83impl<T: ValueObject> OptionalValueObject<T> for Option<T> {
84    fn validate_if_present(&self) -> Result<(), T::Error> {
85        match self {
86            Some(value) => value.validate(),
87            None => Ok(()),
88        }
89    }
90}
91
92/// Common validation error for value objects.
93#[derive(Debug, Clone, thiserror::Error)]
94pub enum ValueObjectError {
95    /// The value is empty when it shouldn't be.
96    #[error("Value cannot be empty")]
97    Empty,
98
99    /// The value exceeds the maximum length.
100    #[error("Value exceeds maximum length of {max} characters")]
101    TooLong { max: usize },
102
103    /// The value is shorter than the minimum length.
104    #[error("Value must be at least {min} characters")]
105    TooShort { min: usize },
106
107    /// The value doesn't match the expected format.
108    #[error("Invalid format: {message}")]
109    InvalidFormat { message: String },
110
111    /// The value is out of the allowed range.
112    #[error("Value out of range: {message}")]
113    OutOfRange { message: String },
114
115    /// Custom validation error.
116    #[error("{0}")]
117    Custom(String),
118}
119
120/// Helper macro for implementing simple string-based value objects.
121#[macro_export]
122macro_rules! define_string_value_object {
123    ($name:ident, $min_len:expr, $max_len:expr) => {
124        #[derive(Clone, Debug, PartialEq, Eq, Hash)]
125        pub struct $name(String);
126
127        impl $crate::ValueObject for $name {
128            type Error = $crate::ValueObjectError;
129
130            fn validate(&self) -> Result<(), Self::Error> {
131                if self.0.is_empty() {
132                    return Err($crate::ValueObjectError::Empty);
133                }
134                if self.0.len() < $min_len {
135                    return Err($crate::ValueObjectError::TooShort { min: $min_len });
136                }
137                if self.0.len() > $max_len {
138                    return Err($crate::ValueObjectError::TooLong { max: $max_len });
139                }
140                Ok(())
141            }
142        }
143
144        impl $name {
145            pub fn new(value: impl Into<String>) -> Result<Self, $crate::ValueObjectError> {
146                use $crate::ValueObject;
147                Self(value.into()).new_validated()
148            }
149
150            pub fn as_str(&self) -> &str {
151                &self.0
152            }
153
154            pub fn into_inner(self) -> String {
155                self.0
156            }
157        }
158
159        impl std::fmt::Display for $name {
160            fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
161                write!(f, "{}", self.0)
162            }
163        }
164
165        impl AsRef<str> for $name {
166            fn as_ref(&self) -> &str {
167                &self.0
168            }
169        }
170    };
171}
172
173#[cfg(test)]
174mod tests {
175    use super::*;
176
177    #[derive(Clone, Debug, PartialEq, Eq, Hash)]
178    struct TestEmail(String);
179
180    impl ValueObject for TestEmail {
181        type Error = ValueObjectError;
182
183        fn validate(&self) -> Result<(), Self::Error> {
184            if !self.0.contains('@') {
185                return Err(ValueObjectError::InvalidFormat {
186                    message: "Email must contain @".to_string(),
187                });
188            }
189            Ok(())
190        }
191    }
192
193    #[test]
194    fn test_valid_value_object() {
195        let email = TestEmail("test@example.com".to_string());
196        assert!(email.validate().is_ok());
197        assert!(email.is_valid());
198    }
199
200    #[test]
201    fn test_invalid_value_object() {
202        let email = TestEmail("invalid".to_string());
203        assert!(email.validate().is_err());
204        assert!(!email.is_valid());
205    }
206
207    #[test]
208    fn test_new_validated() {
209        let valid = TestEmail("test@example.com".to_string()).new_validated();
210        assert!(valid.is_ok());
211
212        let invalid = TestEmail("invalid".to_string()).new_validated();
213        assert!(invalid.is_err());
214    }
215
216    #[test]
217    fn test_optional_value_object() {
218        let some_valid: Option<TestEmail> = Some(TestEmail("test@example.com".to_string()));
219        assert!(some_valid.validate_if_present().is_ok());
220
221        let some_invalid: Option<TestEmail> = Some(TestEmail("invalid".to_string()));
222        assert!(some_invalid.validate_if_present().is_err());
223
224        let none: Option<TestEmail> = None;
225        assert!(none.validate_if_present().is_ok());
226    }
227}