use fs_extra::dir::create_all;
use http::Method;
use regex::Regex;
use reqwest::blocking::Client;
use reqwest::Url;
use std::fs::File;
use std::io;
use std::path::PathBuf;
use std::str::FromStr;
fn has_dynamic_path(route: &str) -> bool {
let regex = Regex::new(r"\[(.*?)\]").expect("Failed to create the regex");
regex.is_match(route)
}
#[derive(Debug, PartialEq, Eq, Clone)]
pub struct AxumInfo {
pub module_import: String,
pub axum_route: String,
}
impl AxumInfo {
pub fn new(route: &Route) -> Self {
let mut module = route.path.chars();
module.next();
let axum_route = route.path.replace("/index", "");
let module_import = module
.as_str()
.to_string()
.replace('/', "_")
.replace('.', "_dot_")
.replace('-', "_hyphen_")
.to_lowercase();
if axum_route.is_empty() {
return AxumInfo {
module_import,
axum_route: "/".to_string(),
};
}
if route.is_dynamic {
return AxumInfo {
module_import: module
.as_str()
.to_string()
.replace('/', "_")
.replace('-', "_hyphen_")
.replace('[', "dyn_")
.replace("...", "catch_all_")
.replace(']', ""),
axum_route: axum_route
.replace("[...", "*")
.replace('[', ":")
.replace(']', ""),
};
}
AxumInfo {
module_import,
axum_route,
}
}
}
const NO_HTML_EXTENSIONS: [&str; 1] = ["xml"];
fn read_http_methods_from_file(path: &String) -> Vec<Method> {
let regex = Regex::new(r"tuono_lib::api\((.*?)\)]").expect("Failed to create API regex");
let file = fs_extra::file::read_to_string(path).expect("Failed to read API file");
regex
.find_iter(&file)
.map(|proc_macro| {
let http_method = proc_macro
.as_str()
.replace("tuono_lib::api(", "")
.replace(")]", "");
Method::from_str(http_method.as_str()).unwrap_or(Method::GET)
})
.collect::<Vec<Method>>()
}
#[derive(Debug, PartialEq, Eq, Clone)]
pub struct ApiData {
pub methods: Vec<Method>,
}
impl ApiData {
pub fn new(path: &String) -> Option<Self> {
if !path.starts_with("/api/") {
return None;
}
let base_path = std::env::current_dir().expect("Failed to get the base_path");
let file_path = base_path
.join(format!("src/routes{path}.rs"))
.to_str()
.unwrap()
.to_string();
let methods = read_http_methods_from_file(&file_path);
Some(ApiData { methods })
}
}
#[derive(Debug, PartialEq, Eq, Clone)]
pub struct Route {
path: String,
pub is_dynamic: bool,
pub axum_info: Option<AxumInfo>,
pub api_data: Option<ApiData>,
}
impl Route {
pub fn new(cleaned_path: String) -> Self {
Route {
path: cleaned_path.clone(),
axum_info: None,
is_dynamic: has_dynamic_path(&cleaned_path),
api_data: ApiData::new(&cleaned_path),
}
}
pub fn is_api(&self) -> bool {
self.api_data.is_some()
}
pub fn update_axum_info(&mut self) {
self.axum_info = Some(AxumInfo::new(self))
}
pub fn save_ssg_file(&self, reqwest: &Client) {
let path = &self.path.replace("index", "");
let mut response = reqwest
.get(format!("http://localhost:3000{path}"))
.send()
.unwrap();
let file_path = self.output_file_path();
let parent_dir = file_path.parent().unwrap();
if !parent_dir.is_dir() {
create_all(parent_dir, false).expect("Failed to create parent directories");
}
let mut file = File::create(file_path).expect("Failed to create the HTML file");
io::copy(&mut response, &mut file).expect("Failed to write the HTML on the file");
if self.axum_info.is_some() {
let data_file_path = PathBuf::from(&format!("out/static/__tuono/data{path}"));
let data_parent_dir = data_file_path.parent().unwrap();
if !data_parent_dir.is_dir() {
create_all(data_parent_dir, false)
.expect("Failed to create data parent directories");
}
let base = Url::parse("http://localhost:3000/__tuono/data").unwrap();
let path = if path == "/" { "" } else { path };
let pathname = &format!("/__tuono/data{path}");
let url = base
.join(pathname)
.expect("Failed to build the reqwest URL");
let mut response = reqwest.get(url).send().unwrap();
let mut data_file =
File::create(data_file_path).expect("Failed to create the JSON file");
io::copy(&mut response, &mut data_file).expect("Failed to write the JSON on the file");
}
}
fn output_file_path(&self) -> PathBuf {
let cleaned_path = self.path.replace("index", "");
if NO_HTML_EXTENSIONS
.iter()
.any(|extension| self.path.ends_with(extension))
{
return PathBuf::from(format!("out/static{}", cleaned_path));
}
PathBuf::from(format!("out/static{}/index.html", cleaned_path))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn should_find_dynamic_paths() {
let routes = [
("/home/user/Documents/tuono/src/routes/about.rs", false),
("/home/user/Documents/tuono/src/routes/index.rs", false),
(
"/home/user/Documents/tuono/src/routes/posts/index.rs",
false,
),
(
"/home/user/Documents/tuono/src/routes/posts/[post].rs",
true,
),
];
routes
.into_iter()
.for_each(|route| assert_eq!(has_dynamic_path(route.0), route.1));
}
#[test]
fn should_correctly_create_the_axum_infos() {
let info = AxumInfo::new(&Route::new("/index".to_string()));
assert_eq!(info.axum_route, "/");
assert_eq!(info.module_import, "index");
let dyn_info = AxumInfo::new(&Route::new("/[posts]".to_string()));
assert_eq!(dyn_info.axum_route, "/:posts");
assert_eq!(dyn_info.module_import, "dyn_posts");
}
#[test]
fn should_define_the_correct_html_build_path() {
let routes = [
("/index", "out/static/index.html"),
("/documentation", "out/static/documentation/index.html"),
("/sitemap.xml", "out/static/sitemap.xml"),
(
"/documentation/routing",
"out/static/documentation/routing/index.html",
),
];
for (path, html) in routes {
let route = Route::new(path.to_string());
assert_eq!(route.output_file_path(), PathBuf::from(html))
}
}
}