Skip to main content

arkhe_macros/
lib.rs

1//! Derive macros for ArkheKernel.
2//!
3//! Three derives, one shared shape:
4//! - `#[derive(ArkheAction)]`    — emits `Sealed` + `ActionDeriv`. The
5//!   user supplies `impl ActionCompute for T { fn compute ... }`; the
6//!   kernel-side blanket
7//!   `impl<T: ActionDeriv + ActionCompute> Action for T` provides the
8//!   postcard-canonical `canonical_bytes`/`from_bytes`/`approx_size`
9//!   defaults.
10//! - `#[derive(ArkheComponent)]` — emits `Sealed` + `Component`. No
11//!   user method to implement; the trait's default methods (postcard)
12//!   handle the data round trip.
13//! - `#[derive(ArkheEvent)]`     — emits `Sealed` + `Event`, same shape
14//!   as Component. The user type must additionally derive `Debug` plus
15//!   `serde::{Serialize, Deserialize}`.
16//!
17//! Every derive expects `#[arkhe(type_code = N, schema_version = M)]`.
18//! `schema_version` defaults to 1 when omitted; `type_code` is mandatory.
19//!
20//! ```ignore
21//! use arkhe_kernel::{ArkheComponent, ArkheEvent};
22//! use serde::{Serialize, Deserialize};
23//!
24//! #[derive(Serialize, Deserialize, ArkheComponent)]
25//! #[arkhe(type_code = 5001, schema_version = 1)]
26//! struct CounterComponent { value: u64 }
27//!
28//! #[derive(Debug, Serialize, Deserialize, ArkheEvent)]
29//! #[arkhe(type_code = 8001)]
30//! struct PostCreatedEvent { author: u64, body: String }
31//! ```
32
33use proc_macro::TokenStream;
34use quote::quote;
35use syn::{parse_macro_input, Attribute, DeriveInput, Generics, Ident};
36
37/// `#[arkhe(type_code = N, schema_version = M)]` parsed values.
38struct ArkheArgs {
39    type_code: u32,
40    schema_version: u32,
41}
42
43/// Parse `#[arkhe(...)]` attributes on the deriving type. Returns the
44/// values, or a `compile_error!` token stream if the attribute is
45/// missing/malformed. `schema_version` defaults to 1.
46fn parse_arkhe_args(attrs: &[Attribute], target: &Ident) -> Result<ArkheArgs, TokenStream> {
47    let mut type_code: Option<u32> = None;
48    let mut schema_version: u32 = 1;
49    let mut parse_error: Option<syn::Error> = None;
50
51    for attr in attrs {
52        if !attr.path().is_ident("arkhe") {
53            continue;
54        }
55        let result = attr.parse_nested_meta(|meta| {
56            if meta.path.is_ident("type_code") {
57                let value = meta.value()?;
58                let lit: syn::LitInt = value.parse()?;
59                type_code = Some(lit.base10_parse()?);
60                return Ok(());
61            }
62            if meta.path.is_ident("schema_version") {
63                let value = meta.value()?;
64                let lit: syn::LitInt = value.parse()?;
65                schema_version = lit.base10_parse()?;
66                return Ok(());
67            }
68            Err(meta
69                .error("unknown #[arkhe(...)] argument; expected `type_code` or `schema_version`"))
70        });
71        if let Err(e) = result {
72            parse_error = Some(e);
73            break;
74        }
75    }
76
77    if let Some(err) = parse_error {
78        return Err(err.to_compile_error().into());
79    }
80
81    let type_code = match type_code {
82        Some(v) => v,
83        None => {
84            return Err(syn::Error::new_spanned(
85                target,
86                "missing #[arkhe(type_code = N)] attribute",
87            )
88            .to_compile_error()
89            .into());
90        }
91    };
92
93    Ok(ArkheArgs {
94        type_code,
95        schema_version,
96    })
97}
98
99/// Emit the `Sealed` impl shared by all three derives.
100fn sealed_impl(name: &Ident, generics: &Generics) -> proc_macro2::TokenStream {
101    let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
102    quote! {
103        impl #impl_generics ::arkhe_kernel::state::traits::_sealed::Sealed
104            for #name #ty_generics #where_clause {}
105    }
106}
107
108/// Derive `Sealed` + `ActionDeriv` for a domain action. Pair with
109/// `impl ActionCompute for ...` to satisfy the kernel `Action` blanket.
110#[proc_macro_derive(ArkheAction, attributes(arkhe))]
111pub fn derive_arkhe_action(input: TokenStream) -> TokenStream {
112    let input = parse_macro_input!(input as DeriveInput);
113    let name = input.ident.clone();
114    let args = match parse_arkhe_args(&input.attrs, &name) {
115        Ok(a) => a,
116        Err(ts) => return ts,
117    };
118    let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
119    let sealed = sealed_impl(&name, &input.generics);
120    let type_code = args.type_code;
121    let schema_version = args.schema_version;
122
123    let expanded = quote! {
124        #sealed
125
126        impl #impl_generics ::arkhe_kernel::state::traits::ActionDeriv
127            for #name #ty_generics #where_clause
128        {
129            const TYPE_CODE: ::arkhe_kernel::abi::TypeCode =
130                ::arkhe_kernel::abi::TypeCode(#type_code);
131            const SCHEMA_VERSION: u32 = #schema_version;
132        }
133    };
134
135    TokenStream::from(expanded)
136}
137
138/// Derive `Sealed` + `Component`. Default trait methods (postcard) cover
139/// `canonical_bytes` / `from_bytes` / `approx_size` — no user method
140/// required beyond `serde::{Serialize, Deserialize}` derives.
141#[proc_macro_derive(ArkheComponent, attributes(arkhe))]
142pub fn derive_arkhe_component(input: TokenStream) -> TokenStream {
143    let input = parse_macro_input!(input as DeriveInput);
144    let name = input.ident.clone();
145    let args = match parse_arkhe_args(&input.attrs, &name) {
146        Ok(a) => a,
147        Err(ts) => return ts,
148    };
149    let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
150    let sealed = sealed_impl(&name, &input.generics);
151    let type_code = args.type_code;
152    let schema_version = args.schema_version;
153
154    let expanded = quote! {
155        #sealed
156
157        impl #impl_generics ::arkhe_kernel::state::traits::Component
158            for #name #ty_generics #where_clause
159        {
160            const TYPE_CODE: ::arkhe_kernel::abi::TypeCode =
161                ::arkhe_kernel::abi::TypeCode(#type_code);
162            const SCHEMA_VERSION: u32 = #schema_version;
163        }
164    };
165
166    TokenStream::from(expanded)
167}
168
169/// Derive `Sealed` + `Event`. Same shape as `ArkheComponent`; the user
170/// type must additionally derive `Debug` plus serde.
171#[proc_macro_derive(ArkheEvent, attributes(arkhe))]
172pub fn derive_arkhe_event(input: TokenStream) -> TokenStream {
173    let input = parse_macro_input!(input as DeriveInput);
174    let name = input.ident.clone();
175    let args = match parse_arkhe_args(&input.attrs, &name) {
176        Ok(a) => a,
177        Err(ts) => return ts,
178    };
179    let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
180    let sealed = sealed_impl(&name, &input.generics);
181    let type_code = args.type_code;
182    let schema_version = args.schema_version;
183
184    let expanded = quote! {
185        #sealed
186
187        impl #impl_generics ::arkhe_kernel::state::traits::Event
188            for #name #ty_generics #where_clause
189        {
190            const TYPE_CODE: ::arkhe_kernel::abi::TypeCode =
191                ::arkhe_kernel::abi::TypeCode(#type_code);
192            const SCHEMA_VERSION: u32 = #schema_version;
193        }
194    };
195
196    TokenStream::from(expanded)
197}