use std::fmt::Debug;
use std::hash::Hash;
pub trait ValueObject: Clone + Eq + Hash + Debug + Send + Sync {
type Error: std::error::Error + Send + Sync;
fn validate(&self) -> Result<(), Self::Error>;
fn new_validated(self) -> Result<Self, Self::Error>
where
Self: Sized,
{
self.validate()?;
Ok(self)
}
fn is_valid(&self) -> bool {
self.validate().is_ok()
}
}
pub trait OptionalValueObject<T: ValueObject> {
fn validate_if_present(&self) -> Result<(), T::Error>;
}
impl<T: ValueObject> OptionalValueObject<T> for Option<T> {
fn validate_if_present(&self) -> Result<(), T::Error> {
match self {
Some(value) => value.validate(),
None => Ok(()),
}
}
}
#[derive(Debug, Clone, thiserror::Error)]
pub enum ValueObjectError {
#[error("Value cannot be empty")]
Empty,
#[error("Value exceeds maximum length of {max} characters")]
TooLong { max: usize },
#[error("Value must be at least {min} characters")]
TooShort { min: usize },
#[error("Invalid format: {message}")]
InvalidFormat { message: String },
#[error("Value out of range: {message}")]
OutOfRange { message: String },
#[error("{0}")]
Custom(String),
}
#[macro_export]
macro_rules! define_string_value_object {
($name:ident, $min_len:expr, $max_len:expr) => {
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct $name(String);
impl $crate::ValueObject for $name {
type Error = $crate::ValueObjectError;
fn validate(&self) -> Result<(), Self::Error> {
if self.0.is_empty() {
return Err($crate::ValueObjectError::Empty);
}
if self.0.len() < $min_len {
return Err($crate::ValueObjectError::TooShort { min: $min_len });
}
if self.0.len() > $max_len {
return Err($crate::ValueObjectError::TooLong { max: $max_len });
}
Ok(())
}
}
impl $name {
pub fn new(value: impl Into<String>) -> Result<Self, $crate::ValueObjectError> {
use $crate::ValueObject;
Self(value.into()).new_validated()
}
pub fn as_str(&self) -> &str {
&self.0
}
pub fn into_inner(self) -> String {
self.0
}
}
impl std::fmt::Display for $name {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
impl AsRef<str> for $name {
fn as_ref(&self) -> &str {
&self.0
}
}
};
}
#[cfg(test)]
mod tests {
use super::*;
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
struct TestEmail(String);
impl ValueObject for TestEmail {
type Error = ValueObjectError;
fn validate(&self) -> Result<(), Self::Error> {
if !self.0.contains('@') {
return Err(ValueObjectError::InvalidFormat {
message: "Email must contain @".to_string(),
});
}
Ok(())
}
}
#[test]
fn test_valid_value_object() {
let email = TestEmail("test@example.com".to_string());
assert!(email.validate().is_ok());
assert!(email.is_valid());
}
#[test]
fn test_invalid_value_object() {
let email = TestEmail("invalid".to_string());
assert!(email.validate().is_err());
assert!(!email.is_valid());
}
#[test]
fn test_new_validated() {
let valid = TestEmail("test@example.com".to_string()).new_validated();
assert!(valid.is_ok());
let invalid = TestEmail("invalid".to_string()).new_validated();
assert!(invalid.is_err());
}
#[test]
fn test_optional_value_object() {
let some_valid: Option<TestEmail> = Some(TestEmail("test@example.com".to_string()));
assert!(some_valid.validate_if_present().is_ok());
let some_invalid: Option<TestEmail> = Some(TestEmail("invalid".to_string()));
assert!(some_invalid.validate_if_present().is_err());
let none: Option<TestEmail> = None;
assert!(none.validate_if_present().is_ok());
}
}