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}