use syn::{Attribute, Ident};
use super::{
super::{
api::{ApiConfig, parse_api_config},
field::IndexType
},
CompositeIndexDef
};
pub fn parse_has_many_attrs(attrs: &[Attribute]) -> Vec<HasManyDef> {
attrs
.iter()
.filter(|attr| attr.path().is_ident("has_many"))
.filter_map(|attr| {
attr.parse_args_with(|input: syn::parse::ParseStream<'_>| {
let entity: Ident = input.parse()?;
let mut through = None;
if input.peek(syn::Token![,]) {
let _: syn::Token![,] = input.parse()?;
let key: Ident = input.parse()?;
if key != "through" {
return Err(syn::Error::new(
key.span(),
"unknown has_many option; expected `through = \"junction_table\"`"
));
}
let _: syn::Token![=] = input.parse()?;
let table: syn::LitStr = input.parse()?;
through = Some(table.value());
}
Ok(HasManyDef {
entity,
through
})
})
.ok()
})
.collect()
}
#[derive(Debug, Clone)]
pub struct HasManyDef {
pub entity: Ident,
pub through: Option<String>
}
pub fn parse_api_attr(attrs: &[Attribute]) -> ApiConfig {
for attr in attrs {
if !attr.path().is_ident("entity") {
continue;
}
let result: syn::Result<Option<ApiConfig>> =
attr.parse_args_with(|input: syn::parse::ParseStream<'_>| {
while !input.is_empty() {
let ident: Ident = input.parse()?;
if ident == "api" {
let content;
syn::parenthesized!(content in input);
let tokens = content.parse::<proc_macro2::TokenStream>()?;
let meta_list = syn::Meta::List(syn::MetaList {
path: syn::parse_quote!(api),
delimiter: syn::MacroDelimiter::Paren(syn::token::Paren::default()),
tokens
});
if let Ok(config) = parse_api_config(&meta_list) {
return Ok(Some(config));
}
} else if input.peek(syn::Token![=]) {
let _: syn::Token![=] = input.parse()?;
let _ = input.parse::<syn::Expr>()?;
} else if input.peek(syn::token::Paren) {
let content;
syn::parenthesized!(content in input);
let _ = content.parse::<proc_macro2::TokenStream>()?;
}
if input.peek(syn::Token![,]) {
let _: syn::Token![,] = input.parse()?;
}
}
Ok(None)
});
if let Ok(Some(config)) = result {
return config;
}
}
ApiConfig::default()
}
#[derive(Debug, Clone)]
pub struct CustomConstraintDef {
pub name: String,
pub kind: String,
pub field: Option<String>
}
pub fn parse_constraint_attrs(attrs: &[Attribute]) -> syn::Result<Vec<CustomConstraintDef>> {
let mut constraints = Vec::new();
for attr in attrs {
if !attr.path().is_ident("entity") {
continue;
}
attr.parse_nested_meta(|meta| {
if meta.path.is_ident("constraint") {
constraints.push(parse_constraint_content(&meta)?);
} else if meta.input.peek(syn::Token![=]) {
let _: syn::Token![=] = meta.input.parse()?;
let _: syn::Expr = meta.input.parse()?;
} else if meta.input.peek(syn::token::Paren) {
let content;
syn::parenthesized!(content in meta.input);
let _: proc_macro2::TokenStream = content.parse()?;
}
Ok(())
})?;
}
Ok(constraints)
}
fn parse_constraint_content(
meta: &syn::meta::ParseNestedMeta<'_>
) -> syn::Result<CustomConstraintDef> {
let mut name: Option<String> = None;
let mut kind: Option<String> = None;
let mut field: Option<String> = None;
meta.parse_nested_meta(|nested| {
if nested.path.is_ident("name") {
let value: syn::LitStr = nested.value()?.parse()?;
name = Some(value.value());
} else if nested.path.is_ident("kind") {
let value: syn::LitStr = nested.value()?.parse()?;
let v = value.value();
if !matches!(v.as_str(), "unique" | "foreign_key" | "check") {
return Err(nested.error(format!(
"unknown constraint kind `{v}`; expected unique, foreign_key or check"
)));
}
kind = Some(v);
} else if nested.path.is_ident("field") {
let value: syn::LitStr = nested.value()?.parse()?;
field = Some(value.value());
} else {
return Err(nested.error("unknown constraint option; expected name, kind, field"));
}
Ok(())
})?;
let name = name.ok_or_else(|| meta.error("constraint requires name = \"...\""))?;
let kind = kind.ok_or_else(|| meta.error("constraint requires kind = \"...\""))?;
Ok(CustomConstraintDef {
name,
kind,
field
})
}
pub fn parse_index_attrs(attrs: &[Attribute]) -> Vec<CompositeIndexDef> {
let mut indexes = Vec::new();
for attr in attrs {
if !attr.path().is_ident("entity") {
continue;
}
let _ = attr.parse_nested_meta(|meta| {
let is_index = meta.path.is_ident("index");
let is_unique_index = meta.path.is_ident("unique_index");
if is_index || is_unique_index {
if let Ok(idx) = parse_index_content(&meta, is_unique_index) {
indexes.push(idx);
}
} else if meta.input.peek(syn::Token![=]) {
let _: syn::Token![=] = meta.input.parse()?;
let _: syn::Expr = meta.input.parse()?;
} else if meta.input.peek(syn::token::Paren) {
let content;
syn::parenthesized!(content in meta.input);
let _: proc_macro2::TokenStream = content.parse()?;
}
Ok(())
});
}
indexes
}
fn parse_index_content(
meta: &syn::meta::ParseNestedMeta<'_>,
unique: bool
) -> syn::Result<CompositeIndexDef> {
let mut columns = Vec::new();
let mut name = None;
let mut index_type = IndexType::default();
let mut where_clause = None;
meta.parse_nested_meta(|nested| {
let has_value = nested.input.peek(syn::Token![=]);
if has_value && nested.path.is_ident("type") {
let _: syn::Token![=] = nested.input.parse()?;
let value: syn::LitStr = nested.input.parse()?;
index_type = IndexType::from_str(&value.value()).unwrap_or_default();
} else if has_value && nested.path.is_ident("name") {
let _: syn::Token![=] = nested.input.parse()?;
let value: syn::LitStr = nested.input.parse()?;
name = Some(value.value());
} else if has_value && nested.path.is_ident("where") {
let _: syn::Token![=] = nested.input.parse()?;
let value: syn::LitStr = nested.input.parse()?;
where_clause = Some(value.value());
} else if let Some(ident) = nested.path.get_ident() {
columns.push(ident.to_string());
}
Ok(())
})?;
if columns.is_empty() {
return Err(meta.error("index must have at least one column"));
}
Ok(CompositeIndexDef {
name,
columns,
index_type,
unique,
where_clause
})
}
#[cfg(test)]
mod tests {
use syn::parse_quote;
use super::*;
#[test]
fn has_many_empty() {
let attrs: Vec<syn::Attribute> = vec![];
let result = parse_has_many_attrs(&attrs);
assert!(result.is_empty());
}
#[test]
fn has_many_single() {
let attrs: Vec<syn::Attribute> = vec![parse_quote!(#[has_many(Post)])];
let result = parse_has_many_attrs(&attrs);
assert_eq!(result.len(), 1);
assert_eq!(result[0].entity.to_string(), "Post");
}
#[test]
fn has_many_multiple() {
let attrs: Vec<syn::Attribute> = vec![
parse_quote!(#[has_many(Post)]),
parse_quote!(#[has_many(Comment)]),
parse_quote!(#[has_many(Like)]),
];
let result = parse_has_many_attrs(&attrs);
assert_eq!(result.len(), 3);
assert_eq!(result[0].entity.to_string(), "Post");
assert_eq!(result[1].entity.to_string(), "Comment");
assert_eq!(result[2].entity.to_string(), "Like");
}
#[test]
fn has_many_through_parsed() {
let attrs: Vec<syn::Attribute> =
vec![parse_quote!(#[has_many(User, through = "team_members")])];
let result = parse_has_many_attrs(&attrs);
assert_eq!(result.len(), 1);
assert_eq!(result[0].entity.to_string(), "User");
assert_eq!(result[0].through.as_deref(), Some("team_members"));
}
#[test]
fn has_many_plain_has_no_through() {
let attrs: Vec<syn::Attribute> = vec![parse_quote!(#[has_many(Post)])];
let result = parse_has_many_attrs(&attrs);
assert!(result[0].through.is_none());
}
#[test]
fn has_many_unknown_option_skipped() {
let attrs: Vec<syn::Attribute> =
vec![parse_quote!(#[has_many(User, via = "team_members")])];
let result = parse_has_many_attrs(&attrs);
assert!(result.is_empty());
}
#[test]
fn has_many_ignores_other_attrs() {
let attrs: Vec<syn::Attribute> = vec![
parse_quote!(#[derive(Debug)]),
parse_quote!(#[has_many(Post)]),
parse_quote!(#[entity(table = "users")]),
];
let result = parse_has_many_attrs(&attrs);
assert_eq!(result.len(), 1);
assert_eq!(result[0].entity.to_string(), "Post");
}
#[test]
fn api_attr_default_when_missing() {
let attrs: Vec<syn::Attribute> = vec![];
let result = parse_api_attr(&attrs);
assert!(result.tag.is_none());
assert!(result.security.is_none());
}
#[test]
fn api_attr_ignores_non_entity() {
let attrs: Vec<syn::Attribute> = vec![parse_quote!(#[derive(Debug)])];
let result = parse_api_attr(&attrs);
assert!(result.tag.is_none());
}
#[test]
fn api_attr_with_tag() {
let attrs: Vec<syn::Attribute> =
vec![parse_quote!(#[entity(table = "users", api(tag = "Users API"))])];
let result = parse_api_attr(&attrs);
assert_eq!(result.tag, Some("Users API".to_string()));
}
#[test]
fn api_attr_with_security() {
let attrs: Vec<syn::Attribute> =
vec![parse_quote!(#[entity(table = "users", api(security = "bearer"))])];
let result = parse_api_attr(&attrs);
assert!(result.security.is_some());
}
#[test]
fn api_attr_entity_without_api() {
let attrs: Vec<syn::Attribute> = vec![parse_quote!(#[entity(table = "users")])];
let result = parse_api_attr(&attrs);
assert!(result.tag.is_none());
}
#[test]
fn index_attrs_empty() {
let attrs: Vec<syn::Attribute> = vec![];
let result = parse_index_attrs(&attrs);
assert!(result.is_empty());
}
#[test]
fn index_attrs_no_indexes() {
let attrs: Vec<syn::Attribute> = vec![parse_quote!(#[entity(table = "users")])];
let result = parse_index_attrs(&attrs);
assert!(result.is_empty());
}
#[test]
fn index_attrs_single_column() {
let attrs: Vec<syn::Attribute> =
vec![parse_quote!(#[entity(table = "users", index(email))])];
let result = parse_index_attrs(&attrs);
assert_eq!(result.len(), 1);
assert_eq!(result[0].columns, vec!["email"]);
assert!(!result[0].unique);
assert_eq!(result[0].index_type, IndexType::BTree);
}
#[test]
fn index_attrs_multiple_columns() {
let attrs: Vec<syn::Attribute> =
vec![parse_quote!(#[entity(table = "users", index(name, email))])];
let result = parse_index_attrs(&attrs);
assert_eq!(result.len(), 1);
assert_eq!(result[0].columns, vec!["name", "email"]);
}
#[test]
fn index_attrs_unique() {
let attrs: Vec<syn::Attribute> =
vec![parse_quote!(#[entity(table = "users", unique_index(tenant_id, email))])];
let result = parse_index_attrs(&attrs);
assert_eq!(result.len(), 1);
assert!(result[0].unique);
assert_eq!(result[0].columns, vec!["tenant_id", "email"]);
}
#[test]
fn index_attrs_with_type_gin() {
let attrs: Vec<syn::Attribute> =
vec![parse_quote!(#[entity(table = "posts", index(type = "gin", tags))])];
let result = parse_index_attrs(&attrs);
assert_eq!(result.len(), 1);
assert_eq!(result[0].index_type, IndexType::Gin);
assert_eq!(result[0].columns, vec!["tags"]);
}
#[test]
fn index_attrs_with_type_gist() {
let attrs: Vec<syn::Attribute> =
vec![parse_quote!(#[entity(table = "locations", index(type = "gist", coordinates))])];
let result = parse_index_attrs(&attrs);
assert_eq!(result.len(), 1);
assert_eq!(result[0].index_type, IndexType::Gist);
}
#[test]
fn index_attrs_with_type_brin() {
let attrs: Vec<syn::Attribute> =
vec![parse_quote!(#[entity(table = "logs", index(type = "brin", created_at))])];
let result = parse_index_attrs(&attrs);
assert_eq!(result.len(), 1);
assert_eq!(result[0].index_type, IndexType::Brin);
}
#[test]
fn index_attrs_with_type_hash() {
let attrs: Vec<syn::Attribute> =
vec![parse_quote!(#[entity(table = "cache", index(type = "hash", key))])];
let result = parse_index_attrs(&attrs);
assert_eq!(result.len(), 1);
assert_eq!(result[0].index_type, IndexType::Hash);
}
#[test]
fn index_attrs_with_custom_name() {
let attrs: Vec<syn::Attribute> =
vec![parse_quote!(#[entity(table = "users", index(name = "idx_custom", status))])];
let result = parse_index_attrs(&attrs);
assert_eq!(result.len(), 1);
assert_eq!(result[0].name, Some("idx_custom".to_string()));
assert_eq!(result[0].columns, vec!["status"]);
}
#[test]
fn index_attrs_with_where_clause() {
let attrs: Vec<syn::Attribute> = vec![
parse_quote!(#[entity(table = "users", index(email, where = "deleted_at IS NULL"))]),
];
let result = parse_index_attrs(&attrs);
assert_eq!(result.len(), 1);
assert_eq!(
result[0].where_clause,
Some("deleted_at IS NULL".to_string())
);
}
#[test]
fn index_attrs_multiple_indexes() {
let attrs: Vec<syn::Attribute> = vec![parse_quote!(
#[entity(
table = "users",
index(email),
unique_index(tenant_id, email),
index(type = "gin", tags)
)]
)];
let result = parse_index_attrs(&attrs);
assert_eq!(result.len(), 3);
assert_eq!(result[0].columns, vec!["email"]);
assert!(!result[0].unique);
assert_eq!(result[1].columns, vec!["tenant_id", "email"]);
assert!(result[1].unique);
assert_eq!(result[2].columns, vec!["tags"]);
assert_eq!(result[2].index_type, IndexType::Gin);
}
#[test]
fn index_attrs_all_options() {
let attrs: Vec<syn::Attribute> = vec![parse_quote!(
#[entity(
table = "users",
unique_index(name = "idx_active_users", type = "btree", email, where = "active = true")
)]
)];
let result = parse_index_attrs(&attrs);
assert_eq!(result.len(), 1);
assert!(result[0].unique);
assert_eq!(result[0].name, Some("idx_active_users".to_string()));
assert_eq!(result[0].index_type, IndexType::BTree);
assert_eq!(result[0].columns, vec!["email"]);
assert_eq!(result[0].where_clause, Some("active = true".to_string()));
}
#[test]
fn index_attrs_ignores_non_entity() {
let attrs: Vec<syn::Attribute> = vec![parse_quote!(#[derive(Debug)])];
let result = parse_index_attrs(&attrs);
assert!(result.is_empty());
}
#[test]
fn index_attrs_unknown_type_defaults_to_btree() {
let attrs: Vec<syn::Attribute> =
vec![parse_quote!(#[entity(table = "users", index(type = "unknown", col))])];
let result = parse_index_attrs(&attrs);
assert_eq!(result.len(), 1);
assert_eq!(result[0].index_type, IndexType::BTree);
}
}