ruxt_macros/
lib.rs

1use proc_macro::TokenStream;
2use quote::quote;
3use regex::Regex;
4use std::fs;
5use std::path::Path as FsPath;
6use syn::{
7    parse_macro_input,
8    token::{Dot, Paren},
9    visit_mut::VisitMut,
10    Expr, ExprCall, ExprClosure, ExprLit, ExprMethodCall, ExprPath, ItemFn, Lit, LitStr, Path,
11    PathSegment,
12};
13
14/// A macro that wraps your main function with the necessary boilerplate to run a Ruxt application.
15/// The main function should be an async function that returns a `std::io::Result<()>`.
16/// The macro will generate the necessary code to run an Actix Web server with the routes defined in the `src/pages` folder.
17///
18/// # Example
19/// ```rust
20/// #[ruxt::main]
21/// async fn main() -> std::io::Result<()> {
22///    let test_data = "Hello, World!";
23///    HttpServer::new(move || App::new().app_data(test_data.to_string()))
24///    .bind(("0.0.0.0", 8080))?
25///    .run()
26///    .await
27/// }
28#[proc_macro_attribute]
29pub fn main(_args: TokenStream, item: TokenStream) -> TokenStream {
30    process_main(item)
31}
32
33pub(crate) fn process_main(item: TokenStream) -> TokenStream {
34    // Parse the input tokens into a syntax tree
35    let mut input = parse_macro_input!(item as ItemFn);
36
37    let mut visitor = ExprVisitor;
38    visitor.visit_item_fn_mut(&mut input);
39
40    // Return the modified function as a token stream
41    quote!(
42    use actix_web::route;
43
44    #[actix_web::main]
45    #input
46    )
47    .into()
48}
49
50/// Empty struct that implements the `VisitMut` trait
51struct ExprVisitor;
52
53impl VisitMut for ExprVisitor {
54    /// The actix web server is always created with this syntax:
55    /// ```rust
56    /// HttpServer::new(move || App::new()))
57    /// ```
58    ///
59    /// The goal of this function is to do the following:
60    /// * Loop over each expression in the current function, looking for one with a path
61    ///    segment that matches `HttpServer::new`
62    /// * Check if the first argument is a closure
63    /// * Generate the necessary route methods for each route in the `src/pages` folder
64    /// * Replace the closure with a new closure that contains the generated route methods
65    fn visit_expr_call_mut(&mut self, call: &mut ExprCall) {
66        if let Expr::Path(ref path) = *call.func {
67            if path
68                .path
69                .segments
70                .iter()
71                .any(|segment| segment.ident == "HttpServer")
72            {
73                for segment in &path.path.segments {
74                    // Check if the call is for the `new` method
75                    if segment.ident == "new" {
76                        // Check if the first argument is a closure
77                        if let Some(closure) = call.args.first_mut() {
78                            if let Expr::Closure(closure_expr) = closure {
79                                let body = *closure_expr.body.clone();
80                                let routes = generate_routes();
81                                let mut current_body = body.clone();
82
83                                for route in routes {
84                                    let verbs =
85                                        locate_verbs(&format!("src/pages/{}.rs", route.join("/")));
86
87                                    if verbs.is_empty() {
88                                        panic!(
89                                            "No route methods found for route: src/pages/{}.rs",
90                                            route.join("/")
91                                        );
92                                    }
93
94                                    for verb in verbs {
95                                        let method_call = generate_route_method_call(
96                                            current_body,
97                                            route.clone(),
98                                            &verb,
99                                        );
100                                        current_body = method_call.clone();
101                                        *closure = Expr::Closure(ExprClosure {
102                                            attrs: vec![],
103                                            asyncness: None,
104                                            movability: None,
105                                            capture: Some(Default::default()),
106                                            or1_token: Default::default(),
107                                            inputs: Default::default(),
108                                            or2_token: Default::default(),
109                                            output: syn::ReturnType::Default,
110                                            body: Box::new(method_call),
111                                            lifetimes: None,
112                                            constness: None,
113                                        });
114                                    }
115                                }
116                            }
117                        }
118                    }
119                }
120            }
121        };
122
123        // If this is not the call expression we're looking for,
124        // continue recursively searching
125        syn::visit_mut::visit_expr_call_mut(self, call);
126    }
127}
128
129const ROUTES: [&str; 5] = ["get", "post", "put", "patch", "delete"];
130
131fn locate_verbs(route: &str) -> Vec<String> {
132    let file = fs::read_to_string(route).expect(format!("Unable to read file: {}", route).as_str());
133    let mut verbs: Vec<String> = Vec::new();
134
135    for verb in ROUTES.iter() {
136        let re = Regex::new(&format!(r#"async fn {}"#, verb)).unwrap();
137        for cap in re.captures_iter(&file) {
138            if let Some(_) = cap.get(0) {
139                verbs.push(verb.to_string());
140            }
141        }
142    }
143
144    verbs
145}
146
147/// Generates the method call for a given route location
148fn generate_route_method_call(receiver: Expr, route_path: Vec<String>, verb: &str) -> Expr {
149    let segments = generate_route_segments(route_path.clone(), verb);
150
151    let path = Path {
152        leading_colon: None,
153        segments: segments.into_iter().collect(),
154    };
155
156    let path_expr = Expr::Path(ExprPath {
157        attrs: Default::default(),
158        qself: None,
159        path,
160    });
161
162    let path_method_fn_expr = Expr::MethodCall(generate_web_get_to_path(path_expr, verb));
163
164    let mut route_vec = route_path
165        .iter()
166        .map(|r| {
167            // Index route is at "/", not "/index"
168            if r == "index" {
169                "".to_string()
170
171            // __ is used to denote a dynamic route
172            } else if r.starts_with("__") {
173                format!("{{{}}}", r.replace("__", "").to_string())
174
175            // Otherwise just return the route
176            } else {
177                r.to_string()
178            }
179        })
180        .collect::<Vec<String>>();
181
182    if let Some(last) = route_vec.last_mut() {
183        if last.is_empty() {
184            // Remove the last element if it's empty
185            route_vec.pop();
186        }
187    }
188
189    let route = format!("/{}", route_vec.join("/"));
190
191    let route_expr = Expr::Lit(ExprLit {
192        attrs: Default::default(),
193        lit: Lit::Str(LitStr::new(&route, proc_macro2::Span::call_site())),
194    });
195
196    let args = vec![route_expr, path_method_fn_expr];
197
198    let method: syn::Ident = syn::Ident::new("route", proc_macro2::Span::call_site());
199
200    Expr::MethodCall(ExprMethodCall {
201        attrs: Default::default(),
202        receiver: Box::new(receiver),
203        method,
204        turbofish: None,
205        args: args.into_iter().collect(),
206        dot_token: Dot::default(),
207        paren_token: Paren::default(),
208    })
209}
210
211/// Generates the path segments for a given route location
212fn generate_route_segments(route_path: Vec<String>, verb: &str) -> Vec<PathSegment> {
213    let route_path = vec!["pages".to_string()]
214        .into_iter()
215        .chain(route_path.into_iter())
216        .collect::<Vec<String>>();
217
218    route_path
219        .iter()
220        .map(|segment| PathSegment {
221            ident: syn::Ident::new(segment, proc_macro2::Span::call_site()),
222            arguments: Default::default(),
223        })
224        .chain(std::iter::once(PathSegment {
225            ident: syn::Ident::new(verb, proc_macro2::Span::call_site()),
226            arguments: Default::default(),
227        }))
228        .collect()
229}
230
231/// Generates the routes for the application from the `src/pages` folder
232fn generate_routes() -> Vec<Vec<String>> {
233    let mut routes = Vec::new();
234    let pages_path = FsPath::new("src/pages");
235
236    // Check if src/pages folder exists
237    if !pages_path.exists() || !pages_path.is_dir() {
238        panic!("The src/pages folder does not exist!");
239    }
240
241    // Recursively loop through each folder in src/pages
242    visit_dirs(&pages_path, &mut Vec::new(), &mut routes);
243
244    routes
245}
246
247fn visit_dirs(dir: &FsPath, current_dir: &mut Vec<String>, result: &mut Vec<Vec<String>>) {
248    if dir.is_dir() {
249        for entry in fs::read_dir(dir).unwrap() {
250            let entry = entry.unwrap();
251            let path = entry.path();
252
253            if path.is_dir() {
254                if path.eq(FsPath::new("src/pages")) {
255                    continue;
256                }
257
258                current_dir.push(path.to_str().unwrap().to_string());
259
260                visit_dirs(&path, current_dir, result);
261            } else {
262                if let Some(ext) = path.extension() {
263                    if ext == "rs" {
264                        let route = path.strip_prefix("src/pages").unwrap().with_extension("");
265                        let route_str = route.to_str().unwrap();
266
267                        if route_str != "src/pages" {
268                            let route_components = route_str.split('/').collect::<Vec<&str>>();
269
270                            if !route_components.iter().any(|s| s == &"mod") {
271                                let route_components = route_components
272                                    .iter()
273                                    .map(|s| s.to_string())
274                                    .collect::<Vec<String>>();
275                                result
276                                    .push(route_components.iter().map(|s| s.to_string()).collect());
277                            }
278                        }
279                    }
280                }
281            }
282        }
283    }
284}
285
286/// Generates the `web::get().to()` method call
287fn generate_web_get_to_path(route_function_path: Expr, verb: &str) -> ExprMethodCall {
288    let method: syn::Ident = syn::Ident::new("to", proc_macro2::Span::call_site());
289    let args = vec![route_function_path];
290
291    ExprMethodCall {
292        attrs: Default::default(),
293        receiver: Box::new(Expr::Call(ExprCall {
294            paren_token: Paren::default(),
295            args: Default::default(),
296            attrs: Default::default(),
297            func: Box::new(Expr::Path(ExprPath {
298                attrs: Default::default(),
299                qself: None,
300                path: Path {
301                    leading_colon: None,
302                    segments: vec![
303                        PathSegment {
304                            ident: syn::Ident::new("actix_web", proc_macro2::Span::call_site()),
305                            arguments: Default::default(),
306                        },
307                        PathSegment {
308                            ident: syn::Ident::new("web", proc_macro2::Span::call_site()),
309                            arguments: Default::default(),
310                        },
311                        PathSegment {
312                            ident: syn::Ident::new(verb, proc_macro2::Span::call_site()),
313                            arguments: Default::default(),
314                        },
315                    ]
316                    .into_iter()
317                    .collect(),
318                },
319            })),
320        })),
321        method,
322        turbofish: None,
323        args: args.into_iter().collect(),
324        dot_token: Dot::default(),
325        paren_token: Paren::default(),
326    }
327}