use crate::crud::utils::{pluralize, to_snake_case};
use syn::{Attribute, Data, DeriveInput, Error, Fields, Ident, LitStr, Type};
#[derive(Debug, Clone)]
pub struct CrudFieldInfo {
pub ident: Ident,
pub ty: Type,
pub is_primary_key: bool,
pub auto_generated: bool,
pub readonly: bool,
pub writeonly: bool,
pub skip: bool,
}
impl CrudFieldInfo {
pub fn include_in_create(&self) -> bool {
!self.auto_generated && !self.readonly && !self.skip
}
pub fn include_in_update(&self) -> bool {
!self.auto_generated && !self.readonly && !self.skip && !self.is_primary_key
}
pub fn include_in_response(&self) -> bool {
!self.writeonly && !self.skip
}
}
#[derive(Debug, Clone)]
#[derive(Default)]
pub struct CrudConfig {
pub path: String,
pub read_only: bool,
pub skip_create: bool,
pub skip_delete: bool,
}
#[derive(Debug)]
pub struct CrudEntityInfo {
pub name: Ident,
pub config: CrudConfig,
pub fields: Vec<CrudFieldInfo>,
}
impl CrudEntityInfo {
pub fn pk_fields(&self) -> Vec<&CrudFieldInfo> {
self.fields.iter().filter(|f| f.is_primary_key).collect()
}
pub fn db_fields(&self) -> Vec<&CrudFieldInfo> {
self.fields.iter().filter(|f| !f.skip).collect()
}
pub fn create_fields(&self) -> Vec<&CrudFieldInfo> {
self.fields
.iter()
.filter(|f| f.include_in_create())
.collect()
}
pub fn update_fields(&self) -> Vec<&CrudFieldInfo> {
self.fields
.iter()
.filter(|f| f.include_in_update())
.collect()
}
pub fn response_fields(&self) -> Vec<&CrudFieldInfo> {
self.fields
.iter()
.filter(|f| f.include_in_response())
.collect()
}
pub fn snake_case_name(&self) -> String {
to_snake_case(&self.name.to_string())
}
pub fn default_path(&self) -> String {
format!("/{}", pluralize(&self.snake_case_name()))
}
pub fn effective_path(&self) -> String {
if self.config.path.is_empty() {
self.default_path()
} else {
self.config.path.clone()
}
}
}
fn parse_crud_config(attrs: &[Attribute]) -> Result<CrudConfig, Error> {
let mut config = CrudConfig::default();
for attr in attrs {
if !attr.path().is_ident("crud") {
continue;
}
attr.parse_nested_meta(|meta| {
let path = &meta.path;
if path.is_ident("path") {
let crud_path = meta.value()?.parse::<LitStr>()?.value();
config.path = crud_path;
} else if path.is_ident("read_only") {
config.read_only = true;
} else if path.is_ident("skip_create") {
config.skip_create = true;
} else if path.is_ident("skip_delete") {
config.skip_delete = true;
};
Ok(())
})?;
}
Ok(config)
}
fn parse_table_name(attrs: &[Attribute]) -> Option<String> {
for attr in attrs {
if attr.path().is_ident("table")
&& let Ok(lit) = attr.parse_args::<syn::LitStr>() {
return Some(lit.value());
}
}
None
}
fn parse_field_info(field: &syn::Field) -> Result<CrudFieldInfo, Error> {
let ident = field
.ident
.clone()
.ok_or(syn::Error::new_spanned(field, "named field required"))?;
let ty = field.ty.clone();
Ok(CrudFieldInfo {
ident,
ty,
is_primary_key: crate::utils::has_attr(field, "primary_key"),
auto_generated: crate::utils::has_attr(field, "auto_generated"),
readonly: crate::utils::has_attr(field, "readonly"),
writeonly: crate::utils::has_attr(field, "writeonly"),
skip: crate::utils::has_attr(field, "skip"),
})
}
pub fn parse_crud_entity(input: &DeriveInput) -> Result<CrudEntityInfo, Error> {
let name = input.ident.clone();
parse_table_name(&input.attrs).ok_or(Error::new_spanned(
input,
"#[table(\"name\")] attribute is required for Crud derive",
))?;
let config = parse_crud_config(&input.attrs)?;
let fields = match &input.data {
Data::Struct(data) => match &data.fields {
Fields::Named(fields) => Ok(fields
.named
.iter()
.map(parse_field_info)
.collect::<Result<Vec<_>, _>>()?),
_ => Err(Error::new_spanned(
input,
"Crud can only be derived for structs with named fields",
)),
},
_ => Err(Error::new_spanned(
input,
"Crud can only be derived for structs",
)),
}?;
Ok(CrudEntityInfo {
name,
config,
fields,
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_to_snake_case() {
assert_eq!(to_snake_case("User"), "user");
assert_eq!(to_snake_case("UserProfile"), "user_profile");
assert_eq!(to_snake_case("HTMLParser"), "h_t_m_l_parser");
}
#[test]
fn test_pluralize() {
assert_eq!(pluralize("user"), "users");
assert_eq!(pluralize("category"), "categories");
assert_eq!(pluralize("box"), "boxes");
assert_eq!(pluralize("key"), "keys");
}
}