1use manifoldb_core::{EdgeId, EdgeType, EntityId};
8use manifoldb_graph::traversal::{Direction, PathPattern, PathStep, PatternMatch, StepFilter};
9
10#[derive(Debug, Clone, PartialEq, Eq)]
12pub struct NeighborResult {
13 pub node: EntityId,
15 pub edge_id: EdgeId,
17 pub direction: Direction,
19}
20
21impl NeighborResult {
22 pub const fn new(node: EntityId, edge_id: EdgeId, direction: Direction) -> Self {
24 Self { node, edge_id, direction }
25 }
26}
27
28#[derive(Debug, Clone, PartialEq, Eq)]
30pub struct TraversalResult {
31 pub node: EntityId,
33 pub edge_id: Option<EdgeId>,
35 pub depth: usize,
37}
38
39impl TraversalResult {
40 pub const fn new(node: EntityId, edge_id: Option<EdgeId>, depth: usize) -> Self {
42 Self { node, edge_id, depth }
43 }
44}
45
46#[derive(Debug, Clone, PartialEq, Eq)]
48pub struct PathMatchResult {
49 pub nodes: Vec<EntityId>,
51 pub step_edges: Vec<Vec<EdgeId>>,
54}
55
56impl PathMatchResult {
57 pub fn new(nodes: Vec<EntityId>, step_edges: Vec<Vec<EdgeId>>) -> Self {
59 Self { nodes, step_edges }
60 }
61
62 pub fn source(&self) -> Option<EntityId> {
64 self.nodes.first().copied()
65 }
66
67 pub fn target(&self) -> Option<EntityId> {
69 self.nodes.last().copied()
70 }
71
72 pub fn all_edges(&self) -> Vec<EdgeId> {
74 self.step_edges.iter().flatten().copied().collect()
75 }
76
77 pub fn length(&self) -> usize {
79 self.step_edges.iter().map(std::vec::Vec::len).sum()
80 }
81}
82
83impl From<PatternMatch> for PathMatchResult {
84 fn from(pm: PatternMatch) -> Self {
85 Self { nodes: pm.nodes, step_edges: pm.step_edges }
86 }
87}
88
89#[derive(Debug, Clone)]
91pub struct PathFindConfig {
92 pub steps: Vec<PathStepConfig>,
94 pub limit: Option<usize>,
96 pub allow_cycles: bool,
98}
99
100#[derive(Debug, Clone)]
102pub struct PathStepConfig {
103 pub direction: Direction,
105 pub edge_types: Vec<EdgeType>,
107 pub min_hops: usize,
109 pub max_hops: Option<usize>,
111}
112
113impl PathStepConfig {
114 pub fn to_path_step(&self) -> PathStep {
116 let filter = if self.edge_types.is_empty() {
117 StepFilter::Any
118 } else if self.edge_types.len() == 1 {
119 StepFilter::EdgeType(self.edge_types[0].clone())
120 } else {
121 StepFilter::EdgeTypes(self.edge_types.clone())
122 };
123
124 PathStep::new(self.direction, filter).with_hops(self.min_hops, self.max_hops)
125 }
126}
127
128#[derive(Debug, Clone, thiserror::Error)]
130pub enum GraphAccessError {
131 #[error("no graph storage available")]
133 NoStorage,
134 #[error("graph error: {0}")]
136 Internal(String),
137}
138
139pub type GraphAccessResult<T> = Result<T, GraphAccessError>;
141
142pub trait GraphAccessor: Send + Sync {
148 fn neighbors(
152 &self,
153 node: EntityId,
154 direction: Direction,
155 ) -> GraphAccessResult<Vec<NeighborResult>>;
156
157 fn neighbors_by_type(
159 &self,
160 node: EntityId,
161 direction: Direction,
162 edge_type: &EdgeType,
163 ) -> GraphAccessResult<Vec<NeighborResult>>;
164
165 fn neighbors_by_types(
167 &self,
168 node: EntityId,
169 direction: Direction,
170 edge_types: &[EdgeType],
171 ) -> GraphAccessResult<Vec<NeighborResult>>;
172
173 fn expand_all(
177 &self,
178 node: EntityId,
179 direction: Direction,
180 min_depth: usize,
181 max_depth: Option<usize>,
182 edge_types: Option<&[EdgeType]>,
183 ) -> GraphAccessResult<Vec<TraversalResult>>;
184
185 fn find_paths(
189 &self,
190 start: EntityId,
191 config: &PathFindConfig,
192 ) -> GraphAccessResult<Vec<PathMatchResult>>;
193}
194
195#[derive(Debug, Clone, Copy, Default)]
199pub struct NullGraphAccessor;
200
201impl GraphAccessor for NullGraphAccessor {
202 fn neighbors(
203 &self,
204 _node: EntityId,
205 _direction: Direction,
206 ) -> GraphAccessResult<Vec<NeighborResult>> {
207 Err(GraphAccessError::NoStorage)
208 }
209
210 fn neighbors_by_type(
211 &self,
212 _node: EntityId,
213 _direction: Direction,
214 _edge_type: &EdgeType,
215 ) -> GraphAccessResult<Vec<NeighborResult>> {
216 Err(GraphAccessError::NoStorage)
217 }
218
219 fn neighbors_by_types(
220 &self,
221 _node: EntityId,
222 _direction: Direction,
223 _edge_types: &[EdgeType],
224 ) -> GraphAccessResult<Vec<NeighborResult>> {
225 Err(GraphAccessError::NoStorage)
226 }
227
228 fn expand_all(
229 &self,
230 _node: EntityId,
231 _direction: Direction,
232 _min_depth: usize,
233 _max_depth: Option<usize>,
234 _edge_types: Option<&[EdgeType]>,
235 ) -> GraphAccessResult<Vec<TraversalResult>> {
236 Err(GraphAccessError::NoStorage)
237 }
238
239 fn find_paths(
240 &self,
241 _start: EntityId,
242 _config: &PathFindConfig,
243 ) -> GraphAccessResult<Vec<PathMatchResult>> {
244 Err(GraphAccessError::NoStorage)
245 }
246}
247
248pub struct TransactionGraphAccessor<T> {
252 tx: T,
253}
254
255impl<T> TransactionGraphAccessor<T> {
256 pub fn new(tx: T) -> Self {
258 Self { tx }
259 }
260}
261
262impl<T> GraphAccessor for TransactionGraphAccessor<T>
263where
264 T: manifoldb_storage::Transaction + Send + Sync,
265{
266 fn neighbors(
267 &self,
268 node: EntityId,
269 direction: Direction,
270 ) -> GraphAccessResult<Vec<NeighborResult>> {
271 manifoldb_graph::traversal::Expand::neighbors(&self.tx, node, direction)
272 .map(|results| {
273 results
274 .into_iter()
275 .map(|r| NeighborResult::new(r.node, r.edge_id, r.direction))
276 .collect()
277 })
278 .map_err(|e| GraphAccessError::Internal(e.to_string()))
279 }
280
281 fn neighbors_by_type(
282 &self,
283 node: EntityId,
284 direction: Direction,
285 edge_type: &EdgeType,
286 ) -> GraphAccessResult<Vec<NeighborResult>> {
287 manifoldb_graph::traversal::Expand::neighbors_by_type(&self.tx, node, direction, edge_type)
288 .map(|results| {
289 results
290 .into_iter()
291 .map(|r| NeighborResult::new(r.node, r.edge_id, r.direction))
292 .collect()
293 })
294 .map_err(|e| GraphAccessError::Internal(e.to_string()))
295 }
296
297 fn neighbors_by_types(
298 &self,
299 node: EntityId,
300 direction: Direction,
301 edge_types: &[EdgeType],
302 ) -> GraphAccessResult<Vec<NeighborResult>> {
303 let mut results = Vec::new();
305 for edge_type in edge_types {
306 let neighbors = manifoldb_graph::traversal::Expand::neighbors_by_type(
307 &self.tx, node, direction, edge_type,
308 )
309 .map_err(|e| GraphAccessError::Internal(e.to_string()))?;
310
311 results.extend(
312 neighbors.into_iter().map(|r| NeighborResult::new(r.node, r.edge_id, r.direction)),
313 );
314 }
315 Ok(results)
316 }
317
318 fn expand_all(
319 &self,
320 node: EntityId,
321 direction: Direction,
322 min_depth: usize,
323 max_depth: Option<usize>,
324 edge_types: Option<&[EdgeType]>,
325 ) -> GraphAccessResult<Vec<TraversalResult>> {
326 let mut expander =
327 manifoldb_graph::traversal::ExpandAll::new(node, direction).with_min_depth(min_depth);
328
329 if let Some(max) = max_depth {
330 expander = expander.with_max_depth(max);
331 }
332
333 if let Some(types) = edge_types {
335 for edge_type in types {
336 expander = expander.with_edge_type(edge_type.clone());
337 }
338 }
339
340 expander
341 .execute(&self.tx)
342 .map(|results| {
343 results.into_iter().map(|r| TraversalResult::new(r.id, None, r.depth)).collect()
344 })
345 .map_err(|e| GraphAccessError::Internal(e.to_string()))
346 }
347
348 fn find_paths(
349 &self,
350 start: EntityId,
351 config: &PathFindConfig,
352 ) -> GraphAccessResult<Vec<PathMatchResult>> {
353 let mut pattern = PathPattern::new();
355
356 for step in &config.steps {
357 pattern = pattern.add_step(step.to_path_step());
358 }
359
360 if let Some(limit) = config.limit {
362 pattern = pattern.with_limit(limit);
363 }
364
365 if config.allow_cycles {
367 pattern = pattern.allow_cycles();
368 }
369
370 pattern
372 .find_from(&self.tx, start)
373 .map(|matches| matches.into_iter().map(PathMatchResult::from).collect())
374 .map_err(|e| GraphAccessError::Internal(e.to_string()))
375 }
376}
377
378#[cfg(test)]
379mod tests {
380 use super::*;
381
382 #[test]
383 fn null_accessor_returns_no_storage() {
384 let accessor = NullGraphAccessor;
385
386 let result = accessor.neighbors(EntityId::new(1), Direction::Outgoing);
387 assert!(matches!(result, Err(GraphAccessError::NoStorage)));
388 }
389
390 #[test]
391 fn neighbor_result_creation() {
392 let result = NeighborResult::new(EntityId::new(1), EdgeId::new(10), Direction::Outgoing);
393 assert_eq!(result.node, EntityId::new(1));
394 assert_eq!(result.edge_id, EdgeId::new(10));
395 assert_eq!(result.direction, Direction::Outgoing);
396 }
397
398 #[test]
399 fn traversal_result_creation() {
400 let result = TraversalResult::new(EntityId::new(2), Some(EdgeId::new(20)), 3);
401 assert_eq!(result.node, EntityId::new(2));
402 assert_eq!(result.edge_id, Some(EdgeId::new(20)));
403 assert_eq!(result.depth, 3);
404 }
405
406 #[test]
407 fn path_match_result_creation() {
408 let result = PathMatchResult::new(
409 vec![EntityId::new(1), EntityId::new(2), EntityId::new(3)],
410 vec![vec![EdgeId::new(10)], vec![EdgeId::new(20)]],
411 );
412 assert_eq!(result.source(), Some(EntityId::new(1)));
413 assert_eq!(result.target(), Some(EntityId::new(3)));
414 assert_eq!(result.length(), 2);
415 assert_eq!(result.all_edges(), vec![EdgeId::new(10), EdgeId::new(20)]);
416 }
417
418 #[test]
419 fn path_step_config_to_path_step() {
420 let config = PathStepConfig {
421 direction: Direction::Outgoing,
422 edge_types: vec![EdgeType::new("FRIEND")],
423 min_hops: 1,
424 max_hops: Some(3),
425 };
426 let step = config.to_path_step();
427 assert_eq!(step.direction, Direction::Outgoing);
428 assert_eq!(step.min_hops, 1);
429 assert_eq!(step.max_hops, Some(3));
430 }
431
432 #[test]
433 fn null_accessor_find_paths_returns_no_storage() {
434 let accessor = NullGraphAccessor;
435 let config = PathFindConfig { steps: vec![], limit: None, allow_cycles: false };
436 let result = accessor.find_paths(EntityId::new(1), &config);
437 assert!(matches!(result, Err(GraphAccessError::NoStorage)));
438 }
439}