mod discovery;
mod rules;
mod scan;
use crate::core::{AppType, ConfigTier};
use crate::env::StdEnv;
use crate::platform::{DirectoryFinder, DirectoryInfo};
use crate::{Fs, StdFs};
use std::path::{Path, PathBuf};
use std::sync::Arc;
#[derive(Debug, Clone)]
pub struct PathsBuilder {
app_name: String,
app_type: AppType,
#[cfg(windows)]
company_name: Option<String>,
#[cfg(all(target_os = "macos", feature = "macos-gui"))]
bundle_id: Option<String>,
legacy_rc: bool,
}
impl PathsBuilder {
pub fn new(app_name: impl Into<String>) -> Self {
Self {
app_name: app_name.into(),
app_type: AppType::default(),
#[cfg(windows)]
company_name: None,
#[cfg(all(target_os = "macos", feature = "macos-gui"))]
bundle_id: None,
legacy_rc: true,
}
}
#[must_use]
pub const fn app_type(mut self, app_type: AppType) -> Self {
self.app_type = app_type;
self
}
#[must_use]
pub const fn legacy_rc(mut self, enabled: bool) -> Self {
self.legacy_rc = enabled;
self
}
#[cfg(windows)]
pub fn company_name(mut self, name: impl Into<String>) -> Self {
self.company_name = Some(name.into());
self
}
#[cfg(all(target_os = "macos", feature = "macos-gui"))]
#[must_use]
pub fn bundle_id(mut self, id: impl Into<String>) -> Self {
self.bundle_id = Some(id.into());
self
}
#[must_use]
pub fn build(self) -> PathFinder {
let preferred_fallback = PathBuf::from(".config").join(&self.app_name);
let dir_finder = self.build_directory_finder();
PathFinder {
dir_finder,
fs: Arc::new(StdFs),
preferred_fallback,
}
}
fn build_directory_finder(self) -> Box<dyn DirectoryFinder> {
cfg_if::cfg_if! {
if #[cfg(all(target_os = "macos", feature = "macos-gui"))] {
use crate::platform::MacOSGuiDirectoryFinder;
use crate::platform::UnixDirectoryFinder;
if self.app_type == AppType::Gui {
let bundle_id = self.bundle_id.unwrap_or_else(|| {
format!("com.example.{}", self.app_name)
});
Box::new(MacOSGuiDirectoryFinder::new(bundle_id))
} else {
Box::new(UnixDirectoryFinder::new(self.app_name).legacy_rc(self.legacy_rc))
}
} else if #[cfg(windows)] {
use crate::platform::WindowsDirectoryFinder;
let company_name = self.company_name.unwrap_or_else(|| {
self.app_name.clone()
});
Box::new(WindowsDirectoryFinder::new(self.app_name, company_name))
} else {
use crate::platform::UnixDirectoryFinder;
Box::new(UnixDirectoryFinder::new(self.app_name).legacy_rc(self.legacy_rc))
}
}
}
}
pub struct PathFinder {
dir_finder: Box<dyn DirectoryFinder>,
fs: Arc<dyn Fs>,
preferred_fallback: PathBuf,
}
impl PathFinder {
#[must_use]
pub fn user_dirs(&self) -> Vec<PathBuf> {
self.dir_finder.user_dirs(&StdEnv)
}
#[must_use]
pub fn local_dirs(&self) -> Vec<PathBuf> {
self.dir_finder.local_dirs(&StdEnv)
}
#[must_use]
pub fn system_dirs(&self) -> Vec<PathBuf> {
self.dir_finder.system_dirs(&StdEnv)
}
#[must_use]
pub fn all_dirs(&self) -> Vec<DirectoryInfo> {
[
self.dirs_with_tier(self.user_dirs(), ConfigTier::User),
self.dirs_with_tier(self.local_dirs(), ConfigTier::Local),
self.dirs_with_tier(self.system_dirs(), ConfigTier::System),
]
.into_iter()
.flatten()
.collect()
}
#[must_use]
pub fn user_config_dir(&self) -> Option<PathBuf> {
self.user_dirs().into_iter().next()
}
pub fn ensure_user_config_dir(&self) -> std::io::Result<PathBuf> {
let path = self.user_config_dir().ok_or_else(|| {
std::io::Error::new(
std::io::ErrorKind::NotFound,
"No user config directory found",
)
})?;
self.fs.create_dir_all(&path)?;
Ok(path)
}
#[must_use]
pub fn preferred_config_path(&self) -> PathBuf {
self.user_dirs()
.into_iter()
.next()
.unwrap_or_else(|| self.preferred_fallback.clone())
}
#[must_use]
pub fn preferred_config_file(&self, filename: impl AsRef<Path>) -> PathBuf {
self.preferred_config_path().join(filename)
}
fn dirs_with_tier(&self, paths: Vec<PathBuf>, tier: ConfigTier) -> Vec<DirectoryInfo> {
paths
.into_iter()
.map(|path| DirectoryInfo {
exists: self.fs.is_dir(&path),
path,
tier,
})
.collect()
}
}
impl std::fmt::Debug for PathFinder {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("PathFinder")
.field("user_dirs", &self.user_dirs())
.field("local_dirs", &self.local_dirs())
.field("system_dirs", &self.system_dirs())
.finish()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::FilePattern;
use std::collections::{BTreeMap, BTreeSet};
use std::iter;
use std::path::Path;
struct EmptyDirectoryFinder;
impl DirectoryFinder for EmptyDirectoryFinder {
fn user_dirs(&self, _env: &dyn crate::Env) -> Vec<PathBuf> {
Vec::new()
}
fn local_dirs(&self, _env: &dyn crate::Env) -> Vec<PathBuf> {
Vec::new()
}
fn system_dirs(&self, _env: &dyn crate::Env) -> Vec<PathBuf> {
Vec::new()
}
}
#[derive(Default)]
struct MemoryFs {
files: BTreeSet<PathBuf>,
dirs: BTreeMap<PathBuf, Vec<PathBuf>>,
}
impl Fs for MemoryFs {
fn exists(&self, path: &Path) -> bool {
self.files.contains(path) || self.dirs.contains_key(path)
}
fn is_file(&self, path: &Path) -> bool {
self.files.contains(path)
}
fn is_dir(&self, path: &Path) -> bool {
self.dirs.contains_key(path)
}
fn create_dir_all(&self, _path: &Path) -> std::io::Result<()> {
Ok(())
}
fn read_dir(&self, path: &Path) -> Vec<PathBuf> {
self.dirs.get(path).cloned().unwrap_or_default()
}
}
#[test]
fn test_preferred_config_path_uses_app_specific_fallback() {
let finder = PathFinder {
dir_finder: Box::new(EmptyDirectoryFinder),
fs: Arc::new(MemoryFs::default()),
preferred_fallback: PathBuf::from(".config").join("custom-app"),
};
assert_eq!(
finder.preferred_config_path(),
PathBuf::from(".config").join("custom-app")
);
}
#[test]
fn test_find_files_in_dirs_uses_shared_scanner() {
let config_dir = PathBuf::from("/config");
let config_file = config_dir.join("app.toml");
let fs = MemoryFs {
files: iter::once(config_file.clone()).collect(),
dirs: iter::once((config_dir.clone(), vec![config_file.clone()])).collect(),
};
let finder = PathFinder {
dir_finder: Box::new(EmptyDirectoryFinder),
fs: Arc::new(fs),
preferred_fallback: PathBuf::from(".config").join("app"),
};
let mut candidates = Vec::new();
finder.find_files_in_dirs(
&[config_dir],
ConfigTier::User,
&FilePattern::glob("*.toml"),
&mut candidates,
);
assert_eq!(candidates.len(), 1);
assert!(
candidates
.iter()
.all(|candidate| candidate.path == config_file)
);
}
}