From 16225b6f4771f184a1a9f003f734547bd9a955ac Mon Sep 17 00:00:00 2001 From: PokeJofeJr4th Date: Thu, 18 May 2023 21:57:42 -0400 Subject: [PATCH 01/18] added enum map macro and lib.rs test need to make error detection (dependency on EnumIter, etc.) more robust --- strum_macros/src/lib.rs | 46 +++++++++++ strum_macros/src/macros/enum_map.rs | 118 ++++++++++++++++++++++++++++ strum_macros/src/macros/mod.rs | 1 + 3 files changed, 165 insertions(+) create mode 100644 strum_macros/src/macros/enum_map.rs diff --git a/strum_macros/src/lib.rs b/strum_macros/src/lib.rs index cc1c2a1d..1b65f036 100644 --- a/strum_macros/src/lib.rs +++ b/strum_macros/src/lib.rs @@ -384,6 +384,52 @@ pub fn enum_iter(input: proc_macro::TokenStream) -> proc_macro::TokenStream { toks.into() } +/// Creates a new type that maps all the variants of an enum to another generic value. +/// +/// Iterate over the variants of an Enum. This macro does not support any additional data on your variants. +/// The macro creates a new type called `YourEnumMap` that is the map object. +/// You cannot derive `EnumIter` on any type with a lifetime bound (`<'a>`) because the map could +/// create [unbounded lifetimes](https://doc.rust-lang.org/nightly/nomicon/unbounded-lifetimes.html). +/// +/// ``` +/// use strum_macros::{EnumIter, EnumMap}; +/// +/// #[derive(Clone, Copy, EnumIter, EnumMap, Debug, PartialEq, Eq)] +/// enum Color { +/// Red, +/// Green, +/// Blue, +/// Yellow, +/// } +/// +/// let mut color_map: ColorMap = ColorMap::new(); +/// color_map[Color::Red] = 15; +/// color_map[Color::Blue] = 12; +/// +/// assert_eq!(15, color_map[Color::Red]); +/// assert_eq!(12, color_map[Color::Blue]); +/// assert_eq!(0, color_map[Color::Green]); +/// +/// color_map[Color::Green] = 75; +/// +/// let mut color_map_iter = color_map.into_iter(); +/// assert_eq!(Some((Color::Red, 15)), color_map_iter.next()); +/// assert_eq!(Some((Color::Green, 75)), color_map_iter.next()); +/// assert_eq!(Some((Color::Blue, 12)), color_map_iter.next()); +/// assert_eq!(Some((Color::Yellow, 0)), color_map_iter.next()); +/// assert_eq!(None, color_map_iter.next()); +/// +/// ``` +#[proc_macro_derive(EnumMap, attributes(strum))] +pub fn enum_map(input: proc_macro::TokenStream) -> proc_macro::TokenStream { + let ast = syn::parse_macro_input!(input as DeriveInput); + + let toks = + macros::enum_map::enum_map_inner(&ast).unwrap_or_else(|err| err.to_compile_error()); + debug_print_generated(&ast, &toks); + toks.into() +} + /// Add a function to enum that allows accessing variants by its discriminant /// /// This macro adds a standalone function to obtain an enum variant by its discriminant. The macro adds diff --git a/strum_macros/src/macros/enum_map.rs b/strum_macros/src/macros/enum_map.rs new file mode 100644 index 00000000..068eeb36 --- /dev/null +++ b/strum_macros/src/macros/enum_map.rs @@ -0,0 +1,118 @@ +use proc_macro2::{Span, TokenStream}; +use quote::quote; +use syn::{spanned::Spanned, Data, DeriveInput, Fields, Ident}; + +use crate::helpers::{non_enum_error, HasStrumVariantProperties}; + +pub fn enum_map_inner(ast: &DeriveInput) -> syn::Result { + let name = &ast.ident; + let gen = &ast.generics; + let vis = &ast.vis; + let doc_comment = format!("A map over the variants of [{}]", name); + + if gen.lifetimes().count() > 0 { + return Err(syn::Error::new( + Span::call_site(), + "This macro doesn't support enums with lifetimes.", + )); + } + + let variants = match &ast.data { + Data::Enum(v) => &v.variants, + _ => return Err(non_enum_error()), + }; + + let mut arms = Vec::new(); + let mut idx = 0usize; + for variant in variants { + if variant.get_variant_properties()?.disabled.is_some() { + continue; + } + + let ident = &variant.ident; + match &variant.fields { + Fields::Unit => {} + _ => { + return Err(syn::Error::new( + variant.fields.span(), + "This macro doesn't support enums with non-unit variants", + )) + } + }; + + arms.push(quote! {#name::#ident => #idx}); + idx += 1; + } + + let variant_count = arms.len(); + let map_name = syn::parse_str::(&format!("{}Map", name)).unwrap(); + + // Create a string literal "MyEnumMap" to use in the debug impl. + let map_name_debug_struct = + syn::parse_str::(&format!("\"{}\"", map_name)).unwrap(); + + Ok(quote! { + #[doc = #doc_comment] + #[allow( + missing_copy_implementations, + )] + #vis struct #map_name { + content: [T; #variant_count] + } + + impl #map_name { + fn new() -> Self { + Self { + content: Default::default() + } + } + } + + impl core::fmt::Debug for #map_name { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + // We don't know if the variants implement debug themselves so the only thing we + // can really show is how many elements are left. + f.debug_struct(#map_name_debug_struct) + .field("len", &#variant_count) + .finish() + } + } + + impl core::ops::Index<#name> for #map_name { + type Output = T; + + fn index(&self, idx: #name) -> &T { + &self.content[{match idx { + #(#arms),* + }}] + } + } + + impl core::ops::IndexMut<#name> for #map_name { + fn index_mut(&mut self, idx: #name) -> &mut T { + self.content.index_mut({match idx { + #(#arms),* + }}) + } + } + + impl core::iter::IntoIterator for #map_name { + type Item = (#name, T); + type IntoIter = as core::iter::IntoIterator>::IntoIter; + + fn into_iter(self) -> Self::IntoIter { + use strum::IntoEnumIterator; + let pairs: Vec<(#name, T)> = #name::iter().map(|variant| (variant, self[variant].clone())).collect(); + pairs.into_iter() + } + } + + impl Clone for #map_name { + fn clone(&self) -> Self { + Self { + content: self.content.clone() + } + } + } + }) +} diff --git a/strum_macros/src/macros/mod.rs b/strum_macros/src/macros/mod.rs index b4129697..335e22c6 100644 --- a/strum_macros/src/macros/mod.rs +++ b/strum_macros/src/macros/mod.rs @@ -1,6 +1,7 @@ pub mod enum_count; pub mod enum_discriminants; pub mod enum_iter; +pub mod enum_map; pub mod enum_messages; pub mod enum_properties; pub mod enum_variant_names; From 0fc2a6bda5d9c9fa4812fb6ed9e5ef3f2bd81b13 Mon Sep 17 00:00:00 2001 From: PokeJofeJr4th Date: Fri, 23 Jun 2023 21:54:46 -0400 Subject: [PATCH 02/18] Merged in some of June's code switched from array to struct --- strum_macros/src/lib.rs | 18 ++-- strum_macros/src/macros/enum_map.rs | 131 +++++++++++++++------------- 2 files changed, 80 insertions(+), 69 deletions(-) diff --git a/strum_macros/src/lib.rs b/strum_macros/src/lib.rs index 1b65f036..d0f0b810 100644 --- a/strum_macros/src/lib.rs +++ b/strum_macros/src/lib.rs @@ -394,7 +394,7 @@ pub fn enum_iter(input: proc_macro::TokenStream) -> proc_macro::TokenStream { /// ``` /// use strum_macros::{EnumIter, EnumMap}; /// -/// #[derive(Clone, Copy, EnumIter, EnumMap, Debug, PartialEq, Eq)] +/// #[derive(Clone, Copy, EnumIter, EnumMap, PartialEq, Eq)] /// enum Color { /// Red, /// Green, @@ -402,7 +402,7 @@ pub fn enum_iter(input: proc_macro::TokenStream) -> proc_macro::TokenStream { /// Yellow, /// } /// -/// let mut color_map: ColorMap = ColorMap::new(); +/// let mut color_map: ColorMap = ColorMap::default(); /// color_map[Color::Red] = 15; /// color_map[Color::Blue] = 12; /// @@ -412,12 +412,14 @@ pub fn enum_iter(input: proc_macro::TokenStream) -> proc_macro::TokenStream { /// /// color_map[Color::Green] = 75; /// -/// let mut color_map_iter = color_map.into_iter(); -/// assert_eq!(Some((Color::Red, 15)), color_map_iter.next()); -/// assert_eq!(Some((Color::Green, 75)), color_map_iter.next()); -/// assert_eq!(Some((Color::Blue, 12)), color_map_iter.next()); -/// assert_eq!(Some((Color::Yellow, 0)), color_map_iter.next()); -/// assert_eq!(None, color_map_iter.next()); +/// assert_eq!(15, color_map[Color::Red]); +/// assert_eq!(color_map[Color::Green], 75); +/// assert_eq!(color_map[Color::Blue], 12); +/// assert_eq!(color_map[Color::Yellow], 0); +/// +/// let mut float_map: ColorMap = ColorMap::default(); +/// +/// float_map[Color::Red] = f32::NAN; /// /// ``` #[proc_macro_derive(EnumMap, attributes(strum))] diff --git a/strum_macros/src/macros/enum_map.rs b/strum_macros/src/macros/enum_map.rs index 068eeb36..1f0e3876 100644 --- a/strum_macros/src/macros/enum_map.rs +++ b/strum_macros/src/macros/enum_map.rs @@ -1,5 +1,5 @@ use proc_macro2::{Span, TokenStream}; -use quote::quote; +use quote::{format_ident, quote}; use syn::{spanned::Spanned, Data, DeriveInput, Fields, Ident}; use crate::helpers::{non_enum_error, HasStrumVariantProperties}; @@ -17,100 +17,109 @@ pub fn enum_map_inner(ast: &DeriveInput) -> syn::Result { )); } - let variants = match &ast.data { - Data::Enum(v) => &v.variants, - _ => return Err(non_enum_error()), + let Data::Enum(data_enum) = &ast.data else { + return Err(non_enum_error()) }; - let mut arms = Vec::new(); - let mut idx = 0usize; + let variants = &data_enum.variants; + + // the identifiers of each variant, in PascalCase + let mut pascal_idents = Vec::new(); + // the identifiers of each struct field, in snake_case + let mut snake_idents = Vec::new(); + // match arms in the form `MyEnumMap::Variant => &self.variant,` + let mut get_matches = Vec::new(); + // match arms in the form `MyEnumMap::Variant => &mut self.variant,` + let mut get_matches_mut = Vec::new(); + // match arms in the form `MyEnumMap::Variant => self.variant = new_value` + let mut set_matches = Vec::new(); + for variant in variants { + // skip disabled variants if variant.get_variant_properties()?.disabled.is_some() { continue; } - - let ident = &variant.ident; - match &variant.fields { - Fields::Unit => {} - _ => { - return Err(syn::Error::new( - variant.fields.span(), - "This macro doesn't support enums with non-unit variants", - )) - } + // Error on fields with data + let Fields::Unit = &variant.fields else { + return Err(syn::Error::new( + variant.fields.span(), + "This macro doesn't support enums with non-unit variants", + )) }; - arms.push(quote! {#name::#ident => #idx}); - idx += 1; + let pascal_case = &variant.ident; + pascal_idents.push(pascal_case); + // switch PascalCase to snake_case. This naively assumes they use PascalCase + let snake_case = format_ident!( + "{}", + pascal_case + .to_string() + .chars() + .enumerate() + .fold(String::new(), |mut s, (i, c)| { + if c.is_uppercase() && i > 0 { + s.push('-'); + } + s.push(c.to_ascii_lowercase()); + s + }) + ); + + get_matches.push(quote! {#name::#pascal_case => &self.#snake_case,}); + get_matches_mut.push(quote! {#name::#pascal_case => &mut self.#snake_case,}); + set_matches.push(quote! {#name::#pascal_case => self.#snake_case = new_value,}); + snake_idents.push(snake_case); } - let variant_count = arms.len(); let map_name = syn::parse_str::(&format!("{}Map", name)).unwrap(); - // Create a string literal "MyEnumMap" to use in the debug impl. - let map_name_debug_struct = - syn::parse_str::(&format!("\"{}\"", map_name)).unwrap(); - Ok(quote! { #[doc = #doc_comment] #[allow( missing_copy_implementations, )] + #[derive(Debug, Clone, Default, PartialEq, Hash)] #vis struct #map_name { - content: [T; #variant_count] + #(#snake_idents: T,)* } - impl #map_name { - fn new() -> Self { - Self { - content: Default::default() + impl #map_name { + #vis fn new( + #(#snake_idents: T,)* + ) -> #map_name { + #map_name { + #(#snake_idents,)* } } - } - impl core::fmt::Debug for #map_name { - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - // We don't know if the variants implement debug themselves so the only thing we - // can really show is how many elements are left. - f.debug_struct(#map_name_debug_struct) - .field("len", &#variant_count) - .finish() - } + // // E.g. so that if you're using EnumIter as well, these functions work nicely + // fn get(&self, variant: #name) -> &T { + // match variant { + // #(#get_matches)* + // } + // } + + // fn set(&mut self, variant: #name, new_value: T) { + // match variant { + // #(#set_matches)* + // } + // } } impl core::ops::Index<#name> for #map_name { type Output = T; fn index(&self, idx: #name) -> &T { - &self.content[{match idx { - #(#arms),* - }}] + match idx { + #(#get_matches)* + } } } impl core::ops::IndexMut<#name> for #map_name { fn index_mut(&mut self, idx: #name) -> &mut T { - self.content.index_mut({match idx { - #(#arms),* - }}) - } - } - - impl core::iter::IntoIterator for #map_name { - type Item = (#name, T); - type IntoIter = as core::iter::IntoIterator>::IntoIter; - - fn into_iter(self) -> Self::IntoIter { - use strum::IntoEnumIterator; - let pairs: Vec<(#name, T)> = #name::iter().map(|variant| (variant, self[variant].clone())).collect(); - pairs.into_iter() - } - } - - impl Clone for #map_name { - fn clone(&self) -> Self { - Self { - content: self.content.clone() + match idx { + #(#get_matches_mut)* } } } From 8e488b27d937dfc119fcb669eea7aac39e599294 Mon Sep 17 00:00:00 2001 From: "pokejofejr4th@gmail.com" Date: Sat, 24 Jun 2023 13:54:14 -0400 Subject: [PATCH 03/18] added features #1, #2, #3 --- strum_macros/src/macros/enum_map.rs | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/strum_macros/src/macros/enum_map.rs b/strum_macros/src/macros/enum_map.rs index 1f0e3876..a021289c 100644 --- a/strum_macros/src/macros/enum_map.rs +++ b/strum_macros/src/macros/enum_map.rs @@ -33,6 +33,10 @@ pub fn enum_map_inner(ast: &DeriveInput) -> syn::Result { let mut get_matches_mut = Vec::new(); // match arms in the form `MyEnumMap::Variant => self.variant = new_value` let mut set_matches = Vec::new(); + // struct fields of the form `variant: func(MyEnum::Variant),* + let mut closure_fields = Vec::new(); + // struct fields of the form `variant: func(MyEnum::Variant, self.variant),` + let mut transform_fields = Vec::new(); for variant in variants { // skip disabled variants @@ -68,6 +72,8 @@ pub fn enum_map_inner(ast: &DeriveInput) -> syn::Result { get_matches.push(quote! {#name::#pascal_case => &self.#snake_case,}); get_matches_mut.push(quote! {#name::#pascal_case => &mut self.#snake_case,}); set_matches.push(quote! {#name::#pascal_case => self.#snake_case = new_value,}); + closure_fields.push(quote!{#snake_case: func(#name::#pascal_case),}); + transform_fields.push(quote!{#snake_case: func(#name::#pascal_case, self.#snake_case),}); snake_idents.push(snake_case); } @@ -91,6 +97,24 @@ pub fn enum_map_inner(ast: &DeriveInput) -> syn::Result { #(#snake_idents,)* } } + + #vis fn filled(value: T) -> #map_name { + #map_name { + #(#snake_idents: value.clone(),)* + } + } + + #vis fn from_closure T> -> #map_name { + #map_name { + #(#closure_fields)* + } + } + + #vis fn transform U> -> #map_name { + #map_name { + #(#transform_fields)* + } + } // // E.g. so that if you're using EnumIter as well, these functions work nicely // fn get(&self, variant: #name) -> &T { From 8e7aa05d8fc71db75a4fcab95770438b7b1c53c7 Mon Sep 17 00:00:00 2001 From: PokeJofeJr4th Date: Sun, 25 Jun 2023 08:11:17 -0400 Subject: [PATCH 04/18] added tests, fixed bad code from last commit --- strum_macros/src/macros/enum_map.rs | 30 ++++++++-------- strum_tests/tests/enum_map.rs | 54 +++++++++++++++++++++++++++++ 2 files changed, 70 insertions(+), 14 deletions(-) create mode 100644 strum_tests/tests/enum_map.rs diff --git a/strum_macros/src/macros/enum_map.rs b/strum_macros/src/macros/enum_map.rs index a021289c..e2d94035 100644 --- a/strum_macros/src/macros/enum_map.rs +++ b/strum_macros/src/macros/enum_map.rs @@ -52,7 +52,6 @@ pub fn enum_map_inner(ast: &DeriveInput) -> syn::Result { }; let pascal_case = &variant.ident; - pascal_idents.push(pascal_case); // switch PascalCase to snake_case. This naively assumes they use PascalCase let snake_case = format_ident!( "{}", @@ -72,8 +71,9 @@ pub fn enum_map_inner(ast: &DeriveInput) -> syn::Result { get_matches.push(quote! {#name::#pascal_case => &self.#snake_case,}); get_matches_mut.push(quote! {#name::#pascal_case => &mut self.#snake_case,}); set_matches.push(quote! {#name::#pascal_case => self.#snake_case = new_value,}); - closure_fields.push(quote!{#snake_case: func(#name::#pascal_case),}); - transform_fields.push(quote!{#snake_case: func(#name::#pascal_case, self.#snake_case),}); + closure_fields.push(quote! {#snake_case: func(#name::#pascal_case),}); + transform_fields.push(quote! {#snake_case: func(#name::#pascal_case, &self.#snake_case),}); + pascal_idents.push(pascal_case); snake_idents.push(snake_case); } @@ -84,11 +84,19 @@ pub fn enum_map_inner(ast: &DeriveInput) -> syn::Result { #[allow( missing_copy_implementations, )] - #[derive(Debug, Clone, Default, PartialEq, Hash)] + #[derive(Debug, Clone, Default, PartialEq, Eq, Hash)] #vis struct #map_name { #(#snake_idents: T,)* } + impl #map_name { + #vis fn filled(value: T) -> #map_name { + #map_name { + #(#snake_idents: value.clone(),)* + } + } + } + impl #map_name { #vis fn new( #(#snake_idents: T,)* @@ -97,20 +105,14 @@ pub fn enum_map_inner(ast: &DeriveInput) -> syn::Result { #(#snake_idents,)* } } - - #vis fn filled(value: T) -> #map_name { - #map_name { - #(#snake_idents: value.clone(),)* - } - } - - #vis fn from_closure T> -> #map_name { + + #vis fn from_closureT>(func: F) -> #map_name { #map_name { #(#closure_fields)* } } - - #vis fn transform U> -> #map_name { + + #vis fn transformU>(&self, func: F) -> #map_name { #map_name { #(#transform_fields)* } diff --git a/strum_tests/tests/enum_map.rs b/strum_tests/tests/enum_map.rs new file mode 100644 index 00000000..25461f06 --- /dev/null +++ b/strum_tests/tests/enum_map.rs @@ -0,0 +1,54 @@ +use strum::EnumMap; + +#[derive(EnumMap)] +enum Color { + Red, + Yellow, + Green, + Blue, +} + +#[test] +fn default() { + assert_eq!(ColorMap::default(), ColorMap::new(0, 0, 0, 0)); +} + +#[test] +fn filled() { + assert_eq!(ColorMap::filled(42), ColorMap::new(42, 42, 42, 42)); +} + +#[test] +fn from_closure() { + assert_eq!( + ColorMap::from_closure(|color| match color { + Color::Red => 1, + _ => 2, + }), + ColorMap::new(1, 2, 2, 2) + ); +} + +#[test] +fn index() { + let map = ColorMap::new(18, 25, 7, 2); + assert_eq!(map[Color::Red], 18); + assert_eq!(map[Color::Yellow], 25); + assert_eq!(map[Color::Green], 7); + assert_eq!(map[Color::Blue], 2); +} + +#[test] +fn index_mut() { + let mut map = ColorMap::new(18, 25, 7, 2); + map[Color::Green] = 5; + map[Color::Red] *= 4; + assert_eq!(map[Color::Green], 5); + assert_eq!(map[Color::Red], 72); +} + +#[test] +fn transform() { + let all_two = ColorMap::filled(2); + assert_eq!(all_two.transform(|_, n| *n * 2), ColorMap::filled(4)); +} From 3ccbb3caed3b0ccaab3a8c31e17b027687dd6622 Mon Sep 17 00:00:00 2001 From: PokeJofeJr4th Date: Sun, 25 Jun 2023 08:50:33 -0400 Subject: [PATCH 05/18] format --- strum_macros/src/macros/enum_map.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/strum_macros/src/macros/enum_map.rs b/strum_macros/src/macros/enum_map.rs index e2d94035..77496eaa 100644 --- a/strum_macros/src/macros/enum_map.rs +++ b/strum_macros/src/macros/enum_map.rs @@ -43,6 +43,7 @@ pub fn enum_map_inner(ast: &DeriveInput) -> syn::Result { if variant.get_variant_properties()?.disabled.is_some() { continue; } + // Error on fields with data let Fields::Unit = &variant.fields else { return Err(syn::Error::new( From 7bd40a3687dddabd287fed12be0f32fae8806aa5 Mon Sep 17 00:00:00 2001 From: PokeJofeJr4th Date: Sun, 25 Jun 2023 09:31:20 -0400 Subject: [PATCH 06/18] added `all` and `all_ok` for options and results --- strum_macros/src/macros/enum_map.rs | 44 ++++++++++++++++++++++++++++- strum_tests/tests/enum_map.rs | 23 +++++++++++++++ 2 files changed, 66 insertions(+), 1 deletion(-) diff --git a/strum_macros/src/macros/enum_map.rs b/strum_macros/src/macros/enum_map.rs index 77496eaa..a8af75b9 100644 --- a/strum_macros/src/macros/enum_map.rs +++ b/strum_macros/src/macros/enum_map.rs @@ -43,7 +43,7 @@ pub fn enum_map_inner(ast: &DeriveInput) -> syn::Result { if variant.get_variant_properties()?.disabled.is_some() { continue; } - + // Error on fields with data let Fields::Unit = &variant.fields else { return Err(syn::Error::new( @@ -80,6 +80,14 @@ pub fn enum_map_inner(ast: &DeriveInput) -> syn::Result { let map_name = syn::parse_str::(&format!("{}Map", name)).unwrap(); + let doc_new = format!("Create a new {map_name} with a value for each variant of {name}"); + let doc_closure = + format!("Create a new {map_name} by running a function on each variant of `{name}`"); + let doc_transform = format!("Create a new `{map_name}` by running a function on each variant of `{name}` and the corresponding value in the current `{map_name}`"); + let doc_filled = format!("Create a new `{map_name}` with the same value in each field."); + let doc_option_all = format!("Converts `{map_name}>` into `Option<{map_name}>`. Returns `Some` if all fields are `Some`, otherwise returns `None`."); + let doc_result_all_ok = format!("Converts `{map_name}>` into `Option<{map_name}>`. Returns `Some` if all fields are `Ok`, otherwise returns `None`."); + Ok(quote! { #[doc = #doc_comment] #[allow( @@ -91,6 +99,7 @@ pub fn enum_map_inner(ast: &DeriveInput) -> syn::Result { } impl #map_name { + #[doc = #doc_filled] #vis fn filled(value: T) -> #map_name { #map_name { #(#snake_idents: value.clone(),)* @@ -99,6 +108,7 @@ pub fn enum_map_inner(ast: &DeriveInput) -> syn::Result { } impl #map_name { + #[doc = #doc_new] #vis fn new( #(#snake_idents: T,)* ) -> #map_name { @@ -107,12 +117,14 @@ pub fn enum_map_inner(ast: &DeriveInput) -> syn::Result { } } + #[doc = #doc_closure] #vis fn from_closureT>(func: F) -> #map_name { #map_name { #(#closure_fields)* } } + #[doc = #doc_transform] #vis fn transformU>(&self, func: F) -> #map_name { #map_name { #(#transform_fields)* @@ -150,5 +162,35 @@ pub fn enum_map_inner(ast: &DeriveInput) -> syn::Result { } } } + + impl #map_name> { + #[doc = #doc_option_all] + #vis fn all(self) -> Option<#map_name> { + if let #map_name { + #(#snake_idents: Some(#snake_idents),)* + } = self { + Some(#map_name { + #(#snake_idents,)* + }) + } else { + None + } + } + } + + impl #map_name> { + #[doc = #doc_result_all_ok] + #vis fn all_ok(self) -> Option<#map_name> { + if let #map_name { + #(#snake_idents: Ok(#snake_idents),)* + } = self { + Some(#map_name { + #(#snake_idents,)* + }) + } else { + None + } + } + } }) } diff --git a/strum_tests/tests/enum_map.rs b/strum_tests/tests/enum_map.rs index 25461f06..fba81419 100644 --- a/strum_tests/tests/enum_map.rs +++ b/strum_tests/tests/enum_map.rs @@ -47,6 +47,29 @@ fn index_mut() { assert_eq!(map[Color::Red], 72); } +#[test] +fn option_all() { + let mut map: ColorMap> = ColorMap::filled(None); + map[Color::Red] = Some(64); + map[Color::Green] = Some(32); + map[Color::Blue] = Some(16); + + assert_eq!(map.clone().all(), None); + + map[Color::Yellow] = Some(8); + assert_eq!(map.all(), Some(ColorMap::new(64, 8, 32, 16))); +} + +#[test] +fn result_all_ok() { + let mut map: ColorMap> = ColorMap::filled(Ok(4)); + assert_eq!(map.clone().all_ok(), Some(ColorMap::filled(4))); + + map[Color::Red] = Err(22); + + assert_eq!(map.all_ok(), None); +} + #[test] fn transform() { let all_two = ColorMap::filled(2); From 6112e6dc56d1266a4f7e9c9c34d12884603a07af Mon Sep 17 00:00:00 2001 From: PokeJofeJr4th <65093167+PokeJofeJr4th@users.noreply.github.com> Date: Sun, 25 Jun 2023 10:55:58 -0400 Subject: [PATCH 07/18] Include EnumMap in README.md --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 491c24f2..b3d77901 100644 --- a/README.md +++ b/README.md @@ -42,6 +42,7 @@ Strum has implemented the following macros: | [IntoStaticStr] | Implements `From for &'static str` on an enum | | [EnumVariantNames] | Adds an associated `VARIANTS` constant which is an array of discriminant names | | [EnumIter] | Creates a new type that iterates of the variants of an enum. | +| [EnumMap] | Creates a new type that stores an item of a specified type for each variant of the enum. | | [EnumProperty] | Add custom properties to enum variants. | | [EnumMessage] | Add a verbose message to an enum variant. | | [EnumDiscriminants] | Generate a new type with only the discriminant names. | From b66a61215709e80ed3a1f3dad93fc1995ca33dbc Mon Sep 17 00:00:00 2001 From: PokeJofeJr4th Date: Sun, 25 Jun 2023 11:26:56 -0400 Subject: [PATCH 08/18] updated documentation --- strum_macros/src/lib.rs | 48 ++++++++++++++++------------------------- 1 file changed, 19 insertions(+), 29 deletions(-) diff --git a/strum_macros/src/lib.rs b/strum_macros/src/lib.rs index 5e5eb3ba..bf688e86 100644 --- a/strum_macros/src/lib.rs +++ b/strum_macros/src/lib.rs @@ -411,48 +411,38 @@ pub fn enum_is(input: proc_macro::TokenStream) -> proc_macro::TokenStream { /// Creates a new type that maps all the variants of an enum to another generic value. /// -/// Iterate over the variants of an Enum. This macro does not support any additional data on your variants. -/// The macro creates a new type called `YourEnumMap` that is the map object. -/// You cannot derive `EnumIter` on any type with a lifetime bound (`<'a>`) because the map could -/// create [unbounded lifetimes](https://doc.rust-lang.org/nightly/nomicon/unbounded-lifetimes.html). -/// +/// This macro does not support any additional data on your variants. +/// The macro creates a new type called `YourEnumMap`. +/// The map has a field of type `T` for each variant of `YourEnum`. The map automatically implements `Index` and `IndexMut`. /// ``` -/// use strum_macros::{EnumIter, EnumMap}; +/// use strum_macros::EnumMap; /// -/// #[derive(Clone, Copy, EnumIter, EnumMap, PartialEq, Eq)] +/// #[derive(EnumMap)] /// enum Color { /// Red, +/// Yellow, /// Green, /// Blue, -/// Yellow, /// } /// -/// let mut color_map: ColorMap = ColorMap::default(); -/// color_map[Color::Red] = 15; -/// color_map[Color::Blue] = 12; -/// -/// assert_eq!(15, color_map[Color::Red]); -/// assert_eq!(12, color_map[Color::Blue]); -/// assert_eq!(0, color_map[Color::Green]); -/// -/// color_map[Color::Green] = 75; -/// -/// assert_eq!(15, color_map[Color::Red]); -/// assert_eq!(color_map[Color::Green], 75); -/// assert_eq!(color_map[Color::Blue], 12); -/// assert_eq!(color_map[Color::Yellow], 0); -/// -/// let mut float_map: ColorMap = ColorMap::default(); -/// -/// float_map[Color::Red] = f32::NAN; -/// +/// assert_eq!(ColorMap::default(), ColorMap::new(0, 0, 0, 0)); +/// assert_eq!(ColorMap::filled(2), ColorMap::new(2, 2, 2, 2)); +/// assert_eq!(ColorMap::from_closure(|_| 3), ColorMap::new(3, 3, 3, 3)); +/// assert_eq!(ColorMap::default().transform(|_, val| val + 2), ColorMap::new(2, 2, 2, 2)); +/// +/// let mut complex_map = ColorMap::from_closure(|color| match color { +/// Color::Red => 0, +/// _ => 3 +/// }); +/// complex_map[Color::Green] = complex_map[Color::Red]; +/// assert_eq!(complex_map, ColorMap::new(0, 3, 0, 3)); +/// /// ``` #[proc_macro_derive(EnumMap, attributes(strum))] pub fn enum_map(input: proc_macro::TokenStream) -> proc_macro::TokenStream { let ast = syn::parse_macro_input!(input as DeriveInput); - let toks = - macros::enum_map::enum_map_inner(&ast).unwrap_or_else(|err| err.to_compile_error()); + let toks = macros::enum_map::enum_map_inner(&ast).unwrap_or_else(|err| err.to_compile_error()); debug_print_generated(&ast, &toks); toks.into() } From 510a160fb2697d2014774736355d847c202a0af9 Mon Sep 17 00:00:00 2001 From: PokeJofeJr4th Date: Sun, 25 Jun 2023 14:16:11 -0400 Subject: [PATCH 09/18] implemented 2nd solution to #5 - panic when indexed --- strum_macros/src/macros/enum_map.rs | 25 +++++++++++++++++++++++-- strum_tests/tests/enum_map.rs | 10 ++++++++++ 2 files changed, 33 insertions(+), 2 deletions(-) diff --git a/strum_macros/src/macros/enum_map.rs b/strum_macros/src/macros/enum_map.rs index a8af75b9..725b1e29 100644 --- a/strum_macros/src/macros/enum_map.rs +++ b/strum_macros/src/macros/enum_map.rs @@ -8,7 +8,7 @@ pub fn enum_map_inner(ast: &DeriveInput) -> syn::Result { let name = &ast.ident; let gen = &ast.generics; let vis = &ast.vis; - let doc_comment = format!("A map over the variants of [{}]", name); + let mut doc_comment = format!("A map over the variants of [{}]", name); if gen.lifetimes().count() > 0 { return Err(syn::Error::new( @@ -20,6 +20,7 @@ pub fn enum_map_inner(ast: &DeriveInput) -> syn::Result { let Data::Enum(data_enum) = &ast.data else { return Err(non_enum_error()) }; + let map_name = syn::parse_str::(&format!("{}Map", name)).unwrap(); let variants = &data_enum.variants; @@ -38,9 +39,19 @@ pub fn enum_map_inner(ast: &DeriveInput) -> syn::Result { // struct fields of the form `variant: func(MyEnum::Variant, self.variant),` let mut transform_fields = Vec::new(); + // identifiers for disabled variants + let mut disabled_variants = Vec::new(); + // match arms for disabled variants + let mut disabled_matches = Vec::new(); + for variant in variants { // skip disabled variants if variant.get_variant_properties()?.disabled.is_some() { + let disabled_ident = &variant.ident; + let panic_message = + format!("Can't use `{disabled_ident}` with `{map_name}` - variant is disabled for Strum features"); + disabled_variants.push(disabled_ident); + disabled_matches.push(quote!(#name::#disabled_ident => panic!(#panic_message),)); continue; } @@ -78,7 +89,15 @@ pub fn enum_map_inner(ast: &DeriveInput) -> syn::Result { snake_idents.push(snake_case); } - let map_name = syn::parse_str::(&format!("{}Map", name)).unwrap(); + // if the index operation can panic, add that to the documentation + if !disabled_variants.is_empty() { + doc_comment.push_str(&format!( + "\n# Panics\nIndexing `{map_name}` with any of the following variants will cause a panic:" + )); + for variant in disabled_variants { + doc_comment.push_str(&format!("\n\n- `{name}::{variant}`")); + } + } let doc_new = format!("Create a new {map_name} with a value for each variant of {name}"); let doc_closure = @@ -151,6 +170,7 @@ pub fn enum_map_inner(ast: &DeriveInput) -> syn::Result { fn index(&self, idx: #name) -> &T { match idx { #(#get_matches)* + #(#disabled_matches)* } } } @@ -159,6 +179,7 @@ pub fn enum_map_inner(ast: &DeriveInput) -> syn::Result { fn index_mut(&mut self, idx: #name) -> &mut T { match idx { #(#get_matches_mut)* + #(#disabled_matches)* } } } diff --git a/strum_tests/tests/enum_map.rs b/strum_tests/tests/enum_map.rs index fba81419..35fed355 100644 --- a/strum_tests/tests/enum_map.rs +++ b/strum_tests/tests/enum_map.rs @@ -5,7 +5,11 @@ enum Color { Red, Yellow, Green, + #[strum(disabled)] + Teal, Blue, + #[strum(disabled)] + Indigo, } #[test] @@ -13,6 +17,12 @@ fn default() { assert_eq!(ColorMap::default(), ColorMap::new(0, 0, 0, 0)); } +#[test] +#[should_panic] +fn disabled() { + let _ = ColorMap::::default()[Color::Indigo]; +} + #[test] fn filled() { assert_eq!(ColorMap::filled(42), ColorMap::new(42, 42, 42, 42)); From 6a20524b4de60484fa41014997ad592a2beae904 Mon Sep 17 00:00:00 2001 From: PokeJofeJr4th <65093167+PokeJofeJr4th@users.noreply.github.com> Date: Wed, 28 Jun 2023 13:57:54 -0400 Subject: [PATCH 10/18] Remove commented get and set Co-authored-by: June <61218022+itsjunetime@users.noreply.github.com> --- strum_macros/src/macros/enum_map.rs | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/strum_macros/src/macros/enum_map.rs b/strum_macros/src/macros/enum_map.rs index 725b1e29..c935642a 100644 --- a/strum_macros/src/macros/enum_map.rs +++ b/strum_macros/src/macros/enum_map.rs @@ -150,18 +150,6 @@ pub fn enum_map_inner(ast: &DeriveInput) -> syn::Result { } } - // // E.g. so that if you're using EnumIter as well, these functions work nicely - // fn get(&self, variant: #name) -> &T { - // match variant { - // #(#get_matches)* - // } - // } - - // fn set(&mut self, variant: #name, new_value: T) { - // match variant { - // #(#set_matches)* - // } - // } } impl core::ops::Index<#name> for #map_name { From e4ec66cfda0c61f19d5d003d5341e4b2abf5d502 Mon Sep 17 00:00:00 2001 From: PokeJofeJr4th Date: Wed, 28 Jun 2023 17:41:09 -0400 Subject: [PATCH 11/18] used snakify from `enum_is`, used `format_ident` macro moved snakify into `helpers` mod --- strum_macros/src/helpers/mod.rs | 18 ++++++++++++++++++ strum_macros/src/macros/enum_is.rs | 20 +------------------- strum_macros/src/macros/enum_map.rs | 22 ++++------------------ 3 files changed, 23 insertions(+), 37 deletions(-) diff --git a/strum_macros/src/helpers/mod.rs b/strum_macros/src/helpers/mod.rs index 11aebc83..a685f49d 100644 --- a/strum_macros/src/helpers/mod.rs +++ b/strum_macros/src/helpers/mod.rs @@ -7,6 +7,7 @@ mod metadata; pub mod type_props; pub mod variant_props; +use heck::ToSnakeCase; use proc_macro2::Span; use quote::ToTokens; use syn::spanned::Spanned; @@ -30,3 +31,20 @@ pub fn occurrence_error(fst: T, snd: T, attr: &str) -> syn::Error { e.combine(syn::Error::new_spanned(fst, "first one here")); e } + +/// heck doesn't treat numbers as new words, but this function does. +/// E.g. for input `Hello2You`, heck would output `hello2_you`, and snakify would output `hello_2_you`. +pub fn snakify(s: &str) -> String { + let mut output: Vec = s.to_string().to_snake_case().chars().collect(); + let mut num_starts = vec![]; + for (pos, c) in output.iter().enumerate() { + if c.is_digit(10) && pos != 0 && !output[pos - 1].is_digit(10) { + num_starts.push(pos); + } + } + // need to do in reverse, because after inserting, all chars after the point of insertion are off + for i in num_starts.into_iter().rev() { + output.insert(i, '_') + } + output.into_iter().collect() +} diff --git a/strum_macros/src/macros/enum_is.rs b/strum_macros/src/macros/enum_is.rs index bde38519..b69fa0c5 100644 --- a/strum_macros/src/macros/enum_is.rs +++ b/strum_macros/src/macros/enum_is.rs @@ -1,5 +1,4 @@ -use crate::helpers::{non_enum_error, HasStrumVariantProperties}; -use heck::ToSnakeCase; +use crate::helpers::{non_enum_error, snakify, HasStrumVariantProperties}; use proc_macro2::TokenStream; use quote::{format_ident, quote}; use syn::{Data, DeriveInput}; @@ -42,20 +41,3 @@ pub fn enum_is_inner(ast: &DeriveInput) -> syn::Result { } .into()) } - -/// heck doesn't treat numbers as new words, but this function does. -/// E.g. for input `Hello2You`, heck would output `hello2_you`, and snakify would output `hello_2_you`. -fn snakify(s: &str) -> String { - let mut output: Vec = s.to_string().to_snake_case().chars().collect(); - let mut num_starts = vec![]; - for (pos, c) in output.iter().enumerate() { - if c.is_digit(10) && pos != 0 && !output[pos - 1].is_digit(10) { - num_starts.push(pos); - } - } - // need to do in reverse, because after inserting, all chars after the point of insertion are off - for i in num_starts.into_iter().rev() { - output.insert(i, '_') - } - output.into_iter().collect() -} diff --git a/strum_macros/src/macros/enum_map.rs b/strum_macros/src/macros/enum_map.rs index c935642a..0260055e 100644 --- a/strum_macros/src/macros/enum_map.rs +++ b/strum_macros/src/macros/enum_map.rs @@ -1,8 +1,8 @@ use proc_macro2::{Span, TokenStream}; use quote::{format_ident, quote}; -use syn::{spanned::Spanned, Data, DeriveInput, Fields, Ident}; +use syn::{spanned::Spanned, Data, DeriveInput, Fields}; -use crate::helpers::{non_enum_error, HasStrumVariantProperties}; +use crate::helpers::{non_enum_error, snakify, HasStrumVariantProperties}; pub fn enum_map_inner(ast: &DeriveInput) -> syn::Result { let name = &ast.ident; @@ -20,7 +20,7 @@ pub fn enum_map_inner(ast: &DeriveInput) -> syn::Result { let Data::Enum(data_enum) = &ast.data else { return Err(non_enum_error()) }; - let map_name = syn::parse_str::(&format!("{}Map", name)).unwrap(); + let map_name = format_ident!("{}Map", name); let variants = &data_enum.variants; @@ -64,21 +64,7 @@ pub fn enum_map_inner(ast: &DeriveInput) -> syn::Result { }; let pascal_case = &variant.ident; - // switch PascalCase to snake_case. This naively assumes they use PascalCase - let snake_case = format_ident!( - "{}", - pascal_case - .to_string() - .chars() - .enumerate() - .fold(String::new(), |mut s, (i, c)| { - if c.is_uppercase() && i > 0 { - s.push('-'); - } - s.push(c.to_ascii_lowercase()); - s - }) - ); + let snake_case = snakify(&pascal_case.to_string()); get_matches.push(quote! {#name::#pascal_case => &self.#snake_case,}); get_matches_mut.push(quote! {#name::#pascal_case => &mut self.#snake_case,}); From c3eed1cf3980bef77977af4aa9f493643f43fa7d Mon Sep 17 00:00:00 2001 From: PokeJofeJr4th Date: Wed, 28 Jun 2023 17:44:01 -0400 Subject: [PATCH 12/18] fixed format_ident --- strum_macros/src/macros/enum_map.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/strum_macros/src/macros/enum_map.rs b/strum_macros/src/macros/enum_map.rs index 0260055e..23144d87 100644 --- a/strum_macros/src/macros/enum_map.rs +++ b/strum_macros/src/macros/enum_map.rs @@ -64,7 +64,7 @@ pub fn enum_map_inner(ast: &DeriveInput) -> syn::Result { }; let pascal_case = &variant.ident; - let snake_case = snakify(&pascal_case.to_string()); + let snake_case = format_ident!("{}", snakify(&pascal_case.to_string())); get_matches.push(quote! {#name::#pascal_case => &self.#snake_case,}); get_matches_mut.push(quote! {#name::#pascal_case => &mut self.#snake_case,}); From 5f2f1c1ec16d83871edb6a1c0b21d963e82f5f5e Mon Sep 17 00:00:00 2001 From: PokeJofeJr4th Date: Fri, 30 Jun 2023 09:06:18 -0400 Subject: [PATCH 13/18] add test for `clone`, change `all_ok` to return `Result` --- strum_macros/src/macros/enum_map.rs | 14 ++++---------- strum_tests/tests/enum_map.rs | 15 +++++++++++---- 2 files changed, 15 insertions(+), 14 deletions(-) diff --git a/strum_macros/src/macros/enum_map.rs b/strum_macros/src/macros/enum_map.rs index 23144d87..eefa02ca 100644 --- a/strum_macros/src/macros/enum_map.rs +++ b/strum_macros/src/macros/enum_map.rs @@ -175,16 +175,10 @@ pub fn enum_map_inner(ast: &DeriveInput) -> syn::Result { impl #map_name> { #[doc = #doc_result_all_ok] - #vis fn all_ok(self) -> Option<#map_name> { - if let #map_name { - #(#snake_idents: Ok(#snake_idents),)* - } = self { - Some(#map_name { - #(#snake_idents,)* - }) - } else { - None - } + #vis fn all_ok(self) -> Result<#map_name, E> { + Ok(#map_name { + #(#snake_idents: self.#snake_idents?,)* + }) } } }) diff --git a/strum_tests/tests/enum_map.rs b/strum_tests/tests/enum_map.rs index 35fed355..05f7f8db 100644 --- a/strum_tests/tests/enum_map.rs +++ b/strum_tests/tests/enum_map.rs @@ -39,6 +39,12 @@ fn from_closure() { ); } +#[test] +fn clone() { + let cm = ColorMap::filled(String::from("Some Text Data")); + assert_eq!(cm.clone(), cm); +} + #[test] fn index() { let map = ColorMap::new(18, 25, 7, 2); @@ -73,11 +79,12 @@ fn option_all() { #[test] fn result_all_ok() { let mut map: ColorMap> = ColorMap::filled(Ok(4)); - assert_eq!(map.clone().all_ok(), Some(ColorMap::filled(4))); - + assert_eq!(map.clone().all_ok(), Ok(ColorMap::filled(4))); map[Color::Red] = Err(22); - - assert_eq!(map.all_ok(), None); + map[Color::Yellow] = Err(100); + assert_eq!(map.clone().all_ok(), Err(22)); + map[Color::Red] = Ok(1); + assert_eq!(map.all_ok(), Err(100)); } #[test] From 69da32643c2eebf2b39725544dac683f406e7485 Mon Sep 17 00:00:00 2001 From: PokeJofeJr4th Date: Sat, 1 Jul 2023 07:55:02 -0400 Subject: [PATCH 14/18] add error for empty enums (resolves #7), added test for keyword collision (#8) --- strum_macros/src/macros/enum_map.rs | 14 +++++++++++--- strum_tests/tests/enum_map.rs | 5 +++++ 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/strum_macros/src/macros/enum_map.rs b/strum_macros/src/macros/enum_map.rs index eefa02ca..014ed4c4 100644 --- a/strum_macros/src/macros/enum_map.rs +++ b/strum_macros/src/macros/enum_map.rs @@ -13,7 +13,7 @@ pub fn enum_map_inner(ast: &DeriveInput) -> syn::Result { if gen.lifetimes().count() > 0 { return Err(syn::Error::new( Span::call_site(), - "This macro doesn't support enums with lifetimes.", + "`EnumMap` doesn't support enums with lifetimes.", )); } @@ -55,11 +55,11 @@ pub fn enum_map_inner(ast: &DeriveInput) -> syn::Result { continue; } - // Error on fields with data + // Error on variants with data let Fields::Unit = &variant.fields else { return Err(syn::Error::new( variant.fields.span(), - "This macro doesn't support enums with non-unit variants", + "`EnumMap` doesn't support enums with non-unit variants", )) }; @@ -75,6 +75,14 @@ pub fn enum_map_inner(ast: &DeriveInput) -> syn::Result { snake_idents.push(snake_case); } + // Error on empty enums + if pascal_idents.is_empty() { + return Err(syn::Error::new( + variants.span(), + "`EnumMap` requires at least one non-disabled variant", + )); + } + // if the index operation can panic, add that to the documentation if !disabled_variants.is_empty() { doc_comment.push_str(&format!( diff --git a/strum_tests/tests/enum_map.rs b/strum_tests/tests/enum_map.rs index 05f7f8db..558bd75b 100644 --- a/strum_tests/tests/enum_map.rs +++ b/strum_tests/tests/enum_map.rs @@ -12,6 +12,11 @@ enum Color { Indigo, } +#[derive(EnumMap)] +enum Keyword { + Const, +} + #[test] fn default() { assert_eq!(ColorMap::default(), ColorMap::new(0, 0, 0, 0)); From f283ab2d32463c6f159c40928fd31d164be326e0 Mon Sep 17 00:00:00 2001 From: PokeJofeJr4th Date: Sun, 16 Jul 2023 21:41:35 -0400 Subject: [PATCH 15/18] escape identifiers with `_` to prevent keyword name collisions resolves #8 --- strum_macros/src/macros/enum_map.rs | 2 +- strum_tests/tests/enum_map.rs | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/strum_macros/src/macros/enum_map.rs b/strum_macros/src/macros/enum_map.rs index 014ed4c4..93f9c683 100644 --- a/strum_macros/src/macros/enum_map.rs +++ b/strum_macros/src/macros/enum_map.rs @@ -64,7 +64,7 @@ pub fn enum_map_inner(ast: &DeriveInput) -> syn::Result { }; let pascal_case = &variant.ident; - let snake_case = format_ident!("{}", snakify(&pascal_case.to_string())); + let snake_case = format_ident!("_{}", snakify(&pascal_case.to_string())); get_matches.push(quote! {#name::#pascal_case => &self.#snake_case,}); get_matches_mut.push(quote! {#name::#pascal_case => &mut self.#snake_case,}); diff --git a/strum_tests/tests/enum_map.rs b/strum_tests/tests/enum_map.rs index 558bd75b..78dd13d0 100644 --- a/strum_tests/tests/enum_map.rs +++ b/strum_tests/tests/enum_map.rs @@ -12,6 +12,8 @@ enum Color { Indigo, } +// even though this isn't used, it needs to be a test +// because if it doesn't compile, enum variants that conflict with keywords won't work #[derive(EnumMap)] enum Keyword { Const, From 5d65bfce42fb9fe94125ce186b3ddc486f1278b0 Mon Sep 17 00:00:00 2001 From: PokeJofeJr4th Date: Sun, 16 Jul 2023 21:53:21 -0400 Subject: [PATCH 16/18] rename EnumMap to EnumTable --- strum_macros/src/lib.rs | 27 +++---- .../src/macros/{enum_map.rs => enum_table.rs} | 74 +++++++++---------- strum_macros/src/macros/mod.rs | 2 +- .../tests/{enum_map.rs => enum_table.rs} | 34 ++++----- 4 files changed, 69 insertions(+), 68 deletions(-) rename strum_macros/src/macros/{enum_map.rs => enum_table.rs} (64%) rename strum_tests/tests/{enum_map.rs => enum_table.rs} (59%) diff --git a/strum_macros/src/lib.rs b/strum_macros/src/lib.rs index bf688e86..a2610a72 100644 --- a/strum_macros/src/lib.rs +++ b/strum_macros/src/lib.rs @@ -412,12 +412,12 @@ pub fn enum_is(input: proc_macro::TokenStream) -> proc_macro::TokenStream { /// Creates a new type that maps all the variants of an enum to another generic value. /// /// This macro does not support any additional data on your variants. -/// The macro creates a new type called `YourEnumMap`. -/// The map has a field of type `T` for each variant of `YourEnum`. The map automatically implements `Index` and `IndexMut`. +/// The macro creates a new type called `YourEnumTable`. +/// The table has a field of type `T` for each variant of `YourEnum`. The table automatically implements `Index` and `IndexMut`. /// ``` -/// use strum_macros::EnumMap; +/// use strum_macros::EnumTable; /// -/// #[derive(EnumMap)] +/// #[derive(EnumTable)] /// enum Color { /// Red, /// Yellow, @@ -425,24 +425,25 @@ pub fn enum_is(input: proc_macro::TokenStream) -> proc_macro::TokenStream { /// Blue, /// } /// -/// assert_eq!(ColorMap::default(), ColorMap::new(0, 0, 0, 0)); -/// assert_eq!(ColorMap::filled(2), ColorMap::new(2, 2, 2, 2)); -/// assert_eq!(ColorMap::from_closure(|_| 3), ColorMap::new(3, 3, 3, 3)); -/// assert_eq!(ColorMap::default().transform(|_, val| val + 2), ColorMap::new(2, 2, 2, 2)); +/// assert_eq!(ColorTable::default(), ColorTable::new(0, 0, 0, 0)); +/// assert_eq!(ColorTable::filled(2), ColorTable::new(2, 2, 2, 2)); +/// assert_eq!(ColorTable::from_closure(|_| 3), ColorTable::new(3, 3, 3, 3)); +/// assert_eq!(ColorTable::default().transform(|_, val| val + 2), ColorTable::new(2, 2, 2, 2)); /// -/// let mut complex_map = ColorMap::from_closure(|color| match color { +/// let mut complex_map = ColorTable::from_closure(|color| match color { /// Color::Red => 0, /// _ => 3 /// }); /// complex_map[Color::Green] = complex_map[Color::Red]; -/// assert_eq!(complex_map, ColorMap::new(0, 3, 0, 3)); +/// assert_eq!(complex_map, ColorTable::new(0, 3, 0, 3)); /// /// ``` -#[proc_macro_derive(EnumMap, attributes(strum))] -pub fn enum_map(input: proc_macro::TokenStream) -> proc_macro::TokenStream { +#[proc_macro_derive(EnumTable, attributes(strum))] +pub fn enum_table(input: proc_macro::TokenStream) -> proc_macro::TokenStream { let ast = syn::parse_macro_input!(input as DeriveInput); - let toks = macros::enum_map::enum_map_inner(&ast).unwrap_or_else(|err| err.to_compile_error()); + let toks = + macros::enum_table::enum_table_inner(&ast).unwrap_or_else(|err| err.to_compile_error()); debug_print_generated(&ast, &toks); toks.into() } diff --git a/strum_macros/src/macros/enum_map.rs b/strum_macros/src/macros/enum_table.rs similarity index 64% rename from strum_macros/src/macros/enum_map.rs rename to strum_macros/src/macros/enum_table.rs index 93f9c683..48d47218 100644 --- a/strum_macros/src/macros/enum_map.rs +++ b/strum_macros/src/macros/enum_table.rs @@ -4,23 +4,23 @@ use syn::{spanned::Spanned, Data, DeriveInput, Fields}; use crate::helpers::{non_enum_error, snakify, HasStrumVariantProperties}; -pub fn enum_map_inner(ast: &DeriveInput) -> syn::Result { +pub fn enum_table_inner(ast: &DeriveInput) -> syn::Result { let name = &ast.ident; let gen = &ast.generics; let vis = &ast.vis; - let mut doc_comment = format!("A map over the variants of [{}]", name); + let mut doc_comment = format!("A map over the variants of `{name}`"); if gen.lifetimes().count() > 0 { return Err(syn::Error::new( Span::call_site(), - "`EnumMap` doesn't support enums with lifetimes.", + "`EnumTable` doesn't support enums with lifetimes.", )); } let Data::Enum(data_enum) = &ast.data else { return Err(non_enum_error()) }; - let map_name = format_ident!("{}Map", name); + let table_name = format_ident!("{}Table", name); let variants = &data_enum.variants; @@ -28,11 +28,11 @@ pub fn enum_map_inner(ast: &DeriveInput) -> syn::Result { let mut pascal_idents = Vec::new(); // the identifiers of each struct field, in snake_case let mut snake_idents = Vec::new(); - // match arms in the form `MyEnumMap::Variant => &self.variant,` + // match arms in the form `MyEnumTable::Variant => &self.variant,` let mut get_matches = Vec::new(); - // match arms in the form `MyEnumMap::Variant => &mut self.variant,` + // match arms in the form `MyEnumTable::Variant => &mut self.variant,` let mut get_matches_mut = Vec::new(); - // match arms in the form `MyEnumMap::Variant => self.variant = new_value` + // match arms in the form `MyEnumTable::Variant => self.variant = new_value` let mut set_matches = Vec::new(); // struct fields of the form `variant: func(MyEnum::Variant),* let mut closure_fields = Vec::new(); @@ -49,7 +49,7 @@ pub fn enum_map_inner(ast: &DeriveInput) -> syn::Result { if variant.get_variant_properties()?.disabled.is_some() { let disabled_ident = &variant.ident; let panic_message = - format!("Can't use `{disabled_ident}` with `{map_name}` - variant is disabled for Strum features"); + format!("Can't use `{disabled_ident}` with `{table_name}` - variant is disabled for Strum features"); disabled_variants.push(disabled_ident); disabled_matches.push(quote!(#name::#disabled_ident => panic!(#panic_message),)); continue; @@ -59,7 +59,7 @@ pub fn enum_map_inner(ast: &DeriveInput) -> syn::Result { let Fields::Unit = &variant.fields else { return Err(syn::Error::new( variant.fields.span(), - "`EnumMap` doesn't support enums with non-unit variants", + "`EnumTable` doesn't support enums with non-unit variants", )) }; @@ -79,27 +79,27 @@ pub fn enum_map_inner(ast: &DeriveInput) -> syn::Result { if pascal_idents.is_empty() { return Err(syn::Error::new( variants.span(), - "`EnumMap` requires at least one non-disabled variant", + "`EnumTable` requires at least one non-disabled variant", )); } // if the index operation can panic, add that to the documentation if !disabled_variants.is_empty() { doc_comment.push_str(&format!( - "\n# Panics\nIndexing `{map_name}` with any of the following variants will cause a panic:" + "\n# Panics\nIndexing `{table_name}` with any of the following variants will cause a panic:" )); for variant in disabled_variants { doc_comment.push_str(&format!("\n\n- `{name}::{variant}`")); } } - let doc_new = format!("Create a new {map_name} with a value for each variant of {name}"); + let doc_new = format!("Create a new {table_name} with a value for each variant of {name}"); let doc_closure = - format!("Create a new {map_name} by running a function on each variant of `{name}`"); - let doc_transform = format!("Create a new `{map_name}` by running a function on each variant of `{name}` and the corresponding value in the current `{map_name}`"); - let doc_filled = format!("Create a new `{map_name}` with the same value in each field."); - let doc_option_all = format!("Converts `{map_name}>` into `Option<{map_name}>`. Returns `Some` if all fields are `Some`, otherwise returns `None`."); - let doc_result_all_ok = format!("Converts `{map_name}>` into `Option<{map_name}>`. Returns `Some` if all fields are `Ok`, otherwise returns `None`."); + format!("Create a new {table_name} by running a function on each variant of `{name}`"); + let doc_transform = format!("Create a new `{table_name}` by running a function on each variant of `{name}` and the corresponding value in the current `{table_name}`"); + let doc_filled = format!("Create a new `{table_name}` with the same value in each field."); + let doc_option_all = format!("Converts `{table_name}>` into `Option<{table_name}>`. Returns `Some` if all fields are `Some`, otherwise returns `None`."); + let doc_result_all_ok = format!("Converts `{table_name}>` into `Result<{table_name}, E>`. Returns `Ok` if all fields are `Ok`, otherwise returns `Err`."); Ok(quote! { #[doc = #doc_comment] @@ -107,46 +107,46 @@ pub fn enum_map_inner(ast: &DeriveInput) -> syn::Result { missing_copy_implementations, )] #[derive(Debug, Clone, Default, PartialEq, Eq, Hash)] - #vis struct #map_name { + #vis struct #table_name { #(#snake_idents: T,)* } - impl #map_name { + impl #table_name { #[doc = #doc_filled] - #vis fn filled(value: T) -> #map_name { - #map_name { + #vis fn filled(value: T) -> #table_name { + #table_name { #(#snake_idents: value.clone(),)* } } } - impl #map_name { + impl #table_name { #[doc = #doc_new] #vis fn new( #(#snake_idents: T,)* - ) -> #map_name { - #map_name { + ) -> #table_name { + #table_name { #(#snake_idents,)* } } #[doc = #doc_closure] - #vis fn from_closureT>(func: F) -> #map_name { - #map_name { + #vis fn from_closureT>(func: F) -> #table_name { + #table_name { #(#closure_fields)* } } #[doc = #doc_transform] - #vis fn transformU>(&self, func: F) -> #map_name { - #map_name { + #vis fn transformU>(&self, func: F) -> #table_name { + #table_name { #(#transform_fields)* } } } - impl core::ops::Index<#name> for #map_name { + impl core::ops::Index<#name> for #table_name { type Output = T; fn index(&self, idx: #name) -> &T { @@ -157,7 +157,7 @@ pub fn enum_map_inner(ast: &DeriveInput) -> syn::Result { } } - impl core::ops::IndexMut<#name> for #map_name { + impl core::ops::IndexMut<#name> for #table_name { fn index_mut(&mut self, idx: #name) -> &mut T { match idx { #(#get_matches_mut)* @@ -166,13 +166,13 @@ pub fn enum_map_inner(ast: &DeriveInput) -> syn::Result { } } - impl #map_name> { + impl #table_name> { #[doc = #doc_option_all] - #vis fn all(self) -> Option<#map_name> { - if let #map_name { + #vis fn all(self) -> Option<#table_name> { + if let #table_name { #(#snake_idents: Some(#snake_idents),)* } = self { - Some(#map_name { + Some(#table_name { #(#snake_idents,)* }) } else { @@ -181,10 +181,10 @@ pub fn enum_map_inner(ast: &DeriveInput) -> syn::Result { } } - impl #map_name> { + impl #table_name> { #[doc = #doc_result_all_ok] - #vis fn all_ok(self) -> Result<#map_name, E> { - Ok(#map_name { + #vis fn all_ok(self) -> Result<#table_name, E> { + Ok(#table_name { #(#snake_idents: self.#snake_idents?,)* }) } diff --git a/strum_macros/src/macros/mod.rs b/strum_macros/src/macros/mod.rs index cc35a70e..9b157844 100644 --- a/strum_macros/src/macros/mod.rs +++ b/strum_macros/src/macros/mod.rs @@ -2,7 +2,7 @@ pub mod enum_count; pub mod enum_discriminants; pub mod enum_is; pub mod enum_iter; -pub mod enum_map; +pub mod enum_table; pub mod enum_messages; pub mod enum_properties; pub mod enum_variant_names; diff --git a/strum_tests/tests/enum_map.rs b/strum_tests/tests/enum_table.rs similarity index 59% rename from strum_tests/tests/enum_map.rs rename to strum_tests/tests/enum_table.rs index 78dd13d0..25e854fd 100644 --- a/strum_tests/tests/enum_map.rs +++ b/strum_tests/tests/enum_table.rs @@ -1,6 +1,6 @@ -use strum::EnumMap; +use strum::EnumTable; -#[derive(EnumMap)] +#[derive(EnumTable)] enum Color { Red, Yellow, @@ -14,47 +14,47 @@ enum Color { // even though this isn't used, it needs to be a test // because if it doesn't compile, enum variants that conflict with keywords won't work -#[derive(EnumMap)] +#[derive(EnumTable)] enum Keyword { Const, } #[test] fn default() { - assert_eq!(ColorMap::default(), ColorMap::new(0, 0, 0, 0)); + assert_eq!(ColorTable::default(), ColorTable::new(0, 0, 0, 0)); } #[test] #[should_panic] fn disabled() { - let _ = ColorMap::::default()[Color::Indigo]; + let _ = ColorTable::::default()[Color::Indigo]; } #[test] fn filled() { - assert_eq!(ColorMap::filled(42), ColorMap::new(42, 42, 42, 42)); + assert_eq!(ColorTable::filled(42), ColorTable::new(42, 42, 42, 42)); } #[test] fn from_closure() { assert_eq!( - ColorMap::from_closure(|color| match color { + ColorTable::from_closure(|color| match color { Color::Red => 1, _ => 2, }), - ColorMap::new(1, 2, 2, 2) + ColorTable::new(1, 2, 2, 2) ); } #[test] fn clone() { - let cm = ColorMap::filled(String::from("Some Text Data")); + let cm = ColorTable::filled(String::from("Some Text Data")); assert_eq!(cm.clone(), cm); } #[test] fn index() { - let map = ColorMap::new(18, 25, 7, 2); + let map = ColorTable::new(18, 25, 7, 2); assert_eq!(map[Color::Red], 18); assert_eq!(map[Color::Yellow], 25); assert_eq!(map[Color::Green], 7); @@ -63,7 +63,7 @@ fn index() { #[test] fn index_mut() { - let mut map = ColorMap::new(18, 25, 7, 2); + let mut map = ColorTable::new(18, 25, 7, 2); map[Color::Green] = 5; map[Color::Red] *= 4; assert_eq!(map[Color::Green], 5); @@ -72,7 +72,7 @@ fn index_mut() { #[test] fn option_all() { - let mut map: ColorMap> = ColorMap::filled(None); + let mut map: ColorTable> = ColorTable::filled(None); map[Color::Red] = Some(64); map[Color::Green] = Some(32); map[Color::Blue] = Some(16); @@ -80,13 +80,13 @@ fn option_all() { assert_eq!(map.clone().all(), None); map[Color::Yellow] = Some(8); - assert_eq!(map.all(), Some(ColorMap::new(64, 8, 32, 16))); + assert_eq!(map.all(), Some(ColorTable::new(64, 8, 32, 16))); } #[test] fn result_all_ok() { - let mut map: ColorMap> = ColorMap::filled(Ok(4)); - assert_eq!(map.clone().all_ok(), Ok(ColorMap::filled(4))); + let mut map: ColorTable> = ColorTable::filled(Ok(4)); + assert_eq!(map.clone().all_ok(), Ok(ColorTable::filled(4))); map[Color::Red] = Err(22); map[Color::Yellow] = Err(100); assert_eq!(map.clone().all_ok(), Err(22)); @@ -96,6 +96,6 @@ fn result_all_ok() { #[test] fn transform() { - let all_two = ColorMap::filled(2); - assert_eq!(all_two.transform(|_, n| *n * 2), ColorMap::filled(4)); + let all_two = ColorTable::filled(2); + assert_eq!(all_two.transform(|_, n| *n * 2), ColorTable::filled(4)); } From c4d4ca555d7839c86ca9f551c99478f0510c3bcd Mon Sep 17 00:00:00 2001 From: PokeJofeJr4th Date: Sat, 26 Aug 2023 20:00:49 -0400 Subject: [PATCH 17/18] absolute paths, get rid of `let ... else` --- strum_macros/src/macros/enum_table.rs | 32 +++++++++++++-------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/strum_macros/src/macros/enum_table.rs b/strum_macros/src/macros/enum_table.rs index 48d47218..78b91bec 100644 --- a/strum_macros/src/macros/enum_table.rs +++ b/strum_macros/src/macros/enum_table.rs @@ -17,12 +17,12 @@ pub fn enum_table_inner(ast: &DeriveInput) -> syn::Result { )); } - let Data::Enum(data_enum) = &ast.data else { - return Err(non_enum_error()) + let variants = match &ast.data { + Data::Enum(v) => &v.variants, + _ => return Err(non_enum_error()), }; - let table_name = format_ident!("{}Table", name); - let variants = &data_enum.variants; + let table_name = format_ident!("{}Table", name); // the identifiers of each variant, in PascalCase let mut pascal_idents = Vec::new(); @@ -56,11 +56,11 @@ pub fn enum_table_inner(ast: &DeriveInput) -> syn::Result { } // Error on variants with data - let Fields::Unit = &variant.fields else { + if variant.fields != Fields::Unit { return Err(syn::Error::new( variant.fields.span(), "`EnumTable` doesn't support enums with non-unit variants", - )) + )); }; let pascal_case = &variant.ident; @@ -146,7 +146,7 @@ pub fn enum_table_inner(ast: &DeriveInput) -> syn::Result { } - impl core::ops::Index<#name> for #table_name { + impl ::core::ops::Index<#name> for #table_name { type Output = T; fn index(&self, idx: #name) -> &T { @@ -157,7 +157,7 @@ pub fn enum_table_inner(ast: &DeriveInput) -> syn::Result { } } - impl core::ops::IndexMut<#name> for #table_name { + impl ::core::ops::IndexMut<#name> for #table_name { fn index_mut(&mut self, idx: #name) -> &mut T { match idx { #(#get_matches_mut)* @@ -166,25 +166,25 @@ pub fn enum_table_inner(ast: &DeriveInput) -> syn::Result { } } - impl #table_name> { + impl #table_name<::core::option::Option> { #[doc = #doc_option_all] - #vis fn all(self) -> Option<#table_name> { + #vis fn all(self) -> ::core::option::Option<#table_name> { if let #table_name { - #(#snake_idents: Some(#snake_idents),)* + #(#snake_idents: ::core::option::Option::Some(#snake_idents),)* } = self { - Some(#table_name { + ::core::option::Option::Some(#table_name { #(#snake_idents,)* }) } else { - None + ::core::option::Option::None } } } - impl #table_name> { + impl #table_name<::core::result::Result> { #[doc = #doc_result_all_ok] - #vis fn all_ok(self) -> Result<#table_name, E> { - Ok(#table_name { + #vis fn all_ok(self) -> ::core::result::Result<#table_name, E> { + ::core::result::Result::Ok(#table_name { #(#snake_idents: self.#snake_idents?,)* }) } From 5df066696339793bf7bd2714b6a38e1e848cf964 Mon Sep 17 00:00:00 2001 From: PokeJofeJr4th Date: Sat, 26 Aug 2023 22:59:20 -0400 Subject: [PATCH 18/18] Replace format macro invocations with arguments rather than inline so they'll work with rust 1.56.1 --- strum_macros/src/macros/enum_table.rs | 35 ++++++++++++++++++--------- 1 file changed, 23 insertions(+), 12 deletions(-) diff --git a/strum_macros/src/macros/enum_table.rs b/strum_macros/src/macros/enum_table.rs index 78b91bec..f9d4e81d 100644 --- a/strum_macros/src/macros/enum_table.rs +++ b/strum_macros/src/macros/enum_table.rs @@ -8,7 +8,7 @@ pub fn enum_table_inner(ast: &DeriveInput) -> syn::Result { let name = &ast.ident; let gen = &ast.generics; let vis = &ast.vis; - let mut doc_comment = format!("A map over the variants of `{name}`"); + let mut doc_comment = format!("A map over the variants of `{}`", name); if gen.lifetimes().count() > 0 { return Err(syn::Error::new( @@ -48,8 +48,10 @@ pub fn enum_table_inner(ast: &DeriveInput) -> syn::Result { // skip disabled variants if variant.get_variant_properties()?.disabled.is_some() { let disabled_ident = &variant.ident; - let panic_message = - format!("Can't use `{disabled_ident}` with `{table_name}` - variant is disabled for Strum features"); + let panic_message = format!( + "Can't use `{}` with `{}` - variant is disabled for Strum features", + disabled_ident, table_name + ); disabled_variants.push(disabled_ident); disabled_matches.push(quote!(#name::#disabled_ident => panic!(#panic_message),)); continue; @@ -86,20 +88,29 @@ pub fn enum_table_inner(ast: &DeriveInput) -> syn::Result { // if the index operation can panic, add that to the documentation if !disabled_variants.is_empty() { doc_comment.push_str(&format!( - "\n# Panics\nIndexing `{table_name}` with any of the following variants will cause a panic:" + "\n# Panics\nIndexing `{}` with any of the following variants will cause a panic:", + table_name )); for variant in disabled_variants { - doc_comment.push_str(&format!("\n\n- `{name}::{variant}`")); + doc_comment.push_str(&format!("\n\n- `{}::{}`", name, variant)); } } - let doc_new = format!("Create a new {table_name} with a value for each variant of {name}"); - let doc_closure = - format!("Create a new {table_name} by running a function on each variant of `{name}`"); - let doc_transform = format!("Create a new `{table_name}` by running a function on each variant of `{name}` and the corresponding value in the current `{table_name}`"); - let doc_filled = format!("Create a new `{table_name}` with the same value in each field."); - let doc_option_all = format!("Converts `{table_name}>` into `Option<{table_name}>`. Returns `Some` if all fields are `Some`, otherwise returns `None`."); - let doc_result_all_ok = format!("Converts `{table_name}>` into `Result<{table_name}, E>`. Returns `Ok` if all fields are `Ok`, otherwise returns `Err`."); + let doc_new = format!( + "Create a new {} with a value for each variant of {}", + table_name, name + ); + let doc_closure = format!( + "Create a new {} by running a function on each variant of `{}`", + table_name, name + ); + let doc_transform = format!("Create a new `{}` by running a function on each variant of `{}` and the corresponding value in the current `{0}`", table_name, name); + let doc_filled = format!( + "Create a new `{}` with the same value in each field.", + table_name + ); + let doc_option_all = format!("Converts `{}>` into `Option<{0}>`. Returns `Some` if all fields are `Some`, otherwise returns `None`.", table_name); + let doc_result_all_ok = format!("Converts `{}>` into `Result<{0}, E>`. Returns `Ok` if all fields are `Ok`, otherwise returns `Err`.", table_name); Ok(quote! { #[doc = #doc_comment]