mod utils;
use proc_macro::TokenStream;
use proc_macro2::{Ident, Span, TokenStream as TokenStream2};
use quote::quote;
use regex::Regex;
use std::{
fs::{read_dir, File},
io::prelude::*,
path::PathBuf,
};
use utils::{
base_file_name, get_all_dirs, get_all_middleware, parse_handler_path, parse_route_path, reverse_route_path, validate_route_handler,
REMIX_ROUTE_PATH,
};
#[proc_macro_attribute]
pub fn main(_: TokenStream, item: TokenStream) -> TokenStream {
let mut output: TokenStream = (quote! {
#[::rapid_web::actix::rt::main(system = "::rapid_web::actix::rt::System")]
})
.into();
output.extend(item);
output
}
#[proc_macro_attribute]
pub fn rapid_handler(_attr: TokenStream, item: TokenStream) -> TokenStream {
item
}
struct Handler {
path: String,
absolute_path: String,
name: String,
is_nested: bool,
}
enum RouteHandler {
Get(Handler),
Post(Handler),
Delete(Handler),
Put(Handler),
Patch(Handler),
}
#[proc_macro]
pub fn routes(item: proc_macro::TokenStream) -> proc_macro::TokenStream {
let routes_path = if let proc_macro::TokenTree::Literal(literal) = item.into_iter().next().unwrap() {
literal.to_string()
} else {
panic!("Error: Invalid routes path!")
};
if routes_path == "/" {
panic!("The 'routes_directory' variable cannot be set to a base path. Please use something nested!");
}
let parsed_path = &routes_path[1..routes_path.len() - 1];
let mut route_dirs: Vec<PathBuf> = vec![];
let mut route_handlers: Vec<RouteHandler> = vec![];
let mut has_root_middleware = false;
get_all_dirs(parsed_path, &mut route_dirs);
let route_files = read_dir(parsed_path)
.unwrap()
.map(|path| {
let path = path.unwrap().path();
path
})
.filter(|item| {
if item.is_dir() {
return false;
}
let file_name = item.file_name().unwrap();
if file_name == "_middleware.rs" {
has_root_middleware = true;
}
file_name != "mod"
})
.collect::<Vec<_>>();
for file_path in route_files {
let mut file = File::open(&file_path).unwrap();
let file_name = file_path.file_stem().unwrap().to_string_lossy().to_string();
let mut file_contents = String::new();
file.read_to_string(&mut file_contents).unwrap();
let dynamic_route_regex = Regex::new(r"_.*?_").unwrap();
let is_dynamic_route = dynamic_route_regex.is_match(&file_name);
let parsed_name = match is_dynamic_route {
true => file_name.replacen("_", r"{", 1).replacen("_", "}", 1),
false => file_name,
};
let handler = Handler {
name: parsed_name,
path: String::from("/"),
absolute_path: parsed_path.to_string(),
is_nested: false,
};
if file_contents.contains("async fn get") && validate_route_handler(&file_contents) {
route_handlers.push(RouteHandler::Get(handler))
} else if file_contents.contains("async fn post") && validate_route_handler(&file_contents) {
route_handlers.push(RouteHandler::Post(handler))
} else if file_contents.contains("async fn delete") && validate_route_handler(&file_contents) {
route_handlers.push(RouteHandler::Delete(handler))
} else if file_contents.contains("async fn put") && validate_route_handler(&file_contents) {
route_handlers.push(RouteHandler::Put(handler))
} else if file_contents.contains("async fn patch") && validate_route_handler(&file_contents) {
route_handlers.push(RouteHandler::Patch(handler))
}
}
for nested_file_path in route_dirs {
let route_files = read_dir(&nested_file_path)
.unwrap()
.map(|path| {
let path = path.unwrap().path();
path
})
.filter(|item| {
if item.is_dir() {
return false;
}
let file_name = item.file_name().unwrap();
file_name != "mod"
})
.collect::<Vec<_>>();
let cleaned_route_path = base_file_name(&nested_file_path, parsed_path);
for file_path in route_files {
let mut file = File::open(&file_path).unwrap();
let file_name = file_path.file_stem().unwrap().to_string_lossy().to_string();
if file_name == String::from("_middleware") {
continue;
}
let dynamic_route_regex = Regex::new(r"_.*?_").unwrap();
let is_dynamic_route = dynamic_route_regex.is_match(&file_name);
let parsed_name = match is_dynamic_route {
true => file_name.replacen("_", r"{", 1).replacen("_", "}", 1),
false => file_name,
};
let mut file_contents = String::new();
file.read_to_string(&mut file_contents).unwrap();
let handler = Handler {
name: parsed_name,
path: parse_route_path(cleaned_route_path.clone()),
absolute_path: nested_file_path.as_os_str().to_str().unwrap().to_string(),
is_nested: true,
};
if file_contents.contains("async fn get") && validate_route_handler(&file_contents) {
route_handlers.push(RouteHandler::Get(handler))
} else if file_contents.contains("async fn post") && validate_route_handler(&file_contents) {
route_handlers.push(RouteHandler::Post(handler))
} else if file_contents.contains("async fn delete") && validate_route_handler(&file_contents) {
route_handlers.push(RouteHandler::Delete(handler))
} else if file_contents.contains("async fn put") && validate_route_handler(&file_contents) {
route_handlers.push(RouteHandler::Put(handler))
} else if file_contents.contains("async fn patch") && validate_route_handler(&file_contents) {
route_handlers.push(RouteHandler::Patch(handler))
}
}
}
let idents = route_handlers
.into_iter()
.map(|it| match it {
RouteHandler::Get(route_handler) => generate_handler_tokens(route_handler, parsed_path, "get"),
RouteHandler::Post(route_handler) => generate_handler_tokens(route_handler, parsed_path, "post"),
RouteHandler::Delete(route_handler) => generate_handler_tokens(route_handler, parsed_path, "delete"),
RouteHandler::Put(route_handler) => generate_handler_tokens(route_handler, parsed_path, "put"),
RouteHandler::Patch(route_handler) => generate_handler_tokens(route_handler, parsed_path, "patch"),
})
.collect::<Vec<_>>();
proc_macro::TokenStream::from(quote!(
web::scope("")
#(#idents)*
))
}
#[proc_macro]
pub fn rapid_configure(item: proc_macro::TokenStream) -> proc_macro::TokenStream {
let path_string = if let proc_macro::TokenTree::Literal(literal) = item.into_iter().next().unwrap() {
literal.to_string()
} else {
panic!("Error: Invalid routes path!")
};
let path = &path_string[1..path_string.len() - 1];
let module_name = Ident::new(&path[path.find("/").map(|it| it + 1).unwrap_or(0)..], Span::call_site());
let mut route_dirs: Vec<PathBuf> = vec![];
get_all_dirs(path, &mut route_dirs);
let base_idents = std::fs::read_dir(path)
.unwrap()
.map(|it| {
let path = it.unwrap().path();
let name = path.file_stem().unwrap().to_string_lossy();
Ident::new(&name, Span::call_site())
})
.filter(|it| it.to_string() != "mod")
.collect::<Vec<_>>();
let mut nested_idents: Vec<TokenStream2> = Vec::new();
for dir in route_dirs {
let string = dir.into_os_string().into_string().unwrap();
let mod_name = format!("{}", string.replace("src/", "").replace("/", "::"));
let tokens: proc_macro2::TokenStream = mod_name.parse().unwrap();
nested_idents.push(quote! { pub use #tokens::*; });
}
proc_macro::TokenStream::from(quote!(
use include_dir::{include_dir, Dir};
mod #module_name { #(pub mod #base_idents;)* }
pub use #module_name::{
#(#base_idents,)*
};
#(#nested_idents)*
#[cfg(debug_assertions)] const ROUTES_DIR: Dir = include_dir!(#path); ))
}
#[proc_macro]
pub fn rapid_configure_remix(_: proc_macro::TokenStream) -> proc_macro::TokenStream {
let path = REMIX_ROUTE_PATH;
let module_name = Ident::new("routes", Span::call_site());
let mut route_dirs: Vec<PathBuf> = vec![];
get_all_dirs(path, &mut route_dirs);
let base_idents = std::fs::read_dir(path)
.unwrap()
.map(|it| {
let path = it.unwrap().path();
let name = path.file_stem().unwrap().to_string_lossy();
Ident::new(&name, Span::call_site())
})
.filter(|it| it.to_string() != "mod")
.collect::<Vec<_>>();
let mut nested_idents: Vec<TokenStream2> = Vec::new();
for dir in route_dirs {
let string = dir.into_os_string().into_string().unwrap();
let mod_name = format!("{}", string.replace("src/", "").replace("/", "::"));
let tokens: proc_macro2::TokenStream = mod_name.parse().unwrap();
nested_idents.push(quote! { pub use #tokens::*; });
}
proc_macro::TokenStream::from(quote!(
use include_dir::{include_dir, Dir};
mod #module_name { #(pub mod #base_idents;)* }
pub use #module_name::{
#(#base_idents,)*
};
#(#nested_idents)*
#[cfg(debug_assertions)] const ROUTES_DIR: Dir = include_dir!(#path); ))
}
fn generate_handler_tokens(route_handler: Handler, parsed_path: &str, handler_type: &str) -> proc_macro2::TokenStream {
let parsed_handler_type: proc_macro2::TokenStream = handler_type.parse().unwrap();
let mut middleware_paths: Vec<PathBuf> = Vec::new();
get_all_middleware(&route_handler.absolute_path, parsed_path, &mut middleware_paths);
let middleware_idents = middleware_paths
.into_iter()
.map(|middleware| {
let base = base_file_name(&middleware.as_path(), parsed_path);
if base == "" {
return quote!(.wrap(_middleware::Middleware));
}
let mod_name = format!("{}", base.replacen("/", "", 1)).replace("/", "::");
let parsed_mod: proc_macro2::TokenStream = mod_name.parse().unwrap();
quote!(.wrap(#parsed_mod::_middleware::Middleware))
})
.collect::<Vec<_>>();
let name = match route_handler.name.as_str() {
"index" => String::from(""),
_ => format!("/{}", route_handler.name),
};
let parsed_path = match route_handler.path.as_str() {
"/" => "",
_ => route_handler.path.as_str(),
};
let handler_mod_name = format!(
"{}",
parse_handler_path(&format!("{}/{}", reverse_route_path(parsed_path.to_string()), route_handler.name)).replacen("/", "", 1)
)
.replace("/", "::");
let handler: proc_macro2::TokenStream = handler_mod_name.parse().unwrap();
let rapid_routes_path = {
if route_handler.is_nested {
format!("{}{}", parsed_path, name)
} else {
let parsed_name = match name.as_str() {
"" => "/",
_ => name.as_str(),
};
format!("{}{}", parsed_path, parsed_name)
}
};
quote!(.route(#rapid_routes_path, web::#parsed_handler_type().to(#handler::#parsed_handler_type)#(#middleware_idents)*))
}