elif_macros/lib.rs
1//! Procedural macros for elif.rs framework
2//!
3//! This crate provides macros that simplify common patterns in elif.rs applications,
4//! particularly around the bootstrap system and server startup.
5
6use proc_macro::TokenStream;
7use quote::quote;
8use syn::{
9 parse::{Parse, ParseStream, Result},
10 parse_macro_input,
11 punctuated::Punctuated,
12 token::Comma,
13 Error, Expr, ItemFn, LitStr, ReturnType, Token, Type,
14};
15
16/// Attribute macro for async main functions in elif.rs applications
17///
18/// This macro simplifies the bootstrap process by handling tokio runtime setup,
19/// logging initialization, and proper error conversion between bootstrap and HTTP errors.
20///
21/// # Examples
22///
23/// ## Basic Bootstrap Usage
24/// ```rust
25/// use elif::prelude::*;
26///
27/// #[elif::main]
28/// async fn main() -> Result<(), HttpError> {
29/// AppModule::bootstrap().listen("127.0.0.1:3000").await
30/// }
31/// ```
32///
33/// ## With Manual Server Setup
34/// ```rust
35/// use elif::prelude::*;
36///
37/// #[elif::main]
38/// async fn main() -> Result<(), HttpError> {
39/// let server = Server::new();
40/// server.listen("127.0.0.1:3000").await
41/// }
42/// ```
43#[proc_macro_attribute]
44pub fn main(_args: TokenStream, input: TokenStream) -> TokenStream {
45 let input_fn = parse_macro_input!(input as ItemFn);
46 let fn_name = &input_fn.sig.ident;
47 let fn_block = &input_fn.block;
48 let fn_inputs = &input_fn.sig.inputs;
49
50 // Check if function returns Result (more precise detection)
51 let returns_result = if let ReturnType::Type(_, ty) = &input_fn.sig.output {
52 if let syn::Type::Path(type_path) = &**ty {
53 type_path.path.segments.last().is_some_and(|s| s.ident == "Result")
54 } else {
55 false
56 }
57 } else {
58 false
59 };
60
61 let expanded = if returns_result {
62 // Function returns Result - handle bootstrap/HTTP errors properly
63 quote! {
64 #[tokio::main]
65 async fn main() -> Result<(), Box<dyn std::error::Error>> {
66 // Initialize logging (if not already done)
67 if std::env::var("RUST_LOG").is_err() {
68 std::env::set_var("RUST_LOG", "info");
69 }
70
71 // Try to initialize logger, but don't fail if already initialized
72 let _ = env_logger::try_init();
73
74 // Define the original async function inline
75 async fn #fn_name(#fn_inputs) -> Result<(), Box<dyn std::error::Error>> #fn_block
76
77 // Run it and handle errors
78 match #fn_name().await {
79 Ok(()) => Ok(()),
80 Err(e) => {
81 eprintln!("Application failed: {}", e);
82 Err(e)
83 }
84 }
85 }
86 }
87 } else {
88 // Function doesn't return Result
89 quote! {
90 #[tokio::main]
91 async fn main() {
92 // Initialize logging (if not already done)
93 if std::env::var("RUST_LOG").is_err() {
94 std::env::set_var("RUST_LOG", "info");
95 }
96
97 // Try to initialize logger, but don't fail if already initialized
98 let _ = env_logger::try_init();
99
100 // Define the original async function inline
101 async fn #fn_name(#fn_inputs) #fn_block
102
103 // Run it
104 #fn_name().await;
105 }
106 }
107 };
108
109 TokenStream::from(expanded)
110}
111
112/// Bootstrap arguments for the bootstrap macro
113struct BootstrapArgs {
114 app_module: Option<Type>,
115 address: Option<String>,
116 config: Option<Expr>,
117 middleware: Option<Vec<Expr>>,
118}
119
120impl Parse for BootstrapArgs {
121 fn parse(input: ParseStream) -> Result<Self> {
122 let mut app_module: Option<Type> = None;
123 let mut address: Option<String> = None;
124 let mut config: Option<Expr> = None;
125 let mut middleware: Option<Vec<Expr>> = None;
126
127 // If input is empty, use auto-discovery mode
128 if input.is_empty() {
129 return Ok(BootstrapArgs {
130 app_module: None,
131 address,
132 config,
133 middleware,
134 });
135 }
136
137 // Try to parse first argument as app module type (for backward compatibility)
138 let lookahead = input.lookahead1();
139 if lookahead.peek(syn::Ident) {
140 // Check if first token is a named parameter or a type
141 let fork = input.fork();
142 let _ident: syn::Ident = fork.parse().unwrap();
143 if fork.peek(Token![=]) {
144 // This is a named parameter, not a type - use auto-discovery
145 app_module = None;
146 // Don't consume the token here, let the main parsing loop handle it
147 } else {
148 // This looks like a type - parse it for backward compatibility
149 app_module = Some(input.parse()?);
150 }
151 } else if lookahead.peek(syn::Token![::]) || lookahead.peek(syn::Token![<]) {
152 // This is definitely a type
153 app_module = Some(input.parse()?);
154 } else if lookahead.peek(LitStr) {
155 // Legacy support: first argument is address string
156 let lit: LitStr = input.parse()?;
157 address = Some(lit.value());
158 }
159
160 // Parse optional named arguments
161 let mut first_param = true;
162 while !input.is_empty() {
163 // Only expect comma if this is not the first parameter or if we parsed a type
164 if !first_param || app_module.is_some() {
165 let _comma: Token![,] = input.parse()?;
166
167 if input.is_empty() {
168 break;
169 }
170 }
171 first_param = false;
172
173 let lookahead = input.lookahead1();
174 if lookahead.peek(syn::Ident) {
175 let ident: syn::Ident = input.parse()?;
176 let _eq: Token![=] = input.parse()?;
177
178 match ident.to_string().as_str() {
179 "addr" => {
180 let lit: LitStr = input.parse()?;
181 address = Some(lit.value());
182 }
183 "config" => {
184 config = Some(input.parse()?);
185 }
186 "middleware" => {
187 let content;
188 syn::bracketed!(content in input);
189 let middleware_list: Punctuated<Expr, Comma> =
190 content.parse_terminated(Expr::parse, Comma)?;
191 middleware = Some(middleware_list.into_iter().collect());
192 }
193 _ => {
194 let ident_name = ident.to_string();
195 return Err(Error::new_spanned(
196 ident,
197 format!(
198 "Unknown bootstrap parameter '{}'. Valid parameters are: addr, config, middleware\n\
199 \n\
200 💡 Usage examples:\n\
201 • #[elif::bootstrap] (auto-discovery)\n\
202 • #[elif::bootstrap(addr = \"127.0.0.1:3000\")]\n\
203 • #[elif::bootstrap(config = my_config())]\n\
204 • #[elif::bootstrap(middleware = [cors(), auth()])]\n\
205 • #[elif::bootstrap(AppModule)] (backward compatibility)",
206 ident_name
207 )
208 ));
209 }
210 }
211 } else if input.peek(LitStr) {
212 // Simple string for address (legacy support)
213 let lit: LitStr = input.parse()?;
214 address = Some(lit.value());
215 } else {
216 return Err(lookahead.error());
217 }
218 }
219
220 Ok(BootstrapArgs {
221 app_module,
222 address,
223 config,
224 middleware,
225 })
226 }
227}
228
229/// Enhanced bootstrap macro for zero-boilerplate application startup
230///
231/// This macro provides Laravel-style "convention over configuration" by automatically
232/// generating all the server setup code using auto-discovery of modules and controllers.
233///
234/// # Examples
235///
236/// ## Zero-Boilerplate Bootstrap (NEW!)
237/// ```rust
238/// use elif::prelude::*;
239///
240/// #[elif::bootstrap]
241/// async fn main() -> Result<(), HttpError> {
242/// // Automatically generated:
243/// // - Module discovery from compile-time registry
244/// // - Controller auto-registration from static registry
245/// // - DI container configuration
246/// // - Router setup with all controllers
247/// // - Server startup on 127.0.0.1:3000
248/// }
249/// ```
250///
251/// ## With Custom Address
252/// ```rust
253/// #[elif::bootstrap(addr = "0.0.0.0:8080")]
254/// async fn main() -> Result<(), HttpError> {}
255/// ```
256///
257/// ## With Custom Configuration
258/// ```rust
259/// #[elif::bootstrap(config = HttpConfig::with_timeout(30))]
260/// async fn main() -> Result<(), HttpError> {}
261/// ```
262///
263/// ## With Middleware
264/// ```rust
265/// #[elif::bootstrap(middleware = [cors(), auth(), logging()])]
266/// async fn main() -> Result<(), HttpError> {}
267/// ```
268///
269/// ## Full Configuration
270/// ```rust
271/// #[elif::bootstrap(
272/// addr = "0.0.0.0:8080",
273/// config = HttpConfig::production(),
274/// middleware = [cors(), auth()]
275/// )]
276/// async fn main() -> Result<(), HttpError> {}
277/// ```
278///
279/// ## Backward Compatibility with AppModule
280/// ```rust
281/// #[elif::bootstrap(AppModule)]
282/// async fn main() -> Result<(), HttpError> {}
283/// ```
284#[proc_macro_attribute]
285pub fn bootstrap(args: TokenStream, input: TokenStream) -> TokenStream {
286 let bootstrap_args = match syn::parse::<BootstrapArgs>(args) {
287 Ok(args) => args,
288 Err(err) => return err.to_compile_error().into(),
289 };
290
291 let input_fn = parse_macro_input!(input as ItemFn);
292 let fn_name = &input_fn.sig.ident;
293 let fn_inputs = &input_fn.sig.inputs;
294
295 // Validate that this is an async function
296 if input_fn.sig.asyncness.is_none() {
297 let error = Error::new_spanned(
298 input_fn.sig.fn_token,
299 "Bootstrap macro can only be applied to async functions\n\
300 \n\
301 💡 Change your function to:\n\
302 async fn main() -> Result<(), HttpError> {}"
303 );
304 return error.to_compile_error().into();
305 }
306
307 // Check if function returns Result
308 let returns_result = if let ReturnType::Type(_, ty) = &input_fn.sig.output {
309 if let syn::Type::Path(type_path) = &**ty {
310 type_path.path.segments.last().is_some_and(|s| s.ident == "Result")
311 } else {
312 false
313 }
314 } else {
315 false
316 };
317
318 if !returns_result {
319 let error = Error::new_spanned(
320 &input_fn.sig.output,
321 "Bootstrap macro requires functions to return Result<(), HttpError>\n\
322 \n\
323 💡 Change your function signature to:\n\
324 async fn main() -> Result<(), HttpError> {}"
325 );
326 return error.to_compile_error().into();
327 }
328
329 // Generate bootstrap code
330 let address = bootstrap_args.address.as_deref().unwrap_or("127.0.0.1:3000");
331 let config_setup = if let Some(config) = &bootstrap_args.config {
332 quote! { .with_config(#config) }
333 } else {
334 quote! {}
335 };
336 let middleware_setup = if let Some(middleware) = &bootstrap_args.middleware {
337 quote! { .with_middleware(vec![#(Box::new(#middleware)),*]) }
338 } else {
339 quote! {}
340 };
341
342 // Generate different bootstrap code based on whether app_module is provided
343 let bootstrap_code = if let Some(app_module) = &bootstrap_args.app_module {
344 // Backward compatibility: use AppModule::bootstrap()
345 quote! {
346 let bootstrapper = #app_module::bootstrap()
347 .map_err(|e| Box::new(e) as Box<dyn std::error::Error>)?
348 #config_setup
349 #middleware_setup;
350 }
351 } else {
352 // New auto-discovery mode: use AppBootstrapper directly
353 quote! {
354 let bootstrapper = {
355 // Import AppBootstrapper from elif-http
356 use elif_http::bootstrap::AppBootstrapper;
357 AppBootstrapper::new()
358 }
359 .map_err(|e| Box::new(e) as Box<dyn std::error::Error>)?
360 #config_setup
361 #middleware_setup;
362 }
363 };
364
365 let expanded = quote! {
366 #[tokio::main]
367 async fn main() -> Result<(), Box<dyn std::error::Error>> {
368 // Initialize logging (if not already done)
369 if std::env::var("RUST_LOG").is_err() {
370 std::env::set_var("RUST_LOG", "info");
371 }
372
373 // Try to initialize logger, but don't fail if already initialized
374 let _ = env_logger::try_init();
375
376 // Define the original async function inline for any custom setup
377 async fn #fn_name(#fn_inputs) -> Result<(), Box<dyn std::error::Error>> {
378 // Generate bootstrap code (auto-discovery or backward compatibility)
379 #bootstrap_code
380
381 // Start the server
382 bootstrapper
383 .listen(#address.parse::<std::net::SocketAddr>().expect("Invalid socket address"))
384 .await
385 .map_err(|e| Box::new(e) as Box<dyn std::error::Error>)?;
386
387 Ok(())
388 }
389
390 // Run it and handle errors
391 match #fn_name().await {
392 Ok(()) => Ok(()),
393 Err(e) => {
394 eprintln!("Application bootstrap failed: {}", e);
395 Err(e)
396 }
397 }
398 }
399 };
400
401 TokenStream::from(expanded)
402}