This is an automated email from the ASF dual-hosted git repository.
kriskras99 pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/avro-rs.git
The following commit(s) were added to refs/heads/main by this push:
new 62ad6dd feat: Allow types to provide default values (#477)
62ad6dd is described below
commit 62ad6dd37091140ceac6d30261fdda3b642e7573
Author: Kriskras99 <[email protected]>
AuthorDate: Wed Feb 25 11:16:53 2026 +0100
feat: Allow types to provide default values (#477)
* 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.
* fix: Apply suggestions from code review
Co-authored-by: Martin Grigorov <[email protected]>
* fix: Improve documentation
* fix: Only implement field default for `Option<T>`
* Use fully qualified names inside quote!()
* Remove overwrites of AvroSchemaComponent::field_default() doing the same
as the default implementation
* Add an IT test for serde skipped field without a default
* formatting
* clippy
---------
Co-authored-by: Martin Grigorov <[email protected]>
Co-authored-by: Martin Tzvetanov Grigorov <[email protected]>
---
avro/src/error.rs | 2 +-
avro/src/serde/derive.rs | 60 ++++++-
avro/src/types.rs | 7 +
avro_derive/src/attributes/avro.rs | 6 +-
avro_derive/src/attributes/mod.rs | 54 +++++-
avro_derive/src/lib.rs | 82 ++++++---
avro_derive/tests/derive.rs | 194 +++++++++++++++++++++
.../tests/ui/avro_rs_226_skip_serializing.rs | 1 +
.../tests/ui/avro_rs_226_skip_serializing.stderr | 5 +-
.../tests/ui/avro_rs_226_skip_serializing_if.rs | 1 +
.../ui/avro_rs_226_skip_serializing_if.stderr | 5 +-
11 files changed, 378 insertions(+), 39 deletions(-)
diff --git a/avro/src/error.rs b/avro/src/error.rs
index b26e5df..50a09af 100644
--- a/avro/src/error.rs
+++ b/avro/src/error.rs
@@ -21,7 +21,7 @@ use crate::{
};
use std::{error::Error as _, fmt};
-/// Errors encounterd by Avro.
+/// Errors encountered by Avro.
///
/// To inspect the details of the error use [`details`](Self::details) or
[`into_details`](Self::into_details)
/// to get a [`Details`] which contains more precise error information.
diff --git a/avro/src/serde/derive.rs b/avro/src/serde/derive.rs
index 8a6b432..f384fb3 100644
--- a/avro/src/serde/derive.rs
+++ b/avro/src/serde/derive.rs
@@ -81,6 +81,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 +117,22 @@ use std::collections::{HashMap, HashSet};
///
/// Set the `doc` attribute of the field. Defaults to the documentation of
the field.
///
-/// - `#[avro(default = "null")]`
+/// - `#[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. To remove the `default` attribute
for a field, set `default` to `false`: `#[avro(default = false)]`.
///
-/// Set the `default` attribute of the field.
+/// To override or set a default value, provide a JSON string:
///
-/// _Note:_ This is a JSON value not a Rust value, as this is put in the
schema itself.
+/// - 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")]`
///
@@ -220,6 +235,11 @@ pub trait AvroSchema {
/// fn get_record_fields_in_ctxt(_: usize, _: &mut HashSet<Name>, _:
&Namespace) -> Option<Vec<RecordField>> {
/// None // A Schema::Int is not a Schema::Record so there are no
fields to return
/// }
+///
+/// fn field_default() -> Option<serde_json::Value> {
+/// // Zero as default value. Can also be None if you don't want to
provide a default value
+/// Some(0u8.into())
+/// }
///}
/// ```
///
@@ -242,6 +262,10 @@ pub trait AvroSchema {
/// fn get_record_fields_in_ctxt(first_field_position: usize,
named_schemas: &mut HashSet<Name>, enclosing_namespace: &Namespace) ->
Option<Vec<RecordField>> {
/// T::get_record_fields_in_ctxt(first_field_position, named_schemas,
enclosing_namespace)
/// }
+///
+/// fn field_default() -> Option<serde_json::Value> {
+/// T::field_default()
+/// }
///}
/// ```
///
@@ -256,6 +280,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 +330,11 @@ pub trait AvroSchema {
/// .build(),
/// ])
/// }
+///
+/// fn field_default() -> Option<serde_json::Value> {
+/// // This type does not provide a default value
+/// None
+/// }
///}
/// ```
pub trait AvroSchemaComponent {
@@ -332,6 +362,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 return `None`
except for `Option<T>`
+ /// which returns `Some(serde_json::Value::Null)`.
+ fn field_default() -> Option<serde_json::Value> {
+ None
+ }
}
/// Get the record fields from `schema_fn` without polluting `named_schemas`
or causing duplicate names
@@ -515,6 +555,10 @@ macro_rules! impl_passthrough_schema (
fn get_record_fields_in_ctxt(first_field_position: usize,
named_schemas: &mut HashSet<Name>, enclosing_namespace: &Namespace) ->
Option<Vec<RecordField>> {
T::get_record_fields_in_ctxt(first_field_position,
named_schemas, enclosing_namespace)
}
+
+ fn field_default() -> Option<serde_json::Value> {
+ T::field_default()
+ }
}
);
);
@@ -609,6 +653,10 @@ where
) -> Option<Vec<RecordField>> {
None
}
+
+ fn field_default() -> Option<serde_json::Value> {
+ Some(serde_json::Value::Null)
+ }
}
impl AvroSchemaComponent for core::time::Duration {
@@ -782,8 +830,10 @@ impl AvroSchemaComponent for i128 {
#[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 6816599..fafea3f 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 1116267..eb11b9e 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<String>,
}
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<String>,
+ 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 12f8b20..27c1cbc 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<String>,
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 `<T as AvroSchemaComponent>::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<Self> {
+ Ok(Self::Value(value.to_string()))
+ }
+
+ fn from_bool(value: bool) -> darling::Result<Self> {
+ 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<String>,
- pub default: Option<String>,
+ pub default: FieldDefault,
pub alias: Vec<String>,
pub rename: Option<String>,
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,
- r#"`#[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 df14b97..d6562c8 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<TokenStream, Vec<syn::Error>
&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<TokenStream, Vec<syn::Error>
&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
+ }
}
}
}
@@ -127,9 +134,9 @@ fn create_trait_definition(
/// Generate the code to check `named_schemas` if this schema already exist
fn handle_named_schemas(full_schema_name: String, schema_def: TokenStream) ->
TokenStream {
quote! {
- let name =
apache_avro::schema::Name::new(#full_schema_name).expect(concat!("Unable to
parse schema name ",
#full_schema_name)).fully_qualified_name(enclosing_namespace);
+ let name =
::apache_avro::schema::Name::new(#full_schema_name).expect(concat!("Unable to
parse schema name ",
#full_schema_name)).fully_qualified_name(enclosing_namespace);
if named_schemas.contains(&name) {
- apache_avro::schema::Schema::Ref{name}
+ ::apache_avro::schema::Schema::Ref{name}
} else {
let enclosing_namespace = &name.namespace;
named_schemas.insert(name.clone());
@@ -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(
@@ -196,10 +205,9 @@ fn get_struct_schema_def(
)]
})?;
quote! {
-
Some(serde_json::from_str(#default_value).expect(format!("Invalid JSON: {:?}",
#default_value).as_str()))
+
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)?;
@@ -247,12 +255,12 @@ fn get_struct_schema_def(
#(#record_field_exprs)*
let schema_field_set: ::std::collections::HashSet<_> =
schema_fields.iter().map(|rf| &rf.name).collect();
assert_eq!(schema_fields.len(), schema_field_set.len(), "Duplicate
field names found: {schema_fields:?}");
- let name =
apache_avro::schema::Name::new(#full_schema_name).expect(&format!("Unable to
parse struct name for schema {}", #full_schema_name)[..]);
+ let name =
::apache_avro::schema::Name::new(#full_schema_name).expect(&format!("Unable to
parse struct name for schema {}", #full_schema_name)[..]);
let lookup: std::collections::BTreeMap<String, usize> =
schema_fields
.iter()
.map(|field| (field.name.to_owned(), field.position))
.collect();
-
apache_avro::schema::Schema::Record(apache_avro::schema::RecordSchema {
+
::apache_avro::schema::Schema::Record(apache_avro::schema::RecordSchema {
name,
aliases: #record_aliases,
doc: #record_doc,
@@ -400,8 +408,8 @@ fn get_data_enum_schema_def(
}
let full_schema_name = &container_attrs.name;
Ok(quote! {
- apache_avro::schema::Schema::Enum(apache_avro::schema::EnumSchema {
- name:
apache_avro::schema::Name::new(#full_schema_name).expect(&format!("Unable to
parse enum name for schema {}", #full_schema_name)[..]),
+
::apache_avro::schema::Schema::Enum(apache_avro::schema::EnumSchema {
+ name:
::apache_avro::schema::Name::new(#full_schema_name).expect(&format!("Unable to
parse enum name for schema {}", #full_schema_name)[..]),
aliases: #enum_aliases,
doc: #doc,
symbols: vec![#(#symbols.to_owned()),*],
@@ -421,7 +429,7 @@ fn get_data_enum_schema_def(
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)},
+ quote! {<#ty as ::
apache_avro::AvroSchemaComponent>::get_schema_in_ctxt(named_schemas,
enclosing_namespace)},
),
Type::Ptr(_) => Err(vec![syn::Error::new_spanned(
ty,
@@ -443,7 +451,7 @@ fn type_to_schema_expr(ty: &Type) -> Result<TokenStream,
Vec<syn::Error>> {
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(field_position,
named_schemas, enclosing_namespace)},
+ quote! {<#ty as ::
apache_avro::AvroSchemaComponent>::get_record_fields_in_ctxt(field_position,
named_schemas, enclosing_namespace)},
),
Type::Ptr(_) => Err(vec![syn::Error::new_spanned(
ty,
@@ -462,6 +470,28 @@ fn type_to_get_record_fields_expr(ty: &Type) ->
Result<TokenStream, Vec<syn::Err
}
}
+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:?}"
+ ),
+ )]),
+ }
+}
+
fn default_enum_variant(
data_enum: &syn::DataEnum,
error_span: Span,
@@ -631,16 +661,16 @@ mod tests {
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("Basic")
+ let name =
::apache_avro::schema::Name::new("Basic")
.expect(concat!("Unable to parse schema name
", "Basic"))
.fully_qualified_name(enclosing_namespace);
if named_schemas.contains(&name) {
- apache_avro::schema::Schema::Ref { 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("Basic").expect(
+
::apache_avro::schema::Schema::Enum(apache_avro::schema::EnumSchema {
+ name:
::apache_avro::schema::Name::new("Basic").expect(
&format!("Unable to parse enum name
for schema {}", "Basic")[..]
),
aliases: None,
@@ -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());
}
@@ -772,9 +806,9 @@ mod tests {
#[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());
+
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());
}
#[test]
@@ -790,7 +824,7 @@ mod tests {
match syn::parse2::<DeriveInput>(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_qualifi [...]
+ 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_qual [...]
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::<DeriveInput>(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_qualifi [...]
+ 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_qual [...]
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::<DeriveInput>(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_qualifi [...]
+ 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_qual [...]
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::<DeriveInput>(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_qualifi [...]
+ 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_qual [...]
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::<DeriveInput>(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_qualifi [...]
+ 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_qual [...]
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 c9f5d44..4b3c5cf 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;
@@ -1372,6 +1374,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<String, i32>,
@@ -1945,6 +1948,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
}
}
@@ -2381,3 +2385,193 @@ fn avro_rs_448_flatten_field_positions() {
.collect::<Vec<_>>();
assert_eq!(positions.as_slice(), &[0, 1, 2, 3][..]);
}
+
+#[test]
+fn avro_rs_476_field_default() {
+ #[derive(AvroSchema)]
+ struct Bar {
+ _field: Box<Bar>,
+ }
+
+ #[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<str>,
+ _m: char,
+ _n: Box<Spam>,
+ _o: Vec<bool>,
+ _p: [u8; 5],
+ _p_alt: [Bar; 5],
+ _q: HashMap<String, String>,
+ _r: Option<f64>,
+ _s: Duration,
+ _t: Uuid,
+ _u: u64,
+ _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"},{"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":"
[...]
+ );
+}
+
+#[test]
+fn avro_rs_476_field_default_false() {
+ #[derive(AvroSchema)]
+ #[avro(default = r#"{"_field": true}"#)]
+ struct Spam {
+ _field: bool,
+ }
+
+ #[derive(AvroSchema)]
+ struct Foo {
+ #[avro(default = false)]
+ _a: bool,
+ #[avro(default = false)]
+ _b: Spam,
+ #[avro(default = false)]
+ _c: Box<Spam>,
+ #[avro(default = false)]
+ _d: HashMap<String, String>,
+ #[avro(default = false)]
+ _e: Option<f64>,
+ }
+
+ 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":{"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"]}]}"#
+ );
+}
+
+#[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<str>,
+ #[avro(default = r#""Z""#)]
+ _m: char,
+ #[avro(default = r#"{"_field": false}"#)]
+ _n: Box<Spam>,
+ #[avro(default = "[true, false, true]")]
+ _o: Vec<bool>,
+ #[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<String, String>,
+ #[avro(default = "42.0")]
+ _r: Option<f64>,
+ #[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,
+ #[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":"str
[...]
+ );
+}
+
+#[test]
+fn avro_rs_476_skip_serializing_fielddefault_trait_none() {
+ #[derive(AvroSchema, Debug, Deserialize, Serialize)]
+ struct T {
+ x: Option<i8>,
+ #[serde(skip_serializing)]
+ // no usage of #[avro(default = ...)], so FieldDefault::Trait will be
used
+ // with AvroSchemaComponent::field_default == None
+ _y: i8,
+ }
+
+ let schema = T::get_schema();
+ assert_eq!(
+ serde_json::to_string(&schema).unwrap(),
+
r#"{"type":"record","name":"T","fields":[{"name":"x","type":["null","int"],"default":null},{"name":"_y","type":"int"}]}"#
+ );
+
+ let t = T { x: Some(1), _y: 2 };
+
+ let mut writer = Writer::new(&schema, Vec::new()).unwrap();
+ match writer.append_ser(t) {
+ Ok(_) => panic!(
+ "The serialization should have failed due to the missing `default`
value for the `_y` field"
+ ),
+ Err(e) => match e.into_details() {
+ apache_avro::error::Details::MissingDefaultForSkippedField {
field_name, .. }
+ if field_name == "_y" => {}
+ d => panic!("Unexpected error: {d:?}"),
+ },
+ }
+}
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 40f4756..28a7395 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<i8>,
y: Option<String>,
#[serde(skip_serializing)]
+ #[avro(default = false)]
z: Option<i8>,
}
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 e974f85..d5316b5 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<i8>,
+25 | | #[avro(default = false)]
+26 | | z: Option<i8>,
| |_________________^
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 2e87a9c..e532066 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<i8>,
#[serde(skip_serializing_if = "Option::is_none")]
+ #[avro(default = false)]
y: Option<String>,
z: Option<i8>,
}
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 9794bfb..f3094d0 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<String>,
+24 | | #[avro(default = false)]
+25 | | y: Option<String>,
| |_____________________^