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 all commits
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: 1 addition & 1 deletion .github/workflows/ci.yml
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
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
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
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
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
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
@@ -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());
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
208 changes: 208 additions & 0 deletions impl/src/try_from.rs
@@ -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);
}
}