Allow deriving `Serialize` and `Deserialize` on generated refinement (#3227)

Marshall Bowers created

This PR adds support for deriving `Serialize` and `Deserialize` on the
refinement type generated by `#[derive(Refineable)]`.

Release Notes:

- N/A

Change summary

crates/refineable/derive_refineable/src/derive_refineable.rs | 44 +++++
crates/theme2/src/colors.rs                                  | 16 ++
2 files changed, 56 insertions(+), 4 deletions(-)

Detailed changes

crates/refineable/derive_refineable/src/derive_refineable.rs 🔗

@@ -16,9 +16,33 @@ pub fn derive_refineable(input: TokenStream) -> TokenStream {
         ..
     } = parse_macro_input!(input);
 
-    let impl_debug_on_refinement = attrs
-        .iter()
-        .any(|attr| attr.path.is_ident("refineable") && attr.tokens.to_string().contains("debug"));
+    let refineable_attr = attrs.iter().find(|attr| attr.path.is_ident("refineable"));
+
+    let mut impl_debug_on_refinement = false;
+    let mut derive_serialize_on_refinement = false;
+    let mut derive_deserialize_on_refinement = false;
+
+    if let Some(refineable_attr) = refineable_attr {
+        if let Ok(syn::Meta::List(meta_list)) = refineable_attr.parse_meta() {
+            for nested in meta_list.nested {
+                let syn::NestedMeta::Meta(syn::Meta::Path(path)) = nested else {
+                    continue;
+                };
+
+                if path.is_ident("debug") {
+                    impl_debug_on_refinement = true;
+                }
+
+                if path.is_ident("serialize") {
+                    derive_serialize_on_refinement = true;
+                }
+
+                if path.is_ident("deserialize") {
+                    derive_deserialize_on_refinement = true;
+                }
+            }
+        }
+    }
 
     let refinement_ident = format_ident!("{}Refinement", ident);
     let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
@@ -235,8 +259,22 @@ pub fn derive_refineable(input: TokenStream) -> TokenStream {
         quote! {}
     };
 
+    let derive_serialize = if derive_serialize_on_refinement {
+        quote! { #[derive(serde::Serialize)]}
+    } else {
+        quote! {}
+    };
+
+    let derive_deserialize = if derive_deserialize_on_refinement {
+        quote! { #[derive(serde::Deserialize)]}
+    } else {
+        quote! {}
+    };
+
     let gen = quote! {
         #[derive(Clone)]
+        #derive_serialize
+        #derive_deserialize
         pub struct #refinement_ident #impl_generics {
             #( #field_visibilities #field_names: #wrapped_types ),*
         }

crates/theme2/src/colors.rs 🔗

@@ -49,7 +49,7 @@ pub struct GitStatusColors {
 }
 
 #[derive(Refineable, Clone, Debug)]
-#[refineable(debug)]
+#[refineable(debug, deserialize)]
 pub struct ThemeColors {
     pub border: Hsla,
     pub border_variant: Hsla,
@@ -105,6 +105,8 @@ pub struct ThemeStyles {
 
 #[cfg(test)]
 mod tests {
+    use serde_json::json;
+
     use super::*;
 
     #[test]
@@ -146,4 +148,16 @@ mod tests {
         assert_eq!(colors.text, magenta);
         assert_eq!(colors.background, green);
     }
+
+    #[test]
+    fn deserialize_theme_colors_refinement_from_json() {
+        let colors: ThemeColorsRefinement = serde_json::from_value(json!({
+            "background": "#ff00ff",
+            "text": "#ff0000"
+        }))
+        .unwrap();
+
+        assert_eq!(colors.background, Some(gpui::rgb(0xff00ff)));
+        assert_eq!(colors.text, Some(gpui::rgb(0xff0000)));
+    }
 }