Skip to main content

devmesh/
graph.rs

1// Phase 7: Service mesh graph (ASCII + JSON)
2
3use crate::discovery;
4use crate::registry::{PeerInfo, Registry};
5use serde::Serialize;
6use std::collections::HashMap;
7
8#[derive(Debug, Serialize)]
9pub struct GraphData {
10    pub namespace: String,
11    pub local: Vec<Node>,
12    pub peers: Vec<PeerNode>,
13    pub version_mismatches: Vec<String>,
14}
15
16#[derive(Debug, Serialize)]
17pub struct Node {
18    pub name: String,
19    pub version: Option<String>,
20    pub port: u16,
21    pub location: String,
22}
23
24#[derive(Debug, Serialize)]
25pub struct PeerNode {
26    pub id: String,
27    pub addr: String,
28    pub proxy_port: u16,
29    pub services: Vec<Node>,
30}
31
32/// Collect graph data for current namespace (or override)
33pub fn collect_graph_data() -> GraphData {
34    collect_graph_data_for(None)
35}
36
37/// Collect graph data for a specific namespace. If None, uses current namespace.
38pub fn collect_graph_data_for(namespace: Option<&str>) -> GraphData {
39    let ns = namespace.map(String::from).unwrap_or_else(crate::namespace::current);
40    let registry = Registry::load();
41    let peers = discovery::load_peers(&ns);
42
43    let local: Vec<Node> = registry
44        .local_for(&ns)
45        .map(|(name, e)| Node {
46            name: name.to_string(),
47            version: e.version.clone(),
48            port: e.port,
49            location: "local".to_string(),
50        })
51        .collect();
52
53    let peer_nodes: Vec<PeerNode> = peers
54        .iter()
55        .map(|(id, p)| PeerNode {
56            id: id.clone(),
57            addr: p.addr.clone(),
58            proxy_port: p.proxy_port,
59            services: p
60                .services
61                .iter()
62                .map(|(name, e)| Node {
63                    name: name.clone(),
64                    version: e.version.clone(),
65                    port: e.port,
66                    location: format!("peer:{}", p.addr),
67                })
68                .collect(),
69        })
70        .collect();
71
72    let version_mismatches = version_mismatches(&registry, &ns, &peers);
73
74    GraphData {
75        namespace: ns.to_string(),
76        local,
77        peers: peer_nodes,
78        version_mismatches,
79    }
80}
81
82fn version_mismatches(
83    registry: &Registry,
84    namespace: &str,
85    peers: &HashMap<String, PeerInfo>,
86) -> Vec<String> {
87    let mut out = Vec::new();
88    for (name, local_entry) in registry.local_for(namespace) {
89        let local_ver = match &local_entry.version {
90            Some(v) => v,
91            None => continue,
92        };
93        for (_, peer) in peers {
94            if let Some(peer_entry) = peer.services.get(name) {
95                if let Some(peer_ver) = &peer_entry.version {
96                    if local_ver != peer_ver {
97                        if let (Ok(a), Ok(b)) =
98                            (semver::Version::parse(local_ver), semver::Version::parse(peer_ver))
99                        {
100                            if a.major != b.major {
101                                out.push(format!(
102                                    "{}: local v{} vs peer v{} (major mismatch)",
103                                    name, local_ver, peer_ver
104                                ));
105                            } else {
106                                out.push(format!(
107                                    "{}: local v{} vs peer v{}",
108                                    name, local_ver, peer_ver
109                                ));
110                            }
111                        }
112                    }
113                }
114            }
115        }
116    }
117    out
118}
119
120pub fn render_ascii(data: &GraphData) -> String {
121    let mut out = String::new();
122
123    out.push_str(&format!("Namespace: {}\n\n", data.namespace));
124    out.push_str("┌─ Local ──────────────────────┐\n");
125    if data.local.is_empty() {
126        out.push_str("│  (none)                       │\n");
127    } else {
128        for n in &data.local {
129            let ver = n.version.as_deref().unwrap_or("?");
130            out.push_str(&format!("│  {} v{} :{}\n", n.name, ver, n.port));
131        }
132    }
133    out.push_str("└───────────────────────────────┘\n\n");
134
135    for peer in &data.peers {
136        out.push_str(&format!("┌─ Peer {} ─────────┐\n", peer.id));
137        out.push_str(&format!("│  {}:{}\n", peer.addr, peer.proxy_port));
138        if peer.services.is_empty() {
139            out.push_str("│  (no services)\n");
140        } else {
141            for n in &peer.services {
142                let ver = n.version.as_deref().unwrap_or("?");
143                out.push_str(&format!("│  {} v{}\n", n.name, ver));
144            }
145        }
146        out.push_str("└───────────────────┘\n\n");
147    }
148
149    if !data.peers.is_empty() && data.peers.iter().all(|p| p.services.is_empty()) {
150        out.push_str("(Peers discovered but no services registered)\n");
151    }
152
153    if !data.version_mismatches.is_empty() {
154        out.push_str("\n⚠ Version mismatches:\n");
155        for m in &data.version_mismatches {
156            out.push_str(&format!("  {}\n", m));
157        }
158    }
159
160    out
161}