Skip to content

Commit

Permalink
Add new derive: AsVariant
Browse files Browse the repository at this point in the history
Similar to TryUnwrap but generates functions returning an Option instead
of a Result.
  • Loading branch information
crazymerlyn committed Sep 27, 2024
1 parent 3216eaf commit b94140d
Show file tree
Hide file tree
Showing 8 changed files with 334 additions and 0 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand Down
6 changes: 6 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
Expand Down Expand Up @@ -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"
Expand Down
2 changes: 2 additions & 0 deletions impl/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 = []
Expand All @@ -78,6 +79,7 @@ full = [
"add",
"add_assign",
"as_ref",
"as_variant",
"constructor",
"debug",
"deref",
Expand Down
70 changes: 70 additions & 0 deletions impl/doc/as_variant.md
Original file line number Diff line number Diff line change
@@ -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<T> {
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<T> {
# Just(T),
# Nothing
# }
impl<T> Maybe<T>{
#[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,
}
}
}
```
135 changes: 135 additions & 0 deletions impl/src/as_variant.rs
Original file line number Diff line number Diff line change
@@ -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<TokenStream> {
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![]),
}
}
10 changes: 10 additions & 0 deletions impl/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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")]
Expand Down Expand Up @@ -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);
Expand Down
1 change: 1 addition & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
106 changes: 106 additions & 0 deletions tests/as_variant.rs
Original file line number Diff line number Diff line change
@@ -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<TLeft, TRight> {
Left(TLeft),
Right(TRight),
}

#[derive(AsVariant)]
#[derive(Debug, PartialEq)]
#[as_variant(ref, ref_mut)]
enum Maybe<T> {
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<T>
where
T: Copy,
{
One(T),
Two,
}

#[derive(AsVariant)]
enum KitchenSink<'a, 'b, T1: Copy, T2: Clone>
where
T2: Into<T1> + '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<T> {
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, !),
}
}

0 comments on commit b94140d

Please sign in to comment.