1use daedalus_data::model::Value;
2use daedalus_planner::{ComputeAffinity, Edge, Graph, NodeInstance, NodeRef, PortRef};
3use daedalus_registry::{ids::NodeId, store::Registry};
4use crate::handles::{NodeHandleLike, PortHandle};
5use crate::host_bridge::HOST_BRIDGE_META_KEY;
6use std::collections::{BTreeMap, HashMap};
7
8use crate::host_bridge::HOST_BRIDGE_ID;
9
10#[derive(Clone, Debug)]
13pub struct NodeSpec {
14 pub id: String,
15}
16
17impl NodeSpec {
18 pub fn new(id: impl Into<String>) -> Self {
19 Self { id: id.into() }
20 }
21
22 pub fn prefixed(prefix: &str, id: &str) -> Self {
23 Self {
24 id: format!("{prefix}:{id}"),
25 }
26 }
27}
28
29impl From<(String, String)> for NodeSpec {
30 fn from(value: (String, String)) -> Self {
31 NodeSpec { id: value.0 }
32 }
33}
34
35impl<'a> From<(&'a str, &'a str)> for NodeSpec {
36 fn from(value: (&'a str, &'a str)) -> Self {
37 NodeSpec {
38 id: value.0.to_string(),
39 }
40 }
41}
42
43#[derive(Clone, Debug)]
45pub struct PortSpec {
46 pub node: String,
47 pub port: String,
48}
49
50pub trait IntoPortSpec {
51 fn into_spec(self) -> PortSpec;
52}
53
54impl IntoPortSpec for &str {
55 fn into_spec(self) -> PortSpec {
56 let mut parts = self.split(':');
57 PortSpec {
58 node: parts.next().unwrap_or("").to_string(),
59 port: parts.next().unwrap_or("").to_string(),
60 }
61 }
62}
63
64impl IntoPortSpec for (&str, &str) {
65 fn into_spec(self) -> PortSpec {
66 PortSpec {
67 node: self.0.to_string(),
68 port: self.1.to_string(),
69 }
70 }
71}
72
73impl IntoPortSpec for (String, String) {
74 fn into_spec(self) -> PortSpec {
75 PortSpec {
76 node: self.0,
77 port: self.1,
78 }
79 }
80}
81
82impl IntoPortSpec for &PortHandle {
83 fn into_spec(self) -> PortSpec {
84 PortSpec {
85 node: self.node_alias.clone(),
86 port: self.port.clone(),
87 }
88 }
89}
90
91fn is_host_bridge(node: &NodeInstance) -> bool {
92 matches!(
93 node.metadata.get(HOST_BRIDGE_META_KEY),
94 Some(Value::Bool(true))
95 )
96}
97
98#[derive(Clone, Debug)]
100pub struct NestedGraph {
101 graph: Graph,
102 host_alias: String,
103 host_index: usize,
104}
105
106impl NestedGraph {
107 pub fn new(graph: Graph, host_alias: impl Into<String>) -> Result<Self, &'static str> {
109 let host_alias = host_alias.into();
110 let host_index = graph
111 .nodes
112 .iter()
113 .position(|n| is_host_bridge(n) && n.label.as_deref() == Some(host_alias.as_str()))
114 .ok_or("host bridge alias not found in nested graph")?;
115
116 Ok(Self {
117 graph,
118 host_alias,
119 host_index,
120 })
121 }
122
123 pub fn first_host(graph: Graph) -> Result<Self, &'static str> {
125 let (host_index, host_alias) = graph
126 .nodes
127 .iter()
128 .enumerate()
129 .find_map(|(idx, n)| {
130 is_host_bridge(n).then(|| (idx, n.label.clone().unwrap_or_else(|| n.id.0.clone())))
131 })
132 .ok_or("nested graph missing host bridge")?;
133
134 Ok(Self {
135 graph,
136 host_alias,
137 host_index,
138 })
139 }
140
141 pub fn host_alias(&self) -> &str {
142 &self.host_alias
143 }
144
145 pub fn graph(&self) -> &Graph {
146 &self.graph
147 }
148}
149
150#[derive(Clone, Debug)]
152pub struct NestedGraphHandle {
153 pub alias: String,
154 pub inputs: BTreeMap<String, Vec<PortRef>>, pub outputs: BTreeMap<String, Vec<PortRef>>, }
157
158impl NestedGraphHandle {
159 pub fn input(&self, port: impl Into<String>) -> PortHandle {
161 PortHandle::new(self.alias.clone(), port)
162 }
163
164 pub fn output(&self, port: impl Into<String>) -> PortHandle {
166 PortHandle::new(self.alias.clone(), port)
167 }
168
169 pub fn input_ports(&self) -> impl Iterator<Item = &str> {
170 self.inputs.keys().map(|k| k.as_str())
171 }
172
173 pub fn output_ports(&self) -> impl Iterator<Item = &str> {
174 self.outputs.keys().map(|k| k.as_str())
175 }
176}
177
178pub struct GraphBuilder<'r> {
180 reg: &'r Registry,
181 nodes: Vec<NodeInstance>,
182 edges: Vec<Edge>,
183 const_overrides: HashMap<String, HashMap<String, Option<Value>>>,
184 node_metadata_overrides: HashMap<String, BTreeMap<String, Value>>,
185 graph_metadata: BTreeMap<String, String>,
186 graph_metadata_values: BTreeMap<String, Value>,
187 injected_node_metadata: BTreeMap<String, Value>,
188 injected_node_metadata_overwrite: BTreeMap<String, Value>,
189 host_bridge_alias: Option<String>,
190 host_bridge_added: bool,
191 nested: HashMap<String, NestedGraphHandle>,
192}
193
194impl<'r> GraphBuilder<'r> {
195 pub fn new(registry: &'r Registry) -> Self {
196 Self {
197 reg: registry,
198 nodes: Vec::new(),
199 edges: Vec::new(),
200 const_overrides: HashMap::new(),
201 node_metadata_overrides: HashMap::new(),
202 graph_metadata: BTreeMap::new(),
203 graph_metadata_values: BTreeMap::new(),
204 injected_node_metadata: BTreeMap::new(),
205 injected_node_metadata_overwrite: BTreeMap::new(),
206 host_bridge_alias: Some("host".to_string()),
207 host_bridge_added: false,
208 nested: HashMap::new(),
209 }
210 }
211
212 pub fn graph_metadata(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
216 let key = key.into();
217 let value = value.into();
218 self.graph_metadata.insert(key.clone(), value.clone());
219 self.graph_metadata_values
220 .insert(key, Value::String(value.into()));
221 self
222 }
223
224 pub fn graph_metadata_value(mut self, key: impl Into<String>, value: Value) -> Self {
228 self.graph_metadata_values.insert(key.into(), value);
229 self
230 }
231
232 pub fn inject_node_metadata(mut self, key: impl Into<String>, value: Value) -> Self {
237 self.injected_node_metadata.insert(key.into(), value);
238 self
239 }
240
241 pub fn inject_node_metadata_overwrite(mut self, key: impl Into<String>, value: Value) -> Self {
243 self.injected_node_metadata_overwrite
244 .insert(key.into(), value);
245 self
246 }
247
248 pub fn node_spec(self, spec: NodeSpec, alias: &str) -> Self {
250 self.node_id(&spec.id, alias)
251 }
252
253 pub fn node_pair<T>(self, pair: T, alias: &str) -> Self
255 where
256 T: Into<NodeSpec>,
257 {
258 let spec: NodeSpec = pair.into();
259 self.node_spec(spec, alias)
260 }
261
262 pub fn node_handle_like(self, handle: &dyn NodeHandleLike) -> Self {
264 self.node_id(handle.id(), handle.alias())
265 }
266
267 pub fn node<H>(self, handle: H) -> Self
269 where
270 H: NodeHandleLike,
271 {
272 self.node_id(handle.id(), handle.alias())
273 }
274
275 pub fn node_from_id(self, id: &str, alias: &str) -> Self {
277 self.node_id(id, alias)
278 }
279
280 pub fn node_id(mut self, id: &str, alias: &str) -> Self {
282 let view = self.reg.view();
283 let desc = view.nodes.get(&NodeId::new(id));
284 let ports = desc
285 .map(|desc| {
286 (
287 desc.inputs.iter().map(|p| p.name.clone()).collect(),
288 desc.outputs.iter().map(|p| p.name.clone()).collect(),
289 )
290 })
291 .unwrap_or_default();
292
293 let mut const_inputs = Vec::new();
295 let mut compute = ComputeAffinity::CpuOnly;
296 let mut metadata: BTreeMap<String, Value> = BTreeMap::new();
297 let mut sync_groups = Vec::new();
298 if let Some(desc) = desc {
299 compute = desc.default_compute;
300 metadata = desc.metadata.clone();
301 sync_groups = desc.sync_groups.clone();
302 for port in &desc.inputs {
303 if let Some(v) = &port.const_value {
304 const_inputs.push((port.name.clone(), v.clone()));
305 }
306 }
307 }
308 if let Some(over) = self.const_overrides.get(alias) {
309 for (p, v) in over {
310 match v {
311 Some(val) => {
312 const_inputs.retain(|(name, _)| name != p);
313 const_inputs.push((p.clone(), val.clone()));
314 }
315 None => const_inputs.retain(|(name, _)| name != p),
316 }
317 }
318 }
319 if let Some(overrides) = self.node_metadata_overrides.get(alias) {
320 for (k, v) in overrides {
321 metadata.insert(k.clone(), v.clone());
322 }
323 }
324
325 self.nodes.push(NodeInstance {
326 id: daedalus_registry::ids::NodeId::new(id),
327 bundle: Some(id.to_string()),
328 label: Some(alias.to_string()),
329 inputs: ports.0,
330 outputs: ports.1,
331 compute,
332 const_inputs,
333 sync_groups,
334 metadata,
335 });
336 self
337 }
338
339 pub fn node_with_compute(mut self, id: &str, alias: &str, compute: ComputeAffinity) -> Self {
341 self = self.node_id(id, alias);
342 if let Some(last) = self.nodes.last_mut() {
343 last.compute = compute;
344 }
345 self
346 }
347
348 pub fn sync_groups(mut self, groups: Vec<daedalus_core::sync::SyncGroup>) -> Self {
350 if let Some(last) = self.nodes.last_mut() {
351 last.sync_groups = groups;
352 }
353 self
354 }
355
356 pub fn node_metadata(
359 self,
360 handle: &impl NodeHandleLike,
361 key: impl Into<String>,
362 value: Value,
363 ) -> Self {
364 self.node_metadata_by_id(handle.alias(), key, value)
365 }
366
367 pub fn node_metadata_by_id(
370 mut self,
371 node_alias: impl Into<String>,
372 key: impl Into<String>,
373 value: Value,
374 ) -> Self {
375 let alias = node_alias.into();
376 let key = key.into();
377 let entry = self
378 .node_metadata_overrides
379 .entry(alias.clone())
380 .or_default();
381 entry.insert(key.clone(), value.clone());
382 if let Some(node) = self
383 .nodes
384 .iter_mut()
385 .find(|n| n.label.as_deref() == Some(alias.as_str()))
386 {
387 node.metadata.insert(key, value);
388 }
389 self
390 }
391
392 pub fn node_metadata_map<H, K, I>(self, handle: &H, metadata: I) -> Self
394 where
395 H: NodeHandleLike,
396 K: Into<String>,
397 I: IntoIterator<Item = (K, Value)>,
398 {
399 self.node_metadata_map_by_id(handle.alias(), metadata)
400 }
401
402 pub fn node_metadata_map_by_id<K, I>(
404 mut self,
405 node_alias: impl Into<String>,
406 metadata: I,
407 ) -> Self
408 where
409 K: Into<String>,
410 I: IntoIterator<Item = (K, Value)>,
411 {
412 let alias = node_alias.into();
413 for (k, v) in metadata {
414 self = self.node_metadata_by_id(alias.clone(), k.into(), v);
415 }
416 self
417 }
418
419 pub fn const_input(mut self, port: &PortHandle, value: Option<Value>) -> Self {
421 let alias = port.node_alias.clone();
422 let port_name = port.port.clone();
423 let entry = self.const_overrides.entry(alias.clone()).or_default();
424 entry.insert(port_name.clone(), value.clone());
425 if let Some(node) = self
426 .nodes
427 .iter_mut()
428 .find(|n| n.label.as_deref() == Some(alias.as_str()))
429 {
430 match value {
431 Some(v) => {
432 node.const_inputs.retain(|(name, _)| name != &port_name);
433 node.const_inputs.push((port_name, v));
434 }
435 None => node.const_inputs.retain(|(name, _)| name != &port_name),
436 }
437 }
438 self
439 }
440
441 pub fn host_bridge(mut self, alias: impl Into<String>) -> Self {
443 let alias = alias.into();
444 self.host_bridge_alias = Some(alias.clone());
445 self.ensure_host_bridge(Some(alias))
446 }
447
448 fn ensure_host_bridge(mut self, alias: Option<String>) -> Self {
449 let alias = alias
450 .or_else(|| self.host_bridge_alias.clone())
451 .unwrap_or_else(|| "host".to_string());
452 if self.host_bridge_added {
453 return self;
454 }
455 let exists = self
456 .nodes
457 .iter()
458 .any(|n| n.label.as_deref() == Some(alias.as_str()) || n.id.0 == HOST_BRIDGE_ID);
459 if exists {
460 self.host_bridge_added = true;
461 return self;
462 }
463 self.host_bridge_added = true;
464 self.nodes.push(NodeInstance {
465 id: daedalus_registry::ids::NodeId::new(HOST_BRIDGE_ID),
466 bundle: None,
467 label: Some(alias),
468 inputs: Vec::new(),
469 outputs: Vec::new(),
470 compute: ComputeAffinity::CpuOnly,
471 const_inputs: Vec::new(),
472 sync_groups: Vec::new(),
473 metadata: BTreeMap::from([
474 (HOST_BRIDGE_META_KEY.to_string(), Value::Bool(true)),
475 (
479 "dynamic_inputs".to_string(),
480 Value::String(std::borrow::Cow::from("generic")),
481 ),
482 (
483 "dynamic_outputs".to_string(),
484 Value::String(std::borrow::Cow::from("generic")),
485 ),
486 ]),
487 });
488 self
489 }
490
491 pub(crate) fn ensure_host_bridge_port(mut self, is_output: bool, port: &str) -> Self {
492 let host_alias = self
493 .host_bridge_alias
494 .clone()
495 .unwrap_or_else(|| "host".to_string());
496 if let Some(host) = self.nodes.iter_mut().find(|n| {
497 is_host_bridge(n)
498 && (n.label.as_deref() == Some(host_alias.as_str()) || n.id.0 == HOST_BRIDGE_ID)
499 }) {
500 let ports = if is_output {
501 &mut host.outputs
502 } else {
503 &mut host.inputs
504 };
505 if !ports.iter().any(|p| p == port) {
506 ports.push(port.to_string());
507 }
508 }
509 self
510 }
511
512 pub fn const_input_by_id(
514 mut self,
515 node_alias: impl Into<String>,
516 port: impl Into<String>,
517 value: Option<Value>,
518 ) -> Self {
519 let node_alias = node_alias.into();
520 let port = port.into();
521 let entry = self.const_overrides.entry(node_alias.clone()).or_default();
522 entry.insert(port.clone(), value.clone());
523 if let Some(node) = self
524 .nodes
525 .iter_mut()
526 .find(|n| n.label.as_deref() == Some(node_alias.as_str()))
527 {
528 match value {
529 Some(v) => {
530 node.const_inputs.retain(|(name, _)| name != &port);
531 node.const_inputs.push((port, v));
532 }
533 None => node.const_inputs.retain(|(name, _)| name != &port),
534 }
535 }
536 self
537 }
538
539 pub fn nest(
542 mut self,
543 nested: &NestedGraph,
544 alias: impl Into<String>,
545 ) -> (Self, NestedGraphHandle) {
546 let alias = alias.into();
547 if self.nested.contains_key(&alias)
548 || self
549 .nodes
550 .iter()
551 .any(|n| n.label.as_deref() == Some(alias.as_str()))
552 {
553 panic!("nested alias '{}' already in use", alias);
554 }
555 let prefix = format!("{alias}::");
556 let mut index_map: Vec<Option<usize>> = vec![None; nested.graph.nodes.len()];
557
558 for (idx, node) in nested.graph.nodes.iter().enumerate() {
559 if idx == nested.host_index {
560 continue;
561 }
562 let mut cloned = node.clone();
563 let base_label = cloned.label.clone().unwrap_or_else(|| cloned.id.0.clone());
564 cloned.label = Some(format!("{prefix}{base_label}"));
565 let new_idx = self.nodes.len();
566 self.nodes.push(cloned);
567 index_map[idx] = Some(new_idx);
568 }
569
570 let mut inputs: BTreeMap<String, Vec<PortRef>> = BTreeMap::new();
571 let mut outputs: BTreeMap<String, Vec<PortRef>> = BTreeMap::new();
572
573 for edge in &nested.graph.edges {
574 let from_is_host = edge.from.node.0 == nested.host_index;
575 let to_is_host = edge.to.node.0 == nested.host_index;
576
577 match (from_is_host, to_is_host) {
578 (true, false) => {
579 if let Some(target_idx) = index_map[edge.to.node.0] {
580 inputs
581 .entry(edge.from.port.clone())
582 .or_default()
583 .push(PortRef {
584 node: NodeRef(target_idx),
585 port: edge.to.port.clone(),
586 });
587 }
588 }
589 (false, true) => {
590 if let Some(source_idx) = index_map[edge.from.node.0] {
591 outputs
592 .entry(edge.to.port.clone())
593 .or_default()
594 .push(PortRef {
595 node: NodeRef(source_idx),
596 port: edge.from.port.clone(),
597 });
598 }
599 }
600 (false, false) => {
601 let Some(from_idx) = index_map[edge.from.node.0] else {
602 continue;
603 };
604 let Some(to_idx) = index_map[edge.to.node.0] else {
605 continue;
606 };
607
608 self.edges.push(Edge {
609 from: PortRef {
610 node: NodeRef(from_idx),
611 port: edge.from.port.clone(),
612 },
613 to: PortRef {
614 node: NodeRef(to_idx),
615 port: edge.to.port.clone(),
616 },
617 metadata: edge.metadata.clone(),
618 });
619 }
620 (true, true) => {}
621 }
622 }
623
624 let handle = NestedGraphHandle {
625 alias: alias.clone(),
626 inputs,
627 outputs,
628 };
629
630 self.nested.insert(alias.clone(), handle.clone());
631 (self, handle)
632 }
633
634 pub fn connect<F, T>(self, from: F, to: T) -> Self
635 where
636 F: IntoPortSpec,
637 T: IntoPortSpec,
638 {
639 self.connect_ports(from, to)
640 }
641
642 pub fn connect_with_metadata<F, T, K>(
644 mut self,
645 from: F,
646 to: T,
647 metadata: impl IntoIterator<Item = (K, Value)>,
648 ) -> Self
649 where
650 F: IntoPortSpec,
651 T: IntoPortSpec,
652 K: Into<String>,
653 {
654 let edge_idx = self.edges.len();
655 self = self.connect_ports(from, to);
656 let meta: Vec<(String, Value)> = metadata.into_iter().map(|(k, v)| (k.into(), v)).collect();
657 for e in self.edges.iter_mut().skip(edge_idx) {
658 for (k, v) in &meta {
659 e.metadata.insert(k.clone(), v.clone());
660 }
661 }
662 self
663 }
664
665 pub fn edge_metadata<F, T>(
667 mut self,
668 from: F,
669 to: T,
670 key: impl Into<String>,
671 value: Value,
672 ) -> Self
673 where
674 F: IntoPortSpec,
675 T: IntoPortSpec,
676 {
677 let from_spec = from.into_spec();
678 let to_spec = to.into_spec();
679 let f_idx = self.find_index(&from_spec.node);
680 let t_idx = self.find_index(&to_spec.node);
681 let key = key.into();
682 for edge in &mut self.edges {
683 if edge.from.node.0 == f_idx
684 && edge.to.node.0 == t_idx
685 && edge.from.port == from_spec.port
686 && edge.to.port == to_spec.port
687 {
688 edge.metadata.insert(key.clone(), value.clone());
689 }
690 }
691 self
692 }
693
694 pub fn connect_handles(mut self, from: &PortHandle, to: &PortHandle) -> Self {
696 self = self.connect_ports(from, to);
697 self
698 }
699
700 pub fn connect_by_id(
702 mut self,
703 from: (impl Into<String>, impl Into<String>),
704 to: (impl Into<String>, impl Into<String>),
705 ) -> Self {
706 self = self.connect_ports((from.0.into(), from.1.into()), (to.0.into(), to.1.into()));
707 self
708 }
709
710 pub fn inputs(self, _ports: &[PortHandle]) -> Self {
712 self
713 }
714
715 pub fn outputs(self, _ports: &[PortHandle]) -> Self {
716 self
717 }
718
719 pub fn connect_ports<F, T>(mut self, from: F, to: T) -> Self
722 where
723 F: IntoPortSpec,
724 T: IntoPortSpec,
725 {
726 let from_spec = from.into_spec();
727 let to_spec = to.into_spec();
728 let host_alias = self
729 .host_bridge_alias
730 .clone()
731 .unwrap_or_else(|| "host".to_string());
732 if from_spec.node == host_alias || to_spec.node == host_alias {
733 self = self.ensure_host_bridge(Some(host_alias.clone()));
734 }
735 if from_spec.node == host_alias {
736 self = self.ensure_host_bridge_port(true, &from_spec.port);
737 }
738 if to_spec.node == host_alias {
739 self = self.ensure_host_bridge_port(false, &to_spec.port);
740 }
741 let from_nested = self.nested.get(&from_spec.node).cloned();
742 let to_nested = self.nested.get(&to_spec.node).cloned();
743
744 if from_nested.is_some() && to_nested.is_some() {
745 panic!(
746 "cannot connect nested graph '{}' directly to nested graph '{}'",
747 from_spec.node, to_spec.node
748 );
749 }
750 if let Some(nested) = from_nested {
751 return self.connect_from_nested_spec(&nested, &from_spec.port, to_spec);
752 }
753 if let Some(nested) = to_nested {
754 return self.connect_to_nested_spec(from_spec, &nested, &to_spec.port);
755 }
756
757 let f_idx = self.find_index(&from_spec.node);
758 let t_idx = self.find_index(&to_spec.node);
759 self.edges.push(Edge {
760 from: PortRef {
761 node: NodeRef(f_idx),
762 port: from_spec.port,
763 },
764 to: PortRef {
765 node: NodeRef(t_idx),
766 port: to_spec.port,
767 },
768 metadata: BTreeMap::new(),
769 });
770 self
771 }
772
773 pub fn connect_to_nested<F>(
775 mut self,
776 from: F,
777 nested: &NestedGraphHandle,
778 port: impl AsRef<str>,
779 ) -> Self
780 where
781 F: IntoPortSpec,
782 {
783 let from_spec = from.into_spec();
784 let host_alias = self
785 .host_bridge_alias
786 .clone()
787 .unwrap_or_else(|| "host".to_string());
788 if from_spec.node == host_alias {
789 self = self.ensure_host_bridge(Some(host_alias));
790 self = self.ensure_host_bridge_port(true, &from_spec.port);
791 }
792 let lookup = |name: &str, nodes: &[NodeInstance]| {
793 nodes
794 .iter()
795 .position(|n| n.id.0 == name || n.label.as_deref() == Some(name))
796 .unwrap_or_else(|| panic!("node alias '{}' not found", name))
797 };
798 let f_idx = lookup(&from_spec.node, &self.nodes);
799 let port = port.as_ref();
800 let targets = nested
801 .inputs
802 .get(port)
803 .unwrap_or_else(|| panic!("nested input '{}' not found", port));
804
805 for target in targets {
806 self.edges.push(Edge {
807 from: PortRef {
808 node: NodeRef(f_idx),
809 port: from_spec.port.clone(),
810 },
811 to: target.clone(),
812 metadata: BTreeMap::new(),
813 });
814 }
815 self
816 }
817
818 pub fn connect_from_nested<T>(
820 mut self,
821 nested: &NestedGraphHandle,
822 port: impl AsRef<str>,
823 to: T,
824 ) -> Self
825 where
826 T: IntoPortSpec,
827 {
828 let to_spec = to.into_spec();
829 let host_alias = self
830 .host_bridge_alias
831 .clone()
832 .unwrap_or_else(|| "host".to_string());
833 if to_spec.node == host_alias {
834 self = self.ensure_host_bridge(Some(host_alias));
835 self = self.ensure_host_bridge_port(false, &to_spec.port);
836 }
837 let lookup = |name: &str, nodes: &[NodeInstance]| {
838 nodes
839 .iter()
840 .position(|n| n.id.0 == name || n.label.as_deref() == Some(name))
841 .unwrap_or_else(|| panic!("node alias '{}' not found", name))
842 };
843 let t_idx = lookup(&to_spec.node, &self.nodes);
844 let port = port.as_ref();
845 let sources = nested
846 .outputs
847 .get(port)
848 .unwrap_or_else(|| panic!("nested output '{}' not found", port));
849
850 for source in sources {
851 self.edges.push(Edge {
852 from: source.clone(),
853 to: PortRef {
854 node: NodeRef(t_idx),
855 port: to_spec.port.clone(),
856 },
857 metadata: BTreeMap::new(),
858 });
859 }
860 self
861 }
862
863 fn connect_from_nested_spec(
864 mut self,
865 nested: &NestedGraphHandle,
866 port: &str,
867 to: PortSpec,
868 ) -> Self {
869 let t_idx = self.find_index(&to.node);
870 let sources = nested
871 .outputs
872 .get(port)
873 .unwrap_or_else(|| panic!("nested output '{}' not found", port));
874
875 for source in sources {
876 self.edges.push(Edge {
877 from: source.clone(),
878 to: PortRef {
879 node: NodeRef(t_idx),
880 port: to.port.clone(),
881 },
882 metadata: BTreeMap::new(),
883 });
884 }
885 self
886 }
887
888 fn connect_to_nested_spec(
889 mut self,
890 from: PortSpec,
891 nested: &NestedGraphHandle,
892 port: &str,
893 ) -> Self {
894 let f_idx = self.find_index(&from.node);
895 let targets = nested
896 .inputs
897 .get(port)
898 .unwrap_or_else(|| panic!("nested input '{}' not found", port));
899
900 for target in targets {
901 self.edges.push(Edge {
902 from: PortRef {
903 node: NodeRef(f_idx),
904 port: from.port.clone(),
905 },
906 to: target.clone(),
907 metadata: BTreeMap::new(),
908 });
909 }
910 self
911 }
912
913 fn find_index(&self, name: &str) -> usize {
914 self.nodes
915 .iter()
916 .position(|n| n.id.0 == name || n.label.as_deref() == Some(name))
917 .unwrap_or_else(|| panic!("node alias '{}' not found", name))
918 }
919
920 pub fn build(self) -> Graph {
921 let mut nodes = self.nodes;
922 if !self.injected_node_metadata.is_empty()
923 || !self.injected_node_metadata_overwrite.is_empty()
924 {
925 for node in &mut nodes {
926 for (k, v) in &self.injected_node_metadata {
927 node.metadata.entry(k.clone()).or_insert_with(|| v.clone());
928 }
929 for (k, v) in &self.injected_node_metadata_overwrite {
930 node.metadata.insert(k.clone(), v.clone());
931 }
932 }
933 }
934 Graph {
935 nodes,
936 edges: self.edges,
937 metadata: self.graph_metadata,
938 metadata_values: self.graph_metadata_values,
939 }
940 }
941}
942
943pub struct GraphCtx<'r> {
945 builder: GraphBuilder<'r>,
946 host_alias: String,
947 expected_inputs: Vec<String>,
948 expected_outputs: Vec<String>,
949}
950
951impl<'r> GraphCtx<'r> {
952 fn take_builder(&mut self) -> GraphBuilder<'r> {
953 let reg = self.builder.reg;
954 std::mem::replace(&mut self.builder, GraphBuilder::new(reg))
955 }
956
957 pub fn new(registry: &'r Registry, inputs: &[&str], outputs: &[&str]) -> Self {
958 Self {
959 builder: GraphBuilder::new(registry),
960 host_alias: "host".to_string(),
961 expected_inputs: inputs.iter().map(|v| v.to_string()).collect(),
962 expected_outputs: outputs.iter().map(|v| v.to_string()).collect(),
963 }
964 }
965
966 pub fn node(&mut self, id: &str) -> crate::handles::NodeHandle {
967 self.node_as(id, id)
968 }
969
970 pub fn node_as(&mut self, id: &str, alias: &str) -> crate::handles::NodeHandle {
971 let builder = self.take_builder();
972 self.builder = builder.node_id(id, alias);
973 crate::handles::NodeHandle {
974 id: id.to_string(),
975 alias: alias.to_string(),
976 }
977 }
978
979 pub fn connect(&mut self, from: &PortHandle, to: &PortHandle) {
980 let builder = self.take_builder();
981 self.builder = builder.connect_handles(from, to);
982 }
983
984 pub fn const_input(&mut self, port: &PortHandle, value: Value) {
985 let builder = self.take_builder();
986 self.builder = builder.const_input(port, Some(value));
987 }
988
989 pub fn input(&self, name: &str) -> PortHandle {
990 PortHandle::new(self.host_alias.clone(), name)
991 }
992
993 pub fn output(&self, name: &str) -> PortHandle {
994 PortHandle::new(self.host_alias.clone(), name)
995 }
996
997 pub fn bind_output(&mut self, name: &str, from: &PortHandle) {
998 let host = self.output(name);
999 let builder = self.take_builder();
1000 self.builder = builder.connect_handles(from, &host);
1001 }
1002
1003 pub fn build(mut self) -> Graph {
1004 self.builder = self.builder.host_bridge(self.host_alias.clone());
1005 for name in &self.expected_inputs {
1006 self.builder = self.builder.ensure_host_bridge_port(false, name);
1007 }
1008 for name in &self.expected_outputs {
1009 self.builder = self.builder.ensure_host_bridge_port(true, name);
1010 }
1011 self.builder.build()
1012 }
1013}
1014
1015pub fn graph_to_json(graph: &Graph) -> Result<String, serde_json::Error> {
1016 serde_json::to_string(graph)
1017}
1018
1019#[cfg(test)]
1020mod tests {
1021 use super::*;
1022 use daedalus_data::model::{TypeExpr, Value, ValueType};
1023 use daedalus_registry::store::{NodeDescriptorBuilder, Registry};
1024
1025 #[test]
1026 fn applies_metadata_overrides() {
1027 let mut reg = Registry::new();
1028 let desc = NodeDescriptorBuilder::new("demo.node")
1029 .metadata("from_desc", Value::Bool(true))
1030 .build()
1031 .unwrap();
1032 reg.register_node(desc).unwrap();
1033
1034 let graph = GraphBuilder::new(®)
1035 .node_from_id("demo.node", "alias")
1036 .node_metadata_by_id("alias", "pos_x", Value::Int(10))
1037 .build();
1038
1039 let meta = &graph.nodes[0].metadata;
1040 assert_eq!(meta.get("from_desc"), Some(&Value::Bool(true)));
1041 assert_eq!(meta.get("pos_x"), Some(&Value::Int(10)));
1042 }
1043
1044 #[test]
1045 fn can_inject_graph_metadata_and_broadcast_to_nodes() {
1046 let mut reg = Registry::new();
1047 let desc = NodeDescriptorBuilder::new("demo.node")
1048 .metadata("existing", Value::String("keep".into()))
1049 .build()
1050 .unwrap();
1051 reg.register_node(desc).unwrap();
1052
1053 let graph = GraphBuilder::new(®)
1054 .graph_metadata("graph_run_id", "run-123")
1055 .graph_metadata_value("multiplier", Value::Int(3))
1056 .inject_node_metadata("trace_id", Value::String("trace-abc".into()))
1057 .inject_node_metadata_overwrite("existing", Value::String("overwrite".into()))
1058 .node_from_id("demo.node", "alias")
1059 .build();
1060
1061 assert_eq!(graph.metadata.get("graph_run_id"), Some(&"run-123".into()));
1062 assert_eq!(
1063 graph.metadata_values.get("graph_run_id"),
1064 Some(&Value::String("run-123".into()))
1065 );
1066 assert_eq!(
1067 graph.metadata_values.get("multiplier"),
1068 Some(&Value::Int(3))
1069 );
1070 let meta = &graph.nodes[0].metadata;
1071 assert_eq!(
1072 meta.get("trace_id"),
1073 Some(&Value::String("trace-abc".into()))
1074 );
1075 assert_eq!(
1076 meta.get("existing"),
1077 Some(&Value::String("overwrite".into()))
1078 );
1079 }
1080
1081 #[test]
1082 fn nests_graph_and_exposes_ports() {
1083 let reg = Registry::new();
1084
1085 let inner = GraphBuilder::new(®)
1086 .host_bridge("inner")
1087 .node_from_id("demo.add", "add")
1088 .connect_by_id(("inner", "lhs"), ("add", "lhs"))
1089 .connect_by_id(("inner", "rhs"), ("add", "rhs"))
1090 .connect_by_id(("add", "sum"), ("inner", "sum"))
1091 .build();
1092 let nested = NestedGraph::new(inner, "inner").expect("inner host bridge missing");
1093
1094 let (builder, nested_handle) = GraphBuilder::new(®)
1095 .node_from_id("demo.src", "src")
1096 .nest(&nested, "adder");
1097
1098 let graph = builder
1099 .node_from_id("demo.sink", "sink")
1100 .connect(("src", "out_lhs"), &nested_handle.input("lhs"))
1101 .connect(("src", "out_rhs"), &nested_handle.input("rhs"))
1102 .connect(&nested_handle.output("sum"), ("sink", "in"))
1103 .build();
1104
1105 assert!(nested_handle.inputs.contains_key("lhs"));
1106 assert!(nested_handle.inputs.contains_key("rhs"));
1107 assert!(nested_handle.outputs.contains_key("sum"));
1108
1109 let find = |name: &str| {
1110 graph
1111 .nodes
1112 .iter()
1113 .position(|n| n.label.as_deref() == Some(name))
1114 .unwrap()
1115 };
1116 let src_idx = find("src");
1117 let sink_idx = find("sink");
1118 let add_idx = find("adder::add");
1119
1120 let has_inbound = graph
1121 .edges
1122 .iter()
1123 .any(|e| e.from.node.0 == src_idx && e.to.node.0 == add_idx && e.to.port == "lhs");
1124 let has_outbound = graph
1125 .edges
1126 .iter()
1127 .any(|e| e.from.node.0 == add_idx && e.to.node.0 == sink_idx && e.from.port == "sum");
1128
1129 assert!(has_inbound, "nested inputs should target inner nodes");
1130 assert!(has_outbound, "nested outputs should feed outer nodes");
1131 }
1132
1133 #[test]
1134 fn applies_edge_metadata() {
1135 let mut reg = Registry::new();
1136 reg.register_node(
1137 NodeDescriptorBuilder::new("demo.src")
1138 .output("out", TypeExpr::Scalar(ValueType::Bool))
1139 .build()
1140 .unwrap(),
1141 )
1142 .unwrap();
1143 reg.register_node(
1144 NodeDescriptorBuilder::new("demo.sink")
1145 .input("in", TypeExpr::Scalar(ValueType::Bool))
1146 .build()
1147 .unwrap(),
1148 )
1149 .unwrap();
1150
1151 let graph = GraphBuilder::new(®)
1152 .node_from_id("demo.src", "a")
1153 .node_from_id("demo.sink", "b")
1154 .connect_with_metadata(
1155 ("a", "out"),
1156 ("b", "in"),
1157 [("ui.color", Value::String("red".into()))],
1158 )
1159 .build();
1160 assert_eq!(graph.edges.len(), 1);
1161 assert!(matches!(
1162 graph.edges[0].metadata.get("ui.color"),
1163 Some(Value::String(s)) if s.as_ref() == "red"
1164 ));
1165 }
1166}