chert_derive 0.1.0

domain-specific expression language
Documentation
use proc_macro::TokenStream;
use proc_macro2::{Ident, TokenStream as TokenStream2};
use quote::quote;
use syn::{
    parse_macro_input, Data::Struct, DataStruct, DeriveInput, Fields::Named, FieldsNamed, Type,
};

fn to_chert_field(ident: &Ident, ty: &Type) -> TokenStream2 {
    let ident_str = ident.to_string();

    if let Type::Path(type_path) = ty {
        quote! {
            (#ident_str, <chert_accessor::ChertField::<Self> as From<Box<dyn Fn(&Self) -> &#type_path>>>::from(Box::new(|o| &o.#ident)))
        }
    } else {
        unreachable!();
    }
}

#[proc_macro_derive(ChertStruct)]
pub fn derive(input: TokenStream) -> TokenStream {
    let DeriveInput { ident, data, .. } = parse_macro_input!(input as DeriveInput);

    let fields = if let Struct(DataStruct {
        fields: Named(FieldsNamed { ref named, .. }),
        ..
    }) = data
    {
        named
            .iter()
            .filter_map(|f| f.ident.as_ref().map(|i| (i, &f.ty)))
            .map(|(i, t)| to_chert_field(i, t))
            .collect::<Vec<_>>()
    } else {
        panic!("must be a struct with named fields");
    };

    quote! {
        impl chert_accessor::ChertStruct for #ident {
            fn fields() -> std::collections::HashMap<String, (usize, chert_accessor::ChertField<Self>)> {
                use std::collections::HashMap;
                use chert_accessor::ChertField;

                let mut field_counts: HashMap<u8, usize> = HashMap::new();
                let mut indexed_fields: HashMap<String, (usize, ChertField<Self>)> = HashMap::new();
                let unindexed_fields: HashMap<&'static str, ChertField<Self>> = HashMap::from([#(#fields),*]);

                for (name, field) in unindexed_fields.into_iter() {
                    let type_key = field.type_key();
                    if let Some(i) = field_counts.get(&type_key) {
                        field_counts.insert(type_key, i + 1);
                    } else {
                        field_counts.insert(type_key, 0);
                    }
                    indexed_fields.insert(name.to_owned(), (field_counts[&type_key], field));
                }

                indexed_fields
            }
        }
    }
    .into()
}