strut_config/scanner/
dir.rs

1use crate::ConfigEntry;
2use std::path::PathBuf;
3use strut_core::AppProfile;
4
5/// Represents a single config-related directory.
6///
7/// Unlike with [`ConfigFile`]s, the path validity is not checked on
8/// instantiation. But given that the filesystem is completely external to the
9/// application, the existence or validity of any given path cannot be assumed
10/// anyway.
11#[derive(Debug, Ord, PartialOrd, Eq, PartialEq)]
12pub enum ConfigDir {
13    /// A directory that is not associated with an [`AppProfile`].
14    Generic(PathBuf),
15
16    /// A directory that is associated with an [`AppProfile`] of the given name.
17    Specific {
18        /// The path to the directory.
19        path: PathBuf,
20
21        /// The associated profile name.
22        profile: String,
23    },
24}
25
26impl ConfigDir {
27    /// Creates a [`ConfigDir`] from the given [`PathBuf`], if the path points
28    /// to a workable directory.
29    pub fn at(path: PathBuf) -> Self {
30        Self::make_with_profile(path, None)
31    }
32
33    /// Creates a [`ConfigDir`] from the given [`PathBuf`], if the path points
34    /// to a workable directory, optionally applying the given known profile
35    /// name.
36    pub fn make_with_profile(path: PathBuf, known_profile: Option<&str>) -> Self {
37        // If a profile is known, assign it
38        if let Some(known_profile) = known_profile {
39            return Self::Specific {
40                path,
41                profile: known_profile.to_string(),
42            };
43        }
44
45        // Otherwise, just make a generic directory
46        Self::Generic(path)
47    }
48
49    /// Creates a [`ConfigDir`] from the given [`PathBuf`], attempting to
50    /// capture its name as a profile name. If the name cannot be captured
51    /// (e.g., if the path doesn’t exist), the [generic](ConfigDir::Generic)
52    /// variant is returned.
53    pub fn make_capturing_profile(path: PathBuf) -> Self {
54        // Read file name
55        match path.file_name().and_then(std::ffi::OsStr::to_str) {
56            Some(name) => {
57                let profile = name.to_string();
58                Self::Specific { path, profile }
59            }
60            None => Self::Generic(path),
61        }
62    }
63}
64
65impl ConfigDir {
66    /// Reports the name of this directory, if it is readable.
67    pub fn name(&self) -> Option<&str> {
68        self.path().file_name().and_then(std::ffi::OsStr::to_str)
69    }
70
71    /// Reports whether this [`ConfigDir`] is applicable regardless of the
72    /// [active](AppProfile::active) [`AppProfile`].
73    pub fn is_generic(&self) -> bool {
74        match *self {
75            Self::Generic(_) => true,
76            Self::Specific { .. } => false,
77        }
78    }
79
80    /// Reports whether this [`ConfigDir`] is applicable only to a particular
81    /// [`AppProfile`].
82    pub fn is_specific(&self) -> bool {
83        !self.is_generic()
84    }
85
86    /// Returns a reference to the internally held [`PathBuf`].
87    pub fn path(&self) -> &PathBuf {
88        match *self {
89            Self::Generic(ref path) => path,
90            Self::Specific { ref path, .. } => path,
91        }
92    }
93
94    /// Returns a reference to the internally held profile name (if this
95    /// variant is [specific](ConfigDir::is_specific)).
96    pub fn profile(&self) -> Option<&str> {
97        match *self {
98            Self::Generic(_) => None,
99            Self::Specific { ref profile, .. } => Some(profile),
100        }
101    }
102
103    /// Reports whether this [`ConfigDir`] [applies](ConfigDir::applies_to) to
104    /// the [active](AppProfile::active) [`AppProfile`].
105    pub fn applies_to_active_profile(&self) -> bool {
106        self.applies_to(AppProfile::active())
107    }
108
109    /// Reports whether this [`ConfigDir`] applies to the given [`AppProfile`].
110    ///
111    /// A generic config file (without a profile name in its file name) applies
112    /// to any profile by default. A specific config file (with a profile name
113    /// in its file name) applies to the given profile if the profile name
114    /// matches.
115    pub fn applies_to(&self, profile: impl AsRef<AppProfile>) -> bool {
116        let given_profile = profile.as_ref();
117
118        match *self {
119            Self::Generic(_) => true,
120            Self::Specific { ref profile, .. } => given_profile.is(profile),
121        }
122    }
123
124    /// Expands this directory into a vector of nested [`ConfigEntry`]s.
125    pub fn expand(&self, profile: Option<&str>) -> Vec<ConfigEntry> {
126        std::fs::read_dir(self.path())
127            .into_iter()
128            .flat_map(|read_dir| {
129                read_dir
130                    .filter_map(Result::ok)
131                    .map(|entry| entry.path())
132                    .filter_map(|path| ConfigEntry::try_from_with_profile(path, profile))
133            })
134            .collect::<Vec<_>>()
135    }
136}
137
138impl From<ConfigDir> for PathBuf {
139    fn from(file: ConfigDir) -> Self {
140        match file {
141            ConfigDir::Generic(path) => path,
142            ConfigDir::Specific { path, .. } => path,
143        }
144    }
145}