1use somatize_compiler::NodeRegistry;
19use somatize_core::cache::CacheKey;
20use somatize_core::error::Result;
21use somatize_core::filter::Filter;
22#[cfg(test)]
23use somatize_core::filter::FilterMeta;
24use somatize_core::node::NodeMeta;
25use somatize_core::state::{MemoryStateStore, StateStore};
26use somatize_core::step::Step;
27use somatize_core::value::Value;
28use std::collections::HashMap;
29use std::sync::Arc;
30
31#[derive(Clone)]
37pub enum NodeImpl {
38 Filter(Arc<dyn Filter>),
40 Step(Arc<dyn Step>),
42}
43
44impl NodeImpl {
45 pub fn meta(&self) -> NodeMeta {
47 match self {
48 Self::Filter(f) => f.meta().into(),
49 Self::Step(s) => s.meta().into(),
50 }
51 }
52
53 pub fn config_hash(&self) -> CacheKey {
55 match self {
56 Self::Filter(f) => f.config_hash(),
57 Self::Step(s) => s.config_hash(),
58 }
59 }
60}
61
62#[derive(Clone)]
79pub struct NodeCatalog {
80 nodes: HashMap<String, NodeImpl>,
81 states: Arc<dyn StateStore>,
82}
83
84impl NodeCatalog {
85 pub fn new() -> Self {
87 Self {
88 nodes: HashMap::new(),
89 states: Arc::new(MemoryStateStore::new()),
90 }
91 }
92
93 pub fn with_state_store(states: Arc<dyn StateStore>) -> Self {
95 Self {
96 nodes: HashMap::new(),
97 states,
98 }
99 }
100
101 pub fn register(&mut self, node_id: impl Into<String>, filter: Box<dyn Filter>) {
103 self.nodes
104 .insert(node_id.into(), NodeImpl::Filter(Arc::from(filter)));
105 }
106
107 pub fn register_step(&mut self, node_id: impl Into<String>, step: Box<dyn Step>) {
109 self.nodes
110 .insert(node_id.into(), NodeImpl::Step(Arc::from(step)));
111 }
112
113 pub fn register_step_arc(&mut self, node_id: impl Into<String>, step: Arc<dyn Step>) {
116 self.nodes.insert(node_id.into(), NodeImpl::Step(step));
117 }
118
119 pub fn len(&self) -> usize {
121 self.nodes.len()
122 }
123
124 pub fn is_empty(&self) -> bool {
126 self.nodes.is_empty()
127 }
128
129 pub fn node(&self, node_id: &str) -> Option<&NodeImpl> {
131 self.nodes.get(node_id)
132 }
133
134 pub fn node_meta(&self, node_id: &str) -> Option<NodeMeta> {
136 self.nodes.get(node_id).map(NodeImpl::meta)
137 }
138
139 pub fn get(&self, node_id: &str) -> Option<Arc<dyn Filter>> {
141 match self.nodes.get(node_id) {
142 Some(NodeImpl::Filter(f)) => Some(f.clone()),
143 _ => None,
144 }
145 }
146
147 pub fn step(&self, node_id: &str) -> Option<Arc<dyn Step>> {
149 match self.nodes.get(node_id) {
150 Some(NodeImpl::Step(s)) => Some(s.clone()),
151 _ => None,
152 }
153 }
154
155 pub fn has_steps(&self) -> bool {
160 self.nodes.values().any(|n| matches!(n, NodeImpl::Step(_)))
161 }
162
163 pub fn merge_from(&mut self, other: &NodeCatalog) -> somatize_core::error::Result<()> {
170 for (id, node) in &other.nodes {
171 if let Some(existing) = self.nodes.get(id)
172 && existing.config_hash() != node.config_hash()
173 {
174 return Err(somatize_core::error::SomaError::Other(format!(
175 "node {id:?} is already registered with a different \
176 configuration; rename one of the two"
177 )));
178 }
179 self.nodes.insert(id.clone(), node.clone());
180 }
181 Ok(())
182 }
183
184 pub fn node_ids(&self) -> Vec<&str> {
186 let mut ids: Vec<&str> = self.nodes.keys().map(String::as_str).collect();
187 ids.sort_unstable();
188 ids
189 }
190
191 pub fn try_set_state(&self, node_id: impl Into<String>, state: Value) -> Result<()> {
196 let id = node_id.into();
197 self.states.set(&id, state)
198 }
199
200 pub fn get_state(&self, node_id: &str) -> Option<Arc<Value>> {
204 self.states.get(node_id).ok().flatten()
205 }
206
207 pub fn clear_states(&self) {
209 let _ = self.states.clear();
210 }
211
212 pub fn state_store(&self) -> &Arc<dyn StateStore> {
215 &self.states
216 }
217}
218
219impl Default for NodeCatalog {
220 fn default() -> Self {
221 Self::new()
222 }
223}
224
225impl NodeRegistry for NodeCatalog {
231 fn node_meta(&self, node_id: &str) -> Option<NodeMeta> {
232 NodeCatalog::node_meta(self, node_id)
233 }
234
235 fn config_hash(&self, node_id: &str) -> Option<CacheKey> {
236 self.nodes.get(node_id).map(NodeImpl::config_hash)
237 }
238}
239
240#[cfg(test)]
241mod tests {
242 use super::*;
243 use somatize_core::error::Result;
244 use somatize_core::filter::{FilterKind, StreamMode};
245
246 struct DummyFilter {
247 name: String,
248 }
249
250 impl Filter for DummyFilter {
251 fn config_hash(&self) -> CacheKey {
252 CacheKey::from_parts(&[self.name.as_bytes()])
253 }
254 fn fit(&self, _x: &Value, _y: Option<&Value>) -> Result<Value> {
255 Ok(Value::Empty)
256 }
257 fn forward(&self, x: &Value, _state: &Value) -> Result<Value> {
258 Ok(x.clone())
259 }
260 fn meta(&self) -> FilterMeta {
261 FilterMeta {
262 name: self.name.clone(),
263 kind: FilterKind::Stateless,
264 cacheable: true,
265 differentiable: false,
266 deterministic: true,
267 stream_mode: StreamMode::FixedState,
268 distribution: somatize_core::filter::Distribution::Local,
269 input_schema: None,
270 output_schema: None,
271 }
272 }
273 }
274
275 #[test]
276 fn register_and_query() {
277 let mut lib = NodeCatalog::new();
278 lib.register("a", Box::new(DummyFilter { name: "A".into() }));
279 lib.register("b", Box::new(DummyFilter { name: "B".into() }));
280
281 assert_eq!(lib.len(), 2);
282 assert!(lib.get("a").is_some());
283 assert!(lib.get("missing").is_none());
284 }
285
286 #[test]
287 fn implements_filter_registry() {
288 let mut lib = NodeCatalog::new();
289 lib.register(
290 "node_1",
291 Box::new(DummyFilter {
292 name: "Scaler".into(),
293 }),
294 );
295
296 let meta = lib.meta("node_1").unwrap();
297 assert_eq!(meta.name, "Scaler");
298 assert!(meta.cacheable);
299
300 let hash = lib.config_hash("node_1").unwrap();
301 assert_eq!(hash, CacheKey::from_parts(&[b"Scaler"]));
302
303 assert!(lib.meta("nonexistent").is_none());
304 }
305
306 struct FailingStateStore;
309
310 impl StateStore for FailingStateStore {
311 fn set(&self, _node_id: &str, _state: Value) -> Result<()> {
312 Err(somatize_core::error::SomaError::Other("disk full".into()))
313 }
314 fn get(&self, _node_id: &str) -> Result<Option<Arc<Value>>> {
315 Ok(None)
316 }
317 fn remove(&self, _node_id: &str) -> Result<()> {
318 Ok(())
319 }
320 fn clear(&self) -> Result<()> {
321 Ok(())
322 }
323 fn keys(&self) -> Result<Vec<String>> {
324 Ok(Vec::new())
325 }
326 }
327
328 #[test]
332 fn a_failing_state_store_is_reported_not_fatal() {
333 let lib = NodeCatalog::with_state_store(Arc::new(FailingStateStore));
334
335 let err = lib.try_set_state("a", Value::Empty).unwrap_err();
336 assert!(err.to_string().contains("disk full"), "got: {err}");
337 }
338
339 struct DummyStep {
340 name: String,
341 }
342
343 impl Step for DummyStep {
344 fn config_hash(&self) -> CacheKey {
345 CacheKey::from_parts(&[self.name.as_bytes()])
346 }
347 fn meta(&self) -> somatize_core::step::StepMeta {
348 somatize_core::step::StepMeta::new(&self.name)
349 }
350 fn poll(
351 &self,
352 _ctx: &somatize_core::step::StepCtx<'_>,
353 ) -> Result<somatize_core::step::Transition> {
354 Ok(somatize_core::step::Transition::Done(Value::Empty))
355 }
356 }
357
358 #[test]
362 fn a_step_registers_beside_filters_not_as_one() {
363 let mut lib = NodeCatalog::new();
364 lib.register("f", Box::new(DummyFilter { name: "F".into() }));
365 lib.register_step("s", Box::new(DummyStep { name: "S".into() }));
366
367 assert_eq!(lib.len(), 2);
368 assert!(lib.step("s").is_some());
369 assert!(lib.get("s").is_none(), "a step must not answer as a filter");
370 assert!(
371 lib.step("f").is_none(),
372 "a filter must not answer as a step"
373 );
374 assert!(lib.step("missing").is_none());
375 }
376
377 #[test]
382 fn a_steps_node_meta_declares_it_effectful_and_uncacheable() {
383 let mut lib = NodeCatalog::new();
384 lib.register("f", Box::new(DummyFilter { name: "F".into() }));
385 lib.register_step("s", Box::new(DummyStep { name: "S".into() }));
386
387 let step_meta = lib.node_meta("s").unwrap();
388 assert!(step_meta.effectful);
389 assert!(!step_meta.cacheable);
390 assert!(!step_meta.deterministic);
391
392 let filter_meta = lib.node_meta("f").unwrap();
393 assert!(!filter_meta.effectful);
394 assert!(filter_meta.cacheable);
395 }
396
397 #[test]
401 fn has_steps_flips_when_the_first_step_arrives() {
402 let mut lib = NodeCatalog::new();
403 assert!(!lib.has_steps());
404
405 lib.register("f", Box::new(DummyFilter { name: "F".into() }));
406 assert!(!lib.has_steps(), "a filter is not a step");
407
408 lib.register_step("s", Box::new(DummyStep { name: "S".into() }));
409 assert!(lib.has_steps());
410 }
411
412 #[test]
417 fn merge_from_merges_and_rejects_a_config_collision() {
418 let mut lib = NodeCatalog::new();
419 lib.register("x", Box::new(DummyFilter { name: "X".into() }));
420
421 let mut other = NodeCatalog::new();
422 other.register("x", Box::new(DummyFilter { name: "X".into() }));
424 other.register("y", Box::new(DummyFilter { name: "Y".into() }));
425 other.register_step("s", Box::new(DummyStep { name: "S".into() }));
426
427 lib.merge_from(&other).unwrap();
428 assert_eq!(lib.len(), 3);
429 assert!(lib.get("y").is_some());
430 assert!(lib.step("s").is_some(), "steps must merge too");
431
432 let mut clashing = NodeCatalog::new();
433 clashing.register(
434 "x",
435 Box::new(DummyFilter {
436 name: "DIFFERENT".into(),
437 }),
438 );
439 let err = lib.merge_from(&clashing).unwrap_err();
440 let msg = err.to_string();
441 assert!(msg.contains("x"), "should name the colliding id: {msg}");
442 assert!(msg.contains("different"), "should say why: {msg}");
443 }
444
445 #[test]
446 fn state_management() {
447 let mut lib = NodeCatalog::new();
448 lib.register("a", Box::new(DummyFilter { name: "A".into() }));
449
450 assert!(lib.get_state("a").is_none());
451
452 lib.try_set_state("a", Value::json(serde_json::json!({"mean": 5.0})))
453 .unwrap();
454 let state = lib.get_state("a").unwrap();
455 assert_eq!(state.as_json().unwrap()["mean"], 5.0);
456 }
457
458 #[test]
459 fn clear_states_keeps_filters() {
460 let mut lib = NodeCatalog::new();
461 lib.register("a", Box::new(DummyFilter { name: "A".into() }));
462 lib.try_set_state("a", Value::Empty).unwrap();
463
464 assert!(lib.get_state("a").is_some());
465 lib.clear_states();
466 assert!(lib.get_state("a").is_none());
467 assert!(lib.get("a").is_some());
468 }
469}