From 13b4e534b7153ed40dd960af53b5f70afceb6d82 Mon Sep 17 00:00:00 2001 From: Kriskras99 Date: Wed, 11 Feb 2026 15:18:55 +0100 Subject: [PATCH 1/4] feat: Allow types to provide default values `AvroSchemaComponent` is extended with a function `field_default` which will be called when deriving a record to set the default value for a field. The default implementation is to return `None`, which means no default. On the derive side, it is now possible to specify a default for a type using `#[avro(default = "..")]`. It is also possible to disable setting a default for a field with `#[avro(default = false)]`. This enables users to use `#[serde(skip_serializing{_if})]` on most fields without having to provide a default value. --- avro/src/serde/derive.rs | 103 ++++++++- avro/src/types.rs | 7 + avro_derive/src/attributes/avro.rs | 6 +- avro_derive/src/attributes/mod.rs | 54 ++++- avro_derive/src/lib.rs | 50 ++++- avro_derive/tests/derive.rs | 204 ++++++++++++++++++ .../tests/ui/avro_rs_226_skip_serializing.rs | 1 + .../ui/avro_rs_226_skip_serializing.stderr | 5 +- .../ui/avro_rs_226_skip_serializing_if.rs | 1 + .../ui/avro_rs_226_skip_serializing_if.stderr | 5 +- 10 files changed, 412 insertions(+), 24 deletions(-) diff --git a/avro/src/serde/derive.rs b/avro/src/serde/derive.rs index 066fe007..5d2fbca2 100644 --- a/avro/src/serde/derive.rs +++ b/avro/src/serde/derive.rs @@ -22,6 +22,10 @@ use crate::schema::{ use std::borrow::Cow; use std::collections::{HashMap, HashSet}; +const FIXED_8_DEFAULT: &str = "\0\0\0\0\0\0\0\0"; +const FIXED_12_DEFAULT: &str = "\0\0\0\0\0\0\0\0\0\0\0\0"; +const FIXED_16_DEFAULT: &str = "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0"; + /// Trait for types that serve as an Avro data model. /// /// **Do not implement directly!** Either derive it or implement [`AvroSchemaComponent`] to get this trait @@ -81,6 +85,10 @@ use std::collections::{HashMap, HashSet}; /// /// Set the `doc` attribute of the schema. Defaults to the documentation of the type. /// +/// - `#[avro(default = r#"{"field": 42, "other": "Spam"}"#)]` +/// +/// Provide the default value for this type when it is used in a field. +/// /// - `#[avro(alias = "name")]` /// /// Set the `alias` attribute of the schema. Can be specified multiple times. @@ -113,11 +121,14 @@ use std::collections::{HashMap, HashSet}; /// /// Set the `doc` attribute of the field. Defaults to the documentation of the field. /// -/// - `#[avro(default = "null")]` +/// - `#[avro(default = "null")]` or `#[avro(default = false)]` /// -/// Set the `default` attribute of the field. +/// Control the `default` attribute of the field. When not used, it will use [`AvroSchemaComponent::field_default`] +/// to get the default value for a type. This default value can be overriden by providing a JSON string. +/// To remove the `default` attribute for a field, set `default` to `false`. /// -/// _Note:_ This is a JSON value not a Rust value, as this is put in the schema itself. +/// _Note:_ This is a JSON value not a Rust value, as this is put in the schema itself. To encode a JSON string +/// you need to use double quotes: `#[avro(default = r#""Some string value""#)]`. /// /// - `#[serde(alias = "name")]` /// @@ -220,6 +231,11 @@ pub trait AvroSchema { /// fn get_record_fields_in_ctxt(_: usize, _: &mut HashSet, _: &Namespace) -> Option> { /// None // A Schema::Int is not a Schema::Record so there are no fields to return /// } +/// +/// fn field_default() -> Option { +/// // Zero as default value. Can also be None if you don't want to provide a default value +/// Some(0u8.into()) +/// } ///} /// ``` /// @@ -242,6 +258,10 @@ pub trait AvroSchema { /// fn get_record_fields_in_ctxt(first_field_position: usize, named_schemas: &mut HashSet, enclosing_namespace: &Namespace) -> Option> { /// T::get_record_fields_in_ctxt(first_field_position, named_schemas, enclosing_namespace) /// } +/// +/// fn field_default() -> Option { +/// T::field_default() +/// } ///} /// ``` /// @@ -256,6 +276,7 @@ pub trait AvroSchema { /// - Implement `get_record_fields_in_ctxt` as the default implementation has to be implemented /// with backtracking and a lot of cloning. /// - Even if your schema is not a record, still implement the function and just return `None` +/// - Implement `field_default()` if you want to use `#[serde(skip_serializing{,_if})]`. /// /// ``` /// # use apache_avro::{Schema, serde::{AvroSchemaComponent}, schema::{Name, Namespace, RecordField, RecordSchema}}; @@ -305,6 +326,11 @@ pub trait AvroSchema { /// .build(), /// ]) /// } +/// +/// fn field_default() -> Option { +/// // This type does not provide a default value +/// None +/// } ///} /// ``` pub trait AvroSchemaComponent { @@ -332,6 +358,16 @@ pub trait AvroSchemaComponent { Self::get_schema_in_ctxt, ) } + + /// The default value of this type when used for a record field. + /// + /// `None` means no default value, which is also the default implementation. + /// + /// Implementations of this trait provided by this crate use the [`Default::default`] value of + /// the type. + fn field_default() -> Option { + None + } } /// Get the record fields from `schema_fn` without polluting `named_schemas` or causing duplicate names @@ -479,6 +515,10 @@ where macro_rules! impl_schema ( ($type:ty, $variant_constructor:expr) => ( + impl_schema!($type, $variant_constructor, <$type as Default>::default()); + ); + + ($type:ty, $variant_constructor:expr, $default_constructor:expr) => ( impl AvroSchemaComponent for $type { fn get_schema_in_ctxt(_: &mut HashSet, _: &Namespace) -> Schema { $variant_constructor @@ -487,6 +527,10 @@ macro_rules! impl_schema ( fn get_record_fields_in_ctxt(_: usize, _: &mut HashSet, _: &Namespace) -> Option> { None } + + fn field_default() -> Option { + Some(serde_json::Value::from($default_constructor)) + } } ); ); @@ -502,8 +546,8 @@ impl_schema!(u32, Schema::Long); impl_schema!(f32, Schema::Float); impl_schema!(f64, Schema::Double); impl_schema!(String, Schema::String); -impl_schema!(str, Schema::String); -impl_schema!(char, Schema::String); +impl_schema!(str, Schema::String, String::default()); +impl_schema!(char, Schema::String, String::from(char::default())); macro_rules! impl_passthrough_schema ( ($type:ty where T: AvroSchemaComponent + ?Sized $(+ $bound:tt)*) => ( @@ -515,6 +559,10 @@ macro_rules! impl_passthrough_schema ( fn get_record_fields_in_ctxt(first_field_position: usize, named_schemas: &mut HashSet, enclosing_namespace: &Namespace) -> Option> { T::get_record_fields_in_ctxt(first_field_position, named_schemas, enclosing_namespace) } + + fn field_default() -> Option { + T::field_default() + } } ); ); @@ -535,6 +583,10 @@ macro_rules! impl_array_schema ( fn get_record_fields_in_ctxt(_: usize, _: &mut HashSet, _: &Namespace) -> Option> { None } + + fn field_default() -> Option { + Some(serde_json::Value::Array(Vec::new())) + } } ); ); @@ -562,6 +614,13 @@ where ) -> Option> { None } + + /// If `T` has a field default, this will return an array of with that default. Otherwise there is no default. + fn field_default() -> Option { + T::field_default().map(|default| { + serde_json::Value::Array(std::array::from_fn::<_, N, _>(|_| default.clone()).to_vec()) + }) + } } impl AvroSchemaComponent for HashMap @@ -582,6 +641,10 @@ where ) -> Option> { None } + + fn field_default() -> Option { + Some(serde_json::Value::Object(serde_json::Map::new())) + } } impl AvroSchemaComponent for Option @@ -609,6 +672,10 @@ where ) -> Option> { None } + + fn field_default() -> Option { + Some(serde_json::Value::Null) + } } impl AvroSchemaComponent for core::time::Duration { @@ -644,6 +711,10 @@ impl AvroSchemaComponent for core::time::Duration { ) -> Option> { None } + + fn field_default() -> Option { + Some(serde_json::Value::String(FIXED_12_DEFAULT.to_string())) + } } impl AvroSchemaComponent for uuid::Uuid { @@ -679,6 +750,10 @@ impl AvroSchemaComponent for uuid::Uuid { ) -> Option> { None } + + fn field_default() -> Option { + Some(serde_json::Value::String(FIXED_16_DEFAULT.to_string())) + } } impl AvroSchemaComponent for u64 { @@ -712,6 +787,10 @@ impl AvroSchemaComponent for u64 { ) -> Option> { None } + + fn field_default() -> Option { + Some(serde_json::Value::String(FIXED_8_DEFAULT.to_string())) + } } impl AvroSchemaComponent for u128 { @@ -745,6 +824,10 @@ impl AvroSchemaComponent for u128 { ) -> Option> { None } + + fn field_default() -> Option { + Some(serde_json::Value::String(FIXED_16_DEFAULT.to_string())) + } } impl AvroSchemaComponent for i128 { @@ -778,12 +861,18 @@ impl AvroSchemaComponent for i128 { ) -> Option> { None } + + fn field_default() -> Option { + Some(serde_json::Value::String(FIXED_16_DEFAULT.to_string())) + } } #[cfg(test)] mod tests { - use crate::schema::{FixedSchema, Name}; - use crate::{AvroSchema, Schema}; + use crate::{ + AvroSchema, Schema, + schema::{FixedSchema, Name}, + }; use apache_avro_test_helper::TestResult; #[test] diff --git a/avro/src/types.rs b/avro/src/types.rs index 9c1f05f3..f99dcb95 100644 --- a/avro/src/types.rs +++ b/avro/src/types.rs @@ -766,6 +766,13 @@ impl Value { } Value::Uuid(Uuid::from_slice(bytes).map_err(Details::ConvertSliceToUuid)?) } + (Value::String(ref string), UuidSchema::Fixed(_)) => { + let bytes = string.as_bytes(); + if bytes.len() != 16 { + return Err(Details::ConvertFixedToUuid(bytes.len()).into()); + } + Value::Uuid(Uuid::from_slice(bytes).map_err(Details::ConvertSliceToUuid)?) + } (other, _) => return Err(Details::GetUuid(other).into()), }; Ok(value) diff --git a/avro_derive/src/attributes/avro.rs b/avro_derive/src/attributes/avro.rs index ea171b56..fdf755e4 100644 --- a/avro_derive/src/attributes/avro.rs +++ b/avro_derive/src/attributes/avro.rs @@ -21,6 +21,7 @@ //! Although a user will mostly use the Serde attributes, there are some Avro specific attributes //! a user can use. These add extra metadata to the generated schema. +use crate::attributes::FieldDefault; use crate::case::RenameRule; use darling::FromMeta; use proc_macro2::Span; @@ -53,6 +54,9 @@ pub struct ContainerAttributes { /// [`serde::ContainerAttributes::rename_all`]: crate::attributes::serde::ContainerAttributes::rename_all #[darling(default)] pub rename_all: RenameRule, + /// Set the default value if this schema is used as a field + #[darling(default)] + pub default: Option, } impl ContainerAttributes { @@ -125,7 +129,7 @@ pub struct FieldAttributes { /// /// This is also used as the default when `skip_serializing{_if}` is used. #[darling(default)] - pub default: Option, + pub default: FieldDefault, /// Deprecated. Use [`serde::FieldAttributes::alias`] instead. /// /// Adds the `aliases` field to the schema. diff --git a/avro_derive/src/attributes/mod.rs b/avro_derive/src/attributes/mod.rs index cc259f18..d05c8c3c 100644 --- a/avro_derive/src/attributes/mod.rs +++ b/avro_derive/src/attributes/mod.rs @@ -17,7 +17,8 @@ use crate::case::RenameRule; use darling::{FromAttributes, FromMeta}; -use proc_macro2::Span; +use proc_macro2::{Span, TokenStream}; +use quote::quote; use syn::{AttrStyle, Attribute, Expr, Ident, Path, spanned::Spanned}; mod avro; @@ -30,6 +31,7 @@ pub struct NamedTypeOptions { pub aliases: Vec, pub rename_all: RenameRule, pub transparent: bool, + pub default: TokenStream, } impl NamedTypeOptions { @@ -116,12 +118,29 @@ impl NamedTypeOptions { let doc = avro.doc.or_else(|| extract_rustdoc(attributes)); + let default = match avro.default { + None => quote! { None }, + Some(default_value) => { + let _: serde_json::Value = + serde_json::from_str(&default_value[..]).map_err(|e| { + vec![syn::Error::new( + 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())) + } + } + }; + Ok(Self { name: full_schema_name, doc, aliases: avro.alias, rename_all: serde.rename_all.serialize, transparent: serde.transparent, + default, }) } } @@ -210,11 +229,38 @@ impl With { } } } +/// How to get the default value for a value. +#[derive(Debug, PartialEq, Default)] +pub enum FieldDefault { + /// Use `::field_default`. + #[default] + Trait, + /// Don't set a default. + Disabled, + /// Use this JSON value. + Value(String), +} + +impl FromMeta for FieldDefault { + fn from_string(value: &str) -> darling::Result { + Ok(Self::Value(value.to_string())) + } + + fn from_bool(value: bool) -> darling::Result { + if value { + Err(darling::Error::custom( + "Expected `false` or a JSON string, got `true`", + )) + } else { + Ok(Self::Disabled) + } + } +} #[derive(Default)] pub struct FieldOptions { pub doc: Option, - pub default: Option, + pub default: FieldDefault, pub alias: Vec, pub rename: Option, pub skip: bool, @@ -274,11 +320,11 @@ impl FieldOptions { } if ((serde.skip_serializing && !serde.skip_deserializing) || serde.skip_serializing_if.is_some()) - && avro.default.is_none() + && avro.default == FieldDefault::Disabled { errors.push(syn::Error::new( span, - "`#[serde(skip_serializing)]` and `#[serde(skip_serializing_if)]` require `#[avro(default = \"..\")]`" + "`#[serde(skip_serializing)]` and `#[serde(skip_serializing_if)]` are incompatible with `#[avro(default = false)]`" )); } let with = match With::from_avro_and_serde(&avro.with, &serde.with, span) { diff --git a/avro_derive/src/lib.rs b/avro_derive/src/lib.rs index df14b97a..f0bcbc20 100644 --- a/avro_derive/src/lib.rs +++ b/avro_derive/src/lib.rs @@ -40,7 +40,7 @@ use syn::{ }; use crate::{ - attributes::{FieldOptions, NamedTypeOptions, VariantOptions, With}, + attributes::{FieldDefault, FieldOptions, NamedTypeOptions, VariantOptions, With}, case::RenameRule, }; @@ -75,6 +75,7 @@ fn derive_avro_schema(input: DeriveInput) -> Result &input.generics, get_schema_impl, get_record_fields_impl, + named_type_options.default, )) } syn::Data::Enum(data_enum) => { @@ -93,6 +94,7 @@ fn derive_avro_schema(input: DeriveInput) -> Result &input.generics, inner, quote! { None }, + named_type_options.default, )) } syn::Data::Union(_) => Err(vec![syn::Error::new( @@ -108,6 +110,7 @@ fn create_trait_definition( generics: &Generics, get_schema_impl: TokenStream, get_record_fields_impl: TokenStream, + field_default_impl: TokenStream, ) -> TokenStream { let (impl_generics, ty_generics, where_clause) = generics.split_for_impl(); quote! { @@ -120,6 +123,10 @@ fn create_trait_definition( fn get_record_fields_in_ctxt(mut field_position: usize, named_schemas: &mut ::std::collections::HashSet<::apache_avro::schema::Name>, enclosing_namespace: &::std::option::Option<::std::string::String>) -> ::std::option::Option<::std::vec::Vec<::apache_avro::schema::RecordField>> { #get_record_fields_impl } + + fn field_default() -> ::std::option::Option<::serde_json::Value> { + ::std::option::Option::#field_default_impl + } } } } @@ -187,7 +194,9 @@ fn get_struct_schema_def( continue; } let default_value = match field_attrs.default { - Some(default_value) => { + 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( @@ -199,7 +208,6 @@ fn get_struct_schema_def( Some(serde_json::from_str(#default_value).expect(format!("Invalid JSON: {:?}", #default_value).as_str())) } } - None => quote! { None }, }; let aliases = aliases(&field_attrs.alias); let schema_expr = get_field_schema_expr(&field, field_attrs.with)?; @@ -462,6 +470,28 @@ fn type_to_get_record_fields_expr(ty: &Type) -> Result Result> { + 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:?}" + ), + )]), + } +} + fn default_enum_variant( data_enum: &syn::DataEnum, error_span: Span, @@ -664,6 +694,10 @@ mod tests { ) -> ::std::option::Option <::std::vec::Vec<::apache_avro::schema::RecordField>> { None } + + fn field_default () -> ::std::option::Option<::serde_json::Value> { + ::std::option::Option::None + } } }.to_string()); } @@ -790,7 +824,7 @@ mod tests { match syn::parse2::(test_struct) { Ok(input) => { let schema_res = derive_avro_schema(input); - let expected_token_stream = r#"# [automatically_derived] impl :: apache_avro :: AvroSchemaComponent for A { fn get_schema_in_ctxt (named_schemas : & mut :: std :: collections :: HashSet < :: apache_avro :: schema :: Name > , enclosing_namespace : & :: std :: option :: Option < :: std :: string :: String >) -> :: apache_avro :: schema :: Schema { let name = apache_avro :: schema :: Name :: new ("A") . expect (concat ! ("Unable to parse schema name " , "A")) . fully_qualified_name (enclosing_namespace) ; if named_schemas . contains (& name) { apache_avro :: schema :: Schema :: Ref { name } } else { let enclosing_namespace = & name . namespace ; named_schemas . insert (name . clone ()) ; { let mut schema_fields = Vec :: with_capacity (1usize) ; let mut field_position = 0 ; schema_fields . push (:: apache_avro :: schema :: RecordField { name : "a3" . to_string () , doc : Some ("a doc" . into ()) , default : Some (serde_json :: from_str ("123") . expect (format ! ("Invalid JSON: {:?}" , "123") . as_str ())) , aliases : Some (vec ! ["a1" . try_into () . expect ("Alias is invalid") , "a2" . try_into () . expect ("Alias is invalid")]) , schema : < i32 as apache_avro :: AvroSchemaComponent > :: get_schema_in_ctxt (named_schemas , enclosing_namespace) , order : :: apache_avro :: schema :: RecordFieldOrder :: Ascending , position : field_position , custom_attributes : Default :: default () , }) ; field_position += 1 ; 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 ("A") . expect (& format ! ("Unable to parse struct name for schema {}" , "A") [..]) ; let lookup : std :: collections :: BTreeMap < String , usize > = schema_fields . iter () . map (| field | (field . name . to_owned () , field . position)) . collect () ; apache_avro :: schema :: Schema :: Record (apache_avro :: schema :: RecordSchema { name , aliases : None , doc : None , fields : schema_fields , lookup , attributes : Default :: default () , }) } } } fn get_record_fields_in_ctxt (mut field_position : usize , named_schemas : & mut :: std :: collections :: HashSet < :: apache_avro :: schema :: Name > , enclosing_namespace : & :: std :: option :: Option < :: std :: string :: String >) -> :: std :: option :: Option < :: std :: vec :: Vec < :: apache_avro :: schema :: RecordField >> { let mut schema_fields = Vec :: with_capacity (1usize) ; schema_fields . push (:: apache_avro :: schema :: RecordField { name : "a3" . to_string () , doc : Some ("a doc" . into ()) , default : Some (serde_json :: from_str ("123") . expect (format ! ("Invalid JSON: {:?}" , "123") . as_str ())) , aliases : Some (vec ! ["a1" . try_into () . expect ("Alias is invalid") , "a2" . try_into () . expect ("Alias is invalid")]) , schema : < i32 as apache_avro :: AvroSchemaComponent > :: get_schema_in_ctxt (named_schemas , enclosing_namespace) , order : :: apache_avro :: schema :: RecordFieldOrder :: Ascending , position : field_position , custom_attributes : Default :: default () , }) ; field_position += 1 ; Some (schema_fields) } }"#; + let expected_token_stream = r#"# [automatically_derived] impl :: apache_avro :: AvroSchemaComponent for A { fn get_schema_in_ctxt (named_schemas : & mut :: std :: collections :: HashSet < :: apache_avro :: schema :: Name > , enclosing_namespace : & :: std :: option :: Option < :: std :: string :: String >) -> :: apache_avro :: schema :: Schema { let name = apache_avro :: schema :: Name :: new ("A") . expect (concat ! ("Unable to parse schema name " , "A")) . fully_qualified_name (enclosing_namespace) ; if named_schemas . contains (& name) { apache_avro :: schema :: Schema :: Ref { name } } else { let enclosing_namespace = & name . namespace ; named_schemas . insert (name . clone ()) ; { let mut schema_fields = Vec :: with_capacity (1usize) ; let mut field_position = 0 ; schema_fields . push (:: apache_avro :: schema :: RecordField { name : "a3" . to_string () , doc : Some ("a doc" . into ()) , default : Some (serde_json :: from_str ("123") . expect (format ! ("Invalid JSON: {:?}" , "123") . as_str ())) , aliases : Some (vec ! ["a1" . try_into () . expect ("Alias is invalid") , "a2" . try_into () . expect ("Alias is invalid")]) , schema : < i32 as apache_avro :: AvroSchemaComponent > :: get_schema_in_ctxt (named_schemas , enclosing_namespace) , order : :: apache_avro :: schema :: RecordFieldOrder :: Ascending , position : field_position , custom_attributes : Default :: default () , }) ; field_position += 1 ; 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 ("A") . expect (& format ! ("Unable to parse struct name for schema {}" , "A") [..]) ; let lookup : std :: collections :: BTreeMap < String , usize > = schema_fields . iter () . map (| field | (field . name . to_owned () , field . position)) . collect () ; apache_avro :: schema :: Schema :: Record (apache_avro :: schema :: RecordSchema { name , aliases : None , doc : None , fields : schema_fields , lookup , attributes : Default :: default () , }) } } } fn get_record_fields_in_ctxt (mut field_position : usize , named_schemas : & mut :: std :: collections :: HashSet < :: apache_avro :: schema :: Name > , enclosing_namespace : & :: std :: option :: Option < :: std :: string :: String >) -> :: std :: option :: Option < :: std :: vec :: Vec < :: apache_avro :: schema :: RecordField >> { let mut schema_fields = Vec :: with_capacity (1usize) ; schema_fields . push (:: apache_avro :: schema :: RecordField { name : "a3" . to_string () , doc : Some ("a doc" . into ()) , default : Some (serde_json :: from_str ("123") . expect (format ! ("Invalid JSON: {:?}" , "123") . as_str ())) , aliases : Some (vec ! ["a1" . try_into () . expect ("Alias is invalid") , "a2" . try_into () . expect ("Alias is invalid")]) , schema : < i32 as apache_avro :: AvroSchemaComponent > :: get_schema_in_ctxt (named_schemas , enclosing_namespace) , order : :: apache_avro :: schema :: RecordFieldOrder :: Ascending , position : field_position , custom_attributes : Default :: default () , }) ; field_position += 1 ; Some (schema_fields) } fn field_default () -> :: std :: option :: Option < :: serde_json :: Value > { :: std :: option :: Option :: None } }"#; let schema_token_stream = schema_res.unwrap().to_string(); assert_eq!(schema_token_stream, expected_token_stream); } @@ -809,7 +843,7 @@ mod tests { match syn::parse2::(test_enum) { Ok(input) => { let schema_res = derive_avro_schema(input); - let expected_token_stream = r#"# [automatically_derived] impl :: apache_avro :: AvroSchemaComponent for A { fn get_schema_in_ctxt (named_schemas : & mut :: std :: collections :: HashSet < :: apache_avro :: schema :: Name > , enclosing_namespace : & :: std :: option :: Option < :: std :: string :: String >) -> :: apache_avro :: schema :: Schema { let name = apache_avro :: schema :: Name :: new ("A") . expect (concat ! ("Unable to parse schema name " , "A")) . fully_qualified_name (enclosing_namespace) ; if named_schemas . contains (& name) { apache_avro :: schema :: Schema :: Ref { name } } else { let enclosing_namespace = & name . namespace ; named_schemas . insert (name . clone ()) ; apache_avro :: schema :: Schema :: Enum (apache_avro :: schema :: EnumSchema { name : apache_avro :: schema :: Name :: new ("A") . expect (& format ! ("Unable to parse enum name for schema {}" , "A") [..]) , aliases : None , doc : None , symbols : vec ! ["A3" . to_owned ()] , default : None , attributes : Default :: default () , }) } } fn get_record_fields_in_ctxt (mut field_position : usize , named_schemas : & mut :: std :: collections :: HashSet < :: apache_avro :: schema :: Name > , enclosing_namespace : & :: std :: option :: Option < :: std :: string :: String >) -> :: std :: option :: Option < :: std :: vec :: Vec < :: apache_avro :: schema :: RecordField >> { None } }"#; + let expected_token_stream = r#"# [automatically_derived] impl :: apache_avro :: AvroSchemaComponent for A { fn get_schema_in_ctxt (named_schemas : & mut :: std :: collections :: HashSet < :: apache_avro :: schema :: Name > , enclosing_namespace : & :: std :: option :: Option < :: std :: string :: String >) -> :: apache_avro :: schema :: Schema { let name = apache_avro :: schema :: Name :: new ("A") . expect (concat ! ("Unable to parse schema name " , "A")) . fully_qualified_name (enclosing_namespace) ; if named_schemas . contains (& name) { apache_avro :: schema :: Schema :: Ref { name } } else { let enclosing_namespace = & name . namespace ; named_schemas . insert (name . clone ()) ; apache_avro :: schema :: Schema :: Enum (apache_avro :: schema :: EnumSchema { name : apache_avro :: schema :: Name :: new ("A") . expect (& format ! ("Unable to parse enum name for schema {}" , "A") [..]) , aliases : None , doc : None , symbols : vec ! ["A3" . to_owned ()] , default : None , attributes : Default :: default () , }) } } fn get_record_fields_in_ctxt (mut field_position : usize , named_schemas : & mut :: std :: collections :: HashSet < :: apache_avro :: schema :: Name > , enclosing_namespace : & :: std :: option :: Option < :: std :: string :: String >) -> :: std :: option :: Option < :: std :: vec :: Vec < :: apache_avro :: schema :: RecordField >> { None } fn field_default () -> :: std :: option :: Option < :: serde_json :: Value > { :: std :: option :: Option :: None } }"#; let schema_token_stream = schema_res.unwrap().to_string(); assert_eq!(schema_token_stream, expected_token_stream); } @@ -832,7 +866,7 @@ mod tests { match syn::parse2::(test_struct) { Ok(input) => { let schema_res = derive_avro_schema(input); - let expected_token_stream = r#"# [automatically_derived] impl :: apache_avro :: AvroSchemaComponent for A { fn get_schema_in_ctxt (named_schemas : & mut :: std :: collections :: HashSet < :: apache_avro :: schema :: Name > , enclosing_namespace : & :: std :: option :: Option < :: std :: string :: String >) -> :: apache_avro :: schema :: Schema { let name = apache_avro :: schema :: Name :: new ("A") . expect (concat ! ("Unable to parse schema name " , "A")) . fully_qualified_name (enclosing_namespace) ; if named_schemas . contains (& name) { apache_avro :: schema :: Schema :: Ref { name } } else { let enclosing_namespace = & name . namespace ; named_schemas . insert (name . clone ()) ; { let mut schema_fields = Vec :: with_capacity (2usize) ; let mut field_position = 0 ; schema_fields . push (:: apache_avro :: schema :: RecordField { name : "ITEM" . to_string () , doc : None , default : None , aliases : None , schema : < i32 as apache_avro :: AvroSchemaComponent > :: get_schema_in_ctxt (named_schemas , enclosing_namespace) , order : :: apache_avro :: schema :: RecordFieldOrder :: Ascending , position : field_position , custom_attributes : Default :: default () , }) ; field_position += 1 ; schema_fields . push (:: apache_avro :: schema :: RecordField { name : "DOUBLE_ITEM" . to_string () , doc : None , default : None , aliases : None , schema : < i32 as apache_avro :: AvroSchemaComponent > :: get_schema_in_ctxt (named_schemas , enclosing_namespace) , order : :: apache_avro :: schema :: RecordFieldOrder :: Ascending , position : field_position , custom_attributes : Default :: default () , }) ; field_position += 1 ; 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 ("A") . expect (& format ! ("Unable to parse struct name for schema {}" , "A") [..]) ; let lookup : std :: collections :: BTreeMap < String , usize > = schema_fields . iter () . map (| field | (field . name . to_owned () , field . position)) . collect () ; apache_avro :: schema :: Schema :: Record (apache_avro :: schema :: RecordSchema { name , aliases : None , doc : None , fields : schema_fields , lookup , attributes : Default :: default () , }) } } } fn get_record_fields_in_ctxt (mut field_position : usize , named_schemas : & mut :: std :: collections :: HashSet < :: apache_avro :: schema :: Name > , enclosing_namespace : & :: std :: option :: Option < :: std :: string :: String >) -> :: std :: option :: Option < :: std :: vec :: Vec < :: apache_avro :: schema :: RecordField >> { let mut schema_fields = Vec :: with_capacity (2usize) ; schema_fields . push (:: apache_avro :: schema :: RecordField { name : "ITEM" . to_string () , doc : None , default : None , aliases : None , schema : < i32 as apache_avro :: AvroSchemaComponent > :: get_schema_in_ctxt (named_schemas , enclosing_namespace) , order : :: apache_avro :: schema :: RecordFieldOrder :: Ascending , position : field_position , custom_attributes : Default :: default () , }) ; field_position += 1 ; schema_fields . push (:: apache_avro :: schema :: RecordField { name : "DOUBLE_ITEM" . to_string () , doc : None , default : None , aliases : None , schema : < i32 as apache_avro :: AvroSchemaComponent > :: get_schema_in_ctxt (named_schemas , enclosing_namespace) , order : :: apache_avro :: schema :: RecordFieldOrder :: Ascending , position : field_position , custom_attributes : Default :: default () , }) ; field_position += 1 ; Some (schema_fields) } }"#; + let expected_token_stream = r#"# [automatically_derived] impl :: apache_avro :: AvroSchemaComponent for A { fn get_schema_in_ctxt (named_schemas : & mut :: std :: collections :: HashSet < :: apache_avro :: schema :: Name > , enclosing_namespace : & :: std :: option :: Option < :: std :: string :: String >) -> :: apache_avro :: schema :: Schema { let name = apache_avro :: schema :: Name :: new ("A") . expect (concat ! ("Unable to parse schema name " , "A")) . fully_qualified_name (enclosing_namespace) ; if named_schemas . contains (& name) { apache_avro :: schema :: Schema :: Ref { name } } else { let enclosing_namespace = & name . namespace ; named_schemas . insert (name . clone ()) ; { let mut schema_fields = Vec :: with_capacity (2usize) ; let mut field_position = 0 ; schema_fields . push (:: apache_avro :: schema :: RecordField { name : "ITEM" . to_string () , doc : None , default : < i32 as apache_avro :: AvroSchemaComponent > :: field_default () , aliases : None , schema : < i32 as apache_avro :: AvroSchemaComponent > :: get_schema_in_ctxt (named_schemas , enclosing_namespace) , order : :: apache_avro :: schema :: RecordFieldOrder :: Ascending , position : field_position , custom_attributes : Default :: default () , }) ; field_position += 1 ; schema_fields . push (:: apache_avro :: schema :: RecordField { name : "DOUBLE_ITEM" . to_string () , doc : None , default : < i32 as apache_avro :: AvroSchemaComponent > :: field_default () , aliases : None , schema : < i32 as apache_avro :: AvroSchemaComponent > :: get_schema_in_ctxt (named_schemas , enclosing_namespace) , order : :: apache_avro :: schema :: RecordFieldOrder :: Ascending , position : field_position , custom_attributes : Default :: default () , }) ; field_position += 1 ; 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 ("A") . expect (& format ! ("Unable to parse struct name for schema {}" , "A") [..]) ; let lookup : std :: collections :: BTreeMap < String , usize > = schema_fields . iter () . map (| field | (field . name . to_owned () , field . position)) . collect () ; apache_avro :: schema :: Schema :: Record (apache_avro :: schema :: RecordSchema { name , aliases : None , doc : None , fields : schema_fields , lookup , attributes : Default :: default () , }) } } } fn get_record_fields_in_ctxt (mut field_position : usize , named_schemas : & mut :: std :: collections :: HashSet < :: apache_avro :: schema :: Name > , enclosing_namespace : & :: std :: option :: Option < :: std :: string :: String >) -> :: std :: option :: Option < :: std :: vec :: Vec < :: apache_avro :: schema :: RecordField >> { let mut schema_fields = Vec :: with_capacity (2usize) ; schema_fields . push (:: apache_avro :: schema :: RecordField { name : "ITEM" . to_string () , doc : None , default : < i32 as apache_avro :: AvroSchemaComponent > :: field_default () , aliases : None , schema : < i32 as apache_avro :: AvroSchemaComponent > :: get_schema_in_ctxt (named_schemas , enclosing_namespace) , order : :: apache_avro :: schema :: RecordFieldOrder :: Ascending , position : field_position , custom_attributes : Default :: default () , }) ; field_position += 1 ; schema_fields . push (:: apache_avro :: schema :: RecordField { name : "DOUBLE_ITEM" . to_string () , doc : None , default : < i32 as apache_avro :: AvroSchemaComponent > :: field_default () , aliases : None , schema : < i32 as apache_avro :: AvroSchemaComponent > :: get_schema_in_ctxt (named_schemas , enclosing_namespace) , order : :: apache_avro :: schema :: RecordFieldOrder :: Ascending , position : field_position , custom_attributes : Default :: default () , }) ; field_position += 1 ; Some (schema_fields) } fn field_default () -> :: std :: option :: Option < :: serde_json :: Value > { :: std :: option :: Option :: None } }"#; let schema_token_stream = schema_res.unwrap().to_string(); assert_eq!(schema_token_stream, expected_token_stream); } @@ -852,7 +886,7 @@ mod tests { match syn::parse2::(test_enum) { Ok(input) => { let schema_res = derive_avro_schema(input); - let expected_token_stream = r#"# [automatically_derived] impl :: apache_avro :: AvroSchemaComponent for B { fn get_schema_in_ctxt (named_schemas : & mut :: std :: collections :: HashSet < :: apache_avro :: schema :: Name > , enclosing_namespace : & :: std :: option :: Option < :: std :: string :: String >) -> :: apache_avro :: schema :: Schema { let name = apache_avro :: schema :: Name :: new ("B") . expect (concat ! ("Unable to parse schema name " , "B")) . fully_qualified_name (enclosing_namespace) ; if named_schemas . contains (& name) { apache_avro :: schema :: Schema :: Ref { name } } else { let enclosing_namespace = & name . namespace ; named_schemas . insert (name . clone ()) ; apache_avro :: schema :: Schema :: Enum (apache_avro :: schema :: EnumSchema { name : apache_avro :: schema :: Name :: new ("B") . expect (& format ! ("Unable to parse enum name for schema {}" , "B") [..]) , aliases : None , doc : None , symbols : vec ! ["ITEM" . to_owned () , "DOUBLE_ITEM" . to_owned ()] , default : None , attributes : Default :: default () , }) } } fn get_record_fields_in_ctxt (mut field_position : usize , named_schemas : & mut :: std :: collections :: HashSet < :: apache_avro :: schema :: Name > , enclosing_namespace : & :: std :: option :: Option < :: std :: string :: String >) -> :: std :: option :: Option < :: std :: vec :: Vec < :: apache_avro :: schema :: RecordField >> { None } }"#; + let expected_token_stream = r#"# [automatically_derived] impl :: apache_avro :: AvroSchemaComponent for B { fn get_schema_in_ctxt (named_schemas : & mut :: std :: collections :: HashSet < :: apache_avro :: schema :: Name > , enclosing_namespace : & :: std :: option :: Option < :: std :: string :: String >) -> :: apache_avro :: schema :: Schema { let name = apache_avro :: schema :: Name :: new ("B") . expect (concat ! ("Unable to parse schema name " , "B")) . fully_qualified_name (enclosing_namespace) ; if named_schemas . contains (& name) { apache_avro :: schema :: Schema :: Ref { name } } else { let enclosing_namespace = & name . namespace ; named_schemas . insert (name . clone ()) ; apache_avro :: schema :: Schema :: Enum (apache_avro :: schema :: EnumSchema { name : apache_avro :: schema :: Name :: new ("B") . expect (& format ! ("Unable to parse enum name for schema {}" , "B") [..]) , aliases : None , doc : None , symbols : vec ! ["ITEM" . to_owned () , "DOUBLE_ITEM" . to_owned ()] , default : None , attributes : Default :: default () , }) } } fn get_record_fields_in_ctxt (mut field_position : usize , named_schemas : & mut :: std :: collections :: HashSet < :: apache_avro :: schema :: Name > , enclosing_namespace : & :: std :: option :: Option < :: std :: string :: String >) -> :: std :: option :: Option < :: std :: vec :: Vec < :: apache_avro :: schema :: RecordField >> { None } fn field_default () -> :: std :: option :: Option < :: serde_json :: Value > { :: std :: option :: Option :: None } }"#; let schema_token_stream = schema_res.unwrap().to_string(); assert_eq!(schema_token_stream, expected_token_stream); } @@ -876,7 +910,7 @@ mod tests { match syn::parse2::(test_struct) { Ok(input) => { let schema_res = derive_avro_schema(input); - let expected_token_stream = r#"# [automatically_derived] impl :: apache_avro :: AvroSchemaComponent for A { fn get_schema_in_ctxt (named_schemas : & mut :: std :: collections :: HashSet < :: apache_avro :: schema :: Name > , enclosing_namespace : & :: std :: option :: Option < :: std :: string :: String >) -> :: apache_avro :: schema :: Schema { let name = apache_avro :: schema :: Name :: new ("A") . expect (concat ! ("Unable to parse schema name " , "A")) . fully_qualified_name (enclosing_namespace) ; if named_schemas . contains (& name) { apache_avro :: schema :: Schema :: Ref { name } } else { let enclosing_namespace = & name . namespace ; named_schemas . insert (name . clone ()) ; { let mut schema_fields = Vec :: with_capacity (2usize) ; let mut field_position = 0 ; schema_fields . push (:: apache_avro :: schema :: RecordField { name : "ITEM" . to_string () , doc : None , default : None , aliases : None , schema : < i32 as apache_avro :: AvroSchemaComponent > :: get_schema_in_ctxt (named_schemas , enclosing_namespace) , order : :: apache_avro :: schema :: RecordFieldOrder :: Ascending , position : field_position , custom_attributes : Default :: default () , }) ; field_position += 1 ; schema_fields . push (:: apache_avro :: schema :: RecordField { name : "DoubleItem" . to_string () , doc : None , default : None , aliases : None , schema : < i32 as apache_avro :: AvroSchemaComponent > :: get_schema_in_ctxt (named_schemas , enclosing_namespace) , order : :: apache_avro :: schema :: RecordFieldOrder :: Ascending , position : field_position , custom_attributes : Default :: default () , }) ; field_position += 1 ; 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 ("A") . expect (& format ! ("Unable to parse struct name for schema {}" , "A") [..]) ; let lookup : std :: collections :: BTreeMap < String , usize > = schema_fields . iter () . map (| field | (field . name . to_owned () , field . position)) . collect () ; apache_avro :: schema :: Schema :: Record (apache_avro :: schema :: RecordSchema { name , aliases : None , doc : None , fields : schema_fields , lookup , attributes : Default :: default () , }) } } } fn get_record_fields_in_ctxt (mut field_position : usize , named_schemas : & mut :: std :: collections :: HashSet < :: apache_avro :: schema :: Name > , enclosing_namespace : & :: std :: option :: Option < :: std :: string :: String >) -> :: std :: option :: Option < :: std :: vec :: Vec < :: apache_avro :: schema :: RecordField >> { let mut schema_fields = Vec :: with_capacity (2usize) ; schema_fields . push (:: apache_avro :: schema :: RecordField { name : "ITEM" . to_string () , doc : None , default : None , aliases : None , schema : < i32 as apache_avro :: AvroSchemaComponent > :: get_schema_in_ctxt (named_schemas , enclosing_namespace) , order : :: apache_avro :: schema :: RecordFieldOrder :: Ascending , position : field_position , custom_attributes : Default :: default () , }) ; field_position += 1 ; schema_fields . push (:: apache_avro :: schema :: RecordField { name : "DoubleItem" . to_string () , doc : None , default : None , aliases : None , schema : < i32 as apache_avro :: AvroSchemaComponent > :: get_schema_in_ctxt (named_schemas , enclosing_namespace) , order : :: apache_avro :: schema :: RecordFieldOrder :: Ascending , position : field_position , custom_attributes : Default :: default () , }) ; field_position += 1 ; Some (schema_fields) } }"#; + let expected_token_stream = r#"# [automatically_derived] impl :: apache_avro :: AvroSchemaComponent for A { fn get_schema_in_ctxt (named_schemas : & mut :: std :: collections :: HashSet < :: apache_avro :: schema :: Name > , enclosing_namespace : & :: std :: option :: Option < :: std :: string :: String >) -> :: apache_avro :: schema :: Schema { let name = apache_avro :: schema :: Name :: new ("A") . expect (concat ! ("Unable to parse schema name " , "A")) . fully_qualified_name (enclosing_namespace) ; if named_schemas . contains (& name) { apache_avro :: schema :: Schema :: Ref { name } } else { let enclosing_namespace = & name . namespace ; named_schemas . insert (name . clone ()) ; { let mut schema_fields = Vec :: with_capacity (2usize) ; let mut field_position = 0 ; schema_fields . push (:: apache_avro :: schema :: RecordField { name : "ITEM" . to_string () , doc : None , default : < i32 as apache_avro :: AvroSchemaComponent > :: field_default () , aliases : None , schema : < i32 as apache_avro :: AvroSchemaComponent > :: get_schema_in_ctxt (named_schemas , enclosing_namespace) , order : :: apache_avro :: schema :: RecordFieldOrder :: Ascending , position : field_position , custom_attributes : Default :: default () , }) ; field_position += 1 ; schema_fields . push (:: apache_avro :: schema :: RecordField { name : "DoubleItem" . to_string () , doc : None , default : < i32 as apache_avro :: AvroSchemaComponent > :: field_default () , aliases : None , schema : < i32 as apache_avro :: AvroSchemaComponent > :: get_schema_in_ctxt (named_schemas , enclosing_namespace) , order : :: apache_avro :: schema :: RecordFieldOrder :: Ascending , position : field_position , custom_attributes : Default :: default () , }) ; field_position += 1 ; 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 ("A") . expect (& format ! ("Unable to parse struct name for schema {}" , "A") [..]) ; let lookup : std :: collections :: BTreeMap < String , usize > = schema_fields . iter () . map (| field | (field . name . to_owned () , field . position)) . collect () ; apache_avro :: schema :: Schema :: Record (apache_avro :: schema :: RecordSchema { name , aliases : None , doc : None , fields : schema_fields , lookup , attributes : Default :: default () , }) } } } fn get_record_fields_in_ctxt (mut field_position : usize , named_schemas : & mut :: std :: collections :: HashSet < :: apache_avro :: schema :: Name > , enclosing_namespace : & :: std :: option :: Option < :: std :: string :: String >) -> :: std :: option :: Option < :: std :: vec :: Vec < :: apache_avro :: schema :: RecordField >> { let mut schema_fields = Vec :: with_capacity (2usize) ; schema_fields . push (:: apache_avro :: schema :: RecordField { name : "ITEM" . to_string () , doc : None , default : < i32 as apache_avro :: AvroSchemaComponent > :: field_default () , aliases : None , schema : < i32 as apache_avro :: AvroSchemaComponent > :: get_schema_in_ctxt (named_schemas , enclosing_namespace) , order : :: apache_avro :: schema :: RecordFieldOrder :: Ascending , position : field_position , custom_attributes : Default :: default () , }) ; field_position += 1 ; schema_fields . push (:: apache_avro :: schema :: RecordField { name : "DoubleItem" . to_string () , doc : None , default : < i32 as apache_avro :: AvroSchemaComponent > :: field_default () , aliases : None , schema : < i32 as apache_avro :: AvroSchemaComponent > :: get_schema_in_ctxt (named_schemas , enclosing_namespace) , order : :: apache_avro :: schema :: RecordFieldOrder :: Ascending , position : field_position , custom_attributes : Default :: default () , }) ; field_position += 1 ; Some (schema_fields) } fn field_default () -> :: std :: option :: Option < :: serde_json :: Value > { :: std :: option :: Option :: None } }"#; let schema_token_stream = schema_res.unwrap().to_string(); assert_eq!(schema_token_stream, expected_token_stream); } diff --git a/avro_derive/tests/derive.rs b/avro_derive/tests/derive.rs index 631e8250..7f536b6f 100644 --- a/avro_derive/tests/derive.rs +++ b/avro_derive/tests/derive.rs @@ -25,7 +25,9 @@ use std::{ borrow::Cow, collections::{HashMap, HashSet}, sync::Mutex, + time::Duration, }; +use uuid::Uuid; use pretty_assertions::assert_eq; @@ -1369,6 +1371,7 @@ fn test_basic_struct_with_defaults() { #[avro(default = "true")] condition: bool, // no default value for 'c' + #[avro(default = false)] c: f64, #[avro(default = r#"{"a": 1, "b": 2}"#)] map: HashMap, @@ -1942,6 +1945,7 @@ fn avro_rs_397_uuid() { "type":"fixed", "logicalType":"uuid", "name":"uuid", + "default":"\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "size":16 } } @@ -2374,3 +2378,203 @@ fn avro_rs_448_flatten_field_positions() { .collect::>(); assert_eq!(positions.as_slice(), &[0, 1, 2, 3][..]); } + +#[test] +fn avro_rs_476_field_default() { + #[derive(AvroSchema)] + struct Bar { + _field: Box, + } + + #[derive(AvroSchema)] + #[avro(default = r#"{"_field": true}"#)] + struct Spam { + _field: bool, + } + + #[derive(AvroSchema)] + struct Foo { + _a: bool, + _b: i8, + _c: i16, + _d: i32, + _e: i64, + _f: u8, + _g: u16, + _h: u32, + _i: f32, + _j: f64, + _k: String, + _l: Box, + _m: char, + _n: Box, + _o: Vec, + _p: [u8; 5], + _p_alt: [Bar; 5], + _q: HashMap, + _r: Option, + _s: Duration, + _t: Uuid, + _u: u64, + _v: u128, + _w: i128, + _x: Bar, + } + + let schema = Foo::get_schema(); + assert_eq!( + serde_json::to_string(&schema).unwrap(), + r#"{"type":"record","name":"Foo","fields":[{"name":"_a","type":"boolean","default":false},{"name":"_b","type":"int","default":0},{"name":"_c","type":"int","default":0},{"name":"_d","type":"int","default":0},{"name":"_e","type":"long","default":0},{"name":"_f","type":"int","default":0},{"name":"_g","type":"int","default":0},{"name":"_h","type":"long","default":0},{"name":"_i","type":"float","default":0.0},{"name":"_j","type":"double","default":0.0},{"name":"_k","type":"string","default":""},{"name":"_l","type":"string","default":""},{"name":"_m","type":"string","default":"\u0000"},{"name":"_n","type":{"type":"record","name":"Spam","fields":[{"name":"_field","type":"boolean","default":false}]},"default":{"_field":true}},{"name":"_o","type":{"type":"array","items":"boolean"},"default":[]},{"name":"_p","type":{"type":"array","items":"int"},"default":[0,0,0,0,0]},{"name":"_p_alt","type":{"type":"array","items":{"type":"record","name":"Bar","fields":[{"name":"_field","type":"Bar"}]}}},{"name":"_q","type":{"type":"map","values":"string"},"default":{}},{"name":"_r","type":["null","double"],"default":null},{"name":"_s","type":{"type":"fixed","name":"duration","size":12,"logicalType":"duration"},"default":"\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000"},{"name":"_t","type":{"type":"fixed","name":"uuid","size":16,"logicalType":"uuid"},"default":"\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000"},{"name":"_u","type":{"type":"fixed","name":"u64","size":8},"default":"\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000"},{"name":"_v","type":{"type":"fixed","name":"u128","size":16},"default":"\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000"},{"name":"_w","type":{"type":"fixed","name":"i128","size":16},"default":"\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000"},{"name":"_x","type":"Bar"}]}"# + ); +} + +#[test] +fn avro_rs_476_field_default_false() { + #[derive(AvroSchema)] + struct Bar { + _field: Box, + } + + #[derive(AvroSchema)] + #[avro(default = r#"{"_field": true}"#)] + struct Spam { + _field: bool, + } + + #[derive(AvroSchema)] + struct Foo { + #[avro(default = false)] + _a: bool, + #[avro(default = false)] + _b: i8, + #[avro(default = false)] + _c: i16, + #[avro(default = false)] + _d: i32, + #[avro(default = false)] + _e: i64, + #[avro(default = false)] + _f: u8, + #[avro(default = false)] + _g: u16, + #[avro(default = false)] + _h: u32, + #[avro(default = false)] + _i: f32, + #[avro(default = false)] + _j: f64, + #[avro(default = false)] + _k: String, + #[avro(default = false)] + _l: Box, + #[avro(default = false)] + _m: char, + #[avro(default = false)] + _n: Box, + #[avro(default = false)] + _o: Vec, + #[avro(default = false)] + _p: [u8; 5], + #[avro(default = false)] + _p_alt: [Bar; 5], + #[avro(default = false)] + _q: HashMap, + #[avro(default = false)] + _r: Option, + #[avro(default = false)] + _s: Duration, + #[avro(default = false)] + _t: Uuid, + #[avro(default = false)] + _u: u64, + #[avro(default = false)] + _v: u128, + #[avro(default = false)] + _w: i128, + #[avro(default = false)] + _x: Bar, + } + + let schema = Foo::get_schema(); + assert_eq!( + serde_json::to_string(&schema).unwrap(), + r#"{"type":"record","name":"Foo","fields":[{"name":"_a","type":"boolean"},{"name":"_b","type":"int"},{"name":"_c","type":"int"},{"name":"_d","type":"int"},{"name":"_e","type":"long"},{"name":"_f","type":"int"},{"name":"_g","type":"int"},{"name":"_h","type":"long"},{"name":"_i","type":"float"},{"name":"_j","type":"double"},{"name":"_k","type":"string"},{"name":"_l","type":"string"},{"name":"_m","type":"string"},{"name":"_n","type":{"type":"record","name":"Spam","fields":[{"name":"_field","type":"boolean","default":false}]}},{"name":"_o","type":{"type":"array","items":"boolean"}},{"name":"_p","type":{"type":"array","items":"int"}},{"name":"_p_alt","type":{"type":"array","items":{"type":"record","name":"Bar","fields":[{"name":"_field","type":"Bar"}]}}},{"name":"_q","type":{"type":"map","values":"string"}},{"name":"_r","type":["null","double"]},{"name":"_s","type":{"type":"fixed","name":"duration","size":12,"logicalType":"duration"}},{"name":"_t","type":{"type":"fixed","name":"uuid","size":16,"logicalType":"uuid"}},{"name":"_u","type":{"type":"fixed","name":"u64","size":8}},{"name":"_v","type":{"type":"fixed","name":"u128","size":16}},{"name":"_w","type":{"type":"fixed","name":"i128","size":16}},{"name":"_x","type":"Bar"}]}"# + ); +} + +#[test] +fn avro_rs_476_field_default_provided() { + #[derive(AvroSchema)] + #[avro(default = r#"{"_field": true}"#)] + struct Spam { + _field: bool, + } + + #[derive(AvroSchema)] + struct Foo { + #[avro(default = "true")] + _a: bool, + #[avro(default = "42")] + _b: i8, + #[avro(default = "42")] + _c: i16, + #[avro(default = "42")] + _d: i32, + #[avro(default = "42")] + _e: i64, + #[avro(default = "42")] + _f: u8, + #[avro(default = "42")] + _g: u16, + #[avro(default = "42")] + _h: u32, + #[avro(default = "42.0")] + _i: f32, + #[avro(default = "42.0")] + _j: f64, + #[avro(default = r#""String""#)] + _k: String, + #[avro(default = r#""str""#)] + _l: Box, + #[avro(default = r#""Z""#)] + _m: char, + #[avro(default = r#"{"_field": false}"#)] + _n: Box, + #[avro(default = "[true, false, true]")] + _o: Vec, + #[avro(default = "[1,2,3,4,5]")] + _p: [u8; 5], + #[avro( + default = r#"[{"_field": true},{"_field": false},{"_field": true},{"_field": false},{"_field": true}]"# + )] + _p_alt: [Spam; 5], + #[avro(default = r#"{"A": "B"}"#)] + _q: HashMap, + #[avro(default = "42.0")] + _r: Option, + #[avro( + default = r#""\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001""# + )] + _s: Duration, + #[avro( + default = r#""\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001""# + )] + _t: Uuid, + #[avro(default = r#""\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001""#)] + _u: u64, + #[avro( + default = r#""\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001""# + )] + _v: u128, + #[avro( + default = r#""\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001""# + )] + _w: i128, + } + + let schema = Foo::get_schema(); + assert_eq!( + serde_json::to_string(&schema).unwrap(), + r#"{"type":"record","name":"Foo","fields":[{"name":"_a","type":"boolean","default":true},{"name":"_b","type":"int","default":42},{"name":"_c","type":"int","default":42},{"name":"_d","type":"int","default":42},{"name":"_e","type":"long","default":42},{"name":"_f","type":"int","default":42},{"name":"_g","type":"int","default":42},{"name":"_h","type":"long","default":42},{"name":"_i","type":"float","default":42.0},{"name":"_j","type":"double","default":42.0},{"name":"_k","type":"string","default":"String"},{"name":"_l","type":"string","default":"str"},{"name":"_m","type":"string","default":"Z"},{"name":"_n","type":{"type":"record","name":"Spam","fields":[{"name":"_field","type":"boolean","default":false}]},"default":{"_field":false}},{"name":"_o","type":{"type":"array","items":"boolean"},"default":[true,false,true]},{"name":"_p","type":{"type":"array","items":"int"},"default":[1,2,3,4,5]},{"name":"_p_alt","type":{"type":"array","items":"Spam"},"default":[{"_field":true},{"_field":false},{"_field":true},{"_field":false},{"_field":true}]},{"name":"_q","type":{"type":"map","values":"string"},"default":{"A":"B"}},{"name":"_r","type":["null","double"],"default":42.0},{"name":"_s","type":{"type":"fixed","name":"duration","size":12,"logicalType":"duration"},"default":"\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001"},{"name":"_t","type":{"type":"fixed","name":"uuid","size":16,"logicalType":"uuid"},"default":"\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001"},{"name":"_u","type":{"type":"fixed","name":"u64","size":8},"default":"\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001"},{"name":"_v","type":{"type":"fixed","name":"u128","size":16},"default":"\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001"},{"name":"_w","type":{"type":"fixed","name":"i128","size":16},"default":"\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001"}]}"# + ); +} diff --git a/avro_derive/tests/ui/avro_rs_226_skip_serializing.rs b/avro_derive/tests/ui/avro_rs_226_skip_serializing.rs index 40f4756a..28a73954 100644 --- a/avro_derive/tests/ui/avro_rs_226_skip_serializing.rs +++ b/avro_derive/tests/ui/avro_rs_226_skip_serializing.rs @@ -22,6 +22,7 @@ struct T { x: Option, y: Option, #[serde(skip_serializing)] + #[avro(default = false)] z: Option, } diff --git a/avro_derive/tests/ui/avro_rs_226_skip_serializing.stderr b/avro_derive/tests/ui/avro_rs_226_skip_serializing.stderr index e974f858..d5316b5d 100644 --- a/avro_derive/tests/ui/avro_rs_226_skip_serializing.stderr +++ b/avro_derive/tests/ui/avro_rs_226_skip_serializing.stderr @@ -1,6 +1,7 @@ -error: `#[serde(skip_serializing)]` and `#[serde(skip_serializing_if)]` require `#[avro(default = "..")]` +error: `#[serde(skip_serializing)]` and `#[serde(skip_serializing_if)]` are incompatible with `#[avro(default = false)]` --> tests/ui/avro_rs_226_skip_serializing.rs:24:5 | 24 | / #[serde(skip_serializing)] -25 | | z: Option, +25 | | #[avro(default = false)] +26 | | z: Option, | |_________________^ diff --git a/avro_derive/tests/ui/avro_rs_226_skip_serializing_if.rs b/avro_derive/tests/ui/avro_rs_226_skip_serializing_if.rs index 2e87a9c0..e5320669 100644 --- a/avro_derive/tests/ui/avro_rs_226_skip_serializing_if.rs +++ b/avro_derive/tests/ui/avro_rs_226_skip_serializing_if.rs @@ -21,6 +21,7 @@ use apache_avro::AvroSchema; struct T { x: Option, #[serde(skip_serializing_if = "Option::is_none")] + #[avro(default = false)] y: Option, z: Option, } diff --git a/avro_derive/tests/ui/avro_rs_226_skip_serializing_if.stderr b/avro_derive/tests/ui/avro_rs_226_skip_serializing_if.stderr index 9794bfb2..f3094d0f 100644 --- a/avro_derive/tests/ui/avro_rs_226_skip_serializing_if.stderr +++ b/avro_derive/tests/ui/avro_rs_226_skip_serializing_if.stderr @@ -1,6 +1,7 @@ -error: `#[serde(skip_serializing)]` and `#[serde(skip_serializing_if)]` require `#[avro(default = "..")]` +error: `#[serde(skip_serializing)]` and `#[serde(skip_serializing_if)]` are incompatible with `#[avro(default = false)]` --> tests/ui/avro_rs_226_skip_serializing_if.rs:23:5 | 23 | / #[serde(skip_serializing_if = "Option::is_none")] -24 | | y: Option, +24 | | #[avro(default = false)] +25 | | y: Option, | |_____________________^ From 40a45065f59c5ec9c9db7de351f19d10845d21be Mon Sep 17 00:00:00 2001 From: Kriskras99 Date: Fri, 20 Feb 2026 15:43:30 +0100 Subject: [PATCH 2/4] fix: Apply suggestions from code review Co-authored-by: Martin Grigorov --- avro/src/serde/derive.rs | 8 +++----- avro_derive/src/attributes/mod.rs | 2 +- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/avro/src/serde/derive.rs b/avro/src/serde/derive.rs index 5d2fbca2..bd61dbdd 100644 --- a/avro/src/serde/derive.rs +++ b/avro/src/serde/derive.rs @@ -124,7 +124,7 @@ const FIXED_16_DEFAULT: &str = "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0"; /// - `#[avro(default = "null")]` or `#[avro(default = false)]` /// /// Control the `default` attribute of the field. When not used, it will use [`AvroSchemaComponent::field_default`] -/// to get the default value for a type. This default value can be overriden by providing a JSON string. +/// to get the default value for a type. This default value can be overridden by providing a JSON string. /// To remove the `default` attribute for a field, set `default` to `false`. /// /// _Note:_ This is a JSON value not a Rust value, as this is put in the schema itself. To encode a JSON string @@ -615,11 +615,9 @@ where None } - /// If `T` has a field default, this will return an array of with that default. Otherwise there is no default. + /// If `T` has a field default, this will return an array of elements with that default. Otherwise there is no default. fn field_default() -> Option { - T::field_default().map(|default| { - serde_json::Value::Array(std::array::from_fn::<_, N, _>(|_| default.clone()).to_vec()) - }) + T::field_default().map(|default| serde_json::Value::Array(vec![default; N])) } } diff --git a/avro_derive/src/attributes/mod.rs b/avro_derive/src/attributes/mod.rs index d05c8c3c..7dbab53b 100644 --- a/avro_derive/src/attributes/mod.rs +++ b/avro_derive/src/attributes/mod.rs @@ -125,7 +125,7 @@ impl NamedTypeOptions { serde_json::from_str(&default_value[..]).map_err(|e| { vec![syn::Error::new( ident.span(), - format!("Invalid avro default json: \n{e}"), + format!("Invalid Avro `default` JSON: \n{e}"), )] })?; quote! { From c5ade4726481948b179056390de59799fbf45757 Mon Sep 17 00:00:00 2001 From: Kriskras99 Date: Fri, 20 Feb 2026 16:36:18 +0100 Subject: [PATCH 3/4] fix: Improve documentation --- avro/src/serde/derive.rs | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/avro/src/serde/derive.rs b/avro/src/serde/derive.rs index bd61dbdd..24f5c185 100644 --- a/avro/src/serde/derive.rs +++ b/avro/src/serde/derive.rs @@ -121,14 +121,22 @@ const FIXED_16_DEFAULT: &str = "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0"; /// /// Set the `doc` attribute of the field. Defaults to the documentation of the field. /// -/// - `#[avro(default = "null")]` or `#[avro(default = false)]` +/// - `#[avro(default = ..)]` /// /// Control the `default` attribute of the field. When not used, it will use [`AvroSchemaComponent::field_default`] -/// to get the default value for a type. This default value can be overridden by providing a JSON string. -/// To remove the `default` attribute for a field, set `default` to `false`. +/// to get the default value for a type. To remove the `default` attribute for a field, set `default` to `false`: `#[avro(default = false)]`. /// -/// _Note:_ This is a JSON value not a Rust value, as this is put in the schema itself. To encode a JSON string -/// you need to use double quotes: `#[avro(default = r#""Some string value""#)]`. +/// To override or set a default value, provide a JSON string: +/// +/// - Null: `#[avro(default = "null")]` +/// - Boolean: `#[avro(default = "true")]`. +/// - Number: `#[avro(default = "42")]` or `#[avro(default = "42.5")]` +/// - String: `#[avro(default = r#""String needs extra quotes""#)]`. +/// - Array: `#[avro(default = r#"["One", "Two", "Three"]"#)]`. +/// - Object: `#[avro(default = r#"{"One": 1}"#)]`. +/// +/// See [the specification](https://avro.apache.org/docs/++version++/specification/#schema-record) +/// for details on how to map a type to a JSON value. /// /// - `#[serde(alias = "name")]` /// From 3915546b26aa8875f418ee042ef8066ff6664218 Mon Sep 17 00:00:00 2001 From: Kriskras99 Date: Mon, 23 Feb 2026 20:36:22 +0100 Subject: [PATCH 4/4] fix: Only implement field default for `Option` --- avro/src/serde/derive.rs | 34 ++++++++------------ avro_derive/tests/derive.rs | 62 ++++++------------------------------- 2 files changed, 23 insertions(+), 73 deletions(-) diff --git a/avro/src/serde/derive.rs b/avro/src/serde/derive.rs index 24f5c185..70bed2c0 100644 --- a/avro/src/serde/derive.rs +++ b/avro/src/serde/derive.rs @@ -22,10 +22,6 @@ use crate::schema::{ use std::borrow::Cow; use std::collections::{HashMap, HashSet}; -const FIXED_8_DEFAULT: &str = "\0\0\0\0\0\0\0\0"; -const FIXED_12_DEFAULT: &str = "\0\0\0\0\0\0\0\0\0\0\0\0"; -const FIXED_16_DEFAULT: &str = "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0"; - /// Trait for types that serve as an Avro data model. /// /// **Do not implement directly!** Either derive it or implement [`AvroSchemaComponent`] to get this trait @@ -371,8 +367,8 @@ pub trait AvroSchemaComponent { /// /// `None` means no default value, which is also the default implementation. /// - /// Implementations of this trait provided by this crate use the [`Default::default`] value of - /// the type. + /// Implementations of this trait provided by this crate return `None` except for `Option` + /// which returns `Some(serde_json::Value::Null)`. fn field_default() -> Option { None } @@ -523,10 +519,6 @@ where macro_rules! impl_schema ( ($type:ty, $variant_constructor:expr) => ( - impl_schema!($type, $variant_constructor, <$type as Default>::default()); - ); - - ($type:ty, $variant_constructor:expr, $default_constructor:expr) => ( impl AvroSchemaComponent for $type { fn get_schema_in_ctxt(_: &mut HashSet, _: &Namespace) -> Schema { $variant_constructor @@ -537,7 +529,7 @@ macro_rules! impl_schema ( } fn field_default() -> Option { - Some(serde_json::Value::from($default_constructor)) + None } } ); @@ -554,8 +546,8 @@ impl_schema!(u32, Schema::Long); impl_schema!(f32, Schema::Float); impl_schema!(f64, Schema::Double); impl_schema!(String, Schema::String); -impl_schema!(str, Schema::String, String::default()); -impl_schema!(char, Schema::String, String::from(char::default())); +impl_schema!(str, Schema::String); +impl_schema!(char, Schema::String); macro_rules! impl_passthrough_schema ( ($type:ty where T: AvroSchemaComponent + ?Sized $(+ $bound:tt)*) => ( @@ -593,7 +585,7 @@ macro_rules! impl_array_schema ( } fn field_default() -> Option { - Some(serde_json::Value::Array(Vec::new())) + None } } ); @@ -625,7 +617,7 @@ where /// If `T` has a field default, this will return an array of elements with that default. Otherwise there is no default. fn field_default() -> Option { - T::field_default().map(|default| serde_json::Value::Array(vec![default; N])) + None } } @@ -649,7 +641,7 @@ where } fn field_default() -> Option { - Some(serde_json::Value::Object(serde_json::Map::new())) + None } } @@ -719,7 +711,7 @@ impl AvroSchemaComponent for core::time::Duration { } fn field_default() -> Option { - Some(serde_json::Value::String(FIXED_12_DEFAULT.to_string())) + None } } @@ -758,7 +750,7 @@ impl AvroSchemaComponent for uuid::Uuid { } fn field_default() -> Option { - Some(serde_json::Value::String(FIXED_16_DEFAULT.to_string())) + None } } @@ -795,7 +787,7 @@ impl AvroSchemaComponent for u64 { } fn field_default() -> Option { - Some(serde_json::Value::String(FIXED_8_DEFAULT.to_string())) + None } } @@ -832,7 +824,7 @@ impl AvroSchemaComponent for u128 { } fn field_default() -> Option { - Some(serde_json::Value::String(FIXED_16_DEFAULT.to_string())) + None } } @@ -869,7 +861,7 @@ impl AvroSchemaComponent for i128 { } fn field_default() -> Option { - Some(serde_json::Value::String(FIXED_16_DEFAULT.to_string())) + None } } diff --git a/avro_derive/tests/derive.rs b/avro_derive/tests/derive.rs index 7f536b6f..7b06a4e5 100644 --- a/avro_derive/tests/derive.rs +++ b/avro_derive/tests/derive.rs @@ -2419,22 +2419,18 @@ fn avro_rs_476_field_default() { _v: u128, _w: i128, _x: Bar, + _z: Spam, } let schema = Foo::get_schema(); assert_eq!( serde_json::to_string(&schema).unwrap(), - r#"{"type":"record","name":"Foo","fields":[{"name":"_a","type":"boolean","default":false},{"name":"_b","type":"int","default":0},{"name":"_c","type":"int","default":0},{"name":"_d","type":"int","default":0},{"name":"_e","type":"long","default":0},{"name":"_f","type":"int","default":0},{"name":"_g","type":"int","default":0},{"name":"_h","type":"long","default":0},{"name":"_i","type":"float","default":0.0},{"name":"_j","type":"double","default":0.0},{"name":"_k","type":"string","default":""},{"name":"_l","type":"string","default":""},{"name":"_m","type":"string","default":"\u0000"},{"name":"_n","type":{"type":"record","name":"Spam","fields":[{"name":"_field","type":"boolean","default":false}]},"default":{"_field":true}},{"name":"_o","type":{"type":"array","items":"boolean"},"default":[]},{"name":"_p","type":{"type":"array","items":"int"},"default":[0,0,0,0,0]},{"name":"_p_alt","type":{"type":"array","items":{"type":"record","name":"Bar","fields":[{"name":"_field","type":"Bar"}]}}},{"name":"_q","type":{"type":"map","values":"string"},"default":{}},{"name":"_r","type":["null","double"],"default":null},{"name":"_s","type":{"type":"fixed","name":"duration","size":12,"logicalType":"duration"},"default":"\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000"},{"name":"_t","type":{"type":"fixed","name":"uuid","size":16,"logicalType":"uuid"},"default":"\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000"},{"name":"_u","type":{"type":"fixed","name":"u64","size":8},"default":"\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000"},{"name":"_v","type":{"type":"fixed","name":"u128","size":16},"default":"\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000"},{"name":"_w","type":{"type":"fixed","name":"i128","size":16},"default":"\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000"},{"name":"_x","type":"Bar"}]}"# + r#"{"type":"record","name":"Foo","fields":[{"name":"_a","type":"boolean"},{"name":"_b","type":"int"},{"name":"_c","type":"int"},{"name":"_d","type":"int"},{"name":"_e","type":"long"},{"name":"_f","type":"int"},{"name":"_g","type":"int"},{"name":"_h","type":"long"},{"name":"_i","type":"float"},{"name":"_j","type":"double"},{"name":"_k","type":"string"},{"name":"_l","type":"string"},{"name":"_m","type":"string"},{"name":"_n","type":{"type":"record","name":"Spam","fields":[{"name":"_field","type":"boolean"}]},"default":{"_field":true}},{"name":"_o","type":{"type":"array","items":"boolean"}},{"name":"_p","type":{"type":"array","items":"int"}},{"name":"_p_alt","type":{"type":"array","items":{"type":"record","name":"Bar","fields":[{"name":"_field","type":"Bar"}]}}},{"name":"_q","type":{"type":"map","values":"string"}},{"name":"_r","type":["null","double"],"default":null},{"name":"_s","type":{"type":"fixed","name":"duration","size":12,"logicalType":"duration"}},{"name":"_t","type":{"type":"fixed","name":"uuid","size":16,"logicalType":"uuid"}},{"name":"_u","type":{"type":"fixed","name":"u64","size":8}},{"name":"_v","type":{"type":"fixed","name":"u128","size":16}},{"name":"_w","type":{"type":"fixed","name":"i128","size":16}},{"name":"_x","type":"Bar"},{"name":"_z","type":"Spam","default":{"_field":true}}]}"# ); } #[test] fn avro_rs_476_field_default_false() { - #[derive(AvroSchema)] - struct Bar { - _field: Box, - } - #[derive(AvroSchema)] #[avro(default = r#"{"_field": true}"#)] struct Spam { @@ -2446,59 +2442,19 @@ fn avro_rs_476_field_default_false() { #[avro(default = false)] _a: bool, #[avro(default = false)] - _b: i8, - #[avro(default = false)] - _c: i16, - #[avro(default = false)] - _d: i32, - #[avro(default = false)] - _e: i64, - #[avro(default = false)] - _f: u8, - #[avro(default = false)] - _g: u16, - #[avro(default = false)] - _h: u32, - #[avro(default = false)] - _i: f32, - #[avro(default = false)] - _j: f64, - #[avro(default = false)] - _k: String, - #[avro(default = false)] - _l: Box, - #[avro(default = false)] - _m: char, + _b: Spam, #[avro(default = false)] - _n: Box, + _c: Box, #[avro(default = false)] - _o: Vec, - #[avro(default = false)] - _p: [u8; 5], - #[avro(default = false)] - _p_alt: [Bar; 5], - #[avro(default = false)] - _q: HashMap, - #[avro(default = false)] - _r: Option, + _d: HashMap, #[avro(default = false)] - _s: Duration, - #[avro(default = false)] - _t: Uuid, - #[avro(default = false)] - _u: u64, - #[avro(default = false)] - _v: u128, - #[avro(default = false)] - _w: i128, - #[avro(default = false)] - _x: Bar, + _e: Option, } let schema = Foo::get_schema(); assert_eq!( serde_json::to_string(&schema).unwrap(), - r#"{"type":"record","name":"Foo","fields":[{"name":"_a","type":"boolean"},{"name":"_b","type":"int"},{"name":"_c","type":"int"},{"name":"_d","type":"int"},{"name":"_e","type":"long"},{"name":"_f","type":"int"},{"name":"_g","type":"int"},{"name":"_h","type":"long"},{"name":"_i","type":"float"},{"name":"_j","type":"double"},{"name":"_k","type":"string"},{"name":"_l","type":"string"},{"name":"_m","type":"string"},{"name":"_n","type":{"type":"record","name":"Spam","fields":[{"name":"_field","type":"boolean","default":false}]}},{"name":"_o","type":{"type":"array","items":"boolean"}},{"name":"_p","type":{"type":"array","items":"int"}},{"name":"_p_alt","type":{"type":"array","items":{"type":"record","name":"Bar","fields":[{"name":"_field","type":"Bar"}]}}},{"name":"_q","type":{"type":"map","values":"string"}},{"name":"_r","type":["null","double"]},{"name":"_s","type":{"type":"fixed","name":"duration","size":12,"logicalType":"duration"}},{"name":"_t","type":{"type":"fixed","name":"uuid","size":16,"logicalType":"uuid"}},{"name":"_u","type":{"type":"fixed","name":"u64","size":8}},{"name":"_v","type":{"type":"fixed","name":"u128","size":16}},{"name":"_w","type":{"type":"fixed","name":"i128","size":16}},{"name":"_x","type":"Bar"}]}"# + r#"{"type":"record","name":"Foo","fields":[{"name":"_a","type":"boolean"},{"name":"_b","type":{"type":"record","name":"Spam","fields":[{"name":"_field","type":"boolean"}]}},{"name":"_c","type":"Spam"},{"name":"_d","type":{"type":"map","values":"string"}},{"name":"_e","type":["null","double"]}]}"# ); } @@ -2570,11 +2526,13 @@ fn avro_rs_476_field_default_provided() { default = r#""\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001""# )] _w: i128, + #[avro(default = r#"{"_field": false}"#)] + _x: Spam, } let schema = Foo::get_schema(); assert_eq!( serde_json::to_string(&schema).unwrap(), - r#"{"type":"record","name":"Foo","fields":[{"name":"_a","type":"boolean","default":true},{"name":"_b","type":"int","default":42},{"name":"_c","type":"int","default":42},{"name":"_d","type":"int","default":42},{"name":"_e","type":"long","default":42},{"name":"_f","type":"int","default":42},{"name":"_g","type":"int","default":42},{"name":"_h","type":"long","default":42},{"name":"_i","type":"float","default":42.0},{"name":"_j","type":"double","default":42.0},{"name":"_k","type":"string","default":"String"},{"name":"_l","type":"string","default":"str"},{"name":"_m","type":"string","default":"Z"},{"name":"_n","type":{"type":"record","name":"Spam","fields":[{"name":"_field","type":"boolean","default":false}]},"default":{"_field":false}},{"name":"_o","type":{"type":"array","items":"boolean"},"default":[true,false,true]},{"name":"_p","type":{"type":"array","items":"int"},"default":[1,2,3,4,5]},{"name":"_p_alt","type":{"type":"array","items":"Spam"},"default":[{"_field":true},{"_field":false},{"_field":true},{"_field":false},{"_field":true}]},{"name":"_q","type":{"type":"map","values":"string"},"default":{"A":"B"}},{"name":"_r","type":["null","double"],"default":42.0},{"name":"_s","type":{"type":"fixed","name":"duration","size":12,"logicalType":"duration"},"default":"\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001"},{"name":"_t","type":{"type":"fixed","name":"uuid","size":16,"logicalType":"uuid"},"default":"\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001"},{"name":"_u","type":{"type":"fixed","name":"u64","size":8},"default":"\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001"},{"name":"_v","type":{"type":"fixed","name":"u128","size":16},"default":"\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001"},{"name":"_w","type":{"type":"fixed","name":"i128","size":16},"default":"\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001"}]}"# + r#"{"type":"record","name":"Foo","fields":[{"name":"_a","type":"boolean","default":true},{"name":"_b","type":"int","default":42},{"name":"_c","type":"int","default":42},{"name":"_d","type":"int","default":42},{"name":"_e","type":"long","default":42},{"name":"_f","type":"int","default":42},{"name":"_g","type":"int","default":42},{"name":"_h","type":"long","default":42},{"name":"_i","type":"float","default":42.0},{"name":"_j","type":"double","default":42.0},{"name":"_k","type":"string","default":"String"},{"name":"_l","type":"string","default":"str"},{"name":"_m","type":"string","default":"Z"},{"name":"_n","type":{"type":"record","name":"Spam","fields":[{"name":"_field","type":"boolean"}]},"default":{"_field":false}},{"name":"_o","type":{"type":"array","items":"boolean"},"default":[true,false,true]},{"name":"_p","type":{"type":"array","items":"int"},"default":[1,2,3,4,5]},{"name":"_p_alt","type":{"type":"array","items":"Spam"},"default":[{"_field":true},{"_field":false},{"_field":true},{"_field":false},{"_field":true}]},{"name":"_q","type":{"type":"map","values":"string"},"default":{"A":"B"}},{"name":"_r","type":["null","double"],"default":42.0},{"name":"_s","type":{"type":"fixed","name":"duration","size":12,"logicalType":"duration"},"default":"\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001"},{"name":"_t","type":{"type":"fixed","name":"uuid","size":16,"logicalType":"uuid"},"default":"\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001"},{"name":"_u","type":{"type":"fixed","name":"u64","size":8},"default":"\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001"},{"name":"_v","type":{"type":"fixed","name":"u128","size":16},"default":"\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001"},{"name":"_w","type":{"type":"fixed","name":"i128","size":16},"default":"\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001"},{"name":"_x","type":"Spam","default":{"_field":false}}]}"# ); }