gen_file_database_macro/
lib.rs1extern 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 = &ast.ident;
14 let mut generics: Generics = ast.generics.clone();
15
16 let mut direct_keys = Vec::new();
17 let mut nested_keys = Vec::new();
18 let mut generics_bounds = 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 = field.ident.as_ref().unwrap();
24 let field_ty = &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 = !direct_keys.is_empty() || !nested_keys.is_empty();
47 let has_generic = !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 = 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 = 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 = 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 = if has_index_fields {
98 quote! {
99 pub fn create(&self, folder: &TargetFolder) -> Result<Capsule<Self>, FileDbError> {
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(FileDbError::MissingIndexKeys(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) -> Result<Capsule<Self>, FileDbError> {
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 = if has_index_fields {
128 quote! {
129 pub fn upsert(&self, folder: &TargetFolder) -> Result<Capsule<Self>, FileDbError> {
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(FileDbError::MissingIndexKeys(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) -> Result<Capsule<Self>, FileDbError> {
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 = quote! {
158 use uuid::Uuid;
159 use convert_case::Case;
160 use convert_case::Casing;
161 use gen_file_database::error::FileDbError;
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 = quote! {
187 use uuid::Uuid;
188 use convert_case::Case;
189 use convert_case::Casing;
190 use gen_file_database::error::FileDbError;
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) -> Result<Self, FileDbError> {
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 = 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}