use petgraph::algo::astar;
use petgraph::visit::EdgeRef;
use serde::{Deserialize, Serialize};
use crate::error::GraphError;
use crate::graph::{AeoGraph, EdgeKind};
use crate::model::{AeoEntity, AeoNode};
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct NeighborView {
pub center: AeoEntity,
pub outbound: Vec<NeighborEdge>,
pub inbound: Vec<NeighborEdge>,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct NeighborEdge {
pub edge: EdgeKind,
pub entity: AeoEntity,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct PathHop {
pub entity: AeoEntity,
pub via: Option<EdgeKind>,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct PathResult {
pub found: bool,
pub length: u32,
pub hops: Vec<PathHop>,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct ClaimMatch {
pub entity: AeoEntity,
pub claim_id: String,
pub predicate: String,
pub value: serde_json::Value,
}
pub fn neighbors(graph: &AeoGraph, id: &str) -> Result<NeighborView, GraphError> {
let idx = graph
.idx(id)
.ok_or_else(|| GraphError::NotFound(id.to_string()))?;
let raw = graph.raw();
let center = raw[idx].entity.clone();
let outbound = raw
.edges_directed(idx, petgraph::Outgoing)
.map(|e| NeighborEdge {
edge: *e.weight(),
entity: raw[e.target()].entity.clone(),
})
.collect();
let inbound = raw
.edges_directed(idx, petgraph::Incoming)
.map(|e| NeighborEdge {
edge: *e.weight(),
entity: raw[e.source()].entity.clone(),
})
.collect();
Ok(NeighborView {
center,
outbound,
inbound,
})
}
pub fn shortest_path(graph: &AeoGraph, from: &str, to: &str) -> Result<PathResult, GraphError> {
let from_idx = graph
.idx(from)
.ok_or_else(|| GraphError::NotFound(from.to_string()))?;
let to_idx = graph
.idx(to)
.ok_or_else(|| GraphError::NotFound(to.to_string()))?;
let raw = graph.raw();
let path = astar(raw, from_idx, |n| n == to_idx, |_| 1u32, |_| 0u32);
let Some((cost, nodes)) = path else {
return Ok(PathResult {
found: false,
length: 0,
hops: Vec::new(),
});
};
let mut hops: Vec<PathHop> = Vec::with_capacity(nodes.len());
for (i, idx) in nodes.iter().enumerate() {
let via = if i + 1 < nodes.len() {
raw.edges_connecting(*idx, nodes[i + 1])
.next()
.map(|e| *e.weight())
} else {
None
};
hops.push(PathHop {
entity: raw[*idx].entity.clone(),
via,
});
}
Ok(PathResult {
found: true,
length: cost,
hops,
})
}
pub fn find_by_claim(
graph: &AeoGraph,
predicate: Option<&str>,
value: Option<&str>,
) -> Result<Vec<ClaimMatch>, GraphError> {
if predicate.is_none() && value.is_none() {
return Err(GraphError::EmptyQuery);
}
let mut out: Vec<ClaimMatch> = Vec::new();
for node in graph.nodes() {
match_node(node, predicate, value, &mut out);
}
Ok(out)
}
fn match_node(
node: &AeoNode,
predicate: Option<&str>,
value: Option<&str>,
out: &mut Vec<ClaimMatch>,
) {
let Some(claims) = node.body.get("claims").and_then(|v| v.as_array()) else {
return;
};
for raw in claims {
let Some(pred) = raw.get("predicate").and_then(|v| v.as_str()) else {
continue;
};
if let Some(want) = predicate {
if pred != want {
continue;
}
}
let claim_value = raw.get("value").cloned().unwrap_or(serde_json::Value::Null);
if let Some(want) = value {
if !value_matches(&claim_value, want) {
continue;
}
}
let claim_id = raw
.get("id")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
out.push(ClaimMatch {
entity: node.entity.clone(),
claim_id,
predicate: pred.to_string(),
value: claim_value,
});
}
}
fn value_matches(actual: &serde_json::Value, query: &str) -> bool {
if let Some(s) = actual.as_str() {
return s == query;
}
serde_json::from_str::<serde_json::Value>(query).is_ok_and(|q| &q == actual)
}