apache_avro_derive/
lib.rs

1// Licensed to the Apache Software Foundation (ASF) under one
2// or more contributor license agreements.  See the NOTICE file
3// distributed with this work for additional information
4// regarding copyright ownership.  The ASF licenses this file
5// to you under the Apache License, Version 2.0 (the
6// "License"); you may not use this file except in compliance
7// with the License.  You may obtain a copy of the License at
8//
9//   http://www.apache.org/licenses/LICENSE-2.0
10//
11// Unless required by applicable law or agreed to in writing,
12// software distributed under the License is distributed on an
13// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14// KIND, either express or implied.  See the License for the
15// specific language governing permissions and limitations
16// under the License.
17
18mod case;
19use case::RenameRule;
20use darling::FromAttributes;
21use proc_macro2::{Span, TokenStream};
22use quote::quote;
23
24use syn::{
25    parse_macro_input, spanned::Spanned, AttrStyle, Attribute, DeriveInput, Ident, Meta, Type,
26    TypePath,
27};
28
29#[derive(darling::FromAttributes)]
30#[darling(attributes(avro))]
31struct FieldOptions {
32    #[darling(default)]
33    doc: Option<String>,
34    #[darling(default)]
35    default: Option<String>,
36    #[darling(multiple)]
37    alias: Vec<String>,
38    #[darling(default)]
39    rename: Option<String>,
40    #[darling(default)]
41    skip: Option<bool>,
42}
43
44#[derive(darling::FromAttributes)]
45#[darling(attributes(avro))]
46struct VariantOptions {
47    #[darling(default)]
48    rename: Option<String>,
49}
50
51#[derive(darling::FromAttributes)]
52#[darling(attributes(avro))]
53struct NamedTypeOptions {
54    #[darling(default)]
55    namespace: Option<String>,
56    #[darling(default)]
57    doc: Option<String>,
58    #[darling(multiple)]
59    alias: Vec<String>,
60    #[darling(default)]
61    rename_all: Option<String>,
62}
63
64#[proc_macro_derive(AvroSchema, attributes(avro))]
65// Templated from Serde
66pub fn proc_macro_derive_avro_schema(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
67    let mut input = parse_macro_input!(input as DeriveInput);
68    derive_avro_schema(&mut input)
69        .unwrap_or_else(to_compile_errors)
70        .into()
71}
72
73fn derive_avro_schema(input: &mut DeriveInput) -> Result<TokenStream, Vec<syn::Error>> {
74    let named_type_options =
75        NamedTypeOptions::from_attributes(&input.attrs[..]).map_err(darling_to_syn)?;
76
77    let rename_all = parse_case(named_type_options.rename_all.as_deref(), input.span())?;
78
79    let full_schema_name = vec![named_type_options.namespace, Some(input.ident.to_string())]
80        .into_iter()
81        .flatten()
82        .collect::<Vec<String>>()
83        .join(".");
84    let schema_def = match &input.data {
85        syn::Data::Struct(s) => get_data_struct_schema_def(
86            &full_schema_name,
87            named_type_options
88                .doc
89                .or_else(|| extract_outer_doc(&input.attrs)),
90            named_type_options.alias,
91            rename_all,
92            s,
93            input.ident.span(),
94        )?,
95        syn::Data::Enum(e) => get_data_enum_schema_def(
96            &full_schema_name,
97            named_type_options
98                .doc
99                .or_else(|| extract_outer_doc(&input.attrs)),
100            named_type_options.alias,
101            rename_all,
102            e,
103            input.ident.span(),
104        )?,
105        _ => {
106            return Err(vec![syn::Error::new(
107                input.ident.span(),
108                "AvroSchema derive only works for structs and simple enums ",
109            )])
110        }
111    };
112    let ident = &input.ident;
113    let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
114    Ok(quote! {
115        #[automatically_derived]
116        impl #impl_generics apache_avro::schema::derive::AvroSchemaComponent for #ident #ty_generics #where_clause {
117            fn get_schema_in_ctxt(named_schemas: &mut std::collections::HashMap<apache_avro::schema::Name, apache_avro::schema::Schema>, enclosing_namespace: &Option<String>) -> apache_avro::schema::Schema {
118                let name =  apache_avro::schema::Name::new(#full_schema_name).expect(&format!("Unable to parse schema name {}", #full_schema_name)[..]).fully_qualified_name(enclosing_namespace);
119                let enclosing_namespace = &name.namespace;
120                if named_schemas.contains_key(&name) {
121                    apache_avro::schema::Schema::Ref{name: name.clone()}
122                } else {
123                    named_schemas.insert(name.clone(), apache_avro::schema::Schema::Ref{name: name.clone()});
124                    #schema_def
125                }
126            }
127        }
128    })
129}
130
131fn get_data_struct_schema_def(
132    full_schema_name: &str,
133    record_doc: Option<String>,
134    aliases: Vec<String>,
135    rename_all: RenameRule,
136    s: &syn::DataStruct,
137    error_span: Span,
138) -> Result<TokenStream, Vec<syn::Error>> {
139    let mut record_field_exprs = vec![];
140    match s.fields {
141        syn::Fields::Named(ref a) => {
142            let mut index: usize = 0;
143            for field in a.named.iter() {
144                let mut name = field.ident.as_ref().unwrap().to_string(); // we know everything has a name
145                if let Some(raw_name) = name.strip_prefix("r#") {
146                    name = raw_name.to_string();
147                }
148                let field_attrs =
149                    FieldOptions::from_attributes(&field.attrs[..]).map_err(darling_to_syn)?;
150                let doc =
151                    preserve_optional(field_attrs.doc.or_else(|| extract_outer_doc(&field.attrs)));
152                match (field_attrs.rename, rename_all) {
153                    (Some(rename), _) => {
154                        name = rename;
155                    }
156                    (None, rename_all) if !matches!(rename_all, RenameRule::None) => {
157                        name = rename_all.apply_to_field(&name);
158                    }
159                    _ => {}
160                }
161                if let Some(true) = field_attrs.skip {
162                    continue;
163                }
164                let default_value = match field_attrs.default {
165                    Some(default_value) => {
166                        let _: serde_json::Value = serde_json::from_str(&default_value[..])
167                            .map_err(|e| {
168                                vec![syn::Error::new(
169                                    field.ident.span(),
170                                    format!("Invalid avro default json: \n{e}"),
171                                )]
172                            })?;
173                        quote! {
174                            Some(serde_json::from_str(#default_value).expect(format!("Invalid JSON: {:?}", #default_value).as_str()))
175                        }
176                    }
177                    None => quote! { None },
178                };
179                let aliases = preserve_vec(field_attrs.alias);
180                let schema_expr = type_to_schema_expr(&field.ty)?;
181                let position = index;
182                record_field_exprs.push(quote! {
183                    apache_avro::schema::RecordField {
184                            name: #name.to_string(),
185                            doc: #doc,
186                            default: #default_value,
187                            aliases: #aliases,
188                            schema: #schema_expr,
189                            order: apache_avro::schema::RecordFieldOrder::Ascending,
190                            position: #position,
191                            custom_attributes: Default::default(),
192                        }
193                });
194                index += 1;
195            }
196        }
197        syn::Fields::Unnamed(_) => {
198            return Err(vec![syn::Error::new(
199                error_span,
200                "AvroSchema derive does not work for tuple structs",
201            )])
202        }
203        syn::Fields::Unit => {
204            return Err(vec![syn::Error::new(
205                error_span,
206                "AvroSchema derive does not work for unit structs",
207            )])
208        }
209    }
210    let record_doc = preserve_optional(record_doc);
211    let record_aliases = preserve_vec(aliases);
212    Ok(quote! {
213        let schema_fields = vec![#(#record_field_exprs),*];
214        let name = apache_avro::schema::Name::new(#full_schema_name).expect(&format!("Unable to parse struct name for schema {}", #full_schema_name)[..]);
215        let lookup: std::collections::BTreeMap<String, usize> = schema_fields
216            .iter()
217            .map(|field| (field.name.to_owned(), field.position))
218            .collect();
219        apache_avro::schema::Schema::Record(apache_avro::schema::RecordSchema {
220            name,
221            aliases: #record_aliases,
222            doc: #record_doc,
223            fields: schema_fields,
224            lookup,
225            attributes: Default::default(),
226        })
227    })
228}
229
230fn get_data_enum_schema_def(
231    full_schema_name: &str,
232    doc: Option<String>,
233    aliases: Vec<String>,
234    rename_all: RenameRule,
235    e: &syn::DataEnum,
236    error_span: Span,
237) -> Result<TokenStream, Vec<syn::Error>> {
238    let doc = preserve_optional(doc);
239    let enum_aliases = preserve_vec(aliases);
240    if e.variants.iter().all(|v| syn::Fields::Unit == v.fields) {
241        let default_value = default_enum_variant(e, error_span)?;
242        let default = preserve_optional(default_value);
243        let mut symbols = Vec::new();
244        for variant in &e.variants {
245            let field_attrs =
246                VariantOptions::from_attributes(&variant.attrs[..]).map_err(darling_to_syn)?;
247            let name = match (field_attrs.rename, rename_all) {
248                (Some(rename), _) => rename,
249                (None, rename_all) if !matches!(rename_all, RenameRule::None) => {
250                    rename_all.apply_to_variant(&variant.ident.to_string())
251                }
252                _ => variant.ident.to_string(),
253            };
254            symbols.push(name);
255        }
256        Ok(quote! {
257            apache_avro::schema::Schema::Enum(apache_avro::schema::EnumSchema {
258                name: apache_avro::schema::Name::new(#full_schema_name).expect(&format!("Unable to parse enum name for schema {}", #full_schema_name)[..]),
259                aliases: #enum_aliases,
260                doc: #doc,
261                symbols: vec![#(#symbols.to_owned()),*],
262                default: #default,
263                attributes: Default::default(),
264            })
265        })
266    } else {
267        Err(vec![syn::Error::new(
268            error_span,
269            "AvroSchema derive does not work for enums with non unit structs",
270        )])
271    }
272}
273
274/// Takes in the Tokens of a type and returns the tokens of an expression with return type `Schema`
275fn type_to_schema_expr(ty: &Type) -> Result<TokenStream, Vec<syn::Error>> {
276    if let Type::Path(p) = ty {
277        let type_string = p.path.segments.last().unwrap().ident.to_string();
278
279        let schema = match &type_string[..] {
280            "bool" => quote! {apache_avro::schema::Schema::Boolean},
281            "i8" | "i16" | "i32" | "u8" | "u16" => quote! {apache_avro::schema::Schema::Int},
282            "u32" | "i64" => quote! {apache_avro::schema::Schema::Long},
283            "f32" => quote! {apache_avro::schema::Schema::Float},
284            "f64" => quote! {apache_avro::schema::Schema::Double},
285            "String" | "str" => quote! {apache_avro::schema::Schema::String},
286            "char" => {
287                return Err(vec![syn::Error::new_spanned(
288                    ty,
289                    "AvroSchema: Cannot guarantee successful deserialization of this type",
290                )])
291            }
292            "u64" => {
293                return Err(vec![syn::Error::new_spanned(
294                ty,
295                "Cannot guarantee successful serialization of this type due to overflow concerns",
296            )])
297            } // Can't guarantee serialization type
298            _ => {
299                // Fails when the type does not implement AvroSchemaComponent directly
300                // TODO check and error report with something like https://docs.rs/quote/1.0.15/quote/macro.quote_spanned.html#example
301                type_path_schema_expr(p)
302            }
303        };
304        Ok(schema)
305    } else if let Type::Array(ta) = ty {
306        let inner_schema_expr = type_to_schema_expr(&ta.elem)?;
307        Ok(quote! {apache_avro::schema::Schema::array(#inner_schema_expr)})
308    } else if let Type::Reference(tr) = ty {
309        type_to_schema_expr(&tr.elem)
310    } else {
311        Err(vec![syn::Error::new_spanned(
312            ty,
313            format!("Unable to generate schema for type: {ty:?}"),
314        )])
315    }
316}
317
318fn default_enum_variant(
319    data_enum: &syn::DataEnum,
320    error_span: Span,
321) -> Result<Option<String>, Vec<syn::Error>> {
322    match data_enum
323        .variants
324        .iter()
325        .filter(|v| v.attrs.iter().any(is_default_attr))
326        .collect::<Vec<_>>()
327    {
328        variants if variants.is_empty() => Ok(None),
329        single if single.len() == 1 => Ok(Some(single[0].ident.to_string())),
330        multiple => Err(vec![syn::Error::new(
331            error_span,
332            format!(
333                "Multiple defaults defined: {:?}",
334                multiple
335                    .iter()
336                    .map(|v| v.ident.to_string())
337                    .collect::<Vec<String>>()
338            ),
339        )]),
340    }
341}
342
343fn is_default_attr(attr: &Attribute) -> bool {
344    matches!(attr, Attribute { meta: Meta::Path(path), .. } if path.get_ident().map(Ident::to_string).as_deref() == Some("default"))
345}
346
347/// Generates the schema def expression for fully qualified type paths using the associated function
348/// - `A -> <A as apache_avro::schema::derive::AvroSchemaComponent>::get_schema_in_ctxt()`
349/// - `A<T> -> <A<T> as apache_avro::schema::derive::AvroSchemaComponent>::get_schema_in_ctxt()`
350fn type_path_schema_expr(p: &TypePath) -> TokenStream {
351    quote! {<#p as apache_avro::schema::derive::AvroSchemaComponent>::get_schema_in_ctxt(named_schemas, enclosing_namespace)}
352}
353
354/// Stolen from serde
355fn to_compile_errors(errors: Vec<syn::Error>) -> proc_macro2::TokenStream {
356    let compile_errors = errors.iter().map(syn::Error::to_compile_error);
357    quote!(#(#compile_errors)*)
358}
359
360fn extract_outer_doc(attributes: &[Attribute]) -> Option<String> {
361    let doc = attributes
362        .iter()
363        .filter(|attr| attr.style == AttrStyle::Outer && attr.path().is_ident("doc"))
364        .filter_map(|attr| {
365            let name_value = attr.meta.require_name_value();
366            match name_value {
367                Ok(name_value) => match &name_value.value {
368                    syn::Expr::Lit(expr_lit) => match expr_lit.lit {
369                        syn::Lit::Str(ref lit_str) => Some(lit_str.value().trim().to_string()),
370                        _ => None,
371                    },
372                    _ => None,
373                },
374                Err(_) => None,
375            }
376        })
377        .collect::<Vec<String>>()
378        .join("\n");
379    if doc.is_empty() {
380        None
381    } else {
382        Some(doc)
383    }
384}
385
386fn preserve_optional(op: Option<impl quote::ToTokens>) -> TokenStream {
387    match op {
388        Some(tt) => quote! {Some(#tt.into())},
389        None => quote! {None},
390    }
391}
392
393fn preserve_vec(op: Vec<impl quote::ToTokens>) -> TokenStream {
394    let items: Vec<TokenStream> = op.iter().map(|tt| quote! {#tt.into()}).collect();
395    if items.is_empty() {
396        quote! {None}
397    } else {
398        quote! {Some(vec![#(#items),*])}
399    }
400}
401
402fn darling_to_syn(e: darling::Error) -> Vec<syn::Error> {
403    let msg = format!("{e}");
404    let token_errors = e.write_errors();
405    vec![syn::Error::new(token_errors.span(), msg)]
406}
407
408fn parse_case(case: Option<&str>, span: Span) -> Result<RenameRule, Vec<syn::Error>> {
409    match case {
410        None => Ok(RenameRule::None),
411        Some(case) => {
412            Ok(RenameRule::from_str(case)
413                .map_err(|e| vec![syn::Error::new(span, e.to_string())])?)
414        }
415    }
416}
417
418#[cfg(test)]
419mod tests {
420    use super::*;
421    #[test]
422    fn basic_case() {
423        let test_struct = quote! {
424            struct A {
425                a: i32,
426                b: String
427            }
428        };
429
430        match syn::parse2::<DeriveInput>(test_struct) {
431            Ok(mut input) => {
432                assert!(derive_avro_schema(&mut input).is_ok())
433            }
434            Err(error) => panic!(
435                "Failed to parse as derive input when it should be able to. Error: {error:?}"
436            ),
437        };
438    }
439
440    #[test]
441    fn tuple_struct_unsupported() {
442        let test_tuple_struct = quote! {
443            struct B (i32, String);
444        };
445
446        match syn::parse2::<DeriveInput>(test_tuple_struct) {
447            Ok(mut input) => {
448                assert!(derive_avro_schema(&mut input).is_err())
449            }
450            Err(error) => panic!(
451                "Failed to parse as derive input when it should be able to. Error: {error:?}"
452            ),
453        };
454    }
455
456    #[test]
457    fn unit_struct_unsupported() {
458        let test_tuple_struct = quote! {
459            struct AbsoluteUnit;
460        };
461
462        match syn::parse2::<DeriveInput>(test_tuple_struct) {
463            Ok(mut input) => {
464                assert!(derive_avro_schema(&mut input).is_err())
465            }
466            Err(error) => panic!(
467                "Failed to parse as derive input when it should be able to. Error: {error:?}"
468            ),
469        };
470    }
471
472    #[test]
473    fn struct_with_optional() {
474        let struct_with_optional = quote! {
475            struct Test4 {
476                a : Option<i32>
477            }
478        };
479        match syn::parse2::<DeriveInput>(struct_with_optional) {
480            Ok(mut input) => {
481                assert!(derive_avro_schema(&mut input).is_ok())
482            }
483            Err(error) => panic!(
484                "Failed to parse as derive input when it should be able to. Error: {error:?}"
485            ),
486        };
487    }
488
489    #[test]
490    fn test_basic_enum() {
491        let basic_enum = quote! {
492            enum Basic {
493                A,
494                B,
495                C,
496                D
497            }
498        };
499        match syn::parse2::<DeriveInput>(basic_enum) {
500            Ok(mut input) => {
501                assert!(derive_avro_schema(&mut input).is_ok())
502            }
503            Err(error) => panic!(
504                "Failed to parse as derive input when it should be able to. Error: {error:?}"
505            ),
506        };
507    }
508
509    #[test]
510    fn avro_3687_basic_enum_with_default() {
511        let basic_enum = quote! {
512            enum Basic {
513                #[default]
514                A,
515                B,
516                C,
517                D
518            }
519        };
520        match syn::parse2::<DeriveInput>(basic_enum) {
521            Ok(mut input) => {
522                let derived = derive_avro_schema(&mut input);
523                assert!(derived.is_ok());
524                assert_eq!(derived.unwrap().to_string(), quote! {
525                    #[automatically_derived]
526                    impl apache_avro::schema::derive::AvroSchemaComponent for Basic {
527                        fn get_schema_in_ctxt(
528                            named_schemas: &mut std::collections::HashMap<
529                                apache_avro::schema::Name,
530                                apache_avro::schema::Schema
531                            >,
532                            enclosing_namespace: &Option<String>
533                        ) -> apache_avro::schema::Schema {
534                            let name = apache_avro::schema::Name::new("Basic")
535                                .expect(&format!("Unable to parse schema name {}", "Basic")[..])
536                                .fully_qualified_name(enclosing_namespace);
537                            let enclosing_namespace = &name.namespace;
538                            if named_schemas.contains_key(&name) {
539                                apache_avro::schema::Schema::Ref { name: name.clone() }
540                            } else {
541                                named_schemas.insert(
542                                    name.clone(),
543                                    apache_avro::schema::Schema::Ref { name: name.clone() }
544                                );
545                                apache_avro::schema::Schema::Enum(apache_avro::schema::EnumSchema {
546                                    name: apache_avro::schema::Name::new("Basic").expect(
547                                        &format!("Unable to parse enum name for schema {}", "Basic")[..]
548                                    ),
549                                    aliases: None,
550                                    doc: None,
551                                    symbols: vec![
552                                        "A".to_owned(),
553                                        "B".to_owned(),
554                                        "C".to_owned(),
555                                        "D".to_owned()
556                                    ],
557                                    default: Some("A".into()),
558                                    attributes: Default::default(),
559                                })
560                            }
561                        }
562                    }
563                }.to_string());
564            }
565            Err(error) => panic!(
566                "Failed to parse as derive input when it should be able to. Error: {error:?}"
567            ),
568        };
569    }
570
571    #[test]
572    fn avro_3687_basic_enum_with_default_twice() {
573        let non_basic_enum = quote! {
574            enum Basic {
575                #[default]
576                A,
577                B,
578                #[default]
579                C,
580                D
581            }
582        };
583        match syn::parse2::<DeriveInput>(non_basic_enum) {
584            Ok(mut input) => match derive_avro_schema(&mut input) {
585                Ok(_) => {
586                    panic!("Should not be able to derive schema for enum with multiple defaults")
587                }
588                Err(errors) => {
589                    assert_eq!(errors.len(), 1);
590                    assert_eq!(
591                        errors[0].to_string(),
592                        r#"Multiple defaults defined: ["A", "C"]"#
593                    );
594                }
595            },
596            Err(error) => panic!(
597                "Failed to parse as derive input when it should be able to. Error: {error:?}"
598            ),
599        };
600    }
601
602    #[test]
603    fn test_non_basic_enum() {
604        let non_basic_enum = quote! {
605            enum Basic {
606                A(i32),
607                B,
608                C,
609                D
610            }
611        };
612        match syn::parse2::<DeriveInput>(non_basic_enum) {
613            Ok(mut input) => {
614                assert!(derive_avro_schema(&mut input).is_err())
615            }
616            Err(error) => panic!(
617                "Failed to parse as derive input when it should be able to. Error: {error:?}"
618            ),
619        };
620    }
621
622    #[test]
623    fn test_namespace() {
624        let test_struct = quote! {
625            #[avro(namespace = "namespace.testing")]
626            struct A {
627                a: i32,
628                b: String
629            }
630        };
631
632        match syn::parse2::<DeriveInput>(test_struct) {
633            Ok(mut input) => {
634                let schema_token_stream = derive_avro_schema(&mut input);
635                assert!(&schema_token_stream.is_ok());
636                assert!(schema_token_stream
637                    .unwrap()
638                    .to_string()
639                    .contains("namespace.testing"))
640            }
641            Err(error) => panic!(
642                "Failed to parse as derive input when it should be able to. Error: {error:?}"
643            ),
644        };
645    }
646
647    #[test]
648    fn test_reference() {
649        let test_reference_struct = quote! {
650            struct A<'a> {
651                a: &'a Vec<i32>,
652                b: &'static str
653            }
654        };
655
656        match syn::parse2::<DeriveInput>(test_reference_struct) {
657            Ok(mut input) => {
658                assert!(derive_avro_schema(&mut input).is_ok())
659            }
660            Err(error) => panic!(
661                "Failed to parse as derive input when it should be able to. Error: {error:?}"
662            ),
663        };
664    }
665
666    #[test]
667    fn test_trait_cast() {
668        assert_eq!(type_path_schema_expr(&syn::parse2::<TypePath>(quote!{i32}).unwrap()).to_string(), quote!{<i32 as apache_avro::schema::derive::AvroSchemaComponent>::get_schema_in_ctxt(named_schemas, enclosing_namespace)}.to_string());
669        assert_eq!(type_path_schema_expr(&syn::parse2::<TypePath>(quote!{Vec<T>}).unwrap()).to_string(), quote!{<Vec<T> as apache_avro::schema::derive::AvroSchemaComponent>::get_schema_in_ctxt(named_schemas, enclosing_namespace)}.to_string());
670        assert_eq!(type_path_schema_expr(&syn::parse2::<TypePath>(quote!{AnyType}).unwrap()).to_string(), quote!{<AnyType as apache_avro::schema::derive::AvroSchemaComponent>::get_schema_in_ctxt(named_schemas, enclosing_namespace)}.to_string());
671    }
672
673    #[test]
674    fn test_avro_3709_record_field_attributes() {
675        let test_struct = quote! {
676            struct A {
677                #[avro(alias = "a1", alias = "a2", doc = "a doc", default = "123", rename = "a3")]
678                a: i32
679            }
680        };
681
682        match syn::parse2::<DeriveInput>(test_struct) {
683            Ok(mut input) => {
684                let schema_res = derive_avro_schema(&mut input);
685                let expected_token_stream = r#"let schema_fields = vec ! [apache_avro :: schema :: RecordField { name : "a3" . to_string () , doc : Some ("a doc" . into ()) , default : Some (serde_json :: from_str ("123") . expect (format ! ("Invalid JSON: {:?}" , "123") . as_str ())) , aliases : Some (vec ! ["a1" . into () , "a2" . into ()]) , schema : apache_avro :: schema :: Schema :: Int , order : apache_avro :: schema :: RecordFieldOrder :: Ascending , position : 0usize , custom_attributes : Default :: default () , }] ;"#;
686                let schema_token_stream = schema_res.unwrap().to_string();
687                assert!(schema_token_stream.contains(expected_token_stream));
688            }
689            Err(error) => panic!(
690                "Failed to parse as derive input when it should be able to. Error: {error:?}"
691            ),
692        };
693
694        let test_enum = quote! {
695            enum A {
696                #[avro(rename = "A3")]
697                Item1,
698            }
699        };
700
701        match syn::parse2::<DeriveInput>(test_enum) {
702            Ok(mut input) => {
703                let schema_res = derive_avro_schema(&mut input);
704                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 () }) ; apache_avro :: schema :: Schema :: Enum (apache_avro :: schema :: EnumSchema { name : apache_avro :: schema :: Name :: new ("A") . expect (& format ! ("Unable to parse enum name for schema {}" , "A") [..]) , aliases : None , doc : None , symbols : vec ! ["A3" . to_owned ()] , default : None , attributes : Default :: default () , })"#;
705                let schema_token_stream = schema_res.unwrap().to_string();
706                assert!(schema_token_stream.contains(expected_token_stream));
707            }
708            Err(error) => panic!(
709                "Failed to parse as derive input when it should be able to. Error: {error:?}"
710            ),
711        };
712    }
713
714    #[test]
715    fn test_avro_rs_207_rename_all_attribute() {
716        let test_struct = quote! {
717            #[avro(rename_all="SCREAMING_SNAKE_CASE")]
718            struct A {
719                item: i32,
720                double_item: i32
721            }
722        };
723
724        match syn::parse2::<DeriveInput>(test_struct) {
725            Ok(mut input) => {
726                let schema_res = derive_avro_schema(&mut input);
727                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_fields = vec ! [apache_avro :: schema :: RecordField { name : "ITEM" . to_string () , doc : None , default : None , aliases : None , schema : apache_avro :: schema :: Schema :: Int , order : apache_avro :: schema :: RecordFieldOrder :: Ascending , position : 0usize , custom_attributes : Default :: default () , } , apache_avro :: schema :: RecordField { name : "DOUBLE_ITEM" . to_string () , doc : None , default : None , aliases : None , schema : apache_avro :: schema :: Schema :: Int , order : apache_avro :: schema :: RecordFieldOrder :: Ascending , position : 1usize , custom_attributes : Default :: default () , }] ;"#;
728                let schema_token_stream = schema_res.unwrap().to_string();
729                assert!(schema_token_stream.contains(expected_token_stream));
730            }
731            Err(error) => panic!(
732                "Failed to parse as derive input when it should be able to. Error: {error:?}"
733            ),
734        };
735
736        let test_enum = quote! {
737            #[avro(rename_all="SCREAMING_SNAKE_CASE")]
738            enum B {
739                Item,
740                DoubleItem,
741            }
742        };
743
744        match syn::parse2::<DeriveInput>(test_enum) {
745            Ok(mut input) => {
746                let schema_res = derive_avro_schema(&mut input);
747                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 () }) ; apache_avro :: schema :: Schema :: Enum (apache_avro :: schema :: EnumSchema { name : apache_avro :: schema :: Name :: new ("B") . expect (& format ! ("Unable to parse enum name for schema {}" , "B") [..]) , aliases : None , doc : None , symbols : vec ! ["ITEM" . to_owned () , "DOUBLE_ITEM" . to_owned ()] , default : None , attributes : Default :: default () , })"#;
748                let schema_token_stream = schema_res.unwrap().to_string();
749                assert!(schema_token_stream.contains(expected_token_stream));
750            }
751            Err(error) => panic!(
752                "Failed to parse as derive input when it should be able to. Error: {error:?}"
753            ),
754        };
755    }
756
757    #[test]
758    fn test_avro_rs_207_rename_attr_has_priority_over_rename_all_attribute() {
759        let test_struct = quote! {
760            #[avro(rename_all="SCREAMING_SNAKE_CASE")]
761            struct A {
762                item: i32,
763                #[avro(rename="DoubleItem")]
764                double_item: i32
765            }
766        };
767
768        match syn::parse2::<DeriveInput>(test_struct) {
769            Ok(mut input) => {
770                let schema_res = derive_avro_schema(&mut input);
771                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_fields = vec ! [apache_avro :: schema :: RecordField { name : "ITEM" . to_string () , doc : None , default : None , aliases : None , schema : apache_avro :: schema :: Schema :: Int , order : apache_avro :: schema :: RecordFieldOrder :: Ascending , position : 0usize , custom_attributes : Default :: default () , } , apache_avro :: schema :: RecordField { name : "DoubleItem" . to_string () , doc : None , default : None , aliases : None , schema : apache_avro :: schema :: Schema :: Int , order : apache_avro :: schema :: RecordFieldOrder :: Ascending , position : 1usize , custom_attributes : Default :: default () , }] ;"#;
772                let schema_token_stream = schema_res.unwrap().to_string();
773                assert!(schema_token_stream.contains(expected_token_stream));
774            }
775            Err(error) => panic!(
776                "Failed to parse as derive input when it should be able to. Error: {error:?}"
777            ),
778        };
779    }
780}