Skip to main content

mlua_swarm/blueprint/store/
inmemory.rs

1//! `InMemoryBlueprintStore` — an in-memory backend used in tests.
2//!
3//! Layout:
4//! `BTreeMap<(BlueprintId, BlueprintVersion), Blueprint>` +
5//! `HashMap<BlueprintId, head>`. `head_history` is an append-only `Vec`
6//! so `history()` can reconstruct lineage.
7
8use super::types::*;
9use super::{blueprint_version, canonical_yaml, BlueprintStore};
10use crate::blueprint::Blueprint;
11use async_trait::async_trait;
12use std::collections::{BTreeMap, HashMap};
13use std::sync::Mutex;
14
15/// In-memory backend, used in tests and as a blank default.
16#[derive(Default)]
17pub struct InMemoryBlueprintStore {
18    inner: Mutex<InMemoryInner>,
19}
20
21#[derive(Default)]
22struct InMemoryInner {
23    /// (id, version) → Blueprint payload
24    objects: BTreeMap<(BlueprintId, BlueprintVersion), StoredObject>,
25    /// id → current head version.
26    heads: HashMap<BlueprintId, BlueprintVersion>,
27    /// id → version history (newest to oldest, head included).
28    history: HashMap<BlueprintId, Vec<BlueprintVersion>>,
29}
30
31struct StoredObject {
32    blueprint: Blueprint,
33    trace: Trace,
34    rationale: String,
35}
36
37impl InMemoryBlueprintStore {
38    /// Start with an empty store (no ids, no objects).
39    pub fn new() -> Self {
40        Self::default()
41    }
42}
43
44#[async_trait]
45impl BlueprintStore for InMemoryBlueprintStore {
46    fn name(&self) -> &str {
47        "in-memory"
48    }
49
50    async fn read_head(&self, id: &BlueprintId) -> Result<Traced<Blueprint>, BlueprintStoreError> {
51        let inner = self.inner.lock().unwrap();
52        let head = inner
53            .heads
54            .get(id)
55            .ok_or_else(|| BlueprintStoreError::HeadEmpty(id.clone()))?;
56        let obj = inner
57            .objects
58            .get(&(id.clone(), *head))
59            .ok_or(BlueprintStoreError::NotFound {
60                id: id.clone(),
61                version: *head,
62            })?;
63        Ok(Traced::new(obj.blueprint.clone(), obj.trace.clone()))
64    }
65
66    async fn write_new(
67        &self,
68        id: &BlueprintId,
69        new_bp: &Blueprint,
70        parents: &[BlueprintVersion],
71        metadata: CommitMetadata,
72    ) -> Result<BlueprintVersion, BlueprintStoreError> {
73        // Materialize the canonical bytes and compute their ContentHash.
74        let _yaml = canonical_yaml(new_bp)?;
75        let version = blueprint_version(new_bp)?;
76
77        let parents_refs: Vec<TraceRef> = parents
78            .iter()
79            .map(|p| TraceRef {
80                origin: TraceOrigin::Inline,
81                hash: p.0,
82            })
83            .collect();
84
85        let trace = Trace::new(
86            TraceOrigin::Inline,
87            version,
88            metadata.epoch_id.started_at_ms,
89        )
90        .with_parents(parents_refs);
91
92        let mut inner = self.inner.lock().unwrap();
93        inner.objects.insert(
94            (id.clone(), version),
95            StoredObject {
96                blueprint: new_bp.clone(),
97                trace,
98                rationale: metadata.rationale.clone(),
99            },
100        );
101        inner.heads.insert(id.clone(), version);
102        inner
103            .history
104            .entry(id.clone())
105            .or_default()
106            .insert(0, version);
107        let _ = metadata;
108        Ok(version)
109    }
110
111    async fn read_commit_rationale(
112        &self,
113        id: &BlueprintId,
114        version: BlueprintVersion,
115    ) -> Result<Option<String>, BlueprintStoreError> {
116        let inner = self.inner.lock().unwrap();
117        Ok(inner
118            .objects
119            .get(&(id.clone(), version))
120            .map(|o| o.rationale.clone()))
121    }
122
123    async fn read_version(
124        &self,
125        id: &BlueprintId,
126        version: BlueprintVersion,
127    ) -> Result<Traced<Blueprint>, BlueprintStoreError> {
128        let inner = self.inner.lock().unwrap();
129        let obj =
130            inner
131                .objects
132                .get(&(id.clone(), version))
133                .ok_or(BlueprintStoreError::NotFound {
134                    id: id.clone(),
135                    version,
136                })?;
137        Ok(Traced::new(obj.blueprint.clone(), obj.trace.clone()))
138    }
139
140    async fn history(
141        &self,
142        id: &BlueprintId,
143        limit: usize,
144    ) -> Result<Vec<BlueprintVersion>, BlueprintStoreError> {
145        let inner = self.inner.lock().unwrap();
146        let hist = inner.history.get(id).cloned().unwrap_or_default();
147        Ok(hist.into_iter().take(limit).collect())
148    }
149
150    async fn list_ids(&self) -> Result<Vec<BlueprintId>, BlueprintStoreError> {
151        let inner = self.inner.lock().unwrap();
152        Ok(inner.heads.keys().cloned().collect())
153    }
154}