This is an automated email from the ASF dual-hosted git repository.
mgrigorov 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 092b36b feat: Add support for rename_all in AvroSchema macro (#207)
092b36b is described below
commit 092b36b2d3f7881fa6a3427b6924649ac6df1602
Author: Vaalla <[email protected]>
AuthorDate: Tue Jun 10 08:24:42 2025 +0300
feat: Add support for rename_all in AvroSchema macro (#207)
* feat: Add support for rename_all in AvroSchema macro
Supports the same settings as serde
* Give better name to the test case
* Add a test case showing that `rename` has priority over `rename_all`
Signed-off-by: Martin Tzvetanov Grigorov <[email protected]>
* Add ASL2 header to the new file
Signed-off-by: Martin Tzvetanov Grigorov <[email protected]>
* Fix a clippy warning - fix Url syntax
Signed-off-by: Martin Tzvetanov Grigorov <[email protected]>
---------
Signed-off-by: Martin Tzvetanov Grigorov <[email protected]>
Co-authored-by: Razvan Rotari <[email protected]>
Co-authored-by: Martin Grigorov <[email protected]>
Co-authored-by: Martin Tzvetanov Grigorov <[email protected]>
---
avro_derive/src/case.rs | 209 ++++++++++++++++++++++++++++++++++++++++++++++++
avro_derive/src/lib.rs | 108 +++++++++++++++++++++++--
2 files changed, 311 insertions(+), 6 deletions(-)
diff --git a/avro_derive/src/case.rs b/avro_derive/src/case.rs
new file mode 100644
index 0000000..89c4125
--- /dev/null
+++ b/avro_derive/src/case.rs
@@ -0,0 +1,209 @@
+// 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.
+
+//! Code to convert the Rust-styled field/variant (e.g. `my_field`, `MyType`)
to the
+//! case of the source (e.g. `my-field`, `MY_FIELD`).
+//! Code copied from serde
<https://github.com/serde-rs/serde/blob/master/serde_derive/src/internals/case.rs>
+use self::RenameRule::*;
+use std::fmt::{self, Debug, Display};
+
+/// The different possible ways to change case of fields in a struct, or
variants in an enum.
+#[derive(Copy, Clone, PartialEq)]
+pub enum RenameRule {
+ /// Don't apply a default rename rule.
+ None,
+ /// Rename direct children to "lowercase" style.
+ LowerCase,
+ /// Rename direct children to "UPPERCASE" style.
+ UpperCase,
+ /// Rename direct children to "PascalCase" style, as typically used for
+ /// enum variants.
+ PascalCase,
+ /// Rename direct children to "camelCase" style.
+ CamelCase,
+ /// Rename direct children to "snake_case" style, as commonly used for
+ /// fields.
+ SnakeCase,
+ /// Rename direct children to "SCREAMING_SNAKE_CASE" style, as commonly
+ /// used for constants.
+ ScreamingSnakeCase,
+ /// Rename direct children to "kebab-case" style.
+ KebabCase,
+ /// Rename direct children to "SCREAMING-KEBAB-CASE" style.
+ ScreamingKebabCase,
+}
+
+static RENAME_RULES: &[(&str, RenameRule)] = &[
+ ("lowercase", LowerCase),
+ ("UPPERCASE", UpperCase),
+ ("PascalCase", PascalCase),
+ ("camelCase", CamelCase),
+ ("snake_case", SnakeCase),
+ ("SCREAMING_SNAKE_CASE", ScreamingSnakeCase),
+ ("kebab-case", KebabCase),
+ ("SCREAMING-KEBAB-CASE", ScreamingKebabCase),
+];
+
+impl RenameRule {
+ pub fn from_str(rename_all_str: &str) -> Result<Self, ParseError<'_>> {
+ for (name, rule) in RENAME_RULES {
+ if rename_all_str == *name {
+ return Ok(*rule);
+ }
+ }
+ Err(ParseError {
+ unknown: rename_all_str,
+ })
+ }
+
+ /// Apply a renaming rule to an enum variant, returning the version
expected in the source.
+ pub fn apply_to_variant(self, variant: &str) -> String {
+ match self {
+ None | PascalCase => variant.to_owned(),
+ LowerCase => variant.to_ascii_lowercase(),
+ UpperCase => variant.to_ascii_uppercase(),
+ CamelCase => variant[..1].to_ascii_lowercase() + &variant[1..],
+ SnakeCase => {
+ let mut snake = String::new();
+ for (i, ch) in variant.char_indices() {
+ if i > 0 && ch.is_uppercase() {
+ snake.push('_');
+ }
+ snake.push(ch.to_ascii_lowercase());
+ }
+ snake
+ }
+ ScreamingSnakeCase =>
SnakeCase.apply_to_variant(variant).to_ascii_uppercase(),
+ KebabCase => SnakeCase.apply_to_variant(variant).replace('_', "-"),
+ ScreamingKebabCase => ScreamingSnakeCase
+ .apply_to_variant(variant)
+ .replace('_', "-"),
+ }
+ }
+
+ /// Apply a renaming rule to a struct field, returning the version
expected in the source.
+ pub fn apply_to_field(self, field: &str) -> String {
+ match self {
+ None | LowerCase | SnakeCase => field.to_owned(),
+ UpperCase => field.to_ascii_uppercase(),
+ PascalCase => {
+ let mut pascal = String::new();
+ let mut capitalize = true;
+ for ch in field.chars() {
+ if ch == '_' {
+ capitalize = true;
+ } else if capitalize {
+ pascal.push(ch.to_ascii_uppercase());
+ capitalize = false;
+ } else {
+ pascal.push(ch);
+ }
+ }
+ pascal
+ }
+ CamelCase => {
+ let pascal = PascalCase.apply_to_field(field);
+ pascal[..1].to_ascii_lowercase() + &pascal[1..]
+ }
+ ScreamingSnakeCase => field.to_ascii_uppercase(),
+ KebabCase => field.replace('_', "-"),
+ ScreamingKebabCase =>
ScreamingSnakeCase.apply_to_field(field).replace('_', "-"),
+ }
+ }
+}
+
+pub struct ParseError<'a> {
+ unknown: &'a str,
+}
+
+impl<'a> Display for ParseError<'a> {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ f.write_str("unknown rename rule `rename_all = ")?;
+ Debug::fmt(self.unknown, f)?;
+ f.write_str("`, expected one of ")?;
+ for (i, (name, _rule)) in RENAME_RULES.iter().enumerate() {
+ if i > 0 {
+ f.write_str(", ")?;
+ }
+ Debug::fmt(name, f)?;
+ }
+ Ok(())
+ }
+}
+
+#[test]
+fn rename_variants() {
+ for &(original, lower, upper, camel, snake, screaming, kebab,
screaming_kebab) in &[
+ (
+ "Outcome", "outcome", "OUTCOME", "outcome", "outcome", "OUTCOME",
"outcome", "OUTCOME",
+ ),
+ (
+ "VeryTasty",
+ "verytasty",
+ "VERYTASTY",
+ "veryTasty",
+ "very_tasty",
+ "VERY_TASTY",
+ "very-tasty",
+ "VERY-TASTY",
+ ),
+ ("A", "a", "A", "a", "a", "A", "a", "A"),
+ ("Z42", "z42", "Z42", "z42", "z42", "Z42", "z42", "Z42"),
+ ] {
+ assert_eq!(None.apply_to_variant(original), original);
+ assert_eq!(LowerCase.apply_to_variant(original), lower);
+ assert_eq!(UpperCase.apply_to_variant(original), upper);
+ assert_eq!(PascalCase.apply_to_variant(original), original);
+ assert_eq!(CamelCase.apply_to_variant(original), camel);
+ assert_eq!(SnakeCase.apply_to_variant(original), snake);
+ assert_eq!(ScreamingSnakeCase.apply_to_variant(original), screaming);
+ assert_eq!(KebabCase.apply_to_variant(original), kebab);
+ assert_eq!(
+ ScreamingKebabCase.apply_to_variant(original),
+ screaming_kebab
+ );
+ }
+}
+
+#[test]
+fn rename_fields() {
+ for &(original, upper, pascal, camel, screaming, kebab, screaming_kebab)
in &[
+ (
+ "outcome", "OUTCOME", "Outcome", "outcome", "OUTCOME", "outcome",
"OUTCOME",
+ ),
+ (
+ "very_tasty",
+ "VERY_TASTY",
+ "VeryTasty",
+ "veryTasty",
+ "VERY_TASTY",
+ "very-tasty",
+ "VERY-TASTY",
+ ),
+ ("a", "A", "A", "a", "A", "a", "A"),
+ ("z42", "Z42", "Z42", "z42", "Z42", "z42", "Z42"),
+ ] {
+ assert_eq!(None.apply_to_field(original), original);
+ assert_eq!(UpperCase.apply_to_field(original), upper);
+ assert_eq!(PascalCase.apply_to_field(original), pascal);
+ assert_eq!(CamelCase.apply_to_field(original), camel);
+ assert_eq!(SnakeCase.apply_to_field(original), original);
+ assert_eq!(ScreamingSnakeCase.apply_to_field(original), screaming);
+ assert_eq!(KebabCase.apply_to_field(original), kebab);
+ assert_eq!(ScreamingKebabCase.apply_to_field(original),
screaming_kebab);
+ }
+}
diff --git a/avro_derive/src/lib.rs b/avro_derive/src/lib.rs
index 6b9914c..bf52f95 100644
--- a/avro_derive/src/lib.rs
+++ b/avro_derive/src/lib.rs
@@ -15,6 +15,8 @@
// specific language governing permissions and limitations
// under the License.
+mod case;
+use case::RenameRule;
use darling::FromAttributes;
use proc_macro2::{Span, TokenStream};
use quote::quote;
@@ -55,6 +57,8 @@ struct NamedTypeOptions {
doc: Option<String>,
#[darling(multiple)]
alias: Vec<String>,
+ #[darling(default)]
+ rename_all: Option<String>,
}
#[proc_macro_derive(AvroSchema, attributes(avro))]
@@ -69,6 +73,9 @@ pub fn proc_macro_derive_avro_schema(input:
proc_macro::TokenStream) -> proc_mac
fn derive_avro_schema(input: &mut DeriveInput) -> Result<TokenStream,
Vec<syn::Error>> {
let named_type_options =
NamedTypeOptions::from_attributes(&input.attrs[..]).map_err(darling_to_syn)?;
+
+ let rename_all = parse_case(named_type_options.rename_all.as_deref(),
input.span())?;
+
let full_schema_name = vec![named_type_options.namespace,
Some(input.ident.to_string())]
.into_iter()
.flatten()
@@ -81,6 +88,7 @@ fn derive_avro_schema(input: &mut DeriveInput) ->
Result<TokenStream, Vec<syn::E
.doc
.or_else(|| extract_outer_doc(&input.attrs)),
named_type_options.alias,
+ rename_all,
s,
input.ident.span(),
)?,
@@ -90,6 +98,7 @@ fn derive_avro_schema(input: &mut DeriveInput) ->
Result<TokenStream, Vec<syn::E
.doc
.or_else(|| extract_outer_doc(&input.attrs)),
named_type_options.alias,
+ rename_all,
e,
input.ident.span(),
)?,
@@ -122,6 +131,7 @@ fn get_data_struct_schema_def(
full_schema_name: &str,
record_doc: Option<String>,
aliases: Vec<String>,
+ rename_all: RenameRule,
s: &syn::DataStruct,
error_span: Span,
) -> Result<TokenStream, Vec<syn::Error>> {
@@ -138,8 +148,14 @@ fn get_data_struct_schema_def(
FieldOptions::from_attributes(&field.attrs[..]).map_err(darling_to_syn)?;
let doc =
preserve_optional(field_attrs.doc.or_else(||
extract_outer_doc(&field.attrs)));
- if let Some(rename) = field_attrs.rename {
- name = rename
+ match (field_attrs.rename, rename_all) {
+ (Some(rename), _) => {
+ name = rename;
+ }
+ (None, rename_all) if !matches!(rename_all,
RenameRule::None) => {
+ name = rename_all.apply_to_field(&name);
+ }
+ _ => {}
}
if let Some(true) = field_attrs.skip {
continue;
@@ -214,6 +230,7 @@ fn get_data_enum_schema_def(
full_schema_name: &str,
doc: Option<String>,
aliases: Vec<String>,
+ rename_all: RenameRule,
e: &syn::DataEnum,
error_span: Span,
) -> Result<TokenStream, Vec<syn::Error>> {
@@ -226,10 +243,12 @@ fn get_data_enum_schema_def(
for variant in &e.variants {
let field_attrs =
VariantOptions::from_attributes(&variant.attrs[..]).map_err(darling_to_syn)?;
- let name = if let Some(rename) = field_attrs.rename {
- rename
- } else {
- variant.ident.to_string()
+ let name = match (field_attrs.rename, 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);
}
@@ -385,6 +404,16 @@ fn darling_to_syn(e: darling::Error) -> Vec<syn::Error> {
vec![syn::Error::new(token_errors.span(), msg)]
}
+fn parse_case(case: Option<&str>, span: Span) -> Result<RenameRule,
Vec<syn::Error>> {
+ match case {
+ None => Ok(RenameRule::None),
+ Some(case) => {
+ Ok(RenameRule::from_str(case)
+ .map_err(|e| vec![syn::Error::new(span, e.to_string())])?)
+ }
+ }
+}
+
#[cfg(test)]
mod tests {
use super::*;
@@ -679,4 +708,71 @@ mod tests {
),
};
}
+
+ #[test]
+ fn test_avro_rs_207_rename_all_attribute() {
+ let test_struct = quote! {
+ #[avro(rename_all="SCREAMING_SNAKE_CASE")]
+ struct A {
+ item: i32,
+ double_item: i32
+ }
+ };
+
+ match syn::parse2::<DeriveInput>(test_struct) {
+ Ok(mut input) => {
+ let schema_res = derive_avro_schema(&mut input);
+ let expected_token_stream = r#"let name = apache_avro ::
schema :: Name :: new ("A") . expect (& format ! ("Unable to parse schema name
{}" , "A") [..]) . fully_qualified_name (enclosing_namespace) ; let
enclosing_namespace = & name . namespace ; if named_schemas . contains_key (&
name) { apache_avro :: schema :: Schema :: Ref { name : name . clone () } }
else { named_schemas . insert (name . clone () , apache_avro :: schema ::
Schema :: Ref { name : name . clone () }) ; [...]
+ let schema_token_stream = schema_res.unwrap().to_string();
+ assert!(schema_token_stream.contains(expected_token_stream));
+ }
+ Err(error) => panic!(
+ "Failed to parse as derive input when it should be able to.
Error: {error:?}"
+ ),
+ };
+
+ let test_enum = quote! {
+ #[avro(rename_all="SCREAMING_SNAKE_CASE")]
+ enum B {
+ Item,
+ DoubleItem,
+ }
+ };
+
+ match syn::parse2::<DeriveInput>(test_enum) {
+ Ok(mut input) => {
+ let schema_res = derive_avro_schema(&mut input);
+ let expected_token_stream = r#"let name = apache_avro ::
schema :: Name :: new ("B") . expect (& format ! ("Unable to parse schema name
{}" , "B") [..]) . fully_qualified_name (enclosing_namespace) ; let
enclosing_namespace = & name . namespace ; if named_schemas . contains_key (&
name) { apache_avro :: schema :: Schema :: Ref { name : name . clone () } }
else { named_schemas . insert (name . clone () , apache_avro :: schema ::
Schema :: Ref { name : name . clone () }) ; [...]
+ let schema_token_stream = schema_res.unwrap().to_string();
+ assert!(schema_token_stream.contains(expected_token_stream));
+ }
+ Err(error) => panic!(
+ "Failed to parse as derive input when it should be able to.
Error: {error:?}"
+ ),
+ };
+ }
+
+ #[test]
+ fn test_avro_rs_207_rename_attr_has_priority_over_rename_all_attribute() {
+ let test_struct = quote! {
+ #[avro(rename_all="SCREAMING_SNAKE_CASE")]
+ struct A {
+ item: i32,
+ #[avro(rename="DoubleItem")]
+ double_item: i32
+ }
+ };
+
+ match syn::parse2::<DeriveInput>(test_struct) {
+ Ok(mut input) => {
+ let schema_res = derive_avro_schema(&mut input);
+ let expected_token_stream = r#"let name = apache_avro ::
schema :: Name :: new ("A") . expect (& format ! ("Unable to parse schema name
{}" , "A") [..]) . fully_qualified_name (enclosing_namespace) ; let
enclosing_namespace = & name . namespace ; if named_schemas . contains_key (&
name) { apache_avro :: schema :: Schema :: Ref { name : name . clone () } }
else { named_schemas . insert (name . clone () , apache_avro :: schema ::
Schema :: Ref { name : name . clone () }) ; [...]
+ let schema_token_stream = schema_res.unwrap().to_string();
+ assert!(schema_token_stream.contains(expected_token_stream));
+ }
+ Err(error) => panic!(
+ "Failed to parse as derive input when it should be able to.
Error: {error:?}"
+ ),
+ };
+ }
}