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