sql_table_macros/
lib.rs

1use proc_macro::TokenStream;
2use quote::quote;
3use syn::{
4    parse::Parser, parse_macro_input, punctuated::Punctuated, Data, DeriveInput, Field, Fields,
5    Token, Type,
6};
7
8/// Auto add `created_by(owner_type)`, `updated_by(owner_type)`, `created_at(time_type)`, `updated_at(time_type)`
9/// and `is_deleted(bool)` fields to struct
10///
11/// The format is
12/// - `crud(owner_and_time_type)`
13/// - `crud(owner_type, time_type)`
14///
15/// owner default type id `u64`, time default type is `String`
16///
17#[proc_macro_attribute]
18pub fn table_common_fields(args: TokenStream, input: TokenStream) -> TokenStream {
19    let args_parsed = parse_macro_input!(args as CommaSeparatedTypes);
20    let owner_type = args_parsed
21        .types
22        .first()
23        .cloned()
24        .unwrap_or(Type::Verbatim(quote! { u64 }));
25    let time_type = args_parsed
26        .types
27        .last()
28        .cloned()
29        .unwrap_or(Type::Verbatim(quote! { String }));
30
31    let input = parse_macro_input!(input as DeriveInput);
32    let struct_name = &input.ident;
33
34    let expanded = match input.data {
35        Data::Struct(mut data_struct) => {
36            if let Fields::Named(ref mut named_fields) = data_struct.fields {
37                let fields_to_add = vec![
38                    ("created_by", quote! { pub created_by: #owner_type }),
39                    ("updated_by", quote! { pub updated_by: #owner_type }),
40                    ("created_at", quote! { pub created_at: #time_type }),
41                    ("updated_at", quote! { pub updated_at: #time_type }),
42                    ("is_deleted", quote! { pub is_deleted: bool }),
43                ];
44
45                for (field_name, field_quote) in fields_to_add {
46                    let already_has_field = named_fields.named.iter().any(|field| {
47                        if let Some(ident) = &field.ident {
48                            ident == field_name
49                        } else {
50                            false
51                        }
52                    });
53
54                    if !already_has_field {
55                        named_fields
56                            .named
57                            .push(Field::parse_named.parse2(field_quote).unwrap());
58                    }
59                }
60            }
61
62            let fields = &data_struct.fields;
63            let attrs = &input.attrs;
64            quote! {
65                #(#attrs)*
66                pub struct #struct_name #fields
67            }
68        }
69        _ => syn::Error::new_spanned(input, "AddField can only be used on structs.")
70            .to_compile_error(),
71    };
72
73    TokenStream::from(expanded)
74}
75
76struct CommaSeparatedTypes {
77    types: Punctuated<Type, Token![,]>,
78}
79
80// Implement parsing for the helper struct
81impl syn::parse::Parse for CommaSeparatedTypes {
82    fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
83        Ok(CommaSeparatedTypes {
84            types: Punctuated::parse_terminated(input)?,
85        })
86    }
87}