This is an automated email from the ASF dual-hosted git repository. Kriskras99 pushed a commit to branch refactor/derive in repository https://gitbox.apache.org/repos/asf/avro-rs.git
commit 829daf0a5211a6933a04c2994ea9f8465bcc2878 Author: Kriskras99 <[email protected]> AuthorDate: Sat Jun 20 10:50:00 2026 +0200 refactor: Improve code structure of the derive code This also changes the implementation of `utils::field_aliases` to remove the unnecessary `try_from`. --- avro_derive/src/attributes/serde.rs | 2 +- avro_derive/src/fields.rs | 165 +++++++++ avro_derive/src/lib.rs | 368 +-------------------- avro_derive/src/structs.rs | 169 ++++++++++ avro_derive/src/utils.rs | 50 +++ .../avro_3709_record_field_attributes.expanded.rs | 10 +- .../avro_rs_207_rename_all_attribute.expanded.rs | 8 +- ...name_attr_over_rename_all_attribute.expanded.rs | 8 +- .../tests/expanded/avro_rs_501_basic.expanded.rs | 8 +- .../expanded/avro_rs_501_namespace.expanded.rs | 8 +- .../expanded/avro_rs_501_reference.expanded.rs | 8 +- .../avro_rs_501_struct_with_optional.expanded.rs | 4 +- 12 files changed, 417 insertions(+), 391 deletions(-) diff --git a/avro_derive/src/attributes/serde.rs b/avro_derive/src/attributes/serde.rs index b1ca6fa..83d0711 100644 --- a/avro_derive/src/attributes/serde.rs +++ b/avro_derive/src/attributes/serde.rs @@ -249,8 +249,8 @@ pub struct FieldAttributes { #[cfg(test)] mod tests { use crate::{ - RenameRule, attributes::serde::{ContainerAttributes, RenameAll, SerdeDefault}, + case::RenameRule, }; use darling::FromAttributes; use syn::DeriveInput; diff --git a/avro_derive/src/fields.rs b/avro_derive/src/fields.rs new file mode 100644 index 0000000..c8024a4 --- /dev/null +++ b/avro_derive/src/fields.rs @@ -0,0 +1,165 @@ +use crate::attributes::{FieldDefault, With}; +use proc_macro2::TokenStream; +use quote::quote; +use syn::spanned::Spanned; +use syn::{Expr, Field, Type}; + +pub fn get_field_schema_expr(field: &Field, with: With) -> Result<TokenStream, Vec<syn::Error>> { + match with { + With::Trait => Ok(type_to_schema_expr(&field.ty)?), + With::Serde(path) => { + Ok(quote! { #path::get_schema_in_ctxt(named_schemas, enclosing_namespace) }) + } + With::Expr(Expr::Closure(closure)) => { + if closure.inputs.is_empty() { + Ok(quote! { (#closure)() }) + } else { + Err(vec![syn::Error::new( + field.span(), + "Expected closure with 0 parameters", + )]) + } + } + With::Expr(Expr::Path(path)) => Ok(quote! { #path(named_schemas, enclosing_namespace) }), + With::Expr(_expr) => Err(vec![syn::Error::new( + field.span(), + "Invalid expression, expected function or closure", + )]), + } +} + +pub fn get_field_get_record_fields_expr( + field: &Field, + with: With, +) -> Result<TokenStream, Vec<syn::Error>> { + match with { + With::Trait => Ok(type_to_get_record_fields_expr(&field.ty)?), + With::Serde(path) => { + Ok(quote! { #path::get_record_fields_in_ctxt(named_schemas, enclosing_namespace) }) + } + With::Expr(Expr::Closure(closure)) => { + if closure.inputs.is_empty() { + Ok(quote! { + ::apache_avro::serde::get_record_fields_in_ctxt( + named_schemas, + enclosing_namespace, + |_, _| (#closure)(), + ) + }) + } else { + Err(vec![syn::Error::new( + field.span(), + "Expected closure with 0 parameters", + )]) + } + } + With::Expr(Expr::Path(path)) => Ok(quote! { + ::apache_avro::serde::get_record_fields_in_ctxt(named_schemas, enclosing_namespace, #path) + }), + With::Expr(_expr) => Err(vec![syn::Error::new( + field.span(), + "Invalid expression, expected function or closure", + )]), + } +} + +pub fn get_field_field_default_expr( + field: &Field, + default: FieldDefault, +) -> Result<TokenStream, Vec<syn::Error>> { + match default { + FieldDefault::Disabled => Ok(quote! { ::std::option::Option::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}"), + )] + })?; + Ok(quote! { + ::std::option::Option::Some(::serde_json::from_str(#default_value).expect("Unreachable! This parsed at compile time!")) + }) + } + } +} + +/// Takes in the Tokens of a type and returns the tokens of an expression with return type `Schema` +fn type_to_schema_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_schema_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 type_to_get_record_fields_expr(ty: &Type) -> Result<TokenStream, Vec<syn::Error>> { + match ty { + Type::Array(_) | Type::Slice(_) | Type::Path(_) | Type::Reference(_) => Ok( + quote! {<#ty as :: apache_avro::AvroSchemaComponent>::get_record_fields_in_ctxt(named_schemas, enclosing_namespace)}, + ), + Type::Ptr(_) => Err(vec![syn::Error::new_spanned( + ty, + "AvroSchema: derive does not support raw pointers", + )]), + Type::Tuple(_) => Err(vec![syn::Error::new_spanned( + ty, + "AvroSchema: derive does not support tuples", + )]), + _ => Err(vec![syn::Error::new_spanned( + ty, + format!( + "AvroSchema: Unexpected type encountered! Please open an issue if this kind of type should be supported: {ty:?}" + ), + )]), + } +} + +fn type_to_field_default_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>::field_default()}) + } + 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:?}" + ), + )]), + } +} + +#[cfg(test)] +mod tests { + use super::*; + use pretty_assertions::assert_eq; + + #[test] + fn test_trait_cast() { + assert_eq!(type_to_schema_expr(&syn::parse2::<Type>(quote!{i32}).unwrap()).unwrap().to_string(), quote!{<i32 as :: apache_avro::AvroSchemaComponent>::get_schema_in_ctxt(named_schemas, enclosing_namespace)}.to_string()); + assert_eq!(type_to_schema_expr(&syn::parse2::<Type>(quote!{Vec<T>}).unwrap()).unwrap().to_string(), quote!{<Vec<T> as :: apache_avro::AvroSchemaComponent>::get_schema_in_ctxt(named_schemas, enclosing_namespace)}.to_string()); + assert_eq!(type_to_schema_expr(&syn::parse2::<Type>(quote!{AnyType}).unwrap()).unwrap().to_string(), quote!{<AnyType as :: apache_avro::AvroSchemaComponent>::get_schema_in_ctxt(named_schemas, enclosing_namespace)}.to_string()); + } +} diff --git a/avro_derive/src/lib.rs b/avro_derive/src/lib.rs index d63ed8f..e2bc329 100644 --- a/avro_derive/src/lib.rs +++ b/avro_derive/src/lib.rs @@ -32,19 +32,18 @@ mod attributes; mod case; mod enums; +mod fields; +mod structs; +mod utils; -use proc_macro2::{Span, TokenStream}; +use proc_macro2::TokenStream; use quote::quote; -use syn::{ - DataStruct, DeriveInput, Expr, Field, Fields, Generics, Ident, Type, parse_macro_input, - spanned::Spanned, -}; +use syn::{DeriveInput, Generics, Ident, parse_macro_input, spanned::Spanned}; +use crate::attributes::NamedTypeOptions; use crate::enums::get_data_enum_schema_def; -use crate::{ - attributes::{FieldDefault, FieldOptions, NamedTypeOptions, With}, - case::RenameRule, -}; +use crate::structs::{get_struct_schema_def, get_transparent_struct_schema_def}; +use crate::utils::{aliases, preserve_optional, to_compile_errors}; #[proc_macro_derive(AvroSchema, attributes(avro, serde))] // Templated from Serde @@ -146,354 +145,3 @@ fn handle_named_schemas(full_schema_name: &str, schema_def: &TokenStream) -> Tok } } } - -/// Generate a schema definition for a struct. -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![]; - 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! { ::std::option::Option::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! { - ::std::option::Option::Some(::serde_json::from_str(#default_value).expect("Unreachable! This parsed at compile time!")) - } - } - }; - 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(), - }); - }); - } - } - 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", - )]); - } - } - - let record_doc = preserve_optional(container_attrs.doc.as_ref()); - let record_aliases = aliases(&container_attrs.aliases); - let full_schema_name = &container_attrs.name; - - // When flatten is involved, there will be more but we don't know how many. This optimises for - // the most common case where there is no flatten. - let minimum_fields = record_field_exprs.len(); - - let schema_def = quote! { - { - let mut schema_fields = ::std::vec::Vec::with_capacity(#minimum_fields); - #(#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:?}"); - let name = ::apache_avro::schema::Name::new(#full_schema_name).expect(&format!("Unable to parse struct name for schema {}", #full_schema_name)[..]); - let lookup: ::std::collections::BTreeMap<String, usize> = schema_fields - .iter() - .enumerate() - .map(|(position, field)| (field.name.to_owned(), position)) - .collect(); - ::apache_avro::schema::Schema::Record(::apache_avro::schema::RecordSchema { - name, - aliases: #record_aliases, - doc: #record_doc, - fields: schema_fields, - lookup, - attributes: ::std::collections::BTreeMap::new(), - }) - } - }; - let record_fields = quote! { - let mut schema_fields = ::std::vec::Vec::with_capacity(#minimum_fields); - #(#record_field_exprs)* - ::std::option::Option::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, TokenStream), Vec<syn::Error>> { - match fields { - Fields::Named(fields_named) => { - let mut found = None; - for field in fields_named.named { - 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::Unnamed(_) => Err(vec![syn::Error::new( - input_span, - "AvroSchema: derive does not work for tuple structs", - )]), - Fields::Unit => Err(vec![syn::Error::new( - input_span, - "AvroSchema: derive does not work for unit structs", - )]), - } -} - -fn get_field_schema_expr(field: &Field, with: With) -> Result<TokenStream, Vec<syn::Error>> { - match with { - With::Trait => Ok(type_to_schema_expr(&field.ty)?), - With::Serde(path) => { - Ok(quote! { #path::get_schema_in_ctxt(named_schemas, enclosing_namespace) }) - } - With::Expr(Expr::Closure(closure)) => { - if closure.inputs.is_empty() { - Ok(quote! { (#closure)() }) - } else { - Err(vec![syn::Error::new( - field.span(), - "Expected closure with 0 parameters", - )]) - } - } - With::Expr(Expr::Path(path)) => Ok(quote! { #path(named_schemas, enclosing_namespace) }), - With::Expr(_expr) => Err(vec![syn::Error::new( - field.span(), - "Invalid expression, expected function or closure", - )]), - } -} - -fn get_field_get_record_fields_expr( - field: &Field, - with: With, -) -> Result<TokenStream, Vec<syn::Error>> { - match with { - With::Trait => Ok(type_to_get_record_fields_expr(&field.ty)?), - With::Serde(path) => { - Ok(quote! { #path::get_record_fields_in_ctxt(named_schemas, enclosing_namespace) }) - } - With::Expr(Expr::Closure(closure)) => { - if closure.inputs.is_empty() { - Ok(quote! { - ::apache_avro::serde::get_record_fields_in_ctxt( - named_schemas, - enclosing_namespace, - |_, _| (#closure)(), - ) - }) - } else { - Err(vec![syn::Error::new( - field.span(), - "Expected closure with 0 parameters", - )]) - } - } - With::Expr(Expr::Path(path)) => Ok(quote! { - ::apache_avro::serde::get_record_fields_in_ctxt(named_schemas, enclosing_namespace, #path) - }), - With::Expr(_expr) => Err(vec![syn::Error::new( - field.span(), - "Invalid expression, expected function or closure", - )]), - } -} - -/// Takes in the Tokens of a type and returns the tokens of an expression with return type `Schema` -fn type_to_schema_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_schema_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 type_to_get_record_fields_expr(ty: &Type) -> Result<TokenStream, Vec<syn::Error>> { - match ty { - Type::Array(_) | Type::Slice(_) | Type::Path(_) | Type::Reference(_) => Ok( - quote! {<#ty as :: apache_avro::AvroSchemaComponent>::get_record_fields_in_ctxt(named_schemas, enclosing_namespace)}, - ), - Type::Ptr(_) => Err(vec![syn::Error::new_spanned( - ty, - "AvroSchema: derive does not support raw pointers", - )]), - Type::Tuple(_) => Err(vec![syn::Error::new_spanned( - ty, - "AvroSchema: derive does not support tuples", - )]), - _ => Err(vec![syn::Error::new_spanned( - ty, - format!( - "AvroSchema: Unexpected type encountered! Please open an issue if this kind of type should be supported: {ty:?}" - ), - )]), - } -} - -fn type_to_field_default_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>::field_default()}) - } - 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:?}" - ), - )]), - } -} - -/// Stolen from serde -fn to_compile_errors(errors: impl AsRef<[syn::Error]>) -> proc_macro2::TokenStream { - let compile_errors = errors.as_ref().iter().map(syn::Error::to_compile_error); - quote!(#(#compile_errors)*) -} - -fn preserve_optional(op: Option<impl quote::ToTokens>) -> TokenStream { - if let Some(tt) = op { - quote! {::std::option::Option::Some(#tt.into())} - } else { - quote! {::std::option::Option::None} - } -} - -fn aliases(op: &[impl quote::ToTokens]) -> TokenStream { - let items: Vec<TokenStream> = op - .iter() - .map(|tt| quote! {#tt.try_into().expect("Alias is invalid")}) - .collect(); - if items.is_empty() { - quote! {::std::option::Option::None} - } else { - quote! {::std::option::Option::Some(vec![#(#items),*])} - } -} - -fn field_aliases(op: &[impl quote::ToTokens]) -> TokenStream { - let items: Vec<TokenStream> = op - .iter() - .map(|tt| quote! {#tt.try_into().expect("Alias is invalid")}) - .collect(); - if items.is_empty() { - quote! {::std::vec::Vec::new()} - } else { - quote! {vec![#(#items),*]} - } -} - -#[cfg(test)] -mod tests { - use super::*; - use pretty_assertions::assert_eq; - - #[test] - fn test_trait_cast() { - assert_eq!(type_to_schema_expr(&syn::parse2::<Type>(quote!{i32}).unwrap()).unwrap().to_string(), quote!{<i32 as :: apache_avro::AvroSchemaComponent>::get_schema_in_ctxt(named_schemas, enclosing_namespace)}.to_string()); - assert_eq!(type_to_schema_expr(&syn::parse2::<Type>(quote!{Vec<T>}).unwrap()).unwrap().to_string(), quote!{<Vec<T> as :: apache_avro::AvroSchemaComponent>::get_schema_in_ctxt(named_schemas, enclosing_namespace)}.to_string()); - assert_eq!(type_to_schema_expr(&syn::parse2::<Type>(quote!{AnyType}).unwrap()).unwrap().to_string(), quote!{<AnyType as :: apache_avro::AvroSchemaComponent>::get_schema_in_ctxt(named_schemas, enclosing_namespace)}.to_string()); - } -} diff --git a/avro_derive/src/structs.rs b/avro_derive/src/structs.rs new file mode 100644 index 0000000..3ab84ae --- /dev/null +++ b/avro_derive/src/structs.rs @@ -0,0 +1,169 @@ +use crate::attributes::{FieldOptions, NamedTypeOptions}; +use crate::case::RenameRule; +use crate::fields::{ + get_field_field_default_expr, get_field_get_record_fields_expr, get_field_schema_expr, +}; +use crate::utils::{aliases, field_aliases, preserve_optional}; +use proc_macro2::{Span, TokenStream}; +use quote::quote; +use syn::spanned::Spanned; +use syn::{DataStruct, Fields}; + +/// Generate a schema definition for a struct. +pub 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![]; + 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 = get_field_field_default_expr(&field, field_attrs.default)?; + 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(), + }); + }); + } + } + 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", + )]); + } + } + + let record_doc = preserve_optional(container_attrs.doc.as_ref()); + let record_aliases = aliases(&container_attrs.aliases); + let full_schema_name = &container_attrs.name; + + // When flatten is involved, there will be more but we don't know how many. This optimises for + // the most common case where there is no flatten. + let minimum_fields = record_field_exprs.len(); + + let schema_def = quote! { + { + let mut schema_fields = ::std::vec::Vec::with_capacity(#minimum_fields); + #(#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:?}"); + let name = ::apache_avro::schema::Name::new(#full_schema_name).expect(&format!("Unable to parse struct name for schema {}", #full_schema_name)[..]); + let lookup: ::std::collections::BTreeMap<String, usize> = schema_fields + .iter() + .enumerate() + .map(|(position, field)| (field.name.to_owned(), position)) + .collect(); + ::apache_avro::schema::Schema::Record(::apache_avro::schema::RecordSchema { + name, + aliases: #record_aliases, + doc: #record_doc, + fields: schema_fields, + lookup, + attributes: ::std::collections::BTreeMap::new(), + }) + } + }; + let record_fields = quote! { + let mut schema_fields = ::std::vec::Vec::with_capacity(#minimum_fields); + #(#record_field_exprs)* + ::std::option::Option::Some(schema_fields) + }; + + Ok((schema_def, record_fields)) +} + +/// Use the schema definition of the only field in the struct as the schema +pub fn get_transparent_struct_schema_def( + fields: Fields, + input_span: Span, +) -> Result<(TokenStream, TokenStream), Vec<syn::Error>> { + match fields { + Fields::Named(fields_named) => { + let mut found = None; + for field in fields_named.named { + 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::Unnamed(_) => Err(vec![syn::Error::new( + input_span, + "AvroSchema: derive does not work for tuple structs", + )]), + Fields::Unit => Err(vec![syn::Error::new( + input_span, + "AvroSchema: derive does not work for unit structs", + )]), + } +} diff --git a/avro_derive/src/utils.rs b/avro_derive/src/utils.rs new file mode 100644 index 0000000..fafc3b9 --- /dev/null +++ b/avro_derive/src/utils.rs @@ -0,0 +1,50 @@ +use proc_macro2::TokenStream; +use quote::quote; + +/// Convert a list of errors into actual compiler errors. +/// +/// Copied from `serde`, originally written by David Tolnay. +pub fn to_compile_errors(errors: impl AsRef<[syn::Error]>) -> TokenStream { + let compile_errors = errors.as_ref().iter().map(syn::Error::to_compile_error); + quote!(#(#compile_errors)*) +} + +pub fn preserve_optional(op: Option<impl quote::ToTokens>) -> TokenStream { + if let Some(tt) = op { + quote! {::std::option::Option::Some(#tt.into())} + } else { + quote! {::std::option::Option::None} + } +} + +/// Convert a list of strings to an expression that resolves to a `Option<Vec<Alias>>`. +pub fn aliases(op: &[impl AsRef<str>]) -> TokenStream { + let items: Vec<TokenStream> = op + .iter() + .map(|alias| { + let alias = alias.as_ref(); + quote! { + ::apache_avro::schema::Alias::new(#alias).expect("Alias is invalid") + } + }) + .collect(); + if items.is_empty() { + quote! {::std::option::Option::None} + } else { + quote! {::std::option::Option::Some(vec![#(#items),*])} + } +} + +/// Convert a list of strings to an expression that resolves to a `Vec<String>`. +pub fn field_aliases(op: &[impl AsRef<str>]) -> TokenStream { + let items: Vec<TokenStream> = op + .iter() + .map(|string| { + let string = string.as_ref(); + quote! { + String::from(#string) + } + }) + .collect(); + quote! {vec![#(#items),*]} +} 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 0d35f4b..e73168a 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 @@ -36,10 +36,7 @@ impl ::apache_avro::AvroSchemaComponent for A { aliases: ::alloc::boxed::box_assume_init_into_vec_unsafe( ::alloc::intrinsics::write_box_via_move( ::alloc::boxed::Box::new_uninit(), - [ - "a1".try_into().expect("Alias is invalid"), - "a2".try_into().expect("Alias is invalid"), - ], + [String::from("a1"), String::from("a2")], ), ), schema: <i32 as ::apache_avro::AvroSchemaComponent>::get_schema_in_ctxt( @@ -111,10 +108,7 @@ impl ::apache_avro::AvroSchemaComponent for A { aliases: ::alloc::boxed::box_assume_init_into_vec_unsafe( ::alloc::intrinsics::write_box_via_move( ::alloc::boxed::Box::new_uninit(), - [ - "a1".try_into().expect("Alias is invalid"), - "a2".try_into().expect("Alias is invalid"), - ], + [String::from("a1"), String::from("a2")], ), ), schema: <i32 as ::apache_avro::AvroSchemaComponent>::get_schema_in_ctxt( diff --git a/avro_derive/tests/expanded/avro_rs_207_rename_all_attribute.expanded.rs b/avro_derive/tests/expanded/avro_rs_207_rename_all_attribute.expanded.rs index d80a114..f13c46c 100644 --- a/avro_derive/tests/expanded/avro_rs_207_rename_all_attribute.expanded.rs +++ b/avro_derive/tests/expanded/avro_rs_207_rename_all_attribute.expanded.rs @@ -29,7 +29,7 @@ impl ::apache_avro::AvroSchemaComponent for A { name: "ITEM".to_string(), doc: ::std::option::Option::None, default: <i32 as ::apache_avro::AvroSchemaComponent>::field_default(), - aliases: ::std::vec::Vec::new(), + aliases: ::alloc::vec::Vec::new(), schema: <i32 as ::apache_avro::AvroSchemaComponent>::get_schema_in_ctxt( named_schemas, enclosing_namespace, @@ -41,7 +41,7 @@ impl ::apache_avro::AvroSchemaComponent for A { name: "DOUBLE_ITEM".to_string(), doc: ::std::option::Option::None, default: <i32 as ::apache_avro::AvroSchemaComponent>::field_default(), - aliases: ::std::vec::Vec::new(), + aliases: ::alloc::vec::Vec::new(), schema: <i32 as ::apache_avro::AvroSchemaComponent>::get_schema_in_ctxt( named_schemas, enclosing_namespace, @@ -105,7 +105,7 @@ impl ::apache_avro::AvroSchemaComponent for A { name: "ITEM".to_string(), doc: ::std::option::Option::None, default: <i32 as ::apache_avro::AvroSchemaComponent>::field_default(), - aliases: ::std::vec::Vec::new(), + aliases: ::alloc::vec::Vec::new(), schema: <i32 as ::apache_avro::AvroSchemaComponent>::get_schema_in_ctxt( named_schemas, enclosing_namespace, @@ -117,7 +117,7 @@ impl ::apache_avro::AvroSchemaComponent for A { name: "DOUBLE_ITEM".to_string(), doc: ::std::option::Option::None, default: <i32 as ::apache_avro::AvroSchemaComponent>::field_default(), - aliases: ::std::vec::Vec::new(), + aliases: ::alloc::vec::Vec::new(), schema: <i32 as ::apache_avro::AvroSchemaComponent>::get_schema_in_ctxt( named_schemas, enclosing_namespace, diff --git a/avro_derive/tests/expanded/avro_rs_207_rename_attr_over_rename_all_attribute.expanded.rs b/avro_derive/tests/expanded/avro_rs_207_rename_attr_over_rename_all_attribute.expanded.rs index 0689cb1..c26134c 100644 --- a/avro_derive/tests/expanded/avro_rs_207_rename_attr_over_rename_all_attribute.expanded.rs +++ b/avro_derive/tests/expanded/avro_rs_207_rename_attr_over_rename_all_attribute.expanded.rs @@ -30,7 +30,7 @@ impl ::apache_avro::AvroSchemaComponent for A { name: "ITEM".to_string(), doc: ::std::option::Option::None, default: <i32 as ::apache_avro::AvroSchemaComponent>::field_default(), - aliases: ::std::vec::Vec::new(), + aliases: ::alloc::vec::Vec::new(), schema: <i32 as ::apache_avro::AvroSchemaComponent>::get_schema_in_ctxt( named_schemas, enclosing_namespace, @@ -42,7 +42,7 @@ impl ::apache_avro::AvroSchemaComponent for A { name: "DoubleItem".to_string(), doc: ::std::option::Option::None, default: <i32 as ::apache_avro::AvroSchemaComponent>::field_default(), - aliases: ::std::vec::Vec::new(), + aliases: ::alloc::vec::Vec::new(), schema: <i32 as ::apache_avro::AvroSchemaComponent>::get_schema_in_ctxt( named_schemas, enclosing_namespace, @@ -106,7 +106,7 @@ impl ::apache_avro::AvroSchemaComponent for A { name: "ITEM".to_string(), doc: ::std::option::Option::None, default: <i32 as ::apache_avro::AvroSchemaComponent>::field_default(), - aliases: ::std::vec::Vec::new(), + aliases: ::alloc::vec::Vec::new(), schema: <i32 as ::apache_avro::AvroSchemaComponent>::get_schema_in_ctxt( named_schemas, enclosing_namespace, @@ -118,7 +118,7 @@ impl ::apache_avro::AvroSchemaComponent for A { name: "DoubleItem".to_string(), doc: ::std::option::Option::None, default: <i32 as ::apache_avro::AvroSchemaComponent>::field_default(), - aliases: ::std::vec::Vec::new(), + aliases: ::alloc::vec::Vec::new(), schema: <i32 as ::apache_avro::AvroSchemaComponent>::get_schema_in_ctxt( named_schemas, enclosing_namespace, diff --git a/avro_derive/tests/expanded/avro_rs_501_basic.expanded.rs b/avro_derive/tests/expanded/avro_rs_501_basic.expanded.rs index 34576a5..3562a9b 100644 --- a/avro_derive/tests/expanded/avro_rs_501_basic.expanded.rs +++ b/avro_derive/tests/expanded/avro_rs_501_basic.expanded.rs @@ -28,7 +28,7 @@ impl ::apache_avro::AvroSchemaComponent for A { name: "a".to_string(), doc: ::std::option::Option::None, default: <i32 as ::apache_avro::AvroSchemaComponent>::field_default(), - aliases: ::std::vec::Vec::new(), + aliases: ::alloc::vec::Vec::new(), schema: <i32 as ::apache_avro::AvroSchemaComponent>::get_schema_in_ctxt( named_schemas, enclosing_namespace, @@ -40,7 +40,7 @@ impl ::apache_avro::AvroSchemaComponent for A { name: "b".to_string(), doc: ::std::option::Option::None, default: <String as ::apache_avro::AvroSchemaComponent>::field_default(), - aliases: ::std::vec::Vec::new(), + aliases: ::alloc::vec::Vec::new(), schema: <String as ::apache_avro::AvroSchemaComponent>::get_schema_in_ctxt( named_schemas, enclosing_namespace, @@ -104,7 +104,7 @@ impl ::apache_avro::AvroSchemaComponent for A { name: "a".to_string(), doc: ::std::option::Option::None, default: <i32 as ::apache_avro::AvroSchemaComponent>::field_default(), - aliases: ::std::vec::Vec::new(), + aliases: ::alloc::vec::Vec::new(), schema: <i32 as ::apache_avro::AvroSchemaComponent>::get_schema_in_ctxt( named_schemas, enclosing_namespace, @@ -116,7 +116,7 @@ impl ::apache_avro::AvroSchemaComponent for A { name: "b".to_string(), doc: ::std::option::Option::None, default: <String as ::apache_avro::AvroSchemaComponent>::field_default(), - aliases: ::std::vec::Vec::new(), + aliases: ::alloc::vec::Vec::new(), schema: <String as ::apache_avro::AvroSchemaComponent>::get_schema_in_ctxt( named_schemas, enclosing_namespace, diff --git a/avro_derive/tests/expanded/avro_rs_501_namespace.expanded.rs b/avro_derive/tests/expanded/avro_rs_501_namespace.expanded.rs index ce84a5d..947173b 100644 --- a/avro_derive/tests/expanded/avro_rs_501_namespace.expanded.rs +++ b/avro_derive/tests/expanded/avro_rs_501_namespace.expanded.rs @@ -29,7 +29,7 @@ impl ::apache_avro::AvroSchemaComponent for A { name: "a".to_string(), doc: ::std::option::Option::None, default: <i32 as ::apache_avro::AvroSchemaComponent>::field_default(), - aliases: ::std::vec::Vec::new(), + aliases: ::alloc::vec::Vec::new(), schema: <i32 as ::apache_avro::AvroSchemaComponent>::get_schema_in_ctxt( named_schemas, enclosing_namespace, @@ -41,7 +41,7 @@ impl ::apache_avro::AvroSchemaComponent for A { name: "b".to_string(), doc: ::std::option::Option::None, default: <String as ::apache_avro::AvroSchemaComponent>::field_default(), - aliases: ::std::vec::Vec::new(), + aliases: ::alloc::vec::Vec::new(), schema: <String as ::apache_avro::AvroSchemaComponent>::get_schema_in_ctxt( named_schemas, enclosing_namespace, @@ -106,7 +106,7 @@ impl ::apache_avro::AvroSchemaComponent for A { name: "a".to_string(), doc: ::std::option::Option::None, default: <i32 as ::apache_avro::AvroSchemaComponent>::field_default(), - aliases: ::std::vec::Vec::new(), + aliases: ::alloc::vec::Vec::new(), schema: <i32 as ::apache_avro::AvroSchemaComponent>::get_schema_in_ctxt( named_schemas, enclosing_namespace, @@ -118,7 +118,7 @@ impl ::apache_avro::AvroSchemaComponent for A { name: "b".to_string(), doc: ::std::option::Option::None, default: <String as ::apache_avro::AvroSchemaComponent>::field_default(), - aliases: ::std::vec::Vec::new(), + aliases: ::alloc::vec::Vec::new(), schema: <String as ::apache_avro::AvroSchemaComponent>::get_schema_in_ctxt( named_schemas, enclosing_namespace, diff --git a/avro_derive/tests/expanded/avro_rs_501_reference.expanded.rs b/avro_derive/tests/expanded/avro_rs_501_reference.expanded.rs index 9916d4f..a7e9293 100644 --- a/avro_derive/tests/expanded/avro_rs_501_reference.expanded.rs +++ b/avro_derive/tests/expanded/avro_rs_501_reference.expanded.rs @@ -30,7 +30,7 @@ impl<'a> ::apache_avro::AvroSchemaComponent for A<'a> { default: <&'a Vec< i32, > as ::apache_avro::AvroSchemaComponent>::field_default(), - aliases: ::std::vec::Vec::new(), + aliases: ::alloc::vec::Vec::new(), schema: <&'a Vec< i32, > as ::apache_avro::AvroSchemaComponent>::get_schema_in_ctxt( @@ -44,7 +44,7 @@ impl<'a> ::apache_avro::AvroSchemaComponent for A<'a> { name: "b".to_string(), doc: ::std::option::Option::None, default: <&'static str as ::apache_avro::AvroSchemaComponent>::field_default(), - aliases: ::std::vec::Vec::new(), + aliases: ::alloc::vec::Vec::new(), schema: <&'static str as ::apache_avro::AvroSchemaComponent>::get_schema_in_ctxt( named_schemas, enclosing_namespace, @@ -110,7 +110,7 @@ impl<'a> ::apache_avro::AvroSchemaComponent for A<'a> { default: <&'a Vec< i32, > as ::apache_avro::AvroSchemaComponent>::field_default(), - aliases: ::std::vec::Vec::new(), + aliases: ::alloc::vec::Vec::new(), schema: <&'a Vec< i32, > as ::apache_avro::AvroSchemaComponent>::get_schema_in_ctxt( @@ -124,7 +124,7 @@ impl<'a> ::apache_avro::AvroSchemaComponent for A<'a> { name: "b".to_string(), doc: ::std::option::Option::None, default: <&'static str as ::apache_avro::AvroSchemaComponent>::field_default(), - aliases: ::std::vec::Vec::new(), + aliases: ::alloc::vec::Vec::new(), schema: <&'static str as ::apache_avro::AvroSchemaComponent>::get_schema_in_ctxt( named_schemas, enclosing_namespace, diff --git a/avro_derive/tests/expanded/avro_rs_501_struct_with_optional.expanded.rs b/avro_derive/tests/expanded/avro_rs_501_struct_with_optional.expanded.rs index 076143e..e199f7c 100644 --- a/avro_derive/tests/expanded/avro_rs_501_struct_with_optional.expanded.rs +++ b/avro_derive/tests/expanded/avro_rs_501_struct_with_optional.expanded.rs @@ -29,7 +29,7 @@ impl ::apache_avro::AvroSchemaComponent for TestOptional { default: <Option< i32, > as ::apache_avro::AvroSchemaComponent>::field_default(), - aliases: ::std::vec::Vec::new(), + aliases: ::alloc::vec::Vec::new(), schema: <Option< i32, > as ::apache_avro::AvroSchemaComponent>::get_schema_in_ctxt( @@ -98,7 +98,7 @@ impl ::apache_avro::AvroSchemaComponent for TestOptional { default: <Option< i32, > as ::apache_avro::AvroSchemaComponent>::field_default(), - aliases: ::std::vec::Vec::new(), + aliases: ::alloc::vec::Vec::new(), schema: <Option< i32, > as ::apache_avro::AvroSchemaComponent>::get_schema_in_ctxt(
