use crate::errors::{IoOperationKind, StoreError};
use std::path::PathBuf;
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub enum PathStrategy {
#[default]
System,
Xdg,
CustomBase(PathBuf),
}
#[derive(Debug, Clone)]
pub struct AppPaths {
app_name: String,
config_strategy: PathStrategy,
data_strategy: PathStrategy,
}
impl AppPaths {
pub fn new(app_name: impl Into<String>) -> Self {
Self {
app_name: app_name.into(),
config_strategy: PathStrategy::default(),
data_strategy: PathStrategy::default(),
}
}
pub fn config_strategy(mut self, strategy: PathStrategy) -> Self {
self.config_strategy = strategy;
self
}
pub fn data_strategy(mut self, strategy: PathStrategy) -> Self {
self.data_strategy = strategy;
self
}
pub fn config_dir(&self) -> Result<PathBuf, StoreError> {
let dir = self.resolve_config_dir()?;
self.ensure_dir_exists(&dir)?;
Ok(dir)
}
pub fn data_dir(&self) -> Result<PathBuf, StoreError> {
let dir = self.resolve_data_dir()?;
self.ensure_dir_exists(&dir)?;
Ok(dir)
}
pub fn config_file(&self, filename: &str) -> Result<PathBuf, StoreError> {
Ok(self.config_dir()?.join(filename))
}
pub fn data_file(&self, filename: &str) -> Result<PathBuf, StoreError> {
Ok(self.data_dir()?.join(filename))
}
fn resolve_config_dir(&self) -> Result<PathBuf, StoreError> {
match &self.config_strategy {
PathStrategy::System => {
let base = dirs::config_dir().ok_or(StoreError::HomeDirNotFound)?;
Ok(base.join(&self.app_name))
}
PathStrategy::Xdg => {
let home = dirs::home_dir().ok_or(StoreError::HomeDirNotFound)?;
Ok(home.join(".config").join(&self.app_name))
}
PathStrategy::CustomBase(base) => Ok(base.join(&self.app_name)),
}
}
fn resolve_data_dir(&self) -> Result<PathBuf, StoreError> {
match &self.data_strategy {
PathStrategy::System => {
let base = dirs::data_dir().ok_or(StoreError::HomeDirNotFound)?;
Ok(base.join(&self.app_name))
}
PathStrategy::Xdg => {
let home = dirs::home_dir().ok_or(StoreError::HomeDirNotFound)?;
Ok(home.join(".local/share").join(&self.app_name))
}
PathStrategy::CustomBase(base) => Ok(base.join("data").join(&self.app_name)),
}
}
fn ensure_dir_exists(&self, path: &PathBuf) -> Result<(), StoreError> {
if !path.exists() {
std::fs::create_dir_all(path).map_err(|e| StoreError::IoError {
operation: IoOperationKind::CreateDir,
path: path.display().to_string(),
context: None,
error: e.to_string(),
})?;
}
Ok(())
}
}
#[derive(Debug, Clone)]
pub struct PrefPath {
app_name: String,
}
impl PrefPath {
pub fn new(app_name: impl Into<String>) -> Self {
Self {
app_name: app_name.into(),
}
}
pub fn pref_dir(&self) -> Result<PathBuf, StoreError> {
let dir = self.resolve_pref_dir()?;
self.ensure_dir_exists(&dir)?;
Ok(dir)
}
pub fn pref_file(&self, filename: &str) -> Result<PathBuf, StoreError> {
Ok(self.pref_dir()?.join(filename))
}
fn resolve_pref_dir(&self) -> Result<PathBuf, StoreError> {
#[cfg(target_os = "macos")]
{
let home = dirs::home_dir().ok_or(StoreError::HomeDirNotFound)?;
Ok(home.join("Library/Preferences").join(&self.app_name))
}
#[cfg(not(target_os = "macos"))]
{
let base = dirs::config_dir().ok_or(StoreError::HomeDirNotFound)?;
Ok(base.join(&self.app_name))
}
}
fn ensure_dir_exists(&self, path: &PathBuf) -> Result<(), StoreError> {
if !path.exists() {
std::fs::create_dir_all(path).map_err(|e| StoreError::IoError {
operation: IoOperationKind::CreateDir,
path: path.display().to_string(),
context: None,
error: e.to_string(),
})?;
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;
#[test]
fn test_path_strategy_default() {
assert_eq!(PathStrategy::default(), PathStrategy::System);
}
#[test]
fn test_app_paths_new() {
let paths = AppPaths::new("testapp");
assert_eq!(paths.app_name, "testapp");
assert_eq!(paths.config_strategy, PathStrategy::System);
assert_eq!(paths.data_strategy, PathStrategy::System);
}
#[test]
fn test_app_paths_builder() {
let paths = AppPaths::new("testapp")
.config_strategy(PathStrategy::Xdg)
.data_strategy(PathStrategy::Xdg);
assert_eq!(paths.config_strategy, PathStrategy::Xdg);
assert_eq!(paths.data_strategy, PathStrategy::Xdg);
}
#[test]
fn test_system_strategy_config_dir() {
let paths = AppPaths::new("testapp").config_strategy(PathStrategy::System);
let config_dir = paths.resolve_config_dir().unwrap();
assert!(config_dir.ends_with("testapp"));
#[cfg(unix)]
{
let home = dirs::home_dir().unwrap();
assert!(
config_dir.starts_with(home.join("Library/Application Support"))
|| config_dir.starts_with(home.join(".config"))
);
}
}
#[test]
fn test_xdg_strategy_config_dir() {
let paths = AppPaths::new("testapp").config_strategy(PathStrategy::Xdg);
let config_dir = paths.resolve_config_dir().unwrap();
let home = dirs::home_dir().unwrap();
assert_eq!(config_dir, home.join(".config/testapp"));
}
#[test]
fn test_xdg_strategy_data_dir() {
let paths = AppPaths::new("testapp").data_strategy(PathStrategy::Xdg);
let data_dir = paths.resolve_data_dir().unwrap();
let home = dirs::home_dir().unwrap();
assert_eq!(data_dir, home.join(".local/share/testapp"));
}
#[test]
fn test_custom_base_strategy() {
let temp_dir = TempDir::new().unwrap();
let custom_base = temp_dir.path().to_path_buf();
let paths = AppPaths::new("testapp")
.config_strategy(PathStrategy::CustomBase(custom_base.clone()))
.data_strategy(PathStrategy::CustomBase(custom_base.clone()));
let config_dir = paths.resolve_config_dir().unwrap();
let data_dir = paths.resolve_data_dir().unwrap();
assert_eq!(config_dir, custom_base.join("testapp"));
assert_eq!(data_dir, custom_base.join("data/testapp"));
}
#[test]
fn test_config_file() {
let temp_dir = TempDir::new().unwrap();
let custom_base = temp_dir.path().to_path_buf();
let paths =
AppPaths::new("testapp").config_strategy(PathStrategy::CustomBase(custom_base.clone()));
let config_file = paths.config_file("config.toml").unwrap();
assert_eq!(config_file, custom_base.join("testapp/config.toml"));
assert!(custom_base.join("testapp").exists());
}
#[test]
fn test_data_file() {
let temp_dir = TempDir::new().unwrap();
let custom_base = temp_dir.path().to_path_buf();
let paths =
AppPaths::new("testapp").data_strategy(PathStrategy::CustomBase(custom_base.clone()));
let data_file = paths.data_file("cache.db").unwrap();
assert_eq!(data_file, custom_base.join("data/testapp/cache.db"));
assert!(custom_base.join("data/testapp").exists());
}
#[test]
fn test_ensure_dir_exists() {
let temp_dir = TempDir::new().unwrap();
let test_path = temp_dir.path().join("nested/test/path");
let paths = AppPaths::new("testapp");
paths.ensure_dir_exists(&test_path).unwrap();
assert!(test_path.exists());
assert!(test_path.is_dir());
}
#[test]
fn test_multiple_calls_idempotent() {
let temp_dir = TempDir::new().unwrap();
let custom_base = temp_dir.path().to_path_buf();
let paths =
AppPaths::new("testapp").config_strategy(PathStrategy::CustomBase(custom_base.clone()));
let dir1 = paths.config_dir().unwrap();
let dir2 = paths.config_dir().unwrap();
let dir3 = paths.config_dir().unwrap();
assert_eq!(dir1, dir2);
assert_eq!(dir2, dir3);
}
#[test]
fn test_pref_path_new() {
let pref = PrefPath::new("com.example.testapp");
assert_eq!(pref.app_name, "com.example.testapp");
}
#[test]
fn test_pref_path_resolve_dir() {
let pref = PrefPath::new("com.example.testapp");
let pref_dir = pref.resolve_pref_dir().unwrap();
assert!(pref_dir.ends_with("com.example.testapp"));
#[cfg(target_os = "macos")]
{
let home = dirs::home_dir().unwrap();
assert_eq!(
pref_dir,
home.join("Library/Preferences/com.example.testapp")
);
}
#[cfg(all(unix, not(target_os = "macos")))]
{
let home = dirs::home_dir().unwrap();
assert_eq!(pref_dir, home.join(".config/com.example.testapp"));
}
#[cfg(target_os = "windows")]
{
assert!(pref_dir.to_string_lossy().contains("AppData"));
}
}
#[test]
fn test_pref_file() {
let pref = PrefPath::new("com.example.testapp");
let pref_file = pref.pref_file("settings.plist").unwrap();
assert!(pref_file.ends_with("settings.plist"));
assert!(pref_file.to_string_lossy().contains("com.example.testapp"));
#[cfg(target_os = "macos")]
{
let home = dirs::home_dir().unwrap();
assert_eq!(
pref_file,
home.join("Library/Preferences/com.example.testapp/settings.plist")
);
}
}
#[test]
fn test_pref_dir_creates_directory() {
let pref = PrefPath::new("test_version_migrate_pref");
let pref_dir = pref.pref_dir().unwrap();
if pref_dir.exists() {
let _ = std::fs::remove_dir_all(&pref_dir);
}
}
#[test]
fn test_pref_path_multiple_calls_idempotent() {
let pref = PrefPath::new("test_version_migrate_pref2");
let dir1 = pref.pref_dir().unwrap();
let dir2 = pref.pref_dir().unwrap();
let dir3 = pref.pref_dir().unwrap();
assert_eq!(dir1, dir2);
assert_eq!(dir2, dir3);
if dir1.exists() {
let _ = std::fs::remove_dir_all(&dir1);
}
}
}