Skip to main content

aida_core/db/
yaml_backend.rs

1//! YAML file storage backend
2//!
3//! This backend stores all data in a single YAML file, using the existing
4//! Storage implementation with file locking support.
5
6use anyhow::Result;
7use std::path::{Path, PathBuf};
8
9use super::traits::{BackendType, DatabaseBackend};
10use crate::models::RequirementsStore;
11use crate::storage::Storage;
12
13/// YAML file backend implementation
14///
15/// This wraps the existing Storage class to implement the DatabaseBackend trait,
16/// providing compatibility with the existing codebase while enabling the new
17/// abstraction layer.
18pub struct YamlBackend {
19    storage: Storage,
20    path: PathBuf,
21}
22
23impl YamlBackend {
24    /// Creates a new YAML backend for the given file path
25    pub fn new<P: AsRef<Path>>(path: P) -> Self {
26        let path = path.as_ref().to_path_buf();
27        Self {
28            storage: Storage::new(&path),
29            path,
30        }
31    }
32
33    /// Gets a reference to the underlying Storage
34    pub fn storage(&self) -> &Storage {
35        &self.storage
36    }
37}
38
39impl DatabaseBackend for YamlBackend {
40    fn backend_type(&self) -> BackendType {
41        BackendType::Yaml
42    }
43
44    fn path(&self) -> &Path {
45        &self.path
46    }
47
48    fn load(&self) -> Result<RequirementsStore> {
49        self.storage.load()
50    }
51
52    fn save(&self, store: &RequirementsStore) -> Result<()> {
53        self.storage.save(store)
54    }
55
56    fn update_atomically<F>(&self, update_fn: F) -> Result<RequirementsStore>
57    where
58        F: FnOnce(&mut RequirementsStore),
59    {
60        self.storage.update_atomically(update_fn)
61    }
62
63    /// Creates a baseline with git tagging support for YAML backend
64    fn create_baseline(
65        &self,
66        name: String,
67        description: Option<String>,
68        created_by: String,
69    ) -> Result<crate::models::Baseline> {
70        let mut store = self.load()?;
71        let mut baseline = store.create_baseline(name, description, created_by).clone();
72
73        // Try to create a git tag for this baseline
74        if let Some(git_tag) = self.create_git_tag_for_baseline(&baseline) {
75            // Update the baseline with the git tag
76            if let Some(b) = store.baselines.iter_mut().find(|b| b.id == baseline.id) {
77                b.git_tag = Some(git_tag.clone());
78                baseline.git_tag = Some(git_tag);
79            }
80        }
81
82        self.save(&store)?;
83        Ok(baseline)
84    }
85}
86
87impl YamlBackend {
88    /// Attempts to create a git tag for a baseline
89    /// Returns the tag name if successful, None if git is not available or fails
90    fn create_git_tag_for_baseline(&self, baseline: &crate::models::Baseline) -> Option<String> {
91        use std::process::Command;
92
93        // Get the directory containing the YAML file
94        let dir = self.path.parent()?;
95
96        // Check if we're in a git repository
97        let git_check = Command::new("git")
98            .args(["rev-parse", "--git-dir"])
99            .current_dir(dir)
100            .output()
101            .ok()?;
102
103        if !git_check.status.success() {
104            return None; // Not a git repo
105        }
106
107        let tag_name = baseline.git_tag_name();
108        let message = baseline.description.as_deref().unwrap_or(&baseline.name);
109
110        // Create an annotated tag
111        let result = Command::new("git")
112            .args(["tag", "-a", &tag_name, "-m", message])
113            .current_dir(dir)
114            .output()
115            .ok()?;
116
117        if result.status.success() {
118            Some(tag_name)
119        } else {
120            // Tag might already exist or other error
121            None
122        }
123    }
124
125    /// Lists git tags that match the baseline pattern
126    #[allow(dead_code)]
127    pub fn list_git_baseline_tags(&self) -> Vec<String> {
128        use std::process::Command;
129
130        let Some(dir) = self.path.parent() else {
131            return Vec::new();
132        };
133
134        let output = Command::new("git")
135            .args(["tag", "-l", "baseline-*"])
136            .current_dir(dir)
137            .output()
138            .ok();
139
140        match output {
141            Some(o) if o.status.success() => String::from_utf8_lossy(&o.stdout)
142                .lines()
143                .map(|s| s.to_string())
144                .collect(),
145            _ => Vec::new(),
146        }
147    }
148}
149
150#[cfg(test)]
151mod tests {
152    use super::*;
153    use tempfile::{NamedTempFile, TempDir};
154
155    #[test]
156    fn test_yaml_backend_create_and_load() {
157        // Use a path that doesn't exist yet
158        let temp_dir = TempDir::new().unwrap();
159        let file_path = temp_dir.path().join("test.yaml");
160        let backend = YamlBackend::new(&file_path);
161
162        // Should create file with empty store
163        backend.create_if_not_exists().unwrap();
164
165        let store = backend.load().unwrap();
166        assert!(store.requirements.is_empty());
167        assert!(store.users.is_empty());
168    }
169
170    #[test]
171    fn test_yaml_backend_save_and_load() {
172        let temp_file = NamedTempFile::new().unwrap();
173        let backend = YamlBackend::new(temp_file.path());
174
175        let mut store = RequirementsStore::new();
176        store.name = "Test DB".to_string();
177        store.title = "Test Database".to_string();
178
179        backend.save(&store).unwrap();
180
181        let loaded = backend.load().unwrap();
182        assert_eq!(loaded.name, "Test DB");
183        assert_eq!(loaded.title, "Test Database");
184    }
185}