initiative_core/command/token/
name.rs

1//! Matches the name of a Thing that exists in recent or journal. Where a real Thing was matched,
2//! the meta field will include the matched Record for further processing (eg. customized
3//! autocomplete comments).
4
5use crate::app::AppMeta;
6use crate::command::prelude::*;
7use crate::utils::{quoted_phrases, CaseInsensitiveStr};
8
9use std::pin::Pin;
10
11use async_stream::stream;
12use futures::join;
13use futures::prelude::*;
14
15pub fn match_input<'a, 'b>(
16    token: &'a Token,
17    input: &'a str,
18    app_meta: &'b AppMeta,
19) -> Pin<Box<dyn Stream<Item = FuzzyMatch<'a>> + 'b>>
20where
21    'a: 'b,
22{
23    assert!(matches!(token.token_type, TokenType::Name));
24
25    let phrases: Vec<_> = quoted_phrases(input).collect();
26    if phrases.is_empty() {
27        return Box::pin(stream::empty());
28    }
29
30    Box::pin(stream! {
31        let first_phrase = phrases.first().unwrap();
32
33        // unwrap: we've checked that there's a first(), so there must be a last()
34        let full_phrase = phrases.last().unwrap();
35
36        let records: Vec<_> = if first_phrase.is_quoted() {
37            // Need to query both quoted and unquoted versions of the phrase
38            let (unquoted_name, quoted_name_matches) = join!(
39                app_meta.repository.get_by_name(first_phrase.as_str()),
40                app_meta.repository.get_by_name_start(first_phrase.as_outer_str()),
41            );
42
43            let mut quoted_name_matches = quoted_name_matches.unwrap_or_default();
44            if let Ok(unquoted_name) = unquoted_name {
45                quoted_name_matches.push(unquoted_name);
46            }
47
48            quoted_name_matches
49        } else {
50            app_meta.repository
51                .get_by_name_start(first_phrase.as_str())
52                .await
53                .unwrap_or_default()
54        };
55
56        for record in records.into_iter() {
57            // unwrap: result of get_by_name_start(), so it must have a name
58            let thing_name = record.thing.name().value().unwrap();
59
60            if thing_name.eq_ci(full_phrase) {
61                yield FuzzyMatch::Exact(TokenMatch::new(token, record));
62                continue;
63            } else if full_phrase.can_complete() {
64                if let Some(completion) = thing_name.strip_prefix_ci(full_phrase).map(str::to_string) {
65                    yield FuzzyMatch::Partial(
66                        TokenMatch::new(token, record),
67                        Some(completion),
68                    );
69
70                    continue;
71                }
72            }
73
74            for (i, phrase) in phrases[0..phrases.len() - 1].iter().enumerate() {
75                if thing_name.eq_ci(phrase) || (i == 0 && thing_name.eq_ci(phrase.as_outer_str())) {
76                    yield FuzzyMatch::Overflow(
77                        TokenMatch::new(token, record),
78                        phrase.after()
79                    );
80                    break;
81                }
82            }
83        }
84    })
85}
86
87#[cfg(test)]
88mod test {
89    use super::*;
90
91    use crate::storage::{Record, RecordStatus};
92    use crate::test_utils as test;
93    use uuid::Uuid;
94
95    #[derive(Hash)]
96    enum Marker {
97        Token,
98    }
99
100    #[tokio::test]
101    async fn match_input_test_simple() {
102        let token = name_m(Marker::Token);
103
104        test::assert_eq_unordered!(
105            [FuzzyMatch::Exact(TokenMatch::new(
106                &token,
107                Record {
108                    status: RecordStatus::Unsaved,
109                    thing: test::thing::odysseus(),
110                },
111            ))],
112            match_input(&token, "Odysseus", &test::app_meta::with_test_data().await)
113                .collect::<Vec<_>>()
114                .await,
115        );
116    }
117
118    #[tokio::test]
119    async fn match_input_test_empty() {
120        test::assert_empty!(
121            match_input(&name(), "    ", &test::app_meta::with_test_data().await)
122                .collect::<Vec<_>>()
123                .await,
124        );
125    }
126
127    #[tokio::test]
128    async fn match_input_test_quoted() {
129        let token = name();
130
131        let things = [
132            test::npc().name("Medium").build_thing(Uuid::new_v4()),
133            test::npc().name("\"Medium\"").build_thing(Uuid::new_v4()),
134            test::npc()
135                .name("\"Medium\" Dave Lilywhite")
136                .build_thing(Uuid::new_v4()),
137        ];
138
139        let app_meta = test::app_meta::with_data_store::memory::with(things.clone(), []);
140
141        test::assert_eq_unordered!(
142            [
143                FuzzyMatch::Overflow(
144                    TokenMatch::new(
145                        &token,
146                        Record {
147                            status: RecordStatus::Saved,
148                            thing: things[0].clone(),
149                        },
150                    ),
151                    " Dave Lily".into(),
152                ),
153                FuzzyMatch::Overflow(
154                    TokenMatch::new(
155                        &token,
156                        Record {
157                            status: RecordStatus::Saved,
158                            thing: things[1].clone(),
159                        },
160                    ),
161                    " Dave Lily".into(),
162                ),
163                FuzzyMatch::Partial(
164                    TokenMatch::new(
165                        &token,
166                        Record {
167                            status: RecordStatus::Saved,
168                            thing: things[2].clone(),
169                        },
170                    ),
171                    Some("white".to_string()),
172                ),
173            ],
174            match_input(&token, "\"Medium\" Dave Lily", &app_meta)
175                .collect::<Vec<_>>()
176                .await,
177        );
178    }
179}