use bae::FromAttributes;
use quote::{format_ident, quote};
use std::collections::{HashMap, HashSet};
use syn::{
parenthesized,
parse::{Parse, ParseStream},
parse_macro_input,
punctuated::Punctuated,
token, Data, DeriveInput, Fields, Ident, LitStr, Token,
};
#[derive(FromAttributes, Default, Debug)]
struct Dynamic {
updated_prefix: Option<LitStr>,
updated_suffix: Option<LitStr>,
setter_prefix: Option<LitStr>,
setter_suffix: Option<LitStr>,
update_prefix: Option<LitStr>,
update_suffix: Option<LitStr>,
}
struct DynamicField {
_paren_token: token::Paren,
dependencies: Punctuated<Ident, Token![,]>,
_comma: Token![,],
method_name: Ident,
}
impl Parse for DynamicField {
fn parse(input: ParseStream) -> syn::Result<Self> {
let content;
Ok(DynamicField {
_paren_token: parenthesized!(content in input),
dependencies: content.parse_terminated(Ident::parse)?,
_comma: input.parse()?,
method_name: input.parse()?,
})
}
}
const DYNAMIC_ATTR_NAME: &str = "dynamic";
const DEFAULT_UPDATED_METHOD_PREFIX: &str = "updated_";
const DEFAULT_UPDATED_METHOD_SUFFIX: &str = "";
const DEFAULT_UPDATE_METHOD_PREFIX: &str = "update_";
const DEFAULT_UPDATE_METHOD_SUFFIX: &str = "";
const DEFAULT_SETTER_METHOD_PREFIX: &str = "update_";
const DEFAULT_SETTER_METHOD_SUFFIX: &str = "";
fn create_ident(ident: &Ident, prefix: &str, suffix: &str) -> Ident {
format_ident!("{}{}{}", prefix, ident, suffix)
}
#[proc_macro_derive(Dynamic, attributes(dynamic))]
pub fn derive_dynamic(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
let DeriveInput {
ident, data, attrs, ..
} = parse_macro_input!(input);
let config = Dynamic::try_from_attributes(&attrs)
.unwrap()
.unwrap_or_default();
let updated_method_prefix = config
.updated_prefix
.as_ref()
.map(|prefix| prefix.value())
.unwrap_or_else(|| DEFAULT_UPDATED_METHOD_PREFIX.to_string());
let updated_method_suffix = config
.updated_suffix
.as_ref()
.map(|prefix| prefix.value())
.unwrap_or_else(|| DEFAULT_UPDATED_METHOD_SUFFIX.to_string());
let setter_method_prefix = config
.setter_prefix
.as_ref()
.map(|prefix| prefix.value())
.unwrap_or_else(|| DEFAULT_SETTER_METHOD_PREFIX.to_string());
let setter_method_suffix = config
.setter_suffix
.as_ref()
.map(|prefix| prefix.value())
.unwrap_or_else(|| DEFAULT_SETTER_METHOD_SUFFIX.to_string());
let update_method_prefix = config
.update_prefix
.as_ref()
.map(|prefix| prefix.value())
.unwrap_or_else(|| DEFAULT_UPDATE_METHOD_PREFIX.to_string());
let update_method_suffix = config
.update_suffix
.as_ref()
.map(|prefix| prefix.value())
.unwrap_or_else(|| DEFAULT_UPDATE_METHOD_SUFFIX.to_string());
let create_updated_ident =
|ident: &Ident| create_ident(ident, &updated_method_prefix, &updated_method_suffix);
let create_setter_ident = |ident: &Ident| -> Ident {
create_ident(ident, &setter_method_prefix, &setter_method_suffix)
};
let create_update_ident = |ident: &Ident| -> Ident {
create_ident(ident, &update_method_prefix, &update_method_suffix)
};
let fields = match data {
Data::Struct(data_struct) => match data_struct.fields {
Fields::Named(fields) => fields.named,
_ => panic!("Only structs with named fields currently supported!"),
},
_ => panic!("Only structs currently supported!"),
};
let (dynamic_fields, non_dynamic_fields): (Vec<_>, Vec<_>) = fields
.iter()
.map(|field| {
let dynamic = field
.attrs
.iter()
.find(|attr| {
attr.path
.get_ident()
.filter(|item| *item == DYNAMIC_ATTR_NAME)
.is_some()
})
.map(|attr| {
attr.parse_args::<DynamicField>()
.expect("Dynamic attribute format is invalid")
});
(field, dynamic)
})
.partition(|(_, dynamic)| dynamic.is_some());
let mut inv_map: HashMap<&Ident, HashSet<&Ident>> = HashMap::new();
dynamic_fields.iter().for_each(|(field, dynamic)| {
let field_name = field.ident.as_ref().unwrap();
dynamic
.as_ref()
.unwrap()
.dependencies
.iter()
.for_each(|dependency| {
inv_map
.entry(dependency)
.and_modify(|impacts| {
impacts.insert(field_name);
})
.or_insert_with(|| HashSet::from([field_name]));
});
});
let updated_methods = fields.iter().map(|field| {
let field_name = field.ident.as_ref().unwrap();
let func_name = create_updated_ident(field_name);
let deps = inv_map
.remove(field_name)
.unwrap_or_default()
.into_iter()
.map(create_update_ident);
quote! {
#[inline]
pub fn #func_name(&mut self) {
#(
self.#deps();
)*
}
}
});
let setter_methods = non_dynamic_fields.iter().map(|(field, _)| {
let field_name = field.ident.as_ref().unwrap();
let func_name = create_setter_ident(field_name);
let updated_func_name = create_updated_ident(field_name);
let typ = &field.ty;
quote! {
#[inline]
pub fn #func_name(&mut self, value: #typ) {
self.#field_name = value;
self.#updated_func_name();
}
}
});
let update_methods = dynamic_fields.iter().map(|(field, dynamic)| {
let field_name = field.ident.as_ref().unwrap();
let func_name = create_update_ident(field_name);
let updated_func_name = create_updated_ident(field_name);
let callable_name = &dynamic.as_ref().unwrap().method_name;
quote! {
#[inline]
pub fn #func_name(&mut self) {
self.#callable_name();
self.#updated_func_name();
}
}
});
let output = quote! {
impl #ident {
#(
#updated_methods
)*
#(
#setter_methods
)*
#(
#update_methods
)*
}
};
output.into()
}