This is an automated email from the ASF dual-hosted git repository.
kriskras99 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 7cff925 fix: `flatten` no longer causes duplicate names (#448)
7cff925 is described below
commit 7cff9252e1bbfcc53059199c48590f28a091df91
Author: Kriskras99 <[email protected]>
AuthorDate: Fri Jan 30 19:11:31 2026 +0100
fix: `flatten` no longer causes duplicate names (#448)
* 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.
* feat: Use macros to reduce code duplication when implementing
`AvroSchemaComponent`
The implementation for `serde_json::Map<String, T>` has been removed. This
is **not** a breaking change, as `serde_json::Map` can only be constructed for
`<String, Value>` and `Value` does not implement `AvroSchemaComponent`.
* fix: Add more tests
* fix: Have flatten update field positions
* feat: Update `AvroSchemaComponent::get_record_fields_in_ctxt` to also
take the wanted field position
* fix: Use `unwrap_or_else(|| panic!(...))` instead of `expect`
* fix: Accidential rename and use more `::` in `quote!` macros
---------
Co-authored-by: default <[email protected]>
---
avro/src/serde/derive.rs | 331 ++++++++++++++++++++++++++++++--------
avro/src/serde/mod.rs | 3 +
avro/src/serde/with.rs | 74 +++++++--
avro/tests/get_record_fields.rs | 149 +++++++++++++++++
avro_derive/src/attributes/mod.rs | 2 +-
avro_derive/src/lib.rs | 157 ++++++++++++++----
avro_derive/tests/derive.rs | 216 +++++++++++++++++++++++++
7 files changed, 819 insertions(+), 113 deletions(-)
diff --git a/avro/src/serde/derive.rs b/avro/src/serde/derive.rs
index b51cee7..5164458 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,160 @@ 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(
+ first_field_position: usize,
+ named_schemas: &mut Names,
+ enclosing_namespace: &Namespace,
+ ) -> Option<Vec<RecordField>> {
+ get_record_fields_in_ctxt(
+ first_field_position,
+ 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(
+ first_field_position: usize,
+ 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)
+ .unwrap_or_else(|| panic!("Name '{name}' should exist in
`named_schemas` otherwise Ref is invalid: {named_schemas:?}"));
+ // 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;
+ };
+ let fields = record
+ .fields
+ .into_iter()
+ .map(|mut f| {
+ f.position += first_field_position;
+ f
+ })
+ .collect();
+ return Some(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 name = match std::mem::replace(schema,
Schema::Record(new_schema)) {
+ Schema::Ref { name } => name,
+ schema => {
+ panic!("Only expected `Schema::Ref` from `find_first_ref`,
got: {schema:?}")
+ }
+ };
+
+ // The schema is used, so reinsert it
+ named_schemas.insert(name.clone(), Schema::Ref { name });
+
+ break;
+ }
+ }
+
+ let fields = record
+ .fields
+ .into_iter()
+ .map(|mut f| {
+ f.position += first_field_position;
+ f
+ })
+ .collect();
+ Some(fields)
}
impl<T> AvroSchema for T
@@ -100,6 +254,10 @@ macro_rules! impl_schema (
fn get_schema_in_ctxt(_: &mut Names, _: &Namespace) -> Schema {
$variant_constructor
}
+
+ fn get_record_fields_in_ctxt(_: usize, _: &mut Names, _:
&Namespace) -> Option<Vec<RecordField>> {
+ None
+ }
}
);
);
@@ -118,32 +276,44 @@ impl_schema!(String, Schema::String);
impl_schema!(str, Schema::String);
impl_schema!(char, Schema::String);
-impl<T> 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<T: AvroSchemaComponent $(+ $bound)* + ?Sized> 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<T> 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(first_field_position: usize,
named_schemas: &mut Names, enclosing_namespace: &Namespace) ->
Option<Vec<RecordField>> {
+ T::get_record_fields_in_ctxt(first_field_position,
named_schemas, enclosing_namespace)
+ }
+ }
+ );
+);
-impl<T> 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<T> where T: AvroSchemaComponent + ?Sized);
+impl_passthrough_schema!(Cow<'_, T> where T: AvroSchemaComponent + ?Sized +
ToOwned);
+impl_passthrough_schema!(std::sync::Mutex<T> where T: AvroSchemaComponent +
?Sized);
+
+macro_rules! impl_array_schema (
+ ($type:ty where T: AvroSchemaComponent) => (
+ impl<T: AvroSchemaComponent> 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(_: usize, _: &mut Names, _:
&Namespace) -> Option<Vec<RecordField>> {
+ None
+ }
+ }
+ );
+);
+
+impl_array_schema!([T] where T: AvroSchemaComponent);
+impl_array_schema!(Vec<T> 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<const N: usize, T> AvroSchemaComponent for [T; N]
where
@@ -152,14 +322,30 @@ 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(
+ _: usize,
+ _: &mut Names,
+ _: &Namespace,
+ ) -> Option<Vec<RecordField>> {
+ None
+ }
}
-impl<T> AvroSchemaComponent for Vec<T>
+impl<T> AvroSchemaComponent for HashMap<String, 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))
+ Schema::map(T::get_schema_in_ctxt(named_schemas, enclosing_namespace))
+ }
+
+ fn get_record_fields_in_ctxt(
+ _: usize,
+ _: &mut Names,
+ _: &Namespace,
+ ) -> Option<Vec<RecordField>> {
+ None
}
}
@@ -177,50 +363,13 @@ where
UnionSchema::new(variants).expect("Option<T> must produce a valid
(non-nested) union"),
)
}
-}
-
-impl<T> AvroSchemaComponent for Map<String, T>
-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<T> AvroSchemaComponent for HashMap<String, T>
-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<T> AvroSchemaComponent for Box<T>
-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<T> AvroSchemaComponent for std::sync::Mutex<T>
-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<T> 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(
+ _: usize,
+ _: &mut Names,
+ _: &Namespace,
+ ) -> Option<Vec<RecordField>> {
+ None
}
}
@@ -248,6 +397,14 @@ impl AvroSchemaComponent for core::time::Duration {
schema
}
}
+
+ fn get_record_fields_in_ctxt(
+ _: usize,
+ _: &mut Names,
+ _: &Namespace,
+ ) -> Option<Vec<RecordField>> {
+ None
+ }
}
impl AvroSchemaComponent for uuid::Uuid {
@@ -274,6 +431,14 @@ impl AvroSchemaComponent for uuid::Uuid {
schema
}
}
+
+ fn get_record_fields_in_ctxt(
+ _: usize,
+ _: &mut Names,
+ _: &Namespace,
+ ) -> Option<Vec<RecordField>> {
+ None
+ }
}
impl AvroSchemaComponent for u64 {
@@ -298,6 +463,14 @@ impl AvroSchemaComponent for u64 {
schema
}
}
+
+ fn get_record_fields_in_ctxt(
+ _: usize,
+ _: &mut Names,
+ _: &Namespace,
+ ) -> Option<Vec<RecordField>> {
+ None
+ }
}
impl AvroSchemaComponent for u128 {
@@ -322,6 +495,14 @@ impl AvroSchemaComponent for u128 {
schema
}
}
+
+ fn get_record_fields_in_ctxt(
+ _: usize,
+ _: &mut Names,
+ _: &Namespace,
+ ) -> Option<Vec<RecordField>> {
+ None
+ }
}
impl AvroSchemaComponent for i128 {
@@ -346,6 +527,14 @@ impl AvroSchemaComponent for i128 {
schema
}
}
+
+ fn get_record_fields_in_ctxt(
+ _: usize,
+ _: &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/src/serde/with.rs b/avro/src/serde/with.rs
index 70dc84a..670955b 100644
--- a/avro/src/serde/with.rs
+++ b/avro/src/serde/with.rs
@@ -95,14 +95,23 @@ pub mod bytes {
use crate::{
Schema,
- schema::{Names, Namespace},
+ schema::{Names, Namespace, RecordField},
};
/// Returns [`Schema::Bytes`]
- pub fn get_schema_in_ctxt(_names: &mut Names, _enclosing_namespace:
&Namespace) -> Schema {
+ pub fn get_schema_in_ctxt(_: &mut Names, _: &Namespace) -> Schema {
Schema::Bytes
}
+ /// Returns `None`
+ pub fn get_record_fields_in_ctxt(
+ _: usize,
+ _: &mut Names,
+ _: &Namespace,
+ ) -> Option<Vec<RecordField>> {
+ None
+ }
+
pub fn serialize<S>(bytes: &[u8], serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
@@ -147,16 +156,25 @@ pub mod bytes_opt {
use crate::{
Schema,
- schema::{Names, Namespace, UnionSchema},
+ schema::{Names, Namespace, RecordField, UnionSchema},
};
/// Returns `Schema::Union(Schema::Null, Schema::Bytes)`
- pub fn get_schema_in_ctxt(_names: &mut Names, _enclosing_namespace:
&Namespace) -> Schema {
+ pub fn get_schema_in_ctxt(_: &mut Names, _: &Namespace) -> Schema {
Schema::Union(
UnionSchema::new(vec![Schema::Null, Schema::Bytes]).expect("This
is a valid union"),
)
}
+ /// Returns `None`
+ pub fn get_record_fields_in_ctxt(
+ _: usize,
+ _: &mut Names,
+ _: &Namespace,
+ ) -> Option<Vec<RecordField>> {
+ None
+ }
+
pub fn serialize<S, B>(bytes: &Option<B>, serializer: S) -> Result<S::Ok,
S::Error>
where
S: Serializer,
@@ -202,7 +220,7 @@ pub mod fixed {
use crate::{
Schema,
- schema::{FixedSchema, Name, Names, Namespace},
+ schema::{FixedSchema, Name, Names, Namespace, RecordField},
};
/// Returns `Schema::Fixed(N)` named `serde_avro_fixed_{N}`
@@ -223,6 +241,15 @@ pub mod fixed {
}
}
+ /// Returns `None`
+ pub fn get_record_fields_in_ctxt(
+ _: usize,
+ _: &mut Names,
+ _: &Namespace,
+ ) -> Option<Vec<RecordField>> {
+ None
+ }
+
pub fn serialize<S>(bytes: &[u8], serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
@@ -269,7 +296,7 @@ pub mod fixed_opt {
use crate::{
Schema,
- schema::{Names, Namespace, UnionSchema},
+ schema::{Names, Namespace, RecordField, UnionSchema},
};
/// Returns `Schema::Union(Schema::Null, Schema::Fixed(N))` where the
fixed schema is named `serde_avro_fixed_{N}`
@@ -286,6 +313,15 @@ pub mod fixed_opt {
)
}
+ /// Returns `None`
+ pub fn get_record_fields_in_ctxt(
+ _: usize,
+ _: &mut Names,
+ _: &Namespace,
+ ) -> Option<Vec<RecordField>> {
+ None
+ }
+
pub fn serialize<S, B>(bytes: &Option<B>, serializer: S) -> Result<S::Ok,
S::Error>
where
S: Serializer,
@@ -333,14 +369,23 @@ pub mod slice {
use crate::{
Schema,
- schema::{Names, Namespace},
+ schema::{Names, Namespace, RecordField},
};
/// Returns [`Schema::Bytes`]
- pub fn get_schema_in_ctxt(_names: &mut Names, _enclosing_namespace:
&Namespace) -> Schema {
+ pub fn get_schema_in_ctxt(_: &mut Names, _: &Namespace) -> Schema {
Schema::Bytes
}
+ /// Returns `None`
+ pub fn get_record_fields_in_ctxt(
+ _: usize,
+ _: &mut Names,
+ _: &Namespace,
+ ) -> Option<Vec<RecordField>> {
+ None
+ }
+
pub fn serialize<S>(bytes: &[u8], serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
@@ -388,16 +433,25 @@ pub mod slice_opt {
use crate::{
Schema,
- schema::{Names, Namespace, UnionSchema},
+ schema::{Names, Namespace, RecordField, UnionSchema},
};
/// Returns `Schema::Union(Schema::Null, Schema::Bytes)`
- pub fn get_schema_in_ctxt(_names: &mut Names, _enclosing_namespace:
&Namespace) -> Schema {
+ pub fn get_schema_in_ctxt(_: &mut Names, _: &Namespace) -> Schema {
Schema::Union(
UnionSchema::new(vec![Schema::Null, Schema::Bytes]).expect("This
is a valid union"),
)
}
+ /// Returns `None`
+ pub fn get_record_fields_in_ctxt(
+ _: usize,
+ _: &mut Names,
+ _: &Namespace,
+ ) -> Option<Vec<RecordField>> {
+ None
+ }
+
pub fn serialize<S, B>(bytes: &Option<B>, serializer: S) -> Result<S::Ok,
S::Error>
where
S: Serializer,
diff --git a/avro/tests/get_record_fields.rs b/avro/tests/get_record_fields.rs
new file mode 100644
index 0000000..9b729fe
--- /dev/null
+++ b/avro/tests/get_record_fields.rs
@@ -0,0 +1,149 @@
+// 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(0, &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: {named_schemas:?}"
+ );
+
+ // Insert Foo into named_schemas
+ match Foo::get_schema_in_ctxt(&mut named_schemas, &None) {
+ Schema::Record(_) => {}
+ schema => panic!("Expected a record got {schema:?}"),
+ }
+ assert_eq!(
+ named_schemas.len(),
+ 1,
+ "Name should have been added: {named_schemas:?}"
+ );
+
+ let fields =
+ get_record_fields_in_ctxt(0, &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: {named_schemas:?}"
+ );
+
+ Ok(())
+}
+
+#[test]
+fn avro_rs_448_default_get_record_fields_recursion() -> TestResult {
+ #[derive(apache_avro_derive::AvroSchema)]
+ struct Foo {
+ _a: i32,
+ _b: Option<Box<Foo>>,
+ }
+
+ let mut named_schemas = HashMap::new();
+ let fields =
+ get_record_fields_in_ctxt(0, &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: {named_schemas:?}"
+ );
+
+ // Insert Foo into named_schemas
+ match Foo::get_schema_in_ctxt(&mut named_schemas, &None) {
+ Schema::Ref { name: _ } => {}
+ schema => panic!("Expected a ref got {schema:?}"),
+ }
+ assert_eq!(named_schemas.len(), 1);
+
+ let fields =
+ get_record_fields_in_ctxt(0, &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: {named_schemas:?}"
+ );
+
+ Ok(())
+}
+
+#[test]
+fn avro_rs_448_default_get_record_fields_position() -> 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(10, &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: {named_schemas:?}"
+ );
+ let positions = fields.into_iter().map(|f| f.position).collect::<Vec<_>>();
+ assert_eq!(positions.as_slice(), &[10, 11][..]);
+
+ // Insert Foo into named_schemas
+ match Foo::get_schema_in_ctxt(&mut named_schemas, &None) {
+ Schema::Record(_) => {}
+ schema => panic!("Expected a record got {schema:?}"),
+ }
+ assert_eq!(
+ named_schemas.len(),
+ 1,
+ "Name should have been added: {named_schemas:?}"
+ );
+
+ let fields =
+ get_record_fields_in_ctxt(5043, &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: {named_schemas:?}"
+ );
+ let positions = fields.into_iter().map(|f| f.position).collect::<Vec<_>>();
+ assert_eq!(positions.as_slice(), &[5043, 5044][..]);
+
+ Ok(())
+}
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..3904452 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
+ 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:
&::std::option::Option<::std::string::String>) -> ::apache_avro::schema::Schema
{
+ #get_schema_impl
+ }
+
+ fn get_record_fields_in_ctxt(mut field_position: usize,
named_schemas: &mut ::apache_avro::schema::Names, enclosing_namespace:
&::std::option::Option<::std::string::String>) ->
::std::option::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,14 @@ 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 {
+ field_position += flattened_fields.len();
+ schema_fields.extend(flattened_fields);
} else {
- panic!("Can only flatten RecordSchema, got {:?}",
#flatten_ty::get_schema())
+ panic!("{} does not have any fields to flatten
to", stringify!(#field));
}
});
@@ -186,9 +203,10 @@ fn get_struct_schema_def(
aliases: #aliases,
schema: #schema_expr,
order:
::apache_avro::schema::RecordFieldOrder::Ascending,
- position: schema_fields.len(),
+ position: field_position,
custom_attributes: Default::default(),
});
+ field_position += 1;
});
}
}
@@ -214,9 +232,10 @@ 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);
+ let mut field_position = 0;
#(#record_field_exprs)*
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:?}");
@@ -234,14 +253,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 +285,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 +331,42 @@ 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(field_position,
named_schemas, enclosing_namespace) },
+ ),
+ With::Expr(Expr::Closure(closure)) => {
+ if closure.inputs.is_empty() {
+ Ok(quote! {
+ ::apache_avro::serde::get_record_fields_in_ctxt(
+ field_position,
+ 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(field_position,
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 +432,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(field_position,
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,
@@ -531,11 +618,11 @@ mod tests {
assert!(derived.is_ok());
assert_eq!(derived.unwrap().to_string(), quote! {
#[automatically_derived]
- impl apache_avro::AvroSchemaComponent for Basic {
+ impl ::apache_avro::AvroSchemaComponent for Basic {
fn get_schema_in_ctxt(
- named_schemas: &mut apache_avro::schema::Names,
- enclosing_namespace: &Option<String>
- ) -> apache_avro::schema::Schema {
+ named_schemas: &mut ::apache_avro::schema::Names,
+ enclosing_namespace:
&::std::option::Option<::std::string::String>
+ ) -> ::apache_avro::schema::Schema {
let name = apache_avro::schema::Name::new("Basic")
.expect(concat!("Unable to parse schema name
", "Basic"))
.fully_qualified_name(enclosing_namespace);
@@ -564,6 +651,14 @@ mod tests {
schema
}
}
+
+ fn get_record_fields_in_ctxt(
+ mut field_position: usize,
+ named_schemas: &mut ::apache_avro::schema::Names,
+ enclosing_namespace:
&::std::option::Option<::std::string::String>
+ ) -> ::std::option::Option
<::std::vec::Vec<::apache_avro::schema::RecordField>> {
+ None
+ }
}
}.to_string());
}
@@ -690,7 +785,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
: & :: std :: option :: Option < :: std :: string :: 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 n [...]
let schema_token_stream = schema_res.unwrap().to_string();
assert_eq!(schema_token_stream, expected_token_stream);
}
@@ -709,7 +804,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
: & :: std :: option :: Option < :: std :: string :: 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 n [...]
let schema_token_stream = schema_res.unwrap().to_string();
assert_eq!(schema_token_stream, expected_token_stream);
}
@@ -732,7 +827,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
: & :: std :: option :: Option < :: std :: string :: 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 n [...]
let schema_token_stream = schema_res.unwrap().to_string();
assert_eq!(schema_token_stream, expected_token_stream);
}
@@ -752,7 +847,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
: & :: std :: option :: Option < :: std :: string :: 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 n [...]
let schema_token_stream = schema_res.unwrap().to_string();
assert_eq!(schema_token_stream, expected_token_stream);
}
@@ -776,7 +871,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
: & :: std :: option :: Option < :: std :: string :: 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 n [...]
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..d6b1c4e 100644
--- a/avro_derive/tests/derive.rs
+++ b/avro_derive/tests/derive.rs
@@ -2155,3 +2155,219 @@ 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(0, &mut named_schemas, &None),
+ None
+ );
+ assert!(
+ named_schemas.is_empty(),
+ "No name should've been added: {named_schemas:?}"
+ );
+}
+
+#[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(0, &mut named_schemas,
&None).unwrap();
+ assert!(
+ named_schemas.is_empty(),
+ "No name should've been added: {named_schemas:?}"
+ );
+ assert_eq!(fields.len(), 2);
+
+ TestStruct::get_schema_in_ctxt(&mut named_schemas, &None);
+ assert_eq!(
+ named_schemas.len(),
+ 1,
+ "One name should've been added: {named_schemas:?}"
+ );
+
+ let fields = TestStruct::get_record_fields_in_ctxt(0, &mut named_schemas,
&None).unwrap();
+ assert_eq!(
+ named_schemas.len(),
+ 1,
+ "No name should've been added: {named_schemas:?}"
+ );
+ assert_eq!(fields.len(), 2);
+}
+
+#[test]
+fn avro_rs_448_flatten_field_positions() {
+ #[derive(AvroSchema)]
+ struct Foo {
+ _a: i32,
+ _b: String,
+ }
+
+ #[derive(AvroSchema)]
+ struct Bar {
+ _c: Vec<i64>,
+ #[serde(flatten)]
+ _d: Foo,
+ _e: bool,
+ }
+
+ let Schema::Record(schema) = Bar::get_schema() else {
+ panic!("Structs should generate records")
+ };
+
+ let positions = schema
+ .fields
+ .into_iter()
+ .map(|f| f.position)
+ .collect::<Vec<_>>();
+ assert_eq!(positions.as_slice(), &[0, 1, 2, 3][..]);
+}