diff --git a/avro/src/serde/derive.rs b/avro/src/serde/derive.rs index b51cee71..b4e5fe22 100644 --- a/avro/src/serde/derive.rs +++ b/avro/src/serde/derive.rs @@ -16,8 +16,9 @@ // under the License. use crate::Schema; -use crate::schema::{FixedSchema, Name, Names, Namespace, UnionSchema, UuidSchema}; -use serde_json::Map; +use crate::schema::{ + FixedSchema, Name, Names, Namespace, RecordField, RecordSchema, UnionSchema, UuidSchema, +}; use std::borrow::Cow; use std::collections::HashMap; @@ -82,7 +83,134 @@ pub trait AvroSchema { ///} /// ``` pub trait AvroSchemaComponent { + /// Get the schema for this component 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> { + get_record_fields_in_ctxt(named_schemas, enclosing_namespace, Self::get_schema_in_ctxt) + } +} + +/// Get the record fields from `schema_fn` without polluting `named_schemas` or causing duplicate names +/// +/// 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> { + 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(); + + // Remove the name in case it is not used + named_schemas.remove(&record.name); + + // 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 Schema::Ref { name } = std::mem::replace(schema, Schema::Record(new_schema)) else { + panic!("Expected only Refs from find_first_ref"); + }; + + // The schema is used, so reinsert it + named_schemas.insert(name.clone(), Schema::Ref { name }); + + break; + } + } + + Some(record.fields) } impl AvroSchema for T @@ -100,6 +228,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> { + None + } } ); ); @@ -118,32 +250,44 @@ impl_schema!(String, Schema::String); impl_schema!(str, Schema::String); impl_schema!(char, Schema::String); -impl AvroSchemaComponent for &T -where - T: AvroSchemaComponent + ?Sized, -{ - fn get_schema_in_ctxt(named_schemas: &mut Names, enclosing_namespace: &Namespace) -> Schema { - T::get_schema_in_ctxt(named_schemas, enclosing_namespace) - } -} +macro_rules! impl_passthrough_schema ( + ($type:ty where T: AvroSchemaComponent + ?Sized $(+ $bound:tt)*) => ( + impl AvroSchemaComponent for $type { + fn get_schema_in_ctxt(named_schemas: &mut Names, enclosing_namespace: &Namespace) -> Schema { + T::get_schema_in_ctxt(named_schemas, enclosing_namespace) + } -impl AvroSchemaComponent for &mut T -where - T: AvroSchemaComponent + ?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> { + T::get_record_fields_in_ctxt(named_schemas, enclosing_namespace) + } + } + ); +); -impl AvroSchemaComponent for [T] -where - T: AvroSchemaComponent, -{ - fn get_schema_in_ctxt(named_schemas: &mut Names, enclosing_namespace: &Namespace) -> Schema { - Schema::array(T::get_schema_in_ctxt(named_schemas, enclosing_namespace)) - } -} +impl_passthrough_schema!(&T where T: AvroSchemaComponent + ?Sized); +impl_passthrough_schema!(&mut T where T: AvroSchemaComponent + ?Sized); +impl_passthrough_schema!(Box where T: AvroSchemaComponent + ?Sized); +impl_passthrough_schema!(Cow<'_, T> where T: AvroSchemaComponent + ?Sized + ToOwned); +impl_passthrough_schema!(std::sync::Mutex where T: AvroSchemaComponent + ?Sized); + +macro_rules! impl_array_schema ( + ($type:ty where T: AvroSchemaComponent) => ( + impl AvroSchemaComponent for $type { + 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> { + None + } + } + ); +); + +impl_array_schema!([T] where T: AvroSchemaComponent); +impl_array_schema!(Vec where T: AvroSchemaComponent); +// This doesn't work as the macro doesn't allow specifying the N parameter +// impl_array_schema!([T; N] where T: AvroSchemaComponent); impl AvroSchemaComponent for [T; N] where @@ -152,14 +296,22 @@ 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> { + None + } } -impl AvroSchemaComponent for Vec +impl AvroSchemaComponent for HashMap where T: AvroSchemaComponent, { fn get_schema_in_ctxt(named_schemas: &mut Names, enclosing_namespace: &Namespace) -> Schema { - Schema::array(T::get_schema_in_ctxt(named_schemas, enclosing_namespace)) + Schema::map(T::get_schema_in_ctxt(named_schemas, enclosing_namespace)) + } + + fn get_record_fields_in_ctxt(_: &mut Names, _: &Namespace) -> Option> { + None } } @@ -177,50 +329,9 @@ where UnionSchema::new(variants).expect("Option must produce a valid (non-nested) union"), ) } -} - -impl AvroSchemaComponent for Map -where - T: AvroSchemaComponent, -{ - fn get_schema_in_ctxt(named_schemas: &mut Names, enclosing_namespace: &Namespace) -> Schema { - Schema::map(T::get_schema_in_ctxt(named_schemas, enclosing_namespace)) - } -} - -impl AvroSchemaComponent for HashMap -where - T: AvroSchemaComponent, -{ - fn get_schema_in_ctxt(named_schemas: &mut Names, enclosing_namespace: &Namespace) -> Schema { - Schema::map(T::get_schema_in_ctxt(named_schemas, enclosing_namespace)) - } -} - -impl AvroSchemaComponent for Box -where - T: AvroSchemaComponent, -{ - fn get_schema_in_ctxt(named_schemas: &mut Names, enclosing_namespace: &Namespace) -> Schema { - T::get_schema_in_ctxt(named_schemas, enclosing_namespace) - } -} -impl AvroSchemaComponent for std::sync::Mutex -where - T: AvroSchemaComponent, -{ - fn get_schema_in_ctxt(named_schemas: &mut Names, enclosing_namespace: &Namespace) -> Schema { - T::get_schema_in_ctxt(named_schemas, enclosing_namespace) - } -} - -impl AvroSchemaComponent for Cow<'_, T> -where - T: AvroSchemaComponent + Clone, -{ - 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(_: &mut Names, _: &Namespace) -> Option> { + None } } @@ -248,6 +359,10 @@ impl AvroSchemaComponent for core::time::Duration { schema } } + + fn get_record_fields_in_ctxt(_: &mut Names, _: &Namespace) -> Option> { + None + } } impl AvroSchemaComponent for uuid::Uuid { @@ -274,6 +389,10 @@ impl AvroSchemaComponent for uuid::Uuid { schema } } + + fn get_record_fields_in_ctxt(_: &mut Names, _: &Namespace) -> Option> { + None + } } impl AvroSchemaComponent for u64 { @@ -298,6 +417,10 @@ impl AvroSchemaComponent for u64 { schema } } + + fn get_record_fields_in_ctxt(_: &mut Names, _: &Namespace) -> Option> { + None + } } impl AvroSchemaComponent for u128 { @@ -322,6 +445,10 @@ impl AvroSchemaComponent for u128 { schema } } + + fn get_record_fields_in_ctxt(_: &mut Names, _: &Namespace) -> Option> { + None + } } impl AvroSchemaComponent for i128 { @@ -346,6 +473,10 @@ impl AvroSchemaComponent for i128 { schema } } + + fn get_record_fields_in_ctxt(_: &mut Names, _: &Namespace) -> Option> { + None + } } #[cfg(test)] diff --git a/avro/src/serde/mod.rs b/avro/src/serde/mod.rs index 9c1dea44..2a62b335 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/tests/get_record_fields.rs b/avro/tests/get_record_fields.rs new file mode 100644 index 00000000..46ba4a88 --- /dev/null +++ b/avro/tests/get_record_fields.rs @@ -0,0 +1,82 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +use apache_avro::{ + Schema, + serde::{AvroSchemaComponent, get_record_fields_in_ctxt}, +}; +use std::collections::HashMap; + +use apache_avro_test_helper::TestResult; + +#[test] +fn avro_rs_448_default_get_record_fields_no_recursion() -> TestResult { + #[derive(apache_avro_derive::AvroSchema)] + struct Foo { + _a: i32, + _b: String, + } + + let mut named_schemas = HashMap::new(); + let fields = + get_record_fields_in_ctxt(&mut named_schemas, &None, Foo::get_schema_in_ctxt).unwrap(); + + assert_eq!(fields.len(), 2); + assert!(named_schemas.is_empty(), "Name shouldn't have been added"); + + // Insert Foo into named_schemas + let Schema::Record(_) = Foo::get_schema_in_ctxt(&mut named_schemas, &None) else { + panic!("This should be a record"); + }; + assert_eq!(named_schemas.len(), 1, "Name should have been added"); + + let fields = + get_record_fields_in_ctxt(&mut named_schemas, &None, Foo::get_schema_in_ctxt).unwrap(); + assert_eq!(fields.len(), 2); + assert_eq!(named_schemas.len(), 1, "Name shouldn't have been removed"); + + Ok(()) +} + +#[test] +fn avro_rs_448_default_get_record_fields_recursion() -> TestResult { + #[derive(apache_avro_derive::AvroSchema)] + struct Foo { + _a: i32, + _b: Option>, + } + + let mut named_schemas = HashMap::new(); + let fields = + get_record_fields_in_ctxt(&mut named_schemas, &None, Foo::get_schema_in_ctxt).unwrap(); + + assert_eq!(fields.len(), 2); + assert_eq!(named_schemas.len(), 1, "Name shouldn't have been removed"); + + // Insert Foo into named_schemas + let Schema::Ref { name: _ } = Foo::get_schema_in_ctxt(&mut named_schemas, &None) else { + panic!("This should be a reference") + }; + assert_eq!(named_schemas.len(), 1); + + let fields = + get_record_fields_in_ctxt(&mut named_schemas, &None, Foo::get_schema_in_ctxt).unwrap(); + assert_eq!(fields.len(), 2); + assert_eq!(named_schemas.len(), 1, "Name shouldn't have been removed"); + + Ok(()) +} diff --git a/avro_derive/src/attributes/mod.rs b/avro_derive/src/attributes/mod.rs index ecf27971..cc259f18 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 `::get_schema_in_ctxt`. #[default] diff --git a/avro_derive/src/lib.rs b/avro_derive/src/lib.rs index 8c49d05a..32be337b 100644 --- a/avro_derive/src/lib.rs +++ b/avro_derive/src/lib.rs @@ -48,14 +48,22 @@ fn derive_avro_schema(input: DeriveInput) -> Result 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 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 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) -> apache_avro::schema::Schema { - #implementation + #get_schema_impl + } + + fn get_record_fields_in_ctxt(named_schemas: &mut apache_avro::schema::Names, enclosing_namespace: &Option) -> Option> { + #get_record_fields_impl } } } @@ -117,7 +135,7 @@ fn get_struct_schema_def( container_attrs: &NamedTypeOptions, data_struct: DataStruct, ident_span: Span, -) -> Result> { +) -> Result<(TokenStream, TokenStream), Vec> { 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> { +) -> Result<(TokenStream, TokenStream), Vec> { 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 Result> { + 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> { } } +fn type_to_get_record_fields_expr(ty: &Type) -> Result> { + 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 + ) -> Option > { + None + } } }.to_string()); } @@ -690,7 +780,7 @@ mod tests { match syn::parse2::(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_avro :: schema :: Schema :: Ref { name } } else { let enclosing_namespace = & name . namespace ; named_schemas . insert (name . clone () , apache_avro :: schema :: Schema :: Ref { name : name . clone () }) ; let schema = { let mut schema_fields = Vec :: with_capacity (1usize) ; schema_fields . push (:: apache_avro :: schema :: RecordField { name : "a3" . to_string () , doc : Some ("a doc" . into ()) , default : Some (serde_json :: from_str ("123") . expect (format ! ("Invalid JSON: {:?}" , "123") . as_str ())) , aliases : Some (vec ! ["a1" . try_into () . expect ("Alias is invalid") , "a2" . try_into () . expect ("Alias is invalid")]) , schema : < i32 as apache_avro :: AvroSchemaComponent > :: get_schema_in_ctxt (named_schemas , enclosing_namespace) , order : :: apache_avro :: schema :: RecordFieldOrder :: Ascending , position : schema_fields . len () , custom_attributes : Default :: default () , }) ; let schema_field_set : :: std :: collections :: HashSet < _ > = schema_fields . iter () . map (| rf | & rf . name) . collect () ; assert_eq ! (schema_fields . len () , schema_field_set . len () , "Duplicate field names found: {schema_fields:?}") ; let name = apache_avro :: schema :: Name :: new ("A") . expect (& format ! ("Unable to parse struct name for schema {}" , "A") [..]) ; let lookup : std :: collections :: BTreeMap < String , usize > = schema_fields . iter () . map (| field | (field . name . to_owned () , field . position)) . collect () ; apache_avro :: schema :: Schema :: Record (apache_avro :: schema :: RecordSchema { name , aliases : None , doc : None , fields : schema_fields , lookup , attributes : Default :: default () , }) } ; named_schemas . insert (name , schema . clone ()) ; schema } } }"#; + 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_avro :: schema :: Schema :: Ref { name } } else { let enclosing_namespace = & name . namespace ; named_schemas . insert (name . clone () , apache_avro :: schema :: Schema :: Ref { name : name . clone () }) ; let schema = { let mut schema_fields = Vec :: with_capacity (1usize) ; schema_fields . push (:: apache_avro :: schema :: RecordField { name : "a3" . to_string () , doc : Some ("a doc" . into ()) , default : Some (serde_json :: from_str ("123") . expect (format ! ("Invalid JSON: {:?}" , "123") . as_str ())) , aliases : Some (vec ! ["a1" . try_into () . expect ("Alias is invalid") , "a2" . try_into () . expect ("Alias is invalid")]) , schema : < i32 as apache_avro :: AvroSchemaComponent > :: get_schema_in_ctxt (named_schemas , enclosing_namespace) , order : :: apache_avro :: schema :: RecordFieldOrder :: Ascending , position : schema_fields . len () , custom_attributes : Default :: default () , }) ; let schema_field_set : :: std :: collections :: HashSet < _ > = schema_fields . iter () . map (| rf | & rf . name) . collect () ; assert_eq ! (schema_fields . len () , schema_field_set . len () , "Duplicate field names found: {schema_fields:?}") ; let name = apache_avro :: schema :: Name :: new ("A") . expect (& format ! ("Unable to parse struct name for schema {}" , "A") [..]) ; let lookup : std :: collections :: BTreeMap < String , usize > = schema_fields . iter () . map (| field | (field . name . to_owned () , field . position)) . collect () ; apache_avro :: schema :: Schema :: Record (apache_avro :: schema :: RecordSchema { name , aliases : None , doc : None , fields : schema_fields , lookup , attributes : Default :: default () , }) } ; named_schemas . insert (name , schema . clone ()) ; 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 >> { let mut schema_fields = Vec :: with_capacity (1usize) ; schema_fields . push (:: apache_avro :: schema :: RecordField { name : "a3" . to_string () , doc : Some ("a doc" . into ()) , default : Some (serde_json :: from_str ("123") . expect (format ! ("Invalid JSON: {:?}" , "123") . as_str ())) , aliases : Some (vec ! ["a1" . try_into () . expect ("Alias is invalid") , "a2" . try_into () . expect ("Alias is invalid")]) , schema : < i32 as apache_avro :: AvroSchemaComponent > :: get_schema_in_ctxt (named_schemas , enclosing_namespace) , order : :: apache_avro :: schema :: RecordFieldOrder :: Ascending , position : schema_fields . len () , custom_attributes : Default :: default () , }) ; Some (schema_fields) } }"#; 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::(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_avro :: schema :: Schema :: Ref { name } } else { let enclosing_namespace = & name . namespace ; named_schemas . insert (name . clone () , apache_avro :: schema :: Schema :: Ref { name : name . clone () }) ; let schema = apache_avro :: schema :: Schema :: Enum (apache_avro :: schema :: EnumSchema { name : apache_avro :: schema :: Name :: new ("A") . expect (& format ! ("Unable to parse enum name for schema {}" , "A") [..]) , aliases : None , doc : None , symbols : vec ! ["A3" . to_owned ()] , default : None , attributes : Default :: default () , }) ; named_schemas . insert (name , schema . clone ()) ; schema } } }"#; + 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_avro :: schema :: Schema :: Ref { name } } else { let enclosing_namespace = & name . namespace ; named_schemas . insert (name . clone () , apache_avro :: schema :: Schema :: Ref { name : name . clone () }) ; let schema = apache_avro :: schema :: Schema :: Enum (apache_avro :: schema :: EnumSchema { name : apache_avro :: schema :: Name :: new ("A") . expect (& format ! ("Unable to parse enum name for schema {}" , "A") [..]) , aliases : None , doc : None , symbols : vec ! ["A3" . to_owned ()] , default : None , attributes : Default :: default () , }) ; named_schemas . insert (name , schema . clone ()) ; 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 } }"#; 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::(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_avro :: schema :: Schema :: Ref { name } } else { let enclosing_namespace = & name . namespace ; named_schemas . insert (name . clone () , apache_avro :: schema :: Schema :: Ref { name : name . clone () }) ; let schema = { let mut schema_fields = Vec :: with_capacity (2usize) ; schema_fields . push (:: apache_avro :: schema :: RecordField { name : "ITEM" . to_string () , doc : None , default : None , aliases : None , schema : < i32 as apache_avro :: AvroSchemaComponent > :: get_schema_in_ctxt (named_schemas , enclosing_namespace) , order : :: apache_avro :: schema :: RecordFieldOrder :: Ascending , position : schema_fields . len () , custom_attributes : Default :: default () , }) ; schema_fields . push (:: apache_avro :: schema :: RecordField { name : "DOUBLE_ITEM" . to_string () , doc : None , default : None , aliases : None , schema : < i32 as apache_avro :: AvroSchemaComponent > :: get_schema_in_ctxt (named_schemas , enclosing_namespace) , order : :: apache_avro :: schema :: RecordFieldOrder :: Ascending , position : schema_fields . len () , custom_attributes : Default :: default () , }) ; let schema_field_set : :: std :: collections :: HashSet < _ > = schema_fields . iter () . map (| rf | & rf . name) . collect () ; assert_eq ! (schema_fields . len () , schema_field_set . len () , "Duplicate field names found: {schema_fields:?}") ; let name = apache_avro :: schema :: Name :: new ("A") . expect (& format ! ("Unable to parse struct name for schema {}" , "A") [..]) ; let lookup : std :: collections :: BTreeMap < String , usize > = schema_fields . iter () . map (| field | (field . name . to_owned () , field . position)) . collect () ; apache_avro :: schema :: Schema :: Record (apache_avro :: schema :: RecordSchema { name , aliases : None , doc : None , fields : schema_fields , lookup , attributes : Default :: default () , }) } ; named_schemas . insert (name , schema . clone ()) ; schema } } }"#; + 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_avro :: schema :: Schema :: Ref { name } } else { let enclosing_namespace = & name . namespace ; named_schemas . insert (name . clone () , apache_avro :: schema :: Schema :: Ref { name : name . clone () }) ; let schema = { let mut schema_fields = Vec :: with_capacity (2usize) ; schema_fields . push (:: apache_avro :: schema :: RecordField { name : "ITEM" . to_string () , doc : None , default : None , aliases : None , schema : < i32 as apache_avro :: AvroSchemaComponent > :: get_schema_in_ctxt (named_schemas , enclosing_namespace) , order : :: apache_avro :: schema :: RecordFieldOrder :: Ascending , position : schema_fields . len () , custom_attributes : Default :: default () , }) ; schema_fields . push (:: apache_avro :: schema :: RecordField { name : "DOUBLE_ITEM" . to_string () , doc : None , default : None , aliases : None , schema : < i32 as apache_avro :: AvroSchemaComponent > :: get_schema_in_ctxt (named_schemas , enclosing_namespace) , order : :: apache_avro :: schema :: RecordFieldOrder :: Ascending , position : schema_fields . len () , custom_attributes : Default :: default () , }) ; let schema_field_set : :: std :: collections :: HashSet < _ > = schema_fields . iter () . map (| rf | & rf . name) . collect () ; assert_eq ! (schema_fields . len () , schema_field_set . len () , "Duplicate field names found: {schema_fields:?}") ; let name = apache_avro :: schema :: Name :: new ("A") . expect (& format ! ("Unable to parse struct name for schema {}" , "A") [..]) ; let lookup : std :: collections :: BTreeMap < String , usize > = schema_fields . iter () . map (| field | (field . name . to_owned () , field . position)) . collect () ; apache_avro :: schema :: Schema :: Record (apache_avro :: schema :: RecordSchema { name , aliases : None , doc : None , fields : schema_fields , lookup , attributes : Default :: default () , }) } ; named_schemas . insert (name , schema . clone ()) ; 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 >> { let mut schema_fields = Vec :: with_capacity (2usize) ; schema_fields . push (:: apache_avro :: schema :: RecordField { name : "ITEM" . to_string () , doc : None , default : None , aliases : None , schema : < i32 as apache_avro :: AvroSchemaComponent > :: get_schema_in_ctxt (named_schemas , enclosing_namespace) , order : :: apache_avro :: schema :: RecordFieldOrder :: Ascending , position : schema_fields . len () , custom_attributes : Default :: default () , }) ; schema_fields . push (:: apache_avro :: schema :: RecordField { name : "DOUBLE_ITEM" . to_string () , doc : None , default : None , aliases : None , schema : < i32 as apache_avro :: AvroSchemaComponent > :: get_schema_in_ctxt (named_schemas , enclosing_namespace) , order : :: apache_avro :: schema :: RecordFieldOrder :: Ascending , position : schema_fields . len () , custom_attributes : Default :: default () , }) ; Some (schema_fields) } }"#; 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::(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_avro :: schema :: Schema :: Ref { name } } else { let enclosing_namespace = & name . namespace ; named_schemas . insert (name . clone () , apache_avro :: schema :: Schema :: Ref { name : name . clone () }) ; let schema = apache_avro :: schema :: Schema :: Enum (apache_avro :: schema :: EnumSchema { name : apache_avro :: schema :: Name :: new ("B") . expect (& format ! ("Unable to parse enum name for schema {}" , "B") [..]) , aliases : None , doc : None , symbols : vec ! ["ITEM" . to_owned () , "DOUBLE_ITEM" . to_owned ()] , default : None , attributes : Default :: default () , }) ; named_schemas . insert (name , schema . clone ()) ; schema } } }"#; + 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_avro :: schema :: Schema :: Ref { name } } else { let enclosing_namespace = & name . namespace ; named_schemas . insert (name . clone () , apache_avro :: schema :: Schema :: Ref { name : name . clone () }) ; let schema = apache_avro :: schema :: Schema :: Enum (apache_avro :: schema :: EnumSchema { name : apache_avro :: schema :: Name :: new ("B") . expect (& format ! ("Unable to parse enum name for schema {}" , "B") [..]) , aliases : None , doc : None , symbols : vec ! ["ITEM" . to_owned () , "DOUBLE_ITEM" . to_owned ()] , default : None , attributes : Default :: default () , }) ; named_schemas . insert (name , schema . clone ()) ; 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 } }"#; 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::(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_avro :: schema :: Schema :: Ref { name } } else { let enclosing_namespace = & name . namespace ; named_schemas . insert (name . clone () , apache_avro :: schema :: Schema :: Ref { name : name . clone () }) ; let schema = { let mut schema_fields = Vec :: with_capacity (2usize) ; schema_fields . push (:: apache_avro :: schema :: RecordField { name : "ITEM" . to_string () , doc : None , default : None , aliases : None , schema : < i32 as apache_avro :: AvroSchemaComponent > :: get_schema_in_ctxt (named_schemas , enclosing_namespace) , order : :: apache_avro :: schema :: RecordFieldOrder :: Ascending , position : schema_fields . len () , custom_attributes : Default :: default () , }) ; schema_fields . push (:: apache_avro :: schema :: RecordField { name : "DoubleItem" . to_string () , doc : None , default : None , aliases : None , schema : < i32 as apache_avro :: AvroSchemaComponent > :: get_schema_in_ctxt (named_schemas , enclosing_namespace) , order : :: apache_avro :: schema :: RecordFieldOrder :: Ascending , position : schema_fields . len () , custom_attributes : Default :: default () , }) ; let schema_field_set : :: std :: collections :: HashSet < _ > = schema_fields . iter () . map (| rf | & rf . name) . collect () ; assert_eq ! (schema_fields . len () , schema_field_set . len () , "Duplicate field names found: {schema_fields:?}") ; let name = apache_avro :: schema :: Name :: new ("A") . expect (& format ! ("Unable to parse struct name for schema {}" , "A") [..]) ; let lookup : std :: collections :: BTreeMap < String , usize > = schema_fields . iter () . map (| field | (field . name . to_owned () , field . position)) . collect () ; apache_avro :: schema :: Schema :: Record (apache_avro :: schema :: RecordSchema { name , aliases : None , doc : None , fields : schema_fields , lookup , attributes : Default :: default () , }) } ; named_schemas . insert (name , schema . clone ()) ; schema } } }"#; + 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_avro :: schema :: Schema :: Ref { name } } else { let enclosing_namespace = & name . namespace ; named_schemas . insert (name . clone () , apache_avro :: schema :: Schema :: Ref { name : name . clone () }) ; let schema = { let mut schema_fields = Vec :: with_capacity (2usize) ; schema_fields . push (:: apache_avro :: schema :: RecordField { name : "ITEM" . to_string () , doc : None , default : None , aliases : None , schema : < i32 as apache_avro :: AvroSchemaComponent > :: get_schema_in_ctxt (named_schemas , enclosing_namespace) , order : :: apache_avro :: schema :: RecordFieldOrder :: Ascending , position : schema_fields . len () , custom_attributes : Default :: default () , }) ; schema_fields . push (:: apache_avro :: schema :: RecordField { name : "DoubleItem" . to_string () , doc : None , default : None , aliases : None , schema : < i32 as apache_avro :: AvroSchemaComponent > :: get_schema_in_ctxt (named_schemas , enclosing_namespace) , order : :: apache_avro :: schema :: RecordFieldOrder :: Ascending , position : schema_fields . len () , custom_attributes : Default :: default () , }) ; let schema_field_set : :: std :: collections :: HashSet < _ > = schema_fields . iter () . map (| rf | & rf . name) . collect () ; assert_eq ! (schema_fields . len () , schema_field_set . len () , "Duplicate field names found: {schema_fields:?}") ; let name = apache_avro :: schema :: Name :: new ("A") . expect (& format ! ("Unable to parse struct name for schema {}" , "A") [..]) ; let lookup : std :: collections :: BTreeMap < String , usize > = schema_fields . iter () . map (| field | (field . name . to_owned () , field . position)) . collect () ; apache_avro :: schema :: Schema :: Record (apache_avro :: schema :: RecordSchema { name , aliases : None , doc : None , fields : schema_fields , lookup , attributes : Default :: default () , }) } ; named_schemas . insert (name , schema . clone ()) ; 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 >> { let mut schema_fields = Vec :: with_capacity (2usize) ; schema_fields . push (:: apache_avro :: schema :: RecordField { name : "ITEM" . to_string () , doc : None , default : None , aliases : None , schema : < i32 as apache_avro :: AvroSchemaComponent > :: get_schema_in_ctxt (named_schemas , enclosing_namespace) , order : :: apache_avro :: schema :: RecordFieldOrder :: Ascending , position : schema_fields . len () , custom_attributes : Default :: default () , }) ; schema_fields . push (:: apache_avro :: schema :: RecordField { name : "DoubleItem" . to_string () , doc : None , default : None , aliases : None , schema : < i32 as apache_avro :: AvroSchemaComponent > :: get_schema_in_ctxt (named_schemas , enclosing_namespace) , order : :: apache_avro :: schema :: RecordFieldOrder :: Ascending , position : schema_fields . len () , custom_attributes : Default :: default () , }) ; Some (schema_fields) } }"#; 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 4ee27f00..4b6f0d69 100644 --- a/avro_derive/tests/derive.rs +++ b/avro_derive/tests/derive.rs @@ -2155,3 +2155,177 @@ fn avro_rs_414_round_trip_char_u64_u128_i128() { d: i128::MAX, }); } + +#[test] +fn avro_rs_448_flatten_recurring_type() { + #[derive(AvroSchema)] + #[expect(dead_code, reason = "Only testing derived schema")] + pub enum Color { + G, + } + + #[derive(AvroSchema)] + pub struct A { + pub _color: Color, + } + + #[derive(AvroSchema)] + pub struct C { + #[serde(flatten)] + pub _a: A, + } + + #[derive(AvroSchema)] + 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); +} + +#[test] +fn avro_rs_448_flatten_transparent_sandwich() { + #[derive(AvroSchema)] + #[expect(dead_code, reason = "Only testing derived schema")] + pub enum Color { + G, + } + + #[derive(AvroSchema)] + pub struct A { + pub _color: Color, + } + + #[derive(AvroSchema)] + pub struct C { + #[serde(flatten)] + pub _a: A, + } + + #[derive(AvroSchema)] + #[serde(transparent)] + pub struct B { + pub _c: C, + } + + #[derive(AvroSchema)] + pub struct TestStruct { + pub _a: Color, + pub _b: B, + pub _c: C, + } + + let schema = Schema::parse_str( + r#"{ + "name": "TestStruct", + "type":"record", + "fields": [ + { + "name":"_a", + "type": { + "name": "Color", + "type": "enum", + "symbols": ["G"] + } + }, + { + "name":"_b", + "type": { + "name":"C", + "type":"record", + "fields": [ + { + "name": "_color", + "type": "Color" + } + ] + } + }, + { + "name":"_c", + "type": "C" + } + ] + }"#, + ) + .unwrap(); + + assert_eq!(TestStruct::get_schema(), schema); +} + +#[test] +fn avro_rs_448_transparent_with() { + #[derive(AvroSchema)] + #[serde(transparent)] + pub struct TestStruct { + #[avro(with = || Schema::Long)] + pub _a: i32, + } + + let mut named_schemas = HashMap::new(); + assert_eq!( + TestStruct::get_record_fields_in_ctxt(&mut named_schemas, &None), + None + ); + assert!(named_schemas.is_empty()); +} + +#[test] +fn avro_rs_448_transparent_with_2() { + #[derive(AvroSchema)] + pub struct Foo { + _field: i32, + _a: String, + } + + #[derive(AvroSchema)] + #[serde(transparent)] + pub struct TestStruct { + #[avro(with = Foo::get_schema_in_ctxt)] + pub _a: Foo, + } + + let mut named_schemas = HashMap::new(); + let fields = TestStruct::get_record_fields_in_ctxt(&mut named_schemas, &None).unwrap(); + assert!(named_schemas.is_empty()); + assert_eq!(fields.len(), 2); + + TestStruct::get_schema_in_ctxt(&mut named_schemas, &None); + assert_eq!(named_schemas.len(), 1); + + let fields = TestStruct::get_record_fields_in_ctxt(&mut named_schemas, &None).unwrap(); + assert_eq!(named_schemas.len(), 1); + assert_eq!(fields.len(), 2); +}