plausible_cli/config/
mod.rs

1use std::path::{Path, PathBuf};
2
3pub mod accounts;
4
5/// Namespace for resolving configuration directories and filenames.
6#[derive(Debug, Clone)]
7pub struct ConfigPaths {
8    base_dir: PathBuf,
9}
10
11impl ConfigPaths {
12    /// Create configuration paths using the user's platform conventions.
13    pub fn with_project_dirs() -> Result<Self, ConfigError> {
14        let project_dirs = directories::ProjectDirs::from("io", "plausible", "plausible-cli")
15            .ok_or(ConfigError::UnsupportedPlatform)?;
16        Ok(Self {
17            base_dir: project_dirs.config_dir().to_path_buf(),
18        })
19    }
20
21    /// Construct from a custom base directory (primarily for testing).
22    pub fn from_base_dir(base_dir: impl Into<PathBuf>) -> Self {
23        Self {
24            base_dir: base_dir.into(),
25        }
26    }
27
28    /// Location of the persisted configuration file.
29    pub fn config_file(&self) -> PathBuf {
30        self.base_dir.join("config.toml")
31    }
32
33    /// Directory that stores rate-limit usage counters.
34    pub fn usage_dir(&self) -> PathBuf {
35        self.base_dir.join("usage")
36    }
37
38    /// Directory that stores account metadata.
39    pub fn accounts_dir(&self) -> PathBuf {
40        self.base_dir.join("accounts")
41    }
42
43    /// File path storing account metadata index.
44    pub fn accounts_file(&self) -> PathBuf {
45        self.base_dir.join("accounts.json")
46    }
47
48    /// Ensure the configuration root exists on disk.
49    pub fn ensure_exists(&self) -> Result<(), ConfigError> {
50        let base = self.base_dir.clone();
51        std::fs::create_dir_all(&base).map_err(|source| ConfigError::Io {
52            path: base.clone(),
53            source,
54        })?;
55
56        let accounts_dir = self.accounts_dir();
57        std::fs::create_dir_all(&accounts_dir).map_err(|source| ConfigError::Io {
58            path: accounts_dir.clone(),
59            source,
60        })?;
61
62        let usage_dir = self.usage_dir();
63        std::fs::create_dir_all(&usage_dir).map_err(|source| ConfigError::Io {
64            path: usage_dir.clone(),
65            source,
66        })?;
67        Ok(())
68    }
69
70    /// Retrieve the base directory path.
71    pub fn base_dir(&self) -> &Path {
72        &self.base_dir
73    }
74}
75
76#[derive(thiserror::Error, Debug)]
77pub enum ConfigError {
78    #[error("failed to determine configuration directory for this platform")]
79    UnsupportedPlatform,
80    #[error("I/O error interacting with {path:?}")]
81    Io {
82        path: PathBuf,
83        #[source]
84        source: std::io::Error,
85    },
86}
87
88#[cfg(test)]
89mod tests {
90    use super::*;
91    use std::fs;
92
93    #[test]
94    fn config_paths_resolve_expected_files() {
95        let tmp = tempfile::tempdir().expect("temporary dir");
96        let paths = ConfigPaths::from_base_dir(tmp.path());
97
98        assert_eq!(paths.config_file(), tmp.path().join("config.toml"));
99        assert_eq!(paths.usage_dir(), tmp.path().join("usage"));
100        assert_eq!(paths.accounts_dir(), tmp.path().join("accounts"));
101        assert_eq!(paths.accounts_file(), tmp.path().join("accounts.json"));
102    }
103
104    #[test]
105    fn ensure_exists_creates_directory_tree() {
106        let tmp = tempfile::tempdir().expect("temporary dir");
107        let base = tmp.path().join("nested").join("plausible");
108        let paths = ConfigPaths::from_base_dir(&base);
109
110        paths.ensure_exists().expect("create dirs");
111        assert!(fs::metadata(paths.accounts_dir())
112            .expect("metadata")
113            .is_dir());
114        assert!(fs::metadata(paths.usage_dir()).expect("metadata").is_dir());
115        assert!(fs::metadata(&base).expect("metadata").is_dir());
116    }
117}