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

Who the demo's picker can show: the thirteen characters of the app, from the one list (whiskers_art::Character). A closed set: a name from a visitor is parsed against it and nothing else. This adds what the page alone needs: the default, the order of the picker (the default first), and the figure as the page's <svg id="cat">.

5use whiskers_art::{Character as Art, Drawn};
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 {

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

16    pub const DEFAULT: Character = Character(Art::Drawn(Drawn::Ginger));

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    }

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    }

The word on the picker's button.

31    pub fn label(self) -> &'static str {
32        self.0.label()
33    }

What it is, for the picture's description.

36    pub fn kind(self) -> &'static str {
37        self.0.kind()
38    }

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    }

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

The character in a mood as the page's <svg id="cat">, with the pupils bound to the pointer by 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}
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}