Remove manual mapping in `FromStr` implementation for `StorySelector` (#3018)

Marshall Bowers created

This PR removes the need for writing manual mappings in the `FromStr`
implementation for the `StorySelector` enum used in the storybook CLI.

We are now using the
[`EnumString`](https://docs.rs/strum/0.25.0/strum/derive.EnumString.html)
trait from `strum` to automatically derive snake_cased names for the
enums.

This will cut down on some of the manual work needed to wire up more
stories to the storybook.

Release Notes:

- N/A

Change summary

Cargo.lock                        | 23 +++++++++++++++++++++++
crates/storybook/Cargo.toml       |  1 +
crates/storybook/src/storybook.rs | 32 +++++++++++++++++++++++---------
3 files changed, 47 insertions(+), 9 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -7381,6 +7381,7 @@ dependencies = [
  "serde",
  "settings",
  "simplelog",
+ "strum",
  "theme",
  "ui",
  "util",
@@ -7403,6 +7404,28 @@ version = "0.10.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
 
+[[package]]
+name = "strum"
+version = "0.25.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "290d54ea6f91c969195bdbcd7442c8c2a2ba87da8bf60a7ee86a235d4bc1e125"
+dependencies = [
+ "strum_macros",
+]
+
+[[package]]
+name = "strum_macros"
+version = "0.25.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ad8d03b598d3d0fff69bf533ee3ef19b8eeb342729596df84bcc7e1f96ec4059"
+dependencies = [
+ "heck 0.4.1",
+ "proc-macro2",
+ "quote",
+ "rustversion",
+ "syn 2.0.37",
+]
+
 [[package]]
 name = "subtle"
 version = "2.4.1"

crates/storybook/Cargo.toml 🔗

@@ -17,6 +17,7 @@ rust-embed.workspace = true
 serde.workspace = true
 settings = { path = "../settings" }
 simplelog = "0.9"
+strum = { version = "0.25.0", features = ["derive"] }
 theme = { path = "../theme" }
 ui = { path = "../ui" }
 util = { path = "../util" }

crates/storybook/src/storybook.rs 🔗

@@ -17,6 +17,7 @@ use simplelog::SimpleLogger;
 use stories::components::facepile::FacepileStory;
 use stories::components::traffic_lights::TrafficLightsStory;
 use stories::elements::avatar::AvatarStory;
+use strum::EnumString;
 use ui::{ElementExt, Theme};
 
 gpui2::actions! {
@@ -33,22 +34,35 @@ enum StorySelector {
 impl FromStr for StorySelector {
     type Err = anyhow::Error;
 
-    fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
-        match s.to_ascii_lowercase().as_str() {
-            "elements/avatar" => Ok(Self::Element(ElementStory::Avatar)),
-            "components/facepile" => Ok(Self::Component(ComponentStory::Facepile)),
-            "components/traffic_lights" => Ok(Self::Component(ComponentStory::TrafficLights)),
-            _ => Err(anyhow!("story not found for '{s}'")),
+    fn from_str(raw_story_name: &str) -> std::result::Result<Self, Self::Err> {
+        let story = raw_story_name.to_ascii_lowercase();
+
+        if let Some((_, story)) = story.split_once("elements/") {
+            let element_story = ElementStory::from_str(story)
+                .with_context(|| format!("story not found for element '{story}'"))?;
+
+            return Ok(Self::Element(element_story));
+        }
+
+        if let Some((_, story)) = story.split_once("components/") {
+            let component_story = ComponentStory::from_str(story)
+                .with_context(|| format!("story not found for component '{story}'"))?;
+
+            return Ok(Self::Component(component_story));
         }
+
+        Err(anyhow!("story not found for '{raw_story_name}'"))
     }
 }
 
-#[derive(Debug, Clone, Copy)]
+#[derive(Debug, Clone, Copy, EnumString)]
+#[strum(serialize_all = "snake_case")]
 enum ElementStory {
     Avatar,
 }
 
-#[derive(Debug, Clone, Copy)]
+#[derive(Debug, Clone, Copy, EnumString)]
+#[strum(serialize_all = "snake_case")]
 enum ComponentStory {
     Facepile,
     TrafficLights,
@@ -122,7 +136,7 @@ fn current_theme<V: 'static>(cx: &mut ViewContext<V>) -> Theme {
         .clone()
 }
 
-use anyhow::{anyhow, Result};
+use anyhow::{anyhow, Context, Result};
 use gpui2::AssetSource;
 use rust_embed::RustEmbed;
 use workspace::WorkspaceElement;