skimmer/engine/
factory.rs

1use crate::engine::all::MatchAllEngine;
2use crate::engine::andor::{AndEngine, OrEngine};
3use crate::engine::exact::{ExactEngine, ExactMatchingParam};
4use crate::engine::fuzzy::{FuzzyAlgorithm, FuzzyEngine};
5use crate::engine::regexp::RegexEngine;
6use crate::item::RankBuilder;
7use crate::{CaseMatching, MatchEngine, MatchEngineFactory};
8use regex::Regex;
9use std::sync::Arc;
10
11lazy_static! {
12    static ref RE_AND: Regex = Regex::new(r"([^ |]+( +\| +[^ |]*)+)|( +)").unwrap();
13    static ref RE_OR: Regex = Regex::new(r" +\| +").unwrap();
14}
15//------------------------------------------------------------------------------
16// Exact engine factory
17pub struct ExactOrFuzzyEngineFactory {
18    exact_mode: bool,
19    fuzzy_algorithm: FuzzyAlgorithm,
20    rank_builder: Arc<RankBuilder>,
21}
22
23impl ExactOrFuzzyEngineFactory {
24    #[must_use]
25    pub fn builder() -> Self {
26        Self {
27            exact_mode: false,
28            fuzzy_algorithm: FuzzyAlgorithm::SkimV2,
29            rank_builder: Default::default(),
30        }
31    }
32
33    #[must_use]
34    pub fn exact_mode(mut self, exact_mode: bool) -> Self {
35        self.exact_mode = exact_mode;
36        self
37    }
38
39    #[must_use]
40    pub fn fuzzy_algorithm(mut self, fuzzy_algorithm: FuzzyAlgorithm) -> Self {
41        self.fuzzy_algorithm = fuzzy_algorithm;
42        self
43    }
44
45    #[must_use]
46    pub fn rank_builder(mut self, rank_builder: Arc<RankBuilder>) -> Self {
47        self.rank_builder = rank_builder;
48        self
49    }
50
51    #[must_use]
52    pub fn build(self) -> Self {
53        self
54    }
55}
56
57impl MatchEngineFactory for ExactOrFuzzyEngineFactory {
58    #[must_use]
59    fn create_engine_with_case(&self, query: &str, case: CaseMatching) -> Box<dyn MatchEngine> {
60        // 'abc => match exact "abc"
61        // ^abc => starts with "abc"
62        // abc$ => ends with "abc"
63        // ^abc$ => match exact "abc"
64        // !^abc => items not starting with "abc"
65        // !abc$ => items not ending with "abc"
66        // !^abc$ => not "abc"
67
68        let mut query = query;
69        let mut exact = false;
70        let mut param = ExactMatchingParam::default();
71        param.case = case;
72
73        if query.starts_with('\'') {
74            if self.exact_mode {
75                return Box::new(
76                    FuzzyEngine::builder()
77                        .query(&query[1..])
78                        .algorithm(self.fuzzy_algorithm)
79                        .case(case)
80                        .rank_builder(self.rank_builder.clone())
81                        .build(),
82                );
83            }
84            exact = true;
85            query = &query[1..];
86        }
87
88        if query.starts_with('!') {
89            query = &query[1..];
90            exact = true;
91            param.inverse = true;
92        }
93
94        if query.is_empty() {
95            // if only "!" was provided, will still show all items
96            return Box::new(
97                MatchAllEngine::builder()
98                    .rank_builder(self.rank_builder.clone())
99                    .build(),
100            );
101        }
102
103        if query.starts_with('^') {
104            query = &query[1..];
105            exact = true;
106            param.prefix = true;
107        }
108
109        if query.ends_with('$') {
110            query = &query[..(query.len() - 1)];
111            exact = true;
112            param.postfix = true;
113        }
114
115        if self.exact_mode {
116            exact = true;
117        }
118
119        if exact {
120            Box::new(
121                ExactEngine::builder(query, param)
122                    .rank_builder(self.rank_builder.clone())
123                    .build(),
124            )
125        } else {
126            Box::new(
127                FuzzyEngine::builder()
128                    .query(query)
129                    .algorithm(self.fuzzy_algorithm)
130                    .case(case)
131                    .rank_builder(self.rank_builder.clone())
132                    .build(),
133            )
134        }
135    }
136}
137
138//------------------------------------------------------------------------------
139pub struct AndOrEngineFactory {
140    inner: Box<dyn MatchEngineFactory>,
141}
142
143impl AndOrEngineFactory {
144    #[must_use]
145    pub fn new(factory: impl MatchEngineFactory + 'static) -> Self {
146        Self {
147            inner: Box::new(factory),
148        }
149    }
150
151    // we want to treat `\ ` as plain white space
152    // regex crate doesn't support look around, so I use a lazy workaround
153    // that replace `\ ` with `\0` ahead of split and replace it back afterwards
154    #[must_use]
155    fn parse_or(&self, query: &str, case: CaseMatching) -> Box<dyn MatchEngine> {
156        if query.trim().is_empty() {
157            self.inner.create_engine_with_case(query, case)
158        } else {
159            let engines = RE_OR
160                .split(&self.mask_escape_space(query))
161                .map(|q| self.parse_and(q, case))
162                .collect();
163            Box::new(OrEngine::builder().engines(engines).build())
164        }
165    }
166
167    #[must_use]
168    fn parse_and(&self, query: &str, case: CaseMatching) -> Box<dyn MatchEngine> {
169        let query_trim = query.trim_matches(|c| c == ' ' || c == '|');
170        let mut engines = vec![];
171        let mut last = 0;
172        for mat in RE_AND.find_iter(query_trim) {
173            let (start, end) = (mat.start(), mat.end());
174            let term = query_trim[last..start].trim_matches(|c| c == ' ' || c == '|');
175            let term = self.unmask_escape_space(term);
176            if !term.is_empty() {
177                engines.push(self.inner.create_engine_with_case(&term, case));
178            }
179
180            if !mat.as_str().trim().is_empty() {
181                engines.push(self.parse_or(mat.as_str().trim(), case));
182            }
183            last = end;
184        }
185
186        let term = query_trim[last..].trim_matches(|c| c == ' ' || c == '|');
187        let term = self.unmask_escape_space(term);
188        if !term.is_empty() {
189            engines.push(self.inner.create_engine_with_case(&term, case));
190        }
191        Box::new(AndEngine::builder().engines(engines).build())
192    }
193
194    #[must_use]
195    fn mask_escape_space(&self, string: &str) -> String {
196        string.replace("\\ ", "\0")
197    }
198
199    #[must_use]
200    fn unmask_escape_space(&self, string: &str) -> String {
201        string.replace('\0', " ")
202    }
203}
204
205impl MatchEngineFactory for AndOrEngineFactory {
206    #[must_use]
207    fn create_engine_with_case(&self, query: &str, case: CaseMatching) -> Box<dyn MatchEngine> {
208        self.parse_or(query, case)
209    }
210}
211
212//------------------------------------------------------------------------------
213pub struct RegexEngineFactory {
214    rank_builder: Arc<RankBuilder>,
215}
216
217impl RegexEngineFactory {
218    #[must_use]
219    pub fn builder() -> Self {
220        Self {
221            rank_builder: Arc::default(),
222        }
223    }
224
225    #[must_use]
226    pub fn rank_builder(mut self, rank_builder: Arc<RankBuilder>) -> Self {
227        self.rank_builder = rank_builder;
228        self
229    }
230
231    #[must_use]
232    pub fn build(self) -> Self {
233        self
234    }
235}
236
237impl MatchEngineFactory for RegexEngineFactory {
238    #[must_use]
239    fn create_engine_with_case(&self, query: &str, case: CaseMatching) -> Box<dyn MatchEngine> {
240        Box::new(
241            RegexEngine::builder(query, case)
242                .rank_builder(self.rank_builder.clone())
243                .build(),
244        )
245    }
246}
247
248mod test {
249    #[test]
250    fn test_engine_factory() {
251        use super::*;
252        let exact_or_fuzzy = ExactOrFuzzyEngineFactory::builder().build();
253        let x = exact_or_fuzzy.create_engine("'abc");
254        assert_eq!(format!("{}", x), "(Exact|(?i)abc)");
255
256        let x = exact_or_fuzzy.create_engine("^abc");
257        assert_eq!(format!("{}", x), "(Exact|(?i)^abc)");
258
259        let x = exact_or_fuzzy.create_engine("abc$");
260        assert_eq!(format!("{}", x), "(Exact|(?i)abc$)");
261
262        let x = exact_or_fuzzy.create_engine("^abc$");
263        assert_eq!(format!("{}", x), "(Exact|(?i)^abc$)");
264
265        let x = exact_or_fuzzy.create_engine("!abc");
266        assert_eq!(format!("{}", x), "(Exact|!(?i)abc)");
267
268        let x = exact_or_fuzzy.create_engine("!^abc");
269        assert_eq!(format!("{}", x), "(Exact|!(?i)^abc)");
270
271        let x = exact_or_fuzzy.create_engine("!abc$");
272        assert_eq!(format!("{}", x), "(Exact|!(?i)abc$)");
273
274        let x = exact_or_fuzzy.create_engine("!^abc$");
275        assert_eq!(format!("{}", x), "(Exact|!(?i)^abc$)");
276
277        let regex_factory = RegexEngineFactory::builder();
278        let and_or_factory = AndOrEngineFactory::new(exact_or_fuzzy);
279
280        let x = and_or_factory.create_engine("'abc | def ^gh ij | kl mn");
281        assert_eq!(
282            format!("{}", x),
283            "(Or: (And: (Exact|(?i)abc)), (And: (Fuzzy: def), (Exact|(?i)^gh), (Fuzzy: ij)), (And: (Fuzzy: kl), (Fuzzy: mn)))"
284        );
285
286        let x = regex_factory.create_engine("'abc | def ^gh ij | kl mn");
287        assert_eq!(format!("{}", x), "(Regex: 'abc | def ^gh ij | kl mn)");
288    }
289}