jellyflow-runtime 0.2.0

Headless store, rules, schema, profile, and change pipeline for Jellyflow.
Documentation
use serde_json::Value;

use jellyflow_core::core::{
    CanvasPoint, Node, NodeId, NodeKindKey, Port, PortCapacity, PortDirection, PortId, PortKey,
    PortKind,
};
use jellyflow_core::types::TypeDesc;

/// Specification for inserting a new node as part of a connection workflow.
///
/// The `ports` list defines the desired UI ordering for the inserted node.
#[derive(Debug, Clone)]
pub struct InsertNodeSpec {
    pub node_id: NodeId,
    pub node: Node,
    pub ports: Vec<(PortId, Port)>,
    pub input: PortId,
    pub output: PortId,
}

/// A template for a port to be instantiated during an insertion workflow.
#[derive(Debug, Clone)]
pub struct PortTemplate {
    pub key: PortKey,
    pub dir: PortDirection,
    pub kind: PortKind,
    pub capacity: PortCapacity,
    pub ty: Option<TypeDesc>,
    pub data: Value,
}

/// A template for inserting a node between two ports.
///
/// This is the preferred UI-to-rules contract: IDs are generated by the rules layer.
#[derive(Debug, Clone)]
pub struct InsertNodeTemplate {
    pub kind: NodeKindKey,
    pub kind_version: u32,
    pub collapsed: bool,
    pub data: Value,
    pub ports: Vec<PortTemplate>,

    /// Which instantiated port should be connected from the upstream edge segment.
    pub input: PortKey,
    /// Which instantiated port should be connected to the downstream edge segment.
    pub output: PortKey,
}

impl InsertNodeTemplate {
    /// Instantiates this template at a given position by allocating fresh IDs.
    pub fn instantiate(&self, at: CanvasPoint) -> Result<InsertNodeSpec, String> {
        let node_id = NodeId::new();
        let ports = InstantiatedTemplatePorts::from_template(self, node_id)?;

        Ok(InsertNodeSpec {
            node_id,
            node: self.instantiate_node(at),
            ports: ports.ports,
            input: ports.input,
            output: ports.output,
        })
    }

    fn instantiate_node(&self, at: CanvasPoint) -> Node {
        Node {
            kind: self.kind.clone(),
            kind_version: self.kind_version,
            pos: at,
            origin: None,
            selectable: None,
            focusable: None,
            draggable: None,
            connectable: None,
            deletable: None,
            parent: None,
            extent: None,
            expand_parent: None,
            size: None,
            hidden: false,
            collapsed: self.collapsed,
            ports: Vec::new(),
            data: self.data.clone(),
        }
    }
}

struct InstantiatedTemplatePorts {
    ports: Vec<(PortId, Port)>,
    input: PortId,
    output: PortId,
}

impl InstantiatedTemplatePorts {
    fn from_template(template: &InsertNodeTemplate, node_id: NodeId) -> Result<Self, String> {
        let mut ports: Vec<(PortId, Port)> = Vec::new();
        let mut input: Option<PortId> = None;
        let mut output: Option<PortId> = None;

        for port_template in &template.ports {
            let port_id = PortId::new();
            if port_template.key == template.input {
                input = Some(port_id);
            }
            if port_template.key == template.output {
                output = Some(port_id);
            }

            ports.push((port_id, instantiate_port(node_id, port_template)));
        }

        let input = required_template_port(input, "input", &template.input)?;
        let output = required_template_port(output, "output", &template.output)?;
        if input == output {
            return Err("template input/output ports must be distinct".to_string());
        }

        Ok(Self {
            ports,
            input,
            output,
        })
    }
}

fn instantiate_port(node_id: NodeId, template: &PortTemplate) -> Port {
    Port {
        node: node_id,
        key: template.key.clone(),
        dir: template.dir,
        kind: template.kind,
        capacity: template.capacity,
        connectable: None,
        connectable_start: None,
        connectable_end: None,
        ty: template.ty.clone(),
        data: template.data.clone(),
    }
}

fn required_template_port(
    port: Option<PortId>,
    role: &str,
    key: &PortKey,
) -> Result<PortId, String> {
    port.ok_or_else(|| format!("template is missing {role} port: {}", key.0))
}