eventide-macros 0.1.1

Procedural macros for the eventide DDD/CQRS toolkit: derive entities, entity ids, value objects and domain events with a single attribute.
Documentation
use proc_macro_crate::{FoundCrate, crate_name};
use proc_macro2::{Span, TokenStream};
use quote::{ToTokens, quote};
use syn::{Attribute, Field, FieldsNamed, Token, Type, punctuated::Punctuated};

/// Resolve a path that points at the `eventide-domain` crate, regardless of
/// whether the downstream user depends on it directly, on the `eventide`
/// umbrella crate (which re-exports it as `eventide::domain`), or under a
/// renamed `[dependencies]` entry.
///
/// Resolution order:
/// 1. `eventide` (umbrella) → `::<rename>::domain`.
/// 2. `eventide-domain` → `::<rename>`.
/// 3. Neither found → assume `::eventide_domain` so the error message points
///    at a concrete missing dependency rather than a panic from this helper.
///
/// `FoundCrate::Itself` is treated the same as `Name`: the source crate is
/// expected to make its own name resolvable via `extern crate self as
/// <name>;` (both `eventide-domain` and `eventide` do this). That keeps the
/// generated code identical for in-crate examples / integration tests / unit
/// tests, where `crate::` would point at the wrong target binary.
pub(crate) fn eventide_domain_path() -> TokenStream {
    if let Ok(found) = crate_name("eventide") {
        let name = match found {
            FoundCrate::Itself => "eventide".to_string(),
            FoundCrate::Name(name) => name,
        };
        let id = syn::Ident::new(&name, Span::call_site());
        return quote!(::#id::domain);
    }
    if let Ok(found) = crate_name("eventide-domain") {
        let name = match found {
            FoundCrate::Itself => "eventide_domain".to_string(),
            FoundCrate::Name(name) => name,
        };
        let id = syn::Ident::new(&name, Span::call_site());
        return quote!(::#id);
    }
    quote!(::eventide_domain)
}

/// Path to the `serde` re-export inside `eventide-domain`. Generated derives
/// use this so users do not need a direct `serde` dependency.
pub(crate) fn serde_path() -> TokenStream {
    let domain = eventide_domain_path();
    quote!(#domain::__serde)
}

/// Build the `#[serde(crate = "...")]` attribute that tells `serde_derive`
/// where to find the `serde` runtime — required when the derive path goes
/// through a re-export rather than `::serde` directly.
pub(crate) fn serde_crate_attr() -> Attribute {
    let path = serde_crate_string();
    syn::parse_quote!(#[serde(crate = #path)])
}

fn serde_crate_string() -> String {
    if let Ok(found) = crate_name("eventide") {
        let name = match found {
            FoundCrate::Itself => "eventide".to_string(),
            FoundCrate::Name(name) => name,
        };
        return format!("::{}::domain::__serde", name);
    }
    if let Ok(found) = crate_name("eventide-domain") {
        let name = match found {
            FoundCrate::Itself => "eventide_domain".to_string(),
            FoundCrate::Name(name) => name,
        };
        return format!("::{}::__serde", name);
    }
    "::eventide_domain::__serde".to_string()
}

// Split a list of attributes into two pieces: the non-`derive`
// attributes (which we want to keep verbatim) and the flat list of
// derive paths that were inside `#[derive(...)]` clauses (which we will
// merge with the macro's required set).
pub(crate) fn split_derives(attrs: &[Attribute]) -> (Vec<Attribute>, Vec<syn::Path>) {
    let mut retained = Vec::new();
    let mut existing = Vec::new();
    for attr in attrs.iter() {
        if attr.path().is_ident("derive") {
            if let Ok(list) = attr.parse_args_with(
                syn::punctuated::Punctuated::<syn::Path, Token![,]>::parse_terminated,
            ) {
                for p in list.into_iter() {
                    existing.push(p);
                }
            }
        } else {
            retained.push(attr.clone());
        }
    }
    (retained, existing)
}

// Merge the macro's required derives with the user's existing derives,
// de-duplicating by normalised key (see `derive_key`). Required derives
// are pushed first, so they always win when there is a name clash, and
// user derives that did not already appear are appended afterwards. The
// final `Attribute` returned is a fresh `#[derive(...)]` containing the
// merged list.
pub(crate) fn merge_derives(existing: Vec<syn::Path>, required: Vec<syn::Path>) -> Attribute {
    let mut seen = std::collections::HashSet::<String>::new();
    let mut final_list: Vec<syn::Path> = Vec::new();
    let mut push_unique = |p: syn::Path| {
        let key = derive_key(&p);
        if seen.insert(key) {
            final_list.push(p);
        }
    };
    for p in required {
        push_unique(p);
    }
    for p in existing {
        push_unique(p);
    }
    syn::parse_quote!(#[derive(#(#final_list),*)])
}

// Compute a deduplication key for a derive path. We collapse the
// last-segment ident to a canonical form so that paths which name the
// same trait under different prefixes (e.g. plain `Serialize` and
// `serde::Serialize`) hash to the same key and are not duplicated in
// the merged list.
pub(crate) fn derive_key(p: &syn::Path) -> String {
    if let Some(last) = p.segments.last() {
        let last_ident = last.ident.to_string();
        match last_ident.as_str() {
            "Serialize" | "Deserialize" => format!("serde::{}", last_ident),
            _ => last_ident,
        }
    } else {
        p.to_token_stream().to_string()
    }
}

// Convenience wrapper that applies the merge in place: it splits the
// existing attribute list, merges the derives, and then replaces
// `*attrs` with a single normalised `#[derive(...)]` attribute followed
// by the caller-supplied helper attributes (e.g. `#[serde(crate = "...")]`)
// and finally the original non-derive attributes the user wrote.
pub(crate) fn apply_derives(
    attrs: &mut Vec<Attribute>,
    required: Vec<syn::Path>,
    helper_attrs: Vec<Attribute>,
) {
    let (retained, existing) = split_derives(attrs);
    let merged = merge_derives(existing, required);
    *attrs = std::iter::once(merged)
        .chain(helper_attrs)
        .chain(retained)
        .collect();
}

/// Ensure a named-field struct or enum variant contains the requested
/// fields, with two slightly different policies depending on the caller.
///
/// Parameters:
/// - `required` — list of `(field_name, field_type)` pairs processed in
///   the given order. The fields are inserted with the supplied types
///   when missing; existing fields with matching names are reused as-is
///   (their original type is preserved, even if it differs from the
///   one passed in).
/// - `reposition_existing`:
///   * `true` — the required fields are moved to the very front of the
///     struct in the order they appear in `required`, regardless of
///     where the user wrote them. This is what `#[entity]` needs so
///     that `id`/`version` always lead the struct layout.
///   * `false` — required fields are only *appended* (in front of the
///     user's fields) when missing; if they already exist their
///     original position is preserved. This is what `#[domain_event]`
///     needs so the user's per-variant fields keep their original
///     ordering.
pub(crate) fn ensure_required_fields(
    fields_named: &mut FieldsNamed,
    required: &[(&str, &Type)],
    reposition_existing: bool,
) {
    let old_named = fields_named.named.clone();
    let mut new_named: Punctuated<Field, Token![,]> = Punctuated::new();

    if reposition_existing {
        // Place the required fields at the front in the requested order:
        // reuse the user's existing field (preserving its attributes /
        // type) if one exists with that name, otherwise synthesise a new
        // field of the supplied type.
        for (name, ty) in required.iter() {
            if let Some(existing) = old_named
                .iter()
                .find(|f| f.ident.as_ref().map(|i| i == *name).unwrap_or(false))
            {
                new_named.push(existing.clone());
            } else {
                let ident: syn::Ident = syn::parse_str(name).expect("valid field ident");
                let field: Field = syn::parse_quote! { #ident: #ty };
                new_named.push(field);
            }
        }

        // Append the remaining (non-required) fields in their original
        // order so the user's intended layout is preserved aside from
        // the lifted required-field prefix.
        for f in old_named.into_iter() {
            let is_required = f
                .ident
                .as_ref()
                .map(|i| required.iter().any(|(n, _)| i == n))
                .unwrap_or(false);
            if !is_required {
                new_named.push(f);
            }
        }
    } else {
        // Append-only mode: only insert required fields that are
        // missing (placing them at the front), and otherwise keep the
        // user's original ordering untouched.
        for (name, ty) in required.iter() {
            if !has_field_named_in(&old_named, name) {
                let ident: syn::Ident = syn::parse_str(name).expect("valid field ident");
                let field: Field = syn::parse_quote! { #ident: #ty };
                new_named.push(field);
            }
        }
        for f in old_named.into_iter() {
            new_named.push(f);
        }
    }

    fields_named.named = new_named;
}

fn has_field_named_in(named: &Punctuated<Field, Token![,]>, name: &str) -> bool {
    named
        .iter()
        .any(|f| f.ident.as_ref().map(|i| i == name).unwrap_or(false))
}