use proc_macro2::TokenStream;
use proc_macro_error::{abort, emit_warning, ResultExt};
use syn::punctuated::Punctuated;
use syn::{Fields, Ident, Meta, MetaNameValue, Token, Variant};
use quote::{format_ident, quote, ToTokens, TokenStreamExt};
use crate::enum_gen::enum_buildable_value_gen::gen_impl_buildable_value;
use crate::parse_string_meta;
use crate::struct_gen::{StructField, StructGenerator};
mod enum_buildable_value_gen;
#[derive(Debug)]
pub struct EnumGenerator {
ident: Ident,
builder_ident: Ident,
variants_builder_ident: Ident,
variants: Vec<EnumVariant>,
metadata: EnumMetadata,
}
#[derive(Debug)]
pub struct EnumMetadata {
prompt: Option<String>,
}
#[derive(Debug)]
pub struct EnumVariant {
ident: Ident,
kind: VariantKind,
metadata: VariantMetadata,
}
#[derive(Debug)]
pub struct VariantMetadata {
prompt: Option<String>,
rename: Option<String>,
hidden: bool,
default: bool,
}
#[derive(Debug)]
pub enum VariantKind {
Empty,
Unnamed(Vec<StructField>),
Named(Vec<StructField>),
}
struct VariantsDefList<'s> {
gen: &'s EnumGenerator,
}
impl EnumGenerator {
pub fn from_enum(ast: &syn::DeriveInput) -> EnumGenerator {
match &ast.data {
syn::Data::Enum(data) => {
let generator = EnumGenerator {
ident: ast.ident.clone(),
builder_ident: gen_builder_ident(&ast.ident),
variants_builder_ident: gen_variants_builder_ident(&ast.ident),
variants: data.variants.iter().map(EnumVariant::from).collect(),
metadata: EnumMetadata::from(ast),
};
if generator.variants.iter().all(|v| v.metadata.hidden) {
abort!(ast, "all the variants are hidden");
}
if generator
.variants
.iter()
.filter(|v| v.metadata.default)
.count()
> 1
{
abort!(ast, "at most one variant can be the default");
}
generator
}
_ => panic!("expecting an enum"),
}
}
fn variants_def_list(&self) -> VariantsDefList {
VariantsDefList { gen: self }
}
}
impl EnumVariant {
fn gen_builder(&self, ident: Ident) -> TokenStream {
let name = self.actual_name();
let mut attrs = Vec::new();
if let Some(prompt) = &self.metadata.prompt {
attrs.push(quote! { prompt = #prompt });
}
attrs.push(quote! { rename = #name });
let fields_def = match &self.kind {
VariantKind::Empty => return TokenStream::new(),
VariantKind::Unnamed(fields) => {
let fields: Vec<_> = fields.iter().map(|f| &f.field).collect();
if fields.len() != 1 {
abort!(
self.ident,
"variants with unnamed fields are supported only with one field"
);
}
quote! { (#(#fields,)*); }
}
VariantKind::Named(fields) => {
let fields: Vec<_> = fields.iter().map(|f| &f.field).collect();
quote! { { #(#fields,)* } }
}
};
quote! {
#[allow(non_camel_case_types)]
#[derive(IBuilder)]
#[ibuilder(#(#attrs,)*)]
struct #ident #fields_def
}
}
fn builder_new(&self, base: &Ident) -> TokenStream {
let variant = &self.ident;
let builder = gen_variants_builder_ident(base);
let variant_builder = gen_variants_builder_variant_ident(base, variant);
let variant_builder = StructGenerator::gen_builder_ident(&variant_builder);
match &self.kind {
VariantKind::Empty => quote! { #builder::#variant },
VariantKind::Unnamed(_) | VariantKind::Named(_) => {
let prompt = match &self.metadata.prompt {
Some(prompt) => quote! {Some(#prompt.into())},
None => quote! {None},
};
quote! {
#builder::#variant(#variant_builder::new(ibuilder::BuildableValueConfig {
default: None,
prompt: #prompt,
}))
}
}
}
}
fn field_names(&self) -> Vec<Ident> {
match &self.kind {
VariantKind::Unnamed(_) | VariantKind::Empty => vec![],
VariantKind::Named(fields) => fields.iter().map(|f| f.ident.clone().unwrap()).collect(),
}
}
fn actual_name(&self) -> TokenStream {
if let Some(renamed) = &self.metadata.rename {
quote! { #renamed }
} else {
let ident = self.ident.to_string();
quote! { #ident }
}
}
}
impl From<&syn::DeriveInput> for EnumMetadata {
fn from(data: &syn::DeriveInput) -> EnumMetadata {
let mut metadata = EnumMetadata { prompt: 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_enum_meta(meta, &mut metadata);
}
}
}
metadata
}
}
fn parse_enum_meta(meta: Meta, metadata: &mut EnumMetadata) {
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") {
abort!(
path,
"renaming an enum is not supported since the name is not exposed"
);
} else {
abort!(path, "unknown attribute");
}
}
_ => abort!(meta, "unknown attribute"),
}
}
impl VariantKind {
fn is_empty(&self) -> bool {
matches!(self, VariantKind::Empty)
}
}
impl From<&Variant> for EnumVariant {
fn from(variant: &Variant) -> EnumVariant {
let metadata = VariantMetadata::from(variant);
EnumVariant {
ident: variant.ident.clone(),
kind: match &variant.fields {
Fields::Named(fields) => {
VariantKind::Named(fields.named.iter().map(StructField::from).collect())
}
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());
}
}
}
VariantKind::Unnamed(fields)
}
Fields::Unit => {
if metadata.prompt.is_some() {
abort!(variant, "prompt not supported for empty variants");
}
VariantKind::Empty
}
},
metadata,
}
}
}
impl From<&Variant> for VariantMetadata {
fn from(var: &Variant) -> Self {
let mut metadata = VariantMetadata {
prompt: None,
rename: None,
hidden: false,
default: false,
};
for attr in &var.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_variant_meta(meta, &mut metadata);
}
}
}
metadata
}
}
fn parse_variant_meta(meta: Meta, metadata: &mut VariantMetadata) {
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");
}
}
Meta::Path(path) => {
if path.is_ident("hidden") {
if metadata.hidden {
emit_warning!(path, "duplicated attribute");
}
metadata.hidden = true;
} else if path.is_ident("default") {
if metadata.default {
emit_warning!(path, "duplicated attribute");
}
metadata.default = true;
} else {
abort!(path, "unknown attribute");
}
}
_ => abort!(meta, "unknown attribute"),
}
}
fn gen_builder_ident(ident: &Ident) -> Ident {
format_ident!("__{}_BuildableValueImpl", ident)
}
fn gen_variants_builder_ident(ident: &Ident) -> Ident {
format_ident!("__{}_Variants_BuildableValueImpl", ident)
}
fn gen_variants_builder_variant_ident(ident: &Ident, variant: &Ident) -> Ident {
format_ident!("__{}_Variants_{}", ident, variant)
}
impl ToTokens for EnumGenerator {
fn to_tokens(&self, tokens: &mut TokenStream) {
tokens.append_all(gen_struct_builder(self));
tokens.append_all(gen_variants_builder(self));
for variant in &self.variants {
tokens.append_all(variant.gen_builder(gen_variants_builder_variant_ident(
&self.ident,
&variant.ident,
)));
}
tokens.append_all(gen_impl_new_buildable_value(self));
tokens.append_all(gen_impl_buildable_value(self));
}
}
impl ToTokens for VariantsDefList<'_> {
fn to_tokens(&self, tokens: &mut TokenStream) {
for variant in &self.gen.variants {
let ident = &variant.ident;
tokens.append_all(quote! {#ident});
match &variant.kind {
VariantKind::Unnamed(_) | VariantKind::Named(_) => {
let variant_builder =
gen_variants_builder_variant_ident(&self.gen.ident, ident);
let variant_builder = StructGenerator::gen_builder_ident(&variant_builder);
tokens.append_all(quote! { (#variant_builder) });
}
VariantKind::Empty => {}
}
tokens.append_all(quote! {,});
}
}
}
fn gen_struct_builder(gen: &EnumGenerator) -> TokenStream {
let builder_ident = &gen.builder_ident;
let variants_builder_ident = &gen.variants_builder_ident;
let prompt = if let Some(prompt) = &gen.metadata.prompt {
prompt
} else {
"Select a variant"
};
let mut default = quote! { None };
for var in &gen.variants {
if var.metadata.default {
let init = var.builder_new(&gen.ident);
default = quote! { Some(#init) };
}
}
quote! {
#[automatically_derived]
#[allow(non_camel_case_types)]
#[doc(hidden)]
#[derive(Debug)]
struct #builder_ident {
value: Option<#variants_builder_ident>,
prompt: String,
}
#[automatically_derived]
impl #builder_ident {
fn new(config: ibuilder::BuildableValueConfig<()>) -> #builder_ident {
#builder_ident {
value: #default,
prompt: config.prompt.unwrap_or_else(|| #prompt.to_string())
}
}
}
}
}
fn gen_variants_builder(gen: &EnumGenerator) -> TokenStream {
let variants_builder_ident = &gen.variants_builder_ident;
let variants = gen.variants_def_list();
quote! {
#[automatically_derived]
#[allow(non_camel_case_types)]
#[doc(hidden)]
#[derive(Debug)]
enum #variants_builder_ident {
#variants
}
}
}
fn gen_impl_new_buildable_value(gen: &EnumGenerator) -> 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))
}
}
}
}