initiative_core/app/command/
alias.rs1use 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 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}