armour-derive 0.28.3

DDL and serialization for key-value storage
Documentation
use darling::FromField;
use proc_macro2::TokenStream;
use quote::{format_ident, quote};
use syn::{Data, Fields, Ident, Index};

use crate::{FieldAttr, rapira_field_attrs::skip_attr};

/// Field participates in refs/visit_ids? Spec ยง1:
/// rapira(skip), no_refs, unimplemented, custom โ€” skip;
/// with_type โ€” recurse on the real field type (participates);
/// idx โ€” ignored (paths use field names).
fn field_participates(field: &syn::Field) -> bool {
    if skip_attr(&field.attrs) {
        return false;
    }
    let attr = match FieldAttr::from_field(field) {
        Ok(a) => a,
        Err(_) => return false, // attribute errors are reported by the GetType branch
    };
    !(attr.no_refs.is_present() || attr.unimplemented.is_present() || attr.custom.is_some())
}

pub fn generate(data: &Data, name: &Ident, path: &TokenStream) -> TokenStream {
    match data {
        Data::Struct(ds) => match &ds.fields {
            Fields::Named(fields) => {
                let mut refs_parts = Vec::new();
                let mut visit_parts = Vec::new();
                for field in &fields.named {
                    if !field_participates(field) {
                        continue;
                    }
                    let ident = field.ident.as_ref().expect("named");
                    let fname = ident.to_string();
                    let ty = &field.ty;
                    refs_parts.push(quote! {
                        out.extend(<#ty as #path::GetRefs>::refs().into_iter()
                            .map(|r| r.prefixed(#path::PathSeg::Field(#fname))));
                    });
                    visit_parts.push(quote! {
                        #path::GetRefs::visit_ids(&self.#ident, __gr_f);
                    });
                }
                impl_block(name, path, refs_parts, visit_parts)
            }
            Fields::Unnamed(fields) => {
                // flatten-newtype: forward without an extra path segment (like Typ).
                if fields.unnamed.len() == 1 {
                    let field = &fields.unnamed[0];
                    if let Ok(attr) = FieldAttr::from_field(field)
                        && attr.flatten.is_present()
                    {
                        let ty = &field.ty;
                        return quote! {
                            impl #path::GetRefs for #name {
                                fn refs() -> ::std::vec::Vec<#path::FieldRef> {
                                    <#ty as #path::GetRefs>::refs()
                                }
                                fn visit_ids(&self, __gr_f: &mut dyn FnMut(&'static str, u64)) {
                                    #path::GetRefs::visit_ids(&self.0, __gr_f);
                                }
                            }
                        };
                    }
                }
                let mut refs_parts = Vec::new();
                let mut visit_parts = Vec::new();
                for (i, field) in fields.unnamed.iter().enumerate() {
                    if !field_participates(field) {
                        continue;
                    }
                    let idx = i as u32;
                    let tuple_idx = Index::from(i);
                    let ty = &field.ty;
                    refs_parts.push(quote! {
                        out.extend(<#ty as #path::GetRefs>::refs().into_iter()
                            .map(|r| r.prefixed(#path::PathSeg::Index(#idx))));
                    });
                    visit_parts.push(quote! {
                        #path::GetRefs::visit_ids(&self.#tuple_idx, __gr_f);
                    });
                }
                impl_block(name, path, refs_parts, visit_parts)
            }
            Fields::Unit => quote! {
                impl #path::GetRefs for #name {}
            },
        },
        Data::Enum(de) => {
            let mut refs_parts = Vec::new();
            let mut visit_arms = Vec::new();
            for variant in &de.variants {
                let vident = &variant.ident;
                let vname = vident.to_string();
                match &variant.fields {
                    Fields::Named(fields) => {
                        let mut bind = Vec::new();
                        let mut visits = Vec::new();
                        for field in &fields.named {
                            let ident = field.ident.as_ref().expect("named");
                            if !field_participates(field) {
                                continue;
                            }
                            let fname = ident.to_string();
                            let ty = &field.ty;
                            refs_parts.push(quote! {
                                out.extend(<#ty as #path::GetRefs>::refs().into_iter()
                                    .map(|r| r.prefixed(#path::PathSeg::Field(#fname))
                                             .prefixed(#path::PathSeg::Variant(#vname))));
                            });
                            bind.push(quote! { #ident, });
                            visits.push(quote! { #path::GetRefs::visit_ids(#ident, __gr_f); });
                        }
                        visit_arms.push(quote! {
                            Self::#vident { #(#bind)* .. } => { #(#visits)* }
                        });
                    }
                    Fields::Unnamed(fields) => {
                        let mut pats = Vec::new();
                        let mut visits = Vec::new();
                        for (i, field) in fields.unnamed.iter().enumerate() {
                            let participates = field_participates(field);
                            let b = format_ident!("f{i}");
                            if participates {
                                let idx = i as u32;
                                let ty = &field.ty;
                                refs_parts.push(quote! {
                                    out.extend(<#ty as #path::GetRefs>::refs().into_iter()
                                        .map(|r| r.prefixed(#path::PathSeg::Index(#idx))
                                                 .prefixed(#path::PathSeg::Variant(#vname))));
                                });
                                pats.push(quote! { #b });
                                visits.push(quote! { #path::GetRefs::visit_ids(#b, __gr_f); });
                            } else {
                                pats.push(quote! { _ });
                            }
                        }
                        visit_arms.push(quote! {
                            Self::#vident( #(#pats),* ) => { #(#visits)* }
                        });
                    }
                    Fields::Unit => {
                        visit_arms.push(quote! { Self::#vident => {} });
                    }
                }
            }
            quote! {
                impl #path::GetRefs for #name {
                    fn refs() -> ::std::vec::Vec<#path::FieldRef> {
                        let mut out = ::std::vec::Vec::new();
                        #(#refs_parts)*
                        out
                    }
                    fn visit_ids(&self, __gr_f: &mut dyn FnMut(&'static str, u64)) {
                        match self { #(#visit_arms)* }
                    }
                }
            }
        }
        Data::Union(_) => TokenStream::new(), // GetType branch already panics on unions
    }
}

fn impl_block(
    name: &Ident,
    path: &TokenStream,
    refs_parts: Vec<TokenStream>,
    visit_parts: Vec<TokenStream>,
) -> TokenStream {
    quote! {
        impl #path::GetRefs for #name {
            fn refs() -> ::std::vec::Vec<#path::FieldRef> {
                let mut out = ::std::vec::Vec::new();
                #(#refs_parts)*
                out
            }
            fn visit_ids(&self, __gr_f: &mut dyn FnMut(&'static str, u64)) {
                #(#visit_parts)*
            }
        }
    }
}