Skip to main content

caldir_core/calendar/
config.rs

1mod error;
2
3use crate::remote::RemoteConfig;
4use crate::utils::{TomlFileError, write_toml};
5use serde::{Deserialize, Serialize};
6use std::path::Path;
7
8pub use error::CalendarConfigError;
9
10#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
11pub struct CalendarConfig {
12    name: Option<String>,
13    color: Option<String>,
14    read_only: Option<bool>,
15
16    #[serde(rename = "remote")]
17    remote_config: Option<RemoteConfig>,
18}
19
20impl CalendarConfig {
21    pub fn new(
22        name: Option<String>,
23        color: Option<String>,
24        read_only: Option<bool>,
25        remote_config: Option<RemoteConfig>,
26    ) -> Self {
27        Self {
28            name,
29            color,
30            read_only,
31            remote_config,
32        }
33    }
34
35    /// Writes the config while preserving comments and unrelated formatting in
36    /// an existing file.
37    pub fn write(&self, path: &Path) -> Result<(), CalendarConfigError> {
38        write_toml(path, self).map_err(|err| match err {
39            TomlFileError::Read(err) => CalendarConfigError::Read(path.into(), err),
40            TomlFileError::Write(err) => CalendarConfigError::Write(path.into(), err),
41            TomlFileError::Deserialize(err) => CalendarConfigError::Deserialize(path.into(), err),
42            TomlFileError::Serialize(err) => CalendarConfigError::Serialize(err),
43            TomlFileError::Parse(err) => CalendarConfigError::Parse(path.into(), err),
44        })
45    }
46
47    pub fn load_optional(path: &Path) -> Result<Option<Self>, CalendarConfigError> {
48        if path.is_file() {
49            let config = Self::load(path)?;
50            Ok(Some(config))
51        } else {
52            Ok(None)
53        }
54    }
55
56    pub(crate) fn load(path: &Path) -> Result<Self, CalendarConfigError> {
57        let contents = std::fs::read_to_string(path)
58            .map_err(|err| CalendarConfigError::Read(path.into(), err))?;
59
60        let config = Self::from_toml(&contents)
61            .map_err(|e| CalendarConfigError::Deserialize(path.into(), e))?;
62
63        Ok(config)
64    }
65
66    pub fn name(&self) -> Option<&str> {
67        self.name.as_deref()
68    }
69
70    pub fn set_name(&mut self, name: Option<String>) {
71        self.name = name;
72    }
73
74    pub fn color(&self) -> Option<&str> {
75        self.color.as_deref()
76    }
77
78    pub fn set_color(&mut self, color: Option<String>) {
79        self.color = color;
80    }
81
82    pub fn read_only(&self) -> Option<bool> {
83        self.read_only
84    }
85
86    fn from_toml(s: &str) -> Result<Self, toml::de::Error> {
87        toml::from_str(s)
88    }
89
90    #[cfg(test)]
91    fn to_toml(&self) -> Result<String, toml::ser::Error> {
92        toml::to_string(self)
93    }
94
95    pub fn remote_config(&self) -> Option<&RemoteConfig> {
96        self.remote_config.as_ref()
97    }
98
99    #[cfg(test)]
100    pub(crate) fn set_remote(&mut self, remote_config: RemoteConfig) {
101        self.remote_config = Some(remote_config);
102    }
103}
104
105#[cfg(test)]
106mod tests {
107    use super::*;
108    use crate::test_utils::test_calendar_config;
109    use crate::{ProviderSlug, RemoteConfig, RemoteConfigParams};
110
111    #[test]
112    fn set_name_only_updates_name() {
113        let mut config = test_calendar_config();
114        let color = config.color.clone();
115        let read_only = config.read_only;
116        let remote_config = config.remote_config.clone();
117
118        config.set_name(Some("Renamed".to_string()));
119
120        assert_eq!(config.name(), Some("Renamed"));
121        assert_eq!(config.color, color);
122        assert_eq!(config.read_only, read_only);
123        assert_eq!(config.remote_config, remote_config);
124    }
125
126    #[test]
127    fn write_saves_config_to_file() {
128        let tmp = tempfile::TempDir::new().unwrap();
129        let path = tmp.path().join("config.toml");
130        let config = test_calendar_config();
131
132        config.write(&path).unwrap();
133
134        let loaded = CalendarConfig::load(&path).unwrap();
135        assert_eq!(loaded, config);
136    }
137
138    #[test]
139    fn write_preserves_remote_table_when_renaming_calendar() {
140        let tmp = tempfile::TempDir::new().unwrap();
141        let path = tmp.path().join("config.toml");
142        let before = "# calendar\nname   =   'Old' # display name\n\n[remote] # connection\nprovider = 'hooli'\nhooli_account = \"user@example.com\"\n";
143        std::fs::write(&path, before).unwrap();
144        let mut config = CalendarConfig::load_optional(&path).unwrap().unwrap();
145        config.set_name(Some("New".to_string()));
146
147        config.write(&path).unwrap();
148
149        assert_eq!(
150            std::fs::read_to_string(path).unwrap(),
151            "# calendar\nname   =   \"New\" # display name\n\n[remote] # connection\nprovider = 'hooli'\nhooli_account = \"user@example.com\"\n"
152        );
153    }
154
155    #[test]
156    fn write_replaces_remote_and_removes_stale_parameters() {
157        let tmp = tempfile::TempDir::new().unwrap();
158        let path = tmp.path().join("config.toml");
159        std::fs::write(
160            &path,
161            "[remote]\nprovider = 'old' # provider\nold_account = 'remove'\nold_calendar_id = 'remove'\n",
162        )
163        .unwrap();
164        let mut config = CalendarConfig::load_optional(&path).unwrap().unwrap();
165        let mut params = RemoteConfigParams::new();
166        params.insert(
167            "new_account".to_string(),
168            toml::Value::String("keep".to_string()),
169        );
170        config.set_remote(RemoteConfig::new(ProviderSlug::from("new"), params));
171
172        config.write(&path).unwrap();
173
174        assert_eq!(
175            std::fs::read_to_string(path).unwrap(),
176            "[remote]\nprovider = \"new\" # provider\nnew_account = \"keep\"\n"
177        );
178    }
179
180    #[test]
181    fn write_clears_remote_table() {
182        let tmp = tempfile::TempDir::new().unwrap();
183        let path = tmp.path().join("config.toml");
184        std::fs::write(
185            &path,
186            "name = \"kept\"\n\n[remote]\nprovider = \"old\"\nold_account = \"remove\"\n",
187        )
188        .unwrap();
189        let mut config = CalendarConfig::load_optional(&path).unwrap().unwrap();
190        config.remote_config = None;
191
192        config.write(&path).unwrap();
193
194        assert_eq!(std::fs::read_to_string(path).unwrap(), "name = \"kept\"\n");
195    }
196
197    #[test]
198    fn load_optional_returns_none_when_file_missing() {
199        let tmp = tempfile::TempDir::new().unwrap();
200        let path = tmp.path().join("does-not-exist.toml");
201
202        let result = CalendarConfig::load_optional(&path).unwrap();
203
204        assert!(result.is_none());
205    }
206
207    #[test]
208    fn load_optional_returns_config_when_file_exists() {
209        let tmp = tempfile::TempDir::new().unwrap();
210        let path = tmp.path().join("config.toml");
211        let config = test_calendar_config();
212        config.write(&path).unwrap();
213
214        let loaded = CalendarConfig::load_optional(&path).unwrap().unwrap();
215
216        assert_eq!(loaded, config);
217    }
218
219    #[test]
220    fn from_toml_parses_full_config_with_remote() {
221        let toml_str = r##"
222name = "Demo"
223color = "#ac725e"
224read_only = false
225
226[remote]
227provider = "hooli"
228hooli_calendar_id = "abc@group.calendar.hooli.com"
229hooli_account = "user@hmail.com"
230"##;
231
232        let config = CalendarConfig::from_toml(toml_str).unwrap();
233
234        assert_eq!(config.name.as_deref(), Some("Demo"));
235        assert_eq!(config.color.as_deref(), Some("#ac725e"));
236        assert_eq!(config.read_only, Some(false));
237
238        let remote_config = config.remote_config.expect("remote should be present");
239        assert_eq!(remote_config.provider_slug().to_string(), "hooli");
240        assert_eq!(
241            remote_config.get("hooli_account"),
242            Some(&toml::Value::String("user@hmail.com".to_string()))
243        );
244    }
245
246    #[test]
247    fn load_optional_errors_on_invalid_toml() {
248        let tmp = tempfile::TempDir::new().unwrap();
249        let path = tmp.path().join("config.toml");
250        std::fs::write(&path, "this is not = valid = toml").unwrap();
251
252        let result = CalendarConfig::load_optional(&path);
253
254        assert!(matches!(
255            result,
256            Err(CalendarConfigError::Deserialize(p, _)) if p == path
257        ));
258    }
259
260    #[test]
261    fn writes_full_config_with_remote_to_expected_toml() {
262        let mut params = RemoteConfigParams::new();
263
264        params.insert(
265            "hooli_calendar_id".to_string(),
266            toml::Value::String("abc@group.calendar.hooli.com".to_string()),
267        );
268        params.insert(
269            "hooli_account".to_string(),
270            toml::Value::String("user@hmail.com".to_string()),
271        );
272        let config = CalendarConfig::new(
273            Some("Demo".to_string()),
274            Some("#ac725e".to_string()),
275            Some(false),
276            Some(RemoteConfig::new(ProviderSlug::from("hooli"), params)),
277        );
278
279        let serialized = config.to_toml().unwrap();
280
281        let expected = r##"name = "Demo"
282color = "#ac725e"
283read_only = false
284
285[remote]
286provider = "hooli"
287hooli_account = "user@hmail.com"
288hooli_calendar_id = "abc@group.calendar.hooli.com"
289"##;
290        assert_eq!(serialized, expected);
291    }
292
293    #[test]
294    fn set_remote_sets_remote_config() {
295        let mut config = test_calendar_config();
296        assert!(config.remote_config().is_none());
297
298        let remote = RemoteConfig::new(ProviderSlug::from("hooli"), RemoteConfigParams::new());
299        config.set_remote(remote.clone());
300
301        assert_eq!(config.remote_config(), Some(&remote));
302    }
303
304    #[test]
305    fn set_remote_overwrites_existing_remote_config() {
306        let mut config = test_calendar_config();
307        config.set_remote(RemoteConfig::new(
308            ProviderSlug::from("hooli"),
309            RemoteConfigParams::new(),
310        ));
311
312        let new_remote = RemoteConfig::new(ProviderSlug::from("aviato"), RemoteConfigParams::new());
313        config.set_remote(new_remote.clone());
314
315        assert_eq!(config.remote_config(), Some(&new_remote));
316    }
317}