confroid-derive 0.0.4

Derive macro for confroid.
Documentation
use syn::{Data, DeriveInput, Expr, Fields, LitStr, Token};

/// The `default` attribute: either bare (`default` -> `Default::default()`) or
/// an explicit expression (`default = expr`).
#[derive(Clone)]
pub(crate) enum DefaultKind {
    Inherit,
    Explicit(Expr),
}

pub(crate) struct FieldOpts {
    pub(crate) ident: syn::Ident,
    pub(crate) ty: syn::Type,
    pub(crate) name: Option<String>,
    pub(crate) default: Option<DefaultKind>,
    pub(crate) example: Option<Expr>,
    pub(crate) auto_vec: bool,
    pub(crate) auto_vec_delimiter: Option<String>,
    pub(crate) humantime: bool,
    pub(crate) attrs: Vec<syn::Attribute>,
}

pub(crate) struct InputOpts {
    pub(crate) ident: syn::Ident,
    pub(crate) generics: syn::Generics,
    pub(crate) fields: Vec<FieldOpts>,
    pub(crate) prefix: Option<String>,
}

impl InputOpts {
    pub(crate) fn parse(input: &DeriveInput) -> syn::Result<Self> {
        let mut prefix = None;
        for attr in input
            .attrs
            .iter()
            .filter(|attr| attr.path().is_ident("confroid"))
        {
            attr.parse_nested_meta(|meta| {
                if meta.path.is_ident("prefix") {
                    set_once(&mut prefix, parse_string(&meta)?, &meta, "prefix")
                } else {
                    Err(meta.error("unsupported container attribute"))
                }
            })?;
        }

        let Data::Struct(data) = &input.data else {
            return Err(syn::Error::new_spanned(
                input,
                "`Config` can only be derived for a struct with named fields",
            ));
        };
        let Fields::Named(fields) = &data.fields else {
            return Err(syn::Error::new_spanned(
                &data.fields,
                "`Config` requires named fields",
            ));
        };

        let fields = fields
            .named
            .iter()
            .map(FieldOpts::parse)
            .collect::<syn::Result<_>>()?;

        Ok(Self {
            ident: input.ident.clone(),
            generics: input.generics.clone(),
            fields,
            prefix,
        })
    }
}

impl FieldOpts {
    fn parse(field: &syn::Field) -> syn::Result<Self> {
        let mut opts = Self {
            ident: field.ident.clone().expect("named fields have identifiers"),
            ty: field.ty.clone(),
            name: None,
            default: None,
            example: None,
            auto_vec: false,
            auto_vec_delimiter: None,
            humantime: false,
            attrs: field.attrs.clone(),
        };

        for attr in field
            .attrs
            .iter()
            .filter(|attr| attr.path().is_ident("confroid"))
        {
            attr.parse_nested_meta(|meta| {
                if meta.path.is_ident("name") {
                    let value = parse_string(&meta)?;
                    set_once(&mut opts.name, value, &meta, "name")
                } else if meta.path.is_ident("default") {
                    let value = if meta.input.peek(Token![=]) {
                        DefaultKind::Explicit(meta.value()?.parse()?)
                    } else {
                        DefaultKind::Inherit
                    };
                    set_once(&mut opts.default, value, &meta, "default")
                } else if meta.path.is_ident("example") {
                    let value = meta.value()?.parse()?;
                    set_once(&mut opts.example, value, &meta, "example")
                } else if meta.path.is_ident("auto_vec") {
                    set_flag(&mut opts.auto_vec, &meta, "auto_vec")
                } else if meta.path.is_ident("auto_vec_delimiter") {
                    let value = parse_string(&meta)?;
                    set_once(
                        &mut opts.auto_vec_delimiter,
                        value,
                        &meta,
                        "auto_vec_delimiter",
                    )
                } else if meta.path.is_ident("humantime") {
                    set_flag(&mut opts.humantime, &meta, "humantime")
                } else {
                    Err(meta.error("unsupported field attribute"))
                }
            })?;
        }

        Ok(opts)
    }
}

fn parse_string(meta: &syn::meta::ParseNestedMeta<'_>) -> syn::Result<String> {
    Ok(meta.value()?.parse::<LitStr>()?.value())
}

fn set_once<T>(
    slot: &mut Option<T>,
    value: T,
    meta: &syn::meta::ParseNestedMeta<'_>,
    name: &str,
) -> syn::Result<()> {
    if slot.replace(value).is_some() {
        Err(meta.error(format!("duplicate `{name}` attribute")))
    } else {
        Ok(())
    }
}

fn set_flag(flag: &mut bool, meta: &syn::meta::ParseNestedMeta<'_>, name: &str) -> syn::Result<()> {
    if *flag {
        Err(meta.error(format!("duplicate `{name}` attribute")))
    } else {
        *flag = true;
        Ok(())
    }
}