1use nom::{
2 Err, IResult, Input, Parser,
3 branch::alt,
4 character::complete::char,
5 combinator::map,
6 error::ErrorKind,
7 multi::many0,
8 sequence::{delimited, preceded, terminated},
9};
10
11pub enum Word {
12 And(String),
13 Not(String),
14 Or(Vec<String>),
15}
16
17pub struct Query(pub Vec<Word>);
18
19impl Query {
20 pub fn db_query(&self) -> String {
21 let mut v = Vec::new();
22 for w in &self.0 {
23 match w {
24 Word::And(s) => {
25 if !s.is_empty() {
26 v.push(format!(r#"+"{s}""#));
27 }
28 }
29 Word::Not(s) => {
30 if !s.is_empty() {
31 v.push(format!(r#"-"{s}""#));
32 }
33 }
34 Word::Or(l) => {
35 let s = l
36 .iter()
37 .filter(|i| !i.is_empty())
38 .map(|i| format!(r#""{i}""#))
39 .collect::<Vec<_>>()
40 .join(" ");
41 if !s.is_empty() {
42 v.push(format!(r#"+({s})"#))
43 }
44 }
45 }
46 }
47 let mut r = v.join(" ");
48 r.retain(|c| !char::is_control(c) && c != '\\');
49 r
50 }
51}
52
53pub fn parse(i: &str) -> Query {
54 let w = parse_words(i).unwrap();
55 let mut v = w.1;
56 if !w.0.is_empty() {
57 let i = w.0.replace(['"', '(', ')'], " ");
58 for r in parse_words(&i).unwrap().1 {
59 v.push(r);
60 }
61 }
62 Query(v)
63}
64
65fn parse_words(i: &str) -> IResult<&str, Vec<Word>> {
66 preceded(space, many0(parse_query)).parse(i)
67}
68
69fn parse_query(i: &str) -> IResult<&str, Word> {
70 terminated(alt((parse_not, parse_and, parse_or, parse_word)), space).parse(i)
71}
72
73fn parse_word(i: &str) -> IResult<&str, Word> {
74 map(alt((quoted, word)), |w| Word::And(w.to_string())).parse(i)
75}
76
77fn parse_and(i: &str) -> IResult<&str, Word> {
78 map(preceded(char('+'), alt((quoted, word))), |w| {
79 Word::And(w.to_string())
80 })
81 .parse(i)
82}
83
84fn parse_not(i: &str) -> IResult<&str, Word> {
85 map(preceded(char('-'), alt((quoted, word))), |w| {
86 Word::Not(w.to_string())
87 })
88 .parse(i)
89}
90
91fn parse_or(i: &str) -> IResult<&str, Word> {
92 map(
93 delimited(
94 alt((char('('), char('('))),
95 many0(delimited(
96 space,
97 alt((
98 quoted,
99 delimited(char('('), take_until_unbalanced('(', ')'), char(')')),
100 delimited(char('('), take_until_unbalanced('(', ')'), char(')')),
101 word,
102 )),
103 space,
104 )),
105 alt((char(')'), char(')'))),
106 ),
107 |w| Word::Or(w.iter().map(|v| v.to_string()).collect()),
108 )
109 .parse(i)
110}
111
112pub fn take_until_unbalanced(
114 opening_bracket: char,
115 closing_bracket: char,
116) -> impl Fn(&str) -> IResult<&str, &str> {
117 move |i: &str| {
118 let mut index = 0;
119 let mut bracket_counter = 0;
120 while let Some(n) = &i[index..].find(&[opening_bracket, closing_bracket][..]) {
121 index += n;
122 let mut it = i[index..].chars();
123 match it.next().unwrap_or_default() {
124 c if c == opening_bracket => {
125 bracket_counter += 1;
126 index += opening_bracket.len_utf8();
127 }
128 c if c == closing_bracket => {
129 bracket_counter -= 1;
131 index += closing_bracket.len_utf8();
132 }
133 _ => unreachable!(),
135 };
136 if bracket_counter == -1 {
138 index -= closing_bracket.len_utf8();
140 return Ok((&i[index..], &i[0..index]));
141 };
142 }
143 if bracket_counter == 0 {
144 Ok(("", i))
145 } else {
146 Err(Err::Error(nom::error::make_error(i, ErrorKind::TakeUntil)))
147 }
148 }
149}
150
151fn quoted(i: &str) -> IResult<&str, &str> {
152 delimited(char('"'), quoted_word, char('"')).parse(i)
153}
154
155fn word(input: &str) -> IResult<&str, &str> {
156 input.split_at_position1_complete(
157 |c| char::is_whitespace(c) || c == '"' || c == '(' || c == ')' || c == '(' || c == ')',
158 ErrorKind::AlphaNumeric,
159 )
160}
161
162fn quoted_word(input: &str) -> IResult<&str, &str> {
163 input.split_at_position_complete(|c| c == '"')
164}
165
166fn space(input: &str) -> IResult<&str, &str> {
167 input.split_at_position_complete(|c| !char::is_whitespace(c))
168}
169
170pub fn escape_like(input: &str) -> String {
171 let mut escaped = String::with_capacity(input.len());
172
173 for ch in input.chars() {
174 match ch {
175 '%' | '_' | '\\' => {
176 escaped.push('\\');
177 escaped.push(ch);
178 }
179 _ => escaped.push(ch),
180 }
181 }
182 escaped
183}
184
185pub fn to_like_contains(input: &str) -> String {
186 format!("%{}%", escape_like(input))
187}
188
189#[cfg(test)]
190mod tests {
191
192 use super::*;
193 #[test]
194 fn test() {
195 assert_eq!(parse("a テスト").db_query(), r#"+"a" +"テスト""#);
196 assert_eq!(parse(" a b ").db_query(), r#"+"a" +"b""#);
197 assert_eq!(parse(r#" a "bb "#).db_query(), r#"+"a" +"bb""#);
198 assert_eq!(parse(r#" a (bb "#).db_query(), r#"+"a" +"bb""#);
199 assert_eq!(parse("a -b").db_query(), r#"+"a" -"b""#);
200 assert_eq!(parse(r#""a(a""#).db_query(), r#"+"a(a""#);
201 assert_eq!(parse("a (b c)").db_query(), r#"+"a" +("b" "c")"#);
202 assert_eq!(parse("a(b c)").db_query(), r#"+"a" +("b" "c")"#);
203 assert_eq!(parse("a(b (c d))").db_query(), r#"+"a" +("b" "c d")"#);
204 assert_eq!(parse("a () b").db_query(), r#"+"a" +"b""#);
205 assert_eq!(parse(r#"a "" b"#).db_query(), r#"+"a" +"b""#);
206 assert_eq!(
207 parse(r#"a "cc dd)\\" b"#).db_query(),
208 r#"+"a" +"cc dd)" +"b""#
209 );
210 assert_eq!(parse("a (b c)").db_query(), r#"+"a" +("b" "c")"#);
211 assert_eq!(parse("a(b (c d))").db_query(), r#"+"a" +("b" "c d")"#);
212 }
213}