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