1mod utils;
2use proc_macro::TokenStream;
3use proc_macro2::{Ident, Span, TokenStream as TokenStream2};
4use quote::quote;
5use regex::Regex;
6use std::{
7 fs::{read_dir, File},
8 io::prelude::*,
9 path::PathBuf,
10};
11use utils::{
12 base_file_name, get_all_dirs, get_all_middleware, parse_handler_path, parse_route_path, reverse_route_path, validate_route_handler,
13 NEXTJS_ROUTE_PATH, REMIX_ROUTE_PATH,
14};
15
16#[proc_macro_attribute]
29pub fn main(_: TokenStream, item: TokenStream) -> TokenStream {
30 let mut output: TokenStream = (quote! {
31 #[::rapid_web::actix::rt::main(system = "::rapid_web::actix::rt::System")]
32 })
33 .into();
34
35 output.extend(item);
36 output
37}
38
39#[proc_macro_attribute]
42pub fn rapid_handler(_attr: TokenStream, item: TokenStream) -> TokenStream {
43 item
44}
45
46struct Handler {
47 path: String,
48 absolute_path: String,
49 name: String,
50 is_nested: bool,
51}
52
53enum RouteHandler {
55 Query(Handler),
56 Mutation(Handler),
57 Get(Handler),
58 Post(Handler),
59 Delete(Handler),
60 Put(Handler),
61 Patch(Handler),
62}
63
64#[proc_macro]
75pub fn routes(item: proc_macro::TokenStream) -> proc_macro::TokenStream {
76 let routes_path = if let proc_macro::TokenTree::Literal(literal) = item.into_iter().next().unwrap() {
78 literal.to_string()
79 } else {
80 panic!("Error: Invalid routes path!")
81 };
82
83 if routes_path == "/" {
85 panic!("The 'routes_directory' variable cannot be set to a base path. Please use something nested!");
86 }
87
88 let parsed_path = &routes_path[1..routes_path.len() - 1];
90
91 let mut route_dirs: Vec<PathBuf> = vec![];
93 let mut route_handlers: Vec<RouteHandler> = vec![];
95 let mut has_root_middleware = false;
97
98 get_all_dirs(parsed_path, &mut route_dirs);
100
101 let route_files = read_dir(parsed_path)
103 .unwrap()
104 .map(|path| {
105 let path = path.unwrap().path();
106 path
107 })
108 .filter(|item| {
109 if item.is_dir() {
110 return false;
111 }
112
113 let file_name = item.file_name().unwrap();
114
115 if file_name == "_middleware.rs" {
116 has_root_middleware = true;
117 }
118
119 file_name != "mod"
120 })
121 .collect::<Vec<_>>();
122
123 for file_path in route_files {
125 let mut file = File::open(&file_path).unwrap();
127 let file_name = file_path.file_stem().unwrap().to_string_lossy().to_string();
130 let mut file_contents = String::new();
132 file.read_to_string(&mut file_contents).unwrap();
133
134 let dynamic_route_regex = Regex::new(r"_.*?_").unwrap();
136
137 let is_dynamic_route = dynamic_route_regex.is_match(&file_name);
138
139 let parsed_name = match is_dynamic_route {
140 true => file_name.replacen("_", r"{", 1).replacen("_", "}", 1),
141 false => file_name,
142 };
143
144 let handler = Handler {
146 name: parsed_name,
147 path: String::from("/"),
148 absolute_path: parsed_path.to_string(),
149 is_nested: false,
150 };
151
152 parse_handlers(&mut route_handlers, file_contents, handler);
155 }
156
157 for nested_file_path in route_dirs {
158 let route_files = read_dir(&nested_file_path)
160 .unwrap()
161 .map(|path| {
162 let path = path.unwrap().path();
163 path
164 })
165 .filter(|item| {
166 if item.is_dir() {
167 return false;
168 }
169
170 let file_name = item.file_name().unwrap();
171
172 file_name != "mod"
173 })
174 .collect::<Vec<_>>();
175
176 let cleaned_route_path = base_file_name(&nested_file_path, parsed_path);
178
179 for file_path in route_files {
180 let mut file = File::open(&file_path).unwrap();
182 let file_name = file_path.file_stem().unwrap().to_string_lossy().to_string();
185
186 if file_name == String::from("_middleware") {
189 continue;
190 }
191
192 let dynamic_route_regex = Regex::new(r"_.*?_").unwrap();
193
194 let is_dynamic_route = dynamic_route_regex.is_match(&file_name);
195
196 let parsed_name = match is_dynamic_route {
197 true => file_name.replacen("_", r"{", 1).replacen("_", "}", 1),
198 false => file_name,
199 };
200
201 let mut file_contents = String::new();
203 file.read_to_string(&mut file_contents).unwrap();
205
206 let handler = Handler {
208 name: parsed_name,
209 path: parse_route_path(cleaned_route_path.clone()),
210 absolute_path: nested_file_path.as_os_str().to_str().unwrap().to_string(),
211 is_nested: true,
212 };
213
214 parse_handlers(&mut route_handlers, file_contents, handler);
217 }
218 }
219
220 let idents = route_handlers
222 .into_iter()
223 .map(|it| match it {
224 RouteHandler::Get(route_handler) => generate_handler_tokens(route_handler, parsed_path, "get"),
225 RouteHandler::Post(route_handler) => generate_handler_tokens(route_handler, parsed_path, "post"),
226 RouteHandler::Delete(route_handler) => generate_handler_tokens(route_handler, parsed_path, "delete"),
227 RouteHandler::Put(route_handler) => generate_handler_tokens(route_handler, parsed_path, "put"),
228 RouteHandler::Patch(route_handler) => generate_handler_tokens(route_handler, parsed_path, "patch"),
229 RouteHandler::Query(route_handler) => generate_handler_tokens(route_handler, parsed_path, "query"),
230 RouteHandler::Mutation(route_handler) => generate_handler_tokens(route_handler, parsed_path, "mutation"),
231 })
232 .collect::<Vec<_>>();
233
234 proc_macro::TokenStream::from(quote!(
235 web::scope("")
236 #(#idents)*
237 ))
238}
239
240#[proc_macro]
252pub fn rapid_configure(item: proc_macro::TokenStream) -> proc_macro::TokenStream {
253 let path_string = if let proc_macro::TokenTree::Literal(literal) = item.into_iter().next().unwrap() {
254 literal.to_string()
255 } else {
256 panic!("Error: Invalid routes path!")
257 };
258 let path = &path_string[1..path_string.len() - 1];
259 let module_name = Ident::new(&path[path.find("/").map(|it| it + 1).unwrap_or(0)..], Span::call_site());
260
261 let mut route_dirs: Vec<PathBuf> = vec![];
262
263 get_all_dirs(path, &mut route_dirs);
265
266 let base_idents = std::fs::read_dir(path)
268 .unwrap()
269 .map(|it| {
270 let path = it.unwrap().path();
271 let name = path.file_stem().unwrap().to_string_lossy();
272 Ident::new(&name, Span::call_site())
273 })
274 .filter(|it| it.to_string() != "mod")
275 .collect::<Vec<_>>();
276
277 let mut nested_idents: Vec<TokenStream2> = Vec::new();
278
279 for dir in route_dirs {
280 let string = dir.into_os_string().into_string().unwrap();
281
282 let mod_name = format!("{}", string.replace("src/", "").replace("/", "::"));
283 let tokens: proc_macro2::TokenStream = mod_name.parse().unwrap();
284 nested_idents.push(quote! { pub use #tokens::*; });
285 }
286
287 proc_macro::TokenStream::from(quote!(
288 use include_dir::{include_dir, Dir};
289 use rapid_web::actix::web;
290 mod #module_name { #(pub mod #base_idents;)* }
291 pub use #module_name::{
292 #(#base_idents,)*
293 };
294 #(#nested_idents)*
295 #[cfg(debug_assertions)] const ROUTES_DIR: Dir = include_dir!(#path); ))
298}
299
300#[proc_macro]
309pub fn rapid_configure_nextjs(tokens: proc_macro::TokenStream) -> proc_macro::TokenStream {
310 generate_route_imports(tokens, NEXTJS_ROUTE_PATH)
311}
312
313#[proc_macro]
322pub fn rapid_configure_remix(tokens: proc_macro::TokenStream) -> proc_macro::TokenStream {
323 generate_route_imports(tokens, REMIX_ROUTE_PATH)
324}
325
326fn generate_handler_tokens(route_handler: Handler, parsed_path: &str, handler_type: &str) -> proc_macro2::TokenStream {
329 let parsed_handler_type: proc_macro2::TokenStream = handler_type.parse().unwrap();
330
331 let mut middleware_paths: Vec<PathBuf> = Vec::new();
332 get_all_middleware(&route_handler.absolute_path, parsed_path, &mut middleware_paths);
333
334 let middleware_idents = middleware_paths
335 .into_iter()
336 .map(|middleware| {
337 let base = base_file_name(&middleware.as_path(), parsed_path);
338
339 if base == "" {
341 return quote!(.wrap(_middleware::Middleware));
342 }
343 let mod_name = format!("{}", base.replacen("/", "", 1)).replace("/", "::");
345
346 let parsed_mod: proc_macro2::TokenStream = mod_name.parse().unwrap();
347
348 quote!(.wrap(#parsed_mod::_middleware::Middleware))
349 })
350 .collect::<Vec<_>>();
351
352 let name = match route_handler.name.as_str() {
354 "index" => String::from(""),
355 _ => format!("/{}", route_handler.name),
356 };
357
358 let parsed_path = match route_handler.path.as_str() {
359 "/" => "",
360 _ => route_handler.path.as_str(),
361 };
362
363 let handler_mod_name = format!(
365 "{}",
366 parse_handler_path(&format!("{}/{}", reverse_route_path(parsed_path.to_string()), route_handler.name)).replacen("/", "", 1)
367 )
368 .replace("/", "::");
369 let handler: proc_macro2::TokenStream = handler_mod_name.parse().unwrap();
370
371 let rapid_routes_path = {
373 if route_handler.is_nested {
374 format!("{}{}", parsed_path, name)
375 } else {
376 let parsed_name = match name.as_str() {
378 "" => "/",
379 _ => name.as_str(),
380 };
381
382 format!("{}{}", parsed_path, parsed_name)
383 }
384 };
385
386 match handler_type {
388 "query" => {
390 quote!(
392 .route(#rapid_routes_path, web::get().to(#handler::#parsed_handler_type)#(#middleware_idents)*)
393 )
394 }
395 "mutation" => {
396 quote!(
399 .route(#rapid_routes_path, web::post().to(#handler::#parsed_handler_type)#(#middleware_idents)*)
400 .route(#rapid_routes_path, web::put().to(#handler::#parsed_handler_type)#(#middleware_idents)*)
401 .route(#rapid_routes_path, web::patch().to(#handler::#parsed_handler_type)#(#middleware_idents)*)
402 .route(#rapid_routes_path, web::delete().to(#handler::#parsed_handler_type)#(#middleware_idents)*)
403 )
404 }
405 _ => quote!(.route(#rapid_routes_path, web::#parsed_handler_type().to(#handler::#parsed_handler_type)#(#middleware_idents)*)),
408 }
409}
410
411fn parse_handlers(route_handlers: &mut Vec<RouteHandler>, file_contents: String, handler: Handler) {
415 if file_contents.contains("async fn get") && validate_route_handler(&file_contents) {
417 route_handlers.push(RouteHandler::Get(handler))
418 } else if file_contents.contains("async fn post") && validate_route_handler(&file_contents) {
419 route_handlers.push(RouteHandler::Post(handler))
420 } else if file_contents.contains("async fn delete") && validate_route_handler(&file_contents) {
421 route_handlers.push(RouteHandler::Delete(handler))
422 } else if file_contents.contains("async fn put") && validate_route_handler(&file_contents) {
423 route_handlers.push(RouteHandler::Put(handler))
424 } else if file_contents.contains("async fn patch") && validate_route_handler(&file_contents) {
425 route_handlers.push(RouteHandler::Patch(handler))
426 } else if file_contents.contains("async fn query") && validate_route_handler(&file_contents) {
427 route_handlers.push(RouteHandler::Query(handler))
428 } else if file_contents.contains("async fn mutation") && validate_route_handler(&file_contents) {
429 route_handlers.push(RouteHandler::Mutation(handler))
430 }
431}
432
433fn generate_route_imports(tokens: proc_macro::TokenStream, routes_directory: &str) -> proc_macro::TokenStream {
435 let path = if tokens.is_empty() {
437 routes_directory.to_string()
439 } else {
440 let path_string = if let proc_macro::TokenTree::Literal(literal) = tokens.into_iter().next().unwrap() {
442 let raw_path = literal.to_string();
443 raw_path[1..raw_path.len() - 1].to_string()
444 } else {
445 routes_directory.to_string()
446 };
447 path_string.to_string()
448 };
449
450 let module_name = Ident::new("routes", Span::call_site());
451
452 let mut route_dirs: Vec<PathBuf> = vec![];
453
454 get_all_dirs(&path, &mut route_dirs);
456
457 let base_idents = std::fs::read_dir(&path)
459 .unwrap()
460 .map(|it| {
461 let path = it.unwrap().path();
462 let name = path.file_stem().unwrap().to_string_lossy();
463 Ident::new(&name, Span::call_site())
464 })
465 .filter(|it| it.to_string() != "mod")
466 .collect::<Vec<_>>();
467
468 let mut nested_idents: Vec<TokenStream2> = Vec::new();
469
470 for dir in route_dirs {
471 let string = dir.into_os_string().into_string().unwrap();
472 let delimiter = "routes";
473 let start_index = match string.find(delimiter) {
474 Some(index) => index + delimiter.len(),
475 None => {
476 panic!("Invalid route directory!");
477 }
478 };
479 let mod_name = format!("routes{}", &string[start_index..].replace("/", "::"));
480 let tokens: proc_macro2::TokenStream = mod_name.parse().unwrap();
481 nested_idents.push(quote! { pub use #tokens::*; });
482 }
483
484 if path != routes_directory {
486 return proc_macro::TokenStream::from(quote!(
487 use include_dir::{include_dir, Dir};
488 use rapid_web::actix::web;
489 pub use #module_name::{
490 #(#base_idents,)*
491 };
492 #(#nested_idents)*
493 #[cfg(debug_assertions)] const ROUTES_DIR: Dir = include_dir!(#path); ));
496 } else {
497 return proc_macro::TokenStream::from(quote!(
499 use include_dir::{include_dir, Dir};
500 use rapid_web::actix::web;
501 mod #module_name { #(pub mod #base_idents;)* }
502 pub use #module_name::{
503 #(#base_idents,)*
504 };
505 #(#nested_idents)*
506 #[cfg(debug_assertions)] const ROUTES_DIR: Dir = include_dir!(#path); ));
509 }
510}