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            canonical_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        }
151    }
152
153    #[test]
154    fn put_and_get() {
155        let mut store = MemoryStore::new();
156        let stage = make_stage("abc123");
157        store.put(stage.clone()).unwrap();
158        let retrieved = store.get(&StageId("abc123".into())).unwrap().unwrap();
159        assert_eq!(retrieved.id, stage.id);
160    }
161
162    #[test]
163    fn duplicate_put_fails() {
164        let mut store = MemoryStore::new();
165        store.put(make_stage("abc123")).unwrap();
166        assert!(store.put(make_stage("abc123")).is_err());
167    }
168
169    #[test]
170    fn valid_lifecycle_transition() {
171        let mut store = MemoryStore::new();
172        let mut draft = make_stage("abc123");
173        draft.lifecycle = StageLifecycle::Draft;
174        store.put(draft).unwrap();
175        store
176            .update_lifecycle(&StageId("abc123".into()), StageLifecycle::Active)
177            .unwrap();
178        let stage = store.get(&StageId("abc123".into())).unwrap().unwrap();
179        assert_eq!(stage.lifecycle, StageLifecycle::Active);
180    }
181
182    #[test]
183    fn invalid_lifecycle_transition_fails() {
184        let mut store = MemoryStore::new();
185        let mut draft = make_stage("abc123");
186        draft.lifecycle = StageLifecycle::Draft;
187        store.put(draft).unwrap();
188        // Draft → Tombstone is invalid
189        let result = store.update_lifecycle(&StageId("abc123".into()), StageLifecycle::Tombstone);
190        assert!(result.is_err());
191    }
192
193    #[test]
194    fn deprecation_requires_valid_successor() {
195        let mut store = MemoryStore::new();
196        store.put(make_stage("old")).unwrap();
197        // Try to deprecate pointing to a nonexistent successor
198        let result = store.update_lifecycle(
199            &StageId("old".into()),
200            StageLifecycle::Deprecated {
201                successor_id: StageId("nonexistent".into()),
202            },
203        );
204        assert!(result.is_err());
205
206        // Now add the successor and try again
207        store.put(make_stage("new")).unwrap();
208        store
209            .update_lifecycle(
210                &StageId("old".into()),
211                StageLifecycle::Deprecated {
212                    successor_id: StageId("new".into()),
213                },
214            )
215            .unwrap();
216    }
217
218    #[test]
219    fn list_filters_by_lifecycle() {
220        let mut store = MemoryStore::new();
221        store.put(make_stage("a")).unwrap();
222        let mut draft = make_stage("b");
223        draft.lifecycle = StageLifecycle::Draft;
224        store.put(draft).unwrap();
225
226        let active = store.list(Some(&StageLifecycle::Active));
227        assert_eq!(active.len(), 1);
228        let all = store.list(None);
229        assert_eq!(all.len(), 2);
230    }
231
232    #[test]
233    fn stats_returns_counts() {
234        let mut store = MemoryStore::new();
235        store.put(make_stage("a")).unwrap();
236        store.put(make_stage("b")).unwrap();
237        let mut draft = make_stage("c");
238        draft.lifecycle = StageLifecycle::Draft;
239        store.put(draft).unwrap();
240
241        let stats = store.stats();
242        assert_eq!(stats.total, 3);
243        assert_eq!(stats.by_lifecycle.get("active"), Some(&2));
244        assert_eq!(stats.by_lifecycle.get("draft"), Some(&1));
245    }
246}