initiative_core/time/
command.rs

1use super::Interval;
2use crate::app::{
3    AppMeta, Autocomplete, AutocompleteSuggestion, CommandMatches, ContextAwareParse, Runnable,
4};
5use crate::storage::{Change, KeyValue};
6use crate::utils::CaseInsensitiveStr;
7use async_trait::async_trait;
8use std::fmt;
9use std::iter;
10
11#[derive(Clone, Debug, Eq, PartialEq)]
12pub enum TimeCommand {
13    Add { interval: Interval },
14    Now,
15    Sub { interval: Interval },
16}
17
18#[async_trait(?Send)]
19impl Runnable for TimeCommand {
20    async fn run(self, _input: &str, app_meta: &mut AppMeta) -> Result<String, String> {
21        let time = {
22            let current_time = app_meta
23                .repository
24                .get_key_value(&KeyValue::Time(None))
25                .await
26                .map_err(|_| "Storage error.".to_string())?
27                .time()
28                .unwrap_or_default();
29
30            match &self {
31                Self::Add { interval } => current_time.checked_add(interval),
32                Self::Sub { interval } => current_time.checked_sub(interval),
33                Self::Now => {
34                    return Ok(format!("It is currently {}.", current_time.display_long()))
35                }
36            }
37        };
38
39        if let Some(time) = time {
40            let response = format!("It is now {}. Use `undo` to reverse.", time.display_long());
41
42            app_meta
43                .repository
44                .modify(Change::SetKeyValue {
45                    key_value: KeyValue::Time(Some(time)),
46                })
47                .await
48                .map(|_| response)
49                .map_err(|_| ())
50        } else {
51            Err(())
52        }
53        .map_err(|_| match &self {
54            Self::Add { interval } => {
55                format!("Unable to advance time by {}.", interval.display_long())
56            }
57            Self::Sub { interval } => {
58                format!("Unable to rewind time by {}.", interval.display_long())
59            }
60            Self::Now => unreachable!(),
61        })
62    }
63}
64
65#[async_trait(?Send)]
66impl ContextAwareParse for TimeCommand {
67    async fn parse_input(input: &str, _app_meta: &AppMeta) -> CommandMatches<Self> {
68        if input.eq_ci("now") {
69            CommandMatches::new_canonical(Self::Now)
70        } else if input.in_ci(&["time", "date"]) {
71            CommandMatches::new_fuzzy(Self::Now)
72        } else if let Some(canonical_match) = input
73            .strip_prefix('+')
74            .and_then(|s| s.parse().ok())
75            .map(|interval| Self::Add { interval })
76            .or_else(|| {
77                input
78                    .strip_prefix('-')
79                    .and_then(|s| s.parse().ok())
80                    .map(|interval| Self::Sub { interval })
81            })
82        {
83            CommandMatches::new_canonical(canonical_match)
84        } else {
85            CommandMatches::default()
86        }
87    }
88}
89
90#[async_trait(?Send)]
91impl Autocomplete for TimeCommand {
92    async fn autocomplete(input: &str, _app_meta: &AppMeta) -> Vec<AutocompleteSuggestion> {
93        if input.starts_with(&['+', '-'][..]) {
94            let suggest = |suffix: &str| -> Result<AutocompleteSuggestion, ()> {
95                let term = format!("{}{}", input, suffix);
96                let summary = if input.starts_with('+') {
97                    format!(
98                        "advance time by {}",
99                        &term[1..].parse::<Interval>()?.display_long(),
100                    )
101                } else {
102                    format!(
103                        "rewind time by {}",
104                        &term[1..].parse::<Interval>()?.display_long(),
105                    )
106                };
107                Ok(AutocompleteSuggestion::new(term, summary))
108            };
109
110            let suggest_all = || {
111                ["", "d", "h", "m", "s", "r"]
112                    .iter()
113                    .filter_map(|suffix| suggest(suffix).ok())
114            };
115
116            match input {
117                "+" | "-" => iter::once(AutocompleteSuggestion::new(
118                    format!("{}[number]", input),
119                    if input == "+" {
120                        "advance time"
121                    } else {
122                        "rewind time"
123                    },
124                ))
125                .chain(suggest_all())
126                .collect(),
127                _ => suggest_all().collect(),
128            }
129        } else if !input.is_empty() {
130            ["now", "time", "date"]
131                .into_iter()
132                .filter(|term| term.starts_with_ci(input))
133                .map(|term| AutocompleteSuggestion::new(term, "get the current time"))
134                .collect()
135        } else {
136            Vec::new()
137        }
138    }
139}
140
141impl fmt::Display for TimeCommand {
142    fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
143        match self {
144            Self::Add { interval } => write!(f, "+{}", interval.display_short()),
145            Self::Now => write!(f, "now"),
146            Self::Sub { interval } => write!(f, "-{}", interval.display_short()),
147        }
148    }
149}
150
151#[cfg(test)]
152mod test {
153    use super::*;
154    use crate::test_utils as test;
155
156    #[tokio::test]
157    async fn parse_input_test() {
158        let app_meta = test::app_meta();
159
160        assert_eq!(
161            CommandMatches::new_canonical(TimeCommand::Add {
162                interval: Interval::new(0, 0, 1, 0, 0),
163            }),
164            TimeCommand::parse_input("+1m", &app_meta).await,
165        );
166
167        assert_eq!(
168            CommandMatches::new_canonical(TimeCommand::Add {
169                interval: Interval::new(1, 0, 0, 0, 0),
170            }),
171            TimeCommand::parse_input("+d", &app_meta).await,
172        );
173
174        assert_eq!(
175            CommandMatches::new_canonical(TimeCommand::Sub {
176                interval: Interval::new(0, 10, 0, 0, 0),
177            }),
178            TimeCommand::parse_input("-10h", &app_meta).await,
179        );
180
181        assert_eq!(
182            CommandMatches::default(),
183            TimeCommand::parse_input("1d2h", &app_meta).await,
184        );
185    }
186
187    #[tokio::test]
188    async fn autocomplete_test() {
189        let app_meta = test::app_meta();
190
191        test::assert_empty!(TimeCommand::autocomplete("", &app_meta).await);
192
193        test::assert_autocomplete_eq!(
194            [
195                ("+[number]", "advance time"),
196                ("+d", "advance time by 1 day"),
197                ("+h", "advance time by 1 hour"),
198                ("+m", "advance time by 1 minute"),
199                ("+s", "advance time by 1 second"),
200                ("+r", "advance time by 1 round"),
201            ],
202            TimeCommand::autocomplete("+", &app_meta).await,
203        );
204
205        test::assert_autocomplete_eq!(
206            [
207                ("-[number]", "rewind time"),
208                ("-d", "rewind time by 1 day"),
209                ("-h", "rewind time by 1 hour"),
210                ("-m", "rewind time by 1 minute"),
211                ("-s", "rewind time by 1 second"),
212                ("-r", "rewind time by 1 round"),
213            ],
214            TimeCommand::autocomplete("-", &app_meta).await,
215        );
216
217        test::assert_autocomplete_eq!(
218            [
219                ("+1d", "advance time by 1 day"),
220                ("+1h", "advance time by 1 hour"),
221                ("+1m", "advance time by 1 minute"),
222                ("+1s", "advance time by 1 second"),
223                ("+1r", "advance time by 1 round"),
224            ],
225            TimeCommand::autocomplete("+1", &app_meta).await,
226        );
227
228        test::assert_autocomplete_eq!(
229            [
230                ("+10d", "advance time by 10 days"),
231                ("+10h", "advance time by 10 hours"),
232                ("+10m", "advance time by 10 minutes"),
233                ("+10s", "advance time by 10 seconds"),
234                ("+10r", "advance time by 10 rounds"),
235            ],
236            TimeCommand::autocomplete("+10", &app_meta).await,
237        );
238
239        test::assert_autocomplete_eq!(
240            [
241                ("+10d5h", "advance time by 10 days, 5 hours"),
242                ("+10d5m", "advance time by 10 days, 5 minutes"),
243                ("+10d5s", "advance time by 10 days, 5 seconds"),
244                ("+10d5r", "advance time by 10 days, 5 rounds"),
245            ],
246            TimeCommand::autocomplete("+10d5", &app_meta).await,
247        );
248
249        test::assert_autocomplete_eq!(
250            [
251                ("+10D5h", "advance time by 10 days, 5 hours"),
252                ("+10D5m", "advance time by 10 days, 5 minutes"),
253                ("+10D5s", "advance time by 10 days, 5 seconds"),
254                ("+10D5r", "advance time by 10 days, 5 rounds"),
255            ],
256            TimeCommand::autocomplete("+10D5", &app_meta).await,
257        );
258
259        test::assert_autocomplete_eq!(
260            [("+1d", "advance time by 1 day")],
261            TimeCommand::autocomplete("+1d", &app_meta).await,
262        );
263        test::assert_autocomplete_eq!(
264            [("+1D", "advance time by 1 day")],
265            TimeCommand::autocomplete("+1D", &app_meta).await,
266        );
267        test::assert_autocomplete_eq!(
268            [("+1h", "advance time by 1 hour")],
269            TimeCommand::autocomplete("+1h", &app_meta).await,
270        );
271        test::assert_autocomplete_eq!(
272            [("+1H", "advance time by 1 hour")],
273            TimeCommand::autocomplete("+1H", &app_meta).await,
274        );
275        test::assert_autocomplete_eq!(
276            [("+1m", "advance time by 1 minute")],
277            TimeCommand::autocomplete("+1m", &app_meta).await,
278        );
279        test::assert_autocomplete_eq!(
280            [("+1M", "advance time by 1 minute")],
281            TimeCommand::autocomplete("+1M", &app_meta).await,
282        );
283        test::assert_autocomplete_eq!(
284            [("+1s", "advance time by 1 second")],
285            TimeCommand::autocomplete("+1s", &app_meta).await,
286        );
287        test::assert_autocomplete_eq!(
288            [("+1S", "advance time by 1 second")],
289            TimeCommand::autocomplete("+1S", &app_meta).await,
290        );
291        test::assert_autocomplete_eq!(
292            [("+1r", "advance time by 1 round")],
293            TimeCommand::autocomplete("+1r", &app_meta).await,
294        );
295        test::assert_autocomplete_eq!(
296            [("+1R", "advance time by 1 round")],
297            TimeCommand::autocomplete("+1R", &app_meta).await,
298        );
299    }
300
301    #[tokio::test]
302    async fn display_test() {
303        let app_meta = test::app_meta();
304
305        for command in [
306            TimeCommand::Add {
307                interval: Interval::new(2, 3, 4, 5, 6),
308            },
309            TimeCommand::Now,
310            TimeCommand::Sub {
311                interval: Interval::new(2, 3, 4, 5, 6),
312            },
313        ] {
314            let command_string = command.to_string();
315            assert_ne!("", command_string);
316
317            assert_eq!(
318                CommandMatches::new_canonical(command.clone()),
319                TimeCommand::parse_input(&command_string, &app_meta).await,
320                "{}",
321                command_string,
322            );
323
324            assert_eq!(
325                CommandMatches::new_canonical(command),
326                TimeCommand::parse_input(&command_string.to_uppercase(), &app_meta).await,
327                "{}",
328                command_string.to_uppercase(),
329            );
330        }
331    }
332}