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)
}