mockforge_core/graph/
builder.rs

1//! Graph builder for aggregating data from multiple sources
2//!
3//! This module builds a complete graph representation by aggregating data from:
4//! - UI Builder endpoints
5//! - Request chains
6//! - State machines
7//! - Workspaces/services
8
9use crate::graph::{
10    ClusterType, EdgeType, GraphCluster, GraphData, GraphEdge, GraphNode, NodeType, Protocol,
11};
12use crate::intelligent_behavior::rules::{StateMachine, StateTransition};
13use crate::request_chaining::ChainDefinition;
14use serde_json::Value;
15use std::collections::HashMap;
16
17/// Convert UI Builder Protocol to Graph Protocol
18pub fn convert_protocol(protocol: &str) -> Protocol {
19    match protocol.to_lowercase().as_str() {
20        "http" => Protocol::Http,
21        "grpc" => Protocol::Grpc,
22        "websocket" => Protocol::Websocket,
23        "graphql" => Protocol::Graphql,
24        "mqtt" => Protocol::Mqtt,
25        "smtp" => Protocol::Smtp,
26        "kafka" => Protocol::Kafka,
27        "amqp" => Protocol::Amqp,
28        "ftp" => Protocol::Ftp,
29        _ => Protocol::Http, // Default to HTTP
30    }
31}
32
33/// Builder for constructing graph data from various sources
34pub struct GraphBuilder {
35    /// Accumulated graph data
36    graph: GraphData,
37
38    /// Map of endpoint IDs to node IDs for quick lookup
39    endpoint_to_node: HashMap<String, String>,
40
41    /// Map of chain IDs to cluster IDs
42    chain_to_cluster: HashMap<String, String>,
43}
44
45impl GraphBuilder {
46    /// Create a new graph builder
47    pub fn new() -> Self {
48        Self {
49            graph: GraphData::new(),
50            endpoint_to_node: HashMap::new(),
51            chain_to_cluster: HashMap::new(),
52        }
53    }
54
55    /// Build the complete graph from all available data sources
56    pub fn build(mut self) -> GraphData {
57        self.graph
58    }
59
60    /// Add an endpoint node from UI Builder endpoint configuration
61    pub fn add_endpoint(
62        &mut self,
63        endpoint_id: String,
64        name: String,
65        protocol: Protocol,
66        metadata: HashMap<String, Value>,
67    ) {
68        let node_id = format!("endpoint:{}", endpoint_id);
69        self.endpoint_to_node.insert(endpoint_id.clone(), node_id.clone());
70
71        let node = GraphNode {
72            id: node_id,
73            label: name,
74            node_type: NodeType::Endpoint,
75            protocol: Some(protocol),
76            current_state: None,
77            metadata,
78        };
79
80        self.graph.add_node(node);
81    }
82
83    /// Add a chain and create edges for dependencies
84    pub fn add_chain(&mut self, chain: &ChainDefinition) {
85        // Create a cluster for this chain
86        let cluster_id = format!("chain:{}", chain.id);
87        let cluster = GraphCluster {
88            id: cluster_id.clone(),
89            label: chain.name.clone(),
90            cluster_type: ClusterType::Chain,
91            node_ids: Vec::new(),
92            metadata: {
93                let mut meta = HashMap::new();
94                meta.insert("chain_id".to_string(), Value::String(chain.id.clone()));
95                meta.insert(
96                    "description".to_string(),
97                    Value::String(chain.description.clone().unwrap_or_default()),
98                );
99                meta
100            },
101        };
102        self.graph.add_cluster(cluster);
103        self.chain_to_cluster.insert(chain.id.clone(), cluster_id.clone());
104
105        // Process each link in the chain
106        for link in &chain.links {
107            let link_node_id = format!("chain_link:{}:{}", chain.id, link.request.id);
108
109            // Create a node for the chain link if it references an endpoint
110            // Otherwise, it's just a step in the chain
111            let link_node = GraphNode {
112                id: link_node_id.clone(),
113                label: format!("{} ({})", link.request.id, link.request.method),
114                node_type: NodeType::Endpoint,
115                protocol: Some(Protocol::Http), // Chains are typically HTTP
116                current_state: None,
117                metadata: {
118                    let mut meta = HashMap::new();
119                    meta.insert("chain_id".to_string(), Value::String(chain.id.clone()));
120                    meta.insert("request_id".to_string(), Value::String(link.request.id.clone()));
121                    meta.insert("method".to_string(), Value::String(link.request.method.clone()));
122                    meta.insert("url".to_string(), Value::String(link.request.url.clone()));
123                    meta
124                },
125            };
126            self.graph.add_node(link_node);
127
128            // Add to cluster
129            if let Some(cluster) = self.graph.clusters.iter_mut().find(|c| c.id == cluster_id) {
130                cluster.node_ids.push(link_node_id.clone());
131            }
132
133            // Create dependency edges
134            for dep_id in &link.request.depends_on {
135                let dep_node_id = format!("chain_link:{}:{}", chain.id, dep_id);
136                let edge = GraphEdge {
137                    from: dep_node_id,
138                    to: link_node_id.clone(),
139                    edge_type: EdgeType::Dependency,
140                    label: Some("depends on".to_string()),
141                    metadata: HashMap::new(),
142                };
143                self.graph.add_edge(edge);
144            }
145        }
146    }
147
148    /// Add a state transition edge
149    pub fn add_state_transition(
150        &mut self,
151        from_node_id: String,
152        to_node_id: String,
153        transition_label: Option<String>,
154    ) {
155        let edge = GraphEdge {
156            from: from_node_id,
157            to: to_node_id,
158            edge_type: EdgeType::StateTransition,
159            label: transition_label,
160            metadata: HashMap::new(),
161        };
162        self.graph.add_edge(edge);
163    }
164
165    /// Add a service call edge (cross-service communication)
166    pub fn add_service_call(
167        &mut self,
168        from_node_id: String,
169        to_node_id: String,
170        call_label: Option<String>,
171    ) {
172        let edge = GraphEdge {
173            from: from_node_id,
174            to: to_node_id,
175            edge_type: EdgeType::ServiceCall,
176            label: call_label,
177            metadata: HashMap::new(),
178        };
179        self.graph.add_edge(edge);
180    }
181
182    /// Add a workspace cluster
183    pub fn add_workspace(
184        &mut self,
185        workspace_id: String,
186        workspace_name: String,
187        endpoint_ids: Vec<String>,
188    ) {
189        let cluster_id = format!("workspace:{}", workspace_id);
190        let cluster = GraphCluster {
191            id: cluster_id,
192            label: workspace_name,
193            cluster_type: ClusterType::Workspace,
194            node_ids: endpoint_ids,
195            metadata: {
196                let mut meta = HashMap::new();
197                meta.insert("workspace_id".to_string(), Value::String(workspace_id));
198                meta
199            },
200        };
201        self.graph.add_cluster(cluster);
202    }
203
204    /// Add a service cluster (micro-mock)
205    pub fn add_service(
206        &mut self,
207        service_id: String,
208        service_name: String,
209        endpoint_ids: Vec<String>,
210    ) {
211        let cluster_id = format!("service:{}", service_id);
212        let cluster = GraphCluster {
213            id: cluster_id,
214            label: service_name,
215            cluster_type: ClusterType::Service,
216            node_ids: endpoint_ids,
217            metadata: {
218                let mut meta = HashMap::new();
219                meta.insert("service_id".to_string(), Value::String(service_id));
220                meta
221            },
222        };
223        self.graph.add_cluster(cluster);
224    }
225
226    /// Update the current state of a node
227    pub fn update_node_state(&mut self, node_id: &str, state: String) {
228        if let Some(node) = self.graph.nodes.iter_mut().find(|n| n.id == node_id) {
229            node.current_state = Some(state);
230        }
231    }
232
233    /// Get the graph data (consumes the builder)
234    pub fn into_graph(self) -> GraphData {
235        self.graph
236    }
237
238    /// Build graph from UI Builder endpoints
239    pub fn from_endpoints(
240        &mut self,
241        endpoints: &[(String, String, String, String)], // (id, name, protocol, description)
242    ) {
243        for (id, name, protocol_str, description) in endpoints {
244            let protocol = convert_protocol(protocol_str);
245            let mut metadata = HashMap::new();
246            if !description.is_empty() {
247                metadata.insert("description".to_string(), Value::String(description.clone()));
248            }
249
250            self.add_endpoint(id.clone(), name.clone(), protocol, metadata);
251        }
252    }
253
254    /// Build graph from chain definitions
255    pub fn from_chains(&mut self, chains: &[ChainDefinition]) {
256        for chain in chains {
257            self.add_chain(chain);
258        }
259    }
260
261    /// Build graph from state machines
262    pub fn from_state_machines(&mut self, state_machines: &[StateMachine]) {
263        for state_machine in state_machines {
264            // Create nodes for each state
265            for state in &state_machine.states {
266                let node_id = format!("state:{}:{}", state_machine.resource_type, state);
267                let node = GraphNode {
268                    id: node_id.clone(),
269                    label: format!("{} ({})", state, state_machine.resource_type),
270                    node_type: NodeType::Endpoint,
271                    protocol: None,
272                    current_state: Some(state.clone()),
273                    metadata: {
274                        let mut meta = HashMap::new();
275                        meta.insert(
276                            "resource_type".to_string(),
277                            Value::String(state_machine.resource_type.clone()),
278                        );
279                        meta.insert(
280                            "is_initial".to_string(),
281                            Value::Bool(*state == state_machine.initial_state),
282                        );
283                        meta
284                    },
285                };
286                self.graph.add_node(node);
287            }
288
289            // Create edges for transitions
290            for transition in &state_machine.transitions {
291                let from_node_id =
292                    format!("state:{}:{}", state_machine.resource_type, transition.from_state);
293                let to_node_id =
294                    format!("state:{}:{}", state_machine.resource_type, transition.to_state);
295
296                self.add_state_transition(
297                    from_node_id,
298                    to_node_id,
299                    Some(format!("{} → {}", transition.from_state, transition.to_state)),
300                );
301            }
302        }
303    }
304}
305
306impl Default for GraphBuilder {
307    fn default() -> Self {
308        Self::new()
309    }
310}