gim-config 0.3.0

Configuration file manager library for GIM applications
Documentation
use std::{fs, io::{Error, ErrorKind, Write as _, Result}};
use std::path::PathBuf;
use toml::{Value, map};

use crate::directory::config_dir;

fn get_config_file() -> Result<PathBuf> {
    let config_dir = config_dir()?;
    let config_file = config_dir.join("config.toml");
    Ok(config_file)
}

pub fn get_config() -> Result<Value> {
    get_config_into_toml(true)
}

pub fn get_config_into_toml(log_dir: bool) -> Result<Value> {
    let config_file = get_config_file().expect("Failed to get config file");
    if !config_file.exists() {
        if let Some(parent) = config_file.parent() {
            fs::create_dir_all(parent)?;
        } else {
            return Err(Error::new(
                ErrorKind::NotFound,
                "config directory not found",
            ));
        }
        let mut update_table = map::Map::new();
        update_table.insert("tried".to_string(), Value::Integer(0));
        update_table.insert("max_try".to_string(), Value::Integer(5));
        update_table.insert(
            "last_try_day".to_string(),
            Value::String("2000-01-01".to_string()),
        );
        update_table.insert("try_interval_days".to_string(), Value::Integer(30));

        let mut ai_table = map::Map::new();
        ai_table.insert("model".to_string(), Value::String(String::new()));
        ai_table.insert("apikey".to_string(), Value::String(String::new()));
        ai_table.insert("url".to_string(), Value::String(String::new()));
        ai_table.insert("language".to_string(), Value::String("English".to_string()));

        let mut default_content = map::Map::new();
        default_content.insert("update".to_string(), Value::Table(update_table));
        default_content.insert("ai".to_string(), Value::Table(ai_table));
        let default_content = toml::to_string(&Value::Table(default_content))
            .map_err(|e| Error::new(ErrorKind::InvalidData, e))?;
        let mut file = fs::File::create(&config_file)?;
        file.write_all(default_content.as_bytes())?;
    }
    if log_dir {
        println!("Config file is {}", config_file.display());
    }
    let content = fs::read_to_string(&config_file)?;
    let config: Value = toml::from_str(&content)
        .map_err(|e| Error::new(ErrorKind::InvalidData, e))?;
    Ok(config)
}

pub fn get_config_value(section: &str, key: &str) -> Result<Value> {
    let config = get_config()?;
    let section_table = config
        .get(section)
        .ok_or_else(|| {
            Error::new(
                ErrorKind::NotFound,
                format!("Section '{}' not found", section),
            )
        })?
        .as_table()
        .ok_or_else(|| {
            Error::new(
                ErrorKind::InvalidData,
                format!("Section '{}' is not a table", section),
            )
        })?;

    section_table
        .get(key)
        .ok_or_else(|| {
            Error::new(
                ErrorKind::NotFound,
                format!("Key '{}' not found in section '{}'", key, section),
            )
        })
        .map(|v| v.clone())
}

pub fn update_config_value(section: &str, key: &str, value: Value) -> Result<()> {
    let mut config = get_config_into_toml(false)?;
    let section_table = config
        .get_mut(section)
        .ok_or_else(|| {
            Error::new(
                ErrorKind::NotFound,
                format!("Section '{}' not found", section),
            )
        })?
        .as_table_mut()
        .ok_or_else(|| {
            Error::new(
                ErrorKind::InvalidData,
                format!("Section '{}' is not a table", section),
            )
        })?;

    if let Some(existing_value) = section_table.get(key) {
        if existing_value == &value {
            return Ok(());
        }
    }

    section_table.insert(key.to_string(), value);
    save_config(&config)?;
    Ok(())
}

pub fn save_config(config: &Value) -> Result<()> {
    let updated_content = toml::to_string(config)
        .map_err(|e| Error::new(ErrorKind::InvalidData, e))?;
    let config_dir = get_config_file()?;
    fs::write(&config_dir, updated_content)?;
    Ok(())
}

#[cfg(test)]
mod tests {
    use crate::config::get_config;

    #[test]
    fn test_ensure_config_file_exists_creates_file() {
        let parsed = get_config().unwrap();
        let update = parsed.get("update");
        let ai = parsed.get("ai");
        assert!(update.is_some(), "Missing update section");
        assert!(ai.is_some(), "Missing ai section");

        let ai_table = ai.unwrap().as_table().unwrap();
        assert!(ai_table.contains_key("model"), "Missing model field");
        assert!(ai_table.contains_key("apikey"), "Missing apikey field");
        assert!(ai_table.contains_key("url"), "Missing url field");
        assert!(ai_table.contains_key("language"), "Missing language field");
        print!("{:?}", parsed)
    }
}