use proc_macro2::{Span, TokenStream};
use proc_macro_error::{abort, emit_warning, ResultExt};
use syn::punctuated::Punctuated;
use syn::{Field, Fields, Ident, Meta, MetaNameValue, Token, Type};
use quote::{format_ident, quote, ToTokens, TokenStreamExt};
use crate::parse_string_meta;
use crate::struct_gen::struct_buildable_value_gen::gen_impl_buildable_value;
mod named_fields;
mod struct_buildable_value_gen;
mod unnamed_fields;
#[derive(Debug)]
pub struct StructGenerator {
ident: Ident,
builder_ident: Ident,
fields: Vec<StructField>,
span: Span,
named_fields: bool,
metadata: StructMetadata,
}
#[derive(Debug)]
pub struct StructMetadata {
prompt: Option<String>,
rename: Option<String>,
}
#[derive(Debug)]
pub struct StructField {
pub ident: Option<Ident>,
pub ty: Type,
pub field: Field,
pub metadata: FieldMetadata,
}
#[derive(Debug)]
pub struct FieldMetadata {
pub default: Option<TokenStream>,
pub prompt: Option<String>,
pub rename: Option<String>,
pub hidden: bool,
}
struct FieldDefList<'s> {
fields: &'s [StructField],
named: bool,
}
struct FieldNewList<'s> {
gen: &'s StructGenerator,
}
struct ImplDebug<'s> {
gen: &'s StructGenerator,
}
impl StructGenerator {
pub fn gen_builder_ident(ident: &Ident) -> Ident {
format_ident!("__{}_BuildableValueImpl", ident)
}
pub fn from_struct(ast: &syn::DeriveInput) -> StructGenerator {
match &ast.data {
syn::Data::Struct(data) => {
let named_fields = matches!(data.fields, Fields::Named(_));
let metadata = StructMetadata::from(ast);
StructGenerator {
ident: ast.ident.clone(),
builder_ident: StructGenerator::gen_builder_ident(&ast.ident),
fields: match &data.fields {
syn::Fields::Named(fields) => {
fields.named.iter().map(StructField::from).collect()
}
syn::Fields::Unnamed(fields) => {
let mut fields: Vec<_> =
fields.unnamed.iter().map(StructField::from).collect();
if let Some(prompt) = &metadata.prompt {
for field in fields.iter_mut() {
if field.metadata.prompt.is_none() {
field.metadata.prompt = Some(prompt.clone());
}
}
}
fields
}
syn::Fields::Unit => vec![],
},
span: ast.ident.span(),
named_fields,
metadata,
}
}
_ => panic!("expecting a struct"),
}
}
fn actual_name(&self) -> TokenStream {
if let Some(renamed) = &self.metadata.rename {
quote! { #renamed }
} else {
let ident = self.ident.to_string();
quote! { #ident }
}
}
fn is_named(&self) -> bool {
self.named_fields
}
fn fields_def_list(&self) -> FieldDefList {
FieldDefList {
fields: &self.fields,
named: self.is_named(),
}
}
fn fields_new_list(&self) -> FieldNewList {
FieldNewList { gen: self }
}
fn impl_debug(&self) -> ImplDebug {
ImplDebug { gen: self }
}
}
impl From<&syn::DeriveInput> for StructMetadata {
fn from(data: &syn::DeriveInput) -> StructMetadata {
let mut metadata = StructMetadata {
prompt: None,
rename: None,
};
for attr in &data.attrs {
if attr.path.is_ident("ibuilder") {
let meta = attr
.parse_args_with(Punctuated::<Meta, Token![,]>::parse_terminated)
.unwrap_or_abort();
for meta in meta {
parse_struct_meta(meta, &mut metadata);
}
}
}
metadata
}
}
fn parse_struct_meta(meta: Meta, metadata: &mut StructMetadata) {
match meta {
Meta::NameValue(MetaNameValue { path, lit, .. }) => {
if path.is_ident("prompt") {
parse_string_meta(&mut metadata.prompt, lit);
} else if path.is_ident("rename") {
parse_string_meta(&mut metadata.rename, lit);
} else {
abort!(path, "unknown attribute");
}
}
_ => abort!(meta, "unknown attribute"),
}
}
impl StructField {
fn builder_type(&self) -> TokenStream {
if self.metadata.hidden {
let ty = &self.ty;
quote! { #ty }
} else if let Some(builtin) = self.builtin_type() {
quote! { #builtin }
} else {
quote! { Box<dyn ibuilder::BuildableValue> }
}
}
fn builder_new(&self) -> TokenStream {
let prompt = match &self.metadata.prompt {
Some(prompt) => quote!(Some(#prompt.to_string())),
None => quote! {None},
};
if self.metadata.hidden {
return if let Some(default) = &self.metadata.default {
quote! { #default }
} else {
quote! { ::std::default::Default::default() }
};
}
if let Some(builtin) = self.builtin_type() {
let default = if let Some(default) = self.metadata.default.clone() {
quote! { Some(#default) }
} else {
quote! { None }
};
quote! {
<#builtin>::new(ibuilder::BuildableValueConfig {
default: #default,
prompt: #prompt,
})
}
} else {
let ty = &self.ty;
quote! {
<#ty as ibuilder::NewBuildableValue>::new_buildable_value(ibuilder::BuildableValueConfig {
default: None,
prompt: #prompt,
})
}
}
}
fn builtin_type(&self) -> Option<TokenStream> {
match &self.ty {
Type::Path(path) => {
let segments = &path.path.segments;
if segments.len() != 1 {
return None;
}
let ty = segments[0].ident.to_string();
let ty = ty.as_str();
match ty {
"i8" | "i16" | "i32" | "i64" | "u8" | "u16" | "u32" | "u64" | "isize"
| "usize" | "f32" | "f64" | "String" | "char" | "bool" => {
let builder =
format_ident!("{}", ty[0..1].to_uppercase() + &ty[1..] + "Builder");
Some(quote! { ibuilder::builders::#builder })
}
_ => None,
}
}
_ => None,
}
}
fn actual_name(&self) -> TokenStream {
if let Some(renamed) = &self.metadata.rename {
quote! { #renamed }
} else {
let ident = self.ident.as_ref().unwrap().to_string();
quote! { #ident }
}
}
}
impl ToTokens for StructGenerator {
fn to_tokens(&self, tokens: &mut TokenStream) {
tokens.append_all(gen_struct_builder(self));
tokens.append_all(gen_impl_new_buildable_value(self));
tokens.append_all(gen_impl_buildable_value(self));
}
}
impl<'s> ToTokens for FieldDefList<'s> {
fn to_tokens(&self, tokens: &mut TokenStream) {
if self.fields.is_empty() {
tokens.append_all(quote! { ; });
return;
}
let mut inner = TokenStream::new();
for field in self.fields {
if let Some(ident) = &field.ident {
inner.append_all(quote! {#ident: });
}
let ty = field.builder_type();
inner.append_all(quote! {#ty,})
}
if self.named {
inner.append_all(quote! { __prompt: String, });
tokens.append_all(quote! { { #inner } });
} else {
tokens.append_all(quote! { ( #inner ); });
}
}
}
impl<'s> ToTokens for FieldNewList<'s> {
fn to_tokens(&self, tokens: &mut TokenStream) {
if self.gen.fields.is_empty() {
tokens.append_all(quote! {});
return;
}
let prompt = &self.gen.metadata.prompt.as_deref();
let prompt = prompt.unwrap_or("Select the field to edit");
let prompt = quote! { config.prompt.unwrap_or_else(|| #prompt.to_string()) };
let mut inner = TokenStream::new();
for field in &self.gen.fields {
if let Some(ident) = &field.ident {
inner.append_all(quote! {#ident: });
}
let init = field.builder_new();
inner.append_all(quote! {#init,})
}
if self.gen.is_named() {
inner.append_all(quote! { __prompt: #prompt, });
tokens.append_all(quote! { { #inner } });
} else {
tokens.append_all(quote! { ( #inner ) });
}
}
}
impl<'s> ToTokens for ImplDebug<'s> {
#[allow(clippy::collapsible_else_if)]
fn to_tokens(&self, tokens: &mut TokenStream) {
let builder_ident = &self.gen.builder_ident;
let mut fields = TokenStream::new();
for (i, field) in self.gen.fields.iter().enumerate() {
if let Some(ident) = &field.ident {
if field.metadata.hidden {
fields.append_all(quote! { .field(stringify!(#ident), &"[hidden]") });
} else {
fields.append_all(quote! { .field(stringify!(#ident), &self.#ident) });
}
} else {
if field.metadata.hidden {
fields.append_all(quote! { .field(stringify!(#i), &"[hidden]") });
} else {
let index = syn::Index::from(i);
fields.append_all(quote! { .field(stringify!(#i), &self.#index) });
}
}
}
tokens.append_all(quote! {
#[automatically_derived]
impl std::fmt::Debug for #builder_ident {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct(stringify!(#builder_ident))
#fields
.finish()
}
}
})
}
}
impl From<&Field> for StructField {
fn from(field: &Field) -> StructField {
let res = StructField {
ident: field.ident.clone(),
ty: field.ty.clone(),
field: field.clone(),
metadata: get_field_metadata(field),
};
if res.metadata.default.is_some() && res.builtin_type().is_none() {
abort!(field, "default value is supported only on plain types");
}
res
}
}
fn get_field_metadata(field: &Field) -> FieldMetadata {
let mut metadata = FieldMetadata {
default: None,
prompt: None,
rename: None,
hidden: false,
};
for attr in &field.attrs {
if attr.path.is_ident("ibuilder") {
let meta = attr
.parse_args_with(Punctuated::<Meta, Token![,]>::parse_terminated)
.unwrap_or_abort();
for meta in meta {
parse_field_meta(meta, &mut metadata, &field.ty);
}
}
}
if metadata.hidden && field.ident.is_none() {
abort!(field, "unnamed fields cannot be hidden");
}
metadata
}
fn parse_field_meta(meta: Meta, metadata: &mut FieldMetadata, ty: &Type) {
match meta {
Meta::NameValue(MetaNameValue { path, lit, .. }) => {
if path.is_ident("default") {
if metadata.default.is_none() {
match lit {
syn::Lit::Str(_) => {
metadata.default =
Some(quote! { <#ty as std::str::FromStr>::from_str(#lit).unwrap() })
}
_ => metadata.default = Some(quote! { #lit }),
}
} else {
abort!(path, "duplicated default");
}
} else if path.is_ident("prompt") {
parse_string_meta(&mut metadata.prompt, lit);
} else if path.is_ident("rename") {
parse_string_meta(&mut metadata.rename, lit);
} else {
abort!(path, "unknown attribute");
}
}
Meta::Path(path) => {
if path.is_ident("hidden") {
if metadata.hidden {
emit_warning!(path, "duplicated attribute");
}
metadata.hidden = true;
} else {
abort!(path, "unknown attribute");
}
}
_ => abort!(meta, "unknown attribute"),
}
}
fn gen_struct_builder(gen: &StructGenerator) -> TokenStream {
let builder_ident = &gen.builder_ident;
let fields_gen = gen.fields_def_list();
let fields_new = gen.fields_new_list();
let impl_debug = gen.impl_debug();
quote! {
#[automatically_derived]
#[allow(non_camel_case_types)]
#[doc(hidden)]
struct #builder_ident #fields_gen
#impl_debug
#[automatically_derived]
#[allow(clippy::unnecessary_cast)]
impl #builder_ident {
fn new(config: ibuilder::BuildableValueConfig<()>) -> #builder_ident {
#builder_ident #fields_new
}
}
}
}
fn gen_impl_new_buildable_value(gen: &StructGenerator) -> TokenStream {
let ident = &gen.ident;
let builder_ident = &gen.builder_ident;
quote! {
#[automatically_derived]
impl ibuilder::NewBuildableValue for #ident {
fn new_buildable_value(config: ibuilder::BuildableValueConfig<()>) -> Box<dyn ibuilder::BuildableValue> {
Box::new(#builder_ident::new(config))
}
}
}
}