structify_derive/
lib.rs

1use convert_case::{Case, Casing};
2use proc_macro::TokenStream;
3use syn::{parse::Parser, parse_macro_input, ItemFn};
4
5fn is_dependency_type(ty: &syn::Type) -> bool {
6    if let syn::Type::Path(type_path) = ty {
7        if let Some(segment) = type_path.path.segments.last() {
8            return segment.ident == "Dep";
9        }
10    }
11    false
12}
13
14fn get_struct_name_ident(attr: &TokenStream, input_fn: &syn::ItemFn) -> syn::Ident {
15    let fn_name_str = input_fn.sig.ident.to_string();
16
17    let default_struct_name = fn_name_str.to_case(Case::Pascal).to_string();
18
19    if let Ok(args) = syn::punctuated::Punctuated::<syn::Path, syn::Token![,]>::parse_terminated
20        .parse(attr.clone())
21    {
22        if args.is_empty() {
23            return syn::Ident::new(&default_struct_name, proc_macro2::Span::call_site());
24        }
25        if args.len() > 1 {
26            panic!("Expected exactly one for struct name #[structify(StructName)]");
27        }
28        syn::Ident::new(
29            &args[0].get_ident().unwrap().to_string(),
30            proc_macro2::Span::call_site(),
31        )
32    } else {
33        panic!("expected only a punctuated path");
34    }
35}
36
37#[proc_macro_attribute]
38pub fn structify(_attr: TokenStream, item: TokenStream) -> TokenStream {
39    let input_fn = parse_macro_input!(item as ItemFn);
40
41    let struct_name_ident = get_struct_name_ident(&_attr, &input_fn);
42
43    let mut fields = Vec::new();
44    let mut execute_bindings = Vec::new();
45    let mut new_struct_fields = Vec::new();
46    let mut execute_args = vec![quote::quote! {&self}];
47
48    let asyncness = match input_fn.sig.asyncness {
49        Some(_) => quote::quote! {async},
50        None => quote::quote! {},
51    };
52
53    for arg in input_fn.sig.inputs.iter() {
54        match arg {
55            syn::FnArg::Typed(pat_type) => {
56                let pat = pat_type.pat.clone();
57                let ty = pat_type.ty.clone();
58                if is_dependency_type(&pat_type.ty) {
59                    execute_args.push(quote::quote! { #pat: impl Into<#ty> });
60                    execute_bindings.push(quote::quote! {
61                        let #pat = #pat.into();
62                    });
63                } else {
64                    fields.push(quote::quote! {
65                        #pat: #ty
66                    });
67                    execute_bindings.push(quote::quote! {
68                        let #pat = &self.#pat;
69                    });
70                    new_struct_fields.push(quote::quote! {
71                        #pat: #pat
72                    });
73                }
74            }
75            syn::FnArg::Receiver(_) => {
76                panic!("Receiver arguments are not supported");
77            }
78        }
79    }
80
81    let block = input_fn.block.clone();
82
83    let ret_ty = match &input_fn.sig.output {
84        syn::ReturnType::Default => quote::quote! { () },
85        syn::ReturnType::Type(_, ty) => quote::quote! { #ty },
86    };
87
88    quote::quote! {
89        #[allow(unused)]
90        #input_fn
91
92        pub struct #struct_name_ident {
93            #(#fields),*
94        }
95
96        impl #struct_name_ident {
97            pub fn new(#(#fields),*) -> Self {
98                Self {
99                    #(#new_struct_fields),*
100                }
101            }
102            pub #asyncness fn execute(#(#execute_args),*) -> #ret_ty {
103                #(#execute_bindings)*
104                #block
105            }
106        }
107    }
108    .into()
109}