1use serde::{Deserialize, Serialize};
2use std::path::PathBuf;
3
4#[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#[derive(Debug, Default, Clone, Serialize, Deserialize)]
17pub struct RegistryConfig {
18 pub token: Option<String>,
19}
20
21impl Config {
22 pub fn load() -> Self {
24 Self::load_from(Self::default_path())
25 }
26
27 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 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}