use nom::{
Err, IResult, Input, Parser,
branch::alt,
character::complete::char,
combinator::map,
error::ErrorKind,
multi::many0,
sequence::{delimited, preceded, terminated},
};
pub enum Word {
And(String),
Not(String),
Or(Vec<String>),
}
pub struct Query(pub Vec<Word>);
impl Query {
pub fn db_query(&self) -> String {
let mut v = Vec::new();
for w in &self.0 {
match w {
Word::And(s) => {
if !s.is_empty() {
v.push(format!(r#"+"{s}""#));
}
}
Word::Not(s) => {
if !s.is_empty() {
v.push(format!(r#"-"{s}""#));
}
}
Word::Or(l) => {
let s = l
.iter()
.filter(|i| !i.is_empty())
.map(|i| format!(r#""{i}""#))
.collect::<Vec<_>>()
.join(" ");
if !s.is_empty() {
v.push(format!(r#"+({s})"#))
}
}
}
}
let mut r = v.join(" ");
r.retain(|c| !char::is_control(c) && c != '\\');
r
}
}
pub fn parse(i: &str) -> Query {
let w = parse_words(i).unwrap();
let mut v = w.1;
if !w.0.is_empty() {
let i = w.0.replace(['"', '(', ')'], " ");
for r in parse_words(&i).unwrap().1 {
v.push(r);
}
}
Query(v)
}
fn parse_words(i: &str) -> IResult<&str, Vec<Word>> {
preceded(space, many0(parse_query)).parse(i)
}
fn parse_query(i: &str) -> IResult<&str, Word> {
terminated(alt((parse_not, parse_and, parse_or, parse_word)), space).parse(i)
}
fn parse_word(i: &str) -> IResult<&str, Word> {
map(alt((quoted, word)), |w| Word::And(w.to_string())).parse(i)
}
fn parse_and(i: &str) -> IResult<&str, Word> {
map(preceded(char('+'), alt((quoted, word))), |w| {
Word::And(w.to_string())
})
.parse(i)
}
fn parse_not(i: &str) -> IResult<&str, Word> {
map(preceded(char('-'), alt((quoted, word))), |w| {
Word::Not(w.to_string())
})
.parse(i)
}
fn parse_or(i: &str) -> IResult<&str, Word> {
map(
delimited(
alt((char('('), char('('))),
many0(delimited(
space,
alt((
quoted,
delimited(char('('), take_until_unbalanced('(', ')'), char(')')),
delimited(char('('), take_until_unbalanced('(', ')'), char(')')),
word,
)),
space,
)),
alt((char(')'), char(')'))),
),
|w| Word::Or(w.iter().map(|v| v.to_string()).collect()),
)
.parse(i)
}
pub fn take_until_unbalanced(
opening_bracket: char,
closing_bracket: char,
) -> impl Fn(&str) -> IResult<&str, &str> {
move |i: &str| {
let mut index = 0;
let mut bracket_counter = 0;
while let Some(n) = &i[index..].find(&[opening_bracket, closing_bracket][..]) {
index += n;
let mut it = i[index..].chars();
match it.next().unwrap_or_default() {
c if c == opening_bracket => {
bracket_counter += 1;
index += opening_bracket.len_utf8();
}
c if c == closing_bracket => {
bracket_counter -= 1;
index += closing_bracket.len_utf8();
}
_ => unreachable!(),
};
if bracket_counter == -1 {
index -= closing_bracket.len_utf8();
return Ok((&i[index..], &i[0..index]));
};
}
if bracket_counter == 0 {
Ok(("", i))
} else {
Err(Err::Error(nom::error::make_error(i, ErrorKind::TakeUntil)))
}
}
}
fn quoted(i: &str) -> IResult<&str, &str> {
delimited(char('"'), quoted_word, char('"')).parse(i)
}
fn word(input: &str) -> IResult<&str, &str> {
input.split_at_position1_complete(
|c| char::is_whitespace(c) || c == '"' || c == '(' || c == ')' || c == '(' || c == ')',
ErrorKind::AlphaNumeric,
)
}
fn quoted_word(input: &str) -> IResult<&str, &str> {
input.split_at_position_complete(|c| c == '"')
}
fn space(input: &str) -> IResult<&str, &str> {
input.split_at_position_complete(|c| !char::is_whitespace(c))
}
pub fn escape_like(input: &str) -> String {
let mut escaped = String::with_capacity(input.len());
for ch in input.chars() {
match ch {
'%' | '_' | '\\' => {
escaped.push('\\');
escaped.push(ch);
}
_ => escaped.push(ch),
}
}
escaped
}
pub fn to_like_contains(input: &str) -> String {
format!("%{}%", escape_like(input))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test() {
assert_eq!(parse("a テスト").db_query(), r#"+"a" +"テスト""#);
assert_eq!(parse(" a b ").db_query(), r#"+"a" +"b""#);
assert_eq!(parse(r#" a "bb "#).db_query(), r#"+"a" +"bb""#);
assert_eq!(parse(r#" a (bb "#).db_query(), r#"+"a" +"bb""#);
assert_eq!(parse("a -b").db_query(), r#"+"a" -"b""#);
assert_eq!(parse(r#""a(a""#).db_query(), r#"+"a(a""#);
assert_eq!(parse("a (b c)").db_query(), r#"+"a" +("b" "c")"#);
assert_eq!(parse("a(b c)").db_query(), r#"+"a" +("b" "c")"#);
assert_eq!(parse("a(b (c d))").db_query(), r#"+"a" +("b" "c d")"#);
assert_eq!(parse("a () b").db_query(), r#"+"a" +"b""#);
assert_eq!(parse(r#"a "" b"#).db_query(), r#"+"a" +"b""#);
assert_eq!(
parse(r#"a "cc dd)\\" b"#).db_query(),
r#"+"a" +"cc dd)" +"b""#
);
assert_eq!(parse("a (b c)").db_query(), r#"+"a" +("b" "c")"#);
assert_eq!(parse("a(b (c d))").db_query(), r#"+"a" +("b" "c d")"#);
}
}