use proc_macro2::TokenStream as TokenStream2;
use quote::ToTokens;
use syn::{spanned::Spanned, Field, Fields, Ident};
enum A2SAttribute {
Builder(BuilderAttribute),
Rename(syn::Lit),
}
#[derive(Clone)]
pub enum BuilderAttribute {
None,
Forward,
Skip,
SkipIf(TokenStream2),
Quoted,
Display,
Debug,
Callback(TokenStream2),
Delegate(syn::Ident),
Option(syn::Lit),
List(Option<TokenStream2>),
}
pub fn generate_builder_methods(
fields: &Fields,
is_enum: bool,
) -> Result<FieldsToBuild<TokenStream2>, syn::Error> {
let mut tokens = vec![];
let fields = annotate_fields(fields)?;
let fields = match fields {
FieldsToBuild::Fields(fields) => fields,
FieldsToBuild::Forward(field) => {
let field_name = if let Some(index) = field.index {
if is_enum {
Ident::new(&format!("operand{}", index), field.field.span()).into_token_stream()
} else {
let index = syn::Index::from(index);
quote! { #index }
}
} else {
field.field.ident.unwrap().into_token_stream()
};
return Ok(FieldsToBuild::Forward(quote! {
#field_name.ast_to_str_impl(__symbols)
}));
}
};
for field in fields {
if matches!(field.attr, BuilderAttribute::Skip) {
continue;
}
let (mut builder_name, name) = match field.index {
Some(index) => (
Ident::new(&format!("field{}", index)[..], field.field.span()),
if is_enum {
Ident::new(&format!("operand{}", index), field.field.span()).into_token_stream()
} else {
let index = syn::Index::from(index);
quote! { #index }
},
),
None => (
field.field.ident.clone().unwrap(),
field.field.ident.clone().unwrap().into_token_stream(),
),
};
if let Some(rename_as) = field.rename_as {
builder_name = match rename_as {
syn::Lit::Str(value) => syn::Ident::new(&value.value()[..], field.field.span()),
_ => unreachable!(),
}
}
let accessor = if !is_enum {
quote! { &self.#name }
} else {
quote! { #name }
};
match field.attr {
BuilderAttribute::None => {
tokens.push(quote! {
builder.field(stringify!(#builder_name), #accessor)
});
}
BuilderAttribute::Quoted => {
tokens.push(quote! {
builder.quoted(stringify!(#builder_name), #accessor)
});
}
BuilderAttribute::Display => {
tokens.push(quote! {
builder.display(stringify!(#builder_name), #accessor)
});
}
BuilderAttribute::Debug => {
tokens.push(quote! {
builder.debug(stringify!(#builder_name), #accessor)
});
}
BuilderAttribute::SkipIf(condition) => {
tokens.push(quote! {
if (#condition)(#accessor) {
builder
} else {
builder.field(stringify!(#builder_name), #accessor)
}
});
}
BuilderAttribute::Callback(cb) => {
tokens.push(quote! {
builder.display(stringify!(#builder_name), (#cb)( #accessor))
});
}
BuilderAttribute::Delegate(method_name) => tokens.push(quote! {
builder.field(stringify!(#builder_name), &self.#method_name())
}),
BuilderAttribute::Option(default) => {
tokens.push(quote! {
builder.option(stringify!(#builder_name), #default, #accessor)
});
}
BuilderAttribute::List(Some(cb)) => {
tokens.push(quote! {
builder.list_map(stringify!(#builder_name), #accessor, #cb)
});
}
BuilderAttribute::List(None) => {
tokens.push(quote! {
builder.list(stringify!(#builder_name), #accessor)
});
}
BuilderAttribute::Skip | BuilderAttribute::Forward => unreachable!(),
}
}
Ok(FieldsToBuild::Fields(tokens))
}
struct AnnotatedField {
pub field: Field,
pub index: Option<usize>,
pub attr: BuilderAttribute,
pub rename_as: Option<syn::Lit>,
}
pub enum FieldsToBuild<T> {
Fields(Vec<T>),
Forward(T),
}
fn annotate_fields(given_fields: &Fields) -> Result<FieldsToBuild<AnnotatedField>, syn::Error> {
let mut forward_field = None;
let mut fields = Vec::with_capacity(given_fields.len());
let mut name_idx = None;
for field in given_fields {
let mut field_attr = BuilderAttribute::None;
let mut rename_as = None;
if field.ident.is_none() {
name_idx = name_idx.or(Some(0)).map(|n| n + 1);
}
for attr in &field.attrs {
let new_attr = match get_attribute(attr)? {
A2SAttribute::Builder(attr) => attr,
A2SAttribute::Rename(value) => {
rename_as = Some(value);
continue;
}
};
if matches!(new_attr, BuilderAttribute::None) {
continue;
} else {
if !matches!(field_attr, BuilderAttribute::None) {
return Err(syn::Error::new(
field.span(),
"Can only have a single formatting attribute",
));
}
field_attr = new_attr;
}
if let BuilderAttribute::Forward = field_attr {
if forward_field.is_some() {
return Err(syn::Error::new(
field.span(),
"Can only have one #[forward] attribute",
));
}
forward_field = Some(AnnotatedField {
field: field.clone(),
index: name_idx.map(|n| n - 1),
attr: BuilderAttribute::Forward,
rename_as: None,
});
}
}
fields.push(AnnotatedField {
field: field.clone(),
index: name_idx.map(|n| n - 1),
attr: field_attr,
rename_as,
});
}
Ok(if let Some(field) = forward_field {
FieldsToBuild::Forward(field)
} else {
FieldsToBuild::Fields(fields)
})
}
pub fn extract_rename_ident(attrs: &[syn::Attribute]) -> Option<String> {
attrs.iter().find_map(|attr| {
get_attribute(attr).ok().and_then(|r| match r {
A2SAttribute::Builder(_) => None,
A2SAttribute::Rename(name) => match name {
syn::Lit::Str(s) => Some(s.value()),
_ => unreachable!(),
},
})
})
}
fn get_attribute(attr: &syn::Attribute) -> Result<A2SAttribute, syn::Error> {
let ident = attr
.path
.segments
.first()
.expect("Attempted to parse an empty (?) attribute")
.ident
.to_string();
let ba = match &ident[..] {
"forward" => BuilderAttribute::Forward,
"skip" => BuilderAttribute::Skip,
"debug" => BuilderAttribute::Debug,
"quoted" => BuilderAttribute::Quoted,
"display" => BuilderAttribute::Display,
"callback" => BuilderAttribute::Callback(attr.tokens.clone()),
"skip_if" => BuilderAttribute::SkipIf({
match parse_key_value_attr(attr)? {
syn::Lit::Str(s) => match s.value().parse::<TokenStream2>() {
Ok(tokens) => tokens,
Err(e) => {
return Err(syn::Error::new(
s.span(),
&format!("Failed to parse the skip_if condition: {}", e),
))
}
},
rest => {
return Err(syn::Error::new(
rest.span(),
"The skip_if condition must be a specified as a string literal.",
))
}
}
}),
"delegate" => BuilderAttribute::Delegate({
match parse_key_value_attr(attr)? {
syn::Lit::Str(s) => Ident::new(&s.value(), s.span()),
rest => {
return Err(syn::Error::new(
rest.span(),
"The delegate method name must be a string.",
))
}
}
}),
"default" => BuilderAttribute::Option(parse_key_value_attr(attr)?),
"rename" => return Ok(A2SAttribute::Rename(parse_key_value_attr(attr)?)),
"list" => {
if attr.tokens.is_empty() {
BuilderAttribute::List(None)
} else {
BuilderAttribute::List(Some(attr.tokens.clone()))
}
}
_ => BuilderAttribute::None,
};
Ok(A2SAttribute::Builder(ba))
}
fn parse_key_value_attr(attr: &syn::Attribute) -> Result<syn::Lit, syn::Error> {
Ok(
match attr
.parse_meta()
.expect("Failed to parse the contents of the attribute")
{
syn::Meta::NameValue(meta) => match meta.lit {
syn::Lit::Str(_) => meta.lit,
_ => {
return Err(syn::Error::new(
meta.span(),
"The default value must be a string literal",
))
}
},
rest => {
return Err(syn::Error::new(
rest.span(),
"Only the key-value syntax is supported: `#[default = \"a default value\"]`",
))
}
},
)
}