use somatize_compiler::NodeRegistry;
use somatize_core::cache::CacheKey;
use somatize_core::error::Result;
use somatize_core::filter::Filter;
#[cfg(test)]
use somatize_core::filter::FilterMeta;
use somatize_core::node::NodeMeta;
use somatize_core::state::{MemoryStateStore, StateStore};
use somatize_core::step::Step;
use somatize_core::value::Value;
use std::collections::HashMap;
use std::sync::Arc;
#[derive(Clone)]
pub enum NodeImpl {
Filter(Arc<dyn Filter>),
Step(Arc<dyn Step>),
}
impl NodeImpl {
pub fn meta(&self) -> NodeMeta {
match self {
Self::Filter(f) => f.meta().into(),
Self::Step(s) => s.meta().into(),
}
}
pub fn config_hash(&self) -> CacheKey {
match self {
Self::Filter(f) => f.config_hash(),
Self::Step(s) => s.config_hash(),
}
}
}
#[derive(Clone)]
pub struct NodeCatalog {
nodes: HashMap<String, NodeImpl>,
states: Arc<dyn StateStore>,
}
impl NodeCatalog {
pub fn new() -> Self {
Self {
nodes: HashMap::new(),
states: Arc::new(MemoryStateStore::new()),
}
}
pub fn with_state_store(states: Arc<dyn StateStore>) -> Self {
Self {
nodes: HashMap::new(),
states,
}
}
pub fn register(&mut self, node_id: impl Into<String>, filter: Box<dyn Filter>) {
self.nodes
.insert(node_id.into(), NodeImpl::Filter(Arc::from(filter)));
}
pub fn register_step(&mut self, node_id: impl Into<String>, step: Box<dyn Step>) {
self.nodes
.insert(node_id.into(), NodeImpl::Step(Arc::from(step)));
}
pub fn register_step_arc(&mut self, node_id: impl Into<String>, step: Arc<dyn Step>) {
self.nodes.insert(node_id.into(), NodeImpl::Step(step));
}
pub fn len(&self) -> usize {
self.nodes.len()
}
pub fn is_empty(&self) -> bool {
self.nodes.is_empty()
}
pub fn node(&self, node_id: &str) -> Option<&NodeImpl> {
self.nodes.get(node_id)
}
pub fn node_meta(&self, node_id: &str) -> Option<NodeMeta> {
self.nodes.get(node_id).map(NodeImpl::meta)
}
pub fn get(&self, node_id: &str) -> Option<Arc<dyn Filter>> {
match self.nodes.get(node_id) {
Some(NodeImpl::Filter(f)) => Some(f.clone()),
_ => None,
}
}
pub fn step(&self, node_id: &str) -> Option<Arc<dyn Step>> {
match self.nodes.get(node_id) {
Some(NodeImpl::Step(s)) => Some(s.clone()),
_ => None,
}
}
pub fn has_steps(&self) -> bool {
self.nodes.values().any(|n| matches!(n, NodeImpl::Step(_)))
}
pub fn merge_from(&mut self, other: &NodeCatalog) -> somatize_core::error::Result<()> {
for (id, node) in &other.nodes {
if let Some(existing) = self.nodes.get(id)
&& existing.config_hash() != node.config_hash()
{
return Err(somatize_core::error::SomaError::Other(format!(
"node {id:?} is already registered with a different \
configuration; rename one of the two"
)));
}
self.nodes.insert(id.clone(), node.clone());
}
Ok(())
}
pub fn node_ids(&self) -> Vec<&str> {
let mut ids: Vec<&str> = self.nodes.keys().map(String::as_str).collect();
ids.sort_unstable();
ids
}
pub fn try_set_state(&self, node_id: impl Into<String>, state: Value) -> Result<()> {
let id = node_id.into();
self.states.set(&id, state)
}
pub fn get_state(&self, node_id: &str) -> Option<Arc<Value>> {
self.states.get(node_id).ok().flatten()
}
pub fn clear_states(&self) {
let _ = self.states.clear();
}
pub fn state_store(&self) -> &Arc<dyn StateStore> {
&self.states
}
}
impl Default for NodeCatalog {
fn default() -> Self {
Self::new()
}
}
impl NodeRegistry for NodeCatalog {
fn node_meta(&self, node_id: &str) -> Option<NodeMeta> {
NodeCatalog::node_meta(self, node_id)
}
fn config_hash(&self, node_id: &str) -> Option<CacheKey> {
self.nodes.get(node_id).map(NodeImpl::config_hash)
}
}
#[cfg(test)]
mod tests {
use super::*;
use somatize_core::error::Result;
use somatize_core::filter::{FilterKind, StreamMode};
struct DummyFilter {
name: String,
}
impl Filter for DummyFilter {
fn config_hash(&self) -> CacheKey {
CacheKey::from_parts(&[self.name.as_bytes()])
}
fn fit(&self, _x: &Value, _y: Option<&Value>) -> Result<Value> {
Ok(Value::Empty)
}
fn forward(&self, x: &Value, _state: &Value) -> Result<Value> {
Ok(x.clone())
}
fn meta(&self) -> FilterMeta {
FilterMeta {
name: self.name.clone(),
kind: FilterKind::Stateless,
cacheable: true,
differentiable: false,
deterministic: true,
stream_mode: StreamMode::FixedState,
distribution: somatize_core::filter::Distribution::Local,
input_schema: None,
output_schema: None,
}
}
}
#[test]
fn register_and_query() {
let mut lib = NodeCatalog::new();
lib.register("a", Box::new(DummyFilter { name: "A".into() }));
lib.register("b", Box::new(DummyFilter { name: "B".into() }));
assert_eq!(lib.len(), 2);
assert!(lib.get("a").is_some());
assert!(lib.get("missing").is_none());
}
#[test]
fn implements_filter_registry() {
let mut lib = NodeCatalog::new();
lib.register(
"node_1",
Box::new(DummyFilter {
name: "Scaler".into(),
}),
);
let meta = lib.meta("node_1").unwrap();
assert_eq!(meta.name, "Scaler");
assert!(meta.cacheable);
let hash = lib.config_hash("node_1").unwrap();
assert_eq!(hash, CacheKey::from_parts(&[b"Scaler"]));
assert!(lib.meta("nonexistent").is_none());
}
struct FailingStateStore;
impl StateStore for FailingStateStore {
fn set(&self, _node_id: &str, _state: Value) -> Result<()> {
Err(somatize_core::error::SomaError::Other("disk full".into()))
}
fn get(&self, _node_id: &str) -> Result<Option<Arc<Value>>> {
Ok(None)
}
fn remove(&self, _node_id: &str) -> Result<()> {
Ok(())
}
fn clear(&self) -> Result<()> {
Ok(())
}
fn keys(&self) -> Result<Vec<String>> {
Ok(Vec::new())
}
}
#[test]
fn a_failing_state_store_is_reported_not_fatal() {
let lib = NodeCatalog::with_state_store(Arc::new(FailingStateStore));
let err = lib.try_set_state("a", Value::Empty).unwrap_err();
assert!(err.to_string().contains("disk full"), "got: {err}");
}
struct DummyStep {
name: String,
}
impl Step for DummyStep {
fn config_hash(&self) -> CacheKey {
CacheKey::from_parts(&[self.name.as_bytes()])
}
fn meta(&self) -> somatize_core::step::StepMeta {
somatize_core::step::StepMeta::new(&self.name)
}
fn poll(
&self,
_ctx: &somatize_core::step::StepCtx<'_>,
) -> Result<somatize_core::step::Transition> {
Ok(somatize_core::step::Transition::Done(Value::Empty))
}
}
#[test]
fn a_step_registers_beside_filters_not_as_one() {
let mut lib = NodeCatalog::new();
lib.register("f", Box::new(DummyFilter { name: "F".into() }));
lib.register_step("s", Box::new(DummyStep { name: "S".into() }));
assert_eq!(lib.len(), 2);
assert!(lib.step("s").is_some());
assert!(lib.get("s").is_none(), "a step must not answer as a filter");
assert!(
lib.step("f").is_none(),
"a filter must not answer as a step"
);
assert!(lib.step("missing").is_none());
}
#[test]
fn a_steps_node_meta_declares_it_effectful_and_uncacheable() {
let mut lib = NodeCatalog::new();
lib.register("f", Box::new(DummyFilter { name: "F".into() }));
lib.register_step("s", Box::new(DummyStep { name: "S".into() }));
let step_meta = lib.node_meta("s").unwrap();
assert!(step_meta.effectful);
assert!(!step_meta.cacheable);
assert!(!step_meta.deterministic);
let filter_meta = lib.node_meta("f").unwrap();
assert!(!filter_meta.effectful);
assert!(filter_meta.cacheable);
}
#[test]
fn has_steps_flips_when_the_first_step_arrives() {
let mut lib = NodeCatalog::new();
assert!(!lib.has_steps());
lib.register("f", Box::new(DummyFilter { name: "F".into() }));
assert!(!lib.has_steps(), "a filter is not a step");
lib.register_step("s", Box::new(DummyStep { name: "S".into() }));
assert!(lib.has_steps());
}
#[test]
fn merge_from_merges_and_rejects_a_config_collision() {
let mut lib = NodeCatalog::new();
lib.register("x", Box::new(DummyFilter { name: "X".into() }));
let mut other = NodeCatalog::new();
other.register("x", Box::new(DummyFilter { name: "X".into() }));
other.register("y", Box::new(DummyFilter { name: "Y".into() }));
other.register_step("s", Box::new(DummyStep { name: "S".into() }));
lib.merge_from(&other).unwrap();
assert_eq!(lib.len(), 3);
assert!(lib.get("y").is_some());
assert!(lib.step("s").is_some(), "steps must merge too");
let mut clashing = NodeCatalog::new();
clashing.register(
"x",
Box::new(DummyFilter {
name: "DIFFERENT".into(),
}),
);
let err = lib.merge_from(&clashing).unwrap_err();
let msg = err.to_string();
assert!(msg.contains("x"), "should name the colliding id: {msg}");
assert!(msg.contains("different"), "should say why: {msg}");
}
#[test]
fn state_management() {
let mut lib = NodeCatalog::new();
lib.register("a", Box::new(DummyFilter { name: "A".into() }));
assert!(lib.get_state("a").is_none());
lib.try_set_state("a", Value::json(serde_json::json!({"mean": 5.0})))
.unwrap();
let state = lib.get_state("a").unwrap();
assert_eq!(state.as_json().unwrap()["mean"], 5.0);
}
#[test]
fn clear_states_keeps_filters() {
let mut lib = NodeCatalog::new();
lib.register("a", Box::new(DummyFilter { name: "A".into() }));
lib.try_set_state("a", Value::Empty).unwrap();
assert!(lib.get_state("a").is_some());
lib.clear_states();
assert!(lib.get_state("a").is_none());
assert!(lib.get("a").is_some());
}
}