parenv-derive 0.1.3

Macro implementation of #[derive(Environment)]
Documentation
use proc_macro::TokenStream;
use proc_macro2::TokenTree;
use quote::{format_ident, quote};
use syn::{parse_macro_input, Data, DeriveInput, Fields};

#[proc_macro_derive(Environment, attributes(parenv))]
pub fn derive_environment(input: TokenStream) -> TokenStream {
    let input = parse_macro_input!(input as DeriveInput);

    let ident = input.ident;
    let Data::Struct(struct_data) = input.data else {
        panic!("environment parser can only be derived on structs");
    };
    let Fields::Named(fields) = struct_data.fields else {
        panic!("environment parser can only be derived on structs whose fields have names");
    };

    let prefix = input
        .attrs
        .into_iter()
        .find_map(|attr| match attr.meta {
            syn::Meta::List(val) if val.path.is_ident("parenv") => Some(val.tokens),
            _ => None,
        })
        .and_then(|expr| {
            let mut tokens = expr.into_iter();
            tokens.next().and_then(|t| match t {
                TokenTree::Ident(ident) if ident.to_string() == "prefix" => Some(()),
                _ => None,
            })?;
            tokens.next().and_then(|t| match t {
                TokenTree::Punct(punct) if punct.as_char() == '=' => Some(()),
                _ => None,
            })?;
            let prefix = tokens.next().and_then(|t| match t {
                TokenTree::Literal(lit) => Some(lit.to_string()),
                _ => None,
            })?;

            Some(if prefix.is_empty() {
                prefix
            } else {
                prefix[1..(prefix.len() - 1)].to_owned()
            })
        })
        .unwrap_or_default();

    let field_descs: Vec<_> = fields
        .named
        .iter()
        .map(|field| {
            let is_option = subty_if_name(&field.ty, "Option").is_some();

            let ident = field.ident.clone().unwrap();
            let ident_uppercase = ident.to_string().to_uppercase();
            let ident_uppercase = format!("{prefix}{ident_uppercase}");

            let doc_comment = extract_doc_comment(field);

            if is_option {
                quote! {
                    [#ident_uppercase.bold().to_string(), #doc_comment.bright_magenta().to_string(), "[optional]".dimmed().to_string()]
                }
            } else {
                quote! {
                    [#ident_uppercase.bold().to_string(), #doc_comment.bright_magenta().to_string(), "".to_string()]
                }
            }
        })
        .collect();

    let field_names: Vec<_> = fields
        .named
        .iter()
        .map(|field| {
            let is_option = subty_if_name(&field.ty, "Option").is_some();
            let ident = field.ident.clone().unwrap();
            if is_option {
                quote! {
                    #ident: #ident
                }
            } else {
                quote! {
                    #ident: #ident.unwrap()
                }
            }
        })
        .collect();

    let parse_fields: Vec<_> = fields
        .named
        .into_iter()
        .enumerate()
        .map(|(i, field)| {
            let ident = field.ident.unwrap();
            let is_option = subty_if_name(&field.ty, "Option");

            let ident_uppercase = ident.to_string().to_uppercase();
            let ident_uppercase = format!("{prefix}{ident_uppercase}");
            let parse_ident = format_ident!("parse_{ident}");

            if let Some(inner_typ) = is_option {
                quote! {
                    fn #parse_ident() -> ::std::option::Option<::parenv::miette::Result<#inner_typ>> {
                        ::std::env::var(#ident_uppercase)
                            .ok()
                            .map(|f| {
                                f.parse::<#inner_typ>()
                                    .into_diagnostic()
                                    .wrap_err_with(||
                                        format!(
                                            "I couldn't parse the value '{}' provided by the environment variable {}.",
                                            f.red().bold(),
                                            #ident_uppercase.red().bold()
                                        )
                                    )
                            })
                    }

                    let #ident = match #parse_ident() {
                        ::std::option::Option::Some(res) => {
                            match res {
                                ::std::result::Result::Err(err) => {
                                    errors[#i] = ::std::option::Option::Some(err);
                                    ::std::option::Option::None
                                },
                                ::std::result::Result::Ok(val) => ::std::option::Option::Some(val),
                            }
                        },
                        ::std::option::Option::None => ::std::option::Option::None
                    };
                }
            } else {
                let ident_typ = field.ty;

                quote! {
                    fn #parse_ident() -> ::parenv::miette::Result<#ident_typ> {
                        let #ident = ::std::env::var(#ident_uppercase)
                            .into_diagnostic()
                            .wrap_err_with(||
                                format!(
                                    "I couldn't find the environment variable {}.",
                                    #ident_uppercase.red().bold()
                                )
                            )?;
                        let #ident: #ident_typ = #ident.parse()
                            .into_diagnostic()
                            .wrap_err_with(||
                                format!(
                                    "I couldn't parse the value '{}' provided by the environment variable {}.",
                                    #ident.red().bold(),
                                    #ident_uppercase.red().bold()
                                )
                            )?;
                        Ok(#ident)
                    }

                    let #ident = match #parse_ident() {
                        ::std::result::Result::Err(err) => {
                            errors[#i] = Some(err);
                            ::std::option::Option::None
                        },
                        ::std::result::Result::Ok(val) => ::std::option::Option::Some(val),
                    };
                }
            }
        })
        .collect();

    let fields_len = field_names.len();
    let nones: Vec<_> = std::iter::repeat_n(quote! { None }, fields_len).collect();

    let expanded = quote! {
        impl #ident {
            fn parse() -> Self {
                use ::parenv::miette::{IntoDiagnostic, WrapErr};
                use ::parenv::owo_colors::OwoColorize;

                let mut errors: [
                    ::std::option::Option<::parenv::miette::Report>;
                    #fields_len
                ] = [#(#nones),*];

                #(#parse_fields)*

                let there_is_a_some = errors.iter().any(|e| e.is_some());
                if there_is_a_some {
                    let crate_name = ::std::env!("CARGO_PKG_NAME").green();

                    ::std::println!("I, {crate_name}, expect the following environment variables.\n");

                    let items: [[::std::string::String; 3]; #fields_len] = [ #(#field_descs),* ];

                    let mut max_widths: [usize; 3] = [0; 3];
                    for col in 0..3 {
                        max_widths[col] = items
                            .iter()
                            .map(|row| row[col].len())
                            .max()
                            .unwrap_or(0);
                    }

                    for row in items {
                        ::std::print!("    ");
                        for (value, width) in row.iter().zip(&max_widths) {
                            ::std::print!("{:<width$}    ", value, width = width);
                        }
                        ::std::println!();
                    }

                    ::std::println!();

                    ::std::println!("I faced an error parsing the following environment variables.\n");

                    for err in errors {
                        if let ::std::option::Option::Some(e) = err {
                            ::std::println!("{:?}", e);
                        }
                    }

                    ::std::process::exit(1);
                }

                Self {
                    #(#field_names),*
                }
            }
        }
    };

    TokenStream::from(expanded)
}

fn extract_doc_comment(field: &syn::Field) -> String {
    field
        .attrs
        .iter()
        .find_map(|attr| match attr.meta.clone() {
            syn::Meta::NameValue(val) if val.path.is_ident("doc") => Some(val.value),
            _ => None,
        })
        .and_then(|expr| match expr {
            syn::Expr::Lit(lit) => Some(lit.lit),
            _ => None,
        })
        .and_then(|lit| match lit {
            syn::Lit::Str(lit_str) => Some(lit_str.value()),
            _ => None,
        })
        .map(|doc| doc.trim().to_owned())
        .unwrap_or_default()
}

pub(crate) fn subty_if_name<'a>(ty: &'a syn::Type, name: &str) -> Option<&'a syn::Type> {
    subty_if(ty, |seg| seg.ident == name)
}

fn subty_if<F>(ty: &syn::Type, f: F) -> Option<&syn::Type>
where
    F: FnOnce(&syn::PathSegment) -> bool,
{
    use syn::{GenericArgument, PathArguments::AngleBracketed};

    only_last_segment(ty)
        .filter(|segment| f(segment))
        .and_then(|segment| {
            if let AngleBracketed(args) = &segment.arguments {
                only_one(args.args.iter()).and_then(|genneric| {
                    if let GenericArgument::Type(ty) = genneric {
                        Some(ty)
                    } else {
                        None
                    }
                })
            } else {
                None
            }
        })
}

fn only_last_segment(mut ty: &syn::Type) -> Option<&syn::PathSegment> {
    use syn::{Path, Type, TypePath};

    while let Type::Group(syn::TypeGroup { elem, .. }) = ty {
        ty = elem;
    }
    match ty {
        Type::Path(TypePath {
            qself: None,
            path:
                Path {
                    leading_colon: None,
                    segments,
                },
        }) => only_one(segments.iter()),

        _ => None,
    }
}

fn only_one<I, T>(mut iter: I) -> Option<T>
where
    I: Iterator<Item = T>,
{
    iter.next().filter(|_| iter.next().is_none())
}