//! 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 = 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>(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>(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>(relative_path: P) -> Result { 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>(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>(relative_path: P) -> Result { 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>( 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>(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>(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>(filename: P) -> PathBuf { resolve_from_root("migrations").join(filename) } /// Read a migration file #[allow(dead_code)] pub fn read_migration_file(filename: &str) -> Result { 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")); } }