use proc_macro2::TokenStream;
use quote::quote;
use syn::{GenericParam, LitInt};
use crate::stable_id;
pub fn expand(ast: &syn::DeriveInput) -> TokenStream {
let custom_id = match get_custom_id(ast) {
Ok(id) => id,
Err(err) => return err.to_compile_error(),
};
let name = &ast.ident;
let (impl_generics, ty_generics, where_clause) = ast.generics.split_for_impl();
if let Some(value) = custom_id {
return quote! {
impl #impl_generics ::acktor::MessageId for #name #ty_generics #where_clause
{
const ID: u64 = #value;
}
};
}
let mut extra_bounds = Vec::<TokenStream>::new();
for param in &ast.generics.params {
if let GenericParam::Type(t) = param {
let ident = &t.ident;
extra_bounds.push(quote! {
#ident: ::acktor::StableId
});
}
}
let where_clause_tokens = match (where_clause, extra_bounds.is_empty()) {
(Some(wc), true) => quote! { #wc },
(Some(wc), false) => {
let sep = if wc.predicates.empty_or_trailing() {
quote! {}
} else {
quote! { , }
};
quote! { #wc #sep #(#extra_bounds),* }
}
(None, true) => quote! {},
(None, false) => quote! { where #(#extra_bounds),* },
};
let stable_id_impl = stable_id::expand(ast);
quote! {
impl #impl_generics ::acktor::MessageId for #name #ty_generics #where_clause_tokens
{
const ID: u64 = <Self as ::acktor::StableId>::TYPE_ID.as_u64();
}
#stable_id_impl
}
}
fn get_custom_id(ast: &syn::DeriveInput) -> syn::Result<Option<LitInt>> {
let Some(attr) = ast.attrs.iter().find(|a| a.path().is_ident("custom_id")) else {
return Ok(None);
};
let syn::Meta::List(list) = &attr.meta else {
return Err(syn::Error::new_spanned(
attr,
"the correct syntax is `#[custom_id(<u64 value>)]`",
));
};
let lit = list.parse_args::<LitInt>()?;
lit.base10_parse::<u64>()?;
Ok(Some(lit))
}
#[cfg(test)]
mod tests {
use pretty_assertions::assert_eq;
use super::*;
fn input(src: &str) -> syn::DeriveInput {
syn::parse_str(src).unwrap()
}
#[test]
fn test_no_generics() {
let out = expand(&input("struct Ping;")).to_string();
assert!(out.contains("impl :: acktor :: StableId for Ping"));
assert!(out.contains("impl :: acktor :: MessageId for Ping"));
assert!(out.contains("TYPE_ID . as_u64 ()"));
}
#[test]
fn test_type_generics() {
let out = expand(&input("struct Wrap<T>(T);")).to_string();
assert!(out.contains("impl < T > :: acktor :: StableId"));
assert!(out.contains("impl < T > :: acktor :: MessageId for Wrap < T >"));
assert_eq!(out.matches("T : :: acktor :: StableId").count(), 2);
}
#[test]
fn test_const_generics() {
let out = expand(&input("struct Buf<const N: usize>;")).to_string();
assert!(out.contains("impl < const N : usize > :: acktor :: MessageId for Buf < N >"));
assert!(!out.contains("where"));
}
#[test]
fn test_lifetime_only_generics() {
let out = expand(&input("struct Borrow<'a>(&'a u8);")).to_string();
assert!(out.contains("impl < 'a > :: acktor :: StableId for Borrow < 'a >"));
assert!(out.contains("impl < 'a > :: acktor :: MessageId for Borrow < 'a >"));
assert!(!out.contains("where"));
}
#[test]
fn test_mixed_generics() {
let out = expand(&input(
"struct Mixed<'a, T, const N: usize>(&'a std::marker::PhantomData<[T; N]>);",
))
.to_string();
assert_eq!(out.matches("T : :: acktor :: StableId").count(), 2);
assert!(out.contains("(N as u64) . to_be_bytes ()"));
}
#[test]
fn test_custom_id() {
let out = expand(&input("#[custom_id(42)] struct Ping;")).to_string();
assert!(out.contains("impl :: acktor :: MessageId for Ping"));
assert!(out.contains("const ID : u64 = 42"));
assert!(!out.contains("StableId for Ping"));
assert!(!out.contains("TYPE_ID"));
let out = expand(&input("#[custom_id(0xdead_beef)] struct Ping;")).to_string();
assert!(out.contains("const ID : u64 = 0xdead_beef"));
let out = expand(&input("#[custom_id(7)] struct Wrap<T>(T);")).to_string();
assert!(out.contains("impl < T > :: acktor :: MessageId for Wrap < T >"));
assert!(!out.contains("T : :: acktor :: StableId"));
assert!(!out.contains("StableId for Wrap"));
let out = expand(&input("#[custom_id(18446744073709551616)] struct Ping;")).to_string();
assert!(out.contains("compile_error"));
let out = expand(&input("#[custom_id(\"oops\")] struct Ping;")).to_string();
assert!(out.contains("compile_error"));
}
}