altgetset 0.1.0

An alternative getters/setters proc-macro library.
Documentation
use proc_macro2::Span;
use proc_macro_error2::abort;
use syn::{
    parse_str, punctuated::Punctuated, spanned::Spanned, Attribute, Expr, Lit, Meta, MetaNameValue,
    Token, Visibility,
};

use crate::Mode;

pub(crate) fn expr_str(expr: &Expr) -> Option<String> {
    if let Expr::Lit(s) = expr {
        if let Lit::Str(s) = &s.lit {
            return Some(s.value());
        }
    }

    None
}

pub(crate) fn parse_vis(vis: &str, span: Span) -> Visibility {
    match parse_str(vis) {
        Ok(vis) => vis,
        Err(e) => abort!(span, "invalid visibility found {}", e),
    }
}

pub(crate) fn parse_global_attr(attrs: &[Attribute], mode: Mode) -> Option<Meta> {
    attrs
        .iter()
        .filter_map(|attr| parse_attr(attr, mode))
        .last()
}

pub(crate) fn parse_attr(attr: &Attribute, mode: Mode) -> Option<Meta> {
    if attr.path().is_ident("getset") {
        let meta_list = match attr.parse_args_with(Punctuated::<Meta, Token![,]>::parse_terminated)
        {
            Ok(list) => list,
            Err(e) => abort!(attr.span(), "Failed to parse getset attribute: {}", e),
        };

        let (last, skip, mut coll) = meta_list
            .into_iter()
            .inspect(|meta| {
                let known_attr_ident = ["get", "set", "get_mut", "get_clone", "skip"];

                if known_attr_ident
                    .into_iter()
                    .all(|attr| !meta.path().is_ident(attr))
                {
                    abort!(meta.path().span(), "unknown getter / setter attribute")
                }
            })
            .fold(
                (None, None, Vec::new()),
                |(last, skip, mut collected), meta| {
                    if meta.path().is_ident(mode.name()) {
                        (Some(meta), skip, collected)
                    } else if meta.path().is_ident("skip") {
                        (last, Some(meta), collected)
                    } else {
                        collected.push(meta);
                        (last, skip, collected)
                    }
                },
            );

        if let Some(skip) = skip {
            if last.is_none() && coll.is_empty() {
                return Some(skip);
            } else {
                abort!(
                    last.or_else(|| coll.pop()).unwrap().path().span(),
                    "use of getters / setters with skip is invalid"
                );
            }
        } else {
            return last;
        }
    } else if attr.path().is_ident(mode.name()) {
        return attr.meta.clone().into();
    }

    None
}

pub(crate) fn parse_visibility(attr: Option<&Meta>, meta_name: &str) -> Option<Visibility> {
    let meta = attr?;

    let Meta::NameValue(MetaNameValue { value, path, .. }) = meta else {
        return None;
    };

    if !path.is_ident(meta_name) {
        return None;
    }

    let valstr = expr_str(value)?;
    Some(parse_vis(&valstr, value.span()))
}