Skip to main content

a3s_flow/
validation.rs

1//! Pre-flight flow validation.
2//!
3//! [`ValidationIssue`] is the unit of output from [`FlowEngine::validate`].
4//! Each issue describes one structural problem found in a flow definition.
5//!
6//! [`FlowEngine::validate`]: crate::engine::FlowEngine::validate
7
8use serde::{Deserialize, Serialize};
9
10/// A structural problem found in a flow definition by [`FlowEngine::validate`].
11///
12/// [`FlowEngine::validate`]: crate::engine::FlowEngine::validate
13#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
14pub struct ValidationIssue {
15    /// The node ID where the issue was found, or `None` for flow-level issues.
16    pub node_id: Option<String>,
17    /// Human-readable description of the problem.
18    pub message: String,
19}
20
21impl std::fmt::Display for ValidationIssue {
22    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
23        match &self.node_id {
24            Some(id) => write!(f, "node '{}': {}", id, self.message),
25            None => write!(f, "{}", self.message),
26        }
27    }
28}
29
30#[cfg(test)]
31mod tests {
32    use super::*;
33
34    #[test]
35    fn display_with_node_id() {
36        let issue = ValidationIssue {
37            node_id: Some("fetch".into()),
38            message: "connection refused".into(),
39        };
40        assert_eq!(format!("{}", issue), "node 'fetch': connection refused");
41    }
42
43    #[test]
44    fn display_without_node_id() {
45        let issue = ValidationIssue {
46            node_id: None,
47            message: "missing start node".into(),
48        };
49        assert_eq!(format!("{}", issue), "missing start node");
50    }
51
52    #[test]
53    fn debug_round_trip() {
54        let issue = ValidationIssue {
55            node_id: Some("a".into()),
56            message: "b".into(),
57        };
58        let debug = format!("{:?}", issue);
59        assert!(debug.contains("ValidationIssue"));
60        assert!(debug.contains("a"));
61        assert!(debug.contains("b"));
62    }
63
64    #[test]
65    fn serde_round_trip() {
66        let issue = ValidationIssue {
67            node_id: Some("n".into()),
68            message: "m".into(),
69        };
70        let json = serde_json::to_string(&issue).unwrap();
71        let back: ValidationIssue = serde_json::from_str(&json).unwrap();
72        assert_eq!(back.node_id, issue.node_id);
73        assert_eq!(back.message, issue.message);
74    }
75
76    #[test]
77    fn clone_is_equal() {
78        let issue = ValidationIssue {
79            node_id: Some("x".into()),
80            message: "y".into(),
81        };
82        let cloned = issue.clone();
83        assert_eq!(cloned, issue);
84    }
85}