use proc_macro2::TokenStream;
use quote::quote;
use syn::parse::{Parse, ParseStream};
use syn::spanned::Spanned;
use crate::diagnostic::{MacroError, MacroErrorCode, MacroResult};
pub fn route_model(attr: TokenStream, item: TokenStream) -> MacroResult {
let RouteModelArgs {
entity,
key_param,
key_type,
} = syn::parse2(attr).map_err(|e| MacroError::from_syn(MacroErrorCode::ArcM005, e))?;
let item_struct: syn::ItemStruct =
syn::parse2(item).map_err(|e| MacroError::from_syn(MacroErrorCode::ArcM001, e))?;
if matches!(item_struct.fields, syn::Fields::Unit) {
return Err(MacroError::new(
MacroErrorCode::ArcM002,
item_struct.fields.span(),
"#[route_model] requires a struct (tuple or named), not a unit struct",
));
}
let struct_name = &item_struct.ident;
let (impl_generics, ty_generics, where_clause) = item_struct.generics.split_for_impl();
Ok(quote! {
#item_struct
impl #impl_generics ::arcature::RouteModel for #struct_name #ty_generics
#where_clause
{
type Key = #key_type;
type Error = ::arcature::sea_orm::DbErr;
const KEY_PARAM: &'static str = #key_param;
async fn load(
key: Self::Key,
db: &::arcature::database::Db,
) -> ::std::result::Result<::std::option::Option<Self>, Self::Error> {
use ::arcature::sea_orm::EntityTrait as _;
let model = #entity::find_by_id(key).one(db.orm()).await?;
::std::result::Result::Ok(model.map(#struct_name))
}
}
})
}
struct RouteModelArgs {
entity: syn::Path,
key_param: String,
key_type: syn::Type,
}
impl Parse for RouteModelArgs {
fn parse(input: ParseStream) -> syn::Result<Self> {
let mut entity: Option<syn::Path> = None;
let mut key_param: Option<String> = None;
let mut key_type: Option<syn::Type> = None;
while !input.is_empty() {
let ident: syn::Ident = input.parse()?;
let _: syn::Token![=] = input.parse()?;
match ident.to_string().as_str() {
"entity" => entity = Some(input.parse()?),
"key" => key_param = Some(input.parse::<syn::LitStr>()?.value()),
"key_type" => key_type = Some(input.parse()?),
other => {
return Err(syn::Error::new(
ident.span(),
format!(
"unknown `#[route_model]` argument `{other}`; \
expected `entity`, `key`, or `key_type`"
),
));
}
}
let _ = input.parse::<syn::Token![,]>();
}
Ok(RouteModelArgs {
entity: entity.ok_or_else(|| {
syn::Error::new(
proc_macro2::Span::call_site(),
"#[route_model] requires an `entity` argument, e.g. `entity = link::Entity`",
)
})?,
key_type: key_type.ok_or_else(|| {
syn::Error::new(
proc_macro2::Span::call_site(),
"#[route_model] requires a `key_type` argument, e.g. `key_type = i64`",
)
})?,
key_param: key_param.unwrap_or_else(|| "id".to_string()),
})
}
}
#[cfg(test)]
mod tests {
use super::*;
fn expand_link(attr: TokenStream) -> String {
route_model(attr, quote! { pub struct Link(pub link::Model); })
.unwrap()
.to_string()
}
#[test]
fn generates_impl_route_model() {
let s = expand_link(quote! { entity = link::Entity, key = "id", key_type = i64 });
assert!(s.contains(":: arcature :: RouteModel"), "got: {s}");
assert!(s.contains("KEY_PARAM"), "got: {s}");
assert!(s.contains("find_by_id"), "got: {s}");
}
#[test]
fn uses_arcature_absolute_paths() {
let s = expand_link(quote! { entity = link::Entity, key_type = i64 });
assert!(s.contains(":: arcature :: database :: Db"), "got: {s}");
assert!(s.contains(":: arcature :: sea_orm :: DbErr"), "got: {s}");
}
#[test]
fn defaults_key_param_to_id() {
let s = expand_link(quote! { entity = post::Entity, key_type = uuid::Uuid });
assert!(s.contains("\"id\""), "got: {s}");
}
#[test]
fn honours_custom_key_param() {
let s = expand_link(quote! { entity = post::Entity, key = "slug", key_type = String });
assert!(s.contains("\"slug\""), "got: {s}");
}
#[test]
fn maps_loaded_model_to_self() {
let s = expand_link(quote! { entity = link::Entity, key_type = i64 });
assert!(s.contains("model . map (Link)"), "got: {s}");
}
#[test]
fn rejects_enum_item() {
let err = route_model(
quote! { entity = link::Entity, key_type = i64 },
quote! { enum Status { Active } },
)
.unwrap_err();
assert_eq!(err.code(), MacroErrorCode::ArcM001);
}
#[test]
fn rejects_unit_struct() {
let err = route_model(
quote! { entity = link::Entity, key_type = i64 },
quote! { pub struct Link; },
)
.unwrap_err();
assert_eq!(err.code(), MacroErrorCode::ArcM002);
}
#[test]
fn rejects_missing_entity() {
let err = route_model(
quote! { key_type = i64 },
quote! { pub struct Link(pub M); },
)
.unwrap_err();
assert_eq!(err.code(), MacroErrorCode::ArcM005);
}
#[test]
fn rejects_missing_key_type() {
let err = route_model(
quote! { entity = link::Entity },
quote! { pub struct Link(pub M); },
)
.unwrap_err();
assert_eq!(err.code(), MacroErrorCode::ArcM005);
}
#[test]
fn rejects_unknown_argument() {
let err = route_model(
quote! { entity = link::Entity, key_type = i64, foo = 42 },
quote! { pub struct Link(pub M); },
)
.unwrap_err();
assert_eq!(err.code(), MacroErrorCode::ArcM005);
}
}