use proc_macro::TokenStream;
use quote::{quote};
use syn::{parse_macro_input, DeriveInput, Ident, Type, Path};
use darling::{FromDeriveInput, FromField, ast};
#[derive(Debug, FromField)]
#[darling(attributes(filter))]
struct FilterField {
ident: Option<Ident>,
ty: Type,
#[darling(default)]
db_column: Option<String>,
#[darling(default)]
expr: Option<String>,
#[darling(default)]
op: Option<String>,
#[darling(default)]
delegate: bool,
#[darling(default)]
skip: bool,
}
#[derive(Debug, FromDeriveInput)]
#[darling(supports(struct_named), attributes(filterable))]
struct FilterableInput {
ident: Ident,
generics: syn::Generics,
#[darling(default)]
after: Option<Path>,
data: ast::Data<(), FilterField>,
}
pub fn derive_filterable(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
let args = match FilterableInput::from_derive_input(&input) {
Ok(a) => a,
Err(e) => return e.write_errors().into(),
};
let ident = &args.ident;
let generics = args.generics.clone();
let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
let fields = match args.data {
ast::Data::Struct(s) => s.fields,
_ => unreachable!("Filterable only supports named structs"),
};
let field_stmts = fields.iter().map(|f| {
let ident = f.ident.as_ref().expect("named field");
let ty = &f.ty;
if f.skip {
return quote! {};
}
if f.delegate {
let (is_option, inner_ty) = option_inner_type(ty);
if is_option {
quote! {
if let Some(inner) = self.#ident.as_ref() {
qs = ::uxar::db::Filterable::filter_query(inner, qs);
}
}
} else {
quote! {
qs = ::uxar::db::Filterable::filter_query(&self.#ident, qs);
}
}
} else {
let col_name = f.db_column.clone().unwrap_or_else(|| ident.to_string());
let expr = f.expr.clone().unwrap_or_else(|| col_name);
let op = f.op.clone().unwrap_or_else(|| "=".to_string());
let filter_str = format!("{expr} {op} ?");
let (is_option, _inner_ty) = option_inner_type(ty);
if is_option {
quote! {
if let Some(value) = self.#ident {
qs = qs.filter(#filter_str).bind(value);
}
}
} else {
quote! {
qs = qs.filter(#filter_str).bind(self.#ident);
}
}
}
});
let after = if let Some(after_path) = args.after {
quote! {
qs = #after_path(self, qs);
}
} else {
quote! {}
};
let expanded = quote! {
impl #impl_generics ::uxar::db::Filterable for #ident #ty_generics #where_clause {
fn apply_filters(self, qs: ::uxar::db::Statement) -> ::uxar::db::Statement {
let mut qs = qs;
#(#field_stmts)*
#after
qs
}
}
};
expanded.into()
}
fn option_inner_type(ty: &Type) -> (bool, Option<Type>) {
if let Type::Path(type_path) = ty {
let path = &type_path.path;
if let Some(last_seg) = path.segments.last() {
if last_seg.ident != "Option" {
return (false, None);
}
let is_option = if path.segments.len() == 1 {
true
} else if path.segments.len() == 3 {
let first = path.segments.first().unwrap();
let second = path.segments.iter().nth(1).unwrap();
(first.ident == "std" || first.ident == "core") && second.ident == "option"
} else if path.segments.len() == 4 && path.leading_colon.is_some() {
let first = path.segments.first().unwrap();
let second = path.segments.iter().nth(1).unwrap();
let third = path.segments.iter().nth(2).unwrap();
(first.ident == "std" || first.ident == "core") && second.ident == "option" && third.ident == "Option"
} else {
false
};
if !is_option {
return (false, None);
}
if let syn::PathArguments::AngleBracketed(ab) = &last_seg.arguments {
if let Some(syn::GenericArgument::Type(inner_ty)) = ab.args.first() {
return (true, Some(inner_ty.clone()));
}
}
return (true, None);
}
}
(false, None)
}