convert-enum 0.1.0

Automatically generate From and reverse TryFrom implementations for enums
Documentation
// SPDX-FileCopyrightText: 2022 Alois Wohlschlager <alois1@gmx-topmail.de>
// SPDX-License-Identifier: Apache-2.0 OR MIT

//! This crate allows to automatically implement `From` and reverse `TryFrom` on suitable
//! enumerations.
//!
//! In cases where you have an enumeration that wraps multiple different types, and you desire a
//! [`From`] or reverse [`TryFrom`] implementation for each of them (for example, this is rather
//! common with error handling, when you want to wrap errors from different libraries), this can
//! be quite tedious to do manually.
//!
//! Using the `From` and `TryInto` derive macros from the present crate, this work can be
//! automated.
//!
//! # Example
//!
//! Define an `Error` type that can be converted from both [`std::fmt::Error`] and
//! [`std::io::Error`] (for facilitating use of the question mark operator), and that in addition
//! offers a variant with a custom message that should not be available for automatic conversion:
//!
//! ```
//! #[derive(convert_enum::From)]
//! enum Error {
//!     #[convert_enum(optout)]
//!     Custom(Cow<'static, str>),
//!     Fmt(std::fmt::Error),
//!     Io(std::io::Error),
//! }
//! ```
//!
//! This results in the following implementations being generated automatically:
//! ```
//! impl From<std::fmt::Error> for Error {
//!     fn from(val: std::fmt::Error) -> Self {
//!         Self::Fmt(val)
//!     }
//! }
//!
//! impl From<std::io::Error> for Error {
//!     fn from(val: std::io::Error) -> Self {
//!         Self::Io(val)
//!     }
//! }
//! ```

use proc_macro::TokenStream;
use syn::{spanned::Spanned, Fields, Generics, Ident, ItemEnum, Meta, NestedMeta, Type, Variant};

fn convert_enum_variant(variant: &Variant, f: impl Fn(&Ident, &Type) -> TokenStream) -> Result<TokenStream, &'static str> {
    let mut optout = false;
    for attr in &variant.attrs {
        if attr.path.is_ident("convert_enum") {
            match attr.parse_meta() {
                Ok(Meta::List(meta)) => {
                    for entry in meta.nested {
                        match entry {
                            NestedMeta::Meta(Meta::Path(path)) => {
                                if path.is_ident("optout") {
                                    optout = true;
                                } else {
                                    return Err("Invalid #[convert_enum] attribute");
                                }
                            }
                            _ => return Err("Invalid #[convert_enum] attribute"),
                        }
                    }
                }
                _ => return Err("Invalid #[convert_enum] attribute"),
            }
        }
    }

    if optout {
        Ok(TokenStream::new())
    } else {
        match &variant.fields {
            Fields::Unnamed(fields) => {
                let fields = fields.unnamed.iter().collect::<Vec<_>>();
                if fields.len() == 1 {
                    let field = fields.into_iter().next().unwrap();
                    Ok(f(&variant.ident, &field.ty))
                } else {
                    Err("ConvertEnum items must have exactly one field")
                }
            }
            _ => Err("ConvertEnum items must be tuple-like"),
        }
    }
}

fn convert_enum(item: TokenStream, f: impl Fn(&Ident, &Generics, &Ident, &Type) -> TokenStream) -> TokenStream {
    let item = syn::parse_macro_input!(item as ItemEnum);

    let name = &item.ident;
    let generics = &item.generics;

    item.variants
        .into_iter()
        .map(|variant| match convert_enum_variant(&variant, |var, ty| f(name, generics, var, ty)) {
            Ok(tokens) => tokens,
            Err(msg) => quote::quote_spanned! {
                variant.span() => compile_error!(#msg);
            }
            .into(),
        })
        .collect()
}

/// Automatically generate [`From`] implementations for enum variants, ignoring ones marked
/// `#[convert_enum(optout)]`.
///
/// All variants not affected by the opt-out must be tuple-like variants with exactly one field. A
/// variant of the form `E::V(T)` will cause the generation of the following code:
/// ```
/// impl From<T> for E {
///     fn from(val: T) -> Self {
///         Self::V(val)
///     }
/// }
/// ```
#[proc_macro_derive(From, attributes(convert_enum))]
pub fn convert_enum_from(item: TokenStream) -> TokenStream {
    convert_enum(item, |name, generics, var, ty| {
        quote::quote! {
            impl #generics ::core::convert::From<#ty> for #name #generics {
                fn from(val: #ty) -> Self {
                    Self::#var(val)
                }
            }
        }
        .into()
    })
}

/// Automatically generate reverse [`TryFrom`] implementations for enum variants, ignore ones
/// marked `#[convert_enum(optout)]`.
///
/// All variants not affected by the opt-out must be tuple-like variants with exactly one field. A
/// variant of the form `E::V(T)` will cause the generation of the following code:
/// ```
/// impl TryFrom<E> for T {
///     type Error = E;
///
///     fn try_from(val: E) -> Result<T, E> {
///         match val {
///             E::V(val) => Ok(val),
///             _ => Err(val),
///         }
///     }
/// }
/// ```
#[proc_macro_derive(TryInto, attributes(convert_enum))]
pub fn convert_enum_try_into(item: TokenStream) -> TokenStream {
    convert_enum(item, |name, generics, var, ty| {
        quote::quote! {
            impl #generics ::core::convert::TryFrom<#name #generics> for #ty {
                type Error = #name #generics;

                fn try_from(val: #name #generics) -> Result<#ty, #name #generics> {
                    match val {
                        #name::#var(val) => Ok(val),
                        _ => Err(val),
                    }
                }
            }
        }
        .into()
    })
}