diff --git a/CHANGELOG.md b/CHANGELOG.md index 833502c2..4738f5ae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,11 @@ and this project adheres to [Semantic Versioning](http://semver.org/). - Add `Hash` derive similar to `std`'s one, but considering generics correctly, and supporting custom hash functions per field or skipping fields. ([#532](https://github.com/JelteF/derive_more/pull/532)) +- Add `IsVariantAnd` derive, generating an `is_foo_and()` method for each enum + variant `foo` that also applies a closure to the variant's fields, similar to + `Option::is_some_and`. + ([#561](https://github.com/JelteF/derive_more/pull/561), + [#365](https://github.com/JelteF/derive_more/issues/365)) ### Fixed diff --git a/Cargo.toml b/Cargo.toml index 5e9c4cc1..ea7de8d0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -71,6 +71,7 @@ index_mut = ["derive_more-impl/index_mut"] into = ["derive_more-impl/into"] into_iterator = ["derive_more-impl/into_iterator"] is_variant = ["derive_more-impl/is_variant"] +is_variant_and = ["derive_more-impl/is_variant_and"] mul = ["derive_more-impl/mul"] mul_assign = ["derive_more-impl/mul_assign"] not = ["derive_more-impl/not"] @@ -100,6 +101,7 @@ full = [ "into", "into_iterator", "is_variant", + "is_variant_and", "mul", "mul_assign", "not", @@ -212,6 +214,11 @@ name = "is_variant" path = "tests/is_variant.rs" required-features = ["is_variant"] +[[test]] +name = "is_variant_and" +path = "tests/is_variant_and.rs" +required-features = ["is_variant_and"] + [[test]] name = "mul" path = "tests/mul.rs" diff --git a/README.md b/README.md index 2d2a2601..71557ba3 100644 --- a/README.md +++ b/README.md @@ -139,8 +139,9 @@ These don't derive traits, but derive static methods instead. This is very basic if you need more customization for your constructor, check out the [`derive-new`] crate. 2. [`IsVariant`], for each variant `foo` of an enum type, derives a `is_foo` method. -3. [`Unwrap`], for each variant `foo` of an enum type, derives an `unwrap_foo` method. -4. [`TryUnwrap`], for each variant `foo` of an enum type, derives an `try_unwrap_foo` method. +3. [`IsVariantAnd`], for each variant `foo` of an enum type, derives a `is_foo_and` method. +4. [`Unwrap`], for each variant `foo` of an enum type, derives an `unwrap_foo` method. +5. [`TryUnwrap`], for each variant `foo` of an enum type, derives an `try_unwrap_foo` method. ### Re-exports @@ -273,6 +274,7 @@ Changing [MSRV] (minimum supported Rust version) of this crate is treated as a * [`Constructor`]: https://docs.rs/derive_more/latest/derive_more/derive.Constructor.html [`IsVariant`]: https://docs.rs/derive_more/latest/derive_more/derive.IsVariant.html +[`IsVariantAnd`]: https://docs.rs/derive_more/latest/derive_more/derive.IsVariantAnd.html [`Unwrap`]: https://docs.rs/derive_more/latest/derive_more/derive.Unwrap.html [`TryUnwrap`]: https://docs.rs/derive_more/latest/derive_more/derive.TryUnwrap.html diff --git a/impl/Cargo.toml b/impl/Cargo.toml index cfd82220..7f8bbab3 100644 --- a/impl/Cargo.toml +++ b/impl/Cargo.toml @@ -67,6 +67,7 @@ index_mut = [] into = ["syn/extra-traits", "syn/visit-mut"] into_iterator = [] is_variant = ["dep:convert_case"] +is_variant_and = ["dep:convert_case"] mul = ["syn/extra-traits", "syn/visit"] mul_assign = ["syn/extra-traits", "syn/visit"] not = ["syn/extra-traits"] @@ -95,6 +96,7 @@ full = [ "into", "into_iterator", "is_variant", + "is_variant_and", "mul", "mul_assign", "not", diff --git a/impl/doc/is_variant_and.md b/impl/doc/is_variant_and.md new file mode 100644 index 00000000..7e51627b --- /dev/null +++ b/impl/doc/is_variant_and.md @@ -0,0 +1,69 @@ +# What `#[derive(IsVariantAnd)]` generates + +When an enum is decorated with `#[derive(IsVariantAnd)]`, for each variant `foo` +in the enum a public instance method `is_foo_and(&self, f) -> bool` is generated. +It returns `true` if the value is the `foo` variant *and* the closure `f`, +applied to the variant's fields (by reference), returns `true`. This mirrors +[`Option::is_some_and`], but for arbitrary enums. + +The closure receives the variant's fields as a tuple of references, in +declaration order. As with a regular tuple, a variant with a single field +collapses to a plain reference (so `Just(T)` yields `is_just_and(|x: &T| ...)`, +exactly like [`Option::is_some_and`]), while a unit variant's method takes a +`FnOnce() -> bool` closure instead. + +If you don't want the `is_foo_and` method generated for a variant you can put +the `#[is_variant_and(ignore)]` attribute on that variant. + +[`Option::is_some_and`]: https://doc.rust-lang.org/core/option/enum.Option.html#method.is_some_and + + + + +## Example usage + +```rust +# use derive_more::IsVariantAnd; +# +#[derive(IsVariantAnd)] +enum Maybe { + Just(T), + Nothing, +} + +let maybe = Maybe::Just(42); +assert!(maybe.is_just_and(|x| *x == 42)); +assert!(!maybe.is_just_and(|x| *x == 0)); +assert!(!maybe.is_nothing_and(|| true)); + +let nothing = Maybe::::Nothing; +assert!(nothing.is_nothing_and(|| true)); +assert!(!nothing.is_just_and(|x| *x == 42)); +``` + + +### What is generated? + +The derive in the above example generates code like this: +```rust +# enum Maybe { +# Just(T), +# Nothing, +# } +impl Maybe { + #[must_use] + pub fn is_just_and(&self, f: impl FnOnce(&T) -> bool) -> bool { + match self { + Self::Just(field_0) => f(field_0), + _ => false, + } + } + #[must_use] + pub fn is_nothing_and(&self, f: impl FnOnce() -> bool) -> bool { + match self { + Self::Nothing => f(), + _ => false, + } + } +} +``` diff --git a/impl/src/is_variant_and.rs b/impl/src/is_variant_and.rs new file mode 100644 index 00000000..6c18e656 --- /dev/null +++ b/impl/src/is_variant_and.rs @@ -0,0 +1,138 @@ +use crate::utils::{AttrParams, DeriveType, State}; +use convert_case::{Case, Casing}; +use proc_macro2::TokenStream; +use quote::{format_ident, quote}; +use syn::{DeriveInput, Fields, Result}; + +pub fn expand(input: &DeriveInput, trait_name: &'static str) -> Result { + let state = State::with_attr_params( + input, + trait_name, + "is_variant_and".into(), + AttrParams { + enum_: vec!["ignore"], + variant: vec!["ignore"], + struct_: vec!["ignore"], + field: vec!["ignore"], + }, + )?; + assert!( + state.derive_type == DeriveType::Enum, + "IsVariantAnd can only be derived for enums", + ); + + let enum_name = &input.ident; + let (imp_generics, type_generics, where_clause) = input.generics.split_for_impl(); + + let mut funcs = vec![]; + for variant_state in state.enabled_variant_data().variant_states { + let variant = variant_state.variant.unwrap(); + let variant_ident = &variant.ident; + let fn_name = format_ident!( + "is_{}_and", + variant_ident.to_string().to_case(Case::Snake), + span = variant_ident.span(), + ); + + let (pattern, bindings, arg_type) = field_info(&variant.fields); + let pattern = quote! { #enum_name ::#variant_ident #pattern }; + + let doc = format!( + "Returns `true` if this value is of type `{variant_ident}` and the \ + given closure returns `true` for the contained value(s).\n\nReturns \ + `false` otherwise.", + ); + + let func = if let Some(arg_type) = arg_type { + quote! { + #[doc = #doc] + #[inline] + #[must_use] + pub fn #fn_name( + &self, + f: impl derive_more::core::ops::FnOnce(#arg_type) -> bool, + ) -> bool { + match self { + #pattern => f(#bindings), + _ => false, + } + } + } + } else { + quote! { + #[doc = #doc] + #[inline] + #[must_use] + pub fn #fn_name( + &self, + f: impl derive_more::core::ops::FnOnce() -> bool, + ) -> bool { + match self { + #pattern => f(), + _ => false, + } + } + } + }; + funcs.push(func); + } + + let imp = quote! { + #[allow(deprecated)] // omit warnings on deprecated fields/variants + #[allow(unreachable_code)] // omit warnings for `!` and other unreachable types + #[allow(unreachable_patterns)] // omit warnings for single-variant enums + #[automatically_derived] + impl #imp_generics #enum_name #type_generics #where_clause { + #(#funcs)* + } + }; + + Ok(imp) +} + +/// Returns the pattern binding the variant's fields, the tuple of bound field +/// references passed to the closure, and the closure's argument type. +/// +/// The argument type is `None` for unit variants, whose generated method takes a +/// `FnOnce() -> bool` closure instead. For a variant with a single field the +/// resulting type collapses to a plain reference (e.g. `&T`), mirroring +/// [`Option::is_some_and`]. +fn field_info(fields: &Fields) -> (TokenStream, TokenStream, Option) { + match fields { + Fields::Named(fields) if !fields.named.is_empty() => { + let (patterns, (bindings, types)): (Vec<_>, (Vec<_>, Vec<_>)) = fields + .named + .iter() + .enumerate() + .map(|(n, field)| { + let name = field.ident.as_ref().unwrap(); + let binding = format_ident!("__field_{n}"); + (quote! { #name: #binding }, (binding, &field.ty)) + }) + .unzip(); + ( + quote! { { #(#patterns),* } }, + quote! { (#(#bindings),*) }, + Some(quote! { (#(&#types),*) }), + ) + } + Fields::Unnamed(fields) if !fields.unnamed.is_empty() => { + let (bindings, types): (Vec<_>, Vec<_>) = fields + .unnamed + .iter() + .enumerate() + .map(|(n, field)| (format_ident!("__field_{n}"), &field.ty)) + .unzip(); + ( + quote! { (#(#bindings),*) }, + quote! { (#(#bindings),*) }, + Some(quote! { (#(&#types),*) }), + ) + } + // Variants with no fields (unit, empty tuple `()` or empty record `{}`) + // all take a `FnOnce() -> bool` closure. + Fields::Named(_) => (quote! { {} }, quote! {}, None), + Fields::Unnamed(_) => (quote! { () }, quote! {}, None), + Fields::Unit => (quote! {}, quote! {}, None), + } +} diff --git a/impl/src/lib.rs b/impl/src/lib.rs index 0133ed05..4ad01486 100644 --- a/impl/src/lib.rs +++ b/impl/src/lib.rs @@ -40,6 +40,8 @@ mod into; mod into_iterator; #[cfg(feature = "is_variant")] mod is_variant; +#[cfg(feature = "is_variant_and")] +mod is_variant_and; #[cfg(feature = "not")] mod not_like; #[cfg(any( @@ -238,6 +240,14 @@ create_derive!( is_variant, ); +create_derive!( + "is_variant_and", + is_variant_and, + IsVariantAnd, + is_variant_and_derive, + is_variant_and, +); + create_derive!("mul", ops::mul, Mul, mul_derive, mul); create_derive!("mul", ops::mul, Div, div_derive, div); create_derive!("mul", ops::mul, Rem, rem_derive, rem); diff --git a/src/lib.rs b/src/lib.rs index cd763273..4bfc0972 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -29,6 +29,7 @@ //! //! [`Constructor`]: macro@crate::Constructor //! [`IsVariant`]: macro@crate::IsVariant +//! [`IsVariantAnd`]: macro@crate::IsVariantAnd //! [`Unwrap`]: macro@crate::Unwrap //! [`TryUnwrap`]: macro@crate::TryUnwrap @@ -306,6 +307,9 @@ pub mod with_trait { #[cfg(feature = "is_variant")] pub use derive_more_impl::IsVariant; + #[cfg(feature = "is_variant_and")] + pub use derive_more_impl::IsVariantAnd; + #[cfg(feature = "mul")] pub use derive_more_impl::{Div, Mul, Rem, Shl, Shr}; @@ -414,6 +418,10 @@ pub mod with_trait { #[doc(hidden)] pub use all_traits_and_derives::IsVariant; + #[cfg(feature = "is_variant_and")] + #[doc(hidden)] + pub use all_traits_and_derives::IsVariantAnd; + #[cfg(feature = "mul")] #[doc(hidden)] pub use all_traits_and_derives::{Div, Mul, Rem, Shl, Shr}; @@ -475,6 +483,7 @@ pub mod with_trait { feature = "into", feature = "into_iterator", feature = "is_variant", + feature = "is_variant_and", feature = "mul", feature = "mul_assign", feature = "not", diff --git a/tests/compile_fail/eq/non_eq_field.stderr b/tests/compile_fail/eq/non_eq_field.stderr index ae02e14a..e93c539b 100644 --- a/tests/compile_fail/eq/non_eq_field.stderr +++ b/tests/compile_fail/eq/non_eq_field.stderr @@ -14,5 +14,4 @@ error[E0277]: the trait bound `f32: Eq` is not satisfied u128 u16 and $N others - = help: see issue #48214 = note: this error originates in the derive macro `derive_more::Eq` (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/tests/is_variant_and.rs b/tests/is_variant_and.rs new file mode 100644 index 00000000..7e9ea526 --- /dev/null +++ b/tests/is_variant_and.rs @@ -0,0 +1,181 @@ +#![cfg_attr(not(feature = "std"), no_std)] +#![cfg_attr(nightly, feature(never_type))] +#![allow(dead_code)] // some code is tested for type checking only + +use derive_more::IsVariantAnd; + +#[derive(IsVariantAnd)] +enum Either { + Left(TLeft), + Right(TRight), +} + +#[test] +fn test_single_field() { + let either: Either = Either::Right(7); + assert!(either.is_right_and(|x| *x == 7)); + assert!(!either.is_right_and(|x| *x == 0)); + assert!(!either.is_left_and(|_| true)); + + let either: Either = Either::Left(7); + assert!(either.is_left_and(|x| *x == 7)); + assert!(!either.is_left_and(|x| *x == 0)); + assert!(!either.is_right_and(|_| true)); +} + +#[derive(IsVariantAnd)] +enum Maybe { + Nothing, + Just(T), +} + +#[test] +fn test_unit_and_single() { + let maybe: Maybe = Maybe::Just(7); + assert!(maybe.is_just_and(|x| *x == 7)); + assert!(!maybe.is_just_and(|x| *x == 0)); + assert!(!maybe.is_nothing_and(|| true)); + + let maybe: Maybe = Maybe::Nothing; + assert!(maybe.is_nothing_and(|| true)); + assert!(!maybe.is_nothing_and(|| false)); + assert!(!maybe.is_just_and(|_| true)); +} + +#[derive(IsVariantAnd)] +enum Color { + Rgb(u8, u8, u8), + Cmyk { c: u8, m: u8, y: u8, k: u8 }, +} + +#[test] +fn test_multi_field_tuple_and_struct() { + let color = Color::Rgb(0, 40, 80); + assert!(color.is_rgb_and(|(r, g, b)| *r == 0 && *g == 40 && *b == 80)); + assert!(!color.is_rgb_and(|(r, _, _)| *r == 255)); + assert!(!color.is_cmyk_and(|_| true)); + + let color = Color::Cmyk { + c: 1, + m: 2, + y: 3, + k: 4, + }; + assert!(color.is_cmyk_and(|(c, m, y, k)| *c + *m + *y + *k == 10)); + assert!(!color.is_cmyk_and(|(c, _, _, _)| *c == 0)); + assert!(!color.is_rgb_and(|_| true)); +} + +#[derive(IsVariantAnd)] +enum Nonsense<'a, T> { + Ref(&'a T), + NoRef, + #[is_variant_and(ignore)] + NoRefIgnored, +} + +#[test] +fn test_ignore_and_references() { + let nonsense: Nonsense = Nonsense::Ref(&7); + assert!(nonsense.is_ref_and(|x| **x == 7)); + assert!(!nonsense.is_no_ref_and(|| true)); + + let nonsense: Nonsense = Nonsense::NoRef; + assert!(nonsense.is_no_ref_and(|| true)); + assert!(!nonsense.is_ref_and(|_| true)); +} + +#[derive(IsVariantAnd)] +enum WithConstraints +where + T: Copy, +{ + One(T), + Two, +} + +#[test] +fn test_generic_constraints() { + let wc: WithConstraints = WithConstraints::One(1); + assert!(wc.is_one_and(|x| *x == 1)); + assert!(!wc.is_two_and(|| true)); + + let wc: WithConstraints = WithConstraints::Two; + assert!(wc.is_two_and(|| true)); + assert!(!wc.is_one_and(|_| true)); +} + +#[derive(IsVariantAnd)] +enum KitchenSink<'a, 'b, T1: Copy, T2: Clone> +where + T2: Into + 'b, +{ + Left(&'a T1), + Right(&'b T2), + OwnBoth { left: T1, right: T2 }, + Empty, + NeverMind(), + NothingToSeeHere {}, +} + +#[test] +fn test_kitchen_sink() { + let ks: KitchenSink = KitchenSink::OwnBoth { left: 1, right: 2 }; + assert!(ks.is_own_both_and(|(left, right)| *left == 1 && *right == 2)); + assert!(!ks.is_own_both_and(|(left, _)| *left == 0)); + assert!(!ks.is_left_and(|_| true)); + + let ks: KitchenSink = KitchenSink::Empty; + assert!(ks.is_empty_and(|| true)); + assert!(!ks.is_never_mind_and(|| true)); + + let ks: KitchenSink = KitchenSink::NeverMind(); + assert!(ks.is_never_mind_and(|| true)); + assert!(!ks.is_nothing_to_see_here_and(|| true)); + + let ks: KitchenSink = KitchenSink::NothingToSeeHere {}; + assert!(ks.is_nothing_to_see_here_and(|| true)); + assert!(!ks.is_empty_and(|| true)); +} + +// A single-variant enum exercises the `unreachable_patterns` path. +#[derive(IsVariantAnd)] +enum Single { + Only(u8), +} + +#[test] +fn test_single_variant() { + let single = Single::Only(5); + assert!(single.is_only_and(|x| *x == 5)); + assert!(!single.is_only_and(|x| *x == 0)); +} + +#[cfg(nightly)] +mod never { + use super::*; + + #[derive(IsVariantAnd)] + enum Enum { + Tuple(!), + Struct { field: ! }, + TupleMulti(i32, !), + StructMulti { field: !, other: i32 }, + } +} + +mod deprecated { + use super::*; + + #[derive(IsVariantAnd)] + #[deprecated(note = "enum")] + enum Enum { + #[deprecated(note = "variant")] + Tuple(#[deprecated(note = "field")] i32), + #[deprecated(note = "variant")] + Struct { + #[deprecated(note = "field")] + field: i32, + }, + } +}