story_selector.rs

 1use std::{str::FromStr, sync::OnceLock};
 2
 3use anyhow::{anyhow, Context};
 4use clap::builder::PossibleValue;
 5use clap::ValueEnum;
 6use strum::{EnumIter, EnumString, IntoEnumIterator};
 7
 8#[derive(Debug, Clone, Copy, strum::Display, EnumString, EnumIter)]
 9#[strum(serialize_all = "snake_case")]
10pub enum ElementStory {
11    Avatar,
12}
13
14#[derive(Debug, Clone, Copy, strum::Display, EnumString, EnumIter)]
15#[strum(serialize_all = "snake_case")]
16pub enum ComponentStory {
17    Breadcrumb,
18    Facepile,
19    Toolbar,
20    TrafficLights,
21}
22
23#[derive(Debug, Clone, Copy)]
24pub enum StorySelector {
25    Element(ElementStory),
26    Component(ComponentStory),
27}
28
29impl FromStr for StorySelector {
30    type Err = anyhow::Error;
31
32    fn from_str(raw_story_name: &str) -> std::result::Result<Self, Self::Err> {
33        let story = raw_story_name.to_ascii_lowercase();
34
35        if let Some((_, story)) = story.split_once("elements/") {
36            let element_story = ElementStory::from_str(story)
37                .with_context(|| format!("story not found for element '{story}'"))?;
38
39            return Ok(Self::Element(element_story));
40        }
41
42        if let Some((_, story)) = story.split_once("components/") {
43            let component_story = ComponentStory::from_str(story)
44                .with_context(|| format!("story not found for component '{story}'"))?;
45
46            return Ok(Self::Component(component_story));
47        }
48
49        Err(anyhow!("story not found for '{raw_story_name}'"))
50    }
51}
52
53/// The list of all stories available in the storybook.
54static ALL_STORIES: OnceLock<Vec<StorySelector>> = OnceLock::new();
55
56impl ValueEnum for StorySelector {
57    fn value_variants<'a>() -> &'a [Self] {
58        let stories = ALL_STORIES.get_or_init(|| {
59            let element_stories = ElementStory::iter().map(Self::Element);
60            let component_stories = ComponentStory::iter().map(Self::Component);
61
62            element_stories.chain(component_stories).collect::<Vec<_>>()
63        });
64
65        stories
66    }
67
68    fn to_possible_value(&self) -> Option<clap::builder::PossibleValue> {
69        let value = match self {
70            Self::Element(story) => format!("elements/{story}"),
71            Self::Component(story) => format!("components/{story}"),
72        };
73
74        Some(PossibleValue::new(value))
75    }
76}