use std::collections::HashMap;
use crate::schema::{Edge, Graph, Kind, Node};
#[derive(Debug)]
pub struct GraphIndex<'a> {
nodes_by_id: HashMap<&'a str, &'a Node>,
incoming_by_id: HashMap<&'a str, Vec<&'a Edge>>,
outgoing_by_id: HashMap<&'a str, Vec<&'a Edge>>,
}
impl<'a> GraphIndex<'a> {
pub fn build(graph: &'a Graph) -> Self {
let mut nodes_by_id = HashMap::with_capacity(graph.nodes.len());
for node in &graph.nodes {
nodes_by_id.insert(node.id.as_str(), node);
}
let mut incoming_by_id: HashMap<&'a str, Vec<&'a Edge>> = HashMap::new();
let mut outgoing_by_id: HashMap<&'a str, Vec<&'a Edge>> = HashMap::new();
for edge in &graph.edges {
outgoing_by_id
.entry(edge.from.as_str())
.or_default()
.push(edge);
incoming_by_id
.entry(edge.to.as_str())
.or_default()
.push(edge);
}
for edges in incoming_by_id.values_mut() {
edges.sort_by(|a, b| (&a.from, &a.to, &a.relation).cmp(&(&b.from, &b.to, &b.relation)));
}
for edges in outgoing_by_id.values_mut() {
edges.sort_by(|a, b| (&a.from, &a.to, &a.relation).cmp(&(&b.from, &b.to, &b.relation)));
}
Self {
nodes_by_id,
incoming_by_id,
outgoing_by_id,
}
}
pub fn node(&self, id: &str) -> Option<&'a Node> {
self.nodes_by_id.get(id).copied()
}
pub fn is_kind(&self, id: &str, kind: Kind) -> bool {
self.node(id).is_some_and(|node| node.kind == kind)
}
pub fn incoming(&self, id: &str) -> impl Iterator<Item = &'a Edge> + '_ {
self.incoming_by_id
.get(id)
.into_iter()
.flat_map(|edges| edges.iter().copied())
}
pub fn outgoing(&self, id: &str) -> impl Iterator<Item = &'a Edge> + '_ {
self.outgoing_by_id
.get(id)
.into_iter()
.flat_map(|edges| edges.iter().copied())
}
pub fn has_outgoing(&self, from: &str, relation: &str, to: &str) -> bool {
self.outgoing(from)
.any(|edge| edge.relation == relation && edge.to == to)
}
pub fn source_of(&self, node_id: &str) -> Option<String> {
let node = self.node(node_id)?;
if let Some(span) = &node.span {
if span.start_line == span.end_line {
return Some(format!("{}:{}", span.path, span.start_line));
}
return Some(format!(
"{}:{}-{}",
span.path, span.start_line, span.end_line
));
}
node.source_files.first().cloned()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::schema::{Edge, Kind, Node, Span, relation};
fn node(id: &str, kind: Kind) -> Node {
Node {
id: id.into(),
kind,
subkind: None,
title: id.into(),
summary: String::new(),
aliases: Vec::new(),
tags: Vec::new(),
query_examples: Vec::new(),
source_files: vec![format!("fixtures/{id}.rs")],
span: None,
partition: None,
}
}
#[test]
fn indexes_nodes_and_edges() {
let graph = Graph {
nodes: vec![node("a", Kind::Function), node("b", Kind::Type)],
edges: vec![Edge {
from: "a".into(),
to: "b".into(),
relation: relation::REFERENCES.into(),
evidence: "test".into(),
..Default::default()
}],
..Default::default()
};
let index = GraphIndex::build(&graph);
assert!(index.is_kind("a", Kind::Function));
assert_eq!(index.node("b").map(|n| n.title.as_str()), Some("b"));
assert!(index.has_outgoing("a", relation::REFERENCES, "b"));
assert_eq!(index.incoming("b").count(), 1);
assert_eq!(index.outgoing("a").count(), 1);
}
#[test]
fn source_receipt_prefers_exact_span() {
let mut n = node("a", Kind::Function);
n.span = Some(Span {
path: "fixtures/a.rs".into(),
start_line: 4,
end_line: 7,
});
let graph = Graph {
nodes: vec![n],
edges: Vec::new(),
..Default::default()
};
let index = GraphIndex::build(&graph);
assert_eq!(index.source_of("a").as_deref(), Some("fixtures/a.rs:4-7"));
}
}