Skip to main content

ValueObject

Derive Macro ValueObject 

Source
#[derive(ValueObject)]
{
    // Attributes available to this derive:
    #[vo]
}
Expand description

Derives ValueObject, Wrapped, TryFrom<Inner>, From<Self> for Inner, and Display for a single-field tuple struct.

§Requirements

The target must be a tuple struct with exactly one field, and that field must not be pub. A pub field would let callers construct the type directly (Email("anything".into())), bypassing the validation this derive puts into the generated TryFrom impl - so both violations are rejected at compile time rather than left as a foot-gun.

§Attributes

All are passed as #[vo(...)] on the struct itself:

  • validate = "path::to::fn" - runs fn(&Inner) -> Result<(), ValidationError> inside the generated TryFrom before the value is constructed. Without it, TryFrom::Error is Infallible.
  • no_display - skips the generated Display impl, so a hand-written one can be provided instead.

rename_all and zeroize are rejected here: rename_all only affects EnumVo’s variant names, and zeroize only makes sense on SecretVo, which owns a value worth scrubbing from memory on drop.

§serde

With this crate’s own serde Cargo feature enabled (and the calling crate depending on serde directly - proc-macro output resolves bare paths at the call site, not here), this also derives Serialize (delegating to the inner value) and Deserialize (deserializing the inner value, then re-running it through the same validating TryFrom this derive generates, so an invalid value is rejected during deserialization exactly as it would be through TryFrom directly).

§Examples

use ddd_toolkit_core::domain::{ValidationError, Wrapped};
use ddd_toolkit_macro::ValueObject;

#[derive(ValueObject, Clone, PartialEq, Debug)]
#[vo(validate = "validate_email")]
struct Email(String);

fn validate_email(s: &str) -> Result<(), ValidationError> {
    if s.contains('@') {
        Ok(())
    } else {
        Err(ValidationError::new("Email", "missing @"))
    }
}

let email = Email::try_from("a@b.com".to_string())?;
assert_eq!(email.as_inner(), "a@b.com");
assert_eq!(email.to_string(), "a@b.com");
assert!(Email::try_from("no-at-sign".to_string()).is_err());