1use async_trait::async_trait;
4use std::collections::BTreeMap;
5use std::sync::Arc;
6use tokio::sync::RwLock;
7
8use super::{BaselineStore, StorageHealth};
9use crate::error::StoreError;
10use crate::models::{
11 BaselineRecord, BaselineSummary, BaselineVersion, ListBaselinesQuery, ListBaselinesResponse,
12 PaginationInfo,
13};
14
15#[derive(Debug, Default)]
17pub struct InMemoryStore {
18 #[allow(clippy::type_complexity)]
19 baselines: Arc<RwLock<BTreeMap<(String, String, String), BaselineRecord>>>,
20}
21
22impl InMemoryStore {
23 pub fn new() -> Self {
25 Self {
26 baselines: Arc::new(RwLock::new(BTreeMap::new())),
27 }
28 }
29
30 fn key(project: &str, benchmark: &str, version: &str) -> (String, String, String) {
31 (
32 project.to_string(),
33 benchmark.to_string(),
34 version.to_string(),
35 )
36 }
37}
38
39#[async_trait]
40impl BaselineStore for InMemoryStore {
41 async fn create(&self, record: &BaselineRecord) -> Result<(), StoreError> {
42 let key = Self::key(&record.project, &record.benchmark, &record.version);
43 let mut baselines = self.baselines.write().await;
44
45 if baselines.contains_key(&key) {
46 return Err(StoreError::AlreadyExists(format!(
47 "project={}, benchmark={}, version={}",
48 record.project, record.benchmark, record.version
49 )));
50 }
51
52 baselines.insert(key, record.clone());
53 Ok(())
54 }
55
56 async fn get(
57 &self,
58 project: &str,
59 benchmark: &str,
60 version: &str,
61 ) -> Result<Option<BaselineRecord>, StoreError> {
62 let key = Self::key(project, benchmark, version);
63 let baselines = self.baselines.read().await;
64 Ok(baselines.get(&key).filter(|r| !r.deleted).cloned())
65 }
66
67 async fn get_latest(
68 &self,
69 project: &str,
70 benchmark: &str,
71 ) -> Result<Option<BaselineRecord>, StoreError> {
72 let baselines = self.baselines.read().await;
73 let latest = baselines
74 .values()
75 .filter(|r| r.project == project && r.benchmark == benchmark && !r.deleted)
76 .max_by_key(|r| r.created_at);
77 Ok(latest.cloned())
78 }
79
80 #[allow(clippy::collapsible_if)]
81 async fn list(
82 &self,
83 project: &str,
84 query: &ListBaselinesQuery,
85 ) -> Result<ListBaselinesResponse, StoreError> {
86 let baselines = self.baselines.read().await;
87 let parsed_tags = query.parsed_tags();
88
89 let mut filtered: Vec<_> = baselines
90 .values()
91 .filter(|r| {
92 if r.project != project || r.deleted {
94 return false;
95 }
96
97 if let Some(ref b) = query.benchmark {
99 if &r.benchmark != b {
100 return false;
101 }
102 }
103
104 if let Some(ref p) = query.benchmark_prefix {
106 if !r.benchmark.starts_with(p) {
107 return false;
108 }
109 }
110
111 if let Some(ref gr) = query.git_ref {
113 if r.git_ref.as_deref() != Some(gr) {
114 return false;
115 }
116 }
117
118 if let Some(ref gs) = query.git_sha {
120 if r.git_sha.as_deref() != Some(gs) {
121 return false;
122 }
123 }
124
125 if let Some(since) = query.since {
127 if r.created_at < since {
128 return false;
129 }
130 }
131
132 if let Some(until) = query.until {
134 if r.created_at > until {
135 return false;
136 }
137 }
138
139 if let Some(ref required_tags) = parsed_tags {
141 for tag in required_tags {
142 if !r.tags.contains(tag) {
143 return false;
144 }
145 }
146 }
147
148 true
149 })
150 .collect();
151
152 filtered.sort_by(|a, b| b.created_at.cmp(&a.created_at));
153
154 let total = filtered.len() as u64;
155 let offset = query.offset as usize;
156 let limit = query.limit as usize;
157
158 let paginated: Vec<_> = filtered
159 .into_iter()
160 .skip(offset)
161 .take(limit)
162 .map(|r| {
163 let mut summary: BaselineSummary = r.clone().into();
164 if query.include_receipt {
165 summary.receipt = Some(r.receipt.clone());
166 }
167 summary
168 })
169 .collect();
170
171 let has_more = (offset + paginated.len()) < total as usize;
172
173 Ok(ListBaselinesResponse {
174 baselines: paginated,
175 pagination: PaginationInfo {
176 total,
177 limit: query.limit,
178 offset: query.offset,
179 has_more,
180 },
181 })
182 }
183
184 async fn update(&self, record: &BaselineRecord) -> Result<(), StoreError> {
185 let key = Self::key(&record.project, &record.benchmark, &record.version);
186 let mut baselines = self.baselines.write().await;
187
188 if !baselines.contains_key(&key) {
189 return Err(StoreError::NotFound(format!(
190 "project={}, benchmark={}, version={}",
191 record.project, record.benchmark, record.version
192 )));
193 }
194
195 baselines.insert(key, record.clone());
196 Ok(())
197 }
198
199 async fn delete(
200 &self,
201 project: &str,
202 benchmark: &str,
203 version: &str,
204 ) -> Result<bool, StoreError> {
205 let key = Self::key(project, benchmark, version);
206 let mut baselines = self.baselines.write().await;
207
208 if let Some(record) = baselines.get_mut(&key) {
209 if record.deleted {
210 return Ok(false);
211 }
212 record.deleted = true;
213 return Ok(true);
214 }
215
216 Ok(false)
217 }
218
219 async fn hard_delete(
220 &self,
221 project: &str,
222 benchmark: &str,
223 version: &str,
224 ) -> Result<bool, StoreError> {
225 let key = Self::key(project, benchmark, version);
226 let mut baselines = self.baselines.write().await;
227 Ok(baselines.remove(&key).is_some())
228 }
229
230 async fn list_versions(
231 &self,
232 project: &str,
233 benchmark: &str,
234 ) -> Result<Vec<BaselineVersion>, StoreError> {
235 let baselines = self.baselines.read().await;
236
237 let mut versions: Vec<_> = baselines
238 .values()
239 .filter(|r| r.project == project && r.benchmark == benchmark && !r.deleted)
240 .map(|r| BaselineVersion {
241 version: r.version.clone(),
242 git_ref: r.git_ref.clone(),
243 git_sha: r.git_sha.clone(),
244 created_at: r.created_at,
245 created_by: None,
246 is_current: false,
247 source: r.source.clone(),
248 })
249 .collect();
250
251 versions.sort_by(|a, b| b.created_at.cmp(&a.created_at));
252
253 if let Some(first) = versions.first_mut() {
254 first.is_current = true;
255 }
256
257 Ok(versions)
258 }
259
260 async fn health_check(&self) -> Result<StorageHealth, StoreError> {
261 Ok(StorageHealth::Healthy)
262 }
263
264 fn backend_type(&self) -> &'static str {
265 "memory"
266 }
267}