Skip to main content

alibi_macros/
lib.rs

1//! Proc macros used by the Alibi root crate.
2
3use proc_macro::TokenStream as ProcMacroTokenStream;
4use proc_macro2::TokenStream;
5use quote::quote;
6use syn::{Data, DeriveInput, Fields, Ident, LitStr, Type, parse_macro_input};
7
8struct FieldInfo {
9    ident: Ident,
10    ty: Type,
11    default_expr: TokenStream,
12    skip: bool,
13}
14
15#[proc_macro_derive(AuthSchema, attributes(auth))]
16pub fn derive_auth_schema(input: ProcMacroTokenStream) -> ProcMacroTokenStream {
17    let input = parse_macro_input!(input as DeriveInput);
18    generate_auth_schema(&input)
19        .unwrap_or_else(syn::Error::into_compile_error)
20        .into()
21}
22
23#[proc_macro_derive(PluginConfig, attributes(plugin, config))]
24pub fn derive_plugin_config(input: ProcMacroTokenStream) -> ProcMacroTokenStream {
25    let input = parse_macro_input!(input as DeriveInput);
26    generate_plugin_config(&input)
27        .unwrap_or_else(syn::Error::into_compile_error)
28        .into()
29}
30
31fn generate_auth_schema(input: &DeriveInput) -> syn::Result<TokenStream> {
32    let user = parse_type_attr(input, "user")?;
33    let session = parse_type_attr(input, "session")?;
34    let account = parse_type_attr(input, "account")?;
35    let verification = parse_type_attr(input, "verification")?;
36    let ident = &input.ident;
37
38    Ok(quote! {
39        impl ::alibi::__private_core::schema::AuthSchema for #ident {
40            type User = #user;
41            type Session = #session;
42            type Account = #account;
43            type Verification = #verification;
44        }
45    })
46}
47
48fn parse_type_attr(input: &DeriveInput, key: &str) -> Result<Type, syn::Error> {
49    let mut parsed = None;
50    for attr in &input.attrs {
51        if !attr.path().is_ident("auth") {
52            continue;
53        }
54
55        attr.parse_nested_meta(|meta| {
56            if meta.path.is_ident(key) {
57                let value = meta.value()?;
58                let ty: LitStr = value.parse()?;
59                parsed = Some(ty.parse()?);
60            }
61            Ok(())
62        })?;
63    }
64
65    parsed.ok_or_else(|| {
66        syn::Error::new_spanned(
67            input,
68            format!("missing #[auth({key} = path::to::Model)] attribute for AuthSchema"),
69        )
70    })
71}
72
73fn generate_plugin_config(input: &DeriveInput) -> syn::Result<TokenStream> {
74    let plugin_name = parse_plugin_name(input)?;
75    let config_name = &input.ident;
76
77    let fields = match &input.data {
78        Data::Struct(data) => match &data.fields {
79            Fields::Named(named) => &named.named,
80            Fields::Unnamed(_) | Fields::Unit => {
81                return Err(syn::Error::new_spanned(
82                    &input.ident,
83                    "PluginConfig requires a struct with named fields",
84                ));
85            }
86        },
87        Data::Enum(_) | Data::Union(_) => {
88            return Err(syn::Error::new_spanned(
89                &input.ident,
90                "PluginConfig requires a struct",
91            ));
92        }
93    };
94
95    let field_infos = fields
96        .iter()
97        .map(parse_field_info)
98        .collect::<syn::Result<Vec<_>>>()?;
99
100    let default_fields: Vec<_> = field_infos
101        .iter()
102        .map(|fi| {
103            let name = &fi.ident;
104            let expr = &fi.default_expr;
105            quote! { #name: #expr }
106        })
107        .collect();
108
109    let default_impl = quote! {
110        impl Default for #config_name {
111            fn default() -> Self {
112                Self {
113                    #(#default_fields,)*
114                }
115            }
116        }
117    };
118
119    let builder_methods: Vec<_> = field_infos
120        .iter()
121        .filter(|fi| !fi.skip)
122        .map(gen_builder_method)
123        .collect();
124
125    let plugin_impl = quote! {
126        impl #plugin_name {
127            #[must_use]
128            pub fn new() -> Self {
129                Self {
130                    config: #config_name::default(),
131                }
132            }
133
134            #[must_use]
135            pub fn with_config(config: #config_name) -> Self {
136                Self { config }
137            }
138
139            #(#builder_methods)*
140        }
141    };
142
143    let plugin_default = quote! {
144        impl Default for #plugin_name {
145            fn default() -> Self {
146                Self::new()
147            }
148        }
149    };
150
151    Ok(quote! {
152        #default_impl
153        #plugin_impl
154        #plugin_default
155    })
156}
157
158fn parse_plugin_name(input: &DeriveInput) -> Result<Ident, syn::Error> {
159    for attr in &input.attrs {
160        if attr.path().is_ident("plugin") {
161            let name: LitStr = attr.parse_args_with(|stream: syn::parse::ParseStream<'_>| {
162                let ident: Ident = stream.parse()?;
163                if ident != "name" {
164                    return Err(syn::Error::new(ident.span(), "expected `name`"));
165                }
166                let _: syn::Token![=] = stream.parse()?;
167                stream.parse::<LitStr>()
168            })?;
169            return name.parse();
170        }
171    }
172    Err(syn::Error::new_spanned(
173        input,
174        "missing #[plugin(name = ...)] attribute",
175    ))
176}
177
178fn parse_field_info(field: &syn::Field) -> Result<FieldInfo, syn::Error> {
179    let ident = field
180        .ident
181        .clone()
182        .ok_or_else(|| syn::Error::new_spanned(field, "PluginConfig requires named fields"))?;
183    let ty = field.ty.clone();
184    let mut default_expr = None;
185    let mut skip = false;
186
187    for attr in &field.attrs {
188        if attr.path().is_ident("config") {
189            attr.parse_nested_meta(|meta| {
190                if meta.path.is_ident("skip") {
191                    skip = true;
192                    Ok(())
193                } else if meta.path.is_ident("default") {
194                    let value = meta.value()?;
195                    let expr: syn::Expr = value.parse()?;
196                    default_expr = Some(quote!(#expr));
197                    Ok(())
198                } else {
199                    Err(meta.error("expected `skip` or `default`"))
200                }
201            })?;
202        }
203    }
204
205    let default_expr = default_expr.ok_or_else(|| {
206        syn::Error::new_spanned(
207            &ident,
208            format!("field `{ident}` requires #[config(default = ...)]"),
209        )
210    })?;
211
212    Ok(FieldInfo {
213        ident,
214        ty,
215        default_expr,
216        skip,
217    })
218}
219
220fn is_string_type(ty: &Type) -> bool {
221    matches!(ty, Type::Path(type_path) if type_path.path.is_ident("String"))
222}
223
224fn extract_option_inner(ty: &Type) -> Option<&Type> {
225    if let Type::Path(type_path) = ty {
226        let segment = type_path.path.segments.last()?;
227        if segment.ident == "Option"
228            && let syn::PathArguments::AngleBracketed(args) = &segment.arguments
229            && let Some(syn::GenericArgument::Type(inner)) = args.args.first()
230        {
231            return Some(inner);
232        }
233    }
234    None
235}
236
237fn gen_builder_method(fi: &FieldInfo) -> TokenStream {
238    let field_name = &fi.ident;
239    let ty = &fi.ty;
240
241    if is_string_type(ty) {
242        quote! {
243            #[must_use]
244            pub fn #field_name(mut self, val: impl Into<String>) -> Self {
245                self.config.#field_name = val.into();
246                self
247            }
248        }
249    } else if let Some(inner) = extract_option_inner(ty) {
250        quote! {
251            #[must_use]
252            pub fn #field_name(mut self, val: #inner) -> Self {
253                self.config.#field_name = Some(val);
254                self
255            }
256        }
257    } else {
258        quote! {
259            #[must_use]
260            pub fn #field_name(mut self, val: #ty) -> Self {
261                self.config.#field_name = val;
262                self
263            }
264        }
265    }
266}