split up searches with a qualifier

This commit is contained in:
Damien Elmes 2020-03-15 20:41:49 +10:00
parent f623f19b3d
commit 289318d92c

View file

@ -7,6 +7,7 @@ use nom::character::complete::{char, one_of};
use nom::combinator::{all_consuming, map}; use nom::combinator::{all_consuming, map};
use nom::sequence::{delimited, preceded}; use nom::sequence::{delimited, preceded};
use nom::{multi::many0, IResult}; use nom::{multi::many0, IResult};
use std::borrow::Cow;
#[derive(Debug, PartialEq)] #[derive(Debug, PartialEq)]
pub(super) enum Node<'a> { pub(super) enum Node<'a> {
@ -14,7 +15,11 @@ pub(super) enum Node<'a> {
Or, Or,
Not(Box<Node<'a>>), Not(Box<Node<'a>>),
Group(Vec<Node<'a>>), Group(Vec<Node<'a>>),
Text(&'a str), UnqualifiedText(Cow<'a, str>),
QualifiedText {
key: Cow<'a, str>,
val: Cow<'a, str>,
},
} }
/// Parse the input string into a list of nodes. /// Parse the input string into a list of nodes.
@ -83,6 +88,32 @@ fn text(s: &str) -> IResult<&str, Node> {
alt((quoted_term, unquoted_term))(s) alt((quoted_term, unquoted_term))(s)
} }
/// Determine if text is a qualified search, and handle escaped chars.
fn node_for_text(s: &str) -> Node {
let mut it = s.splitn(2, ':');
let (head, tail) = (
without_escapes(it.next().unwrap()),
it.next().map(without_escapes),
);
if let Some(tail) = tail {
Node::QualifiedText {
key: head,
val: tail,
}
} else {
Node::UnqualifiedText(head)
}
}
fn without_escapes(s: &str) -> Cow<str> {
if s.find('\\').is_some() {
s.replace('\\', "").into()
} else {
s.into()
}
}
/// Unquoted text, terminated by a space or ) /// Unquoted text, terminated by a space or )
fn unquoted_term(s: &str) -> IResult<&str, Node> { fn unquoted_term(s: &str) -> IResult<&str, Node> {
map(take_while1(|c| c != ' ' && c != ')'), |text: &str| { map(take_while1(|c| c != ' ' && c != ')'), |text: &str| {
@ -91,7 +122,7 @@ fn unquoted_term(s: &str) -> IResult<&str, Node> {
} else if text.len() == 3 && text.to_ascii_lowercase() == "and" { } else if text.len() == 3 && text.to_ascii_lowercase() == "and" {
Node::And Node::And
} else { } else {
Node::Text(text) node_for_text(text)
} }
})(s) })(s)
} }
@ -102,10 +133,10 @@ fn quoted_term(s: &str) -> IResult<&str, Node> {
} }
/// Quoted text, terminated by a non-escaped double quote /// Quoted text, terminated by a non-escaped double quote
/// Can escape :, " and \ /// Can escape " and \
fn quoted_term_inner(s: &str) -> IResult<&str, Node> { fn quoted_term_inner(s: &str) -> IResult<&str, Node> {
map(escaped(is_not(r#""\"#), '\\', one_of(r#"":\"#)), |o| { map(escaped(is_not(r#""\"#), '\\', one_of(r#""\"#)), |o| {
Node::Text(o) node_for_text(o)
})(s) })(s)
} }
@ -116,14 +147,22 @@ mod test {
#[test] #[test]
fn parsing() -> Result<(), String> { fn parsing() -> Result<(), String> {
use Node::*; use Node::*;
assert_eq!( assert_eq!(
parse(r#"hello -(world and "foo bar") OR test"#)?, parse(r#"hello -(world and "foo:bar baz") OR test"#)?,
vec![ vec![
Text("hello"), UnqualifiedText("hello".into()),
And, And,
Not(Box::new(Group(vec![Text("world"), And, Text("foo bar")]))), Not(Box::new(Group(vec![
UnqualifiedText("world".into()),
And,
QualifiedText {
key: "foo".into(),
val: "bar baz".into()
}
]))),
Or, Or,
Text("test") UnqualifiedText("test".into())
] ]
); );