diff --git a/Cargo.lock b/Cargo.lock index 50ea46c..208a62d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -338,7 +338,7 @@ checksum = "f63a545481291138910575129486daeaf8ac54aee4387fe7906919f7830c7d9d" [[package]] name = "visit-rs" -version = "0.1.9" +version = "0.1.10" dependencies = [ "async-stream", "futures", @@ -349,7 +349,7 @@ dependencies = [ [[package]] name = "visit-rs-derive" -version = "0.1.7" +version = "0.1.8" dependencies = [ "proc-macro2", "quote", diff --git a/visit-rs-derive/Cargo.toml b/visit-rs-derive/Cargo.toml index 386b2ef..2c77930 100644 --- a/visit-rs-derive/Cargo.toml +++ b/visit-rs-derive/Cargo.toml @@ -3,7 +3,7 @@ description = "Procedural macros for visit-rs" edition = "2024" license = "MIT" name = "visit-rs-derive" -version = "0.1.7" +version = "0.1.8" [lib] proc-macro = true diff --git a/visit-rs-derive/src/attrs.rs b/visit-rs-derive/src/attrs.rs index 773ea17..893e59c 100644 --- a/visit-rs-derive/src/attrs.rs +++ b/visit-rs-derive/src/attrs.rs @@ -1,6 +1,7 @@ use proc_macro2::TokenStream; use quote::{ToTokens, quote}; -use syn::{Attribute, Expr, Lit, Meta}; +use syn::punctuated::Punctuated; +use syn::{Attribute, Expr, Lit, Meta, Token}; /// Parse syn::Meta into our AttributeMeta representation fn parse_meta_to_attribute_meta(meta: &Meta) -> TokenStream { @@ -15,23 +16,24 @@ fn parse_meta_to_attribute_meta(meta: &Meta) -> TokenStream { } Meta::List(list) => { let path_str = list.path.to_token_stream().to_string(); - let tokens_str = list.tokens.to_string(); - // Try to parse the list contents - if let Ok(nested_meta) = syn::parse2::(list.tokens.clone()) { - let nested = parse_meta_to_attribute_meta(&nested_meta); - quote! { - visit_rs::metadata::AttributeMeta::List { - path: #path_str, - items: &[#nested], + match list.parse_args_with(Punctuated::::parse_terminated) { + Ok(nested) => { + let items = nested.iter().map(parse_meta_to_attribute_meta); + quote! { + visit_rs::metadata::AttributeMeta::List { + path: #path_str, + items: &[#(#items),*], + } } } - } else { - // Fallback to unparsed - quote! { - visit_rs::metadata::AttributeMeta::Unparsed { - path: #path_str, - tokens: #tokens_str, + Err(_) => { + let tokens_str = list.tokens.to_string(); + quote! { + visit_rs::metadata::AttributeMeta::Unparsed { + path: #path_str, + tokens: #tokens_str, + } } } } diff --git a/visit-rs-derive/src/enum_variants.rs b/visit-rs-derive/src/enum_variants.rs index a36be22..11e5598 100644 --- a/visit-rs-derive/src/enum_variants.rs +++ b/visit-rs-derive/src/enum_variants.rs @@ -8,6 +8,24 @@ use crate::helpers::{ get_field_rename, get_rename_all_attribute, get_rename_attribute, get_variant_rename, }; +/// Build the impl pieces for a `Visit*` impl that introduces an extra `__visit_rs__V` type +/// parameter, including `__visit_rs__V` in the correct position so generic/lifetime enums emit +/// valid tokens. Returns `(impl_generics, ty_generics, where_preds)` where `where_preds` carries +/// the enum's own where-clause predicates (comma-terminated) to splice into the impl `where`. +fn generics_for_visit(generics: &syn::Generics) -> (TokenStream, TokenStream, TokenStream) { + let mut with_visitor = generics.clone(); + with_visitor.params.push(syn::parse_quote!(__visit_rs__V)); + let (impl_generics, _, _) = with_visitor.split_for_impl(); + let impl_generics = quote!(#impl_generics); + let (_, ty_generics, where_clause) = generics.split_for_impl(); + let ty_generics = quote!(#ty_generics); + let where_preds = match where_clause { + Some(w) => { let preds = w.predicates.iter(); quote!(#(#preds,)*) } + None => quote!(), + }; + (impl_generics, ty_generics, where_preds) +} + pub fn derive_all_variant_traits( ast: &DeriveInput, data: &DataEnum, @@ -27,20 +45,27 @@ pub fn derive_all_variant_traits( let visit_variant_fields_static_named_async = derive_visit_variant_fields_static_named_async(ast, data)?; + // Wrap in an anonymous const so the generated method bodies can use trait-method call + // syntax (`.visit(..)`, `Self::variants()`) without the caller importing the traits — the + // impls still apply globally (the technique serde's derive uses). Ok(quote! { - #enum_info - #visit_variant - #visit_variants_static - #visit_variant_fields - #visit_variant_fields_covered - #visit_variant_fields_static - #visit_variant_fields_named - #visit_variant_fields_static_named - #visit_variant_fields_async - #visit_variant_fields_covered_async - #visit_variant_fields_static_async - #visit_variant_fields_named_async - #visit_variant_fields_static_named_async + const _: () = { + #[allow(unused_imports)] + use visit_rs::{EnumInfo as _, Visit as _, VisitAsync as _}; + #enum_info + #visit_variant + #visit_variants_static + #visit_variant_fields + #visit_variant_fields_covered + #visit_variant_fields_static + #visit_variant_fields_named + #visit_variant_fields_static_named + #visit_variant_fields_async + #visit_variant_fields_covered_async + #visit_variant_fields_static_async + #visit_variant_fields_named_async + #visit_variant_fields_static_named_async + }; }) } @@ -57,12 +82,7 @@ fn derive_enum_info(ast: &DeriveInput, data: &DataEnum) -> Result Result Result Result Result Result Result Result { let ident = &ast.ident; - let (impl_generics, ty_generics, where_clause) = ast.generics.split_for_impl(); + let (impl_generics, ty_generics, where_preds) = generics_for_visit(&ast.generics); Ok(quote! { - impl<__visit_rs__V, #impl_generics> visit_rs::VisitVariant<__visit_rs__V> for #ident #ty_generics - #where_clause + impl #impl_generics visit_rs::VisitVariant<__visit_rs__V> for #ident #ty_generics where + #where_preds + Self: 'static, __visit_rs__V: visit_rs::Visitor, - for<'a> visit_rs::Variant<'a, Self>: visit_rs::Visit<__visit_rs__V>, + for<'__visit_rs__a> visit_rs::Variant<'__visit_rs__a, Self>: visit_rs::Visit<__visit_rs__V>, { fn visit_variant(&self, visitor: &mut __visit_rs__V) -> <__visit_rs__V as visit_rs::Visitor>::Result { visit_rs::Variant { @@ -246,16 +258,17 @@ fn derive_visit_variants_static( _data: &DataEnum, ) -> Result { let ident = &ast.ident; - let (impl_generics, ty_generics, where_clause) = ast.generics.split_for_impl(); + let (impl_generics, ty_generics, where_preds) = generics_for_visit(&ast.generics); Ok(quote! { - impl<__visit_rs__V, #impl_generics> visit_rs::VisitVariantsStatic<__visit_rs__V> for #ident #ty_generics - #where_clause + impl #impl_generics visit_rs::VisitVariantsStatic<__visit_rs__V> for #ident #ty_generics where + #where_preds + Self: 'static, __visit_rs__V: visit_rs::Visitor, - for<'a> visit_rs::Variant<'a, visit_rs::Static>: visit_rs::Visit<__visit_rs__V>, + for<'__visit_rs__a> visit_rs::Variant<'__visit_rs__a, visit_rs::Static>: visit_rs::Visit<__visit_rs__V>, { - fn visit_variants_static<'a>(visitor: &'a mut __visit_rs__V) -> impl Iterator::Result> + 'a { + fn visit_variants_static<'__visit_rs__a>(visitor: &'__visit_rs__a mut __visit_rs__V) -> impl Iterator::Result> { Self::variants().into_iter().map(|info| { visit_rs::Variant { info, @@ -273,7 +286,7 @@ fn derive_visit_variant_fields( data: &DataEnum, ) -> Result { let ident = &ast.ident; - let (impl_generics, ty_generics, where_clause) = ast.generics.split_for_impl(); + let (impl_generics, ty_generics, where_preds) = generics_for_visit(&ast.generics); // Collect all unique field types for trait bounds let mut ty_set = HashSet::new(); @@ -337,16 +350,17 @@ fn derive_visit_variant_fields( }); Ok(quote! { - impl<__visit_rs__V, #impl_generics> visit_rs::VisitVariantFields<__visit_rs__V> for #ident #ty_generics - #where_clause + impl #impl_generics visit_rs::VisitVariantFields<__visit_rs__V> for #ident #ty_generics where + #where_preds + Self: 'static, __visit_rs__V: visit_rs::Visitor, #(#field_predicates),* { - fn visit_variant_fields<'a>( - &'a self, - visitor: &'a mut __visit_rs__V, - ) -> impl Iterator::Result> + 'a { + fn visit_variant_fields<'__visit_rs__a>( + &'__visit_rs__a self, + visitor: &'__visit_rs__a mut __visit_rs__V, + ) -> impl Iterator::Result> { let mut i = 0; std::iter::from_fn(move || { let res = match self { @@ -365,7 +379,7 @@ fn derive_visit_variant_fields_covered( data: &DataEnum, ) -> Result { let ident = &ast.ident; - let (impl_generics, ty_generics, where_clause) = ast.generics.split_for_impl(); + let (impl_generics, ty_generics, where_preds) = generics_for_visit(&ast.generics); // Collect all unique field types for trait bounds with Covered wrapper let mut ty_set = HashSet::new(); @@ -429,16 +443,17 @@ fn derive_visit_variant_fields_covered( }); Ok(quote! { - impl<__visit_rs__V, #impl_generics> visit_rs::VisitVariantFieldsCovered<__visit_rs__V> for #ident #ty_generics - #where_clause + impl #impl_generics visit_rs::VisitVariantFieldsCovered<__visit_rs__V> for #ident #ty_generics where + #where_preds + Self: 'static, __visit_rs__V: visit_rs::Visitor, #(#field_predicates),* { - fn visit_variant_fields_covered<'a>( - &'a self, - visitor: &'a mut __visit_rs__V - ) -> impl Iterator::Result> + 'a { + fn visit_variant_fields_covered<'__visit_rs__a>( + &'__visit_rs__a self, + visitor: &'__visit_rs__a mut __visit_rs__V + ) -> impl Iterator::Result> { let mut i = 0; std::iter::from_fn(move || { let res = match self { @@ -457,7 +472,7 @@ fn derive_visit_variant_fields_static( data: &DataEnum, ) -> Result { let ident = &ast.ident; - let (impl_generics, ty_generics, where_clause) = ast.generics.split_for_impl(); + let (impl_generics, ty_generics, where_preds) = generics_for_visit(&ast.generics); // Collect all unique field types for trait bounds let mut ty_set = HashSet::new(); @@ -506,16 +521,17 @@ fn derive_visit_variant_fields_static( }); Ok(quote! { - impl<__visit_rs__V, #impl_generics> visit_rs::VisitVariantFieldsStatic<__visit_rs__V> for #ident #ty_generics - #where_clause + impl #impl_generics visit_rs::VisitVariantFieldsStatic<__visit_rs__V> for #ident #ty_generics where + #where_preds + Self: 'static, __visit_rs__V: visit_rs::Visitor, #(#field_predicates),* { - fn visit_variant_fields_static<'a>( - info: &'a visit_rs::StructInfoData, - visitor: &'a mut __visit_rs__V, - ) -> impl Iterator::Result> + 'a { + fn visit_variant_fields_static<'__visit_rs__a>( + info: &'__visit_rs__a visit_rs::StructInfoData, + visitor: &'__visit_rs__a mut __visit_rs__V, + ) -> impl Iterator::Result> + '__visit_rs__a { let mut i = 0; std::iter::from_fn(move || { let res = match info.name { @@ -538,7 +554,7 @@ fn derive_visit_variant_fields_named( data: &DataEnum, ) -> Result { let ident = &ast.ident; - let (impl_generics, ty_generics, where_clause) = ast.generics.split_for_impl(); + let (impl_generics, ty_generics, where_preds) = generics_for_visit(&ast.generics); let mut ty_set = HashSet::new(); let mut field_predicates = Vec::new(); @@ -580,12 +596,7 @@ fn derive_visit_variant_fields_named( let metas = &variant_field_metas[variant_idx][idx]; let count = metas.len(); quote! { - if cfg!(feature = "meta") { - const META: [visit_rs::metadata::AttributeMeta; #count] = [#(#metas),*]; - &META - } else { - &[] - } + { const META: [visit_rs::metadata::AttributeMeta; #count] = [#(#metas),*]; &META } } } else { quote! { &[] } @@ -594,7 +605,6 @@ fn derive_visit_variant_fields_named( #idx => { let named = visit_rs::Named { name: Some(#renamed_field), - #[cfg(feature = "meta")] metadata: #metadata_ref, value: #field_name, }; @@ -620,12 +630,7 @@ fn derive_visit_variant_fields_named( let metas = &variant_field_metas[variant_idx][idx]; let count = metas.len(); quote! { - if cfg!(feature = "meta") { - const META: [visit_rs::metadata::AttributeMeta; #count] = [#(#metas),*]; - &META - } else { - &[] - } + { const META: [visit_rs::metadata::AttributeMeta; #count] = [#(#metas),*]; &META } } } else { quote! { &[] } @@ -634,7 +639,6 @@ fn derive_visit_variant_fields_named( #idx => { let named = visit_rs::Named { name: None, - #[cfg(feature = "meta")] metadata: #metadata_ref, value: #field_ident, }; @@ -661,16 +665,17 @@ fn derive_visit_variant_fields_named( }); Ok(quote! { - impl<__visit_rs__V, #impl_generics> visit_rs::VisitVariantFieldsNamed<__visit_rs__V> for #ident #ty_generics - #where_clause + impl #impl_generics visit_rs::VisitVariantFieldsNamed<__visit_rs__V> for #ident #ty_generics where + #where_preds + Self: 'static, __visit_rs__V: visit_rs::Visitor, #(#field_predicates),* { - fn visit_variant_fields_named<'a>( - &'a self, - visitor: &'a mut __visit_rs__V - ) -> impl Iterator::Result> + 'a { + fn visit_variant_fields_named<'__visit_rs__a>( + &'__visit_rs__a self, + visitor: &'__visit_rs__a mut __visit_rs__V + ) -> impl Iterator::Result> { let mut i = 0; std::iter::from_fn(move || { let res = match self { @@ -689,7 +694,7 @@ fn derive_visit_variant_fields_static_named( data: &DataEnum, ) -> Result { let ident = &ast.ident; - let (impl_generics, ty_generics, where_clause) = ast.generics.split_for_impl(); + let (impl_generics, ty_generics, where_preds) = generics_for_visit(&ast.generics); let mut ty_set = HashSet::new(); let mut field_predicates = Vec::new(); @@ -732,12 +737,7 @@ fn derive_visit_variant_fields_static_named( let metas = &variant_field_metas[variant_idx][idx]; let count = metas.len(); quote! { - if cfg!(feature = "meta") { - const META: [visit_rs::metadata::AttributeMeta; #count] = [#(#metas),*]; - &META - } else { - &[] - } + { const META: [visit_rs::metadata::AttributeMeta; #count] = [#(#metas),*]; &META } } } else { quote! { &[] } @@ -746,7 +746,6 @@ fn derive_visit_variant_fields_static_named( #idx => { let named = visit_rs::Named { name: Some(#renamed_field), - #[cfg(feature = "meta")] metadata: #metadata_ref, value: &visit_rs::Static::<#ty>::new(), }; @@ -769,12 +768,7 @@ fn derive_visit_variant_fields_static_named( let metas = &variant_field_metas[variant_idx][idx]; let count = metas.len(); quote! { - if cfg!(feature = "meta") { - const META: [visit_rs::metadata::AttributeMeta; #count] = [#(#metas),*]; - &META - } else { - &[] - } + { const META: [visit_rs::metadata::AttributeMeta; #count] = [#(#metas),*]; &META } } } else { quote! { &[] } @@ -783,7 +777,6 @@ fn derive_visit_variant_fields_static_named( #idx => { let named = visit_rs::Named { name: None, - #[cfg(feature = "meta")] metadata: #metadata_ref, value: &visit_rs::Static::<#ty>::new(), }; @@ -810,16 +803,17 @@ fn derive_visit_variant_fields_static_named( }); Ok(quote! { - impl<__visit_rs__V, #impl_generics> visit_rs::VisitVariantFieldsStaticNamed<__visit_rs__V> for #ident #ty_generics - #where_clause + impl #impl_generics visit_rs::VisitVariantFieldsStaticNamed<__visit_rs__V> for #ident #ty_generics where + #where_preds + Self: 'static, __visit_rs__V: visit_rs::Visitor, #(#field_predicates),* { - fn visit_variant_fields_static_named<'a>( - info: &'a visit_rs::StructInfoData, - visitor: &'a mut __visit_rs__V - ) -> impl Iterator::Result> + 'a { + fn visit_variant_fields_static_named<'__visit_rs__a>( + info: &'__visit_rs__a visit_rs::StructInfoData, + visitor: &'__visit_rs__a mut __visit_rs__V + ) -> impl Iterator::Result> + '__visit_rs__a { let mut i = 0; std::iter::from_fn(move || { let res = match info.name { @@ -842,7 +836,7 @@ fn derive_visit_variant_fields_async( data: &DataEnum, ) -> Result { let ident = &ast.ident; - let (impl_generics, ty_generics, where_clause) = ast.generics.split_for_impl(); + let (impl_generics, ty_generics, where_preds) = generics_for_visit(&ast.generics); let mut ty_set = HashSet::new(); let mut field_predicates = Vec::new(); @@ -851,6 +845,7 @@ fn derive_visit_variant_fields_async( let ty = &field.ty; if ty_set.insert(ty) { field_predicates.push(quote! { #ty: visit_rs::VisitAsync<__visit_rs__V> }); + field_predicates.push(quote! { #ty: Sync }); } } } @@ -894,17 +889,22 @@ fn derive_visit_variant_fields_async( }); Ok(quote! { - impl<__visit_rs__V, #impl_generics> visit_rs::VisitVariantFieldsAsync<__visit_rs__V> for #ident #ty_generics - #where_clause + impl #impl_generics visit_rs::VisitVariantFieldsAsync<__visit_rs__V> for #ident #ty_generics where + #where_preds + Self: 'static, __visit_rs__V: visit_rs::Visitor + Send, <__visit_rs__V as visit_rs::Visitor>::Result: Send, #(#field_predicates),* { - fn visit_variant_fields_async<'a>( - &'a self, - visitor: &'a mut __visit_rs__V - ) -> impl visit_rs::lib::futures::Stream::Result> + Send + 'a { + fn visit_variant_fields_async<'__visit_rs__a>( + &'__visit_rs__a self, + visitor: &'__visit_rs__a mut __visit_rs__V + ) -> impl visit_rs::lib::futures::Stream::Result> + Send + '__visit_rs__a + where + __visit_rs__V: Send, + <__visit_rs__V as visit_rs::Visitor>::Result: Send, + { visit_rs::lib::async_stream::stream! { match self { #(#variant_arms)* @@ -924,7 +924,7 @@ fn derive_visit_variant_fields_covered_async( data: &DataEnum, ) -> Result { let ident = &ast.ident; - let (impl_generics, ty_generics, where_clause) = ast.generics.split_for_impl(); + let (impl_generics, ty_generics, where_preds) = generics_for_visit(&ast.generics); let mut ty_set = HashSet::new(); let mut field_predicates = Vec::new(); @@ -933,6 +933,7 @@ fn derive_visit_variant_fields_covered_async( let ty = &field.ty; if ty_set.insert(ty) { field_predicates.push(quote! { for<'__visit_rs__covered> visit_rs::Covered<'__visit_rs__covered, #ty>: visit_rs::VisitAsync<__visit_rs__V> }); + field_predicates.push(quote! { #ty: Sync }); } } } @@ -976,17 +977,22 @@ fn derive_visit_variant_fields_covered_async( }); Ok(quote! { - impl<__visit_rs__V, #impl_generics> visit_rs::VisitVariantFieldsCoveredAsync<__visit_rs__V> for #ident #ty_generics - #where_clause + impl #impl_generics visit_rs::VisitVariantFieldsCoveredAsync<__visit_rs__V> for #ident #ty_generics where + #where_preds + Self: 'static, __visit_rs__V: visit_rs::Visitor + Send, <__visit_rs__V as visit_rs::Visitor>::Result: Send, #(#field_predicates),* { - fn visit_variant_fields_covered_async<'a>( - &'a self, - visitor: &'a mut __visit_rs__V - ) -> impl visit_rs::lib::futures::Stream::Result> + Send + 'a { + fn visit_variant_fields_covered_async<'__visit_rs__a>( + &'__visit_rs__a self, + visitor: &'__visit_rs__a mut __visit_rs__V + ) -> impl visit_rs::lib::futures::Stream::Result> + Send + '__visit_rs__a + where + __visit_rs__V: Send, + <__visit_rs__V as visit_rs::Visitor>::Result: Send, + { visit_rs::lib::async_stream::stream! { match self { #(#variant_arms)* @@ -1006,7 +1012,7 @@ fn derive_visit_variant_fields_static_async( data: &DataEnum, ) -> Result { let ident = &ast.ident; - let (impl_generics, ty_generics, where_clause) = ast.generics.split_for_impl(); + let (impl_generics, ty_generics, where_preds) = generics_for_visit(&ast.generics); let mut ty_set = HashSet::new(); let mut field_predicates = Vec::new(); @@ -1061,17 +1067,18 @@ fn derive_visit_variant_fields_static_async( }); Ok(quote! { - impl<__visit_rs__V, #impl_generics> visit_rs::VisitVariantFieldsStaticAsync<__visit_rs__V> for #ident #ty_generics - #where_clause + impl #impl_generics visit_rs::VisitVariantFieldsStaticAsync<__visit_rs__V> for #ident #ty_generics where + #where_preds + Self: 'static, __visit_rs__V: visit_rs::Visitor + Send, <__visit_rs__V as visit_rs::Visitor>::Result: Send, #(#field_predicates),* { - fn visit_variant_fields_static_async<'a>( - info: &'a visit_rs::StructInfoData, - visitor: &'a mut __visit_rs__V - ) -> impl visit_rs::lib::futures::Stream::Result> + 'a { + fn visit_variant_fields_static_async<'__visit_rs__a>( + info: &'__visit_rs__a visit_rs::StructInfoData, + visitor: &'__visit_rs__a mut __visit_rs__V + ) -> impl visit_rs::lib::futures::Stream::Result> { visit_rs::lib::async_stream::stream! { match info.name { #(#variant_match_arms,)* @@ -1094,7 +1101,7 @@ fn derive_visit_variant_fields_named_async( data: &DataEnum, ) -> Result { let ident = &ast.ident; - let (impl_generics, ty_generics, where_clause) = ast.generics.split_for_impl(); + let (impl_generics, ty_generics, where_preds) = generics_for_visit(&ast.generics); let mut ty_set = HashSet::new(); let mut field_predicates = Vec::new(); @@ -1103,6 +1110,7 @@ fn derive_visit_variant_fields_named_async( let ty = &field.ty; if ty_set.insert(ty) { field_predicates.push(quote! { for<'__visit_rs__named> visit_rs::Named<'__visit_rs__named, #ty>: visit_rs::VisitAsync<__visit_rs__V> }); + field_predicates.push(quote! { #ty: Sync }); } } } @@ -1136,12 +1144,7 @@ fn derive_visit_variant_fields_named_async( let metas = &variant_field_metas[variant_idx][idx]; let count = metas.len(); quote! { - if cfg!(feature = "meta") { - const META: [visit_rs::metadata::AttributeMeta; #count] = [#(#metas),*]; - &META - } else { - &[] - } + { const META: [visit_rs::metadata::AttributeMeta; #count] = [#(#metas),*]; &META } } } else { quote! { &[] } @@ -1150,7 +1153,6 @@ fn derive_visit_variant_fields_named_async( { let named = visit_rs::Named { name: Some(#renamed_field), - #[cfg(feature = "meta")] metadata: #metadata_ref, value: #field_name, }; @@ -1174,12 +1176,7 @@ fn derive_visit_variant_fields_named_async( let metas = &variant_field_metas[variant_idx][idx]; let count = metas.len(); quote! { - if cfg!(feature = "meta") { - const META: [visit_rs::metadata::AttributeMeta; #count] = [#(#metas),*]; - &META - } else { - &[] - } + { const META: [visit_rs::metadata::AttributeMeta; #count] = [#(#metas),*]; &META } } } else { quote! { &[] } @@ -1188,7 +1185,6 @@ fn derive_visit_variant_fields_named_async( { let named = visit_rs::Named { name: None, - #[cfg(feature = "meta")] metadata: #metadata_ref, value: #field_ident, }; @@ -1212,17 +1208,22 @@ fn derive_visit_variant_fields_named_async( }); Ok(quote! { - impl<__visit_rs__V, #impl_generics> visit_rs::VisitVariantFieldsNamedAsync<__visit_rs__V> for #ident #ty_generics - #where_clause + impl #impl_generics visit_rs::VisitVariantFieldsNamedAsync<__visit_rs__V> for #ident #ty_generics where + #where_preds + Self: 'static, __visit_rs__V: visit_rs::Visitor + Send, <__visit_rs__V as visit_rs::Visitor>::Result: Send, #(#field_predicates),* { - fn visit_variant_fields_named_async<'a>( - &'a self, - visitor: &'a mut __visit_rs__V - ) -> impl visit_rs::lib::futures::Stream::Result> + Send + 'a { + fn visit_variant_fields_named_async<'__visit_rs__a>( + &'__visit_rs__a self, + visitor: &'__visit_rs__a mut __visit_rs__V + ) -> impl visit_rs::lib::futures::Stream::Result> + Send + '__visit_rs__a + where + __visit_rs__V: Send, + <__visit_rs__V as visit_rs::Visitor>::Result: Send, + { visit_rs::lib::async_stream::stream! { match self { #(#variant_arms)* @@ -1242,7 +1243,7 @@ fn derive_visit_variant_fields_static_named_async( data: &DataEnum, ) -> Result { let ident = &ast.ident; - let (impl_generics, ty_generics, where_clause) = ast.generics.split_for_impl(); + let (impl_generics, ty_generics, where_preds) = generics_for_visit(&ast.generics); let mut ty_set = HashSet::new(); let mut field_predicates = Vec::new(); @@ -1285,12 +1286,7 @@ fn derive_visit_variant_fields_static_named_async( let metas = &variant_field_metas[variant_idx][idx]; let count = metas.len(); quote! { - if cfg!(feature = "meta") { - const META: [visit_rs::metadata::AttributeMeta; #count] = [#(#metas),*]; - &META - } else { - &[] - } + { const META: [visit_rs::metadata::AttributeMeta; #count] = [#(#metas),*]; &META } } } else { quote! { &[] } @@ -1299,7 +1295,6 @@ fn derive_visit_variant_fields_static_named_async( { let named = visit_rs::Named { name: Some(#renamed_field), - #[cfg(feature = "meta")] metadata: #metadata_ref, value: &visit_rs::Static::<#ty>::new(), }; @@ -1321,12 +1316,7 @@ fn derive_visit_variant_fields_static_named_async( let metas = &variant_field_metas[variant_idx][idx]; let count = metas.len(); quote! { - if cfg!(feature = "meta") { - const META: [visit_rs::metadata::AttributeMeta; #count] = [#(#metas),*]; - &META - } else { - &[] - } + { const META: [visit_rs::metadata::AttributeMeta; #count] = [#(#metas),*]; &META } } } else { quote! { &[] } @@ -1335,7 +1325,6 @@ fn derive_visit_variant_fields_static_named_async( { let named = visit_rs::Named { name: None, - #[cfg(feature = "meta")] metadata: #metadata_ref, value: &visit_rs::Static::<#ty>::new(), }; @@ -1359,17 +1348,18 @@ fn derive_visit_variant_fields_static_named_async( }); Ok(quote! { - impl<__visit_rs__V, #impl_generics> visit_rs::VisitVariantFieldsStaticNamedAsync<__visit_rs__V> for #ident #ty_generics - #where_clause + impl #impl_generics visit_rs::VisitVariantFieldsStaticNamedAsync<__visit_rs__V> for #ident #ty_generics where + #where_preds + Self: 'static, __visit_rs__V: visit_rs::Visitor + Send, <__visit_rs__V as visit_rs::Visitor>::Result: Send, #(#field_predicates),* { - fn visit_variant_fields_static_named_async<'a>( - info: &'a visit_rs::StructInfoData, - visitor: &'a mut __visit_rs__V - ) -> impl visit_rs::lib::futures::Stream::Result> + 'a { + fn visit_variant_fields_static_named_async<'__visit_rs__a>( + info: &'__visit_rs__a visit_rs::StructInfoData, + visitor: &'__visit_rs__a mut __visit_rs__V + ) -> impl visit_rs::lib::futures::Stream::Result> { visit_rs::lib::async_stream::stream! { match info.name { #(#variant_match_arms,)* diff --git a/visit-rs-derive/src/helpers.rs b/visit-rs-derive/src/helpers.rs index 3ff7f57..3bdc201 100644 --- a/visit-rs-derive/src/helpers.rs +++ b/visit-rs-derive/src/helpers.rs @@ -1,4 +1,5 @@ -use syn::{DeriveInput, Lit, Meta, Variant}; +use syn::punctuated::Punctuated; +use syn::{Attribute, DeriveInput, Lit, Meta, Token, Variant}; #[derive(Debug, Clone, Copy)] pub enum RenameRule { @@ -28,98 +29,119 @@ impl RenameRule { } } - pub fn apply(&self, s: &str) -> String { + /// Apply a renaming rule to an enum variant, mirroring serde's `apply_to_variant`. + /// Serde assumes variant identifiers are already PascalCase. + pub fn apply_to_variant(self, variant: &str) -> String { + use RenameRule::*; match self { - RenameRule::None => s.to_string(), - RenameRule::LowerCase => s.to_lowercase(), - RenameRule::UpperCase => s.to_uppercase(), - RenameRule::PascalCase => to_pascal_case(s), - RenameRule::CamelCase => to_camel_case(s), - RenameRule::SnakeCase => to_snake_case(s), - RenameRule::ScreamingSnakeCase => to_snake_case(s).to_uppercase(), - RenameRule::KebabCase => to_kebab_case(s), - RenameRule::ScreamingKebabCase => to_kebab_case(s).to_uppercase(), + None | PascalCase => variant.to_owned(), + LowerCase => variant.to_ascii_lowercase(), + UpperCase => variant.to_ascii_uppercase(), + CamelCase => variant[..1].to_ascii_lowercase() + &variant[1..], + SnakeCase => { + let mut snake = String::new(); + for (i, ch) in variant.char_indices() { + if i > 0 && ch.is_uppercase() { + snake.push('_'); + } + snake.push(ch.to_ascii_lowercase()); + } + snake + } + ScreamingSnakeCase => SnakeCase.apply_to_variant(variant).to_ascii_uppercase(), + KebabCase => SnakeCase.apply_to_variant(variant).replace('_', "-"), + ScreamingKebabCase => ScreamingSnakeCase + .apply_to_variant(variant) + .replace('_', "-"), } } -} -fn to_pascal_case(s: &str) -> String { - let mut result = String::new(); - let mut capitalize_next = true; - for ch in s.chars() { - if ch == '_' || ch == '-' { - capitalize_next = true; - } else if capitalize_next { - result.push(ch.to_uppercase().next().unwrap()); - capitalize_next = false; - } else { - result.push(ch); + /// Apply a renaming rule to a struct field, mirroring serde's `apply_to_field`. + /// Serde assumes field identifiers are already snake_case. + pub fn apply_to_field(self, field: &str) -> String { + use RenameRule::*; + match self { + None | LowerCase | SnakeCase => field.to_owned(), + UpperCase => field.to_ascii_uppercase(), + PascalCase => { + let mut pascal = String::new(); + let mut capitalize = true; + for ch in field.chars() { + if ch == '_' { + capitalize = true; + } else if capitalize { + pascal.push(ch.to_ascii_uppercase()); + capitalize = false; + } else { + pascal.push(ch); + } + } + pascal + } + CamelCase => { + let pascal = PascalCase.apply_to_field(field); + pascal[..1].to_ascii_lowercase() + &pascal[1..] + } + ScreamingSnakeCase => field.to_ascii_uppercase(), + KebabCase => field.replace('_', "-"), + ScreamingKebabCase => field.to_ascii_uppercase().replace('_', "-"), } } - result } -fn to_camel_case(s: &str) -> String { - let pascal = to_pascal_case(s); - let mut chars = pascal.chars(); - match chars.next() { - None => String::new(), - Some(first) => first.to_lowercase().chain(chars).collect(), +/// Yield every nested `Meta` from a `#[visit(...)]` or `#[serde(...)]` attribute, +/// parsing the comma-separated list so multi-item attributes survive intact. +fn meta_items(attr: &Attribute) -> Vec { + if !(attr.path().is_ident("visit") || attr.path().is_ident("serde")) { + return Vec::new(); } + let Ok(list) = attr.meta.require_list() else { + return Vec::new(); + }; + list.parse_args_with(Punctuated::::parse_terminated) + .map(|p| p.into_iter().collect()) + .unwrap_or_default() } -fn to_snake_case(s: &str) -> String { - let mut result = String::new(); - let mut prev_is_lowercase = false; - for (i, ch) in s.chars().enumerate() { - if ch == '-' { - result.push('_'); - prev_is_lowercase = false; - } else if ch.is_uppercase() { - if i > 0 && prev_is_lowercase { - result.push('_'); +/// Resolve a `rename` meta to its wire name. Handles both `rename = "x"` and the split +/// form `rename(serialize = "s", deserialize = "d")` (serialize wins for the wire shape). +fn rename_value(meta: &Meta) -> Option { + match meta { + Meta::NameValue(nv) => { + if let syn::Expr::Lit(syn::ExprLit { lit: Lit::Str(s), .. }) = &nv.value { + return Some(s.value()); } - result.push(ch.to_lowercase().next().unwrap()); - prev_is_lowercase = false; - } else { - result.push(ch); - prev_is_lowercase = ch.is_lowercase(); + None } - } - result -} - -fn to_kebab_case(s: &str) -> String { - to_snake_case(s).replace('_', "-") -} - -pub fn get_rename_attribute(ast: &DeriveInput) -> Option { - for attr in &ast.attrs { - // Check for #[visit(rename = "...")] - if attr.path().is_ident("visit") { - if let Ok(meta_list) = attr.meta.require_list() { - if let Ok(Meta::NameValue(nv)) = syn::parse2::(meta_list.tokens.clone()) { - if nv.path.is_ident("rename") { - if let syn::Expr::Lit(lit) = &nv.value { - if let Lit::Str(s) = &lit.lit { - return Some(s.value()); - } + Meta::List(list) => { + let nested = list + .parse_args_with(Punctuated::::parse_terminated) + .ok()?; + let mut serialize = None; + let mut deserialize = None; + for item in &nested { + if let Meta::NameValue(nv) = item { + if let syn::Expr::Lit(syn::ExprLit { lit: Lit::Str(s), .. }) = &nv.value { + if nv.path.is_ident("serialize") { + serialize = Some(s.value()); + } else if nv.path.is_ident("deserialize") { + deserialize = Some(s.value()); } } } } + serialize.or(deserialize) } - // Check for #[serde(rename = "...")] - if attr.path().is_ident("serde") { - if let Ok(meta_list) = attr.meta.require_list() { - if let Ok(Meta::NameValue(nv)) = syn::parse2::(meta_list.tokens.clone()) { - if nv.path.is_ident("rename") { - if let syn::Expr::Lit(lit) = &nv.value { - if let Lit::Str(s) = &lit.lit { - return Some(s.value()); - } - } - } + Meta::Path(_) => None, + } +} + +fn find_rename(attrs: &[Attribute]) -> Option { + for attr in attrs { + for meta in meta_items(attr) { + if meta.path().is_ident("rename") { + if let Some(name) = rename_value(&meta) { + return Some(name); } } } @@ -127,37 +149,12 @@ pub fn get_rename_attribute(ast: &DeriveInput) -> Option { None } -pub fn get_rename_all_attribute(ast: &DeriveInput) -> RenameRule { - for attr in &ast.attrs { - // Check for #[visit(rename_all = "...")] - if attr.path().is_ident("visit") { - if let Ok(meta_list) = attr.meta.require_list() { - if let Ok(Meta::NameValue(nv)) = syn::parse2::(meta_list.tokens.clone()) { - if nv.path.is_ident("rename_all") { - if let syn::Expr::Lit(lit) = &nv.value { - if let Lit::Str(s) = &lit.lit { - if let Some(rule) = RenameRule::from_str(&s.value()) { - return rule; - } - } - } - } - } - } - } - // Check for #[serde(rename_all = "...")] - if attr.path().is_ident("serde") { - if let Ok(meta_list) = attr.meta.require_list() { - if let Ok(Meta::NameValue(nv)) = syn::parse2::(meta_list.tokens.clone()) { - if nv.path.is_ident("rename_all") { - if let syn::Expr::Lit(lit) = &nv.value { - if let Lit::Str(s) = &lit.lit { - if let Some(rule) = RenameRule::from_str(&s.value()) { - return rule; - } - } - } - } +fn find_rename_all(attrs: &[Attribute]) -> RenameRule { + for attr in attrs { + for meta in meta_items(attr) { + if meta.path().is_ident("rename_all") { + if let Some(rule) = rename_value(&meta).and_then(|s| RenameRule::from_str(&s)) { + return rule; } } } @@ -165,49 +162,30 @@ pub fn get_rename_all_attribute(ast: &DeriveInput) -> RenameRule { RenameRule::None } -pub fn get_variant_rename(variant: &Variant, default_rule: RenameRule) -> String { - // First check for explicit rename attribute - for attr in &variant.attrs { - if attr.path().is_ident("visit") || attr.path().is_ident("serde") { - if let Ok(meta_list) = attr.meta.require_list() { - if let Ok(Meta::NameValue(nv)) = syn::parse2::(meta_list.tokens.clone()) { - if nv.path.is_ident("rename") { - if let syn::Expr::Lit(lit) = &nv.value { - if let Lit::Str(s) = &lit.lit { - return s.value(); - } - } - } - } - } - } - } +pub fn get_rename_attribute(ast: &DeriveInput) -> Option { + find_rename(&ast.attrs) +} - // Apply rename_all rule - default_rule.apply(&variant.ident.to_string()) +pub fn get_rename_all_attribute(ast: &DeriveInput) -> RenameRule { + find_rename_all(&ast.attrs) +} + +pub fn get_variant_rename(variant: &Variant, default_rule: RenameRule) -> String { + find_rename(&variant.attrs) + .unwrap_or_else(|| default_rule.apply_to_variant(&variant.ident.to_string())) } pub fn get_field_rename(field: &syn::Field, default_rule: RenameRule) -> Option { let field_name = field.ident.as_ref()?.to_string(); - - // First check for explicit rename attribute - for attr in &field.attrs { - if attr.path().is_ident("visit") || attr.path().is_ident("serde") { - if let Ok(meta_list) = attr.meta.require_list() { - if let Ok(Meta::NameValue(nv)) = syn::parse2::(meta_list.tokens.clone()) { - if nv.path.is_ident("rename") { - if let syn::Expr::Lit(lit) = &nv.value { - if let Lit::Str(s) = &lit.lit { - return Some(s.value()); - } - } - } - } - } - } - } - - // Apply rename_all rule - Some(default_rule.apply(&field_name)) + Some(find_rename(&field.attrs).unwrap_or_else(|| default_rule.apply_to_field(&field_name))) } +/// A field is skipped if it carries `#[visit(skip)]` or serde's `skip` / `skip_serializing` +/// (both of which drop it from the serialized wire format). +pub fn is_skipped(field: &syn::Field) -> bool { + field.attrs.iter().any(|attr| { + meta_items(attr) + .iter() + .any(|m| m.path().is_ident("skip") || m.path().is_ident("skip_serializing")) + }) +} diff --git a/visit-rs-derive/src/lib.rs b/visit-rs-derive/src/lib.rs index bd213c2..5e8b9c9 100644 --- a/visit-rs-derive/src/lib.rs +++ b/visit-rs-derive/src/lib.rs @@ -3,47 +3,12 @@ use std::collections::HashSet; use proc_macro2::{Span, TokenStream}; use quote::quote; use syn::{ - DataStruct, DeriveInput, Fields, Ident, Lit, Meta, Path, WhereClause, WherePredicate, - parse_quote, + DataStruct, DeriveInput, Fields, Path, WhereClause, WherePredicate, parse_quote, }; mod attrs; mod helpers; -use helpers::{get_field_rename, get_rename_all_attribute}; - -fn get_rename_attribute(ast: &DeriveInput) -> Option { - for attr in &ast.attrs { - // Check for #[visit(rename = "...")] - if attr.path().is_ident("visit") { - if let Ok(meta_list) = attr.meta.require_list() { - if let Ok(Meta::NameValue(nv)) = syn::parse2::(meta_list.tokens.clone()) { - if nv.path.is_ident("rename") { - if let syn::Expr::Lit(lit) = &nv.value { - if let Lit::Str(s) = &lit.lit { - return Some(s.value()); - } - } - } - } - } - } - // Check for #[serde(rename = "...")] - if attr.path().is_ident("serde") { - if let Ok(meta_list) = attr.meta.require_list() { - if let Ok(Meta::NameValue(nv)) = syn::parse2::(meta_list.tokens.clone()) { - if nv.path.is_ident("rename") { - if let syn::Expr::Lit(lit) = &nv.value { - if let Lit::Str(s) = &lit.lit { - return Some(s.value()); - } - } - } - } - } - } - } - None -} +use helpers::{get_field_rename, get_rename_all_attribute, get_rename_attribute}; fn make_impl( input: &DeriveInput, @@ -72,7 +37,7 @@ fn make_impl( predicates.push(syn::parse_quote! { __visit_rs__V: visit_rs::Visitor }); if sync { - predicates.extend(fields.iter().map(|f| &f.ty).map(|t| -> WherePredicate { + predicates.extend(field_iter(fields).map(|(_, f)| &f.ty).map(|t| -> WherePredicate { parse_quote! { #t: Sync } })); } @@ -109,12 +74,10 @@ fn make_impl( } fn field_iter(fields: &Fields) -> impl Iterator { - fields.iter().enumerate().filter(|(_, field)| { - !field.attrs.iter().any(|attr| { - attr.path().is_ident("visit") - && attr.parse_args::().map_or(false, |id| id == "skip") - }) - }) + fields + .iter() + .enumerate() + .filter(|(_, field)| !helpers::is_skipped(field)) } fn field_idx_iter(fields: &Fields) -> impl Iterator { @@ -210,12 +173,7 @@ fn derive_struct_info(ast: &DeriveInput, data: &DataStruct) -> Result Result = visit_rs::Static::new(); let named = visit_rs::Named { name: #name, - #[cfg(feature = "meta")] metadata: #metadata_ref, value: unsafe { // SAFETY: Static is zero-sized and contains only PhantomData, @@ -764,12 +703,7 @@ fn derive_visit_fields_static_named_async( let metas = &field_metas[num]; let count = metas.len(); quote! { - if cfg!(feature = "meta") { - const META: [visit_rs::metadata::AttributeMeta; #count] = [#(#metas),*]; - &META - } else { - &[] - } + { const META: [visit_rs::metadata::AttributeMeta; #count] = [#(#metas),*]; &META } } } else { quote! { &[] } @@ -784,7 +718,6 @@ fn derive_visit_fields_static_named_async( static __VISIT_RS_STATIC: visit_rs::Static<()> = visit_rs::Static::new(); let named = visit_rs::Named { name: #name, - #[cfg(feature = "meta")] metadata: #metadata_ref, value: unsafe { // SAFETY: Static is zero-sized and contains only PhantomData, diff --git a/visit-rs/Cargo.toml b/visit-rs/Cargo.toml index 2e641d4..4ebb89a 100644 --- a/visit-rs/Cargo.toml +++ b/visit-rs/Cargo.toml @@ -5,7 +5,7 @@ edition = "2024" license = "MIT" name = "visit-rs" repository = "https://github.com/dr-bonez/visit-rs" -version = "0.1.9" +version = "0.1.10" [features] default = ["serde", "meta"] @@ -14,7 +14,7 @@ meta = [] [dependencies] async-stream = "0.3" futures = "0.3" -visit-rs-derive = { version = "=0.1.7", path = "../visit-rs-derive" } +visit-rs-derive = { version = "=0.1.8", path = "../visit-rs-derive" } serde = { version = "1", optional = true } diff --git a/visit-rs/src/lib.rs b/visit-rs/src/lib.rs index 510a152..1b518ef 100644 --- a/visit-rs/src/lib.rs +++ b/visit-rs/src/lib.rs @@ -7,7 +7,6 @@ pub use visit_rs_derive::*; #[cfg(feature = "serde")] pub mod serde; -#[cfg(feature = "meta")] pub mod metadata; pub mod lib { @@ -45,7 +44,6 @@ pub struct StructInfoData { pub name: &'static str, pub named_fields: bool, pub field_count: usize, - #[cfg(feature = "meta")] pub metadata: &'static [metadata::AttributeMeta], } @@ -122,7 +120,6 @@ pub trait VisitFieldsStaticNamedAsync: StructInfo { #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub struct Named<'a, T: ?Sized> { pub name: Option<&'static str>, - #[cfg(feature = "meta")] pub metadata: &'static [metadata::AttributeMeta], pub value: &'a T, } @@ -182,7 +179,6 @@ pub trait EnumInfo { pub struct EnumInfoData { pub name: &'static str, pub variant_count: usize, - #[cfg(feature = "meta")] pub metadata: &'static [metadata::AttributeMeta], } diff --git a/visit-rs/tests/enum.rs b/visit-rs/tests/enum.rs index f883e3c..188c032 100644 --- a/visit-rs/tests/enum.rs +++ b/visit-rs/tests/enum.rs @@ -402,3 +402,68 @@ fn test_visit_variant_fields_static() { assert_eq!(fields[0], "Static"); assert_eq!(fields[1], "Static"); } + +// Generic / bounded enums must derive VisitVariants and impl every Visit* trait (for T: 'static). +#[derive(VisitVariants)] +enum GenEnum { + A(T), + B { x: T }, + Unit, +} + +#[derive(VisitVariants)] +#[visit(rename_all = "snake_case")] +enum Bounded +where + T: Send, +{ + FirstVariant(T), + SecondVariant, +} + +// The field-visiting traits (the ones that don't need a per-enum `Variant: Visit` impl) must +// exist for concrete instantiations of a generic enum. (`VisitVariant`/`VisitVariantsStatic` +// additionally need the user's `Variant<'_, _>: Visit` impl, as for any enum.) +fn assert_field_traits() +where + V: Visitor, + T: EnumInfo + + VisitVariantFields + + VisitVariantFieldsAsync + + VisitVariantFieldsCovered + + VisitVariantFieldsCoveredAsync + + VisitVariantFieldsNamed + + VisitVariantFieldsNamedAsync + + VisitVariantFieldsStatic + + VisitVariantFieldsStaticAsync + + VisitVariantFieldsStaticNamed + + VisitVariantFieldsStaticNamedAsync, +{ +} + +#[test] +fn test_generic_enum() { + assert_field_traits::, FmtVisitor>(); + assert_field_traits::, FmtVisitor>(); + assert_field_traits::, FmtVisitor>(); + + assert_eq!( as EnumInfo>::DATA.variant_count, 3); + assert!(GenEnum::::variant_info_by_name("A").is_some()); + + // Runtime visit of a generic variant. + let val = GenEnum::B { + x: String::from("hi"), + }; + let mut visitor = FmtVisitor(String::new()); + let _: Vec<_> = val.visit_variant_fields(&mut visitor).collect(); + assert!(visitor.0.contains("hi")); + + // where-clause + rename_all on a bounded generic enum. + assert_eq!( + Bounded::::variants() + .into_iter() + .map(|v| v.name) + .collect::>(), + ["first_variant", "second_variant"] + ); +} diff --git a/visit-rs/tests/enum_rename.rs b/visit-rs/tests/enum_rename.rs index 367e5d5..95255a8 100644 --- a/visit-rs/tests/enum_rename.rs +++ b/visit-rs/tests/enum_rename.rs @@ -101,8 +101,32 @@ fn test_case_conversions() { TestVariant, } - assert_eq!(PascalCase::variants().into_iter().next().unwrap().name, "TestVariant"); + // serde's `apply_to_variant` treats PascalCase as identity (variants are assumed + // already PascalCase), so `test_variant` is left unchanged — matching serde_json. + assert_eq!(PascalCase::variants().into_iter().next().unwrap().name, "test_variant"); assert_eq!(CamelCase::variants().into_iter().next().unwrap().name, "testVariant"); assert_eq!(KebabCase::variants().into_iter().next().unwrap().name, "test-variant"); assert_eq!(ScreamingKebabCase::variants().into_iter().next().unwrap().name, "TEST-VARIANT"); } + +// Variants with acronyms / consecutive capitals must match serde_json's wire output, +// which inserts a separator before every interior uppercase letter. +#[test] +fn test_acronym_variant_conversions() { + #[derive(VisitVariants)] + #[visit(rename_all = "snake_case")] + enum Snake { + IOError, + JSONData, + HttpResponse, + } + let names: Vec<_> = Snake::variants().into_iter().map(|v| v.name).collect(); + assert_eq!(names, ["i_o_error", "j_s_o_n_data", "http_response"]); + + #[derive(VisitVariants)] + #[visit(rename_all = "kebab-case")] + enum Kebab { + IOError, + } + assert_eq!(Kebab::variants().into_iter().next().unwrap().name, "i-o-error"); +} diff --git a/visit-rs/tests/import_free.rs b/visit-rs/tests/import_free.rs new file mode 100644 index 0000000..11e5358 --- /dev/null +++ b/visit-rs/tests/import_free.rs @@ -0,0 +1,16 @@ +// The `VisitVariants` derive must compile without the caller importing `Visit` / `EnumInfo` +// (the generated impls bring the traits into scope themselves). +#[derive(visit_rs::VisitVariants)] +enum E { + A(u32), + B { x: String }, + Unit, +} + +#[test] +fn derives_without_trait_imports() { + // Accessing trait items here does require the trait in scope — that's the test's choice, + // not the derive's requirement. The derive itself compiled above with no imports. + use visit_rs::EnumInfo; + assert_eq!(::DATA.variant_count, 3); +} diff --git a/visit-rs/tests/metadata.rs b/visit-rs/tests/metadata.rs new file mode 100644 index 0000000..21c4b0c --- /dev/null +++ b/visit-rs/tests/metadata.rs @@ -0,0 +1,58 @@ +#![cfg(feature = "meta")] + +use visit_rs::metadata::{AttributeMeta, MetaValue}; +use visit_rs::{EnumInfo, Visit, VisitVariants}; + +#[derive(VisitVariants)] +#[visit(tag = "type", content = "data")] +enum Adjacent { + A(u32), + B { x: String }, +} + +fn find_list<'a>(metas: &'a [AttributeMeta], path: &str) -> Option<&'a [AttributeMeta]> { + metas.iter().find_map(|m| match m { + AttributeMeta::List { path: p, items } if *p == path => Some(*items), + _ => None, + }) +} + +fn find_str(metas: &[AttributeMeta], name: &str) -> Option<&'static str> { + metas.iter().find_map(|m| match m { + AttributeMeta::NameValue { + name: n, + value: MetaValue::Str(s), + .. + } if *n == name => Some(*s), + _ => None, + }) +} + +// Multi-item attributes (e.g. adjacently-tagged enums) must survive as structured +// metadata rather than collapsing to AttributeMeta::Unparsed. +#[test] +fn tag_and_content_survive_as_structured_metadata() { + let meta = ::DATA.metadata; + assert!( + !meta.iter().any(|m| matches!(m, AttributeMeta::Unparsed { .. })), + "multi-item attribute should not be Unparsed: {meta:?}" + ); + let items = find_list(meta, "visit").expect("visit(...) list present"); + assert_eq!(find_str(items, "tag"), Some("type")); + assert_eq!(find_str(items, "content"), Some("data")); +} + +// A `rename` combined with another item in the same attribute list must still be honored. +#[derive(VisitVariants)] +enum Combined { + #[visit(rename = "renamed", alias = "legacy")] + Foo, +} + +#[test] +fn rename_honored_in_multi_item_list() { + assert_eq!( + Combined::variants().into_iter().next().unwrap().name, + "renamed" + ); +}