1#[warn(clippy::pedantic)]
2#[warn(clippy::nursery)]
3pub mod fuzzy;
4pub mod query_parser;
5use query_parser::parse_query;
6use serde::{Deserialize, Serialize};
7use std::{
8 collections::HashMap,
9 fmt::{Display, Write},
10};
11
12#[cfg(target_arch = "wasm32")]
13use wasm_bindgen::prelude::*;
14
15use fuzzy::weighted_compare;
16use hemoglobin::{
17 cards::{
18 Card, CardId, Keyword, KeywordData,
19 kins::KinComparison,
20 properties::{Array, Number, Read, Text},
21 },
22 clean_ascii,
23 numbers::{Comparison, ImpreciseOrd, compare::Ternary},
24};
25use regex::Regex;
26
27#[derive(Debug)]
29pub enum Errors {
30 NonRegexable(String),
31 InvalidOr,
32 InvalidComparisonString,
33 UnknownSubQueryParam(String),
34 UnknownStringParam(String),
35 InvalidOrdering(String),
36 InvalidPolarity,
37 NotSortable,
38 UnclosedSubquery,
39 UnclosedString,
40 UnclosedRegex,
41 RegexErr(regex::Error),
42 AttemptedEmptyParamName,
43}
44
45#[derive(Debug, Clone, Serialize, Deserialize)]
47pub struct Query {
48 pub name: String,
49 pub restrictions: Vec<QueryRestriction>,
50 pub sort: Sort,
51}
52
53impl Query {
54 fn from_restrictions(restrictions: Vec<QueryRestriction>) -> Self {
55 let mut name = String::new();
56
57 for restriction in &restrictions {
58 if let QueryRestriction::Fuzzy(a) = restriction {
59 name += a;
60 name += " ";
61 }
62 }
63
64 let sort = if name.is_empty() {
65 Sort::Alphabet(Text::Name, Ordering::Ascending)
66 } else {
67 Sort::Fuzzy
68 };
69
70 Self {
71 name: name.trim().to_string(),
72 restrictions,
73 sort,
74 }
75 }
76 fn triviality(&self) -> Triviality {
77 if !self.name.is_empty() {
78 return Triviality::NonTrivial;
79 }
80
81 if self.restrictions.is_empty() {
82 return Triviality::Tautology;
83 }
84
85 let mut triviality = Triviality::Tautology;
86
87 for restriction in &self.restrictions {
88 match restriction.triviality() {
89 Triviality::NonTrivial => return Triviality::NonTrivial,
90 Triviality::Contradiction => triviality = Triviality::Contradiction,
91 Triviality::Tautology => (),
92 }
93 }
94
95 triviality
96 }
97 fn keyword_is_match<'card, 'cardvec, 'query, T, I>(
98 &'query self,
99 keyword: &'card Keyword,
100 cards: &'cardvec I,
101 cache: &'query mut Cache<&'card T>,
102 ) -> bool
103 where
104 T: Read + 'card + Clone,
105 &'card T: Read,
106 I: Iterator<Item = &'card T> + Clone,
107 {
108 if keyword.name == "devours" {
109 if let Some(KeywordData::CardId(ref devoured_id)) = keyword.data {
110 return matches_query(devoured_id.as_ref(), self, cards, cache).is_true();
111 }
112 }
113
114 false
115 }
116}
117
118impl Display for Query {
119 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
120 let mut text_properties = vec![];
121 for restriction in &self.restrictions {
122 text_properties.push(format!("{restriction}"));
123 }
124 match &self.sort {
125 Sort::None => (),
126 Sort::Fuzzy if !self.name.is_empty() => {
127 text_properties.push("sorted by fuzzy match".to_string());
128 }
129 Sort::Fuzzy => text_properties.push("sorted by Name in Ascending order".to_owned()),
130 Sort::Alphabet(property, order) => {
131 text_properties.push(format!("sorted by {property} in {order} order"));
132 }
133 Sort::Numeric(property, order) => {
134 text_properties.push(format!("sorted by {property} in {order} order"));
135 }
136 }
137
138 let text_properties = text_properties.into_iter().reduce(|mut acc, el| {
139 write!(&mut acc, ", {el}").unwrap();
140 acc
141 });
142 match text_properties {
143 Some(text_properties) => write!(f, "Cards {text_properties}"),
144 None => write!(f, "Cards"),
145 }
146 }
147}
148
149#[allow(clippy::match_wildcard_for_single_variants)]
150impl Display for QueryRestriction {
151 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
152 match self {
153 Self::DevouredBy(devourer) => {
154 write!(f, "which are devoured by [{devourer}]")
155 }
156 Self::Fuzzy(text) => write!(f, "with \"{text}\" written on them"),
157 Self::Devours(devourees, _) => write!(f, "that devour [{devourees}]"),
158 Self::NumberComparison(property, comparison) => {
159 write!(f, "with {property} {comparison}")
160 }
161 Self::TextComparison(property, text) => match text {
162 TextComparison::Contains(text) => write!(f, "whose {property} contains \"{text}\""),
163 TextComparison::HasMatch(regex) => write!(f, "whose {property} matches /{regex}/"),
164 TextComparison::EqualTo(text) => {
165 write!(f, "whose {property} is equal to \"{text}\"")
166 }
167 },
168 Self::Has(property, text) => match property {
169 Array::Functions => match text {
170 TextComparison::Contains(text) => {
171 write!(f, "which can be used to \"{text}\"")
172 }
173 TextComparison::EqualTo(text) => write!(f, "which can be used to \"{text}\""),
174 TextComparison::HasMatch(regex) => write!(f, "which can be used to /{regex}/"),
175 },
176 },
177 Self::HasKw(keyword) => match keyword {
178 TextComparison::Contains(text) => {
179 write!(f, "with a \"{text}\" keyword")
180 }
181 TextComparison::EqualTo(text) => {
182 write!(f, "with exactly a \"{text}\" keyword")
183 }
184 TextComparison::HasMatch(regex) => {
185 write!(f, "with a /{regex}/ keyword")
186 }
187 },
188 Self::Not(query) => write!(f, "that aren't [{query}]"),
189 Self::LenientNot(query) => write!(
190 f,
191 "that aren't [{query}], counting lacks of a property as a non-match"
192 ),
193 Self::Group(query) => write!(f, "which are [{query}]"),
194 Self::Or(a, b) => write!(f, "which are [{a}] or [{b}]"),
195 Self::Xor(a, b) => write!(f, "which are [{a}] xor [{b}] but not both"),
196 Self::KinComparison(comparison) => match comparison {
197 KinComparison::Equal(kin) => write!(f, "whose kin is exactly {kin}"),
198 KinComparison::Similar(kin) => write!(f, "whose kin is {kin}"),
199 KinComparison::TextContains(kin) => write!(f, "whose kin is \"{kin}\""),
200 KinComparison::TextEqual(kin) => write!(f, "whose kin is exactly \"{kin}\""),
201 KinComparison::RegexMatch(regex) => write!(f, "whose kin matches /{regex}/"),
202 },
203 }
204 }
205}
206
207#[derive(Debug, Clone, Serialize, Deserialize)]
208pub enum TextComparison {
209 Contains(String),
210 EqualTo(String),
211 #[serde(with = "serde_regex")]
212 HasMatch(Regex),
213}
214
215impl TextComparison {
216 pub fn is_match(&self, other: &'_ str) -> bool {
217 let clean_other = clean_ascii(other);
218 match self {
219 TextComparison::Contains(self_text) => clean_other.contains(&clean_ascii(self_text)),
220 TextComparison::EqualTo(self_text) => clean_ascii(self_text) == clean_other,
221 TextComparison::HasMatch(regex) => regex.is_match(&clean_other),
222 }
223 }
224}
225
226#[derive(Debug, Clone, Serialize, Deserialize)]
228pub enum QueryRestriction {
229 Fuzzy(String),
230 Devours(Query, ComparisonKind),
231 DevouredBy(Query),
232 NumberComparison(Number, Comparison),
233 TextComparison(Text, TextComparison),
234 Has(Array, TextComparison),
235 KinComparison(KinComparison),
236 HasKw(TextComparison),
237 Not(Box<QueryRestriction>),
238 LenientNot(Box<QueryRestriction>),
239 Group(Query),
240 Or(Box<QueryRestriction>, Box<QueryRestriction>),
241 Xor(Box<QueryRestriction>, Box<QueryRestriction>),
242}
243
244impl QueryRestriction {
245 fn not(self) -> Self {
246 Self::Not(Box::new(self))
247 }
248 fn lnot(self) -> Self {
249 Self::LenientNot(Box::new(self))
250 }
251 fn or(self, b: Self) -> Self {
252 Self::Or(Box::new(self), Box::new(b))
253 }
254 fn xor(self, b: Self) -> Self {
255 Self::Xor(Box::new(self), Box::new(b))
256 }
257}
258
259#[derive(Debug, Clone, Copy, PartialEq, Eq)]
260enum Triviality {
261 NonTrivial,
262 Contradiction,
263 Tautology,
264}
265
266impl QueryRestriction {
267 fn triviality(&self) -> Triviality {
268 match self {
269 QueryRestriction::NumberComparison(Number::Cost, Comparison::LessThan(0)) => {
270 Triviality::Contradiction
271 }
272 QueryRestriction::NumberComparison(Number::Cost, Comparison::GreaterThanOrEqual(0)) => {
273 Triviality::Tautology
274 }
275 QueryRestriction::Not(query) => match query.triviality() {
276 Triviality::Tautology => Triviality::Contradiction,
277 Triviality::Contradiction => Triviality::Tautology,
278 Triviality::NonTrivial => Triviality::NonTrivial,
279 },
280 QueryRestriction::Group(query) => query.triviality(),
281 QueryRestriction::Or(query, query1) => {
282 match (query.triviality(), query1.triviality()) {
283 (Triviality::NonTrivial, _) => Triviality::NonTrivial,
284 (_, Triviality::NonTrivial) => Triviality::NonTrivial,
285 (Triviality::Contradiction, Triviality::Contradiction) => {
286 Triviality::Contradiction
287 }
288 (Triviality::Contradiction, Triviality::Tautology) => Triviality::Tautology,
289 (Triviality::Tautology, Triviality::Contradiction) => Triviality::Tautology,
290 (Triviality::Tautology, Triviality::Tautology) => Triviality::Tautology,
291 }
292 }
293 QueryRestriction::Xor(query, query1) => {
294 match (query.triviality(), query1.triviality()) {
295 (Triviality::NonTrivial, _) => Triviality::NonTrivial,
296 (_, Triviality::NonTrivial) => Triviality::NonTrivial,
297 (Triviality::Contradiction, Triviality::Contradiction) => {
298 Triviality::Contradiction
299 }
300 (Triviality::Contradiction, Triviality::Tautology) => Triviality::Tautology,
301 (Triviality::Tautology, Triviality::Contradiction) => Triviality::Tautology,
302 (Triviality::Tautology, Triviality::Tautology) => Triviality::Contradiction,
303 }
304 }
305 _ => Triviality::NonTrivial,
306 }
307 }
308 fn is_match<'card, 'cardvec, 'query, C, T, I>(
309 &'query self,
310 card: &'card C,
311 query: &'query Query,
312 cards: &'cardvec I,
313 cache: &'query mut Cache<&'card T>,
314 ) -> Ternary
315 where
316 C: Read,
317 T: Read + 'card + Clone,
318 &'card T: Read,
319 I: Iterator<Item = &'card T> + Clone,
320 {
321 match self {
322 QueryRestriction::TextComparison(property, comparison) => card
323 .get_text_property(property)
324 .map_or(Ternary::Void, |property| {
325 comparison.is_match(&property).into()
326 }),
327 QueryRestriction::Xor(group1, group2) => {
328 let res1 = group1.is_match(card, query, cards, cache);
329 let res2 = group2.is_match(card, query, cards, cache);
330 res1.xor(res2)
331 }
332 QueryRestriction::Or(group1, group2) => {
333 let res1 = group1.is_match(card, query, cards, cache);
334 let res2 = group2.is_match(card, query, cards, cache);
335 res1.or(res2)
336 }
337 QueryRestriction::Group(group) => matches_query(card, group, cards, cache),
338 QueryRestriction::Fuzzy(x) => fuzzy(card, x).into(),
339
340 QueryRestriction::NumberComparison(field, comparison) => {
341 comparison.compare(&card.get_num_property(field))
342 }
343 QueryRestriction::Has(property, comparison) => card
344 .get_vec_property(property)
345 .map_or(Ternary::Void, |field| {
346 field.iter().any(|x| comparison.is_match(x)).into()
347 }),
348 QueryRestriction::HasKw(comparison) => {
349 card.get_keywords().map_or(Ternary::Void, |keyword| {
350 keyword.iter().any(|x| comparison.is_match(&x.name)).into()
351 })
352 }
353 QueryRestriction::Not(res) => !res.is_match(card, query, cards, cache),
354 QueryRestriction::LenientNot(res) => match res.is_match(card, query, cards, cache) {
355 Ternary::Void => Ternary::True,
356 Ternary::False => Ternary::True,
357 Ternary::True => Ternary::False,
358 },
359 QueryRestriction::Devours(devours, comparison) => match comparison {
360 ComparisonKind::Contains => devours_match(card, cards, devours, cache),
361 ComparisonKind::Equals => card.get_keywords().map_or(Ternary::Void, |keyword| {
362 keyword
363 .iter()
364 .any(|keyword| devours.keyword_is_match(keyword, cards, cache))
365 .into()
366 }),
367 },
368 QueryRestriction::DevouredBy(devoured_by) => {
369 devouredby_match(card, cards, query, devoured_by, cache)
370 }
371 QueryRestriction::KinComparison(comparison) => card
372 .get_kin()
373 .map_or(Ternary::Void, |kin| comparison.is_match(*kin).into()),
374 }
375 }
376}
377
378fn devouredby_match<'card, 'cardvec, 'query, T, I, C>(
379 card: &'card C,
380 cards: &'cardvec I,
381 query: &'query Query,
382 devoured_by: &'query Query,
383 cache: &'query mut Cache<&'card T>,
384) -> Ternary
385where
386 C: Read,
387 T: Read + 'card + Clone,
388 &'card T: Read,
389 I: Iterator<Item = &'card T> + Clone,
390{
391 let key = format!("{devoured_by}");
392 let devoured_cards;
393 let maybe_devourees = cache.devouredby.get(&key).cloned();
394 if let Some(value) = maybe_devourees {
395 devoured_cards = value;
396 } else {
397 let devourers = cards
398 .clone()
399 .filter(|card| matches_query(*card, devoured_by, cards, cache).is_true());
400
401 let mut no_devourers = true;
402 let mut queries = vec![];
403
404 for devourer in devourers {
405 if let Some(Keyword {
406 name: _,
407 data: Some(KeywordData::CardId(card_id)),
408 }) = devourer
409 .get_keywords()
410 .and_then(|x| x.iter().find(|x| x.name == "devours"))
411 {
412 no_devourers = false;
413 queries.push(QueryRestriction::Group(Query {
414 name: String::new(),
415 restrictions: card_id.get_as_query(),
416 sort: Sort::None,
417 }));
418 }
419 }
420
421 if no_devourers {
422 devoured_cards = vec![];
423 cache.devouredby.insert(key, vec![]);
424 } else {
425 let devourees_restriction = queries
426 .into_iter()
427 .reduce(|first, second| first.or(second))
428 .map(|x| vec![x])
429 .unwrap_or_default();
430
431 let devourees_query = Query {
432 name: query.name.clone(),
433 restrictions: devourees_restriction,
434 sort: query.sort,
435 };
436
437 devoured_cards = search(&devourees_query, cards.clone());
438 cache.devouredby.insert(key, devoured_cards.clone());
439 }
440 }
441 devoured_cards
442 .iter()
443 .any(|x| x.get_name() == card.get_name())
444 .into()
445}
446
447fn devours_match<'card, 'cardvec, 'query, T, I, C>(
448 card: &'card C,
449 cards: &'cardvec I,
450 devourees: &'query Query,
451 cache: &'query mut Cache<&'card T>,
452) -> Ternary
453where
454 C: Read,
455 T: Read + 'card + Clone,
456 &'card T: Read,
457 I: Iterator<Item = &'card T> + Clone,
458{
459 let devourees = cache
460 .devourers
461 .entry(devourees.to_string())
462 .or_insert(search(devourees, cards.clone()));
463
464 if let Some(Keyword {
465 name: _,
466 data: Some(KeywordData::CardId(card_id)),
467 }) = card
468 .get_keywords()
469 .and_then(|x| x.iter().find(|x| x.name == "devours"))
470 {
471 let query = Query::from_restrictions(card_id.get_as_query());
472 let specific_devourees = search(&query, cards.clone());
473 if specific_devourees
474 .into_iter()
475 .any(|x| devourees.iter().any(|y| x.get_name() == y.get_name()))
476 {
477 return Ternary::True;
478 }
479 }
480
481 Ternary::False
482}
483
484#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
486pub enum Ordering {
487 Ascending,
488 Descending,
489}
490
491impl Display for Ordering {
492 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
493 match self {
494 Self::Ascending => write!(f, "Ascending"),
495 Self::Descending => write!(f, "Descending"),
496 }
497 }
498}
499
500#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
502pub enum Sort {
503 None,
505 Fuzzy,
507 Alphabet(Text, Ordering),
508 Numeric(Number, Ordering),
509}
510
511#[must_use]
513pub fn fuzzy(card: &impl Read, query: &str) -> bool {
514 card.get_description()
515 .is_some_and(|x| clean_ascii(x).contains(&clean_ascii(query)))
516 || card
517 .get_name()
518 .is_some_and(|x| clean_ascii(x).contains(&clean_ascii(query)))
519 || card
520 .get_type()
521 .is_some_and(|x| clean_ascii(x).contains(&clean_ascii(query)))
522 || card
523 .get_kin()
524 .is_some_and(|x| clean_ascii(x.get_name()).contains(&clean_ascii(query)))
525 || card.get_keywords().is_some_and(|x| {
526 x.iter()
527 .any(|x| clean_ascii(&x.name).contains(&clean_ascii(query)))
528 })
529}
530
531pub type CachePart<T> = HashMap<String, Vec<T>>;
533
534#[derive(Default)]
535struct Cache<T> {
536 devouredby: CachePart<T>,
537 devourers: CachePart<T>,
538}
539
540#[derive(Deserialize)]
541enum SearchError {
542 ParseError,
543 FromJsError,
544}
545
546#[cfg(target_arch = "wasm32")]
547#[wasm_bindgen]
548pub fn search_wasm(query: &str, cards: JsValue) -> Result<Vec<JsValue>, String> {
549 console_error_panic_hook::set_once();
550 let cards: Vec<Card> =
551 serde_wasm_bindgen::from_value(cards).map_err(|_| "FromJsError".to_string())?;
552 let query: Query = parse_query(query).map_err(|_| "ParseError".to_string())?;
553
554 let matches = search(&query, cards.iter());
555 let mut out = vec![];
556
557 for card in matches {
558 let card = serde_wasm_bindgen::to_value(&card).map_err(|_| "ToJsError".to_string())?;
559
560 out.push(card);
561 }
562
563 Ok(out)
564}
565
566#[must_use]
568pub fn search<'card, 'query, C, I>(query: &'query Query, cards: I) -> Vec<&'card C>
569where
570 C: Read + Clone + 'card,
571 I: IntoIterator<Item = &'card C> + Clone + 'query,
572 I::IntoIter: Clone,
573 &'card C: Read,
574{
575 let mut results: Vec<_> = match query.triviality() {
576 Triviality::NonTrivial => {
577 let iter = cards.into_iter();
578 let iter_clone = iter.clone();
579 let mut cache = Cache {
580 devouredby: HashMap::new(),
581 devourers: HashMap::new(),
582 };
583 iter.filter(|card| {
584 matches_query(*card, query, &iter_clone, &mut cache) == Ternary::True
585 })
586 .collect()
587 }
588 Triviality::Contradiction => return vec![],
589 Triviality::Tautology => cards.into_iter().collect(),
590 };
591
592 match &query.sort {
593 Sort::None => (),
594 Sort::Fuzzy if !query.name.is_empty() => results.sort_by(|a, b| {
595 weighted_compare(b, &query.name)
596 .partial_cmp(&weighted_compare(a, &query.name))
597 .unwrap_or(std::cmp::Ordering::Equal)
598 }),
599 Sort::Fuzzy => results.sort_by(|a, b| Ord::cmp(&a.get_name(), &b.get_name())),
600 Sort::Alphabet(property, Ordering::Ascending) => results.sort_by(|a, b| {
601 Ord::cmp(
602 &a.get_text_property(property),
603 &b.get_text_property(property),
604 )
605 }),
606 Sort::Numeric(property, Ordering::Ascending) => results.sort_by(|a, b| {
607 ImpreciseOrd::imprecise_cmp(
608 &a.get_num_property(property),
609 &b.get_num_property(property),
610 )
611 }),
612 Sort::Alphabet(property, Ordering::Descending) => {
613 results.sort_by(|a, b| {
614 Ord::cmp(
615 &a.get_text_property(property),
616 &b.get_text_property(property),
617 )
618 .reverse()
619 });
620 }
621 Sort::Numeric(property, Ordering::Descending) => results.sort_by(|a, b| {
622 ImpreciseOrd::imprecise_cmp(
623 &a.get_num_property(property),
624 &b.get_num_property(property),
625 )
626 .reverse()
627 }),
628 }
629
630 results
631}
632
633#[allow(clippy::too_many_lines)]
638fn matches_query<'card, 'cardvec, 'query, C, T, I>(
639 card: &'card C,
640 query: &'query Query,
641 cards: &'cardvec I,
642 cache: &'query mut Cache<&'card T>,
643) -> Ternary
644where
645 C: Read,
646 T: Read + 'card + Clone,
647 &'card T: Read,
648 I: Iterator<Item = &'card T> + Clone,
649{
650 let mut filtered = Ternary::True;
651 for res in &query.restrictions {
652 match res.triviality() {
653 Triviality::NonTrivial => {
654 filtered = filtered.and(res.is_match(card, query, cards, cache))
655 }
656 Triviality::Contradiction => filtered = filtered.and(Ternary::False),
657 Triviality::Tautology => filtered = filtered.and(Ternary::True),
658 }
659 }
660 filtered
661}
662
663pub trait QueryFrom {
664 fn get_as_query(&self) -> Vec<QueryRestriction>;
667}
668
669impl QueryFrom for CardId {
670 fn get_as_query(&self) -> Vec<QueryRestriction> {
671 let mut restrictions = vec![];
672
673 if let Some(name) = &self.name {
674 restrictions.push(QueryRestriction::TextComparison(
675 Text::Name,
676 TextComparison::EqualTo(name.clone()),
677 ));
678 }
679
680 if let Some(kin) = &self.kin {
681 restrictions.push(QueryRestriction::KinComparison(KinComparison::Equal(*kin)));
682 }
683
684 if let Some(keywords) = &self.keywords {
685 for keyword in keywords {
686 let keyword = TextComparison::EqualTo(keyword.name.clone());
687 restrictions.push(QueryRestriction::HasKw(keyword));
688 }
689 }
690
691 if let Some(cost) = &self.cost {
692 restrictions.push(QueryRestriction::NumberComparison(
693 Number::Cost,
694 cost.as_comparison(),
695 ));
696 }
697
698 if let Some(health) = &self.health {
699 restrictions.push(QueryRestriction::NumberComparison(
700 Number::Health,
701 health.as_comparison(),
702 ));
703 }
704
705 if let Some(power) = &self.power {
706 restrictions.push(QueryRestriction::NumberComparison(
707 Number::Power,
708 power.as_comparison(),
709 ));
710 }
711
712 if let Some(defense) = &self.defense {
713 restrictions.push(QueryRestriction::NumberComparison(
714 Number::Defense,
715 defense.as_comparison(),
716 ));
717 }
718
719 restrictions
720 }
721}
722
723#[derive(Debug, Clone, Serialize, Deserialize)]
724pub enum ComparisonKind {
725 Contains,
726 Equals,
727}
728
729#[cfg(test)]
730mod test {
731 use crate::*;
732 use hemoglobin::cards::Card;
733 use query_parser::parse_query;
734
735 #[test]
736 fn test_equals_search() {
737 let cards: Vec<Card> = serde_json::from_str(
738 &std::fs::read_to_string("tests/search.json").expect("Couldn't load search.json"),
739 )
740 .expect("Couldn't convert search.json to a vec of cards");
741 for val in 4..=6 {
742 let result = search(
743 &parse_query(&format!("c={val}")).expect("couldn't parse query"),
744 cards.iter(),
745 );
746
747 let fail = match val {
748 4 => result
749 .iter()
750 .any(|x| x.name == "eq 5" || x.name == "gteq 5" || x.name == "gt 5"),
751 5 => result
752 .iter()
753 .any(|x| x.name == "lt 5" || x.name == "gt 5" || x.name == "neq 5"),
754 6 => result
755 .iter()
756 .any(|x| x.name == "lt 5" || x.name == "lteq 5" || x.name == "eq 5"),
757 _ => unreachable!(),
758 };
759
760 assert!(!fail);
761 }
762 }
763
764 #[test]
765 fn test_gt_search() {
766 let cards: Vec<Card> = serde_json::from_str(
767 &std::fs::read_to_string("tests/search.json").expect("Couldn't load search.json"),
768 )
769 .expect("Couldn't convert search.json to a vec of cards");
770 for val in 4..=6 {
771 let result = search(
772 &parse_query(&format!("c>{val}")).expect("couldn't parse query"),
773 cards.iter(),
774 );
775
776 let fail = match val {
777 4 => result.iter().any(|x| x.name == "lt 5"),
778 5 => result
779 .iter()
780 .any(|x| x.name == "lt 5" || x.name == "lteq 5" || x.name == "eq 5"),
781 6 => result
782 .iter()
783 .any(|x| x.name == "lt 5" || x.name == "lteq 5" || x.name == "eq 5"),
784 _ => unreachable!(),
785 };
786
787 assert!(!fail);
788 }
789 }
790
791 #[test]
792 fn test_gteq_search() {
793 let cards: Vec<Card> = serde_json::from_str(
794 &std::fs::read_to_string("tests/search.json").expect("Couldn't load search.json"),
795 )
796 .expect("Couldn't convert search.json to a vec of cards");
797
798 for val in 4..=6 {
799 let result = search(
800 &parse_query(&format!("c>={val}")).expect("couldn't parse query"),
801 cards.iter(),
802 );
803
804 let fail = match val {
805 4 => false,
806 5 => result.iter().any(|x| x.name == "lt 5"),
807 6 => result
808 .iter()
809 .any(|x| x.name == "lt 5" || x.name == "lteq 5" || x.name == "eq 5"),
810 _ => unreachable!(),
811 };
812
813 assert!(!fail);
814 }
815 }
816
817 #[test]
818 fn search_all() {
819 let cards: Vec<Card> = serde_json::from_str(
820 &std::fs::read_to_string("tests/search.json").expect("Couldn't load search.json"),
821 )
822 .expect("Couldn't convert search.json to a vec of cards");
823
824 let query = parse_query("()").expect("Could not parse query");
825 let results = search(&query, &cards);
826
827 assert_eq!(cards.len(), results.len())
828 }
829
830 #[test]
831 fn no_fuzzy_confusion_avoiding_correct_parse() {
832 let query = parse_query("c=0").expect("Could not parse query");
833 assert!(query.name.is_empty());
834 let query = parse_query("()").expect("Could not parse query");
835 assert!(query.name.is_empty());
836 }
837
838 #[test]
839 fn no_fuzzy_confusion_avoiding_failure() {
840 let _ = parse_query("c=").expect_err("Query should not be parseable");
841 let _ = parse_query("power<").expect_err("Query should not be parseable");
842 let _ = parse_query(r#"d="awawa"#).expect("Query should be parseable");
843 }
844}