use darling::ToTokens;
use proc_macro2::TokenStream;
use quote::{TokenStreamExt, quote};
use super::options::*;
#[cfg(test)]
use convert_case::{Case, Casing};
#[cfg(test)]
use proc_macro2::Span;
#[derive(Clone)]
pub struct ScopeCol<'a> {
pub variant: syn::Ident,
pub column_name: &'a syn::Ident,
pub column_ty: &'a syn::Type,
}
impl ScopeCol<'_> {
pub fn predicate(&self, param_idx: u32) -> String {
format!("{} = ${}", self.column_name, param_idx)
}
pub fn arg_tokens(&self) -> TokenStream {
let column_ty = self.column_ty;
quote! { __scope_val as &#column_ty, }
}
}
#[cfg(test)]
fn variant_ident(column_name: &syn::Ident) -> syn::Ident {
syn::Ident::new(
&column_name.to_string().to_case(Case::UpperCamel),
Span::call_site(),
)
}
#[derive(Clone)]
pub struct ScopeInfo<'a> {
pub scope_ty: syn::Ident,
pub cols: Vec<ScopeCol<'a>>,
}
impl<'a> ScopeInfo<'a> {
pub fn from_opts(opts: &'a RepositoryOptions) -> Option<Self> {
let cols: Vec<_> = opts
.columns
.scope_columns()
.into_iter()
.map(|col| ScopeCol {
variant: col.scope_variant(),
column_name: col.name(),
column_ty: col.ty(),
})
.collect();
if cols.is_empty() {
return None;
}
Some(ScopeInfo {
scope_ty: opts.scope_type_ident(),
cols,
})
}
pub fn fn_arg(&self) -> TokenStream {
let scope_ty = &self.scope_ty;
quote! { scope: impl Into<#scope_ty>, }
}
pub fn fn_pass(&self) -> TokenStream {
quote! { scope, }
}
pub fn convert(&self) -> TokenStream {
quote! { let __scope = scope.into(); }
}
pub fn dispatch(
&self,
all_arm: TokenStream,
scoped_arm: impl Fn(&ScopeCol<'a>) -> TokenStream,
) -> TokenStream {
let scope_ty = &self.scope_ty;
let arms = self.cols.iter().map(|col| {
let variant = &col.variant;
let body = scoped_arm(col);
quote! { #scope_ty::#variant(__scope_val) => #body, }
});
quote! {
match &__scope {
#scope_ty::All => #all_arm,
#(#arms)*
}
}
}
}
pub struct ScopeType<'a> {
entity: &'a syn::Ident,
info: ScopeInfo<'a>,
}
impl<'a> ScopeType<'a> {
pub fn new(opts: &'a RepositoryOptions) -> Option<Self> {
ScopeInfo::from_opts(opts).map(|info| Self {
entity: opts.entity(),
info,
})
}
}
impl ToTokens for ScopeType<'_> {
fn to_tokens(&self, tokens: &mut TokenStream) {
let scope_ty = &self.info.scope_ty;
let doc = format!(
"Scope argument for [`{}`] repository reads: each column variant filters every \
query by that scope column, `All` reads across all scopes (audited escape hatch).",
self.entity,
);
let variants = self.info.cols.iter().map(|col| {
let variant = &col.variant;
let column_ty = col.column_ty;
let vdoc = format!(
"Restricts every read to rows whose `{}` column equals the value.",
col.column_name,
);
quote! {
#[doc = #vdoc]
#variant(#column_ty),
}
});
let from_impls = self.info.cols.iter().map(|col| {
let variant = &col.variant;
let column_ty = col.column_ty;
quote! {
impl From<#column_ty> for #scope_ty {
fn from(value: #column_ty) -> Self {
#scope_ty::#variant(value)
}
}
impl From<&#column_ty> for #scope_ty {
fn from(value: &#column_ty) -> Self {
#scope_ty::#variant(*value)
}
}
}
});
tokens.append_all(quote! {
#[doc = #doc]
#[derive(Debug, Clone, Copy)]
pub enum #scope_ty {
All,
#(#variants)*
}
#(#from_impls)*
impl From<&#scope_ty> for #scope_ty {
fn from(value: &#scope_ty) -> Self {
*value
}
}
});
}
}
#[cfg(test)]
mod tests {
use super::*;
fn test_info<'a>(cols: &[(&'a syn::Ident, &'a syn::Type)]) -> (ScopeInfo<'a>, syn::Ident) {
let entity = syn::Ident::new("Entity", Span::call_site());
(
ScopeInfo {
scope_ty: syn::Ident::new("EntityScope", Span::call_site()),
cols: cols
.iter()
.map(|(name, ty)| ScopeCol {
variant: variant_ident(name),
column_name: name,
column_ty: ty,
})
.collect(),
},
entity,
)
}
#[test]
fn scope_type_tokens_single() {
let column_name = syn::Ident::new("partner_id", Span::call_site());
let column_ty: syn::Type = syn::parse_str("PartnerId").unwrap();
let (info, entity) = test_info(&[(&column_name, &column_ty)]);
let scope_type = ScopeType {
entity: &entity,
info,
};
let mut tokens = TokenStream::new();
scope_type.to_tokens(&mut tokens);
let token_str = tokens.to_string();
assert!(token_str.contains("pub enum EntityScope"));
assert!(token_str.contains("PartnerId (PartnerId)"));
assert!(!token_str.contains("Only"));
assert!(token_str.contains("impl From < PartnerId > for EntityScope"));
assert!(token_str.contains("impl From < & PartnerId > for EntityScope"));
assert!(!token_str.contains("Option"));
}
#[test]
fn scope_type_tokens_multi() {
let partner = syn::Ident::new("partner_id", Span::call_site());
let partner_ty: syn::Type = syn::parse_str("PartnerId").unwrap();
let customer = syn::Ident::new("customer_id", Span::call_site());
let customer_ty: syn::Type = syn::parse_str("CustomerId").unwrap();
let (info, entity) = test_info(&[(&partner, &partner_ty), (&customer, &customer_ty)]);
let scope_type = ScopeType {
entity: &entity,
info,
};
let mut tokens = TokenStream::new();
scope_type.to_tokens(&mut tokens);
let token_str = tokens.to_string();
assert!(token_str.contains("pub enum EntityScope"));
assert!(token_str.contains("PartnerId (PartnerId)"));
assert!(token_str.contains("CustomerId (CustomerId)"));
assert!(token_str.contains("impl From < PartnerId > for EntityScope"));
assert!(token_str.contains("impl From < CustomerId > for EntityScope"));
assert!(token_str.contains("impl From < & CustomerId > for EntityScope"));
}
#[test]
fn scope_col_predicate() {
let column_name = syn::Ident::new("partner_id", Span::call_site());
let column_ty: syn::Type = syn::parse_str("PartnerId").unwrap();
let (info, _entity) = test_info(&[(&column_name, &column_ty)]);
assert_eq!(info.cols[0].predicate(1), "partner_id = $1");
assert_eq!(info.cols[0].predicate(3), "partner_id = $3");
}
#[test]
fn dispatch_emits_one_arm_per_column() {
let partner = syn::Ident::new("partner_id", Span::call_site());
let partner_ty: syn::Type = syn::parse_str("PartnerId").unwrap();
let customer = syn::Ident::new("customer_id", Span::call_site());
let customer_ty: syn::Type = syn::parse_str("CustomerId").unwrap();
let (info, _entity) = test_info(&[(&partner, &partner_ty), (&customer, &customer_ty)]);
let tokens = info
.dispatch(quote! { all() }, |col| {
let p = col.predicate(2);
quote! { scoped(#p) }
})
.to_string();
assert!(tokens.contains("EntityScope :: All => all ()"));
assert!(
tokens
.contains("EntityScope :: PartnerId (__scope_val) => scoped (\"partner_id = $2\")")
);
assert!(
tokens.contains(
"EntityScope :: CustomerId (__scope_val) => scoped (\"customer_id = $2\")"
)
);
}
}