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
37pub 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 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 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 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 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 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 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 let fuzzy = word
278 .filter(|x| x != "XOR" && x != "OR" && x != "SORT")
279 .map(QueryRestriction::Fuzzy)
280 .labelled("basic query word");
281
282 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}