initiative_core/app/command/
mod.rs

1pub use alias::CommandAlias;
2pub use app::AppCommand;
3pub use runnable::{
4    Autocomplete, AutocompleteSuggestion, CommandMatches, ContextAwareParse, Runnable,
5};
6pub use tutorial::TutorialCommand;
7
8mod alias;
9mod app;
10mod runnable;
11mod tutorial;
12
13use super::AppMeta;
14use crate::command::TransitionalCommand;
15use crate::reference::ReferenceCommand;
16use crate::storage::StorageCommand;
17use crate::time::TimeCommand;
18use crate::world::WorldCommand;
19use async_trait::async_trait;
20use futures::join;
21use initiative_macros::From;
22use std::fmt;
23
24#[derive(Clone, Debug, Default, Eq, From, PartialEq)]
25pub struct Command {
26    matches: CommandMatches<CommandType>,
27}
28
29impl Command {
30    pub fn get_type(&self) -> Option<&CommandType> {
31        let command_type = if let Some(command) = &self.matches.canonical_match {
32            Some(command)
33        } else if self.matches.fuzzy_matches.len() == 1 {
34            self.matches.fuzzy_matches.first()
35        } else {
36            None
37        };
38
39        if let Some(CommandType::Alias(alias)) = command_type {
40            alias.get_command().get_type()
41        } else {
42            command_type
43        }
44    }
45
46    pub async fn parse_input_irrefutable(input: &str, app_meta: &AppMeta) -> Self {
47        let parse_results = join!(
48            CommandAlias::parse_input(input, app_meta),
49            AppCommand::parse_input(input, app_meta),
50            ReferenceCommand::parse_input(input, app_meta),
51            StorageCommand::parse_input(input, app_meta),
52            TimeCommand::parse_input(input, app_meta),
53            TransitionalCommand::parse_input(input, app_meta),
54            TutorialCommand::parse_input(input, app_meta),
55            WorldCommand::parse_input(input, app_meta),
56        );
57
58        // We deliberately skip parse_results.0 and handle it afterwards.
59        let mut result = CommandMatches::default()
60            .union(parse_results.1)
61            .union(parse_results.2)
62            .union(parse_results.3)
63            .union(parse_results.4)
64            .union(parse_results.5)
65            .union(parse_results.6)
66            .union(parse_results.7);
67
68        // While it is normally a fatal error to encounter two command subtypes claiming canonical
69        // matches on a given input, the exception is where aliases are present. In this case, we
70        // want the alias to overwrite the canonical match that would otherwise be returned.
71        result = result.union_with_overwrite(parse_results.0);
72
73        result.into()
74    }
75}
76
77#[async_trait(?Send)]
78impl Runnable for Command {
79    async fn run(self, input: &str, app_meta: &mut AppMeta) -> Result<String, String> {
80        if let Some(command) = &self.matches.canonical_match {
81            let other_interpretations_message = if !self.matches.fuzzy_matches.is_empty()
82                && !matches!(
83                    command,
84                    CommandType::Alias(CommandAlias::StrictWildcard { .. })
85                ) {
86                let mut message = "\n\n! There are other possible interpretations of this command. Did you mean:\n".to_string();
87                let mut lines: Vec<_> = self
88                    .matches
89                    .fuzzy_matches
90                    .iter()
91                    .map(|command| format!("\n* `{}`", command))
92                    .collect();
93                lines.sort();
94                lines.into_iter().for_each(|line| message.push_str(&line));
95                Some(message)
96            } else {
97                None
98            };
99
100            let result = self
101                .matches
102                .canonical_match
103                .unwrap()
104                .run(input, app_meta)
105                .await;
106            if let Some(message) = other_interpretations_message {
107                result
108                    .map(|mut s| {
109                        s.push_str(&message);
110                        s
111                    })
112                    .map_err(|mut s| {
113                        s.push_str(&message);
114                        s
115                    })
116            } else {
117                result
118            }
119        } else {
120            match &self.matches.fuzzy_matches.len() {
121                0 => Err(format!("Unknown command: \"{}\"", input)),
122                1 => {
123                    let mut fuzzy_matches = self.matches.fuzzy_matches;
124                    fuzzy_matches.pop().unwrap().run(input, app_meta).await
125                }
126                _ => {
127                    let mut message =
128                        "There are several possible interpretations of this command. Did you mean:\n"
129                            .to_string();
130                    let mut lines: Vec<_> = self
131                        .matches
132                        .fuzzy_matches
133                        .iter()
134                        .map(|command| format!("\n* `{}`", command))
135                        .collect();
136                    lines.sort();
137                    lines.into_iter().for_each(|line| message.push_str(&line));
138                    Err(message)
139                }
140            }
141        }
142    }
143}
144
145#[async_trait(?Send)]
146impl ContextAwareParse for Command {
147    async fn parse_input(input: &str, app_meta: &AppMeta) -> CommandMatches<Self> {
148        CommandMatches::new_canonical(Self::parse_input_irrefutable(input, app_meta).await)
149    }
150}
151
152#[async_trait(?Send)]
153impl Autocomplete for Command {
154    async fn autocomplete(input: &str, app_meta: &AppMeta) -> Vec<AutocompleteSuggestion> {
155        let results = join!(
156            CommandAlias::autocomplete(input, app_meta),
157            AppCommand::autocomplete(input, app_meta),
158            ReferenceCommand::autocomplete(input, app_meta),
159            StorageCommand::autocomplete(input, app_meta),
160            TimeCommand::autocomplete(input, app_meta),
161            TransitionalCommand::autocomplete(input, app_meta),
162            TutorialCommand::autocomplete(input, app_meta),
163            WorldCommand::autocomplete(input, app_meta),
164        );
165
166        std::iter::empty()
167            .chain(results.0)
168            .chain(results.1)
169            .chain(results.2)
170            .chain(results.3)
171            .chain(results.4)
172            .chain(results.5)
173            .chain(results.6)
174            .chain(results.7)
175            .collect()
176    }
177}
178
179#[derive(Clone, Debug, Eq, From, PartialEq)]
180pub enum CommandType {
181    Alias(CommandAlias),
182    App(AppCommand),
183    Reference(ReferenceCommand),
184    Storage(StorageCommand),
185    Time(TimeCommand),
186    Transitional(TransitionalCommand),
187    Tutorial(TutorialCommand),
188    World(WorldCommand),
189}
190
191impl CommandType {
192    async fn run(self, input: &str, app_meta: &mut AppMeta) -> Result<String, String> {
193        if !matches!(self, Self::Alias(_) | Self::Tutorial(_)) {
194            app_meta.command_aliases.clear();
195        }
196
197        match self {
198            Self::Alias(c) => c.run(input, app_meta).await,
199            Self::App(c) => c.run(input, app_meta).await,
200            Self::Reference(c) => c.run(input, app_meta).await,
201            Self::Storage(c) => c.run(input, app_meta).await,
202            Self::Time(c) => c.run(input, app_meta).await,
203            Self::Transitional(c) => c.run(input, app_meta).await,
204            Self::Tutorial(c) => c.run(input, app_meta).await,
205            Self::World(c) => c.run(input, app_meta).await,
206        }
207    }
208}
209
210impl fmt::Display for CommandType {
211    fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
212        match self {
213            Self::Alias(c) => write!(f, "{}", c),
214            Self::App(c) => write!(f, "{}", c),
215            Self::Reference(c) => write!(f, "{}", c),
216            Self::Storage(c) => write!(f, "{}", c),
217            Self::Time(c) => write!(f, "{}", c),
218            Self::Transitional(c) => write!(f, "{}", c),
219            Self::Tutorial(c) => write!(f, "{}", c),
220            Self::World(c) => write!(f, "{}", c),
221        }
222    }
223}
224
225impl<T: Into<CommandType>> From<T> for Command {
226    fn from(c: T) -> Command {
227        Command {
228            matches: CommandMatches::new_canonical(c.into()),
229        }
230    }
231}
232
233#[cfg(test)]
234mod test {
235    use super::*;
236    use crate::test_utils as test;
237    use crate::world::npc::NpcData;
238    use crate::world::ParsedThing;
239    use tokio_test::block_on;
240
241    #[test]
242    fn parse_input_test() {
243        let app_meta = test::app_meta();
244
245        assert_eq!(
246            Command::from(CommandMatches::new_canonical(CommandType::App(
247                AppCommand::Changelog
248            ))),
249            block_on(Command::parse_input("changelog", &app_meta))
250                .take_best_match()
251                .unwrap(),
252        );
253
254        assert_eq!(
255            Command::from(CommandMatches::new_canonical(CommandType::Reference(
256                ReferenceCommand::OpenGameLicense
257            ))),
258            block_on(Command::parse_input("Open Game License", &app_meta))
259                .take_best_match()
260                .unwrap(),
261        );
262
263        assert_eq!(
264            Command::from(CommandMatches::default()),
265            block_on(Command::parse_input("Odysseus", &app_meta))
266                .take_best_match()
267                .unwrap(),
268        );
269
270        assert_eq!(
271            Command::from(CommandMatches::new_canonical(CommandType::Transitional(
272                TransitionalCommand::new("about"),
273            ))),
274            block_on(Command::parse_input("about", &app_meta))
275                .take_best_match()
276                .unwrap(),
277        );
278
279        assert_eq!(
280            Command::from(CommandMatches::new_canonical(CommandType::World(
281                WorldCommand::Create {
282                    parsed_thing_data: ParsedThing {
283                        thing_data: NpcData::default().into(),
284                        unknown_words: Vec::new(),
285                        word_count: 1,
286                    },
287                }
288            ))),
289            block_on(Command::parse_input("create npc", &app_meta))
290                .take_best_match()
291                .unwrap(),
292        );
293    }
294
295    #[tokio::test]
296    async fn autocomplete_test() {
297        test::assert_autocomplete_eq!(
298            [
299                ("Pass Without Trace", "SRD spell"),
300                ("Passwall", "SRD spell"),
301                ("Penelope", "middle-aged human, she/her"),
302                ("Phantasmal Killer", "SRD spell"),
303                ("Phantom Steed", "SRD spell"),
304                ("Phoenicia", "territory"),
305                ("Planar Ally", "SRD spell"),
306                ("Planar Binding", "SRD spell"),
307                ("Plane Shift", "SRD spell"),
308                ("Plant Growth", "SRD spell"),
309                ("Poison Spray", "SRD spell"),
310                ("Polymorph", "SRD spell"),
311                ("Polyphemus", "adult half-orc, he/him (unsaved)"),
312                ("Pylos", "city (unsaved)"),
313                ("palace", "create palace"),
314                ("parish", "create town"),
315                ("pass", "create pass"),
316                ("peninsula", "create peninsula"),
317                ("person", "create person"),
318                ("pet-store", "create pet-store"),
319                ("pier", "create pier"),
320                ("place", "create place"),
321                ("plain", "create plain"),
322                ("plateau", "create plateau"),
323                ("portal", "create portal"),
324                ("principality", "create principality"),
325                ("prison", "create prison"),
326                ("province", "create province"),
327                ("pub", "create bar"),
328            ],
329            Command::autocomplete("p", &test::app_meta::with_test_data().await).await,
330        );
331    }
332
333    #[test]
334    fn into_command_test() {
335        assert_eq!(
336            CommandType::App(AppCommand::Debug),
337            AppCommand::Debug.into(),
338        );
339
340        assert_eq!(
341            CommandType::Storage(StorageCommand::Load {
342                name: "Odysseus".to_string(),
343            }),
344            StorageCommand::Load {
345                name: "Odysseus".to_string(),
346            }
347            .into(),
348        );
349
350        assert_eq!(
351            CommandType::World(WorldCommand::Create {
352                parsed_thing_data: ParsedThing {
353                    thing_data: NpcData::default().into(),
354                    unknown_words: Vec::new(),
355                    word_count: 1,
356                },
357            }),
358            WorldCommand::Create {
359                parsed_thing_data: ParsedThing {
360                    thing_data: NpcData::default().into(),
361                    unknown_words: Vec::new(),
362                    word_count: 1,
363                },
364            }
365            .into(),
366        );
367    }
368}