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 77531c6 refactor: Improve code structure of the derive code (#560)
77531c6 is described below
commit 77531c692565729142cac0741d3a384bc59d600c
Author: Kriskras99 <[email protected]>
AuthorDate: Tue Jun 23 21:07:10 2026 +0200
refactor: Improve code structure of the derive code (#560)
* refactor: Improve code structure of the derive code
This also changes the implementation of `utils::field_aliases` to remove
the unnecessary `try_from`.
* fix: Review feedback
---
avro_derive/src/attributes/serde.rs | 2 +-
avro_derive/src/fields.rs | 182 ++++++++++
avro_derive/src/lib.rs | 368 +--------------------
avro_derive/src/structs.rs | 185 +++++++++++
avro_derive/src/utils.rs | 67 ++++
.../avro_3709_record_field_attributes.expanded.rs | 14 +-
.../avro_rs_207_rename_all_attribute.expanded.rs | 8 +-
...name_attr_over_rename_all_attribute.expanded.rs | 8 +-
.../tests/expanded/avro_rs_501_basic.expanded.rs | 8 +-
.../expanded/avro_rs_501_namespace.expanded.rs | 8 +-
.../expanded/avro_rs_501_reference.expanded.rs | 8 +-
.../avro_rs_501_struct_with_optional.expanded.rs | 4 +-
.../tests/ui/avro_rs_397_with_expr_string.stderr | 2 +-
13 files changed, 474 insertions(+), 390 deletions(-)
diff --git a/avro_derive/src/attributes/serde.rs
b/avro_derive/src/attributes/serde.rs
index 6149752..69d594b 100644
--- a/avro_derive/src/attributes/serde.rs
+++ b/avro_derive/src/attributes/serde.rs
@@ -254,8 +254,8 @@ pub struct FieldAttributes {
#[cfg(test)]
mod tests {
use crate::{
- RenameRule,
attributes::serde::{ContainerAttributes, RenameAll, SerdeDefault},
+ case::RenameRule,
};
use darling::FromAttributes;
use syn::DeriveInput;
diff --git a/avro_derive/src/fields.rs b/avro_derive/src/fields.rs
new file mode 100644
index 0000000..f6bcb80
--- /dev/null
+++ b/avro_derive/src/fields.rs
@@ -0,0 +1,182 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+use crate::attributes::{FieldDefault, With};
+use proc_macro2::TokenStream;
+use quote::quote;
+use syn::spanned::Spanned;
+use syn::{Expr, Field, Type};
+
+pub fn field_to_schema_expr(field: &Field, with: With) -> Result<TokenStream,
Vec<syn::Error>> {
+ match with {
+ With::Trait => Ok(type_to_schema_expr(&field.ty)?),
+ With::Serde(path) => {
+ Ok(quote! { #path::get_schema_in_ctxt(named_schemas,
enclosing_namespace) })
+ }
+ With::Expr(Expr::Closure(closure)) => {
+ if closure.inputs.is_empty() {
+ Ok(quote! { (#closure)() })
+ } else {
+ Err(vec![syn::Error::new(
+ field.span(),
+ "Expected closure with 0 parameters",
+ )])
+ }
+ }
+ With::Expr(Expr::Path(path)) => Ok(quote! { #path(named_schemas,
enclosing_namespace) }),
+ With::Expr(_expr) => Err(vec![syn::Error::new(
+ field.span(),
+ "Invalid expression, expected a function or a closure",
+ )]),
+ }
+}
+
+pub fn field_to_record_fields_expr(
+ field: &Field,
+ with: With,
+) -> Result<TokenStream, Vec<syn::Error>> {
+ match with {
+ With::Trait => Ok(type_to_record_fields_expr(&field.ty)?),
+ With::Serde(path) => {
+ Ok(quote! { #path::get_record_fields_in_ctxt(named_schemas,
enclosing_namespace) })
+ }
+ With::Expr(Expr::Closure(closure)) => {
+ if closure.inputs.is_empty() {
+ Ok(quote! {
+ ::apache_avro::serde::get_record_fields_in_ctxt(
+ named_schemas,
+ enclosing_namespace,
+ |_, _| (#closure)(),
+ )
+ })
+ } else {
+ Err(vec![syn::Error::new(
+ field.span(),
+ "Expected closure with 0 parameters",
+ )])
+ }
+ }
+ With::Expr(Expr::Path(path)) => Ok(quote! {
+ ::apache_avro::serde::get_record_fields_in_ctxt(named_schemas,
enclosing_namespace, #path)
+ }),
+ With::Expr(_expr) => Err(vec![syn::Error::new(
+ field.span(),
+ "Invalid expression, expected a function or a closure",
+ )]),
+ }
+}
+
+pub fn field_to_field_default_expr(
+ field: &Field,
+ default: FieldDefault,
+) -> Result<TokenStream, Vec<syn::Error>> {
+ match default {
+ FieldDefault::Disabled => Ok(quote! { ::std::option::Option::None }),
+ FieldDefault::Trait => type_to_field_default_expr(&field.ty),
+ FieldDefault::Value(default_value) => {
+ let _: serde_json::Value =
serde_json::from_str(&default_value[..]).map_err(|e| {
+ vec![syn::Error::new(
+ field.span(),
+ format!("Invalid avro default json: \n{e}"),
+ )]
+ })?;
+ Ok(quote! {
+
::std::option::Option::Some(::serde_json::from_str(#default_value).expect("Unreachable!
This parsed successfully at compile time!"))
+ })
+ }
+ }
+}
+
+/// Takes in the Tokens of a type and returns the tokens of an expression with
return type `Schema`
+fn type_to_schema_expr(ty: &Type) -> Result<TokenStream, Vec<syn::Error>> {
+ match ty {
+ Type::Array(_) | Type::Slice(_) | Type::Path(_) | Type::Reference(_)
=> Ok(
+ quote! {<#ty as ::
apache_avro::AvroSchemaComponent>::get_schema_in_ctxt(named_schemas,
enclosing_namespace)},
+ ),
+ Type::Ptr(_) => Err(vec![syn::Error::new_spanned(
+ ty,
+ "AvroSchema: derive does not support raw pointers",
+ )]),
+ Type::Tuple(_) => Err(vec![syn::Error::new_spanned(
+ ty,
+ "AvroSchema: derive does not support tuples",
+ )]),
+ _ => Err(vec![syn::Error::new_spanned(
+ ty,
+ format!(
+ "AvroSchema: Unexpected type encountered! Please open an issue
if this kind of type should be supported: {ty:?}"
+ ),
+ )]),
+ }
+}
+
+fn type_to_record_fields_expr(ty: &Type) -> Result<TokenStream,
Vec<syn::Error>> {
+ match ty {
+ Type::Array(_) | Type::Slice(_) | Type::Path(_) | Type::Reference(_)
=> Ok(
+ quote! {<#ty as ::
apache_avro::AvroSchemaComponent>::get_record_fields_in_ctxt(named_schemas,
enclosing_namespace)},
+ ),
+ Type::Ptr(_) => Err(vec![syn::Error::new_spanned(
+ ty,
+ "AvroSchema: derive does not support raw pointers",
+ )]),
+ Type::Tuple(_) => Err(vec![syn::Error::new_spanned(
+ ty,
+ "AvroSchema: derive does not support tuples",
+ )]),
+ _ => Err(vec![syn::Error::new_spanned(
+ ty,
+ format!(
+ "AvroSchema: Unexpected type encountered! Please open an issue
if this kind of type should be supported: {ty:?}"
+ ),
+ )]),
+ }
+}
+
+fn type_to_field_default_expr(ty: &Type) -> Result<TokenStream,
Vec<syn::Error>> {
+ match ty {
+ Type::Array(_) | Type::Slice(_) | Type::Path(_) | Type::Reference(_)
=> {
+ Ok(quote! {<#ty as ::
apache_avro::AvroSchemaComponent>::field_default()})
+ }
+ Type::Ptr(_) => Err(vec![syn::Error::new_spanned(
+ ty,
+ "AvroSchema: derive does not support raw pointers",
+ )]),
+ Type::Tuple(_) => Err(vec![syn::Error::new_spanned(
+ ty,
+ "AvroSchema: derive does not support tuples",
+ )]),
+ _ => Err(vec![syn::Error::new_spanned(
+ ty,
+ format!(
+ "AvroSchema: Unexpected type encountered! Please open an issue
if this kind of type should be supported: {ty:?}"
+ ),
+ )]),
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use pretty_assertions::assert_eq;
+
+ #[test]
+ fn test_trait_cast() {
+
assert_eq!(type_to_schema_expr(&syn::parse2::<Type>(quote!{i32}).unwrap()).unwrap().to_string(),
quote!{<i32 as ::
apache_avro::AvroSchemaComponent>::get_schema_in_ctxt(named_schemas,
enclosing_namespace)}.to_string());
+
assert_eq!(type_to_schema_expr(&syn::parse2::<Type>(quote!{Vec<T>}).unwrap()).unwrap().to_string(),
quote!{<Vec<T> as ::
apache_avro::AvroSchemaComponent>::get_schema_in_ctxt(named_schemas,
enclosing_namespace)}.to_string());
+
assert_eq!(type_to_schema_expr(&syn::parse2::<Type>(quote!{AnyType}).unwrap()).unwrap().to_string(),
quote!{<AnyType as ::
apache_avro::AvroSchemaComponent>::get_schema_in_ctxt(named_schemas,
enclosing_namespace)}.to_string());
+ }
+}
diff --git a/avro_derive/src/lib.rs b/avro_derive/src/lib.rs
index d63ed8f..e2bc329 100644
--- a/avro_derive/src/lib.rs
+++ b/avro_derive/src/lib.rs
@@ -32,19 +32,18 @@
mod attributes;
mod case;
mod enums;
+mod fields;
+mod structs;
+mod utils;
-use proc_macro2::{Span, TokenStream};
+use proc_macro2::TokenStream;
use quote::quote;
-use syn::{
- DataStruct, DeriveInput, Expr, Field, Fields, Generics, Ident, Type,
parse_macro_input,
- spanned::Spanned,
-};
+use syn::{DeriveInput, Generics, Ident, parse_macro_input, spanned::Spanned};
+use crate::attributes::NamedTypeOptions;
use crate::enums::get_data_enum_schema_def;
-use crate::{
- attributes::{FieldDefault, FieldOptions, NamedTypeOptions, With},
- case::RenameRule,
-};
+use crate::structs::{get_struct_schema_def, get_transparent_struct_schema_def};
+use crate::utils::{aliases, preserve_optional, to_compile_errors};
#[proc_macro_derive(AvroSchema, attributes(avro, serde))]
// Templated from Serde
@@ -146,354 +145,3 @@ fn handle_named_schemas(full_schema_name: &str,
schema_def: &TokenStream) -> Tok
}
}
}
-
-/// Generate a schema definition for a struct.
-fn get_struct_schema_def(
- container_attrs: &NamedTypeOptions,
- data_struct: DataStruct,
- ident_span: Span,
-) -> Result<(TokenStream, TokenStream), Vec<syn::Error>> {
- let mut record_field_exprs = vec![];
- match data_struct.fields {
- Fields::Named(a) => {
- for field in a.named {
- let mut name = field
- .ident
- .as_ref()
- .expect("Field must have a name")
- .to_string();
- if let Some(raw_name) = name.strip_prefix("r#") {
- name = raw_name.to_string();
- }
- let field_attrs = FieldOptions::new(&field.attrs,
field.span())?;
- let doc = preserve_optional(field_attrs.doc);
- match (field_attrs.rename, container_attrs.rename_all) {
- (Some(rename), _) => {
- name = rename;
- }
- (None, rename_all) if rename_all != RenameRule::None => {
- name = rename_all.apply_to_field(&name);
- }
- _ => {}
- }
- if field_attrs.skip {
- continue;
- } else if field_attrs.flatten {
- // Inline the fields of the child record at runtime, as we
don't have access to
- // the schema here.
- let get_record_fields =
- get_field_get_record_fields_expr(&field,
field_attrs.with)?;
- record_field_exprs.push(quote! {
- if let Some(flattened_fields) = #get_record_fields {
- schema_fields.extend(flattened_fields);
- } else {
- panic!("{} does not have any fields to flatten
to", stringify!(#field));
- }
- });
-
- // Don't add this field as it's been replaced by the child
record fields
- continue;
- }
- let default_value = match field_attrs.default {
- FieldDefault::Disabled => quote! {
::std::option::Option::None },
- FieldDefault::Trait =>
type_to_field_default_expr(&field.ty)?,
- FieldDefault::Value(default_value) => {
- let _: serde_json::Value =
serde_json::from_str(&default_value[..])
- .map_err(|e| {
- vec![syn::Error::new(
- field.ident.span(),
- format!("Invalid avro default json:
\n{e}"),
- )]
- })?;
- quote! {
-
::std::option::Option::Some(::serde_json::from_str(#default_value).expect("Unreachable!
This parsed at compile time!"))
- }
- }
- };
- let aliases = field_aliases(&field_attrs.alias);
- let schema_expr = get_field_schema_expr(&field,
field_attrs.with)?;
- record_field_exprs.push(quote! {
- schema_fields.push(::apache_avro::schema::RecordField {
- name: #name.to_string(),
- doc: #doc,
- default: #default_value,
- aliases: #aliases,
- schema: #schema_expr,
- custom_attributes: ::std::collections::BTreeMap::new(),
- });
- });
- }
- }
- Fields::Unnamed(_) => {
- return Err(vec![syn::Error::new(
- ident_span,
- "AvroSchema derive does not work for tuple structs",
- )]);
- }
- Fields::Unit => {
- return Err(vec![syn::Error::new(
- ident_span,
- "AvroSchema derive does not work for unit structs",
- )]);
- }
- }
-
- let record_doc = preserve_optional(container_attrs.doc.as_ref());
- let record_aliases = aliases(&container_attrs.aliases);
- let full_schema_name = &container_attrs.name;
-
- // When flatten is involved, there will be more but we don't know how
many. This optimises for
- // the most common case where there is no flatten.
- let minimum_fields = record_field_exprs.len();
-
- let schema_def = quote! {
- {
- let mut schema_fields =
::std::vec::Vec::with_capacity(#minimum_fields);
- #(#record_field_exprs)*
- let schema_field_set: ::std::collections::HashSet<_> =
schema_fields.iter().map(|rf| &rf.name).collect();
- assert_eq!(schema_fields.len(), schema_field_set.len(), "Duplicate
field names found: {schema_fields:?}");
- let name =
::apache_avro::schema::Name::new(#full_schema_name).expect(&format!("Unable to
parse struct name for schema {}", #full_schema_name)[..]);
- let lookup: ::std::collections::BTreeMap<String, usize> =
schema_fields
- .iter()
- .enumerate()
- .map(|(position, field)| (field.name.to_owned(), position))
- .collect();
-
::apache_avro::schema::Schema::Record(::apache_avro::schema::RecordSchema {
- name,
- aliases: #record_aliases,
- doc: #record_doc,
- fields: schema_fields,
- lookup,
- attributes: ::std::collections::BTreeMap::new(),
- })
- }
- };
- let record_fields = quote! {
- let mut schema_fields =
::std::vec::Vec::with_capacity(#minimum_fields);
- #(#record_field_exprs)*
- ::std::option::Option::Some(schema_fields)
- };
-
- Ok((schema_def, record_fields))
-}
-
-/// Use the schema definition of the only field in the struct as the schema
-fn get_transparent_struct_schema_def(
- fields: Fields,
- input_span: Span,
-) -> Result<(TokenStream, TokenStream), Vec<syn::Error>> {
- match fields {
- Fields::Named(fields_named) => {
- let mut found = None;
- for field in fields_named.named {
- let attrs = FieldOptions::new(&field.attrs, field.span())?;
- if attrs.skip {
- continue;
- }
- if found.replace((field, attrs)).is_some() {
- return Err(vec![syn::Error::new(
- input_span,
- "AvroSchema: #[serde(transparent)] is only allowed on
structs with one unskipped field",
- )]);
- }
- }
-
- if let Some((field, attrs)) = found {
- Ok((
- get_field_schema_expr(&field, attrs.with.clone())?,
- get_field_get_record_fields_expr(&field, attrs.with)?,
- ))
- } else {
- Err(vec![syn::Error::new(
- input_span,
- "AvroSchema: #[serde(transparent)] is only allowed on
structs with one unskipped field",
- )])
- }
- }
- Fields::Unnamed(_) => Err(vec![syn::Error::new(
- input_span,
- "AvroSchema: derive does not work for tuple structs",
- )]),
- Fields::Unit => Err(vec![syn::Error::new(
- input_span,
- "AvroSchema: derive does not work for unit structs",
- )]),
- }
-}
-
-fn get_field_schema_expr(field: &Field, with: With) -> Result<TokenStream,
Vec<syn::Error>> {
- match with {
- With::Trait => Ok(type_to_schema_expr(&field.ty)?),
- With::Serde(path) => {
- Ok(quote! { #path::get_schema_in_ctxt(named_schemas,
enclosing_namespace) })
- }
- With::Expr(Expr::Closure(closure)) => {
- if closure.inputs.is_empty() {
- Ok(quote! { (#closure)() })
- } else {
- Err(vec![syn::Error::new(
- field.span(),
- "Expected closure with 0 parameters",
- )])
- }
- }
- With::Expr(Expr::Path(path)) => Ok(quote! { #path(named_schemas,
enclosing_namespace) }),
- With::Expr(_expr) => Err(vec![syn::Error::new(
- field.span(),
- "Invalid expression, expected function or closure",
- )]),
- }
-}
-
-fn get_field_get_record_fields_expr(
- field: &Field,
- with: With,
-) -> Result<TokenStream, Vec<syn::Error>> {
- match with {
- With::Trait => Ok(type_to_get_record_fields_expr(&field.ty)?),
- With::Serde(path) => {
- Ok(quote! { #path::get_record_fields_in_ctxt(named_schemas,
enclosing_namespace) })
- }
- With::Expr(Expr::Closure(closure)) => {
- if closure.inputs.is_empty() {
- Ok(quote! {
- ::apache_avro::serde::get_record_fields_in_ctxt(
- named_schemas,
- enclosing_namespace,
- |_, _| (#closure)(),
- )
- })
- } else {
- Err(vec![syn::Error::new(
- field.span(),
- "Expected closure with 0 parameters",
- )])
- }
- }
- With::Expr(Expr::Path(path)) => Ok(quote! {
- ::apache_avro::serde::get_record_fields_in_ctxt(named_schemas,
enclosing_namespace, #path)
- }),
- With::Expr(_expr) => Err(vec![syn::Error::new(
- field.span(),
- "Invalid expression, expected function or closure",
- )]),
- }
-}
-
-/// Takes in the Tokens of a type and returns the tokens of an expression with
return type `Schema`
-fn type_to_schema_expr(ty: &Type) -> Result<TokenStream, Vec<syn::Error>> {
- match ty {
- Type::Array(_) | Type::Slice(_) | Type::Path(_) | Type::Reference(_)
=> Ok(
- quote! {<#ty as ::
apache_avro::AvroSchemaComponent>::get_schema_in_ctxt(named_schemas,
enclosing_namespace)},
- ),
- Type::Ptr(_) => Err(vec![syn::Error::new_spanned(
- ty,
- "AvroSchema: derive does not support raw pointers",
- )]),
- Type::Tuple(_) => Err(vec![syn::Error::new_spanned(
- ty,
- "AvroSchema: derive does not support tuples",
- )]),
- _ => Err(vec![syn::Error::new_spanned(
- ty,
- format!(
- "AvroSchema: Unexpected type encountered! Please open an issue
if this kind of type should be supported: {ty:?}"
- ),
- )]),
- }
-}
-
-fn type_to_get_record_fields_expr(ty: &Type) -> Result<TokenStream,
Vec<syn::Error>> {
- match ty {
- Type::Array(_) | Type::Slice(_) | Type::Path(_) | Type::Reference(_)
=> Ok(
- quote! {<#ty as ::
apache_avro::AvroSchemaComponent>::get_record_fields_in_ctxt(named_schemas,
enclosing_namespace)},
- ),
- Type::Ptr(_) => Err(vec![syn::Error::new_spanned(
- ty,
- "AvroSchema: derive does not support raw pointers",
- )]),
- Type::Tuple(_) => Err(vec![syn::Error::new_spanned(
- ty,
- "AvroSchema: derive does not support tuples",
- )]),
- _ => Err(vec![syn::Error::new_spanned(
- ty,
- format!(
- "AvroSchema: Unexpected type encountered! Please open an issue
if this kind of type should be supported: {ty:?}"
- ),
- )]),
- }
-}
-
-fn type_to_field_default_expr(ty: &Type) -> Result<TokenStream,
Vec<syn::Error>> {
- match ty {
- Type::Array(_) | Type::Slice(_) | Type::Path(_) | Type::Reference(_)
=> {
- Ok(quote! {<#ty as ::
apache_avro::AvroSchemaComponent>::field_default()})
- }
- Type::Ptr(_) => Err(vec![syn::Error::new_spanned(
- ty,
- "AvroSchema: derive does not support raw pointers",
- )]),
- Type::Tuple(_) => Err(vec![syn::Error::new_spanned(
- ty,
- "AvroSchema: derive does not support tuples",
- )]),
- _ => Err(vec![syn::Error::new_spanned(
- ty,
- format!(
- "AvroSchema: Unexpected type encountered! Please open an issue
if this kind of type should be supported: {ty:?}"
- ),
- )]),
- }
-}
-
-/// Stolen from serde
-fn to_compile_errors(errors: impl AsRef<[syn::Error]>) ->
proc_macro2::TokenStream {
- let compile_errors =
errors.as_ref().iter().map(syn::Error::to_compile_error);
- quote!(#(#compile_errors)*)
-}
-
-fn preserve_optional(op: Option<impl quote::ToTokens>) -> TokenStream {
- if let Some(tt) = op {
- quote! {::std::option::Option::Some(#tt.into())}
- } else {
- quote! {::std::option::Option::None}
- }
-}
-
-fn aliases(op: &[impl quote::ToTokens]) -> TokenStream {
- let items: Vec<TokenStream> = op
- .iter()
- .map(|tt| quote! {#tt.try_into().expect("Alias is invalid")})
- .collect();
- if items.is_empty() {
- quote! {::std::option::Option::None}
- } else {
- quote! {::std::option::Option::Some(vec![#(#items),*])}
- }
-}
-
-fn field_aliases(op: &[impl quote::ToTokens]) -> TokenStream {
- let items: Vec<TokenStream> = op
- .iter()
- .map(|tt| quote! {#tt.try_into().expect("Alias is invalid")})
- .collect();
- if items.is_empty() {
- quote! {::std::vec::Vec::new()}
- } else {
- quote! {vec![#(#items),*]}
- }
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
- use pretty_assertions::assert_eq;
-
- #[test]
- fn test_trait_cast() {
-
assert_eq!(type_to_schema_expr(&syn::parse2::<Type>(quote!{i32}).unwrap()).unwrap().to_string(),
quote!{<i32 as ::
apache_avro::AvroSchemaComponent>::get_schema_in_ctxt(named_schemas,
enclosing_namespace)}.to_string());
-
assert_eq!(type_to_schema_expr(&syn::parse2::<Type>(quote!{Vec<T>}).unwrap()).unwrap().to_string(),
quote!{<Vec<T> as ::
apache_avro::AvroSchemaComponent>::get_schema_in_ctxt(named_schemas,
enclosing_namespace)}.to_string());
-
assert_eq!(type_to_schema_expr(&syn::parse2::<Type>(quote!{AnyType}).unwrap()).unwrap().to_string(),
quote!{<AnyType as ::
apache_avro::AvroSchemaComponent>::get_schema_in_ctxt(named_schemas,
enclosing_namespace)}.to_string());
- }
-}
diff --git a/avro_derive/src/structs.rs b/avro_derive/src/structs.rs
new file mode 100644
index 0000000..db51bd1
--- /dev/null
+++ b/avro_derive/src/structs.rs
@@ -0,0 +1,185 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+use crate::attributes::{FieldOptions, NamedTypeOptions};
+use crate::case::RenameRule;
+use crate::fields::{
+ field_to_field_default_expr, field_to_record_fields_expr,
field_to_schema_expr,
+};
+use crate::utils::{aliases, field_aliases, preserve_optional};
+use proc_macro2::{Span, TokenStream};
+use quote::quote;
+use syn::spanned::Spanned;
+use syn::{DataStruct, Fields};
+
+/// Generate a schema definition for a struct.
+pub fn get_struct_schema_def(
+ container_attrs: &NamedTypeOptions,
+ data_struct: DataStruct,
+ ident_span: Span,
+) -> Result<(TokenStream, TokenStream), Vec<syn::Error>> {
+ let mut record_field_exprs = vec![];
+ match data_struct.fields {
+ Fields::Named(a) => {
+ for field in a.named {
+ let mut name = field
+ .ident
+ .as_ref()
+ .expect("Field must have a name")
+ .to_string();
+ if let Some(raw_name) = name.strip_prefix("r#") {
+ name = raw_name.to_string();
+ }
+ let field_attrs = FieldOptions::new(&field.attrs,
field.span())?;
+ let doc = preserve_optional(field_attrs.doc);
+ match (field_attrs.rename, container_attrs.rename_all) {
+ (Some(rename), _) => {
+ name = rename;
+ }
+ (None, rename_all) if rename_all != RenameRule::None => {
+ name = rename_all.apply_to_field(&name);
+ }
+ _ => {}
+ }
+ if field_attrs.skip {
+ continue;
+ } else if field_attrs.flatten {
+ // Inline the fields of the child record at runtime, as we
don't have access to
+ // the schema here.
+ let get_record_fields =
field_to_record_fields_expr(&field, field_attrs.with)?;
+ record_field_exprs.push(quote! {
+ if let Some(flattened_fields) = #get_record_fields {
+ schema_fields.extend(flattened_fields);
+ } else {
+ panic!("{} does not have any fields to flatten
to", stringify!(#field));
+ }
+ });
+
+ // Don't add this field as it's been replaced by the child
record fields
+ continue;
+ }
+ let default_value = field_to_field_default_expr(&field,
field_attrs.default)?;
+ let aliases = field_aliases(&field_attrs.alias);
+ let schema_expr = field_to_schema_expr(&field,
field_attrs.with)?;
+ record_field_exprs.push(quote! {
+ schema_fields.push(::apache_avro::schema::RecordField {
+ name: #name.to_string(),
+ doc: #doc,
+ default: #default_value,
+ aliases: #aliases,
+ schema: #schema_expr,
+ custom_attributes: ::std::collections::BTreeMap::new(),
+ });
+ });
+ }
+ }
+ Fields::Unnamed(_) => {
+ return Err(vec![syn::Error::new(
+ ident_span,
+ "AvroSchema derive does not work for tuple structs",
+ )]);
+ }
+ Fields::Unit => {
+ return Err(vec![syn::Error::new(
+ ident_span,
+ "AvroSchema derive does not work for unit structs",
+ )]);
+ }
+ }
+
+ let record_doc = preserve_optional(container_attrs.doc.as_ref());
+ let record_aliases = aliases(&container_attrs.aliases);
+ let full_schema_name = &container_attrs.name;
+
+ // When flatten is involved, there will be more but we don't know how
many. This optimises for
+ // the most common case where there is no flatten.
+ let minimum_fields = record_field_exprs.len();
+
+ let schema_def = quote! {
+ {
+ let mut schema_fields =
::std::vec::Vec::with_capacity(#minimum_fields);
+ #(#record_field_exprs)*
+ let schema_field_set: ::std::collections::HashSet<_> =
schema_fields.iter().map(|rf| &rf.name).collect();
+ assert_eq!(schema_fields.len(), schema_field_set.len(), "Duplicate
field names found: {schema_fields:?}");
+ let name =
::apache_avro::schema::Name::new(#full_schema_name).expect(&format!("Unable to
parse struct name for schema {}", #full_schema_name)[..]);
+ let lookup: ::std::collections::BTreeMap<String, usize> =
schema_fields
+ .iter()
+ .enumerate()
+ .map(|(position, field)| (field.name.to_owned(), position))
+ .collect();
+
::apache_avro::schema::Schema::Record(::apache_avro::schema::RecordSchema {
+ name,
+ aliases: #record_aliases,
+ doc: #record_doc,
+ fields: schema_fields,
+ lookup,
+ attributes: ::std::collections::BTreeMap::new(),
+ })
+ }
+ };
+ let record_fields = quote! {
+ let mut schema_fields =
::std::vec::Vec::with_capacity(#minimum_fields);
+ #(#record_field_exprs)*
+ ::std::option::Option::Some(schema_fields)
+ };
+
+ Ok((schema_def, record_fields))
+}
+
+/// Use the schema definition of the only field in the struct as the schema
+pub fn get_transparent_struct_schema_def(
+ fields: Fields,
+ input_span: Span,
+) -> Result<(TokenStream, TokenStream), Vec<syn::Error>> {
+ match fields {
+ Fields::Named(fields_named) => {
+ let mut found = None;
+ for field in fields_named.named {
+ let attrs = FieldOptions::new(&field.attrs, field.span())?;
+ if attrs.skip {
+ continue;
+ }
+ if found.replace((field, attrs)).is_some() {
+ return Err(vec![syn::Error::new(
+ input_span,
+ "AvroSchema: #[serde(transparent)] is only allowed on
structs with one unskipped field",
+ )]);
+ }
+ }
+
+ if let Some((field, attrs)) = found {
+ Ok((
+ field_to_schema_expr(&field, attrs.with.clone())?,
+ field_to_record_fields_expr(&field, attrs.with)?,
+ ))
+ } else {
+ Err(vec![syn::Error::new(
+ input_span,
+ "AvroSchema: #[serde(transparent)] is only allowed on
structs with one unskipped field",
+ )])
+ }
+ }
+ Fields::Unnamed(_) => Err(vec![syn::Error::new(
+ input_span,
+ "AvroSchema: derive does not work for tuple structs",
+ )]),
+ Fields::Unit => Err(vec![syn::Error::new(
+ input_span,
+ "AvroSchema: derive does not work for unit structs",
+ )]),
+ }
+}
diff --git a/avro_derive/src/utils.rs b/avro_derive/src/utils.rs
new file mode 100644
index 0000000..6affff0
--- /dev/null
+++ b/avro_derive/src/utils.rs
@@ -0,0 +1,67 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+use proc_macro2::TokenStream;
+use quote::quote;
+
+/// Convert a list of errors into actual compiler errors.
+///
+/// Copied from `serde`, originally written by David Tolnay.
+pub fn to_compile_errors(errors: impl AsRef<[syn::Error]>) -> TokenStream {
+ let compile_errors =
errors.as_ref().iter().map(syn::Error::to_compile_error);
+ quote!(#(#compile_errors)*)
+}
+
+pub fn preserve_optional(op: Option<impl quote::ToTokens>) -> TokenStream {
+ if let Some(tt) = op {
+ quote! {::std::option::Option::Some(#tt.into())}
+ } else {
+ quote! {::std::option::Option::None}
+ }
+}
+
+/// Convert a list of strings to an expression that resolves to a
`Option<Vec<Alias>>`.
+pub fn aliases(op: &[impl AsRef<str>]) -> TokenStream {
+ let items: Vec<TokenStream> = op
+ .iter()
+ .map(|alias| {
+ let alias = alias.as_ref();
+ quote! {
+ ::apache_avro::schema::Alias::new(#alias).expect("Alias is
invalid")
+ }
+ })
+ .collect();
+ if items.is_empty() {
+ quote! {::std::option::Option::None}
+ } else {
+ quote! {::std::option::Option::Some(vec![#(#items),*])}
+ }
+}
+
+/// Convert a list of strings to an expression that resolves to a
`Vec<String>`.
+pub fn field_aliases(op: &[impl AsRef<str>]) -> TokenStream {
+ let items: Vec<TokenStream> = op
+ .iter()
+ .map(|string| {
+ let string = string.as_ref();
+ quote! {
+ ::std::string::String::from(#string)
+ }
+ })
+ .collect();
+ quote! {vec![#(#items),*]}
+}
diff --git
a/avro_derive/tests/expanded/avro_3709_record_field_attributes.expanded.rs
b/avro_derive/tests/expanded/avro_3709_record_field_attributes.expanded.rs
index 0d35f4b..e228d8c 100644
--- a/avro_derive/tests/expanded/avro_3709_record_field_attributes.expanded.rs
+++ b/avro_derive/tests/expanded/avro_3709_record_field_attributes.expanded.rs
@@ -31,14 +31,16 @@ impl ::apache_avro::AvroSchemaComponent for A {
doc: ::std::option::Option::Some("a doc".into()),
default: ::std::option::Option::Some(
::serde_json::from_str("123")
- .expect("Unreachable! This parsed at compile
time!"),
+ .expect(
+ "Unreachable! This parsed successfully at
compile time!",
+ ),
),
aliases:
::alloc::boxed::box_assume_init_into_vec_unsafe(
::alloc::intrinsics::write_box_via_move(
::alloc::boxed::Box::new_uninit(),
[
- "a1".try_into().expect("Alias is invalid"),
- "a2".try_into().expect("Alias is invalid"),
+ ::std::string::String::from("a1"),
+ ::std::string::String::from("a2"),
],
),
),
@@ -106,14 +108,14 @@ impl ::apache_avro::AvroSchemaComponent for A {
doc: ::std::option::Option::Some("a doc".into()),
default: ::std::option::Option::Some(
::serde_json::from_str("123")
- .expect("Unreachable! This parsed at compile time!"),
+ .expect("Unreachable! This parsed successfully at
compile time!"),
),
aliases: ::alloc::boxed::box_assume_init_into_vec_unsafe(
::alloc::intrinsics::write_box_via_move(
::alloc::boxed::Box::new_uninit(),
[
- "a1".try_into().expect("Alias is invalid"),
- "a2".try_into().expect("Alias is invalid"),
+ ::std::string::String::from("a1"),
+ ::std::string::String::from("a2"),
],
),
),
diff --git
a/avro_derive/tests/expanded/avro_rs_207_rename_all_attribute.expanded.rs
b/avro_derive/tests/expanded/avro_rs_207_rename_all_attribute.expanded.rs
index d80a114..f13c46c 100644
--- a/avro_derive/tests/expanded/avro_rs_207_rename_all_attribute.expanded.rs
+++ b/avro_derive/tests/expanded/avro_rs_207_rename_all_attribute.expanded.rs
@@ -29,7 +29,7 @@ impl ::apache_avro::AvroSchemaComponent for A {
name: "ITEM".to_string(),
doc: ::std::option::Option::None,
default: <i32 as
::apache_avro::AvroSchemaComponent>::field_default(),
- aliases: ::std::vec::Vec::new(),
+ aliases: ::alloc::vec::Vec::new(),
schema: <i32 as
::apache_avro::AvroSchemaComponent>::get_schema_in_ctxt(
named_schemas,
enclosing_namespace,
@@ -41,7 +41,7 @@ impl ::apache_avro::AvroSchemaComponent for A {
name: "DOUBLE_ITEM".to_string(),
doc: ::std::option::Option::None,
default: <i32 as
::apache_avro::AvroSchemaComponent>::field_default(),
- aliases: ::std::vec::Vec::new(),
+ aliases: ::alloc::vec::Vec::new(),
schema: <i32 as
::apache_avro::AvroSchemaComponent>::get_schema_in_ctxt(
named_schemas,
enclosing_namespace,
@@ -105,7 +105,7 @@ impl ::apache_avro::AvroSchemaComponent for A {
name: "ITEM".to_string(),
doc: ::std::option::Option::None,
default: <i32 as
::apache_avro::AvroSchemaComponent>::field_default(),
- aliases: ::std::vec::Vec::new(),
+ aliases: ::alloc::vec::Vec::new(),
schema: <i32 as
::apache_avro::AvroSchemaComponent>::get_schema_in_ctxt(
named_schemas,
enclosing_namespace,
@@ -117,7 +117,7 @@ impl ::apache_avro::AvroSchemaComponent for A {
name: "DOUBLE_ITEM".to_string(),
doc: ::std::option::Option::None,
default: <i32 as
::apache_avro::AvroSchemaComponent>::field_default(),
- aliases: ::std::vec::Vec::new(),
+ aliases: ::alloc::vec::Vec::new(),
schema: <i32 as
::apache_avro::AvroSchemaComponent>::get_schema_in_ctxt(
named_schemas,
enclosing_namespace,
diff --git
a/avro_derive/tests/expanded/avro_rs_207_rename_attr_over_rename_all_attribute.expanded.rs
b/avro_derive/tests/expanded/avro_rs_207_rename_attr_over_rename_all_attribute.expanded.rs
index 0689cb1..c26134c 100644
---
a/avro_derive/tests/expanded/avro_rs_207_rename_attr_over_rename_all_attribute.expanded.rs
+++
b/avro_derive/tests/expanded/avro_rs_207_rename_attr_over_rename_all_attribute.expanded.rs
@@ -30,7 +30,7 @@ impl ::apache_avro::AvroSchemaComponent for A {
name: "ITEM".to_string(),
doc: ::std::option::Option::None,
default: <i32 as
::apache_avro::AvroSchemaComponent>::field_default(),
- aliases: ::std::vec::Vec::new(),
+ aliases: ::alloc::vec::Vec::new(),
schema: <i32 as
::apache_avro::AvroSchemaComponent>::get_schema_in_ctxt(
named_schemas,
enclosing_namespace,
@@ -42,7 +42,7 @@ impl ::apache_avro::AvroSchemaComponent for A {
name: "DoubleItem".to_string(),
doc: ::std::option::Option::None,
default: <i32 as
::apache_avro::AvroSchemaComponent>::field_default(),
- aliases: ::std::vec::Vec::new(),
+ aliases: ::alloc::vec::Vec::new(),
schema: <i32 as
::apache_avro::AvroSchemaComponent>::get_schema_in_ctxt(
named_schemas,
enclosing_namespace,
@@ -106,7 +106,7 @@ impl ::apache_avro::AvroSchemaComponent for A {
name: "ITEM".to_string(),
doc: ::std::option::Option::None,
default: <i32 as
::apache_avro::AvroSchemaComponent>::field_default(),
- aliases: ::std::vec::Vec::new(),
+ aliases: ::alloc::vec::Vec::new(),
schema: <i32 as
::apache_avro::AvroSchemaComponent>::get_schema_in_ctxt(
named_schemas,
enclosing_namespace,
@@ -118,7 +118,7 @@ impl ::apache_avro::AvroSchemaComponent for A {
name: "DoubleItem".to_string(),
doc: ::std::option::Option::None,
default: <i32 as
::apache_avro::AvroSchemaComponent>::field_default(),
- aliases: ::std::vec::Vec::new(),
+ aliases: ::alloc::vec::Vec::new(),
schema: <i32 as
::apache_avro::AvroSchemaComponent>::get_schema_in_ctxt(
named_schemas,
enclosing_namespace,
diff --git a/avro_derive/tests/expanded/avro_rs_501_basic.expanded.rs
b/avro_derive/tests/expanded/avro_rs_501_basic.expanded.rs
index 34576a5..3562a9b 100644
--- a/avro_derive/tests/expanded/avro_rs_501_basic.expanded.rs
+++ b/avro_derive/tests/expanded/avro_rs_501_basic.expanded.rs
@@ -28,7 +28,7 @@ impl ::apache_avro::AvroSchemaComponent for A {
name: "a".to_string(),
doc: ::std::option::Option::None,
default: <i32 as
::apache_avro::AvroSchemaComponent>::field_default(),
- aliases: ::std::vec::Vec::new(),
+ aliases: ::alloc::vec::Vec::new(),
schema: <i32 as
::apache_avro::AvroSchemaComponent>::get_schema_in_ctxt(
named_schemas,
enclosing_namespace,
@@ -40,7 +40,7 @@ impl ::apache_avro::AvroSchemaComponent for A {
name: "b".to_string(),
doc: ::std::option::Option::None,
default: <String as
::apache_avro::AvroSchemaComponent>::field_default(),
- aliases: ::std::vec::Vec::new(),
+ aliases: ::alloc::vec::Vec::new(),
schema: <String as
::apache_avro::AvroSchemaComponent>::get_schema_in_ctxt(
named_schemas,
enclosing_namespace,
@@ -104,7 +104,7 @@ impl ::apache_avro::AvroSchemaComponent for A {
name: "a".to_string(),
doc: ::std::option::Option::None,
default: <i32 as
::apache_avro::AvroSchemaComponent>::field_default(),
- aliases: ::std::vec::Vec::new(),
+ aliases: ::alloc::vec::Vec::new(),
schema: <i32 as
::apache_avro::AvroSchemaComponent>::get_schema_in_ctxt(
named_schemas,
enclosing_namespace,
@@ -116,7 +116,7 @@ impl ::apache_avro::AvroSchemaComponent for A {
name: "b".to_string(),
doc: ::std::option::Option::None,
default: <String as
::apache_avro::AvroSchemaComponent>::field_default(),
- aliases: ::std::vec::Vec::new(),
+ aliases: ::alloc::vec::Vec::new(),
schema: <String as
::apache_avro::AvroSchemaComponent>::get_schema_in_ctxt(
named_schemas,
enclosing_namespace,
diff --git a/avro_derive/tests/expanded/avro_rs_501_namespace.expanded.rs
b/avro_derive/tests/expanded/avro_rs_501_namespace.expanded.rs
index ce84a5d..947173b 100644
--- a/avro_derive/tests/expanded/avro_rs_501_namespace.expanded.rs
+++ b/avro_derive/tests/expanded/avro_rs_501_namespace.expanded.rs
@@ -29,7 +29,7 @@ impl ::apache_avro::AvroSchemaComponent for A {
name: "a".to_string(),
doc: ::std::option::Option::None,
default: <i32 as
::apache_avro::AvroSchemaComponent>::field_default(),
- aliases: ::std::vec::Vec::new(),
+ aliases: ::alloc::vec::Vec::new(),
schema: <i32 as
::apache_avro::AvroSchemaComponent>::get_schema_in_ctxt(
named_schemas,
enclosing_namespace,
@@ -41,7 +41,7 @@ impl ::apache_avro::AvroSchemaComponent for A {
name: "b".to_string(),
doc: ::std::option::Option::None,
default: <String as
::apache_avro::AvroSchemaComponent>::field_default(),
- aliases: ::std::vec::Vec::new(),
+ aliases: ::alloc::vec::Vec::new(),
schema: <String as
::apache_avro::AvroSchemaComponent>::get_schema_in_ctxt(
named_schemas,
enclosing_namespace,
@@ -106,7 +106,7 @@ impl ::apache_avro::AvroSchemaComponent for A {
name: "a".to_string(),
doc: ::std::option::Option::None,
default: <i32 as
::apache_avro::AvroSchemaComponent>::field_default(),
- aliases: ::std::vec::Vec::new(),
+ aliases: ::alloc::vec::Vec::new(),
schema: <i32 as
::apache_avro::AvroSchemaComponent>::get_schema_in_ctxt(
named_schemas,
enclosing_namespace,
@@ -118,7 +118,7 @@ impl ::apache_avro::AvroSchemaComponent for A {
name: "b".to_string(),
doc: ::std::option::Option::None,
default: <String as
::apache_avro::AvroSchemaComponent>::field_default(),
- aliases: ::std::vec::Vec::new(),
+ aliases: ::alloc::vec::Vec::new(),
schema: <String as
::apache_avro::AvroSchemaComponent>::get_schema_in_ctxt(
named_schemas,
enclosing_namespace,
diff --git a/avro_derive/tests/expanded/avro_rs_501_reference.expanded.rs
b/avro_derive/tests/expanded/avro_rs_501_reference.expanded.rs
index 9916d4f..a7e9293 100644
--- a/avro_derive/tests/expanded/avro_rs_501_reference.expanded.rs
+++ b/avro_derive/tests/expanded/avro_rs_501_reference.expanded.rs
@@ -30,7 +30,7 @@ impl<'a> ::apache_avro::AvroSchemaComponent for A<'a> {
default: <&'a Vec<
i32,
> as
::apache_avro::AvroSchemaComponent>::field_default(),
- aliases: ::std::vec::Vec::new(),
+ aliases: ::alloc::vec::Vec::new(),
schema: <&'a Vec<
i32,
> as
::apache_avro::AvroSchemaComponent>::get_schema_in_ctxt(
@@ -44,7 +44,7 @@ impl<'a> ::apache_avro::AvroSchemaComponent for A<'a> {
name: "b".to_string(),
doc: ::std::option::Option::None,
default: <&'static str as
::apache_avro::AvroSchemaComponent>::field_default(),
- aliases: ::std::vec::Vec::new(),
+ aliases: ::alloc::vec::Vec::new(),
schema: <&'static str as
::apache_avro::AvroSchemaComponent>::get_schema_in_ctxt(
named_schemas,
enclosing_namespace,
@@ -110,7 +110,7 @@ impl<'a> ::apache_avro::AvroSchemaComponent for A<'a> {
default: <&'a Vec<
i32,
> as ::apache_avro::AvroSchemaComponent>::field_default(),
- aliases: ::std::vec::Vec::new(),
+ aliases: ::alloc::vec::Vec::new(),
schema: <&'a Vec<
i32,
> as ::apache_avro::AvroSchemaComponent>::get_schema_in_ctxt(
@@ -124,7 +124,7 @@ impl<'a> ::apache_avro::AvroSchemaComponent for A<'a> {
name: "b".to_string(),
doc: ::std::option::Option::None,
default: <&'static str as
::apache_avro::AvroSchemaComponent>::field_default(),
- aliases: ::std::vec::Vec::new(),
+ aliases: ::alloc::vec::Vec::new(),
schema: <&'static str as
::apache_avro::AvroSchemaComponent>::get_schema_in_ctxt(
named_schemas,
enclosing_namespace,
diff --git
a/avro_derive/tests/expanded/avro_rs_501_struct_with_optional.expanded.rs
b/avro_derive/tests/expanded/avro_rs_501_struct_with_optional.expanded.rs
index 076143e..e199f7c 100644
--- a/avro_derive/tests/expanded/avro_rs_501_struct_with_optional.expanded.rs
+++ b/avro_derive/tests/expanded/avro_rs_501_struct_with_optional.expanded.rs
@@ -29,7 +29,7 @@ impl ::apache_avro::AvroSchemaComponent for TestOptional {
default: <Option<
i32,
> as
::apache_avro::AvroSchemaComponent>::field_default(),
- aliases: ::std::vec::Vec::new(),
+ aliases: ::alloc::vec::Vec::new(),
schema: <Option<
i32,
> as
::apache_avro::AvroSchemaComponent>::get_schema_in_ctxt(
@@ -98,7 +98,7 @@ impl ::apache_avro::AvroSchemaComponent for TestOptional {
default: <Option<
i32,
> as ::apache_avro::AvroSchemaComponent>::field_default(),
- aliases: ::std::vec::Vec::new(),
+ aliases: ::alloc::vec::Vec::new(),
schema: <Option<
i32,
> as ::apache_avro::AvroSchemaComponent>::get_schema_in_ctxt(
diff --git a/avro_derive/tests/ui/avro_rs_397_with_expr_string.stderr
b/avro_derive/tests/ui/avro_rs_397_with_expr_string.stderr
index 9107607..1b150c4 100644
--- a/avro_derive/tests/ui/avro_rs_397_with_expr_string.stderr
+++ b/avro_derive/tests/ui/avro_rs_397_with_expr_string.stderr
@@ -1,4 +1,4 @@
-error: Invalid expression, expected function or closure
+error: Invalid expression, expected a function or a closure
--> tests/ui/avro_rs_397_with_expr_string.rs:22:5
|
22 | / #[avro(with = "Schema::Bytes")]