Skip to main content

tensorlogic_oxirs_bridge/
property_path.rs

1//! SPARQL 1.1 property path query support.
2//!
3//! Implements property path expressions for traversing RDF graphs with
4//! sequence, alternative, closure (zero-or-more, one-or-more), and
5//! inverse path patterns.
6
7use serde::{Deserialize, Serialize};
8use std::collections::HashSet;
9use thiserror::Error;
10
11/// Errors from property path evaluation.
12#[derive(Debug, Error)]
13pub enum PathError {
14    #[error("Max depth {0} exceeded for closure path")]
15    MaxDepthExceeded(usize),
16    #[error("Empty path")]
17    EmptyPath,
18    #[error("Invalid path: {0}")]
19    InvalidPath(String),
20}
21
22/// A SPARQL 1.1 property path expression.
23#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
24pub enum PropertyPath {
25    /// A simple IRI predicate: `foaf:knows`
26    Iri(String),
27    /// Sequence path: `a / b` (a then b)
28    Sequence(Box<PropertyPath>, Box<PropertyPath>),
29    /// Alternative path: `a | b` (a or b)
30    Alternative(Box<PropertyPath>, Box<PropertyPath>),
31    /// Zero or more: `a*`
32    ZeroOrMore(Box<PropertyPath>),
33    /// One or more: `a+`
34    OneOrMore(Box<PropertyPath>),
35    /// Zero or one: `a?`
36    ZeroOrOne(Box<PropertyPath>),
37    /// Inverse path: `^a` (reverse direction)
38    Inverse(Box<PropertyPath>),
39}
40
41impl PropertyPath {
42    /// Create a simple IRI property path.
43    pub fn iri(s: impl Into<String>) -> Self {
44        PropertyPath::Iri(s.into())
45    }
46
47    /// Create a sequence path: `a / b`.
48    pub fn seq(a: PropertyPath, b: PropertyPath) -> Self {
49        PropertyPath::Sequence(Box::new(a), Box::new(b))
50    }
51
52    /// Create an alternative path: `a | b`.
53    pub fn alt(a: PropertyPath, b: PropertyPath) -> Self {
54        PropertyPath::Alternative(Box::new(a), Box::new(b))
55    }
56
57    /// Create a zero-or-more closure path: `a*`.
58    pub fn zero_or_more(p: PropertyPath) -> Self {
59        PropertyPath::ZeroOrMore(Box::new(p))
60    }
61
62    /// Create a one-or-more closure path: `a+`.
63    pub fn one_or_more(p: PropertyPath) -> Self {
64        PropertyPath::OneOrMore(Box::new(p))
65    }
66
67    /// Create a zero-or-one optional path: `a?`.
68    pub fn zero_or_one(p: PropertyPath) -> Self {
69        PropertyPath::ZeroOrOne(Box::new(p))
70    }
71
72    /// Create an inverse path: `^a`.
73    pub fn inverse(p: PropertyPath) -> Self {
74        PropertyPath::Inverse(Box::new(p))
75    }
76
77    /// Nesting depth of the path expression.
78    pub fn depth(&self) -> usize {
79        match self {
80            PropertyPath::Iri(_) => 1,
81            PropertyPath::Sequence(a, b) | PropertyPath::Alternative(a, b) => {
82                1 + a.depth().max(b.depth())
83            }
84            PropertyPath::ZeroOrMore(p)
85            | PropertyPath::OneOrMore(p)
86            | PropertyPath::ZeroOrOne(p)
87            | PropertyPath::Inverse(p) => 1 + p.depth(),
88        }
89    }
90
91    /// Whether this path contains a closure operator (`*` or `+`).
92    pub fn contains_closure(&self) -> bool {
93        match self {
94            PropertyPath::ZeroOrMore(_) | PropertyPath::OneOrMore(_) => true,
95            PropertyPath::Sequence(a, b) | PropertyPath::Alternative(a, b) => {
96                a.contains_closure() || b.contains_closure()
97            }
98            PropertyPath::ZeroOrOne(p) | PropertyPath::Inverse(p) => p.contains_closure(),
99            PropertyPath::Iri(_) => false,
100        }
101    }
102
103    /// Whether this path is a simple IRI (no operators).
104    pub fn is_simple(&self) -> bool {
105        matches!(self, PropertyPath::Iri(_))
106    }
107}
108
109impl std::fmt::Display for PropertyPath {
110    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
111        match self {
112            PropertyPath::Iri(s) => write!(f, "{}", s),
113            PropertyPath::Sequence(a, b) => write!(f, "{}/{}", a, b),
114            PropertyPath::Alternative(a, b) => write!(f, "{}|{}", a, b),
115            PropertyPath::ZeroOrMore(p) => write!(f, "{}*", p),
116            PropertyPath::OneOrMore(p) => write!(f, "{}+", p),
117            PropertyPath::ZeroOrOne(p) => write!(f, "{}?", p),
118            PropertyPath::Inverse(p) => write!(f, "^{}", p),
119        }
120    }
121}
122
123/// Simple in-memory triple store for property path evaluation.
124#[derive(Debug, Clone, Default)]
125pub struct TripleStore {
126    triples: Vec<(String, String, String)>,
127}
128
129impl TripleStore {
130    /// Create an empty triple store.
131    pub fn new() -> Self {
132        Self::default()
133    }
134
135    /// Add a triple (subject, predicate, object) to the store.
136    pub fn add(
137        &mut self,
138        subject: impl Into<String>,
139        predicate: impl Into<String>,
140        object: impl Into<String>,
141    ) {
142        self.triples
143            .push((subject.into(), predicate.into(), object.into()));
144    }
145
146    /// Get all (subject, object) pairs for a given predicate.
147    pub fn triples_with_predicate(&self, pred: &str) -> Vec<(&str, &str)> {
148        self.triples
149            .iter()
150            .filter(|(_, p, _)| p == pred)
151            .map(|(s, _, o)| (s.as_str(), o.as_str()))
152            .collect()
153    }
154
155    /// Get all objects reachable from a subject via a predicate.
156    pub fn objects_from(&self, subject: &str, predicate: &str) -> Vec<&str> {
157        self.triples
158            .iter()
159            .filter(|(s, p, _)| s == subject && p == predicate)
160            .map(|(_, _, o)| o.as_str())
161            .collect()
162    }
163
164    /// Get all subjects that reach an object via a predicate (for inverse).
165    pub fn subjects_to(&self, object: &str, predicate: &str) -> Vec<&str> {
166        self.triples
167            .iter()
168            .filter(|(_, p, o)| o == object && p == predicate)
169            .map(|(s, _, _)| s.as_str())
170            .collect()
171    }
172
173    /// Number of triples in the store.
174    pub fn len(&self) -> usize {
175        self.triples.len()
176    }
177
178    /// Whether the store is empty.
179    pub fn is_empty(&self) -> bool {
180        self.triples.is_empty()
181    }
182
183    /// All unique subjects in the store.
184    pub fn all_subjects(&self) -> HashSet<&str> {
185        self.triples.iter().map(|(s, _, _)| s.as_str()).collect()
186    }
187
188    /// All unique nodes (subjects and objects) in the store.
189    pub fn all_nodes(&self) -> HashSet<&str> {
190        self.triples
191            .iter()
192            .flat_map(|(s, _, o)| [s.as_str(), o.as_str()])
193            .collect()
194    }
195}
196
197/// Expands property path expressions against a triple store.
198pub struct PropertyPathExpander {
199    max_depth: usize,
200}
201
202impl PropertyPathExpander {
203    /// Create a new expander with the given maximum recursion depth.
204    pub fn new(max_depth: usize) -> Self {
205        PropertyPathExpander {
206            max_depth: max_depth.max(1),
207        }
208    }
209
210    /// Expand a property path from a starting node, returning all reachable objects.
211    pub fn expand(
212        &self,
213        start: &str,
214        path: &PropertyPath,
215        store: &TripleStore,
216    ) -> Result<Vec<String>, PathError> {
217        self.expand_inner(start, path, store, 0)
218    }
219
220    fn expand_inner(
221        &self,
222        start: &str,
223        path: &PropertyPath,
224        store: &TripleStore,
225        depth: usize,
226    ) -> Result<Vec<String>, PathError> {
227        if depth > self.max_depth {
228            return Err(PathError::MaxDepthExceeded(self.max_depth));
229        }
230        match path {
231            PropertyPath::Iri(pred) => Ok(store
232                .objects_from(start, pred)
233                .into_iter()
234                .map(String::from)
235                .collect()),
236            PropertyPath::Inverse(inner) => self.expand_inverse(start, inner, store, depth),
237            PropertyPath::Sequence(a, b) => {
238                let intermediate = self.expand_inner(start, a, store, depth + 1)?;
239                let mut results = Vec::new();
240                for mid in &intermediate {
241                    let next = self.expand_inner(mid, b, store, depth + 1)?;
242                    results.extend(next);
243                }
244                Ok(results)
245            }
246            PropertyPath::Alternative(a, b) => {
247                let mut results = self.expand_inner(start, a, store, depth + 1)?;
248                results.extend(self.expand_inner(start, b, store, depth + 1)?);
249                // Deduplicate while preserving deterministic order
250                let mut seen = HashSet::new();
251                results.retain(|item| seen.insert(item.clone()));
252                Ok(results)
253            }
254            PropertyPath::ZeroOrMore(inner) => {
255                let mut visited = HashSet::new();
256                visited.insert(start.to_string()); // zero hops: include start
257                self.closure(start, inner, store, &mut visited, depth)?;
258                let mut result: Vec<String> = visited.into_iter().collect();
259                result.sort();
260                Ok(result)
261            }
262            PropertyPath::OneOrMore(inner) => {
263                let mut visited = HashSet::new();
264                // One or more: start NOT included unless reachable via cycle
265                let first = self.expand_inner(start, inner, store, depth + 1)?;
266                for node in &first {
267                    if visited.insert(node.clone()) {
268                        self.closure(node, inner, store, &mut visited, depth)?;
269                    }
270                }
271                let mut result: Vec<String> = visited.into_iter().collect();
272                result.sort();
273                Ok(result)
274            }
275            PropertyPath::ZeroOrOne(inner) => {
276                let mut results = HashSet::new();
277                results.insert(start.to_string()); // zero hops
278                for obj in self.expand_inner(start, inner, store, depth + 1)? {
279                    results.insert(obj);
280                }
281                let mut result: Vec<String> = results.into_iter().collect();
282                result.sort();
283                Ok(result)
284            }
285        }
286    }
287
288    fn expand_inverse(
289        &self,
290        start: &str,
291        inner: &PropertyPath,
292        store: &TripleStore,
293        depth: usize,
294    ) -> Result<Vec<String>, PathError> {
295        match inner {
296            PropertyPath::Iri(pred) => Ok(store
297                .subjects_to(start, pred)
298                .into_iter()
299                .map(String::from)
300                .collect()),
301            _ => {
302                // For complex inverse paths, check all nodes as potential sources
303                let mut results = Vec::new();
304                for subj in store.all_nodes() {
305                    let reachable = self.expand_inner(subj, inner, store, depth + 1)?;
306                    if reachable.iter().any(|r| r == start) {
307                        results.push(subj.to_string());
308                    }
309                }
310                results.sort();
311                results.dedup();
312                Ok(results)
313            }
314        }
315    }
316
317    /// Fixed-point closure: keep expanding until no new nodes are found.
318    fn closure(
319        &self,
320        start: &str,
321        path: &PropertyPath,
322        store: &TripleStore,
323        visited: &mut HashSet<String>,
324        depth: usize,
325    ) -> Result<(), PathError> {
326        if depth > self.max_depth {
327            return Err(PathError::MaxDepthExceeded(self.max_depth));
328        }
329        let next = self.expand_inner(start, path, store, depth + 1)?;
330        for node in next {
331            if visited.insert(node.clone()) {
332                // New node discovered, recurse
333                self.closure(&node, path, store, visited, depth + 1)?;
334            }
335        }
336        Ok(())
337    }
338
339    /// Expand a property path for ALL subjects, returning (subject, object) pairs.
340    pub fn expand_all(
341        &self,
342        path: &PropertyPath,
343        store: &TripleStore,
344    ) -> Result<Vec<(String, String)>, PathError> {
345        let subjects = store.all_subjects();
346        let mut results = Vec::new();
347        for subj in subjects {
348            let objects = self.expand(subj, path, store)?;
349            for obj in objects {
350                results.push((subj.to_string(), obj));
351            }
352        }
353        results.sort();
354        results.dedup();
355        Ok(results)
356    }
357}
358
359#[cfg(test)]
360mod tests {
361    use super::*;
362
363    #[test]
364    fn test_path_iri_construction() {
365        let path = PropertyPath::iri("knows");
366        assert_eq!(path, PropertyPath::Iri("knows".to_string()));
367    }
368
369    #[test]
370    fn test_path_display() {
371        let seq = PropertyPath::seq(PropertyPath::iri("a"), PropertyPath::iri("b"));
372        assert_eq!(format!("{}", seq), "a/b");
373
374        let alt = PropertyPath::alt(PropertyPath::iri("a"), PropertyPath::iri("b"));
375        assert_eq!(format!("{}", alt), "a|b");
376
377        let star = PropertyPath::zero_or_more(PropertyPath::iri("a"));
378        assert_eq!(format!("{}", star), "a*");
379
380        let plus = PropertyPath::one_or_more(PropertyPath::iri("a"));
381        assert_eq!(format!("{}", plus), "a+");
382
383        let opt = PropertyPath::zero_or_one(PropertyPath::iri("a"));
384        assert_eq!(format!("{}", opt), "a?");
385
386        let inv = PropertyPath::inverse(PropertyPath::iri("a"));
387        assert_eq!(format!("{}", inv), "^a");
388    }
389
390    #[test]
391    fn test_path_depth_simple() {
392        let path = PropertyPath::iri("knows");
393        assert_eq!(path.depth(), 1);
394    }
395
396    #[test]
397    fn test_path_depth_nested() {
398        // seq(iri, seq(iri, iri)) => depth 3
399        let inner = PropertyPath::seq(PropertyPath::iri("b"), PropertyPath::iri("c"));
400        let path = PropertyPath::seq(PropertyPath::iri("a"), inner);
401        assert_eq!(path.depth(), 3);
402    }
403
404    #[test]
405    fn test_path_contains_closure_true() {
406        let path = PropertyPath::zero_or_more(PropertyPath::iri("knows"));
407        assert!(path.contains_closure());
408
409        let path2 = PropertyPath::one_or_more(PropertyPath::iri("knows"));
410        assert!(path2.contains_closure());
411
412        // Nested inside a sequence
413        let path3 = PropertyPath::seq(
414            PropertyPath::iri("a"),
415            PropertyPath::zero_or_more(PropertyPath::iri("b")),
416        );
417        assert!(path3.contains_closure());
418    }
419
420    #[test]
421    fn test_path_contains_closure_false() {
422        let path = PropertyPath::iri("knows");
423        assert!(!path.contains_closure());
424
425        let path2 = PropertyPath::seq(PropertyPath::iri("a"), PropertyPath::iri("b"));
426        assert!(!path2.contains_closure());
427
428        let path3 = PropertyPath::zero_or_one(PropertyPath::iri("a"));
429        assert!(!path3.contains_closure());
430    }
431
432    #[test]
433    fn test_path_is_simple() {
434        assert!(PropertyPath::iri("knows").is_simple());
435        assert!(!PropertyPath::seq(PropertyPath::iri("a"), PropertyPath::iri("b")).is_simple());
436        assert!(!PropertyPath::zero_or_more(PropertyPath::iri("a")).is_simple());
437    }
438
439    #[test]
440    fn test_triple_store_add_and_len() {
441        let mut store = TripleStore::new();
442        assert!(store.is_empty());
443        store.add("A", "knows", "B");
444        store.add("B", "knows", "C");
445        store.add("A", "likes", "D");
446        assert_eq!(store.len(), 3);
447        assert!(!store.is_empty());
448    }
449
450    #[test]
451    fn test_triple_store_predicate_filter() {
452        let mut store = TripleStore::new();
453        store.add("A", "knows", "B");
454        store.add("B", "knows", "C");
455        store.add("A", "likes", "D");
456
457        let knows = store.triples_with_predicate("knows");
458        assert_eq!(knows.len(), 2);
459
460        let likes = store.triples_with_predicate("likes");
461        assert_eq!(likes.len(), 1);
462        assert_eq!(likes[0], ("A", "D"));
463    }
464
465    #[test]
466    fn test_expander_simple_iri() {
467        let mut store = TripleStore::new();
468        store.add("A", "knows", "B");
469        store.add("A", "knows", "C");
470
471        let expander = PropertyPathExpander::new(10);
472        let result = expander
473            .expand("A", &PropertyPath::iri("knows"), &store)
474            .expect("expansion should succeed");
475        assert_eq!(result.len(), 2);
476        assert!(result.contains(&"B".to_string()));
477        assert!(result.contains(&"C".to_string()));
478    }
479
480    #[test]
481    fn test_expander_sequence() {
482        let mut store = TripleStore::new();
483        store.add("A", "knows", "B");
484        store.add("B", "likes", "C");
485
486        let expander = PropertyPathExpander::new(10);
487        let path = PropertyPath::seq(PropertyPath::iri("knows"), PropertyPath::iri("likes"));
488        let result = expander
489            .expand("A", &path, &store)
490            .expect("expansion should succeed");
491        assert_eq!(result, vec!["C".to_string()]);
492    }
493
494    #[test]
495    fn test_expander_alternative() {
496        let mut store = TripleStore::new();
497        store.add("A", "knows", "B");
498        store.add("A", "likes", "C");
499
500        let expander = PropertyPathExpander::new(10);
501        let path = PropertyPath::alt(PropertyPath::iri("knows"), PropertyPath::iri("likes"));
502        let mut result = expander
503            .expand("A", &path, &store)
504            .expect("expansion should succeed");
505        result.sort();
506        assert_eq!(result, vec!["B".to_string(), "C".to_string()]);
507    }
508
509    #[test]
510    fn test_expander_one_or_more() {
511        let mut store = TripleStore::new();
512        store.add("A", "knows", "B");
513        store.add("B", "knows", "C");
514        store.add("C", "knows", "D");
515
516        let expander = PropertyPathExpander::new(20);
517        let path = PropertyPath::one_or_more(PropertyPath::iri("knows"));
518        let mut result = expander
519            .expand("A", &path, &store)
520            .expect("expansion should succeed");
521        result.sort();
522        assert_eq!(
523            result,
524            vec!["B".to_string(), "C".to_string(), "D".to_string()]
525        );
526        // Start node A should NOT be included (no cycle back)
527        assert!(!result.contains(&"A".to_string()));
528    }
529
530    #[test]
531    fn test_expander_zero_or_more() {
532        let mut store = TripleStore::new();
533        store.add("A", "knows", "B");
534        store.add("B", "knows", "C");
535
536        let expander = PropertyPathExpander::new(20);
537        let path = PropertyPath::zero_or_more(PropertyPath::iri("knows"));
538        let mut result = expander
539            .expand("A", &path, &store)
540            .expect("expansion should succeed");
541        result.sort();
542        // Zero-or-more includes start node
543        assert_eq!(
544            result,
545            vec!["A".to_string(), "B".to_string(), "C".to_string()]
546        );
547    }
548
549    #[test]
550    fn test_expander_inverse() {
551        let mut store = TripleStore::new();
552        store.add("A", "knows", "B");
553        store.add("C", "knows", "B");
554
555        let expander = PropertyPathExpander::new(10);
556        let path = PropertyPath::inverse(PropertyPath::iri("knows"));
557        let mut result = expander
558            .expand("B", &path, &store)
559            .expect("expansion should succeed");
560        result.sort();
561        assert_eq!(result, vec!["A".to_string(), "C".to_string()]);
562    }
563
564    #[test]
565    fn test_expander_cycle_safe() {
566        let mut store = TripleStore::new();
567        store.add("A", "knows", "B");
568        store.add("B", "knows", "A");
569
570        let expander = PropertyPathExpander::new(50);
571        let path = PropertyPath::one_or_more(PropertyPath::iri("knows"));
572        let mut result = expander
573            .expand("A", &path, &store)
574            .expect("expansion should not infinite loop");
575        result.sort();
576        // A->B->A cycle: from A, one_or_more should find B, then from B find A
577        assert_eq!(result, vec!["A".to_string(), "B".to_string()]);
578    }
579}