use proc_macro2::Ident; use quote::quote; use syn::{DataEnum, DeriveInput, parse_quote, Variant}; use crate::add_trait_bounds; static ASCII_LOWER: [char; 26] = [ 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', ]; #[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)] enum VariantType { Unit, Struct, Tuple, } impl From<&Variant> for VariantType { fn from(value: &Variant) -> Self { match value.fields { syn::Fields::Named(_) => VariantType::Struct, syn::Fields::Unnamed(_) => VariantType::Tuple, syn::Fields::Unit => VariantType::Unit, } } } /// Generates the following different outputs: /// /// ```compile_fail /// // for struct variants /// TestEnum::Error { msg, code } /// /// // for tuple variants /// TestEnum::Middle(a, b) /// /// // for unit variants /// TestEnum::Start /// ``` fn gen_variant_full_id(enum_id: &proc_macro2::Ident, variant: &Variant) -> proc_macro2::TokenStream { let var_ty = VariantType::from(variant); let var_id = &variant.ident; let fields = variant.fields.iter().enumerate().map(|(idx, field)| { match var_ty { VariantType::Unit => { return quote! { }; }, VariantType::Struct => { let id = field.ident.as_ref().unwrap(); return quote! { #id } }, VariantType::Tuple => { let id = Ident::new(ASCII_LOWER[idx].to_string().as_str(), proc_macro2::Span::call_site()); return quote! { #id } } } }); let fields = match var_ty { VariantType::Unit => quote! {}, VariantType::Struct => { quote!{ { #( #fields ),* } } }, VariantType::Tuple => { quote!{ ( #( #fields ),* ) } }, }; quote! { #enum_id::#var_id #fields } } /// Generates an if statement to check if `self` is `variant`. The body of the if statement is replaced by `if_body` fn gen_variant_if(enum_id: &proc_macro2::Ident, variant: &Variant, if_body: proc_macro2::TokenStream, prepend_else: bool) -> proc_macro2::TokenStream { let variant_check = gen_variant_full_id(enum_id, variant); let optional_else = if prepend_else { quote! { else } } else { quote! {} }; quote! { #optional_else if let #variant_check = self { #if_body } } } /// Generates the following: /// /// ```compile_fail /// /// generated one field here /// if name == "msg" { /// return Some(msg); /// } /// /// /// another field here /// if name == "code" { /// return Some(code); /// } /// ``` /// Continues to generate if statements until the variant runs out of fields. fn gen_if_field_names(variant: &Variant) -> proc_macro2::TokenStream { let field_ifs = variant.fields.iter().map(|field| { let id = field.ident.as_ref().unwrap(); let id_str = id.to_string(); quote! { if name == #id_str { return Some(#id); } } }); quote! { #( #field_ifs )* } } /// Generates the following rust code: /// /// ```compile_fail /// match name { /// "msg" | "code" => true, /// _ => false, /// } /// ``` /// More strings may be added to the true match arm depending on the enum struct variant fields fn gen_match_names(variant: &Variant) -> proc_macro2::TokenStream { let field_name_strs = variant.fields.iter().map(|field| { let id = field.ident.as_ref() .expect("Could not find identifier for enum field!"); id.to_string() }); quote! { return match name { #( #field_name_strs )|* => true, _ => false, }; } } /// Generates the following: /// /// ```compile_fail /// /// generated one field here /// if idx == 0 { /// return Some(a); /// } /// /// /// another field here /// if idx == 1 { /// return Some(b); /// } /// ``` /// Continues to generate if statements until the variant runs out of fields. fn gen_if_field_indices(variant: &Variant) -> proc_macro2::TokenStream { let vty = VariantType::from(variant); let field_ifs = variant.fields.iter().enumerate() .map(|(idx, field)| { let id = if vty == VariantType::Tuple { Ident::new(ASCII_LOWER[idx].to_string().as_str(), proc_macro2::Span::call_site()) } else { field.ident.clone().unwrap() }; quote! { if idx == #idx { return Some(#id); } } }); quote! { #( #field_ifs )* } } /// Generates the following: /// /// ```compile_fail /// /// generated one field here /// if idx == 0 { /// return Some("a"); /// } /// /// /// another field here /// if idx == 1 { /// return Some("b"); /// } /// ``` /// Continues to generate if statements until the variant runs out of fields. fn gen_if_field_indices_names(variant: &Variant) -> proc_macro2::TokenStream { let vty = VariantType::from(variant); let field_ifs = variant.fields.iter().enumerate() .map(|(idx, field)| { let id_str = if vty == VariantType::Tuple { ASCII_LOWER[idx].to_string() } else { field.ident.clone().unwrap().to_string() }; quote! { if idx == #idx { return Some(#id_str.to_string()); } } }); quote! { #( #field_ifs )* } } /// Generates the following: /// ```compile_fail /// /// when `by_index` is false: /// /// if let TestEnum::Error{ msg, code} = self { /// if name == "msg" { /// return Some(msg); /// } /// /// if name == "code" { /// return Some(code); /// } /// } /// /// /// when `by_index` is true: /// /// if let TestEnum::Something(a, b) = self { /// if idx == 0 { /// return Some(a); /// } /// /// if idx == 1 { /// return Some(b); /// } /// } /// /// if let TestEnum::Error{ msg, code} = self { /// if idx == 0 { /// return Some(msg); /// } /// /// if idx == 1 { /// return Some(code); /// } /// } /// ``` /// And so on, for each variant that the enum provided has. /// /// Parameters /// * `by_index`: Should the if statements be generated to check indices. fn gen_enum_if_stmts(enum_id: &proc_macro2::Ident, data: &DataEnum, by_index: bool) -> proc_macro2::TokenStream { let mut if_statement_count = 0; let struct_vars = data.variants.iter().map(|var| { let vty = VariantType::from(var); let prepend_else = if_statement_count > 0; match vty { VariantType::Struct => if by_index { if_statement_count += 1; let if_body = gen_if_field_indices(var); gen_variant_if(enum_id, var, if_body, prepend_else) } else { if_statement_count += 1; let if_body = gen_if_field_names(var); gen_variant_if(enum_id, var, if_body, prepend_else) }, VariantType::Tuple => if by_index { if_statement_count += 1; let if_body = gen_if_field_indices(var); gen_variant_if(enum_id, var, if_body, prepend_else) } else { quote! { } }, _ => quote! { }, } }); quote! { #( #struct_vars )* } } /// Generates the following rust code: /// /// ```compile_fail /// if let TestEnum::Error { msg, code } = self { /// return match name { /// // expands for continuing struct fields /// "msg" | "code" => true, /// _ => false, /// }; /// } /// ``` /// And so on until the enum runs out of struct variants. fn gen_enum_has_field(enum_id: &proc_macro2::Ident, data: &DataEnum) -> proc_macro2::TokenStream { let struct_vars = data.variants.iter().map(|var| { let vty = VariantType::from(var); match vty { VariantType::Struct => { let match_name = gen_match_names(var); gen_variant_if(enum_id, var, match_name, false) }, _ => quote! { }, } }); quote! { #( #struct_vars )* } } /// Generates the following code: /// /// ```compile_fail /// match self { /// TestEnum::Start => 0, /// TestEnum::Middle(a, b) => 2, /// TestEnum::Error { msg, code } => 2, /// } /// ``` /// and so on for each variant of the enum fn gen_enum_fields_len(enum_id: &proc_macro2::Ident, data: &DataEnum) -> proc_macro2::TokenStream { let variant_arms = data.variants.iter().map(|var| { let variant_ident = gen_variant_full_id(enum_id, var); let field_len = var.fields.len(); quote! { #variant_ident => #field_len } }); quote! { match self { #( #variant_arms ),* } } } /// Generates the following code: /// /// ```compile_fail /// if let TestEnum::Error { msg, code } = self { /// if idx == 0 { /// return Some("msg"); /// } /// if idx == 1 { /// return Some("code"); /// } /// } /// ``` /// and so on for each struct variant of the enum. The inner if statements expand for each /// field of the variant. fn gen_enum_field_name_at(enum_id: &proc_macro2::Ident, data: &DataEnum) -> proc_macro2::TokenStream { let variant_ifs = data.variants.iter().map(|var| { let vty = VariantType::from(var); match vty { VariantType::Struct => { let match_name = gen_if_field_indices_names(var); gen_variant_if(enum_id, var, match_name, false) }, _ => quote! { }, } }); quote! { #( #variant_ifs )* } } /// Generates the following code: /// ```compile_fail /// match self { /// TestEnum::Start => 0, /// TestEnum::Middle(a, b) => 1, /// TestEnum::Error { msg, code } => 2, /// } /// ``` /// The match arms will expand for each variant the enum has. fn gen_enum_variant_name(enum_id: &proc_macro2::Ident, data: &DataEnum, gen_index: bool) -> proc_macro2::TokenStream { let variant_arms = data.variants.iter().enumerate().map(|(idx, var)| { let variant_ident = gen_variant_full_id(enum_id, var); let var_name = var.ident.to_string(); let arm_result = if gen_index { quote! { #idx } } else { quote! { #var_name.to_string() } }; quote! { #variant_ident => #arm_result } }); quote! { match self { #( #variant_arms ),* } } } /// Generates a match statement that returns the types of the variants of the enum. /// /// Example: /// ```compile_fail /// match self { /// TestEnum::Start => EnumType::Unit, /// TestEnum::Middle(a, b) => EnumType::Tuple, /// TestEnum::Error { msg, code } => EnumType::Struct, /// } /// ``` /// Match arms will be added for each variant of the enum. fn gen_enum_variant_type(enum_id: &proc_macro2::Ident, data: &DataEnum) -> proc_macro2::TokenStream { let variant_arms = data.variants.iter().map(|var| { let variant_ident = gen_variant_full_id(enum_id, var); let vty = VariantType::from(var); match vty { VariantType::Struct => quote! { #variant_ident => lyra_engine::reflect::EnumType::Struct }, VariantType::Tuple => quote! { #variant_ident => lyra_engine::reflect::EnumType::Tuple }, VariantType::Unit => quote! { #variant_ident => lyra_engine::reflect::EnumType::Unit }, } }); quote! { match self { #( #variant_arms ),* } } } /// Create a reflect implementation for an enum pub fn derive_reflect_enum(input: &DeriveInput, data_enum: &DataEnum) -> proc_macro2::TokenStream { let input_ident = &input.ident; let ident_str = input.ident.to_string(); let variant_count = data_enum.variants.len(); let field_ifs = gen_enum_if_stmts(input_ident, data_enum, false); let field_mut_ifs = gen_enum_if_stmts(input_ident, data_enum, false); let field_at_ifs = gen_enum_if_stmts(input_ident, data_enum, true); let field_at_mut_ifs = gen_enum_if_stmts(input_ident, data_enum, true); let has_field = gen_enum_has_field(input_ident, data_enum); let field_len = gen_enum_fields_len(input_ident, data_enum); let field_name_at = gen_enum_field_name_at(input_ident, data_enum); let variant_name_match = gen_enum_variant_name(input_ident, data_enum, false); let variant_idx_match = gen_enum_variant_name(input_ident, data_enum, true); let variant_type = gen_enum_variant_type(input_ident, data_enum); let generics = add_trait_bounds(input.generics.clone(), vec![parse_quote!(Reflect), parse_quote!(Clone)]); let (impl_generics, ty_generics, where_clause) = generics.split_for_impl(); return proc_macro2::TokenStream::from(quote! { impl #impl_generics lyra_engine::reflect::Reflect for #input_ident #ty_generics #where_clause { fn name(&self) -> ::std::string::String { #ident_str.to_string() } fn type_id(&self) -> std::any::TypeId { std::any::TypeId::of::<#input_ident #ty_generics>() } fn as_any(&self) -> &dyn std::any::Any { self } fn as_any_mut(&mut self) -> &mut dyn std::any::Any { self } fn as_boxed_any(self: Box) -> Box { self } fn apply(&mut self, val: &dyn lyra_engine::reflect::Reflect) { let val = val.as_any().downcast_ref::() .expect("The type of `val` is not the same as `self`"); *self = val.clone(); } fn clone_inner(&self) -> Box { Box::new(self.clone()) } fn reflect_ref(&self) -> lyra_engine::reflect::ReflectRef { lyra_engine::reflect::ReflectRef::Enum(self) } fn reflect_mut(&mut self) -> lyra_engine::reflect::ReflectMut { lyra_engine::reflect::ReflectMut::Enum(self) } fn reflect_val(&self) -> &dyn lyra_engine::reflect::Reflect { self } fn reflect_val_mut(&mut self) -> &mut dyn lyra_engine::reflect::Reflect { self } } impl #impl_generics lyra_engine::reflect::Enum for #input_ident #ty_generics #where_clause { fn field(&self, name: &str) -> Option<&dyn lyra_engine::reflect::Reflect> { let name = name.to_lowercase(); let name = name.as_str(); #field_ifs None } fn field_mut(&mut self, name: &str) -> Option<&mut dyn lyra_engine::reflect::Reflect> { let name = name.to_lowercase(); let name = name.as_str(); #field_mut_ifs None } fn field_at(&self, idx: usize) -> Option<&dyn lyra_engine::reflect::Reflect> { #field_at_ifs None } fn field_at_mut(&mut self, idx: usize) -> Option<&mut dyn lyra_engine::reflect::Reflect> { #field_at_mut_ifs None } fn has_field(&self, name: &str) -> bool { let name = name.to_lowercase(); let name = name.as_str(); #has_field false } fn fields_len(&self) -> usize { #field_len } fn variants_len(&self) -> usize { #variant_count } fn field_name_at(&self, idx: usize) -> Option { #field_name_at None } fn variant_name(&self) -> String { #variant_name_match } fn variant_index(&self) -> usize { #variant_idx_match } fn variant_type(&self) -> lyra_engine::reflect::EnumType { #variant_type } fn is_variant_name(&self, name: &str) -> bool { let name = name.to_lowercase(); self.variant_name().to_lowercase() == name } } }) }