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 6 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 @@ -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
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/2021/10/21/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
5 changes: 3 additions & 2 deletions impl/Cargo.toml
Expand Up @@ -2,15 +2,14 @@
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"
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 Down Expand Up @@ -66,6 +65,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 +91,7 @@ full = [
"mul_assign",
"not",
"sum",
"try_from",
"try_into",
"try_unwrap",
"unwrap",
Expand Down
28 changes: 28 additions & 0 deletions impl/doc/try_from.md
@@ -0,0 +1,28 @@
# 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
# use derive_more::TryFrom;
#[derive(TryFrom, Debug, PartialEq)]
#[try_from(repr)]
#[repr(u32)]
enum Enum {
Implicit,
Explicit = 5,
Field(usize),
Empty{},
ModProg marked this conversation as resolved.
Show resolved Hide resolved
}

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, 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
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
181 changes: 181 additions & 0 deletions impl/src/try_from.rs
@@ -0,0 +1,181 @@
//! Implementation of a [`TryFrom`] derive macro.

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(),
variants: data.variants.clone().into_iter().collect(),
generics: input.generics.clone(),
}
.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)]
/// ```
#[derive(Default)]
struct ItemAttribute {
/// plain `repr`
repr: bool,
}

impl ItemAttribute {
/// 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("try_from"))
.try_fold(ItemAttribute::default(), |mut attrs, attr| {
attr.parse_nested_meta(|meta| {
if meta.path.is_ident("repr") {
attrs.repr = true;
Ok(())
} else {
Err(meta.error("only `repr` is allowed here"))
}
})
.map(|_| attrs)
})
}
}

/// Representation of a [`Repr`] derive macro struct container attribute.
///
/// Note: This disregards any non integer representation reprs.
///
/// ```rust,ignore
/// #[repr(<type>)]
/// ```
struct ReprAttribute(syn::Ident);

impl ReprAttribute {
/// Parses a [`ReprAttribute`] 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(|| syn::Ident::new("isize", Span::call_site()))
})
.map(Self)
}
}

/// Expansion of a macro for generating [`TryFrom`] implementation of an enum
struct Expansion {
/// Enum `#[repr(u/i*)]`
repr: ReprAttribute,
/// Attributes on item.
attr: ItemAttribute,
/// Enum [`Ident`].
ident: syn::Ident,
/// Variant [`Ident`] in case of enum expansion.
variants: Vec<syn::Variant>,
/// Struct or enum [`syn::Generics`].
generics: syn::Generics,
}

impl ToTokens for Expansion {
/// Expands [`TryFrom`] implementations for a struct.
fn to_tokens(&self, tokens: &mut TokenStream) {
if !self.attr.repr {
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(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();

quote! {
#[automatically_derived]
impl #impl_generics
::core::convert::TryFrom<#repr #ty_generics> for #ident
#where_clause
{
type Error = ::derive_more::TryFromReprError<#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::TryFromReprError::new(value)),
}
}
}
}.to_tokens(tokens);
}
}