Skip to main content

yog_command/
fit.rs

1//! Which of a command's handlers its arguments fit, and how: several
2//! handlers share a name (subcommands of different shapes), tried in
3//! [`Fit::ORDER`] until one answers.
4
5/// What separates a typed command's arguments in [`crate::CommandContext::args`].
6/// What joins the words a greedy argument takes.
7const WORD_SEP: &str = " ";
8/// The argument type that takes the rest of the line.
9pub const GREEDY: &str = "string";
10
11/// How a handler takes a command's arguments.
12#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13pub enum Fit {
14    /// A typed handler with exactly as many arguments as were given.
15    Exact,
16    /// A typed handler ending in a greedy `string`, the extra words joined
17    /// into it (the game may have parsed them as separate words).
18    Joined,
19    /// A plain (schema-less) handler, with whatever was typed.
20    Plain,
21}
22
23impl Fit {
24    /// The order handlers are tried in.
25    pub const ORDER: [Fit; 3] = [Fit::Exact, Fit::Joined, Fit::Plain];
26
27    /// The ways a handler with `schema` (`None`: plain) takes arguments.
28    pub fn ways(schema: Option<&str>) -> &'static [Fit] {
29        match schema {
30            Some(_) => &Self::ORDER[..2],
31            None => &Self::ORDER[2..],
32        }
33    }
34
35    /// Where it's tried: earlier ways rank higher.
36    pub fn rank(self) -> usize {
37        Self::ORDER.len()
38            - Self::ORDER
39                .iter()
40                .position(|f| *f == self)
41                .unwrap_or(Self::ORDER.len())
42    }
43
44    /// `args` as a handler with `schema` takes them this way, or `None` if
45    /// they don't fit it.
46    pub fn take(self, schema: Option<&str>, args: &[String]) -> Option<Vec<String>> {
47        match (self, schema) {
48            (Fit::Exact, Some(schema)) => {
49                (schema.split_whitespace().count() == args.len()).then(|| args.to_vec())
50            }
51            (Fit::Joined, Some(schema)) => {
52                let types: Vec<&str> = schema.split_whitespace().collect();
53                let k = types.len();
54                if types.last() != Some(&GREEDY) || args.len() <= k {
55                    return None;
56                }
57                let mut parts = args[..k - 1].to_vec();
58                parts.push(args[k - 1..].join(WORD_SEP));
59                Some(parts)
60            }
61            (Fit::Plain, None) => Some(args.to_vec()),
62            _ => None,
63        }
64    }
65}
66
67#[cfg(test)]
68mod tests {
69    use super::*;
70
71    fn v(words: &[&str]) -> Vec<String> {
72        words.iter().map(|w| (*w).to_owned()).collect()
73    }
74
75    #[test]
76    fn exact_takes_as_many_arguments_as_the_schema_has() {
77        assert_eq!(
78            Fit::Exact.take(Some("int word"), &v(&["1", "x"])),
79            Some(v(&["1", "x"]))
80        );
81        assert_eq!(Fit::Exact.take(Some("int word"), &v(&["1"])), None);
82        assert_eq!(Fit::Exact.take(Some(""), &[]), Some(Vec::new()));
83    }
84
85    #[test]
86    fn joined_folds_the_extra_words_into_a_last_greedy_string() {
87        assert_eq!(
88            Fit::Joined.take(Some("int string"), &v(&["1", "a", "b"])),
89            Some(v(&["1", "a b"]))
90        );
91        assert_eq!(
92            Fit::Joined.take(Some("int word"), &v(&["1", "a", "b"])),
93            None
94        );
95        assert_eq!(
96            Fit::Joined.take(Some("int string"), &v(&["1", "a"])),
97            None,
98            "an exact fit isn't a join"
99        );
100    }
101
102    #[test]
103    fn plain_takes_anything_and_only_plain_handlers() {
104        assert_eq!(Fit::Plain.take(None, &v(&["a", "b"])), Some(v(&["a", "b"])));
105        assert_eq!(Fit::Plain.take(Some("int"), &v(&["1"])), None);
106        assert_eq!(Fit::Exact.take(None, &[]), None);
107    }
108
109    #[test]
110    fn earlier_ways_rank_higher() {
111        assert!(Fit::Exact.rank() > Fit::Joined.rank() && Fit::Joined.rank() > Fit::Plain.rank());
112        assert_eq!(Fit::ways(None), [Fit::Plain]);
113    }
114}