Skip to main content

jellyflow_runtime/schema/kit/
mod.rs

1use std::collections::BTreeMap;
2
3use serde::{Deserialize, Serialize};
4use serde_json::Value;
5
6use super::registry::NodeRegistry;
7use super::types::NodeSchema;
8use jellyflow_core::core::{
9    CanvasPoint, Edge, EdgeId, EdgeKind, EdgeViewDescriptor, Graph, GraphBuilder, GraphId, NodeId,
10    NodeKindKey, PortDirection, PortId,
11};
12
13mod builtins;
14
15pub use builtins::{
16    builtin_node_kits, erd_table_manifest, mind_map_knowledge_canvas_manifest,
17    shader_blueprint_manifest, workflow_automation_manifest,
18};
19
20/// Stable identifier for a node kit.
21#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
22#[serde(transparent)]
23pub struct NodeKitKey(pub String);
24
25impl NodeKitKey {
26    pub fn new(key: impl Into<String>) -> Self {
27        Self(key.into())
28    }
29}
30
31impl From<&str> for NodeKitKey {
32    fn from(value: &str) -> Self {
33        Self::new(value)
34    }
35}
36
37impl From<String> for NodeKitKey {
38    fn from(value: String) -> Self {
39        Self::new(value)
40    }
41}
42
43/// Stable identifier for an adapter supported by a kit.
44#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
45#[serde(transparent)]
46pub struct NodeKitAdapterKey(pub String);
47
48impl NodeKitAdapterKey {
49    pub fn new(key: impl Into<String>) -> Self {
50        Self(key.into())
51    }
52}
53
54impl From<&str> for NodeKitAdapterKey {
55    fn from(value: &str) -> Self {
56        Self::new(value)
57    }
58}
59
60impl From<String> for NodeKitAdapterKey {
61    fn from(value: String) -> Self {
62        Self::new(value)
63    }
64}
65
66/// Zoom and spacing hints a kit can publish to adapters.
67#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
68pub struct NodeKitLayoutHints {
69    /// Zoom threshold where full content should be visible.
70    #[serde(default = "NodeKitLayoutHints::default_full_zoom_min")]
71    pub full_zoom_min: f32,
72    /// Zoom threshold where compact content should be visible.
73    #[serde(default = "NodeKitLayoutHints::default_compact_zoom_min")]
74    pub compact_zoom_min: f32,
75    /// Default vertical spacing between field rows.
76    #[serde(default, skip_serializing_if = "Option::is_none")]
77    pub field_spacing: Option<f32>,
78    /// Default vertical spacing between action rows.
79    #[serde(default, skip_serializing_if = "Option::is_none")]
80    pub action_spacing: Option<f32>,
81    /// Human-readable measurement guidance for adapters.
82    #[serde(default, skip_serializing_if = "Option::is_none")]
83    pub measurement_note: Option<String>,
84}
85
86/// Coarse surface density tiers derived from a kit's zoom hints.
87#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
88#[serde(rename_all = "snake_case")]
89pub enum NodeKitContentDensity {
90    Compact,
91    Regular,
92    Full,
93}
94
95impl Default for NodeKitLayoutHints {
96    fn default() -> Self {
97        Self {
98            full_zoom_min: Self::default_full_zoom_min(),
99            compact_zoom_min: Self::default_compact_zoom_min(),
100            field_spacing: None,
101            action_spacing: None,
102            measurement_note: None,
103        }
104    }
105}
106
107impl NodeKitLayoutHints {
108    fn default_full_zoom_min() -> f32 {
109        0.62
110    }
111
112    fn default_compact_zoom_min() -> f32 {
113        0.18
114    }
115
116    pub fn with_full_zoom_min(mut self, zoom_min: f32) -> Self {
117        self.full_zoom_min = zoom_min;
118        self
119    }
120
121    pub fn with_compact_zoom_min(mut self, zoom_min: f32) -> Self {
122        self.compact_zoom_min = zoom_min;
123        self
124    }
125
126    pub fn with_zoom_range(mut self, compact_zoom_min: f32, full_zoom_min: f32) -> Self {
127        self.compact_zoom_min = compact_zoom_min;
128        self.full_zoom_min = full_zoom_min;
129        self
130    }
131
132    pub fn with_field_spacing(mut self, spacing: f32) -> Self {
133        self.field_spacing = Some(spacing);
134        self
135    }
136
137    pub fn with_action_spacing(mut self, spacing: f32) -> Self {
138        self.action_spacing = Some(spacing);
139        self
140    }
141
142    pub fn with_measurement_note(mut self, note: impl Into<String>) -> Self {
143        self.measurement_note = Some(note.into());
144        self
145    }
146
147    pub fn content_density_for_zoom(&self, zoom: f32) -> NodeKitContentDensity {
148        let compact_zoom_min = self.compact_zoom_min.min(self.full_zoom_min);
149        let full_zoom_min = self.full_zoom_min.max(self.compact_zoom_min);
150
151        if zoom >= full_zoom_min {
152            NodeKitContentDensity::Full
153        } else if zoom >= compact_zoom_min {
154            NodeKitContentDensity::Regular
155        } else {
156            NodeKitContentDensity::Compact
157        }
158    }
159}
160
161/// A node instance inside a kit fixture.
162#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
163pub struct NodeKitFixtureNode {
164    pub alias: String,
165    pub kind: NodeKindKey,
166    pub pos: CanvasPoint,
167    #[serde(default, skip_serializing_if = "Option::is_none")]
168    pub data: Option<Value>,
169}
170
171impl NodeKitFixtureNode {
172    pub fn new(alias: impl Into<String>, kind: impl Into<NodeKindKey>, pos: CanvasPoint) -> Self {
173        Self {
174            alias: alias.into(),
175            kind: kind.into(),
176            pos,
177            data: None,
178        }
179    }
180
181    pub fn with_data(mut self, data: Value) -> Self {
182        self.data = Some(data);
183        self
184    }
185}
186
187/// An edge instance inside a kit fixture.
188#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
189pub struct NodeKitFixtureEdge {
190    pub from: String,
191    #[serde(default, skip_serializing_if = "Option::is_none")]
192    pub from_port: Option<String>,
193    pub to: String,
194    #[serde(default, skip_serializing_if = "Option::is_none")]
195    pub to_port: Option<String>,
196    pub kind: EdgeKind,
197    #[serde(default, skip_serializing_if = "Option::is_none")]
198    pub data: Option<Value>,
199    #[serde(default, skip_serializing_if = "EdgeViewDescriptor::is_default")]
200    pub view: EdgeViewDescriptor,
201}
202
203impl NodeKitFixtureEdge {
204    pub fn new(from: impl Into<String>, to: impl Into<String>, kind: EdgeKind) -> Self {
205        Self {
206            from: from.into(),
207            from_port: None,
208            to: to.into(),
209            to_port: None,
210            kind,
211            data: None,
212            view: EdgeViewDescriptor::default(),
213        }
214    }
215
216    pub fn with_from_port(mut self, from_port: impl Into<String>) -> Self {
217        self.from_port = Some(from_port.into());
218        self
219    }
220
221    pub fn with_to_port(mut self, to_port: impl Into<String>) -> Self {
222        self.to_port = Some(to_port.into());
223        self
224    }
225
226    pub fn with_data(mut self, data: Value) -> Self {
227        self.data = Some(data);
228        self
229    }
230
231    pub fn with_view(mut self, view: EdgeViewDescriptor) -> Self {
232        self.view = view;
233        self
234    }
235}
236
237/// A reusable fixture graph for a node kit.
238#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
239pub struct NodeKitFixture {
240    pub key: String,
241    pub title: String,
242    #[serde(default, skip_serializing_if = "Option::is_none")]
243    pub description: Option<String>,
244    #[serde(default, skip_serializing_if = "Vec::is_empty")]
245    pub nodes: Vec<NodeKitFixtureNode>,
246    #[serde(default, skip_serializing_if = "Vec::is_empty")]
247    pub edges: Vec<NodeKitFixtureEdge>,
248    #[serde(default, skip_serializing_if = "Option::is_none")]
249    pub expected_node_count: Option<usize>,
250    #[serde(default, skip_serializing_if = "Option::is_none")]
251    pub expected_edge_count: Option<usize>,
252}
253
254impl NodeKitFixture {
255    pub fn new(key: impl Into<String>, title: impl Into<String>) -> Self {
256        Self {
257            key: key.into(),
258            title: title.into(),
259            description: None,
260            nodes: Vec::new(),
261            edges: Vec::new(),
262            expected_node_count: None,
263            expected_edge_count: None,
264        }
265    }
266
267    pub fn with_description(mut self, description: impl Into<String>) -> Self {
268        self.description = Some(description.into());
269        self
270    }
271
272    pub fn node(mut self, node: NodeKitFixtureNode) -> Self {
273        self.nodes.push(node);
274        self
275    }
276
277    pub fn edge(mut self, edge: NodeKitFixtureEdge) -> Self {
278        self.edges.push(edge);
279        self
280    }
281
282    pub fn expect_counts(mut self, nodes: usize, edges: usize) -> Self {
283        self.expected_node_count = Some(nodes);
284        self.expected_edge_count = Some(edges);
285        self
286    }
287
288    pub fn build_graph(
289        &self,
290        kit_key: &NodeKitKey,
291        registry: &NodeRegistry,
292    ) -> Result<Graph, NodeKitFixtureError> {
293        let mut builder = GraphBuilder::new(GraphId::from_u128(stable_u128(&format!(
294            "kit.fixture.graph::{}::{}",
295            kit_key.0, self.key
296        ))));
297        let mut alias_to_kind = BTreeMap::new();
298        let mut alias_to_ports: BTreeMap<String, BTreeMap<String, PortId>> = BTreeMap::new();
299
300        for node in &self.nodes {
301            let schema =
302                registry
303                    .get(&node.kind)
304                    .ok_or_else(|| NodeKitFixtureError::MissingSchema {
305                        fixture: self.key.clone(),
306                        kind: node.kind.clone(),
307                    })?;
308            let node_id = NodeId::from_u128(stable_u128(&format!(
309                "kit.fixture.node::{}::{}",
310                self.key, node.alias
311            )));
312            let port_ids = schema
313                .ports
314                .iter()
315                .map(|decl| {
316                    PortId::from_u128(stable_u128(&format!(
317                        "kit.fixture.port::{}::{}::{}",
318                        self.key, node.alias, decl.key.0
319                    )))
320                })
321                .collect::<Vec<_>>();
322            let mut inst = schema
323                .instantiate_with_ids(node_id, node.pos, port_ids)
324                .map_err(|error| NodeKitFixtureError::Instantiation {
325                    fixture: self.key.clone(),
326                    kind: node.kind.clone(),
327                    error,
328                })?;
329
330            if let Some(data) = &node.data {
331                inst.node.data = data.clone();
332            }
333
334            alias_to_kind.insert(node.alias.clone(), node.kind.clone());
335            let mut port_map = BTreeMap::new();
336            for ((port_id, port), decl) in inst.ports.into_iter().zip(schema.ports.iter()) {
337                port_map.insert(decl.key.0.clone(), port_id);
338                builder = builder.with_port(port_id, port);
339            }
340            alias_to_ports.insert(node.alias.clone(), port_map);
341            builder = builder.with_node(node_id, inst.node);
342        }
343
344        for edge in &self.edges {
345            let from_kind = alias_to_kind.get(&edge.from).ok_or_else(|| {
346                NodeKitFixtureError::MissingNodeAlias {
347                    fixture: self.key.clone(),
348                    alias: edge.from.clone(),
349                }
350            })?;
351            let to_kind = alias_to_kind.get(&edge.to).ok_or_else(|| {
352                NodeKitFixtureError::MissingNodeAlias {
353                    fixture: self.key.clone(),
354                    alias: edge.to.clone(),
355                }
356            })?;
357            let from_schema =
358                registry
359                    .get(from_kind)
360                    .ok_or_else(|| NodeKitFixtureError::MissingSchema {
361                        fixture: self.key.clone(),
362                        kind: from_kind.clone(),
363                    })?;
364            let to_schema =
365                registry
366                    .get(to_kind)
367                    .ok_or_else(|| NodeKitFixtureError::MissingSchema {
368                        fixture: self.key.clone(),
369                        kind: to_kind.clone(),
370                    })?;
371            let from_port_key = resolve_fixture_port_key(
372                from_schema,
373                PortDirection::Out,
374                edge.from_port.as_deref(),
375            )?;
376            let to_port_key =
377                resolve_fixture_port_key(to_schema, PortDirection::In, edge.to_port.as_deref())?;
378            let from_port_id = alias_to_ports
379                .get(&edge.from)
380                .and_then(|ports| ports.get(&from_port_key))
381                .copied()
382                .ok_or_else(|| NodeKitFixtureError::MissingPort {
383                    fixture: self.key.clone(),
384                    alias: edge.from.clone(),
385                    port: from_port_key.clone(),
386                })?;
387            let to_port_id = alias_to_ports
388                .get(&edge.to)
389                .and_then(|ports| ports.get(&to_port_key))
390                .copied()
391                .ok_or_else(|| NodeKitFixtureError::MissingPort {
392                    fixture: self.key.clone(),
393                    alias: edge.to.clone(),
394                    port: to_port_key.clone(),
395                })?;
396
397            let edge_id = EdgeId::from_u128(stable_u128(&format!(
398                "kit.fixture.edge::{}::{}::{}:{}->{}:{}::{:?}",
399                kit_key.0, self.key, edge.from, from_port_key, edge.to, to_port_key, edge.kind
400            )));
401            let mut record = Edge::new(edge.kind, from_port_id, to_port_id);
402            if let Some(data) = &edge.data {
403                record.data = data.clone();
404            }
405            record.view = edge.view.clone();
406            builder = builder.with_edge(edge_id, record);
407        }
408
409        Ok(builder.build_unchecked())
410    }
411}
412
413/// Error returned when a kit fixture cannot be materialized.
414#[derive(Debug, thiserror::Error)]
415pub enum NodeKitFixtureError {
416    #[error("node kit fixture `{fixture}` missing node schema for `{kind:?}`")]
417    MissingSchema { fixture: String, kind: NodeKindKey },
418    #[error("node kit `{kit}` missing fixture `{fixture}`")]
419    MissingFixture { kit: String, fixture: String },
420    #[error("node kit registry missing kit `{kit:?}`")]
421    MissingKit { kit: NodeKitKey },
422    #[error("node kit fixture `{fixture}` missing node alias `{alias}`")]
423    MissingNodeAlias { fixture: String, alias: String },
424    #[error("node kit fixture `{fixture}` missing port `{port}` on node `{alias}`")]
425    MissingPort {
426        fixture: String,
427        alias: String,
428        port: String,
429    },
430    #[error("node kit fixture `{fixture}` failed to instantiate `{kind:?}`: {error}")]
431    Instantiation {
432        fixture: String,
433        kind: NodeKindKey,
434        error: super::NodeInstantiationError,
435    },
436}
437
438/// A versioned package of semantic node families, layout hints, and fixtures.
439#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
440pub struct NodeKitManifest {
441    pub key: NodeKitKey,
442    pub title: String,
443    pub version: u32,
444    #[serde(default, skip_serializing_if = "Vec::is_empty")]
445    pub supported_adapters: Vec<NodeKitAdapterKey>,
446    #[serde(default, skip_serializing_if = "Vec::is_empty")]
447    pub capabilities: Vec<String>,
448    #[serde(default)]
449    pub layout_hints: NodeKitLayoutHints,
450    #[serde(default, skip_serializing_if = "Vec::is_empty")]
451    pub recipes: Vec<NodeSchema>,
452    #[serde(default, skip_serializing_if = "Vec::is_empty")]
453    pub fixtures: Vec<NodeKitFixture>,
454}
455
456impl NodeKitManifest {
457    pub fn new(key: impl Into<NodeKitKey>, title: impl Into<String>) -> Self {
458        Self {
459            key: key.into(),
460            title: title.into(),
461            version: 1,
462            supported_adapters: Vec::new(),
463            capabilities: Vec::new(),
464            layout_hints: NodeKitLayoutHints::default(),
465            recipes: Vec::new(),
466            fixtures: Vec::new(),
467        }
468    }
469
470    pub fn with_version(mut self, version: u32) -> Self {
471        self.version = version;
472        self
473    }
474
475    pub fn with_supported_adapter(mut self, adapter: impl Into<NodeKitAdapterKey>) -> Self {
476        self.supported_adapters.push(adapter.into());
477        self
478    }
479
480    pub fn with_capability(mut self, capability: impl Into<String>) -> Self {
481        self.capabilities.push(capability.into());
482        self
483    }
484
485    pub fn with_layout_hints(mut self, layout_hints: NodeKitLayoutHints) -> Self {
486        self.layout_hints = layout_hints;
487        self
488    }
489
490    pub fn recipe(mut self, recipe: NodeSchema) -> Self {
491        self.recipes.push(recipe);
492        self
493    }
494
495    pub fn recipes(mut self, recipes: impl IntoIterator<Item = NodeSchema>) -> Self {
496        self.recipes.extend(recipes);
497        self
498    }
499
500    pub fn fixture(mut self, fixture: NodeKitFixture) -> Self {
501        self.fixtures.push(fixture);
502        self
503    }
504
505    pub fn fixtures(mut self, fixtures: impl IntoIterator<Item = NodeKitFixture>) -> Self {
506        self.fixtures.extend(fixtures);
507        self
508    }
509
510    pub fn recipe_for_kind(&self, kind: &NodeKindKey) -> Option<&NodeSchema> {
511        self.recipes.iter().find(|recipe| &recipe.kind == kind)
512    }
513
514    pub fn fixture_for_key(&self, key: &str) -> Option<&NodeKitFixture> {
515        self.fixtures.iter().find(|fixture| fixture.key == key)
516    }
517
518    pub fn layout_hints(&self) -> &NodeKitLayoutHints {
519        &self.layout_hints
520    }
521
522    pub fn node_registry(&self) -> NodeRegistry {
523        let mut registry = NodeRegistry::new();
524        for recipe in &self.recipes {
525            registry.register(recipe.clone());
526        }
527        registry
528    }
529
530    pub fn build_fixture_graph(&self, key: &str) -> Result<Graph, NodeKitFixtureError> {
531        let fixture =
532            self.fixture_for_key(key)
533                .ok_or_else(|| NodeKitFixtureError::MissingFixture {
534                    kit: self.key.0.clone(),
535                    fixture: key.to_owned(),
536                })?;
537        fixture.build_graph(&self.key, &self.node_registry())
538    }
539}
540
541/// Registry for reusable node kits.
542#[derive(Default, Clone)]
543pub struct NodeKitRegistry {
544    kits: BTreeMap<NodeKitKey, NodeKitManifest>,
545}
546
547impl std::fmt::Debug for NodeKitRegistry {
548    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
549        f.debug_struct("NodeKitRegistry")
550            .field("kit_count", &self.kits.len())
551            .finish()
552    }
553}
554
555impl NodeKitRegistry {
556    pub fn new() -> Self {
557        Self::default()
558    }
559
560    pub fn builtin() -> Self {
561        builtin_node_kits()
562    }
563
564    pub fn register(&mut self, manifest: NodeKitManifest) -> &mut Self {
565        self.kits.insert(manifest.key.clone(), manifest);
566        self
567    }
568
569    pub fn manifest(&self, key: &NodeKitKey) -> Option<&NodeKitManifest> {
570        self.kits.get(key)
571    }
572
573    pub fn manifest_for_kind(&self, kind: &NodeKindKey) -> Option<&NodeKitManifest> {
574        self.manifests()
575            .find(|manifest| manifest.recipe_for_kind(kind).is_some())
576    }
577
578    pub fn manifests(&self) -> impl Iterator<Item = &NodeKitManifest> {
579        self.kits.values()
580    }
581
582    pub fn node_registry(&self) -> NodeRegistry {
583        let mut registry = NodeRegistry::new();
584        for manifest in self.manifests() {
585            for recipe in &manifest.recipes {
586                registry.register(recipe.clone());
587            }
588        }
589        registry
590    }
591
592    pub fn recipe_for_kind(&self, kind: &NodeKindKey) -> Option<&NodeSchema> {
593        self.manifests()
594            .find_map(|manifest| manifest.recipe_for_kind(kind))
595    }
596
597    pub fn layout_hints_for_kind(&self, kind: &NodeKindKey) -> Option<&NodeKitLayoutHints> {
598        self.manifest_for_kind(kind)
599            .map(NodeKitManifest::layout_hints)
600    }
601
602    pub fn fixture_graph(
603        &self,
604        kit_key: &NodeKitKey,
605        fixture_key: &str,
606    ) -> Result<Graph, NodeKitFixtureError> {
607        let manifest = self
608            .manifest(kit_key)
609            .ok_or_else(|| NodeKitFixtureError::MissingKit {
610                kit: kit_key.clone(),
611            })?;
612        manifest.build_fixture_graph(fixture_key)
613    }
614}
615
616fn resolve_fixture_port_key(
617    schema: &NodeSchema,
618    direction: PortDirection,
619    requested: Option<&str>,
620) -> Result<String, NodeKitFixtureError> {
621    if let Some(requested) = requested {
622        if schema.ports.iter().any(|decl| decl.key.0 == requested) {
623            return Ok(requested.to_owned());
624        }
625        return Err(NodeKitFixtureError::MissingPort {
626            fixture: schema.kind.0.clone(),
627            alias: schema.title.clone(),
628            port: requested.to_owned(),
629        });
630    }
631
632    schema
633        .ports
634        .iter()
635        .find(|decl| decl.dir == direction)
636        .map(|decl| decl.key.0.clone())
637        .ok_or_else(|| NodeKitFixtureError::MissingPort {
638            fixture: schema.kind.0.clone(),
639            alias: schema.title.clone(),
640            port: format!("{direction:?}"),
641        })
642}
643
644fn stable_u128(text: &str) -> u128 {
645    const OFFSET: u128 = 0x6c62_272e_07bb_0142_62b8_2175_6295_c58d;
646    const PRIME: u128 = 0x0000_0000_0100_0000_0000_0000_0000_013b;
647    let mut hash = OFFSET;
648    for byte in text.as_bytes() {
649        hash ^= *byte as u128;
650        hash = hash.wrapping_mul(PRIME);
651    }
652    hash
653}