sashiko 0.2.1

Agentic code review system for Linux kernel
Documentation
use serde::Deserialize;
use std::collections::HashMap;
use std::fs;
use std::path::Path;

#[derive(Deserialize, Debug, Clone, Default)]
pub struct PatchworkPolicy {
    #[serde(default)]
    pub enabled: bool,
    pub api_url: Option<String>,
    pub token: Option<String>,
}

#[derive(Deserialize, Debug, Clone, Default)]
pub struct EmailPolicyConfig {
    #[serde(default)]
    pub defaults: SubsystemPolicy,
    #[serde(default)]
    pub subsystems: HashMap<String, SubsystemPolicy>,
}

#[derive(Deserialize, Debug, Clone, Default)]
pub struct SubsystemPolicy {
    #[serde(default)]
    pub lists: Vec<String>,
    #[serde(default)]
    pub reply_all: bool,
    #[serde(default)]
    pub reply_to_author: bool,
    #[serde(default)]
    pub cc_individuals: bool,
    #[serde(default)]
    pub mute_all: bool,
    #[serde(default)]
    pub cc: Vec<String>,
    #[serde(default)]
    pub ignored_emails: Vec<String>,
    #[serde(default)]
    pub subject_prefixes: Vec<String>,
    #[serde(default)]
    pub patchwork: PatchworkPolicy,
    #[serde(default)]
    pub embargo_hours: Option<u32>,
    #[serde(default)]
    pub send_positive_review: bool,
}

impl EmailPolicyConfig {
    /// Loads the email policy configuration from a TOML file.
    /// Returns a default configuration if the file does not exist.
    pub fn load(path: impl AsRef<Path>) -> Result<Self, Box<dyn std::error::Error + Send + Sync>> {
        let path = path.as_ref();
        if !path.exists() {
            return Ok(Self {
                defaults: SubsystemPolicy::default(),
                subsystems: HashMap::new(),
            });
        }

        let content = fs::read_to_string(path)?;
        let config: Self = toml::from_str(&content)?;
        Ok(config)
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::io::Write;
    use tempfile::NamedTempFile;

    #[test]
    fn test_load_policy() {
        let toml_content = r#"
            [defaults]
            reply_all = false
            reply_to_author = true
            cc_individuals = true
            mute_all = false
            cc = []

            [subsystems.mm]
            lists = ["linux-mm@kvack.org", "linux-mm@vger.kernel.org"]
            reply_all = true
            reply_to_author = true
            cc_individuals = true

            [subsystems.bpf]
            lists = ["bpf@vger.kernel.org"]
            reply_all = false
            reply_to_author = true
            cc_individuals = false

            [subsystems.net]
            lists = ["netdev@vger.kernel.org"]
            mute_all = true

            [subsystems.net.patchwork]
            enabled = true
            api_url = "https://patchwork.kernel.org/api/1.2"
        "#;

        let mut file = NamedTempFile::new().unwrap();
        write!(file, "{}", toml_content).unwrap();

        let config = EmailPolicyConfig::load(file.path()).expect("Failed to load policy");

        assert!(!config.defaults.reply_all);
        assert!(config.defaults.reply_to_author);
        assert!(!config.defaults.patchwork.enabled);

        let mm_policy = config.subsystems.get("mm").expect("mm subsystem missing");
        assert_eq!(
            mm_policy.lists,
            vec!["linux-mm@kvack.org", "linux-mm@vger.kernel.org"]
        );
        assert!(mm_policy.reply_all);
        assert!(!mm_policy.patchwork.enabled);

        let bpf_policy = config.subsystems.get("bpf").expect("bpf subsystem missing");
        assert!(!bpf_policy.reply_all);
        assert!(bpf_policy.reply_to_author);
        assert!(!bpf_policy.cc_individuals);

        let net_policy = config.subsystems.get("net").expect("net subsystem missing");
        assert!(net_policy.mute_all);
        assert!(net_policy.patchwork.enabled);
        assert_eq!(
            net_policy.patchwork.api_url.as_deref(),
            Some("https://patchwork.kernel.org/api/1.2")
        );
    }

    #[test]
    fn test_load_missing_policy() {
        let config = EmailPolicyConfig::load("non_existent_file.toml")
            .expect("Failed to load default policy");
        assert!(!config.defaults.reply_to_author);
        assert!(config.subsystems.is_empty());
    }
}