1use std::collections::HashMap;
16
17use async_trait::async_trait;
18use serde::{Deserialize, Serialize};
19
20use super::{
21 BspContext,
22 condition::Condition,
23 error::BspError,
24 graph::{BspGraph, Edge, InputPin, NodeId},
25 node::{Activation, BspNode, NodeOutcome},
26 recovery::{Recovery, RecoveryPolicy, RetryPolicy},
27 registry::NodeRegistry,
28 run::RunContext,
29 value::BspValue,
30};
31
32pub(crate) fn value_to_json(value: &BspValue) -> Option<serde_json::Value> {
39 if let Some(v) = value.downcast_ref::<serde_json::Value>() {
40 Some(v.clone())
41 } else if let Some(b) = value.downcast_ref::<bool>() {
42 Some(serde_json::Value::from(*b))
43 } else if let Some(i) = value.downcast_ref::<i64>() {
44 Some(serde_json::Value::from(*i))
45 } else if let Some(u) = value.downcast_ref::<u64>() {
46 Some(serde_json::Value::from(*u))
47 } else if let Some(f) = value.downcast_ref::<f64>() {
48 serde_json::Number::from_f64(*f).map(serde_json::Value::Number)
50 } else {
51 value
52 .downcast_ref::<String>()
53 .map(|s| serde_json::Value::from(s.clone()))
54 }
55}
56
57pub const GRAPH_DEF_VERSION: u32 = 1;
59
60#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
65pub struct StaticInputDef {
66 pub node: String,
68 pub port: usize,
70 pub value: serde_json::Value,
72}
73
74#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
76pub struct GraphDef {
77 pub version: u32,
79 pub nodes: Vec<NodeDef>,
81 pub edges: Vec<EdgeDef>,
83 #[serde(default, skip_serializing_if = "Vec::is_empty")]
85 pub entry: Vec<String>,
86 #[serde(default, skip_serializing_if = "Vec::is_empty")]
88 pub static_inputs: Vec<StaticInputDef>,
89}
90
91impl GraphDef {
92 pub fn from_json(json: &str) -> Result<Self, BspError> {
94 serde_json::from_str(json).map_err(|e| BspError::DefError(e.to_string()))
95 }
96
97 pub fn to_json(&self) -> Result<String, BspError> {
99 serde_json::to_string_pretty(self).map_err(|e| BspError::DefError(e.to_string()))
100 }
101}
102
103#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
105pub struct NodeDef {
106 pub id: String,
108 pub type_name: String,
110 #[serde(default, skip_serializing_if = "serde_json::Value::is_null")]
113 pub config: serde_json::Value,
114 #[serde(default, skip_serializing_if = "Option::is_none")]
116 pub activation: Option<ActivationDef>,
117 #[serde(default, skip_serializing_if = "Option::is_none")]
121 pub recovery: Option<RecoveryDef>,
122 #[serde(default, skip_serializing_if = "Option::is_none")]
124 pub merger: Option<String>,
125 #[serde(default, skip_serializing_if = "Option::is_none")]
127 pub max_executions: Option<u64>,
128 #[serde(default, skip_serializing_if = "Option::is_none")]
131 pub position: Option<(f64, f64)>,
132}
133
134#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
136#[serde(rename_all = "snake_case")]
137pub enum ActivationDef {
138 Any,
140 All,
142}
143
144#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
147#[serde(rename_all = "snake_case")]
148pub enum RecoveryDef {
149 FailFast,
151 Retry {
153 #[serde(default = "default_max_retries")]
155 max_retries: u32,
156 #[serde(default = "default_base_delay_ms")]
158 base_delay_ms: u64,
159 #[serde(default = "default_max_delay_ms")]
161 max_delay_ms: u64,
162 #[serde(default = "default_jitter")]
164 jitter: f64,
165 },
166 ErrorEdge,
168}
169
170const fn default_max_retries() -> u32 {
171 3
172}
173const fn default_base_delay_ms() -> u64 {
174 500
175}
176const fn default_max_delay_ms() -> u64 {
177 8_000
178}
179const fn default_jitter() -> f64 {
180 0.25
181}
182
183#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
189#[serde(rename_all = "snake_case")]
190pub enum ConditionDef {
191 #[default]
193 Always,
194 Never,
196 OnSkip,
198 OnError,
200 Named(String),
202 And(Vec<ConditionDef>),
204 Or(Vec<ConditionDef>),
206 Not(Box<ConditionDef>),
208 Custom {
210 label: String,
212 },
213}
214
215#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
217pub struct EdgeDef {
218 pub from: String,
220 pub to: String,
222 #[serde(default)]
224 pub condition: ConditionDef,
225 #[serde(default, skip_serializing_if = "Option::is_none")]
227 pub max_iterations: Option<u64>,
228 #[serde(default, skip_serializing_if = "Option::is_none")]
230 pub source_port: Option<usize>,
231 #[serde(default, skip_serializing_if = "Option::is_none")]
233 pub target_port: Option<usize>,
234}
235
236impl ConditionDef {
237 fn to_condition<S>(
239 &self,
240 registry: &NodeRegistry<S>,
241 unknown: &mut Vec<String>,
242 ) -> Condition<S> {
243 match self {
244 Self::Always => Condition::Always,
245 Self::Never => Condition::Never,
246 Self::OnSkip => Condition::OnSkip,
247 Self::OnError => Condition::OnError,
248 Self::Named(name) => {
249 if !registry.conditions.contains_key(name) {
250 unknown.push(format!("condition '{name}'"));
251 }
252 Condition::Named(name.clone().into())
253 }
254 Self::And(conds) => Condition::And(
255 conds
256 .iter()
257 .map(|c| c.to_condition(registry, unknown))
258 .collect(),
259 ),
260 Self::Or(conds) => Condition::Or(
261 conds
262 .iter()
263 .map(|c| c.to_condition(registry, unknown))
264 .collect(),
265 ),
266 Self::Not(cond) => Condition::Not(Box::new(cond.to_condition(registry, unknown))),
267 Self::Custom { label } => match registry.conditions.get(label) {
268 Some(f) => Condition::Custom {
270 label: label.clone(),
271 f: f.clone(),
272 },
273 None => {
274 unknown.push(format!(
275 "custom condition '{label}' (re-register it to instantiate)"
276 ));
277 Condition::Never
278 }
279 },
280 }
281 }
282
283 fn from_condition<S>(condition: &Condition<S>) -> Self {
285 match condition {
286 Condition::Always => Self::Always,
287 Condition::Never => Self::Never,
288 Condition::OnSkip => Self::OnSkip,
289 Condition::OnError => Self::OnError,
290 Condition::Named(id) => Self::Named(id.0.clone()),
291 Condition::And(conds) => Self::And(conds.iter().map(Self::from_condition).collect()),
292 Condition::Or(conds) => Self::Or(conds.iter().map(Self::from_condition).collect()),
293 Condition::Not(cond) => Self::Not(Box::new(Self::from_condition(cond))),
294 Condition::Custom { label, .. } => Self::Custom {
295 label: label.clone(),
296 },
297 }
298 }
299}
300
301struct DefNode<S> {
304 inner: Box<dyn BspNode<S>>,
305 type_name: String,
306 activation: Option<Activation>,
307 recovery: Option<RecoveryPolicy>,
308 merger: Option<String>,
309 max_executions: Option<u64>,
310}
311
312#[async_trait]
313impl<S: Send + Sync> BspNode<S> for DefNode<S> {
314 fn name(&self) -> &str {
315 self.inner.name()
316 }
317
318 fn type_name(&self) -> &str {
319 &self.type_name
320 }
321
322 fn input_ports(&self) -> Vec<super::graph::PortIndex> {
323 self.inner.input_ports()
324 }
325
326 fn output_ports(&self) -> Vec<super::graph::PortIndex> {
327 self.inner.output_ports()
328 }
329
330 fn is_dynamic_output(&self) -> bool {
331 self.inner.is_dynamic_output()
332 }
333
334 fn activation(&self) -> Activation {
335 self.activation.unwrap_or_else(|| self.inner.activation())
336 }
337
338 fn recovery(&self) -> RecoveryPolicy {
339 match &self.recovery {
340 Some(policy) => policy.clone(),
341 None => self.inner.recovery(),
342 }
343 }
344
345 fn merger(&self) -> Option<&str> {
346 self.merger.as_deref().or_else(|| self.inner.merger())
347 }
348
349 fn max_executions(&self) -> Option<u64> {
350 self.max_executions.or_else(|| self.inner.max_executions())
351 }
352
353 async fn execute(&self, run: &mut RunContext<'_, S>) -> Result<NodeOutcome, BspError> {
354 self.inner.execute(run).await
355 }
356}
357
358impl From<ActivationDef> for Activation {
359 fn from(def: ActivationDef) -> Self {
360 match def {
361 ActivationDef::Any => Activation::Any,
362 ActivationDef::All => Activation::All,
363 }
364 }
365}
366
367impl From<Activation> for ActivationDef {
368 fn from(activation: Activation) -> Self {
369 match activation {
370 Activation::Any => ActivationDef::Any,
371 Activation::All => ActivationDef::All,
372 }
373 }
374}
375
376impl RecoveryDef {
377 fn to_retry_policy(self) -> Option<RetryPolicy> {
379 match self {
380 Self::Retry {
381 max_retries,
382 base_delay_ms,
383 max_delay_ms,
384 jitter,
385 } => Some(RetryPolicy {
386 max_retries,
387 base_delay: std::time::Duration::from_millis(base_delay_ms),
388 max_delay: std::time::Duration::from_millis(max_delay_ms),
389 jitter,
390 }),
391 _ => None,
392 }
393 }
394}
395
396impl<S> BspGraph<S> {
397 pub fn from_def(
405 def: &GraphDef,
406 registry: &NodeRegistry<S>,
407 ctx: &BspContext,
408 ) -> Result<Self, BspError>
409 where
410 S: Send + Sync + 'static,
411 {
412 if def.version != GRAPH_DEF_VERSION {
413 return Err(BspError::DefError(format!(
414 "unsupported graph definition version {} (this crate understands {GRAPH_DEF_VERSION})",
415 def.version
416 )));
417 }
418
419 let mut unknown: Vec<String> = Vec::new();
421 for node_def in &def.nodes {
422 if !registry.factories.contains_key(&node_def.type_name) {
423 unknown.push(format!("node type '{}'", node_def.type_name));
424 }
425 if let Some(merger) = &node_def.merger
426 && !registry.mergers.contains_key(merger)
427 {
428 unknown.push(format!("merger '{merger}'"));
429 }
430 }
431
432 let mut id_map: HashMap<&str, NodeId> = HashMap::new();
434 for (idx, node_def) in def.nodes.iter().enumerate() {
435 let id = idx + 1;
436 if id_map.insert(node_def.id.as_str(), id).is_some() {
437 return Err(BspError::ValidationError(format!(
438 "duplicate node id '{}' in graph definition",
439 node_def.id
440 )));
441 }
442 }
443
444 let mut edges: Vec<Edge<S>> = Vec::with_capacity(def.edges.len());
446 for edge_def in &def.edges {
447 let source = id_map.get(edge_def.from.as_str()).copied().ok_or_else(|| {
448 BspError::ValidationError(format!(
449 "edge references unknown source node '{}'",
450 edge_def.from
451 ))
452 })?;
453 let target = id_map.get(edge_def.to.as_str()).copied().ok_or_else(|| {
454 BspError::ValidationError(format!(
455 "edge references unknown target node '{}'",
456 edge_def.to
457 ))
458 })?;
459 edges.push(Edge {
460 source,
461 target,
462 condition: edge_def.condition.to_condition(registry, &mut unknown),
463 max_iterations: edge_def.max_iterations,
464 source_port: edge_def.source_port,
465 target_port: edge_def.target_port,
466 });
467 }
468
469 if !unknown.is_empty() {
470 return Err(BspError::ValidationError(format!(
471 "graph definition references unregistered names: {}",
472 unknown.join(", ")
473 )));
474 }
475
476 let mut graph = BspGraph::new();
477 graph.conditions = registry.conditions.clone();
478 graph.mergers = registry.mergers.clone();
479
480 for (idx, node_def) in def.nodes.iter().enumerate() {
481 let id = idx + 1;
482 let factory = registry
483 .factories
484 .get(&node_def.type_name)
485 .expect("completeness checked above");
486 let inner = factory(node_def, ctx)?;
487
488 let recovery = match node_def.recovery {
491 None => None,
492 Some(RecoveryDef::FailFast) => Some(RecoveryPolicy::FailFast),
493 Some(RecoveryDef::ErrorEdge) => Some(RecoveryPolicy::RouteToErrorEdge),
494 Some(def @ RecoveryDef::Retry { .. }) => {
495 let policy = def.to_retry_policy().expect("retry variant");
496 let classify = match inner.recovery() {
497 RecoveryPolicy::Retry { classify, .. } => classify,
498 _ => std::sync::Arc::new(|_: &BspError| Recovery::Retry { after: None }),
499 };
500 Some(RecoveryPolicy::Retry { policy, classify })
501 }
502 };
503
504 graph.add_node_with_id(
505 id,
506 Box::new(DefNode {
507 inner,
508 type_name: node_def.type_name.clone(),
509 activation: node_def.activation.map(Activation::from),
510 recovery,
511 merger: node_def.merger.clone(),
512 max_executions: node_def.max_executions,
513 }),
514 );
515 graph.labels.insert(id, node_def.id.clone());
516 graph.node_defs.insert(id, node_def.clone());
517 }
518
519 for edge in edges {
520 graph.push_edge(edge);
521 }
522
523 for entry in &def.entry {
524 let id = id_map.get(entry.as_str()).copied().ok_or_else(|| {
525 BspError::ValidationError(format!("entry references unknown node '{entry}'"))
526 })?;
527 graph.add_entry(id);
528 }
529
530 for static_input in &def.static_inputs {
533 let id = id_map
534 .get(static_input.node.as_str())
535 .copied()
536 .ok_or_else(|| {
537 BspError::ValidationError(format!(
538 "static input references unknown node '{}'",
539 static_input.node
540 ))
541 })?;
542 graph.static_inputs.insert(
543 InputPin {
544 node_id: id,
545 port: static_input.port,
546 },
547 (registry.value_factory)(&static_input.value),
548 );
549 }
550
551 Ok(graph)
552 }
553
554 #[must_use]
561 pub fn to_def(&self) -> GraphDef {
562 let nodes = self
563 .nodes
564 .iter()
565 .map(|(id, node)| {
566 if let Some(retained) = self.node_defs.get(id) {
567 return retained.clone();
568 }
569 NodeDef {
570 id: self
571 .labels
572 .get(id)
573 .cloned()
574 .unwrap_or_else(|| id.to_string()),
575 type_name: node.type_name().to_string(),
576 config: serde_json::Value::Null,
577 activation: Some(node.activation().into()),
578 recovery: Some(match node.recovery() {
579 RecoveryPolicy::FailFast => RecoveryDef::FailFast,
580 RecoveryPolicy::RouteToErrorEdge => RecoveryDef::ErrorEdge,
581 RecoveryPolicy::Retry { policy, .. } => RecoveryDef::Retry {
582 max_retries: policy.max_retries,
583 base_delay_ms: u64::try_from(policy.base_delay.as_millis())
584 .unwrap_or(u64::MAX),
585 max_delay_ms: u64::try_from(policy.max_delay.as_millis())
586 .unwrap_or(u64::MAX),
587 jitter: policy.jitter,
588 },
589 }),
590 merger: node.merger().map(str::to_string),
591 max_executions: node.max_executions(),
592 position: None,
593 }
594 })
595 .collect();
596
597 let edges = self
598 .edges
599 .iter()
600 .map(|edge| EdgeDef {
601 from: self
602 .labels
603 .get(&edge.source)
604 .cloned()
605 .unwrap_or_else(|| edge.source.to_string()),
606 to: self
607 .labels
608 .get(&edge.target)
609 .cloned()
610 .unwrap_or_else(|| edge.target.to_string()),
611 condition: ConditionDef::from_condition(&edge.condition),
612 max_iterations: edge.max_iterations,
613 source_port: edge.source_port,
614 target_port: edge.target_port,
615 })
616 .collect();
617
618 let entry = self
619 .entry
620 .iter()
621 .map(|id| {
622 self.labels
623 .get(id)
624 .cloned()
625 .unwrap_or_else(|| id.to_string())
626 })
627 .collect();
628
629 let static_inputs = self
632 .static_inputs
633 .iter()
634 .filter_map(|(pin, value)| {
635 value_to_json(value).map(|json| StaticInputDef {
636 node: self
637 .labels
638 .get(&pin.node_id)
639 .cloned()
640 .unwrap_or_else(|| pin.node_id.to_string()),
641 port: pin.port,
642 value: json,
643 })
644 })
645 .collect();
646
647 GraphDef {
648 version: GRAPH_DEF_VERSION,
649 nodes,
650 edges,
651 entry,
652 static_inputs,
653 }
654 }
655}
656
657#[cfg(test)]
658mod tests {
659 use super::*;
660
661 #[test]
662 fn test_condition_def_json_shapes() {
663 assert_eq!(
664 serde_json::to_string(&ConditionDef::Always).unwrap(),
665 "\"always\""
666 );
667 assert_eq!(
668 serde_json::to_string(&ConditionDef::Named("has_more".to_string())).unwrap(),
669 "{\"named\":\"has_more\"}"
670 );
671 assert_eq!(
672 serde_json::to_string(&ConditionDef::Not(Box::new(ConditionDef::OnError))).unwrap(),
673 "{\"not\":\"on_error\"}"
674 );
675 assert_eq!(
676 serde_json::to_string(&ConditionDef::Custom {
677 label: "hand_rolled".to_string()
678 })
679 .unwrap(),
680 "{\"custom\":{\"label\":\"hand_rolled\"}}"
681 );
682 }
683
684 #[test]
685 fn test_activation_and_recovery_def_json_shapes() {
686 assert_eq!(
687 serde_json::to_string(&ActivationDef::All).unwrap(),
688 "\"all\""
689 );
690 assert_eq!(
691 serde_json::to_string(&RecoveryDef::FailFast).unwrap(),
692 "\"fail_fast\""
693 );
694 let retry = RecoveryDef::Retry {
695 max_retries: 2,
696 base_delay_ms: 100,
697 max_delay_ms: 1_000,
698 jitter: 0.1,
699 };
700 let json = serde_json::to_string(&retry).unwrap();
701 assert!(json.starts_with("{\"retry\":{"));
702 let parsed: RecoveryDef =
704 serde_json::from_str("{\"retry\": {\"max_retries\": 1}}").unwrap();
705 assert_eq!(
706 parsed.to_retry_policy().unwrap().base_delay,
707 std::time::Duration::from_millis(500)
708 );
709 }
710
711 #[test]
712 fn test_graph_def_version_roundtrip_and_mismatch() {
713 let json = r#"{"version": 1, "nodes": [], "edges": []}"#;
714 let def = GraphDef::from_json(json).unwrap();
715 assert_eq!(def.version, GRAPH_DEF_VERSION);
716 assert!(def.entry.is_empty());
717
718 let registry: NodeRegistry<()> = NodeRegistry::new();
719 let ctx = BspContext::new();
720 let result = BspGraph::from_def(
721 &GraphDef {
722 version: 99,
723 nodes: Vec::new(),
724 edges: Vec::new(),
725 entry: Vec::new(),
726 static_inputs: Vec::new(),
727 },
728 ®istry,
729 &ctx,
730 );
731 assert!(matches!(result, Err(BspError::DefError(_))));
732 }
733}