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
18#![cfg_attr(nightly, feature(proc_macro_diagnostic))]
19
20mod attributes;
21mod case;
22
23use proc_macro2::{Span, TokenStream};
24use quote::quote;
25use syn::{
26    Attribute, DataEnum, DataStruct, DeriveInput, Expr, Field, Fields, Generics, Ident, Meta, Type,
27    parse_macro_input, spanned::Spanned,
28};
29
30use crate::{
31    attributes::{FieldOptions, NamedTypeOptions, VariantOptions, With},
32    case::RenameRule,
33};
34
35#[proc_macro_derive(AvroSchema, attributes(avro, serde))]
36// Templated from Serde
37pub fn proc_macro_derive_avro_schema(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
38    let input = parse_macro_input!(input as DeriveInput);
39    derive_avro_schema(input)
40        .unwrap_or_else(to_compile_errors)
41        .into()
42}
43
44fn derive_avro_schema(input: DeriveInput) -> Result<TokenStream, Vec<syn::Error>> {
45    // It would be nice to parse the attributes before the `match`, but we first need to validate that `input` is not a union.
46    // Otherwise a user could get errors related to the attributes and after fixing those get an error because the attributes were on a union.
47    let input_span = input.span();
48    match input.data {
49        syn::Data::Struct(data_struct) => {
50            let named_type_options = NamedTypeOptions::new(&input.ident, &input.attrs, input_span)?;
51            let inner = if named_type_options.transparent {
52                get_transparent_struct_schema_def(data_struct.fields, input_span)?
53            } else {
54                let schema_def =
55                    get_struct_schema_def(&named_type_options, data_struct, input.ident.span())?;
56                handle_named_schemas(named_type_options.name, schema_def)
57            };
58            Ok(create_trait_definition(input.ident, &input.generics, inner))
59        }
60        syn::Data::Enum(data_enum) => {
61            let named_type_options = NamedTypeOptions::new(&input.ident, &input.attrs, input_span)?;
62            if named_type_options.transparent {
63                return Err(vec![syn::Error::new(
64                    input_span,
65                    "AvroSchema: `#[serde(transparent)]` is only supported on structs",
66                )]);
67            }
68            let schema_def =
69                get_data_enum_schema_def(&named_type_options, data_enum, input.ident.span())?;
70            let inner = handle_named_schemas(named_type_options.name, schema_def);
71            Ok(create_trait_definition(input.ident, &input.generics, inner))
72        }
73        syn::Data::Union(_) => Err(vec![syn::Error::new(
74            input_span,
75            "AvroSchema: derive only works for structs and simple enums",
76        )]),
77    }
78}
79
80/// Generate the trait definition with the correct generics
81fn create_trait_definition(
82    ident: Ident,
83    generics: &Generics,
84    implementation: TokenStream,
85) -> TokenStream {
86    let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
87    quote! {
88        #[automatically_derived]
89        impl #impl_generics apache_avro::AvroSchemaComponent for #ident #ty_generics #where_clause {
90            fn get_schema_in_ctxt(named_schemas: &mut apache_avro::schema::Names, enclosing_namespace: &Option<String>) -> apache_avro::schema::Schema {
91                #implementation
92            }
93        }
94    }
95}
96
97/// Generate the code to check `named_schemas` if this schema already exist
98fn handle_named_schemas(full_schema_name: String, schema_def: TokenStream) -> TokenStream {
99    quote! {
100        let name = apache_avro::schema::Name::new(#full_schema_name).expect(concat!("Unable to parse schema name ", #full_schema_name)).fully_qualified_name(enclosing_namespace);
101        if named_schemas.contains_key(&name) {
102            apache_avro::schema::Schema::Ref{name}
103        } else {
104            let enclosing_namespace = &name.namespace;
105            // This is needed because otherwise recursive types will recurse forever and cause a stack overflow
106            // TODO: Breaking change to AvroSchemaComponent, have named_schemas be a set instead
107            named_schemas.insert(name.clone(), apache_avro::schema::Schema::Ref{name: name.clone()});
108            let schema = #schema_def;
109            named_schemas.insert(name, schema.clone());
110            schema
111        }
112    }
113}
114
115/// Generate a schema definition for a struct.
116fn get_struct_schema_def(
117    container_attrs: &NamedTypeOptions,
118    data_struct: DataStruct,
119    ident_span: Span,
120) -> Result<TokenStream, Vec<syn::Error>> {
121    let mut record_field_exprs = vec![];
122    match data_struct.fields {
123        Fields::Named(a) => {
124            for field in a.named {
125                let mut name = field
126                    .ident
127                    .as_ref()
128                    .expect("Field must have a name")
129                    .to_string();
130                if let Some(raw_name) = name.strip_prefix("r#") {
131                    name = raw_name.to_string();
132                }
133                let field_attrs = FieldOptions::new(&field.attrs, field.span())?;
134                let doc = preserve_optional(field_attrs.doc);
135                match (field_attrs.rename, container_attrs.rename_all) {
136                    (Some(rename), _) => {
137                        name = rename;
138                    }
139                    (None, rename_all) if rename_all != RenameRule::None => {
140                        name = rename_all.apply_to_field(&name);
141                    }
142                    _ => {}
143                }
144                if field_attrs.skip {
145                    continue;
146                } else if field_attrs.flatten {
147                    // Inline the fields of the child record at runtime, as we don't have access to
148                    // the schema here.
149                    let flatten_ty = &field.ty;
150                    record_field_exprs.push(quote! {
151                        if let ::apache_avro::schema::Schema::Record(::apache_avro::schema::RecordSchema { fields, .. }) = #flatten_ty::get_schema() {
152                            for mut field in fields {
153                                field.position = schema_fields.len();
154                                schema_fields.push(field)
155                            }
156                        } else {
157                            panic!("Can only flatten RecordSchema, got {:?}", #flatten_ty::get_schema())
158                        }
159                    });
160
161                    // Don't add this field as it's been replaced by the child record fields
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 = aliases(&field_attrs.alias);
180                let schema_expr = get_field_schema_expr(&field, field_attrs.with)?;
181                record_field_exprs.push(quote! {
182                    schema_fields.push(::apache_avro::schema::RecordField {
183                        name: #name.to_string(),
184                        doc: #doc,
185                        default: #default_value,
186                        aliases: #aliases,
187                        schema: #schema_expr,
188                        order: ::apache_avro::schema::RecordFieldOrder::Ascending,
189                        position: schema_fields.len(),
190                        custom_attributes: Default::default(),
191                    });
192                });
193            }
194        }
195        Fields::Unnamed(_) => {
196            return Err(vec![syn::Error::new(
197                ident_span,
198                "AvroSchema derive does not work for tuple structs",
199            )]);
200        }
201        Fields::Unit => {
202            return Err(vec![syn::Error::new(
203                ident_span,
204                "AvroSchema derive does not work for unit structs",
205            )]);
206        }
207    }
208
209    let record_doc = preserve_optional(container_attrs.doc.as_ref());
210    let record_aliases = aliases(&container_attrs.aliases);
211    let full_schema_name = &container_attrs.name;
212
213    // When flatten is involved, there will be more but we don't know how many. This optimises for
214    // the most common case where there is no flatten.
215    let minimum_fields = record_field_exprs.len();
216
217    Ok(quote! {
218        {
219            let mut schema_fields = Vec::with_capacity(#minimum_fields);
220            #(#record_field_exprs)*
221            let schema_field_set: ::std::collections::HashSet<_> = schema_fields.iter().map(|rf| &rf.name).collect();
222            assert_eq!(schema_fields.len(), schema_field_set.len(), "Duplicate field names found: {schema_fields:?}");
223            let name = apache_avro::schema::Name::new(#full_schema_name).expect(&format!("Unable to parse struct name for schema {}", #full_schema_name)[..]);
224            let lookup: std::collections::BTreeMap<String, usize> = schema_fields
225                .iter()
226                .map(|field| (field.name.to_owned(), field.position))
227                .collect();
228            apache_avro::schema::Schema::Record(apache_avro::schema::RecordSchema {
229                name,
230                aliases: #record_aliases,
231                doc: #record_doc,
232                fields: schema_fields,
233                lookup,
234                attributes: Default::default(),
235            })
236        }
237    })
238}
239
240/// Use the schema definition of the only field in the struct as the schema
241fn get_transparent_struct_schema_def(
242    fields: Fields,
243    input_span: Span,
244) -> Result<TokenStream, Vec<syn::Error>> {
245    match fields {
246        Fields::Named(fields_named) => {
247            let mut found = None;
248            for field in fields_named.named {
249                let attrs = FieldOptions::new(&field.attrs, field.span())?;
250                if attrs.skip {
251                    continue;
252                }
253                if found.replace((field, attrs)).is_some() {
254                    return Err(vec![syn::Error::new(
255                        input_span,
256                        "AvroSchema: #[serde(transparent)] is only allowed on structs with one unskipped field",
257                    )]);
258                }
259            }
260
261            if let Some((field, attrs)) = found {
262                get_field_schema_expr(&field, attrs.with)
263            } else {
264                Err(vec![syn::Error::new(
265                    input_span,
266                    "AvroSchema: #[serde(transparent)] is only allowed on structs with one unskipped field",
267                )])
268            }
269        }
270        Fields::Unnamed(_) => Err(vec![syn::Error::new(
271            input_span,
272            "AvroSchema: derive does not work for tuple structs",
273        )]),
274        Fields::Unit => Err(vec![syn::Error::new(
275            input_span,
276            "AvroSchema: derive does not work for unit structs",
277        )]),
278    }
279}
280
281fn get_field_schema_expr(field: &Field, with: With) -> Result<TokenStream, Vec<syn::Error>> {
282    match with {
283        With::Trait => Ok(type_to_schema_expr(&field.ty)?),
284        With::Serde(path) => {
285            Ok(quote! { #path::get_schema_in_ctxt(named_schemas, enclosing_namespace) })
286        }
287        With::Expr(Expr::Closure(closure)) => {
288            if closure.inputs.is_empty() {
289                Ok(quote! { (#closure)() })
290            } else {
291                Err(vec![syn::Error::new(
292                    field.span(),
293                    "Expected closure with 0 parameters",
294                )])
295            }
296        }
297        With::Expr(Expr::Path(path)) => Ok(quote! { #path(named_schemas, enclosing_namespace) }),
298        With::Expr(_expr) => Err(vec![syn::Error::new(
299            field.span(),
300            "Invalid expression, expected function or closure",
301        )]),
302    }
303}
304
305/// Generate a schema definition for a enum.
306fn get_data_enum_schema_def(
307    container_attrs: &NamedTypeOptions,
308    data_enum: DataEnum,
309    ident_span: Span,
310) -> Result<TokenStream, Vec<syn::Error>> {
311    let doc = preserve_optional(container_attrs.doc.as_ref());
312    let enum_aliases = aliases(&container_attrs.aliases);
313    if data_enum.variants.iter().all(|v| Fields::Unit == v.fields) {
314        let default_value = default_enum_variant(&data_enum, ident_span)?;
315        let default = preserve_optional(default_value);
316        let mut symbols = Vec::new();
317        for variant in &data_enum.variants {
318            let field_attrs = VariantOptions::new(&variant.attrs, variant.span())?;
319            let name = match (field_attrs.rename, container_attrs.rename_all) {
320                (Some(rename), _) => rename,
321                (None, rename_all) if !matches!(rename_all, RenameRule::None) => {
322                    rename_all.apply_to_variant(&variant.ident.to_string())
323                }
324                _ => variant.ident.to_string(),
325            };
326            symbols.push(name);
327        }
328        let full_schema_name = &container_attrs.name;
329        Ok(quote! {
330            apache_avro::schema::Schema::Enum(apache_avro::schema::EnumSchema {
331                name: apache_avro::schema::Name::new(#full_schema_name).expect(&format!("Unable to parse enum name for schema {}", #full_schema_name)[..]),
332                aliases: #enum_aliases,
333                doc: #doc,
334                symbols: vec![#(#symbols.to_owned()),*],
335                default: #default,
336                attributes: Default::default(),
337            })
338        })
339    } else {
340        Err(vec![syn::Error::new(
341            ident_span,
342            "AvroSchema: derive does not work for enums with non unit structs",
343        )])
344    }
345}
346
347/// Takes in the Tokens of a type and returns the tokens of an expression with return type `Schema`
348fn type_to_schema_expr(ty: &Type) -> Result<TokenStream, Vec<syn::Error>> {
349    match ty {
350        Type::Array(_) | Type::Slice(_) | Type::Path(_) | Type::Reference(_) => Ok(
351            quote! {<#ty as apache_avro::AvroSchemaComponent>::get_schema_in_ctxt(named_schemas, enclosing_namespace)},
352        ),
353        Type::Ptr(_) => Err(vec![syn::Error::new_spanned(
354            ty,
355            "AvroSchema: derive does not support raw pointers",
356        )]),
357        Type::Tuple(_) => Err(vec![syn::Error::new_spanned(
358            ty,
359            "AvroSchema: derive does not support tuples",
360        )]),
361        _ => Err(vec![syn::Error::new_spanned(
362            ty,
363            format!(
364                "AvroSchema: Unexpected type encountered! Please open an issue if this kind of type should be supported: {ty:?}"
365            ),
366        )]),
367    }
368}
369
370fn default_enum_variant(
371    data_enum: &syn::DataEnum,
372    error_span: Span,
373) -> Result<Option<String>, Vec<syn::Error>> {
374    match data_enum
375        .variants
376        .iter()
377        .filter(|v| v.attrs.iter().any(is_default_attr))
378        .collect::<Vec<_>>()
379    {
380        variants if variants.is_empty() => Ok(None),
381        single if single.len() == 1 => Ok(Some(single[0].ident.to_string())),
382        multiple => Err(vec![syn::Error::new(
383            error_span,
384            format!(
385                "Multiple defaults defined: {:?}",
386                multiple
387                    .iter()
388                    .map(|v| v.ident.to_string())
389                    .collect::<Vec<String>>()
390            ),
391        )]),
392    }
393}
394
395fn is_default_attr(attr: &Attribute) -> bool {
396    matches!(attr, Attribute { meta: Meta::Path(path), .. } if path.get_ident().map(Ident::to_string).as_deref() == Some("default"))
397}
398
399/// Stolen from serde
400fn to_compile_errors(errors: Vec<syn::Error>) -> proc_macro2::TokenStream {
401    let compile_errors = errors.iter().map(syn::Error::to_compile_error);
402    quote!(#(#compile_errors)*)
403}
404
405fn preserve_optional(op: Option<impl quote::ToTokens>) -> TokenStream {
406    match op {
407        Some(tt) => quote! {Some(#tt.into())},
408        None => quote! {None},
409    }
410}
411
412fn aliases(op: &[impl quote::ToTokens]) -> TokenStream {
413    let items: Vec<TokenStream> = op
414        .iter()
415        .map(|tt| quote! {#tt.try_into().expect("Alias is invalid")})
416        .collect();
417    if items.is_empty() {
418        quote! {None}
419    } else {
420        quote! {Some(vec![#(#items),*])}
421    }
422}
423
424#[cfg(test)]
425mod tests {
426    use super::*;
427    use pretty_assertions::assert_eq;
428
429    #[test]
430    fn basic_case() {
431        let test_struct = quote! {
432            struct A {
433                a: i32,
434                b: String
435            }
436        };
437
438        match syn::parse2::<DeriveInput>(test_struct) {
439            Ok(input) => {
440                assert!(derive_avro_schema(input).is_ok())
441            }
442            Err(error) => panic!(
443                "Failed to parse as derive input when it should be able to. Error: {error:?}"
444            ),
445        };
446    }
447
448    #[test]
449    fn tuple_struct_unsupported() {
450        let test_tuple_struct = quote! {
451            struct B (i32, String);
452        };
453
454        match syn::parse2::<DeriveInput>(test_tuple_struct) {
455            Ok(input) => {
456                assert!(derive_avro_schema(input).is_err())
457            }
458            Err(error) => panic!(
459                "Failed to parse as derive input when it should be able to. Error: {error:?}"
460            ),
461        };
462    }
463
464    #[test]
465    fn unit_struct_unsupported() {
466        let test_tuple_struct = quote! {
467            struct AbsoluteUnit;
468        };
469
470        match syn::parse2::<DeriveInput>(test_tuple_struct) {
471            Ok(input) => {
472                assert!(derive_avro_schema(input).is_err())
473            }
474            Err(error) => panic!(
475                "Failed to parse as derive input when it should be able to. Error: {error:?}"
476            ),
477        };
478    }
479
480    #[test]
481    fn struct_with_optional() {
482        let struct_with_optional = quote! {
483            struct Test4 {
484                a : Option<i32>
485            }
486        };
487        match syn::parse2::<DeriveInput>(struct_with_optional) {
488            Ok(input) => {
489                assert!(derive_avro_schema(input).is_ok())
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 test_basic_enum() {
499        let basic_enum = quote! {
500            enum Basic {
501                A,
502                B,
503                C,
504                D
505            }
506        };
507        match syn::parse2::<DeriveInput>(basic_enum) {
508            Ok(input) => {
509                assert!(derive_avro_schema(input).is_ok())
510            }
511            Err(error) => panic!(
512                "Failed to parse as derive input when it should be able to. Error: {error:?}"
513            ),
514        };
515    }
516
517    #[test]
518    fn avro_3687_basic_enum_with_default() {
519        let basic_enum = quote! {
520            enum Basic {
521                #[default]
522                A,
523                B,
524                C,
525                D
526            }
527        };
528        match syn::parse2::<DeriveInput>(basic_enum) {
529            Ok(input) => {
530                let derived = derive_avro_schema(input);
531                assert!(derived.is_ok());
532                assert_eq!(derived.unwrap().to_string(), quote! {
533                    #[automatically_derived]
534                    impl apache_avro::AvroSchemaComponent for Basic {
535                        fn get_schema_in_ctxt(
536                            named_schemas: &mut apache_avro::schema::Names,
537                            enclosing_namespace: &Option<String>
538                        ) -> apache_avro::schema::Schema {
539                            let name = apache_avro::schema::Name::new("Basic")
540                                .expect(concat!("Unable to parse schema name ", "Basic"))
541                                .fully_qualified_name(enclosing_namespace);
542                            if named_schemas.contains_key(&name) {
543                                apache_avro::schema::Schema::Ref { name }
544                            } else {
545                                let enclosing_namespace = &name.namespace;
546                                named_schemas.insert(name.clone(), apache_avro::schema::Schema::Ref{name: name.clone()});
547                                let schema =
548                                apache_avro::schema::Schema::Enum(apache_avro::schema::EnumSchema {
549                                    name: apache_avro::schema::Name::new("Basic").expect(
550                                        &format!("Unable to parse enum name for schema {}", "Basic")[..]
551                                    ),
552                                    aliases: None,
553                                    doc: None,
554                                    symbols: vec![
555                                        "A".to_owned(),
556                                        "B".to_owned(),
557                                        "C".to_owned(),
558                                        "D".to_owned()
559                                    ],
560                                    default: Some("A".into()),
561                                    attributes: Default::default(),
562                                });
563                                named_schemas.insert(name, schema.clone());
564                                schema
565                            }
566                        }
567                    }
568                }.to_string());
569            }
570            Err(error) => panic!(
571                "Failed to parse as derive input when it should be able to. Error: {error:?}"
572            ),
573        };
574    }
575
576    #[test]
577    fn avro_3687_basic_enum_with_default_twice() {
578        let non_basic_enum = quote! {
579            enum Basic {
580                #[default]
581                A,
582                B,
583                #[default]
584                C,
585                D
586            }
587        };
588        match syn::parse2::<DeriveInput>(non_basic_enum) {
589            Ok(input) => match derive_avro_schema(input) {
590                Ok(_) => {
591                    panic!("Should not be able to derive schema for enum with multiple defaults")
592                }
593                Err(errors) => {
594                    assert_eq!(errors.len(), 1);
595                    assert_eq!(
596                        errors[0].to_string(),
597                        r#"Multiple defaults defined: ["A", "C"]"#
598                    );
599                }
600            },
601            Err(error) => panic!(
602                "Failed to parse as derive input when it should be able to. Error: {error:?}"
603            ),
604        };
605    }
606
607    #[test]
608    fn test_non_basic_enum() {
609        let non_basic_enum = quote! {
610            enum Basic {
611                A(i32),
612                B,
613                C,
614                D
615            }
616        };
617        match syn::parse2::<DeriveInput>(non_basic_enum) {
618            Ok(input) => {
619                assert!(derive_avro_schema(input).is_err())
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_namespace() {
629        let test_struct = quote! {
630            #[avro(namespace = "namespace.testing")]
631            struct A {
632                a: i32,
633                b: String
634            }
635        };
636
637        match syn::parse2::<DeriveInput>(test_struct) {
638            Ok(input) => {
639                let schema_token_stream = derive_avro_schema(input);
640                assert!(&schema_token_stream.is_ok());
641                assert!(
642                    schema_token_stream
643                        .unwrap()
644                        .to_string()
645                        .contains("namespace.testing")
646                )
647            }
648            Err(error) => panic!(
649                "Failed to parse as derive input when it should be able to. Error: {error:?}"
650            ),
651        };
652    }
653
654    #[test]
655    fn test_reference() {
656        let test_reference_struct = quote! {
657            struct A<'a> {
658                a: &'a Vec<i32>,
659                b: &'static str
660            }
661        };
662
663        match syn::parse2::<DeriveInput>(test_reference_struct) {
664            Ok(input) => {
665                assert!(derive_avro_schema(input).is_ok())
666            }
667            Err(error) => panic!(
668                "Failed to parse as derive input when it should be able to. Error: {error:?}"
669            ),
670        };
671    }
672
673    #[test]
674    fn test_trait_cast() {
675        assert_eq!(type_to_schema_expr(&syn::parse2::<Type>(quote!{i32}).unwrap()).unwrap().to_string(), quote!{<i32 as apache_avro::AvroSchemaComponent>::get_schema_in_ctxt(named_schemas, enclosing_namespace)}.to_string());
676        assert_eq!(type_to_schema_expr(&syn::parse2::<Type>(quote!{Vec<T>}).unwrap()).unwrap().to_string(), quote!{<Vec<T> as apache_avro::AvroSchemaComponent>::get_schema_in_ctxt(named_schemas, enclosing_namespace)}.to_string());
677        assert_eq!(type_to_schema_expr(&syn::parse2::<Type>(quote!{AnyType}).unwrap()).unwrap().to_string(), quote!{<AnyType as apache_avro::AvroSchemaComponent>::get_schema_in_ctxt(named_schemas, enclosing_namespace)}.to_string());
678    }
679
680    #[test]
681    fn test_avro_3709_record_field_attributes() {
682        let test_struct = quote! {
683            struct A {
684                #[serde(alias = "a1", alias = "a2", rename = "a3")]
685                #[avro(doc = "a doc", default = "123")]
686                a: i32
687            }
688        };
689
690        match syn::parse2::<DeriveInput>(test_struct) {
691            Ok(input) => {
692                let schema_res = derive_avro_schema(input);
693                let expected_token_stream = r#"# [automatically_derived] impl apache_avro :: AvroSchemaComponent for A { fn get_schema_in_ctxt (named_schemas : & mut apache_avro :: schema :: Names , enclosing_namespace : & Option < String >) -> apache_avro :: schema :: Schema { let name = apache_avro :: schema :: Name :: new ("A") . expect (concat ! ("Unable to parse schema name " , "A")) . fully_qualified_name (enclosing_namespace) ; if named_schemas . contains_key (& name) { apache_avro :: schema :: Schema :: Ref { name } } else { let enclosing_namespace = & name . namespace ; named_schemas . insert (name . clone () , apache_avro :: schema :: Schema :: Ref { name : name . clone () }) ; let schema = { 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" . try_into () . expect ("Alias is invalid") , "a2" . try_into () . expect ("Alias is invalid")]) , schema : < i32 as apache_avro :: AvroSchemaComponent > :: get_schema_in_ctxt (named_schemas , enclosing_namespace) , order : :: apache_avro :: schema :: RecordFieldOrder :: Ascending , position : schema_fields . len () , custom_attributes : Default :: default () , }) ; let schema_field_set : :: std :: collections :: HashSet < _ > = schema_fields . iter () . map (| rf | & rf . name) . collect () ; assert_eq ! (schema_fields . len () , schema_field_set . len () , "Duplicate field names found: {schema_fields:?}") ; let name = apache_avro :: schema :: Name :: new ("A") . expect (& format ! ("Unable to parse struct name for schema {}" , "A") [..]) ; let lookup : std :: collections :: BTreeMap < String , usize > = schema_fields . iter () . map (| field | (field . name . to_owned () , field . position)) . collect () ; apache_avro :: schema :: Schema :: Record (apache_avro :: schema :: RecordSchema { name , aliases : None , doc : None , fields : schema_fields , lookup , attributes : Default :: default () , }) } ; named_schemas . insert (name , schema . clone ()) ; schema } } }"#;
694                let schema_token_stream = schema_res.unwrap().to_string();
695                assert_eq!(schema_token_stream, expected_token_stream);
696            }
697            Err(error) => panic!(
698                "Failed to parse as derive input when it should be able to. Error: {error:?}"
699            ),
700        };
701
702        let test_enum = quote! {
703            enum A {
704                #[serde(rename = "A3")]
705                Item1,
706            }
707        };
708
709        match syn::parse2::<DeriveInput>(test_enum) {
710            Ok(input) => {
711                let schema_res = derive_avro_schema(input);
712                let expected_token_stream = r#"# [automatically_derived] impl apache_avro :: AvroSchemaComponent for A { fn get_schema_in_ctxt (named_schemas : & mut apache_avro :: schema :: Names , enclosing_namespace : & Option < String >) -> apache_avro :: schema :: Schema { let name = apache_avro :: schema :: Name :: new ("A") . expect (concat ! ("Unable to parse schema name " , "A")) . fully_qualified_name (enclosing_namespace) ; if named_schemas . contains_key (& name) { apache_avro :: schema :: Schema :: Ref { name } } else { let enclosing_namespace = & name . namespace ; named_schemas . insert (name . clone () , apache_avro :: schema :: Schema :: Ref { name : name . clone () }) ; let schema = 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 () , }) ; named_schemas . insert (name , schema . clone ()) ; schema } } }"#;
713                let schema_token_stream = schema_res.unwrap().to_string();
714                assert_eq!(schema_token_stream, 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
722    #[test]
723    fn test_avro_rs_207_rename_all_attribute() {
724        let test_struct = quote! {
725            #[serde(rename_all="SCREAMING_SNAKE_CASE")]
726            struct A {
727                item: i32,
728                double_item: i32
729            }
730        };
731
732        match syn::parse2::<DeriveInput>(test_struct) {
733            Ok(input) => {
734                let schema_res = derive_avro_schema(input);
735                let expected_token_stream = r#"# [automatically_derived] impl apache_avro :: AvroSchemaComponent for A { fn get_schema_in_ctxt (named_schemas : & mut apache_avro :: schema :: Names , enclosing_namespace : & Option < String >) -> apache_avro :: schema :: Schema { let name = apache_avro :: schema :: Name :: new ("A") . expect (concat ! ("Unable to parse schema name " , "A")) . fully_qualified_name (enclosing_namespace) ; if named_schemas . contains_key (& name) { apache_avro :: schema :: Schema :: Ref { name } } else { let enclosing_namespace = & name . namespace ; named_schemas . insert (name . clone () , apache_avro :: schema :: Schema :: Ref { name : name . clone () }) ; let schema = { 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 : < i32 as apache_avro :: AvroSchemaComponent > :: get_schema_in_ctxt (named_schemas , enclosing_namespace) , 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 : < i32 as apache_avro :: AvroSchemaComponent > :: get_schema_in_ctxt (named_schemas , enclosing_namespace) , order : :: apache_avro :: schema :: RecordFieldOrder :: Ascending , position : schema_fields . len () , custom_attributes : Default :: default () , }) ; let schema_field_set : :: std :: collections :: HashSet < _ > = schema_fields . iter () . map (| rf | & rf . name) . collect () ; assert_eq ! (schema_fields . len () , schema_field_set . len () , "Duplicate field names found: {schema_fields:?}") ; let name = apache_avro :: schema :: Name :: new ("A") . expect (& format ! ("Unable to parse struct name for schema {}" , "A") [..]) ; let lookup : std :: collections :: BTreeMap < String , usize > = schema_fields . iter () . map (| field | (field . name . to_owned () , field . position)) . collect () ; apache_avro :: schema :: Schema :: Record (apache_avro :: schema :: RecordSchema { name , aliases : None , doc : None , fields : schema_fields , lookup , attributes : Default :: default () , }) } ; named_schemas . insert (name , schema . clone ()) ; schema } } }"#;
736                let schema_token_stream = schema_res.unwrap().to_string();
737                assert_eq!(schema_token_stream, expected_token_stream);
738            }
739            Err(error) => panic!(
740                "Failed to parse as derive input when it should be able to. Error: {error:?}"
741            ),
742        };
743
744        let test_enum = quote! {
745            #[serde(rename_all="SCREAMING_SNAKE_CASE")]
746            enum B {
747                Item,
748                DoubleItem,
749            }
750        };
751
752        match syn::parse2::<DeriveInput>(test_enum) {
753            Ok(input) => {
754                let schema_res = derive_avro_schema(input);
755                let expected_token_stream = r#"# [automatically_derived] impl apache_avro :: AvroSchemaComponent for B { fn get_schema_in_ctxt (named_schemas : & mut apache_avro :: schema :: Names , enclosing_namespace : & Option < String >) -> apache_avro :: schema :: Schema { let name = apache_avro :: schema :: Name :: new ("B") . expect (concat ! ("Unable to parse schema name " , "B")) . fully_qualified_name (enclosing_namespace) ; if named_schemas . contains_key (& name) { apache_avro :: schema :: Schema :: Ref { name } } else { let enclosing_namespace = & name . namespace ; named_schemas . insert (name . clone () , apache_avro :: schema :: Schema :: Ref { name : name . clone () }) ; let schema = 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 () , }) ; named_schemas . insert (name , schema . clone ()) ; schema } } }"#;
756                let schema_token_stream = schema_res.unwrap().to_string();
757                assert_eq!(schema_token_stream, expected_token_stream);
758            }
759            Err(error) => panic!(
760                "Failed to parse as derive input when it should be able to. Error: {error:?}"
761            ),
762        };
763    }
764
765    #[test]
766    fn test_avro_rs_207_rename_attr_has_priority_over_rename_all_attribute() {
767        let test_struct = quote! {
768            #[serde(rename_all="SCREAMING_SNAKE_CASE")]
769            struct A {
770                item: i32,
771                #[serde(rename="DoubleItem")]
772                double_item: i32
773            }
774        };
775
776        match syn::parse2::<DeriveInput>(test_struct) {
777            Ok(input) => {
778                let schema_res = derive_avro_schema(input);
779                let expected_token_stream = r#"# [automatically_derived] impl apache_avro :: AvroSchemaComponent for A { fn get_schema_in_ctxt (named_schemas : & mut apache_avro :: schema :: Names , enclosing_namespace : & Option < String >) -> apache_avro :: schema :: Schema { let name = apache_avro :: schema :: Name :: new ("A") . expect (concat ! ("Unable to parse schema name " , "A")) . fully_qualified_name (enclosing_namespace) ; if named_schemas . contains_key (& name) { apache_avro :: schema :: Schema :: Ref { name } } else { let enclosing_namespace = & name . namespace ; named_schemas . insert (name . clone () , apache_avro :: schema :: Schema :: Ref { name : name . clone () }) ; let schema = { 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 : < i32 as apache_avro :: AvroSchemaComponent > :: get_schema_in_ctxt (named_schemas , enclosing_namespace) , 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 : < i32 as apache_avro :: AvroSchemaComponent > :: get_schema_in_ctxt (named_schemas , enclosing_namespace) , order : :: apache_avro :: schema :: RecordFieldOrder :: Ascending , position : schema_fields . len () , custom_attributes : Default :: default () , }) ; let schema_field_set : :: std :: collections :: HashSet < _ > = schema_fields . iter () . map (| rf | & rf . name) . collect () ; assert_eq ! (schema_fields . len () , schema_field_set . len () , "Duplicate field names found: {schema_fields:?}") ; let name = apache_avro :: schema :: Name :: new ("A") . expect (& format ! ("Unable to parse struct name for schema {}" , "A") [..]) ; let lookup : std :: collections :: BTreeMap < String , usize > = schema_fields . iter () . map (| field | (field . name . to_owned () , field . position)) . collect () ; apache_avro :: schema :: Schema :: Record (apache_avro :: schema :: RecordSchema { name , aliases : None , doc : None , fields : schema_fields , lookup , attributes : Default :: default () , }) } ; named_schemas . insert (name , schema . clone ()) ; schema } } }"#;
780                let schema_token_stream = schema_res.unwrap().to_string();
781                assert_eq!(schema_token_stream, expected_token_stream);
782            }
783            Err(error) => panic!(
784                "Failed to parse as derive input when it should be able to. Error: {error:?}"
785            ),
786        };
787    }
788}