Skip to main content

noether_store/
memory.rs

1use crate::lifecycle::validate_transition;
2use crate::traits::{StageStore, StoreError, StoreStats};
3use noether_core::stage::{Stage, StageId, StageLifecycle};
4use std::collections::{BTreeMap, HashMap};
5
6/// In-memory stage store for testing and development.
7#[derive(Debug, Default)]
8pub struct MemoryStore {
9    stages: HashMap<String, Stage>,
10}
11
12impl MemoryStore {
13    pub fn new() -> Self {
14        Self::default()
15    }
16
17    pub fn len(&self) -> usize {
18        self.stages.len()
19    }
20
21    pub fn is_empty(&self) -> bool {
22        self.stages.is_empty()
23    }
24}
25
26impl StageStore for MemoryStore {
27    fn put(&mut self, stage: Stage) -> Result<StageId, StoreError> {
28        let id = stage.id.clone();
29        if self.stages.contains_key(&id.0) {
30            return Err(StoreError::AlreadyExists(id));
31        }
32        self.stages.insert(id.0.clone(), stage);
33        Ok(id)
34    }
35
36    fn upsert(&mut self, stage: Stage) -> Result<StageId, StoreError> {
37        let id = stage.id.clone();
38        self.stages.insert(id.0.clone(), stage);
39        Ok(id)
40    }
41
42    fn remove(&mut self, id: &StageId) -> Result<(), StoreError> {
43        self.stages.remove(&id.0);
44        Ok(())
45    }
46
47    fn get(&self, id: &StageId) -> Result<Option<&Stage>, StoreError> {
48        Ok(self.stages.get(&id.0))
49    }
50
51    fn contains(&self, id: &StageId) -> bool {
52        self.stages.contains_key(&id.0)
53    }
54
55    fn list(&self, lifecycle: Option<&StageLifecycle>) -> Vec<&Stage> {
56        self.stages
57            .values()
58            .filter(|s| lifecycle.is_none() || lifecycle == Some(&s.lifecycle))
59            .collect()
60    }
61
62    fn update_lifecycle(
63        &mut self,
64        id: &StageId,
65        lifecycle: StageLifecycle,
66    ) -> Result<(), StoreError> {
67        // Validate all preconditions before taking a mutable borrow
68        let current = self
69            .stages
70            .get(&id.0)
71            .ok_or_else(|| StoreError::NotFound(id.clone()))?;
72
73        validate_transition(&current.lifecycle, &lifecycle)
74            .map_err(|reason| StoreError::InvalidTransition { reason })?;
75
76        if let StageLifecycle::Deprecated { ref successor_id } = lifecycle {
77            if !self.stages.contains_key(&successor_id.0) {
78                return Err(StoreError::InvalidSuccessor {
79                    reason: format!("successor {successor_id:?} not found in store"),
80                });
81            }
82        }
83
84        // Now safe to mutate
85        self.stages.get_mut(&id.0).unwrap().lifecycle = lifecycle;
86        Ok(())
87    }
88
89    fn stats(&self) -> StoreStats {
90        let mut by_lifecycle: BTreeMap<String, usize> = BTreeMap::new();
91        let mut by_effect: BTreeMap<String, usize> = BTreeMap::new();
92
93        for stage in self.stages.values() {
94            let lc_name = match &stage.lifecycle {
95                StageLifecycle::Draft => "draft",
96                StageLifecycle::Active => "active",
97                StageLifecycle::Deprecated { .. } => "deprecated",
98                StageLifecycle::Tombstone => "tombstone",
99            };
100            *by_lifecycle.entry(lc_name.into()).or_default() += 1;
101
102            for effect in stage.signature.effects.iter() {
103                let effect_name = format!("{effect:?}");
104                *by_effect.entry(effect_name).or_default() += 1;
105            }
106        }
107
108        StoreStats {
109            total: self.stages.len(),
110            by_lifecycle,
111            by_effect,
112        }
113    }
114}
115
116#[cfg(test)]
117mod tests {
118    use super::*;
119    use noether_core::effects::EffectSet;
120    use noether_core::stage::{CostEstimate, StageSignature};
121    use noether_core::types::NType;
122    use std::collections::BTreeSet;
123
124    fn make_stage(id: &str) -> Stage {
125        Stage {
126            id: StageId(id.into()),
127            signature_id: None,
128            signature: StageSignature {
129                input: NType::Text,
130                output: NType::Number,
131                effects: EffectSet::pure(),
132                implementation_hash: format!("impl_{id}"),
133            },
134            capabilities: BTreeSet::new(),
135            cost: CostEstimate {
136                time_ms_p50: None,
137                tokens_est: None,
138                memory_mb: None,
139            },
140            description: "test stage".into(),
141            examples: vec![],
142            lifecycle: StageLifecycle::Active,
143            ed25519_signature: None,
144            signer_public_key: None,
145            implementation_code: None,
146            implementation_language: None,
147            ui_style: None,
148            tags: vec![],
149            aliases: vec![],
150            name: None,
151            properties: Vec::new(),
152        }
153    }
154
155    #[test]
156    fn put_and_get() {
157        let mut store = MemoryStore::new();
158        let stage = make_stage("abc123");
159        store.put(stage.clone()).unwrap();
160        let retrieved = store.get(&StageId("abc123".into())).unwrap().unwrap();
161        assert_eq!(retrieved.id, stage.id);
162    }
163
164    #[test]
165    fn duplicate_put_fails() {
166        let mut store = MemoryStore::new();
167        store.put(make_stage("abc123")).unwrap();
168        assert!(store.put(make_stage("abc123")).is_err());
169    }
170
171    #[test]
172    fn valid_lifecycle_transition() {
173        let mut store = MemoryStore::new();
174        let mut draft = make_stage("abc123");
175        draft.lifecycle = StageLifecycle::Draft;
176        store.put(draft).unwrap();
177        store
178            .update_lifecycle(&StageId("abc123".into()), StageLifecycle::Active)
179            .unwrap();
180        let stage = store.get(&StageId("abc123".into())).unwrap().unwrap();
181        assert_eq!(stage.lifecycle, StageLifecycle::Active);
182    }
183
184    #[test]
185    fn invalid_lifecycle_transition_fails() {
186        let mut store = MemoryStore::new();
187        let mut draft = make_stage("abc123");
188        draft.lifecycle = StageLifecycle::Draft;
189        store.put(draft).unwrap();
190        // Draft → Tombstone is invalid
191        let result = store.update_lifecycle(&StageId("abc123".into()), StageLifecycle::Tombstone);
192        assert!(result.is_err());
193    }
194
195    #[test]
196    fn deprecation_requires_valid_successor() {
197        let mut store = MemoryStore::new();
198        store.put(make_stage("old")).unwrap();
199        // Try to deprecate pointing to a nonexistent successor
200        let result = store.update_lifecycle(
201            &StageId("old".into()),
202            StageLifecycle::Deprecated {
203                successor_id: StageId("nonexistent".into()),
204            },
205        );
206        assert!(result.is_err());
207
208        // Now add the successor and try again
209        store.put(make_stage("new")).unwrap();
210        store
211            .update_lifecycle(
212                &StageId("old".into()),
213                StageLifecycle::Deprecated {
214                    successor_id: StageId("new".into()),
215                },
216            )
217            .unwrap();
218    }
219
220    #[test]
221    fn get_by_signature_returns_active_impl() {
222        use noether_core::stage::SignatureId;
223        let mut store = MemoryStore::new();
224        let mut stage = make_stage("impl_a");
225        stage.signature_id = Some(SignatureId("sig_one".into()));
226        store.put(stage).unwrap();
227
228        let found = store.get_by_signature(&SignatureId("sig_one".into()));
229        assert!(found.is_some(), "stage pinned by signature should resolve");
230        assert_eq!(found.unwrap().id, StageId("impl_a".into()));
231
232        assert!(store
233            .get_by_signature(&SignatureId("sig_missing".into()))
234            .is_none());
235    }
236
237    #[test]
238    fn get_by_signature_skips_deprecated() {
239        use noether_core::stage::SignatureId;
240        let mut store = MemoryStore::new();
241        // Old implementation of "sig" goes Active, new Active stage becomes successor.
242        let mut old = make_stage("impl_old");
243        old.signature_id = Some(SignatureId("sig".into()));
244        store.put(old).unwrap();
245        let mut new = make_stage("impl_new");
246        new.signature_id = Some(SignatureId("sig".into()));
247        store.put(new).unwrap();
248
249        // Deprecate old → new. Resolver should return new.
250        store
251            .update_lifecycle(
252                &StageId("impl_old".into()),
253                StageLifecycle::Deprecated {
254                    successor_id: StageId("impl_new".into()),
255                },
256            )
257            .unwrap();
258
259        let found = store.get_by_signature(&SignatureId("sig".into())).unwrap();
260        assert_eq!(found.id, StageId("impl_new".into()));
261    }
262
263    #[test]
264    fn list_filters_by_lifecycle() {
265        let mut store = MemoryStore::new();
266        store.put(make_stage("a")).unwrap();
267        let mut draft = make_stage("b");
268        draft.lifecycle = StageLifecycle::Draft;
269        store.put(draft).unwrap();
270
271        let active = store.list(Some(&StageLifecycle::Active));
272        assert_eq!(active.len(), 1);
273        let all = store.list(None);
274        assert_eq!(all.len(), 2);
275    }
276
277    #[test]
278    fn stats_returns_counts() {
279        let mut store = MemoryStore::new();
280        store.put(make_stage("a")).unwrap();
281        store.put(make_stage("b")).unwrap();
282        let mut draft = make_stage("c");
283        draft.lifecycle = StageLifecycle::Draft;
284        store.put(draft).unwrap();
285
286        let stats = store.stats();
287        assert_eq!(stats.total, 3);
288        assert_eq!(stats.by_lifecycle.get("active"), Some(&2));
289        assert_eq!(stats.by_lifecycle.get("draft"), Some(&1));
290    }
291}