aeo_graph_explorer/
query.rs1use petgraph::algo::astar;
4use petgraph::visit::EdgeRef;
5use serde::{Deserialize, Serialize};
6
7use crate::error::GraphError;
8use crate::graph::{AeoGraph, EdgeKind};
9use crate::model::{AeoEntity, AeoNode};
10
11#[derive(Clone, Debug, Deserialize, Serialize)]
13pub struct NeighborView {
14 pub center: AeoEntity,
16 pub outbound: Vec<NeighborEdge>,
18 pub inbound: Vec<NeighborEdge>,
20}
21
22#[derive(Clone, Debug, Deserialize, Serialize)]
24pub struct NeighborEdge {
25 pub edge: EdgeKind,
27 pub entity: AeoEntity,
29}
30
31#[derive(Clone, Debug, Deserialize, Serialize)]
33pub struct PathHop {
34 pub entity: AeoEntity,
36 pub via: Option<EdgeKind>,
39}
40
41#[derive(Clone, Debug, Deserialize, Serialize)]
43pub struct PathResult {
44 pub found: bool,
46 pub length: u32,
48 pub hops: Vec<PathHop>,
50}
51
52#[derive(Clone, Debug, Deserialize, Serialize)]
54pub struct ClaimMatch {
55 pub entity: AeoEntity,
57 pub claim_id: String,
59 pub predicate: String,
61 pub value: serde_json::Value,
63}
64
65pub fn neighbors(graph: &AeoGraph, id: &str) -> Result<NeighborView, GraphError> {
67 let idx = graph
68 .idx(id)
69 .ok_or_else(|| GraphError::NotFound(id.to_string()))?;
70 let raw = graph.raw();
71 let center = raw[idx].entity.clone();
72
73 let outbound = raw
74 .edges_directed(idx, petgraph::Outgoing)
75 .map(|e| NeighborEdge {
76 edge: *e.weight(),
77 entity: raw[e.target()].entity.clone(),
78 })
79 .collect();
80
81 let inbound = raw
82 .edges_directed(idx, petgraph::Incoming)
83 .map(|e| NeighborEdge {
84 edge: *e.weight(),
85 entity: raw[e.source()].entity.clone(),
86 })
87 .collect();
88
89 Ok(NeighborView {
90 center,
91 outbound,
92 inbound,
93 })
94}
95
96pub fn shortest_path(graph: &AeoGraph, from: &str, to: &str) -> Result<PathResult, GraphError> {
99 let from_idx = graph
100 .idx(from)
101 .ok_or_else(|| GraphError::NotFound(from.to_string()))?;
102 let to_idx = graph
103 .idx(to)
104 .ok_or_else(|| GraphError::NotFound(to.to_string()))?;
105 let raw = graph.raw();
106
107 let path = astar(raw, from_idx, |n| n == to_idx, |_| 1u32, |_| 0u32);
108 let Some((cost, nodes)) = path else {
109 return Ok(PathResult {
110 found: false,
111 length: 0,
112 hops: Vec::new(),
113 });
114 };
115
116 let mut hops: Vec<PathHop> = Vec::with_capacity(nodes.len());
117 for (i, idx) in nodes.iter().enumerate() {
118 let via = if i + 1 < nodes.len() {
119 raw.edges_connecting(*idx, nodes[i + 1])
120 .next()
121 .map(|e| *e.weight())
122 } else {
123 None
124 };
125 hops.push(PathHop {
126 entity: raw[*idx].entity.clone(),
127 via,
128 });
129 }
130
131 Ok(PathResult {
132 found: true,
133 length: cost,
134 hops,
135 })
136}
137
138pub fn find_by_claim(
148 graph: &AeoGraph,
149 predicate: Option<&str>,
150 value: Option<&str>,
151) -> Result<Vec<ClaimMatch>, GraphError> {
152 if predicate.is_none() && value.is_none() {
153 return Err(GraphError::EmptyQuery);
154 }
155
156 let mut out: Vec<ClaimMatch> = Vec::new();
157 for node in graph.nodes() {
158 match_node(node, predicate, value, &mut out);
159 }
160 Ok(out)
161}
162
163fn match_node(
164 node: &AeoNode,
165 predicate: Option<&str>,
166 value: Option<&str>,
167 out: &mut Vec<ClaimMatch>,
168) {
169 let Some(claims) = node.body.get("claims").and_then(|v| v.as_array()) else {
170 return;
171 };
172 for raw in claims {
173 let Some(pred) = raw.get("predicate").and_then(|v| v.as_str()) else {
174 continue;
175 };
176 if let Some(want) = predicate {
177 if pred != want {
178 continue;
179 }
180 }
181 let claim_value = raw.get("value").cloned().unwrap_or(serde_json::Value::Null);
182 if let Some(want) = value {
183 if !value_matches(&claim_value, want) {
184 continue;
185 }
186 }
187 let claim_id = raw
188 .get("id")
189 .and_then(|v| v.as_str())
190 .unwrap_or("")
191 .to_string();
192 out.push(ClaimMatch {
193 entity: node.entity.clone(),
194 claim_id,
195 predicate: pred.to_string(),
196 value: claim_value,
197 });
198 }
199}
200
201fn value_matches(actual: &serde_json::Value, query: &str) -> bool {
202 if let Some(s) = actual.as_str() {
203 return s == query;
204 }
205 serde_json::from_str::<serde_json::Value>(query).is_ok_and(|q| &q == actual)
206}