project_dirs_builder/
lib.rs

1use project_dirs::{Directory, ProjectDirs};
2use serde::{Deserialize, Serialize};
3use std::{collections::HashMap, path::PathBuf};
4
5fn default_true() -> bool {
6    true
7}
8
9#[derive(Debug, Clone, Serialize, Deserialize, Default)]
10#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
11#[serde(rename_all = "kebab-case")]
12pub enum Fhs {
13    Local,
14
15    #[default]
16    Shared,
17}
18
19#[derive(Debug, Clone, Serialize, Deserialize)]
20#[serde(rename_all = "kebab-case")]
21#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
22pub enum Unix {
23    Pwd,
24    Home,
25    Binary,
26
27    #[serde(untagged)]
28    Custom {
29        path: PathBuf,
30        #[serde(default)]
31        prefix: Option<String>,
32    },
33}
34
35#[derive(Debug, Clone, Serialize, Deserialize)]
36#[serde(rename_all = "kebab-case")]
37#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
38pub enum Windows {
39    /// User installation for windows
40    Standard,
41    /// User (local only) installation for windows
42    Local,
43    /// User (roamed/shared) installation for windows
44    Shared,
45    /// Global installation for windows
46    System,
47}
48
49#[derive(Debug, Clone, Serialize, Deserialize)]
50#[serde(rename_all = "kebab-case")]
51#[serde(tag = "strategy", content = "strategy_config")]
52#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
53pub enum Strategy {
54    /// Get local directories based on the current system
55    CurrentLocal,
56    /// Get user directories based on the current system
57    CurrentUser,
58    /// Get system directories based on the current system
59    CurrentSystem,
60    /// Get directories using FHS standard
61    Fhs(#[serde(default)] Option<Fhs>),
62    /// Get directories using XDG standard
63    Xdg,
64    /// Get directories using unix-style directory
65    Unix(Unix),
66    /// Get directories for windows
67    Windows(Windows),
68}
69
70#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)]
71#[serde(rename_all = "kebab-case")]
72#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
73pub enum Filter {
74    /// Return only directories that exist on the fs
75    FsPresent,
76
77    /// Return only directories that don't exist on the fs
78    FsAbsent,
79
80    /// Return only directories that exist on the fs and are NOT directories
81    FsNotDir,
82
83    /// Return only access denied directories
84    FsDenied,
85
86    /// Return everything that is not a valid dir. Negation of the FsPresent
87    FsNonValidDir,
88}
89
90#[derive(Clone, Debug, Serialize, Deserialize)]
91#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
92pub struct SpecEntry {
93    #[serde(flatten)]
94    pub strategy: Strategy,
95    #[serde(default)]
96    pub directories: Vec<Directory>,
97    pub filter: Option<Filter>,
98
99    #[serde(default)]
100    pub mountpoint: Option<PathBuf>,
101}
102
103#[derive(Clone, Debug, Default, Serialize, Deserialize)]
104#[serde(rename_all = "kebab-case")]
105#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
106pub enum Spec {
107    /// Use system defaults in the [`Scoped`] format
108    #[default]
109    SystemDefault,
110
111    /// Define own spec, with custom filters, mountpoints etc.
112    #[serde(untagged)]
113    Custom(HashMap<String, SpecEntry>),
114}
115
116#[derive(Clone, Debug, Serialize, Deserialize)]
117#[serde(rename_all = "snake_case")]
118#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
119/// Specify env for the builder
120pub struct CustomEnv {
121    /// Custom env definition. Empty strings are treated as undefined by default
122    #[serde(default)]
123    pub env: HashMap<String, Option<String>>,
124
125    /// Use system as a fallback
126    #[serde(default = "default_true")]
127    pub fallback_to_system: bool,
128
129    /// Allow variable clearing by empty string or undefined values
130    #[serde(default)]
131    pub allow_variable_clearing: bool,
132}
133
134impl Default for CustomEnv {
135    fn default() -> Self {
136        CustomEnv {
137            env: HashMap::new(),
138            fallback_to_system: true,
139            allow_variable_clearing: false,
140        }
141    }
142}
143
144#[derive(Clone, Debug, Serialize, Deserialize)]
145#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
146pub struct Builder {
147    pub qualifier: String,
148    pub organization: String,
149    pub application: String,
150
151    #[serde(default)]
152    pub spec: Spec,
153
154    /// Specify env for the custom builder
155    /// **NOTE**: It does only work for custom spec builders
156    #[serde(default)]
157    pub custom_env: CustomEnv,
158}
159
160#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
161#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
162pub struct BuilderResult {
163    pub application_name: String,
164    pub dirs: HashMap<String, ProjectDirs>,
165}
166
167impl Builder {
168    fn system_default(&self, project: &project_dirs::Project) -> HashMap<String, ProjectDirs> {
169        let dirs = project.project_dirs();
170        HashMap::from([
171            ("local".to_string(), dirs.local),
172            ("user".to_string(), dirs.user),
173            ("system".to_string(), dirs.system),
174        ])
175    }
176
177    pub fn process_spec_entry(
178        &self,
179        project: &project_dirs::Project,
180        entry: &SpecEntry,
181    ) -> ProjectDirs {
182        use project_dirs::dir_utils::{Filter as _, Mounted as _};
183        use project_dirs::strategy::fhs::Fhs as _;
184        use project_dirs::strategy::unix::Unix as _;
185        use project_dirs::strategy::windows::{Windows as _, WindowsEnv};
186        use project_dirs::strategy::xdg::{Xdg as _, XdgEnv};
187
188        let mut pd: ProjectDirs = match &entry.strategy {
189            Strategy::CurrentLocal => project.project_dirs().local,
190            Strategy::CurrentUser => project.project_dirs().user,
191            Strategy::CurrentSystem => project.project_dirs().system,
192            Strategy::Fhs(fhs) => match fhs {
193                Some(Fhs::Local) => project.fhs_local().into(),
194                Some(Fhs::Shared) | None => project.fhs().into(),
195            },
196            Strategy::Xdg => {
197                let mut env = if self.custom_env.fallback_to_system {
198                    XdgEnv::new_system()
199                } else {
200                    XdgEnv::default()
201                };
202
203                env.extend_with_env(
204                    self.custom_env.env.iter().map(|x| (x.0, x.1.as_ref())),
205                    self.custom_env.allow_variable_clearing,
206                );
207
208                if self.custom_env.fallback_to_system {
209                    project
210                        .xdg_with_env(env)
211                        .map(ProjectDirs::from)
212                        .unwrap_or(ProjectDirs::empty())
213                } else {
214                    project.xdg_with_env_exclude_missing(env)
215                }
216            }
217            Strategy::Unix(unix) => match unix {
218                Unix::Pwd => project
219                    .unix_pwd()
220                    .map(Into::into)
221                    .unwrap_or(ProjectDirs::empty()),
222                Unix::Home => project
223                    .unix_home()
224                    .map(Into::into)
225                    .unwrap_or(ProjectDirs::empty()),
226                Unix::Binary => project
227                    .unix_binary()
228                    .map(Into::into)
229                    .unwrap_or(ProjectDirs::empty()),
230                Unix::Custom { path, prefix } => match prefix {
231                    Some(prefix) => project.unix_prefixed(path, prefix).into(),
232                    None => project.unix(path).into(),
233                },
234            },
235            Strategy::Windows(windows) => {
236                #[cfg(target_os = "windows")]
237                let mut env = if self.custom_env.fallback_to_system {
238                    WindowsEnv::new_system()
239                } else {
240                    WindowsEnv::default()
241                };
242
243                #[cfg(not(target_os = "windows"))]
244                let mut env = WindowsEnv::default();
245
246                env.extend_with_env(
247                    self.custom_env.env.iter().map(|x| (x.0, x.1.as_ref())),
248                    self.custom_env.allow_variable_clearing,
249                );
250
251                match windows {
252                    Windows::Standard => project.windows_user_with_env(env),
253                    Windows::Local => project.windows_user_local_with_env(env),
254                    Windows::Shared => project.windows_user_shared_with_env(env),
255                    Windows::System => project.windows_system_with_env(env),
256                }
257            }
258        };
259
260        if let Some(filter) = &entry.filter {
261            pd = match filter {
262                Filter::FsPresent => pd.filter_existing_dirs(),
263                Filter::FsAbsent => pd.filter_absent(),
264                Filter::FsNotDir => pd.filter_non_dirs(),
265                Filter::FsDenied => pd.filter_denied(),
266                Filter::FsNonValidDir => pd.filter_non_valid(),
267            };
268        }
269
270        if let Some(mountpoint) = &entry.mountpoint {
271            pd = pd.mounted(mountpoint);
272        }
273
274        if !entry.directories.is_empty() {
275            pd = ProjectDirs::new(
276                pd.0.into_iter()
277                    .filter(|d| entry.directories.contains(&d.0))
278                    .collect(),
279            );
280        }
281
282        pd
283    }
284
285    pub fn build(&self) -> BuilderResult {
286        let project =
287            project_dirs::Project::new(&self.qualifier, &self.organization, &self.application);
288
289        let application_name = project.application_name().to_string();
290
291        BuilderResult {
292            application_name,
293            dirs: match &self.spec {
294                Spec::SystemDefault => self.system_default(&project),
295                Spec::Custom(items) => items.iter().fold(HashMap::new(), |mut acc, item| {
296                    acc.insert(item.0.clone(), self.process_spec_entry(&project, item.1));
297                    acc
298                }),
299            },
300        }
301    }
302}