Skip to content

Commit

Permalink
Add TryFrom to convert repr to enum (#300, #146)
Browse files Browse the repository at this point in the history
## Synopsis

Conversion from the repr integer to an enum.


## Solution

A `TryFrom` derive macro implementing `TryFrom<integertype> for Enum`.


## Additionally

Bump up MSRV to 1.72.0.
  • Loading branch information
ModProg authored Sep 18, 2023
1 parent 7f50fed commit 557e801
Show file tree
Hide file tree
Showing 18 changed files with 479 additions and 46 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ jobs:
strategy:
fail-fast: false
matrix:
msrv: ["1.65.0"]
msrv: ["1.72.0"]
os:
- ubuntu
- macOS
Expand Down
4 changes: 3 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/).

### Breaking changes

- The minimum supported Rust version (MSRV) is now Rust 1.65.
- The minimum supported Rust version (MSRV) is now Rust 1.72.
- Add the `std` feature which should be disabled in `no_std` environments.
- All Cargo features, except `std`, are now disabled by default. The `full`
feature can be used to get the old behavior of supporting all possible
Expand Down Expand Up @@ -61,6 +61,8 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
([#290](https://github.com/JelteF/derive_more/pull/290))
- Add support for specifying concrete types to `AsRef`/`AsMut` derives.
([#298](https://github.com/JelteF/derive_more/pull/298))
- Add `TryFrom` derive for enums to convert from their discriminant.
([#300](https://github.com/JelteF/derive_more/pull/300))

### Changed

Expand Down
9 changes: 8 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
name = "derive_more"
version = "1.0.0-beta.3"
edition = "2021"
rust-version = "1.65.0"
rust-version = "1.72.0"
description = "Adds #[derive(x)] macros for more traits"
authors = ["Jelte Fennema <github-tech@jeltef.nl>"]
license = "MIT"
Expand Down 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
11 changes: 6 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
[![Latest Version](https://img.shields.io/crates/v/derive_more.svg)](https://crates.io/crates/derive_more)
[![Rust Documentation](https://docs.rs/derive_more/badge.svg)](https://docs.rs/derive_more)
[![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg)](https://raw.githubusercontent.com/JelteF/derive_more/master/LICENSE)
[![Rust 1.65+](https://img.shields.io/badge/rustc-1.65+-lightgray.svg)](https://blog.rust-lang.org/2021/10/21/Rust-1.65.0.html)
[![Rust 1.72+](https://img.shields.io/badge/rustc-1.72+-lightgray.svg)](https://blog.rust-lang.org/2023/08/24/Rust-1.72.0.html)
[![Unsafe Forbidden](https://img.shields.io/badge/unsafe-forbidden-success.svg)](https://github.com/rust-secure-code/safety-dance)

Rust has lots of builtin traits that are implemented for its basic types, such
Expand Down 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 Expand Up @@ -140,7 +141,7 @@ These don't derive traits, but derive static methods instead.

## Installation

This library requires Rust 1.65 or higher. To avoid redundant compilation times, by
This library requires Rust 1.72 or higher. To avoid redundant compilation times, by
default no derives are supported. You have to enable each type of derive as a feature
in `Cargo.toml`:

Expand Down
2 changes: 1 addition & 1 deletion clippy.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
# See full lints list at:
# https://rust-lang.github.io/rust-clippy/master/index.html

msrv = "1.65.0"
msrv = "1.72.0"

# Ensures consistent bracing for macro calls in the codebase.
# Extends default settings:
Expand Down
4 changes: 3 additions & 1 deletion impl/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
name = "derive_more-impl"
version = "1.0.0-beta.3"
edition = "2021"
rust-version = "1.65.0"
rust-version = "1.72.0"
description = "Internal implementation of `derive_more` crate"
authors = ["Jelte Fennema <github-tech@jeltef.nl>"]
license = "MIT"
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
32 changes: 32 additions & 0 deletions impl/doc/try_from.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# What `#[derive(TryFrom)]` generates

Derive `TryFrom` allows you to convert enum discriminants into their corresponding variants.




## Enums

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.

```rust
# use derive_more::TryFrom;
#[derive(TryFrom, Debug, PartialEq)]
#[try_from(repr)]
#[repr(u32)]
enum Enum {
ImplicitZero,
ExplicitFive = 5,
FieldSix(usize),
EmptySeven{},
}

assert_eq!(Enum::ImplicitZero, Enum::try_from(0).unwrap());
assert_eq!(Enum::ExplicitFive, Enum::try_from(5).unwrap());
assert_eq!(Enum::EmptySeven{}, Enum::try_from(7).unwrap());

// Variants with fields are not supported, as the value for their fields would be undefined.
assert!(Enum::try_from(6).is_err());
```
4 changes: 4 additions & 0 deletions impl/src/lib.rs
Original file line number Diff line number Diff line change
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
208 changes: 208 additions & 0 deletions impl/src/try_from.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
//! Implementation of a [`TryFrom`] derive macro.

use std::mem;

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

/// 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) => Ok(Expansion {
repr: ReprAttribute::parse_attrs(&input.attrs)?,
attr: ItemAttribute::parse_attrs(&input.attrs)?,
ident: input.ident.clone(),
generics: input.generics.clone(),
variants: data.variants.clone().into_iter().collect(),
}
.into_token_stream()),
syn::Data::Union(data) => Err(syn::Error::new(
data.union_token.span(),
"`TryFrom` cannot be derived for unions",
)),
}
}

/// Representation of a [`TryFrom`] derive macro struct item attribute.
///
/// ```rust,ignore
/// #[try_from(repr)]
/// ```
struct ItemAttribute;

impl ItemAttribute {
/// Parses am [`ItemAttribute`] from the provided [`syn::Attribute`]s.
fn parse_attrs(attrs: impl AsRef<[syn::Attribute]>) -> syn::Result<Option<Self>> {
attrs
.as_ref()
.iter()
.filter(|attr| attr.path().is_ident("try_from"))
.try_fold(None, |mut attrs, attr| {
let mut parsed = None;
attr.parse_nested_meta(|meta| {
if meta.path.is_ident("repr") {
parsed = Some(ItemAttribute);
Ok(())
} else {
Err(meta.error("only `repr` is allowed here"))
}
})?;
if mem::replace(&mut attrs, parsed).is_some() {
Err(syn::Error::new(
attr.span(),
"only single `#[try_from(repr)]` attribute is allowed here",
))
} else {
Ok(attrs)
}
})
}
}

/// Representation of a [`#[repr(u/i*)]` Rust attribute][0].
///
/// **NOTE**: Disregards any non-integer representation `#[repr]`s.
///
/// ```rust,ignore
/// #[repr(<type>)]
/// ```
///
/// [0]: https://doc.rust-lang.org/reference/type-layout.html#primitive-representations
struct ReprAttribute(syn::Ident);

impl ReprAttribute {
/// Parses a [`ReprAttribute`] from the provided [`syn::Attribute`]s.
///
/// If there is no [`ReprAttribute`], then parses a [default `isize` discriminant][0].
///
/// [0]: https://doc.rust-lang.org/reference/items/enumerations.html#discriminants
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 matches!(
ident.to_string().as_str(),
"u8" | "u16"
| "u32"
| "u64"
| "u128"
| "usize"
| "i8"
| "i16"
| "i32"
| "i64"
| "i128"
| "isize"
) {
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)
})
.map(|repr| {
// Default discriminant is interpreted as `isize`:
// https://doc.rust-lang.org/reference/items/enumerations.html#discriminants
repr.unwrap_or_else(|| syn::Ident::new("isize", Span::call_site()))
})
.map(Self)
}
}

/// Expansion of a macro for generating [`TryFrom`] implementation of an enum.
struct Expansion {
/// `#[repr(u/i*)]` of the enum.
repr: ReprAttribute,

/// [`ItemAttribute`] of the enum.
attr: Option<ItemAttribute>,

/// [`syn::Ident`] of the enum.
ident: syn::Ident,

/// [`syn::Generics`] of the enum.
generics: syn::Generics,

/// [`syn::Variant`]s of the enum.
variants: Vec<syn::Variant>,
}

impl ToTokens for Expansion {
/// Expands [`TryFrom`] implementations for a struct.
fn to_tokens(&self, tokens: &mut TokenStream) {
if self.attr.is_none() {
return;
}
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<syn::Ident>,
(Vec<TokenStream>, Vec<TokenStream>),
) = self
.variants
.iter()
.filter_map(
|syn::Variant {
ident,
fields,
discriminant,
..
}| {
if let Some(d) = discriminant {
last_discriminant = d.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();

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

#[allow(non_upper_case_globals)]
#[inline]
fn try_from(val: #repr) -> ::core::result::Result<Self, Self::Error> {
#( const #consts: #repr = #discriminants; )*
match val {
#(#consts => ::core::result::Result::Ok(#ident::#variants),)*
_ => ::core::result::Result::Err(::derive_more::TryFromReprError::new(val)),
}
}
}
}.to_tokens(tokens);
}
}
Loading

0 comments on commit 557e801

Please sign in to comment.