This is an automated email from the ASF dual-hosted git repository. kriskras99 pushed a commit to branch fix/avro_schema_flatten in repository https://gitbox.apache.org/repos/asf/avro-rs.git
commit 79b30c71ee879ccdca0ad00875f8d1c084bd6396 Author: Kriskras99 <[email protected]> AuthorDate: Wed Jan 28 22:44:38 2026 +0100 fix: `flatten` no longer causes duplicate names When a type was used both via `flatten` and directly, the schema generated would contain duplicate names (and schemas). This is because `flatten` would use an empty `named_schemas` to get the record schema. If the existing `named_schemas` was used, `flatten` might get a `Schema::Ref` if the type was already used. Or when `flatten` was used first, if the type was used after it would create a `Schema::Ref` to a schema that does not exist. This is solved by adding a new function to the `AvroSchemaComponent` that returns the fields directly. To not break code currently implementing this trait, it has a default implementation that will work around the issues above. This default implementation is also used for fields with the `#[avro(with = ||)]` and `#[avro(with = path)]` attributes, as they don't have a way to provide the field directly. Users of `#[avro(with)]` will need to implement `get_record_fields_in_ctxt` in their module. --- avro/src/serde/derive.rs | 205 +++++++++++++++++++++++++++++++++++++- avro/src/serde/mod.rs | 3 + avro_derive/src/attributes/mod.rs | 2 +- avro_derive/src/lib.rs | 138 ++++++++++++++++++++----- avro_derive/tests/derive.rs | 62 ++++++++++++ 5 files changed, 383 insertions(+), 27 deletions(-) diff --git a/avro/src/serde/derive.rs b/avro/src/serde/derive.rs index b51cee7..5abd650 100644 --- a/avro/src/serde/derive.rs +++ b/avro/src/serde/derive.rs @@ -16,7 +16,9 @@ // under the License. use crate::Schema; -use crate::schema::{FixedSchema, Name, Names, Namespace, UnionSchema, UuidSchema}; +use crate::schema::{ + FixedSchema, Name, Names, Namespace, RecordField, RecordSchema, UnionSchema, UuidSchema, +}; use serde_json::Map; use std::borrow::Cow; use std::collections::HashMap; @@ -83,6 +85,122 @@ pub trait AvroSchema { /// ``` pub trait AvroSchemaComponent { fn get_schema_in_ctxt(named_schemas: &mut Names, enclosing_namespace: &Namespace) -> Schema; + + /// Get the fields of this schema if it is a record. + /// + /// This returns `None` if the schema is not a record. + /// + /// The default implementation has to do a lot of extra work, so it is strongly recommended to + /// implement this function when manually implementing this trait. + fn get_record_fields_in_ctxt( + named_schemas: &mut Names, + enclosing_namespace: &Namespace, + ) -> Option<Vec<RecordField>> { + get_record_fields_in_ctxt(named_schemas, enclosing_namespace, Self::get_schema_in_ctxt) + } +} + +/// This is public so the derive macro can use it for `#[avro(with = ||)]` and `#[avro(with = path)]` +pub fn get_record_fields_in_ctxt( + named_schemas: &mut Names, + enclosing_namespace: &Namespace, + schema_fn: fn(named_schemas: &mut Names, enclosing_namespace: &Namespace) -> Schema, +) -> Option<Vec<RecordField>> { + let mut record = match schema_fn(named_schemas, enclosing_namespace) { + Schema::Record(record) => record, + Schema::Ref { name } => { + // This schema already exists in `named_schemas` so temporarily remove it so we can + // get the actual schema. + let temp = named_schemas + .remove(&name) + .expect("Name should exist in `named_schemas` otherwise Ref is invalid"); + // Get the schema + let schema = schema_fn(named_schemas, enclosing_namespace); + // Reinsert the old value + named_schemas.insert(name, temp); + + // Now check if we actually got a record and return the fields if that is the case + let Schema::Record(record) = schema else { + return None; + }; + return Some(record.fields); + } + _ => return None, + }; + // This schema did not yet exist in `named_schemas`, so we need to remove it if and only if + // it isn't used somewhere in the schema (recursive type). + + // Find the first Schema::Ref that has the target name + fn find_first_ref<'a>(schema: &'a mut Schema, target: &Name) -> Option<&'a mut Schema> { + match schema { + Schema::Ref { name } if name == target => Some(schema), + Schema::Array(array) => find_first_ref(&mut array.items, target), + Schema::Map(map) => find_first_ref(&mut map.types, target), + Schema::Union(union) => { + for schema in &mut union.schemas { + if let Some(schema) = find_first_ref(schema, target) { + return Some(schema); + } + } + None + } + Schema::Record(record) => { + assert_ne!( + &record.name, target, + "Only expecting a Ref named {target:?}" + ); + for field in &mut record.fields { + if let Some(schema) = find_first_ref(&mut field.schema, target) { + return Some(schema); + } + } + None + } + _ => None, + } + } + + // Prepare the fields for the new record. All named types will become references. + let new_fields = record + .fields + .iter() + .map(|field| RecordField { + name: field.name.clone(), + doc: field.doc.clone(), + aliases: field.aliases.clone(), + default: field.default.clone(), + schema: if field.schema.is_named() { + Schema::Ref { + name: field.schema.name().expect("Schema is named").clone(), + } + } else { + field.schema.clone() + }, + order: field.order.clone(), + position: field.position, + custom_attributes: field.custom_attributes.clone(), + }) + .collect(); + + // Find the first reference to this schema so we can replace it with the actual schema + for field in &mut record.fields { + if let Some(schema) = find_first_ref(&mut field.schema, &record.name) { + let new_schema = RecordSchema { + name: record.name, + aliases: record.aliases, + doc: record.doc, + fields: new_fields, + lookup: record.lookup, + attributes: record.attributes, + }; + + let _old = std::mem::replace(schema, Schema::Record(new_schema)); + + break; + } + } + + Some(record.fields) } impl<T> AvroSchema for T @@ -100,6 +218,10 @@ macro_rules! impl_schema ( fn get_schema_in_ctxt(_: &mut Names, _: &Namespace) -> Schema { $variant_constructor } + + fn get_record_fields_in_ctxt(_: &mut Names, _: &Namespace) -> Option<Vec<RecordField>> { + None + } } ); ); @@ -125,6 +247,13 @@ where fn get_schema_in_ctxt(named_schemas: &mut Names, enclosing_namespace: &Namespace) -> Schema { T::get_schema_in_ctxt(named_schemas, enclosing_namespace) } + + fn get_record_fields_in_ctxt( + named_schemas: &mut Names, + enclosing_namespace: &Namespace, + ) -> Option<Vec<RecordField>> { + T::get_record_fields_in_ctxt(named_schemas, enclosing_namespace) + } } impl<T> AvroSchemaComponent for &mut T @@ -134,6 +263,13 @@ where fn get_schema_in_ctxt(named_schemas: &mut Names, enclosing_namespace: &Namespace) -> Schema { T::get_schema_in_ctxt(named_schemas, enclosing_namespace) } + + fn get_record_fields_in_ctxt( + named_schemas: &mut Names, + enclosing_namespace: &Namespace, + ) -> Option<Vec<RecordField>> { + T::get_record_fields_in_ctxt(named_schemas, enclosing_namespace) + } } impl<T> AvroSchemaComponent for [T] @@ -143,6 +279,10 @@ where fn get_schema_in_ctxt(named_schemas: &mut Names, enclosing_namespace: &Namespace) -> Schema { Schema::array(T::get_schema_in_ctxt(named_schemas, enclosing_namespace)) } + + fn get_record_fields_in_ctxt(_: &mut Names, _: &Namespace) -> Option<Vec<RecordField>> { + None + } } impl<const N: usize, T> AvroSchemaComponent for [T; N] @@ -152,6 +292,10 @@ where fn get_schema_in_ctxt(named_schemas: &mut Names, enclosing_namespace: &Namespace) -> Schema { Schema::array(T::get_schema_in_ctxt(named_schemas, enclosing_namespace)) } + + fn get_record_fields_in_ctxt(_: &mut Names, _: &Namespace) -> Option<Vec<RecordField>> { + None + } } impl<T> AvroSchemaComponent for Vec<T> @@ -161,6 +305,10 @@ where fn get_schema_in_ctxt(named_schemas: &mut Names, enclosing_namespace: &Namespace) -> Schema { Schema::array(T::get_schema_in_ctxt(named_schemas, enclosing_namespace)) } + + fn get_record_fields_in_ctxt(_: &mut Names, _: &Namespace) -> Option<Vec<RecordField>> { + None + } } impl<T> AvroSchemaComponent for Option<T> @@ -177,6 +325,10 @@ where UnionSchema::new(variants).expect("Option<T> must produce a valid (non-nested) union"), ) } + + fn get_record_fields_in_ctxt(_: &mut Names, _: &Namespace) -> Option<Vec<RecordField>> { + None + } } impl<T> AvroSchemaComponent for Map<String, T> @@ -186,6 +338,10 @@ where fn get_schema_in_ctxt(named_schemas: &mut Names, enclosing_namespace: &Namespace) -> Schema { Schema::map(T::get_schema_in_ctxt(named_schemas, enclosing_namespace)) } + + fn get_record_fields_in_ctxt(_: &mut Names, _: &Namespace) -> Option<Vec<RecordField>> { + None + } } impl<T> AvroSchemaComponent for HashMap<String, T> @@ -195,6 +351,10 @@ where fn get_schema_in_ctxt(named_schemas: &mut Names, enclosing_namespace: &Namespace) -> Schema { Schema::map(T::get_schema_in_ctxt(named_schemas, enclosing_namespace)) } + + fn get_record_fields_in_ctxt(_: &mut Names, _: &Namespace) -> Option<Vec<RecordField>> { + None + } } impl<T> AvroSchemaComponent for Box<T> @@ -204,6 +364,13 @@ where fn get_schema_in_ctxt(named_schemas: &mut Names, enclosing_namespace: &Namespace) -> Schema { T::get_schema_in_ctxt(named_schemas, enclosing_namespace) } + + fn get_record_fields_in_ctxt( + named_schemas: &mut Names, + enclosing_namespace: &Namespace, + ) -> Option<Vec<RecordField>> { + T::get_record_fields_in_ctxt(named_schemas, enclosing_namespace) + } } impl<T> AvroSchemaComponent for std::sync::Mutex<T> @@ -213,15 +380,29 @@ where fn get_schema_in_ctxt(named_schemas: &mut Names, enclosing_namespace: &Namespace) -> Schema { T::get_schema_in_ctxt(named_schemas, enclosing_namespace) } + + fn get_record_fields_in_ctxt( + named_schemas: &mut Names, + enclosing_namespace: &Namespace, + ) -> Option<Vec<RecordField>> { + T::get_record_fields_in_ctxt(named_schemas, enclosing_namespace) + } } impl<T> AvroSchemaComponent for Cow<'_, T> where - T: AvroSchemaComponent + Clone, + T: AvroSchemaComponent + ToOwned + ?Sized, { fn get_schema_in_ctxt(named_schemas: &mut Names, enclosing_namespace: &Namespace) -> Schema { T::get_schema_in_ctxt(named_schemas, enclosing_namespace) } + + fn get_record_fields_in_ctxt( + named_schemas: &mut Names, + enclosing_namespace: &Namespace, + ) -> Option<Vec<RecordField>> { + T::get_record_fields_in_ctxt(named_schemas, enclosing_namespace) + } } impl AvroSchemaComponent for core::time::Duration { @@ -248,6 +429,10 @@ impl AvroSchemaComponent for core::time::Duration { schema } } + + fn get_record_fields_in_ctxt(_: &mut Names, _: &Namespace) -> Option<Vec<RecordField>> { + None + } } impl AvroSchemaComponent for uuid::Uuid { @@ -274,6 +459,10 @@ impl AvroSchemaComponent for uuid::Uuid { schema } } + + fn get_record_fields_in_ctxt(_: &mut Names, _: &Namespace) -> Option<Vec<RecordField>> { + None + } } impl AvroSchemaComponent for u64 { @@ -298,6 +487,10 @@ impl AvroSchemaComponent for u64 { schema } } + + fn get_record_fields_in_ctxt(_: &mut Names, _: &Namespace) -> Option<Vec<RecordField>> { + None + } } impl AvroSchemaComponent for u128 { @@ -322,6 +515,10 @@ impl AvroSchemaComponent for u128 { schema } } + + fn get_record_fields_in_ctxt(_: &mut Names, _: &Namespace) -> Option<Vec<RecordField>> { + None + } } impl AvroSchemaComponent for i128 { @@ -346,6 +543,10 @@ impl AvroSchemaComponent for i128 { schema } } + + fn get_record_fields_in_ctxt(_: &mut Names, _: &Namespace) -> Option<Vec<RecordField>> { + None + } } #[cfg(test)] diff --git a/avro/src/serde/mod.rs b/avro/src/serde/mod.rs index 9c1dea4..2a62b33 100644 --- a/avro/src/serde/mod.rs +++ b/avro/src/serde/mod.rs @@ -26,3 +26,6 @@ pub use de::from_value; pub use derive::{AvroSchema, AvroSchemaComponent}; pub use ser::to_value; pub use with::{bytes, bytes_opt, fixed, fixed_opt, slice, slice_opt}; + +#[doc(hidden)] +pub use derive::get_record_fields_in_ctxt; diff --git a/avro_derive/src/attributes/mod.rs b/avro_derive/src/attributes/mod.rs index ecf2797..cc259f1 100644 --- a/avro_derive/src/attributes/mod.rs +++ b/avro_derive/src/attributes/mod.rs @@ -169,7 +169,7 @@ impl VariantOptions { } /// How to get the schema for this field or variant. -#[derive(Debug, PartialEq, Default)] +#[derive(Debug, PartialEq, Default, Clone)] pub enum With { /// Use `<T as AvroSchemaComponent>::get_schema_in_ctxt`. #[default] diff --git a/avro_derive/src/lib.rs b/avro_derive/src/lib.rs index 8c49d05..32be337 100644 --- a/avro_derive/src/lib.rs +++ b/avro_derive/src/lib.rs @@ -48,14 +48,22 @@ fn derive_avro_schema(input: DeriveInput) -> Result<TokenStream, Vec<syn::Error> 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 { + let (get_schema_impl, get_record_fields_impl) = if named_type_options.transparent { get_transparent_struct_schema_def(data_struct.fields, input_span)? } else { - let schema_def = + let (schema_def, record_fields) = get_struct_schema_def(&named_type_options, data_struct, input.ident.span())?; - handle_named_schemas(named_type_options.name, schema_def) + ( + handle_named_schemas(named_type_options.name, schema_def), + record_fields, + ) }; - Ok(create_trait_definition(input.ident, &input.generics, inner)) + Ok(create_trait_definition( + input.ident, + &input.generics, + get_schema_impl, + get_record_fields_impl, + )) } syn::Data::Enum(data_enum) => { let named_type_options = NamedTypeOptions::new(&input.ident, &input.attrs, input_span)?; @@ -68,7 +76,12 @@ fn derive_avro_schema(input: DeriveInput) -> Result<TokenStream, Vec<syn::Error> 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)) + Ok(create_trait_definition( + input.ident, + &input.generics, + inner, + quote! { None }, + )) } syn::Data::Union(_) => Err(vec![syn::Error::new( input_span, @@ -81,14 +94,19 @@ fn derive_avro_schema(input: DeriveInput) -> Result<TokenStream, Vec<syn::Error> fn create_trait_definition( ident: Ident, generics: &Generics, - implementation: TokenStream, + get_schema_impl: TokenStream, + get_record_fields_impl: 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 apache_avro::schema::Names, enclosing_namespace: &Option<String>) -> apache_avro::schema::Schema { - #implementation + #get_schema_impl + } + + fn get_record_fields_in_ctxt(named_schemas: &mut apache_avro::schema::Names, enclosing_namespace: &Option<String>) -> Option<std::vec::Vec<apache_avro::schema::RecordField>> { + #get_record_fields_impl } } } @@ -117,7 +135,7 @@ fn get_struct_schema_def( container_attrs: &NamedTypeOptions, data_struct: DataStruct, ident_span: Span, -) -> Result<TokenStream, Vec<syn::Error>> { +) -> Result<(TokenStream, TokenStream), Vec<syn::Error>> { let mut record_field_exprs = vec![]; match data_struct.fields { Fields::Named(a) => { @@ -146,15 +164,13 @@ fn get_struct_schema_def( } else if field_attrs.flatten { // Inline the fields of the child record at runtime, as we don't have access to // the schema here. - let flatten_ty = &field.ty; + let get_record_fields = + get_field_get_record_fields_expr(&field, field_attrs.with)?; record_field_exprs.push(quote! { - if let ::apache_avro::schema::Schema::Record(::apache_avro::schema::RecordSchema { fields, .. }) = #flatten_ty::get_schema() { - for mut field in fields { - field.position = schema_fields.len(); - schema_fields.push(field) - } + if let Some(flattened_fields) = #get_record_fields { + schema_fields.extend(flattened_fields) } else { - panic!("Can only flatten RecordSchema, got {:?}", #flatten_ty::get_schema()) + panic!("#field does not have any fields to flatten to") } }); @@ -214,7 +230,7 @@ fn get_struct_schema_def( // the most common case where there is no flatten. let minimum_fields = record_field_exprs.len(); - Ok(quote! { + let schema_def = quote! { { let mut schema_fields = Vec::with_capacity(#minimum_fields); #(#record_field_exprs)* @@ -234,14 +250,21 @@ fn get_struct_schema_def( attributes: Default::default(), }) } - }) + }; + let record_fields = quote! { + let mut schema_fields = Vec::with_capacity(#minimum_fields); + #(#record_field_exprs)* + Some(schema_fields) + }; + + Ok((schema_def, record_fields)) } /// 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>> { +) -> Result<(TokenStream, TokenStream), Vec<syn::Error>> { match fields { Fields::Named(fields_named) => { let mut found = None; @@ -259,7 +282,10 @@ fn get_transparent_struct_schema_def( } if let Some((field, attrs)) = found { - get_field_schema_expr(&field, attrs.with) + Ok(( + get_field_schema_expr(&field, attrs.with.clone())?, + get_field_get_record_fields_expr(&field, attrs.with)?, + )) } else { Err(vec![syn::Error::new( input_span, @@ -302,6 +328,41 @@ fn get_field_schema_expr(field: &Field, with: With) -> Result<TokenStream, Vec<s } } +fn get_field_get_record_fields_expr( + field: &Field, + with: With, +) -> Result<TokenStream, Vec<syn::Error>> { + match with { + With::Trait => Ok(type_to_get_record_fields_expr(&field.ty)?), + With::Serde(path) => { + Ok(quote! { #path::get_record_fields_in_ctxt(named_schemas, enclosing_namespace) }) + } + With::Expr(Expr::Closure(closure)) => { + if closure.inputs.is_empty() { + Ok(quote! { + ::apache_avro::serde::get_record_fields_in_ctxt( + named_schemas, + enclosing_namespace, + |_, _| (#closure)(), + ) + }) + } else { + Err(vec![syn::Error::new( + field.span(), + "Expected closure with 0 parameters", + )]) + } + } + With::Expr(Expr::Path(path)) => Ok(quote! { + ::apache_avro::serde::get_record_fields_in_ctxt(named_schemas, enclosing_namespace, #path) + }), + 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( container_attrs: &NamedTypeOptions, @@ -367,6 +428,28 @@ fn type_to_schema_expr(ty: &Type) -> Result<TokenStream, Vec<syn::Error>> { } } +fn type_to_get_record_fields_expr(ty: &Type) -> Result<TokenStream, Vec<syn::Error>> { + match ty { + Type::Array(_) | Type::Slice(_) | Type::Path(_) | Type::Reference(_) => Ok( + quote! {<#ty as apache_avro::AvroSchemaComponent>::get_record_fields_in_ctxt(named_schemas, enclosing_namespace)}, + ), + Type::Ptr(_) => Err(vec![syn::Error::new_spanned( + ty, + "AvroSchema: derive does not support raw pointers", + )]), + Type::Tuple(_) => Err(vec![syn::Error::new_spanned( + ty, + "AvroSchema: derive does not support tuples", + )]), + _ => Err(vec![syn::Error::new_spanned( + ty, + format!( + "AvroSchema: Unexpected type encountered! Please open an issue if this kind of type should be supported: {ty:?}" + ), + )]), + } +} + fn default_enum_variant( data_enum: &syn::DataEnum, error_span: Span, @@ -564,6 +647,13 @@ mod tests { schema } } + + fn get_record_fields_in_ctxt( + named_schemas: &mut apache_avro::schema::Names, + enclosing_namespace: &Option<String> + ) -> Option <std::vec::Vec<apache_avro::schema::RecordField>> { + None + } } }.to_string()); } @@ -690,7 +780,7 @@ mod tests { match syn::parse2::<DeriveInput>(test_struct) { 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 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); } @@ -709,7 +799,7 @@ mod tests { match syn::parse2::<DeriveInput>(test_enum) { 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 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); } @@ -732,7 +822,7 @@ mod tests { match syn::parse2::<DeriveInput>(test_struct) { 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 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); } @@ -752,7 +842,7 @@ mod tests { match syn::parse2::<DeriveInput>(test_enum) { 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 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); } @@ -776,7 +866,7 @@ mod tests { match syn::parse2::<DeriveInput>(test_struct) { 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 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 4ee27f0..6f04c8d 100644 --- a/avro_derive/tests/derive.rs +++ b/avro_derive/tests/derive.rs @@ -2155,3 +2155,65 @@ fn avro_rs_414_round_trip_char_u64_u128_i128() { d: i128::MAX, }); } + +#[test] +fn avro_rs_xxx_transparent_recurring_type() { + #[derive(AvroSchema)] + #[expect(dead_code, reason = "Only testing derived schema")] + pub enum Color { + G, + } + + #[derive(AvroSchema)] + #[expect(dead_code, reason = "Only testing derived schema")] + pub struct A { + pub color: Color, + } + + #[derive(AvroSchema)] + #[expect(dead_code, reason = "Only testing derived schema")] + pub struct C { + #[serde(flatten)] + pub a: A, + } + + #[derive(AvroSchema)] + #[expect(dead_code, reason = "Only testing derived schema")] + pub struct TestStruct { + pub a: Color, + pub c: C, + } + + let schema = Schema::parse_str( + r#"{ + "name": "TestStruct", + "type":"record", + "fields": [ + { + "name":"a", + "type": { + "name": "Color", + "type": "enum", + "symbols": ["G"] + } + }, + { + "name":"c", + "type": { + "name":"C", + "type":"record", + "fields": [ + { + "name": "color", + "type": "Color" + } + ] + } + } + ] + }"#, + ) + .unwrap(); + + assert_eq!(TestStruct::get_schema(), schema); +}
