backbone_core/
value_object.rs1use std::fmt::Debug;
35use std::hash::Hash;
36
37pub trait ValueObject: Clone + Eq + Hash + Debug + Send + Sync {
51 type Error: std::error::Error + Send + Sync;
53
54 fn validate(&self) -> Result<(), Self::Error>;
59
60 fn new_validated(self) -> Result<Self, Self::Error>
64 where
65 Self: Sized,
66 {
67 self.validate()?;
68 Ok(self)
69 }
70
71 fn is_valid(&self) -> bool {
73 self.validate().is_ok()
74 }
75}
76
77pub trait OptionalValueObject<T: ValueObject> {
79 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#[derive(Debug, Clone, thiserror::Error)]
94pub enum ValueObjectError {
95 #[error("Value cannot be empty")]
97 Empty,
98
99 #[error("Value exceeds maximum length of {max} characters")]
101 TooLong { max: usize },
102
103 #[error("Value must be at least {min} characters")]
105 TooShort { min: usize },
106
107 #[error("Invalid format: {message}")]
109 InvalidFormat { message: String },
110
111 #[error("Value out of range: {message}")]
113 OutOfRange { message: String },
114
115 #[error("{0}")]
117 Custom(String),
118}
119
120#[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}