This is an automated email from the ASF dual-hosted git repository. kriskras99 pushed a commit to branch feat/derive_with in repository https://gitbox.apache.org/repos/asf/avro-rs.git
commit 49353fac7bbd262b46c041359bdbb64666f9026b Author: default <[email protected]> AuthorDate: Fri Jan 9 13:31:30 2026 +0000 feat(derive): Allow overriding the schema used for a field --- avro_derive/src/attributes/avro.rs | 27 +++++++++++ avro_derive/src/attributes/mod.rs | 55 ++++++++++++++++++++-- avro_derive/src/attributes/serde.rs | 3 +- avro_derive/src/lib.rs | 10 +++- avro_derive/tests/derive.rs | 54 ++++++++++++++++++++- .../tests/ui/avro_rs_396_with_expr_string.rs | 27 +++++++++++ .../tests/ui/avro_rs_396_with_expr_string.stderr | 8 ++++ avro_derive/tests/ui/avro_rs_396_with_expr_type.rs | 27 +++++++++++ .../tests/ui/avro_rs_396_with_expr_type.stderr | 8 ++++ .../ui/avro_rs_396_with_word_without_serde.rs | 27 +++++++++++ .../ui/avro_rs_396_with_word_without_serde.stderr | 6 +++ 11 files changed, 243 insertions(+), 9 deletions(-) diff --git a/avro_derive/src/attributes/avro.rs b/avro_derive/src/attributes/avro.rs index ceeafe5..b4b880c 100644 --- a/avro_derive/src/attributes/avro.rs +++ b/avro_derive/src/attributes/avro.rs @@ -22,7 +22,9 @@ //! a user can use. These add extra metadata to the generated schema. use crate::case::RenameRule; +use darling::FromMeta; use proc_macro2::Span; +use syn::Expr; /// All the Avro attributes a container can have. #[derive(darling::FromAttributes)] @@ -97,6 +99,21 @@ impl VariantAttributes { } } +/// How to get the schema for this field. +#[derive(Debug, FromMeta, PartialEq, Default)] +#[darling(from_expr = |expr| Ok(With::Expr(expr.clone())))] +pub enum With { + /// Use `<T as AvroSchemaComponent>::get_schema_with_ctxt`. + #[default] + #[darling(skip)] + Trait, + /// Use `module::get_schema_with_ctxt` where the module is defined by Serde's `with` attribute. + #[darling(word, skip)] + Serde, + /// Call the function in this expression. + Expr(Expr), +} + /// All the Avro attributes a field can have. #[derive(darling::FromAttributes)] #[darling(attributes(avro))] @@ -137,6 +154,16 @@ pub struct FieldAttributes { /// [`serde::FieldAttributes::flatten`]: crate::attributes::serde::FieldAttributes::flatten #[darling(default)] pub flatten: bool, + /// How to get the schema for this field. + /// + /// By default uses `<T as AvroSchemaComponent>::get_schema_in_ctxt`. + /// + /// When it's provided without an argument (`#[avro(with)]`), it will use the function `get_schema_in_ctxt` defined + /// in the same module as the `#[serde(with = "..")]` attribute. + /// + /// When it's provided with an argument (`#[avro(with = ..)]`), it will use that function. + #[darling(default)] + pub with: With, } impl FieldAttributes { diff --git a/avro_derive/src/attributes/mod.rs b/avro_derive/src/attributes/mod.rs index 6f26a62..e66fc0e 100644 --- a/avro_derive/src/attributes/mod.rs +++ b/avro_derive/src/attributes/mod.rs @@ -16,12 +16,12 @@ // under the License. use crate::case::RenameRule; -use darling::FromAttributes; +use darling::{FromAttributes, FromMeta}; use proc_macro2::Span; -use syn::{Attribute, spanned::Spanned}; +use syn::{Attribute, Expr, Path, spanned::Spanned}; -pub mod avro; -pub mod serde; +mod avro; +mod serde; #[derive(Default)] pub struct NamedTypeOptions { @@ -128,6 +128,7 @@ impl VariantOptions { )); } + // Check for conflicts between Serde and Avro if avro.rename.is_some() && serde.rename != avro.rename { errors.push(syn::Error::new( span, @@ -145,6 +146,42 @@ impl VariantOptions { } } +/// How to get the schema for this field or variant. +#[derive(Debug, PartialEq, Default)] +pub enum With { + /// Use `<T as AvroSchemaComponent>::get_schema_with_ctxt`. + #[default] + Trait, + /// Use `module::get_schema_with_ctxt` where the module is defined by Serde's `with` attribute. + Serde(Path), + /// Call the function in this expression. + Expr(Expr), +} + +impl With { + fn from_avro_and_serde( + avro: &avro::With, + serde: &Option<String>, + span: Span, + ) -> Result<Self, syn::Error> { + match &avro { + avro::With::Trait => Ok(Self::Trait), + avro::With::Serde => { + if let Some(serde) = serde { + let path = Path::from_string(serde).unwrap(); + Ok(Self::Serde(path)) + } else { + Err(syn::Error::new( + span, + "`#[avro(with)]` requires `#[serde(with = \"..\")]` or provide a function to call `#[avro(width = ..)]`", + )) + } + } + avro::With::Expr(expr) => Ok(Self::Expr(expr.clone())), + } + } +} + pub struct FieldOptions { pub doc: Option<String>, pub default: Option<String>, @@ -152,6 +189,7 @@ pub struct FieldOptions { pub rename: Option<String>, pub skip: bool, pub flatten: bool, + pub with: With, } impl FieldOptions { @@ -213,6 +251,14 @@ impl FieldOptions { "`#[serde(skip_serializing)]` and `#[serde(skip_serializing_if)]` require `#[avro(default = \"..\")]`" )); } + let with = match With::from_avro_and_serde(&avro.with, &serde.with, span) { + Ok(with) => with, + Err(error) => { + errors.push(error); + // This won't actually be used, but it does simplify the code + With::Trait + } + }; if !errors.is_empty() { return Err(errors); @@ -225,6 +271,7 @@ impl FieldOptions { rename: serde.rename, skip: serde.skip || (serde.skip_serializing && serde.skip_deserializing), flatten: serde.flatten, + with, }) } } diff --git a/avro_derive/src/attributes/serde.rs b/avro_derive/src/attributes/serde.rs index 9f23f83..b1ca6fa 100644 --- a/avro_derive/src/attributes/serde.rs +++ b/avro_derive/src/attributes/serde.rs @@ -238,8 +238,7 @@ pub struct FieldAttributes { #[darling(rename = "deserialize_with")] pub _deserialize_with: Option<String>, /// Use this module for (de)serializing. - #[darling(rename = "with")] - pub _with: Option<String>, + pub with: Option<String>, /// Put bounds on the lifetimes. #[darling(rename = "borrow")] pub _borrow: Option<SerdeBorrow>, diff --git a/avro_derive/src/lib.rs b/avro_derive/src/lib.rs index 8723593..b4e500b 100644 --- a/avro_derive/src/lib.rs +++ b/avro_derive/src/lib.rs @@ -28,7 +28,7 @@ use syn::{ }; use crate::{ - attributes::{FieldOptions, NamedTypeOptions, VariantOptions}, + attributes::{FieldOptions, NamedTypeOptions, VariantOptions, With}, case::RenameRule, }; @@ -167,7 +167,13 @@ fn get_data_struct_schema_def( None => quote! { None }, }; let aliases = preserve_vec(field_attrs.alias); - let schema_expr = type_to_schema_expr(&field.ty)?; + let schema_expr = match field_attrs.with { + With::Trait => type_to_schema_expr(&field.ty)?, + With::Serde(path) => { + quote! {#path::get_schema_in_ctxt(named_schemas, enclosing_namespace)} + } + With::Expr(expr) => quote! {#expr(named_schemas, enclosing_namespace)}, + }; record_field_exprs.push(quote! { schema_fields.push(::apache_avro::schema::RecordField { name: #name.to_string(), diff --git a/avro_derive/tests/derive.rs b/avro_derive/tests/derive.rs index 72c6db0..e6978e5 100644 --- a/avro_derive/tests/derive.rs +++ b/avro_derive/tests/derive.rs @@ -17,7 +17,7 @@ use apache_avro::{ Reader, Schema, Writer, from_value, - schema::{AvroSchema, derive::AvroSchemaComponent}, + schema::{AvroSchema, Name, Namespace, derive::AvroSchemaComponent}, }; use apache_avro_derive::*; use proptest::prelude::*; @@ -1827,3 +1827,55 @@ fn avro_rs_247_serde_flatten_support_with_skip() { b: 321, }); } + +#[test] +fn avro_rs_396_with() { + let schema = Schema::parse_str( + r#" + { + "type":"record", + "name":"Foo", + "fields": [ + { + "name":"a", + "type":"bytes" + }, + { + "name":"b", + "type":"long" + } + ] + } + "#, + ) + .unwrap(); + + fn long_schema( + _named_schemas: &mut HashMap<Name, Schema>, + _enclosing_namespace: &Namespace, + ) -> Schema { + Schema::Long + } + + mod module { + use super::*; + pub fn get_schema_in_ctxt( + _named_schemas: &mut HashMap<Name, Schema>, + _enclosing_namespace: &Namespace, + ) -> Schema { + Schema::Bytes + } + } + + #[allow(dead_code)] + #[derive(AvroSchema)] + struct Foo { + #[avro(with)] + #[serde(with = "module")] + a: String, + #[avro(with = long_schema)] + b: i32, + } + + assert_eq!(schema, Foo::get_schema()); +} diff --git a/avro_derive/tests/ui/avro_rs_396_with_expr_string.rs b/avro_derive/tests/ui/avro_rs_396_with_expr_string.rs new file mode 100644 index 0000000..dd24c69 --- /dev/null +++ b/avro_derive/tests/ui/avro_rs_396_with_expr_string.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)] +struct Foo { + #[avro(with = "Schema::Bytes")] + a: String, + b: i32, +} + +pub fn main() {} diff --git a/avro_derive/tests/ui/avro_rs_396_with_expr_string.stderr b/avro_derive/tests/ui/avro_rs_396_with_expr_string.stderr new file mode 100644 index 0000000..68a0aac --- /dev/null +++ b/avro_derive/tests/ui/avro_rs_396_with_expr_string.stderr @@ -0,0 +1,8 @@ +error[E0618]: expected function, found `&'static str` + --> tests/ui/avro_rs_396_with_expr_string.rs:22:19 + | +20 | #[derive(AvroSchema)] + | ---------- call expression requires function +21 | struct Foo { +22 | #[avro(with = "Schema::Bytes")] + | ^^^^^^^^^^^^^^^ diff --git a/avro_derive/tests/ui/avro_rs_396_with_expr_type.rs b/avro_derive/tests/ui/avro_rs_396_with_expr_type.rs new file mode 100644 index 0000000..93829a4 --- /dev/null +++ b/avro_derive/tests/ui/avro_rs_396_with_expr_type.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, Schema}; + +#[derive(AvroSchema)] +struct Foo { + #[avro(with = Schema::Bytes)] + a: String, + b: i32, +} + +pub fn main() {} diff --git a/avro_derive/tests/ui/avro_rs_396_with_expr_type.stderr b/avro_derive/tests/ui/avro_rs_396_with_expr_type.stderr new file mode 100644 index 0000000..02b8bfd --- /dev/null +++ b/avro_derive/tests/ui/avro_rs_396_with_expr_type.stderr @@ -0,0 +1,8 @@ +error[E0618]: expected function, found `Schema` + --> tests/ui/avro_rs_396_with_expr_type.rs:22:19 + | +20 | #[derive(AvroSchema)] + | ---------- call expression requires function +21 | struct Foo { +22 | #[avro(with = Schema::Bytes)] + | ^^^^^^^^^^^^^ diff --git a/avro_derive/tests/ui/avro_rs_396_with_word_without_serde.rs b/avro_derive/tests/ui/avro_rs_396_with_word_without_serde.rs new file mode 100644 index 0000000..eb44682 --- /dev/null +++ b/avro_derive/tests/ui/avro_rs_396_with_word_without_serde.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)] +struct Foo { + #[avro(with)] + a: String, + b: i32, +} + +pub fn main() {} diff --git a/avro_derive/tests/ui/avro_rs_396_with_word_without_serde.stderr b/avro_derive/tests/ui/avro_rs_396_with_word_without_serde.stderr new file mode 100644 index 0000000..0527568 --- /dev/null +++ b/avro_derive/tests/ui/avro_rs_396_with_word_without_serde.stderr @@ -0,0 +1,6 @@ +error: `#[avro(with)]` requires `#[serde(with = "..")]` or provide a function to call `#[avro(width = ..)]` + --> tests/ui/avro_rs_396_with_word_without_serde.rs:22:5 + | +22 | / #[avro(with)] +23 | | a: String, + | |_____________^
