1//! Who the demo's picker can show: the thirteen characters of the app, from the one list (`whiskers_art::Character`). 2//! A closed set: a name from a visitor is parsed against it and nothing else. This adds what the page alone needs: 3//! the default, the order of the picker (the default first), and the figure as the page's `<svg id="cat">`. 4 5use whiskers_art::{Character as Art, Drawn}; 6 7use crate::mood::Mood; 8use crate::scene::scene; 9use crate::svg::{Mode, svg}; 10 11#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] 12pub struct Character(pub Art); 13 14impl Character { 15 /// The ginger cat: the default, and first in the picker. 16 pub const DEFAULT: Character = Character(Art::Drawn(Drawn::Ginger)); 17 18 /// Every character, in the order the picker lists them: the default first, then the app's order. 19 pub fn all() -> Vec<Character> { 20 let mut all: Vec<Character> = Art::all().into_iter().map(Character).collect(); 21 all.sort_by_key(|c| *c != Character::DEFAULT); 22 all 23 } 24 25 /// The word the page and its signals use: the app's id for the character. 26 pub fn name(self) -> &'static str { 27 self.0.id() 28 } 29 30 /// The word on the picker's button. 31 pub fn label(self) -> &'static str { 32 self.0.label() 33 } 34 35 /// What it is, for the picture's description. 36 pub fn kind(self) -> &'static str { 37 self.0.kind() 38 } 39 40 /// A name from outside, against the closed set. Anything else is `None`. 41 pub fn parse(name: &str) -> Option<Character> { 42 Art::parse(name).map(Character) 43 } 44 45 /// Like [`Character::parse`] for a value that may be missing or wrong, such as a query: the 46 /// default for anything that is not a character. 47 pub fn parse_or_default(name: Option<&str>) -> Character { 48 name.and_then(Character::parse).unwrap_or(Character::DEFAULT) 49 } 50 51 /// The character in a mood as the page's `<svg id="cat">`, with the pupils bound to the pointer by 52 /// `eyes_attrs` (raw markup the caller writes). 53 pub fn figure(self, mood: Mood, label: &str, eyes_attrs: &str) -> String { 54 svg(&scene(self, mood), label, &Mode::Live { eyes_attrs }) 55 } 56} 57 58#[cfg(test)] 59mod tests { 60 use super::*; 61 62 #[test] 63 fn the_default_is_first_and_the_rest_keep_the_apps_order() { 64 let labels: Vec<_> = Character::all().iter().map(|c| c.label()).collect(); 65 assert_eq!(labels, ["Ginger", "Grey", "Bunny", "Panda", "Koala", "Capybara", "Penguin", "Fox", "Rabbit", "Owl", "Unicorn", "Dog", "Sloth"]); 66 } 67 68 #[test] 69 fn every_name_is_unique_and_round_trips_and_nothing_else_parses() { 70 let mut names: Vec<_> = Character::all().iter().map(|c| c.name()).collect(); 71 names.sort(); 72 names.dedup(); 73 assert_eq!(names.len(), 13); 74 for c in Character::all() { 75 assert_eq!(Character::parse(c.name()), Some(c)); 76 } 77 for bad in [ 78 "", "Bunny", "BUNNY", "Ginger", " bunny", "bunny ", "rabbit", "Panda", "panda ", "rabbit white", "owl", "dog", "cat", "grey", "ginger", "zebra", "whiskers", "../etc", "bunny&x=1", "grey-cat\n", "beer", "star", 79 ] { 80 assert_eq!(Character::parse(bad), None, "{bad:?}"); 81 } 82 assert_eq!(Character::parse_or_default(Some("nonsense")), Character::DEFAULT); 83 assert_eq!(Character::parse_or_default(None), Character::DEFAULT); 84 assert_eq!(Character::parse_or_default(Some("owl-1")).name(), "owl-1"); 85 assert_eq!(Character::parse_or_default(Some("bunny")).label(), "Bunny"); 86 } 87}