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 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 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 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 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 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 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}