Skip to main content

jellyflow_runtime/schema/
types.rs

1use serde::{Deserialize, Serialize};
2use serde_json::Value;
3
4use jellyflow_core::core::{
5    CanvasPoint, CanvasSize, Node, NodeId, NodeKindKey, Port, PortCapacity, PortDirection, PortId,
6    PortKey, PortKind,
7};
8use jellyflow_core::ops::{GraphOp, GraphTransaction};
9use jellyflow_core::types::TypeDesc;
10
11/// Declares a port for a node kind.
12#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
13pub struct PortDecl {
14    /// Stable schema key for this port.
15    pub key: PortKey,
16    /// Direction.
17    pub dir: PortDirection,
18    /// Kind.
19    pub kind: PortKind,
20    /// Capacity.
21    pub capacity: PortCapacity,
22    /// Optional type descriptor.
23    #[serde(default, skip_serializing_if = "Option::is_none")]
24    pub ty: Option<TypeDesc>,
25    /// UI-facing label.
26    #[serde(default, skip_serializing_if = "Option::is_none")]
27    pub label: Option<String>,
28}
29
30/// Schema for a node kind.
31#[derive(Debug, Clone, Serialize, Deserialize)]
32pub struct NodeSchema {
33    /// Canonical kind key.
34    pub kind: NodeKindKey,
35    /// Latest schema version for this kind.
36    pub latest_kind_version: u32,
37    /// Kind aliases (renames).
38    #[serde(default, skip_serializing_if = "Vec::is_empty")]
39    pub kind_aliases: Vec<NodeKindKey>,
40
41    /// UI-facing title.
42    pub title: String,
43    /// Category path (for create-node search/palette).
44    #[serde(default, skip_serializing_if = "Vec::is_empty")]
45    pub category: Vec<String>,
46    /// Search keywords.
47    #[serde(default, skip_serializing_if = "Vec::is_empty")]
48    pub keywords: Vec<String>,
49    /// Adapter-facing renderer key.
50    ///
51    /// Runtime keeps this as data instead of a component reference so React, Svelte, native, and
52    /// future adapters can map the key to their own renderer registry.
53    #[serde(default, skip_serializing_if = "Option::is_none")]
54    pub renderer_key: Option<String>,
55    /// Default logical node size for adapters that need an initial rect before measurement.
56    #[serde(default, skip_serializing_if = "Option::is_none")]
57    pub default_size: Option<CanvasSize>,
58
59    /// Declared ports.
60    #[serde(default, skip_serializing_if = "Vec::is_empty")]
61    pub ports: Vec<PortDecl>,
62
63    /// Default node payload.
64    #[serde(default)]
65    pub default_data: Value,
66}
67
68/// Error returned when a node cannot be instantiated from schema.
69#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
70pub enum NodeInstantiationError {
71    /// The requested node kind is not registered.
72    #[error("node kind schema not found: {0:?}")]
73    MissingSchema(NodeKindKey),
74    /// The caller supplied a different number of port ids than the schema declares.
75    #[error("port id count mismatch: expected {expected}, got {actual}")]
76    PortIdCountMismatch { expected: usize, actual: usize },
77}
78
79/// Concrete graph records produced from a node schema.
80#[derive(Debug, Clone)]
81pub struct NodeInstantiation {
82    /// Allocated node id.
83    pub node_id: NodeId,
84    /// Node record to add to the graph.
85    pub node: Node,
86    /// Port records to add to the graph, in schema/UI order.
87    pub ports: Vec<(PortId, Port)>,
88}
89
90impl NodeInstantiation {
91    /// Consumes this instantiation into graph records.
92    pub fn into_parts(self) -> (NodeId, Node, Vec<(PortId, Port)>) {
93        (self.node_id, self.node, self.ports)
94    }
95
96    /// Consumes this instantiation into add-node/add-port operations.
97    pub fn into_ops(self) -> Vec<GraphOp> {
98        let port_order = self.node.ports.clone();
99        let mut node = self.node;
100        node.ports = Vec::new();
101
102        let mut ops =
103            Vec::with_capacity(self.ports.len() + usize::from(!port_order.is_empty()) + 1);
104        ops.push(GraphOp::AddNode {
105            id: self.node_id,
106            node,
107        });
108        ops.extend(
109            self.ports
110                .into_iter()
111                .map(|(id, port)| GraphOp::AddPort { id, port }),
112        );
113        if !port_order.is_empty() {
114            ops.push(GraphOp::SetNodePorts {
115                id: self.node_id,
116                from: Vec::new(),
117                to: port_order,
118            });
119        }
120        ops
121    }
122
123    /// Consumes this instantiation into an unlabeled graph transaction.
124    pub fn into_transaction(self) -> GraphTransaction {
125        GraphTransaction::from_ops(self.into_ops())
126    }
127
128    /// Consumes this instantiation into a labeled graph transaction.
129    pub fn into_labeled_transaction(self, label: impl Into<String>) -> GraphTransaction {
130        self.into_transaction().with_label(label)
131    }
132}
133
134impl NodeSchema {
135    /// Instantiates a node and its declared ports with freshly allocated ids.
136    pub fn instantiate(&self, pos: CanvasPoint) -> NodeInstantiation {
137        let node_id = NodeId::new();
138        let port_ids = std::iter::repeat_with(PortId::new)
139            .take(self.ports.len())
140            .collect();
141        self.instantiate_from_port_ids(node_id, pos, port_ids)
142    }
143
144    /// Instantiates a node and its declared ports with caller-provided ids.
145    pub fn instantiate_with_ids(
146        &self,
147        node_id: NodeId,
148        pos: CanvasPoint,
149        port_ids: impl IntoIterator<Item = PortId>,
150    ) -> Result<NodeInstantiation, NodeInstantiationError> {
151        let port_ids: Vec<PortId> = port_ids.into_iter().collect();
152        if port_ids.len() != self.ports.len() {
153            return Err(NodeInstantiationError::PortIdCountMismatch {
154                expected: self.ports.len(),
155                actual: port_ids.len(),
156            });
157        }
158
159        Ok(self.instantiate_from_port_ids(node_id, pos, port_ids))
160    }
161
162    fn instantiate_from_port_ids(
163        &self,
164        node_id: NodeId,
165        pos: CanvasPoint,
166        port_ids: Vec<PortId>,
167    ) -> NodeInstantiation {
168        let ports = self
169            .ports
170            .iter()
171            .zip(port_ids.iter().copied())
172            .map(|(decl, port_id)| (port_id, decl.instantiate(node_id)))
173            .collect();
174
175        NodeInstantiation {
176            node_id,
177            node: Node {
178                kind: self.kind.clone(),
179                kind_version: self.latest_kind_version,
180                pos,
181                origin: None,
182                selectable: None,
183                focusable: None,
184                draggable: None,
185                connectable: None,
186                deletable: None,
187                parent: None,
188                extent: None,
189                expand_parent: None,
190                size: self.default_size,
191                hidden: false,
192                collapsed: false,
193                ports: port_ids,
194                data: self.default_data.clone(),
195            },
196            ports,
197        }
198    }
199}
200
201impl PortDecl {
202    fn instantiate(&self, node: NodeId) -> Port {
203        Port {
204            node,
205            key: self.key.clone(),
206            dir: self.dir,
207            kind: self.kind,
208            capacity: self.capacity,
209            connectable: None,
210            connectable_start: None,
211            connectable_end: None,
212            ty: self.ty.clone(),
213            data: Value::Null,
214        }
215    }
216}
217
218/// Renderer-neutral node-kind descriptor for adapter palettes and renderer lookup.
219#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
220pub struct NodeKindViewDescriptor {
221    /// Canonical kind key.
222    pub kind: NodeKindKey,
223    /// Adapter-owned renderer lookup key.
224    pub renderer_key: String,
225    /// UI-facing title.
226    pub title: String,
227    /// Category path for create-node search/palette grouping.
228    #[serde(default, skip_serializing_if = "Vec::is_empty")]
229    pub category: Vec<String>,
230    /// Search keywords.
231    #[serde(default, skip_serializing_if = "Vec::is_empty")]
232    pub keywords: Vec<String>,
233    /// Default logical node size for initial adapter layout before measurement.
234    #[serde(default, skip_serializing_if = "Option::is_none")]
235    pub default_size: Option<CanvasSize>,
236    /// Declared ports.
237    #[serde(default, skip_serializing_if = "Vec::is_empty")]
238    pub ports: Vec<PortDecl>,
239    /// Default node payload.
240    #[serde(default)]
241    pub default_data: Value,
242}
243
244impl NodeKindViewDescriptor {
245    pub(crate) fn from_schema(schema: &NodeSchema) -> Self {
246        Self {
247            kind: schema.kind.clone(),
248            renderer_key: schema
249                .renderer_key
250                .clone()
251                .unwrap_or_else(|| schema.kind.0.clone()),
252            title: schema.title.clone(),
253            category: schema.category.clone(),
254            keywords: schema.keywords.clone(),
255            default_size: schema.default_size,
256            ports: schema.ports.clone(),
257            default_data: schema.default_data.clone(),
258        }
259    }
260}