feat(macros): add #[derive(Integrable)] proc-macro

This commit is contained in:
CleverWild
2026-06-29 16:58:35 +02:00
parent 647f0c8519
commit d861ff80be
4 changed files with 95 additions and 0 deletions

View File

@@ -0,0 +1,62 @@
use crate::utils::INTEGRABLE_TRAIT_PATH;
use proc_macro2::TokenStream;
use quote::quote;
use syn::{DeriveInput, LitInt, LitStr};
struct IntegrableAttr {
kind: String,
version: i32,
}
impl IntegrableAttr {
fn from_attrs(attrs: &[syn::Attribute], span: proc_macro2::Span) -> Result<Self, syn::Error> {
let mut kind: Option<String> = None;
let mut version: i32 = 1;
for attr in attrs {
if !attr.path().is_ident("integrable") {
continue;
}
attr.parse_nested_meta(|meta| {
if meta.path.is_ident("kind") {
let lit: LitStr = meta.value()?.parse()?;
kind = Some(lit.value());
} else if meta.path.is_ident("version") {
let lit: LitInt = meta.value()?.parse()?;
version = lit.base10_parse()?;
} else {
return Err(meta.error("unknown key; expected `kind` or `version`"));
}
Ok(())
})?;
}
let kind = kind.ok_or_else(|| {
syn::Error::new(span, "#[integrable(kind = \"...\")] is required")
})?;
Ok(Self { kind, version })
}
}
pub(crate) fn derive(input: &DeriveInput) -> TokenStream {
let integrable_trait = INTEGRABLE_TRAIT_PATH.to_path();
let ident = &input.ident;
let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
let attr = match IntegrableAttr::from_attrs(&input.attrs, proc_macro2::Span::call_site()) {
Ok(a) => a,
Err(e) => return e.to_compile_error(),
};
let kind = attr.kind;
let version = attr.version;
quote! {
#[automatically_derived]
impl #impl_generics #integrable_trait for #ident #ty_generics #where_clause {
const KIND: &'static str = #kind;
const VERSION: i32 = #version;
}
}
}

View File

@@ -1,6 +1,7 @@
use syn::{DeriveInput, parse_macro_input}; use syn::{DeriveInput, parse_macro_input};
mod hashable; mod hashable;
mod integrable;
mod utils; mod utils;
#[proc_macro_derive(Hashable)] #[proc_macro_derive(Hashable)]
@@ -8,3 +9,9 @@ pub fn derive_hashable(input: proc_macro::TokenStream) -> proc_macro::TokenStrea
let input = parse_macro_input!(input as DeriveInput); let input = parse_macro_input!(input as DeriveInput);
hashable::derive(&input).into() hashable::derive(&input).into()
} }
#[proc_macro_derive(Integrable, attributes(integrable))]
pub fn derive_integrable(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
let input = parse_macro_input!(input as DeriveInput);
integrable::derive(&input).into()
}

View File

@@ -22,3 +22,4 @@ macro_rules! ensure_path {
ensure_path!(::arbiter_crypto::hashing::Hashable as HASHABLE_TRAIT_PATH); ensure_path!(::arbiter_crypto::hashing::Hashable as HASHABLE_TRAIT_PATH);
ensure_path!(::arbiter_crypto::hashing::Digest as HMAC_DIGEST_PATH); ensure_path!(::arbiter_crypto::hashing::Digest as HMAC_DIGEST_PATH);
ensure_path!(::arbiter_crypto::integrity::Integrable as INTEGRABLE_TRAIT_PATH);

View File

@@ -0,0 +1,25 @@
use arbiter_crypto::integrity::Integrable;
#[derive(arbiter_macros::Hashable, arbiter_macros::Integrable)]
#[integrable(kind = "test_entity")]
struct TestEntity {
value: i32,
}
#[test]
fn default_version_is_one() {
assert_eq!(<TestEntity as Integrable>::VERSION, 1, "default version must be 1");
assert_eq!(<TestEntity as Integrable>::KIND, "test_entity");
}
#[derive(arbiter_macros::Hashable, arbiter_macros::Integrable)]
#[integrable(kind = "versioned_entity", version = 3)]
struct VersionedEntity {
data: String,
}
#[test]
fn explicit_version_attribute() {
assert_eq!(<VersionedEntity as Integrable>::VERSION, 3);
assert_eq!(<VersionedEntity as Integrable>::KIND, "versioned_entity");
}