2025-07-07 23:05:19 +01:00

319 lines
8.9 KiB
Rust

//! Path utilities module for centralized path resolution
//!
//! This module provides utilities for resolving paths relative to the project root,
//! eliminating the need for hardcoded "../.." paths throughout the codebase.
#![allow(dead_code)]
use std::env;
use std::path::{Path, PathBuf};
use std::sync::OnceLock;
/// Global project root path, initialized once at startup
static PROJECT_ROOT: OnceLock<PathBuf> = OnceLock::new();
/// Initialize the project root path
pub fn init_project_root() -> PathBuf {
PROJECT_ROOT
.get_or_init(|| {
// Try to get root path from environment variable first
if let Ok(root_path) = env::var("PROJECT_ROOT") {
return PathBuf::from(root_path);
}
// Try to get root path from config if available
if let Ok(config_root) = env::var("CONFIG_ROOT") {
return PathBuf::from(config_root);
}
// Fall back to detecting based on current executable or working directory
detect_project_root()
})
.clone()
}
/// Detect the project root directory
fn detect_project_root() -> PathBuf {
// Start with current working directory
let mut current_dir = env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
// Look for Cargo.toml in current directory and parent directories
loop {
let cargo_toml = current_dir.join("Cargo.toml");
if cargo_toml.exists() {
// Check if this is a workspace root (has [workspace] section)
if let Ok(contents) = std::fs::read_to_string(&cargo_toml) {
if contents.contains("[workspace]") {
return current_dir;
}
}
}
// Move up one directory
if let Some(parent) = current_dir.parent() {
current_dir = parent.to_path_buf();
} else {
break;
}
}
// If we couldn't find workspace root, use current directory
env::current_dir().unwrap_or_else(|_| PathBuf::from("."))
}
/// Get the project root path
pub fn get_project_root() -> &'static PathBuf {
PROJECT_ROOT.get_or_init(|| detect_project_root())
}
/// Resolve a path relative to the project root
pub fn resolve_from_root<P: AsRef<Path>>(relative_path: P) -> PathBuf {
get_project_root().join(relative_path)
}
/// Resolve a path relative to the project root and return as string
#[allow(dead_code)]
pub fn resolve_from_root_str<P: AsRef<Path>>(relative_path: P) -> String {
resolve_from_root(relative_path)
.to_string_lossy()
.to_string()
}
/// Get the absolute path for a given relative path from project root
#[allow(dead_code)]
pub fn get_absolute_path<P: AsRef<Path>>(relative_path: P) -> Result<PathBuf, std::io::Error> {
let resolved = resolve_from_root(relative_path);
resolved.canonicalize()
}
/// Check if a path exists relative to project root
#[allow(dead_code)]
pub fn exists_from_root<P: AsRef<Path>>(relative_path: P) -> bool {
resolve_from_root(relative_path).exists()
}
/// Read a file relative to project root
#[allow(dead_code)]
pub fn read_file_from_root<P: AsRef<Path>>(relative_path: P) -> Result<String, std::io::Error> {
let path = resolve_from_root(relative_path);
std::fs::read_to_string(path)
}
/// Read a file relative to project root, with fallback to embedded content
#[allow(dead_code)]
pub fn read_file_from_root_or_embedded<P: AsRef<Path>>(
relative_path: P,
embedded_content: &str,
) -> String {
read_file_from_root(relative_path).unwrap_or_else(|_| embedded_content.to_string())
}
/// Path constants for commonly used directories
pub mod paths {
use super::*;
/// Get the config directory path
#[allow(dead_code)]
pub fn config_dir() -> PathBuf {
resolve_from_root("config")
}
/// Get the content directory path
#[allow(dead_code)]
pub fn content_dir() -> PathBuf {
resolve_from_root("content")
}
/// Get the migrations directory path
pub fn migrations_dir() -> PathBuf {
resolve_from_root("migrations")
}
/// Get the public directory path
#[allow(dead_code)]
pub fn public_dir() -> PathBuf {
resolve_from_root("public")
}
/// Get the uploads directory path
#[allow(dead_code)]
pub fn uploads_dir() -> PathBuf {
resolve_from_root("uploads")
}
/// Get the logs directory path
#[allow(dead_code)]
pub fn logs_dir() -> PathBuf {
resolve_from_root("logs")
}
/// Get the certs directory path
#[allow(dead_code)]
pub fn certs_dir() -> PathBuf {
resolve_from_root("certs")
}
/// Get the cache directory path
#[allow(dead_code)]
pub fn cache_dir() -> PathBuf {
resolve_from_root("cache")
}
/// Get the data directory path
#[allow(dead_code)]
pub fn data_dir() -> PathBuf {
resolve_from_root("data")
}
/// Get the backup directory path
#[allow(dead_code)]
pub fn backup_dir() -> PathBuf {
resolve_from_root("backups")
}
}
/// Configuration file utilities
pub mod config {
use super::*;
/// Get the path to a configuration file
#[allow(dead_code)]
pub fn config_file<P: AsRef<Path>>(filename: P) -> PathBuf {
resolve_from_root(filename)
}
/// Get the path to the main config file
#[allow(dead_code)]
pub fn main_config() -> PathBuf {
config_file("config.toml")
}
/// Get the path to the development config file
#[allow(dead_code)]
pub fn dev_config() -> PathBuf {
config_file("config.dev.toml")
}
/// Get the path to the production config file
#[allow(dead_code)]
pub fn prod_config() -> PathBuf {
config_file("config.prod.toml")
}
/// Read a config file with fallback to embedded content
#[allow(dead_code)]
pub fn read_config_or_embedded(filename: &str, embedded: &str) -> String {
read_file_from_root_or_embedded(filename, embedded)
}
}
/// Content file utilities
pub mod content {
use super::*;
/// Get the path to a content file
#[allow(dead_code)]
pub fn content_file<P: AsRef<Path>>(filename: P) -> PathBuf {
resolve_from_root("content").join(filename)
}
/// Read a content file with fallback to embedded content
#[allow(dead_code)]
pub fn read_content_or_embedded(filename: &str, embedded: &str) -> String {
let path = content_file(filename);
std::fs::read_to_string(path).unwrap_or_else(|_| embedded.to_string())
}
}
/// Migration file utilities
pub mod migrations {
use super::*;
/// Get the path to a migration file
#[allow(dead_code)]
pub fn migration_file<P: AsRef<Path>>(filename: P) -> PathBuf {
resolve_from_root("migrations").join(filename)
}
/// Read a migration file
#[allow(dead_code)]
pub fn read_migration_file(filename: &str) -> Result<String, std::io::Error> {
let path = migration_file(filename);
std::fs::read_to_string(path)
}
}
/// Macro for getting paths from project root
#[macro_export]
macro_rules! project_file {
($relative_path:expr) => {
$crate::utils::read_file_from_root($relative_path)
};
}
/// Macro for getting absolute paths from project root
#[macro_export]
macro_rules! project_path {
($relative_path:expr) => {
$crate::utils::resolve_from_root($relative_path)
};
}
/// Initialize the path utilities system
pub fn init() {
init_project_root();
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_project_root_detection() {
let root = get_project_root();
assert!(root.is_absolute());
// Should find the workspace Cargo.toml
let cargo_toml = root.join("Cargo.toml");
assert!(cargo_toml.exists());
}
#[test]
fn test_path_resolution() {
let config_path = resolve_from_root("config.toml");
assert!(config_path.is_absolute());
assert!(config_path.to_string_lossy().ends_with("config.toml"));
}
#[test]
fn test_path_constants() {
let config_dir = paths::config_dir();
assert!(config_dir.is_absolute());
assert!(config_dir.to_string_lossy().ends_with("config"));
let content_dir = paths::content_dir();
assert!(content_dir.is_absolute());
assert!(content_dir.to_string_lossy().ends_with("content"));
}
#[test]
fn test_config_utilities() {
let main_config = config::main_config();
assert!(main_config.is_absolute());
assert!(main_config.to_string_lossy().ends_with("config.toml"));
let dev_config = config::dev_config();
assert!(dev_config.is_absolute());
assert!(dev_config.to_string_lossy().ends_with("config.dev.toml"));
}
#[test]
fn test_exists_from_root() {
// Test with a file that should exist
assert!(exists_from_root("Cargo.toml"));
// Test with a file that shouldn't exist
assert!(!exists_from_root("non_existent_file.txt"));
}
}