use std::{ffi::OsString, fs::{self, DirEntry}, path::Path, sync::{Arc, Mutex}};
use axum::{
body::Body, response::IntoResponse, routing::{delete, get, options, patch, post, put, MethodRouter}, extract::{Path as AxumPath, Json}, http::StatusCode
};
use http::{header::CONTENT_TYPE, HeaderMap, HeaderValue};
use mime_guess::from_path;
use once_cell::sync::Lazy;
use regex::Regex;
use serde_json::Value;
use tokio::fs::File;
use tokio_util::io::ReaderStream;
use crate::{app::App, id_manager::IdType, in_memory_collection::InMemoryCollection};
pub fn load_mock_dir(app: &mut App) {
load_dir(app, "", &app.root_path.clone());
}
fn load_dir(app: &mut App, parent_route: &str, entries_path: &str) {
let entries = fs::read_dir(entries_path).unwrap();
for entry in entries {
let entry = entry.unwrap();
load_entry(app, parent_route, &entry);
}
}
fn load_entry(app: &mut App, parent_route: &str, entry: &DirEntry) {
let binding = entry.file_name();
let end_point = binding.to_string_lossy();
let full_route = format!("{}/{}", parent_route, end_point);
let file_name = String::from(end_point);
if entry.file_type().unwrap().is_dir() {
if file_name.starts_with("public") {
return app.build_public_router(file_name,String::from(entry.path().to_string_lossy()));
}
return load_dir(app, &full_route, entry.path().to_str().unwrap());
}
if entry.file_type().unwrap().is_file() && !file_name.starts_with(".") {
load_file_route(app, parent_route, entry);
}
}
fn get_rest_options(descriptor: &str) -> (&str, IdType) {
let parts: Vec<&str> = descriptor.split(':').collect();
if parts.len() == 1 {
let part = parts[0];
match part {
"uuid" => ("id", IdType::Uuid),
"int" => ("id", IdType::Int),
id_key => (id_key, IdType::Uuid), }
} else if parts.len() == 2 {
let id_key = parts[0];
let type_str = parts[1];
let id_type = match type_str {
"uuid" => IdType::Uuid,
"int" => IdType::Int,
_ => IdType::Uuid, };
(id_key, id_type)
} else {
("id", IdType::Uuid)
}
}
fn load_file_route(app: &mut App, parent_route: &str, entry: &DirEntry) {
let binding = entry.file_name();
let file_name = binding.to_string_lossy();
let file_stem = file_name.split('.').next().unwrap_or("");
static RE_METHODS: Lazy<Regex> = Lazy::new(|| {
Regex::new(r"^(get|post|put|patch|delete|options)(\{(.+)\})?$").unwrap()
});
static RE_REST: Lazy<Regex> = Lazy::new(|| {
Regex::new(r"^(rest)(\{(.+)\})?$").unwrap()
});
let file_path = entry.path().into_os_string();
if let Some(captures) = RE_METHODS.captures(file_stem) {
let method = captures.get(1).unwrap().as_str();
let pattern = captures.get(3);
if let Some(pattern) = pattern {
let pattern = pattern.as_str();
if pattern == "id" {
let route_path = format!("{}/{}", parent_route, "{id}");
let router = build_method_router(&file_path, method);
println!("✔️ Mapped {} to {} {}", file_name, method.to_uppercase(), &route_path);
app.route(&route_path, router, Some(method.to_string()));
return;
}
if pattern.contains('-') {
if let Some((start_str, end_str)) = pattern.split_once('-') {
if let (Ok(start), Ok(end)) = (start_str.parse::<i32>(), end_str.parse::<i32>()) {
for i in start..=end {
let route_path = format!("{}/{}", parent_route, i);
let router = build_method_router(&file_path, method);
app.route(&route_path, router, Some(method.to_string()));
}
println!("✔️ Mapped {} to {} {}/[{}-{}]", file_name, method.to_uppercase(), parent_route, start, end);
return;
}
}
}
let route_path = format!("{}/{}", parent_route, pattern);
let router = build_method_router(&file_path, method);
println!("✔️ Mapped {} to {} {}", file_name, method.to_uppercase(), &route_path);
app.route(&route_path, router, Some(method.to_string()));
return;
}
let method = captures.get(1).unwrap().as_str();
let route_path = if parent_route.is_empty() { "/" } else { parent_route };
let router = build_method_router(&file_path, method);
println!("✔️ Mapped {} to {} {}", file_name, method.to_uppercase(), route_path);
app.route(route_path, router, Some(method.to_string()));
return;
}
if let Some(captures) = RE_REST.captures(file_stem) {
let descriptor = if let Some(pattern) = captures.get(3) {
pattern.as_str()
} else {
"id:uuid"
};
let (id_key, id_type) = get_rest_options(descriptor);
let route_path = if parent_route.is_empty() { "/" } else { parent_route };
build_in_memory_routes(app, route_path, file_path, id_key, id_type);
return;
}
let route_path = if parent_route.is_empty() { "/" } else { parent_route };
let route_path = format!("{}/{}", route_path, file_stem);
let router = build_stream_handler(file_path, "GET");
println!("✔️ Mapped {} to GET {}", file_name, route_path);
app.route(&route_path, router, Some(String::from("GET")));
}
fn get_file_content(file_path: &OsString) -> String {
fs::read_to_string(file_path).unwrap()
}
fn build_stream_handler(
file_path: OsString,
method: &str
) -> MethodRouter {
let handler = move || {
let file_path = file_path.clone();
async move {
let file = File::open(&file_path).await;
if file.is_err() {
return (
StatusCode::NOT_FOUND,
format!("File not found: {}", file_path.display()),
).into_response();
}
let file = file.unwrap();
let mime_type = from_path(&file_path)
.first_or_octet_stream()
.to_string();
let stream = ReaderStream::new(file);
let body = Body::from_stream(stream);
let mut headers = HeaderMap::new();
headers.insert(CONTENT_TYPE, HeaderValue::from_str(&mime_type).unwrap());
(headers, body).into_response()
}
};
match method.to_uppercase().as_str() {
"GET" => get(handler),
"POST" => post(handler),
"PUT" => put(handler),
"PATCH" => patch(handler),
"DELETE" => delete(handler),
"OPTIONS" => options(handler),
_ => get(|| async { "Unknown method in filename" }),
}
}
fn get_file_extension(file_path: &OsString) -> String {
Path::new(file_path)
.extension()
.and_then(std::ffi::OsStr::to_str)
.unwrap_or_default()
.to_string()
}
fn is_text_file(file_path: &OsString) -> bool {
let extension = get_file_extension(file_path);
extension == "txt" || extension == "md" || extension == "json"
}
fn content_handler(file_path: OsString, method: &str) -> MethodRouter {
let file_path = file_path.clone();
let handler = move || async move { get_file_content(&file_path) };
match method.to_uppercase().as_str() {
"GET" => get(handler),
"POST" => post(handler),
"PUT" => put(handler),
"PATCH" => patch(handler),
"DELETE" => delete(handler),
"OPTIONS" => options(handler),
_ => get(|| async { "Unknown method in filename" }),
}
}
fn build_method_router(file_path: &OsString, method: &str) -> MethodRouter {
let file_path = file_path.clone();
if is_text_file(&file_path) {
content_handler(file_path, method)
} else {
build_stream_handler(file_path, method)
}
}
fn build_in_memory_routes(app: &mut App, route_path: &str, file_path: OsString, id_key: &str, id_type: IdType) {
let in_memory_collection = InMemoryCollection::new(id_type, id_key.to_string());
let collection = Arc::new(Mutex::new(in_memory_collection));
let load_collection = Arc::clone(&collection);
load_initial_data(file_path, load_collection);
let list_collection = Arc::clone(&collection);
let list_router = get(move || {
async move {
let list_collection = list_collection.lock().unwrap();
let items = list_collection.get_all();
Json(items).into_response()
}
});
app.route(route_path, list_router, Some("GET".to_string()));
let create_collection = Arc::clone(&collection);
let create_router = post(move |Json(payload): Json<Value>| {
async move {
let mut create_collection = create_collection.lock().unwrap();
match create_collection.add(payload) {
Some(item) => (StatusCode::CREATED, Json(item)).into_response(),
None => StatusCode::BAD_REQUEST.into_response(),
}
}
});
app.route(route_path, create_router, Some("POST".to_string()));
let id_route = format!("{}/{{{}}}", route_path, id_key);
let get_collection = Arc::clone(&collection);
let get_router = get(move |AxumPath(id): AxumPath<String>| {
async move {
let get_collection = get_collection.lock().unwrap();
match get_collection.get(&id) {
Some(item) => Json(item).into_response(),
None => StatusCode::NOT_FOUND.into_response(),
}
}
});
app.route(&id_route, get_router, Some("GET".to_string()));
let update_collection = Arc::clone(&collection);
let put_router = put(move |AxumPath(id): AxumPath<String>, Json(payload): Json<Value>| {
async move {
let mut update_collection = update_collection.lock().unwrap();
match update_collection.update(&id, payload) {
Some(item) => Json(item).into_response(),
None => StatusCode::NOT_FOUND.into_response(),
}
}
});
app.route(&id_route, put_router, Some("PUT".to_string()));
let patch_collection = Arc::clone(&collection);
let patch_router = patch(move |AxumPath(id): AxumPath<String>, Json(payload): Json<Value>| {
async move {
let mut patch_collection = patch_collection.lock().unwrap();
match patch_collection.update_partial(&id, payload) {
Some(item) => Json(item).into_response(),
None => StatusCode::NOT_FOUND.into_response(),
}
}
});
app.route(&id_route, patch_router, Some("PATCH".to_string()));
let delete_collection = Arc::clone(&collection);
let delete_router = delete(move |AxumPath(id): AxumPath<String>| {
async move {
let mut delete_collection = delete_collection.lock().unwrap();
match delete_collection.delete(&id) {
Some(item) => Json(item).into_response(),
None => StatusCode::NOT_FOUND.into_response(),
}
}
});
app.route(&id_route, delete_router, Some("DELETE".to_string()));
println!("✔️ Built REST routes for {}", route_path);
}
fn load_initial_data(file_path: OsString, load_collection: Arc<Mutex<InMemoryCollection>>) {
if let Ok(file_content) = fs::read_to_string(&file_path) {
if let Ok(json_value) = serde_json::from_str::<Value>(&file_content) {
if let Value::Array(_) = json_value {
let mut collection = load_collection.lock().unwrap();
let added_items = collection.add_batch(json_value);
println!("✔️ Loaded {} initial items from {}", added_items.len(), file_path.to_string_lossy());
} else {
println!("⚠️ File {} does not contain a JSON array, skipping initial data load", file_path.to_string_lossy());
}
} else {
println!("⚠️ File {} does not contain valid JSON, skipping initial data load", file_path.to_string_lossy());
}
} else {
println!("⚠️ Could not read file {}, skipping initial data load", file_path.to_string_lossy());
}
}