ddd-toolkit-core 0.2.3

Traits and reference implementations for Domain-Driven Design building blocks: entities, value objects, aggregates, repositories, and event dispatch.
Documentation
use std::fmt::Debug;

/// A domain object with no identity, whose equality is purely structural:
/// two value objects are the same if their data is the same.
///
/// # Examples
///
/// ```
/// use ddd_toolkit_core::domain::ValueObject;
///
/// #[derive(Debug, Clone, PartialEq)]
/// struct Money(u32);
///
/// impl ValueObject for Money {}
///
/// assert_eq!(Money(500), Money(500));
/// assert_ne!(Money(500), Money(100));
/// ```
pub trait ValueObject: Clone + PartialEq + Debug + Send + Sync {}

/// A type that wraps a single inner value.
///
/// Always implemented alongside [`Wrapped`]; see [`Wrapped`]'s docs for a
/// worked example.
pub trait WrappedInner {
    /// The wrapped type.
    type Inner;
}

/// A [`ValueObject`] newtype around a single [`WrappedInner::Inner`] value,
/// only constructible through validation (`TryFrom<Self::Inner>`).
///
/// Note this invariant only holds if the wrapped field is private - see
/// `#[derive(ValueObject)]` in `ddd-toolkit-macro`, which enforces that at
/// derive time.
///
/// # Examples
///
/// ```
/// use ddd_toolkit_core::domain::{ValidationError, ValueObject, Wrapped, WrappedInner};
///
/// #[derive(Debug, Clone, PartialEq)]
/// struct NonEmptyString(String);
///
/// impl ValueObject for NonEmptyString {}
///
/// impl WrappedInner for NonEmptyString {
///     type Inner = String;
/// }
///
/// impl Wrapped for NonEmptyString {
///     fn as_inner(&self) -> &Self::Inner {
///         &self.0
///     }
///
///     fn into_inner(self) -> Self::Inner {
///         self.0
///     }
/// }
///
/// impl TryFrom<String> for NonEmptyString {
///     type Error = ValidationError;
///
///     fn try_from(value: String) -> Result<Self, Self::Error> {
///         if value.trim().is_empty() {
///             return Err(ValidationError::new("NonEmptyString", "empty"));
///         }
///         Ok(Self(value))
///     }
/// }
///
/// let name: NonEmptyString = "alice".to_string().try_into()?;
/// assert_eq!(name.as_inner(), "alice");
/// assert_eq!(name.into_inner(), "alice".to_string());
///
/// assert!(NonEmptyString::try_from(String::new()).is_err());
/// # Ok::<(), ValidationError>(())
/// ```
pub trait Wrapped:
    ValueObject
    + WrappedInner
    + TryFrom<<Self as WrappedInner>::Inner, Error: std::error::Error + Send + Sync + 'static>
{
    /// Borrows the wrapped value.
    fn as_inner(&self) -> &Self::Inner;
    /// Consumes `self`, returning the wrapped value.
    fn into_inner(self) -> Self::Inner;
}

#[cfg(test)]
mod test {
    use super::*;
    use crate::domain::ValidationError;

    #[derive(Clone, PartialEq, Debug)]
    struct FooValue(String);

    impl ValueObject for FooValue {}

    impl WrappedInner for FooValue {
        type Inner = String;
    }

    impl Wrapped for FooValue {
        fn as_inner(&self) -> &Self::Inner {
            &self.0
        }

        fn into_inner(self) -> Self::Inner {
            self.0
        }
    }

    impl TryFrom<String> for FooValue {
        type Error = ValidationError;

        fn try_from(value: String) -> Result<Self, Self::Error> {
            if value.trim().is_empty() {
                return Err(ValidationError::new("FooValue", "empty"));
            }
            Ok(Self(value))
        }
    }

    #[test]
    fn try_from_non_empty_string_succeeds() {
        assert!(FooValue::try_from("value".to_string()).is_ok())
    }

    #[test]
    fn as_inner_returns_reference_to_original_value() {
        let foo = FooValue("value".to_string());
        assert_eq!(foo.as_inner(), "value")
    }

    #[test]
    fn into_inner_returns_owned_original_value() {
        let foo = FooValue("value".to_string());
        assert_eq!(foo.into_inner(), "value".to_string())
    }

    #[test]
    fn round_trip_preserves_value() {
        let foo = FooValue::try_from("value".to_string()).unwrap();
        assert_eq!(foo.into_inner(), "value".to_string())
    }

    #[test]
    fn clone_produces_equal_value() {
        let foo = FooValue("value".to_string());
        let bar = foo.clone();

        assert_eq!(foo, bar);
        assert_eq!(foo.into_inner(), bar.into_inner());
    }

    #[test]
    fn eq_returns_true_for_same_inner_value() {
        let foo = FooValue("foo".to_string());
        let bar = FooValue("foo".to_string());

        assert_eq!(foo, bar);
        assert_eq!(foo.into_inner(), bar.into_inner());
    }

    #[test]
    fn eq_returns_false_for_different_inner_value() {
        let foo = FooValue("foo".to_string());
        let bar = FooValue("bar".to_string());

        assert_ne!(foo, bar);
        assert_ne!(foo.into_inner(), bar.into_inner());
    }

    #[test]
    fn try_from_empty_string_returns_err() {
        assert!(FooValue::try_from("".to_string()).is_err());
        assert!(FooValue::try_from("  ".to_string()).is_err());
    }

    #[test]
    fn try_from_empty_string_error_has_expected_type_name() {
        let err = FooValue::try_from("".to_string()).unwrap_err();
        assert_eq!(err.type_name, "FooValue")
    }

    #[test]
    fn try_from_empty_string_error_has_expected_reason() {
        let err = FooValue::try_from("".to_string()).unwrap_err();
        assert_eq!(err.reason, "empty")
    }

    #[test]
    fn try_from_empty_string_error_display_message() {
        let err = FooValue::try_from("".to_string()).unwrap_err();
        assert_eq!(format!("{err}"), "invalid FooValue: empty")
    }
}