initiative_core/app/command/
alias.rs

1use super::{
2    Autocomplete, AutocompleteSuggestion, Command, CommandMatches, ContextAwareParse, Runnable,
3};
4use crate::app::AppMeta;
5use crate::utils::CaseInsensitiveStr;
6use async_trait::async_trait;
7use std::borrow::Cow;
8use std::fmt;
9use std::hash::{Hash, Hasher};
10use std::mem;
11
12#[derive(Clone, Debug)]
13pub enum CommandAlias {
14    Literal {
15        term: Cow<'static, str>,
16        summary: Cow<'static, str>,
17        command: Box<Command>,
18    },
19    StrictWildcard {
20        command: Box<Command>,
21    },
22}
23
24impl CommandAlias {
25    pub fn literal(
26        term: impl Into<Cow<'static, str>>,
27        summary: impl Into<Cow<'static, str>>,
28        command: Command,
29    ) -> Self {
30        Self::Literal {
31            term: term.into(),
32            summary: summary.into(),
33            command: Box::new(command),
34        }
35    }
36
37    pub fn strict_wildcard(command: Command) -> Self {
38        Self::StrictWildcard {
39            command: Box::new(command),
40        }
41    }
42
43    pub fn get_command(&self) -> &Command {
44        match self {
45            Self::Literal { command, .. } => command,
46            Self::StrictWildcard { command, .. } => command,
47        }
48    }
49}
50
51impl Hash for CommandAlias {
52    fn hash<H: Hasher>(&self, state: &mut H) {
53        match self {
54            Self::Literal { term, .. } => {
55                if term.chars().any(char::is_uppercase) {
56                    term.to_lowercase().hash(state);
57                } else {
58                    term.hash(state);
59                }
60            }
61            Self::StrictWildcard { .. } => {}
62        }
63    }
64}
65
66impl PartialEq for CommandAlias {
67    fn eq(&self, other: &Self) -> bool {
68        match (self, other) {
69            (
70                Self::Literal { term, .. },
71                Self::Literal {
72                    term: other_term, ..
73                },
74            ) => term.eq_ci(other_term),
75            (Self::StrictWildcard { .. }, Self::StrictWildcard { .. }) => true,
76            _ => false,
77        }
78    }
79}
80
81impl Eq for CommandAlias {}
82
83#[async_trait(?Send)]
84impl Runnable for CommandAlias {
85    async fn run(self, input: &str, app_meta: &mut AppMeta) -> Result<String, String> {
86        match self {
87            Self::Literal { command, .. } => {
88                let mut temp_aliases = mem::take(&mut app_meta.command_aliases);
89
90                let result = command.run(input, app_meta).await;
91
92                if app_meta.command_aliases.is_empty() {
93                    app_meta.command_aliases = temp_aliases;
94                } else {
95                    temp_aliases.drain().for_each(|command| {
96                        if !app_meta.command_aliases.contains(&command) {
97                            app_meta.command_aliases.insert(command);
98                        }
99                    });
100                }
101
102                result
103            }
104            Self::StrictWildcard { .. } => {
105                app_meta.command_aliases.remove(&self);
106                if let Self::StrictWildcard { command } = self {
107                    command.run(input, app_meta).await
108                } else {
109                    unreachable!();
110                }
111            }
112        }
113    }
114}
115
116#[async_trait(?Send)]
117impl ContextAwareParse for CommandAlias {
118    async fn parse_input(input: &str, app_meta: &AppMeta) -> CommandMatches<Self> {
119        app_meta
120            .command_aliases
121            .iter()
122            .find(|c| matches!(c, Self::StrictWildcard { .. }))
123            .or_else(|| {
124                app_meta
125                    .command_aliases
126                    .iter()
127                    .find(|command| match command {
128                        Self::Literal { term, .. } => term.eq_ci(input),
129                        Self::StrictWildcard { .. } => false,
130                    })
131            })
132            .cloned()
133            .map(CommandMatches::from)
134            .unwrap_or_default()
135    }
136}
137
138#[async_trait(?Send)]
139impl Autocomplete for CommandAlias {
140    async fn autocomplete(input: &str, app_meta: &AppMeta) -> Vec<AutocompleteSuggestion> {
141        app_meta
142            .command_aliases
143            .iter()
144            .filter_map(|command| match command {
145                Self::Literal { term, summary, .. } => {
146                    if term.starts_with_ci(input) {
147                        Some(AutocompleteSuggestion::new(
148                            term.to_string(),
149                            summary.to_string(),
150                        ))
151                    } else {
152                        None
153                    }
154                }
155                Self::StrictWildcard { .. } => None,
156            })
157            .collect()
158    }
159}
160
161impl fmt::Display for CommandAlias {
162    fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
163        match self {
164            Self::Literal { term, .. } => {
165                write!(f, "{}", term)?;
166            }
167            Self::StrictWildcard { .. } => {}
168        }
169
170        Ok(())
171    }
172}
173
174#[cfg(test)]
175mod tests {
176    use super::*;
177    use crate::app::{AppCommand, Command};
178    use crate::command::TransitionalCommand;
179    use crate::test_utils as test;
180    use std::collections::HashSet;
181
182    #[test]
183    fn literal_constructor_test() {
184        let alias = CommandAlias::literal("term".to_string(), "summary".to_string(), about());
185
186        if let CommandAlias::Literal {
187            term,
188            summary,
189            command,
190        } = alias
191        {
192            assert_eq!("term", term);
193            assert_eq!("summary", summary);
194            assert_eq!(Box::new(about()), command);
195        } else {
196            panic!("{:?}", alias);
197        }
198    }
199
200    #[test]
201    fn wildcard_constructor_test() {
202        let alias = CommandAlias::strict_wildcard(about());
203
204        if let CommandAlias::StrictWildcard { command } = alias {
205            assert_eq!(Box::new(about()), command);
206        } else {
207            panic!("{:?}", alias);
208        }
209    }
210
211    #[test]
212    fn eq_test() {
213        assert_eq!(
214            literal("foo", "foo", about()),
215            literal("foo", "bar", AppCommand::Help.into()),
216        );
217        assert_ne!(
218            literal("foo", "foo", about()),
219            literal("bar", "foo", about()),
220        );
221
222        assert_eq!(
223            strict_wildcard(about()),
224            strict_wildcard(AppCommand::Help.into()),
225        );
226        assert_ne!(literal("", "", about()), strict_wildcard(about()));
227    }
228
229    #[test]
230    fn hash_test() {
231        let mut set = HashSet::with_capacity(2);
232
233        assert!(set.insert(literal("foo", "", about())));
234        assert!(set.insert(literal("bar", "", about())));
235        assert!(set.insert(strict_wildcard(about())));
236        assert!(!set.insert(literal("foo", "", AppCommand::Help.into())));
237        assert!(!set.insert(literal("FOO", "", AppCommand::Help.into())));
238        assert!(!set.insert(strict_wildcard(AppCommand::Help.into())));
239    }
240
241    #[tokio::test]
242    async fn runnable_test_literal() {
243        let about_alias = literal("about alias", "about summary", about());
244
245        let mut app_meta = test::app_meta();
246        app_meta.command_aliases.insert(about_alias.clone());
247        app_meta.command_aliases.insert(literal(
248            "help alias",
249            "help summary",
250            AppCommand::Help.into(),
251        ));
252
253        test::assert_autocomplete_eq!(
254            [("about alias", "about summary")],
255            CommandAlias::autocomplete("a", &app_meta).await,
256        );
257
258        assert_eq!(
259            CommandAlias::autocomplete("a", &app_meta).await,
260            CommandAlias::autocomplete("A", &app_meta).await,
261        );
262
263        assert_eq!(
264            CommandMatches::default(),
265            CommandAlias::parse_input("blah", &app_meta).await,
266        );
267
268        assert_eq!(
269            CommandMatches::new_canonical(about_alias.clone()),
270            CommandAlias::parse_input("about alias", &app_meta).await,
271        );
272
273        {
274            let about_alias_result = about_alias.run("about alias", &mut app_meta).await;
275            assert!(!app_meta.command_aliases.is_empty());
276
277            let about_result = about().run("about", &mut app_meta).await;
278            assert!(app_meta.command_aliases.is_empty());
279
280            assert!(about_result.is_ok(), "{:?}", about_result);
281            assert_eq!(about_result, about_alias_result);
282        }
283    }
284
285    #[tokio::test]
286    async fn runnable_test_strict_wildcard() {
287        let about_alias = strict_wildcard(about());
288
289        let mut app_meta = test::app_meta();
290        app_meta.command_aliases.insert(about_alias.clone());
291        app_meta.command_aliases.insert(literal(
292            "literal alias",
293            "literally a summary",
294            AppCommand::Help.into(),
295        ));
296
297        // Should be caught by the wildcard, not the literal alias
298        assert_eq!(
299            CommandMatches::new_canonical(about_alias.clone()),
300            CommandAlias::parse_input("literal alias", &app_meta).await,
301        );
302
303        {
304            assert_eq!(2, app_meta.command_aliases.len());
305
306            let (about_result, about_alias_result) = (
307                about().run("about", &mut app_meta).await,
308                about_alias.run("about", &mut app_meta).await,
309            );
310
311            assert!(about_result.is_ok(), "{:?}", about_result);
312            assert_eq!(about_result, about_alias_result);
313            assert!(app_meta.command_aliases.is_empty());
314        }
315    }
316
317    fn about() -> Command {
318        Command::from(TransitionalCommand::new("about"))
319    }
320
321    fn literal(
322        term: impl Into<Cow<'static, str>>,
323        summary: impl Into<Cow<'static, str>>,
324        command: Command,
325    ) -> CommandAlias {
326        CommandAlias::Literal {
327            term: term.into(),
328            summary: summary.into(),
329            command: Box::new(command),
330        }
331    }
332
333    fn strict_wildcard(command: Command) -> CommandAlias {
334        CommandAlias::StrictWildcard {
335            command: Box::new(command),
336        }
337    }
338}