use core::panic;
use std::collections::HashMap;
use proc_macro2::Span as Span2;
use syn::{
Attribute,
Expr,
Field,
Fields,
Ident,
Lit,
LitStr,
MetaNameValue,
Path,
Token,
Visibility,
parse_str,
punctuated::Punctuated,
};
const CONFIG_ATTRIBUTE: &str = "configure_setter";
const DISABLE_ATTRIBUTE: &str = "disable_setters";
const NAME_PARAM: &str = "name";
const PREFIX_PARAM: &str = "prefix";
const SUFFIX_PARAM: &str = "suffix";
const VISIBILITY_PARAM: &str = "visibility";
const DEFAULT_PREFIX: &str = "with";
pub type SetterConfigs<'a> = HashMap<&'a Field, Vec<SetterConfig>>;
#[derive(Debug)]
pub struct SetterConfig {
name: String,
visibility: Visibility,
}
impl SetterConfig {
pub fn name(&self) -> &str {
&self.name
}
pub fn visibility(&self) -> &Visibility {
&self.visibility
}
}
pub fn make_setter_configs(fields: &mut Fields) -> SetterConfigs {
let fields = match fields {
Fields::Named(fields) => &mut fields.named,
_ => panic!("Macro supports only structs with named fields."),
};
let mut setter_configs: SetterConfigs = SetterConfigs::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 field_setter_configs = extract_configs(field);
remove_attributes(field);
setter_configs.insert(field, field_setter_configs);
}
setter_configs
}
fn extract_configs(field: &Field) -> Vec<SetterConfig> {
let field_ident = field.ident.as_ref().unwrap();
let attributes: Vec<&Attribute> = field
.attrs
.iter()
.filter(|attr| attr.path().is_ident(CONFIG_ATTRIBUTE))
.collect();
if attributes.is_empty() {
let name = format!("{DEFAULT_PREFIX}_{field_ident}");
let visibility = default_visibility_factory();
return vec![SetterConfig { name, visibility }];
}
let mut setter_configs: Vec<SetterConfig> = Vec::new();
for attribute in attributes {
let setter_config = extract_config(field_ident, attribute);
setter_configs.push(setter_config);
}
setter_configs
}
fn extract_config(field_ident: &Ident, attribute: &Attribute) -> SetterConfig {
let name_values: Punctuated<MetaNameValue, Token![,]> = attribute
.parse_args_with(Punctuated::parse_terminated)
.unwrap();
let mut name: Option<String> = None;
let mut prefix: Option<String> = None;
let mut suffix: Option<String> = None;
let mut raw_visibility: Option<String> = None;
for name_value in name_values {
if let Expr::Lit(lit_expr) = name_value.value {
if let Lit::Str(lit_str) = lit_expr.lit {
parse_attribute_param(
name_value.path,
lit_str,
&mut name,
&mut prefix,
&mut suffix,
&mut raw_visibility,
);
}
}
}
let name = make_name(name, prefix, suffix, field_ident);
let visibility = match raw_visibility.as_ref() {
Some(raw_visibility) => parse_str(raw_visibility).unwrap(),
None => default_visibility_factory(),
};
SetterConfig { name, visibility }
}
fn parse_attribute_param(
param_path: Path,
param_value: LitStr,
name: &mut Option<String>,
prefix: &mut Option<String>,
suffix: &mut Option<String>,
raw_visibility: &mut Option<String>,
) {
let param_name = param_path.get_ident().unwrap().to_string();
match param_name.as_str() {
NAME_PARAM => name.insert(param_value.value()),
PREFIX_PARAM => prefix.insert(param_value.value()),
SUFFIX_PARAM => suffix.insert(param_value.value()),
VISIBILITY_PARAM => raw_visibility.insert(param_value.value()),
_ => panic!("Unexpected param."),
};
}
fn make_name(
name: Option<String>,
prefix: Option<String>,
suffix: Option<String>,
field_ident: &Ident,
) -> String {
let field_name = field_ident.to_string();
match (name, prefix, suffix) {
(None, Some(prefix), Some(suffix)) => format!("{prefix}_{suffix}"),
(None, Some(prefix), None) => format!("{prefix}_{field_name}"),
(None, None, Some(suffix)) => format!("{DEFAULT_PREFIX}_{suffix}"),
(None, None, None) => format!("{DEFAULT_PREFIX}_{field_name}"),
(Some(_), Some(_), Some(_)) => panic!(
"'{NAME_PARAM}' param cannot be set with \
{PREFIX_PARAM} and {SUFFIX_PARAM} params."
),
(Some(_), Some(_), None) => panic!(
"'{NAME_PARAM}' param cannot be set with \
{PREFIX_PARAM} param."
),
(Some(_), None, Some(_)) => panic!(
"{NAME_PARAM} param cannot be set with \
{SUFFIX_PARAM} param."
),
(Some(name), None, None) => name,
}
}
fn default_visibility_factory() -> Visibility {
let span = Span2::call_site();
let pub_token = Token;
Visibility::Public(pub_token)
}
fn remove_attributes(field: &mut Field) {
field.attrs.retain(|attr| {
let path = attr.path();
!(path.is_ident(DISABLE_ATTRIBUTE) || path.is_ident(CONFIG_ATTRIBUTE))
});
}