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}