Skip to main content

urushi_derive/
lib.rs

1//! Derive macros for urushi component data.
2
3use proc_macro::TokenStream;
4use proc_macro_crate::{FoundCrate, crate_name};
5use quote::quote;
6use syn::{Data, DeriveInput, parse_macro_input, parse_quote};
7
8/// Derives urushi's canonical row formatter for a struct.
9#[proc_macro_derive(TableRow, attributes(table))]
10pub fn derive_table_row(input: TokenStream) -> TokenStream {
11    let input = parse_macro_input!(input as DeriveInput);
12    expand_table_row(input)
13        .unwrap_or_else(syn::Error::into_compile_error)
14        .into()
15}
16
17fn expand_table_row(input: DeriveInput) -> syn::Result<proc_macro2::TokenStream> {
18    let name = input.ident;
19    let mut generics = input.generics;
20    let fields = match input.data {
21        Data::Struct(data) => data.fields,
22        _ => {
23            return Err(syn::Error::new_spanned(
24                name,
25                "TableRow can only be derived for structs",
26            ));
27        }
28    };
29
30    let mut writes = Vec::new();
31    let mut displayed_types = Vec::new();
32    for (index, field) in fields.iter().enumerate() {
33        let mut skip = false;
34        for attribute in &field.attrs {
35            if !attribute.path().is_ident("table") {
36                continue;
37            }
38            attribute.parse_nested_meta(|meta| {
39                if meta.path.is_ident("skip") {
40                    skip = true;
41                    Ok(())
42                } else {
43                    Err(meta.error("only #[table(skip)] is supported"))
44                }
45            })?;
46        }
47        if skip {
48            continue;
49        }
50        let member = field
51            .ident
52            .clone()
53            .map(syn::Member::Named)
54            .unwrap_or_else(|| syn::Member::Unnamed(index.into()));
55        displayed_types.push(field.ty.clone());
56        writes.push(quote! { cells.display(&self.#member); });
57    }
58
59    for ty in displayed_types {
60        generics
61            .make_where_clause()
62            .predicates
63            .push(parse_quote!(#ty: ::core::fmt::Display));
64    }
65    let urushi = match crate_name("urushi") {
66        Ok(FoundCrate::Itself) => quote!(crate),
67        Ok(FoundCrate::Name(name)) => {
68            let name = syn::Ident::new(&name, proc_macro2::Span::call_site());
69            quote!(::#name)
70        }
71        Err(error) => {
72            return Err(syn::Error::new(
73                proc_macro2::Span::call_site(),
74                format!("could not locate the urushi crate: {error}"),
75            ));
76        }
77    };
78    let (impl_generics, type_generics, where_clause) = generics.split_for_impl();
79    Ok(quote! {
80        impl #impl_generics #urushi::TableRow for #name #type_generics #where_clause {
81            fn write_cells(&self, cells: &mut #urushi::TableRowCells) {
82                #(#writes)*
83            }
84        }
85    })
86}