pub fn build_forward_hop_pattern(
start_term: &str,
end_var: &str,
hops: usize,
predicate_prefix: &str,
node_prefix: &str,
) -> String {
let hops = hops.max(1);
let branches: Vec<String> = (1..=hops)
.map(|hop| {
let mut pattern = String::new();
let mut prev = start_term.to_string();
for step in 1..=hop {
let next = if step == hop {
end_var.to_string()
} else {
format!("?{node_prefix}{step}")
};
pattern.push_str(&format!("{prev} ?{predicate_prefix}{step} {next} . "));
prev = next;
}
format!("{{ {} }}", pattern.trim_end())
})
.collect();
branches.join(" UNION ")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn regression_single_hop_is_one_branch() {
let pattern = build_forward_hop_pattern("?seed", "?neighbor", 1, "hp", "hn");
assert_eq!(pattern, "{ ?seed ?hp1 ?neighbor . }");
}
#[test]
fn regression_zero_hops_clamped_to_one() {
let zero = build_forward_hop_pattern("?seed", "?neighbor", 0, "hp", "hn");
let one = build_forward_hop_pattern("?seed", "?neighbor", 1, "hp", "hn");
assert_eq!(zero, one);
}
#[test]
fn regression_multi_hop_has_one_branch_per_hop_count() {
let pattern = build_forward_hop_pattern("?seed", "?neighbor", 3, "hp", "hn");
assert_eq!(pattern.matches("UNION").count(), 2);
assert!(pattern.contains("?seed ?hp1 ?neighbor ."));
assert!(pattern.contains("?seed ?hp1 ?hn1 . ?hn1 ?hp2 ?neighbor ."));
assert!(pattern.contains("?seed ?hp1 ?hn1 . ?hn1 ?hp2 ?hn2 . ?hn2 ?hp3 ?neighbor ."));
}
#[test]
fn regression_no_bogus_empty_prefix_tokens() {
let pattern = build_forward_hop_pattern("?seed", "?neighbor", 5, "hp", "hn");
assert!(!pattern.contains(":|!:"));
assert!(!pattern.contains("!("));
}
#[test]
fn regression_supports_fixed_iri_start_term() {
let pattern =
build_forward_hop_pattern("<http://example.org/e>", "?hopnode", 2, "hp", "hn");
assert!(pattern.contains("<http://example.org/e> ?hp1 ?hn1 ."));
assert!(pattern.contains("?hn1 ?hp2 ?hopnode ."));
}
}