diff --git a/CHANGELOG.md b/CHANGELOG.md index 413d1968..73d7e854 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,10 @@ and this project adheres to [Semantic Versioning](http://semver.org/). its trait along is possible now via `use derive_more::with_trait::SomeTrait`. ([#406](https://github.com/JelteF/derive_more/pull/406)) +### Added + +- Derive for `AsVariant`. + ### Fixed - Associated types of type parameters not being treated as generics in `Debug` diff --git a/Cargo.toml b/Cargo.toml index 7eedea82..3191a594 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -53,6 +53,7 @@ default = ["std"] add = ["derive_more-impl/add"] add_assign = ["derive_more-impl/add_assign"] as_ref = ["derive_more-impl/as_ref"] +as_variant = ["derive_more-impl/as_variant"] constructor = ["derive_more-impl/constructor"] debug = ["derive_more-impl/debug"] deref = ["derive_more-impl/deref"] @@ -125,6 +126,11 @@ name = "as_ref" path = "tests/as_ref.rs" required-features = ["as_ref"] +[[test]] +name = "as_variant" +path = "tests/as_variant.rs" +required-features = ["as_variant"] + [[test]] name = "boats_display_derive" path = "tests/boats_display_derive.rs" diff --git a/impl/Cargo.toml b/impl/Cargo.toml index 3a398fb6..96d16450 100644 --- a/impl/Cargo.toml +++ b/impl/Cargo.toml @@ -52,6 +52,7 @@ default = [] add = [] add_assign = [] as_ref = ["syn/extra-traits", "syn/visit"] +as_variant = ["dep:convert_case"] constructor = [] debug = ["syn/extra-traits", "dep:unicode-xid"] deref = [] @@ -78,6 +79,7 @@ full = [ "add", "add_assign", "as_ref", + "as_variant", "constructor", "debug", "deref", diff --git a/impl/doc/as_variant.md b/impl/doc/as_variant.md new file mode 100644 index 00000000..1e7b2c70 --- /dev/null +++ b/impl/doc/as_variant.md @@ -0,0 +1,70 @@ +# What `#[derive(AsVariant)]` generates + +When an enum is decorated with `#[derive(AsVariant)]`, for each variant `foo` in +the enum, with fields `(a, b, c, ...)`, a public instance method `as_foo(self) -> Option<(a, b, c, ...)>` is generated. +If you don't want the `as_foo` method generated for a variant you can put the +`#[as_variant(ignore)]` attribute on that variant. +If you want to treat a reference, you can put the `#[as_variant(ref)]` attribute on the enum declaration or that variant, then `as_foo_ref(self) -> Option<(&a, &b, &c, ...)>` will be generated. You can also use mutable references by putting `#[as_variant(ref_mut)]`. + + + + +## Example usage + +```rust +# use derive_more::AsVariant; +# +#[derive(AsVariant)] +#[as_variant(ref)] +enum Maybe { + Just(T), + Nothing +} + +assert_eq!(Maybe::<()>::Nothing.as_nothing(), Some(())); +assert_eq!(Maybe::<()>::Nothing.as_just(), None); +assert_eq!(Maybe::Just(1).as_just(), Some(1)); +assert_eq!((&Maybe::Just(42)).as_just_ref(), Some(&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 as_just(self) -> Option<(T)> { + match self { + Maybe::Just(field_0) => Some((field_0)), + _ => None, + } + } + #[must_use] + pub fn as_just_ref(&self) -> Option<(&T)> { + match self { + Maybe::Just(field_0) => Some((field_0)), + _ => None, + } + } + + #[must_use] + pub fn as_nothing(self) -> Option<()> { + match self { + Maybe::Nothing => Some(()), + _ => None, + } + } + #[must_use] + pub fn as_nothing_ref(&self) -> Option<()> { + match self { + Maybe::Nothing => Some(()), + _ => None, + } + } +} +``` diff --git a/impl/src/as_variant.rs b/impl/src/as_variant.rs new file mode 100644 index 00000000..33443b4d --- /dev/null +++ b/impl/src/as_variant.rs @@ -0,0 +1,135 @@ +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, Type}; + +pub fn expand(input: &DeriveInput, trait_name: &'static str) -> Result { + let state = State::with_attr_params( + input, + trait_name, + "as_variant".into(), + AttrParams { + enum_: vec!["ignore", "owned", "ref", "ref_mut"], + variant: vec!["ignore", "owned", "ref", "ref_mut"], + struct_: vec!["ignore"], + field: vec!["ignore"], + }, + )?; + assert!( + state.derive_type == DeriveType::Enum, + "AsVariant can only be derived for enums", + ); + + let enum_name = &input.ident; + let (imp_generics, type_generics, where_clause) = input.generics.split_for_impl(); + + let variant_data = state.enabled_variant_data(); + + let mut funcs = vec![]; + for (variant_state, info) in + Iterator::zip(variant_data.variant_states.iter(), variant_data.infos) + { + let variant = variant_state.variant.unwrap(); + let fn_name = format_ident!( + "as_{}", + variant.ident.to_string().to_case(Case::Snake), + span = variant.ident.span(), + ); + let ref_fn_name = format_ident!( + "as_{}_ref", + variant.ident.to_string().to_case(Case::Snake), + span = variant.ident.span(), + ); + let mut_fn_name = format_ident!( + "as_{}_mut", + variant.ident.to_string().to_case(Case::Snake), + span = variant.ident.span(), + ); + let variant_ident = &variant.ident; + + let (data_pattern, ret_value, data_types) = get_field_info(&variant.fields); + let pattern = quote! { #enum_name :: #variant_ident #data_pattern }; + let doc_owned = format!( + "Attempts to convert this value to the `{enum_name}::{variant_ident}` variant.\n", + ); + let doc_ref = format!( + "Attempts to convert this reference to the `{enum_name}::{variant_ident}` variant.\n", + ); + let doc_mut = format!( + "Attempts to convert this mutable reference to the `{enum_name}::{variant_ident}` variant.\n", + ); + let doc_else = "Returns Some(..) if successful and None if this value is of any other type."; + let func = quote! { + #[doc = #doc_owned] + #[doc = #doc_else] + #[inline] + #[must_use] + pub fn #fn_name(self) -> Option<(#(#data_types),*)> { + match self { + #pattern => Some(#ret_value), + _ => None + } + } + }; + let ref_func = quote! { + #[doc = #doc_ref] + #[doc = #doc_else] + #[inline] + #[must_use] + pub fn #ref_fn_name(&self) -> Option<(#(&#data_types),*)> { + match self { + #pattern => Some(#ret_value), + _ => None + } + } + }; + let mut_func = quote! { + #[doc = #doc_mut] + #[doc = #doc_else] + #[inline] + #[must_use] + pub fn #mut_fn_name(&mut self) -> Option<(#(&mut #data_types),*)> { + match self { + #pattern => Some(#ret_value), + _ => None + } + } + }; + if info.owned && state.default_info.owned { + funcs.push(func); + } + if info.ref_ && state.default_info.ref_ { + funcs.push(ref_func); + } + if info.ref_mut && state.default_info.ref_mut { + funcs.push(mut_func); + } + } + + let imp = quote! { + #[allow(unreachable_code)] // omit warnings for `!` and other unreachable types + #[automatically_derived] + impl #imp_generics #enum_name #type_generics #where_clause { + #(#funcs)* + } + }; + + Ok(imp) +} + +fn get_field_info(fields: &Fields) -> (TokenStream, TokenStream, Vec<&Type>) { + match fields { + Fields::Named(_) => panic!("cannot extract anonymous records in as_variant"), + Fields::Unnamed(ref fields) => { + let (idents, types) = fields + .unnamed + .iter() + .enumerate() + .map(|(n, it)| (format_ident!("field_{n}"), &it.ty)) + .unzip::<_, _, Vec<_>, Vec<_>>(); + (quote! { (#(#idents),*) }, quote! { (#(#idents),*) }, types) + } + Fields::Unit => (quote! {}, quote! { () }, vec![]), + } +} diff --git a/impl/src/lib.rs b/impl/src/lib.rs index eeccdb07..455305a2 100644 --- a/impl/src/lib.rs +++ b/impl/src/lib.rs @@ -23,6 +23,8 @@ mod add_helpers; mod add_like; #[cfg(feature = "as_ref")] mod r#as; +#[cfg(feature = "as_variant")] +mod as_variant; #[cfg(feature = "constructor")] mod constructor; #[cfg(feature = "deref")] @@ -137,6 +139,14 @@ create_derive!( create_derive!("as_ref", r#as::r#mut, AsMut, as_mut_derive, as_mut); create_derive!("as_ref", r#as::r#ref, AsRef, as_ref_derive, as_ref); +create_derive!( + "as_variant", + as_variant, + AsVariant, + as_variant_derive, + as_variant, +); + create_derive!("constructor", constructor, Constructor, constructor_derive); create_derive!("debug", fmt::debug, Debug, debug_derive, debug); diff --git a/src/lib.rs b/src/lib.rs index e878f5df..b6a8adcf 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -436,6 +436,7 @@ pub mod with_trait { feature = "add", feature = "add_assign", feature = "as_ref", + feature = "as_variant", feature = "constructor", feature = "debug", feature = "deref", diff --git a/tests/as_variant.rs b/tests/as_variant.rs new file mode 100644 index 00000000..19642345 --- /dev/null +++ b/tests/as_variant.rs @@ -0,0 +1,106 @@ +#![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::AsVariant; + +#[derive(AsVariant)] +enum Either { + Left(TLeft), + Right(TRight), +} + +#[derive(AsVariant)] +#[derive(Debug, PartialEq)] +#[as_variant(ref, ref_mut)] +enum Maybe { + Nothing, + Just(T), +} + +#[derive(AsVariant)] +enum Color { + Rgb(u8, u8, u8), + Cmyk(u8, u8, u8, u8), +} + +/// With lifetime +#[derive(AsVariant)] +enum Nonsense<'a, T> { + Ref(&'a T), + NoRef, + #[as_variant(ignore)] + NoRefIgnored, +} + +#[derive(AsVariant)] +enum WithConstraints +where + T: Copy, +{ + One(T), + Two, +} + +#[derive(AsVariant)] +enum KitchenSink<'a, 'b, T1: Copy, T2: Clone> +where + T2: Into + 'b, +{ + Left(&'a T1), + Right(&'b T2), + OwnBoth(T1, T2), + Empty, + NeverMind(), + NothingToSeeHere(), +} + +/// Single variant enum +#[derive(AsVariant)] +enum Single { + Value(i32), +} + +#[derive(AsVariant)] +#[derive(Debug, PartialEq)] +#[as_variant(ref, ref_mut)] +enum Tuple { + None, + Single(T), + Double(T, T), + Triple(T, T, T), +} + +#[test] +pub fn test_as_variant() { + assert_eq!(Maybe::<()>::Nothing.as_nothing(), Some(())); + assert_eq!(Maybe::Just(1).as_just_ref(), Some(&1)); + assert_eq!(Maybe::Just(42).as_just_mut(), Some(&mut 42)); + + assert_eq!(Maybe::<()>::Nothing.as_just(), None); + assert_eq!(Maybe::Just(1).as_nothing_ref(), None); + assert_eq!(Maybe::Just(42).as_nothing_mut(), None); +} + +#[test] +pub fn test_as_variant_mut() { + let mut value = Tuple::Double(1, 12); + + if let Some((a, b)) = value.as_double_mut() { + *a = 9; + *b = 10; + } + + assert_eq!(value, Tuple::Double(9, 10)); +} + +#[cfg(nightly)] +mod never { + use super::*; + + #[derive(AsVariant)] + enum Enum { + Tuple(!), + TupleMulti(i32, !), + } +}