1use async_trait::async_trait;
4use std::collections::BTreeMap;
5use std::sync::Arc;
6use tokio::sync::RwLock;
7
8use super::{AuditStore, BaselineStore, StorageHealth};
9use crate::error::StoreError;
10use crate::models::{
11 AuditEvent, BaselineRecord, BaselineSummary, BaselineVersion, DecisionRecord,
12 ListAuditEventsQuery, ListAuditEventsResponse, ListBaselinesQuery, ListBaselinesResponse,
13 ListDecisionsQuery, ListDecisionsResponse, ListVerdictsQuery, ListVerdictsResponse,
14 PaginationInfo, PruneDecisionsResponse, VerdictRecord,
15};
16
17#[derive(Debug, Default)]
19pub struct InMemoryStore {
20 #[allow(clippy::type_complexity)]
21 baselines: Arc<RwLock<BTreeMap<(String, String, String), BaselineRecord>>>,
22 verdicts: Arc<RwLock<Vec<VerdictRecord>>>,
23 decisions: Arc<RwLock<Vec<DecisionRecord>>>,
24 audit_events: Arc<RwLock<Vec<AuditEvent>>>,
25}
26
27impl InMemoryStore {
28 pub fn new() -> Self {
30 Self {
31 baselines: Arc::new(RwLock::new(BTreeMap::new())),
32 verdicts: Arc::new(RwLock::new(Vec::new())),
33 decisions: Arc::new(RwLock::new(Vec::new())),
34 audit_events: Arc::new(RwLock::new(Vec::new())),
35 }
36 }
37
38 fn key(project: &str, benchmark: &str, version: &str) -> (String, String, String) {
39 (
40 project.to_string(),
41 benchmark.to_string(),
42 version.to_string(),
43 )
44 }
45}
46
47#[async_trait]
48impl BaselineStore for InMemoryStore {
49 async fn create(&self, record: &BaselineRecord) -> Result<(), StoreError> {
50 let key = Self::key(&record.project, &record.benchmark, &record.version);
51 let mut baselines = self.baselines.write().await;
52
53 if baselines.contains_key(&key) {
54 return Err(StoreError::AlreadyExists(format!(
55 "project={}, benchmark={}, version={}",
56 record.project, record.benchmark, record.version
57 )));
58 }
59
60 baselines.insert(key, record.clone());
61 Ok(())
62 }
63
64 async fn get(
65 &self,
66 project: &str,
67 benchmark: &str,
68 version: &str,
69 ) -> Result<Option<BaselineRecord>, StoreError> {
70 let key = Self::key(project, benchmark, version);
71 let baselines = self.baselines.read().await;
72 Ok(baselines.get(&key).filter(|r| !r.deleted).cloned())
73 }
74
75 async fn get_latest(
76 &self,
77 project: &str,
78 benchmark: &str,
79 ) -> Result<Option<BaselineRecord>, StoreError> {
80 let baselines = self.baselines.read().await;
81 let latest = baselines
82 .values()
83 .filter(|r| r.project == project && r.benchmark == benchmark && !r.deleted)
84 .max_by_key(|r| r.created_at);
85 Ok(latest.cloned())
86 }
87
88 #[allow(clippy::collapsible_if)]
89 async fn list(
90 &self,
91 project: &str,
92 query: &ListBaselinesQuery,
93 ) -> Result<ListBaselinesResponse, StoreError> {
94 let baselines = self.baselines.read().await;
95 let parsed_tags = query.parsed_tags();
96
97 let mut filtered: Vec<_> = baselines
98 .values()
99 .filter(|r| {
100 if r.project != project || r.deleted {
102 return false;
103 }
104
105 if let Some(ref b) = query.benchmark {
107 if &r.benchmark != b {
108 return false;
109 }
110 }
111
112 if let Some(ref p) = query.benchmark_prefix {
114 if !r.benchmark.starts_with(p) {
115 return false;
116 }
117 }
118
119 if let Some(ref gr) = query.git_ref {
121 if r.git_ref.as_deref() != Some(gr) {
122 return false;
123 }
124 }
125
126 if let Some(ref gs) = query.git_sha {
128 if r.git_sha.as_deref() != Some(gs) {
129 return false;
130 }
131 }
132
133 if let Some(since) = query.since {
135 if r.created_at < since {
136 return false;
137 }
138 }
139
140 if let Some(until) = query.until {
142 if r.created_at > until {
143 return false;
144 }
145 }
146
147 if !parsed_tags.is_empty() {
149 for tag in &parsed_tags {
150 if !r.tags.contains(tag) {
151 return false;
152 }
153 }
154 }
155
156 true
157 })
158 .collect();
159
160 filtered.sort_by_key(|b| std::cmp::Reverse(b.created_at));
161
162 let total = filtered.len() as u64;
163 let offset = query.offset as usize;
164 let limit = query.limit as usize;
165
166 let paginated: Vec<_> = filtered
167 .into_iter()
168 .skip(offset)
169 .take(limit)
170 .map(|r| {
171 let mut summary: BaselineSummary = r.clone().into();
172 if query.include_receipt {
173 summary.receipt = Some(r.receipt.clone());
174 }
175 summary
176 })
177 .collect();
178
179 let has_more = (offset + paginated.len()) < total as usize;
180
181 Ok(ListBaselinesResponse {
182 baselines: paginated,
183 pagination: PaginationInfo {
184 total,
185 limit: query.limit,
186 offset: query.offset,
187 has_more,
188 },
189 })
190 }
191
192 async fn update(&self, record: &BaselineRecord) -> Result<(), StoreError> {
193 let key = Self::key(&record.project, &record.benchmark, &record.version);
194 let mut baselines = self.baselines.write().await;
195
196 if !baselines.contains_key(&key) {
197 return Err(StoreError::NotFound(format!(
198 "project={}, benchmark={}, version={}",
199 record.project, record.benchmark, record.version
200 )));
201 }
202
203 baselines.insert(key, record.clone());
204 Ok(())
205 }
206
207 async fn delete(
208 &self,
209 project: &str,
210 benchmark: &str,
211 version: &str,
212 ) -> Result<bool, StoreError> {
213 let key = Self::key(project, benchmark, version);
214 let mut baselines = self.baselines.write().await;
215
216 if let Some(record) = baselines.get_mut(&key) {
217 if record.deleted {
218 return Ok(false);
219 }
220 record.deleted = true;
221 return Ok(true);
222 }
223
224 Ok(false)
225 }
226
227 async fn hard_delete(
228 &self,
229 project: &str,
230 benchmark: &str,
231 version: &str,
232 ) -> Result<bool, StoreError> {
233 let key = Self::key(project, benchmark, version);
234 let mut baselines = self.baselines.write().await;
235 Ok(baselines.remove(&key).is_some())
236 }
237
238 async fn list_versions(
239 &self,
240 project: &str,
241 benchmark: &str,
242 ) -> Result<Vec<BaselineVersion>, StoreError> {
243 let baselines = self.baselines.read().await;
244
245 let mut versions: Vec<_> = baselines
246 .values()
247 .filter(|r| r.project == project && r.benchmark == benchmark && !r.deleted)
248 .map(|r| BaselineVersion {
249 version: r.version.clone(),
250 git_ref: r.git_ref.clone(),
251 git_sha: r.git_sha.clone(),
252 created_at: r.created_at,
253 created_by: None,
254 is_current: false,
255 source: r.source.clone(),
256 })
257 .collect();
258
259 versions.sort_by_key(|b| std::cmp::Reverse(b.created_at));
260
261 if let Some(first) = versions.first_mut() {
262 first.is_current = true;
263 }
264
265 Ok(versions)
266 }
267
268 async fn health_check(&self) -> Result<StorageHealth, StoreError> {
269 Ok(StorageHealth::Healthy)
270 }
271
272 fn backend_type(&self) -> &'static str {
273 "memory"
274 }
275
276 async fn create_verdict(&self, record: &VerdictRecord) -> Result<(), StoreError> {
277 let mut verdicts = self.verdicts.write().await;
278 verdicts.push(record.clone());
279 Ok(())
280 }
281
282 async fn list_verdicts(
283 &self,
284 project: &str,
285 query: &ListVerdictsQuery,
286 ) -> Result<ListVerdictsResponse, StoreError> {
287 let verdicts = self.verdicts.read().await;
288
289 let mut filtered: Vec<_> = verdicts
290 .iter()
291 .filter(|r| {
292 if r.project != project {
293 return false;
294 }
295
296 if let Some(ref b) = query.benchmark
297 && &r.benchmark != b
298 {
299 return false;
300 }
301
302 if let Some(ref s) = query.status
303 && &r.status != s
304 {
305 return false;
306 }
307
308 if let Some(since) = query.since
309 && r.created_at < since
310 {
311 return false;
312 }
313
314 if let Some(until) = query.until
315 && r.created_at > until
316 {
317 return false;
318 }
319
320 true
321 })
322 .cloned()
323 .collect();
324
325 filtered.sort_by_key(|b| std::cmp::Reverse(b.created_at));
326
327 let total = filtered.len() as u64;
328 let offset = query.offset as usize;
329 let limit = query.limit as usize;
330
331 let paginated: Vec<_> = filtered.into_iter().skip(offset).take(limit).collect();
332
333 let has_more = (offset + paginated.len()) < total as usize;
334
335 Ok(ListVerdictsResponse {
336 verdicts: paginated,
337 pagination: PaginationInfo {
338 total,
339 limit: query.limit,
340 offset: query.offset,
341 has_more,
342 },
343 })
344 }
345
346 async fn create_decision(&self, record: &DecisionRecord) -> Result<(), StoreError> {
347 let mut decisions = self.decisions.write().await;
348 decisions.push(record.clone());
349 Ok(())
350 }
351
352 async fn latest_decision(&self, project: &str) -> Result<Option<DecisionRecord>, StoreError> {
353 let decisions = self.decisions.read().await;
354 Ok(decisions
355 .iter()
356 .filter(|record| record.project == project)
357 .max_by_key(|record| record.created_at)
358 .cloned())
359 }
360
361 async fn list_decisions(
362 &self,
363 project: &str,
364 query: &ListDecisionsQuery,
365 ) -> Result<ListDecisionsResponse, StoreError> {
366 let decisions = self.decisions.read().await;
367 let mut filtered: Vec<_> = decisions
368 .iter()
369 .filter(|record| {
370 if record.project != project {
371 return false;
372 }
373 if let Some(ref scenario) = query.scenario
374 && record.scenario.as_deref() != Some(scenario)
375 {
376 return false;
377 }
378 if let Some(status) = query.status
379 && record.status != status
380 {
381 return false;
382 }
383 if let Some(verdict) = query.verdict
384 && record.verdict != verdict
385 {
386 return false;
387 }
388 if let Some(review_required) = query.review_required
389 && record.review_required != review_required
390 {
391 return false;
392 }
393 if let Some(accepted) = query.accepted {
394 let has_accepted_tradeoff = !record.accepted_rules.is_empty();
395 if has_accepted_tradeoff != accepted {
396 return false;
397 }
398 }
399 if let Some(ref rule) = query.rule
400 && !record
401 .accepted_rules
402 .iter()
403 .any(|accepted| accepted == rule)
404 {
405 return false;
406 }
407 true
408 })
409 .cloned()
410 .collect();
411
412 filtered.sort_by_key(|record| std::cmp::Reverse(record.created_at));
413 let total = filtered.len() as u64;
414 let offset = query.offset as usize;
415 let limit = query.limit as usize;
416 let paginated: Vec<_> = filtered.into_iter().skip(offset).take(limit).collect();
417 let has_more = (offset + paginated.len()) < total as usize;
418
419 Ok(ListDecisionsResponse {
420 decisions: paginated,
421 pagination: PaginationInfo {
422 total,
423 limit: query.limit,
424 offset: query.offset,
425 has_more,
426 },
427 })
428 }
429
430 async fn prune_decisions(
431 &self,
432 project: &str,
433 older_than: chrono::DateTime<chrono::Utc>,
434 dry_run: bool,
435 ) -> Result<PruneDecisionsResponse, StoreError> {
436 let mut decisions = self.decisions.write().await;
437 let decision_ids: Vec<String> = decisions
438 .iter()
439 .filter(|record| record.project == project && record.created_at < older_than)
440 .map(|record| record.id.clone())
441 .collect();
442 let matched = decision_ids.len() as u64;
443
444 let deleted = if dry_run {
445 0
446 } else {
447 decisions
448 .retain(|record| !(record.project == project && record.created_at < older_than));
449 matched
450 };
451
452 Ok(PruneDecisionsResponse {
453 project: project.to_string(),
454 older_than,
455 dry_run,
456 matched,
457 deleted,
458 decision_ids,
459 })
460 }
461}
462
463#[async_trait]
464impl AuditStore for InMemoryStore {
465 async fn log_event(&self, event: &AuditEvent) -> Result<(), StoreError> {
466 let mut events = self.audit_events.write().await;
467 events.push(event.clone());
468 Ok(())
469 }
470
471 async fn list_events(
472 &self,
473 query: &ListAuditEventsQuery,
474 ) -> Result<ListAuditEventsResponse, StoreError> {
475 let events = self.audit_events.read().await;
476
477 let mut filtered: Vec<_> = events
478 .iter()
479 .filter(|e| {
480 if let Some(ref project) = query.project
481 && &e.project != project
482 {
483 return false;
484 }
485
486 if let Some(ref action) = query.action
487 && e.action.to_string() != *action
488 {
489 return false;
490 }
491
492 if let Some(ref resource_type) = query.resource_type
493 && e.resource_type.to_string() != *resource_type
494 {
495 return false;
496 }
497
498 if let Some(ref actor) = query.actor
499 && &e.actor != actor
500 {
501 return false;
502 }
503
504 if let Some(since) = query.since
505 && e.timestamp < since
506 {
507 return false;
508 }
509
510 if let Some(until) = query.until
511 && e.timestamp > until
512 {
513 return false;
514 }
515
516 true
517 })
518 .cloned()
519 .collect();
520
521 filtered.sort_by_key(|b| std::cmp::Reverse(b.timestamp));
522
523 let total = filtered.len() as u64;
524 let offset = query.offset as usize;
525 let limit = query.limit as usize;
526
527 let paginated: Vec<_> = filtered.into_iter().skip(offset).take(limit).collect();
528
529 let has_more = (offset + paginated.len()) < total as usize;
530
531 Ok(ListAuditEventsResponse {
532 events: paginated,
533 pagination: PaginationInfo {
534 total,
535 limit: query.limit,
536 offset: query.offset,
537 has_more,
538 },
539 })
540 }
541}