use quote::quote;
use syn::parse::{Parse, ParseStream};
use syn::punctuated::Punctuated;
use syn::{Data, DeriveInput, Fields, Ident, LitStr, Token};
use crate::shared::named_field_ident;
#[derive(Default)]
pub(crate) struct RuleAttrs {
title: Option<LitStr>,
characters: Option<LitStr>,
style: Option<LitStr>,
align: Option<LitStr>,
end: Option<LitStr>,
}
pub(crate) struct RuleAttr {
pub(crate) key: Ident,
pub(crate) value: RuleAttrValue,
}
pub(crate) enum RuleAttrValue {
Str(LitStr),
}
impl Parse for RuleAttr {
fn parse(input: ParseStream) -> syn::Result<Self> {
let key: Ident = input.parse()?;
if input.peek(Token![=]) {
let _eq: Token![=] = input.parse()?;
if input.peek(LitStr) {
let lit: LitStr = input.parse()?;
Ok(RuleAttr {
key,
value: RuleAttrValue::Str(lit),
})
} else {
Err(input.error("expected string literal"))
}
} else {
Err(input.error("expected `= \"...\"`"))
}
}
}
pub(crate) fn parse_rule_attrs(input: &DeriveInput) -> syn::Result<RuleAttrs> {
let mut attrs = RuleAttrs::default();
for attr in &input.attrs {
if !attr.path().is_ident("rule") {
continue;
}
let items: Punctuated<RuleAttr, Token![,]> =
attr.parse_args_with(Punctuated::parse_terminated)?;
for item in items {
let key_str = item.key.to_string();
match key_str.as_str() {
"title" => {
attrs.title = Some(rule_expect_str(&item, "title")?);
}
"characters" => {
attrs.characters = Some(rule_expect_str(&item, "characters")?);
}
"style" => {
attrs.style = Some(rule_expect_str(&item, "style")?);
}
"align" => {
attrs.align = Some(rule_expect_str(&item, "align")?);
}
"end" => {
attrs.end = Some(rule_expect_str(&item, "end")?);
}
_ => {
return Err(syn::Error::new_spanned(
&item.key,
format!("unknown rule attribute `{}`", key_str),
));
}
}
}
}
Ok(attrs)
}
pub(crate) fn rule_expect_str(attr: &RuleAttr, _name: &str) -> syn::Result<LitStr> {
match &attr.value {
RuleAttrValue::Str(s) => Ok(s.clone()),
}
}
fn align_tokens(lit: &LitStr) -> syn::Result<proc_macro2::TokenStream> {
let val = lit.value();
match val.as_str() {
"left" => Ok(quote! { gilt::align_widget::HorizontalAlign::Left }),
"center" => Ok(quote! { gilt::align_widget::HorizontalAlign::Center }),
"right" => Ok(quote! { gilt::align_widget::HorizontalAlign::Right }),
other => Err(syn::Error::new_spanned(
lit,
format!("unknown align `{other}`. Expected one of: left, center, right"),
)),
}
}
pub(crate) fn has_rule_title_attr(field: &syn::Field) -> syn::Result<bool> {
for attr in &field.attrs {
if !attr.path().is_ident("rule") {
continue;
}
let ident: Ident = attr.parse_args()?;
if ident == "title" {
return Ok(true);
}
return Err(syn::Error::new_spanned(
&ident,
format!("unknown rule field attribute `{}`. Expected: title", ident),
));
}
Ok(false)
}
pub(crate) fn derive_rule_impl(input: &DeriveInput) -> syn::Result<proc_macro2::TokenStream> {
let struct_name = &input.ident;
let struct_name_str = struct_name.to_string();
let fields = match &input.data {
Data::Struct(data_struct) => match &data_struct.fields {
Fields::Named(named) => &named.named,
Fields::Unnamed(_) => {
return Err(syn::Error::new_spanned(
struct_name,
"Rule derive only supports structs with named fields",
));
}
Fields::Unit => {
return Err(syn::Error::new_spanned(
struct_name,
"Rule derive does not support unit structs",
));
}
},
Data::Enum(_) => {
return Err(syn::Error::new_spanned(
struct_name,
"Rule derive does not support enums",
));
}
Data::Union(_) => {
return Err(syn::Error::new_spanned(
struct_name,
"Rule derive does not support unions",
));
}
};
let rule_attrs = parse_rule_attrs(input)?;
let mut title_field: Option<Ident> = None;
for field in fields.iter() {
let ident = named_field_ident(field)?.clone();
if has_rule_title_attr(field)? {
if title_field.is_some() {
return Err(syn::Error::new_spanned(
&ident,
"only one field may be annotated with `#[rule(title)]`",
));
}
title_field = Some(ident);
}
}
let title_expr = if let Some(ref field_ident) = title_field {
quote! { self.#field_ident.to_string() }
} else if let Some(ref lit) = rule_attrs.title {
let val = lit.value();
quote! { #val.to_string() }
} else {
quote! { #struct_name_str.to_string() }
};
let mut rule_config = Vec::new();
if let Some(ref lit) = rule_attrs.characters {
let val = lit.value();
rule_config.push(quote! {
rule = rule.with_characters(#val);
});
}
if let Some(ref lit) = rule_attrs.style {
let val = lit.value();
rule_config.push(quote! {
rule = rule.with_style(gilt::style::Style::parse(#val).unwrap_or_else(|_| gilt::style::Style::null()));
});
}
if let Some(ref lit) = rule_attrs.align {
let align_ts = align_tokens(lit)?;
rule_config.push(quote! {
rule = rule.with_align(#align_ts);
});
}
if let Some(ref lit) = rule_attrs.end {
let val = lit.value();
rule_config.push(quote! {
rule = rule.with_end(#val);
});
}
let expanded = quote! {
impl #struct_name {
pub fn to_rule(&self) -> gilt::rule::Rule {
let title_text = #title_expr;
let mut rule = gilt::rule::Rule::with_title(&title_text);
#(#rule_config)*
rule
}
}
};
Ok(expanded)
}