aida_core/db/
yaml_backend.rs1use anyhow::Result;
7use std::path::{Path, PathBuf};
8
9use super::traits::{BackendType, DatabaseBackend};
10use crate::models::RequirementsStore;
11use crate::storage::Storage;
12
13pub struct YamlBackend {
19 storage: Storage,
20 path: PathBuf,
21}
22
23impl YamlBackend {
24 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 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 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 if let Some(git_tag) = self.create_git_tag_for_baseline(&baseline) {
75 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 fn create_git_tag_for_baseline(&self, baseline: &crate::models::Baseline) -> Option<String> {
91 use std::process::Command;
92
93 let dir = self.path.parent()?;
95
96 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; }
106
107 let tag_name = baseline.git_tag_name();
108 let message = baseline.description.as_deref().unwrap_or(&baseline.name);
109
110 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 None
122 }
123 }
124
125 #[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 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 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}