1use std::collections::BTreeSet;
8
9#[derive(Debug, Clone, PartialEq)]
15pub enum PathExpr {
16 Iri(String),
18 Inverse(Box<PathExpr>),
20 Sequence(Box<PathExpr>, Box<PathExpr>),
22 Alternative(Box<PathExpr>, Box<PathExpr>),
24 ZeroOrMore(Box<PathExpr>),
26 OneOrMore(Box<PathExpr>),
28 ZeroOrOne(Box<PathExpr>),
30 NegatedSet(Vec<String>),
32}
33
34impl PathExpr {
35 pub fn iri(iri: &str) -> Self {
37 PathExpr::Iri(iri.to_string())
38 }
39
40 pub fn inverse(inner: PathExpr) -> Self {
42 PathExpr::Inverse(Box::new(inner))
43 }
44
45 pub fn sequence(left: PathExpr, right: PathExpr) -> Self {
47 PathExpr::Sequence(Box::new(left), Box::new(right))
48 }
49
50 pub fn alternative(left: PathExpr, right: PathExpr) -> Self {
52 PathExpr::Alternative(Box::new(left), Box::new(right))
53 }
54
55 pub fn zero_or_more(inner: PathExpr) -> Self {
57 PathExpr::ZeroOrMore(Box::new(inner))
58 }
59
60 pub fn one_or_more(inner: PathExpr) -> Self {
62 PathExpr::OneOrMore(Box::new(inner))
63 }
64
65 pub fn zero_or_one(inner: PathExpr) -> Self {
67 PathExpr::ZeroOrOne(Box::new(inner))
68 }
69
70 pub fn negated_set(iris: &[&str]) -> Self {
74 PathExpr::NegatedSet(iris.iter().map(|s| s.to_string()).collect())
75 }
76
77 pub fn depth(&self) -> usize {
81 match self {
82 PathExpr::Iri(_) => 1,
83 PathExpr::NegatedSet(_) => 1,
84 PathExpr::Inverse(inner) => 1 + inner.depth(),
85 PathExpr::ZeroOrMore(inner) => 1 + inner.depth(),
86 PathExpr::OneOrMore(inner) => 1 + inner.depth(),
87 PathExpr::ZeroOrOne(inner) => 1 + inner.depth(),
88 PathExpr::Sequence(left, right) => 1 + left.depth().max(right.depth()),
89 PathExpr::Alternative(left, right) => 1 + left.depth().max(right.depth()),
90 }
91 }
92
93 pub fn iris(&self) -> Vec<String> {
95 let mut set = BTreeSet::new();
96 self.collect_iris(&mut set);
97 set.into_iter().collect()
98 }
99
100 fn collect_iris(&self, set: &mut BTreeSet<String>) {
102 match self {
103 PathExpr::Iri(iri) => {
104 set.insert(iri.clone());
105 }
106 PathExpr::NegatedSet(iris) => {
107 for iri in iris {
108 set.insert(iri.clone());
109 }
110 }
111 PathExpr::Inverse(inner) => inner.collect_iris(set),
112 PathExpr::ZeroOrMore(inner) => inner.collect_iris(set),
113 PathExpr::OneOrMore(inner) => inner.collect_iris(set),
114 PathExpr::ZeroOrOne(inner) => inner.collect_iris(set),
115 PathExpr::Sequence(left, right) => {
116 left.collect_iris(set);
117 right.collect_iris(set);
118 }
119 PathExpr::Alternative(left, right) => {
120 left.collect_iris(set);
121 right.collect_iris(set);
122 }
123 }
124 }
125
126 pub fn can_match_zero(&self) -> bool {
136 match self {
137 PathExpr::Iri(_) => false,
138 PathExpr::NegatedSet(_) => false,
139 PathExpr::ZeroOrMore(_) => true,
140 PathExpr::ZeroOrOne(_) => true,
141 PathExpr::OneOrMore(inner) => inner.can_match_zero(),
142 PathExpr::Inverse(inner) => inner.can_match_zero(),
143 PathExpr::Sequence(left, right) => left.can_match_zero() && right.can_match_zero(),
144 PathExpr::Alternative(left, right) => left.can_match_zero() || right.can_match_zero(),
145 }
146 }
147
148 pub fn to_sparql(&self) -> String {
152 match self {
153 PathExpr::Iri(iri) => iri.clone(),
154 PathExpr::Inverse(inner) => format!("^({})", inner.to_sparql()),
155 PathExpr::Sequence(left, right) => {
156 format!("({}/{})", left.to_sparql(), right.to_sparql())
157 }
158 PathExpr::Alternative(left, right) => {
159 format!("({}|{})", left.to_sparql(), right.to_sparql())
160 }
161 PathExpr::ZeroOrMore(inner) => format!("({})*", inner.to_sparql()),
162 PathExpr::OneOrMore(inner) => format!("({})+", inner.to_sparql()),
163 PathExpr::ZeroOrOne(inner) => format!("({})?", inner.to_sparql()),
164 PathExpr::NegatedSet(iris) => {
165 if iris.is_empty() {
166 "!()".to_string()
167 } else if iris.len() == 1 {
168 format!("!{}", iris[0])
169 } else {
170 format!("!({})", iris.join("|"))
171 }
172 }
173 }
174 }
175
176 pub fn is_simple_iri(&self) -> bool {
178 matches!(self, PathExpr::Iri(_))
179 }
180
181 pub fn as_iri(&self) -> Option<&str> {
183 if let PathExpr::Iri(iri) = self {
184 Some(iri.as_str())
185 } else {
186 None
187 }
188 }
189
190 pub fn iri_count(&self) -> usize {
192 match self {
193 PathExpr::Iri(_) => 1,
194 PathExpr::NegatedSet(iris) => iris.len(),
195 PathExpr::Inverse(inner) => inner.iri_count(),
196 PathExpr::ZeroOrMore(inner) => inner.iri_count(),
197 PathExpr::OneOrMore(inner) => inner.iri_count(),
198 PathExpr::ZeroOrOne(inner) => inner.iri_count(),
199 PathExpr::Sequence(left, right) => left.iri_count() + right.iri_count(),
200 PathExpr::Alternative(left, right) => left.iri_count() + right.iri_count(),
201 }
202 }
203}
204
205impl std::fmt::Display for PathExpr {
206 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
207 write!(f, "{}", self.to_sparql())
208 }
209}
210
211#[cfg(test)]
212mod tests {
213 use super::*;
214
215 #[test]
218 fn test_iri_constructor() {
219 let p = PathExpr::iri("http://example.org/p");
220 assert_eq!(p, PathExpr::Iri("http://example.org/p".to_string()));
221 }
222
223 #[test]
224 fn test_inverse_constructor() {
225 let p = PathExpr::inverse(PathExpr::iri(":p"));
226 assert!(matches!(p, PathExpr::Inverse(_)));
227 }
228
229 #[test]
230 fn test_sequence_constructor() {
231 let s = PathExpr::sequence(PathExpr::iri(":p"), PathExpr::iri(":q"));
232 assert!(matches!(s, PathExpr::Sequence(_, _)));
233 }
234
235 #[test]
236 fn test_alternative_constructor() {
237 let a = PathExpr::alternative(PathExpr::iri(":p"), PathExpr::iri(":q"));
238 assert!(matches!(a, PathExpr::Alternative(_, _)));
239 }
240
241 #[test]
242 fn test_zero_or_more_constructor() {
243 let z = PathExpr::zero_or_more(PathExpr::iri(":p"));
244 assert!(matches!(z, PathExpr::ZeroOrMore(_)));
245 }
246
247 #[test]
248 fn test_one_or_more_constructor() {
249 let o = PathExpr::one_or_more(PathExpr::iri(":p"));
250 assert!(matches!(o, PathExpr::OneOrMore(_)));
251 }
252
253 #[test]
254 fn test_zero_or_one_constructor() {
255 let z = PathExpr::zero_or_one(PathExpr::iri(":p"));
256 assert!(matches!(z, PathExpr::ZeroOrOne(_)));
257 }
258
259 #[test]
260 fn test_negated_set_constructor() {
261 let n = PathExpr::negated_set(&[":p", ":q"]);
262 assert!(matches!(n, PathExpr::NegatedSet(_)));
263 if let PathExpr::NegatedSet(iris) = &n {
264 assert_eq!(iris.len(), 2);
265 }
266 }
267
268 #[test]
269 fn test_negated_set_empty() {
270 let n = PathExpr::negated_set(&[]);
271 if let PathExpr::NegatedSet(iris) = &n {
272 assert!(iris.is_empty());
273 }
274 }
275
276 #[test]
277 fn test_negated_set_single() {
278 let n = PathExpr::negated_set(&[":p"]);
279 if let PathExpr::NegatedSet(iris) = &n {
280 assert_eq!(iris.len(), 1);
281 assert_eq!(iris[0], ":p");
282 }
283 }
284
285 #[test]
288 fn test_depth_iri() {
289 assert_eq!(PathExpr::iri(":p").depth(), 1);
290 }
291
292 #[test]
293 fn test_depth_negated_set() {
294 assert_eq!(PathExpr::negated_set(&[":p"]).depth(), 1);
295 }
296
297 #[test]
298 fn test_depth_inverse() {
299 let p = PathExpr::inverse(PathExpr::iri(":p"));
300 assert_eq!(p.depth(), 2);
301 }
302
303 #[test]
304 fn test_depth_zero_or_more() {
305 let p = PathExpr::zero_or_more(PathExpr::iri(":p"));
306 assert_eq!(p.depth(), 2);
307 }
308
309 #[test]
310 fn test_depth_one_or_more() {
311 let p = PathExpr::one_or_more(PathExpr::iri(":p"));
312 assert_eq!(p.depth(), 2);
313 }
314
315 #[test]
316 fn test_depth_zero_or_one() {
317 let p = PathExpr::zero_or_one(PathExpr::iri(":p"));
318 assert_eq!(p.depth(), 2);
319 }
320
321 #[test]
322 fn test_depth_sequence() {
323 let p = PathExpr::sequence(PathExpr::iri(":p"), PathExpr::iri(":q"));
325 assert_eq!(p.depth(), 2);
326 }
327
328 #[test]
329 fn test_depth_alternative() {
330 let p = PathExpr::alternative(PathExpr::iri(":p"), PathExpr::iri(":q"));
331 assert_eq!(p.depth(), 2);
332 }
333
334 #[test]
335 fn test_depth_nested_three_levels() {
336 let seq = PathExpr::sequence(PathExpr::iri(":p"), PathExpr::iri(":q"));
338 let p = PathExpr::inverse(seq);
339 assert_eq!(p.depth(), 3);
340 }
341
342 #[test]
343 fn test_depth_deeply_nested() {
344 let inner = PathExpr::inverse(PathExpr::iri(":x"));
346 let mid = PathExpr::one_or_more(inner);
347 let outer = PathExpr::zero_or_more(mid);
348 assert_eq!(outer.depth(), 4);
349 }
350
351 #[test]
352 fn test_depth_asymmetric_sequence() {
353 let p = PathExpr::sequence(
355 PathExpr::iri(":p"),
356 PathExpr::zero_or_more(PathExpr::iri(":q")),
357 );
358 assert_eq!(p.depth(), 3);
359 }
360
361 #[test]
364 fn test_iris_single_iri() {
365 let p = PathExpr::iri(":a");
366 assert_eq!(p.iris(), vec![":a".to_string()]);
367 }
368
369 #[test]
370 fn test_iris_sequence_two() {
371 let p = PathExpr::sequence(PathExpr::iri(":a"), PathExpr::iri(":b"));
372 let mut iris = p.iris();
373 iris.sort();
374 assert!(iris.contains(&":a".to_string()));
375 assert!(iris.contains(&":b".to_string()));
376 }
377
378 #[test]
379 fn test_iris_deduplicates() {
380 let p = PathExpr::alternative(PathExpr::iri(":a"), PathExpr::iri(":a"));
382 assert_eq!(p.iris(), vec![":a".to_string()]);
383 }
384
385 #[test]
386 fn test_iris_negated_set() {
387 let p = PathExpr::negated_set(&[":x", ":y"]);
388 let mut iris = p.iris();
389 iris.sort();
390 assert_eq!(iris, vec![":x".to_string(), ":y".to_string()]);
391 }
392
393 #[test]
394 fn test_iris_sorted_order() {
395 let p = PathExpr::sequence(PathExpr::iri("z:c"), PathExpr::iri("a:b"));
396 let iris = p.iris();
397 assert_eq!(iris, vec!["a:b".to_string(), "z:c".to_string()]);
399 }
400
401 #[test]
402 fn test_iris_negated_set_empty() {
403 let p = PathExpr::negated_set(&[]);
404 assert!(p.iris().is_empty());
405 }
406
407 #[test]
410 fn test_can_match_zero_iri_false() {
411 assert!(!PathExpr::iri(":p").can_match_zero());
412 }
413
414 #[test]
415 fn test_can_match_zero_negated_set_false() {
416 assert!(!PathExpr::negated_set(&[":p"]).can_match_zero());
417 }
418
419 #[test]
420 fn test_can_match_zero_star_true() {
421 assert!(PathExpr::zero_or_more(PathExpr::iri(":p")).can_match_zero());
422 }
423
424 #[test]
425 fn test_can_match_zero_question_mark_true() {
426 assert!(PathExpr::zero_or_one(PathExpr::iri(":p")).can_match_zero());
427 }
428
429 #[test]
430 fn test_can_match_zero_plus_non_nullable_inner() {
431 assert!(!PathExpr::one_or_more(PathExpr::iri(":p")).can_match_zero());
433 }
434
435 #[test]
436 fn test_can_match_zero_plus_nullable_inner() {
437 let star = PathExpr::zero_or_more(PathExpr::iri(":p"));
439 assert!(PathExpr::one_or_more(star).can_match_zero());
440 }
441
442 #[test]
443 fn test_can_match_zero_sequence_both_nullable() {
444 let s = PathExpr::sequence(
445 PathExpr::zero_or_more(PathExpr::iri(":p")),
446 PathExpr::zero_or_one(PathExpr::iri(":q")),
447 );
448 assert!(s.can_match_zero());
449 }
450
451 #[test]
452 fn test_can_match_zero_sequence_one_not_nullable() {
453 let s = PathExpr::sequence(
454 PathExpr::zero_or_more(PathExpr::iri(":p")),
455 PathExpr::iri(":q"),
456 );
457 assert!(!s.can_match_zero());
458 }
459
460 #[test]
461 fn test_can_match_zero_alternative_one_nullable() {
462 let a = PathExpr::alternative(
463 PathExpr::iri(":p"),
464 PathExpr::zero_or_more(PathExpr::iri(":q")),
465 );
466 assert!(a.can_match_zero());
467 }
468
469 #[test]
470 fn test_can_match_zero_alternative_none_nullable() {
471 let a = PathExpr::alternative(PathExpr::iri(":p"), PathExpr::iri(":q"));
472 assert!(!a.can_match_zero());
473 }
474
475 #[test]
476 fn test_can_match_zero_inverse_non_nullable() {
477 assert!(!PathExpr::inverse(PathExpr::iri(":p")).can_match_zero());
478 }
479
480 #[test]
481 fn test_can_match_zero_inverse_of_star() {
482 let star = PathExpr::zero_or_more(PathExpr::iri(":p"));
483 assert!(PathExpr::inverse(star).can_match_zero());
484 }
485
486 #[test]
489 fn test_to_sparql_iri() {
490 assert_eq!(PathExpr::iri(":p").to_sparql(), ":p");
491 }
492
493 #[test]
494 fn test_to_sparql_inverse() {
495 let p = PathExpr::inverse(PathExpr::iri(":p"));
496 assert_eq!(p.to_sparql(), "^(:p)");
497 }
498
499 #[test]
500 fn test_to_sparql_sequence() {
501 let p = PathExpr::sequence(PathExpr::iri(":p"), PathExpr::iri(":q"));
502 assert_eq!(p.to_sparql(), "(:p/:q)");
503 }
504
505 #[test]
506 fn test_to_sparql_alternative() {
507 let p = PathExpr::alternative(PathExpr::iri(":p"), PathExpr::iri(":q"));
508 assert_eq!(p.to_sparql(), "(:p|:q)");
509 }
510
511 #[test]
512 fn test_to_sparql_zero_or_more() {
513 let p = PathExpr::zero_or_more(PathExpr::iri(":p"));
514 assert_eq!(p.to_sparql(), "(:p)*");
515 }
516
517 #[test]
518 fn test_to_sparql_one_or_more() {
519 let p = PathExpr::one_or_more(PathExpr::iri(":p"));
520 assert_eq!(p.to_sparql(), "(:p)+");
521 }
522
523 #[test]
524 fn test_to_sparql_zero_or_one() {
525 let p = PathExpr::zero_or_one(PathExpr::iri(":p"));
526 assert_eq!(p.to_sparql(), "(:p)?");
527 }
528
529 #[test]
530 fn test_to_sparql_negated_set_empty() {
531 let p = PathExpr::negated_set(&[]);
532 assert_eq!(p.to_sparql(), "!()");
533 }
534
535 #[test]
536 fn test_to_sparql_negated_set_single() {
537 let p = PathExpr::negated_set(&[":p"]);
538 assert_eq!(p.to_sparql(), "!:p");
539 }
540
541 #[test]
542 fn test_to_sparql_negated_set_multiple() {
543 let p = PathExpr::negated_set(&[":p", ":q"]);
544 assert_eq!(p.to_sparql(), "!(:p|:q)");
545 }
546
547 #[test]
548 fn test_to_sparql_nested_sequence_of_alternatives() {
549 let alt1 = PathExpr::alternative(PathExpr::iri(":a"), PathExpr::iri(":b"));
551 let alt2 = PathExpr::alternative(PathExpr::iri(":c"), PathExpr::iri(":d"));
552 let seq = PathExpr::sequence(alt1, alt2);
553 assert_eq!(seq.to_sparql(), "((:a|:b)/(:c|:d))");
554 }
555
556 #[test]
557 fn test_to_sparql_round_trip_display() {
558 let p = PathExpr::one_or_more(PathExpr::inverse(PathExpr::iri(":knows")));
559 let s = p.to_sparql();
560 assert!(s.contains("knows"));
561 assert!(s.contains('+'));
562 assert!(s.contains('^'));
563 }
564
565 #[test]
568 fn test_nested_sequence_depth() {
569 let ab = PathExpr::sequence(PathExpr::iri(":a"), PathExpr::iri(":b"));
571 let abc = PathExpr::sequence(ab, PathExpr::iri(":c"));
572 assert_eq!(abc.depth(), 3);
573 }
574
575 #[test]
576 fn test_nested_alternative_iris() {
577 let a = PathExpr::alternative(
578 PathExpr::iri(":p"),
579 PathExpr::alternative(PathExpr::iri(":q"), PathExpr::iri(":r")),
580 );
581 let mut iris = a.iris();
582 iris.sort();
583 assert_eq!(
584 iris,
585 vec![":p".to_string(), ":q".to_string(), ":r".to_string()]
586 );
587 }
588
589 #[test]
590 fn test_clone_equality() {
591 let p =
592 PathExpr::zero_or_more(PathExpr::sequence(PathExpr::iri(":a"), PathExpr::iri(":b")));
593 assert_eq!(p.clone(), p);
594 }
595
596 #[test]
597 fn test_is_simple_iri_true() {
598 assert!(PathExpr::iri(":p").is_simple_iri());
599 }
600
601 #[test]
602 fn test_is_simple_iri_false() {
603 assert!(!PathExpr::inverse(PathExpr::iri(":p")).is_simple_iri());
604 }
605
606 #[test]
607 fn test_as_iri_some() {
608 let p = PathExpr::iri(":x");
609 assert_eq!(p.as_iri(), Some(":x"));
610 }
611
612 #[test]
613 fn test_as_iri_none_for_inverse() {
614 let p = PathExpr::inverse(PathExpr::iri(":x"));
615 assert_eq!(p.as_iri(), None);
616 }
617
618 #[test]
619 fn test_iri_count_sequence() {
620 let p = PathExpr::sequence(PathExpr::iri(":a"), PathExpr::iri(":a"));
621 assert_eq!(p.iri_count(), 2);
622 }
623
624 #[test]
625 fn test_iri_count_negated_set() {
626 let p = PathExpr::negated_set(&[":a", ":b", ":c"]);
627 assert_eq!(p.iri_count(), 3);
628 }
629
630 #[test]
631 fn test_display_trait() {
632 let p = PathExpr::iri(":hello");
633 assert_eq!(format!("{}", p), ":hello");
634 }
635
636 #[test]
637 fn test_deep_nesting_can_match_zero() {
638 let s = PathExpr::sequence(
640 PathExpr::zero_or_more(PathExpr::iri(":x")),
641 PathExpr::zero_or_more(PathExpr::iri(":y")),
642 );
643 assert!(s.can_match_zero());
644 }
645
646 #[test]
647 fn test_alternative_of_sequences_iris() {
648 let s1 = PathExpr::sequence(PathExpr::iri(":a"), PathExpr::iri(":b"));
650 let s2 = PathExpr::sequence(PathExpr::iri(":c"), PathExpr::iri(":d"));
651 let alt = PathExpr::alternative(s1, s2);
652 let mut iris = alt.iris();
653 iris.sort();
654 assert_eq!(
655 iris,
656 vec![
657 ":a".to_string(),
658 ":b".to_string(),
659 ":c".to_string(),
660 ":d".to_string()
661 ]
662 );
663 }
664
665 #[test]
666 fn test_to_sparql_complex_path() {
667 let seq = PathExpr::sequence(PathExpr::iri(":a"), PathExpr::iri(":b"));
669 let star = PathExpr::zero_or_more(seq);
670 let inv = PathExpr::inverse(star);
671 let sparql = inv.to_sparql();
672 assert!(sparql.contains(":a"));
673 assert!(sparql.contains(":b"));
674 assert!(sparql.contains('*'));
675 assert!(sparql.contains('^'));
676 }
677
678 #[test]
679 fn test_negated_set_three_iris() {
680 let n = PathExpr::negated_set(&[":a", ":b", ":c"]);
681 if let PathExpr::NegatedSet(iris) = &n {
682 assert_eq!(iris.len(), 3);
683 } else {
684 panic!("Expected NegatedSet");
685 }
686 }
687
688 #[test]
689 fn test_depth_alternative_asymmetric() {
690 let deep = PathExpr::zero_or_more(PathExpr::inverse(PathExpr::iri(":x")));
692 let alt = PathExpr::alternative(PathExpr::iri(":y"), deep);
693 assert_eq!(alt.depth(), 4);
694 }
695}