initiative_core/command/token/
any_of.rs

1use crate::app::AppMeta;
2use crate::command::prelude::*;
3
4use std::pin::Pin;
5
6use async_stream::stream;
7use futures::prelude::*;
8
9pub fn match_input<'a, 'b>(
10    token: &'a Token,
11    input: &'a str,
12    app_meta: &'b AppMeta,
13) -> Pin<Box<dyn Stream<Item = FuzzyMatch<'a>> + 'b>>
14where
15    'a: 'b,
16{
17    let tokens = if let TokenType::AnyOf(tokens) = &token.token_type {
18        tokens.iter().collect()
19    } else {
20        unreachable!();
21    };
22
23    match_input_with_tokens(token, input, app_meta, tokens)
24}
25
26pub fn match_input_with_tokens<'a, 'b>(
27    token: &'a Token,
28    input: &'a str,
29    app_meta: &'b AppMeta,
30    tokens: Vec<&'a Token>,
31) -> Pin<Box<dyn Stream<Item = FuzzyMatch<'a>> + 'b>>
32where
33    'a: 'b,
34{
35    Box::pin(stream! {
36        // Attempt to match each token in turn
37        for (test_token_index, test_token) in tokens.iter().enumerate() {
38            for await fuzzy_match in test_token.match_input(input, app_meta) {
39                match fuzzy_match {
40                    // The token is a partial match, so the phrase is incomplete.
41                    FuzzyMatch::Partial(token_match, completion) => {
42                        yield FuzzyMatch::Partial(
43                            TokenMatch::new(token, vec![token_match]),
44                            completion,
45                        );
46                    }
47
48                    // First token is an exact match, so the phrase is also an exact match.
49                    FuzzyMatch::Exact(token_match) => {
50                        yield FuzzyMatch::Exact(TokenMatch::new(token, vec![token_match]));
51                    }
52
53                    // First token overflows and is the only token in the phrase, so the phrase
54                    // also overflows.
55                    FuzzyMatch::Overflow(token_match, remainder) if tokens.len() == 1 => {
56                        yield FuzzyMatch::Overflow(
57                            TokenMatch::new(token, vec![token_match]),
58                            remainder,
59                        );
60                    }
61
62                    // First token overflows but there are other tokens in the phrase, so we
63                    // recurse with the remainder of the phrase.
64                    FuzzyMatch::Overflow(token_match, remainder) => {
65                        let remainder_str = remainder.as_str();
66
67                        yield FuzzyMatch::Overflow(
68                            TokenMatch::new(token, vec![token_match.clone()]),
69                            remainder,
70                        );
71
72                        let next_tokens: Vec<_> = tokens
73                            .iter()
74                            .take(test_token_index)
75                            .chain(tokens.iter().skip(test_token_index + 1))
76                            .copied()
77                            .collect();
78
79                        for await next_fuzzy_match in
80                            match_input_with_tokens(token, remainder_str, app_meta, next_tokens)
81                        {
82                            yield next_fuzzy_match.map(|next_match| {
83                                let mut next_meta_sequence = next_match.match_meta.into_sequence().unwrap();
84                                next_meta_sequence.insert(0, token_match.clone());
85
86                                TokenMatch::new(token, next_meta_sequence)
87                            });
88                        }
89                    }
90                }
91            }
92        }
93    })
94}
95
96#[cfg(test)]
97mod test {
98    use super::*;
99
100    use crate::test_utils as test;
101
102    #[derive(Hash)]
103    enum Marker {
104        Phrase,
105        Keyword,
106        AnyWord,
107    }
108
109    #[tokio::test]
110    async fn match_input_test_partial() {
111        let tokens = [keyword("badger"), keyword("mushroom"), keyword("snake")];
112        let keyword_token = tokens[1].clone();
113        let any_of_token = any_of(tokens);
114
115        test::assert_eq_unordered!(
116            [FuzzyMatch::Partial(
117                TokenMatch::new(&any_of_token, vec![TokenMatch::from(&keyword_token)]),
118                Some("room".to_string())
119            )],
120            any_of_token
121                .match_input("mush", &test::app_meta())
122                .collect::<Vec<_>>()
123                .await,
124        );
125    }
126
127    #[tokio::test]
128    async fn match_input_test_exact() {
129        let tokens = [
130            keyword_m(Marker::Keyword, "badger"),
131            any_word_m(Marker::AnyWord),
132        ];
133        let [keyword_token, any_word_token] = tokens.clone();
134
135        let any_of_token = any_of_m(Marker::Phrase, tokens);
136
137        test::assert_eq_unordered!(
138            [
139                FuzzyMatch::Overflow(
140                    TokenMatch::new(&any_of_token, vec![TokenMatch::from(&keyword_token)]),
141                    " badger".into(),
142                ),
143                FuzzyMatch::Overflow(
144                    TokenMatch::new(
145                        &any_of_token,
146                        vec![TokenMatch::new(&any_word_token, "badger")],
147                    ),
148                    " badger".into(),
149                ),
150                FuzzyMatch::Exact(TokenMatch::new(
151                    &any_of_token,
152                    vec![
153                        TokenMatch::from(&keyword_token),
154                        TokenMatch::new(&any_word_token, "badger"),
155                    ],
156                )),
157                FuzzyMatch::Exact(TokenMatch::new(
158                    &any_of_token,
159                    vec![
160                        TokenMatch::new(&any_word_token, "badger"),
161                        TokenMatch::from(&keyword_token),
162                    ],
163                )),
164            ],
165            any_of_token
166                .match_input("badger badger", &test::app_meta())
167                .collect::<Vec<_>>()
168                .await,
169        );
170    }
171
172    #[tokio::test]
173    async fn match_input_test_exact_overflow() {
174        let tokens = [keyword("badger"), keyword("mushroom"), keyword("snake")];
175        let [badger_token, mushroom_token, _] = tokens.clone();
176        let any_of_token = any_of_m(Marker::Phrase, tokens);
177
178        test::assert_eq_unordered!(
179            [
180                FuzzyMatch::Overflow(
181                    TokenMatch::new(&any_of_token, vec![TokenMatch::from(&badger_token)]),
182                    " mushroom".into(),
183                ),
184                FuzzyMatch::Exact(TokenMatch::new(
185                    &any_of_token,
186                    vec![
187                        TokenMatch::from(&badger_token),
188                        TokenMatch::from(&mushroom_token)
189                    ]
190                )),
191            ],
192            any_of_token
193                .match_input("badger mushroom", &test::app_meta())
194                .collect::<Vec<_>>()
195                .await,
196        );
197    }
198
199    #[tokio::test]
200    async fn match_input_test_overflow() {
201        let badger_token = keyword("badger");
202        let any_of_token = any_of([badger_token.clone()]);
203
204        test::assert_eq_unordered!(
205            [FuzzyMatch::Overflow(
206                TokenMatch::new(&any_of_token, vec![TokenMatch::from(&badger_token)]),
207                " badger".into(),
208            )],
209            any_of_token
210                .match_input("badger badger", &test::app_meta())
211                .collect::<Vec<_>>()
212                .await,
213        );
214    }
215}