use std::collections::HashMap;
use saphyr::{LoadableYamlNode, Yaml};
pub(crate) mod adapters;
mod value;
use crate::prelude::*;
use value::Value;
pub struct Settings {
global_config_dir: std::path::PathBuf,
global_config_file: std::path::PathBuf,
project_config_dir: std::path::PathBuf,
project_config_file: std::path::PathBuf,
settings: HashMap<String, Value>,
}
impl Settings {
pub fn new(project_config_file: &std::path::Path) -> Result<Self> {
let global_config_dir = directories::ProjectDirs::from("com", "cyloncore", "oo")
.ok_or_else(|| anyhow!("Failed to retrieve project dir."))?
.config_dir()
.to_path_buf();
let project_config_dir = project_config_file
.parent()
.unwrap_or(project_config_file)
.to_path_buf();
let mut settings = Self {
global_config_file: global_config_dir.join("config.yaml"),
global_config_dir,
project_config_file: project_config_file.into(),
project_config_dir,
settings: Default::default(),
};
settings.load_settings()?;
Ok(settings)
}
fn load_settings(&mut self) -> Result<()> {
self.settings = Default::default();
self.load_yaml(include_str!("../data/default_settings.yaml"))?;
if self.global_config_file.exists() {
let data = std::fs::read_to_string(&self.global_config_file)?;
self.load_yaml(&data)?;
} else {
log::info!("No global config file {:?}", self.global_config_file);
}
if self.project_config_file.exists() {
let data = std::fs::read_to_string(&self.project_config_file)?;
self.load_yaml(&data)?;
} else {
log::info!("No project config file {:?}", self.project_config_file);
}
Ok(())
}
fn to_string<'a>(node: &'a Yaml) -> Result<&'a str> {
node.as_str()
.ok_or_else(|| anyhow!("Expected string, got {:?}", node))
}
fn load_node(&mut self, key: String, node: &saphyr::Yaml) -> Result<()> {
use saphyr::{Scalar, Yaml};
match node {
Yaml::Alias(_) => Err(anyhow!("aliases are not supported")),
Yaml::Mapping(mapping) => {
for (k, v) in mapping.iter() {
let k = Self::to_string(k)?;
self.load_node(
if key.is_empty() {
k.to_string()
} else {
format!("{}.{}", key, k)
},
v,
)?;
}
Ok(())
}
Yaml::Representation(_, _, _) => Err(anyhow!("Unsupported representation")),
Yaml::Tagged(_, _) => Err(anyhow!("Unsupported tagged")),
Yaml::BadValue => Err(anyhow!("Bad value")),
Yaml::Sequence(seq) => {
self.settings.insert(
key,
Value::StringVec(
seq.iter()
.map(|x| Ok(Self::to_string(x)?.to_string()))
.collect::<Result<_>>()?,
),
);
Ok(())
}
Yaml::Value(value) => {
match value {
Scalar::Boolean(b) => {
self.settings.insert(key, Value::Boolean(*b));
}
Scalar::Null => {}
Scalar::Integer(i) => {
self.settings.insert(key, Value::Integer(*i));
}
Scalar::FloatingPoint(f) => {
self.settings.insert(key, Value::Float(f.into_inner()));
}
Scalar::String(string) => {
self.settings.insert(key, Value::String(string.to_string()));
}
}
Ok(())
}
}
}
fn load_yaml(&mut self, data: &str) -> Result<()> {
let nodes = saphyr::Yaml::load_from_str(data)?;
if let Some(node) = nodes.first() {
self.load_node("".to_string(), node)?;
}
Ok(())
}
pub fn global_config_dir(&self) -> &std::path::Path {
&self.global_config_dir
}
pub fn project_config_dir(&self) -> &std::path::Path {
&self.project_config_dir
}
pub(crate) fn get_optional<'a, T>(&'a self, key: impl AsRef<str>) -> Option<&'a T>
where
&'a T: TryFrom<&'a Value> + std::fmt::Debug,
T: value::ConstantDefault,
<&'a T as TryFrom<&'a Value>>::Error: std::fmt::Debug,
{
let key = key.as_ref();
let v = self.settings.get(key)?;
match v.try_into() {
Ok(v) => Some(v),
Err(e) => {
log::error!("Failed to cast '{v:?}' for key '{key}' with error: '{e:?}'");
None
}
}
}
pub(crate) fn get<'a, T>(&'a self, key: impl AsRef<str>) -> &'a T
where
&'a T: TryFrom<&'a Value> + std::fmt::Debug,
T: value::ConstantDefault,
<&'a T as TryFrom<&'a Value>>::Error: std::fmt::Debug,
{
let key = key.as_ref();
if let Some(v) = self.settings.get(key) {
match v.try_into() {
Ok(v) => v,
Err(e) => {
log::error!("Failed to cast '{v:?}' for key '{key}' with error: '{e:?}'");
T::constant_default()
}
}
} else {
log::error!("Unknown '{key}' key in settings, make sure to define a default.");
T::constant_default()
}
}
pub(crate) fn merge_extension_defaults(&mut self, defaults: Vec<(String, String)>) {
for (ext_name, yaml) in defaults {
log::debug!("Loading defaults for extension '{}'", ext_name);
if let Err(e) = self.load_yaml(&yaml) {
log::warn!("Failed to load defaults for extension '{}': {}", ext_name, e);
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn create_test_settings() -> (Settings, tempfile::TempDir) {
let dir = tempfile::tempdir().unwrap();
let project_config = dir.path().join(".oo").join("config.yaml");
std::fs::create_dir(dir.path().join(".oo")).unwrap();
let settings = Settings::new(&project_config).expect("Failed to create test settings");
(settings, dir)
}
#[test]
fn test_load_yaml_boolean() {
let (mut settings, _dir) = create_test_settings();
let yaml = r#"
test_bool: true
"#;
settings.load_yaml(yaml).unwrap();
assert!(settings.settings.contains_key("test_bool"));
}
#[test]
fn test_load_yaml_integer() {
let (mut settings, _dir) = create_test_settings();
let yaml = r#"
test_int: 42
"#;
settings.load_yaml(yaml).unwrap();
assert!(settings.settings.contains_key("test_int"));
}
#[test]
fn test_load_yaml_float() {
let (mut settings, _dir) = create_test_settings();
let yaml = r#"
test_float: 3.14
"#;
settings.load_yaml(yaml).unwrap();
assert!(settings.settings.contains_key("test_float"));
}
#[test]
fn test_load_yaml_string() {
let (mut settings, _dir) = create_test_settings();
let yaml = r#"
test_string: "hello"
"#;
settings.load_yaml(yaml).unwrap();
assert!(settings.settings.contains_key("test_string"));
}
#[test]
fn test_load_yaml_sequence() {
let (mut settings, _dir) = create_test_settings();
let yaml = r#"
test_seq:
- item1
- item2
- item3
"#;
settings.load_yaml(yaml).unwrap();
assert!(settings.settings.contains_key("test_seq"));
}
#[test]
fn test_load_yaml_nested_mapping() {
let (mut settings, _dir) = create_test_settings();
let yaml = r#"
parent:
child1: value1
child2: value2
"#;
settings.load_yaml(yaml).unwrap();
assert!(settings.settings.contains_key("parent.child1"));
assert!(settings.settings.contains_key("parent.child2"));
}
#[test]
fn test_load_yaml_deeply_nested() {
let (mut settings, _dir) = create_test_settings();
let yaml = r#"
level1:
level2:
level3:
value: deep
"#;
settings.load_yaml(yaml).unwrap();
assert!(settings.settings.contains_key("level1.level2.level3.value"));
}
#[test]
fn test_load_yaml_null_value() {
let (mut settings, _dir) = create_test_settings();
let yaml = r#"
test_null:
"#;
settings.load_yaml(yaml).unwrap();
assert!(!settings.settings.contains_key("test_null"));
}
#[test]
fn test_load_yaml_invalid_yaml() {
let (mut settings, _dir) = create_test_settings();
let invalid_yaml = "invalid: [unclosed";
let result = settings.load_yaml(invalid_yaml);
assert!(result.is_err());
}
#[test]
fn test_load_yaml_bad_value() {
let (mut settings, _dir) = create_test_settings();
let yaml = "badval: !!binary\n sdlkfjlskdjf";
let result = settings.load_yaml(yaml);
assert!(result.is_err());
}
#[test]
fn test_get_existing_bool() {
let (mut settings, _dir) = create_test_settings();
let yaml = "test_bool: true";
settings.load_yaml(yaml).unwrap();
let val: &bool = settings.get("test_bool");
assert!(*val);
}
#[test]
fn test_get_existing_integer() {
let (mut settings, _dir) = create_test_settings();
let yaml = "test_int: 100";
settings.load_yaml(yaml).unwrap();
let val: &i64 = settings.get("test_int");
assert_eq!(*val, 100);
}
#[test]
fn test_get_existing_float() {
let (mut settings, _dir) = create_test_settings();
let yaml = "test_float: 2.71";
settings.load_yaml(yaml).unwrap();
let val: &f64 = settings.get("test_float");
assert_eq!(*val, 2.71);
}
#[test]
fn test_get_existing_string() {
let (mut settings, _dir) = create_test_settings();
let yaml = "test_string: \"world\"";
settings.load_yaml(yaml).unwrap();
let val: &String = settings.get("test_string");
assert_eq!(*val, "world");
}
#[test]
fn test_get_existing_vec_string() {
let (mut settings, _dir) = create_test_settings();
let yaml = r#"
test_vec:
- a
- b
- c
"#;
settings.load_yaml(yaml).unwrap();
let val: &Vec<String> = settings.get("test_vec");
assert_eq!(val.len(), 3);
assert_eq!(val[0], "a");
}
#[test]
fn test_get_missing_key_returns_default() {
let (settings, _dir) = create_test_settings();
let val: &bool = settings.get("nonexistent_bool");
assert!(!*val);
}
#[test]
fn test_get_optional_existing_key() {
let (mut settings, _dir) = create_test_settings();
let yaml = "test_bool: true";
settings.load_yaml(yaml).unwrap();
let val: Option<&bool> = settings.get_optional("test_bool");
assert_eq!(val, Some(&true));
}
#[test]
fn test_get_optional_missing_key() {
let (settings, _dir) = create_test_settings();
let val: Option<&bool> = settings.get_optional("nonexistent");
assert_eq!(val, None);
}
#[test]
fn test_get_type_mismatch_returns_default() {
let (mut settings, _dir) = create_test_settings();
let yaml = "test_int: 42";
settings.load_yaml(yaml).unwrap();
let val: &bool = settings.get("test_int");
assert!(!*val);
}
#[test]
fn test_get_optional_type_mismatch_returns_none() {
let (mut settings, _dir) = create_test_settings();
let yaml = "test_int: 42";
settings.load_yaml(yaml).unwrap();
let val: Option<&bool> = settings.get_optional("test_int");
assert_eq!(val, None);
}
#[test]
fn test_global_config_dir() {
let (settings, _dir) = create_test_settings();
let dir = settings.global_config_dir();
assert!(dir.is_absolute() || !dir.as_os_str().is_empty());
}
#[test]
fn test_project_config_dir() {
let (settings, _dir) = create_test_settings();
let config_dir = settings.project_config_dir();
let is_oo_dir = config_dir
.file_name()
.and_then(|s| s.to_str())
.map(|name| name == ".oo" || name == "oo")
.unwrap_or(false);
assert!(is_oo_dir);
}
#[test]
fn test_settings_merge_multiple_loads() {
let (mut settings, _dir) = create_test_settings();
let yaml1 = "key1: value1";
let yaml2 = "key2: value2";
settings.load_yaml(yaml1).unwrap();
settings.load_yaml(yaml2).unwrap();
assert!(settings.settings.contains_key("key1"));
assert!(settings.settings.contains_key("key2"));
}
#[test]
fn test_load_yaml_overwrites_previous() {
let (mut settings, _dir) = create_test_settings();
let yaml1 = "same_key: 1";
let yaml2 = "same_key: 2";
settings.load_yaml(yaml1).unwrap();
let val1: &i64 = settings.get("same_key");
assert_eq!(*val1, 1);
settings.load_yaml(yaml2).unwrap();
let val2: &i64 = settings.get("same_key");
assert_eq!(*val2, 2);
}
#[test]
fn test_to_string_helper_valid() {
let (mut settings, _dir) = create_test_settings();
let yaml = "test: hello";
settings.load_yaml(yaml).unwrap();
assert!(settings.settings.contains_key("test"));
}
#[test]
fn test_load_yaml_empty_document() {
let (mut settings, _dir) = create_test_settings();
let yaml = "";
let result = settings.load_yaml(yaml);
assert!(result.is_ok());
}
#[test]
fn test_language_lsp_falls_back_to_global() {
let (settings, _dir) = create_test_settings();
assert!(!*adapters::language::lsp::enable(&settings, "rust"));
}
#[test]
fn test_language_lsp_enable_parses() {
let (mut settings, _dir) = create_test_settings();
let yaml = r#"
languages:
cpp:
lsp:
enable: true
server_command: "clangd"
"#;
settings.load_yaml(yaml).unwrap();
assert!(*adapters::language::lsp::enable(&settings, "cpp"));
assert_eq!(adapters::language::lsp::server_command(&settings, "cpp"), "clangd");
}
#[test]
fn test_language_lsp_quoted_true_parses() {
let (mut settings, _dir) = create_test_settings();
let yaml = r#"
languages:
rust:
lsp:
enable: "true"
server_command: "rust-analyzer"
"#;
settings.load_yaml(yaml).unwrap();
assert!(!*adapters::language::lsp::enable(&settings, "rust"));
assert_eq!(adapters::language::lsp::server_command(&settings, "rust"), "rust-analyzer");
}
#[test]
fn test_language_lsp_uses_override() {
let (mut settings, _dir) = create_test_settings();
let yaml = r#"
languages:
rust:
lsp:
enable: true
server_command: "rust-analyzer"
"#;
settings.load_yaml(yaml).unwrap();
assert!(*adapters::language::lsp::enable(&settings, "rust"));
assert_eq!(adapters::language::lsp::server_command(&settings, "rust"), "rust-analyzer");
}
#[test]
fn test_multiple_languages_independent() {
let (mut settings, _dir) = create_test_settings();
let yaml = r#"
languages:
cpp:
lsp:
enable: true
server_command: "clangd"
rust:
lsp:
enable: false
server_command: "rust-analyzer"
"#;
settings.load_yaml(yaml).unwrap();
assert!(*adapters::language::lsp::enable(&settings, "cpp"));
assert_eq!(adapters::language::lsp::server_command(&settings, "cpp"), "clangd");
assert!(!*adapters::language::lsp::enable(&settings, "rust"));
assert_eq!(adapters::language::lsp::server_command(&settings, "rust"), "rust-analyzer");
}
#[test]
fn test_global_lsp_unaffected_by_language_override() {
let (mut settings, _dir) = create_test_settings();
let yaml = r#"
languages:
cpp:
lsp:
enable: true
"#;
settings.load_yaml(yaml).unwrap();
let global_enable: &bool = settings.get("lsp.enable");
assert!(!*global_enable);
}
}