use proc_macro2::TokenStream;
use quote::{format_ident, quote, ToTokens};
use syn::{
parse::{Parse, ParseStream},
Attribute, Data, DeriveInput, Fields, Ident, Meta, Token,
};
#[derive(Clone)]
struct ProjectionConfig {
model: syn::Path,
}
impl Parse for ProjectionConfig {
fn parse(input: ParseStream) -> syn::Result<Self> {
let mut model = None;
while !input.is_empty() {
let ident: Ident = input.parse()?;
let _: Token![=] = input.parse()?;
let ident_str = ident.to_string();
match ident_str.as_str() {
"model" => {
let path: syn::Path = input.parse()?;
model = Some(path);
}
_ => {
return Err(syn::Error::new_spanned(
ident,
format!("Unknown attribute: {}", ident_str),
));
}
}
if input.peek(Token![,]) {
let _: Token![,] = input.parse()?;
}
}
Ok(ProjectionConfig {
model: model.ok_or_else(|| {
syn::Error::new(input.span(), "Missing required 'model' attribute")
})?,
})
}
}
fn is_computed_field(attrs: &[Attribute]) -> bool {
attrs.iter().any(|attr| {
if let Meta::Path(ref path) = attr.meta {
path.is_ident("computed")
} else {
false
}
})
}
fn to_pascal_case(s: &str) -> String {
s.split('_')
.map(|word| {
let mut chars = word.chars();
match chars.next() {
None => String::new(),
Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(),
}
})
.collect()
}
pub fn generate_projection(attr: TokenStream, input: TokenStream) -> syn::Result<TokenStream> {
let config: ProjectionConfig = syn::parse2(attr)?;
let input: DeriveInput = syn::parse2(input)?;
let struct_name = &input.ident;
let model_path = &config.model;
let model_str = model_path.to_token_stream().to_string();
let parts: Vec<&str> = model_str.rsplitn(2, "::").collect();
let module_path =
if parts.len() == 2 { syn::parse_str::<syn::Path>(parts[1])? } else { model_path.clone() };
let column_path = quote! { #module_path::Column };
let fields = match &input.data {
Data::Struct(data) => match &data.fields {
Fields::Named(fields) => &fields.named,
_ => {
return Err(syn::Error::new_spanned(
input,
"ormada_projection only works with structs with named fields",
));
}
},
_ => {
return Err(syn::Error::new_spanned(
input,
"ormada_projection only works with structs",
));
}
};
let mut validations = Vec::new();
let mut column_selections = Vec::new();
for field in fields {
let field_name = field.ident.as_ref().unwrap();
let is_computed = is_computed_field(&field.attrs);
if !is_computed {
let column_name = format_ident!("{}", to_pascal_case(&field_name.to_string()));
validations.push(quote! {
let _ : #column_path = #column_path::#column_name;
});
column_selections.push(quote! {
#column_path::#column_name
});
}
}
let filtered_fields: Vec<_> = fields
.iter()
.map(|field| {
let mut field = field.clone();
field.attrs.retain(|attr| !attr.path().is_ident("computed"));
field
})
.collect();
Ok(quote! {
#[derive(Clone, Debug, ::sea_orm::FromQueryResult)]
pub struct #struct_name {
#(#filtered_fields),*
}
impl #struct_name {
const _VALIDATE_PROJECTION_FIELDS: () = {
#(#validations)*
};
pub(crate) fn columns() -> ::std::vec::Vec<impl ::sea_orm::ColumnTrait> {
::std::vec![#(#column_selections),*]
}
}
})
}