1use std::collections::{HashSet, VecDeque};
6
7#[derive(Debug, Clone, PartialEq, Eq, Hash)]
11pub struct GraphTriple {
12 pub subject: String,
13 pub predicate: String,
14 pub object: String,
15}
16
17impl GraphTriple {
18 pub fn new(
20 subject: impl Into<String>,
21 predicate: impl Into<String>,
22 object: impl Into<String>,
23 ) -> Self {
24 GraphTriple {
25 subject: subject.into(),
26 predicate: predicate.into(),
27 object: object.into(),
28 }
29 }
30}
31
32pub struct RdfGraph<'a> {
34 triples: &'a [GraphTriple],
35}
36
37impl<'a> RdfGraph<'a> {
38 pub fn new(triples: &'a [GraphTriple]) -> Self {
40 RdfGraph { triples }
41 }
42
43 pub fn objects_of(&self, subject: &str, predicate: &str) -> Vec<String> {
45 self.triples
46 .iter()
47 .filter(|t| t.subject == subject && t.predicate == predicate)
48 .map(|t| t.object.clone())
49 .collect()
50 }
51
52 pub fn subjects_of(&self, predicate: &str, object: &str) -> Vec<String> {
54 self.triples
55 .iter()
56 .filter(|t| t.predicate == predicate && t.object == object)
57 .map(|t| t.subject.clone())
58 .collect()
59 }
60}
61
62#[derive(Debug, Clone, PartialEq, Eq)]
66pub enum PropertyPath {
67 Predicate(String),
69 Inverse(Box<PropertyPath>),
71 Sequence(Vec<PropertyPath>),
73 Alternative(Vec<PropertyPath>),
75 ZeroOrMore(Box<PropertyPath>),
77 OneOrMore(Box<PropertyPath>),
79 ZeroOrOne(Box<PropertyPath>),
81}
82
83#[derive(Debug, Clone, PartialEq, Eq)]
87pub struct PathResult {
88 pub nodes: Vec<String>,
90 pub steps_taken: usize,
93}
94
95impl PathResult {
96 pub fn is_empty(&self) -> bool {
98 self.nodes.is_empty()
99 }
100
101 pub fn len(&self) -> usize {
103 self.nodes.len()
104 }
105
106 pub fn contains(&self, node: &str) -> bool {
108 self.nodes.iter().any(|n| n == node)
109 }
110}
111
112pub struct PathExecutor;
116
117impl PathExecutor {
118 pub fn evaluate(path: &PropertyPath, start: &str, graph: &RdfGraph<'_>) -> PathResult {
122 let mut visited: HashSet<String> = HashSet::new();
123 let mut steps = 0usize;
124 let nodes = eval_path(path, start, graph, &mut visited, &mut steps);
125 PathResult {
126 nodes,
127 steps_taken: steps,
128 }
129 }
130
131 pub fn has_any(path: &PropertyPath, start: &str, graph: &RdfGraph<'_>) -> bool {
133 !Self::evaluate(path, start, graph).is_empty()
134 }
135
136 pub fn reaches(path: &PropertyPath, start: &str, target: &str, graph: &RdfGraph<'_>) -> bool {
138 Self::evaluate(path, start, graph).contains(target)
139 }
140}
141
142fn eval_path(
145 path: &PropertyPath,
146 start: &str,
147 graph: &RdfGraph<'_>,
148 global_visited: &mut HashSet<String>,
149 steps: &mut usize,
150) -> Vec<String> {
151 match path {
152 PropertyPath::Predicate(iri) => eval_predicate(start, iri, graph, steps),
153 PropertyPath::Inverse(inner) => eval_inverse(inner, start, graph, steps),
154 PropertyPath::Sequence(parts) => eval_sequence(parts, start, graph, global_visited, steps),
155 PropertyPath::Alternative(alts) => {
156 eval_alternative(alts, start, graph, global_visited, steps)
157 }
158 PropertyPath::ZeroOrMore(inner) => eval_zero_or_more(inner, start, graph, steps),
159 PropertyPath::OneOrMore(inner) => eval_one_or_more(inner, start, graph, steps),
160 PropertyPath::ZeroOrOne(inner) => eval_zero_or_one(inner, start, graph, steps),
161 }
162}
163
164fn eval_predicate(
166 start: &str,
167 predicate: &str,
168 graph: &RdfGraph<'_>,
169 steps: &mut usize,
170) -> Vec<String> {
171 let nodes = graph.objects_of(start, predicate);
172 *steps += nodes.len();
173 dedup(nodes)
174}
175
176fn eval_inverse(
178 inner: &PropertyPath,
179 start: &str,
180 graph: &RdfGraph<'_>,
181 steps: &mut usize,
182) -> Vec<String> {
183 match inner {
184 PropertyPath::Predicate(iri) => {
185 let nodes = graph.subjects_of(iri, start);
186 *steps += nodes.len();
187 dedup(nodes)
188 }
189 other => {
190 let mut candidates: HashSet<String> = HashSet::new();
193 for triple in graph.triples {
194 let mut vis = HashSet::new();
196 let reachable = eval_path(other, &triple.subject, graph, &mut vis, steps);
197 if reachable.iter().any(|n| n == start) {
198 candidates.insert(triple.subject.clone());
199 }
200 }
201 candidates.into_iter().collect()
202 }
203 }
204}
205
206fn eval_sequence(
208 parts: &[PropertyPath],
209 start: &str,
210 graph: &RdfGraph<'_>,
211 global_visited: &mut HashSet<String>,
212 steps: &mut usize,
213) -> Vec<String> {
214 let mut frontier: Vec<String> = vec![start.to_owned()];
215 for segment in parts {
216 let mut next: HashSet<String> = HashSet::new();
217 for node in &frontier {
218 let reached = eval_path(segment, node, graph, global_visited, steps);
219 next.extend(reached);
220 }
221 frontier = next.into_iter().collect();
222 }
223 frontier
224}
225
226fn eval_alternative(
228 alts: &[PropertyPath],
229 start: &str,
230 graph: &RdfGraph<'_>,
231 global_visited: &mut HashSet<String>,
232 steps: &mut usize,
233) -> Vec<String> {
234 let mut result: HashSet<String> = HashSet::new();
235 for alt in alts {
236 let reached = eval_path(alt, start, graph, global_visited, steps);
237 result.extend(reached);
238 }
239 result.into_iter().collect()
240}
241
242fn eval_zero_or_more(
244 inner: &PropertyPath,
245 start: &str,
246 graph: &RdfGraph<'_>,
247 steps: &mut usize,
248) -> Vec<String> {
249 let mut visited: HashSet<String> = HashSet::new();
250 let mut queue: VecDeque<String> = VecDeque::new();
251 visited.insert(start.to_owned());
253 queue.push_back(start.to_owned());
254
255 while let Some(current) = queue.pop_front() {
256 let mut inner_vis = HashSet::new();
257 let reached = eval_path(inner, ¤t, graph, &mut inner_vis, steps);
258 for node in reached {
259 if visited.insert(node.clone()) {
260 queue.push_back(node);
261 }
262 }
263 }
264 visited.into_iter().collect()
265}
266
267fn eval_one_or_more(
269 inner: &PropertyPath,
270 start: &str,
271 graph: &RdfGraph<'_>,
272 steps: &mut usize,
273) -> Vec<String> {
274 let mut expanded: HashSet<String> = HashSet::new();
276 let mut queue: VecDeque<String> = VecDeque::new();
277
278 let mut seed_vis = HashSet::new();
280 let first = eval_path(inner, start, graph, &mut seed_vis, steps);
281 for node in first {
282 if expanded.insert(node.clone()) {
283 queue.push_back(node);
284 }
285 }
286
287 while let Some(current) = queue.pop_front() {
289 let mut inner_vis = HashSet::new();
290 let reached = eval_path(inner, ¤t, graph, &mut inner_vis, steps);
291 for node in reached {
292 if expanded.insert(node.clone()) {
293 queue.push_back(node);
294 }
295 }
296 }
297 expanded.into_iter().collect()
298}
299
300fn eval_zero_or_one(
302 inner: &PropertyPath,
303 start: &str,
304 graph: &RdfGraph<'_>,
305 steps: &mut usize,
306) -> Vec<String> {
307 let mut result: HashSet<String> = HashSet::new();
308 result.insert(start.to_owned());
310 let mut vis = HashSet::new();
312 let one_step = eval_path(inner, start, graph, &mut vis, steps);
313 result.extend(one_step);
314 result.into_iter().collect()
315}
316
317fn dedup(nodes: Vec<String>) -> Vec<String> {
319 let mut seen = HashSet::new();
320 nodes
321 .into_iter()
322 .filter(|n| seen.insert(n.clone()))
323 .collect()
324}
325
326impl PropertyPath {
329 pub fn predicate(iri: impl Into<String>) -> Self {
331 PropertyPath::Predicate(iri.into())
332 }
333
334 pub fn inverse(inner: PropertyPath) -> Self {
336 PropertyPath::Inverse(Box::new(inner))
337 }
338
339 pub fn sequence(parts: Vec<PropertyPath>) -> Self {
341 PropertyPath::Sequence(parts)
342 }
343
344 pub fn alternative(alts: Vec<PropertyPath>) -> Self {
346 PropertyPath::Alternative(alts)
347 }
348
349 pub fn zero_or_more(inner: PropertyPath) -> Self {
351 PropertyPath::ZeroOrMore(Box::new(inner))
352 }
353
354 pub fn one_or_more(inner: PropertyPath) -> Self {
356 PropertyPath::OneOrMore(Box::new(inner))
357 }
358
359 pub fn zero_or_one(inner: PropertyPath) -> Self {
361 PropertyPath::ZeroOrOne(Box::new(inner))
362 }
363}
364
365#[cfg(test)]
368mod tests {
369 use super::*;
370
371 fn build_graph() -> Vec<GraphTriple> {
373 vec![
374 GraphTriple::new("A", "parent", "B"),
376 GraphTriple::new("B", "parent", "C"),
377 GraphTriple::new("C", "parent", "D"),
378 GraphTriple::new("A", "label", "Alice"),
380 GraphTriple::new("B", "label", "Bob"),
381 GraphTriple::new("C", "label", "Carol"),
382 GraphTriple::new("A", "sibling", "E"),
384 GraphTriple::new("D", "parent", "A"),
386 ]
387 }
388
389 #[test]
392 fn test_predicate_path_basic() {
393 let triples = build_graph();
394 let graph = RdfGraph::new(&triples);
395 let path = PropertyPath::predicate("parent");
396 let result = PathExecutor::evaluate(&path, "A", &graph);
397 assert!(result.contains("B"), "A parent B");
398 assert!(!result.contains("C"), "A does not directly parent C");
399 }
400
401 #[test]
402 fn test_predicate_path_no_match() {
403 let triples = build_graph();
404 let graph = RdfGraph::new(&triples);
405 let path = PropertyPath::predicate("nonexistent");
406 let result = PathExecutor::evaluate(&path, "A", &graph);
407 assert!(result.is_empty());
408 }
409
410 #[test]
413 fn test_inverse_path() {
414 let triples = build_graph();
415 let graph = RdfGraph::new(&triples);
416 let path = PropertyPath::inverse(PropertyPath::predicate("parent"));
418 let result = PathExecutor::evaluate(&path, "B", &graph);
419 assert!(result.contains("A"), "^parent from B gives A");
420 }
421
422 #[test]
423 fn test_inverse_path_chain() {
424 let triples = build_graph();
425 let graph = RdfGraph::new(&triples);
426 let path = PropertyPath::inverse(PropertyPath::predicate("parent"));
428 let result = PathExecutor::evaluate(&path, "C", &graph);
429 assert!(result.contains("B"));
430 }
431
432 #[test]
435 fn test_sequence_path_two_hops() {
436 let triples = build_graph();
437 let graph = RdfGraph::new(&triples);
438 let path = PropertyPath::sequence(vec![
440 PropertyPath::predicate("parent"),
441 PropertyPath::predicate("parent"),
442 ]);
443 let result = PathExecutor::evaluate(&path, "A", &graph);
444 assert!(result.contains("C"), "A parent/parent C");
445 assert!(!result.contains("B"), "B not in two-hop result");
446 }
447
448 #[test]
449 fn test_sequence_path_three_hops() {
450 let triples = build_graph();
451 let graph = RdfGraph::new(&triples);
452 let path = PropertyPath::sequence(vec![
453 PropertyPath::predicate("parent"),
454 PropertyPath::predicate("parent"),
455 PropertyPath::predicate("parent"),
456 ]);
457 let result = PathExecutor::evaluate(&path, "A", &graph);
458 assert!(result.contains("D"), "A →³ D");
459 }
460
461 #[test]
464 fn test_alternative_path() {
465 let triples = build_graph();
466 let graph = RdfGraph::new(&triples);
467 let path = PropertyPath::alternative(vec![
469 PropertyPath::predicate("parent"),
470 PropertyPath::predicate("sibling"),
471 ]);
472 let result = PathExecutor::evaluate(&path, "A", &graph);
473 assert!(result.contains("B"), "parent gives B");
474 assert!(result.contains("E"), "sibling gives E");
475 }
476
477 #[test]
478 fn test_alternative_path_deduplication() {
479 let triples = vec![
481 GraphTriple::new("X", "rel1", "Y"),
482 GraphTriple::new("X", "rel2", "Y"),
483 ];
484 let graph = RdfGraph::new(&triples);
485 let path = PropertyPath::alternative(vec![
486 PropertyPath::predicate("rel1"),
487 PropertyPath::predicate("rel2"),
488 ]);
489 let result = PathExecutor::evaluate(&path, "X", &graph);
490 assert_eq!(result.len(), 1);
491 assert!(result.contains("Y"));
492 }
493
494 #[test]
497 fn test_zero_or_more_includes_start() {
498 let triples = build_graph();
499 let graph = RdfGraph::new(&triples);
500 let path = PropertyPath::zero_or_more(PropertyPath::predicate("parent"));
501 let result = PathExecutor::evaluate(&path, "A", &graph);
502 assert!(result.contains("A"), "start node included");
504 assert!(result.contains("B"));
505 assert!(result.contains("C"));
506 assert!(result.contains("D"));
507 }
508
509 #[test]
510 fn test_zero_or_more_cycle_prevention() {
511 let triples = build_graph();
513 let graph = RdfGraph::new(&triples);
514 let path = PropertyPath::zero_or_more(PropertyPath::predicate("parent"));
515 let result = PathExecutor::evaluate(&path, "A", &graph);
517 assert!(result.len() >= 4, "all nodes reachable: {:?}", result);
518 }
519
520 #[test]
523 fn test_one_or_more_excludes_start() {
524 let triples = build_graph();
525 let graph = RdfGraph::new(&triples);
526 let path = PropertyPath::one_or_more(PropertyPath::predicate("parent"));
527 let result = PathExecutor::evaluate(&path, "A", &graph);
528 assert!(result.contains("B"), "B reachable in one step");
531 assert!(result.contains("C"), "C reachable in two steps");
532 assert!(result.contains("D"), "D reachable in three steps");
533 }
534
535 #[test]
536 fn test_one_or_more_no_successors() {
537 let triples = vec![GraphTriple::new("X", "p", "Y")];
538 let graph = RdfGraph::new(&triples);
539 let path = PropertyPath::one_or_more(PropertyPath::predicate("p"));
541 let result = PathExecutor::evaluate(&path, "Y", &graph);
542 assert!(result.is_empty());
543 }
544
545 #[test]
548 fn test_zero_or_one_includes_start_and_successor() {
549 let triples = vec![GraphTriple::new("X", "rel", "Y")];
550 let graph = RdfGraph::new(&triples);
551 let path = PropertyPath::zero_or_one(PropertyPath::predicate("rel"));
552 let result = PathExecutor::evaluate(&path, "X", &graph);
553 assert!(result.contains("X"), "zero steps → start");
554 assert!(result.contains("Y"), "one step → Y");
555 }
556
557 #[test]
558 fn test_zero_or_one_no_successors() {
559 let triples: Vec<GraphTriple> = vec![];
560 let graph = RdfGraph::new(&triples);
561 let path = PropertyPath::zero_or_one(PropertyPath::predicate("rel"));
562 let result = PathExecutor::evaluate(&path, "X", &graph);
563 assert_eq!(result.len(), 1);
565 assert!(result.contains("X"));
566 }
567
568 #[test]
571 fn test_has_any_true() {
572 let triples = build_graph();
573 let graph = RdfGraph::new(&triples);
574 assert!(PathExecutor::has_any(
575 &PropertyPath::predicate("parent"),
576 "A",
577 &graph
578 ));
579 }
580
581 #[test]
582 fn test_has_any_false() {
583 let triples: Vec<GraphTriple> = vec![];
584 let graph = RdfGraph::new(&triples);
585 assert!(!PathExecutor::has_any(
586 &PropertyPath::predicate("anything"),
587 "X",
588 &graph
589 ));
590 }
591
592 #[test]
593 fn test_reaches_target() {
594 let triples = build_graph();
595 let graph = RdfGraph::new(&triples);
596 let path = PropertyPath::zero_or_more(PropertyPath::predicate("parent"));
597 assert!(PathExecutor::reaches(&path, "A", "D", &graph));
598 }
599
600 #[test]
601 fn test_reaches_target_false() {
602 let triples = build_graph();
603 let graph = RdfGraph::new(&triples);
604 let path = PropertyPath::predicate("parent");
605 assert!(!PathExecutor::reaches(&path, "A", "D", &graph));
607 }
608
609 #[test]
612 fn test_steps_taken_nonzero_for_traversal() {
613 let triples = build_graph();
614 let graph = RdfGraph::new(&triples);
615 let path = PropertyPath::zero_or_more(PropertyPath::predicate("parent"));
616 let result = PathExecutor::evaluate(&path, "A", &graph);
617 assert!(result.steps_taken > 0, "steps_taken={}", result.steps_taken);
619 }
620
621 #[test]
624 fn test_sequence_then_alternative() {
625 let triples = vec![
626 GraphTriple::new("S", "hop", "M"),
627 GraphTriple::new("M", "a", "X"),
628 GraphTriple::new("M", "b", "Y"),
629 ];
630 let graph = RdfGraph::new(&triples);
631 let path = PropertyPath::sequence(vec![
633 PropertyPath::predicate("hop"),
634 PropertyPath::alternative(vec![
635 PropertyPath::predicate("a"),
636 PropertyPath::predicate("b"),
637 ]),
638 ]);
639 let result = PathExecutor::evaluate(&path, "S", &graph);
640 assert!(result.contains("X"));
641 assert!(result.contains("Y"));
642 }
643
644 #[test]
645 fn test_inverse_then_zero_or_more() {
646 let triples = vec![
648 GraphTriple::new("A", "child", "B"),
649 GraphTriple::new("B", "child", "C"),
650 ];
651 let graph = RdfGraph::new(&triples);
652 let path =
654 PropertyPath::zero_or_more(PropertyPath::inverse(PropertyPath::predicate("child")));
655 let result = PathExecutor::evaluate(&path, "B", &graph);
656 assert!(result.contains("B"), "start included");
657 assert!(result.contains("A"), "A reaches B via child");
658 }
659
660 #[test]
663 fn test_path_result_len_and_is_empty() {
664 let r = PathResult {
665 nodes: vec!["X".into(), "Y".into()],
666 steps_taken: 2,
667 };
668 assert_eq!(r.len(), 2);
669 assert!(!r.is_empty());
670
671 let empty = PathResult {
672 nodes: vec![],
673 steps_taken: 0,
674 };
675 assert!(empty.is_empty());
676 assert_eq!(empty.len(), 0);
677 }
678
679 #[test]
680 fn test_path_result_contains() {
681 let r = PathResult {
682 nodes: vec!["Alpha".into(), "Beta".into()],
683 steps_taken: 2,
684 };
685 assert!(r.contains("Alpha"));
686 assert!(!r.contains("Gamma"));
687 }
688
689 #[test]
692 fn test_graph_triple_new() {
693 let t = GraphTriple::new("s", "p", "o");
694 assert_eq!(t.subject, "s");
695 assert_eq!(t.predicate, "p");
696 assert_eq!(t.object, "o");
697 }
698
699 #[test]
700 fn test_rdf_graph_objects_of() {
701 let triples = vec![
702 GraphTriple::new("A", "p", "B"),
703 GraphTriple::new("A", "p", "C"),
704 GraphTriple::new("B", "p", "D"),
705 ];
706 let graph = RdfGraph::new(&triples);
707 let objs = graph.objects_of("A", "p");
708 assert_eq!(objs.len(), 2);
709 assert!(objs.contains(&"B".to_owned()));
710 assert!(objs.contains(&"C".to_owned()));
711 }
712
713 #[test]
714 fn test_rdf_graph_subjects_of() {
715 let triples = vec![
716 GraphTriple::new("A", "p", "Z"),
717 GraphTriple::new("B", "p", "Z"),
718 ];
719 let graph = RdfGraph::new(&triples);
720 let subs = graph.subjects_of("p", "Z");
721 assert_eq!(subs.len(), 2);
722 assert!(subs.contains(&"A".to_owned()));
723 assert!(subs.contains(&"B".to_owned()));
724 }
725
726 #[test]
729 fn test_predicate_path_multiple_objects() {
730 let triples = vec![
731 GraphTriple::new("X", "rel", "Y1"),
732 GraphTriple::new("X", "rel", "Y2"),
733 GraphTriple::new("X", "rel", "Y3"),
734 ];
735 let graph = RdfGraph::new(&triples);
736 let path = PropertyPath::predicate("rel");
737 let result = PathExecutor::evaluate(&path, "X", &graph);
738 assert_eq!(result.len(), 3);
739 }
740
741 #[test]
742 fn test_zero_or_more_single_hop() {
743 let triples = vec![GraphTriple::new("A", "p", "B")];
744 let graph = RdfGraph::new(&triples);
745 let path = PropertyPath::zero_or_more(PropertyPath::predicate("p"));
746 let result = PathExecutor::evaluate(&path, "A", &graph);
747 assert!(result.contains("A"));
748 assert!(result.contains("B"));
749 }
750
751 #[test]
752 fn test_one_or_more_single_hop() {
753 let triples = vec![GraphTriple::new("A", "p", "B")];
754 let graph = RdfGraph::new(&triples);
755 let path = PropertyPath::one_or_more(PropertyPath::predicate("p"));
756 let result = PathExecutor::evaluate(&path, "A", &graph);
757 assert!(result.contains("B"));
758 assert!(
759 !result.contains("A"),
760 "start excluded for one-or-more unless reachable via cycle"
761 );
762 }
763
764 #[test]
765 fn test_zero_or_one_at_most_one_step() {
766 let triples = vec![
767 GraphTriple::new("A", "p", "B"),
768 GraphTriple::new("B", "p", "C"),
769 ];
770 let graph = RdfGraph::new(&triples);
771 let path = PropertyPath::zero_or_one(PropertyPath::predicate("p"));
772 let result = PathExecutor::evaluate(&path, "A", &graph);
773 assert!(result.contains("A"));
775 assert!(result.contains("B"));
776 assert!(!result.contains("C"));
777 }
778
779 #[test]
780 fn test_alternative_path_single_element() {
781 let triples = vec![GraphTriple::new("X", "q", "Y")];
782 let graph = RdfGraph::new(&triples);
783 let path = PropertyPath::alternative(vec![PropertyPath::predicate("q")]);
784 let result = PathExecutor::evaluate(&path, "X", &graph);
785 assert!(result.contains("Y"));
786 }
787
788 #[test]
789 fn test_sequence_path_single_segment() {
790 let triples = vec![GraphTriple::new("X", "rel", "Y")];
791 let graph = RdfGraph::new(&triples);
792 let path = PropertyPath::sequence(vec![PropertyPath::predicate("rel")]);
793 let result = PathExecutor::evaluate(&path, "X", &graph);
794 assert!(result.contains("Y"));
795 }
796
797 #[test]
798 fn test_empty_graph_always_empty_result() {
799 let triples: Vec<GraphTriple> = vec![];
800 let graph = RdfGraph::new(&triples);
801 let path = PropertyPath::one_or_more(PropertyPath::predicate("p"));
802 let result = PathExecutor::evaluate(&path, "X", &graph);
803 assert!(result.is_empty());
804 }
805
806 #[test]
807 fn test_inverse_path_no_subjects() {
808 let triples = vec![GraphTriple::new("A", "p", "B")];
809 let graph = RdfGraph::new(&triples);
810 let path = PropertyPath::inverse(PropertyPath::predicate("p"));
812 let result = PathExecutor::evaluate(&path, "A", &graph);
813 assert!(result.is_empty());
814 }
815
816 #[test]
817 fn test_zero_or_more_deep_chain() {
818 let triples = vec![
820 GraphTriple::new("A", "next", "B"),
821 GraphTriple::new("B", "next", "C"),
822 GraphTriple::new("C", "next", "D"),
823 GraphTriple::new("D", "next", "E"),
824 GraphTriple::new("E", "next", "F"),
825 ];
826 let graph = RdfGraph::new(&triples);
827 let path = PropertyPath::zero_or_more(PropertyPath::predicate("next"));
828 let result = PathExecutor::evaluate(&path, "A", &graph);
829 assert_eq!(result.len(), 6, "all 6 nodes reachable: {:?}", result);
830 }
831
832 #[test]
833 fn test_property_path_equality() {
834 let p1 = PropertyPath::predicate("http://example.org/p");
835 let p2 = PropertyPath::predicate("http://example.org/p");
836 assert_eq!(p1, p2);
837 }
838
839 #[test]
840 fn test_property_path_inverse_equality() {
841 let p = PropertyPath::inverse(PropertyPath::predicate("p"));
842 let q = PropertyPath::inverse(PropertyPath::predicate("p"));
843 assert_eq!(p, q);
844 }
845
846 #[test]
847 fn test_graph_triple_hash_equality() {
848 use std::collections::HashSet;
849 let mut set = HashSet::new();
850 set.insert(GraphTriple::new("s", "p", "o"));
851 set.insert(GraphTriple::new("s", "p", "o")); assert_eq!(set.len(), 1);
853 }
854
855 #[test]
856 fn test_sequence_empty_gives_start_node() {
857 let triples: Vec<GraphTriple> = vec![];
858 let graph = RdfGraph::new(&triples);
859 let path = PropertyPath::sequence(vec![]);
860 let result = PathExecutor::evaluate(&path, "S", &graph);
862 assert!(result.contains("S"));
864 }
865
866 #[test]
867 fn test_alternative_empty_gives_empty() {
868 let triples: Vec<GraphTriple> = vec![];
869 let graph = RdfGraph::new(&triples);
870 let path = PropertyPath::alternative(vec![PropertyPath::predicate("p")]);
871 let result = PathExecutor::evaluate(&path, "X", &graph);
872 assert!(result.is_empty());
873 }
874
875 #[test]
876 fn test_reaches_self_via_zero_or_more() {
877 let triples: Vec<GraphTriple> = vec![];
878 let graph = RdfGraph::new(&triples);
879 let path = PropertyPath::zero_or_more(PropertyPath::predicate("any"));
880 assert!(PathExecutor::reaches(&path, "X", "X", &graph));
882 }
883
884 #[test]
885 fn test_steps_taken_zero_for_empty_predicate() {
886 let triples: Vec<GraphTriple> = vec![];
887 let graph = RdfGraph::new(&triples);
888 let path = PropertyPath::predicate("p");
889 let result = PathExecutor::evaluate(&path, "X", &graph);
890 assert_eq!(result.steps_taken, 0);
891 }
892}