1use 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}