1use serde::{Deserialize, Serialize};
4use std::path::PathBuf;
5
6#[derive(Debug, Default, Serialize, Deserialize)]
7pub struct Config {
8 pub gitlab: Option<GitlabConfig>,
9}
10
11#[derive(Debug, Serialize, Deserialize)]
12pub struct GitlabConfig {
13 pub access_token: String,
14}
15
16pub fn config_path() -> Result<PathBuf, Box<dyn std::error::Error>> {
17 let config_dir = std::env::var("XDG_CONFIG_HOME")
18 .map(PathBuf::from)
19 .unwrap_or_else(|_| {
20 let home = std::env::var("HOME")
21 .unwrap_or_else(|_| ".".into());
22 PathBuf::from(home).join(".config")
23 });
24 Ok(config_dir.join("hs-relmon").join("config.toml"))
25}
26
27pub fn load() -> Result<Config, Box<dyn std::error::Error>> {
28 load_from(&config_path()?)
29}
30
31pub fn save(config: &Config) -> Result<(), Box<dyn std::error::Error>> {
32 save_to(config, &config_path()?)
33}
34
35pub fn load_from(
36 path: &std::path::Path,
37) -> Result<Config, Box<dyn std::error::Error>> {
38 let contents = std::fs::read_to_string(path)?;
39 Ok(toml::from_str(&contents)?)
40}
41
42pub fn save_to(
43 config: &Config,
44 path: &std::path::Path,
45) -> Result<(), Box<dyn std::error::Error>> {
46 if let Some(parent) = path.parent() {
47 std::fs::create_dir_all(parent)?;
48 }
49 let contents = toml::to_string_pretty(config)?;
50 std::fs::write(path, contents)?;
51 Ok(())
52}
53
54#[cfg(test)]
55mod tests {
56 use super::*;
57
58 #[test]
59 fn test_config_deserialize() {
60 let toml_str = r#"
61[gitlab]
62access_token = "glpat-abc123"
63"#;
64 let config: Config = toml::from_str(toml_str).unwrap();
65 assert_eq!(
66 config.gitlab.as_ref().unwrap().access_token,
67 "glpat-abc123"
68 );
69 }
70
71 #[test]
72 fn test_config_deserialize_no_gitlab() {
73 let config: Config = toml::from_str("").unwrap();
74 assert!(config.gitlab.is_none());
75 }
76
77 #[test]
78 fn test_config_serialize() {
79 let config = Config {
80 gitlab: Some(GitlabConfig {
81 access_token: "glpat-abc123".into(),
82 }),
83 };
84 let s = toml::to_string_pretty(&config).unwrap();
85 assert!(s.contains("[gitlab]"));
86 assert!(s.contains("access_token = \"glpat-abc123\""));
87 }
88
89 #[test]
90 fn test_config_roundtrip() {
91 let config = Config {
92 gitlab: Some(GitlabConfig {
93 access_token: "test-token".into(),
94 }),
95 };
96 let s = toml::to_string_pretty(&config).unwrap();
97 let parsed: Config = toml::from_str(&s).unwrap();
98 assert_eq!(
99 parsed.gitlab.unwrap().access_token,
100 "test-token"
101 );
102 }
103
104 #[test]
105 fn test_config_path() {
106 let path = config_path().unwrap();
107 assert!(path.ends_with("hs-relmon/config.toml"));
108 }
109
110 #[test]
111 fn test_save_and_load() {
112 let dir = std::env::temp_dir().join("hs-relmon-test");
113 let path = dir.join("config.toml");
114 let config = Config {
115 gitlab: Some(GitlabConfig {
116 access_token: "test-save-load".into(),
117 }),
118 };
119 save_to(&config, &path).unwrap();
120 let loaded = load_from(&path).unwrap();
121 assert_eq!(
122 loaded.gitlab.unwrap().access_token,
123 "test-save-load"
124 );
125 let _ = std::fs::remove_dir_all(&dir);
126 }
127
128 #[test]
129 fn test_load_missing_file() {
130 let path = PathBuf::from("/tmp/hs-relmon-nonexistent/x");
131 assert!(load_from(&path).is_err());
132 }
133}