Skip to main content

aaai_core/profile/
store.rs

1//! Audit profiles — named before/after/definition combos saved to
2//! `~/.aaai/profiles.yaml`.
3
4use serde::{Deserialize, Serialize};
5use std::path::PathBuf;
6
7/// One saved audit project configuration.
8#[derive(Debug, Clone, Serialize, Deserialize)]
9pub struct AuditProfile {
10    /// Display name for this profile.
11    pub name: String,
12    pub before: String,
13    pub after: String,
14    pub definition: Option<String>,
15    #[serde(default, skip_serializing_if = "Option::is_none")]
16    pub ignore_file: Option<String>,
17}
18
19/// Root document for `~/.aaai/profiles.yaml`.
20#[derive(Debug, Clone, Default, Serialize, Deserialize)]
21pub struct ProfileStore {
22    #[serde(default)]
23    pub profiles: Vec<AuditProfile>,
24}
25
26impl ProfileStore {
27    /// Load from `~/.aaai/profiles.yaml`, returning empty store if absent.
28    pub fn load() -> anyhow::Result<Self> {
29        let path = profile_path()?;
30        if !path.exists() {
31            return Ok(Self::default());
32        }
33        let text = std::fs::read_to_string(&path)?;
34        Ok(serde_yaml::from_str(&text)?)
35    }
36
37    /// Save to `~/.aaai/profiles.yaml`.
38    pub fn save(&self) -> anyhow::Result<()> {
39        let path = profile_path()?;
40        let yaml = serde_yaml::to_string(self)?;
41        std::fs::write(&path, yaml)?;
42        Ok(())
43    }
44
45    pub fn add(&mut self, profile: AuditProfile) {
46        // Replace existing profile with the same name.
47        if let Some(pos) = self.profiles.iter().position(|p| p.name == profile.name) {
48            self.profiles[pos] = profile;
49        } else {
50            self.profiles.push(profile);
51        }
52    }
53
54    pub fn remove(&mut self, name: &str) {
55        self.profiles.retain(|p| p.name != name);
56    }
57}
58
59fn profile_path() -> anyhow::Result<PathBuf> {
60    let base = dirs::home_dir()
61        .ok_or_else(|| anyhow::anyhow!("Cannot determine home directory"))?
62        .join(".aaai");
63    std::fs::create_dir_all(&base)?;
64    Ok(base.join("profiles.yaml"))
65}
66
67#[cfg(test)]
68mod tests {
69    use super::*;
70
71    #[test]
72    fn add_and_remove() {
73        let mut store = ProfileStore::default();
74        store.add(AuditProfile {
75            name: "prod".into(), before: "/before".into(),
76            after: "/after".into(), definition: None, ignore_file: None,
77        });
78        assert_eq!(store.profiles.len(), 1);
79        store.remove("prod");
80        assert!(store.profiles.is_empty());
81    }
82
83    #[test]
84    fn add_replaces_same_name() {
85        let mut store = ProfileStore::default();
86        store.add(AuditProfile {
87            name: "p".into(), before: "/a".into(),
88            after: "/b".into(), definition: None, ignore_file: None,
89        });
90        store.add(AuditProfile {
91            name: "p".into(), before: "/x".into(),
92            after: "/y".into(), definition: None, ignore_file: None,
93        });
94        assert_eq!(store.profiles.len(), 1);
95        assert_eq!(store.profiles[0].before, "/x");
96    }
97}