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