caldir_core/calendar/
config.rs1mod 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 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}