diff --git a/server/crates/arbiter-macros/src/integrable.rs b/server/crates/arbiter-macros/src/integrable.rs new file mode 100644 index 0000000..4f979ed --- /dev/null +++ b/server/crates/arbiter-macros/src/integrable.rs @@ -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 { + let mut kind: Option = 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; + } + } +} diff --git a/server/crates/arbiter-macros/src/lib.rs b/server/crates/arbiter-macros/src/lib.rs index 51b8f79..a814f16 100644 --- a/server/crates/arbiter-macros/src/lib.rs +++ b/server/crates/arbiter-macros/src/lib.rs @@ -1,6 +1,7 @@ use syn::{DeriveInput, parse_macro_input}; mod hashable; +mod integrable; mod utils; #[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); 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() +} diff --git a/server/crates/arbiter-macros/src/utils.rs b/server/crates/arbiter-macros/src/utils.rs index c1ccf2e..cecd5ae 100644 --- a/server/crates/arbiter-macros/src/utils.rs +++ b/server/crates/arbiter-macros/src/utils.rs @@ -22,3 +22,4 @@ macro_rules! ensure_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::integrity::Integrable as INTEGRABLE_TRAIT_PATH); diff --git a/server/crates/arbiter-macros/tests/integrable.rs b/server/crates/arbiter-macros/tests/integrable.rs new file mode 100644 index 0000000..086764b --- /dev/null +++ b/server/crates/arbiter-macros/tests/integrable.rs @@ -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!(::VERSION, 1, "default version must be 1"); + assert_eq!(::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!(::VERSION, 3); + assert_eq!(::KIND, "versioned_entity"); +}