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}