whiskers.git / web / cat / src / mascot.rs

Who the page can show. A closed set: the scene is an exhaustive match over it, so a mascot cannot be added without art for every mood (a compile error, not a fallback at run time), and a name from a visitor is parsed against it and nothing else.

5#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
6pub enum Mascot {

The ginger cat. The default, and first in the picker.

8    Ginger,

The grey cat, drawn once in whiskers-art and shared with the app.

10    Grey,

A rabbit, for the page.

12    Bunny,
13}
15impl Mascot {
16    pub const DEFAULT: Mascot = Mascot::Ginger;

Every mascot, in the order the picker lists them.

19    pub const ALL: &'static [Mascot] = &[Mascot::Ginger, Mascot::Grey, Mascot::Bunny];

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    }

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    }

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    }

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    }

Like [Mascot::parse] for a value that may be missing or wrong, such as a query: the 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}
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}