use proc_macro2::TokenStream;
use quote::{format_ident, quote};
use crate::entity::parse::{FieldDef, FilterType};
pub fn join_columns(fields: &[&FieldDef]) -> String {
fields
.iter()
.copied()
.map(crate::entity::parse::FieldDef::name_str)
.collect::<Vec<_>>()
.join(", ")
}
pub fn insert_bindings(fields: &[FieldDef]) -> Vec<TokenStream> {
fields
.iter()
.filter(|f| f.embed.is_none())
.map(|f| {
let name = f.name();
quote! { .bind(insertable.#name) }
})
.collect()
}
pub fn generate_where_conditions(fields: &[&FieldDef], soft_delete: bool) -> TokenStream {
let conditions: Vec<TokenStream> = fields
.iter()
.flat_map(|f| {
let name = f.name();
let name_str = f.name_str();
let filter = f.filter();
match filter.filter_type {
FilterType::Eq => {
vec![quote! {
if query.#name.is_some() {
conditions.push(format!("{} = ${}", #name_str, param_idx));
param_idx += 1;
}
}]
}
FilterType::Like => {
vec![quote! {
if query.#name.is_some() {
conditions.push(format!("{} ILIKE ${}", #name_str, param_idx));
param_idx += 1;
}
}]
}
FilterType::Search => {
vec![quote! {
if query.#name.is_some() {
conditions.push(format!("{} ILIKE '%' || ${} || '%'", #name_str, param_idx));
param_idx += 1;
}
}]
}
FilterType::Range => {
let from_name = format_ident!("{}_from", name);
let to_name = format_ident!("{}_to", name);
vec![
quote! {
if query.#from_name.is_some() {
conditions.push(format!("{} >= ${}", #name_str, param_idx));
param_idx += 1;
}
},
quote! {
if query.#to_name.is_some() {
conditions.push(format!("{} <= ${}", #name_str, param_idx));
param_idx += 1;
}
},
]
}
FilterType::None => vec![]
}
})
.collect();
let soft_delete_condition = if soft_delete {
quote! {
conditions.push("deleted_at IS NULL".to_string());
}
} else {
TokenStream::new()
};
quote! {
#soft_delete_condition
#(#conditions)*
}
}
pub fn generate_query_bindings(fields: &[&FieldDef]) -> TokenStream {
let bindings: Vec<TokenStream> = fields
.iter()
.flat_map(|f| {
let name = f.name();
let filter = f.filter();
match filter.filter_type {
FilterType::Eq => {
vec![quote! {
if let Some(ref v) = query.#name {
q = q.bind(v);
}
}]
}
FilterType::Like => {
vec![quote! {
if let Some(ref v) = query.#name {
let escaped = v
.replace('\\', "\\\\")
.replace('%', "\\%")
.replace('_', "\\_");
q = q.bind(format!("%{}%", escaped));
}
}]
}
FilterType::Search => {
vec![quote! {
if let Some(ref v) = query.#name {
q = q.bind(v);
}
}]
}
FilterType::Range => {
let from_name = format_ident!("{}_from", name);
let to_name = format_ident!("{}_to", name);
vec![
quote! {
if let Some(ref v) = query.#from_name {
q = q.bind(v);
}
},
quote! {
if let Some(ref v) = query.#to_name {
q = q.bind(v);
}
},
]
}
FilterType::None => vec![]
}
})
.collect();
quote! { #(#bindings)* }
}
pub fn dynamic_set_stmts(fields: &[&FieldDef]) -> TokenStream {
let per_field: Vec<TokenStream> = fields
.iter()
.map(|f| {
let name = f.name();
let column = f.name_str();
let assignment = format!("{column} = ${{}}");
let null_assignment = format!("{column} = NULL");
if let Some(embed) = &f.embed {
let pushes: Vec<TokenStream> = embed
.subfields
.iter()
.map(|(sub, _)| {
let col = format!("{}{} = ${{}}", embed.prefix, sub);
quote! {
__sets.push(format!(#col, __idx));
__idx += 1;
}
})
.collect();
return quote! {
if dto.#name.is_some() {
#(#pushes)*
}
};
}
if f.is_option() {
quote! {
match &dto.#name {
None => {}
Some(None) => __sets.push(#null_assignment.to_string()),
Some(Some(_)) => {
__sets.push(format!(#assignment, __idx));
__idx += 1;
}
}
}
} else {
quote! {
if dto.#name.is_some() {
__sets.push(format!(#assignment, __idx));
__idx += 1;
}
}
}
})
.collect();
quote! {
let mut __sets: Vec<String> = Vec::new();
let mut __idx: usize = 1;
#(#per_field)*
}
}
pub fn version_guard(
entity: &crate::entity::parse::EntityDef,
where_index: &TokenStream
) -> (TokenStream, TokenStream, TokenStream) {
match entity.version_field() {
Some(field) => {
let column = field.name_str();
let bump = format!("{column} = {column} + 1");
let where_fmt = format!(" AND {column} = ${{}}");
(
quote! {
__sets.push(#bump.to_string());
let __version_where = format!(#where_fmt, #where_index);
},
quote! { __version_where },
quote! { q = q.bind(dto.expected_version); }
)
}
None => (
quote! { let __version_where = ""; },
quote! { __version_where },
TokenStream::new()
)
}
}
pub fn dynamic_set_binds(fields: &[&FieldDef]) -> TokenStream {
let per_field: Vec<TokenStream> = fields
.iter()
.map(|f| {
let name = f.name();
if let Some(embed) = &f.embed {
let binds: Vec<TokenStream> = embed
.subfields
.iter()
.map(|(sub, _)| quote! { q = q.bind(__v.#sub.clone()); })
.collect();
return quote! {
if let Some(__v) = dto.#name {
#(#binds)*
}
};
}
if f.is_option() {
quote! {
if let Some(Some(__v)) = dto.#name {
q = q.bind(__v);
}
}
} else {
quote! {
if let Some(__v) = dto.#name {
q = q.bind(__v);
}
}
}
})
.collect();
quote! { #(#per_field)* }
}
#[cfg(test)]
mod tests {
use syn::{Field, parse_quote};
use super::*;
use crate::entity::parse::FieldDef;
fn parse_field(tokens: proc_macro2::TokenStream) -> FieldDef {
let field: Field = parse_quote!(#tokens);
FieldDef::from_field(&field).unwrap()
}
#[test]
fn join_columns_single() {
let field = parse_field(quote! { pub name: String });
let result = join_columns(&[&field]);
assert_eq!(result, "name");
}
#[test]
fn join_columns_multiple() {
let fields = [
parse_field(quote! { pub id: Uuid }),
parse_field(quote! { pub name: String }),
parse_field(quote! { pub email: String })
];
let refs: Vec<&FieldDef> = fields.iter().collect();
let result = join_columns(&refs);
assert_eq!(result, "id, name, email");
}
#[test]
fn join_columns_empty() {
let result = join_columns(&[]);
assert_eq!(result, "");
}
#[test]
fn insert_bindings_generates_bind_calls() {
let fields = vec![
parse_field(quote! { pub id: Uuid }),
parse_field(quote! { pub name: String }),
];
let bindings = insert_bindings(&fields);
assert_eq!(bindings.len(), 2);
let first = bindings[0].to_string();
assert!(first.contains("bind"), "Expected 'bind' in: {}", first);
assert!(
first.contains("insertable"),
"Expected 'insertable' in: {}",
first
);
assert!(first.contains("id"), "Expected 'id' in: {}", first);
let second = bindings[1].to_string();
assert!(second.contains("bind"), "Expected 'bind' in: {}", second);
assert!(
second.contains("insertable"),
"Expected 'insertable' in: {}",
second
);
assert!(second.contains("name"), "Expected 'name' in: {}", second);
}
#[test]
fn insert_bindings_empty() {
let bindings = insert_bindings(&[]);
assert!(bindings.is_empty());
}
#[test]
fn where_conditions_eq_filter() {
let field = parse_field(quote! {
#[filter(eq)]
pub status: String
});
let refs: Vec<&FieldDef> = vec![&field];
let result = generate_where_conditions(&refs, false);
let code = result.to_string();
assert!(code.contains("query . status . is_some"));
assert!(code.contains("= $"));
}
#[test]
fn where_conditions_like_filter() {
let field = parse_field(quote! {
#[filter(like)]
pub name: String
});
let refs: Vec<&FieldDef> = vec![&field];
let result = generate_where_conditions(&refs, false);
let code = result.to_string();
assert!(code.contains("query . name . is_some"));
assert!(code.contains("ILIKE"));
}
#[test]
fn where_conditions_range_filter() {
let field = parse_field(quote! {
#[filter(range)]
pub age: i32
});
let refs: Vec<&FieldDef> = vec![&field];
let result = generate_where_conditions(&refs, false);
let code = result.to_string();
assert!(code.contains("age_from"));
assert!(code.contains("age_to"));
assert!(code.contains(">="));
assert!(code.contains("<="));
}
#[test]
fn where_conditions_none_filter() {
let field = parse_field(quote! { pub name: String });
let refs: Vec<&FieldDef> = vec![&field];
let result = generate_where_conditions(&refs, false);
let code = result.to_string();
assert!(!code.contains("query"));
}
#[test]
fn where_conditions_with_soft_delete() {
let result = generate_where_conditions(&[], true);
let code = result.to_string();
assert!(code.contains("deleted_at IS NULL"));
}
#[test]
fn where_conditions_without_soft_delete() {
let result = generate_where_conditions(&[], false);
let code = result.to_string();
assert!(!code.contains("deleted_at"));
}
#[test]
fn query_bindings_eq_filter() {
let field = parse_field(quote! {
#[filter(eq)]
pub status: String
});
let refs: Vec<&FieldDef> = vec![&field];
let result = generate_query_bindings(&refs);
let code = result.to_string();
assert!(code.contains("if let Some (ref v) = query . status"));
assert!(code.contains("q = q . bind (v)"));
}
#[test]
fn query_bindings_like_filter() {
let field = parse_field(quote! {
#[filter(like)]
pub name: String
});
let refs: Vec<&FieldDef> = vec![&field];
let result = generate_query_bindings(&refs);
let code = result.to_string();
assert!(code.contains("escaped"));
assert!(code.contains("format !"));
}
#[test]
fn query_bindings_range_filter() {
let field = parse_field(quote! {
#[filter(range)]
pub age: i32
});
let refs: Vec<&FieldDef> = vec![&field];
let result = generate_query_bindings(&refs);
let code = result.to_string();
assert!(code.contains("age_from"));
assert!(code.contains("age_to"));
}
#[test]
fn query_bindings_none_filter() {
let field = parse_field(quote! { pub name: String });
let refs: Vec<&FieldDef> = vec![&field];
let result = generate_query_bindings(&refs);
let code = result.to_string();
assert!(!code.contains("bind"));
}
#[test]
fn query_bindings_empty() {
let result = generate_query_bindings(&[]);
assert!(result.is_empty());
}
#[test]
fn where_conditions_multiple_filters() {
let fields = [
parse_field(quote! {
#[filter(eq)]
pub status: String
}),
parse_field(quote! {
#[filter(like)]
pub name: String
})
];
let refs: Vec<&FieldDef> = fields.iter().collect();
let result = generate_where_conditions(&refs, false);
let code = result.to_string();
assert!(code.contains("status"));
assert!(code.contains("name"));
assert!(code.contains("= $"));
assert!(code.contains("ILIKE"));
}
}