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#[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 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 quote!(
42 use actix_web::route;
43
44 #[actix_web::main]
45 #input
46 )
47 .into()
48}
49
50struct ExprVisitor;
52
53impl VisitMut for ExprVisitor {
54 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 if segment.ident == "new" {
76 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 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
147fn 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 if r == "index" {
169 "".to_string()
170
171 } else if r.starts_with("__") {
173 format!("{{{}}}", r.replace("__", "").to_string())
174
175 } 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 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
211fn 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
231fn generate_routes() -> Vec<Vec<String>> {
233 let mut routes = Vec::new();
234 let pages_path = FsPath::new("src/pages");
235
236 if !pages_path.exists() || !pages_path.is_dir() {
238 panic!("The src/pages folder does not exist!");
239 }
240
241 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
286fn 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}