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 101c8c30736385286556b6688942de45bcfa9225 Author: default <[email protected]> AuthorDate: Thu Mar 5 19:57:24 2026 +0000 more testing --- avro_derive/src/attributes/mod.rs | 25 ++- avro_derive/src/enums/bare_union.rs | 44 +--- avro_derive/src/enums/record_internally_tagged.rs | 20 +- avro_derive/src/enums/record_tag_content.rs | 43 +--- avro_derive/src/enums/union_of_records.rs | 53 +---- avro_derive/src/lib.rs | 222 +++++++++---------- avro_derive/src/tuple.rs | 59 +++++ .../avro_3709_record_field_attributes.expanded.rs | 4 +- .../expanded/avro_rs_xxx_bare_union.expanded.rs | 239 +++++++++++++++++++++ ...de_transparent.rs => avro_rs_xxx_bare_union.rs} | 31 +-- ...s => avro_rs_xxx_internally_tagged.expanded.rs} | 0 ...sparent.rs => avro_rs_xxx_internally_tagged.rs} | 0 .../avro_rs_xxx_serde_from_into.expanded.rs | 12 +- .../avro_rs_xxx_serde_transparent.expanded.rs | 26 +++ .../expanded/avro_rs_xxx_serde_transparent.rs | 6 +- ...nded.rs => avro_rs_xxx_tag_content.expanded.rs} | 0 ...e_transparent.rs => avro_rs_xxx_tag_content.rs} | 0 ...rs => avro_rs_xxx_union_of_records.expanded.rs} | 0 ...nsparent.rs => avro_rs_xxx_union_of_records.rs} | 0 avro_derive/tests/expanded/mod.rs | 4 + .../avro_rs_xxx_bare_union_and_untagged.rs} | 25 ++- .../ui/avro_rs_xxx_bare_union_and_untagged.stderr | 20 ++ .../ui/avro_rs_xxx_serde_transparent_enum.stderr | 8 + 23 files changed, 553 insertions(+), 288 deletions(-) diff --git a/avro_derive/src/attributes/mod.rs b/avro_derive/src/attributes/mod.rs index 21fe772..ce609cd 100644 --- a/avro_derive/src/attributes/mod.rs +++ b/avro_derive/src/attributes/mod.rs @@ -15,11 +15,11 @@ // specific language governing permissions and limitations // under the License. -use crate::case::RenameRule; +use crate::{case::RenameRule, type_to_field_default_expr}; use darling::{FromAttributes, FromMeta}; use proc_macro2::{Span, TokenStream}; use quote::quote; -use syn::{AttrStyle, Attribute, Expr, Ident, Path, spanned::Spanned}; +use syn::{AttrStyle, Attribute, Expr, Ident, Path, Type, spanned::Spanned}; mod avro; mod serde; @@ -389,6 +389,27 @@ pub enum FieldDefault { Value(String), } +impl FieldDefault { + pub fn into_tokenstream(self, span: Span, field_type: &Type) -> Result<TokenStream, Vec<syn::Error>> { + match self { + FieldDefault::Disabled => Ok(quote! { None }), + FieldDefault::Trait => type_to_field_default_expr(field_type), + FieldDefault::Value(default_value) => { + let _: serde_json::Value = serde_json::from_str(&default_value[..]) + .map_err(|e| { + vec![syn::Error::new( + span, + format!("Invalid avro default json: \n{e}"), + )] + })?; + Ok(quote! { + Some(::serde_json::from_str(#default_value).expect(format!("Invalid JSON: {:?}", #default_value).as_str())) + }) + } + } + } +} + impl FromMeta for FieldDefault { fn from_string(value: &str) -> darling::Result<Self> { Ok(Self::Value(value.to_string())) diff --git a/avro_derive/src/enums/bare_union.rs b/avro_derive/src/enums/bare_union.rs index b0342b2..3385343 100644 --- a/avro_derive/src/enums/bare_union.rs +++ b/avro_derive/src/enums/bare_union.rs @@ -1,5 +1,6 @@ use crate::attributes::{NamedTypeOptions, VariantOptions}; -use crate::type_to_schema_expr; +use crate::tuple::tuple_to_record_schema; +use crate::{named_to_record_fields, type_to_schema_expr}; use proc_macro2::{Span, TokenStream}; use quote::quote; use syn::spanned::Spanned; @@ -13,28 +14,15 @@ pub fn get_data_enum_schema_def( let mut variant_expr = Vec::new(); let mut have_null = false; for variant in data_enum.variants { - let field_attrs = VariantOptions::new(&variant.attrs, variant.span())?; - let name = field_attrs.rename.unwrap_or_else(|| { + let variant_attrs = VariantOptions::new(&variant.attrs, variant.span())?; + let name = variant_attrs.rename.unwrap_or_else(|| { container_attrs .rename_all .apply_to_variant(&variant.ident.to_string()) }); match variant.fields { Fields::Named(named) => { - let mut fields = Vec::with_capacity(named.named.len()); - for field in named.named { - let ident = field_attrs - .rename_all - .or(container_attrs.rename_all_fields) - .apply_to_field(&field.ident.unwrap().to_string()); - let schema_expr = type_to_schema_expr(&field.ty)?; - fields.push(quote! { - ::apache_avro::schema::RecordField::builder() - .name(#ident.to_string()) - .schema(#schema_expr) - .build() - }); - } + let fields = named_to_record_fields(named, variant_attrs.rename_all.or(container_attrs.rename_all_fields))?; let schema_expr = quote! { ::apache_avro::schema::Schema::Record( @@ -59,28 +47,8 @@ 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 mut fields = Vec::with_capacity(unnamed.unnamed.len()); - for (index, field) in unnamed.unnamed.iter().enumerate() { - let field_schema_expr = type_to_schema_expr(&field.ty)?; - fields.push(quote! { - ::apache_avro::schema::RecordField::builder() - .name(format!("field_{}", #index)) - .schema(#field_schema_expr) - .build() - }); - } + let schema_expr = tuple_to_record_schema(unnamed, &name, &[])?; - let schema_expr = 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() - ) - }; variant_expr.push(schema_expr); } } diff --git a/avro_derive/src/enums/record_internally_tagged.rs b/avro_derive/src/enums/record_internally_tagged.rs index f3331dc..8c3c76e 100644 --- a/avro_derive/src/enums/record_internally_tagged.rs +++ b/avro_derive/src/enums/record_internally_tagged.rs @@ -1,5 +1,5 @@ use crate::attributes::{NamedTypeOptions, VariantOptions}; -use crate::{aliases, preserve_optional, type_to_schema_expr}; +use crate::{aliases, named_to_record_fields, preserve_optional, type_to_schema_expr}; use proc_macro2::TokenStream; use quote::quote; use syn::spanned::Spanned; @@ -15,27 +15,15 @@ pub fn get_data_enum_schema_def( let mut symbols = Vec::new(); let mut field_additions = Vec::new(); for variant in data_enum.variants { - let field_attrs = VariantOptions::new(&variant.attrs, variant.span())?; - let name = field_attrs.rename.unwrap_or_else(|| { + let variant_attrs = VariantOptions::new(&variant.attrs, variant.span())?; + let name = variant_attrs.rename.unwrap_or_else(|| { container_attrs .rename_all .apply_to_variant(&variant.ident.to_string()) }); match variant.fields { Fields::Named(named) => { - for field in named.named { - let ident = field_attrs - .rename_all - .or(container_attrs.rename_all_fields) - .apply_to_field(&field.ident.unwrap().to_string()); - let schema_expr = type_to_schema_expr(&field.ty)?; - field_additions.push(quote! { - fields.push(::apache_avro::schema::RecordField::builder() - .name(#ident.to_string()) - .schema(#schema_expr) - .build()) - }); - } + field_additions.extend(named_to_record_fields(named, variant_attrs.rename_all.or(container_attrs.rename_all_fields))?); } Fields::Unnamed(unnamed) => { if unnamed.unnamed.len() == 1 { diff --git a/avro_derive/src/enums/record_tag_content.rs b/avro_derive/src/enums/record_tag_content.rs index 5227de2..c02eca9 100644 --- a/avro_derive/src/enums/record_tag_content.rs +++ b/avro_derive/src/enums/record_tag_content.rs @@ -1,5 +1,6 @@ use crate::attributes::{NamedTypeOptions, VariantOptions}; -use crate::{aliases, preserve_optional, type_to_schema_expr}; +use crate::tuple::tuple_to_record_schema; +use crate::{aliases, named_to_record_fields, preserve_optional, type_to_schema_expr}; use proc_macro2::TokenStream; use quote::quote; use syn::spanned::Spanned; @@ -16,28 +17,15 @@ pub fn get_data_enum_schema_def( let mut symbols = Vec::new(); let mut schema_definitions = Vec::new(); for variant in data_enum.variants { - let field_attrs = VariantOptions::new(&variant.attrs, variant.span())?; - let name = field_attrs.rename.unwrap_or_else(|| { + let variant_attrs = VariantOptions::new(&variant.attrs, variant.span())?; + let name = variant_attrs.rename.unwrap_or_else(|| { container_attrs .rename_all .apply_to_variant(&variant.ident.to_string()) }); match variant.fields { Fields::Named(named) => { - let mut fields = Vec::with_capacity(named.named.len()); - for field in named.named { - let ident = field_attrs - .rename_all - .or(container_attrs.rename_all_fields) - .apply_to_field(&field.ident.unwrap().to_string()); - let schema_expr = type_to_schema_expr(&field.ty)?; - fields.push(quote! { - ::apache_avro::schema::RecordField::builder() - .name(#ident.to_string()) - .schema(#schema_expr) - .build() - }); - } + let fields = named_to_record_fields(named, variant_attrs.rename_all.or(container_attrs.rename_all_fields))?; let schema_expr = quote! { ::apache_avro::schema::Schema::Record( @@ -62,27 +50,8 @@ 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 mut fields = Vec::with_capacity(unnamed.unnamed.len()); - for (index, field) in unnamed.unnamed.iter().enumerate() { - let field_schema_expr = type_to_schema_expr(&field.ty)?; - fields.push(quote! { - ::apache_avro::schema::RecordField::builder() - .name(format!("field_{}", #index)) - .schema(#field_schema_expr) - .build() - }); - } + let schema_expr = tuple_to_record_schema(unnamed, &name, &[])?; - let schema_expr = 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,)* - ]) - .build() - ) - }; 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 4a68a0e..44a853c 100644 --- a/avro_derive/src/enums/union_of_records.rs +++ b/avro_derive/src/enums/union_of_records.rs @@ -1,5 +1,6 @@ use crate::attributes::{NamedTypeOptions, VariantOptions}; -use crate::type_to_schema_expr; +use crate::tuple::tuple_to_record_schema; +use crate::{named_to_record_fields}; use proc_macro2::TokenStream; use quote::quote; use syn::spanned::Spanned; @@ -11,28 +12,15 @@ pub fn get_data_enum_schema_def( ) -> Result<TokenStream, Vec<syn::Error>> { let mut variant_expr = Vec::new(); for variant in data_enum.variants { - let field_attrs = VariantOptions::new(&variant.attrs, variant.span())?; - let name = field_attrs.rename.unwrap_or_else(|| { + let variant_attrs = VariantOptions::new(&variant.attrs, variant.span())?; + let name = variant_attrs.rename.unwrap_or_else(|| { container_attrs .rename_all .apply_to_variant(&variant.ident.to_string()) }); match variant.fields { Fields::Named(named) => { - let mut fields = Vec::with_capacity(named.named.len()); - for field in named.named { - let ident = field_attrs - .rename_all - .or(container_attrs.rename_all_fields) - .apply_to_field(&field.ident.unwrap().to_string()); - let schema_expr = type_to_schema_expr(&field.ty)?; - fields.push(quote! { - ::apache_avro::schema::RecordField::builder() - .name(#ident.to_string()) - .schema(#schema_expr) - .build() - }); - } + let fields = named_to_record_fields(named, variant_attrs.rename_all.or(container_attrs.rename_all_fields))?; let schema_expr = quote! { ::apache_avro::schema::Schema::Record( @@ -47,33 +35,12 @@ pub fn get_data_enum_schema_def( variant_expr.push(schema_expr); } Fields::Unnamed(unnamed) => { - let mut fields = Vec::with_capacity(unnamed.unnamed.len()); - for (index, field) in unnamed.unnamed.iter().enumerate() { - let field_schema_expr = type_to_schema_expr(&field.ty)?; - fields.push(quote! { - ::apache_avro::schema::RecordField::builder() - .name(format!("field_{}", #index)) - .schema(#field_schema_expr) - .build() - }); - } - - let amount_of_fields = unnamed.unnamed.len(); - - let schema_expr = quote! { - let mut builder = ::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,)* - ]); - if #amount_of_fields == 1 { - builder = builder.attributes([("org.apache.avro.rust.union_of_records".to_string(), ::serde_json::value::Value::Bool(true))].into()); - } else if #amount_of_fields > 1 { - builder = builder.attributes([("org.apache.avro.rust.tuple".to_string(), ::serde_json::value::Value::Bool(true))].into()); - } - - ::apache_avro::schema::Schema::Record(builder.build()) + let schema_expr = if unnamed.unnamed.len() == 1 { + tuple_to_record_schema(unnamed, &name, &["org.apache.avro.rust.union_of_records"])? + } else { + tuple_to_record_schema(unnamed, &name, &[])? }; + variant_expr.push(schema_expr); } Fields::Unit => { diff --git a/avro_derive/src/lib.rs b/avro_derive/src/lib.rs index d2f2a00..96583db 100644 --- a/avro_derive/src/lib.rs +++ b/avro_derive/src/lib.rs @@ -32,17 +32,17 @@ mod attributes; mod case; mod enums; +mod tuple; use proc_macro2::{Span, TokenStream}; use quote::quote; use syn::{ - DataStruct, DeriveInput, Expr, Field, Fields, Generics, Ident, Type, parse_macro_input, - spanned::Spanned, + DataStruct, DeriveInput, Expr, Field, Fields, FieldsNamed, Generics, Ident, Type, parse_macro_input, spanned::Spanned }; use crate::{ - attributes::{FieldDefault, FieldOptions, NamedTypeOptions, With}, - case::RenameRule, + attributes::{FieldOptions, NamedTypeOptions, With}, + case::RenameRule, tuple::unnamed_to_record_fields, }; #[proc_macro_derive(AvroSchema, attributes(avro, serde))] @@ -84,7 +84,6 @@ fn derive_avro_schema(input: DeriveInput) -> Result<TokenStream, Vec<syn::Error> 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), @@ -173,91 +172,16 @@ fn handle_named_schemas(full_schema_name: String, schema_def: TokenStream) -> To fn get_struct_schema_def( container_attrs: &NamedTypeOptions, data_struct: DataStruct, - ident_span: Span, ) -> Result<(TokenStream, TokenStream), Vec<syn::Error>> { - let mut record_field_exprs = vec![]; + let mut record_field_exprs = Vec::new(); match data_struct.fields { Fields::Named(a) => { - for field in a.named { - let mut name = field - .ident - .as_ref() - .expect("Field must have a name") - .to_string(); - if let Some(raw_name) = name.strip_prefix("r#") { - name = raw_name.to_string(); - } - let field_attrs = FieldOptions::new(&field.attrs, field.span())?; - let doc = preserve_optional(field_attrs.doc); - match (field_attrs.rename, container_attrs.rename_all) { - (Some(rename), _) => { - name = rename; - } - (None, rename_all) if rename_all != RenameRule::None => { - name = rename_all.apply_to_field(&name); - } - _ => {} - } - if field_attrs.skip { - continue; - } 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 get_record_fields = - get_field_get_record_fields_expr(&field, field_attrs.with)?; - record_field_exprs.push(quote! { - if let Some(flattened_fields) = #get_record_fields { - schema_fields.extend(flattened_fields); - } else { - panic!("{} does not have any fields to flatten to", stringify!(#field)); - } - }); - - // Don't add this field as it's been replaced by the child record fields - continue; - } - let default_value = match field_attrs.default { - FieldDefault::Disabled => quote! { None }, - FieldDefault::Trait => type_to_field_default_expr(&field.ty)?, - FieldDefault::Value(default_value) => { - let _: serde_json::Value = serde_json::from_str(&default_value[..]) - .map_err(|e| { - vec![syn::Error::new( - field.ident.span(), - format!("Invalid avro default json: \n{e}"), - )] - })?; - quote! { - Some(::serde_json::from_str(#default_value).expect(format!("Invalid JSON: {:?}", #default_value).as_str())) - } - } - }; - let aliases = field_aliases(&field_attrs.alias); - let schema_expr = get_field_schema_expr(&field, field_attrs.with)?; - record_field_exprs.push(quote! { - schema_fields.push(::apache_avro::schema::RecordField { - name: #name.to_string(), - doc: #doc, - default: #default_value, - aliases: #aliases, - schema: #schema_expr, - custom_attributes: ::std::collections::BTreeMap::new(), - }); - }); - } + record_field_exprs.extend(named_to_record_fields(a, container_attrs.rename_all)?); } - Fields::Unnamed(_) => { - return Err(vec![syn::Error::new( - ident_span, - "AvroSchema derive does not work for tuple structs", - )]); - } - Fields::Unit => { - return Err(vec![syn::Error::new( - ident_span, - "AvroSchema derive does not work for unit structs", - )]); + Fields::Unnamed(unnamed) => { + record_field_exprs.extend(unnamed_to_record_fields(unnamed)?); } + Fields::Unit => {} } let record_doc = preserve_optional(container_attrs.doc.as_ref()); @@ -332,13 +256,36 @@ fn get_transparent_struct_schema_def( )]) } } - Fields::Unnamed(_) => Err(vec![syn::Error::new( - input_span, - "AvroSchema: derive does not work for tuple structs", - )]), + Fields::Unnamed(unnamed) => { + let mut found = None; + for field in unnamed.unnamed { + let attrs = FieldOptions::new(&field.attrs, field.span())?; + if attrs.skip { + continue; + } + if found.replace((field, attrs)).is_some() { + return Err(vec![syn::Error::new( + input_span, + "AvroSchema: #[serde(transparent)] is only allowed on structs with one unskipped field", + )]); + } + } + + if let Some((field, attrs)) = found { + 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, + "AvroSchema: #[serde(transparent)] is only allowed on structs with one unskipped field", + )]) + } + }, Fields::Unit => Err(vec![syn::Error::new( input_span, - "AvroSchema: derive does not work for unit structs", + "AvroSchema: `#[serde(transparent)` does not work for unit structs", )]), } } @@ -469,6 +416,62 @@ fn type_to_field_default_expr(ty: &Type) -> Result<TokenStream, Vec<syn::Error>> } } +/// Create a vector of `RecordField`s. +fn named_to_record_fields(named: FieldsNamed, rename_all: RenameRule) -> Result<Vec<TokenStream>, Vec<syn::Error>> { + let mut fields = Vec::with_capacity(named.named.len()); + for field in named.named { + let field_attrs = FieldOptions::new(&field.attrs, field.span())?; + if field_attrs.skip { + continue; + } 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 get_record_fields = get_field_get_record_fields_expr(&field, field_attrs.with)?; + fields.push(quote! { + if let Some(flattened_fields) = #get_record_fields { + schema_fields.extend(flattened_fields); + } else { + panic!("{} does not have any fields to flatten to", stringify!(#field)); + } + }); + + // Don't add this field as it's been replaced by the child record fields + continue; + } + let mut name = field + .ident + .as_ref() + .expect("Field must have a name") + .to_string(); + if let Some(raw_name) = name.strip_prefix("r#") { + name = raw_name.to_string(); + } + match (field_attrs.rename, rename_all) { + (Some(rename), _) => { + name = rename; + } + (None, rename_all) if rename_all != RenameRule::None => { + name = rename_all.apply_to_field(&name); + } + _ => {} + } + let default_value = field_attrs.default.into_tokenstream(field.ident.span(), &field.ty)?; + let aliases = field_aliases(&field_attrs.alias); + let doc = doc_into_tokenstream(field_attrs.doc); + let field_schema_expr = type_to_schema_expr(&field.ty)?; + fields.push(quote! { + ::apache_avro::schema::RecordField::builder() + .name(#name.to_string()) + .maybe_doc(#doc) + .maybe_default(#default_value) + .aliases(#aliases) + .schema(#field_schema_expr) + .build() + }); + } + Ok(fields) +} + /// Stolen from serde fn to_compile_errors(errors: Vec<syn::Error>) -> proc_macro2::TokenStream { let compile_errors = errors.iter().map(syn::Error::to_compile_error); @@ -482,6 +485,13 @@ fn preserve_optional(op: Option<impl quote::ToTokens>) -> TokenStream { } } +fn doc_into_tokenstream(doc: Option<String>) -> TokenStream { + match doc { + Some(doc) => quote! {::std::option::Option::Some(#doc.to_string())}, + None => quote! {::std::option::Option::None}, + } +} + fn aliases(op: &[impl quote::ToTokens]) -> TokenStream { let items: Vec<TokenStream> = op .iter() @@ -511,38 +521,6 @@ mod tests { use super::*; use pretty_assertions::assert_eq; - #[test] - fn tuple_struct_unsupported() { - let test_tuple_struct = quote! { - struct B (i32, String); - }; - - match syn::parse2::<DeriveInput>(test_tuple_struct) { - Ok(input) => { - assert!(derive_avro_schema(input).is_err()) - } - Err(error) => panic!( - "Failed to parse as derive input when it should be able to. Error: {error:?}" - ), - }; - } - - #[test] - fn unit_struct_unsupported() { - let test_tuple_struct = quote! { - struct AbsoluteUnit; - }; - - match syn::parse2::<DeriveInput>(test_tuple_struct) { - Ok(input) => { - assert!(derive_avro_schema(input).is_err()) - } - Err(error) => panic!( - "Failed to parse as derive input when it should be able to. Error: {error:?}" - ), - }; - } - #[test] fn struct_with_optional() { let struct_with_optional = quote! { diff --git a/avro_derive/src/tuple.rs b/avro_derive/src/tuple.rs new file mode 100644 index 0000000..8d98995 --- /dev/null +++ b/avro_derive/src/tuple.rs @@ -0,0 +1,59 @@ +use proc_macro2::TokenStream; +use syn::{FieldsUnnamed, spanned::Spanned}; +use quote::quote; + +use crate::{FieldOptions, doc_into_tokenstream, field_aliases, type_to_schema_expr}; + +/// Create a `Schema::Record` from this tuple definition. +/// +/// Fields are named `field_{field_index}` and the struct will have the provided name. +/// +/// 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(unnamed: FieldsUnnamed, name: &str, extra_attributes: &[&str]) -> Result<TokenStream, Vec<syn::Error>> { + let fields = unnamed_to_record_fields(unnamed)?; + + 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)), + #((#extra_attributes.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<Vec<TokenStream>, Vec<syn::Error>> { + let mut fields = Vec::with_capacity(unnamed.unnamed.len()); + for (index, field) in unnamed.unnamed.into_iter().enumerate() { + let field_attrs = FieldOptions::new(&field.attrs, field.span())?; + if field_attrs.skip { + continue; + } else if field_attrs.flatten { + return Err(vec![ + syn::Error::new(field.span(), "AvroSchema: `#[serde(flatten)]` is not supported on tuple fields") + ]); + } + let default_value = field_attrs.default.into_tokenstream(field.ident.span(), &field.ty)?; + let aliases = field_aliases(&field_attrs.alias); + let doc = doc_into_tokenstream(field_attrs.doc); + let name = field_attrs.rename.unwrap_or_else(|| format!("field_{index}")); + let field_schema_expr = type_to_schema_expr(&field.ty)?; + fields.push(quote! { + ::apache_avro::schema::RecordField::builder() + .name(#name.to_string()) + .maybe_doc(#doc) + .maybe_default(#default_value) + .aliases(#aliases) + .schema(#field_schema_expr) + .build() + }); + } + Ok(fields) +} diff --git a/avro_derive/tests/expanded/avro_3709_record_field_attributes.expanded.rs b/avro_derive/tests/expanded/avro_3709_record_field_attributes.expanded.rs index e0e46b5..142b403 100644 --- a/avro_derive/tests/expanded/avro_3709_record_field_attributes.expanded.rs +++ b/avro_derive/tests/expanded/avro_3709_record_field_attributes.expanded.rs @@ -28,7 +28,7 @@ impl ::apache_avro::AvroSchemaComponent for A { schema_fields .push(::apache_avro::schema::RecordField { name: "a3".to_string(), - doc: ::std::option::Option::Some("a doc".into()), + doc: ::std::option::Option::Some("a doc".to_string()), default: Some( ::serde_json::from_str("123") .expect( @@ -110,7 +110,7 @@ impl ::apache_avro::AvroSchemaComponent for A { schema_fields .push(::apache_avro::schema::RecordField { name: "a3".to_string(), - doc: ::std::option::Option::Some("a doc".into()), + doc: ::std::option::Option::Some("a doc".to_string()), default: Some( ::serde_json::from_str("123") .expect( diff --git a/avro_derive/tests/expanded/avro_rs_xxx_bare_union.expanded.rs b/avro_derive/tests/expanded/avro_rs_xxx_bare_union.expanded.rs new file mode 100644 index 0000000..653186e --- /dev/null +++ b/avro_derive/tests/expanded/avro_rs_xxx_bare_union.expanded.rs @@ -0,0 +1,239 @@ +use apache_avro::AvroSchema; +#[serde(untagged)] +enum Abc { + A, + B(bool), + C(#[avro(doc = "This is an int")] i32, i64), + D {}, + E { is_it_true: bool }, + F { #[avro(doc = "This is X")] x: f64, y: f32 }, +} +#[automatically_derived] +impl ::apache_avro::AvroSchemaComponent for Abc { + fn get_schema_in_ctxt( + named_schemas: &mut ::std::collections::HashSet<::apache_avro::schema::Name>, + enclosing_namespace: ::apache_avro::schema::NamespaceRef, + ) -> ::apache_avro::schema::Schema { + let name = ::apache_avro::schema::Name::new_with_enclosing_namespace( + "Abc", + enclosing_namespace, + ) + .expect("Unable to parse schema name Abc"); + if named_schemas.contains(&name) { + ::apache_avro::schema::Schema::Ref { + name, + } + } else { + let enclosing_namespace = name.namespace(); + named_schemas.insert(name.clone()); + let mut builder = ::apache_avro::schema::UnionSchema::builder(); + builder + .variant(::apache_avro::schema::Schema::Null) + .expect("Duplicate Schema found"); + builder + .variant( + <bool as ::apache_avro::AvroSchemaComponent>::get_schema_in_ctxt( + named_schemas, + enclosing_namespace, + ), + ) + .expect("Duplicate Schema found"); + builder + .variant( + ::apache_avro::schema::Schema::Record( + ::apache_avro::schema::RecordSchema::builder() + .name( + ::apache_avro::schema::Name::new_with_enclosing_namespace( + "C", + enclosing_namespace, + ) + .expect( + &::alloc::__export::must_use({ + ::alloc::fmt::format( + format_args!( + "Unable to parse variant record name for schema {0}", "C", + ), + ) + })[..], + ), + ) + .fields( + ::alloc::boxed::box_assume_init_into_vec_unsafe( + ::alloc::intrinsics::write_box_via_move( + ::alloc::boxed::Box::new_uninit(), + [ + ::apache_avro::schema::RecordField::builder() + .name("field_0".to_string()) + .maybe_doc( + ::std::option::Option::Some("This is an int".to_string()), + ) + .maybe_default( + <i32 as ::apache_avro::AvroSchemaComponent>::field_default(), + ) + .aliases(::std::vec::Vec::new()) + .schema( + <i32 as ::apache_avro::AvroSchemaComponent>::get_schema_in_ctxt( + named_schemas, + enclosing_namespace, + ), + ) + .build(), + ::apache_avro::schema::RecordField::builder() + .name("field_1".to_string()) + .maybe_doc(::std::option::Option::None) + .maybe_default( + <i64 as ::apache_avro::AvroSchemaComponent>::field_default(), + ) + .aliases(::std::vec::Vec::new()) + .schema( + <i64 as ::apache_avro::AvroSchemaComponent>::get_schema_in_ctxt( + named_schemas, + enclosing_namespace, + ), + ) + .build(), + ], + ), + ), + ) + .attributes( + [ + ( + "org.apache.avro.rust.tuple".to_string(), + ::serde_json::value::Value::Bool(true), + ), + ] + .into(), + ) + .build(), + ), + ) + .expect("Duplicate Schema found"); + builder + .variant( + ::apache_avro::schema::Schema::Record( + ::apache_avro::schema::RecordSchema::builder() + .name( + ::apache_avro::schema::Name::new_with_enclosing_namespace( + "D", + enclosing_namespace, + ) + .expect( + &::alloc::__export::must_use({ + ::alloc::fmt::format( + format_args!( + "Unable to parse variant record name for schema {0}", "D", + ), + ) + })[..], + ), + ) + .fields(::alloc::vec::Vec::new()) + .build(), + ), + ) + .expect("Duplicate Schema found"); + builder + .variant( + ::apache_avro::schema::Schema::Record( + ::apache_avro::schema::RecordSchema::builder() + .name( + ::apache_avro::schema::Name::new_with_enclosing_namespace( + "E", + enclosing_namespace, + ) + .expect( + &::alloc::__export::must_use({ + ::alloc::fmt::format( + format_args!( + "Unable to parse variant record name for schema {0}", "E", + ), + ) + })[..], + ), + ) + .fields( + ::alloc::boxed::box_assume_init_into_vec_unsafe( + ::alloc::intrinsics::write_box_via_move( + ::alloc::boxed::Box::new_uninit(), + [ + ::apache_avro::schema::RecordField::builder() + .name("is_it_true".to_string()) + .schema( + <bool as ::apache_avro::AvroSchemaComponent>::get_schema_in_ctxt( + named_schemas, + enclosing_namespace, + ), + ) + .build(), + ], + ), + ), + ) + .build(), + ), + ) + .expect("Duplicate Schema found"); + builder + .variant( + ::apache_avro::schema::Schema::Record( + ::apache_avro::schema::RecordSchema::builder() + .name( + ::apache_avro::schema::Name::new_with_enclosing_namespace( + "F", + enclosing_namespace, + ) + .expect( + &::alloc::__export::must_use({ + ::alloc::fmt::format( + format_args!( + "Unable to parse variant record name for schema {0}", "F", + ), + ) + })[..], + ), + ) + .fields( + ::alloc::boxed::box_assume_init_into_vec_unsafe( + ::alloc::intrinsics::write_box_via_move( + ::alloc::boxed::Box::new_uninit(), + [ + ::apache_avro::schema::RecordField::builder() + .name("x".to_string()) + .schema( + <f64 as ::apache_avro::AvroSchemaComponent>::get_schema_in_ctxt( + named_schemas, + enclosing_namespace, + ), + ) + .build(), + ::apache_avro::schema::RecordField::builder() + .name("y".to_string()) + .schema( + <f32 as ::apache_avro::AvroSchemaComponent>::get_schema_in_ctxt( + named_schemas, + enclosing_namespace, + ), + ) + .build(), + ], + ), + ), + ) + .build(), + ), + ) + .expect("Duplicate Schema found"); + ::apache_avro::schema::Schema::Union(builder.build()) + } + } + fn get_record_fields_in_ctxt( + named_schemas: &mut ::std::collections::HashSet<::apache_avro::schema::Name>, + enclosing_namespace: ::apache_avro::schema::NamespaceRef, + ) -> ::std::option::Option<::std::vec::Vec<::apache_avro::schema::RecordField>> { + None + } + fn field_default() -> ::std::option::Option<::serde_json::Value> { + ::std::option::Option::None + } +} diff --git a/avro_derive/tests/expanded/avro_rs_xxx_serde_transparent.rs b/avro_derive/tests/expanded/avro_rs_xxx_bare_union.rs similarity index 76% copy from avro_derive/tests/expanded/avro_rs_xxx_serde_transparent.rs copy to avro_derive/tests/expanded/avro_rs_xxx_bare_union.rs index b23413b..34a3c51 100644 --- a/avro_derive/tests/expanded/avro_rs_xxx_serde_transparent.rs +++ b/avro_derive/tests/expanded/avro_rs_xxx_bare_union.rs @@ -18,17 +18,22 @@ use apache_avro::AvroSchema; #[derive(AvroSchema)] -struct A { - a: i32, - b: String, +#[serde(untagged)] +enum Abc { + A, + B(bool), + C( + #[avro(doc = "This is an int")] + i32, + i64 + ), + D {}, + E { + is_it_true: bool, + }, + F { + #[avro(doc = "This is X")] + x: f64, + y: f32, + } } - -#[derive(AvroSchema)] -#[serde(transparent)] -struct B { - a: A, -} - -// #[derive(AvroSchema)] -// #[serde(transparent)] -// struct C(A); diff --git a/avro_derive/tests/expanded/avro_rs_xxx_serde_transparent.expanded.rs b/avro_derive/tests/expanded/avro_rs_xxx_internally_tagged.expanded.rs similarity index 100% copy from avro_derive/tests/expanded/avro_rs_xxx_serde_transparent.expanded.rs copy to avro_derive/tests/expanded/avro_rs_xxx_internally_tagged.expanded.rs diff --git a/avro_derive/tests/expanded/avro_rs_xxx_serde_transparent.rs b/avro_derive/tests/expanded/avro_rs_xxx_internally_tagged.rs similarity index 100% copy from avro_derive/tests/expanded/avro_rs_xxx_serde_transparent.rs copy to avro_derive/tests/expanded/avro_rs_xxx_internally_tagged.rs diff --git a/avro_derive/tests/expanded/avro_rs_xxx_serde_from_into.expanded.rs b/avro_derive/tests/expanded/avro_rs_xxx_serde_from_into.expanded.rs index 83b87d8..77cf011 100644 --- a/avro_derive/tests/expanded/avro_rs_xxx_serde_from_into.expanded.rs +++ b/avro_derive/tests/expanded/avro_rs_xxx_serde_from_into.expanded.rs @@ -139,15 +139,21 @@ impl ::apache_avro::AvroSchemaComponent for B { named_schemas: &mut ::std::collections::HashSet<::apache_avro::schema::Name>, enclosing_namespace: ::apache_avro::schema::NamespaceRef, ) -> ::apache_avro::schema::Schema { - A::get_schema_in_ctxt(named_schemas, enclosing_namespace) + <A as ::apache_avro::AvroSchemaComponent>::get_schema_in_ctxt( + named_schemas, + enclosing_namespace, + ) } fn get_record_fields_in_ctxt( named_schemas: &mut ::std::collections::HashSet<::apache_avro::schema::Name>, enclosing_namespace: ::apache_avro::schema::NamespaceRef, ) -> ::std::option::Option<::std::vec::Vec<::apache_avro::schema::RecordField>> { - A::get_record_fields_in_ctxt(named_schemas, enclosing_namespace) + <A as ::apache_avro::AvroSchemaComponent>::get_record_fields_in_ctxt( + named_schemas, + enclosing_namespace, + ) } fn field_default() -> ::std::option::Option<::serde_json::Value> { - A::field_default() + <A as ::apache_avro::AvroSchemaComponent>::field_default() } } diff --git a/avro_derive/tests/expanded/avro_rs_xxx_serde_transparent.expanded.rs b/avro_derive/tests/expanded/avro_rs_xxx_serde_transparent.expanded.rs index b85a630..f391930 100644 --- a/avro_derive/tests/expanded/avro_rs_xxx_serde_transparent.expanded.rs +++ b/avro_derive/tests/expanded/avro_rs_xxx_serde_transparent.expanded.rs @@ -157,3 +157,29 @@ impl ::apache_avro::AvroSchemaComponent for B { ::std::option::Option::None } } +#[serde(transparent)] +struct C(A); +#[automatically_derived] +impl ::apache_avro::AvroSchemaComponent for C { + fn get_schema_in_ctxt( + named_schemas: &mut ::std::collections::HashSet<::apache_avro::schema::Name>, + enclosing_namespace: ::apache_avro::schema::NamespaceRef, + ) -> ::apache_avro::schema::Schema { + <A as ::apache_avro::AvroSchemaComponent>::get_schema_in_ctxt( + named_schemas, + enclosing_namespace, + ) + } + fn get_record_fields_in_ctxt( + named_schemas: &mut ::std::collections::HashSet<::apache_avro::schema::Name>, + enclosing_namespace: ::apache_avro::schema::NamespaceRef, + ) -> ::std::option::Option<::std::vec::Vec<::apache_avro::schema::RecordField>> { + <A as ::apache_avro::AvroSchemaComponent>::get_record_fields_in_ctxt( + named_schemas, + enclosing_namespace, + ) + } + fn field_default() -> ::std::option::Option<::serde_json::Value> { + ::std::option::Option::None + } +} diff --git a/avro_derive/tests/expanded/avro_rs_xxx_serde_transparent.rs b/avro_derive/tests/expanded/avro_rs_xxx_serde_transparent.rs index b23413b..aa0a1bc 100644 --- a/avro_derive/tests/expanded/avro_rs_xxx_serde_transparent.rs +++ b/avro_derive/tests/expanded/avro_rs_xxx_serde_transparent.rs @@ -29,6 +29,6 @@ struct B { a: A, } -// #[derive(AvroSchema)] -// #[serde(transparent)] -// struct C(A); +#[derive(AvroSchema)] +#[serde(transparent)] +struct C(A); diff --git a/avro_derive/tests/expanded/avro_rs_xxx_serde_transparent.expanded.rs b/avro_derive/tests/expanded/avro_rs_xxx_tag_content.expanded.rs similarity index 100% copy from avro_derive/tests/expanded/avro_rs_xxx_serde_transparent.expanded.rs copy to avro_derive/tests/expanded/avro_rs_xxx_tag_content.expanded.rs diff --git a/avro_derive/tests/expanded/avro_rs_xxx_serde_transparent.rs b/avro_derive/tests/expanded/avro_rs_xxx_tag_content.rs similarity index 100% copy from avro_derive/tests/expanded/avro_rs_xxx_serde_transparent.rs copy to avro_derive/tests/expanded/avro_rs_xxx_tag_content.rs diff --git a/avro_derive/tests/expanded/avro_rs_xxx_serde_transparent.expanded.rs b/avro_derive/tests/expanded/avro_rs_xxx_union_of_records.expanded.rs similarity index 100% copy from avro_derive/tests/expanded/avro_rs_xxx_serde_transparent.expanded.rs copy to avro_derive/tests/expanded/avro_rs_xxx_union_of_records.expanded.rs diff --git a/avro_derive/tests/expanded/avro_rs_xxx_serde_transparent.rs b/avro_derive/tests/expanded/avro_rs_xxx_union_of_records.rs similarity index 100% copy from avro_derive/tests/expanded/avro_rs_xxx_serde_transparent.rs copy to avro_derive/tests/expanded/avro_rs_xxx_union_of_records.rs diff --git a/avro_derive/tests/expanded/mod.rs b/avro_derive/tests/expanded/mod.rs index edea4cf..b21cdd8 100644 --- a/avro_derive/tests/expanded/mod.rs +++ b/avro_derive/tests/expanded/mod.rs @@ -23,3 +23,7 @@ mod avro_rs_207_rename_attr_over_rename_all_attribute; mod avro_rs_xxx_basic; mod avro_rs_xxx_serde_from_into; mod avro_rs_xxx_serde_transparent; +// mod avro_rs_xxx_bare_union; +mod avro_rs_xxx_internally_tagged; +mod avro_rs_xxx_tag_content; +mod avro_rs_xxx_union_of_records; diff --git a/avro_derive/tests/expanded/avro_rs_xxx_serde_transparent.rs b/avro_derive/tests/ui/avro_rs_xxx_bare_union_and_untagged.rs similarity index 79% copy from avro_derive/tests/expanded/avro_rs_xxx_serde_transparent.rs copy to avro_derive/tests/ui/avro_rs_xxx_bare_union_and_untagged.rs index b23413b..d209315 100644 --- a/avro_derive/tests/expanded/avro_rs_xxx_serde_transparent.rs +++ b/avro_derive/tests/ui/avro_rs_xxx_bare_union_and_untagged.rs @@ -18,17 +18,24 @@ use apache_avro::AvroSchema; #[derive(AvroSchema)] -struct A { - a: i32, - b: String, +#[avro(repr = "bare_union")] +enum A { + A } #[derive(AvroSchema)] -#[serde(transparent)] -struct B { - a: A, +#[avro(repr = "bare_union")] +#[serde(untagged)] +enum B { + A, + B, } -// #[derive(AvroSchema)] -// #[serde(transparent)] -// struct C(A); +#[derive(AvroSchema)] +#[avro(repr = "bare_union")] +#[serde(untagged)] +enum C { + A(), +} + +pub fn main() {} diff --git a/avro_derive/tests/ui/avro_rs_xxx_bare_union_and_untagged.stderr b/avro_derive/tests/ui/avro_rs_xxx_bare_union_and_untagged.stderr new file mode 100644 index 0000000..dbe041a --- /dev/null +++ b/avro_derive/tests/ui/avro_rs_xxx_bare_union_and_untagged.stderr @@ -0,0 +1,20 @@ +error: AvroSchema: `#[avro(repr = "bare_union")]` requires `#[serde(untagged)]` + --> tests/ui/avro_rs_xxx_bare_union_and_untagged.rs:21:1 + | +21 | / #[avro(repr = "bare_union")] +22 | | enum A { +23 | | A +24 | | } + | |_^ + +error: More than one variant maps to Schema::Null, this is not supported for bare unions + --> tests/ui/avro_rs_xxx_bare_union_and_untagged.rs:29:6 + | +29 | enum B { + | ^ + +error: AvroSchema: Empty tuple variants are not supported for bare unions + --> tests/ui/avro_rs_xxx_bare_union_and_untagged.rs:38:6 + | +38 | A(), + | ^^ diff --git a/avro_derive/tests/ui/avro_rs_xxx_serde_transparent_enum.stderr b/avro_derive/tests/ui/avro_rs_xxx_serde_transparent_enum.stderr new file mode 100644 index 0000000..bad93cc --- /dev/null +++ b/avro_derive/tests/ui/avro_rs_xxx_serde_transparent_enum.stderr @@ -0,0 +1,8 @@ +error: AvroSchema: `#[serde(transparent)]` is only supported on structs + --> tests/ui/avro_rs_xxx_serde_transparent_enum.rs:21:1 + | +21 | / #[serde(transparent)] +22 | | enum D { +23 | | A(A) +24 | | } + | |_^
