Skip to main content

truf_cli/
config.rs

1use serde::{Deserialize, Serialize};
2use std::path::PathBuf;
3
4/// Configuration loaded from `~/.config/truf/config.toml`.
5#[derive(Debug, Default, Clone, Serialize, Deserialize)]
6pub struct Config {
7    #[serde(default)]
8    pub npm: RegistryConfig,
9    #[serde(default)]
10    pub cratesio: RegistryConfig,
11    #[serde(default)]
12    pub pypi: RegistryConfig,
13}
14
15/// Per-registry configuration.
16#[derive(Debug, Default, Clone, Serialize, Deserialize)]
17pub struct RegistryConfig {
18    pub token: Option<String>,
19}
20
21impl Config {
22    /// Load config from the default path (`~/.config/truf/config.toml`).
23    pub fn load() -> Self {
24        Self::load_from(Self::default_path())
25    }
26
27    /// Load config from a specific path.
28    pub fn load_from(path: Option<PathBuf>) -> Self {
29        let Some(path) = path else {
30            return Self::default();
31        };
32        if !path.exists() {
33            return Self::default();
34        }
35        match std::fs::read_to_string(&path) {
36            Ok(contents) => match toml::from_str(&contents) {
37                Ok(config) => config,
38                Err(e) => {
39                    tracing::warn!(path = %path.display(), error = %e, "failed to parse config");
40                    Self::default()
41                }
42            },
43            Err(e) => {
44                tracing::warn!(path = %path.display(), error = %e, "failed to read config");
45                Self::default()
46            }
47        }
48    }
49
50    /// Default config file path.
51    pub fn default_path() -> Option<PathBuf> {
52        dirs::config_dir().map(|d| d.join("truf").join("config.toml"))
53    }
54}
55
56#[cfg(test)]
57mod tests {
58    use super::*;
59
60    #[test]
61    fn default_config_has_no_tokens() {
62        let config = Config::default();
63        assert!(config.npm.token.is_none());
64        assert!(config.cratesio.token.is_none());
65        assert!(config.pypi.token.is_none());
66    }
67
68    #[test]
69    fn parse_config_toml() {
70        let toml_str = r#"
71[npm]
72token = "npm_abc123"
73
74[cratesio]
75token = "cio_xyz789"
76
77[pypi]
78token = "pypi-tok456"
79"#;
80        let config: Config = toml::from_str(toml_str).unwrap();
81        assert_eq!(config.npm.token.as_deref(), Some("npm_abc123"));
82        assert_eq!(config.cratesio.token.as_deref(), Some("cio_xyz789"));
83        assert_eq!(config.pypi.token.as_deref(), Some("pypi-tok456"));
84    }
85
86    #[test]
87    fn parse_partial_config() {
88        let toml_str = r#"
89[npm]
90token = "npm_only"
91"#;
92        let config: Config = toml::from_str(toml_str).unwrap();
93        assert_eq!(config.npm.token.as_deref(), Some("npm_only"));
94        assert!(config.cratesio.token.is_none());
95        assert!(config.pypi.token.is_none());
96    }
97
98    #[test]
99    fn parse_empty_config() {
100        let config: Config = toml::from_str("").unwrap();
101        assert!(config.npm.token.is_none());
102    }
103
104    #[test]
105    fn load_nonexistent_returns_default() {
106        let config = Config::load_from(Some(PathBuf::from("/nonexistent/path/config.toml")));
107        assert!(config.npm.token.is_none());
108    }
109}