use proc_macro2::TokenStream;
use quote::{ToTokens, quote};
use syn::parse::{Parse, ParseStream};
use crate::diagnostic::{MacroError, MacroErrorCode, MacroResult};
pub fn policy(attr: TokenStream, item: TokenStream) -> MacroResult {
let args: PolicyArgs =
syn::parse2(attr).map_err(|e| MacroError::from_syn(MacroErrorCode::ArcM002, e))?;
let item_struct: syn::ItemStruct =
syn::parse2(item).map_err(|e| MacroError::from_syn(MacroErrorCode::ArcM001, e))?;
let struct_name = &item_struct.ident;
let (impl_generics, ty_generics, where_clause) = item_struct.generics.split_for_impl();
let name_lit = args.name.unwrap_or_else(|| struct_name.to_string());
let model_name = simple_path_name(&args.model);
Ok(quote! {
#item_struct
impl #impl_generics ::arcature::DxComponent for #struct_name #ty_generics #where_clause {
const NAME: &'static str = #name_lit;
}
impl #impl_generics #struct_name #ty_generics #where_clause {
pub const POLICY_FOR: &'static str = #model_name;
}
})
}
struct PolicyArgs {
model: syn::Path,
name: Option<String>,
}
impl Parse for PolicyArgs {
fn parse(input: ParseStream) -> syn::Result<Self> {
let model: syn::Path = input.parse()?;
let mut name: Option<String> = None;
while input.peek(syn::Token![,]) {
let _: syn::Token![,] = input.parse()?;
let ident: syn::Ident = input.parse()?;
let _: syn::Token![=] = input.parse()?;
match ident.to_string().as_str() {
"name" => {
let lit: syn::LitStr = input.parse()?;
name = Some(lit.value());
}
other => {
return Err(syn::Error::new(
ident.span(),
format!("unknown `#[policy]` argument `{other}`; expected `name`"),
));
}
}
}
Ok(PolicyArgs { model, name })
}
}
fn simple_path_name(path: &syn::Path) -> String {
path.segments
.last()
.map(|seg| seg.ident.to_string())
.unwrap_or_else(|| path.to_token_stream().to_string())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn generates_dx_component_and_policy_for() {
let expanded = policy(quote! { Link }, quote! { pub struct LinkPolicy; })
.unwrap()
.to_string();
assert!(
expanded.contains("DxComponent"),
"missing DxComponent: {expanded}"
);
assert!(
expanded.contains("\"LinkPolicy\""),
"wrong NAME: {expanded}"
);
assert!(
expanded.contains("POLICY_FOR"),
"missing POLICY_FOR: {expanded}"
);
assert!(
expanded.contains("\"Link\""),
"missing model name: {expanded}"
);
assert!(
!expanded.contains(":: arcature :: Policy"),
"should not generate Policy trait impl: {expanded}"
);
}
#[test]
fn uses_name_override() {
let expanded = policy(
quote! { Link, name = "LinkAuthz" },
quote! { pub struct LinkPolicy; },
)
.unwrap()
.to_string();
assert!(
expanded.contains("\"LinkAuthz\""),
"expected LinkAuthz: {expanded}"
);
}
#[test]
fn qualified_model_path_uses_last_segment() {
let expanded = policy(
quote! { myapp::models::Link },
quote! { pub struct LinkPolicy; },
)
.unwrap()
.to_string();
assert!(
expanded.contains("\"Link\""),
"expected simple name: {expanded}"
);
}
#[test]
fn rejects_enum() {
let err = policy(quote! { Link }, quote! { enum Status { Active } }).unwrap_err();
assert_eq!(err.code(), MacroErrorCode::ArcM001);
}
#[test]
fn rejects_unknown_argument() {
let err = policy(quote! { Link, foo = 42 }, quote! { pub struct LinkPolicy; }).unwrap_err();
assert_eq!(err.code(), MacroErrorCode::ArcM002);
}
}