gen_file_database_macro/
lib.rs

1extern crate proc_macro;
2use proc_macro::TokenStream;
3use quote::quote;
4use syn::parse_macro_input;
5use syn::DeriveInput;
6use syn::Ident;
7
8#[proc_macro_derive(Encapsulate, attributes(index_key, index_keys))]
9pub fn encapsulate_derive(input: TokenStream) -> TokenStream {
10    use syn::{GenericParam, Data, Fields, Generics};
11
12    let ast: DeriveInput = parse_macro_input!(input as DeriveInput);
13    let name: &Ident = &ast.ident;
14    let mut generics: Generics = ast.generics.clone();
15
16    let mut direct_keys: Vec<proc_macro2::TokenStream> = Vec::new();
17    let mut nested_keys: Vec<proc_macro2::TokenStream> = Vec::new();
18    let mut generics_bounds: Vec<proc_macro2::TokenStream> = Vec::new();
19
20    if let Data::Struct(data_struct) = &ast.data {
21        if let Fields::Named(fields_named) = &data_struct.fields {
22            for field in &fields_named.named {
23                let field_name: &Ident = field.ident.as_ref().unwrap();
24                let field_ty: &syn::Type = &field.ty;
25
26                for attr in &field.attrs {
27                    if attr.path().is_ident("index_key") {
28                        direct_keys.push(quote! {
29                            keys.insert(self.#field_name.to_string());
30                        });
31                    }
32
33                    if attr.path().is_ident("index_keys") {
34                        nested_keys.push(quote! {
35                            keys.extend(self.#field_name.index_keys());
36                        });
37                        generics_bounds.push(quote! {
38                            #field_ty: Indexable
39                        });
40                    }
41                }
42            }
43        }
44    }
45
46    let has_index_fields: bool = !direct_keys.is_empty() || !nested_keys.is_empty();
47    let has_generic: bool = !ast.generics.params.is_empty();
48
49    if has_generic {
50        generics_bounds.push(quote! {
51            T: FileDbKey
52        });
53    }
54
55    for param in generics.params.iter_mut() {
56        if let GenericParam::Type(ty) = param {
57            ty.bounds.push(syn::parse_quote!(Clone));
58        }
59    }
60
61    let where_clause: &mut syn::WhereClause = generics.make_where_clause();
62    for bound in &generics_bounds {
63        where_clause.predicates.push(syn::parse2(bound.clone()).unwrap());
64    }
65
66    let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
67
68    let indexable_impl: proc_macro2::TokenStream = quote! {
69        impl #impl_generics Indexable for #name #ty_generics #where_clause {
70            fn index_keys(&self) -> std::collections::BTreeSet<String> {
71                let mut keys = std::collections::BTreeSet::new();
72                #(#direct_keys)*
73                #(#nested_keys)*
74                keys
75            }
76        }
77    };
78
79    let file_db_key_impl: proc_macro2::TokenStream = if has_generic {
80        quote! {
81            impl #impl_generics FileDbKey for #name #ty_generics #where_clause {
82                fn file_db_key() -> String {
83                    format!("{}_{}", stringify!(#name).to_case(Case::Snake), T::file_db_key().to_case(Case::Snake))
84                }
85            }
86        }
87    } else {
88        quote! {
89            impl #impl_generics FileDbKey for #name #ty_generics #where_clause {
90                fn file_db_key() -> String {
91                    stringify!(#name).to_case(Case::Snake)
92                }
93            }
94        }
95    };
96
97    let create_method: proc_macro2::TokenStream = if has_index_fields {
98        quote! {
99            pub fn create(&self, folder: &TargetFolder) -> io::Result<Capsule<Self>> {
100                let capsule = Capsule::<Self> {
101                    id: Uuid::new_v4(),
102                    key: Self::file_db_key(),
103                    folder: folder.clone(),
104                    inner: self.clone(),
105                };
106                let keys = self.index_keys();
107                if keys.is_empty() {
108                    return Err(io::Error::new(io::ErrorKind::InvalidInput, String::from("index defined on empty value")));
109                }
110                save_capsule(capsule.clone(), keys)
111            }
112        }
113    } else {
114        quote! {
115            pub fn create(&self, folder: &TargetFolder) -> io::Result<Capsule<Self>> {
116                let capsule = Capsule::<Self> {
117                    id: Uuid::new_v4(),
118                    key: Self::file_db_key(),
119                    folder: folder.clone(),
120                    inner: self.clone(),
121                };
122                save_capsule(capsule.clone(), std::collections::BTreeSet::new())
123            }
124        }
125    };
126
127    let upsert_method: proc_macro2::TokenStream = if has_index_fields {
128        quote! {
129            pub fn upsert(&self, folder: &TargetFolder) -> io::Result<Capsule<Self>> {
130                let capsule = Capsule::<Self> {
131                    id: Uuid::new_v4(),
132                    key: Self::file_db_key(),
133                    folder: folder.clone(),
134                    inner: self.clone(),
135                };
136                let keys = self.index_keys();
137                if keys.is_empty() {
138                    return Err(io::Error::new(io::ErrorKind::InvalidInput, String::from("index defined on empty value")));
139                }
140                upsert_capsule(capsule.clone(), keys)
141            }
142        }
143    } else {
144        quote! {
145            pub fn upsert(&self, folder: &TargetFolder) -> io::Result<Capsule<Self>> {
146                let capsule = Capsule::<Self> {
147                    id: Uuid::new_v4(),
148                    key: Self::file_db_key(),
149                    folder: folder.clone(),
150                    inner: self.clone(),
151                };
152                upsert_capsule(capsule.clone(), std::collections::BTreeSet::new())
153            }
154        }
155    };
156
157    let expanded: proc_macro2::TokenStream = quote! {
158        use uuid::Uuid;
159        use std::io;
160        use convert_case::Case;
161        use convert_case::Casing;
162        use gen_file_database::persistence::Capsule;
163        use gen_file_database::persistence::TargetFolder;
164        use gen_file_database::persistence::save_capsule;
165        use gen_file_database::persistence::upsert_capsule;
166        use gen_file_database::persistence::Indexable;
167        use gen_file_database::persistence::FileDbKey;
168
169        impl #impl_generics #name #ty_generics #where_clause {
170            #create_method
171            #upsert_method
172        }
173
174        #file_db_key_impl
175        #indexable_impl
176    };
177
178    TokenStream::from(expanded)
179}
180
181#[proc_macro_derive(Persist)]
182pub fn persist(input: TokenStream) -> TokenStream {
183    let ast: DeriveInput = parse_macro_input!(input as DeriveInput);
184    let name: &Ident = &ast.ident;
185
186    let expanded: proc_macro2::TokenStream = quote! {
187        use uuid::Uuid;
188        use std::io;
189        use convert_case::Case;
190        use convert_case::Casing;
191        use gen_file_database::persistence::Capsule;
192        use gen_file_database::persistence::TargetFolder;
193        use gen_file_database::persistence::save_object;
194        use gen_file_database::persistence::FileDbKey;
195
196        impl #name {
197            pub fn create(&self, folder: &TargetFolder) -> io::Result<Self> {
198                save_object(self, folder)
199            }
200        }
201
202        impl FileDbKey for #name {
203            fn file_db_key() -> String {
204                stringify!(#name).to_case(convert_case::Case::Snake)
205            }
206        }
207    };
208
209    TokenStream::from(expanded)
210}
211
212#[proc_macro_derive(Persistable)]
213pub fn persistable(input: TokenStream) -> TokenStream {
214    let ast: DeriveInput = parse_macro_input!(input as DeriveInput);
215    let name: &Ident = &ast.ident;
216
217    let expanded: proc_macro2::TokenStream = quote! {
218        use convert_case::Case;
219        use convert_case::Casing;
220        use gen_file_database::persistence::FileDbKey;
221
222        impl FileDbKey for #name {
223            fn file_db_key() -> String {
224                stringify!(#name).to_case(convert_case::Case::Snake)
225            }
226        }
227    };
228
229    TokenStream::from(expanded)
230}