Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add TryFrom to convert repr to enum #300

Merged
merged 10 commits into from Sep 18, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Expand Up @@ -59,6 +59,8 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
([#279](https://github.com/JelteF/derive_more/pull/279))
- `derive_more::derive` module exporting only macros, without traits.
([#290](https://github.com/JelteF/derive_more/pull/290))
- Add `TryFrom` derive for enums to convert from their discriminant.
([#300](https://github.com/JelteF/derive_more/pull/300))

### Changed

Expand Down
7 changes: 7 additions & 0 deletions Cargo.toml
Expand Up @@ -65,6 +65,7 @@ mul_assign = ["derive_more-impl/mul_assign"]
mul = ["derive_more-impl/mul"]
not = ["derive_more-impl/not"]
sum = ["derive_more-impl/sum"]
try_from = ["derive_more-impl/try_from"]
try_into = ["derive_more-impl/try_into"]
is_variant = ["derive_more-impl/is_variant"]
unwrap = ["derive_more-impl/unwrap"]
Expand Down Expand Up @@ -92,6 +93,7 @@ full = [
"mul_assign",
"not",
"sum",
"try_from",
"try_into",
"try_unwrap",
"unwrap",
Expand Down Expand Up @@ -204,6 +206,11 @@ name = "sum"
path = "tests/sum.rs"
required-features = ["sum"]

[[test]]
name = "try_from"
path = "tests/try_from.rs"
required-features = ["try_from"]

[[test]]
name = "try_into"
path = "tests/try_into.rs"
Expand Down
7 changes: 4 additions & 3 deletions README.md
Expand Up @@ -85,9 +85,10 @@ These are traits that are used to convert automatically between types.
1. [`From`]
2. [`Into`]
3. [`FromStr`]
4. [`TryInto`]
5. [`IntoIterator`]
6. [`AsRef`], [`AsMut`]
4. [`TryFrom`]
5. [`TryInto`]
6. [`IntoIterator`]
7. [`AsRef`], [`AsMut`]


### Formatting traits
Expand Down
4 changes: 3 additions & 1 deletion impl/Cargo.toml
Expand Up @@ -10,7 +10,6 @@ repository = "https://github.com/JelteF/derive_more"
documentation = "https://docs.rs/derive_more"

# explicitly no keywords or categories so it cannot be found easily

include = [
"src/**/*.rs",
"doc/**/*.md",
Expand All @@ -35,6 +34,7 @@ rustc_version = { version = "0.4", optional = true }
[dev-dependencies]
derive_more = { path = "..", features = ["full"] }
itertools = "0.11.0"
rustversion = "1.0"

[badges]
github = { repository = "JelteF/derive_more", workflow = "CI" }
Expand Down Expand Up @@ -66,6 +66,7 @@ mul = ["syn/extra-traits"]
mul_assign = ["syn/extra-traits"]
not = ["syn/extra-traits"]
sum = []
try_from = []
try_into = ["syn/extra-traits"]
try_unwrap = ["dep:convert_case"]
unwrap = ["dep:convert_case"]
Expand All @@ -91,6 +92,7 @@ full = [
"mul_assign",
"not",
"sum",
"try_from",
"try_into",
"try_unwrap",
"unwrap",
Expand Down
37 changes: 37 additions & 0 deletions impl/doc/try_from.md
@@ -0,0 +1,37 @@
# What `#[derive(TryFrom)]` generates

This derive allows you to convert enum discriminants into their corresponding variants.
By default a `TryFrom<isize>` is generated, matching the [type of the discriminant](https://doc.rust-lang.org/reference/items/enumerations.html#discriminants).
The type can be changed with a `#[repr(u/i*)]` attribute, e.g., `#[repr(u8)]` or `#[repr(i32)]`.
Only field-less variants can be constructed from their variant, therefor the `TryFrom` implementation will return an error for a discriminant representing a variant with fields.

## Example usage

```rust
# #[rustversion::since(1.66)]
# mod discriminant_on_non_unit_enum {
# use derive_more::TryFrom;
#[derive(TryFrom, Debug, PartialEq)]
#[repr(u32)]
enum Enum {
Implicit,
Explicit = 5,
Field(usize),
Empty{},
ModProg marked this conversation as resolved.
Show resolved Hide resolved
}

# #[rustversion::since(1.66)]
# pub fn test(){
assert_eq!(Enum::Implicit, Enum::try_from(0).unwrap());
assert_eq!(Enum::Explicit, Enum::try_from(5).unwrap());
assert_eq!(Enum::Empty{}, Enum::try_from(7).unwrap());
ModProg marked this conversation as resolved.
Show resolved Hide resolved

// variants with fields are not supported
assert!(Enum::try_from(6).is_err());
ModProg marked this conversation as resolved.
Show resolved Hide resolved
# }
# }
# // We need to use a `function` declaration, because we cannot put `rustversion` on a statement.
# #[rustversion::since(1.66)] use discriminant_on_non_unit_enum::test;
# #[rustversion::before(1.66)] fn test() {}
# test();
ModProg marked this conversation as resolved.
Show resolved Hide resolved
```
4 changes: 4 additions & 0 deletions impl/src/lib.rs
Expand Up @@ -64,6 +64,8 @@ mod not_like;
pub(crate) mod parsing;
#[cfg(feature = "sum")]
mod sum_like;
#[cfg(feature = "try_from")]
mod try_from;
#[cfg(feature = "try_into")]
mod try_into;
#[cfg(feature = "try_unwrap")]
Expand Down Expand Up @@ -265,6 +267,8 @@ create_derive!("not", not_like, Neg, neg_derive);
create_derive!("sum", sum_like, Sum, sum_derive);
create_derive!("sum", sum_like, Product, product_derive);

create_derive!("try_from", try_from, TryFrom, try_from_derive, try_from);

create_derive!("try_into", try_into, TryInto, try_into_derive, try_into);

create_derive!(
Expand Down
4 changes: 2 additions & 2 deletions impl/src/parsing.rs
Expand Up @@ -245,8 +245,8 @@ pub fn seq<const N: usize>(
move |c| {
parsers
.iter_mut()
.fold(Some((TokenStream::new(), c)), |out, parser| {
let (mut out, mut c) = out?;
.try_fold((TokenStream::new(), c), |out, parser| {
let (mut out, mut c) = out;
let (stream, cursor) = parser(c)?;
out.extend(stream);
c = cursor;
Expand Down
141 changes: 141 additions & 0 deletions impl/src/try_from.rs
@@ -0,0 +1,141 @@
//! Implementation of a [`TryFrom`] derive macro.

use proc_macro2::{Literal, Span, TokenStream};
use quote::{format_ident, quote, ToTokens as _};
use syn::{spanned::Spanned as _, Ident, Variant};

/// Expands a [`TryFrom`] derive macro.
pub fn expand(input: &syn::DeriveInput, _: &'static str) -> syn::Result<TokenStream> {
match &input.data {
syn::Data::Struct(data) => Err(syn::Error::new(
data.struct_token.span(),
"`TryFrom` cannot be derived for structs",
)),
syn::Data::Enum(data) => Expansion {
repr: ReprAttribute::parse_attrs(&input.attrs)?,
ident: &input.ident,
variants: data.variants.iter().collect(),
generics: &input.generics,
}
.expand(),
syn::Data::Union(data) => Err(syn::Error::new(
data.union_token.span(),
"`TryFrom` cannot be derived for unions",
)),
}
}

/// Representation of a [`Repr`] derive macro struct container attribute.
///
/// ```rust,ignore
/// #[repr(<type>)]
/// ```
struct ReprAttribute(Ident);

impl ReprAttribute {
/// Parses a [`StructAttribute`] from the provided [`syn::Attribute`]s.
fn parse_attrs(attrs: impl AsRef<[syn::Attribute]>) -> syn::Result<Self> {
attrs
.as_ref()
.iter()
.filter(|attr| attr.path().is_ident("repr"))
.try_fold(None, |mut repr, attr| {
attr.parse_nested_meta(|meta| {
if let Some(ident) = meta.path.get_ident() {
if let "u8" | "u16" | "u32" | "u64" | "u128" | "usize" | "i8"
| "i16" | "i32" | "i64" | "i128" | "isize" =
ModProg marked this conversation as resolved.
Show resolved Hide resolved
ident.to_string().as_str()
{
repr = Some(ident.clone());
return Ok(());
}
}
// ignore all other attributes that could have a body e.g. `align`
_ = meta.input.parse::<proc_macro2::Group>();
Ok(())
})
.map(|_| repr)
})
// Default discriminant is interpreted as `isize` (https://doc.rust-lang.org/reference/items/enumerations.html#discriminants)
.map(|repr| repr.unwrap_or_else(|| Ident::new("isize", Span::call_site())))
ModProg marked this conversation as resolved.
Show resolved Hide resolved
.map(Self)
}
}

/// Expansion of a macro for generating [`TryFrom`] implementation of an enum
struct Expansion<'a> {
/// Enum `#[repr(u/i*)]`
repr: ReprAttribute,
/// Enum [`Ident`].
ident: &'a Ident,

/// Variant [`Ident`] in case of enum expansion.
variants: Vec<&'a syn::Variant>,
ModProg marked this conversation as resolved.
Show resolved Hide resolved

/// Struct or enum [`syn::Generics`].
generics: &'a syn::Generics,
}

impl<'a> Expansion<'a> {
/// Expands [`TryFrom`] implementations for a struct.
fn expand(&self) -> syn::Result<TokenStream> {
ModProg marked this conversation as resolved.
Show resolved Hide resolved
let ident = self.ident;
let (impl_generics, ty_generics, where_clause) = self.generics.split_for_impl();

let repr = &self.repr.0;

let mut last_discriminant = quote! {0};
let mut inc = 0usize;
let (consts, (discriminants, variants)): (
Vec<Ident>,
(Vec<TokenStream>, Vec<TokenStream>),
) = self
.variants
.iter()
.filter_map(
|Variant {
ident,
fields,
discriminant,
..
}| {
if let Some(discriminant) = discriminant {
last_discriminant = discriminant.1.to_token_stream();
inc = 0;
}
let ret = {
let inc = Literal::usize_unsuffixed(inc);
fields.is_empty().then_some((
format_ident!("__DISCRIMINANT_{ident}"),
(
quote! {#last_discriminant + #inc},
quote! {#ident #fields},
),
))
};
inc += 1;
ret
},
)
.unzip();

Ok(quote! {
#[automatically_derived]
impl #impl_generics
::core::convert::TryFrom<#repr #ty_generics> for #ident
#where_clause
{
type Error = ::derive_more::TryFromError<#repr>;

#[inline]
fn try_from(value: #repr) -> ::core::result::Result<Self, Self::Error> {
#(#[allow(non_upper_case_globals)] const #consts: #repr = #discriminants;)*
match value {
#(#consts => ::core::result::Result::Ok(#ident::#variants),)*
_ => ::core::result::Result::Err(::derive_more::TryFromError::new(value)),
}
}
}
})
}
}
32 changes: 32 additions & 0 deletions src/convert.rs
Expand Up @@ -45,3 +45,35 @@ impl<T> fmt::Display for TryIntoError<T> {

#[cfg(feature = "std")]
impl<T: fmt::Debug> std::error::Error for TryIntoError<T> {}

/// Error returned by the derived [`TryFrom`] implementation.
///
/// [`TryFrom`]: macro@crate::TryFrom
#[derive(Clone, Copy, Debug)]
pub struct TryFromError<T> {
/// Original input value which failed to convert via the derived
/// [`TryFrom`] implementation.
///
/// [`TryFrom`]: macro@crate::TryFrom
pub input: T,
}

impl<T> TryFromError<T> {
#[doc(hidden)]
#[must_use]
#[inline]
pub const fn new(input: T) -> Self {
Self { input }
}
}

// `T` should only be an integer type and therefor display
impl<T: fmt::Display> fmt::Display for TryFromError<T> {
ModProg marked this conversation as resolved.
Show resolved Hide resolved
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "`{}` does not respond to a unit variant", self.input)
}
}

#[cfg(feature = "std")]
// `T` should only be an integer type and therefor display and debug
impl<T: fmt::Debug + fmt::Display> std::error::Error for TryFromError<T> {}
ModProg marked this conversation as resolved.
Show resolved Hide resolved
12 changes: 11 additions & 1 deletion src/lib.rs
Expand Up @@ -3,6 +3,7 @@
//! [`From`]: macro@crate::From
//! [`Into`]: macro@crate::Into
//! [`FromStr`]: macro@crate::FromStr
//! [`TryFrom`]: macro@crate::TryInto
//! [`TryInto`]: macro@crate::TryInto
//! [`IntoIterator`]: macro@crate::IntoIterator
//! [`AsRef`]: macro@crate::AsRef
Expand Down Expand Up @@ -89,8 +90,11 @@ mod r#str;
#[doc(inline)]
pub use crate::r#str::FromStrError;

#[cfg(feature = "try_into")]
#[cfg(any(feature = "try_into", feature = "try_from"))]
mod convert;
#[cfg(feature = "try_from")]
#[doc(inline)]
pub use crate::convert::TryFromError;
#[cfg(feature = "try_into")]
#[doc(inline)]
pub use crate::convert::TryIntoError;
Expand Down Expand Up @@ -203,6 +207,8 @@ re_export_traits!("not", not_traits, core::ops, Neg, Not);

re_export_traits!("sum", sum_traits, core::iter, Product, Sum);

re_export_traits!("try_from", try_from_traits, core::convert, TryFrom);

re_export_traits!("try_into", try_into_traits, core::convert, TryInto);

// Now re-export our own derives by their exact name to overwrite any derives that the trait
Expand Down Expand Up @@ -271,6 +277,9 @@ pub use derive_more_impl::{Neg, Not};
#[cfg(feature = "sum")]
pub use derive_more_impl::{Product, Sum};

#[cfg(feature = "try_from")]
pub use derive_more_impl::TryFrom;

#[cfg(feature = "try_into")]
pub use derive_more_impl::TryInto;

Expand Down Expand Up @@ -303,6 +312,7 @@ pub use derive_more_impl::Unwrap;
feature = "mul_assign",
feature = "not",
feature = "sum",
feature = "try_from",
feature = "try_into",
feature = "try_unwrap",
feature = "unwrap",
Expand Down
7 changes: 7 additions & 0 deletions tests/compile_fail/try_from/invalid_repr.rs
@@ -0,0 +1,7 @@
#[derive(derive_more::TryFrom)]
#[repr(a + b)]
enum Enum {
ModProg marked this conversation as resolved.
Show resolved Hide resolved
Variant
}

fn main() {}
11 changes: 11 additions & 0 deletions tests/compile_fail/try_from/invalid_repr.stderr
@@ -0,0 +1,11 @@
error: expected `,`
--> tests/compile_fail/try_from/invalid_repr.rs:2:10
|
2 | #[repr(a + b)]
| ^

error: expected one of `(`, `,`, `::`, or `=`, found `+`
--> tests/compile_fail/try_from/invalid_repr.rs:2:10
|
2 | #[repr(a + b)]
| ^ expected one of `(`, `,`, `::`, or `=`