1use std::collections::HashMap;
2
3use saphyr::{LoadableYamlNode, Yaml};
4
5pub(crate) mod adapters;
6mod value;
7
8use crate::prelude::*;
9use value::Value;
10
11pub struct Settings {
12 global_config_dir: std::path::PathBuf,
13 global_config_file: std::path::PathBuf,
14 project_config_dir: std::path::PathBuf,
15 project_config_file: std::path::PathBuf,
16 settings: HashMap<String, Value>,
17}
18
19impl Settings {
20 pub fn new(project_config_file: &std::path::Path) -> Result<Self> {
21 let global_config_dir = directories::ProjectDirs::from("com", "cyloncore", "oo")
22 .ok_or_else(|| anyhow!("Failed to retrieve project dir."))?
23 .config_dir()
24 .to_path_buf();
25 let project_config_dir = project_config_file
26 .parent()
27 .unwrap_or(project_config_file)
28 .to_path_buf();
29 let mut settings = Self {
30 global_config_file: global_config_dir.join("config.yaml"),
31 global_config_dir,
32 project_config_file: project_config_file.into(),
33 project_config_dir,
34 settings: Default::default(),
35 };
36 settings.load_settings()?;
37 Ok(settings)
38 }
39 fn load_settings(&mut self) -> Result<()> {
40 self.settings = Default::default();
41 self.load_yaml(include_str!("../data/default_settings.yaml"))?;
42 if self.global_config_file.exists() {
43 let data = std::fs::read_to_string(&self.global_config_file)?;
44 self.load_yaml(&data)?;
45 } else {
46 log::info!("No global config file {:?}", self.global_config_file);
47 }
48 if self.project_config_file.exists() {
49 let data = std::fs::read_to_string(&self.project_config_file)?;
50 self.load_yaml(&data)?;
51 } else {
52 log::info!("No project config file {:?}", self.project_config_file);
53 }
54 Ok(())
55 }
56 fn to_string<'a>(node: &'a Yaml) -> Result<&'a str> {
57 node.as_str()
58 .ok_or_else(|| anyhow!("Expected string, got {:?}", node))
59 }
60 fn load_node(&mut self, key: String, node: &saphyr::Yaml) -> Result<()> {
61 use saphyr::{Scalar, Yaml};
62 match node {
63 Yaml::Alias(_) => Err(anyhow!("aliases are not supported")),
64 Yaml::Mapping(mapping) => {
65 for (k, v) in mapping.iter() {
66 let k = Self::to_string(k)?;
67 self.load_node(
68 if key.is_empty() {
69 k.to_string()
70 } else {
71 format!("{}.{}", key, k)
72 },
73 v,
74 )?;
75 }
76 Ok(())
77 }
78 Yaml::Representation(_, _, _) => Err(anyhow!("Unsupported representation")),
79 Yaml::Tagged(_, _) => Err(anyhow!("Unsupported tagged")),
80 Yaml::BadValue => Err(anyhow!("Bad value")),
81 Yaml::Sequence(seq) => {
82 self.settings.insert(
83 key,
84 Value::StringVec(
85 seq.iter()
86 .map(|x| Ok(Self::to_string(x)?.to_string()))
87 .collect::<Result<_>>()?,
88 ),
89 );
90 Ok(())
91 }
92 Yaml::Value(value) => {
93 match value {
94 Scalar::Boolean(b) => {
95 self.settings.insert(key, Value::Boolean(*b));
96 }
97 Scalar::Null => {}
98 Scalar::Integer(i) => {
99 self.settings.insert(key, Value::Integer(*i));
100 }
101 Scalar::FloatingPoint(f) => {
102 self.settings.insert(key, Value::Float(f.into_inner()));
103 }
104 Scalar::String(string) => {
105 self.settings.insert(key, Value::String(string.to_string()));
106 }
107 }
108 Ok(())
109 }
110 }
111 }
112 fn load_yaml(&mut self, data: &str) -> Result<()> {
113 let nodes = saphyr::Yaml::load_from_str(data)?;
114 if let Some(node) = nodes.first() {
115 self.load_node("".to_string(), node)?;
116 }
117 Ok(())
118 }
119 pub fn global_config_dir(&self) -> &std::path::Path {
121 &self.global_config_dir
122 }
123
124 pub fn project_config_dir(&self) -> &std::path::Path {
126 &self.project_config_dir
127 }
128
129 pub(crate) fn get_optional<'a, T>(&'a self, key: impl AsRef<str>) -> Option<&'a T>
132 where
133 &'a T: TryFrom<&'a Value> + std::fmt::Debug,
134 T: value::ConstantDefault,
135 <&'a T as TryFrom<&'a Value>>::Error: std::fmt::Debug,
136 {
137 let key = key.as_ref();
138 let v = self.settings.get(key)?;
139 match v.try_into() {
140 Ok(v) => Some(v),
141 Err(e) => {
142 log::error!("Failed to cast '{v:?}' for key '{key}' with error: '{e:?}'");
143 None
144 }
145 }
146 }
147
148 pub(crate) fn get<'a, T>(&'a self, key: impl AsRef<str>) -> &'a T
149 where
150 &'a T: TryFrom<&'a Value> + std::fmt::Debug,
151 T: value::ConstantDefault,
152 <&'a T as TryFrom<&'a Value>>::Error: std::fmt::Debug,
153 {
154 let key = key.as_ref();
155 if let Some(v) = self.settings.get(key) {
156 match v.try_into() {
157 Ok(v) => v,
158 Err(e) => {
159 log::error!("Failed to cast '{v:?}' for key '{key}' with error: '{e:?}'");
160 T::constant_default()
161 }
162 }
163 } else {
164 log::error!("Unknown '{key}' key in settings, make sure to define a default.");
165 T::constant_default()
166 }
167 }
168
169 pub(crate) fn merge_extension_defaults(&mut self, defaults: Vec<(String, String)>) {
170 for (ext_name, yaml) in defaults {
171 log::debug!("Loading defaults for extension '{}'", ext_name);
172 if let Err(e) = self.load_yaml(&yaml) {
173 log::warn!("Failed to load defaults for extension '{}': {}", ext_name, e);
174 }
175 }
176 }
177}
178
179#[cfg(test)]
180mod tests {
181 use super::*;
182
183 fn create_test_settings() -> (Settings, tempfile::TempDir) {
184 let dir = tempfile::tempdir().unwrap();
185 let project_config = dir.path().join(".oo").join("config.yaml");
186 std::fs::create_dir(dir.path().join(".oo")).unwrap();
187
188 let settings = Settings::new(&project_config).expect("Failed to create test settings");
189 (settings, dir)
190 }
191
192 #[test]
193 fn test_load_yaml_boolean() {
194 let (mut settings, _dir) = create_test_settings();
195 let yaml = r#"
196test_bool: true
197"#;
198 settings.load_yaml(yaml).unwrap();
199 assert!(settings.settings.contains_key("test_bool"));
200 }
201
202 #[test]
203 fn test_load_yaml_integer() {
204 let (mut settings, _dir) = create_test_settings();
205 let yaml = r#"
206test_int: 42
207"#;
208 settings.load_yaml(yaml).unwrap();
209 assert!(settings.settings.contains_key("test_int"));
210 }
211
212 #[test]
213 fn test_load_yaml_float() {
214 let (mut settings, _dir) = create_test_settings();
215 let yaml = r#"
216test_float: 3.14
217"#;
218 settings.load_yaml(yaml).unwrap();
219 assert!(settings.settings.contains_key("test_float"));
220 }
221
222 #[test]
223 fn test_load_yaml_string() {
224 let (mut settings, _dir) = create_test_settings();
225 let yaml = r#"
226test_string: "hello"
227"#;
228 settings.load_yaml(yaml).unwrap();
229 assert!(settings.settings.contains_key("test_string"));
230 }
231
232 #[test]
233 fn test_load_yaml_sequence() {
234 let (mut settings, _dir) = create_test_settings();
235 let yaml = r#"
236test_seq:
237 - item1
238 - item2
239 - item3
240"#;
241 settings.load_yaml(yaml).unwrap();
242 assert!(settings.settings.contains_key("test_seq"));
243 }
244
245 #[test]
246 fn test_load_yaml_nested_mapping() {
247 let (mut settings, _dir) = create_test_settings();
248 let yaml = r#"
249parent:
250 child1: value1
251 child2: value2
252"#;
253 settings.load_yaml(yaml).unwrap();
254 assert!(settings.settings.contains_key("parent.child1"));
255 assert!(settings.settings.contains_key("parent.child2"));
256 }
257
258 #[test]
259 fn test_load_yaml_deeply_nested() {
260 let (mut settings, _dir) = create_test_settings();
261 let yaml = r#"
262level1:
263 level2:
264 level3:
265 value: deep
266"#;
267 settings.load_yaml(yaml).unwrap();
268 assert!(settings.settings.contains_key("level1.level2.level3.value"));
269 }
270
271 #[test]
272 fn test_load_yaml_null_value() {
273 let (mut settings, _dir) = create_test_settings();
274 let yaml = r#"
275test_null:
276"#;
277 settings.load_yaml(yaml).unwrap();
278 assert!(!settings.settings.contains_key("test_null"));
280 }
281
282 #[test]
283 fn test_load_yaml_invalid_yaml() {
284 let (mut settings, _dir) = create_test_settings();
285 let invalid_yaml = "invalid: [unclosed";
286 let result = settings.load_yaml(invalid_yaml);
287 assert!(result.is_err());
288 }
289
290 #[test]
291 fn test_load_yaml_bad_value() {
292 let (mut settings, _dir) = create_test_settings();
293 let yaml = "badval: !!binary\n sdlkfjlskdjf";
294 let result = settings.load_yaml(yaml);
295 assert!(result.is_err());
296 }
297
298 #[test]
299 fn test_get_existing_bool() {
300 let (mut settings, _dir) = create_test_settings();
301 let yaml = "test_bool: true";
302 settings.load_yaml(yaml).unwrap();
303 let val: &bool = settings.get("test_bool");
304 assert!(*val);
305 }
306
307 #[test]
308 fn test_get_existing_integer() {
309 let (mut settings, _dir) = create_test_settings();
310 let yaml = "test_int: 100";
311 settings.load_yaml(yaml).unwrap();
312 let val: &i64 = settings.get("test_int");
313 assert_eq!(*val, 100);
314 }
315
316 #[test]
317 fn test_get_existing_float() {
318 let (mut settings, _dir) = create_test_settings();
319 let yaml = "test_float: 2.71";
320 settings.load_yaml(yaml).unwrap();
321 let val: &f64 = settings.get("test_float");
322 assert_eq!(*val, 2.71);
323 }
324
325 #[test]
326 fn test_get_existing_string() {
327 let (mut settings, _dir) = create_test_settings();
328 let yaml = "test_string: \"world\"";
329 settings.load_yaml(yaml).unwrap();
330 let val: &String = settings.get("test_string");
331 assert_eq!(*val, "world");
332 }
333
334 #[test]
335 fn test_get_existing_vec_string() {
336 let (mut settings, _dir) = create_test_settings();
337 let yaml = r#"
338test_vec:
339 - a
340 - b
341 - c
342"#;
343 settings.load_yaml(yaml).unwrap();
344 let val: &Vec<String> = settings.get("test_vec");
345 assert_eq!(val.len(), 3);
346 assert_eq!(val[0], "a");
347 }
348
349 #[test]
350 fn test_get_missing_key_returns_default() {
351 let (settings, _dir) = create_test_settings();
352 let val: &bool = settings.get("nonexistent_bool");
353 assert!(!*val);
354 }
355
356 #[test]
357 fn test_get_optional_existing_key() {
358 let (mut settings, _dir) = create_test_settings();
359 let yaml = "test_bool: true";
360 settings.load_yaml(yaml).unwrap();
361 let val: Option<&bool> = settings.get_optional("test_bool");
362 assert_eq!(val, Some(&true));
363 }
364
365 #[test]
366 fn test_get_optional_missing_key() {
367 let (settings, _dir) = create_test_settings();
368 let val: Option<&bool> = settings.get_optional("nonexistent");
369 assert_eq!(val, None);
370 }
371
372 #[test]
373 fn test_get_type_mismatch_returns_default() {
374 let (mut settings, _dir) = create_test_settings();
375 let yaml = "test_int: 42";
376 settings.load_yaml(yaml).unwrap();
377 let val: &bool = settings.get("test_int");
379 assert!(!*val);
380 }
381
382 #[test]
383 fn test_get_optional_type_mismatch_returns_none() {
384 let (mut settings, _dir) = create_test_settings();
385 let yaml = "test_int: 42";
386 settings.load_yaml(yaml).unwrap();
387 let val: Option<&bool> = settings.get_optional("test_int");
389 assert_eq!(val, None);
390 }
391
392 #[test]
393 fn test_global_config_dir() {
394 let (settings, _dir) = create_test_settings();
395 let dir = settings.global_config_dir();
396 assert!(dir.is_absolute() || !dir.as_os_str().is_empty());
397 }
398
399 #[test]
400 fn test_project_config_dir() {
401 let (settings, _dir) = create_test_settings();
402 let config_dir = settings.project_config_dir();
403 let is_oo_dir = config_dir
405 .file_name()
406 .and_then(|s| s.to_str())
407 .map(|name| name == ".oo" || name == "oo")
408 .unwrap_or(false);
409 assert!(is_oo_dir);
410 }
411
412 #[test]
413 fn test_settings_merge_multiple_loads() {
414 let (mut settings, _dir) = create_test_settings();
415 let yaml1 = "key1: value1";
416 let yaml2 = "key2: value2";
417
418 settings.load_yaml(yaml1).unwrap();
419 settings.load_yaml(yaml2).unwrap();
420
421 assert!(settings.settings.contains_key("key1"));
422 assert!(settings.settings.contains_key("key2"));
423 }
424
425 #[test]
426 fn test_load_yaml_overwrites_previous() {
427 let (mut settings, _dir) = create_test_settings();
428 let yaml1 = "same_key: 1";
429 let yaml2 = "same_key: 2";
430
431 settings.load_yaml(yaml1).unwrap();
432 let val1: &i64 = settings.get("same_key");
433 assert_eq!(*val1, 1);
434
435 settings.load_yaml(yaml2).unwrap();
436 let val2: &i64 = settings.get("same_key");
437 assert_eq!(*val2, 2);
438 }
439
440 #[test]
441 fn test_to_string_helper_valid() {
442 let (mut settings, _dir) = create_test_settings();
443 let yaml = "test: hello";
444 settings.load_yaml(yaml).unwrap();
445 assert!(settings.settings.contains_key("test"));
446 }
447
448 #[test]
449 fn test_load_yaml_empty_document() {
450 let (mut settings, _dir) = create_test_settings();
451 let yaml = "";
452 let result = settings.load_yaml(yaml);
453 assert!(result.is_ok());
454 }
455
456 #[test]
459 fn test_language_lsp_falls_back_to_global() {
460 let (settings, _dir) = create_test_settings();
461 assert!(!*adapters::language::lsp::enable(&settings, "rust"));
464 }
465
466 #[test]
467 fn test_language_lsp_enable_parses() {
468 let (mut settings, _dir) = create_test_settings();
469 let yaml = r#"
470languages:
471 cpp:
472 lsp:
473 enable: true
474 server_command: "clangd"
475"#;
476 settings.load_yaml(yaml).unwrap();
477
478 assert!(*adapters::language::lsp::enable(&settings, "cpp"));
479 assert_eq!(adapters::language::lsp::server_command(&settings, "cpp"), "clangd");
480 }
481
482 #[test]
483 fn test_language_lsp_quoted_true_parses() {
484 let (mut settings, _dir) = create_test_settings();
485 let yaml = r#"
486languages:
487 rust:
488 lsp:
489 enable: "true"
490 server_command: "rust-analyzer"
491"#;
492 settings.load_yaml(yaml).unwrap();
493
494 assert!(!*adapters::language::lsp::enable(&settings, "rust"));
496 assert_eq!(adapters::language::lsp::server_command(&settings, "rust"), "rust-analyzer");
497 }
498
499 #[test]
500 fn test_language_lsp_uses_override() {
501 let (mut settings, _dir) = create_test_settings();
502 let yaml = r#"
503languages:
504 rust:
505 lsp:
506 enable: true
507 server_command: "rust-analyzer"
508"#;
509 settings.load_yaml(yaml).unwrap();
510
511 assert!(*adapters::language::lsp::enable(&settings, "rust"));
512 assert_eq!(adapters::language::lsp::server_command(&settings, "rust"), "rust-analyzer");
513 }
514
515 #[test]
516 fn test_multiple_languages_independent() {
517 let (mut settings, _dir) = create_test_settings();
518 let yaml = r#"
519languages:
520 cpp:
521 lsp:
522 enable: true
523 server_command: "clangd"
524 rust:
525 lsp:
526 enable: false
527 server_command: "rust-analyzer"
528"#;
529 settings.load_yaml(yaml).unwrap();
530
531 assert!(*adapters::language::lsp::enable(&settings, "cpp"));
532 assert_eq!(adapters::language::lsp::server_command(&settings, "cpp"), "clangd");
533 assert!(!*adapters::language::lsp::enable(&settings, "rust"));
534 assert_eq!(adapters::language::lsp::server_command(&settings, "rust"), "rust-analyzer");
535 }
536
537 #[test]
538 fn test_global_lsp_unaffected_by_language_override() {
539 let (mut settings, _dir) = create_test_settings();
540 let yaml = r#"
541languages:
542 cpp:
543 lsp:
544 enable: true
545"#;
546 settings.load_yaml(yaml).unwrap();
547
548 let global_enable: &bool = settings.get("lsp.enable");
550 assert!(!*global_enable);
551 }
552}
553
554
555