differ-derive 0.0.1

Differ? but I just (🚧under active development🚧)
Documentation
// differ-derive/src/derive_fields.rs

use proc_macro::TokenStream;
use quote::{format_ident, quote};
use syn::parse::Parse as _; // required for Ident::parse
use syn::{Attribute, Data, DeriveInput, Fields, Ident, Type, parse_macro_input};

pub enum NestedAttr {
    Auto,                 // `#[config(nested)]`
    Explicit(Vec<Ident>), // `#[config(nested(a,b))]`
}

pub fn parse_nested_attr(attr: &Attribute) -> Option<NestedAttr> {
    if !attr.path().is_ident("config") {
        return None;
    }
    let parser = |input: syn::parse::ParseStream<'_>| -> syn::Result<NestedAttr> {
        // expecting: nested [ ( ident (, ident)* ) ] ?
        let nested_kw: Ident = input.parse()?;
        if nested_kw != "nested" {
            return Err(syn::Error::new_spanned(nested_kw, "expected `nested`"));
        }
        if input.is_empty() {
            return Ok(NestedAttr::Auto);
        }
        let content;
        syn::parenthesized!(content in input);
        if content.is_empty() {
            return Ok(NestedAttr::Auto);
        }
        let idents = content.parse_terminated(Ident::parse, syn::Token![,])?;
        Ok(NestedAttr::Explicit(idents.into_iter().collect()))
    };
    match attr.parse_args_with(parser) {
        Ok(r) => Some(r),
        Err(_) => None,
    }
}

const PREFIX_FIELD: &str = "__prefix";

pub fn derive_fields_impl(input: TokenStream) -> TokenStream {
    let DeriveInput { ident, data, .. } = parse_macro_input!(input as DeriveInput);

    // we only support structs with named fields
    let fields_named = match data {
        Data::Struct(s) => match s.fields {
            Fields::Named(n) => n,
            _ => {
                return syn::Error::new_spanned(ident, "Fields needs named fields")
                    .to_compile_error()
                    .into();
            }
        },
        _ => {
            return syn::Error::new_spanned(ident, "Fields can only be derived for structs")
                .to_compile_error()
                .into();
        }
    };

    let root_fields_ident = format_ident!("{}Fields", ident);
    let prefix_ident = format_ident!("{}", PREFIX_FIELD);

    let mut leaf_methods = Vec::new();
    let mut nested_impls = Vec::new(); // auto + explicit
    let mut proxy_structs = Vec::new(); // explicit only

    for field in fields_named.named {
        let field_ident = field.ident.unwrap();
        let field_name = field_ident.to_string();

        // first (if any) #[config(nested …)]
        let nested_attr = field.attrs.iter().find_map(parse_nested_attr);

        match nested_attr {
            // Simple “leaf” field
            None => {
                leaf_methods.push(quote! {
                    pub fn #field_ident(&self) -> ::differ::FieldName {
                        ::differ::FieldName::join(self.#prefix_ident.as_ref(), #field_name)
                    }
                });
            }

            // Explicit list: #[config(nested(a,b,…))]
            Some(NestedAttr::Explicit(subs)) => {
                let proxy_ident = format_ident!("__{}_Proxy", field_ident);

                // --- methods on the proxy itself ---------------------------------
                let mut proxy_methods = Vec::new();

                // `.self_()` – the field itself
                proxy_methods.push(quote! {
                    pub fn self_(&self) -> ::differ::FieldName {
                        ::differ::FieldName::join(self.#prefix_ident.as_ref(), "")
                    }
                });

                // `.a()`, `.b()`, …
                for sub in subs {
                    let sub_str = sub.to_string();
                    proxy_methods.push(quote! {
                        pub fn #sub(&self) -> ::differ::FieldName {
                            ::differ::FieldName::join(self.#prefix_ident.as_ref(), #sub_str)
                        }
                    });
                }

                proxy_structs.push(quote! {
                    #[allow(non_camel_case_types)]
                    pub struct #proxy_ident {
                        #prefix_ident: ::std::borrow::Cow<'static, str>,
                    }

                    impl #proxy_ident {
                        #(#proxy_methods)*
                    }

                    // ---- NEW: make every proxy itself usable as a FieldName ----------
                    impl ::differ::AsField for #proxy_ident {
                        fn as_field(&self) -> ::differ::FieldName {
                            ::differ::FieldName::from_string(self.#prefix_ident.to_string())
                        }
                    }

                    impl #root_fields_ident {
                        pub fn #field_ident(&self) -> #proxy_ident {
                            let p = if self.#prefix_ident.is_empty() {
                                ::std::borrow::Cow::Borrowed(#field_name)
                            } else {
                                ::std::borrow::Cow::Owned(
                                    format!("{}.{}", self.#prefix_ident, #field_name)
                                )
                            };
                            #proxy_ident { #prefix_ident: p }
                        }
                    }
                });
            }

            // Automatic mirror: #[config(nested)]
            Some(NestedAttr::Auto) => {
                let Type::Path(tp) = &field.ty else {
                    // fallback to leaf
                    leaf_methods.push(quote! {
                        pub fn #field_ident(&self) -> ::differ::FieldName {
                            ::differ::FieldName::join(self.#prefix_ident.as_ref(), #field_name)
                        }
                    });
                    continue;
                };

                if let Some(last) = tp.path.segments.last() {
                    let nested_fields_ident = format_ident!("{}Fields", last.ident);
                    nested_impls.push(quote! {
                        impl #root_fields_ident {
                            pub fn #field_ident(&self) -> #nested_fields_ident {
                                let p = if self.#prefix_ident.is_empty() {
                                    ::std::borrow::Cow::Borrowed(#field_name)
                                } else {
                                    ::std::borrow::Cow::Owned(
                                        format!("{}.{}", self.#prefix_ident, #field_name)
                                    )
                                };
                                #nested_fields_ident { #prefix_ident: p }
                            }
                        }
                    });
                }
            }
        }
    }

    // Assemble the final generated code
    let expanded = quote! {
        #[allow(non_camel_case_types)]
        pub struct #root_fields_ident {
            #prefix_ident: ::std::borrow::Cow<'static, str>,
        }

        impl #root_fields_ident {
            /// Internal root – users start from `Foo::fields()`.
            pub const fn __root() -> Self {
                Self { #prefix_ident: ::std::borrow::Cow::Borrowed("") }
            }

            #(#leaf_methods)*
        }

        // treat any *Fields struct itself as a path
        impl ::differ::AsField for #root_fields_ident {
            fn as_field(&self) -> ::differ::FieldName {
                ::differ::FieldName::from_string(self.#prefix_ident.to_string())
            }
        }

        #(#proxy_structs)*
        #(#nested_impls)*

        // -----------------------------------------------------------------
        // New ergonomic entry-point via a blanket trait
        // -----------------------------------------------------------------
        impl ::differ::HasFields for #ident {
            type Fields = #root_fields_ident;

            #[inline]
            fn fields() -> Self::Fields {
                #root_fields_ident::__root()
            }
        }
    };

    TokenStream::from(expanded)
}