initiative_core/app/command/
runnable.rs

1use 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/// Represents all possible parse results for a given input.
54///
55/// One of the key usability features (and major headaches) of initiative.sh is its use of fuzzy
56/// command matching. Most parsers assume only one possible valid interpretation, but initiative.sh
57/// has the concept of *canonical* and *fuzzy* matches.
58///
59/// **Canonical matches** are inputs that cannot possibly conflict with one another. This is
60/// achieved using differing prefixes, eg. all SRD reference lookups are prefixed as "srd item
61/// shield" (or "srd spell shield").
62///
63/// **Fuzzy matches** are commands that the user could possibly have meant with a given input. The
64/// reason we need to be particularly careful with fuzzy matches is because the user can add
65/// arbitrary names to their journal, and typing that arbitrary name is *usually* enough to pull it
66/// up again. However, that arbitrary name could easily be another command, which the user may not
67/// want to overwrite. (The notable built-in conflict is the example above, where "shield" is both
68/// an item and a spell.)
69///
70/// || Canonical matches || Fuzzy matches || Result ||
71/// | 0 | 0 | Error: "Unknown command: '...'" |
72/// | 0 | 1 | The fuzzy match is run. |
73/// | 0 | 2+ | Error: "There are several possible interpretations of this command. Did you mean:" |
74/// | 1 | 0 | The canonical match is run. |
75/// | 1 | 1+ | The canonical match is run, suffixed with the error: "There are other possible interpretations of this command. Did you mean:" |
76///
77/// Since the parsing logic in the code base runs several layers deep across a number of different
78/// structs, this struct provides utilities for combining multiple CommandMatches instances of
79/// differing types, and for transforming the inner type as needed.
80#[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    /// Create a new instance of CommandMatches with a canonical match.
88    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    /// Create a new instance of `CommandMatches` with a single fuzzy match.
96    pub fn new_fuzzy(fuzzy_match: T) -> Self {
97        CommandMatches {
98            canonical_match: None,
99            fuzzy_matches: vec![fuzzy_match],
100        }
101    }
102
103    /// Push a new canonical match. Panics if a canonical match is already present.
104    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    /// Add a new fuzzy match to the list of possibilities.
116    pub fn push_fuzzy(&mut self, fuzzy_match: T) {
117        self.fuzzy_matches.push(fuzzy_match);
118    }
119
120    /// Combine the current CommandMatches with another object that can be massaged into the same
121    /// type. The Vecs of fuzzy matches are combined. Panics if both objects lay claim to a
122    /// canonical match.
123    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    /// Variant of union() that resolves canonical conflicts by overwriting self.canonical_match
146    /// with other.canonical_match instead of panicking.
147    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    /// Convert a `CommandMatches<T>' into a `CommandMatches<O>`, massaging the inner type using
164    /// its `From<T>` trait.
165    ///
166    /// This could be an impl of `From<CommandMatches<T>> for CommandMatches<O>`, but that produces
167    /// a trait conflict due to limitations of Rust's type system.
168    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    /// Consumes the struct, returning the following in priority order:
185    ///
186    /// 1. The canonical match, if present.
187    /// 2. The first fuzzy match, if any are present.
188    /// 3. `None`
189    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}