useless_setter_maker 0.1.0

Macro for generating setter methods.
Documentation
use syn::{
    Expr,
    Field,
    Fields,
    Lit,
    Meta,
};

const DISABLE_ATTRIBUTE: &str = "disable_setter";
const NAME_ATTRIBUTE: &str = "setter_name";
const PREFIX_ATTRIBUTE: &str = "setter_prefix";
const SUFFIX_ATTRIBUTE: &str = "setter_suffix";
const VISIBILITY_ATTRIBUTE: &str = "setter_visibility";

const DEFAULT_PREFIX: &str = "with";
const ALLOWED_VISIBILITIES: [&str; 3] = ["pub", "pub(crate)", "private"];

pub struct SetterConfig<'a> {
    field: &'a Field,
    name: String,
    visibility: SetterVisibility,
}

#[derive(Clone, Copy)]
pub enum SetterVisibility {
    Pub,
    PubForCrate,
    Private,
}

impl TryFrom<String> for SetterVisibility {
    type Error = String;

    fn try_from(value: String) -> Result<Self, Self::Error> {
        match value.as_str() {
            "pub" => Ok(Self::Pub),
            "pub(crate)" => Ok(Self::PubForCrate),
            "private" => Ok(Self::Private),
            _ => Err(format!("Invalid visibility: '{value}'.")),
        }
    }
}

impl<'a> SetterConfig<'a> {
    pub fn field(&self) -> &Field {
        self.field
    }

    pub fn name(&self) -> &str {
        &self.name
    }

    pub fn visibility(&self) -> SetterVisibility {
        self.visibility
    }
}

pub fn make_setter_configs(fields: &mut Fields) -> Vec<SetterConfig> {
    let fields = match fields {
        Fields::Named(fields) => &mut fields.named,
        _ => panic!("`add_setters` only supports structs with named fields."),
    };
    let mut setter_configs: Vec<SetterConfig> = Vec::new();

    for field in fields {
        let is_disabled = field
            .attrs
            .iter()
            .any(|attr| attr.path().is_ident(DISABLE_ATTRIBUTE));
        if is_disabled {
            remove_attributes(field);
            continue;
        }

        let name = extract_name(field).unwrap_or_else(|| {
            let prefix =
                extract_prefix(field).unwrap_or(String::from(DEFAULT_PREFIX));
            let suffix = extract_suffix(field)
                .unwrap_or(field.ident.as_ref().unwrap().to_string());

            format!("{prefix}_{suffix}")
        });
        let visibility = extract_visibility(field)
            .map(|v| v.try_into().unwrap_or_else(|e| panic!("{}", e)))
            .unwrap_or(SetterVisibility::Pub);

        remove_attributes(field);

        let setter_config = SetterConfig {
            field: field,
            name: name,
            visibility: visibility,
        };
        setter_configs.push(setter_config);
    }

    setter_configs
}

fn extract_name(field: &Field) -> Option<String> {
    let attribute = field
        .attrs
        .iter()
        .filter(|attr| attr.path().is_ident(NAME_ATTRIBUTE))
        .last()?;

    if let Meta::NameValue(meta) = &attribute.meta {
        if let Expr::Lit(expr) = &meta.value {
            if let Lit::Str(lit_str) = &expr.lit {
                return Some(lit_str.value());
            }
        }
    }

    panic!(
        "'{0}' attribute must have #[{0} = \"<name>\"] format.",
        NAME_ATTRIBUTE,
    );
}

fn extract_prefix(field: &Field) -> Option<String> {
    let attribute = field
        .attrs
        .iter()
        .filter(|attr| attr.path().is_ident(PREFIX_ATTRIBUTE))
        .last()?;

    if let Meta::NameValue(meta) = &attribute.meta {
        if let Expr::Lit(expr) = &meta.value {
            if let Lit::Str(lit_str) = &expr.lit {
                return Some(lit_str.value());
            }
        }
    }

    panic!(
        "'{0}' attribute must have #[{0} = \"<prefix>\"] format.",
        PREFIX_ATTRIBUTE,
    );
}

fn extract_suffix(field: &Field) -> Option<String> {
    let attribute = field
        .attrs
        .iter()
        .filter(|attr| attr.path().is_ident(SUFFIX_ATTRIBUTE))
        .last()?;

    if let Meta::NameValue(meta) = &attribute.meta {
        if let Expr::Lit(expr) = &meta.value {
            if let Lit::Str(lit_str) = &expr.lit {
                return Some(lit_str.value());
            }
        }
    }

    panic!(
        "'{0}' attribute must have #[{0} = \"<suffix>\"] format.",
        SUFFIX_ATTRIBUTE,
    );
}

fn extract_visibility(field: &Field) -> Option<String> {
    let attribute = field
        .attrs
        .iter()
        .filter(|attr| attr.path().is_ident(VISIBILITY_ATTRIBUTE))
        .last()?;

    if let Meta::NameValue(meta) = &attribute.meta {
        if let Expr::Lit(expr) = &meta.value {
            if let Lit::Str(lit_str) = &expr.lit {
                return Some(lit_str.value());
            }
        }
    }

    panic!(
        "'{0}' attribute must have #[{0} = \"<visibility>\"] format, \
        where <visibility> must be one of ({1}).",
        VISIBILITY_ATTRIBUTE,
        ALLOWED_VISIBILITIES.join(", "),
    );
}

fn remove_attributes(field: &mut Field) {
    field.attrs.retain(|attr| {
        let path = attr.path();

        !(path.is_ident(DISABLE_ATTRIBUTE)
            || path.is_ident(NAME_ATTRIBUTE)
            || path.is_ident(PREFIX_ATTRIBUTE)
            || path.is_ident(SUFFIX_ATTRIBUTE)
            || path.is_ident(VISIBILITY_ATTRIBUTE))
    });
}