config-manager-proc 0.4.5

Macro implementation for config-manager-rs derives
Documentation
// SPDX-License-Identifier: MIT
// Copyright (c) 2022 JSRPC “Kryptonite”

use std::fmt::Display;

use super::*;

impl ToTokens for ClapInitialization {
    fn to_tokens(&self, tokens: &mut TokenStream) {
        tokens.extend(match self {
            Self::None => unreachable!(),
            Self::Flatten(tp) => {
                quote_spanned! {tp.span()=>
                    args(<#tp as ::config_manager::__private::Flatten>::get_args())
                }
            }
            Self::Subcommand(tp) => {
                quote_spanned! {tp.span()=>
                    <#tp as ::config_manager::__private::clap::Subcommand>::augment_subcommands(app)
                }
            }
            Self::Normal(info) => quote_spanned! {info.span()=>
                 arg(#info)
            },
        })
    }
}

impl ToTokens for NormalClapFieldInfo {
    fn to_tokens(&self, tokens: &mut TokenStream) {
        tokens.extend({
            let span = self.span;
            let name = self.long.clone();
            let mut attrs = self.attributes.clone();
            let mut field = quote_spanned! {span=>
                clap::Arg::new(#name).long(#name).required(false)
            };

            field.extend(if attrs.remove("flag").is_some() {
                quote_spanned!(span=> .num_args(0..=1).default_missing_value("true"))
            } else {
                quote_spanned!(span=> .num_args(1))
            });

            for (attr, val) in attrs {
                let method_name = Ident::new(&attr, span);
                field.extend(quote_spanned!(span=> .#method_name(#val)));
            }

            field
        })
    }
}

#[derive(Clone)]
pub(crate) struct ExtractedAttributes {
    pub(crate) span: Span,
    pub(crate) variables: Vec<FieldAttribute>,
    pub(crate) default: Option<Default>,
    pub(crate) deserializer: Option<(TokenStream, Span)>,
}

impl ExtractedAttributes {
    pub(crate) fn new(span: Span) -> Self {
        Self {
            span,
            variables: vec![],
            default: None,
            deserializer: None,
        }
    }

    fn deserializer(&self) -> TokenStream {
        let span = self.span;
        match &self.deserializer {
            None => quote_spanned! {span=>
                let value = if value.is_empty() {
                    "\"\"".to_string()
                } else {
                    value
                };
                ::config_manager::__private::deser_hjson::from_str(&value)
            },
            Some((deser_fn, span)) => {
                let ident = Ident::new(deser_fn.to_string().trim_matches('\"'), *span);
                quote_spanned! {*span=> (#ident)(&value) }
            }
        }
    }

    fn gen_err(&self, field_name: &str) -> TokenStream {
        let span = self.span;
        let err = format!(
            "field {field_name} not found nor in {} nor as a default",
            self.variables
                .iter()
                .map(ToString::to_string)
                .collect::<Vec<_>>()
                .as_slice()
                .join(", ")
        );
        quote_spanned! {span=>
            ::config_manager::Error::MissingArgument(#err.to_string())
        }
    }

    fn gen_rest_init(&self, field_name: &str) -> TokenStream {
        let default_span = self.span;
        self.variables.iter().fold(
            quote_spanned!(default_span=> ::std::option::Option::<::std::string::String>::None),
            |acc, attribute_init| {
                let attribute_init = attribute_init.gen_init(field_name);
                let span = attribute_init.span();
                quote_spanned! {span=>
                    #acc.or(#attribute_init)
                }
            },
        )
    }

    pub(super) fn clap_field(self, field_name: &str) -> Result<Option<NormalClapFieldInfo>> {
        for attr in self.variables {
            if let FieldAttribute::Clap(clap) = attr {
                return Some(clap.normalize(field_name)).transpose();
            }
        }
        Ok(None)
    }

    pub(super) fn gen_init(&self, field: &Field) -> TokenStream {
        let field_name = field.ident.clone().unwrap().to_string();
        let tp = &field.ty;
        let default_initialization = match &self.default {
            None => quote_spanned!(field.span()=> ::std::option::Option::None),
            Some(Default { inner: None }) => {
                quote_spanned!(field.span()=> ::std::option::Option::Some::<#tp>(
                    ::std::default::Default::default()
                ))
            }
            Some(Default { inner: Some(def) }) => quote_spanned! {field.span()=>
                ::std::option::Option::Some::<#tp>(#def)
            },
        };
        let deserializer = self.deserializer();
        let rest = self.gen_rest_init(&field_name);
        let missing_err = self.gen_err(&field_name);

        quote_spanned! {field.span()=>
            (|| -> ::std::result::Result<_, ::config_manager::Error> {
                let init_without_default = #rest;
                match (init_without_default, #default_initialization) {
                    (::std::option::Option::<::std::string::String>::None, ::std::option::Option::None) => {
                            ::std::result::Result::<_, ::config_manager::Error>::Err(#missing_err)?
                        },
                    (::std::option::Option::<::std::string::String>::None, ::std::option::Option::Some(default_value)) => ::std::result::Result::Ok(default_value),
                    (::std::option::Option::<::std::string::String>::Some(value), _) => {
                        #deserializer.map_err(|err| {
                            ::config_manager::Error::FailedParse(
                                ::std::format!("Can't deserialize from value: {} of field {}; error is {}", value, #field_name, err)
                            )
                        })
                    }
                }
            })()?
        }
    }
}

#[derive(Clone)]
pub(crate) enum FieldAttribute {
    Clap(ClapFieldParseResult),
    Env(Env),
    Config(Config),
}

impl FieldAttribute {
    fn span(&self) -> Span {
        match self {
            Self::Clap(v) => v.span,
            Self::Env(v) => v.span,
            Self::Config(v) => v.span,
        }
    }

    fn gen_init(&self, field_name: &str) -> TokenStream {
        let span = self.span();
        match &self {
            Self::Env(env) => {
                let prefixed_name = env.prefixed_name(field_name);
                quote_spanned! {span=>
                    env_data.get(&(#prefixed_name) as &::std::primitive::str).map(::std::string::ToString::to_string)
                }
            }
            Self::Config(cfg) => {
                let table = cfg.table();
                let key = cfg.key(field_name);
                quote_spanned! {span=>
                    ::config_manager::__private::find_field_in_table(config_file_data, #table, #key.to_string())?
                }
            }
            Self::Clap(clap) => {
                let long = clap.normal_long(field_name);
                quote_spanned! {span=>
                    clap_data.get_one::<::std::string::String>(#long).map(::std::string::ToString::to_string)
                }
            }
        }
    }
}

impl Display for FieldAttribute {
    fn fmt(&self, f: &mut __private::Formatter<'_>) -> std::fmt::Result {
        let source = match self {
            Self::Clap(_) => "command line",
            Self::Config(_) => "configuration file",
            Self::Env(_) => "env",
        };
        write!(f, "{source}",)
    }
}

#[derive(Clone)]
pub(crate) struct Env {
    pub(super) inner: Option<TokenStream>,
    pub(super) span: Span,
}

impl std::default::Default for Env {
    fn default() -> Self {
        Self {
            inner: None,
            span: Span::call_site(),
        }
    }
}

impl Env {
    fn prefixed_name(&self, field_name: &str) -> TokenStream {
        let span = self.span;
        let env_attribute = match &self.inner {
            None => quote_spanned!(span=> ::std::option::Option::<&::std::primitive::str>::None),
            Some(value) => {
                quote_spanned!(span=> ::std::option::Option::<&::std::primitive::str>::Some(#value))
            }
        };
        let binary_name = binary_name();
        let field_name_lowercase = str_to_tokens(field_name.to_lowercase(), span);

        quote_spanned! {span=>
            {
                let env_prefix = env_prefix.clone();
                match (#env_attribute, env_prefix) {
                    (::std::option::Option::Some(name), _) => name.to_string(),
                    (::std::option::Option::None, ::std::option::Option::None) => {
                        let binary_name = #binary_name?;
                        ::std::format!("{}_{}", binary_name, #field_name_lowercase)
                    },
                    (::std::option::Option::None, ::std::option::Option::Some(pref)) if pref.is_empty() => {
                        #field_name_lowercase.to_string()
                    },
                    (::std::option::Option::None, ::std::option::Option::Some(pref)) => {
                        ::std::format!("{}_{}", pref, #field_name_lowercase)
                    }
                }.to_lowercase()
            }
        }
    }
}

#[derive(Clone)]
pub(crate) struct Config {
    span: Span,
    pub(super) key: Option<TokenStream>,
    pub(super) table: Option<TokenStream>,
}

impl std::default::Default for Config {
    fn default() -> Self {
        Self {
            span: Span::call_site(),
            key: None,
            table: None,
        }
    }
}

impl Config {
    fn key(&self, field_name: &str) -> TokenStream {
        self.key
            .clone()
            .unwrap_or_else(|| str_to_tokens(field_name, self.span))
    }
    fn table(&self) -> TokenStream {
        self.table
            .clone()
            .map(|table| quote_spanned!(table.span()=> ::std::option::Option::Some(#table.to_string())))
            .unwrap_or_else(|| quote_spanned!(self.span=> ::std::option::Option::None))
    }
}

#[derive(Default, Clone)]
pub(crate) struct Default {
    pub(super) inner: Option<TokenStream>,
}

pub(super) fn extract_attributes(
    field: &Field,
    table_name: &Option<TokenStream>,
) -> Result<Option<ExtractedAttributes>> {
    let is_bool = field.ty.to_token_stream().to_string() == "bool";
    let is_string = is_string(&field.ty);
    let docs = extract_docs(&field.attrs);

    let mut res = ExtractedAttributes::new(field.span());

    let attr = match field.attrs.iter().find(|a| a.path().is_ident(SOURCE_KEY)) {
        None => return Ok(None),
        Some(attr) => attr,
    };

    let nested = attr.parse_args_with(Punctuated::<Meta, Token![,]>::parse_terminated)?;

    for arg in nested {
        match path_to_string(arg.path()).as_str() {
            CLAP_KEY => match &arg {
                Meta::Path(_p) => res
                    .variables
                    .push(FieldAttribute::Clap(ClapFieldParseResult::new(arg.span()))),
                Meta::List(clap_metalist) => {
                    let mut clap_attributes = parse_clap_field_attribute(clap_metalist, is_bool)?;
                    clap_attributes.docs = docs.clone();
                    res.variables.push(FieldAttribute::Clap(clap_attributes));
                }
                _ => {
                    panic_span!(
                        arg.span(),
                        "clap attribute must match #[clap(...)] or #[clap]"
                    )
                }
            },
            DEFAULT => {
                if res.default.is_some() {
                    panic_span!(arg.span(), "Default can be assigned only once per field")
                }
                let mut default_init = extract_default(&arg)?;
                if is_string {
                    default_init = default_init
                        .map(|d| quote_spanned!(d.span()=> ::std::convert::Into::into(#d)));
                }
                res.default = Some(Default {
                    inner: default_init,
                })
            }
            ENV_KEY => res.variables.push(FieldAttribute::Env(Env {
                inner: meta_to_option(&arg)?,
                span: arg.span(),
            })),
            CONFIG_KEY => res.variables.push(FieldAttribute::Config(Config {
                span: arg.span(),
                key: meta_to_option(&arg)?,
                table: table_name.clone(),
            })),
            DESERIALIZER => {
                if res.deserializer.is_some() {
                    panic_span!(
                        arg.span(),
                        "deserialize_with can be assigned only once per field"
                    )
                }
                if matches!(arg, Meta::Path(_)) {
                    panic_span!(arg.span(), "deserialize_with can't be empty")
                }
                res.deserializer = meta_to_option(&arg)?.map(|val| (val, arg.span()));
            }
            _ => panic_span!(arg.span(), "Unknown source attribute"),
        };
    }

    Ok(Some(res))
}

fn is_string(ty: &Type) -> bool {
    let path = match ty {
        Type::Path(path) if path.qself.is_none() => &path.path,
        _ => return false,
    };
    let idents_of_path = path.segments.iter().fold(String::new(), |mut acc, v| {
        acc.push_str(&v.ident.to_string());
        acc.push('|');
        acc
    });

    vec![
        "std|string|String|",
        "core|string|String|",
        "string|String|",
        "String|",
    ]
    .into_iter()
    .any(|s| idents_of_path == *s)
}