1use serde::{Deserialize, Serialize};
8use std::collections::HashSet;
9use thiserror::Error;
10
11#[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#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
24pub enum PropertyPath {
25 Iri(String),
27 Sequence(Box<PropertyPath>, Box<PropertyPath>),
29 Alternative(Box<PropertyPath>, Box<PropertyPath>),
31 ZeroOrMore(Box<PropertyPath>),
33 OneOrMore(Box<PropertyPath>),
35 ZeroOrOne(Box<PropertyPath>),
37 Inverse(Box<PropertyPath>),
39}
40
41impl PropertyPath {
42 pub fn iri(s: impl Into<String>) -> Self {
44 PropertyPath::Iri(s.into())
45 }
46
47 pub fn seq(a: PropertyPath, b: PropertyPath) -> Self {
49 PropertyPath::Sequence(Box::new(a), Box::new(b))
50 }
51
52 pub fn alt(a: PropertyPath, b: PropertyPath) -> Self {
54 PropertyPath::Alternative(Box::new(a), Box::new(b))
55 }
56
57 pub fn zero_or_more(p: PropertyPath) -> Self {
59 PropertyPath::ZeroOrMore(Box::new(p))
60 }
61
62 pub fn one_or_more(p: PropertyPath) -> Self {
64 PropertyPath::OneOrMore(Box::new(p))
65 }
66
67 pub fn zero_or_one(p: PropertyPath) -> Self {
69 PropertyPath::ZeroOrOne(Box::new(p))
70 }
71
72 pub fn inverse(p: PropertyPath) -> Self {
74 PropertyPath::Inverse(Box::new(p))
75 }
76
77 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 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 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#[derive(Debug, Clone, Default)]
125pub struct TripleStore {
126 triples: Vec<(String, String, String)>,
127}
128
129impl TripleStore {
130 pub fn new() -> Self {
132 Self::default()
133 }
134
135 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 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 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 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 pub fn len(&self) -> usize {
175 self.triples.len()
176 }
177
178 pub fn is_empty(&self) -> bool {
180 self.triples.is_empty()
181 }
182
183 pub fn all_subjects(&self) -> HashSet<&str> {
185 self.triples.iter().map(|(s, _, _)| s.as_str()).collect()
186 }
187
188 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
197pub struct PropertyPathExpander {
199 max_depth: usize,
200}
201
202impl PropertyPathExpander {
203 pub fn new(max_depth: usize) -> Self {
205 PropertyPathExpander {
206 max_depth: max_depth.max(1),
207 }
208 }
209
210 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 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()); 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 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()); 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 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 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 self.closure(&node, path, store, visited, depth + 1)?;
334 }
335 }
336 Ok(())
337 }
338
339 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 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 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 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 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 assert_eq!(result, vec!["A".to_string(), "B".to_string()]);
578 }
579}