Skip to main content

mockforge_core/graph/
relationships.rs

1//! Relationship discovery for graph visualization
2//!
3//! This module analyzes various data sources to discover relationships
4//! between endpoints, services, and chains.
5
6use crate::graph::{EdgeType, GraphEdge};
7use crate::request_chaining::ChainDefinition;
8use std::collections::HashMap;
9
10/// Discover relationships from chain definitions
11pub fn discover_chain_relationships(chains: &[ChainDefinition]) -> Vec<GraphEdge> {
12    let mut edges = Vec::new();
13
14    for chain in chains {
15        for link in &chain.links {
16            let link_node_id = format!("chain_link:{}:{}", chain.id, link.request.id);
17
18            // Create dependency edges
19            for dep_id in &link.request.depends_on {
20                let dep_node_id = format!("chain_link:{}:{}", chain.id, dep_id);
21                edges.push(GraphEdge {
22                    from: dep_node_id,
23                    to: link_node_id.clone(),
24                    edge_type: EdgeType::Dependency,
25                    label: Some("depends on".to_string()),
26                    metadata: HashMap::new(),
27                });
28            }
29
30            // Try to discover cross-service calls from URLs
31            if let Some(service_call) = discover_service_call_from_url(&link.request.url) {
32                edges.push(service_call);
33            }
34        }
35    }
36
37    edges
38}
39
40/// Discover service call relationships from URL patterns.
41///
42/// Extracts service identity from the URL's host component and creates
43/// a graph edge representing a cross-service call. Relative URLs (no host)
44/// are treated as calls within the same service and return None.
45fn discover_service_call_from_url(url: &str) -> Option<GraphEdge> {
46    // Only process absolute URLs that reference another service
47    let host =
48        if let Some(rest) = url.strip_prefix("https://").or_else(|| url.strip_prefix("http://")) {
49            // Extract host (everything before the first '/' or end of string)
50            let host_part = rest.split('/').next().unwrap_or(rest);
51            // Strip port if present
52            host_part.split(':').next().unwrap_or(host_part)
53        } else {
54            // Relative URL — same service, no cross-service edge
55            return None;
56        };
57
58    if host.is_empty() || host == "localhost" || host == "127.0.0.1" || host == "0.0.0.0" {
59        return None;
60    }
61
62    // Extract the service name from the host (first subdomain or the domain itself)
63    let service_name = host.split('.').next().unwrap_or(host);
64
65    // Extract the path for the edge label
66    let path = url
67        .find("://")
68        .and_then(|i| url[i + 3..].find('/'))
69        .map(|i| &url[url.find("://").unwrap() + 3 + i..])
70        .unwrap_or("/");
71
72    Some(GraphEdge {
73        from: "caller".to_string(),
74        to: format!("service:{service_name}"),
75        edge_type: EdgeType::ServiceCall,
76        label: Some(format!("calls {path}")),
77        metadata: {
78            let mut meta = HashMap::new();
79            meta.insert("target_host".to_string(), serde_json::Value::String(host.to_string()));
80            meta.insert("url".to_string(), serde_json::Value::String(url.to_string()));
81            meta
82        },
83    })
84}
85
86/// Discover state transition relationships from state machines
87pub fn discover_state_transitions(
88    state_machines: &[crate::intelligent_behavior::rules::StateMachine],
89) -> Vec<GraphEdge> {
90    use serde_json;
91    let mut edges = Vec::new();
92
93    for state_machine in state_machines {
94        for transition in &state_machine.transitions {
95            // Create edges for state transitions
96            // Note: This requires mapping states to endpoint nodes
97            // For now, we'll create placeholder edges that need to be
98            // connected to actual nodes by the builder
99            let from_node_id =
100                format!("state:{}:{}", state_machine.resource_type, transition.from_state);
101            let to_node_id =
102                format!("state:{}:{}", state_machine.resource_type, transition.to_state);
103
104            edges.push(GraphEdge {
105                from: from_node_id,
106                to: to_node_id,
107                edge_type: EdgeType::StateTransition,
108                label: Some(format!("{} → {}", transition.from_state, transition.to_state)),
109                metadata: {
110                    let mut meta = HashMap::new();
111                    meta.insert(
112                        "resource_type".to_string(),
113                        serde_json::Value::String(state_machine.resource_type.clone()),
114                    );
115                    meta.insert(
116                        "probability".to_string(),
117                        serde_json::Value::Number(
118                            serde_json::Number::from_f64(transition.probability)
119                                .unwrap_or_else(|| serde_json::Number::from(0)),
120                        ),
121                    );
122                    meta
123                },
124            });
125        }
126    }
127
128    edges
129}
130
131/// Extract endpoint IDs from chain links
132pub fn extract_endpoint_ids_from_chain(chain: &ChainDefinition) -> Vec<String> {
133    chain.links.iter().map(|link| link.request.id.clone()).collect()
134}
135
136/// Group endpoints by service based on URL patterns
137pub fn group_endpoints_by_service(
138    endpoints: &[(String, String)], // (endpoint_id, url)
139) -> HashMap<String, Vec<String>> {
140    let mut service_groups: HashMap<String, Vec<String>> = HashMap::new();
141
142    for (endpoint_id, url) in endpoints {
143        // Simple heuristic: extract service name from URL
144        // In production, this would be more sophisticated
145        let service_name = extract_service_name_from_url(url);
146        service_groups.entry(service_name).or_default().push(endpoint_id.clone());
147    }
148
149    service_groups
150}
151
152/// Extract service name from URL
153fn extract_service_name_from_url(url: &str) -> String {
154    // Simple heuristic: use domain or first path segment
155    if let Some(domain) = url.split("://").nth(1) {
156        if let Some(host) = domain.split('/').next() {
157            return host.split('.').next().unwrap_or("default").to_string();
158        }
159    }
160
161    // Fallback: use first path segment
162    if let Some(first_segment) = url.split('/').nth(1) {
163        return first_segment.to_string();
164    }
165
166    "default".to_string()
167}