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"
+ );
+}