ddd-toolkit-core 0.2.2

Traits and reference implementations for Domain-Driven Design building blocks: entities, value objects, aggregates, repositories, and event dispatch.
Documentation
use crate::domain::ValueObject;
use core::str::FromStr;
use std::fmt::Display;

/// A fieldless enum treated as a closed set of named values, round-trippable
/// through its `Display`/`FromStr` string form.
///
/// `variants()` and `FromStr::from_str` are independent trait items with no
/// compiler-enforced relationship between them: nothing requires that every
/// variant `from_str` accepts also appears in `variants()`, or vice versa.
/// `#[derive(EnumVo)]` in `ddd-toolkit-macro` keeps them consistent by
/// construction, generating both from the same variant list. A hand-written
/// impl (as in this crate's own test fixtures) must keep them in sync by
/// hand - adding, renaming, or removing a variant in one without mirroring
/// the change in the other breaks the round-trip silently.
///
/// # Examples
///
/// ```
/// use ddd_toolkit_core::domain::{EnumVo, ValidationError, ValueObject};
/// use std::fmt::Display;
/// use std::str::FromStr;
///
/// #[derive(Debug, Clone, Copy, PartialEq)]
/// enum Status {
///     Draft,
///     Published,
/// }
///
/// impl Display for Status {
///     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
///         match self {
///             Status::Draft => write!(f, "draft"),
///             Status::Published => write!(f, "published"),
///         }
///     }
/// }
///
/// impl FromStr for Status {
///     type Err = ValidationError;
///
///     fn from_str(s: &str) -> Result<Self, Self::Err> {
///         match s {
///             "draft" => Ok(Status::Draft),
///             "published" => Ok(Status::Published),
///             _ => Err(ValidationError::new("Status", "unknown variant")),
///         }
///     }
/// }
///
/// impl ValueObject for Status {}
///
/// impl EnumVo for Status {
///     fn variants() -> &'static [Self] {
///         &[Status::Draft, Status::Published]
///     }
/// }
///
/// // every variant round-trips through its string form...
/// for status in Status::variants() {
///     assert_eq!(status.to_string().parse(), Ok(*status));
/// }
///
/// // ...and unknown strings are rejected rather than panicking.
/// assert!("archived".parse::<Status>().is_err());
/// ```
pub trait EnumVo:
    ValueObject + FromStr<Err: std::error::Error + Send + Sync + 'static> + Display + Copy + 'static
{
    /// Every variant, in the order chosen by the implementation.
    fn variants() -> &'static [Self];
}

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

    use super::*;
    use std::fmt::Display;

    #[derive(Debug, Clone, Copy, PartialEq)]
    pub enum Baz {
        Foo,
        Bar,
    }

    impl Display for Baz {
        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
            match self {
                Baz::Foo => write!(f, "foo"),
                Baz::Bar => write!(f, "bar"),
            }
        }
    }

    impl FromStr for Baz {
        type Err = ValidationError;

        fn from_str(s: &str) -> Result<Self, Self::Err> {
            if s.eq("foo") {
                Ok(Self::Foo)
            } else if s.eq("bar") {
                Ok(Self::Bar)
            } else {
                Err(ValidationError::new("Baz", "unknown variant"))
            }
        }
    }

    impl ValueObject for Baz {}

    impl EnumVo for Baz {
        fn variants() -> &'static [Self] {
            &[Self::Bar, Self::Foo]
        }
    }

    #[test]
    fn from_str_foo_returns_foo_variant() {
        assert_eq!("foo".parse(), Ok(Baz::Foo))
    }

    #[test]
    fn from_str_bar_returns_bar_variant() {
        assert_eq!("bar".parse(), Ok(Baz::Bar))
    }

    #[test]
    fn display_formats_foo_variant() {
        assert_eq!(format!("{}", Baz::Foo), "foo")
    }

    #[test]
    fn display_formats_bar_variant() {
        assert_eq!(format!("{}", Baz::Bar), "bar")
    }

    #[test]
    fn variants_returns_all_variants() {
        let variants = Baz::variants();

        assert_eq!(variants.len(), 2);
        assert!(variants.contains(&Baz::Foo));
        assert!(variants.contains(&Baz::Bar))
    }

    #[test]
    fn round_trip_from_str_of_display_preserves_variant() {
        for v in Baz::variants() {
            assert_eq!(v.to_string().parse(), Ok(*v))
        }
    }

    #[test]
    fn copy_produces_equal_value() {
        let foo = Baz::Foo;
        let copied = foo;

        assert_eq!(foo, copied)
    }

    #[test]
    fn eq_returns_true_for_same_variant() {
        assert_eq!(Baz::Foo, Baz::Foo)
    }

    #[test]
    fn eq_returns_false_for_different_variant() {
        assert_ne!(Baz::Foo, Baz::Bar)
    }

    #[test]
    fn from_str_unknown_string_returns_err() {
        assert!("qux".parse::<Baz>().is_err())
    }

    #[test]
    fn from_str_unknown_string_error_has_expected_type_name() {
        let err = "qux".parse::<Baz>().unwrap_err();
        assert_eq!(err.type_name, "Baz")
    }

    #[test]
    fn from_str_unknown_string_error_has_expected_reason() {
        let err = "qux".parse::<Baz>().unwrap_err();
        assert_eq!(err.reason, "unknown variant")
    }

    #[test]
    fn from_str_unknown_string_error_display_message() {
        let err = "qux".parse::<Baz>().unwrap_err();
        assert_eq!(format!("{err}"), "invalid Baz: unknown variant")
    }

    #[test]
    fn from_str_empty_string_returns_err() {
        assert!("".parse::<Baz>().is_err())
    }

    #[test]
    fn from_str_is_case_sensitive() {
        assert!("Foo".parse::<Baz>().is_err());
        assert!("BAR".parse::<Baz>().is_err())
    }

    #[test]
    fn from_str_rejects_surrounding_whitespace() {
        assert!(" foo".parse::<Baz>().is_err());
        assert!("bar ".parse::<Baz>().is_err())
    }
}