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 791e77f refactor: Centralize generating the actual `impl` in the
`Implementation` struct (#561)
791e77f is described below
commit 791e77fc227f65d35234eb6f45017b0049b6857a
Author: Kriskras99 <[email protected]>
AuthorDate: Fri Jun 26 10:45:24 2026 +0200
refactor: Centralize generating the actual `impl` in the `Implementation`
struct (#561)
* refactor: Centralize generating the actual `impl` in the `Implementation`
struct
This makes it clearer how everything fits together, and allows extending
shared
logic further (for example, adding generated tests). And because it is not
a `TokenStream`, accidentally returning only a schema expression instead of
the
entire implementation results in a compilation error.
* fix: Review feedback
- Only allow the default attribute on the field of a transparent struct
- Improve the plain enum schema generation
- Skipped variants are now actually skipped
- The container default attribute is used first, then comes the variant
default attribute
- `serde_json::Value`s are now constructed instead of parsed at runtime
* fix: Match the plain enum check in the implementation
---
avro/src/serde/ser_schema/mod.rs | 6 +-
avro_derive/src/attributes/mod.rs | 40 +++---
avro_derive/src/attributes/serde.rs | 6 +-
avro_derive/src/enums/mod.rs | 72 +++++------
avro_derive/src/enums/plain.rs | 143 ++++++++++++++++-----
avro_derive/src/fields.rs | 14 +-
avro_derive/src/implementation.rs | 114 ++++++++++++++++
avro_derive/src/lib.rs | 99 +++-----------
avro_derive/src/structs.rs | 82 ++++++++----
avro_derive/src/utils.rs | 63 +++++++++
avro_derive/tests/derive.rs | 15 +++
.../avro_3687_basic_enum_with_default.expanded.rs | 4 +-
.../avro_3709_record_field_attributes.expanded.rs | 10 +-
avro_derive/tests/serde.rs | 2 +-
.../avro_3687_basic_enum_with_default_twice.stderr | 6 -
.../tests/ui/avro_rs_373_rename_all_fields.stderr | 11 +-
.../tests/ui/avro_rs_501_non_basic_enum.stderr | 11 +-
.../tests/ui/avro_rs_501_tuple_struct.stderr | 4 +-
.../tests/ui/avro_rs_501_unit_struct.stderr | 4 +-
.../tests/ui/avro_rs_561_transparent_default.rs | 27 ++++
.../ui/avro_rs_561_transparent_default.stderr | 9 ++
21 files changed, 509 insertions(+), 233 deletions(-)
diff --git a/avro/src/serde/ser_schema/mod.rs b/avro/src/serde/ser_schema/mod.rs
index 337268a..5aba68b 100644
--- a/avro/src/serde/ser_schema/mod.rs
+++ b/avro/src/serde/ser_schema/mod.rs
@@ -435,8 +435,12 @@ impl<'s, 'w, W: Write, S: Borrow<Schema>> Serializer for
SchemaAwareSerializer<'
match self.schema {
Schema::Enum(enum_schema) => {
// Plain enum
- if variant.as_ptr() == SERIALIZING_SCHEMA_DEFAULT.as_ptr() ||
enum_schema.symbols[variant_index as usize] == variant {
+ if variant.as_ptr() == SERIALIZING_SCHEMA_DEFAULT.as_ptr() ||
enum_schema.symbols.get(variant_index as usize).map(String::as_str) ==
Some(variant) {
+ // Fast path for when the index matches our symbol index
zig_i32(variant_index as i32, &mut *self.writer)
+ } else if let Some((index, _)) =
enum_schema.symbols.iter().enumerate().find(|(_, s)| s.as_str() == variant) {
+ // Slow path for when `#[serde(skip)]` is used
+ zig_i32(index as i32, &mut *self.writer)
} else {
Err(self.error("unit variant", format!(r#"Expected symbol
"{variant}" at index {variant_index} in enum"#)))
}
diff --git a/avro_derive/src/attributes/mod.rs
b/avro_derive/src/attributes/mod.rs
index e68bbbb..24e71f9 100644
--- a/avro_derive/src/attributes/mod.rs
+++ b/avro_derive/src/attributes/mod.rs
@@ -17,8 +17,8 @@
use crate::case::RenameRule;
use darling::{FromAttributes, FromMeta};
-use proc_macro2::{Span, TokenStream};
-use quote::quote;
+use proc_macro2::Span;
+use serde_json::Value;
use syn::{AttrStyle, Attribute, Expr, Ident, Path, spanned::Spanned};
mod avro;
@@ -31,7 +31,7 @@ pub struct NamedTypeOptions {
pub aliases: Vec<String>,
pub rename_all: RenameRule,
pub transparent: bool,
- pub default: TokenStream,
+ pub default: Option<Value>,
}
impl NamedTypeOptions {
@@ -88,6 +88,7 @@ impl NamedTypeOptions {
|| avro.name.is_some()
|| avro.namespace.is_some()
|| avro.doc.is_some()
+ || avro.default.is_some()
|| !avro.alias.is_empty()
|| avro.rename_all != RenameRule::None
|| serde.rename_all.serialize != RenameRule::None
@@ -112,20 +113,15 @@ 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()))
- }
- }
+ let default = if let Some(default_value) = avro.default {
+ Some(serde_json::from_str(default_value.as_str()).map_err(|e| {
+ vec![syn::Error::new(
+ ident.span(),
+ format!("Invalid Avro `default` JSON: \n{e}"),
+ )]
+ })?)
+ } else {
+ None
};
Ok(Self {
@@ -141,6 +137,7 @@ impl NamedTypeOptions {
pub struct VariantOptions {
pub rename: Option<String>,
+ pub skip: bool,
}
impl VariantOptions {
@@ -177,6 +174,9 @@ impl VariantOptions {
Ok(Self {
rename: serde.rename,
+ // For variants we don't care about defaults for skipping, as
Serde will error if a skipped
+ // variant is serialized or deserialized.
+ skip: serde.skip || (serde.skip_serializing &&
serde.skip_deserializing),
})
}
}
@@ -232,12 +232,14 @@ pub enum FieldDefault {
/// Don't set a default.
Disabled,
/// Use this JSON value.
- Value(String),
+ Value(Value),
}
impl FromMeta for FieldDefault {
fn from_string(value: &str) -> darling::Result<Self> {
- Ok(Self::Value(value.to_string()))
+ Ok(Self::Value(serde_json::from_str(value).map_err(|e| {
+ darling::Error::custom(format!("Failed to parse field default:
{e:?}"))
+ })?))
}
fn from_bool(value: bool) -> darling::Result<Self> {
diff --git a/avro_derive/src/attributes/serde.rs
b/avro_derive/src/attributes/serde.rs
index 69d594b..f53efd4 100644
--- a/avro_derive/src/attributes/serde.rs
+++ b/avro_derive/src/attributes/serde.rs
@@ -163,13 +163,13 @@ pub struct VariantAttributes {
pub _rename_all: RenameAll,
/// Do not serialize or deserialize this variant.
#[darling(default, rename = "skip")]
- pub _skip: bool,
+ pub skip: bool,
/// Do not serialize this variant.
#[darling(default, rename = "skip_serializing")]
- pub _skip_serializing: bool,
+ pub skip_serializing: bool,
/// Do not deserialize this variant.
#[darling(default, rename = "skip_deserializing")]
- pub _skip_deserializing: bool,
+ pub skip_deserializing: bool,
/// Use this function for serializing.
#[darling(rename = "serialize_with")]
pub _serialize_with: Option<String>,
diff --git a/avro_derive/src/enums/mod.rs b/avro_derive/src/enums/mod.rs
index 1511f65..6425745 100644
--- a/avro_derive/src/enums/mod.rs
+++ b/avro_derive/src/enums/mod.rs
@@ -17,51 +17,43 @@
mod plain;
-use crate::attributes::NamedTypeOptions;
-use proc_macro2::{Ident, Span, TokenStream};
-use syn::{Attribute, DataEnum, Fields, Meta};
+use crate::attributes::{NamedTypeOptions, VariantOptions};
+use crate::implementation::Implementation;
+use proc_macro2::{Ident, Span};
+use syn::spanned::Spanned;
+use syn::{DataEnum, Fields, Generics};
/// Generate a schema definition for a enum.
-pub fn get_data_enum_schema_def(
- container_attrs: &NamedTypeOptions,
- data_enum: &DataEnum,
- ident_span: Span,
-) -> Result<TokenStream, Vec<syn::Error>> {
- if data_enum.variants.iter().all(|v| Fields::Unit == v.fields) {
- plain::schema_def(container_attrs, data_enum, ident_span)
- } else {
- Err(vec![syn::Error::new(
- ident_span,
- "AvroSchema: derive does not work for enums with non unit structs",
- )])
+pub fn to_implementation(
+ input_span: Span,
+ ident: Ident,
+ generics: Generics,
+ container_attrs: NamedTypeOptions,
+ data: DataEnum,
+) -> Result<Implementation, Vec<syn::Error>> {
+ if container_attrs.transparent {
+ return Err(vec![syn::Error::new(
+ input_span,
+ "AvroSchema: `#[serde(transparent)]` is only supported on structs",
+ )]);
}
-}
-
-fn default_enum_variant(
- data_enum: &DataEnum,
- error_span: Span,
-) -> Result<Option<String>, Vec<syn::Error>> {
- match data_enum
+ if data
.variants
.iter()
- .filter(|v| v.attrs.iter().any(is_default_attr))
- .collect::<Vec<_>>()
+ .filter(|v| {
+ // Filter skipped variants, if the attributes fail to parse we'll
also filter them out
+ // `plain` will throw proper errors for that.
+ VariantOptions::new(&v.attrs, v.span())
+ .map(|o| !o.skip)
+ .unwrap_or(false)
+ })
+ .all(|v| Fields::Unit == v.fields)
{
- variants if variants.is_empty() => Ok(None),
- single if single.len() == 1 => Ok(Some(single[0].ident.to_string())),
- multiple => Err(vec![syn::Error::new(
- error_span,
- format!(
- "Multiple defaults defined: {:?}",
- multiple
- .iter()
- .map(|v| v.ident.to_string())
- .collect::<Vec<String>>()
- ),
- )]),
+ plain::to_implementation(input_span, ident, generics, container_attrs,
data)
+ } else {
+ Err(vec![syn::Error::new(
+ input_span,
+ "AvroSchema: derive does not work for enums with non unit structs",
+ )])
}
}
-
-fn is_default_attr(attr: &Attribute) -> bool {
- matches!(attr, Attribute { meta: Meta::Path(path), .. } if
path.get_ident().map(Ident::to_string).as_deref() == Some("default"))
-}
diff --git a/avro_derive/src/enums/plain.rs b/avro_derive/src/enums/plain.rs
index 4fb0adb..8270e0f 100644
--- a/avro_derive/src/enums/plain.rs
+++ b/avro_derive/src/enums/plain.rs
@@ -17,49 +17,128 @@
use crate::attributes::{NamedTypeOptions, VariantOptions};
use crate::case::RenameRule;
-use crate::enums::default_enum_variant;
+use crate::implementation::Implementation;
+use crate::utils::json_value_expr;
use crate::{aliases, preserve_optional};
-use proc_macro2::{Span, TokenStream};
+use proc_macro2::{Ident, Span};
use quote::quote;
+use serde_json::Value;
use syn::spanned::Spanned;
-use syn::{DataEnum, Fields};
+use syn::{Attribute, DataEnum, Fields, Generics, Meta};
-pub fn schema_def(
- container_attrs: &NamedTypeOptions,
- data_enum: &DataEnum,
- ident_span: Span,
-) -> Result<TokenStream, Vec<syn::Error>> {
+pub fn to_implementation(
+ input_span: Span,
+ ident: Ident,
+ generics: Generics,
+ container_attrs: NamedTypeOptions,
+ data: DataEnum,
+) -> Result<Implementation, Vec<syn::Error>> {
let doc = preserve_optional(container_attrs.doc.as_ref());
let enum_aliases = aliases(&container_attrs.aliases);
- if data_enum.variants.iter().all(|v| Fields::Unit == v.fields) {
- let default_value = default_enum_variant(data_enum, ident_span)?;
- let default = preserve_optional(default_value);
- let mut symbols = Vec::new();
- for variant in &data_enum.variants {
- let field_attrs = VariantOptions::new(&variant.attrs,
variant.span())?;
- let name = match (field_attrs.rename, container_attrs.rename_all) {
- (Some(rename), _) => rename,
+
+ let mut errors = Vec::new();
+ let mut symbols = Vec::new();
+ let mut default = match &container_attrs.default {
+ None => None,
+ Some(Value::String(s)) => Some(s.clone()),
+ Some(_) => {
+ errors.push(syn::Error::new(
+ input_span,
+ "Expected a JSON string for an enum default",
+ ));
+ None
+ }
+ };
+
+ for variant in data.variants {
+ let variant_attrs = match VariantOptions::new(&variant.attrs,
variant.span()) {
+ Ok(attrs) => attrs,
+ Err(errs) => {
+ errors.extend(errs);
+ continue;
+ }
+ };
+
+ // Check the skip attribute before the check if it is a unit variant,
because a skipped (un)named
+ // variant is not a problem
+ if variant_attrs.skip {
+ continue;
+ }
+
+ if !matches!(variant.fields, Fields::Unit) {
+ errors.push(syn::Error::new(
+ variant.span(),
+ "AvroSchema: derive does not work for enums with non unit
structs",
+ ));
+ continue;
+ }
+
+ // When a variant has the `#[default]` attribute and we don't have a
default yet, we assign
+ // it as the default. If an enum has multiple variants with the
default attribute, the compiler
+ // throws an error anyway so we can ignore that situation. If the
default attribute is on a
+ // skipped variant, we ignore it as Serde will throw an error if we
try to deserialize that
+ // variant.
+ if default.is_none() && has_default_attr(&variant.attrs) {
+ let renamed = match (&variant_attrs.rename,
container_attrs.rename_all) {
+ (Some(rename), _) => rename.clone(),
(None, rename_all) if !matches!(rename_all, RenameRule::None)
=> {
rename_all.apply_to_variant(&variant.ident.to_string())
}
_ => variant.ident.to_string(),
};
- symbols.push(name);
+ default = Some(renamed);
}
- Ok(quote! {
-
::apache_avro::schema::Schema::Enum(::apache_avro::schema::EnumSchema {
- name,
- aliases: #enum_aliases,
- doc: #doc,
- symbols: vec![#(#symbols.to_owned()),*],
- default: #default,
- attributes: ::std::collections::BTreeMap::new(),
- })
- })
- } else {
- Err(vec![syn::Error::new(
- ident_span,
- "AvroSchema: derive does not work for enums with non unit structs",
- )])
+
+ let name = match (variant_attrs.rename, container_attrs.rename_all) {
+ (Some(rename), _) => rename,
+ (None, rename_all) if !matches!(rename_all, RenameRule::None) => {
+ rename_all.apply_to_variant(&variant.ident.to_string())
+ }
+ _ => variant.ident.to_string(),
+ };
+ symbols.push(name);
}
+
+ if let Some(default) = &default
+ && symbols.iter().all(|n| default != n)
+ {
+ errors.push(syn::Error::new(
+ input_span,
+ format!("The enum default {default} is not one of the variants:
{symbols:?}"),
+ ));
+ }
+
+ if !errors.is_empty() {
+ return Err(errors);
+ }
+
+ let default = preserve_optional(default.map(|d| quote! {
::std::string::String::from(#d)}));
+ let schema_expr = quote! {
+ ::apache_avro::schema::Schema::Enum(::apache_avro::schema::EnumSchema {
+ name,
+ aliases: #enum_aliases,
+ doc: #doc,
+ symbols: vec![#(#symbols.to_owned()),*],
+ default: #default,
+ attributes: ::std::collections::BTreeMap::new(),
+ })
+ };
+
+ Ok(Implementation::named(
+ ident,
+ generics,
+ &container_attrs.name,
+ schema_expr,
+ None,
+ container_attrs
+ .default
+ .map(json_value_expr)
+ .map(|t| quote! { ::std::option::Option::Some(#t)}),
+ ))
+}
+
+fn has_default_attr(attrs: &[Attribute]) -> bool {
+ attrs.iter().any(|attr| {
+ matches!(attr, Attribute { meta: Meta::Path(path), .. } if
path.get_ident().map(Ident::to_string).as_deref() == Some("default"))
+ })
}
diff --git a/avro_derive/src/fields.rs b/avro_derive/src/fields.rs
index f6bcb80..960c6aa 100644
--- a/avro_derive/src/fields.rs
+++ b/avro_derive/src/fields.rs
@@ -16,12 +16,13 @@
// under the License.
use crate::attributes::{FieldDefault, With};
+use crate::utils::json_value_expr;
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>> {
+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) => {
@@ -47,7 +48,7 @@ pub fn field_to_schema_expr(field: &Field, with: With) ->
Result<TokenStream, Ve
pub fn field_to_record_fields_expr(
field: &Field,
- with: With,
+ with: &With,
) -> Result<TokenStream, Vec<syn::Error>> {
match with {
With::Trait => Ok(type_to_record_fields_expr(&field.ty)?),
@@ -88,14 +89,9 @@ pub fn field_to_field_default_expr(
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}"),
- )]
- })?;
+ let value = json_value_expr(default_value);
Ok(quote! {
-
::std::option::Option::Some(::serde_json::from_str(#default_value).expect("Unreachable!
This parsed successfully at compile time!"))
+ ::std::option::Option::Some(#value)
})
}
}
diff --git a/avro_derive/src/implementation.rs
b/avro_derive/src/implementation.rs
new file mode 100644
index 0000000..0124ffd
--- /dev/null
+++ b/avro_derive/src/implementation.rs
@@ -0,0 +1,114 @@
+// 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::{Ident, TokenStream};
+use quote::quote;
+use syn::Generics;
+
+pub struct Implementation {
+ ident: Ident,
+ generics: Generics,
+ /// An expression that resolves to a `Schema`
+ ///
+ /// The variables `named_schemas: &mut HashSet<Name>` and
`enclosing_namespace: NamespaceRef` are
+ /// defined.
+ schema_expr: TokenStream,
+ /// An expression that resolves to a `Option<Vec<RecordField>>`
+ ///
+ /// The variables `named_schemas: &mut HashSet<Name>` and
`enclosing_namespace: NamespaceRef` are
+ /// defined.
+ record_fields_expr: TokenStream,
+ /// An expression that resolves to a `Option<serde_json::Value>`
+ field_default_expr: TokenStream,
+}
+
+impl Implementation {
+ #[expect(
+ clippy::needless_pass_by_value,
+ reason = "Makes it match the `unnamed` variant"
+ )]
+ pub fn named(
+ ident: Ident,
+ generics: Generics,
+ name: &str,
+ schema_expr: TokenStream,
+ record_fields_expr: Option<TokenStream>,
+ field_default_expr: Option<TokenStream>,
+ ) -> Implementation {
+ let schema_expr = quote! {
+ let name =
::apache_avro::schema::Name::new_with_enclosing_namespace(#name,
enclosing_namespace).expect(concat!("Unable to parse schema name ", #name));
+ if named_schemas.contains(&name) {
+ ::apache_avro::schema::Schema::Ref{name}
+ } else {
+ let enclosing_namespace = name.namespace();
+ named_schemas.insert(name.clone());
+ #schema_expr
+ }
+ };
+
+ Self::unnamed(
+ ident,
+ generics,
+ schema_expr,
+ record_fields_expr,
+ field_default_expr,
+ )
+ }
+
+ pub fn unnamed(
+ ident: Ident,
+ generics: Generics,
+ schema_expr: TokenStream,
+ record_fields_expr: Option<TokenStream>,
+ field_default_expr: Option<TokenStream>,
+ ) -> Implementation {
+ Self {
+ ident,
+ generics,
+ schema_expr,
+ record_fields_expr: record_fields_expr
+ .unwrap_or_else(|| quote! { ::std::option::Option::None }),
+ field_default_expr: field_default_expr
+ .unwrap_or_else(|| quote! { ::std::option::Option::None }),
+ }
+ }
+
+ pub fn into_token_stream(self) -> TokenStream {
+ let (impl_generics, ty_generics, where_clause) =
self.generics.split_for_impl();
+ let ident = self.ident;
+ let schema_expr = self.schema_expr;
+ let record_fields_expr = self.record_fields_expr;
+ let field_default_expr = self.field_default_expr;
+
+ quote! {
+ #[automatically_derived]
+ impl #impl_generics ::apache_avro::AvroSchemaComponent for #ident
#ty_generics #where_clause {
+ fn get_schema_in_ctxt(named_schemas: &mut
::std::collections::HashSet<::apache_avro::schema::Name>, enclosing_namespace:
::apache_avro::schema::NamespaceRef) -> ::apache_avro::schema::Schema {
+ #schema_expr
+ }
+
+ fn get_record_fields_in_ctxt(named_schemas: &mut
::std::collections::HashSet<::apache_avro::schema::Name>, enclosing_namespace:
::apache_avro::schema::NamespaceRef) ->
::std::option::Option<::std::vec::Vec<::apache_avro::schema::RecordField>> {
+ #record_fields_expr
+ }
+
+ fn field_default() ->
::std::option::Option<::serde_json::Value> {
+ #field_default_expr
+ }
+ }
+ }
+ }
+}
diff --git a/avro_derive/src/lib.rs b/avro_derive/src/lib.rs
index e2bc329..2401eab 100644
--- a/avro_derive/src/lib.rs
+++ b/avro_derive/src/lib.rs
@@ -33,16 +33,14 @@ mod attributes;
mod case;
mod enums;
mod fields;
+mod implementation;
mod structs;
mod utils;
-use proc_macro2::TokenStream;
-use quote::quote;
-use syn::{DeriveInput, Generics, Ident, parse_macro_input, spanned::Spanned};
+use syn::{DeriveInput, parse_macro_input, spanned::Spanned};
use crate::attributes::NamedTypeOptions;
-use crate::enums::get_data_enum_schema_def;
-use crate::structs::{get_struct_schema_def, get_transparent_struct_schema_def};
+use crate::implementation::Implementation;
use crate::utils::{aliases, preserve_optional, to_compile_errors};
#[proc_macro_derive(AvroSchema, attributes(avro, serde))]
@@ -50,53 +48,35 @@ use crate::utils::{aliases, preserve_optional,
to_compile_errors};
pub fn proc_macro_derive_avro_schema(input: proc_macro::TokenStream) ->
proc_macro::TokenStream {
let input = parse_macro_input!(input as DeriveInput);
derive_avro_schema(input)
+ .map(Implementation::into_token_stream)
.unwrap_or_else(to_compile_errors)
.into()
}
-fn derive_avro_schema(input: DeriveInput) -> Result<TokenStream,
Vec<syn::Error>> {
+fn derive_avro_schema(input: DeriveInput) -> Result<Implementation,
Vec<syn::Error>> {
// It would be nice to parse the attributes before the `match`, but we
first need to validate that `input` is not a union.
// Otherwise a user could get errors related to the attributes and after
fixing those get an error because the attributes were on a union.
let input_span = input.span();
match input.data {
syn::Data::Struct(data_struct) => {
let named_type_options = NamedTypeOptions::new(&input.ident,
&input.attrs, input_span)?;
- let (get_schema_impl, get_record_fields_impl) = if
named_type_options.transparent {
- get_transparent_struct_schema_def(data_struct.fields,
input_span)?
- } else {
- let (schema_def, record_fields) =
- get_struct_schema_def(&named_type_options, data_struct,
input.ident.span())?;
- (
- handle_named_schemas(&named_type_options.name,
&schema_def),
- record_fields,
- )
- };
- Ok(create_trait_definition(
- &input.ident,
- &input.generics,
- &get_schema_impl,
- &get_record_fields_impl,
- &named_type_options.default,
- ))
+ structs::to_implementation(
+ input_span,
+ input.ident,
+ input.generics,
+ named_type_options,
+ data_struct,
+ )
}
syn::Data::Enum(data_enum) => {
let named_type_options = NamedTypeOptions::new(&input.ident,
&input.attrs, input_span)?;
- if named_type_options.transparent {
- return Err(vec![syn::Error::new(
- input_span,
- "AvroSchema: `#[serde(transparent)]` is only supported on
structs",
- )]);
- }
- let schema_def =
- get_data_enum_schema_def(&named_type_options, &data_enum,
input.ident.span())?;
- let inner = handle_named_schemas(&named_type_options.name,
&schema_def);
- Ok(create_trait_definition(
- &input.ident,
- &input.generics,
- &inner,
- "e! { ::std::option::Option::None },
- &named_type_options.default,
- ))
+ enums::to_implementation(
+ input_span,
+ input.ident,
+ input.generics,
+ named_type_options,
+ data_enum,
+ )
}
syn::Data::Union(_) => Err(vec![syn::Error::new(
input_span,
@@ -104,44 +84,3 @@ fn derive_avro_schema(input: DeriveInput) ->
Result<TokenStream, Vec<syn::Error>
)]),
}
}
-
-/// Generate the trait definition with the correct generics
-fn create_trait_definition(
- ident: &Ident,
- 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! {
- #[automatically_derived]
- impl #impl_generics ::apache_avro::AvroSchemaComponent for #ident
#ty_generics #where_clause {
- fn get_schema_in_ctxt(named_schemas: &mut
::std::collections::HashSet<::apache_avro::schema::Name>, enclosing_namespace:
::apache_avro::schema::NamespaceRef) -> ::apache_avro::schema::Schema {
- #get_schema_impl
- }
-
- fn get_record_fields_in_ctxt(named_schemas: &mut
::std::collections::HashSet<::apache_avro::schema::Name>, enclosing_namespace:
::apache_avro::schema::NamespaceRef) ->
::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
- }
- }
- }
-}
-
-/// Generate the code to check `named_schemas` if this schema already exist
-fn handle_named_schemas(full_schema_name: &str, schema_def: &TokenStream) ->
TokenStream {
- quote! {
- let name =
::apache_avro::schema::Name::new_with_enclosing_namespace(#full_schema_name,
enclosing_namespace).expect(concat!("Unable to parse schema name ",
#full_schema_name));
- if named_schemas.contains(&name) {
- ::apache_avro::schema::Schema::Ref{name}
- } else {
- let enclosing_namespace = name.namespace();
- named_schemas.insert(name.clone());
- #schema_def
- }
- }
-}
diff --git a/avro_derive/src/structs.rs b/avro_derive/src/structs.rs
index db51bd1..78d0032 100644
--- a/avro_derive/src/structs.rs
+++ b/avro_derive/src/structs.rs
@@ -20,20 +20,36 @@ 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 crate::implementation::Implementation;
+use crate::utils::{aliases, field_aliases, json_value_expr, preserve_optional};
+use proc_macro2::{Ident, Span};
use quote::quote;
use syn::spanned::Spanned;
-use syn::{DataStruct, Fields};
+use syn::{DataStruct, Fields, Generics};
-/// 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>> {
+pub fn to_implementation(
+ input_span: Span,
+ ident: Ident,
+ generics: Generics,
+ container_attrs: NamedTypeOptions,
+ data: DataStruct,
+) -> Result<Implementation, Vec<syn::Error>> {
+ if container_attrs.transparent {
+ transparent(input_span, ident, generics, data)
+ } else {
+ normal(input_span, ident, generics, container_attrs, data)
+ }
+}
+
+fn normal(
+ input_span: Span,
+ ident: Ident,
+ generics: Generics,
+ container_attrs: NamedTypeOptions,
+ data: DataStruct,
+) -> Result<Implementation, Vec<syn::Error>> {
let mut record_field_exprs = vec![];
- match data_struct.fields {
+ match data.fields {
Fields::Named(a) => {
for field in a.named {
let mut name = field
@@ -60,7 +76,7 @@ pub fn get_struct_schema_def(
} 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)?;
+ 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);
@@ -74,7 +90,7 @@ pub fn get_struct_schema_def(
}
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)?;
+ 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(),
@@ -89,13 +105,13 @@ pub fn get_struct_schema_def(
}
Fields::Unnamed(_) => {
return Err(vec![syn::Error::new(
- ident_span,
+ input_span,
"AvroSchema derive does not work for tuple structs",
)]);
}
Fields::Unit => {
return Err(vec![syn::Error::new(
- ident_span,
+ input_span,
"AvroSchema derive does not work for unit structs",
)]);
}
@@ -109,7 +125,7 @@ pub fn get_struct_schema_def(
// the most common case where there is no flatten.
let minimum_fields = record_field_exprs.len();
- let schema_def = quote! {
+ let schema_expr = quote! {
{
let mut schema_fields =
::std::vec::Vec::with_capacity(#minimum_fields);
#(#record_field_exprs)*
@@ -131,21 +147,32 @@ pub fn get_struct_schema_def(
})
}
};
- let record_fields = quote! {
+ let record_fields_expr = 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))
+ Ok(Implementation::named(
+ ident,
+ generics,
+ &container_attrs.name,
+ schema_expr,
+ Some(record_fields_expr),
+ container_attrs
+ .default
+ .map(json_value_expr)
+ .map(|t| quote! { ::std::option::Option::Some(#t)}),
+ ))
}
-/// Use the schema definition of the only field in the struct as the schema
-pub fn get_transparent_struct_schema_def(
- fields: Fields,
+fn transparent(
input_span: Span,
-) -> Result<(TokenStream, TokenStream), Vec<syn::Error>> {
- match fields {
+ ident: Ident,
+ generics: Generics,
+ data: DataStruct,
+) -> Result<Implementation, Vec<syn::Error>> {
+ match data.fields {
Fields::Named(fields_named) => {
let mut found = None;
for field in fields_named.named {
@@ -162,9 +189,14 @@ pub fn get_transparent_struct_schema_def(
}
if let Some((field, attrs)) = found {
- Ok((
- field_to_schema_expr(&field, attrs.with.clone())?,
- field_to_record_fields_expr(&field, attrs.with)?,
+ let field_default_expr = field_to_field_default_expr(&field,
attrs.default)?;
+
+ Ok(Implementation::unnamed(
+ ident,
+ generics,
+ field_to_schema_expr(&field, &attrs.with)?,
+ Some(field_to_record_fields_expr(&field, &attrs.with)?),
+ Some(field_default_expr),
))
} else {
Err(vec![syn::Error::new(
diff --git a/avro_derive/src/utils.rs b/avro_derive/src/utils.rs
index 6affff0..7699494 100644
--- a/avro_derive/src/utils.rs
+++ b/avro_derive/src/utils.rs
@@ -17,6 +17,7 @@
use proc_macro2::TokenStream;
use quote::quote;
+use serde_json::Value;
/// Convert a list of errors into actual compiler errors.
///
@@ -65,3 +66,65 @@ pub fn field_aliases(op: &[impl AsRef<str>]) -> TokenStream {
.collect();
quote! {vec![#(#items),*]}
}
+
+/// Convert a `Value` into a `TokenStream`.
+///
+/// The `Value` is constructed, not parsed from a JSON string.
+pub fn json_value_expr(value: Value) -> TokenStream {
+ match value {
+ Value::Null => quote! {
+ ::serde_json::Value::Null
+ },
+ Value::Bool(bool) => quote! {
+ ::serde_json::Value::Bool(#bool)
+ },
+ Value::Number(number) => {
+ if let Some(n) = number.as_u64() {
+ quote! {
+ ::serde_json::Value::Number(::serde_json::Number::from(#n))
+ }
+ } else if let Some(n) = number.as_i64() {
+ quote! {
+ ::serde_json::Value::Number(::serde_json::Number::from(#n))
+ }
+ } else if let Some(n) = number.as_f64() {
+ quote! {
+
::serde_json::Value::Number(::serde_json::Number::from_f64(#n).expect("Unreachable,
f64 is finite"))
+ }
+ } else {
+ // This is needed when the arbitrary-precision feature flag is
enabled on serde_json
+ let s = number.to_string();
+ quote! {
+
::serde_json::Value::Number(#s.parse().expect(concat!("This was a valid number
at compile time: ", #s)))
+ }
+ }
+ }
+ Value::String(string) => quote! {
+ ::serde_json::Value::String(::std::string::String::from(#string))
+ },
+ Value::Array(array) => {
+ let array = array.into_iter().map(json_value_expr);
+ quote! {
+ ::serde_json::Value::Array(vec![#(#array),*])
+ }
+ }
+ Value::Object(object) => {
+ let len = object.len();
+
+ let mut keys = Vec::with_capacity(len);
+ let mut values = Vec::with_capacity(len);
+ for (key, value) in object {
+ keys.push(key);
+ values.push(json_value_expr(value));
+ }
+
+ quote! {
+ ::serde_json::Value::Object({
+ let mut map = ::serde_json::Map::with_capacity(#len);
+ #(map.insert(::std::string::String::from(#keys),
#values);)*
+ map
+ })
+ }
+ }
+ }
+}
diff --git a/avro_derive/tests/derive.rs b/avro_derive/tests/derive.rs
index e2509c3..837ef3d 100644
--- a/avro_derive/tests/derive.rs
+++ b/avro_derive/tests/derive.rs
@@ -2570,3 +2570,18 @@ fn
avro_rs_476_skip_serializing_fielddefault_trait_none() {
},
}
}
+
+#[test]
+fn avro_rs_561_transparent_struct_with_default_override() {
+ #[derive(AvroSchema)]
+ #[serde(transparent)]
+ struct T {
+ #[avro(default = "42")]
+ _field: i32,
+ }
+
+ let schema = T::get_schema();
+ let field_default = T::field_default();
+ assert_eq!(schema, Schema::Int);
+ assert_eq!(field_default, Some(serde_json::Value::Number(42i32.into())));
+}
diff --git
a/avro_derive/tests/expanded/avro_3687_basic_enum_with_default.expanded.rs
b/avro_derive/tests/expanded/avro_3687_basic_enum_with_default.expanded.rs
index 62fcff7..1db9146 100644
--- a/avro_derive/tests/expanded/avro_3687_basic_enum_with_default.expanded.rs
+++ b/avro_derive/tests/expanded/avro_3687_basic_enum_with_default.expanded.rs
@@ -35,7 +35,9 @@ impl ::apache_avro::AvroSchemaComponent for Basic {
["A".to_owned(), "B".to_owned(), "C".to_owned(),
"D".to_owned()],
),
),
- default: ::std::option::Option::Some("A".into()),
+ default: ::std::option::Option::Some(
+ ::std::string::String::from("A").into(),
+ ),
attributes: ::std::collections::BTreeMap::new(),
})
}
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 f65b9a0..11e668c 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
@@ -30,10 +30,9 @@ impl ::apache_avro::AvroSchemaComponent for A {
name: "a3".to_string(),
doc: ::std::option::Option::Some("a doc".into()),
default: ::std::option::Option::Some(
- ::serde_json::from_str("123")
- .expect(
- "Unreachable! This parsed successfully at
compile time!",
- ),
+ ::serde_json::Value::Number(
+ ::serde_json::Number::from(123u64),
+ ),
),
aliases:
::alloc::boxed::box_assume_init_into_vec_unsafe(
::alloc::intrinsics::write_box_via_move(
@@ -109,8 +108,7 @@ impl ::apache_avro::AvroSchemaComponent for A {
name: "a3".to_string(),
doc: ::std::option::Option::Some("a doc".into()),
default: ::std::option::Option::Some(
- ::serde_json::from_str("123")
- .expect("Unreachable! This parsed successfully at
compile time!"),
+
::serde_json::Value::Number(::serde_json::Number::from(123u64)),
),
aliases: ::alloc::boxed::box_assume_init_into_vec_unsafe(
::alloc::intrinsics::write_box_via_move(
diff --git a/avro_derive/tests/serde.rs b/avro_derive/tests/serde.rs
index 0f03713..3b51074 100644
--- a/avro_derive/tests/serde.rs
+++ b/avro_derive/tests/serde.rs
@@ -508,7 +508,7 @@ mod variant_attributes {
"type":"enum",
"name":"Foo",
"symbols": [
- "One", "Two"
+ "Two"
]
}
"#;
diff --git
a/avro_derive/tests/ui/avro_3687_basic_enum_with_default_twice.stderr
b/avro_derive/tests/ui/avro_3687_basic_enum_with_default_twice.stderr
index 14a3798..7b8fe98 100644
--- a/avro_derive/tests/ui/avro_3687_basic_enum_with_default_twice.stderr
+++ b/avro_derive/tests/ui/avro_3687_basic_enum_with_default_twice.stderr
@@ -1,9 +1,3 @@
-error: Multiple defaults defined: ["A", "C"]
- --> tests/ui/avro_3687_basic_enum_with_default_twice.rs:21:6
- |
-21 | enum Basic {
- | ^^^^^
-
error: multiple declared defaults
--> tests/ui/avro_3687_basic_enum_with_default_twice.rs:20:22
|
diff --git a/avro_derive/tests/ui/avro_rs_373_rename_all_fields.stderr
b/avro_derive/tests/ui/avro_rs_373_rename_all_fields.stderr
index 7a5a531..c779c05 100644
--- a/avro_derive/tests/ui/avro_rs_373_rename_all_fields.stderr
+++ b/avro_derive/tests/ui/avro_rs_373_rename_all_fields.stderr
@@ -1,5 +1,10 @@
error: AvroSchema: derive does not work for enums with non unit structs
- --> tests/ui/avro_rs_373_rename_all_fields.rs:22:6
+ --> tests/ui/avro_rs_373_rename_all_fields.rs:21:1
|
-22 | enum Foo {
- | ^^^
+21 | / #[serde(rename_all_fields = "UPPERCASE")]
+22 | | enum Foo {
+23 | | Bar {
+24 | | a: String,
+... |
+27 | | }
+ | |_^
diff --git a/avro_derive/tests/ui/avro_rs_501_non_basic_enum.stderr
b/avro_derive/tests/ui/avro_rs_501_non_basic_enum.stderr
index 961253b..2ef79de 100644
--- a/avro_derive/tests/ui/avro_rs_501_non_basic_enum.stderr
+++ b/avro_derive/tests/ui/avro_rs_501_non_basic_enum.stderr
@@ -1,5 +1,10 @@
error: AvroSchema: derive does not work for enums with non unit structs
- --> tests/ui/avro_rs_501_non_basic_enum.rs:21:6
+ --> tests/ui/avro_rs_501_non_basic_enum.rs:21:1
|
-21 | enum NonBasic {
- | ^^^^^^^^
+21 | / enum NonBasic {
+22 | | A(i32),
+23 | | B,
+24 | | C,
+25 | | D
+26 | | }
+ | |_^
diff --git a/avro_derive/tests/ui/avro_rs_501_tuple_struct.stderr
b/avro_derive/tests/ui/avro_rs_501_tuple_struct.stderr
index 1091ec0..5051fc7 100644
--- a/avro_derive/tests/ui/avro_rs_501_tuple_struct.stderr
+++ b/avro_derive/tests/ui/avro_rs_501_tuple_struct.stderr
@@ -1,5 +1,5 @@
error: AvroSchema derive does not work for tuple structs
- --> tests/ui/avro_rs_501_tuple_struct.rs:21:8
+ --> tests/ui/avro_rs_501_tuple_struct.rs:21:1
|
21 | struct B(i32, String);
- | ^
+ | ^^^^^^^^^^^^^^^^^^^^^^
diff --git a/avro_derive/tests/ui/avro_rs_501_unit_struct.stderr
b/avro_derive/tests/ui/avro_rs_501_unit_struct.stderr
index 2a16ef6..655af14 100644
--- a/avro_derive/tests/ui/avro_rs_501_unit_struct.stderr
+++ b/avro_derive/tests/ui/avro_rs_501_unit_struct.stderr
@@ -1,5 +1,5 @@
error: AvroSchema derive does not work for unit structs
- --> tests/ui/avro_rs_501_unit_struct.rs:21:8
+ --> tests/ui/avro_rs_501_unit_struct.rs:21:1
|
21 | struct AbsoluteUnit;
- | ^^^^^^^^^^^^
+ | ^^^^^^^^^^^^^^^^^^^^
diff --git a/avro_derive/tests/ui/avro_rs_561_transparent_default.rs
b/avro_derive/tests/ui/avro_rs_561_transparent_default.rs
new file mode 100644
index 0000000..91d61db
--- /dev/null
+++ b/avro_derive/tests/ui/avro_rs_561_transparent_default.rs
@@ -0,0 +1,27 @@
+// 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 apache_avro::AvroSchema;
+
+#[derive(AvroSchema)]
+#[serde(transparent)]
+#[avro(default = "0")]
+struct T {
+ _field: i32,
+}
+
+fn main() {}
diff --git a/avro_derive/tests/ui/avro_rs_561_transparent_default.stderr
b/avro_derive/tests/ui/avro_rs_561_transparent_default.stderr
new file mode 100644
index 0000000..a0ca2c1
--- /dev/null
+++ b/avro_derive/tests/ui/avro_rs_561_transparent_default.stderr
@@ -0,0 +1,9 @@
+error: AvroSchema: #[serde(transparent)] is incompatible with all other
attributes
+ --> tests/ui/avro_rs_561_transparent_default.rs:21:1
+ |
+21 | / #[serde(transparent)]
+22 | | #[avro(default = "0")]
+23 | | struct T {
+24 | | _field: i32,
+25 | | }
+ | |_^