cfgmatic-paths 5.0.0

Cross-platform configuration path discovery following XDG and platform conventions
Documentation
//! Configuration discovery helpers for [`PathFinder`](super::PathFinder).

use std::path::{Path, PathBuf};

use crate::core::{
    ConfigCandidate, ConfigDiscovery, ConfigTier, DiscoveryOptions, FilePattern, PathStatus,
    SourceType,
};
use crate::env::StdEnv;

use super::{PathFinder, scan};

impl PathFinder {
    /// Discover configuration with full diagnostics.
    ///
    /// Searches all configuration locations and returns comprehensive
    /// information about what was found, including the preferred path
    /// for creating new configurations.
    ///
    /// # Examples
    ///
    /// ```
    /// use cfgmatic_paths::PathsBuilder;
    ///
    /// let finder = PathsBuilder::new("myapp").build();
    /// let discovery = finder.discover_config();
    ///
    /// println!("Preferred path: {}", discovery.preferred_path.display());
    /// if let Some(found) = &discovery.found_path {
    ///     println!("Found config at: {}", found.display());
    /// }
    ///
    /// for candidate in discovery.candidates {
    ///     println!("  - {:?}: {} ({:?})",
    ///         candidate.tier,
    ///         candidate.path.display(),
    ///         candidate.status
    ///     );
    /// }
    /// ```
    #[must_use]
    pub fn discover_config(&self) -> ConfigDiscovery {
        self.discover_config_with_options(&DiscoveryOptions::default())
    }

    /// Discover configuration with custom options.
    ///
    /// Allows customization of the discovery process including
    /// file patterns, fragment discovery, and legacy path inclusion.
    ///
    /// # Examples
    ///
    /// ```
    /// use cfgmatic_paths::{DiscoveryOptions, FilePattern, PathsBuilder};
    ///
    /// let finder = PathsBuilder::new("myapp").build();
    ///
    /// let options = DiscoveryOptions::new()
    ///     .with_pattern(FilePattern::extensions("config", &["toml", "yaml"]))
    ///     .with_fragments(true)
    ///     .with_fragment_dir("conf.d");
    ///
    /// let discovery = finder.discover_config_with_options(&options);
    /// ```
    #[must_use]
    pub fn discover_config_with_options(&self, options: &DiscoveryOptions) -> ConfigDiscovery {
        let mut candidates = Vec::new();
        let mut fragments = Vec::new();
        let mut found_path: Option<PathBuf> = None;

        let tiers = [
            (self.dir_finder.user_dirs(&StdEnv), ConfigTier::User),
            (self.dir_finder.local_dirs(&StdEnv), ConfigTier::Local),
            (self.dir_finder.system_dirs(&StdEnv), ConfigTier::System),
        ];

        for (dirs, tier) in tiers {
            self.build_candidates_for_tier(
                &dirs,
                tier,
                options,
                &mut candidates,
                &mut fragments,
                &mut found_path,
            );
        }

        candidates.sort_by(|a, b| b.tier.cmp(&a.tier));

        ConfigDiscovery {
            preferred_path: self.preferred_config_path(),
            found_path,
            candidates,
            fragments,
        }
    }

    /// Find all configuration files matching a pattern.
    ///
    /// Searches all configuration directories for files matching the
    /// given pattern and returns them as candidates with status information.
    ///
    /// # Examples
    ///
    /// ```
    /// use cfgmatic_paths::{FilePattern, PathsBuilder};
    ///
    /// let finder = PathsBuilder::new("myapp").build();
    /// let pattern = FilePattern::extensions("config", &["toml", "yaml", "json"]);
    ///
    /// let configs = finder.find_config_files(&pattern);
    /// for config in configs {
    ///     if config.exists() {
    ///         println!("Found: {}", config.path.display());
    ///     }
    /// }
    /// ```
    #[must_use]
    pub fn find_config_files(&self, pattern: &FilePattern) -> Vec<ConfigCandidate> {
        let mut candidates = Vec::new();

        let tiers = [
            (self.dir_finder.user_dirs(&StdEnv), ConfigTier::User),
            (self.dir_finder.local_dirs(&StdEnv), ConfigTier::Local),
            (self.dir_finder.system_dirs(&StdEnv), ConfigTier::System),
        ];

        for (dirs, tier) in tiers {
            self.find_files_in_dirs(&dirs, tier, pattern, &mut candidates);
        }

        candidates.sort_by(|a, b| b.tier.cmp(&a.tier));
        candidates
    }

    /// Find configuration fragments from conf.d-style directories.
    ///
    /// Searches for fragment directories (like `/etc/myapp/conf.d/`) and
    /// returns all matching configuration files within them.
    ///
    /// # Examples
    ///
    /// ```
    /// use cfgmatic_paths::{FilePattern, PathsBuilder};
    ///
    /// let finder = PathsBuilder::new("myapp").build();
    /// let pattern = FilePattern::glob("*.conf");
    ///
    /// let fragments = finder.find_fragments(&pattern, "conf.d");
    /// for frag in &fragments {
    ///     println!("Fragment: {}", frag.display());
    /// }
    /// ```
    #[must_use]
    pub fn find_fragments(&self, pattern: &FilePattern, fragment_dir_name: &str) -> Vec<PathBuf> {
        let mut fragments = Vec::new();

        let dirs_by_tier: [(Vec<PathBuf>, ConfigTier); 3] = [
            (self.dir_finder.user_dirs(&StdEnv), ConfigTier::User),
            (self.dir_finder.local_dirs(&StdEnv), ConfigTier::Local),
            (self.dir_finder.system_dirs(&StdEnv), ConfigTier::System),
        ];

        for (dirs, _tier) in dirs_by_tier {
            for base_dir in dirs {
                let conf_d = base_dir.join(fragment_dir_name);
                if self.fs.is_dir(&conf_d) {
                    for entry in self.fs.read_dir(&conf_d) {
                        if pattern.matches(&entry) && self.fs.is_file(&entry) {
                            fragments.push(entry);
                        }
                    }
                }
            }
        }

        fragments.sort();
        fragments
    }

    /// Get all config directories where a file could be placed.
    ///
    /// Returns all directories in priority order where configuration
    /// files could be located, regardless of whether they exist.
    ///
    /// # Examples
    ///
    /// ```
    /// use cfgmatic_paths::PathsBuilder;
    ///
    /// let finder = PathsBuilder::new("myapp").build();
    /// let dirs = finder.config_directories();
    ///
    /// for dir in dirs {
    ///     println!("Config directory: {}", dir.display());
    /// }
    /// ```
    #[must_use]
    pub fn config_directories(&self) -> Vec<PathBuf> {
        [
            self.dir_finder.user_dirs(&StdEnv),
            self.dir_finder.local_dirs(&StdEnv),
            self.dir_finder.system_dirs(&StdEnv),
        ]
        .into_iter()
        .flatten()
        .collect()
    }

    /// Get the path status of a specific configuration path.
    ///
    /// # Examples
    ///
    /// ```
    /// use cfgmatic_paths::PathsBuilder;
    ///
    /// let finder = PathsBuilder::new("myapp").build();
    /// let path = finder.preferred_config_file("config.toml");
    ///
    /// let status = finder.path_status(&path);
    /// println!("Path status: {:?}", status);
    /// ```
    #[must_use]
    pub fn path_status(&self, path: &Path) -> PathStatus {
        if !self.fs.exists(path) {
            PathStatus::NotFound
        } else if self.fs.is_file(path) {
            PathStatus::File
        } else {
            PathStatus::Directory
        }
    }

    /// Build candidates for a specific tier.
    fn build_candidates_for_tier(
        &self,
        dirs: &[PathBuf],
        tier: ConfigTier,
        options: &DiscoveryOptions,
        candidates: &mut Vec<ConfigCandidate>,
        fragments: &mut Vec<PathBuf>,
        found_path: &mut Option<PathBuf>,
    ) {
        for dir in dirs {
            let status = if self.fs.exists(dir) {
                if self.fs.is_dir(dir) {
                    PathStatus::Directory
                } else {
                    PathStatus::File
                }
            } else {
                PathStatus::NotFound
            };

            let source_type = if candidates.is_empty() && found_path.is_none() {
                SourceType::MainFile
            } else {
                SourceType::Legacy
            };

            candidates.push(ConfigCandidate::new(dir.clone(), status, tier, source_type));

            if found_path.is_none() && status.exists() {
                *found_path = Some(dir.clone());
            }

            if status == PathStatus::Directory {
                self.check_dir_for_configs(dir, tier, options, candidates, found_path);
            }

            if options.include_fragments {
                let conf_d = dir.join(&options.fragment_dir);
                if self.fs.is_dir(&conf_d) {
                    candidates.push(ConfigCandidate::new(
                        conf_d.clone(),
                        PathStatus::Directory,
                        tier,
                        SourceType::FragmentsDir,
                    ));

                    for entry in self.fs.read_dir(&conf_d) {
                        if self.fs.is_file(&entry)
                            && (fragments.is_empty() || !fragments.contains(&entry))
                        {
                            fragments.push(entry);
                        }
                    }
                }
            }
        }
    }

    /// Check a directory for configuration files.
    fn check_dir_for_configs(
        &self,
        dir: &Path,
        tier: ConfigTier,
        options: &DiscoveryOptions,
        candidates: &mut Vec<ConfigCandidate>,
        found_path: &mut Option<PathBuf>,
    ) {
        if let Some(filenames) = options.pattern.concrete_filenames() {
            for filename in filenames {
                let file_path = dir.join(&filename);
                let status = self.path_status(&file_path);

                candidates.push(ConfigCandidate::new(
                    file_path.clone(),
                    status,
                    tier,
                    SourceType::MainFile,
                ));

                if found_path.is_none() && status.exists() {
                    *found_path = Some(file_path);
                }
            }
        }
    }

    /// Find files matching a pattern in a list of directories.
    pub(super) fn find_files_in_dirs(
        &self,
        dirs: &[PathBuf],
        tier: ConfigTier,
        pattern: &FilePattern,
        candidates: &mut Vec<ConfigCandidate>,
    ) {
        candidates.extend(scan::collect_matching_candidates(
            self.fs.as_ref(),
            dirs,
            tier,
            pattern,
            SourceType::MainFile,
            |path| self.path_status(path),
        ));
    }
}