1#![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#[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 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 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 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}