This is an automated email from the ASF dual-hosted git repository.

mgrigorov pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/avro-rs.git


The following commit(s) were added to refs/heads/main by this push:
     new 060c34f  feat(derive): Support `#[serde(transparent)]` (#398)
060c34f is described below

commit 060c34f5de1f1b78c9bc78ed7a36aa194c9773bb
Author: Kriskras99 <[email protected]>
AuthorDate: Mon Jan 19 13:05:31 2026 +0100

    feat(derive): Support `#[serde(transparent)]` (#398)
    
    * feat(derive): Support `#[serde(transparent)]`
    
    * feat(derive): Support `with` and `skip` for `#[serde(transparent)]`
    
    * fix(derive): Actually test the right thing
---
 avro_derive/src/attributes/mod.rs                  |  83 ++++-
 avro_derive/src/lib.rs                             | 382 +++++++++++----------
 avro_derive/tests/derive.rs                        |  21 ++
 avro_derive/tests/serde.rs                         |  63 ++++
 .../tests/ui/avro_rs_373_rename_all_fields.stderr  |   2 +-
 .../tests/ui/avro_rs_373_transparent.stderr        |   8 -
 ...3_transparent.rs => avro_rs_398_transparent.rs} |   5 +-
 .../tests/ui/avro_rs_398_transparent.stderr        |  18 +
 ...sparent.rs => avro_rs_398_transparent_skips.rs} |  11 +-
 .../tests/ui/avro_rs_398_transparent_skips.stderr  |  11 +
 10 files changed, 392 insertions(+), 212 deletions(-)

diff --git a/avro_derive/src/attributes/mod.rs 
b/avro_derive/src/attributes/mod.rs
index 5657179..ecf2797 100644
--- a/avro_derive/src/attributes/mod.rs
+++ b/avro_derive/src/attributes/mod.rs
@@ -18,22 +18,26 @@
 use crate::case::RenameRule;
 use darling::{FromAttributes, FromMeta};
 use proc_macro2::Span;
-use syn::{Attribute, Expr, Path, spanned::Spanned};
+use syn::{AttrStyle, Attribute, Expr, Ident, Path, spanned::Spanned};
 
 mod avro;
 mod serde;
 
 #[derive(Default)]
 pub struct NamedTypeOptions {
-    pub name: Option<String>,
-    pub namespace: Option<String>,
+    pub name: String,
     pub doc: Option<String>,
-    pub alias: Vec<String>,
+    pub aliases: Vec<String>,
     pub rename_all: RenameRule,
+    pub transparent: bool,
 }
 
 impl NamedTypeOptions {
-    pub fn new(attributes: &[Attribute], span: Span) -> Result<Self, 
Vec<syn::Error>> {
+    pub fn new(
+        ident: &Ident,
+        attributes: &[Attribute],
+        span: Span,
+    ) -> Result<Self, Vec<syn::Error>> {
         let avro =
             
avro::ContainerAttributes::from_attributes(attributes).map_err(darling_to_syn)?;
         let serde =
@@ -63,12 +67,6 @@ impl NamedTypeOptions {
                 "AvroSchema derive does not support the Serde `remote` 
attribute",
             ));
         }
-        if serde.transparent {
-            errors.push(syn::Error::new(
-                span,
-                "AvroSchema derive does not support Serde `transparent` 
attribute",
-            ));
-        }
         if serde.rename_all.deserialize != serde.rename_all.serialize {
             errors.push(syn::Error::new(
                 span,
@@ -89,17 +87,41 @@ impl NamedTypeOptions {
                 "#[avro(rename_all = \"..\")] must match #[serde(rename_all = 
\"..\")], it's also deprecated. Please use only `#[serde(rename_all = 
\"..\")]`",
             ));
         }
+        if serde.transparent
+            && (serde.rename.is_some()
+                || avro.name.is_some()
+                || avro.namespace.is_some()
+                || avro.doc.is_some()
+                || !avro.alias.is_empty()
+                || avro.rename_all != RenameRule::None
+                || serde.rename_all.serialize != RenameRule::None
+                || serde.rename_all.deserialize != RenameRule::None)
+        {
+            errors.push(syn::Error::new(
+                span,
+                "AvroSchema: #[serde(transparent)] is incompatible with all 
other attributes",
+            ));
+        }
 
         if !errors.is_empty() {
             return Err(errors);
         }
 
+        let name = serde.rename.unwrap_or(ident.to_string());
+        let full_schema_name = vec![avro.namespace, Some(name)]
+            .into_iter()
+            .flatten()
+            .collect::<Vec<String>>()
+            .join(".");
+
+        let doc = avro.doc.or_else(|| extract_rustdoc(attributes));
+
         Ok(Self {
-            name: serde.rename,
-            namespace: avro.namespace,
-            doc: avro.doc,
-            alias: avro.alias,
+            name: full_schema_name,
+            doc,
+            aliases: avro.alias,
             rename_all: serde.rename_all.serialize,
+            transparent: serde.transparent,
         })
     }
 }
@@ -171,7 +193,9 @@ impl With {
                     let path = Path::from_string(serde).map_err(|err| {
                         syn::Error::new(
                             span,
-                            format!("Expected a path for `#[serde(with = 
\"..\")]`: {err:?}"),
+                            format!(
+                                "AvroSchema: Expected a path for `#[serde(with 
= \"..\")]`: {err:?}"
+                            ),
                         )
                     })?;
                     Ok(Self::Serde(path))
@@ -187,6 +211,7 @@ impl With {
     }
 }
 
+#[derive(Default)]
 pub struct FieldOptions {
     pub doc: Option<String>,
     pub default: Option<String>,
@@ -269,8 +294,10 @@ impl FieldOptions {
             return Err(errors);
         }
 
+        let doc = avro.doc.or_else(|| extract_rustdoc(attributes));
+
         Ok(Self {
-            doc: avro.doc,
+            doc,
             default: avro.default,
             alias: serde.alias,
             rename: serde.rename,
@@ -281,6 +308,28 @@ impl FieldOptions {
     }
 }
 
+fn extract_rustdoc(attributes: &[Attribute]) -> Option<String> {
+    let doc = attributes
+        .iter()
+        .filter(|attr| attr.style == AttrStyle::Outer && 
attr.path().is_ident("doc"))
+        .filter_map(|attr| {
+            let name_value = attr.meta.require_name_value();
+            match name_value {
+                Ok(name_value) => match &name_value.value {
+                    syn::Expr::Lit(expr_lit) => match expr_lit.lit {
+                        syn::Lit::Str(ref lit_str) => 
Some(lit_str.value().trim().to_string()),
+                        _ => None,
+                    },
+                    _ => None,
+                },
+                Err(_) => None,
+            }
+        })
+        .collect::<Vec<String>>()
+        .join("\n");
+    if doc.is_empty() { None } else { Some(doc) }
+}
+
 fn darling_to_syn(e: darling::Error) -> Vec<syn::Error> {
     let msg = format!("{e}");
     let token_errors = e.write_errors();
diff --git a/avro_derive/src/lib.rs b/avro_derive/src/lib.rs
index b976756..12551f8 100644
--- a/avro_derive/src/lib.rs
+++ b/avro_derive/src/lib.rs
@@ -23,7 +23,8 @@ mod case;
 use proc_macro2::{Span, TokenStream};
 use quote::quote;
 use syn::{
-    AttrStyle, Attribute, DeriveInput, Expr, Ident, Meta, Type, 
parse_macro_input, spanned::Spanned,
+    Attribute, DataEnum, DataStruct, DeriveInput, Expr, Field, Fields, 
Generics, Ident, Meta, Type,
+    parse_macro_input, spanned::Spanned,
 };
 
 use crate::{
@@ -34,86 +35,93 @@ use crate::{
 #[proc_macro_derive(AvroSchema, attributes(avro, serde))]
 // Templated from Serde
 pub fn proc_macro_derive_avro_schema(input: proc_macro::TokenStream) -> 
proc_macro::TokenStream {
-    let mut input = parse_macro_input!(input as DeriveInput);
-    derive_avro_schema(&mut input)
+    let input = parse_macro_input!(input as DeriveInput);
+    derive_avro_schema(input)
         .unwrap_or_else(to_compile_errors)
         .into()
 }
 
-fn derive_avro_schema(input: &mut DeriveInput) -> Result<TokenStream, 
Vec<syn::Error>> {
-    let named_type_options = NamedTypeOptions::new(&input.attrs, 
input.span())?;
-
-    let rename_all = named_type_options.rename_all;
-    let name = named_type_options.name.unwrap_or(input.ident.to_string());
-
-    let full_schema_name = vec![named_type_options.namespace, Some(name)]
-        .into_iter()
-        .flatten()
-        .collect::<Vec<String>>()
-        .join(".");
-    let schema_def = match &input.data {
-        syn::Data::Struct(s) => get_data_struct_schema_def(
-            &full_schema_name,
-            named_type_options
-                .doc
-                .or_else(|| extract_outer_doc(&input.attrs)),
-            named_type_options.alias,
-            rename_all,
-            s,
-            input.ident.span(),
-        )?,
-        syn::Data::Enum(e) => get_data_enum_schema_def(
-            &full_schema_name,
-            named_type_options
-                .doc
-                .or_else(|| extract_outer_doc(&input.attrs)),
-            named_type_options.alias,
-            rename_all,
-            e,
-            input.ident.span(),
-        )?,
-        _ => {
-            return Err(vec![syn::Error::new(
-                input.ident.span(),
-                "AvroSchema derive only works for structs and simple enums ",
-            )]);
+fn derive_avro_schema(input: DeriveInput) -> Result<TokenStream, 
Vec<syn::Error>> {
+    // It would be nice to parse the attributes before the `match`, but we 
first need to validate that `input` is not a union.
+    // Otherwise a user could get errors related to the attributes and after 
fixing those get an error because the attributes were on a union.
+    let input_span = input.span();
+    match input.data {
+        syn::Data::Struct(data_struct) => {
+            let named_type_options = NamedTypeOptions::new(&input.ident, 
&input.attrs, input_span)?;
+            let inner = if named_type_options.transparent {
+                get_transparent_struct_schema_def(data_struct.fields, 
input_span)?
+            } else {
+                let schema_def =
+                    get_struct_schema_def(&named_type_options, data_struct, 
input.ident.span())?;
+                handle_named_schemas(named_type_options.name, schema_def)
+            };
+            Ok(create_trait_definition(input.ident, &input.generics, inner))
         }
-    };
-    let ident = &input.ident;
-    let (impl_generics, ty_generics, where_clause) = 
input.generics.split_for_impl();
-    Ok(quote! {
+        syn::Data::Enum(data_enum) => {
+            let named_type_options = NamedTypeOptions::new(&input.ident, 
&input.attrs, input_span)?;
+            if named_type_options.transparent {
+                return Err(vec![syn::Error::new(
+                    input_span,
+                    "AvroSchema: `#[serde(transparent)]` is only supported on 
structs",
+                )]);
+            }
+            let schema_def =
+                get_data_enum_schema_def(&named_type_options, data_enum, 
input.ident.span())?;
+            let inner = handle_named_schemas(named_type_options.name, 
schema_def);
+            Ok(create_trait_definition(input.ident, &input.generics, inner))
+        }
+        syn::Data::Union(_) => Err(vec![syn::Error::new(
+            input_span,
+            "AvroSchema: derive only works for structs and simple enums",
+        )]),
+    }
+}
+
+/// Generate the trait definition with the correct generics
+fn create_trait_definition(
+    ident: Ident,
+    generics: &Generics,
+    implementation: TokenStream,
+) -> TokenStream {
+    let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
+    quote! {
         #[automatically_derived]
         impl #impl_generics apache_avro::AvroSchemaComponent for #ident 
#ty_generics #where_clause {
-            fn get_schema_in_ctxt(named_schemas: &mut 
std::collections::HashMap<apache_avro::schema::Name, 
apache_avro::schema::Schema>, enclosing_namespace: &Option<String>) -> 
apache_avro::schema::Schema {
-                let name = 
apache_avro::schema::Name::new(#full_schema_name).expect(concat!("Unable to 
parse schema name ", 
#full_schema_name)).fully_qualified_name(enclosing_namespace);
-                if named_schemas.contains_key(&name) {
-                    apache_avro::schema::Schema::Ref{name}
-                } else {
-                    let enclosing_namespace = &name.namespace;
-                    // This is needed because otherwise recursive types will 
recurse forever and cause a stack overflow
-                    // TODO: Breaking change to AvroSchemaComponent, have 
named_schemas be a set instead
-                    named_schemas.insert(name.clone(), 
apache_avro::schema::Schema::Ref{name: name.clone()});
-                    let schema = #schema_def;
-                    named_schemas.insert(name, schema.clone());
-                    schema
-                }
+            fn get_schema_in_ctxt(named_schemas: &mut 
apache_avro::schema::Names, enclosing_namespace: &Option<String>) -> 
apache_avro::schema::Schema {
+                #implementation
             }
         }
-    })
+    }
 }
 
-fn get_data_struct_schema_def(
-    full_schema_name: &str,
-    record_doc: Option<String>,
-    aliases: Vec<String>,
-    rename_all: RenameRule,
-    s: &syn::DataStruct,
-    error_span: Span,
+/// Generate the code to check `named_schemas` if this schema already exist
+fn handle_named_schemas(full_schema_name: String, schema_def: TokenStream) -> 
TokenStream {
+    quote! {
+        let name = 
apache_avro::schema::Name::new(#full_schema_name).expect(concat!("Unable to 
parse schema name ", 
#full_schema_name)).fully_qualified_name(enclosing_namespace);
+        if named_schemas.contains_key(&name) {
+            apache_avro::schema::Schema::Ref{name}
+        } else {
+            let enclosing_namespace = &name.namespace;
+            // This is needed because otherwise recursive types will recurse 
forever and cause a stack overflow
+            // TODO: Breaking change to AvroSchemaComponent, have 
named_schemas be a set instead
+            named_schemas.insert(name.clone(), 
apache_avro::schema::Schema::Ref{name: name.clone()});
+            let schema = #schema_def;
+            named_schemas.insert(name, schema.clone());
+            schema
+        }
+    }
+}
+
+/// Generate a schema definition for a struct.
+fn get_struct_schema_def(
+    container_attrs: &NamedTypeOptions,
+    data_struct: DataStruct,
+    ident_span: Span,
 ) -> Result<TokenStream, Vec<syn::Error>> {
     let mut record_field_exprs = vec![];
-    match s.fields {
-        syn::Fields::Named(ref a) => {
-            for field in a.named.iter() {
+    match data_struct.fields {
+        Fields::Named(a) => {
+            for field in a.named {
                 let mut name = field
                     .ident
                     .as_ref()
@@ -123,9 +131,8 @@ fn get_data_struct_schema_def(
                     name = raw_name.to_string();
                 }
                 let field_attrs = FieldOptions::new(&field.attrs, 
field.span())?;
-                let doc =
-                    preserve_optional(field_attrs.doc.or_else(|| 
extract_outer_doc(&field.attrs)));
-                match (field_attrs.rename, rename_all) {
+                let doc = preserve_optional(field_attrs.doc);
+                match (field_attrs.rename, container_attrs.rename_all) {
                     (Some(rename), _) => {
                         name = rename;
                     }
@@ -169,32 +176,8 @@ fn get_data_struct_schema_def(
                     }
                     None => quote! { None },
                 };
-                let aliases = preserve_vec(field_attrs.alias);
-                let schema_expr = match field_attrs.with {
-                    With::Trait => type_to_schema_expr(&field.ty)?,
-                    With::Serde(path) => {
-                        quote! { #path::get_schema_in_ctxt(named_schemas, 
enclosing_namespace) }
-                    }
-                    With::Expr(Expr::Closure(closure)) => {
-                        if closure.inputs.is_empty() {
-                            quote! { (#closure)() }
-                        } else {
-                            return Err(vec![syn::Error::new(
-                                field.span(),
-                                "Expected closure with 0 parameters",
-                            )]);
-                        }
-                    }
-                    With::Expr(Expr::Path(path)) => {
-                        quote! { #path(named_schemas, enclosing_namespace) }
-                    }
-                    With::Expr(_expr) => {
-                        return Err(vec![syn::Error::new(
-                            field.span(),
-                            "Invalid expression, expected function or closure",
-                        )]);
-                    }
-                };
+                let aliases = preserve_vec(&field_attrs.alias);
+                let schema_expr = get_field_schema_expr(&field, 
field_attrs.with)?;
                 record_field_exprs.push(quote! {
                     schema_fields.push(::apache_avro::schema::RecordField {
                         name: #name.to_string(),
@@ -209,24 +192,28 @@ fn get_data_struct_schema_def(
                 });
             }
         }
-        syn::Fields::Unnamed(_) => {
+        Fields::Unnamed(_) => {
             return Err(vec![syn::Error::new(
-                error_span,
+                ident_span,
                 "AvroSchema derive does not work for tuple structs",
             )]);
         }
-        syn::Fields::Unit => {
+        Fields::Unit => {
             return Err(vec![syn::Error::new(
-                error_span,
+                ident_span,
                 "AvroSchema derive does not work for unit structs",
             )]);
         }
     }
-    let record_doc = preserve_optional(record_doc);
-    let record_aliases = preserve_vec(aliases);
+
+    let record_doc = preserve_optional(container_attrs.doc.as_ref());
+    let record_aliases = preserve_vec(&container_attrs.aliases);
+    let full_schema_name = &container_attrs.name;
+
     // When flatten is involved, there will be more but we don't know how 
many. This optimises for
     // the most common case where there is no flatten.
     let minimum_fields = record_field_exprs.len();
+
     Ok(quote! {
         {
             let mut schema_fields = Vec::with_capacity(#minimum_fields);
@@ -250,23 +237,86 @@ fn get_data_struct_schema_def(
     })
 }
 
+/// Use the schema definition of the only field in the struct as the schema
+fn get_transparent_struct_schema_def(
+    fields: Fields,
+    input_span: Span,
+) -> Result<TokenStream, Vec<syn::Error>> {
+    match fields {
+        Fields::Named(fields_named) => {
+            let mut found = None;
+            for field in fields_named.named {
+                let attrs = FieldOptions::new(&field.attrs, field.span())?;
+                if attrs.skip {
+                    continue;
+                }
+                if found.replace((field, attrs)).is_some() {
+                    return Err(vec![syn::Error::new(
+                        input_span,
+                        "AvroSchema: #[serde(transparent)] is only allowed on 
structs with one unskipped field",
+                    )]);
+                }
+            }
+
+            if let Some((field, attrs)) = found {
+                get_field_schema_expr(&field, attrs.with)
+            } else {
+                Err(vec![syn::Error::new(
+                    input_span,
+                    "AvroSchema: #[serde(transparent)] is only allowed on 
structs with one unskipped field",
+                )])
+            }
+        }
+        Fields::Unnamed(_) => Err(vec![syn::Error::new(
+            input_span,
+            "AvroSchema: derive does not work for tuple structs",
+        )]),
+        Fields::Unit => Err(vec![syn::Error::new(
+            input_span,
+            "AvroSchema: derive does not work for unit structs",
+        )]),
+    }
+}
+
+fn get_field_schema_expr(field: &Field, with: With) -> Result<TokenStream, 
Vec<syn::Error>> {
+    match with {
+        With::Trait => Ok(type_to_schema_expr(&field.ty)?),
+        With::Serde(path) => {
+            Ok(quote! { #path::get_schema_in_ctxt(named_schemas, 
enclosing_namespace) })
+        }
+        With::Expr(Expr::Closure(closure)) => {
+            if closure.inputs.is_empty() {
+                Ok(quote! { (#closure)() })
+            } else {
+                Err(vec![syn::Error::new(
+                    field.span(),
+                    "Expected closure with 0 parameters",
+                )])
+            }
+        }
+        With::Expr(Expr::Path(path)) => Ok(quote! { #path(named_schemas, 
enclosing_namespace) }),
+        With::Expr(_expr) => Err(vec![syn::Error::new(
+            field.span(),
+            "Invalid expression, expected function or closure",
+        )]),
+    }
+}
+
+/// Generate a schema definition for a enum.
 fn get_data_enum_schema_def(
-    full_schema_name: &str,
-    doc: Option<String>,
-    aliases: Vec<String>,
-    rename_all: RenameRule,
-    e: &syn::DataEnum,
-    error_span: Span,
+    container_attrs: &NamedTypeOptions,
+    data_enum: DataEnum,
+    ident_span: Span,
 ) -> Result<TokenStream, Vec<syn::Error>> {
-    let doc = preserve_optional(doc);
-    let enum_aliases = preserve_vec(aliases);
-    if e.variants.iter().all(|v| syn::Fields::Unit == v.fields) {
-        let default_value = default_enum_variant(e, error_span)?;
+    let doc = preserve_optional(container_attrs.doc.as_ref());
+    let enum_aliases = preserve_vec(&container_attrs.aliases);
+    if data_enum.variants.iter().all(|v| Fields::Unit == v.fields) {
+        let default_value = default_enum_variant(&data_enum, ident_span)?;
         let default = preserve_optional(default_value);
         let mut symbols = Vec::new();
-        for variant in &e.variants {
+        for variant in &data_enum.variants {
             let field_attrs = VariantOptions::new(&variant.attrs, 
variant.span())?;
-            let name = match (field_attrs.rename, rename_all) {
+            let name = match (field_attrs.rename, container_attrs.rename_all) {
                 (Some(rename), _) => rename,
                 (None, rename_all) if !matches!(rename_all, RenameRule::None) 
=> {
                     rename_all.apply_to_variant(&variant.ident.to_string())
@@ -275,6 +325,7 @@ fn get_data_enum_schema_def(
             };
             symbols.push(name);
         }
+        let full_schema_name = &container_attrs.name;
         Ok(quote! {
             apache_avro::schema::Schema::Enum(apache_avro::schema::EnumSchema {
                 name: 
apache_avro::schema::Name::new(#full_schema_name).expect(&format!("Unable to 
parse enum name for schema {}", #full_schema_name)[..]),
@@ -287,8 +338,8 @@ fn get_data_enum_schema_def(
         })
     } else {
         Err(vec![syn::Error::new(
-            error_span,
-            "AvroSchema derive does not work for enums with non unit structs",
+            ident_span,
+            "AvroSchema: derive does not work for enums with non unit structs",
         )])
     }
 }
@@ -351,28 +402,6 @@ fn to_compile_errors(errors: Vec<syn::Error>) -> 
proc_macro2::TokenStream {
     quote!(#(#compile_errors)*)
 }
 
-fn extract_outer_doc(attributes: &[Attribute]) -> Option<String> {
-    let doc = attributes
-        .iter()
-        .filter(|attr| attr.style == AttrStyle::Outer && 
attr.path().is_ident("doc"))
-        .filter_map(|attr| {
-            let name_value = attr.meta.require_name_value();
-            match name_value {
-                Ok(name_value) => match &name_value.value {
-                    syn::Expr::Lit(expr_lit) => match expr_lit.lit {
-                        syn::Lit::Str(ref lit_str) => 
Some(lit_str.value().trim().to_string()),
-                        _ => None,
-                    },
-                    _ => None,
-                },
-                Err(_) => None,
-            }
-        })
-        .collect::<Vec<String>>()
-        .join("\n");
-    if doc.is_empty() { None } else { Some(doc) }
-}
-
 fn preserve_optional(op: Option<impl quote::ToTokens>) -> TokenStream {
     match op {
         Some(tt) => quote! {Some(#tt.into())},
@@ -380,7 +409,7 @@ fn preserve_optional(op: Option<impl quote::ToTokens>) -> 
TokenStream {
     }
 }
 
-fn preserve_vec(op: Vec<impl quote::ToTokens>) -> TokenStream {
+fn preserve_vec(op: &[impl quote::ToTokens]) -> TokenStream {
     let items: Vec<TokenStream> = op.iter().map(|tt| quote! 
{#tt.into()}).collect();
     if items.is_empty() {
         quote! {None}
@@ -404,8 +433,8 @@ mod tests {
         };
 
         match syn::parse2::<DeriveInput>(test_struct) {
-            Ok(mut input) => {
-                assert!(derive_avro_schema(&mut input).is_ok())
+            Ok(input) => {
+                assert!(derive_avro_schema(input).is_ok())
             }
             Err(error) => panic!(
                 "Failed to parse as derive input when it should be able to. 
Error: {error:?}"
@@ -420,8 +449,8 @@ mod tests {
         };
 
         match syn::parse2::<DeriveInput>(test_tuple_struct) {
-            Ok(mut input) => {
-                assert!(derive_avro_schema(&mut input).is_err())
+            Ok(input) => {
+                assert!(derive_avro_schema(input).is_err())
             }
             Err(error) => panic!(
                 "Failed to parse as derive input when it should be able to. 
Error: {error:?}"
@@ -436,8 +465,8 @@ mod tests {
         };
 
         match syn::parse2::<DeriveInput>(test_tuple_struct) {
-            Ok(mut input) => {
-                assert!(derive_avro_schema(&mut input).is_err())
+            Ok(input) => {
+                assert!(derive_avro_schema(input).is_err())
             }
             Err(error) => panic!(
                 "Failed to parse as derive input when it should be able to. 
Error: {error:?}"
@@ -453,8 +482,8 @@ mod tests {
             }
         };
         match syn::parse2::<DeriveInput>(struct_with_optional) {
-            Ok(mut input) => {
-                assert!(derive_avro_schema(&mut input).is_ok())
+            Ok(input) => {
+                assert!(derive_avro_schema(input).is_ok())
             }
             Err(error) => panic!(
                 "Failed to parse as derive input when it should be able to. 
Error: {error:?}"
@@ -473,8 +502,8 @@ mod tests {
             }
         };
         match syn::parse2::<DeriveInput>(basic_enum) {
-            Ok(mut input) => {
-                assert!(derive_avro_schema(&mut input).is_ok())
+            Ok(input) => {
+                assert!(derive_avro_schema(input).is_ok())
             }
             Err(error) => panic!(
                 "Failed to parse as derive input when it should be able to. 
Error: {error:?}"
@@ -494,17 +523,14 @@ mod tests {
             }
         };
         match syn::parse2::<DeriveInput>(basic_enum) {
-            Ok(mut input) => {
-                let derived = derive_avro_schema(&mut input);
+            Ok(input) => {
+                let derived = derive_avro_schema(input);
                 assert!(derived.is_ok());
                 assert_eq!(derived.unwrap().to_string(), quote! {
                     #[automatically_derived]
                     impl apache_avro::AvroSchemaComponent for Basic {
                         fn get_schema_in_ctxt(
-                            named_schemas: &mut std::collections::HashMap<
-                                apache_avro::schema::Name,
-                                apache_avro::schema::Schema
-                            >,
+                            named_schemas: &mut apache_avro::schema::Names,
                             enclosing_namespace: &Option<String>
                         ) -> apache_avro::schema::Schema {
                             let name = apache_avro::schema::Name::new("Basic")
@@ -557,7 +583,7 @@ mod tests {
             }
         };
         match syn::parse2::<DeriveInput>(non_basic_enum) {
-            Ok(mut input) => match derive_avro_schema(&mut input) {
+            Ok(input) => match derive_avro_schema(input) {
                 Ok(_) => {
                     panic!("Should not be able to derive schema for enum with 
multiple defaults")
                 }
@@ -586,8 +612,8 @@ mod tests {
             }
         };
         match syn::parse2::<DeriveInput>(non_basic_enum) {
-            Ok(mut input) => {
-                assert!(derive_avro_schema(&mut input).is_err())
+            Ok(input) => {
+                assert!(derive_avro_schema(input).is_err())
             }
             Err(error) => panic!(
                 "Failed to parse as derive input when it should be able to. 
Error: {error:?}"
@@ -606,8 +632,8 @@ mod tests {
         };
 
         match syn::parse2::<DeriveInput>(test_struct) {
-            Ok(mut input) => {
-                let schema_token_stream = derive_avro_schema(&mut input);
+            Ok(input) => {
+                let schema_token_stream = derive_avro_schema(input);
                 assert!(&schema_token_stream.is_ok());
                 assert!(
                     schema_token_stream
@@ -632,8 +658,8 @@ mod tests {
         };
 
         match syn::parse2::<DeriveInput>(test_reference_struct) {
-            Ok(mut input) => {
-                assert!(derive_avro_schema(&mut input).is_ok())
+            Ok(input) => {
+                assert!(derive_avro_schema(input).is_ok())
             }
             Err(error) => panic!(
                 "Failed to parse as derive input when it should be able to. 
Error: {error:?}"
@@ -659,9 +685,9 @@ mod tests {
         };
 
         match syn::parse2::<DeriveInput>(test_struct) {
-            Ok(mut input) => {
-                let schema_res = derive_avro_schema(&mut input);
-                let expected_token_stream = r#"# [automatically_derived] impl 
apache_avro :: AvroSchemaComponent for A { fn get_schema_in_ctxt (named_schemas 
: & mut std :: collections :: HashMap < apache_avro :: schema :: Name , 
apache_avro :: schema :: Schema > , enclosing_namespace : & Option < String >) 
-> apache_avro :: schema :: Schema { let name = apache_avro :: schema :: Name 
:: new ("A") . expect (concat ! ("Unable to parse schema name " , "A")) . 
fully_qualified_name (enclosing [...]
+            Ok(input) => {
+                let schema_res = derive_avro_schema(input);
+                let expected_token_stream = r#"# [automatically_derived] impl 
apache_avro :: AvroSchemaComponent for A { fn get_schema_in_ctxt (named_schemas 
: & mut apache_avro :: schema :: Names , enclosing_namespace : & Option < 
String >) -> apache_avro :: schema :: Schema { let name = apache_avro :: schema 
:: Name :: new ("A") . expect (concat ! ("Unable to parse schema name " , "A")) 
. fully_qualified_name (enclosing_namespace) ; if named_schemas . contains_key 
(& name) { apache_avr [...]
                 let schema_token_stream = schema_res.unwrap().to_string();
                 assert_eq!(schema_token_stream, expected_token_stream);
             }
@@ -678,9 +704,9 @@ mod tests {
         };
 
         match syn::parse2::<DeriveInput>(test_enum) {
-            Ok(mut input) => {
-                let schema_res = derive_avro_schema(&mut input);
-                let expected_token_stream = r#"# [automatically_derived] impl 
apache_avro :: AvroSchemaComponent for A { fn get_schema_in_ctxt (named_schemas 
: & mut std :: collections :: HashMap < apache_avro :: schema :: Name , 
apache_avro :: schema :: Schema > , enclosing_namespace : & Option < String >) 
-> apache_avro :: schema :: Schema { let name = apache_avro :: schema :: Name 
:: new ("A") . expect (concat ! ("Unable to parse schema name " , "A")) . 
fully_qualified_name (enclosing [...]
+            Ok(input) => {
+                let schema_res = derive_avro_schema(input);
+                let expected_token_stream = r#"# [automatically_derived] impl 
apache_avro :: AvroSchemaComponent for A { fn get_schema_in_ctxt (named_schemas 
: & mut apache_avro :: schema :: Names , enclosing_namespace : & Option < 
String >) -> apache_avro :: schema :: Schema { let name = apache_avro :: schema 
:: Name :: new ("A") . expect (concat ! ("Unable to parse schema name " , "A")) 
. fully_qualified_name (enclosing_namespace) ; if named_schemas . contains_key 
(& name) { apache_avr [...]
                 let schema_token_stream = schema_res.unwrap().to_string();
                 assert_eq!(schema_token_stream, expected_token_stream);
             }
@@ -701,9 +727,9 @@ mod tests {
         };
 
         match syn::parse2::<DeriveInput>(test_struct) {
-            Ok(mut input) => {
-                let schema_res = derive_avro_schema(&mut input);
-                let expected_token_stream = r#"# [automatically_derived] impl 
apache_avro :: AvroSchemaComponent for A { fn get_schema_in_ctxt (named_schemas 
: & mut std :: collections :: HashMap < apache_avro :: schema :: Name , 
apache_avro :: schema :: Schema > , enclosing_namespace : & Option < String >) 
-> apache_avro :: schema :: Schema { let name = apache_avro :: schema :: Name 
:: new ("A") . expect (concat ! ("Unable to parse schema name " , "A")) . 
fully_qualified_name (enclosing [...]
+            Ok(input) => {
+                let schema_res = derive_avro_schema(input);
+                let expected_token_stream = r#"# [automatically_derived] impl 
apache_avro :: AvroSchemaComponent for A { fn get_schema_in_ctxt (named_schemas 
: & mut apache_avro :: schema :: Names , enclosing_namespace : & Option < 
String >) -> apache_avro :: schema :: Schema { let name = apache_avro :: schema 
:: Name :: new ("A") . expect (concat ! ("Unable to parse schema name " , "A")) 
. fully_qualified_name (enclosing_namespace) ; if named_schemas . contains_key 
(& name) { apache_avr [...]
                 let schema_token_stream = schema_res.unwrap().to_string();
                 assert_eq!(schema_token_stream, expected_token_stream);
             }
@@ -721,9 +747,9 @@ mod tests {
         };
 
         match syn::parse2::<DeriveInput>(test_enum) {
-            Ok(mut input) => {
-                let schema_res = derive_avro_schema(&mut input);
-                let expected_token_stream = r#"# [automatically_derived] impl 
apache_avro :: AvroSchemaComponent for B { fn get_schema_in_ctxt (named_schemas 
: & mut std :: collections :: HashMap < apache_avro :: schema :: Name , 
apache_avro :: schema :: Schema > , enclosing_namespace : & Option < String >) 
-> apache_avro :: schema :: Schema { let name = apache_avro :: schema :: Name 
:: new ("B") . expect (concat ! ("Unable to parse schema name " , "B")) . 
fully_qualified_name (enclosing [...]
+            Ok(input) => {
+                let schema_res = derive_avro_schema(input);
+                let expected_token_stream = r#"# [automatically_derived] impl 
apache_avro :: AvroSchemaComponent for B { fn get_schema_in_ctxt (named_schemas 
: & mut apache_avro :: schema :: Names , enclosing_namespace : & Option < 
String >) -> apache_avro :: schema :: Schema { let name = apache_avro :: schema 
:: Name :: new ("B") . expect (concat ! ("Unable to parse schema name " , "B")) 
. fully_qualified_name (enclosing_namespace) ; if named_schemas . contains_key 
(& name) { apache_avr [...]
                 let schema_token_stream = schema_res.unwrap().to_string();
                 assert_eq!(schema_token_stream, expected_token_stream);
             }
@@ -745,9 +771,9 @@ mod tests {
         };
 
         match syn::parse2::<DeriveInput>(test_struct) {
-            Ok(mut input) => {
-                let schema_res = derive_avro_schema(&mut input);
-                let expected_token_stream = r#"# [automatically_derived] impl 
apache_avro :: AvroSchemaComponent for A { fn get_schema_in_ctxt (named_schemas 
: & mut std :: collections :: HashMap < apache_avro :: schema :: Name , 
apache_avro :: schema :: Schema > , enclosing_namespace : & Option < String >) 
-> apache_avro :: schema :: Schema { let name = apache_avro :: schema :: Name 
:: new ("A") . expect (concat ! ("Unable to parse schema name " , "A")) . 
fully_qualified_name (enclosing [...]
+            Ok(input) => {
+                let schema_res = derive_avro_schema(input);
+                let expected_token_stream = r#"# [automatically_derived] impl 
apache_avro :: AvroSchemaComponent for A { fn get_schema_in_ctxt (named_schemas 
: & mut apache_avro :: schema :: Names , enclosing_namespace : & Option < 
String >) -> apache_avro :: schema :: Schema { let name = apache_avro :: schema 
:: Name :: new ("A") . expect (concat ! ("Unable to parse schema name " , "A")) 
. fully_qualified_name (enclosing_namespace) ; if named_schemas . contains_key 
(& name) { apache_avr [...]
                 let schema_token_stream = schema_res.unwrap().to_string();
                 assert_eq!(schema_token_stream, expected_token_stream);
             }
diff --git a/avro_derive/tests/derive.rs b/avro_derive/tests/derive.rs
index 758774e..74c0e1e 100644
--- a/avro_derive/tests/derive.rs
+++ b/avro_derive/tests/derive.rs
@@ -1996,6 +1996,27 @@ fn avro_rs_397_derive_with_expr_lambda() {
     assert_eq!(expected_schema, derived_schema);
 }
 
+#[test]
+fn avro_rs_398_transparent_with_skip() {
+    fn long_schema(_named_schemas: &mut Names, _enclosing_namespace: 
&Namespace) -> Schema {
+        Schema::Long
+    }
+
+    #[allow(dead_code)]
+    #[derive(AvroSchema)]
+    #[serde(transparent)]
+    struct Foo {
+        #[serde(skip)]
+        a: String,
+        #[avro(with = long_schema)]
+        b: i32,
+        #[serde(skip)]
+        c: String,
+    }
+
+    assert_eq!(Schema::Long, Foo::get_schema());
+}
+
 #[test]
 fn avro_rs_401_do_not_match_typename() {
     #[expect(nonstandard_style, reason = "It needs to be exactly this")]
diff --git a/avro_derive/tests/serde.rs b/avro_derive/tests/serde.rs
index 5283b5e..043a245 100644
--- a/avro_derive/tests/serde.rs
+++ b/avro_derive/tests/serde.rs
@@ -314,6 +314,69 @@ mod container_attributes {
             "Invalid field name c",
         );
     }
+
+    #[test]
+    fn avro_rs_398_transparent() {
+        #[derive(Debug, Serialize, Deserialize, AvroSchema, Clone, PartialEq)]
+        #[serde(transparent)]
+        struct Foo {
+            a: String,
+        }
+
+        let schema = r#"
+        {
+            "type":"string"
+        }
+        "#;
+
+        let schema = Schema::parse_str(schema).unwrap();
+        assert_eq!(schema, Foo::get_schema());
+
+        serde_assert(Foo {
+            a: "spam".to_string(),
+        });
+    }
+
+    #[test]
+    fn avro_rs_398_transparent_ref() {
+        #[derive(Debug, Serialize, Deserialize, AvroSchema, Clone, PartialEq)]
+        #[serde(transparent)]
+        struct Foo<'a> {
+            a: &'a str,
+        }
+
+        let schema = r#"
+        {
+            "type":"string"
+        }
+        "#;
+
+        let schema = Schema::parse_str(schema).unwrap();
+        assert_eq!(schema, Foo::get_schema());
+    }
+
+    #[test]
+    fn avro_rs_398_transparent_array() {
+        #[derive(Debug, Serialize, Deserialize, AvroSchema, Clone, PartialEq)]
+        #[serde(transparent)]
+        struct Foo {
+            a: Vec<String>,
+        }
+
+        let schema = r#"
+        {
+            "type":"array",
+            "items":"string"
+        }
+        "#;
+
+        let schema = Schema::parse_str(schema).unwrap();
+        assert_eq!(schema, Foo::get_schema());
+
+        serde_assert(Foo {
+            a: vec!["spam".to_string()],
+        });
+    }
 }
 
 mod variant_attributes {
diff --git a/avro_derive/tests/ui/avro_rs_373_rename_all_fields.stderr 
b/avro_derive/tests/ui/avro_rs_373_rename_all_fields.stderr
index 6784672..7a5a531 100644
--- a/avro_derive/tests/ui/avro_rs_373_rename_all_fields.stderr
+++ b/avro_derive/tests/ui/avro_rs_373_rename_all_fields.stderr
@@ -1,4 +1,4 @@
-error: AvroSchema derive does not work for enums with non unit structs
+error: AvroSchema: derive does not work for enums with non unit structs
   --> tests/ui/avro_rs_373_rename_all_fields.rs:22:6
    |
 22 | enum Foo {
diff --git a/avro_derive/tests/ui/avro_rs_373_transparent.stderr 
b/avro_derive/tests/ui/avro_rs_373_transparent.stderr
deleted file mode 100644
index d300405..0000000
--- a/avro_derive/tests/ui/avro_rs_373_transparent.stderr
+++ /dev/null
@@ -1,8 +0,0 @@
-error: AvroSchema derive does not support Serde `transparent` attribute
-  --> tests/ui/avro_rs_373_transparent.rs:27:1
-   |
-27 | / #[serde(transparent)]
-28 | | struct Bar {
-29 | |     foo: Foo,
-30 | | }
-   | |_^
diff --git a/avro_derive/tests/ui/avro_rs_373_transparent.rs 
b/avro_derive/tests/ui/avro_rs_398_transparent.rs
similarity index 96%
copy from avro_derive/tests/ui/avro_rs_373_transparent.rs
copy to avro_derive/tests/ui/avro_rs_398_transparent.rs
index 83fe2b1..3b6ba49 100644
--- a/avro_derive/tests/ui/avro_rs_373_transparent.rs
+++ b/avro_derive/tests/ui/avro_rs_398_transparent.rs
@@ -18,6 +18,7 @@
 use apache_avro::AvroSchema;
 
 #[derive(AvroSchema)]
+#[serde(transparent)]
 struct Foo {
     a: String,
     b: i32,
@@ -25,8 +26,8 @@ struct Foo {
 
 #[derive(AvroSchema)]
 #[serde(transparent)]
-struct Bar {
-    foo: Foo,
+enum Bar {
+    A
 }
 
 pub fn main() {}
diff --git a/avro_derive/tests/ui/avro_rs_398_transparent.stderr 
b/avro_derive/tests/ui/avro_rs_398_transparent.stderr
new file mode 100644
index 0000000..617dc43
--- /dev/null
+++ b/avro_derive/tests/ui/avro_rs_398_transparent.stderr
@@ -0,0 +1,18 @@
+error: AvroSchema: #[serde(transparent)] is only allowed on structs with one 
unskipped field
+  --> tests/ui/avro_rs_398_transparent.rs:21:1
+   |
+21 | / #[serde(transparent)]
+22 | | struct Foo {
+23 | |     a: String,
+24 | |     b: i32,
+25 | | }
+   | |_^
+
+error: AvroSchema: `#[serde(transparent)]` is only supported on structs
+  --> tests/ui/avro_rs_398_transparent.rs:28:1
+   |
+28 | / #[serde(transparent)]
+29 | | enum Bar {
+30 | |     A
+31 | | }
+   | |_^
diff --git a/avro_derive/tests/ui/avro_rs_373_transparent.rs 
b/avro_derive/tests/ui/avro_rs_398_transparent_skips.rs
similarity index 92%
rename from avro_derive/tests/ui/avro_rs_373_transparent.rs
rename to avro_derive/tests/ui/avro_rs_398_transparent_skips.rs
index 83fe2b1..7f324f6 100644
--- a/avro_derive/tests/ui/avro_rs_373_transparent.rs
+++ b/avro_derive/tests/ui/avro_rs_398_transparent_skips.rs
@@ -18,15 +18,14 @@
 use apache_avro::AvroSchema;
 
 #[derive(AvroSchema)]
+#[serde(transparent)]
 struct Foo {
+    #[serde(skip)]
     a: String,
+    #[serde(skip)]
     b: i32,
-}
-
-#[derive(AvroSchema)]
-#[serde(transparent)]
-struct Bar {
-    foo: Foo,
+    #[serde(skip)]
+    c: Vec<u8>
 }
 
 pub fn main() {}
diff --git a/avro_derive/tests/ui/avro_rs_398_transparent_skips.stderr 
b/avro_derive/tests/ui/avro_rs_398_transparent_skips.stderr
new file mode 100644
index 0000000..21eb874
--- /dev/null
+++ b/avro_derive/tests/ui/avro_rs_398_transparent_skips.stderr
@@ -0,0 +1,11 @@
+error: AvroSchema: #[serde(transparent)] is only allowed on structs with one 
unskipped field
+  --> tests/ui/avro_rs_398_transparent_skips.rs:21:1
+   |
+21 | / #[serde(transparent)]
+22 | | struct Foo {
+23 | |     #[serde(skip)]
+24 | |     a: String,
+...  |
+28 | |     c: Vec<u8>
+29 | | }
+   | |_^


Reply via email to