initiative_core/command/token/
sequence.rs

1use crate::app::AppMeta;
2use crate::command::prelude::*;
3
4use std::iter;
5use std::pin::Pin;
6
7use async_stream::stream;
8use futures::prelude::*;
9
10pub fn match_input<'a, 'b>(
11    token: &'a Token,
12    input: &'a str,
13    app_meta: &'b AppMeta,
14) -> Pin<Box<dyn Stream<Item = FuzzyMatch<'a>> + 'b>>
15where
16    'a: 'b,
17{
18    let tokens = if let TokenType::Sequence(tokens) = &token.token_type {
19        tokens
20    } else {
21        unreachable!();
22    };
23
24    match_input_with_tokens(token, input, app_meta, tokens)
25}
26
27pub fn match_input_with_tokens<'a, 'b>(
28    token: &'a Token,
29    input: &'a str,
30    app_meta: &'b AppMeta,
31    tokens: &'a [Token],
32) -> Pin<Box<dyn Stream<Item = FuzzyMatch<'a>> + 'b>>
33where
34    'a: 'b,
35{
36    if tokens.is_empty() {
37        // No tokens, no matches
38        Box::pin(stream::iter([FuzzyMatch::Overflow(
39            token.into(),
40            input.into(),
41        )]))
42    } else {
43        Box::pin(stream! {
44            // TokenMatch the first token in the sequence
45            for await fuzzy_match in tokens[0].match_input(input, app_meta) {
46                match fuzzy_match {
47
48                    // First token is a partial match, so the sequence is incomplete
49                    FuzzyMatch::Partial(token_match, completion) => {
50                        yield FuzzyMatch::Partial(
51                            TokenMatch::new(token, vec![token_match]),
52                            completion,
53                        );
54                    }
55
56                    // First token is an exact match and is the only token in the sequence, so the
57                    // sequence is also an exact match.
58                    FuzzyMatch::Exact(token_match) if tokens.len() == 1 => {
59                        yield FuzzyMatch::Exact(
60                            TokenMatch::new(token, vec![token_match]),
61                        );
62                    }
63
64                    // First token is an exact match but there are more unmatched tokens, so the
65                    // sequence is incomplete.
66                    FuzzyMatch::Exact(token_match) => {
67                        yield FuzzyMatch::Partial(
68                            TokenMatch::new(token, vec![token_match]),
69                            None,
70                        );
71                    }
72
73                    // First token overflows and is the only token in the sequence, so the sequence
74                    // also overflows.
75                    FuzzyMatch::Overflow(token_match, remainder) if tokens.len() == 1 => {
76                        yield FuzzyMatch::Overflow(
77                            TokenMatch::new(token, vec![token_match]),
78                            remainder,
79                        );
80                    }
81
82                    // First token overflows but there are other tokens in the sequence, so we
83                    // recurse with the remainder of the sequence.
84                    FuzzyMatch::Overflow(token_match, remainder) => {
85                        for await next_fuzzy_match in match_input_with_tokens(token, remainder.as_str(), app_meta, &tokens[1..]) {
86                            yield next_fuzzy_match.map(|next_match| {
87                                TokenMatch::new(
88                                    token,
89                                    iter::once(token_match.clone())
90                                        .chain(
91                                            next_match
92                                                .match_meta
93                                                .into_sequence()
94                                                .unwrap()
95                                                .into_iter(),
96                                        )
97                                        .collect::<Vec<_>>(),
98                                )
99                            });
100                        }
101                    }
102                }
103            }
104        })
105    }
106}
107
108#[cfg(test)]
109mod test {
110    use super::*;
111
112    use crate::test_utils as test;
113
114    #[derive(Hash)]
115    enum Marker {
116        Token,
117    }
118
119    #[tokio::test]
120    async fn match_input_test_empty() {
121        let sequence_token = sequence_m(Marker::Token, []);
122        test::assert_eq_unordered!(
123            [FuzzyMatch::Overflow(
124                TokenMatch::from(&sequence_token),
125                "badger".into()
126            )],
127            sequence_token
128                .match_input("badger", &test::app_meta())
129                .collect::<Vec<_>>()
130                .await,
131        );
132    }
133
134    #[tokio::test]
135    async fn match_input_test_exact() {
136        let tokens = [keyword("badger"), keyword("mushroom"), keyword("snake")];
137        let sequence_token = sequence(tokens.clone());
138
139        test::assert_eq_unordered!(
140            [FuzzyMatch::Exact(TokenMatch::new(
141                &sequence_token,
142                vec![
143                    TokenMatch::from(&tokens[0]),
144                    TokenMatch::from(&tokens[1]),
145                    TokenMatch::from(&tokens[2]),
146                ],
147            ))],
148            sequence_token
149                .match_input("badger mushroom snake", &test::app_meta())
150                .collect::<Vec<_>>()
151                .await,
152        );
153    }
154
155    #[tokio::test]
156    async fn match_input_test_incomplete() {
157        let tokens = [keyword("badger"), keyword("mushroom"), keyword("snake")];
158        let sequence_token = sequence(tokens.clone());
159
160        test::assert_eq_unordered!(
161            [FuzzyMatch::Partial(
162                TokenMatch::new(
163                    &sequence_token,
164                    vec![TokenMatch::from(&tokens[0]), TokenMatch::from(&tokens[1])],
165                ),
166                None,
167            )],
168            sequence_token
169                .match_input("badger mushroom", &test::app_meta())
170                .collect::<Vec<_>>()
171                .await,
172        );
173    }
174
175    #[tokio::test]
176    async fn match_input_test_partial() {
177        let tokens = [keyword("badger"), keyword("mushroom"), keyword("snake")];
178        let sequence_token = sequence(tokens.clone());
179
180        test::assert_eq_unordered!(
181            [FuzzyMatch::Partial(
182                TokenMatch::new(
183                    &sequence_token,
184                    vec![
185                        TokenMatch::from(&tokens[0]),
186                        TokenMatch::from(&tokens[1]),
187                        TokenMatch::from(&tokens[2]),
188                    ],
189                ),
190                Some("ake".to_string()),
191            )],
192            sequence_token
193                .match_input("badger mushroom sn", &test::app_meta())
194                .collect::<Vec<_>>()
195                .await,
196        );
197    }
198
199    #[tokio::test]
200    async fn match_input_test_overflowing() {
201        let tokens = [keyword("badger"), keyword("mushroom"), keyword("snake")];
202        let sequence_token = sequence_m(Marker::Token, tokens.clone());
203
204        test::assert_eq_unordered!(
205            [FuzzyMatch::Overflow(
206                TokenMatch::new(
207                    &sequence_token,
208                    vec![
209                        TokenMatch::from(&tokens[0]),
210                        TokenMatch::from(&tokens[1]),
211                        TokenMatch::from(&tokens[2]),
212                    ],
213                ),
214                " hippopotumus".into(),
215            )],
216            sequence_token
217                .match_input("badger mushroom snake hippopotumus", &test::app_meta())
218                .collect::<Vec<_>>()
219                .await,
220        );
221    }
222
223    #[tokio::test]
224    async fn match_input_test_with_any_phrase() {
225        let tokens = [keyword("badger"), any_phrase(), any_word()];
226        let [keyword_token, any_phrase_token, any_word_token] = tokens.clone();
227        let sequence_token = sequence(tokens);
228
229        test::assert_eq_unordered!(
230            [
231                FuzzyMatch::Overflow(
232                    TokenMatch::new(
233                        &sequence_token,
234                        vec![
235                            TokenMatch::from(&keyword_token),
236                            TokenMatch::new(&any_phrase_token, "badger"),
237                            TokenMatch::new(&any_word_token, "badger"),
238                        ],
239                    ),
240                    " mushroom".into(),
241                ),
242                FuzzyMatch::Exact(TokenMatch::new(
243                    &sequence_token,
244                    vec![
245                        TokenMatch::from(&keyword_token),
246                        TokenMatch::new(&any_phrase_token, "badger badger"),
247                        TokenMatch::new(&any_word_token, "mushroom"),
248                    ],
249                )),
250                FuzzyMatch::Partial(
251                    TokenMatch::new(
252                        &sequence_token,
253                        vec![
254                            TokenMatch::from(&keyword_token),
255                            TokenMatch::new(&any_phrase_token, "badger badger mushroom"),
256                        ],
257                    ),
258                    None,
259                ),
260            ],
261            sequence_token
262                .match_input("badger badger badger mushroom", &test::app_meta())
263                .collect::<Vec<_>>()
264                .await,
265        );
266    }
267}