strut_config/
scanner.rs

1use crate::{ConfigEntry, ConfigFile};
2use std::env;
3use std::path::PathBuf;
4use strut_core::Pivot;
5
6pub mod dir;
7pub mod entry;
8pub mod file;
9
10/// A small facade for finding the [`ConfigFile`]s relevant for the current
11/// binary crate.
12pub struct Scanner;
13
14impl Scanner {
15    /// Discovers and returns all [`ConfigFile`]s relevant to the current binary
16    /// crate and the [active](AppProfile::active) [`AppProfile`].
17    ///
18    /// The returned vector is **ordered for precedence**: later files in the
19    /// list should override earlier files when keys overlap.
20    ///
21    /// ## Search Location
22    ///
23    /// Scans for configuration files within the resolved
24    /// [configuration directory](Self::resolve_config_dir).
25    ///
26    /// ## Supported Formats
27    ///
28    /// The following file formats are recognized:
29    /// - TOML (`.toml`)
30    /// - YAML (`.yml`, `.yaml`)
31    ///
32    /// ## File Types
33    ///
34    /// - **Generic config files**: Apply to all profiles.
35    ///   - Pattern: `config/{any_name}.{ext}`
36    /// - **Profile-specific config files**: Apply only if the file’s profile
37    ///   matches the [active](AppProfile::active) [`AppProfile`].
38    ///   - Patterns:
39    ///     - `config/{any_name}.{profile}.{ext}`
40    ///     - `config/{profile}/{any_name}.{ext}`
41    ///
42    /// ## Ordering
43    ///
44    /// - Generic files always precede profile-specific files.
45    /// - Within each group (generic and profile-specific), files are ordered
46    ///   lexicographically by full path.
47    ///
48    /// ## Notes
49    ///
50    /// - File and directory names are matched case-insensitively.
51    ///
52    /// ## Returns
53    ///
54    /// An ordered `Vec<ConfigFile>` containing all discovered configuration
55    /// files.
56    pub fn find_config_files(dir_name: Option<&str>) -> Vec<ConfigFile> {
57        // Resolve the config directory
58        let config_dir = Self::resolve_config_dir(dir_name);
59
60        // Resolve the config files
61        let mut config_files = ConfigEntry::dir(config_dir) // start with config dir
62            .cd() // dive one level in
63            .flat_map(ConfigEntry::cd_capturing_profile) // dive another level in, capturing profile name from directory name
64            .filter(ConfigEntry::applies_to_active_profile) // keep everything associated with active profile
65            .filter_map(ConfigEntry::to_config_file) // keep only config files (discard any further nested directories)
66            .collect::<Vec<_>>(); // collect into a vector
67
68        // Sort logically in place
69        config_files.sort();
70
71        config_files
72    }
73
74    /// Resolves the application’s **configuration directory**: where Strut
75    /// looks for configuration files.
76    ///
77    /// Dynamically determines the path at runtime.
78    ///
79    /// Resolution order:
80    /// 1. If the `APP_CONFIG_DIR` environment variable is set, its value is
81    ///    used.
82    /// 2. Otherwise, if a non-empty `path` argument is provided, it is used.
83    /// 3. Otherwise, defaults to a directory named `"config"`.
84    ///
85    /// If the resolved path is relative, it is interpreted relative to the
86    /// [pivot directory](Self::resolve_pivot_dir). Returns an absolute
87    /// [`PathBuf`] of the configuration directory.
88    fn resolve_config_dir(path: Option<&str>) -> PathBuf {
89        let input_path = env::var("APP_CONFIG_DIR") // environment takes highest priority
90            .map(PathBuf::from)
91            .ok()
92            .or_else(|| {
93                path.map(str::trim) // if no environment, then argument
94                    .filter(|s| !s.is_empty())
95                    .map(PathBuf::from)
96            })
97            .unwrap_or(PathBuf::from("config")); // if no argument, then global default
98
99        if input_path.is_absolute() {
100            input_path
101        } else {
102            Pivot::resolve().join(input_path)
103        }
104    }
105}