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#[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 let current = self
69 .stages
70 .get(&id.0)
71 .ok_or_else(|| StoreError::NotFound(id.clone()))?;
72
73 validate_transition(¤t.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 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 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 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 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}