232 lines
6.1 KiB
Rust
232 lines
6.1 KiB
Rust
//! Filter segment prefix parsing. Determines which filter
|
|
//! strategy to use based on the query prefix.
|
|
|
|
/// The type of filter to apply for a query segment.
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
pub enum FilterKind {
|
|
Fuzzy,
|
|
Exact,
|
|
Regex,
|
|
}
|
|
|
|
/// A parsed filter segment with its kind, inversion flag,
|
|
/// and the actual query text (prefix stripped).
|
|
#[derive(Debug, PartialEq, Eq)]
|
|
pub struct ParsedSegment<'a> {
|
|
pub kind: FilterKind,
|
|
pub inverse: bool,
|
|
pub query: &'a str,
|
|
}
|
|
|
|
/// Parse a single filter segment's prefix to determine
|
|
/// the filter strategy and extract the query text.
|
|
///
|
|
/// Prefix rules (order matters):
|
|
/// 1. `!/pattern/` -> Regex, inverse, inner pattern
|
|
/// 2. `/pattern/` -> Regex, inner pattern
|
|
/// 3. `!'query` -> Exact, inverse, after `!'`
|
|
/// 4. `!query` -> Fuzzy, inverse, after `!`
|
|
/// 5. `'query` -> Exact, after `'`
|
|
/// 6. Everything else -> Fuzzy
|
|
///
|
|
/// A `/` with no closing slash is treated as fuzzy (user
|
|
/// is still typing the regex delimiter).
|
|
pub fn parse_segment(segment: &str) -> ParsedSegment<'_> {
|
|
// Check for inverse regex: !/pattern/
|
|
if let Some(rest) = segment.strip_prefix("!/") {
|
|
if let Some(inner) = rest.strip_suffix('/') {
|
|
return ParsedSegment {
|
|
kind: FilterKind::Regex,
|
|
inverse: true,
|
|
query: inner,
|
|
};
|
|
}
|
|
// No closing slash: treat the whole thing as fuzzy inverse
|
|
return ParsedSegment {
|
|
kind: FilterKind::Fuzzy,
|
|
inverse: true,
|
|
query: &segment[1..],
|
|
};
|
|
}
|
|
|
|
// Check for regex: /pattern/
|
|
if let Some(rest) = segment.strip_prefix('/') {
|
|
if let Some(inner) = rest.strip_suffix('/') {
|
|
return ParsedSegment {
|
|
kind: FilterKind::Regex,
|
|
inverse: false,
|
|
query: inner,
|
|
};
|
|
}
|
|
// No closing slash: treat as fuzzy (still typing)
|
|
return ParsedSegment {
|
|
kind: FilterKind::Fuzzy,
|
|
inverse: false,
|
|
query: segment,
|
|
};
|
|
}
|
|
|
|
// Check for inverse exact: !'query
|
|
if let Some(rest) = segment.strip_prefix("!'") {
|
|
return ParsedSegment {
|
|
kind: FilterKind::Exact,
|
|
inverse: true,
|
|
query: rest,
|
|
};
|
|
}
|
|
|
|
// Check for inverse fuzzy: !query
|
|
if let Some(rest) = segment.strip_prefix('!') {
|
|
return ParsedSegment {
|
|
kind: FilterKind::Fuzzy,
|
|
inverse: true,
|
|
query: rest,
|
|
};
|
|
}
|
|
|
|
// Check for exact: 'query
|
|
if let Some(rest) = segment.strip_prefix('\'') {
|
|
return ParsedSegment {
|
|
kind: FilterKind::Exact,
|
|
inverse: false,
|
|
query: rest,
|
|
};
|
|
}
|
|
|
|
// Default: fuzzy
|
|
ParsedSegment {
|
|
kind: FilterKind::Fuzzy,
|
|
inverse: false,
|
|
query: segment,
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn plain_text_is_fuzzy() {
|
|
let p = parse_segment("hello");
|
|
assert_eq!(p.kind, FilterKind::Fuzzy);
|
|
assert!(!p.inverse);
|
|
assert_eq!(p.query, "hello");
|
|
}
|
|
|
|
#[test]
|
|
fn empty_is_fuzzy() {
|
|
let p = parse_segment("");
|
|
assert_eq!(p.kind, FilterKind::Fuzzy);
|
|
assert!(!p.inverse);
|
|
assert_eq!(p.query, "");
|
|
}
|
|
|
|
#[test]
|
|
fn exact_prefix() {
|
|
let p = parse_segment("'exact match");
|
|
assert_eq!(p.kind, FilterKind::Exact);
|
|
assert!(!p.inverse);
|
|
assert_eq!(p.query, "exact match");
|
|
}
|
|
|
|
#[test]
|
|
fn regex_delimiters() {
|
|
let p = parse_segment("/[0-9]+/");
|
|
assert_eq!(p.kind, FilterKind::Regex);
|
|
assert!(!p.inverse);
|
|
assert_eq!(p.query, "[0-9]+");
|
|
}
|
|
|
|
#[test]
|
|
fn inverse_fuzzy() {
|
|
let p = parse_segment("!temp");
|
|
assert_eq!(p.kind, FilterKind::Fuzzy);
|
|
assert!(p.inverse);
|
|
assert_eq!(p.query, "temp");
|
|
}
|
|
|
|
#[test]
|
|
fn inverse_exact() {
|
|
let p = parse_segment("!'temp");
|
|
assert_eq!(p.kind, FilterKind::Exact);
|
|
assert!(p.inverse);
|
|
assert_eq!(p.query, "temp");
|
|
}
|
|
|
|
#[test]
|
|
fn inverse_regex() {
|
|
let p = parse_segment("!/[0-9]+/");
|
|
assert_eq!(p.kind, FilterKind::Regex);
|
|
assert!(p.inverse);
|
|
assert_eq!(p.query, "[0-9]+");
|
|
}
|
|
|
|
#[test]
|
|
fn unclosed_regex_is_fuzzy() {
|
|
let p = parse_segment("/still typing");
|
|
assert_eq!(p.kind, FilterKind::Fuzzy);
|
|
assert!(!p.inverse);
|
|
assert_eq!(p.query, "/still typing");
|
|
}
|
|
|
|
#[test]
|
|
fn unclosed_inverse_regex_is_fuzzy_inverse() {
|
|
let p = parse_segment("!/still typing");
|
|
assert_eq!(p.kind, FilterKind::Fuzzy);
|
|
assert!(p.inverse);
|
|
assert_eq!(p.query, "/still typing");
|
|
}
|
|
|
|
#[test]
|
|
fn just_slash_is_fuzzy() {
|
|
let p = parse_segment("/");
|
|
assert_eq!(p.kind, FilterKind::Fuzzy);
|
|
assert!(!p.inverse);
|
|
assert_eq!(p.query, "/");
|
|
}
|
|
|
|
#[test]
|
|
fn empty_regex_pattern() {
|
|
let p = parse_segment("//");
|
|
assert_eq!(p.kind, FilterKind::Regex);
|
|
assert!(!p.inverse);
|
|
assert_eq!(p.query, "");
|
|
}
|
|
|
|
#[test]
|
|
fn just_exclamation() {
|
|
let p = parse_segment("!");
|
|
assert_eq!(p.kind, FilterKind::Fuzzy);
|
|
assert!(p.inverse);
|
|
assert_eq!(p.query, "");
|
|
}
|
|
|
|
#[test]
|
|
fn just_quote() {
|
|
let p = parse_segment("'");
|
|
assert_eq!(p.kind, FilterKind::Exact);
|
|
assert!(!p.inverse);
|
|
assert_eq!(p.query, "");
|
|
}
|
|
|
|
// -- Double-prefix edge cases --
|
|
|
|
#[test]
|
|
fn double_exclamation() {
|
|
// "!!query" -> first ! is inverse, rest is "!query" which is fuzzy inverse
|
|
let p = parse_segment("!!query");
|
|
assert_eq!(p.kind, FilterKind::Fuzzy);
|
|
assert!(p.inverse);
|
|
assert_eq!(p.query, "!query");
|
|
}
|
|
|
|
#[test]
|
|
fn inverse_exact_regex_like() {
|
|
// "!'[0-9]" -> exact inverse, query is "[0-9]" (not regex)
|
|
let p = parse_segment("!'[0-9]");
|
|
assert_eq!(p.kind, FilterKind::Exact);
|
|
assert!(p.inverse);
|
|
assert_eq!(p.query, "[0-9]");
|
|
}
|
|
}
|