better_auth_macros/
lib.rs1use 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 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 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}