use std::collections::HashSet;
use crate::graph::store::GraphStore;
use crate::graph::GraphQuery;
pub fn extract_grpc_contracts(store: &GraphStore) -> Vec<super::Contract> {
let conn = match store.connection() {
Ok(c) => c,
Err(_) => return vec![],
};
let gq = GraphQuery::new(&conn);
let query = "MATCH (s:Symbol) WHERE s.kind = 'Class' AND s.file ENDS WITH '.proto' RETURN s.name, s.file, s.id";
let services = match gq.raw_query(query) {
Ok(r) => r,
Err(_) => return vec![],
};
let mut contracts = Vec::new();
for svc_row in &services {
if svc_row.len() < 3 {
continue;
}
let svc_name = &svc_row[0];
let svc_file = &svc_row[1];
let rpc_query = format!(
"MATCH (s:Symbol) WHERE s.kind = 'Method' AND s.file = '{}' AND s.parent = '{}' RETURN s.name, s.id",
svc_file.replace('\'', "\\'"),
svc_name.replace('\'', "\\'"),
);
if let Ok(rpcs) = gq.raw_query(&rpc_query) {
for rpc in &rpcs {
if rpc.is_empty() {
continue;
}
contracts.push(super::Contract {
kind: super::ContractKind::GrpcService,
service: svc_name.clone(),
method: "GRPC".to_string(),
path: format!("/{}/{}", svc_name, rpc[0]),
symbol_id: rpc.get(1).cloned().unwrap_or_default(),
file: svc_file.clone(),
});
}
}
}
contracts
}
pub fn detect_grpc_clients(
store: &GraphStore,
contracts: &[super::Contract],
) -> Vec<super::CrossServiceDep> {
if contracts.is_empty() {
return vec![];
}
let conn = match store.connection() {
Ok(c) => c,
Err(_) => return vec![],
};
let gq = GraphQuery::new(&conn);
let svc_names: HashSet<&str> = contracts
.iter()
.filter(|c| c.kind == super::ContractKind::GrpcService)
.map(|c| c.service.as_str())
.collect();
let mut deps = Vec::new();
for svc_name in &svc_names {
let patterns = [
format!("{}Stub", svc_name),
format!("{}Client", svc_name),
format!("{}Grpc", svc_name),
format!("{}_pb2_grpc", to_snake_case(svc_name)),
];
for pattern in &patterns {
let query = format!(
"MATCH (s:Symbol) WHERE s.name CONTAINS '{}' AND NOT s.file ENDS WITH '.proto' RETURN s.name, s.file, s.id",
pattern.replace('\'', "\\'"),
);
if let Ok(rows) = gq.raw_query(&query) {
for row in &rows {
if row.len() < 2 {
continue;
}
deps.push(super::CrossServiceDep {
caller_service: String::new(), caller_file: row[1].clone(),
caller_symbol: row.get(2).cloned().unwrap_or_default(),
target_service: svc_name.to_string(),
target_method: "GRPC".to_string(),
target_path: format!("/{}", svc_name),
url_found: format!("grpc://{}", svc_name),
});
}
}
}
}
deps
}
fn to_snake_case(s: &str) -> String {
let mut result = String::new();
for (i, ch) in s.chars().enumerate() {
if ch.is_uppercase() && i > 0 {
result.push('_');
}
result.push(ch.to_lowercase().next().unwrap_or(ch));
}
result
}