exum_macros/
lib.rs

1use percent_encoding::{utf8_percent_encode, NON_ALPHANUMERIC};
2use proc_macro::{TokenStream};
3use proc_macro2::Span;
4use quote::{format_ident, quote};
5use convert_case::{Case, Casing};
6use syn::{ parse::Parser, parse_macro_input, parse_quote, punctuated::Punctuated, Block, Expr, ExprLit, FnArg, Ident, ItemFn, ItemImpl, ItemStruct, Lit, LitStr, Meta, MetaNameValue, Pat, Signature, Token};
7
8
9fn method_to_ident(method: &str) -> syn::Ident {
10    syn::Ident::new(&method.to_uppercase(), Span::call_site())
11}
12
13fn collect_methods(expr: Expr) -> Vec<String> {
14    match expr {
15        Expr::Lit(ExprLit { lit: Lit::Str(s), .. }) => s.value().split(',').map(|s| s.to_uppercase()).collect(),
16        Expr::Array(arr) => arr
17            .elems
18            .iter()
19            .map(|e| {
20                if let Expr::Lit(ExprLit { lit: Lit::Str(s), .. }) = e {
21                    s.value().to_uppercase()
22                } else {
23                    panic!("Array element must be a string literal")
24                }
25            })
26            .collect(),
27        _ => panic!("Expression must be a string literal or an array of string literals"),
28    }
29}
30
31fn extract_params(path: &str) -> Vec<String> {
32    path.split('/')
33        .filter_map(|s| {
34            if s.starts_with('{') && s.ends_with('}') {
35                let inner = &s[1..s.len() - 1];
36                let name = inner.trim_start_matches('*').trim_start_matches('*');
37                Some(name.to_string())
38            } else {
39                None
40            }
41        })
42        .collect()
43}
44
45fn normalize_path(path: &str) -> String {
46    if path.is_empty() {
47        return "/".to_string();
48    }
49
50    if path == "/" {
51        return "/".to_string();
52    }
53
54    let mut out = String::new();
55    for seg in path.split('/') {
56        if seg.is_empty() {
57            continue;
58        }
59        out.push('/');
60
61        if seg.starts_with(':') {
62            let name = seg[1..].trim();
63            if name.is_empty() {
64                out.push_str(seg);
65            } else {
66                out.push('{');
67                out.push_str(name);
68                out.push('}');
69            }
70        } else if seg.starts_with('{') && seg.ends_with('}') {
71            out.push_str(seg);
72        } else if seg.starts_with('*') {
73            let name = &seg[1..];
74            if name.starts_with('*') {
75                out.push_str("{**");
76                out.push_str(&name[1..]);
77                out.push('}');
78            } else {
79                out.push_str("{*");
80                out.push_str(name);
81                out.push('}');
82            }
83        } else {
84            out.push_str(&utf8_percent_encode(seg, NON_ALPHANUMERIC).to_string());
85        }
86    }
87
88    if out.is_empty() {
89        out.push('/');
90    }
91    out
92}
93
94fn extract_path(args: &Punctuated<Meta, Token![,]>) -> String {
95    let mut path = "/".to_string();
96    for meta in args {
97        if let Meta::NameValue(MetaNameValue {path: path_meta, value, ..}) = meta {
98            if path_meta.is_ident("path") {
99                if let Expr::Lit(ExprLit { lit: Lit::Str(s), .. }) = value {
100                    path = s.value();
101                }
102            }
103        }
104    }
105    normalize_path(&path)
106}
107
108fn extract_methods(args: &Punctuated<Meta, Token![,]>) -> Vec<String> {
109    let mut methods = Vec::new();
110    for meta in args {
111        if let Meta::NameValue(MetaNameValue {path: path_meta, value, ..}) = meta {
112            if path_meta.is_ident("method") {
113                methods.extend(collect_methods(value.clone()));
114            }
115        }
116    }
117    if methods.is_empty() {
118        methods.push("POST".to_string());
119    }
120    methods
121}
122fn parse_args(args: TokenStream) -> Punctuated<Meta, Token![,]> {
123    let attr_ts2: proc_macro2::TokenStream = args.into();
124    let parser = Punctuated::<Meta, Token![,]>::parse_terminated;
125    parser.parse2(attr_ts2).unwrap()
126}
127
128mod handle_input;
129use handle_input::{handle_b_attr, handle_q_attr};
130
131fn process_inputs(inputs: &Punctuated<FnArg, Token![,]>, path: &str, fn_name: &Ident) 
132    -> (Option<FnArg>, Vec<FnArg>, Option<ItemStruct>, Vec<syn::Stmt>)
133{
134    let params = extract_params(path);
135    let mut path_idents = Vec::new();
136    let mut path_types = Vec::new();
137    let mut other_inputs = Vec::new();
138    let mut q_fields: Vec<syn::Field> = Vec::new();
139    let mut inject_segs = Vec::new();
140
141    for input in inputs {
142        if let FnArg::Typed(pat_type) = input {
143            let has_q_attr = pat_type.attrs.iter().any(|a| a.path().is_ident("q"));
144            let has_b_attr = pat_type.attrs.iter().any(|a| a.path().is_ident("b"));
145            let has_dep_attr = pat_type.attrs.iter().any(|a| a.path().is_ident("dep"));
146            if has_q_attr {
147                handle_q_attr(pat_type, &mut q_fields);
148            } else if has_b_attr {
149                handle_b_attr(pat_type, &mut other_inputs);
150            } else if has_dep_attr {
151                handle_dep_attr(pat_type, &mut inject_segs);
152            } else if let Pat::Ident(ident) = &*pat_type.pat {
153                let name = ident.ident.to_string();
154                if params.contains(&name) {
155                    path_idents.push(ident.ident.clone());
156                    path_types.push(&pat_type.ty);
157                    continue;
158                } else {
159                    other_inputs.push(input.clone());
160                }
161            } else {
162                other_inputs.push(input.clone());
163            }
164        }
165    }
166
167    let path_arg: Option<FnArg> = if !path_idents.is_empty() {
168        Some(parse_quote! {
169            axum::extract::Path((#(#path_idents),*)): axum::extract::Path<(#(#path_types),*)>
170        })
171    } else {
172        None
173    };
174
175    let q_struct = if !q_fields.is_empty() {
176        let struct_ident = Ident::new(&format!("{}Query", fn_name.to_string().to_case(Case::Pascal)), Span::call_site());
177        let q_struct: ItemStruct = parse_quote! {
178            #[derive(serde::Deserialize)]
179            struct #struct_ident {
180                #(#q_fields),*
181            }
182        };
183        let fields: Vec<Ident> = q_fields.iter().map(|f| f.ident.clone().unwrap()).collect();
184
185        let query_arg: FnArg = parse_quote! {
186            axum::extract::Query(#struct_ident { #(#fields),* }): axum::extract::Query<#struct_ident>
187        };
188        other_inputs.push(query_arg);
189        Some(q_struct)
190    } else {
191        None
192    };
193
194    (path_arg, other_inputs, q_struct, inject_segs)
195}
196fn build_signature(
197    path_arg: Option<FnArg>,
198    mut other_inputs: Vec<FnArg>,
199    original_sig: &Signature,
200) -> Signature {
201    let mut new_inputs = Vec::new();
202    if let Some(p) = path_arg {
203        new_inputs.push(p);
204    }
205    new_inputs.append(&mut other_inputs);
206
207    let mut new_sig = original_sig.clone();
208    new_sig.inputs.clear();
209    for arg in new_inputs {
210        new_sig.inputs.push(arg);
211    }
212    if matches!(new_sig.output, syn::ReturnType::Default) {
213        new_sig.output = parse_quote!(-> impl axum::response::IntoResponse);
214    }
215
216    new_sig
217}
218fn build_router_expr(methods: &[String], path: &str, fn_name: &Ident) -> proc_macro2::TokenStream {
219    let path_lit = LitStr::new(path, Span::call_site());
220    let mut router_expr = quote! { router };
221    for m in methods {
222        let method_ident = method_to_ident(m);
223        router_expr = quote! {
224            #router_expr.route(#path_lit, axum::routing::on(axum::routing::MethodFilter::#method_ident, #fn_name))
225        };
226    }
227    router_expr
228}
229fn expand(
230    new_sig: Signature,
231    block: Box<Block>,
232    router_expr: proc_macro2::TokenStream,
233    q_struct: Option<ItemStruct>,
234    inject_segs: Vec<syn::Stmt>,
235
236) -> TokenStream {
237    let expanded = quote! {
238        #q_struct
239        #new_sig {
240            #(#inject_segs),*
241            #block
242        }
243
244        inventory::submit! {
245            exum::RouteDef {
246                router: |router| #router_expr,
247            }
248        }
249    };
250    TokenStream::from(expanded)
251}
252
253
254
255#[proc_macro_attribute]
256pub fn route(args: TokenStream, item: TokenStream) -> TokenStream {
257    let args = parse_args(args);
258    let input_fn = parse_macro_input!(item as ItemFn);
259
260    let path = extract_path(&args);
261    let methods = extract_methods(&args);
262
263    let (path_arg, other_inputs, q_struct, inject_segs) = process_inputs(&input_fn.sig.inputs, &path, &input_fn.sig.ident);
264    let new_sig = build_signature(path_arg, other_inputs, &input_fn.sig);
265
266    let router_expr = build_router_expr(&methods, &path, &input_fn.sig.ident);
267    expand(new_sig, input_fn.block, router_expr, q_struct, inject_segs)
268}
269
270mod derive_route_macro;
271use derive_route_macro::make_wrapper;
272
273use crate::{handle_input::handle_dep_attr, process::RouteAttrType};
274
275#[proc_macro_attribute]
276pub fn get(attr: TokenStream, item: TokenStream) -> TokenStream {
277    make_wrapper(attr, item, "GET")
278}
279#[proc_macro_attribute]
280pub fn post(attr: TokenStream, item: TokenStream) -> TokenStream {
281    make_wrapper(attr, item, "POST")
282}
283#[proc_macro_attribute]
284pub fn put(attr: TokenStream, item: TokenStream) -> TokenStream {
285    make_wrapper(attr, item, "PUT")
286}
287#[proc_macro_attribute]
288pub fn delete(attr: TokenStream, item: TokenStream) -> TokenStream {
289    make_wrapper(attr, item, "DELETE")
290}
291#[proc_macro_attribute]
292pub fn options(attr: TokenStream, item: TokenStream) -> TokenStream {
293    make_wrapper(attr, item, "OPTIONS")
294}
295#[proc_macro_attribute]
296pub fn head(attr: TokenStream, item: TokenStream) -> TokenStream {
297    make_wrapper(attr, item, "HEAD")
298}
299#[proc_macro_attribute]
300pub fn trace(attr: TokenStream, item: TokenStream) -> TokenStream {
301    make_wrapper(attr, item, "TRACE")
302}
303
304mod arg_parser;
305#[proc_macro_attribute]
306pub fn main(attr: TokenStream, item: TokenStream) -> TokenStream {
307    let args = parse_macro_input!(attr as arg_parser::MainArgs);
308    let input_fn = parse_macro_input!(item as ItemFn);
309
310    let config_expr = if let Some(path) = args.config {
311        quote! { ::exum::config::ApplicationConfig::from_file(#path) }
312    } else {
313        quote! { ::exum::config::ApplicationConfig::load() }
314    };
315
316    let vis = &input_fn.vis;
317    let block = &input_fn.block;
318
319    quote! {
320        #[tokio::main]
321        #vis async fn main() {
322            let _CONFIG = #config_expr;
323            init_global_state().await;
324            let mut app = ::exum::Application::build(_CONFIG);
325            {
326                #block
327            }
328            global_container().prewarm_all().await;
329            app.run().await;
330        }
331    }
332    .into()
333}
334
335#[proc_macro_attribute]
336pub fn state(args: TokenStream, input: TokenStream) -> TokenStream {
337    let input_fn = parse_macro_input!(input as ItemFn);
338    let args = parse_macro_input!(args as arg_parser::StateArgs);
339    let prewarm = args.prewarm;
340
341    let fn_name = &input_fn.sig.ident;
342    let vis = &input_fn.vis;
343    let sig = &input_fn.sig;
344    let block = &input_fn.block;
345
346    let output_ty = match &input_fn.sig.output {
347        syn::ReturnType::Type(_, ty) => ty.clone(),
348        syn::ReturnType::Default => {
349            return syn::Error::new_spanned(
350                &input_fn.sig.ident,
351                "state function must return a type",
352            )
353            .to_compile_error()
354            .into();
355        }
356    };
357
358    let init_fn_name = format_ident!("__init_{}", fn_name);
359    let def_fn_name = format_ident!("__state_def_{}", fn_name);
360
361    let expanded = quote! {
362        #vis #sig #block
363
364        #[allow(non_upper_case_globals)]
365        fn #init_fn_name() -> ::std::pin::Pin<
366            ::std::boxed::Box<
367                dyn ::std::future::Future<
368                    Output = ::std::sync::Arc<
369                        dyn ::std::any::Any + Send + Sync
370                    >
371                > + Send
372            >
373        > {
374            Box::pin(async {
375                let val: #output_ty = #fn_name().await;
376                ::std::sync::Arc::new(val) as ::std::sync::Arc<dyn ::std::any::Any + Send + Sync>
377            })
378        }
379
380        fn #def_fn_name() -> ::exum::StateDef {
381            ::exum::StateDef {
382                type_id: ::std::any::TypeId::of::<#output_ty>(),
383                prewarm: #prewarm,
384                init_fn: #init_fn_name,
385            }
386        }
387
388        ::inventory::submit! {
389            ::exum::StateDefFn(#def_fn_name)
390        }
391    };
392
393    expanded.into()
394}
395
396mod process;
397
398#[proc_macro_attribute]
399pub fn controller(attr: TokenStream, item: TokenStream) -> TokenStream {
400    let prefix = parse_macro_input!(attr as syn::LitStr).value();
401    let mut impl_block = parse_macro_input!(item as ItemImpl);
402    let controller_ident = &impl_block.self_ty;
403    let controller_name = match &**controller_ident {
404        syn::Type::Path(tp) => tp.path.segments.last().unwrap().ident.to_string(),
405        _ => "UnknownController".to_string(),
406    };
407    for item in &mut impl_block.items {
408        if let syn::ImplItem::Fn(method) = item {
409            let mut is_route_fn = false;
410            for attr in &mut method.attrs {
411                if let Some(ident) = attr.path().get_ident() {
412                    let name = ident.to_string();
413                    match process::valid_route_macro(&name) {
414                        RouteAttrType::Route => {
415                            let mut new_tokens = proc_macro2::TokenStream::new();
416                            let mut has_path = false;
417                            let _ = attr.parse_nested_meta(|meta| {
418                                if meta.path.is_ident("path") {
419                                    let lit: syn::LitStr = meta.value()?.parse()?;
420                                    let joined = process::join_path(&prefix, &lit.value()); 
421                                    new_tokens.extend(quote!(path = #joined,));
422                                    has_path = true;
423                                } else {
424                                    let method = meta.input.to_string();
425                                    new_tokens.extend(quote! {#method,});
426                                }
427                                Ok(())
428                            });
429                            is_route_fn = true;
430                            *attr = syn::parse_quote!(#[#ident(#new_tokens)])
431                        }
432                        RouteAttrType::Derive => {
433                            let lit = attr.parse_args::<syn::LitStr>().unwrap();
434                            let joined = process::join_path(&prefix, &lit.value());
435                            is_route_fn = true;
436                            *attr = syn::parse_quote! {#[#ident(#joined)]}
437                        }
438                        RouteAttrType::Not => {}
439                    }
440                }
441            }
442            if is_route_fn {
443                let orig_ident = &method.sig.ident;
444                let new_name = format!("__exum_flat_{}_{}", controller_name, orig_ident);
445                let new_ident = syn::Ident::new(&new_name, orig_ident.span());
446                method.sig.ident = new_ident;
447            }
448        }
449    }
450    let mod_name = format!("__exum_generated_{}", controller_name);
451    let mod_ident = syn::Ident::new(&mod_name, Span::call_site());
452    let items = impl_block.items;
453    TokenStream::from(quote! {
454        #[doc(hidden)]
455        #[allow(non_snake_case)]
456        #[allow(dead_code)]
457        mod #mod_ident {
458            use super::*;
459            #(#items)*
460        }
461    })
462}