difflore-core 0.3.0

Core library for the difflore CLI — rule store, retrieval, MCP server, hooks, cloud sync. Not intended for direct use; depend on `difflore-cli` instead.
use crate::domain::models::AppSettingsRecord;
use crate::error::CoreError;

pub async fn get() -> crate::Result<AppSettingsRecord> {
    let dir = crate::infra::paths::data_home()?;
    std::fs::create_dir_all(&dir)?;
    let path = dir.join("settings.json");
    if !path.exists() {
        let defaults = AppSettingsRecord::default();
        let json = serde_json::to_string_pretty(&defaults)?;
        std::fs::write(&path, json)?;
        return Ok(defaults);
    }
    let content = std::fs::read_to_string(&path)?;
    let settings: AppSettingsRecord = serde_json::from_str(&content).map_err(|e| {
        CoreError::Internal(format!(
            "settings.json is corrupted and could not be parsed: {e}"
        ))
    })?;
    Ok(settings)
}

pub async fn update(input: AppSettingsRecord) -> crate::Result<AppSettingsRecord> {
    let dir = crate::infra::paths::data_home()?;
    std::fs::create_dir_all(&dir)?;
    let path = dir.join("settings.json");

    let mut current: serde_json::Value = if path.exists() {
        let content = std::fs::read_to_string(&path)?;
        serde_json::from_str(&content).map_err(|e| {
            CoreError::Internal(format!(
                "settings.json is corrupted and could not be parsed: {e}"
            ))
        })?
    } else {
        serde_json::json!({})
    };

    let patch: serde_json::Value = serde_json::to_value(&input)?;

    if let (Some(base), Some(overlay)) = (current.as_object_mut(), patch.as_object()) {
        for (k, v) in overlay {
            base.insert(k.clone(), v.clone());
        }
    }

    let merged: AppSettingsRecord = serde_json::from_value(current.clone())
        .map_err(|e| CoreError::Internal(format!("Failed to merge settings: {e}")))?;

    let json = serde_json::to_string_pretty(&merged)?;
    std::fs::write(&path, json)?;
    Ok(merged)
}

#[cfg(test)]
mod tests {
    use super::{get, update};
    use crate::domain::models::AppSettingsRecord;

    #[test]
    fn get_creates_missing_data_dir_before_default_settings_write() {
        let tmp = tempfile::TempDir::new().expect("tempdir");
        let home = tmp.path().join("nested").join("difflore-home");

        temp_env::with_var("DIFFLORE_HOME", Some(&home), || {
            let rt = tokio::runtime::Builder::new_current_thread()
                .enable_all()
                .build()
                .expect("runtime");
            let settings = rt.block_on(get()).expect("settings");

            assert_eq!(settings.theme, "dark");
            assert!(home.is_dir(), "settings::get should create DIFFLORE_HOME");
            assert!(
                home.join("settings.json").is_file(),
                "settings::get should write default settings"
            );
        });
    }

    #[test]
    fn update_rejects_corrupt_settings_without_overwriting() {
        let tmp = tempfile::TempDir::new().expect("tempdir");
        let home = tmp.path().join("difflore-home");
        std::fs::create_dir_all(&home).expect("home dir");
        let path = home.join("settings.json");
        std::fs::write(&path, "{broken").expect("write corrupt settings");

        temp_env::with_var("DIFFLORE_HOME", Some(&home), || {
            let rt = tokio::runtime::Builder::new_current_thread()
                .enable_all()
                .build()
                .expect("runtime");
            let err = rt
                .block_on(update(AppSettingsRecord::default()))
                .expect_err("corrupt settings should fail");

            assert!(
                err.to_string().contains("settings.json is corrupted"),
                "unexpected error: {err}"
            );
            assert_eq!(
                std::fs::read_to_string(&path).expect("settings still exists"),
                "{broken"
            );
        });
    }
}