async-nodes 0.1.0

An asynchronous, type-safe node engine implementing the Bulk Synchronous Parallel (BSP) model: control-flow loops, recovery, suspension, and serializable graphs, with dataflow-once pipelines as a profile.
Documentation
//! The node & condition registry: the engine/definition separation made concrete.
//!
//! Consumers register their node types (as factories), named routing predicates, and
//! mergers; the engine instantiates a [`GraphDef`](super::def::GraphDef) against the
//! registry. A GUI can then load, edit, save, and *instantiate* graphs — the full n8n
//! loop. Registries are per-`S`: consumers with multiple state types build one registry
//! per state type.
//!
//! Node config schemas are consumer-owned and consumer-versioned (e.g. version inside the
//! config blob as `{"v": 2, ...}`); the engine stays out of it.

use std::{collections::HashMap, sync::Arc};

use super::{
    BspContext, condition::ConditionFn, def::NodeDef, error::BspError, merge::Merger,
    node::BspNode, value::BspValue,
};

/// Factory building a node instance from its definition (config blob included) and the
/// dependency-injection context.
pub type NodeFactory<S> =
    Box<dyn Fn(&NodeDef, &BspContext) -> Result<Box<dyn BspNode<S>>, BspError> + Send + Sync>;

/// Converts a JSON static-input value from a [`GraphDef`](super::def::GraphDef) into a
/// typed [`BspValue`](super::value::BspValue) at instantiation time.
pub type ValueFactory = Arc<dyn Fn(&serde_json::Value) -> BspValue + Send + Sync>;

/// The default static-input mapping: JSON bools/numbers/strings become *typed* values
/// (so dataflow nodes downcast them exactly as if `set_static_input` had been called with
/// the Rust value), and anything else arrives as a wrapped `serde_json::Value`.
///
/// Consumers wanting different typing (e.g. the snarl bridge, which historically injects
/// `bool`/`i64`/`f64`/`String` from property JSON) override this with
/// [`NodeRegistry::register_value_factory`].
pub fn default_value_factory(value: &serde_json::Value) -> BspValue {
    match value {
        serde_json::Value::Bool(b) => BspValue::new(*b),
        serde_json::Value::Number(n) => {
            if let Some(i) = n.as_i64() {
                BspValue::new(i)
            } else if let Some(u) = n.as_u64() {
                BspValue::new(u)
            } else {
                BspValue::new(n.as_f64().unwrap_or_default())
            }
        }
        serde_json::Value::String(s) => BspValue::new(s.clone()),
        other => BspValue::new(other.clone()),
    }
}

/// A per-`S` registry of node factories, named conditions, and mergers.
///
/// Registration is fail-fast at graph-build time: unknown `type_name`s, condition names,
/// or merger names are reported by [`BspGraph::from_def`](super::graph::BspGraph::from_def)
/// as a [`BspError::ValidationError`] listing every unknown name.
pub struct NodeRegistry<S = ()> {
    /// Node type name -> factory.
    pub(crate) factories: HashMap<String, NodeFactory<S>>,
    /// Condition name -> predicate (also the re-registration point for `Custom` labels).
    pub(crate) conditions: HashMap<String, ConditionFn<S>>,
    /// Merger name -> merger.
    pub(crate) mergers: HashMap<String, Arc<dyn Merger<S>>>,
    /// JSON static-input value -> typed `BspValue` (used by `from_def`).
    pub(crate) value_factory: ValueFactory,
}

impl<S> Default for NodeRegistry<S> {
    fn default() -> Self {
        Self::new()
    }
}

impl<S> NodeRegistry<S> {
    /// Instantiates an empty registry.
    #[must_use]
    pub fn new() -> Self {
        Self {
            factories: HashMap::new(),
            conditions: HashMap::new(),
            mergers: HashMap::new(),
            value_factory: Arc::new(default_value_factory),
        }
    }

    /// Registers a node type factory under its `type_name`.
    pub fn register_node(
        &mut self,
        type_name: impl Into<String>,
        factory: impl Fn(&NodeDef, &BspContext) -> Result<Box<dyn BspNode<S>>, BspError>
        + Send
        + Sync
        + 'static,
    ) {
        self.factories.insert(type_name.into(), Box::new(factory));
    }

    /// Registers a named condition predicate.
    pub fn register_condition(
        &mut self,
        name: impl Into<String>,
        f: impl Fn(&S, &super::run::ValueStore) -> bool + Send + Sync + 'static,
    ) {
        self.conditions.insert(name.into(), Arc::new(f));
    }

    /// Registers a named merger.
    pub fn register_merger<M: Merger<S> + 'static>(&mut self, name: impl Into<String>, merger: M) {
        self.mergers.insert(name.into(), Arc::new(merger));
    }

    /// Overrides how [`GraphDef`](super::def::GraphDef) static-input JSON becomes typed
    /// pin values at instantiation.
    ///
    /// The default ([`default_value_factory`]) produces typed `bool` / `i64` / `u64` /
    /// `f64` / `String` values. Override this when your nodes expect different types —
    /// e.g. everything as raw `serde_json::Value`:
    /// `registry.register_value_factory(|v| BspValue::new(v.clone()))`.
    pub fn register_value_factory(
        &mut self,
        factory: impl Fn(&serde_json::Value) -> BspValue + Send + Sync + 'static,
    ) {
        self.value_factory = Arc::new(factory);
    }

    /// Returns whether a node type is registered.
    #[must_use]
    pub fn contains_node(&self, type_name: &str) -> bool {
        self.factories.contains_key(type_name)
    }

    /// Returns whether a condition is registered.
    #[must_use]
    pub fn contains_condition(&self, name: &str) -> bool {
        self.conditions.contains_key(name)
    }

    /// Returns whether a merger is registered.
    #[must_use]
    pub fn contains_merger(&self, name: &str) -> bool {
        self.mergers.contains_key(name)
    }

    /// The registered node type names (the GUI palette catalog).
    #[must_use]
    pub fn node_type_names(&self) -> Vec<&str> {
        self.factories.keys().map(String::as_str).collect()
    }

    #[must_use]
    fn with_mut(mut self, f: impl FnOnce(&mut Self)) -> Self {
        f(&mut self);
        self
    }

    /// Builder variant of [`NodeRegistry::register_node`].
    #[must_use]
    pub fn with_node(
        self,
        type_name: impl Into<String>,
        factory: impl Fn(&NodeDef, &BspContext) -> Result<Box<dyn BspNode<S>>, BspError>
        + Send
        + Sync
        + 'static,
    ) -> Self {
        self.with_mut(|r| r.register_node(type_name, factory))
    }

    /// Builder variant of [`NodeRegistry::register_condition`].
    #[must_use]
    pub fn with_condition(
        self,
        name: impl Into<String>,
        f: impl Fn(&S, &super::run::ValueStore) -> bool + Send + Sync + 'static,
    ) -> Self {
        self.with_mut(|r| r.register_condition(name, f))
    }

    /// Builder variant of [`NodeRegistry::register_merger`].
    #[must_use]
    pub fn with_merger<M: Merger<S> + 'static>(self, name: impl Into<String>, merger: M) -> Self {
        self.with_mut(|r| r.register_merger(name, merger))
    }

    /// Builder variant of [`NodeRegistry::register_value_factory`].
    #[must_use]
    pub fn with_value_factory(
        self,
        factory: impl Fn(&serde_json::Value) -> BspValue + Send + Sync + 'static,
    ) -> Self {
        self.with_mut(|r| r.register_value_factory(factory))
    }
}