diff --git a/CHANGELOG.md b/CHANGELOG.md index af69b519..eecb0a0c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,19 @@ # Changelog +## 0.26.4 (strum_macros) + +* [#360](/~https://github.com/Peternator7/strum/pull/360): Fixes bug introduced with new string interpolation feature where + unit structs took an implicit unnecessary dependency on `::core::alloc`. + +## 0.26.3 (strum_macros) + +* [#344](/~https://github.com/Peternator7/strum/pull/344): Hide `EnumTable` because it's going to be deprecated in the next + version. +* [#357](/~https://github.com/Peternator7/strum/pull/357): Fixes an incompatiblity with `itertools` by using the fully + qualified name rather than the inherent method. +* [#345](/~https://github.com/Peternator7/strum/pull/345): Allows unnamed tuple like variants to use their variants in + string interpolation. `#[strum(to_string = "Field 0: {0}, Field 1: {1})")]` will now work for tuple variants + ## 0.26.2 * [#337](/~https://github.com/Peternator7/strum/pull/337): Fix missing generic impls for `EnumTryAs` diff --git a/strum/src/lib.rs b/strum/src/lib.rs index 65c2e55f..3b5af9cf 100644 --- a/strum/src/lib.rs +++ b/strum/src/lib.rs @@ -67,7 +67,7 @@ impl std::error::Error for ParseError { } /// This trait designates that an `Enum` can be iterated over. It can -/// be auto generated using `strum_macros` on your behalf. +/// be auto generated using the [`EnumIter`](derive.EnumIter.html) derive macro. /// /// # Example /// diff --git a/strum_macros/Cargo.toml b/strum_macros/Cargo.toml index 16a52c72..c053ac26 100644 --- a/strum_macros/Cargo.toml +++ b/strum_macros/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "strum_macros" -version = "0.26.2" +version = "0.26.4" edition = "2018" authors = ["Peter Glotfelty "] license = "MIT" diff --git a/strum_macros/src/lib.rs b/strum_macros/src/lib.rs index 089dfc12..38e81f25 100644 --- a/strum_macros/src/lib.rs +++ b/strum_macros/src/lib.rs @@ -3,6 +3,10 @@ //! Strum is a set of macros and traits for working with //! enums and strings easier in Rust. //! +//! This crate only contains derive macros for use with the +//! [`strum`](https://docs.rs/strum) +//! crate. The macros provied by this crate are also available by +//! enabling the `derive` feature in aforementioned `strum` crate. #![recursion_limit = "128"] @@ -367,7 +371,7 @@ pub fn to_string(input: proc_macro::TokenStream) -> proc_macro::TokenStream { /// 3. The name of the variant will be used if there are no `serialize` or `to_string` attributes. /// 4. If the enum has a `strum(prefix = "some_value_")`, every variant will have that prefix prepended /// to the serialization. -/// 5. Enums with named fields support named field interpolation. The value will be interpolated into the output string. +/// 5. Enums with fields support string interpolation. /// Note this means the variant will not "round trip" if you then deserialize the string. /// /// ```rust @@ -375,6 +379,8 @@ pub fn to_string(input: proc_macro::TokenStream) -> proc_macro::TokenStream { /// pub enum Color { /// #[strum(to_string = "saturation is {sat}")] /// Red { sat: usize }, +/// #[strum(to_string = "hue is {1}, saturation is {0}")] +/// Blue(usize, usize), /// } /// ``` /// @@ -426,7 +432,7 @@ pub fn display(input: proc_macro::TokenStream) -> proc_macro::TokenStream { /// Creates a new type that iterates of the variants of an enum. /// /// Iterate over the variants of an Enum. Any additional data on your variants will be set to `Default::default()`. -/// The macro implements `strum::IntoEnumIterator` on your enum and creates a new type called `YourEnumIter` that is the iterator object. +/// The macro implements [`strum::IntoEnumIterator`](https://docs.rs/strum/latest/strum/trait.IntoEnumIterator.html) on your enum and creates a new type called `YourEnumIter` that is the iterator object. /// You cannot derive `EnumIter` on any type with a lifetime bound (`<'a>`) because the iterator would surely /// create [unbounded lifetimes](https://doc.rust-lang.org/nightly/nomicon/unbounded-lifetimes.html). /// diff --git a/strum_macros/src/macros/enum_iter.rs b/strum_macros/src/macros/enum_iter.rs index 5e810019..f8a20700 100644 --- a/strum_macros/src/macros/enum_iter.rs +++ b/strum_macros/src/macros/enum_iter.rs @@ -131,7 +131,7 @@ pub fn enum_iter_inner(ast: &DeriveInput) -> syn::Result { ::core::option::Option::None } else { self.idx = idx; - self.get(idx - 1) + #iter_name::get(self, idx - 1) } } } @@ -154,7 +154,7 @@ pub fn enum_iter_inner(ast: &DeriveInput) -> syn::Result { ::core::option::Option::None } else { self.back_idx = back_idx; - self.get(#variant_count - self.back_idx) + #iter_name::get(self, #variant_count - self.back_idx) } } } diff --git a/strum_macros/src/macros/strings/display.rs b/strum_macros/src/macros/strings/display.rs index a2f6f24a..5e7a4229 100644 --- a/strum_macros/src/macros/strings/display.rs +++ b/strum_macros/src/macros/strings/display.rs @@ -29,7 +29,20 @@ pub fn display_inner(ast: &DeriveInput) -> syn::Result { let params = match variant.fields { Fields::Unit => quote! {}, - Fields::Unnamed(..) => quote! { (..) }, + Fields::Unnamed(ref unnamed_fields) => { + // Transform unnamed params '(String, u8)' to '(ref field0, ref field1)' + let names: Punctuated<_, Token!(,)> = unnamed_fields + .unnamed + .iter() + .enumerate() + .map(|(index, field)| { + assert!(field.ident.is_none()); + let ident = syn::parse_str::(format!("field{}", index).as_str()).unwrap(); + quote! { ref #ident } + }) + .collect(); + quote! { (#names) } + } Fields::Named(ref field_names) => { // Transform named params '{ name: String, age: u8 }' to '{ ref name, ref age }' let names: Punctuated = field_names @@ -58,33 +71,70 @@ pub fn display_inner(ast: &DeriveInput) -> syn::Result { } } } else { - let arm = if let Fields::Named(ref field_names) = variant.fields { - let used_vars = capture_format_string_idents(&output)?; - if used_vars.is_empty() { - quote! { #name::#ident #params => ::core::fmt::Display::fmt(#output, f) } - } else { - // Create args like 'name = name, age = age' for format macro - let args: Punctuated<_, Token!(,)> = field_names - .named - .iter() - .filter_map(|field| { - let ident = field.ident.as_ref().unwrap(); - // Only contain variables that are used in format string - if !used_vars.contains(ident) { - None - } else { - Some(quote! { #ident = #ident }) - } - }) - .collect(); - - quote! { - #[allow(unused_variables)] - #name::#ident #params => ::core::fmt::Display::fmt(&format!(#output, #args), f) + let arm = match variant.fields { + Fields::Named(ref field_names) => { + let used_vars = capture_format_string_idents(&output)?; + if used_vars.is_empty() { + quote! { #name::#ident #params => ::core::fmt::Display::fmt(#output, f) } + } else { + // Create args like 'name = name, age = age' for format macro + let args: Punctuated<_, Token!(,)> = field_names + .named + .iter() + .filter_map(|field| { + let ident = field.ident.as_ref().unwrap(); + // Only contain variables that are used in format string + if !used_vars.contains(ident) { + None + } else { + Some(quote! { #ident = #ident }) + } + }) + .collect(); + + quote! { + #[allow(unused_variables)] + #name::#ident #params => ::core::fmt::Display::fmt(&format!(#output, #args), f) + } + } + }, + Fields::Unnamed(ref unnamed_fields) => { + let used_vars = capture_format_strings(&output)?; + if used_vars.iter().any(String::is_empty) { + return Err(syn::Error::new_spanned( + &output, + "Empty {} is not allowed; Use manual numbering ({0})", + )) + } + if used_vars.is_empty() { + quote! { #name::#ident #params => ::core::fmt::Display::fmt(#output, f) } + } else { + let args: Punctuated<_, Token!(,)> = unnamed_fields + .unnamed + .iter() + .enumerate() + .map(|(index, field)| { + assert!(field.ident.is_none()); + syn::parse_str::(format!("field{}", index).as_str()).unwrap() + }) + .collect(); + quote! { + #[allow(unused_variables)] + #name::#ident #params => ::core::fmt::Display::fmt(&format!(#output, #args), f) + } } } - } else { - quote! { #name::#ident #params => ::core::fmt::Display::fmt(#output, f) } + Fields::Unit => { + let used_vars = capture_format_strings(&output)?; + if !used_vars.is_empty() { + return Err(syn::Error::new_spanned( + &output, + "Unit variants do not support interpolation", + )); + } + + quote! { #name::#ident #params => ::core::fmt::Display::fmt(#output, f) } + } }; arms.push(arm); @@ -107,11 +157,22 @@ pub fn display_inner(ast: &DeriveInput) -> syn::Result { } fn capture_format_string_idents(string_literal: &LitStr) -> syn::Result> { + capture_format_strings(string_literal)?.into_iter().map(|ident| { + syn::parse_str::(ident.as_str()).map_err(|_| { + syn::Error::new_spanned( + string_literal, + "Invalid identifier inside format string bracket", + ) + }) + }).collect() +} + +fn capture_format_strings(string_literal: &LitStr) -> syn::Result> { // Remove escaped brackets let format_str = string_literal.value().replace("{{", "").replace("}}", ""); let mut new_var_start_index: Option = None; - let mut var_used: Vec = Vec::new(); + let mut var_used = Vec::new(); for (i, chr) in format_str.bytes().enumerate() { if chr == b'{' { @@ -132,14 +193,8 @@ fn capture_format_string_idents(string_literal: &LitStr) -> syn::Result(ident_str).map_err(|_| { - syn::Error::new_spanned( - string_literal, - "Invalid identifier inside format string bracket", - ) - })?; - var_used.push(ident); + let ident_str = inside_brackets.split(":").next().unwrap().trim_end(); + var_used.push(ident_str.to_owned()); } } diff --git a/strum_nostd_tests/src/lib.rs b/strum_nostd_tests/src/lib.rs index 816c0736..251fe038 100644 --- a/strum_nostd_tests/src/lib.rs +++ b/strum_nostd_tests/src/lib.rs @@ -5,7 +5,7 @@ mod tests { use core::str::FromStr; use strum::EnumString; - #[derive(Debug, Eq, PartialEq, EnumString)] + #[derive(Debug, Eq, PartialEq, EnumString, strum::Display)] enum Color { Red, Blue { diff --git a/strum_tests/tests/display.rs b/strum_tests/tests/display.rs index c3b5041d..4c38d784 100644 --- a/strum_tests/tests/display.rs +++ b/strum_tests/tests/display.rs @@ -14,6 +14,8 @@ enum Color { Purple { sat: usize }, #[strum(default)] Green(String), + #[strum(to_string = "Orange({0})")] + Orange(usize), } #[test] @@ -64,6 +66,14 @@ fn to_green_string() { ); } +#[test] +fn to_orange_string() { + assert_eq!( + String::from("Orange(10)"), + Color::Orange(10).to_string().as_ref() + ); +} + #[derive(Debug, Eq, PartialEq, EnumString, strum::Display)] enum ColorWithDefaultAndToString { #[strum(default, to_string = "GreenGreen")]