infigraph_core/multi/
grpc.rs1use std::collections::HashSet;
2
3use crate::graph::store::GraphStore;
4use crate::graph::GraphQuery;
5
6pub 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 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 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
58pub 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 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 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(), 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
123fn 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}