use crate::TextQuery;
pub const RELAXED_BRANCH_CAP: usize = 4;
pub const FALLBACK_TRIGGER_K: usize = 1;
#[must_use]
pub fn derive_relaxed(strict: &TextQuery) -> (Option<TextQuery>, bool) {
let children = match strict {
TextQuery::And(children) => children,
TextQuery::Empty
| TextQuery::Term(_)
| TextQuery::Phrase(_)
| TextQuery::Or(_)
| TextQuery::Not(_) => return (None, false),
};
let mut kept: Vec<TextQuery> = Vec::with_capacity(children.len());
for child in children {
match child {
TextQuery::Term(_) | TextQuery::Phrase(_) => kept.push(child.clone()),
TextQuery::Not(_) | TextQuery::Empty => {}
TextQuery::And(_) | TextQuery::Or(_) => return (None, false),
}
}
if kept.len() < 2 {
return (None, false);
}
let was_degraded = kept.len() > RELAXED_BRANCH_CAP;
if was_degraded {
kept.truncate(RELAXED_BRANCH_CAP);
}
(Some(TextQuery::Or(kept)), was_degraded)
}
#[cfg(test)]
#[allow(clippy::expect_used, clippy::panic)]
mod tests {
use super::*;
fn term(s: &str) -> TextQuery {
TextQuery::Term(s.to_owned())
}
fn phrase(s: &str) -> TextQuery {
TextQuery::Phrase(s.to_owned())
}
#[test]
fn derive_relaxed_breaks_implicit_and_into_or() {
let strict = TextQuery::And(vec![term("a"), term("b"), term("c")]);
let (relaxed, was_degraded) = derive_relaxed(&strict);
assert_eq!(
relaxed,
Some(TextQuery::Or(vec![term("a"), term("b"), term("c")]))
);
assert!(!was_degraded);
}
#[test]
fn derive_relaxed_preserves_phrases_as_single_alternatives() {
let strict = TextQuery::And(vec![term("a"), phrase("b c")]);
let (relaxed, was_degraded) = derive_relaxed(&strict);
assert_eq!(relaxed, Some(TextQuery::Or(vec![term("a"), phrase("b c")])));
assert!(!was_degraded);
}
#[test]
fn derive_relaxed_drops_top_level_not() {
let strict = TextQuery::And(vec![term("a"), TextQuery::Not(Box::new(term("b")))]);
let (relaxed, was_degraded) = derive_relaxed(&strict);
assert_eq!(relaxed, None);
assert!(!was_degraded);
let strict2 = TextQuery::And(vec![
term("a"),
term("b"),
TextQuery::Not(Box::new(term("c"))),
]);
let (relaxed2, was_degraded2) = derive_relaxed(&strict2);
assert_eq!(relaxed2, Some(TextQuery::Or(vec![term("a"), term("b")])));
assert!(!was_degraded2);
}
#[test]
fn derive_relaxed_all_top_level_nots_returns_none() {
let strict = TextQuery::And(vec![
TextQuery::Not(Box::new(term("a"))),
TextQuery::Not(Box::new(term("b"))),
]);
let (relaxed, was_degraded) = derive_relaxed(&strict);
assert_eq!(relaxed, None);
assert!(!was_degraded);
}
#[test]
fn derive_relaxed_returns_none_for_or_at_root() {
let strict = TextQuery::Or(vec![term("a"), term("b")]);
let (relaxed, was_degraded) = derive_relaxed(&strict);
assert_eq!(relaxed, None);
assert!(!was_degraded);
}
#[test]
fn derive_relaxed_returns_none_for_single_term() {
let (relaxed, was_degraded) = derive_relaxed(&term("budget"));
assert_eq!(relaxed, None);
assert!(!was_degraded);
let (relaxed, was_degraded) = derive_relaxed(&phrase("release notes"));
assert_eq!(relaxed, None);
assert!(!was_degraded);
}
#[test]
fn derive_relaxed_caps_at_four_alternatives_and_marks_degraded() {
let strict = TextQuery::And(vec![term("a"), term("b"), term("c"), term("d"), term("e")]);
let (relaxed, was_degraded) = derive_relaxed(&strict);
assert_eq!(
relaxed,
Some(TextQuery::Or(vec![
term("a"),
term("b"),
term("c"),
term("d"),
]))
);
assert!(was_degraded);
}
#[test]
fn derive_relaxed_cap_preserves_token_order() {
let strict = TextQuery::And(vec![
term("alpha"),
term("bravo"),
term("charlie"),
term("delta"),
term("echo"),
term("foxtrot"),
]);
let (relaxed, was_degraded) = derive_relaxed(&strict);
let Some(TextQuery::Or(kept)) = relaxed else {
panic!("expected Or");
};
assert_eq!(
kept,
vec![term("alpha"), term("bravo"), term("charlie"), term("delta"),]
);
assert!(was_degraded);
}
#[test]
fn derive_relaxed_returns_none_for_nested_and_or_child() {
let strict = TextQuery::And(vec![term("a"), TextQuery::And(vec![term("b"), term("c")])]);
let (relaxed, was_degraded) = derive_relaxed(&strict);
assert_eq!(relaxed, None);
assert!(!was_degraded);
}
}