cfgmatic-paths 0.1.4

Cross-platform configuration path discovery following XDG and platform conventions
Documentation
//! Builder for creating path finders and directory scanners.

use crate::core::AppType;
use crate::env::StdEnv;
use crate::platform::{DirectoryFinder, DirectoryInfo};
use crate::{ConfigTier, Fs, StdFs};
use std::path::PathBuf;
use std::sync::Arc;

/// Builder for creating platform-specific path finders.
///
/// # Examples
///
/// ```
/// use cfgmatic_paths::{PathsBuilder, AppType};
///
/// // CLI application with XDG directories
/// let finder = PathsBuilder::new("myapp")
///     .app_type(AppType::Cli)
///     .build();
///
/// let user_dirs = finder.user_dirs();
/// ```
#[derive(Debug, Clone)]
pub struct PathsBuilder {
    /// Application name used in paths.
    app_name: String,
    /// Type of application (CLI, GUI, Service).
    app_type: AppType,
    /// Windows-specific company name.
    #[cfg(windows)]
    company_name: Option<String>,
    /// macOS GUI-specific bundle ID.
    #[cfg(all(target_os = "macos", feature = "macos-gui"))]
    bundle_id: Option<String>,
    /// Whether to include legacy `~/.apprc` fallback.
    legacy_rc: bool,
}

impl PathsBuilder {
    /// Create a new builder.
    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,
        }
    }

    /// Set the application type.
    #[must_use]
    pub const fn app_type(mut self, app_type: AppType) -> Self {
        self.app_type = app_type;
        self
    }

    /// Enable/disable legacy `~/.apprc` fallback (Unix only).
    #[must_use]
    pub const fn legacy_rc(mut self, enabled: bool) -> Self {
        self.legacy_rc = enabled;
        self
    }

    /// Windows: set the company name.
    #[cfg(windows)]
    pub fn company_name(mut self, name: impl Into<String>) -> Self {
        self.company_name = Some(name.into());
        self
    }

    /// macOS GUI: set the bundle ID.
    #[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
    }

    /// Build the platform-specific directory finder.
    #[must_use]
    pub fn build(self) -> PathFinder {
        let dir_finder = self.build_directory_finder();
        PathFinder {
            dir_finder,
            fs: Arc::new(StdFs),
        }
    }

    /// Build the directory finder (internal).
    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 {
                    // CLI on macOS uses XDG
                    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;
                // Unix/Linux or macOS CLI
                Box::new(UnixDirectoryFinder::new(self.app_name).legacy_rc(self.legacy_rc))
            }
        }
    }
}

/// Path finder for discovering configuration directories.
///
/// Provides methods to find configuration directories following
/// platform conventions (XDG on Unix, Known Folders on Windows,
/// Application Support on macOS).
pub struct PathFinder {
    /// Platform-specific directory finder.
    dir_finder: Box<dyn DirectoryFinder>,
    /// Filesystem abstraction.
    fs: Arc<dyn Fs>,
}

impl PathFinder {
    /// Get all user directories.
    #[must_use]
    pub fn user_dirs(&self) -> Vec<PathBuf> {
        self.dir_finder.user_dirs(&StdEnv)
    }

    /// Get all local directories.
    #[must_use]
    pub fn local_dirs(&self) -> Vec<PathBuf> {
        self.dir_finder.local_dirs(&StdEnv)
    }

    /// Get all system directories.
    #[must_use]
    pub fn system_dirs(&self) -> Vec<PathBuf> {
        self.dir_finder.system_dirs(&StdEnv)
    }

    /// Get all directories with their tiers.
    #[must_use]
    pub fn all_dirs(&self) -> Vec<DirectoryInfo> {
        let mut dirs = Vec::new();
        dirs.extend(self.dirs_with_tier(self.user_dirs(), ConfigTier::User));
        dirs.extend(self.dirs_with_tier(self.local_dirs(), ConfigTier::Local));
        dirs.extend(self.dirs_with_tier(self.system_dirs(), ConfigTier::System));
        dirs
    }

    /// Get the primary user config directory (first user directory).
    #[must_use]
    pub fn user_config_dir(&self) -> Option<PathBuf> {
        self.user_dirs().into_iter().next()
    }

    /// Ensure the user config directory exists, creating it if necessary.
    ///
    /// Returns the path to the directory, or an error if creation fails.
    ///
    /// # Errors
    ///
    /// Returns an error if no user config directory is found or if creation fails.
    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)
    }

    /// Convert paths to directory info with tier.
    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::*;

    #[test]
    fn test_builder_default() {
        let builder = PathsBuilder::new("myapp");
        assert_eq!(builder.app_name, "myapp");
        assert_eq!(builder.app_type, AppType::Cli);
    }

    #[test]
    fn test_builder_app_type() {
        let builder = PathsBuilder::new("myapp").app_type(AppType::Gui);
        assert_eq!(builder.app_type, AppType::Gui);
    }

    #[test]
    fn test_finder_creation() {
        let finder = PathsBuilder::new("myapp").build();
        let dirs = finder.user_dirs();
        assert!(!dirs.is_empty());
    }
}