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 dc9af96ddc80ef8c078439871a5fc2b177f2aa94 Author: Kriskras99 <[email protected]> AuthorDate: Wed Mar 11 10:06:46 2026 +0100 make type_to_* private --- avro_derive/src/attributes/mod.rs | 27 +- avro_derive/src/enums/bare_union.rs | 7 +- avro_derive/src/enums/record_internally_tagged.rs | 7 +- avro_derive/src/enums/record_tag_content.rs | 9 +- avro_derive/src/fields.rs | 390 ++++++++++++++++++++++ avro_derive/src/lib.rs | 177 +--------- avro_derive/src/tuple.rs | 148 ++------ 7 files changed, 437 insertions(+), 328 deletions(-) diff --git a/avro_derive/src/attributes/mod.rs b/avro_derive/src/attributes/mod.rs index 13fadfe..ab66911 100644 --- a/avro_derive/src/attributes/mod.rs +++ b/avro_derive/src/attributes/mod.rs @@ -15,7 +15,7 @@ // specific language governing permissions and limitations // under the License. -use crate::{case::RenameRule, type_to_field_default_expr}; +use crate::case::RenameRule; use darling::{FromAttributes, FromMeta}; use proc_macro2::{Span, TokenStream}; use quote::quote; @@ -389,31 +389,6 @@ 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! { ::std::option::Option::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! { - ::std::option::Option::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 c465fb2..7752b81 100644 --- a/avro_derive/src/enums/bare_union.rs +++ b/avro_derive/src/enums/bare_union.rs @@ -1,6 +1,6 @@ -use crate::attributes::{NamedTypeOptions, VariantOptions}; +use crate::attributes::{FieldOptions, NamedTypeOptions, VariantOptions}; use crate::tuple::tuple_struct_variant_to_record_schema; -use crate::{named_to_record_fields, type_to_schema_expr}; +use crate::{fields, named_to_record_fields}; use proc_macro2::{Span, TokenStream}; use quote::quote; use syn::spanned::Spanned; @@ -47,7 +47,8 @@ pub fn get_data_enum_schema_def( )]); } else if unnamed.unnamed.len() == 1 { let only_one = unnamed.unnamed.iter().next().expect("There is one"); - let schema_expr = type_to_schema_expr(&only_one.ty)?; + let field_attrs = FieldOptions::new(&only_one.attrs, only_one.span())?; + let schema_expr = fields::to_schema(&only_one, field_attrs.with)?; variant_expr.push(schema_expr); } else if unnamed.unnamed.len() > 1 { let schema_expr = tuple_struct_variant_to_record_schema(unnamed, &name, &[])?; diff --git a/avro_derive/src/enums/record_internally_tagged.rs b/avro_derive/src/enums/record_internally_tagged.rs index 7d09b0d..e9173cd 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, named_to_record_fields, preserve_optional, type_to_schema_expr}; +use crate::attributes::{FieldOptions, NamedTypeOptions, VariantOptions}; +use crate::{aliases, fields, named_to_record_fields, preserve_optional}; use proc_macro2::TokenStream; use quote::quote; use syn::spanned::Spanned; @@ -36,7 +36,8 @@ pub fn get_data_enum_schema_def( Fields::Unnamed(unnamed) => { if unnamed.unnamed.len() == 1 { let only_one = unnamed.unnamed.iter().next().expect("There is one"); - let schema_expr = type_to_schema_expr(&only_one.ty)?; + let field_attrs = FieldOptions::new(&only_one.attrs, only_one.span())?; + let schema_expr = fields::to_schema(&only_one, field_attrs.with)?; field_additions.push(quote! { fields.push(#schema_expr); }); diff --git a/avro_derive/src/enums/record_tag_content.rs b/avro_derive/src/enums/record_tag_content.rs index e39177e..a1aebf5 100644 --- a/avro_derive/src/enums/record_tag_content.rs +++ b/avro_derive/src/enums/record_tag_content.rs @@ -1,6 +1,6 @@ -use crate::attributes::{NamedTypeOptions, VariantOptions}; +use crate::attributes::{FieldOptions, NamedTypeOptions, VariantOptions}; use crate::tuple::tuple_struct_variant_to_record_schema; -use crate::{aliases, named_to_record_fields, preserve_optional, type_to_schema_expr}; +use crate::{aliases, fields, named_to_record_fields, preserve_optional}; use proc_macro2::TokenStream; use quote::quote; use syn::spanned::Spanned; @@ -50,8 +50,9 @@ pub fn get_data_enum_schema_def( schema_definitions.push(schema_expr); } else if unnamed.unnamed.len() == 1 { let only_one = unnamed.unnamed.iter().next().expect("There is one"); - let field_schema_expr = type_to_schema_expr(&only_one.ty)?; - schema_definitions.push(field_schema_expr); + let field_attrs = FieldOptions::new(&only_one.attrs, only_one.span())?; + let schema_expr = fields::to_schema(&only_one, field_attrs.with)?; + schema_definitions.push(schema_expr); } else if unnamed.unnamed.len() > 1 { let schema_expr = tuple_struct_variant_to_record_schema(unnamed, &name, &[])?; diff --git a/avro_derive/src/fields.rs b/avro_derive/src/fields.rs new file mode 100644 index 0000000..af5b3aa --- /dev/null +++ b/avro_derive/src/fields.rs @@ -0,0 +1,390 @@ +use crate::attributes::{FieldDefault, With}; +use proc_macro2::TokenStream; +use quote::{ToTokens, quote}; +use syn::spanned::Spanned; +use syn::{Expr, ExprLit, Field, Lit, Type, TypeArray, TypeTuple}; + +pub fn to_schema(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", + )]), + } +} + +/// Call `get_record_fields_in_ctxt` for this field. +/// +/// # `TokenStream` +/// ## Context +/// The token stream expects the following variables to be defined: +/// - `named_schemas`: `&mut HashSet<Name>` +/// - `enclosing_namespace`: `Option<&str>` +/// ## Returns +/// An `Expr` that resolves to an instance of `Option<Vec<RecordField>>`. +pub fn to_record_fields(field: &Field, with: With) -> Result<TokenStream, Vec<syn::Error>> { + match with { + With::Trait => Ok(type_to_record_fields(&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 to_default(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(&field.ty), + FieldDefault::Value(default_value) => { + let _: serde_json::Value = serde_json::from_str(&default_value[..]).map_err(|e| { + vec![syn::Error::new( + field.span(), + format!("Invalid avro default json: \n{e}"), + )] + })?; + Ok(quote! { + ::std::option::Option::Some(::serde_json::from_str(#default_value).expect("Unreachable! Checked at compile time")) + }) + } + } +} + +/// Takes in the Tokens of a type and returns the tokens of an expression with return type `Schema` +/// +/// # `TokenStream` +/// ## Context +/// The token stream expects the following variables to be defined: +/// - `named_schemas`: `&mut HashSet<Name>` +/// - `enclosing_namespace`: `Option<&str>` +/// ## Returns +/// An `Expr` that resolves to an instance of `Schema`. +fn type_to_schema_expr(ty: &Type) -> Result<TokenStream, Vec<syn::Error>> { + match ty { + Type::Slice(_) | Type::Path(_) | Type::Reference(_) => Ok( + quote! {<#ty as :: apache_avro::AvroSchemaComponent>::get_schema_in_ctxt(named_schemas, enclosing_namespace)}, + ), + Type::Tuple(tuple) => tuple_to_schema(tuple), + Type::Array(array) => array_to_schema(array), + Type::Ptr(_) => Err(vec![syn::Error::new_spanned( + ty, + "AvroSchema: derive does not support raw pointers", + )]), + _ => 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:?}" + ), + )]), + } +} + +/// Create a schema definition for a tuple. +/// +/// # Mapping +/// - `0-tuple` => `Schema::Null`, +/// - `1-tuple` => Schema of the only element, +/// - `n-tuple` => `Schema::Record`. +/// +/// # `TokenStream` +/// ## Context +/// The token stream expects the following variables to be defined: +/// - `named_schemas`: `&mut HashSet<Name>` +/// - `enclosing_namespace`: `Option<&str>` +/// ## Returns +/// An `Expr` that resolves to an instance of `Schema`. +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`. +/// +/// # `TokenStream` +/// ## Context +/// The token stream expects the following variables to be defined: +/// - `named_schemas`: `&mut HashSet<Name>` +/// - `enclosing_namespace`: `Option<&str>` +/// ## Returns +/// An `Expr` that resolves to an instance of `Schema`. +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() + ) + }) + } +} + +fn type_to_record_fields(ty: &Type) -> Result<TokenStream, Vec<syn::Error>> { + match ty { + Type::Slice(_) | Type::Path(_) | Type::Reference(_) => Ok( + quote! {<#ty as :: apache_avro::AvroSchemaComponent>::get_record_fields_in_ctxt(named_schemas, enclosing_namespace)}, + ), + Type::Array(array) => array_to_record_fields(array), + Type::Tuple(tuple) => tuple_to_record_fields(tuple), + Type::Ptr(_) => Err(vec![syn::Error::new_spanned( + ty, + "AvroSchema: derive does not support raw pointers", + )]), + _ => 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:?}" + ), + )]), + } +} + +/// Create a schema definition for a tuple. +/// +/// # Mapping +/// - `0-tuple` => `Schema::Null`, +/// - `1-tuple` => Schema of the only element, +/// - `n-tuple` => `Schema::Record`. +/// +/// # `TokenStream` +/// ## Context +/// The token stream expects the following variables to be defined: +/// - `named_schemas`: `&mut HashSet<Name>` +/// - `enclosing_namespace`: `Option<&str>` +/// ## Returns +/// An `Expr` that resolves to an instance of `Schema`. +fn tuple_to_record_fields(tuple: &TypeTuple) -> Result<TokenStream, Vec<syn::Error>> { + if tuple.elems.is_empty() { + Ok(quote! {::std::option::Option::None}) + } else if tuple.elems.len() == 1 { + type_to_record_fields(&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() + }); + } + + Ok(quote! { + ::std::option::Option::Some(vec![#(#fields, )*]) + }) + } +} +/// Create a schema definition for an array. +/// +/// # Mapping +/// - `[T; 0]` => `Schema::Null`, +/// - `[T; 1]` => Schema of `T`, +/// - `[T; N]` => `Schema::Record`. +/// +/// # `TokenStream` +/// ## Context +/// The token stream expects the following variables to be defined: +/// - `named_schemas`: `&mut HashSet<Name>` +/// - `enclosing_namespace`: `Option<&str>` +/// ## Returns +/// An `Expr` that resolves to an instance of `Schema`. +fn array_to_record_fields(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! {::std::option::Option::None}) + } else if len == 1 { + type_to_record_fields(&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() + } + }); + + Ok(quote! { + ::std::option::Option::Some(vec![#(#fields, )*]) + }) + } +} + +fn type_to_field_default(ty: &Type) -> Result<TokenStream, Vec<syn::Error>> { + match ty { + 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(_) | Type::Array(_) => Ok(quote! { ::std::option::Option::None }), + _ => 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 84daff0..ab3e8c2 100644 --- a/avro_derive/src/lib.rs +++ b/avro_derive/src/lib.rs @@ -32,18 +32,18 @@ mod attributes; mod case; mod enums; +mod fields; mod tuple; use proc_macro2::{Span, TokenStream}; use quote::quote; use syn::{ - DataStruct, DeriveInput, Expr, Field, Fields, FieldsNamed, Generics, Ident, Type, - parse_macro_input, spanned::Spanned, + DataStruct, DeriveInput, Fields, FieldsNamed, Generics, Ident, Type, parse_macro_input, + spanned::Spanned, }; -use crate::tuple::tuple_to_schema; use crate::{ - attributes::{FieldOptions, NamedTypeOptions, With}, + attributes::{FieldOptions, NamedTypeOptions}, case::RenameRule, tuple::unnamed_to_record_fields, }; @@ -235,8 +235,8 @@ fn get_transparent_struct_schema_def( if let Some((field, attrs)) = found { Ok(( - get_field_schema_expr(&field, attrs.with.clone())?, - get_field_get_record_fields_expr(&field, attrs.with)?, + fields::to_schema(&field, attrs.with.clone())?, + fields::to_record_fields(&field, attrs.with)?, )) } else { Err(vec![syn::Error::new( @@ -262,8 +262,8 @@ fn get_transparent_struct_schema_def( if let Some((field, attrs)) = found { Ok(( - get_field_schema_expr(&field, attrs.with.clone())?, - get_field_get_record_fields_expr(&field, attrs.with)?, + fields::to_schema(&field, attrs.with.clone())?, + fields::to_record_fields(&field, attrs.with)?, )) } else { Err(vec![syn::Error::new( @@ -279,146 +279,6 @@ fn get_transparent_struct_schema_def( } } -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", - )]), - } -} - -/// Call `get_record_fields_in_ctxt` for this field. -/// -/// # `TokenStream` -/// ## Context -/// The token stream expects the following variables to be defined: -/// - `named_schemas`: `&mut HashSet<Name>` -/// - `enclosing_namespace`: `Option<&str>` -/// ## Returns -/// A call to a `get_record_fields_in_ctxt(named_schemas, enclosing_namespace) -> Option<Vec<RecordField>>` -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` -/// -/// # `TokenStream` -/// ## Context -/// The token stream expects the following variables to be defined: -/// - `named_schemas`: `&mut HashSet<Name>` -/// - `enclosing_namespace`: `Option<&str>` -/// ## Returns -/// A call to a `get_schema_in_ctxt(named_schemas, enclosing_namespace) -> 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(tuple) => tuple_to_schema(tuple), - _ => 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:?}" - ), - )]), - } -} - /// Create a vector of `RecordField`s. fn named_to_record_fields( named: FieldsNamed, @@ -432,7 +292,7 @@ fn named_to_record_fields( } 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)?; + let get_record_fields = fields::to_record_fields(&field, field_attrs.with)?; fields.push(quote! { if let Some(flattened_fields) = #get_record_fields { fields.extend(flattened_fields); @@ -461,12 +321,10 @@ fn named_to_record_fields( } _ => {} } - let default_value = field_attrs - .default - .into_tokenstream(field.ident.span(), &field.ty)?; + let default_value = fields::to_default(&field, field_attrs.default)?; let aliases = field_aliases(&field_attrs.alias); let doc = doc_into_tokenstream(field_attrs.doc); - let field_schema_expr = get_field_schema_expr(&field, field_attrs.with)?; + let field_schema_expr = fields::to_schema(&field, field_attrs.with)?; fields.push(quote! { fields.push(::apache_avro::schema::RecordField::builder() .name(#name.to_string()) @@ -534,16 +392,3 @@ fn field_aliases(op: &[impl quote::ToTokens]) -> TokenStream { 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/tuple.rs b/avro_derive/src/tuple.rs index adf56c4..8253332 100644 --- a/avro_derive/src/tuple.rs +++ b/avro_derive/src/tuple.rs @@ -1,15 +1,23 @@ use proc_macro2::TokenStream; -use quote::{ToTokens, quote}; -use syn::{Expr, ExprLit, FieldsUnnamed, Lit, TypeArray, TypeTuple, spanned::Spanned}; +use quote::quote; +use syn::{FieldsUnnamed, spanned::Spanned}; -use crate::{FieldOptions, doc_into_tokenstream, field_aliases, type_to_schema_expr}; +use crate::{FieldOptions, doc_into_tokenstream, field_aliases, fields}; /// 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`. +/// The schema will have the attribute `org.apache.avro.rust.tuple` any other specified in `extra_attributes`. /// All attributes will have a value of `true`. +/// +/// # `TokenStream` +/// ## Context +/// The token stream expects the following variables to be defined: +/// - `named_schemas`: `&mut HashSet<Name>` +/// - `enclosing_namespace`: `Option<&str>` +/// ## Returns +/// An `Expr` that resolves to an instance of `Schema`. pub fn tuple_struct_variant_to_record_schema( unnamed: FieldsUnnamed, name: &str, @@ -32,125 +40,15 @@ pub fn tuple_struct_variant_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}`. +/// +/// # `TokenStream` +/// ## Context +/// The token stream expects the following variables to be defined: +/// - `named_schemas`: `&mut HashSet<Name>` +/// - `enclosing_namespace`: `Option<&str>` +/// ## Returns +/// An `Expr` that resolves to an instance of `Vec<RecordField>`. pub fn unnamed_to_record_fields(unnamed: FieldsUnnamed) -> Result<TokenStream, Vec<syn::Error>> { let mut fields = Vec::with_capacity(unnamed.unnamed.len()); for (index, field) in unnamed.unnamed.into_iter().enumerate() { @@ -163,15 +61,13 @@ pub fn unnamed_to_record_fields(unnamed: FieldsUnnamed) -> Result<TokenStream, V "AvroSchema: `#[serde(flatten)]` is not supported on tuple fields", )]); } - let default_value = field_attrs - .default - .into_tokenstream(field.ident.span(), &field.ty)?; + let default_value = fields::to_default(&field, field_attrs.default)?; 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)?; + let field_schema_expr = crate::fields::to_schema(&field, field_attrs.with)?; fields.push(quote! { ::apache_avro::schema::RecordField::builder() .name(#name.to_string())
