serdavro-macros 1.0.0

Macros for serdavro
Documentation
use proc_macro2::TokenStream;
use quote::{format_ident, quote};
use syn::{Fields, ItemStruct, parse_macro_input, spanned::Spanned};

#[proc_macro_attribute]
pub fn serdavro(
    _: proc_macro::TokenStream,
    item: proc_macro::TokenStream,
) -> proc_macro::TokenStream {
    let item = parse_macro_input!(item as ItemStruct);
    serdavro_impl(&item)
        .unwrap_or_else(syn::Error::into_compile_error)
        .into()
}

fn serdavro_impl(item: &ItemStruct) -> syn::Result<TokenStream> {
    let ident = &item.ident;

    let fake_ident = format_ident!("__SERDAVRO__{ident}__");
    let mut fake = item.clone();
    fake.ident = fake_ident.clone();

    let fields = match &item.fields {
        Fields::Named(fields) => fields
            .named
            .iter()
            .filter_map(|field| {
                field
                    .attrs
                    .iter()
                    .any(|attr| {
                        let mut flatten = false;
                        if attr.path().is_ident("serde") {
                            let _ = attr.parse_nested_meta(|meta| {
                                if meta.path.is_ident("flatten") {
                                    flatten = true
                                }
                                Ok(())
                            });
                        }
                        flatten
                    })
                    .then_some(field.ident.clone().unwrap().to_string())
            })
            .collect::<Vec<_>>(),
        _ => {
            return Err(syn::Error::new(
                item.fields.span(),
                "Only named fields are supported",
            ));
        }
    };

    let (impl_generics, ty_generics, where_clause) = item.generics.split_for_impl();

    Ok(quote! {
        const _: () = {
            use ::std::collections::HashMap;

            use ::apache_avro::{
                AvroSchema,
                schema::{Name, RecordField, RecordSchema, Schema, derive::AvroSchemaComponent},
            };

            #[derive(AvroSchema)]
            #fake

            #[automatically_derived]
            impl #impl_generics AvroSchemaComponent for #ident #ty_generics #where_clause {
                fn get_schema_in_ctxt(named_schemas: &mut HashMap<Name, Schema>, enclosing_namespace: &Option<String>) -> Schema {
                    let mut fake_schema = #fake_ident::get_schema_in_ctxt(named_schemas, enclosing_namespace);
                    match fake_schema {
                        Schema::Record(fake) => {
                            let mut record = fake.clone();
                            record.name = Name::new(stringify!(#ident)).expect("Unable to parse schema name");
                            record.lookup.clear();
                            record.fields.clear();

                            fn add(record: &mut RecordSchema, mut field: RecordField, index: &mut usize) {
                                record.lookup.insert(field.name.clone(), *index);
                                field.position = *index;
                                record.fields.push(field);
                                *index += 1;
                            };

                            let mut index = 0;

                            for field in fake.fields {
                                if [#(#fields,)*].contains(&field.name.as_str()) {
                                    match field.schema {
                                        Schema::Record(r) => {
                                            for field in r.fields {
                                                add(&mut record, field, &mut index);
                                            }
                                        }
                                        _ => panic!("Only record schemas can be flattened")
                                    }
                                } else {
                                    add(&mut record, field, &mut index);
                                }
                            }

                            Schema::Record(record)
                        }
                        _ => panic!("Only record schemas are supported")
                    }
                }
            }
        };

        #item
    })
}