use proc_macro2::TokenStream;
use quote::quote;
use syn::{GenericParam, Type};
pub fn expand(ast: &syn::DeriveInput) -> TokenStream {
let name = &ast.ident;
let mut combines = Vec::<TokenStream>::new();
let mut extra_bounds = Vec::<TokenStream>::new();
for param in &ast.generics.params {
match param {
GenericParam::Lifetime(_) => {}
GenericParam::Type(t) => {
let ident = &t.ident;
combines.push(quote! {
.combine(
<#ident as ::acktor::StableId>::TYPE_ID.as_bytes()
)
});
extra_bounds.push(quote! {
#ident: ::acktor::StableId
});
}
GenericParam::Const(c) => {
let ident = &c.ident;
match combine_const_param(ident, &c.ty) {
Ok(tokens) => combines.push(tokens),
Err(err) => return err.to_compile_error(),
}
}
}
}
let (impl_generics, ty_generics, where_clause) = ast.generics.split_for_impl();
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),* },
};
quote! {
impl #impl_generics ::acktor::StableId for #name #ty_generics #where_clause_tokens
{
const TYPE_ID: ::acktor::StableTypeId =
::acktor::StableTypeId::from_stable_type_name(
concat!(module_path!(), "::", stringify!(#name))
)
#(#combines)*;
}
}
}
fn combine_const_param(ident: &syn::Ident, ty: &Type) -> syn::Result<TokenStream> {
const SUPPORTED: &str = "StableId derive only supports legal const generic parameter types";
let Type::Path(tp) = ty else {
return Err(syn::Error::new_spanned(ty, SUPPORTED));
};
let seg = tp
.path
.segments
.last()
.expect("type syntax should guarantee at least one segment");
let name = seg.ident.to_string();
let value_bytes = match name.as_str() {
"u8" | "u16" | "u32" | "u64" | "u128" | "i8" | "i16" | "i32" | "i64" | "i128" => quote! {
&#ident.to_be_bytes()
},
"usize" => quote! { &(#ident as u64).to_be_bytes() },
"isize" => quote! { &(#ident as i64).to_be_bytes() },
"bool" => quote! { &[#ident as u8] },
"char" => quote! { &(#ident as u32).to_be_bytes() },
_ => {
return Err(syn::Error::new_spanned(ty, SUPPORTED));
}
};
Ok(quote! {
.combine(&::acktor::stable_type_id::take16(
::acktor::sha2_const::Sha256::new()
.update(#value_bytes)
.finalize(),
))
})
}
#[cfg(test)]
mod tests {
use super::*;
use pretty_assertions::assert_eq;
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(":: from_stable_type_name"));
assert!(out.contains("concat ! (module_path ! ()"));
assert!(out.contains("stringify ! (Ping)"));
assert!(!out.contains(". combine ("));
}
#[test]
fn test_type_generics() {
let out = expand(&input("struct Wrap<T>(T);")).to_string();
assert!(out.contains("T : :: acktor :: StableId"));
assert!(out.contains(":: TYPE_ID . as_bytes ()"));
assert_eq!(out.matches(". combine (").count(), 1);
let out = expand(&input("struct Pair<A, B>(A, B);")).to_string();
assert_eq!(out.matches(". combine (").count(), 2);
assert!(out.contains("A : :: acktor :: StableId"));
assert!(out.contains("B : :: acktor :: StableId"));
let out = expand(&input("struct Wrap<T, U>(T, U) where T: Clone, U: Clone,;")).to_string();
assert!(!out.contains(", ,"));
assert!(out.contains("T : :: acktor :: StableId"));
assert!(out.contains("U : :: acktor :: StableId"));
let out = expand(&input("struct Wrap<T, U>(T, U) where T: Clone, U: Clone;")).to_string();
assert!(!out.contains(", ,"));
assert!(out.contains("T : :: acktor :: StableId"));
assert!(out.contains("U : :: acktor :: StableId"));
}
#[test]
fn test_const_generics() {
let out = expand(&input("struct USize<const N: usize>;")).to_string();
assert!(out.contains("(N as u64) . to_be_bytes ()"));
assert!(out.contains(":: sha2_const :: Sha256 :: new"));
assert_eq!(out.matches(". combine (").count(), 1);
let out = expand(&input("struct ISize<const I: isize>;")).to_string();
assert!(out.contains("(I as i64) . to_be_bytes ()"));
let out = expand(&input("struct I32<const I: i32>;")).to_string();
assert!(out.contains("I . to_be_bytes ()"));
let out = expand(&input("struct Bool<const F: bool>;")).to_string();
assert!(out.contains("[F as u8]"));
let out = expand(&input("struct Char<const C: char>;")).to_string();
assert!(out.contains("(C as u32) . to_be_bytes ()"));
let out = expand(&input("struct Bad<const S: SomeUnknown>;")).to_string();
assert!(out.contains("StableId derive only supports legal const generic parameter types"));
}
#[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(". combine ("));
}
#[test]
fn test_mixed_generics() {
let out = expand(&input(
"struct Mixed<T, const N: usize>(std::marker::PhantomData<T>);",
))
.to_string();
assert_eq!(out.matches(". combine (").count(), 2);
assert!(out.contains("T : :: acktor :: StableId"));
assert!(out.contains("(N as u64) . to_be_bytes ()"));
}
}