initiative_core/app/command/
app.rs1use crate::app::{
2 AppMeta, Autocomplete, AutocompleteSuggestion, CommandMatches, ContextAwareParse, Runnable,
3};
4use crate::utils::CaseInsensitiveStr;
5use async_trait::async_trait;
6use caith::Roller;
7use initiative_macros::changelog;
8use std::fmt;
9
10#[derive(Clone, Debug, Eq, PartialEq)]
11pub enum AppCommand {
12 Changelog,
13 Debug,
14 Help,
15 Roll(String),
16}
17
18#[async_trait(?Send)]
19impl Runnable for AppCommand {
20 async fn run(self, _input: &str, app_meta: &mut AppMeta) -> Result<String, String> {
21 Ok(match self {
22 Self::Debug => format!(
23 "{:?}\n\n{:?}",
24 app_meta,
25 app_meta.repository.journal().await,
26 ),
27 Self::Changelog => changelog!().to_string(),
28 Self::Help => include_str!("../../../../data/help.md")
29 .trim_end()
30 .to_string(),
31 Self::Roll(s) => Roller::new(&s)
32 .ok()
33 .and_then(|r| r.roll_with(&mut app_meta.rng).ok())
34 .map(|result| {
35 result
36 .to_string()
37 .trim_end()
38 .replace('\n', "\\\n")
39 .replace('`', "")
40 })
41 .ok_or_else(|| {
42 format!(
43 "\"{}\" is not a valid dice formula. See `help` for some examples.",
44 s
45 )
46 })?,
47 })
48 }
49}
50
51#[async_trait(?Send)]
52impl ContextAwareParse for AppCommand {
53 async fn parse_input(input: &str, _app_meta: &AppMeta) -> CommandMatches<Self> {
54 if input.eq_ci("changelog") {
55 CommandMatches::new_canonical(Self::Changelog)
56 } else if input.eq_ci("debug") {
57 CommandMatches::new_canonical(Self::Debug)
58 } else if input.eq_ci("help") {
59 CommandMatches::new_canonical(Self::Help)
60 } else if input.starts_with_ci("roll ") {
61 CommandMatches::new_canonical(Self::Roll(input[5..].to_string()))
62 } else if !input.chars().all(|c| c.is_ascii_digit())
63 && Roller::new(input).is_ok_and(|r| r.roll().is_ok())
64 {
65 CommandMatches::new_fuzzy(Self::Roll(input.to_string()))
66 } else {
67 CommandMatches::default()
68 }
69 }
70}
71
72#[async_trait(?Send)]
73impl Autocomplete for AppCommand {
74 async fn autocomplete(input: &str, _app_meta: &AppMeta) -> Vec<AutocompleteSuggestion> {
75 if input.is_empty() {
76 return Vec::new();
77 }
78
79 [
80 AutocompleteSuggestion::new("changelog", "show latest updates"),
81 AutocompleteSuggestion::new("help", "how to use initiative.sh"),
82 ]
83 .into_iter()
84 .filter(|suggestion| suggestion.term.starts_with_ci(input))
85 .chain(
86 ["roll"]
87 .into_iter()
88 .filter(|s| s.starts_with_ci(input))
89 .map(|_| AutocompleteSuggestion::new("roll [dice]", "roll eg. 8d6 or d20+3")),
90 )
91 .collect()
92 }
93}
94
95impl fmt::Display for AppCommand {
96 fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
97 match self {
98 Self::Changelog => write!(f, "changelog"),
99 Self::Debug => write!(f, "debug"),
100 Self::Help => write!(f, "help"),
101 Self::Roll(s) => write!(f, "roll {}", s),
102 }
103 }
104}
105
106#[cfg(test)]
107mod test {
108 use super::*;
109 use crate::test_utils as test;
110
111 #[tokio::test]
112 async fn parse_input_test() {
113 let app_meta = test::app_meta();
114
115 assert_eq!(
116 CommandMatches::new_canonical(AppCommand::Debug),
117 AppCommand::parse_input("debug", &app_meta).await,
118 );
119
120 assert_eq!(
121 CommandMatches::new_canonical(AppCommand::Roll("d20".to_string())),
122 AppCommand::parse_input("roll d20", &app_meta).await,
123 );
124
125 assert_eq!(
126 CommandMatches::new_fuzzy(AppCommand::Roll("d20".to_string())),
127 AppCommand::parse_input("d20", &app_meta).await,
128 );
129
130 assert_eq!(
131 CommandMatches::default(),
132 AppCommand::parse_input("potato", &app_meta).await,
133 );
134 }
135
136 #[tokio::test]
137 async fn autocomplete_test() {
138 let app_meta = test::app_meta();
139
140 for (term, summary) in [
141 ("changelog", "show latest updates"),
142 ("help", "how to use initiative.sh"),
143 ] {
144 test::assert_autocomplete_eq!(
145 [(term, summary)],
146 AppCommand::autocomplete(term, &app_meta).await,
147 );
148
149 test::assert_autocomplete_eq!(
150 [(term, summary)],
151 AppCommand::autocomplete(&term.to_uppercase(), &app_meta).await,
152 );
153 }
154
155 test::assert_autocomplete_eq!(
156 [("roll [dice]", "roll eg. 8d6 or d20+3")],
157 AppCommand::autocomplete("roll", &app_meta).await,
158 );
159
160 assert_eq!(
162 Vec::<AutocompleteSuggestion>::new(),
163 AppCommand::autocomplete("debug", &app_meta).await,
164 );
165 }
166
167 #[tokio::test]
168 async fn display_test() {
169 let app_meta = test::app_meta();
170
171 for command in [AppCommand::Changelog, AppCommand::Debug, AppCommand::Help] {
172 let command_string = command.to_string();
173 assert_ne!("", command_string);
174
175 assert_eq!(
176 CommandMatches::new_canonical(command.clone()),
177 AppCommand::parse_input(&command_string, &app_meta).await,
178 "{}",
179 command_string,
180 );
181
182 assert_eq!(
183 CommandMatches::new_canonical(command),
184 AppCommand::parse_input(&command_string.to_uppercase(), &app_meta).await,
185 "{}",
186 command_string.to_uppercase(),
187 );
188 }
189
190 assert_eq!("roll d20", AppCommand::Roll("d20".to_string()).to_string());
191
192 assert_eq!(
193 CommandMatches::new_canonical(AppCommand::Roll("d20".to_string())),
194 AppCommand::parse_input("roll d20", &app_meta).await,
195 );
196
197 assert_eq!(
198 CommandMatches::new_canonical(AppCommand::Roll("D20".to_string())),
199 AppCommand::parse_input("ROLL D20", &app_meta).await,
200 );
201 }
202}