Skip to main content

argos_arpa_item_macro/
lib.rs

1//! Derive macro crate for `argos-arpa`.
2
3#![warn(
4    missing_docs,
5    clippy::all,
6    clippy::pedantic,
7    clippy::nursery,
8    clippy::cargo
9)]
10#![allow(clippy::must_use_candidate)]
11
12extern crate proc_macro;
13extern crate quote;
14extern crate syn;
15
16use proc_macro2::TokenStream;
17use quote::quote;
18use syn::{DataStruct, DeriveInput, parse_macro_input, spanned::Spanned};
19
20mod item;
21use item::Item;
22
23type Field = (syn::Ident, Vec<syn::Attribute>, syn::Type);
24
25/// Implements a set of functions to ease the SQL data handling. A struct using
26/// `derive(CacheTableItem)` must have a field `id: i32`, and all other fields
27/// must be named and implement `sqlx::FromRow`.
28///
29/// - Use `#[unique]` for values that may not be repeated in any row.
30/// - Use `#[derived]` for values that are set automatically by the DB and thus
31///     not `insert`ed.
32/// - Use `#[secret]` for values that should not be selected when downloading.
33#[proc_macro_derive(TableItem, attributes(table, unique, derived, secret))]
34pub fn item_macro(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
35    let input = parse_macro_input!(input as DeriveInput);
36
37    let item = match Item::make(&input) {
38        Ok(i) => i,
39        Err(err) => return err,
40    };
41
42    let Some(attribute) = input
43        .attrs
44        .iter()
45        .find(|a| a.path().segments.len() == 1 && a.path().segments[0].ident == "table")
46    else {
47        return syn::Error::new(
48            input.span(),
49            "'const TABLE: &'static str' must be set! This can be done with \
50            '#[table(TABLE)]'.",
51        )
52        .to_compile_error()
53        .into();
54    };
55
56    let Ok(table) = attribute.parse_args::<syn::Ident>() else {
57        return syn::Error::new(input.span(), "Malformed table argument!")
58            .to_compile_error()
59            .into();
60    };
61
62    let name = input.ident;
63    let fields = match input.data {
64        syn::Data::Struct(DataStruct {
65            fields: syn::Fields::Named(ref fields),
66            ..
67        }) => fields
68            .named
69            .iter()
70            .filter_map(|field| {
71                field
72                    .ident
73                    .clone()
74                    .map(|id| (id, field.attrs.clone(), field.ty.clone()))
75            })
76            .collect::<Vec<_>>(),
77
78        _ => unimplemented!(),
79    };
80
81    let exists = exists(&item);
82    let select_all = select_all(&item);
83    let select_by_id = select_by_id(&item);
84    let delete = delete(&item);
85    let check_unique = check_unique(&item);
86    let insert = insert(&item);
87    let update = update(&item);
88    // panic!("{select_all}");
89
90    let insert_columns = insert_columns(&fields);
91    let insert_values = insert_values(&fields);
92    let unique_values = unique_values(&fields);
93    let select = select(&fields);
94
95    let table_name = format!("{table}");
96    let test_query = format!("select * from {table_name};");
97
98    let expanded = quote! {
99        impl TableItem for #name {
100            const TABLE: &'static str = #table_name;
101
102            fn id(&self) -> i32 {
103                self.id
104            }
105            #insert_columns
106            #insert_values
107            #unique_values
108            #select
109
110            #exists
111            #select_all
112            #select_by_id
113            #delete
114            #check_unique
115            #insert
116            #update
117        }
118
119        impl #name {
120            fn test_table_name_existance() {
121                _ = sqlx::query!(#test_query);
122            }
123        }
124    };
125
126    proc_macro::TokenStream::from(expanded)
127}
128
129fn exists(item: &Item) -> TokenStream {
130    let query = format!("select exists (select 1 from {} where id=$1);", item.table,);
131
132    quote! {
133        async fn exists(pool: &sqlx::PgPool, id: i32) -> sqlx::Result<bool> {
134            sqlx::query!(#query, id,)
135            .fetch_one(pool)
136            .await
137            .map(|r| r.exists.unwrap_or_default())
138        }
139    }
140}
141
142fn select_all(item: &Item) -> TokenStream {
143    let items = item.columns.iter().filter(|c| !c.secret).map(|f| &f.ident);
144
145    let get_string = items
146        .clone()
147        .map(ToString::to_string)
148        .collect::<Vec<_>>()
149        .join(",");
150
151    let ids = items;
152    let default_ids = item.columns.iter().filter(|c| c.secret).map(|f| &f.ident);
153    // .map(|id| quote! {
154    //     self.#id: data.#id
155    // });
156
157    let query = format!("select {} from {};", get_string, item.table);
158    quote! {
159        async fn select_all(pool: &sqlx::PgPool) -> sqlx::Result<Vec<Self>> {
160            sqlx::query_as!(Self, #query)
161            .fetch_all(pool)
162            .await
163            .map(|ok| ok
164                .into_iter()
165                .map(|data| Self {
166                    #(#ids: data.#ids,)*
167                    #(#default_ids: Default::default(),)*
168                })
169                .collect::<Vec<_>>()
170            )
171        }
172    }
173}
174
175fn select_by_id(item: &Item) -> TokenStream {
176    let items = item
177        .columns
178        .iter()
179        .filter(|c| !c.secret)
180        .map(|f| f.ident.to_string())
181        .collect::<Vec<_>>()
182        .join(",");
183
184    let query = format!("select {} from {} where id=$1;", items, item.table,);
185
186    quote! {
187        async fn select_by_id(
188            pool: &sqlx::PgPool,
189            id: i32,
190        ) -> sqlx::Result<Self> {
191            sqlx::query_as!(
192                Self,
193                #query,
194                id,
195            )
196            .fetch_one(pool)
197            .await
198        }
199    }
200}
201
202fn check_unique(item: &Item) -> TokenStream {
203    let items = item.columns.iter().filter(|c| c.unique);
204
205    if !items.clone().any(|_| true) {
206        return quote! {
207            async fn check_unique(
208                &self,
209                _: &sqlx::PgPool,
210            ) -> sqlx::Result<Option<i32>> {
211                Ok(None)
212            }
213        };
214    }
215
216    let ids = items.clone().map(|f| &f.ident);
217
218    let checks = items
219        .enumerate()
220        .map(|(i, f)| format!("{}=${}", f.ident, i + 1,))
221        .collect::<Vec<_>>()
222        .join(" or ");
223
224    let query = format!("select id from {} where {};", item.table, checks,);
225
226    quote! {
227        async fn check_unique(
228            &self,
229            pool: &sqlx::PgPool,
230        ) -> sqlx::Result<Option<i32>> {
231            struct Id{id:i32}
232            sqlx::query_as!(Id, #query, #(self.#ids,)*)
233            .fetch_optional(pool)
234            .await
235            .map(|i| i.map(|j| j.id))
236        }
237    }
238}
239
240fn delete(item: &Item) -> TokenStream {
241    let query = format!("delete from {} where id=$1;", item.table,);
242
243    quote! {
244        async fn delete(
245            tx: &mut sqlx::PgConnection,
246            id: i32,
247        ) -> sqlx::Result<()> {
248            sqlx::query!(
249                #query,
250                id,
251            )
252            .execute(tx)
253            .await?;
254            Ok(())
255        }
256    }
257}
258
259fn insert(item: &Item) -> TokenStream {
260    let items = item.columns.iter().filter(|c| !c.derived);
261
262    let (numbers, columns): (Vec<String>, Vec<String>) = items
263        .clone()
264        .enumerate()
265        .map(|(n, i)| (format!("${}", n + 1), i.ident.to_string()))
266        .unzip();
267
268    let ids = items.map(|i| &i.ident).collect::<Vec<_>>();
269
270    let query = format!(
271        "insert into {}({}) values({}) returning id;",
272        item.table,
273        columns.join(","),
274        numbers.join(","),
275    );
276    quote! {
277        async fn insert(
278            &self,
279            tx: &mut sqlx::PgConnection,
280        ) -> sqlx::Result<i32> {
281            struct Id{id:i32}
282            sqlx::query_as!(Id, #query, #(self.#ids,)*)
283            .fetch_one(tx)
284            .await
285            .map(|i| i.id)
286        }
287    }
288}
289
290fn update(item: &Item) -> TokenStream {
291    let items = item.columns.iter().filter(|c| !c.derived);
292
293    let columns = items
294        .clone()
295        .enumerate()
296        .map(|(i, f)| format!("{}=${}", f.ident, i + 1,))
297        .collect::<Vec<_>>()
298        .join(",");
299
300    let ids = items.map(|i| &i.ident).collect::<Vec<_>>();
301
302    let query = format!(
303        "update {} set {} where id=${};",
304        item.table,
305        columns,
306        ids.len() + 1
307    );
308
309    quote! {
310        async fn update(
311            &self,
312            tx: &mut sqlx::PgConnection,
313            id: i32,
314        ) -> sqlx::Result<()> {
315            sqlx::query!(#query, #(self.#ids,)* id,)
316            .execute(tx)
317            .await?;
318            Ok(())
319        }
320    }
321}
322
323fn insert_columns(fields: &[Field]) -> TokenStream {
324    let line = collect_named_data_inverse(fields, Some("derived"), ",", |f| f.0.to_string());
325
326    quote! {
327        fn insert_columns() -> &'static str {
328            #line
329        }
330    }
331}
332
333fn insert_values(fields: &[Field]) -> TokenStream {
334    let (brackets, ids): (Vec<_>, Vec<_>) = fields
335        .iter()
336        .filter(|(_, atts, _)| atts.iter().all(|a| !is_attribute(a, Some("derived"))))
337        .map(|(id, _, t)| match get_typetype(t) {
338            TypeType::OptionStringish => (
339                false,
340                quote! {
341                    self.#id
342                        .as_ref()
343                        .map_or_else(|| "NULL".into(), |v| format!("'{v}'"))
344                },
345            ),
346
347            TypeType::Option => (
348                false,
349                quote! {
350                    self.#id
351                        .as_ref()
352                        .map_or_else(|| "NULL".into(), |v| format!("{v}"))
353                },
354            ),
355
356            TypeType::Stringish => (true, quote! {self.#id}),
357            TypeType::Casual => (false, quote! {self.#id}),
358        })
359        .unzip();
360
361    let line = brackets
362        .into_iter()
363        .map(|to_quote| if to_quote { "'{}' " } else { "{} " })
364        .collect::<Vec<_>>()
365        .join(",");
366
367    quote! {
368        fn insert_values(&self) -> String {
369            format!(#line, #(#ids,)*)
370        }
371    }
372}
373
374enum TypeType {
375    Casual,
376    Stringish,
377    Option,
378    OptionStringish,
379}
380
381fn get_typetype(t: &syn::Type) -> TypeType {
382    if is_stringish(t) {
383        return TypeType::Stringish;
384    }
385
386    if let syn::Type::Path(path) = t {
387        let option = path.path.segments.iter().find(|seg| seg.ident.eq("Option"));
388
389        option.map_or(TypeType::Casual, |op| match &op.arguments {
390            syn::PathArguments::None => unimplemented!("Empty options have no support"),
391
392            syn::PathArguments::AngleBracketed(args) => match &args.args[0] {
393                // Options only have one argument
394                syn::GenericArgument::Type(t) => {
395                    if is_stringish(t) {
396                        TypeType::OptionStringish
397                    } else {
398                        TypeType::Option
399                    }
400                }
401                _ => unimplemented!("Only types go in options"),
402            },
403
404            syn::PathArguments::Parenthesized(_) => unimplemented!("Tuples have no support"),
405        })
406    } else {
407        TypeType::Casual
408    }
409}
410
411fn is_stringish(t: &syn::Type) -> bool {
412    match t {
413        syn::Type::Path(path) => path.path.segments.iter().any(|seg| {
414            seg.ident.eq("String") || seg.ident.eq("Uuid") || seg.ident.eq("OffsetDateTime")
415        }),
416
417        _ => false,
418    }
419}
420
421fn unique_values(fields: &[Field]) -> TokenStream {
422    let (brackets, ids): (Vec<_>, Vec<_>) = fields
423        .iter()
424        .filter(|(_, atts, _)| atts.iter().any(|a| is_attribute(a, Some("unique"))))
425        .map(|(id, _, t)| ((is_stringish(t), id.to_string()), id))
426        .unzip();
427
428    let line = brackets
429        .into_iter()
430        .map(|(is_str, name)| format!("{}={}", name, if is_str { "'{}'" } else { "{}" }))
431        .collect::<Vec<_>>()
432        .join(" or ");
433
434    quote! {
435        fn unique_values(&self) -> String {
436            format!(#line, #(self.#ids,)*)
437        }
438    }
439}
440
441fn select(fields: &[Field]) -> TokenStream {
442    let line = collect_named_data_inverse(fields, Some("secret"), ",", |f| f.0.to_string());
443
444    quote! {
445        fn select() -> &'static str {
446            #line
447        }
448    }
449}
450
451fn collect_named_data_inverse<F>(
452    fields: &[Field],
453    attribute: Option<&'static str>,
454    joiner: &'static str,
455    treatment: F,
456) -> String
457where
458    F: Fn(&Field) -> String,
459{
460    fields
461        .iter()
462        .filter(|&field| field.1.iter().all(|a| !is_attribute(a, attribute)))
463        .map(treatment)
464        .collect::<Vec<_>>()
465        .join(joiner)
466}
467
468fn is_attribute(a: &syn::Attribute, attr: Option<&'static str>) -> bool {
469    attr.is_none_or(|attr| match &a.meta {
470        syn::Meta::Path(path) => path.is_ident(attr),
471        _ => false,
472    })
473}