This is an automated email from the ASF dual-hosted git repository. kriskras99 pushed a commit to branch feat/serde_transparent in repository https://gitbox.apache.org/repos/asf/avro-rs.git
commit faf2db64ea7b0046bdb8900bc6f6966af649fa8a Author: Kriskras99 <[email protected]> AuthorDate: Sat Jan 10 20:40:16 2026 +0100 feat(derive): Support `#[serde(transparent)]` --- avro_derive/src/attributes/mod.rs | 22 ++- avro_derive/src/lib.rs | 200 +++++++++++++++------ avro_derive/tests/serde.rs | 22 +++ avro_derive/tests/ui/avro_rs_373_transparent.rs | 5 +- .../tests/ui/avro_rs_373_transparent.stderr | 22 ++- 5 files changed, 198 insertions(+), 73 deletions(-) diff --git a/avro_derive/src/attributes/mod.rs b/avro_derive/src/attributes/mod.rs index 6f26a62..d7de7da 100644 --- a/avro_derive/src/attributes/mod.rs +++ b/avro_derive/src/attributes/mod.rs @@ -30,6 +30,7 @@ pub struct NamedTypeOptions { pub doc: Option<String>, pub alias: Vec<String>, pub rename_all: RenameRule, + pub transparent: bool, } impl NamedTypeOptions { @@ -63,12 +64,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,6 +84,19 @@ 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.namespace.is_some() + || avro.doc.is_some() + || !avro.alias.is_empty() + || serde.rename_all.serialize != RenameRule::None + || serde.rename_all.deserialize != RenameRule::None) + { + errors.push(syn::Error::new( + span, + "#[serde(transparent)] is incompatible with all other attributes", + )); + } if !errors.is_empty() { return Err(errors); @@ -100,6 +108,7 @@ impl NamedTypeOptions { doc: avro.doc, alias: avro.alias, rename_all: serde.rename_all.serialize, + transparent: serde.transparent, }) } } @@ -145,6 +154,7 @@ impl VariantOptions { } } +#[derive(Default, PartialEq, Eq)] pub struct FieldOptions { pub doc: Option<String>, pub default: Option<String>, diff --git a/avro_derive/src/lib.rs b/avro_derive/src/lib.rs index 8723593..bed6a04 100644 --- a/avro_derive/src/lib.rs +++ b/avro_derive/src/lib.rs @@ -44,71 +44,85 @@ pub fn proc_macro_derive_avro_schema(input: proc_macro::TokenStream) -> proc_mac 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 ", - )]); - } - }; - let ident = &input.ident; - let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl(); - Ok(quote! { - #[automatically_derived] - impl #impl_generics apache_avro::schema::derive::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(&format!("Unable to parse schema name {}", #full_schema_name)[..]).fully_qualified_name(enclosing_namespace); - let enclosing_namespace = &name.namespace; - if named_schemas.contains_key(&name) { - apache_avro::schema::Schema::Ref{name: name.clone()} - } else { - named_schemas.insert(name.clone(), apache_avro::schema::Schema::Ref{name: name.clone()}); + if named_type_options.transparent { + let schema_def = get_transparent_data_schema_def(&input.data, input.span())?; + let ident = &input.ident; + let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl(); + Ok(quote! { + #[automatically_derived] + impl #impl_generics apache_avro::schema::derive::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 { #schema_def } } - } - }) + }) + } else { + 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(data_struct) => get_data_struct_schema_def( + &full_schema_name, + named_type_options + .doc + .or_else(|| extract_outer_doc(&input.attrs)), + named_type_options.alias, + named_type_options.rename_all, + data_struct, + input.ident.span(), + )?, + syn::Data::Enum(data_enum) => get_data_enum_schema_def( + &full_schema_name, + named_type_options + .doc + .or_else(|| extract_outer_doc(&input.attrs)), + named_type_options.alias, + named_type_options.rename_all, + data_enum, + input.ident.span(), + )?, + syn::Data::Union(_) => { + return Err(vec![syn::Error::new( + input.ident.span(), + "AvroSchema derive only works for structs and simple enums", + )]); + } + }; + let ident = &input.ident; + let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl(); + Ok(quote! { + #[automatically_derived] + impl #impl_generics apache_avro::schema::derive::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(&format!("Unable to parse schema name {}", #full_schema_name)[..]).fully_qualified_name(enclosing_namespace); + let enclosing_namespace = &name.namespace; + if named_schemas.contains_key(&name) { + apache_avro::schema::Schema::Ref{name: name.clone()} + } else { + named_schemas.insert(name.clone(), apache_avro::schema::Schema::Ref{name: name.clone()}); + #schema_def + } + } + } + }) + } } +/// Generate a schema definition for a struct. 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, + data_struct: &syn::DataStruct, + ident_span: Span, ) -> Result<TokenStream, Vec<syn::Error>> { let mut record_field_exprs = vec![]; - match s.fields { + match data_struct.fields { syn::Fields::Named(ref a) => { for field in a.named.iter() { let mut name = field @@ -184,13 +198,13 @@ fn get_data_struct_schema_def( } syn::Fields::Unnamed(_) => { return Err(vec![syn::Error::new( - error_span, + ident_span, "AvroSchema derive does not work for tuple structs", )]); } syn::Fields::Unit => { return Err(vec![syn::Error::new( - error_span, + ident_span, "AvroSchema derive does not work for unit structs", )]); } @@ -221,21 +235,26 @@ fn get_data_struct_schema_def( }) } +/// 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, + data_enum: &syn::DataEnum, error_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)?; + if data_enum + .variants + .iter() + .all(|v| syn::Fields::Unit == v.fields) + { + let default_value = default_enum_variant(data_enum, error_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) { (Some(rename), _) => rename, @@ -264,6 +283,69 @@ fn get_data_enum_schema_def( } } +/// Generate a schema definition for a type marked transparent. +fn get_transparent_data_schema_def( + data: &syn::Data, + input_span: Span, +) -> Result<TokenStream, Vec<syn::Error>> { + match data { + syn::Data::Struct(data_struct) => match &data_struct.fields { + syn::Fields::Named(fields_named) => { + if fields_named.named.len() != 1 { + return Err(vec![syn::Error::new( + input_span, + "#[serde(transparent)] is only allowed on structs with one field", + )]); + } + let field = fields_named + .named + .first() + .expect("There is exactly one field"); + let field_attrs = FieldOptions::new(&field.attrs, field.span())?; + if field_attrs != FieldOptions::default() { + return Err(vec![syn::Error::new( + input_span, + "#[serde(transparent)] is incompatible with all other attributes", + )]); + } + let ty = &field.ty; + Ok(quote! { + #ty::get_schema_in_ctxt(named_schemas, enclosing_namespace) + }) + } + syn::Fields::Unnamed(_) => Err(vec![syn::Error::new( + input_span, + "AvroSchema derive does not work for tuple structs", + )]), + syn::Fields::Unit => Err(vec![syn::Error::new( + input_span, + "AvroSchema derive does not work for unit structs", + )]), + }, + syn::Data::Enum(data_enum) => { + if data_enum + .variants + .iter() + .all(|v| syn::Fields::Unit == v.fields) + { + Err(vec![syn::Error::new( + input_span, + "AvroSchema derive does not support #[serde(transparent)] on simple enums", + )]) + } else { + Err(vec![syn::Error::new( + input_span, + "AvroSchema derive does not work for enums with non unit structs", + )]) + } + } + syn::Data::Union(_) => Err(vec![syn::Error::new( + input_span, + "AvroSchema derive only works for structs and simple enums", + )]), + } +} + /// Takes in the Tokens of a type and returns the tokens of an expression with return type `Schema` fn type_to_schema_expr(ty: &Type) -> Result<TokenStream, Vec<syn::Error>> { if let Type::Path(p) = ty { diff --git a/avro_derive/tests/serde.rs b/avro_derive/tests/serde.rs index f42e97b..63974ef 100644 --- a/avro_derive/tests/serde.rs +++ b/avro_derive/tests/serde.rs @@ -314,6 +314,28 @@ 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(), + }); + } } mod variant_attributes { diff --git a/avro_derive/tests/ui/avro_rs_373_transparent.rs b/avro_derive/tests/ui/avro_rs_373_transparent.rs index 83fe2b1..3b6ba49 100644 --- a/avro_derive/tests/ui/avro_rs_373_transparent.rs +++ b/avro_derive/tests/ui/avro_rs_373_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_373_transparent.stderr b/avro_derive/tests/ui/avro_rs_373_transparent.stderr index d300405..025f4fd 100644 --- a/avro_derive/tests/ui/avro_rs_373_transparent.stderr +++ b/avro_derive/tests/ui/avro_rs_373_transparent.stderr @@ -1,8 +1,18 @@ -error: AvroSchema derive does not support Serde `transparent` attribute - --> tests/ui/avro_rs_373_transparent.rs:27:1 +error: #[serde(transparent)] is only allowed on structs with one field + --> tests/ui/avro_rs_373_transparent.rs:21:1 | -27 | / #[serde(transparent)] -28 | | struct Bar { -29 | | foo: Foo, -30 | | } +21 | / #[serde(transparent)] +22 | | struct Foo { +23 | | a: String, +24 | | b: i32, +25 | | } + | |_^ + +error: AvroSchema derive does not support #[serde(transparent)] on simple enums + --> tests/ui/avro_rs_373_transparent.rs:28:1 + | +28 | / #[serde(transparent)] +29 | | enum Bar { +30 | | A +31 | | } | |_^
