convert_enum/lib.rs
1// SPDX-FileCopyrightText: 2022 Alois Wohlschlager <alois1@gmx-topmail.de>
2// SPDX-License-Identifier: Apache-2.0 OR MIT
3
4//! This crate allows to automatically implement `From` and reverse `TryFrom` on suitable
5//! enumerations.
6//!
7//! In cases where you have an enumeration that wraps multiple different types, and you desire a
8//! [`From`] or reverse [`TryFrom`] implementation for each of them (for example, this is rather
9//! common with error handling, when you want to wrap errors from different libraries), this can
10//! be quite tedious to do manually.
11//!
12//! Using the `From` and `TryInto` derive macros from the present crate, this work can be
13//! automated.
14//!
15//! # Example
16//!
17//! Define an `Error` type that can be converted from both [`std::fmt::Error`] and
18//! [`std::io::Error`] (for facilitating use of the question mark operator), and that in addition
19//! offers a variant with a custom message that should not be available for automatic conversion:
20//!
21//! ```
22//! #[derive(convert_enum::From)]
23//! enum Error {
24//! #[convert_enum(optout)]
25//! Custom(Cow<'static, str>),
26//! Fmt(std::fmt::Error),
27//! Io(std::io::Error),
28//! }
29//! ```
30//!
31//! This results in the following implementations being generated automatically:
32//! ```
33//! impl From<std::fmt::Error> for Error {
34//! fn from(val: std::fmt::Error) -> Self {
35//! Self::Fmt(val)
36//! }
37//! }
38//!
39//! impl From<std::io::Error> for Error {
40//! fn from(val: std::io::Error) -> Self {
41//! Self::Io(val)
42//! }
43//! }
44//! ```
45
46use proc_macro::TokenStream;
47use syn::{spanned::Spanned, Fields, Generics, Ident, ItemEnum, Meta, NestedMeta, Type, Variant};
48
49fn convert_enum_variant(variant: &Variant, f: impl Fn(&Ident, &Type) -> TokenStream) -> Result<TokenStream, &'static str> {
50 let mut optout = false;
51 for attr in &variant.attrs {
52 if attr.path.is_ident("convert_enum") {
53 match attr.parse_meta() {
54 Ok(Meta::List(meta)) => {
55 for entry in meta.nested {
56 match entry {
57 NestedMeta::Meta(Meta::Path(path)) => {
58 if path.is_ident("optout") {
59 optout = true;
60 } else {
61 return Err("Invalid #[convert_enum] attribute");
62 }
63 }
64 _ => return Err("Invalid #[convert_enum] attribute"),
65 }
66 }
67 }
68 _ => return Err("Invalid #[convert_enum] attribute"),
69 }
70 }
71 }
72
73 if optout {
74 Ok(TokenStream::new())
75 } else {
76 match &variant.fields {
77 Fields::Unnamed(fields) => {
78 let fields = fields.unnamed.iter().collect::<Vec<_>>();
79 if fields.len() == 1 {
80 let field = fields.into_iter().next().unwrap();
81 Ok(f(&variant.ident, &field.ty))
82 } else {
83 Err("ConvertEnum items must have exactly one field")
84 }
85 }
86 _ => Err("ConvertEnum items must be tuple-like"),
87 }
88 }
89}
90
91fn convert_enum(item: TokenStream, f: impl Fn(&Ident, &Generics, &Ident, &Type) -> TokenStream) -> TokenStream {
92 let item = syn::parse_macro_input!(item as ItemEnum);
93
94 let name = &item.ident;
95 let generics = &item.generics;
96
97 item.variants
98 .into_iter()
99 .map(|variant| match convert_enum_variant(&variant, |var, ty| f(name, generics, var, ty)) {
100 Ok(tokens) => tokens,
101 Err(msg) => quote::quote_spanned! {
102 variant.span() => compile_error!(#msg);
103 }
104 .into(),
105 })
106 .collect()
107}
108
109/// Automatically generate [`From`] implementations for enum variants, ignoring ones marked
110/// `#[convert_enum(optout)]`.
111///
112/// All variants not affected by the opt-out must be tuple-like variants with exactly one field. A
113/// variant of the form `E::V(T)` will cause the generation of the following code:
114/// ```
115/// impl From<T> for E {
116/// fn from(val: T) -> Self {
117/// Self::V(val)
118/// }
119/// }
120/// ```
121#[proc_macro_derive(From, attributes(convert_enum))]
122pub fn convert_enum_from(item: TokenStream) -> TokenStream {
123 convert_enum(item, |name, generics, var, ty| {
124 quote::quote! {
125 impl #generics ::core::convert::From<#ty> for #name #generics {
126 fn from(val: #ty) -> Self {
127 Self::#var(val)
128 }
129 }
130 }
131 .into()
132 })
133}
134
135/// Automatically generate reverse [`TryFrom`] implementations for enum variants, ignore ones
136/// marked `#[convert_enum(optout)]`.
137///
138/// All variants not affected by the opt-out must be tuple-like variants with exactly one field. A
139/// variant of the form `E::V(T)` will cause the generation of the following code:
140/// ```
141/// impl TryFrom<E> for T {
142/// type Error = E;
143///
144/// fn try_from(val: E) -> Result<T, E> {
145/// match val {
146/// E::V(val) => Ok(val),
147/// _ => Err(val),
148/// }
149/// }
150/// }
151/// ```
152#[proc_macro_derive(TryInto, attributes(convert_enum))]
153pub fn convert_enum_try_into(item: TokenStream) -> TokenStream {
154 convert_enum(item, |name, generics, var, ty| {
155 quote::quote! {
156 impl #generics ::core::convert::TryFrom<#name #generics> for #ty {
157 type Error = #name #generics;
158
159 fn try_from(val: #name #generics) -> Result<#ty, #name #generics> {
160 match val {
161 #name::#var(val) => Ok(val),
162 _ => Err(val),
163 }
164 }
165 }
166 }
167 .into()
168 })
169}