Skip to main content

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