From b852eb2ae92cfa648c3919d9d2fb2da3311d4921 Mon Sep 17 00:00:00 2001 From: pgherveou Date: Fri, 29 May 2026 16:36:26 +0200 Subject: [PATCH 1/4] update versioned types --- rust/crates/truapi/src/versioned/mod.rs | 29 +++++++++++-------------- 1 file changed, 13 insertions(+), 16 deletions(-) diff --git a/rust/crates/truapi/src/versioned/mod.rs b/rust/crates/truapi/src/versioned/mod.rs index 9834e5c8..9bb9d280 100644 --- a/rust/crates/truapi/src/versioned/mod.rs +++ b/rust/crates/truapi/src/versioned/mod.rs @@ -1,23 +1,16 @@ //! Versioned request and response wrappers for the unified TrUAPI contract. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -pub enum Version { - /// Initial protocol version. - V1, -} - -pub mod latest { - use super::Version; - - pub const VERSION: Version = Version::V1; -} - #[allow(clippy::result_unit_err)] pub trait IntoVersion: Sized { - fn into_version(self, version: Version) -> Result; + /// The latest version this envelope supports. + const LATEST: u8; + + /// Convert this envelope to the requested version; `N` selects the `Vn` variant. + fn into_version(self, version: u8) -> Result; + /// Convert this envelope to its latest supported version. fn into_latest(self) -> Result { - self.into_version(latest::VERSION) + self.into_version(Self::LATEST) } } @@ -59,7 +52,9 @@ macro_rules! versioned_type { } impl $crate::versioned::IntoVersion for $name { - fn into_version(self, _version: $crate::versioned::Version) -> Result { + const LATEST: u8 = 1; + + fn into_version(self, _version: u8) -> Result { Ok(self) } } @@ -83,7 +78,9 @@ macro_rules! versioned_type { } impl $crate::versioned::IntoVersion for $name { - fn into_version(self, _version: $crate::versioned::Version) -> Result { + const LATEST: u8 = 1; + + fn into_version(self, _version: u8) -> Result { Ok(self) } } From 968af41796101b6f74fa7ffe2643abb4970b44c1 Mon Sep 17 00:00:00 2001 From: pgherveou Date: Fri, 29 May 2026 16:57:33 +0200 Subject: [PATCH 2/4] simplify for now --- rust/crates/truapi/src/versioned/mod.rs | 30 ------------------------- 1 file changed, 30 deletions(-) diff --git a/rust/crates/truapi/src/versioned/mod.rs b/rust/crates/truapi/src/versioned/mod.rs index 9bb9d280..65387211 100644 --- a/rust/crates/truapi/src/versioned/mod.rs +++ b/rust/crates/truapi/src/versioned/mod.rs @@ -1,19 +1,5 @@ //! Versioned request and response wrappers for the unified TrUAPI contract. -#[allow(clippy::result_unit_err)] -pub trait IntoVersion: Sized { - /// The latest version this envelope supports. - const LATEST: u8; - - /// Convert this envelope to the requested version; `N` selects the `Vn` variant. - fn into_version(self, version: u8) -> Result; - - /// Convert this envelope to its latest supported version. - fn into_latest(self) -> Result { - self.into_version(Self::LATEST) - } -} - macro_rules! versioned_type { ( $( @@ -50,14 +36,6 @@ macro_rules! versioned_type { #[codec(index = 0)] V1($v1), } - - impl $crate::versioned::IntoVersion for $name { - const LATEST: u8 = 1; - - fn into_version(self, _version: u8) -> Result { - Ok(self) - } - } }; ( @@ -76,14 +54,6 @@ macro_rules! versioned_type { #[codec(index = 0)] V1, } - - impl $crate::versioned::IntoVersion for $name { - const LATEST: u8 = 1; - - fn into_version(self, _version: u8) -> Result { - Ok(self) - } - } }; } From b3b574d095f95d769237dd81e483d7bba7671d9b Mon Sep 17 00:00:00 2001 From: pgherveou Date: Fri, 29 May 2026 17:23:22 +0200 Subject: [PATCH 3/4] feat(versioned): proc-macro envelopes + conversion traits Generate versioned message envelopes with a function-like `versioned_type!` proc macro that accepts an N-variant list, assigns positional SCALE codec indices, and implements `Versioned` (Latest/LATEST/version). Single-version envelopes also get trivial `IntoLatest`/`FromLatest`; multi-version envelopes leave those bespoke conversions to hand-written impls. The three traits in `truapi::versioned` define a total upgrade/downgrade contract for a server to normalize requests to the latest version, handle them in latest-only terms, and map results back to the caller's version. Generated TypeScript output is byte-identical; codegen skips the helper traits. --- rust/crates/truapi-codegen/src/rustdoc.rs | 8 +- rust/crates/truapi-macros/src/lib.rs | 219 +++++++++++++++++- rust/crates/truapi/src/versioned/account.rs | 1 + rust/crates/truapi/src/versioned/chain.rs | 1 + rust/crates/truapi/src/versioned/chat.rs | 1 + .../truapi/src/versioned/coin_payment.rs | 1 + rust/crates/truapi/src/versioned/entropy.rs | 1 + .../truapi/src/versioned/local_storage.rs | 1 + rust/crates/truapi/src/versioned/mod.rs | 79 +++---- .../truapi/src/versioned/notifications.rs | 1 + rust/crates/truapi/src/versioned/payment.rs | 1 + .../truapi/src/versioned/permissions.rs | 1 + rust/crates/truapi/src/versioned/preimage.rs | 1 + .../src/versioned/resource_allocation.rs | 1 + rust/crates/truapi/src/versioned/signing.rs | 1 + .../truapi/src/versioned/statement_store.rs | 1 + rust/crates/truapi/src/versioned/system.rs | 1 + rust/crates/truapi/src/versioned/theme.rs | 1 + 18 files changed, 262 insertions(+), 59 deletions(-) diff --git a/rust/crates/truapi-codegen/src/rustdoc.rs b/rust/crates/truapi-codegen/src/rustdoc.rs index 7f585342..2e8aa459 100644 --- a/rust/crates/truapi-codegen/src/rustdoc.rs +++ b/rust/crates/truapi-codegen/src/rustdoc.rs @@ -257,10 +257,10 @@ pub fn extract_api(krate: &Crate) -> Result { let mut traits = Vec::new(); for (name, candidates) in trait_candidates { - // `Versioned` is a runtime-helper trait on the wrapper enums, not a - // protocol-method trait. The codegen only cares about the protocol - // surface (TrUAPI methods); skip anything declared outside - // `truapi::api::*`. + // `Versioned`, `IntoLatest`, and `FromLatest` are runtime-helper traits + // on the wrapper enums, not protocol-method traits. The codegen only + // cares about the protocol surface (TrUAPI methods); skip anything + // declared outside `truapi::api::*`. let candidate = select_candidate(&name, &candidates)?; if !candidate.path.iter().any(|s| s == "api") { continue; diff --git a/rust/crates/truapi-macros/src/lib.rs b/rust/crates/truapi-macros/src/lib.rs index 20d063de..5520713e 100644 --- a/rust/crates/truapi-macros/src/lib.rs +++ b/rust/crates/truapi-macros/src/lib.rs @@ -1,6 +1,10 @@ //! Proc-macros for TrUAPI trait annotations. //! -//! The single attribute exposed is [`wire`], which marks a trait method with +//! `versioned_type!` is a function-like macro that generates versioned message +//! envelopes: the `Vn` enums (with SCALE codec indices) plus their +//! `Versioned`/`IntoLatest`/`FromLatest` impls from `truapi::versioned`. +//! +//! The `wire` attribute marks a trait method with //! its wire-protocol discriminant ids. The ids appear on the wire as the u8 discriminant in the //! `Struct { request_id: str, payload: Enum() }` envelope; method //! ordering becomes part of the wire protocol. @@ -17,9 +21,10 @@ //! rustdoc through the only attribute that is always preserved verbatim. use proc_macro::TokenStream; +use proc_macro2::Literal; use quote::quote; use syn::parse::{Parse, ParseStream}; -use syn::{Ident, ItemFn, LitInt, Token, TraitItemFn, parse_macro_input}; +use syn::{Attribute, Ident, ItemFn, LitInt, Token, TraitItemFn, Type, braced, parse_macro_input}; #[derive(Default)] struct WireArgs { @@ -138,3 +143,213 @@ fn wire_tags(args: &WireArgs) -> Vec { .filter_map(|(name, value)| value.map(|id| format!("@wire_{name}={id}"))) .collect() } + +/// One sequence of versioned envelope declarations passed to `versioned_type!`. +struct VersionedInput { + enums: Vec, +} + +impl Parse for VersionedInput { + fn parse(input: ParseStream<'_>) -> syn::Result { + let mut enums = Vec::new(); + while !input.is_empty() { + enums.push(input.parse()?); + } + Ok(Self { enums }) + } +} + +/// A single `pub enum Name { V1 => Ty, ... }` declaration. +struct VersionedEnum { + attrs: Vec, + name: Ident, + variants: Vec, +} + +impl Parse for VersionedEnum { + fn parse(input: ParseStream<'_>) -> syn::Result { + let attrs = input.call(Attribute::parse_outer)?; + input.parse::()?; + input.parse::()?; + let name: Ident = input.parse()?; + + let body; + braced!(body in input); + let mut variants = Vec::new(); + while !body.is_empty() { + variants.push(body.parse()?); + if body.peek(Token![,]) { + body.parse::()?; + } else { + break; + } + } + + Ok(Self { + attrs, + name, + variants, + }) + } +} + +/// A single `Vn` or `Vn => Ty` variant. +struct VersionedVariant { + attrs: Vec, + ident: Ident, + ty: Option, +} + +impl Parse for VersionedVariant { + fn parse(input: ParseStream<'_>) -> syn::Result { + let attrs = input.call(Attribute::parse_outer)?; + let ident: Ident = input.parse()?; + let ty = if input.peek(Token![=>]) { + input.parse::]>()?; + Some(input.parse()?) + } else { + None + }; + Ok(Self { attrs, ident, ty }) + } +} + +/// Parse the `Vn` version number from a variant identifier. +fn variant_version(ident: &Ident) -> syn::Result { + let name = ident.to_string(); + let err = || syn::Error::new(ident.span(), "variant must be named `Vn` where n is a u8"); + name.strip_prefix('V') + .ok_or_else(err)? + .parse::() + .map_err(|_| err()) +} + +/// Generate versioned message envelopes. +/// +/// ```ignore +/// versioned_type! { +/// pub enum HostFooRequest { V1 => v01::HostFooRequest } +/// pub enum HostFooResponse { V1 } +/// } +/// ``` +/// +/// Each declaration becomes a SCALE enum with positional codec indices and an +/// `impl Versioned` exposing `Latest`, `LATEST`, and `version()`. Single-version +/// envelopes also get trivial `IntoLatest`/`FromLatest` impls; multi-version +/// envelopes leave those to be written by hand, since the conversion is bespoke. +#[proc_macro] +pub fn versioned_type(item: TokenStream) -> TokenStream { + let input = parse_macro_input!(item as VersionedInput); + match expand_versioned(&input) { + Ok(tokens) => tokens.into(), + Err(err) => err.to_compile_error().into(), + } +} + +fn expand_versioned(input: &VersionedInput) -> syn::Result { + let mut out = proc_macro2::TokenStream::new(); + for enum_def in &input.enums { + out.extend(expand_versioned_enum(enum_def)?); + } + Ok(out) +} + +fn expand_versioned_enum(def: &VersionedEnum) -> syn::Result { + let VersionedEnum { + attrs, + name, + variants, + } = def; + + if variants.is_empty() { + return Err(syn::Error::new( + name.span(), + "versioned enum needs at least one variant", + )); + } + + let mut variant_defs = Vec::new(); + let mut version_arms = Vec::new(); + for (i, variant) in variants.iter().enumerate() { + let expected = (i as u8) + 1; + let version = variant_version(&variant.ident)?; + if version != expected { + return Err(syn::Error::new( + variant.ident.span(), + format!("expected variant `V{expected}`; versions must be contiguous from 1"), + )); + } + + let index = Literal::u8_unsuffixed(i as u8); + let version_lit = Literal::u8_unsuffixed(version); + let vattrs = &variant.attrs; + let vident = &variant.ident; + match &variant.ty { + Some(ty) => { + variant_defs.push(quote! { #(#vattrs)* #[codec(index = #index)] #vident(#ty) }); + version_arms.push(quote! { Self::#vident(..) => #version_lit }); + } + None => { + variant_defs.push(quote! { #(#vattrs)* #[codec(index = #index)] #vident }); + version_arms.push(quote! { Self::#vident => #version_lit }); + } + } + } + + let doc = format!("Versioned envelope for [`{name}`]."); + let latest_lit = Literal::u8_unsuffixed(variants.len() as u8); + let latest_ty = match &variants.last().expect("checked non-empty").ty { + Some(ty) => quote! { #ty }, + None => quote! { () }, + }; + + let mut tokens = quote! { + #(#attrs)* + #[doc = #doc] + #[derive(Debug, Clone, PartialEq, Eq, parity_scale_codec::Encode, parity_scale_codec::Decode)] + pub enum #name { + #(#variant_defs),* + } + + impl crate::versioned::Versioned for #name { + type Latest = #latest_ty; + const LATEST: u8 = #latest_lit; + fn version(&self) -> u8 { + match self { + #(#version_arms),* + } + } + } + }; + + if let [only] = &variants[..] { + let vident = &only.ident; + let (into_body, from_param, from_body) = match &only.ty { + Some(_) => ( + quote! { match self { Self::#vident(inner) => inner } }, + quote! { latest }, + quote! { Self::#vident(latest) }, + ), + None => ( + quote! { match self { Self::#vident => () } }, + quote! { _latest }, + quote! { Self::#vident }, + ), + }; + tokens.extend(quote! { + impl crate::versioned::IntoLatest for #name { + fn into_latest(self) -> Self::Latest { + #into_body + } + } + + impl crate::versioned::FromLatest for #name { + fn from_latest(#from_param: Self::Latest, _target: u8) -> Self { + #from_body + } + } + }); + } + + Ok(tokens) +} diff --git a/rust/crates/truapi/src/versioned/account.rs b/rust/crates/truapi/src/versioned/account.rs index 41f49af9..e7d8b220 100644 --- a/rust/crates/truapi/src/versioned/account.rs +++ b/rust/crates/truapi/src/versioned/account.rs @@ -1,6 +1,7 @@ //! Versioned wrappers for [`Account`](crate::api::Account) methods. use crate::v01; +use truapi_macros::versioned_type; versioned_type! { pub enum HostAccountGetRequest { V1 => v01::HostAccountGetRequest } diff --git a/rust/crates/truapi/src/versioned/chain.rs b/rust/crates/truapi/src/versioned/chain.rs index eaa12131..49ce407e 100644 --- a/rust/crates/truapi/src/versioned/chain.rs +++ b/rust/crates/truapi/src/versioned/chain.rs @@ -1,6 +1,7 @@ //! Versioned wrappers for [`Chain`](crate::api::Chain) methods. use crate::v01; +use truapi_macros::versioned_type; versioned_type! { pub enum RemoteChainHeadFollowRequest { V1 => v01::RemoteChainHeadFollowRequest } diff --git a/rust/crates/truapi/src/versioned/chat.rs b/rust/crates/truapi/src/versioned/chat.rs index 44eb7324..c732f157 100644 --- a/rust/crates/truapi/src/versioned/chat.rs +++ b/rust/crates/truapi/src/versioned/chat.rs @@ -1,6 +1,7 @@ //! Versioned wrappers for [`Chat`](crate::api::Chat) methods. use crate::v01; +use truapi_macros::versioned_type; versioned_type! { pub enum HostChatCreateRoomRequest { V1 => v01::HostChatCreateRoomRequest } diff --git a/rust/crates/truapi/src/versioned/coin_payment.rs b/rust/crates/truapi/src/versioned/coin_payment.rs index 6d6c894f..cb9128f0 100644 --- a/rust/crates/truapi/src/versioned/coin_payment.rs +++ b/rust/crates/truapi/src/versioned/coin_payment.rs @@ -1,6 +1,7 @@ //! Versioned wrappers for [`CoinPayment`](crate::api::CoinPayment) methods. use crate::v01; +use truapi_macros::versioned_type; versioned_type! { pub enum HostCoinPaymentCreatePurseRequest { V1 => v01::HostCoinPaymentCreatePurseRequest } diff --git a/rust/crates/truapi/src/versioned/entropy.rs b/rust/crates/truapi/src/versioned/entropy.rs index 9eed9400..c904e90c 100644 --- a/rust/crates/truapi/src/versioned/entropy.rs +++ b/rust/crates/truapi/src/versioned/entropy.rs @@ -1,6 +1,7 @@ //! Versioned wrappers for [`Entropy`](crate::api::Entropy) methods. use crate::v01; +use truapi_macros::versioned_type; versioned_type! { pub enum HostDeriveEntropyRequest { V1 => v01::HostDeriveEntropyRequest } diff --git a/rust/crates/truapi/src/versioned/local_storage.rs b/rust/crates/truapi/src/versioned/local_storage.rs index 3ed0261c..699c5af8 100644 --- a/rust/crates/truapi/src/versioned/local_storage.rs +++ b/rust/crates/truapi/src/versioned/local_storage.rs @@ -1,6 +1,7 @@ //! Versioned wrappers for [`LocalStorage`](crate::api::LocalStorage) methods. use crate::v01; +use truapi_macros::versioned_type; versioned_type! { pub enum HostLocalStorageReadRequest { V1 => v01::HostLocalStorageReadRequest } diff --git a/rust/crates/truapi/src/versioned/mod.rs b/rust/crates/truapi/src/versioned/mod.rs index 65387211..0c5ac12a 100644 --- a/rust/crates/truapi/src/versioned/mod.rs +++ b/rust/crates/truapi/src/versioned/mod.rs @@ -1,60 +1,33 @@ //! Versioned request and response wrappers for the unified TrUAPI contract. +//! +//! A versioned envelope is a SCALE enum whose variants (`V1`, `V2`, ...) are +//! successive versions of one logical message, newest last. A server normalizes +//! incoming values to [`Versioned::Latest`] with [`IntoLatest`], handles them in +//! latest terms, then maps results back to the caller's version with +//! [`FromLatest`]. The envelopes themselves are generated by `versioned_type!`. -macro_rules! versioned_type { - ( - $( - $(#[$enum_meta:meta])* - pub enum $name:ident { - $($body:tt)* - } - )* - ) => { - $( - versioned_type! { - @one - $(#[$enum_meta])* - pub enum $name { - $($body)* - } - } - )* - }; +/// A versioned message envelope. +pub trait Versioned: Sized { + /// The newest version's payload. Handlers operate exclusively on this. + type Latest; - ( - @one - $(#[$enum_meta:meta])* - pub enum $name:ident { - $(#[$variant_meta:meta])* - V1 => $v1:ty $(,)? - } - ) => { - $(#[$enum_meta])* - #[doc = concat!("Versioned envelope for [`", stringify!($name), "`].")] - #[derive(Debug, Clone, PartialEq, Eq, parity_scale_codec::Encode, parity_scale_codec::Decode)] - pub enum $name { - $(#[$variant_meta])* - #[codec(index = 0)] - V1($v1), - } - }; + /// Version number of the newest variant. + const LATEST: u8; - ( - @one - $(#[$enum_meta:meta])* - pub enum $name:ident { - $(#[$variant_meta:meta])* - V1 $(,)? - } - ) => { - $(#[$enum_meta])* - #[doc = concat!("Versioned envelope for [`", stringify!($name), "`].")] - #[derive(Debug, Clone, PartialEq, Eq, parity_scale_codec::Encode, parity_scale_codec::Decode)] - pub enum $name { - $(#[$variant_meta])* - #[codec(index = 0)] - V1, - } - }; + /// Version number of the variant currently held. + fn version(&self) -> u8; +} + +/// Upgrade a received envelope to its latest payload. Total by construction. +pub trait IntoLatest: Versioned { + /// Convert whatever version is held into the latest payload. + fn into_latest(self) -> Self::Latest; +} + +/// Downgrade a latest payload into the variant a peer at `target` understands. +pub trait FromLatest: Versioned { + /// Build the envelope for protocol version `target` (highest variant ≤ target). + fn from_latest(latest: Self::Latest, target: u8) -> Self; } pub mod account; diff --git a/rust/crates/truapi/src/versioned/notifications.rs b/rust/crates/truapi/src/versioned/notifications.rs index e2f19524..07c4d11a 100644 --- a/rust/crates/truapi/src/versioned/notifications.rs +++ b/rust/crates/truapi/src/versioned/notifications.rs @@ -1,6 +1,7 @@ //! Versioned wrappers for [`Notifications`](crate::api::Notifications) methods. use crate::v01; +use truapi_macros::versioned_type; versioned_type! { pub enum HostPushNotificationRequest { V1 => v01::HostPushNotificationRequest } diff --git a/rust/crates/truapi/src/versioned/payment.rs b/rust/crates/truapi/src/versioned/payment.rs index c33c36b4..3392da75 100644 --- a/rust/crates/truapi/src/versioned/payment.rs +++ b/rust/crates/truapi/src/versioned/payment.rs @@ -1,6 +1,7 @@ //! Versioned wrappers for [`Payment`](crate::api::Payment) methods. use crate::v01; +use truapi_macros::versioned_type; versioned_type! { pub enum HostPaymentBalanceSubscribeRequest { V1 => v01::HostPaymentBalanceSubscribeRequest } diff --git a/rust/crates/truapi/src/versioned/permissions.rs b/rust/crates/truapi/src/versioned/permissions.rs index 53e97079..d6921959 100644 --- a/rust/crates/truapi/src/versioned/permissions.rs +++ b/rust/crates/truapi/src/versioned/permissions.rs @@ -1,6 +1,7 @@ //! Versioned wrappers for [`Permissions`](crate::api::Permissions) methods. use crate::v01; +use truapi_macros::versioned_type; versioned_type! { #[derive(derive_more::Display)] diff --git a/rust/crates/truapi/src/versioned/preimage.rs b/rust/crates/truapi/src/versioned/preimage.rs index eaa81e16..ad4b7dc5 100644 --- a/rust/crates/truapi/src/versioned/preimage.rs +++ b/rust/crates/truapi/src/versioned/preimage.rs @@ -1,6 +1,7 @@ //! Versioned wrappers for [`Preimage`](crate::api::Preimage) methods. use crate::v01; +use truapi_macros::versioned_type; versioned_type! { pub enum RemotePreimageLookupSubscribeRequest { V1 => v01::RemotePreimageLookupSubscribeRequest } diff --git a/rust/crates/truapi/src/versioned/resource_allocation.rs b/rust/crates/truapi/src/versioned/resource_allocation.rs index 36466a10..d7528c08 100644 --- a/rust/crates/truapi/src/versioned/resource_allocation.rs +++ b/rust/crates/truapi/src/versioned/resource_allocation.rs @@ -1,6 +1,7 @@ //! Versioned wrappers for [`ResourceAllocation`](crate::api::ResourceAllocation) methods. use crate::v01; +use truapi_macros::versioned_type; versioned_type! { pub enum HostRequestResourceAllocationRequest { V1 => v01::HostRequestResourceAllocationRequest } diff --git a/rust/crates/truapi/src/versioned/signing.rs b/rust/crates/truapi/src/versioned/signing.rs index 718d5c0d..2423ed34 100644 --- a/rust/crates/truapi/src/versioned/signing.rs +++ b/rust/crates/truapi/src/versioned/signing.rs @@ -1,6 +1,7 @@ //! Versioned wrappers for [`Signing`](crate::api::Signing) methods. use crate::v01; +use truapi_macros::versioned_type; versioned_type! { pub enum HostSignPayloadRequest { V1 => v01::HostSignPayloadRequest } diff --git a/rust/crates/truapi/src/versioned/statement_store.rs b/rust/crates/truapi/src/versioned/statement_store.rs index b9a6fe27..fc73a83d 100644 --- a/rust/crates/truapi/src/versioned/statement_store.rs +++ b/rust/crates/truapi/src/versioned/statement_store.rs @@ -1,6 +1,7 @@ //! Versioned wrappers for [`StatementStore`](crate::api::StatementStore) methods. use crate::v01; +use truapi_macros::versioned_type; versioned_type! { pub enum RemoteStatementStoreSubscribeRequest { V1 => v01::RemoteStatementStoreSubscribeRequest } diff --git a/rust/crates/truapi/src/versioned/system.rs b/rust/crates/truapi/src/versioned/system.rs index 404465d8..8891c680 100644 --- a/rust/crates/truapi/src/versioned/system.rs +++ b/rust/crates/truapi/src/versioned/system.rs @@ -1,6 +1,7 @@ //! Versioned wrappers for [`System`](crate::api::System) methods. use crate::v01; +use truapi_macros::versioned_type; versioned_type! { pub enum HostHandshakeRequest { V1 => v01::HostHandshakeRequest } diff --git a/rust/crates/truapi/src/versioned/theme.rs b/rust/crates/truapi/src/versioned/theme.rs index ee044934..3d224465 100644 --- a/rust/crates/truapi/src/versioned/theme.rs +++ b/rust/crates/truapi/src/versioned/theme.rs @@ -1,6 +1,7 @@ //! Versioned wrappers for [`Theme`](crate::api::Theme) methods. use crate::v01; +use truapi_macros::versioned_type; versioned_type! { pub enum HostThemeSubscribeItem { V1 => v01::HostThemeSubscribeItem } From bb5b018ab3b94fdd649628a590d42126ba75dd0e Mon Sep 17 00:00:00 2001 From: pgherveou Date: Fri, 29 May 2026 20:36:54 +0000 Subject: [PATCH 4/4] fix(versioned): harden contiguity check and pin multi-version codec indices - compute the contiguity check in usize so a >255-variant enum reports a clean error instead of overflowing the u8 counter - accept an optional visibility on each declared enum (pub/pub(crate)/none) instead of requiring pub - document that versioned_type! expands to crate::versioned::* paths and is meant for use inside the truapi crate - add a multi-version regression test pinning positional codec indices (V1->0, V2->1), version(), and LATEST - call versioned_type! via its fully-qualified path in the versioned modules - refresh the local-e2e troubleshooting note that referenced the removed IntoVersion/Version API --- docs/local-e2e-testing.md | 15 ++++---- rust/crates/truapi-macros/src/lib.rs | 24 +++++++++---- rust/crates/truapi/src/versioned/account.rs | 3 +- rust/crates/truapi/src/versioned/chain.rs | 3 +- rust/crates/truapi/src/versioned/chat.rs | 3 +- .../truapi/src/versioned/coin_payment.rs | 3 +- rust/crates/truapi/src/versioned/entropy.rs | 3 +- .../truapi/src/versioned/local_storage.rs | 3 +- rust/crates/truapi/src/versioned/mod.rs | 35 +++++++++++++++++++ .../truapi/src/versioned/notifications.rs | 3 +- rust/crates/truapi/src/versioned/payment.rs | 3 +- .../truapi/src/versioned/permissions.rs | 3 +- rust/crates/truapi/src/versioned/preimage.rs | 3 +- .../src/versioned/resource_allocation.rs | 3 +- rust/crates/truapi/src/versioned/signing.rs | 3 +- .../truapi/src/versioned/statement_store.rs | 3 +- rust/crates/truapi/src/versioned/system.rs | 3 +- rust/crates/truapi/src/versioned/theme.rs | 3 +- 18 files changed, 75 insertions(+), 44 deletions(-) diff --git a/docs/local-e2e-testing.md b/docs/local-e2e-testing.md index b6c4d371..c832bfc3 100644 --- a/docs/local-e2e-testing.md +++ b/docs/local-e2e-testing.md @@ -275,16 +275,15 @@ Fix: turn the link into a fully-qualified path (`super::T`, `crate::vXY::T`, or just drop the link to a sibling that won't resolve across the doc-namespace boundary). Re-run `./scripts/codegen.sh`. -### V0.2-only wrapper has no V1 variant +### Payloadless request has no payload arm -Symptom: codegen omits a V1 arm for an enum like `HostGetUserIdRequest`. -Wire-table loop test passes a smaller `` than expected. +Symptom: codegen omits a payload arm for a request enum like +`HostGetUserIdRequest`. The wire-table loop test passes a smaller `` +than expected. -This is intentional. V0.2-only methods (`host_get_user_id`, -`host_chat_create_simple_group`, all `EntropyDerivation`, all `Payment`) -have only the `V2` variant in their versioned wrapper because no V0.1 -host ever spoke them. `IntoVersion::into_version(Version::V1)` returns -`Err(())` for these. +This is intentional. A request that takes no arguments declares a +payloadless `V1` variant (`pub enum HostGetUserIdRequest { V1 }`), so +there is no inner type for codegen to emit. ## Definition of done diff --git a/rust/crates/truapi-macros/src/lib.rs b/rust/crates/truapi-macros/src/lib.rs index 5520713e..14c919f0 100644 --- a/rust/crates/truapi-macros/src/lib.rs +++ b/rust/crates/truapi-macros/src/lib.rs @@ -24,7 +24,10 @@ use proc_macro::TokenStream; use proc_macro2::Literal; use quote::quote; use syn::parse::{Parse, ParseStream}; -use syn::{Attribute, Ident, ItemFn, LitInt, Token, TraitItemFn, Type, braced, parse_macro_input}; +use syn::{ + Attribute, Ident, ItemFn, LitInt, Token, TraitItemFn, Type, Visibility, braced, + parse_macro_input, +}; #[derive(Default)] struct WireArgs { @@ -159,9 +162,10 @@ impl Parse for VersionedInput { } } -/// A single `pub enum Name { V1 => Ty, ... }` declaration. +/// A single `[vis] enum Name { V1 => Ty, ... }` declaration. struct VersionedEnum { attrs: Vec, + vis: Visibility, name: Ident, variants: Vec, } @@ -169,7 +173,7 @@ struct VersionedEnum { impl Parse for VersionedEnum { fn parse(input: ParseStream<'_>) -> syn::Result { let attrs = input.call(Attribute::parse_outer)?; - input.parse::()?; + let vis: Visibility = input.parse()?; input.parse::()?; let name: Ident = input.parse()?; @@ -187,6 +191,7 @@ impl Parse for VersionedEnum { Ok(Self { attrs, + vis, name, variants, }) @@ -237,6 +242,12 @@ fn variant_version(ident: &Ident) -> syn::Result { /// `impl Versioned` exposing `Latest`, `LATEST`, and `version()`. Single-version /// envelopes also get trivial `IntoLatest`/`FromLatest` impls; multi-version /// envelopes leave those to be written by hand, since the conversion is bespoke. +/// +/// The declared visibility (`pub`, `pub(crate)`, or none) carries through to the +/// generated enum. +/// +/// The generated impls name `crate::versioned::*` traits, so invoke this from +/// within the `truapi` crate. #[proc_macro] pub fn versioned_type(item: TokenStream) -> TokenStream { let input = parse_macro_input!(item as VersionedInput); @@ -257,6 +268,7 @@ fn expand_versioned(input: &VersionedInput) -> syn::Result syn::Result { let VersionedEnum { attrs, + vis, name, variants, } = def; @@ -271,9 +283,9 @@ fn expand_versioned_enum(def: &VersionedEnum) -> syn::Result syn::Result v01::HostAccountGetRequest } pub enum HostAccountGetResponse { V1 => v01::HostAccountGetResponse } pub enum HostAccountGetError { V1 => v01::HostAccountGetError } diff --git a/rust/crates/truapi/src/versioned/chain.rs b/rust/crates/truapi/src/versioned/chain.rs index 49ce407e..b8a2ccb3 100644 --- a/rust/crates/truapi/src/versioned/chain.rs +++ b/rust/crates/truapi/src/versioned/chain.rs @@ -1,9 +1,8 @@ //! Versioned wrappers for [`Chain`](crate::api::Chain) methods. use crate::v01; -use truapi_macros::versioned_type; -versioned_type! { +truapi_macros::versioned_type! { pub enum RemoteChainHeadFollowRequest { V1 => v01::RemoteChainHeadFollowRequest } pub enum RemoteChainHeadFollowItem { V1 => v01::RemoteChainHeadFollowItem } pub enum RemoteChainHeadHeaderRequest { V1 => v01::RemoteChainHeadHeaderRequest } diff --git a/rust/crates/truapi/src/versioned/chat.rs b/rust/crates/truapi/src/versioned/chat.rs index c732f157..5c821275 100644 --- a/rust/crates/truapi/src/versioned/chat.rs +++ b/rust/crates/truapi/src/versioned/chat.rs @@ -1,9 +1,8 @@ //! Versioned wrappers for [`Chat`](crate::api::Chat) methods. use crate::v01; -use truapi_macros::versioned_type; -versioned_type! { +truapi_macros::versioned_type! { pub enum HostChatCreateRoomRequest { V1 => v01::HostChatCreateRoomRequest } pub enum HostChatCreateRoomResponse { V1 => v01::HostChatCreateRoomResponse } pub enum HostChatCreateRoomError { V1 => v01::HostChatCreateRoomError } diff --git a/rust/crates/truapi/src/versioned/coin_payment.rs b/rust/crates/truapi/src/versioned/coin_payment.rs index cb9128f0..3e488a7f 100644 --- a/rust/crates/truapi/src/versioned/coin_payment.rs +++ b/rust/crates/truapi/src/versioned/coin_payment.rs @@ -1,9 +1,8 @@ //! Versioned wrappers for [`CoinPayment`](crate::api::CoinPayment) methods. use crate::v01; -use truapi_macros::versioned_type; -versioned_type! { +truapi_macros::versioned_type! { pub enum HostCoinPaymentCreatePurseRequest { V1 => v01::HostCoinPaymentCreatePurseRequest } pub enum HostCoinPaymentCreatePurseResponse { V1 => v01::HostCoinPaymentCreatePurseResponse } pub enum HostCoinPaymentCreatePurseError { V1 => v01::CoinPaymentError } diff --git a/rust/crates/truapi/src/versioned/entropy.rs b/rust/crates/truapi/src/versioned/entropy.rs index c904e90c..5027e4f0 100644 --- a/rust/crates/truapi/src/versioned/entropy.rs +++ b/rust/crates/truapi/src/versioned/entropy.rs @@ -1,9 +1,8 @@ //! Versioned wrappers for [`Entropy`](crate::api::Entropy) methods. use crate::v01; -use truapi_macros::versioned_type; -versioned_type! { +truapi_macros::versioned_type! { pub enum HostDeriveEntropyRequest { V1 => v01::HostDeriveEntropyRequest } pub enum HostDeriveEntropyResponse { V1 => v01::HostDeriveEntropyResponse } pub enum HostDeriveEntropyError { V1 => v01::HostDeriveEntropyError } diff --git a/rust/crates/truapi/src/versioned/local_storage.rs b/rust/crates/truapi/src/versioned/local_storage.rs index 699c5af8..708eb4c7 100644 --- a/rust/crates/truapi/src/versioned/local_storage.rs +++ b/rust/crates/truapi/src/versioned/local_storage.rs @@ -1,9 +1,8 @@ //! Versioned wrappers for [`LocalStorage`](crate::api::LocalStorage) methods. use crate::v01; -use truapi_macros::versioned_type; -versioned_type! { +truapi_macros::versioned_type! { pub enum HostLocalStorageReadRequest { V1 => v01::HostLocalStorageReadRequest } pub enum HostLocalStorageReadResponse { V1 => v01::HostLocalStorageReadResponse } pub enum HostLocalStorageReadError { V1 => v01::HostLocalStorageReadError } diff --git a/rust/crates/truapi/src/versioned/mod.rs b/rust/crates/truapi/src/versioned/mod.rs index 0c5ac12a..9da72067 100644 --- a/rust/crates/truapi/src/versioned/mod.rs +++ b/rust/crates/truapi/src/versioned/mod.rs @@ -50,6 +50,41 @@ pub mod theme; mod tests { use parity_scale_codec::{Decode, Encode}; + #[derive(Debug, Clone, PartialEq, Eq, Encode, Decode)] + struct ProbeV1 { + a: u32, + } + + #[derive(Debug, Clone, PartialEq, Eq, Encode, Decode)] + struct ProbeV2 { + b: Vec, + } + + truapi_macros::versioned_type! { + enum MultiVersionProbe { + V1 => ProbeV1, + V2 => ProbeV2, + } + } + + // Multi-version envelopes assign positional SCALE codec indices (V1 -> 0, + // V2 -> 1) and 1-based version numbers. + #[test] + fn multi_version_codec_indices_are_positional() { + use super::Versioned; + + let v1 = MultiVersionProbe::V1(ProbeV1 { a: 7 }); + let v2 = MultiVersionProbe::V2(ProbeV2 { + b: b"hello".to_vec(), + }); + + assert_eq!(v1.encode()[0], 0, "V1 encodes codec index 0"); + assert_eq!(v2.encode()[0], 1, "V2 encodes codec index 1"); + assert_eq!(v1.version(), 1); + assert_eq!(v2.version(), 2); + assert_eq!(MultiVersionProbe::LATEST, 2); + } + #[test] fn v1_discriminant_is_zero() { let v1 = super::permissions::HostDevicePermissionRequest::V1( diff --git a/rust/crates/truapi/src/versioned/notifications.rs b/rust/crates/truapi/src/versioned/notifications.rs index 07c4d11a..3a8b7c5e 100644 --- a/rust/crates/truapi/src/versioned/notifications.rs +++ b/rust/crates/truapi/src/versioned/notifications.rs @@ -1,9 +1,8 @@ //! Versioned wrappers for [`Notifications`](crate::api::Notifications) methods. use crate::v01; -use truapi_macros::versioned_type; -versioned_type! { +truapi_macros::versioned_type! { pub enum HostPushNotificationRequest { V1 => v01::HostPushNotificationRequest } pub enum HostPushNotificationResponse { V1 => v01::HostPushNotificationResponse } pub enum HostPushNotificationError { V1 => v01::HostPushNotificationError } diff --git a/rust/crates/truapi/src/versioned/payment.rs b/rust/crates/truapi/src/versioned/payment.rs index 3392da75..d252cf4e 100644 --- a/rust/crates/truapi/src/versioned/payment.rs +++ b/rust/crates/truapi/src/versioned/payment.rs @@ -1,9 +1,8 @@ //! Versioned wrappers for [`Payment`](crate::api::Payment) methods. use crate::v01; -use truapi_macros::versioned_type; -versioned_type! { +truapi_macros::versioned_type! { pub enum HostPaymentBalanceSubscribeRequest { V1 => v01::HostPaymentBalanceSubscribeRequest } pub enum HostPaymentBalanceSubscribeItem { V1 => v01::HostPaymentBalanceSubscribeItem } pub enum HostPaymentBalanceSubscribeError { V1 => v01::HostPaymentBalanceSubscribeError } diff --git a/rust/crates/truapi/src/versioned/permissions.rs b/rust/crates/truapi/src/versioned/permissions.rs index d6921959..721ce570 100644 --- a/rust/crates/truapi/src/versioned/permissions.rs +++ b/rust/crates/truapi/src/versioned/permissions.rs @@ -1,9 +1,8 @@ //! Versioned wrappers for [`Permissions`](crate::api::Permissions) methods. use crate::v01; -use truapi_macros::versioned_type; -versioned_type! { +truapi_macros::versioned_type! { #[derive(derive_more::Display)] #[display("{_0}")] pub enum HostDevicePermissionRequest { V1 => v01::HostDevicePermissionRequest } diff --git a/rust/crates/truapi/src/versioned/preimage.rs b/rust/crates/truapi/src/versioned/preimage.rs index ad4b7dc5..61bf4b2f 100644 --- a/rust/crates/truapi/src/versioned/preimage.rs +++ b/rust/crates/truapi/src/versioned/preimage.rs @@ -1,9 +1,8 @@ //! Versioned wrappers for [`Preimage`](crate::api::Preimage) methods. use crate::v01; -use truapi_macros::versioned_type; -versioned_type! { +truapi_macros::versioned_type! { pub enum RemotePreimageLookupSubscribeRequest { V1 => v01::RemotePreimageLookupSubscribeRequest } pub enum RemotePreimageLookupSubscribeItem { V1 => v01::RemotePreimageLookupSubscribeItem } pub enum RemotePreimageSubmitRequest { V1 => Vec } diff --git a/rust/crates/truapi/src/versioned/resource_allocation.rs b/rust/crates/truapi/src/versioned/resource_allocation.rs index d7528c08..7d69c24b 100644 --- a/rust/crates/truapi/src/versioned/resource_allocation.rs +++ b/rust/crates/truapi/src/versioned/resource_allocation.rs @@ -1,9 +1,8 @@ //! Versioned wrappers for [`ResourceAllocation`](crate::api::ResourceAllocation) methods. use crate::v01; -use truapi_macros::versioned_type; -versioned_type! { +truapi_macros::versioned_type! { pub enum HostRequestResourceAllocationRequest { V1 => v01::HostRequestResourceAllocationRequest } pub enum HostRequestResourceAllocationResponse { V1 => v01::HostRequestResourceAllocationResponse } pub enum HostRequestResourceAllocationError { V1 => v01::ResourceAllocationError } diff --git a/rust/crates/truapi/src/versioned/signing.rs b/rust/crates/truapi/src/versioned/signing.rs index 2423ed34..8c9acc6a 100644 --- a/rust/crates/truapi/src/versioned/signing.rs +++ b/rust/crates/truapi/src/versioned/signing.rs @@ -1,9 +1,8 @@ //! Versioned wrappers for [`Signing`](crate::api::Signing) methods. use crate::v01; -use truapi_macros::versioned_type; -versioned_type! { +truapi_macros::versioned_type! { pub enum HostSignPayloadRequest { V1 => v01::HostSignPayloadRequest } pub enum HostSignPayloadResponse { V1 => v01::HostSignPayloadResponse } pub enum HostSignPayloadError { V1 => v01::HostSignPayloadError } diff --git a/rust/crates/truapi/src/versioned/statement_store.rs b/rust/crates/truapi/src/versioned/statement_store.rs index fc73a83d..979885ba 100644 --- a/rust/crates/truapi/src/versioned/statement_store.rs +++ b/rust/crates/truapi/src/versioned/statement_store.rs @@ -1,9 +1,8 @@ //! Versioned wrappers for [`StatementStore`](crate::api::StatementStore) methods. use crate::v01; -use truapi_macros::versioned_type; -versioned_type! { +truapi_macros::versioned_type! { pub enum RemoteStatementStoreSubscribeRequest { V1 => v01::RemoteStatementStoreSubscribeRequest } pub enum RemoteStatementStoreSubscribeItem { V1 => v01::RemoteStatementStoreSubscribeItem } pub enum RemoteStatementStoreCreateProofRequest { V1 => v01::RemoteStatementStoreCreateProofRequest } diff --git a/rust/crates/truapi/src/versioned/system.rs b/rust/crates/truapi/src/versioned/system.rs index 8891c680..0e8e65fe 100644 --- a/rust/crates/truapi/src/versioned/system.rs +++ b/rust/crates/truapi/src/versioned/system.rs @@ -1,9 +1,8 @@ //! Versioned wrappers for [`System`](crate::api::System) methods. use crate::v01; -use truapi_macros::versioned_type; -versioned_type! { +truapi_macros::versioned_type! { pub enum HostHandshakeRequest { V1 => v01::HostHandshakeRequest } pub enum HostHandshakeResponse { V1 } pub enum HostHandshakeError { V1 => v01::HostHandshakeError } diff --git a/rust/crates/truapi/src/versioned/theme.rs b/rust/crates/truapi/src/versioned/theme.rs index 3d224465..cac165ea 100644 --- a/rust/crates/truapi/src/versioned/theme.rs +++ b/rust/crates/truapi/src/versioned/theme.rs @@ -1,8 +1,7 @@ //! Versioned wrappers for [`Theme`](crate::api::Theme) methods. use crate::v01; -use truapi_macros::versioned_type; -versioned_type! { +truapi_macros::versioned_type! { pub enum HostThemeSubscribeItem { V1 => v01::HostThemeSubscribeItem } }