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 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 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}