whiskers.git / web / cat / src / mascot.rs
1//! Who the page can show. A closed set: the scene is an exhaustive `match` over it, so a mascot
2//! cannot be added without art for every mood (a compile error, not a fallback at run time), and
3//! a name from a visitor is parsed against it and nothing else.
4
5#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
6pub enum Mascot {
7    /// The ginger cat. The default, and first in the picker.
8    Ginger,
9    /// The grey cat, drawn once in `whiskers-art` and shared with the app.
10    Grey,
11    /// A rabbit, for the page.
12    Bunny,
13}
14
15impl Mascot {
16    pub const DEFAULT: Mascot = Mascot::Ginger;
17
18    /// Every mascot, in the order the picker lists them.
19    pub const ALL: &'static [Mascot] = &[Mascot::Ginger, Mascot::Grey, Mascot::Bunny];
20
21    /// The word the page and its signals use.
22    pub fn name(self) -> &'static str {
23        match self {
24            Mascot::Ginger => "ginger",
25            Mascot::Grey => "grey",
26            Mascot::Bunny => "bunny",
27        }
28    }
29
30    /// The word on the picker's button.
31    pub fn label(self) -> &'static str {
32        match self {
33            Mascot::Ginger => "Ginger",
34            Mascot::Grey => "Grey",
35            Mascot::Bunny => "Bunny",
36        }
37    }
38
39    /// What it is, for the picture's description.
40    pub fn kind(self) -> &'static str {
41        match self {
42            Mascot::Ginger => "ginger cat",
43            Mascot::Grey => "grey cat",
44            Mascot::Bunny => "rabbit",
45        }
46    }
47
48    /// A name from outside, against the closed set. Anything else is `None`.
49    pub fn parse(name: &str) -> Option<Mascot> {
50        Mascot::ALL.iter().copied().find(|m| m.name() == name)
51    }
52
53    /// Like [`Mascot::parse`] for a value that may be missing or wrong, such as a query: the
54    /// default for anything that is not a mascot.
55    pub fn parse_or_default(name: Option<&str>) -> Mascot {
56        name.and_then(Mascot::parse).unwrap_or(Mascot::DEFAULT)
57    }
58}
59
60#[cfg(test)]
61mod tests {
62    use super::*;
63
64    #[test]
65    fn ginger_is_the_default_and_listed_first_then_grey_then_bunny() {
66        assert_eq!(Mascot::DEFAULT, Mascot::Ginger);
67        assert_eq!(Mascot::ALL, &[Mascot::Ginger, Mascot::Grey, Mascot::Bunny]);
68        let labels: Vec<_> = Mascot::ALL.iter().map(|m| m.label()).collect();
69        assert_eq!(labels, ["Ginger", "Grey", "Bunny"]);
70    }
71
72    #[test]
73    fn names_round_trip_and_nothing_else_parses() {
74        for m in Mascot::ALL {
75            assert_eq!(Mascot::parse(m.name()), Some(*m));
76        }
77        for bad in ["", "Bunny", "BUNNY", "Ginger", " bunny", "bunny ", "rabbit", "cat", "zebra", "whiskers", "../etc", "bunny&x=1", "grey\n"] {
78            assert_eq!(Mascot::parse(bad), None, "{bad:?}");
79        }
80        assert_eq!(Mascot::parse_or_default(Some("nonsense")), Mascot::Ginger);
81        assert_eq!(Mascot::parse_or_default(None), Mascot::Ginger);
82        assert_eq!(Mascot::parse_or_default(Some("bunny")), Mascot::Bunny);
83        assert_eq!(Mascot::parse_or_default(Some("grey")), Mascot::Grey);
84        assert_eq!(Mascot::parse_or_default(Some("ginger")), Mascot::Ginger);
85    }
86}