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:?}"
+            ),
+        };
+    }
 }

Reply via email to