Skip to main content

hemoglobin_search/
query_parser.rs

1use std::cell::LazyCell;
2
3use crate::ComparisonKind;
4use chumsky::{
5    IterParser, Parser,
6    error::Rich,
7    extra,
8    prelude::{any, choice, end, just, recursive},
9    text::ident,
10};
11use hemoglobin::{
12    cards::{
13        kins::{Kin, KinComparison},
14        properties::{Array, Number, Text},
15    },
16    numbers::Comparison,
17};
18use regex::Regex;
19
20use super::{Ordering, Query, QueryRestriction, Sort, TextComparison};
21
22#[derive(Clone, Copy)]
23pub enum Properties {
24    NumProperty(Number),
25    StringProperty(Text),
26    ArrayProperty(Array),
27    Sort(Ordering),
28    Kin,
29    Keywords,
30}
31
32enum TextComparable {
33    String(String),
34    Regex(Regex),
35}
36
37/// Parses a query.
38/// # Errors
39/// If the parser fails.
40pub fn parse_query(string: &str) -> Result<Query, std::vec::Vec<chumsky::error::Rich<'_, char>>> {
41    let parser = LazyCell::new(make_query_parser);
42    parser.parse(string).into_result()
43}
44
45#[allow(clippy::too_many_lines)]
46pub fn make_query_parser<'a>() -> impl Parser<'a, &'a str, Query, extra::Err<Rich<'a, char>>> + 'a {
47    let word = any()
48        .filter(|c: &char| {
49            !c.is_whitespace()
50                && *c != '('
51                && *c != ')'
52                && *c != ':'
53                && *c != '='
54                && *c != '<'
55                && *c != '>'
56                && *c != '!'
57                && *c != '-'
58        })
59        .repeated()
60        .at_least(1)
61        .collect::<String>()
62        .labelled("ident")
63        .as_context();
64
65    let quoted_word = |delim: char| {
66        any()
67            .filter(move |c: &char| *c != delim)
68            .repeated()
69            .collect::<String>()
70            .labelled(format!("string wrapped in {delim}"))
71            .as_context()
72    };
73
74    let keyword = |mat: &'static str| {
75        ident()
76            .try_map(move |kw, span| {
77                if mat == kw {
78                    Ok(())
79                } else {
80                    Err(Rich::custom(span, format!("Expected {kw}")))
81                }
82            })
83            .labelled(format!("`{mat}`"))
84            .as_context()
85    };
86
87    let name_property_name = choice((keyword("name"), keyword("n"))).to(Text::Name);
88    let desc_property_name =
89        choice((keyword("description"), keyword("desc"), keyword("d"))).to(Text::Description);
90    let flavor_property_name =
91        choice((keyword("flavortext"), keyword("flavor"), keyword("ft"))).to(Text::FlavorText);
92    let id_property_name = keyword("id").to(Text::Id);
93    let type_property_name = choice((keyword("type"), keyword("t"))).to(Text::Type);
94    let set_property_name = choice((keyword("set"), keyword("s"))).to(Text::SetId);
95    let chapter_property_name = choice((keyword("chapter"), keyword("ch"))).to(Text::Chapter);
96
97    let text_property_name = choice((
98        name_property_name,
99        desc_property_name,
100        flavor_property_name,
101        id_property_name,
102        type_property_name,
103        set_property_name,
104        chapter_property_name,
105    ))
106    .padded();
107
108    let cost_property_name = choice((keyword("cost"), keyword("c"))).to(Number::Cost);
109    let flip_cost_property_name = choice((keyword("flip"), keyword("fc"))).to(Number::FlipCost);
110    let power_property_name = choice((keyword("power"), keyword("p"))).to(Number::Power);
111    let def_property_name =
112        choice((keyword("defense"), keyword("def"), keyword("d"))).to(Number::Defense);
113    let health_property_name =
114        choice((keyword("health"), keyword("hp"), keyword("h"))).to(Number::Health);
115
116    let num_property_name = choice((
117        cost_property_name,
118        flip_cost_property_name,
119        power_property_name,
120        def_property_name,
121        health_property_name,
122    ));
123
124    let number = any()
125        .filter(|c: &char| c.is_numeric())
126        .repeated()
127        .at_least(1)
128        .collect::<String>()
129        .try_map(|x, span| {
130            x.parse()
131                .map_err(|x| Rich::custom(span, format!("Not a number: {x}")))
132        })
133        .labelled("number")
134        .as_context();
135
136    let regex_text = quoted_word('/')
137        .try_map(|x, span| {
138            Regex::new(x.as_str())
139                .map_err(|x| Rich::custom(span, format!("Not a valid regex: {x}")))
140        })
141        .delimited_by(just('/'), just('/'))
142        .labelled("regex expression")
143        .as_context();
144
145    let quoted_text = quoted_word('"')
146        .delimited_by(just('"'), just('"'))
147        .labelled("quoted text");
148
149    let expr = recursive(|expr| {
150        let group = expr
151            .clone()
152            .repeated()
153            .collect()
154            .delimited_by(just('(').padded(), just(')').padded())
155            .labelled("subquery")
156            .as_context();
157
158        let group_restriction = group
159            .clone()
160            .map(|x| QueryRestriction::Group(Query::from_restrictions(x)));
161
162        let group_query = group.clone().map(Query::from_restrictions);
163
164        // Num Properties
165        let num_comparison_symbol = choice((
166            just("!=").to(NumberComparisonSymbol::NotEqual),
167            just(">=").to(NumberComparisonSymbol::GreaterThanOrEqual),
168            just("<=").to(NumberComparisonSymbol::LessThanOrEqual),
169            just('>').to(NumberComparisonSymbol::GreaterThan),
170            just('<').to(NumberComparisonSymbol::LessThan),
171            just('=').to(NumberComparisonSymbol::Equal),
172        ))
173        .labelled("comparison operator");
174
175        let num_comparison = num_comparison_symbol
176            .padded()
177            .then(number.padded())
178            .map(|(comparison, number)| match comparison {
179                NumberComparisonSymbol::GreaterThan => Comparison::GreaterThan(number),
180                NumberComparisonSymbol::LessThan => Comparison::LessThan(number),
181                NumberComparisonSymbol::GreaterThanOrEqual => {
182                    Comparison::GreaterThanOrEqual(number)
183                }
184                NumberComparisonSymbol::LessThanOrEqual => Comparison::LessThanOrEqual(number),
185                NumberComparisonSymbol::Equal => Comparison::Equal(number),
186                NumberComparisonSymbol::NotEqual => Comparison::NotEqual(number),
187            })
188            .padded();
189
190        let num_property = num_property_name
191            .clone()
192            .padded()
193            .then(num_comparison)
194            .map(|(property, cost)| QueryRestriction::NumberComparison(property, cost));
195
196        // Text Properties
197        let text_comparison_symbol = choice((
198            just('=').to(ComparisonKind::Equals),
199            just(':').to(ComparisonKind::Contains),
200        ))
201        .padded();
202
203        let text_comparable = choice((
204            quoted_text.map(TextComparable::String),
205            regex_text.map(TextComparable::Regex),
206            word.map(TextComparable::String),
207        ))
208        .padded();
209
210        let text_comparison = text_comparison_symbol
211            .clone()
212            .then(text_comparable)
213            .map(|(symbol, text)| match text {
214                TextComparable::String(string) => match symbol {
215                    ComparisonKind::Contains => TextComparison::Contains(string),
216                    ComparisonKind::Equals => TextComparison::EqualTo(string),
217                },
218                TextComparable::Regex(regex) => TextComparison::HasMatch(regex),
219            })
220            .padded();
221
222        let text_property = text_property_name
223            .clone()
224            .then(text_comparison.clone())
225            .map(|(property, comparison)| QueryRestriction::TextComparison(property, comparison))
226            .padded();
227
228        // Kins
229        let kin_property_name = choice((keyword("kins"), keyword("k"))).to(Properties::Kin);
230
231        let kin_comparison = text_comparison.clone().map(|x| match x {
232            TextComparison::Contains(string) => match Kin::from_string(&string) {
233                Some(kin) => KinComparison::Similar(kin),
234                None => KinComparison::TextContains(string),
235            },
236            TextComparison::EqualTo(string) => match Kin::from_string(&string) {
237                Some(kin) => KinComparison::Equal(kin),
238                None => KinComparison::TextEqual(string),
239            },
240            TextComparison::HasMatch(regex) => KinComparison::RegexMatch(regex),
241        });
242
243        let kin_property = kin_property_name
244            .padded()
245            .ignore_then(kin_comparison.clone())
246            .map(QueryRestriction::KinComparison);
247
248        // Keywords
249        let kws_property_name = choice((keyword("keyword"), keyword("kw")));
250
251        let kw_property = kws_property_name
252            .ignore_then(text_comparison)
253            .map(QueryRestriction::HasKw);
254
255        // Devours
256        let devours_property_name = choice((keyword("devours"), keyword("dev"))).to(Text::Name);
257
258        let null_comparison_symbol = choice((just('=').to(()), just(':').to(()))).padded();
259
260        let devours_property = devours_property_name
261            .ignore_then(text_comparison_symbol)
262            .then(group_query.clone())
263            .map(|(comparison, query)| QueryRestriction::Devours(query, comparison))
264            .padded();
265
266        // Devoured by
267        let devouredby_property_name =
268            choice((keyword("devouredby"), keyword("deby"), keyword("dby"))).to(Text::Name);
269
270        let devouredby_property = devouredby_property_name
271            .ignore_then(null_comparison_symbol)
272            .ignore_then(group_query.clone())
273            .map(QueryRestriction::DevouredBy)
274            .padded();
275
276        // Fuzzy
277        let fuzzy = word
278            .filter(|x| x != "XOR" && x != "OR" && x != "SORT")
279            .map(QueryRestriction::Fuzzy)
280            .labelled("basic query word");
281
282        // Atom
283        let atom = choice((
284            num_property,
285            text_property,
286            devours_property,
287            devouredby_property,
288            kin_property,
289            kw_property,
290            fuzzy,
291        ))
292        .padded();
293
294        let atom = group_restriction.or(atom);
295
296        let uniop = choice((
297            just('-').to(QueryOp::Not),
298            just('!').to(QueryOp::LenientNot),
299        ))
300        .labelled("negation operator");
301
302        let atom = uniop
303            .padded()
304            .repeated()
305            .foldr(atom, |op, atom| match op {
306                QueryOp::Not => atom.not(),
307                QueryOp::LenientNot => atom.lnot(),
308            })
309            .labelled("search atom");
310
311        let operation = choice((
312            keyword("OR").to(QueryBinOp::Or),
313            keyword("XOR").to(QueryBinOp::Xor),
314        ));
315
316        atom.then(operation.then(expr).or_not())
317            .map(
318                |(first, op): (QueryRestriction, Option<(QueryBinOp, QueryRestriction)>)| match op {
319                    None => first,
320                    Some((op, right)) => match op {
321                        QueryBinOp::Or => first.or(right),
322                        QueryBinOp::Xor => first.xor(right),
323                    },
324                },
325            )
326            .labelled("search expression")
327    });
328
329    let order = choice((
330        keyword("ascending").to(Ordering::Ascending),
331        keyword("descending").to(Ordering::Descending),
332    ))
333    .labelled("ascending or descending");
334
335    let sort_type = choice((
336        text_property_name.map(Sortable::Text),
337        num_property_name.map(Sortable::Num),
338    ))
339    .labelled("sortable trait");
340
341    let order =
342        sort_type
343            .padded()
344            .then(order)
345            .map(|(sort, order): (Sortable, Ordering)| match sort {
346                Sortable::Text(text) => Sort::Alphabet(text, order),
347                Sortable::Num(number) => Sort::Numeric(number, order),
348            });
349
350    let sort = keyword("SORT")
351        .ignore_then(order)
352        .labelled("sorting method")
353        .or_not()
354        .labelled("sorting clause or lack thereof")
355        .map(|x| x.map_or(Sort::Fuzzy, |x| x));
356
357    expr.padded()
358        .repeated()
359        .collect()
360        .map(Query::from_restrictions)
361        .then(sort.padded())
362        .map(|(mut query, sort)| {
363            query.sort = sort;
364            query
365        })
366        .then_ignore(end())
367}
368#[derive(Clone)]
369enum NumberComparisonSymbol {
370    GreaterThan,
371    LessThan,
372    GreaterThanOrEqual,
373    LessThanOrEqual,
374    Equal,
375    NotEqual,
376}
377
378#[derive(Clone)]
379enum QueryOp {
380    Not,
381    LenientNot,
382}
383
384#[derive(Clone)]
385enum QueryBinOp {
386    Or,
387    Xor,
388}
389
390enum Sortable {
391    Text(Text),
392    Num(Number),
393}