gen-file-database-macro 1.0.6

File based minimal database macro
Documentation
extern crate proc_macro;
use proc_macro::TokenStream;
use quote::quote;
use syn::parse_macro_input;
use syn::DeriveInput;
use syn::Ident;

#[proc_macro_derive(Encapsulate, attributes(index_key, index_keys))]
pub fn encapsulate_derive(input: TokenStream) -> TokenStream {
    use syn::{GenericParam, Data, Fields, Generics};

    let ast: DeriveInput = parse_macro_input!(input as DeriveInput);
    let name: &Ident = &ast.ident;
    let mut generics: Generics = ast.generics.clone();

    let mut direct_keys: Vec<proc_macro2::TokenStream> = Vec::new();
    let mut nested_keys: Vec<proc_macro2::TokenStream> = Vec::new();
    let mut generics_bounds: Vec<proc_macro2::TokenStream> = Vec::new();

    if let Data::Struct(data_struct) = &ast.data {
        if let Fields::Named(fields_named) = &data_struct.fields {
            for field in &fields_named.named {
                let field_name: &Ident = field.ident.as_ref().unwrap();
                let field_ty: &syn::Type = &field.ty;

                for attr in &field.attrs {
                    if attr.path().is_ident("index_key") {
                        direct_keys.push(quote! {
                            keys.insert(self.#field_name.to_string());
                        });
                    }

                    if attr.path().is_ident("index_keys") {
                        nested_keys.push(quote! {
                            keys.extend(self.#field_name.index_keys());
                        });
                        generics_bounds.push(quote! {
                            #field_ty: Indexable
                        });
                    }
                }
            }
        }
    }

    let has_index_fields: bool = !direct_keys.is_empty() || !nested_keys.is_empty();
    let has_generic: bool = !ast.generics.params.is_empty();

    if has_generic {
        generics_bounds.push(quote! {
            T: FileDbKey
        });
    }

    for param in generics.params.iter_mut() {
        if let GenericParam::Type(ty) = param {
            ty.bounds.push(syn::parse_quote!(Clone));
        }
    }

    let where_clause: &mut syn::WhereClause = generics.make_where_clause();
    for bound in &generics_bounds {
        where_clause.predicates.push(syn::parse2(bound.clone()).unwrap());
    }

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

    let indexable_impl: proc_macro2::TokenStream = quote! {
        impl #impl_generics Indexable for #name #ty_generics #where_clause {
            fn index_keys(&self) -> std::collections::BTreeSet<String> {
                let mut keys = std::collections::BTreeSet::new();
                #(#direct_keys)*
                #(#nested_keys)*
                keys
            }
        }
    };

    let file_db_key_impl: proc_macro2::TokenStream = if has_generic {
        quote! {
            impl #impl_generics FileDbKey for #name #ty_generics #where_clause {
                fn file_db_key() -> String {
                    format!("{}_{}", stringify!(#name).to_case(Case::Snake), T::file_db_key().to_case(Case::Snake))
                }
            }
        }
    } else {
        quote! {
            impl #impl_generics FileDbKey for #name #ty_generics #where_clause {
                fn file_db_key() -> String {
                    stringify!(#name).to_case(Case::Snake)
                }
            }
        }
    };

    let create_method: proc_macro2::TokenStream = if has_index_fields {
        quote! {
            pub fn create(&self, folder: &TargetFolder) -> io::Result<Capsule<Self>> {
                let capsule = Capsule::<Self> {
                    id: Uuid::new_v4(),
                    key: Self::file_db_key(),
                    folder: folder.clone(),
                    inner: self.clone(),
                };
                let keys = self.index_keys();
                if keys.is_empty() {
                    return Err(io::Error::new(io::ErrorKind::InvalidInput, String::from("index defined on empty value")));
                }
                save_capsule(capsule.clone(), keys)
            }
        }
    } else {
        quote! {
            pub fn create(&self, folder: &TargetFolder) -> io::Result<Capsule<Self>> {
                let capsule = Capsule::<Self> {
                    id: Uuid::new_v4(),
                    key: Self::file_db_key(),
                    folder: folder.clone(),
                    inner: self.clone(),
                };
                save_capsule(capsule.clone(), std::collections::BTreeSet::new())
            }
        }
    };

    let upsert_method: proc_macro2::TokenStream = if has_index_fields {
        quote! {
            pub fn upsert(&self, folder: &TargetFolder) -> io::Result<Capsule<Self>> {
                let capsule = Capsule::<Self> {
                    id: Uuid::new_v4(),
                    key: Self::file_db_key(),
                    folder: folder.clone(),
                    inner: self.clone(),
                };
                let keys = self.index_keys();
                if keys.is_empty() {
                    return Err(io::Error::new(io::ErrorKind::InvalidInput, String::from("index defined on empty value")));
                }
                upsert_capsule(capsule.clone(), keys)
            }
        }
    } else {
        quote! {
            pub fn upsert(&self, folder: &TargetFolder) -> io::Result<Capsule<Self>> {
                let capsule = Capsule::<Self> {
                    id: Uuid::new_v4(),
                    key: Self::file_db_key(),
                    folder: folder.clone(),
                    inner: self.clone(),
                };
                upsert_capsule(capsule.clone(), std::collections::BTreeSet::new())
            }
        }
    };

    let expanded: proc_macro2::TokenStream = quote! {
        use uuid::Uuid;
        use std::io;
        use convert_case::Case;
        use convert_case::Casing;
        use gen_file_database::persistence::Capsule;
        use gen_file_database::persistence::TargetFolder;
        use gen_file_database::persistence::save_capsule;
        use gen_file_database::persistence::upsert_capsule;
        use gen_file_database::persistence::Indexable;
        use gen_file_database::persistence::FileDbKey;

        impl #impl_generics #name #ty_generics #where_clause {
            #create_method
            #upsert_method
        }

        #file_db_key_impl
        #indexable_impl
    };

    TokenStream::from(expanded)
}

#[proc_macro_derive(Persist)]
pub fn persist(input: TokenStream) -> TokenStream {
    let ast: DeriveInput = parse_macro_input!(input as DeriveInput);
    let name: &Ident = &ast.ident;

    let expanded: proc_macro2::TokenStream = quote! {
        use uuid::Uuid;
        use std::io;
        use convert_case::Case;
        use convert_case::Casing;
        use gen_file_database::persistence::Capsule;
        use gen_file_database::persistence::TargetFolder;
        use gen_file_database::persistence::save_object;
        use gen_file_database::persistence::FileDbKey;

        impl #name {
            pub fn create(&self, folder: &TargetFolder) -> io::Result<Self> {
                save_object(self, folder)
            }
        }

        impl FileDbKey for #name {
            fn file_db_key() -> String {
                stringify!(#name).to_case(convert_case::Case::Snake)
            }
        }
    };

    TokenStream::from(expanded)
}

#[proc_macro_derive(Persistable)]
pub fn persistable(input: TokenStream) -> TokenStream {
    let ast: DeriveInput = parse_macro_input!(input as DeriveInput);
    let name: &Ident = &ast.ident;

    let expanded: proc_macro2::TokenStream = quote! {
        use convert_case::Case;
        use convert_case::Casing;
        use gen_file_database::persistence::FileDbKey;

        impl FileDbKey for #name {
            fn file_db_key() -> String {
                stringify!(#name).to_case(convert_case::Case::Snake)
            }
        }
    };

    TokenStream::from(expanded)
}