use proc_macro2::TokenStream as TokenStream2;
use quote::quote;
use syn::{Expr, ExprField, ExprLit, ExprPath, Ident, Lit, Member, Type};
pub(crate) struct LinqCtx<'a> {
pub entity: &'a Type,
pub param: Option<&'a Ident>,
pub params: Vec<(Ident, Type)>,
}
impl<'a> LinqCtx<'a> {
pub(crate) fn single(entity: &'a Type, param: Option<&'a Ident>) -> Self {
Self {
entity,
param,
params: Vec::new(),
}
}
pub(crate) fn multi(
entity: &'a Type,
primary: &'a Ident,
all_params: &[(Ident, Type)],
) -> Self {
Self {
entity,
param: Some(primary),
params: all_params
.iter()
.map(|(i, t)| (i.clone(), t.clone()))
.collect(),
}
}
}
pub(crate) enum FieldKind {
Column,
Navigation,
}
pub(crate) struct FieldRef {
pub entity: Type,
pub field_name: String,
}
pub(crate) fn extract_field(ctx: &LinqCtx<'_>, expr: &Expr) -> syn::Result<TokenStream2> {
let field_ref = extract_field_ref(ctx, expr)?;
Ok(field_const(
&field_ref.entity,
&field_ref.field_name,
FieldKind::Column,
))
}
pub(crate) fn extract_field_nav(ctx: &LinqCtx<'_>, expr: &Expr) -> syn::Result<TokenStream2> {
let field_ref = extract_field_ref(ctx, expr)?;
Ok(field_const(
&field_ref.entity,
&field_ref.field_name,
FieldKind::Navigation,
))
}
pub(crate) fn extract_field_ref(ctx: &LinqCtx<'_>, expr: &Expr) -> syn::Result<FieldRef> {
match expr {
Expr::Path(ExprPath { path, .. }) if path.segments.len() == 1 => {
let field = path.segments[0].ident.to_string();
Ok(FieldRef {
entity: ctx.entity.clone(),
field_name: field,
})
}
Expr::Field(ExprField { base, member, .. }) => {
let field_name = match member {
Member::Named(name) => name.to_string(),
_ => return Err(syn::Error::new_spanned(expr, "expected field name")),
};
if let Expr::Path(ExprPath { path, .. }) = &**base {
if path.segments.len() == 1 {
let base_ident = &path.segments[0].ident;
if let Some(param) = ctx.param {
if param == base_ident {
return Ok(FieldRef {
entity: ctx.entity.clone(),
field_name,
});
}
}
if let Some((_, entity)) = ctx.params.iter().find(|(p, _)| p == base_ident) {
return Ok(FieldRef {
entity: entity.clone(),
field_name,
});
}
}
if type_path_matches(path, ctx.entity) {
return Ok(FieldRef {
entity: ctx.entity.clone(),
field_name,
});
}
for (_, entity) in &ctx.params {
if type_path_matches(path, entity) {
return Ok(FieldRef {
entity: entity.clone(),
field_name,
});
}
}
}
Ok(FieldRef {
entity: ctx.entity.clone(),
field_name,
})
}
_ => Err(syn::Error::new_spanned(
expr,
"expected field access like `b.rating`",
)),
}
}
pub(crate) fn type_path_matches(path: &syn::Path, entity: &Type) -> bool {
let entity_name = if let Type::Path(p) = entity {
p.path
.segments
.last()
.map(|s| s.ident.to_string())
.unwrap_or_default()
} else {
String::new()
};
path.segments.last().is_some_and(|s| s.ident == entity_name)
}
pub(crate) fn field_const(entity: &Type, field: &str, kind: FieldKind) -> TokenStream2 {
let prefix = match kind {
FieldKind::Column => "COLUMN_",
FieldKind::Navigation => "FIELD_",
};
let const_name = Ident::new(
&format!("{}{}", prefix, field.to_uppercase()),
proc_macro2::Span::call_site(),
);
quote! { #entity::#const_name }
}
pub(crate) fn extract_value(expr: &Expr) -> syn::Result<TokenStream2> {
match expr {
Expr::Lit(ExprLit { lit, .. }) => match &lit {
Lit::Int(i) => Ok(quote! { #i }),
Lit::Float(f) => Ok(quote! { #f }),
Lit::Str(s) => Ok(quote! { #s }),
Lit::Bool(b) => Ok(quote! { #b }),
_ => Err(syn::Error::new_spanned(lit, "unsupported literal type")),
},
Expr::Path(p) if p.path.is_ident("None") => Ok(quote! { None::<String> }),
other => Ok(quote! { #other }),
}
}