shared-struct 0.1.0

Proc-macro for turning a struct into a shared concurrent wrapper
Documentation
use proc_macro::TokenStream;
use proc_macro2::TokenStream as TokenStream2;
use quote::{format_ident, quote};
use syn::{Data, DeriveInput, Fields, parse_macro_input};

/// Transforms a struct into a shared, cheaply-cloneable wrapper.
///
/// Always generates:
/// - `XxxInner` with each field wrapped in `Mutex<T>` or stored raw
/// - `Xxx(Arc<XxxInner>)` with `Clone = Arc::clone`
/// - `Xxx::new(...)` constructor
/// - Per-field async accessors
///
/// Field attributes:
/// - (default) `Mutex<T>`, generates `async fn field() -> MutexGuard<T>`
/// - `#[raw]` stored as-is, generates `fn field() -> &T`
#[proc_macro_attribute]
pub fn shared(_attr: TokenStream, item: TokenStream) -> TokenStream {
    let input = parse_macro_input!(item as DeriveInput);
    expand(input)
        .unwrap_or_else(|e| e.to_compile_error())
        .into()
}

enum FieldKind {
    Mutex,
    Raw,
}

fn field_kind(field: &syn::Field) -> FieldKind {
    for attr in &field.attrs {
        if attr.path().is_ident("raw") {
            return FieldKind::Raw;
        }
    }
    FieldKind::Mutex
}

fn strip_field_attrs(field: &syn::Field) -> syn::Field {
    let mut f = field.clone();
    f.attrs.retain(|a| !a.path().is_ident("raw"));
    f
}

fn expand(input: DeriveInput) -> syn::Result<TokenStream2> {
    let vis = &input.vis;
    let name = &input.ident;
    let inner_name = format_ident!("{}Inner", name);

    let inner_attrs: Vec<_> = input
        .attrs
        .iter()
        .filter(|a| !a.path().is_ident("shared"))
        .map(|a| quote! { #a })
        .collect();

    let Data::Struct(data) = &input.data else {
        return Err(syn::Error::new_spanned(
            name,
            "#[shared] only supports structs",
        ));
    };
    let Fields::Named(fields) = &data.fields else {
        return Err(syn::Error::new_spanned(
            name,
            "#[shared] only supports named fields",
        ));
    };

    let mut inner_fields = Vec::new();
    let mut inner_inits = Vec::new();
    let mut new_params = Vec::new();
    let mut accessors = Vec::new();

    for field in &fields.named {
        let fname = field.ident.as_ref().unwrap();
        let ftype = &field.ty;
        let fvis = &field.vis;
        let kind = field_kind(field);
        let stripped = strip_field_attrs(field);
        let extra_attrs: Vec<_> = stripped.attrs.iter().collect();

        match kind {
            FieldKind::Mutex => {
                inner_fields.push(quote! {
                    #(#extra_attrs)*
                    #fvis #fname: ::tokio::sync::Mutex<#ftype>
                });
                inner_inits.push(quote! { #fname: ::tokio::sync::Mutex::new(#fname) });
                new_params.push(quote! { #fname: #ftype });
                accessors.push(quote! {
                    #fvis async fn #fname(&self) -> ::tokio::sync::MutexGuard<'_, #ftype> {
                        self.0.#fname.lock().await
                    }
                });
            }
            FieldKind::Raw => {
                inner_fields.push(quote! {
                    #(#extra_attrs)*
                    #fvis #fname: #ftype
                });
                inner_inits.push(quote! { #fname });
                new_params.push(quote! { #fname: #ftype });
                accessors.push(quote! {
                    #fvis fn #fname(&self) -> &#ftype {
                        &self.0.#fname
                    }
                });
            }
        }
    }

    Ok(quote! {
        #(#inner_attrs)*
        #vis struct #inner_name {
            #(#inner_fields),*
        }

        #[derive(Clone)]
        #vis struct #name(::std::sync::Arc<#inner_name>);

        impl #name {
            #vis fn new(#(#new_params),*) -> Self {
                Self(::std::sync::Arc::new(#inner_name {
                    #(#inner_inits),*
                }))
            }

            #(#accessors)*
        }
    })
}