use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct NodeInfo {
pub id: String,
pub service: String,
pub depth: usize,
pub predecessors: Vec<String>,
pub successors: Vec<String>,
pub in_degree: usize,
pub out_degree: usize,
pub config: serde_json::Value,
pub metadata: serde_json::Value,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EdgeInfo {
pub from: String,
pub to: String,
pub config: serde_json::Value,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ExecutionWave {
pub level: usize,
pub node_ids: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CriticalPath {
pub nodes: Vec<String>,
pub length: usize,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ConnectivityMetrics {
pub is_connected: bool,
pub component_count: usize,
pub root_nodes: Vec<String>,
pub leaf_nodes: Vec<String>,
pub max_depth: usize,
pub avg_degree: f64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GraphSnapshot {
pub node_count: usize,
pub edge_count: usize,
pub nodes: Vec<NodeInfo>,
pub edges: Vec<EdgeInfo>,
pub waves: Vec<ExecutionWave>,
pub topological_order: Vec<String>,
pub critical_path: CriticalPath,
pub connectivity: ConnectivityMetrics,
pub service_distribution: HashMap<String, usize>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct NodeQuery {
pub service: Option<String>,
pub id: Option<String>,
pub id_pattern: Option<String>,
pub is_root: Option<bool>,
pub is_leaf: Option<bool>,
pub min_depth: Option<usize>,
pub max_depth: Option<usize>,
}
impl NodeQuery {
#[must_use]
pub fn all() -> Self {
Self::default()
}
#[must_use]
pub fn by_service(service: impl Into<String>) -> Self {
Self {
service: Some(service.into()),
..Default::default()
}
}
#[must_use]
pub fn roots() -> Self {
Self {
is_root: Some(true),
..Default::default()
}
}
#[must_use]
pub fn leaves() -> Self {
Self {
is_leaf: Some(true),
..Default::default()
}
}
#[must_use]
pub fn matches(&self, node: &NodeInfo) -> bool {
if let Some(ref service) = self.service
&& &node.service != service
{
return false;
}
if let Some(ref id) = self.id
&& &node.id != id
{
return false;
}
if let Some(ref pattern) = self.id_pattern
&& !node.id.contains(pattern)
{
return false;
}
if let Some(is_root) = self.is_root
&& is_root != (node.in_degree == 0)
{
return false;
}
if let Some(is_leaf) = self.is_leaf
&& is_leaf != (node.out_degree == 0)
{
return false;
}
if let Some(min_depth) = self.min_depth
&& node.depth < min_depth
{
return false;
}
if let Some(max_depth) = self.max_depth
&& node.depth > max_depth
{
return false;
}
true
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DependencyChain {
pub from: String,
pub to: String,
pub path: Vec<String>,
pub distance: usize,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ImpactAnalysis {
pub node_id: String,
pub upstream: Vec<String>,
pub downstream: Vec<String>,
pub total_affected: usize,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_node_query_all() {
let query = NodeQuery::all();
let node = NodeInfo {
id: "test".to_string(),
service: "http".to_string(),
depth: 0,
predecessors: vec![],
successors: vec![],
in_degree: 0,
out_degree: 0,
config: serde_json::Value::Null,
metadata: serde_json::Value::Null,
};
assert!(query.matches(&node));
}
#[test]
fn test_node_query_by_service() {
let query = NodeQuery::by_service("http");
let http_node = NodeInfo {
id: "fetch".to_string(),
service: "http".to_string(),
depth: 0,
predecessors: vec![],
successors: vec![],
in_degree: 0,
out_degree: 0,
config: serde_json::Value::Null,
metadata: serde_json::Value::Null,
};
let ai_node = NodeInfo {
id: "extract".to_string(),
service: "ai".to_string(),
depth: 1,
predecessors: vec!["fetch".to_string()],
successors: vec![],
in_degree: 1,
out_degree: 0,
config: serde_json::Value::Null,
metadata: serde_json::Value::Null,
};
assert!(query.matches(&http_node));
assert!(!query.matches(&ai_node));
}
#[test]
fn test_node_query_roots() {
let query = NodeQuery::roots();
let root = NodeInfo {
id: "fetch".to_string(),
service: "http".to_string(),
depth: 0,
predecessors: vec![],
successors: vec!["extract".to_string()],
in_degree: 0,
out_degree: 1,
config: serde_json::Value::Null,
metadata: serde_json::Value::Null,
};
let non_root = NodeInfo {
id: "extract".to_string(),
service: "ai".to_string(),
depth: 1,
predecessors: vec!["fetch".to_string()],
successors: vec![],
in_degree: 1,
out_degree: 0,
config: serde_json::Value::Null,
metadata: serde_json::Value::Null,
};
assert!(query.matches(&root));
assert!(!query.matches(&non_root));
}
#[test]
fn test_node_query_depth_range() {
let query = NodeQuery {
min_depth: Some(1),
max_depth: Some(2),
..Default::default()
};
let depth_0 = NodeInfo {
id: "a".to_string(),
service: "http".to_string(),
depth: 0,
predecessors: vec![],
successors: vec![],
in_degree: 0,
out_degree: 0,
config: serde_json::Value::Null,
metadata: serde_json::Value::Null,
};
let depth_1 = NodeInfo {
id: "b".to_string(),
service: "http".to_string(),
depth: 1,
predecessors: vec![],
successors: vec![],
in_degree: 1,
out_degree: 0,
config: serde_json::Value::Null,
metadata: serde_json::Value::Null,
};
let depth_3 = NodeInfo {
id: "c".to_string(),
service: "http".to_string(),
depth: 3,
predecessors: vec![],
successors: vec![],
in_degree: 1,
out_degree: 0,
config: serde_json::Value::Null,
metadata: serde_json::Value::Null,
};
assert!(!query.matches(&depth_0));
assert!(query.matches(&depth_1));
assert!(!query.matches(&depth_3));
}
}