initiative_core/app/command/
runnable.rs1use crate::app::AppMeta;
2use async_trait::async_trait;
3use serde::Serialize;
4use std::borrow::Cow;
5
6#[async_trait(?Send)]
7pub trait Runnable: Sized {
8 async fn run(self, input: &str, app_meta: &mut AppMeta) -> Result<String, String>;
9}
10
11#[async_trait(?Send)]
12pub trait ContextAwareParse: Sized {
13 async fn parse_input(input: &str, app_meta: &AppMeta) -> CommandMatches<Self>;
14}
15
16#[async_trait(?Send)]
17pub trait Autocomplete {
18 async fn autocomplete(input: &str, app_meta: &AppMeta) -> Vec<AutocompleteSuggestion>;
19}
20
21#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
22#[serde(into = "(Cow<'static, str>, Cow<'static, str>)")]
23pub struct AutocompleteSuggestion {
24 pub term: Cow<'static, str>,
25 pub summary: Cow<'static, str>,
26}
27
28impl AutocompleteSuggestion {
29 pub fn new(term: impl Into<Cow<'static, str>>, summary: impl Into<Cow<'static, str>>) -> Self {
30 Self {
31 term: term.into(),
32 summary: summary.into(),
33 }
34 }
35}
36
37impl From<AutocompleteSuggestion> for (Cow<'static, str>, Cow<'static, str>) {
38 fn from(input: AutocompleteSuggestion) -> Self {
39 (input.term, input.summary)
40 }
41}
42
43impl<A, B> From<(A, B)> for AutocompleteSuggestion
44where
45 A: Into<Cow<'static, str>>,
46 B: Into<Cow<'static, str>>,
47{
48 fn from(input: (A, B)) -> Self {
49 AutocompleteSuggestion::new(input.0.into(), input.1.into())
50 }
51}
52
53#[derive(Clone, Debug, Eq, PartialEq)]
81pub struct CommandMatches<T> {
82 pub canonical_match: Option<T>,
83 pub fuzzy_matches: Vec<T>,
84}
85
86impl<T: std::fmt::Debug> CommandMatches<T> {
87 pub fn new_canonical(canonical_match: T) -> Self {
89 CommandMatches {
90 canonical_match: Some(canonical_match),
91 fuzzy_matches: Vec::new(),
92 }
93 }
94
95 pub fn new_fuzzy(fuzzy_match: T) -> Self {
97 CommandMatches {
98 canonical_match: None,
99 fuzzy_matches: vec![fuzzy_match],
100 }
101 }
102
103 pub fn push_canonical(&mut self, canonical_match: T) {
105 if let Some(old_canonical_match) = &self.canonical_match {
106 panic!(
107 "trying to overwrite existing canonical match {:?} with new match {:?}",
108 old_canonical_match, canonical_match,
109 );
110 }
111
112 self.canonical_match = Some(canonical_match);
113 }
114
115 pub fn push_fuzzy(&mut self, fuzzy_match: T) {
117 self.fuzzy_matches.push(fuzzy_match);
118 }
119
120 pub fn union<O>(mut self, other: CommandMatches<O>) -> Self
124 where
125 O: std::fmt::Debug,
126 T: From<O>,
127 {
128 let CommandMatches {
129 canonical_match,
130 fuzzy_matches,
131 } = other.into_subtype::<T>();
132
133 if let Some(canonical_match) = canonical_match {
134 self.push_canonical(canonical_match);
135 }
136
137 self.fuzzy_matches.reserve(fuzzy_matches.len());
138 fuzzy_matches
139 .into_iter()
140 .for_each(|fuzzy_match| self.push_fuzzy(fuzzy_match));
141
142 self
143 }
144
145 pub fn union_with_overwrite<O>(mut self, other: CommandMatches<O>) -> Self
148 where
149 O: std::fmt::Debug,
150 T: From<O>,
151 {
152 let self_canonical_match = self.canonical_match.take();
153
154 let mut result = self.union(other);
155
156 if result.canonical_match.is_none() {
157 result.canonical_match = self_canonical_match;
158 }
159
160 result
161 }
162
163 pub fn into_subtype<O>(self) -> CommandMatches<O>
169 where
170 O: From<T> + std::fmt::Debug,
171 {
172 CommandMatches {
173 canonical_match: self
174 .canonical_match
175 .map(|canonical_match| canonical_match.into()),
176 fuzzy_matches: self
177 .fuzzy_matches
178 .into_iter()
179 .map(|fuzzy_match| fuzzy_match.into())
180 .collect(),
181 }
182 }
183
184 pub fn take_best_match(self) -> Option<T> {
190 self.canonical_match
191 .or_else(|| self.fuzzy_matches.into_iter().next())
192 }
193}
194
195impl<T> From<T> for CommandMatches<T> {
196 fn from(input: T) -> Self {
197 CommandMatches {
198 canonical_match: Some(input),
199 fuzzy_matches: Vec::default(),
200 }
201 }
202}
203
204impl<T> Default for CommandMatches<T> {
205 fn default() -> Self {
206 CommandMatches {
207 canonical_match: None,
208 fuzzy_matches: Vec::default(),
209 }
210 }
211}
212
213#[cfg(test)]
214mod test {
215 use super::*;
216
217 #[test]
218 fn command_matches_new_test() {
219 {
220 let command_matches = CommandMatches::new_canonical(true);
221 assert_eq!(Some(true), command_matches.canonical_match);
222 assert!(command_matches.fuzzy_matches.is_empty());
223 }
224
225 {
226 let command_matches = CommandMatches::new_fuzzy(true);
227 assert_eq!(Option::<bool>::None, command_matches.canonical_match);
228 assert_eq!([true][..], command_matches.fuzzy_matches[..]);
229 }
230
231 {
232 let command_matches = CommandMatches::default();
233 assert_eq!(Option::<bool>::None, command_matches.canonical_match);
234 assert!(command_matches.fuzzy_matches.is_empty());
235 }
236 }
237
238 #[test]
239 fn command_matches_push_test() {
240 {
241 let mut command_matches = CommandMatches::default();
242 command_matches.push_canonical(true);
243 assert_eq!(Some(true), command_matches.canonical_match);
244 assert!(command_matches.fuzzy_matches.is_empty());
245 }
246
247 {
248 let mut command_matches = CommandMatches::default();
249 command_matches.push_fuzzy(1u8);
250 command_matches.push_fuzzy(2);
251 assert_eq!(Option::<u8>::None, command_matches.canonical_match);
252 assert_eq!([1u8, 2][..], command_matches.fuzzy_matches[..]);
253 }
254 }
255
256 #[test]
257 #[should_panic(
258 expected = "trying to overwrite existing canonical match true with new match false"
259 )]
260 fn command_matches_push_test_with_conflict() {
261 let mut command_matches = CommandMatches::default();
262 command_matches.push_canonical(true);
263 command_matches.push_canonical(false);
264 }
265
266 #[test]
267 fn command_matches_union_test() {
268 let command_matches_1 = {
269 let mut command_matches = CommandMatches::new_canonical(1u16);
270 command_matches.push_fuzzy(2);
271 command_matches.push_fuzzy(3);
272 command_matches
273 };
274 let command_matches_2 = CommandMatches::new_fuzzy(4u8);
275
276 let command_matches_result = command_matches_1.union(command_matches_2);
277
278 assert_eq!(Some(1u16), command_matches_result.canonical_match);
279 assert_eq!([2u16, 3, 4][..], command_matches_result.fuzzy_matches[..]);
280 }
281
282 #[test]
283 #[should_panic(
284 expected = "trying to overwrite existing canonical match true with new match false"
285 )]
286 fn command_matches_union_test_with_conflict() {
287 CommandMatches::new_canonical(true).union(CommandMatches::new_canonical(false));
288 }
289
290 #[test]
291 fn command_matches_union_with_overwrite_test() {
292 assert_eq!(
293 Some(2u16),
294 CommandMatches::new_canonical(1u16)
295 .union_with_overwrite(CommandMatches::new_canonical(2u8))
296 .canonical_match,
297 );
298 }
299
300 #[test]
301 fn command_matches_take_best_match_test() {
302 assert_eq!(
303 Some(true),
304 CommandMatches::new_canonical(true).take_best_match(),
305 );
306 assert_eq!(
307 Some(true),
308 CommandMatches::new_fuzzy(true).take_best_match(),
309 );
310
311 {
312 let mut command_matches = CommandMatches::new_canonical(true);
313 command_matches.push_fuzzy(false);
314 assert_eq!(Some(true), command_matches.take_best_match());
315 }
316
317 assert_eq!(
318 Option::<bool>::None,
319 CommandMatches::<bool>::default().take_best_match(),
320 );
321 }
322}