config-manager-proc 0.3.1

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!(args(<#tp as ::config_manager::__private::Flatten>::get_args()))
            }
            Self::Subcommand(tp) => {
                quote!(<#tp as ::config_manager::__private::clap::Subcommand>::augment_subcommands(app))
            }
            Self::Normal(info) => quote!(arg(#info)),
        })
    }
}

impl ToTokens for NormalClapFieldInfo {
    fn to_tokens(&self, tokens: &mut TokenStream) {
        tokens.extend({
            let long = format_to_tokens!(".long({})", self.long);
            let name = format_to_tokens!("{}", self.long);
            let short = match &self.short {
                None => TokenStream::new(),
                Some(short) => format_to_tokens!(".short({short})"),
            };
            let flag = if self.flag {
                format_to_tokens!(".num_args(0..=1).default_missing_value(\"true\")")
            } else {
                format_to_tokens!(".num_args(1)")
            };
            let help = match &self.help {
                None => TokenStream::new(),
                Some(help) => format_to_tokens!(".help({help})"),
            };
            let long_help = match &self.long_help {
                None => TokenStream::new(),
                Some(long_help) => format_to_tokens!(".long_help({long_help})"),
            };
            let help_heading = match &self.help_heading {
                None => TokenStream::new(),
                Some(help_heading) => format_to_tokens!(".help_heading({help_heading})"),
            };
            quote! {
                clap::Arg::new(#name)
                #long
                #short
                #flag
                #help
                #long_help
                #help_heading
                .required(false)
            }
        })
    }
}

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

impl ExtractedAttributes {
    fn deserializer(&self) -> TokenStream {
        match &self.deserializer {
            None => quote! {
                let value = if value.is_empty() {
                    "\"\"".to_string()
                } else {
                    value
                };
                ::config_manager::__private::deser_hjson::from_str(&value)
            },
            Some(deser_fn) => {
                let deser_fn = deser_fn.trim_matches('\"');
                format_to_tokens!("({deser_fn})(&value)")
            }
        }
    }

    fn gen_err(&self, field_name: &str) -> TokenStream {
        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! {
            ::config_manager::Error::MissingArgument(#err.to_string())
        }
    }

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

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

    pub(super) fn gen_init(&self, field_name: &str) -> TokenStream {
        if self.variables.is_empty() && self.default.is_none() {
            panic!("No source is set for the {field_name} field");
        }
        let default_initialization = match &self.default {
            None => quote!(::std::option::Option::None),
            Some(d) if d.inner.is_none() => quote!(::std::option::Option::Some(
                ::std::default::Default::default()
            )),
            Some(d) => {
                format_to_tokens!("::std::option::Option::Some({})", d.inner.clone().unwrap())
            }
        };
        let deserializer = self.deserializer();
        let rest = self.gen_rest_init(field_name);
        let missing_err = self.gen_err(field_name);

        quote! {
            (|| -> ::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 gen_init(&self, field_name: &str) -> TokenStream {
        match &self {
            Self::Env(env) => {
                format_to_tokens!(
                    "env_data.get(&({}) as \
                     &::std::primitive::str).map(::std::string::ToString::to_string)",
                    env.prefixed_name(field_name)
                )
            }
            Self::Config(cfg) => {
                format_to_tokens!(
                    "::config_manager::__private::find_field_in_table(&config_file_data, {}, \
                     {}.to_string())?",
                    cfg.table(),
                    cfg.key(field_name)
                )
            }
            Self::Clap(clap) => {
                format_to_tokens!(
                    "clap_data.get_one::<::std::string::String>({}).\
                     map(::std::string::ToString::to_string)",
                    clap.normal_long(field_name)
                )
            }
        }
    }
}

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

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

impl Env {
    fn prefixed_name(&self, field_name: &str) -> String {
        let env_attribute = match &self.inner {
            None => quote!(::std::option::Option::<&::std::primitive::str>::None),
            Some(value) => {
                format_to_tokens!("::std::option::Option::<&::std::primitive::str>::Some({value})")
            }
        };
        let binary_name = binary_name();
        let field_name_lowercase = field_name.to_lowercase();

        quote! {
            {
                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()
            }
        }
        .to_string()
    }
}

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

impl Config {
    fn key(&self, field_name: &str) -> String {
        self.key
            .clone()
            .unwrap_or_else(|| format!("\"{field_name}\""))
    }
    fn table(&self) -> String {
        self.table
            .clone()
            .map(|table| format!("::std::option::Option::Some(\"{table}\".to_string())"))
            .unwrap_or_else(|| "::std::option::Option::None".to_string())
    }
}

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

pub(super) fn extract_attributes(
    field: Field,
    table_name: &Option<String>,
) -> 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 field_name = field.ident.expect("Unnamed fields are forbidden");

    let mut res = ExtractedAttributes::default();

    let attr = field.attrs.iter().find(|a| a.path().is_ident(SOURCE_KEY))?;

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

    for arg in nested {
        match path_to_string(arg.path()).as_str() {
            CLAP_KEY => match arg {
                Meta::Path(_) => res
                    .variables
                    .push(FieldAttribute::Clap(ClapFieldParseResult::default())),
                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!(
                        "clap attribute must match #[clap(...)] or \
                                                     #[clap]"
                    )
                }
            },
            DEFAULT => {
                if res.default.is_some() {
                    panic!("Default can be assigned only once per field")
                }
                let mut default_init = extract_default(&arg);
                if is_string {
                    default_init = default_init.map(|s| format!("::std::convert::Into::into({s})"));
                }
                res.default = Some(Default {
                    inner: default_init,
                })
            }
            ENV_KEY => res.variables.push(FieldAttribute::Env(Env {
                inner: match_literal_or_init_from(&arg, AcceptedLiterals::String)
                    .as_ref()
                    .map(InitFrom::as_string),
            })),
            CONFIG_KEY => res.variables.push(FieldAttribute::Config(Config {
                key: match_literal_or_init_from(&arg, AcceptedLiterals::String)
                    .as_ref()
                    .map(InitFrom::as_string),
                table: table_name.clone(),
            })),
            DESERIALIZER => {
                if res.deserializer.is_some() {
                    panic!(
                        "Deserialize_with can be assigned only once \
                                                     per field"
                    )
                }
                res.deserializer = match_literal_or_init_from(&arg, AcceptedLiterals::String)
                    .as_ref()
                    .map(InitFrom::as_string);
            }
            other => panic!(
                "Unknown source attribute {other} of the field \
                                             {field_name}"
            ),
        };
    }

    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)
}