Separate out the logic for parsing a singular feature named by a string.

This should have ever-so-marginal improvements to performance, as now we deserialize a vector of integers in `SerializedCrubitFeatures`, instead of a vector of strings.

PiperOrigin-RevId: 665568861
Change-Id: Id8c5ed053e136123c19bbf175e45f127d32dbe3e
diff --git a/common/crubit_feature.rs b/common/crubit_feature.rs
index dda334a..0a81d12 100644
--- a/common/crubit_feature.rs
+++ b/common/crubit_feature.rs
@@ -4,6 +4,7 @@
 
 //! Supporting types to read and display Crubit feature flags
 //! (<internal link>)
+use serde::Deserialize;
 
 flagset::flags! {
     pub enum CrubitFeature : u8 {
@@ -32,28 +33,45 @@
     }
 }
 
-/// A newtype around a flagset of features, so that it can be deserialized from
-/// an array of strings instead of an integer.
+/// A newtype around a single named feature flagset, so that it can be
+/// deserialized from a string instead of an integer.
+#[derive(Debug, Default, PartialEq, Eq, Clone)]
+struct SerializedCrubitFeature(pub flagset::FlagSet<CrubitFeature>);
+
+impl<'de> Deserialize<'de> for SerializedCrubitFeature {
+    fn deserialize<D>(deserializer: D) -> Result<SerializedCrubitFeature, D::Error>
+    where
+        D: serde::Deserializer<'de>,
+    {
+        let features = match <String as Deserialize<'de>>::deserialize(deserializer)?.as_str() {
+            "all" => flagset::FlagSet::<CrubitFeature>::full(),
+            "supported" => CrubitFeature::Supported.into(),
+            "experimental" => CrubitFeature::Experimental.into(),
+            other => {
+                return Err(<D::Error as serde::de::Error>::custom(format!(
+                    "Unexpected Crubit feature: {other}"
+                )));
+            }
+        };
+        Ok(SerializedCrubitFeature(features))
+    }
+}
+
+/// A newtype around a union of named feature flagsets, so that it can be
+/// deserialized from an array of strings instead of an integer.
 #[derive(Debug, Default, PartialEq, Eq, Clone)]
 pub struct SerializedCrubitFeatures(pub flagset::FlagSet<CrubitFeature>);
 
-impl<'de> serde::Deserialize<'de> for SerializedCrubitFeatures {
+impl<'de> Deserialize<'de> for SerializedCrubitFeatures {
     fn deserialize<D>(deserializer: D) -> Result<SerializedCrubitFeatures, D::Error>
     where
         D: serde::Deserializer<'de>,
     {
         let mut features = flagset::FlagSet::<CrubitFeature>::default();
-        for feature in <Vec<String> as serde::Deserialize<'de>>::deserialize(deserializer)? {
-            features |= match &*feature {
-                "all" => flagset::FlagSet::<CrubitFeature>::full(),
-                "supported" => CrubitFeature::Supported.into(),
-                "experimental" => CrubitFeature::Experimental.into(),
-                other => {
-                    return Err(<D::Error as serde::de::Error>::custom(format!(
-                        "Unexpected Crubit feature: {other}"
-                    )));
-                }
-            };
+        for SerializedCrubitFeature(feature) in
+            <Vec<SerializedCrubitFeature> as Deserialize<'de>>::deserialize(deserializer)?
+        {
+            features |= feature;
         }
         Ok(SerializedCrubitFeatures(features))
     }
@@ -65,6 +83,18 @@
     use googletest::prelude::*;
 
     #[gtest]
+    fn test_serialized_crubit_feature() {
+        let SerializedCrubitFeature(features) = serde_json::from_str("\"supported\"").unwrap();
+        assert_eq!(features, CrubitFeature::Supported);
+    }
+
+    #[gtest]
+    fn test_serialized_crubit_feature_all() {
+        let SerializedCrubitFeature(features) = serde_json::from_str("\"all\"").unwrap();
+        assert_eq!(features, CrubitFeature::Supported | CrubitFeature::Experimental);
+    }
+
+    #[gtest]
     fn test_serialized_crubit_features_empty() {
         let SerializedCrubitFeatures(features) = serde_json::from_str("[]").unwrap();
         assert!(features.is_empty());