Skip to main content

jellyflow_core/core/
symbol_ref.rs

1use std::collections::BTreeSet;
2
3use serde_json::Value;
4use uuid::Uuid;
5
6use super::{Graph, Node, NodeId, SymbolId};
7
8/// Reserved node kind for a "symbol reference node" (blackboard/variable reference).
9///
10/// This is intentionally a string constant (not an enum) so unknown kinds remain preservable.
11pub const SYMBOL_REF_NODE_KIND: &str = "jellyflow.symbol_ref";
12
13#[derive(Debug, Clone, thiserror::Error, PartialEq, Eq)]
14pub enum SymbolRefNodeError {
15    #[error("symbol ref node missing symbol_id: node={node:?}")]
16    MissingSymbolId { node: NodeId },
17
18    #[error("symbol ref node symbol_id is not a string: node={node:?}")]
19    SymbolIdNotString { node: NodeId },
20
21    #[error("symbol ref node symbol_id is not a valid uuid: node={node:?} value={value:?}")]
22    InvalidSymbolId { node: NodeId, value: String },
23}
24
25#[derive(Debug, Clone, thiserror::Error, PartialEq, Eq)]
26pub enum SymbolRefBindingError {
27    #[error(
28        "symbol ref target is not declared in graph symbols: node={node:?} symbol_id={symbol_id:?}"
29    )]
30    TargetNotDeclared { node: NodeId, symbol_id: SymbolId },
31}
32
33pub fn is_symbol_ref_node(node: &Node) -> bool {
34    node.kind.0 == SYMBOL_REF_NODE_KIND
35}
36
37/// Parses the referenced symbol id from a symbol reference node.
38///
39/// Contract:
40/// - `node.kind == SYMBOL_REF_NODE_KIND` implies `node.data` is an object with a `symbol_id` string.
41/// - `symbol_id` must parse as a UUID.
42pub fn symbol_ref_target_symbol_id(
43    node_id: NodeId,
44    node: &Node,
45) -> Result<Option<SymbolId>, SymbolRefNodeError> {
46    if !is_symbol_ref_node(node) {
47        return Ok(None);
48    }
49
50    let Some(obj) = node.data.as_object() else {
51        return Err(SymbolRefNodeError::MissingSymbolId { node: node_id });
52    };
53
54    let Some(raw) = obj.get("symbol_id") else {
55        return Err(SymbolRefNodeError::MissingSymbolId { node: node_id });
56    };
57
58    let Some(s) = raw.as_str() else {
59        return Err(SymbolRefNodeError::SymbolIdNotString { node: node_id });
60    };
61
62    let uuid = Uuid::parse_str(s).map_err(|_| SymbolRefNodeError::InvalidSymbolId {
63        node: node_id,
64        value: s.to_string(),
65    })?;
66
67    Ok(Some(SymbolId(uuid)))
68}
69
70/// Collects all referenced symbol targets in a graph.
71///
72/// Invalid symbol ref nodes return an error.
73pub fn collect_symbol_ref_targets(graph: &Graph) -> Result<BTreeSet<SymbolId>, SymbolRefNodeError> {
74    let mut out = BTreeSet::new();
75    for (node_id, node) in &graph.nodes {
76        if let Some(target) = symbol_ref_target_symbol_id(*node_id, node)? {
77            out.insert(target);
78        }
79    }
80    Ok(out)
81}
82
83/// Validates that every referenced symbol id is declared in `graph.symbols`.
84pub fn validate_symbol_ref_targets_are_declared(
85    graph: &Graph,
86) -> Result<(), Vec<SymbolRefBindingError>> {
87    let mut errors = Vec::new();
88    for (node_id, node) in &graph.nodes {
89        let Ok(Some(target)) = symbol_ref_target_symbol_id(*node_id, node) else {
90            continue;
91        };
92        if !graph.symbols.contains_key(&target) {
93            errors.push(SymbolRefBindingError::TargetNotDeclared {
94                node: *node_id,
95                symbol_id: target,
96            });
97        }
98    }
99
100    if errors.is_empty() {
101        Ok(())
102    } else {
103        Err(errors)
104    }
105}
106
107/// Helper for creating a symbol ref node payload.
108pub fn symbol_ref_node_data(symbol_id: SymbolId) -> Value {
109    serde_json::json!({ "symbol_id": symbol_id })
110}