ddd-toolkit-macro 0.2.2

Derive macros for ddd-toolkit-core's ValueObject, EntityId, SecretVo, and EnumVo traits.
Documentation
use syn::{DeriveInput, LitStr, Path};

#[derive(Default)]
pub struct VoAttrs {
    pub validate: Option<Path>,
    pub no_display: bool,
    pub rename_all: Option<String>,
    pub zeroize: bool,
}

pub fn parse_vo_attrs(input: &DeriveInput) -> syn::Result<VoAttrs> {
    let mut attrs = VoAttrs::default();

    for attr in &input.attrs {
        if !attr.path().is_ident("vo") {
            continue;
        }

        attr.parse_nested_meta(|meta| {
            if meta.path.is_ident("validate") {
                let lit: LitStr = meta.value()?.parse()?;
                attrs.validate = Some(lit.parse()?);
                Ok(())            } else if meta.path.is_ident("no_display") {
                attrs.no_display = true;
                Ok(())
            } else if meta.path.is_ident("rename_all") {
                let lit: LitStr = meta.value()?.parse()?;
                attrs.rename_all = Some(lit.value());
                Ok(())
            } else if meta.path.is_ident("zeroize") {
                attrs.zeroize = true;
                Ok(())
            } else {
                Err(meta.error(
                    "unknown `vo` attribute (expected `validate`, `no_display`, `rename_all`, or `zeroize`)",
                ))
            }
        })?;
    }
    Ok(attrs)
}

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

    #[test]
    fn parse_vo_attrs_without_vo_attribute_returns_defaults() {
        let input: DeriveInput = syn::parse_str("struct Foo(String);").unwrap();
        let attrs = parse_vo_attrs(&input).unwrap();
        assert!(attrs.validate.is_none());
        assert!(!attrs.no_display);
        assert!(attrs.rename_all.is_none());
        assert!(!attrs.zeroize);
    }

    #[test]
    fn parse_vo_attrs_validate_parses_path() {
        let input: DeriveInput =
            syn::parse_str(r#"#[vo(validate = "my_mod::check")] struct Foo(String);"#).unwrap();
        let attrs = parse_vo_attrs(&input).unwrap();
        let path = attrs.validate.unwrap();
        assert_eq!(
            quote::quote!(#path).to_string(),
            quote::quote!(my_mod::check).to_string()
        );
    }

    #[test]
    fn parse_vo_attrs_no_display_sets_flag() {
        let input: DeriveInput = syn::parse_str("#[vo(no_display)] struct Foo(String);").unwrap();
        let attrs = parse_vo_attrs(&input).unwrap();
        assert!(attrs.no_display);
    }

    #[test]
    fn parse_vo_attrs_rename_all_parses_string_value() {
        let input: DeriveInput =
            syn::parse_str(r#"#[vo(rename_all = "snake_case")] struct Foo(String);"#).unwrap();
        let attrs = parse_vo_attrs(&input).unwrap();
        assert_eq!(attrs.rename_all, Some("snake_case".to_string()));
    }

    #[test]
    fn parse_vo_attrs_zeroize_sets_flag() {
        let input: DeriveInput = syn::parse_str("#[vo(zeroize)] struct Foo(String);").unwrap();
        let attrs = parse_vo_attrs(&input).unwrap();
        assert!(attrs.zeroize);
    }

    #[test]
    fn parse_vo_attrs_multiple_keys_in_single_attribute_all_applied() {
        let input: DeriveInput = syn::parse_str(
            r#"#[vo(validate = "my_mod::check", no_display, zeroize)] struct Foo(String);"#,
        )
        .unwrap();
        let attrs = parse_vo_attrs(&input).unwrap();
        assert!(attrs.validate.is_some());
        assert!(attrs.no_display);
        assert!(attrs.zeroize);
    }

    #[test]
    fn parse_vo_attrs_ignores_non_vo_attributes() {
        let input: DeriveInput = syn::parse_str("#[derive(Debug)] struct Foo(String);").unwrap();
        let attrs = parse_vo_attrs(&input).unwrap();
        assert!(attrs.validate.is_none());
        assert!(!attrs.no_display);
        assert!(attrs.rename_all.is_none());
        assert!(!attrs.zeroize);
    }

    #[test]
    fn parse_vo_attrs_unknown_key_returns_err() {
        let input: DeriveInput = syn::parse_str("#[vo(bogus)] struct Foo(String);").unwrap();
        match parse_vo_attrs(&input) {
            Err(err) => assert!(err.to_string().contains("unknown `vo` attribute")),
            Ok(_) => panic!("expected parse_vo_attrs to reject an unknown `vo` attribute"),
        }
    }

    #[test]
    fn parse_vo_attrs_validate_missing_value_returns_err() {
        let input: DeriveInput = syn::parse_str("#[vo(validate)] struct Foo(String);").unwrap();
        assert!(parse_vo_attrs(&input).is_err());
    }

    #[test]
    fn parse_vo_attrs_duplicate_vo_attributes_last_one_wins() {
        let input: DeriveInput = syn::parse_str(
            r#"#[vo(rename_all = "snake_case")] #[vo(rename_all = "kebab-case")] struct Foo(String);"#,
        )
        .unwrap();
        let attrs = parse_vo_attrs(&input).unwrap();
        assert_eq!(attrs.rename_all, Some("kebab-case".to_string()));
    }
}