ddd-toolkit-macro 0.4.0

Derive macros for ddd-toolkit-core's ValueObject, Entity, DomainEvent, and AggregateRoot traits.
Documentation
use proc_macro2::TokenStream;
use quote::quote;
use syn::{Data, DeriveInput, Fields, Result, parse_quote};

use crate::path::core_path;

/// **English:** Generates identity access, equality, and hashing from one marked
/// named field; rejects invalid shapes, misplaced attributes, or ambiguous identity.
///
/// **日本語:** 指定された一つの名前付きフィールドから識別子の取得・比較・ハッシュを
/// 生成し、不正な形状、属性の誤配置、識別子の曖昧さをエラーにします。
pub fn expand(input: DeriveInput) -> Result<TokenStream> {
    for attr in &input.attrs {
        if attr.path().is_ident("entity") {
            return Err(syn::Error::new_spanned(
                attr,
                "entity(id) must be placed on a named field",
            ));
        }
    }

    let fields = match &input.data {
        Data::Struct(data) => match &data.fields {
            Fields::Named(fields) => &fields.named,
            _ => {
                return Err(syn::Error::new_spanned(
                    &input.ident,
                    "Entity requires a named-field struct",
                ));
            }
        },
        _ => {
            return Err(syn::Error::new_spanned(
                &input.ident,
                "Entity can only be derived for a named-field struct",
            ));
        }
    };

    let mut id_field = None;
    for field in fields {
        if has_id_attribute(field)? {
            if id_field.is_some() {
                return Err(syn::Error::new_spanned(
                    field,
                    "Entity requires exactly one #[entity(id)] field",
                ));
            }
            id_field = Some(field);
        }
    }
    let id_field = id_field.ok_or_else(|| {
        syn::Error::new_spanned(
            &input.ident,
            "Entity requires exactly one #[entity(id)] field",
        )
    })?;
    let id_name = id_field.ident.as_ref().expect("named fields have names");
    let id_ty = &id_field.ty;
    let core = core_path()?;
    let name = &input.ident;
    let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();

    let mut partial_eq_generics = input.generics.clone();
    partial_eq_generics
        .make_where_clause()
        .predicates
        .push(parse_quote!(#id_ty: ::core::cmp::PartialEq));
    let (partial_eq_impl, partial_eq_ty, partial_eq_where) = partial_eq_generics.split_for_impl();

    let mut eq_generics = input.generics.clone();
    eq_generics
        .make_where_clause()
        .predicates
        .push(parse_quote!(#id_ty: ::core::cmp::Eq));
    let (eq_impl, eq_ty, eq_where) = eq_generics.split_for_impl();

    let mut hash_generics = input.generics.clone();
    hash_generics
        .make_where_clause()
        .predicates
        .push(parse_quote!(#id_ty: ::core::hash::Hash));
    let (hash_impl, hash_ty, hash_where) = hash_generics.split_for_impl();

    Ok(quote! {
        impl #impl_generics #core::Entity for #name #ty_generics #where_clause {
            type Id = #id_ty;

            fn id(&self) -> &Self::Id {
                &self.#id_name
            }
        }

        impl #impl_generics #name #ty_generics #where_clause {
            /// **English:** Borrows the field marked as this entity's identifier.
            ///
            /// **日本語:** このエンティティの識別子に指定されたフィールドを借用します。
            pub fn id(&self) -> &#id_ty {
                &self.#id_name
            }
        }

        impl #partial_eq_impl ::core::cmp::PartialEq for #name #partial_eq_ty #partial_eq_where {
            fn eq(&self, other: &Self) -> bool {
                self.#id_name == other.#id_name
            }
        }
        impl #eq_impl ::core::cmp::Eq for #name #eq_ty #eq_where {}
        impl #hash_impl ::core::hash::Hash for #name #hash_ty #hash_where {
            fn hash<H: ::core::hash::Hasher>(&self, state: &mut H) {
                ::core::hash::Hash::hash(&self.#id_name, state);
            }
        }
    })
}

fn has_id_attribute(field: &syn::Field) -> Result<bool> {
    let mut found = false;
    for attr in &field.attrs {
        if !attr.path().is_ident("entity") {
            continue;
        }
        let mut option_found = false;
        attr.parse_nested_meta(|meta| {
            option_found = true;
            if !meta.path.is_ident("id") {
                return Err(meta.error("unknown entity attribute"));
            }
            if !meta.input.is_empty() {
                return Err(meta.error("id is a flag and takes no value"));
            }
            if found {
                return Err(meta.error("duplicate id attribute"));
            }
            found = true;
            Ok(())
        })?;
        if !option_found {
            return Err(syn::Error::new_spanned(attr, "entity requires the id flag"));
        }
    }
    Ok(found)
}