Skip to main content

infigraph_core/multi/
grpc.rs

1use std::collections::HashSet;
2
3use crate::graph::store::GraphStore;
4use crate::graph::GraphQuery;
5
6/// Extract gRPC service contracts from .proto files in the graph.
7///
8/// Queries for proto Service symbols (kind='Class') in .proto files
9/// and their child Method symbols (RPC methods), producing one Contract
10/// per RPC endpoint with kind=GrpcService.
11pub fn extract_grpc_contracts(store: &GraphStore) -> Vec<super::Contract> {
12    let conn = match store.connection() {
13        Ok(c) => c,
14        Err(_) => return vec![],
15    };
16    let gq = GraphQuery::new(&conn);
17
18    // Find services in .proto files
19    let query = "MATCH (s:Symbol) WHERE s.kind = 'Class' AND s.file ENDS WITH '.proto' RETURN s.name, s.file, s.id";
20    let services = match gq.raw_query(query) {
21        Ok(r) => r,
22        Err(_) => return vec![],
23    };
24
25    let mut contracts = Vec::new();
26    for svc_row in &services {
27        if svc_row.len() < 3 {
28            continue;
29        }
30        let svc_name = &svc_row[0];
31        let svc_file = &svc_row[1];
32
33        // Find RPC methods for this service
34        let rpc_query = format!(
35            "MATCH (s:Symbol) WHERE s.kind = 'Method' AND s.file = '{}' AND s.parent = '{}' RETURN s.name, s.id",
36            svc_file.replace('\'', "\\'"),
37            svc_name.replace('\'', "\\'"),
38        );
39        if let Ok(rpcs) = gq.raw_query(&rpc_query) {
40            for rpc in &rpcs {
41                if rpc.is_empty() {
42                    continue;
43                }
44                contracts.push(super::Contract {
45                    kind: super::ContractKind::GrpcService,
46                    service: svc_name.clone(),
47                    method: "GRPC".to_string(),
48                    path: format!("/{}/{}", svc_name, rpc[0]),
49                    symbol_id: rpc.get(1).cloned().unwrap_or_default(),
50                    file: svc_file.clone(),
51                });
52            }
53        }
54    }
55    contracts
56}
57
58/// Detect gRPC client usage patterns in source files.
59///
60/// Looks for symbols referencing gRPC service stubs/clients:
61///   - `ServiceNameStub`
62///   - `ServiceNameClient`
63///   - `ServiceNameGrpc`
64///   - `service_name_pb2_grpc` (Python pattern)
65pub fn detect_grpc_clients(
66    store: &GraphStore,
67    contracts: &[super::Contract],
68) -> Vec<super::CrossServiceDep> {
69    if contracts.is_empty() {
70        return vec![];
71    }
72
73    let conn = match store.connection() {
74        Ok(c) => c,
75        Err(_) => return vec![],
76    };
77    let gq = GraphQuery::new(&conn);
78
79    // Build unique service names from gRPC contracts
80    let svc_names: HashSet<&str> = contracts
81        .iter()
82        .filter(|c| c.kind == super::ContractKind::GrpcService)
83        .map(|c| c.service.as_str())
84        .collect();
85
86    let mut deps = Vec::new();
87
88    for svc_name in &svc_names {
89        // Search for symbols referencing this service (Stub, Client patterns)
90        let patterns = [
91            format!("{}Stub", svc_name),
92            format!("{}Client", svc_name),
93            format!("{}Grpc", svc_name),
94            format!("{}_pb2_grpc", to_snake_case(svc_name)),
95        ];
96
97        for pattern in &patterns {
98            let query = format!(
99                "MATCH (s:Symbol) WHERE s.name CONTAINS '{}' AND NOT s.file ENDS WITH '.proto' RETURN s.name, s.file, s.id",
100                pattern.replace('\'', "\\'"),
101            );
102            if let Ok(rows) = gq.raw_query(&query) {
103                for row in &rows {
104                    if row.len() < 2 {
105                        continue;
106                    }
107                    deps.push(super::CrossServiceDep {
108                        caller_service: String::new(), // filled by caller
109                        caller_file: row[1].clone(),
110                        caller_symbol: row.get(2).cloned().unwrap_or_default(),
111                        target_service: svc_name.to_string(),
112                        target_method: "GRPC".to_string(),
113                        target_path: format!("/{}", svc_name),
114                        url_found: format!("grpc://{}", svc_name),
115                    });
116                }
117            }
118        }
119    }
120    deps
121}
122
123/// Convert PascalCase/camelCase to snake_case for Python gRPC pattern matching.
124fn to_snake_case(s: &str) -> String {
125    let mut result = String::new();
126    for (i, ch) in s.chars().enumerate() {
127        if ch.is_uppercase() && i > 0 {
128            result.push('_');
129        }
130        result.push(ch.to_lowercase().next().unwrap_or(ch));
131    }
132    result
133}