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}