This is an automated email from the ASF dual-hosted git repository. kriskras99 pushed a commit to branch feat/enums in repository https://gitbox.apache.org/repos/asf/avro-rs.git
commit f7f9e127396749f980848f01755bb4ec82b3efef Author: Kriskras99 <[email protected]> AuthorDate: Tue Mar 10 21:35:36 2026 +0100 naming tuples and arrays sucks --- avro_derive/src/enums/bare_union.rs | 4 +- avro_derive/src/enums/record_tag_content.rs | 4 +- avro_derive/src/enums/union_of_records.rs | 6 +- avro_derive/src/lib.rs | 6 +- avro_derive/src/tuple.rs | 124 +++++++++++++++++++++++++++- 5 files changed, 130 insertions(+), 14 deletions(-) diff --git a/avro_derive/src/enums/bare_union.rs b/avro_derive/src/enums/bare_union.rs index 75cb558..c465fb2 100644 --- a/avro_derive/src/enums/bare_union.rs +++ b/avro_derive/src/enums/bare_union.rs @@ -1,5 +1,5 @@ use crate::attributes::{NamedTypeOptions, VariantOptions}; -use crate::tuple::tuple_to_record_schema; +use crate::tuple::tuple_struct_variant_to_record_schema; use crate::{named_to_record_fields, type_to_schema_expr}; use proc_macro2::{Span, TokenStream}; use quote::quote; @@ -50,7 +50,7 @@ pub fn get_data_enum_schema_def( let schema_expr = type_to_schema_expr(&only_one.ty)?; variant_expr.push(schema_expr); } else if unnamed.unnamed.len() > 1 { - let schema_expr = tuple_to_record_schema(unnamed, &name, &[])?; + let schema_expr = tuple_struct_variant_to_record_schema(unnamed, &name, &[])?; variant_expr.push(schema_expr); } diff --git a/avro_derive/src/enums/record_tag_content.rs b/avro_derive/src/enums/record_tag_content.rs index 2565cc9..e39177e 100644 --- a/avro_derive/src/enums/record_tag_content.rs +++ b/avro_derive/src/enums/record_tag_content.rs @@ -1,5 +1,5 @@ use crate::attributes::{NamedTypeOptions, VariantOptions}; -use crate::tuple::tuple_to_record_schema; +use crate::tuple::tuple_struct_variant_to_record_schema; use crate::{aliases, named_to_record_fields, preserve_optional, type_to_schema_expr}; use proc_macro2::TokenStream; use quote::quote; @@ -53,7 +53,7 @@ pub fn get_data_enum_schema_def( let field_schema_expr = type_to_schema_expr(&only_one.ty)?; schema_definitions.push(field_schema_expr); } else if unnamed.unnamed.len() > 1 { - let schema_expr = tuple_to_record_schema(unnamed, &name, &[])?; + let schema_expr = tuple_struct_variant_to_record_schema(unnamed, &name, &[])?; schema_definitions.push(schema_expr); } diff --git a/avro_derive/src/enums/union_of_records.rs b/avro_derive/src/enums/union_of_records.rs index 8786abc..c0f8902 100644 --- a/avro_derive/src/enums/union_of_records.rs +++ b/avro_derive/src/enums/union_of_records.rs @@ -1,6 +1,6 @@ use crate::attributes::{NamedTypeOptions, VariantOptions}; use crate::named_to_record_fields; -use crate::tuple::tuple_to_record_schema; +use crate::tuple::tuple_struct_variant_to_record_schema; use proc_macro2::TokenStream; use quote::quote; use syn::spanned::Spanned; @@ -39,13 +39,13 @@ pub fn get_data_enum_schema_def( } Fields::Unnamed(unnamed) => { let schema_expr = if unnamed.unnamed.len() == 1 { - tuple_to_record_schema( + tuple_struct_variant_to_record_schema( unnamed, &name, &["org.apache.avro.rust.union_of_records"], )? } else { - tuple_to_record_schema(unnamed, &name, &[])? + tuple_struct_variant_to_record_schema(unnamed, &name, &[])? }; variant_expr.push(schema_expr); diff --git a/avro_derive/src/lib.rs b/avro_derive/src/lib.rs index ccdffa3..84daff0 100644 --- a/avro_derive/src/lib.rs +++ b/avro_derive/src/lib.rs @@ -41,6 +41,7 @@ use syn::{ parse_macro_input, spanned::Spanned, }; +use crate::tuple::tuple_to_schema; use crate::{ attributes::{FieldOptions, NamedTypeOptions, With}, case::RenameRule, @@ -364,10 +365,7 @@ fn type_to_schema_expr(ty: &Type) -> Result<TokenStream, Vec<syn::Error>> { ty, "AvroSchema: derive does not support raw pointers", )]), - Type::Tuple(_) => Err(vec![syn::Error::new_spanned( - ty, - "AvroSchema: derive does not support tuples", - )]), + Type::Tuple(tuple) => tuple_to_schema(tuple), _ => Err(vec![syn::Error::new_spanned( ty, format!( diff --git a/avro_derive/src/tuple.rs b/avro_derive/src/tuple.rs index 387bee5..adf56c4 100644 --- a/avro_derive/src/tuple.rs +++ b/avro_derive/src/tuple.rs @@ -1,6 +1,6 @@ use proc_macro2::TokenStream; -use quote::quote; -use syn::{FieldsUnnamed, spanned::Spanned}; +use quote::{ToTokens, quote}; +use syn::{Expr, ExprLit, FieldsUnnamed, Lit, TypeArray, TypeTuple, spanned::Spanned}; use crate::{FieldOptions, doc_into_tokenstream, field_aliases, type_to_schema_expr}; @@ -10,7 +10,7 @@ use crate::{FieldOptions, doc_into_tokenstream, field_aliases, type_to_schema_ex /// /// The schema will have the attribute `org.apache.avro.rust.tuple` any any other specified in `extra_attributes`. /// All attributes will have a value of `true`. -pub fn tuple_to_record_schema( +pub fn tuple_struct_variant_to_record_schema( unnamed: FieldsUnnamed, name: &str, extra_attributes: &[&str], @@ -32,6 +32,124 @@ pub fn tuple_to_record_schema( }) } +/// Create a schema definition for a tuple. +/// +/// # Mapping +/// - `0-tuple` => `Schema::Null`, +/// - `1-tuple` => Schema of the only element, +/// - `n-tuple` => `Schema::Record`. +pub fn tuple_to_schema(tuple: &TypeTuple) -> Result<TokenStream, Vec<syn::Error>> { + if tuple.elems.is_empty() { + Ok(quote! {::apache_avro::schema::Schema::Null}) + } else if tuple.elems.len() == 1 { + type_to_schema_expr(&tuple.elems.iter().next().unwrap()) + } else { + let mut fields = Vec::with_capacity(tuple.elems.len()); + + for (index, elem) in tuple.elems.iter().enumerate() { + let name = format!("field_{index}"); + let field_schema_expr = type_to_schema_expr(elem)?; + fields.push(quote! { + ::apache_avro::schema::RecordField::builder() + .name(#name.to_string()) + .schema(#field_schema_expr) + .build() + }); + } + + // Try to create a unique name for this record, this is done in a best effort way and the + // name is NOT recorded in `names`. + // This will always start and end with a `_` as `(` and `)` are not valid characters + let tuple_as_valid_name = tuple + .to_token_stream() + .to_string() + .chars() + .map(|c| if c.is_ascii_alphanumeric() { c } else { '_' }) + .collect::<String>(); + + let name = format!("tuple_{}{tuple_as_valid_name}", tuple.elems.len()); + + Ok(quote! { + ::apache_avro::schema::Schema::Record(::apache_avro::schema::RecordSchema::builder() + .name(::apache_avro::schema::Name::new_with_enclosing_namespace(#name, enclosing_namespace).expect(&format!("Unable to parse variant record name for schema {}", #name)[..])) + .fields(vec![ + #(#fields, )* + ]) + .attributes( + [ + ("org.apache.avro.rust.tuple".to_string(), ::serde_json::value::Value::Bool(true)), + ].into() + ) + .build() + ) + }) + } +} + +/// Create a schema definition for an array. +/// +/// # Mapping +/// - `[T; 0]` => `Schema::Null`, +/// - `[T; 1]` => Schema of `T`, +/// - `[T; N]` => `Schema::Record`. +pub fn array_to_schema(array: &TypeArray) -> Result<TokenStream, Vec<syn::Error>> { + let Expr::Lit(ExprLit { + lit: Lit::Int(lit), .. + }) = &array.len + else { + return Err(vec![syn::Error::new( + array.span(), + "AvroSchema: Expected a integer literal for the array length", + )]); + }; + // This should always work as the length always needs to fit in a usize + let len: usize = lit.base10_parse().map_err(|e| vec![e])?; + + if len == 0 { + Ok(quote! {::apache_avro::schema::Schema::Null}) + } else if len == 1 { + type_to_schema_expr(&array.elem) + } else { + let t_schema_expr = type_to_schema_expr(&array.elem)?; + let fields = (0..len).map(|index| { + let name = format!("field_{index}"); + quote! { + ::apache_avro::schema::RecordField::builder() + .name(#name.to_string()) + .schema(#t_schema_expr) + .build() + } + }); + + // Try to create a unique name for this record, this is done as best effort and the + // name is NOT recorded in `names`. + let array_elem_as_valid_name = array + .elem + .to_token_stream() + .to_string() + .chars() + .map(|c| if c.is_ascii_alphanumeric() { c } else { '_' }) + .collect::<String>(); + + let name = format!("array_{len}_{array_elem_as_valid_name}"); + + Ok(quote! { + ::apache_avro::schema::Schema::Record(::apache_avro::schema::RecordSchema::builder() + .name(::apache_avro::schema::Name::new_with_enclosing_namespace(#name, enclosing_namespace).expect(&format!("Unable to parse variant record name for schema {}", #name)[..])) + .fields(vec![ + #(#fields, )* + ]) + .attributes( + [ + ("org.apache.avro.rust.tuple".to_string(), ::serde_json::value::Value::Bool(true)), + ].into() + ) + .build() + ) + }) + } +} + /// Create a vector of `RecordField`s named `field_{field_index}`. pub fn unnamed_to_record_fields(unnamed: FieldsUnnamed) -> Result<TokenStream, Vec<syn::Error>> { let mut fields = Vec::with_capacity(unnamed.unnamed.len());
