Skip to content

Commit f6fbcd7

Browse files
authored
Merge branch 'main' into ci/skip-check-rfc-for-bot
2 parents 4d9c3bd + d9c28ad commit f6fbcd7

19 files changed

Lines changed: 312 additions & 111 deletions

docs/local-e2e-testing.md

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -275,16 +275,15 @@ Fix: turn the link into a fully-qualified path (`super::T`,
275275
`crate::vXY::T`, or just drop the link to a sibling that won't resolve
276276
across the doc-namespace boundary). Re-run `./scripts/codegen.sh`.
277277

278-
### V0.2-only wrapper has no V1 variant
278+
### Payloadless request has no payload arm
279279

280-
Symptom: codegen omits a V1 arm for an enum like `HostGetUserIdRequest`.
281-
Wire-table loop test passes a smaller `<N>` than expected.
280+
Symptom: codegen omits a payload arm for a request enum like
281+
`HostGetUserIdRequest`. The wire-table loop test passes a smaller `<N>`
282+
than expected.
282283

283-
This is intentional. V0.2-only methods (`host_get_user_id`,
284-
`host_chat_create_simple_group`, all `EntropyDerivation`, all `Payment`)
285-
have only the `V2` variant in their versioned wrapper because no V0.1
286-
host ever spoke them. `IntoVersion::into_version(Version::V1)` returns
287-
`Err(())` for these.
284+
This is intentional. A request that takes no arguments declares a
285+
payloadless `V1` variant (`pub enum HostGetUserIdRequest { V1 }`), so
286+
there is no inner type for codegen to emit.
288287

289288
## Definition of done
290289

rust/crates/truapi-codegen/src/rustdoc.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -257,10 +257,10 @@ pub fn extract_api(krate: &Crate) -> Result<ApiDefinition> {
257257

258258
let mut traits = Vec::new();
259259
for (name, candidates) in trait_candidates {
260-
// `Versioned` is a runtime-helper trait on the wrapper enums, not a
261-
// protocol-method trait. The codegen only cares about the protocol
262-
// surface (TrUAPI methods); skip anything declared outside
263-
// `truapi::api::*`.
260+
// `Versioned`, `IntoLatest`, and `FromLatest` are runtime-helper traits
261+
// on the wrapper enums, not protocol-method traits. The codegen only
262+
// cares about the protocol surface (TrUAPI methods); skip anything
263+
// declared outside `truapi::api::*`.
264264
let candidate = select_candidate(&name, &candidates)?;
265265
if !candidate.path.iter().any(|s| s == "api") {
266266
continue;

rust/crates/truapi-macros/src/lib.rs

Lines changed: 229 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
//! Proc-macros for TrUAPI trait annotations.
22
//!
3-
//! The single attribute exposed is [`wire`], which marks a trait method with
3+
//! `versioned_type!` is a function-like macro that generates versioned message
4+
//! envelopes: the `Vn` enums (with SCALE codec indices) plus their
5+
//! `Versioned`/`IntoLatest`/`FromLatest` impls from `truapi::versioned`.
6+
//!
7+
//! The `wire` attribute marks a trait method with
48
//! its wire-protocol discriminant ids. The ids appear on the wire as the u8 discriminant in the
59
//! `Struct { request_id: str, payload: Enum(<methods>) }` envelope; method
610
//! ordering becomes part of the wire protocol.
@@ -17,9 +21,13 @@
1721
//! rustdoc through the only attribute that is always preserved verbatim.
1822
1923
use proc_macro::TokenStream;
24+
use proc_macro2::Literal;
2025
use quote::quote;
2126
use syn::parse::{Parse, ParseStream};
22-
use syn::{Ident, ItemFn, LitInt, Token, TraitItemFn, parse_macro_input};
27+
use syn::{
28+
Attribute, Ident, ItemFn, LitInt, Token, TraitItemFn, Type, Visibility, braced,
29+
parse_macro_input,
30+
};
2331

2432
#[derive(Default)]
2533
struct WireArgs {
@@ -138,3 +146,222 @@ fn wire_tags(args: &WireArgs) -> Vec<String> {
138146
.filter_map(|(name, value)| value.map(|id| format!("@wire_{name}={id}")))
139147
.collect()
140148
}
149+
150+
/// One sequence of versioned envelope declarations passed to `versioned_type!`.
151+
struct VersionedInput {
152+
enums: Vec<VersionedEnum>,
153+
}
154+
155+
impl Parse for VersionedInput {
156+
fn parse(input: ParseStream<'_>) -> syn::Result<Self> {
157+
let mut enums = Vec::new();
158+
while !input.is_empty() {
159+
enums.push(input.parse()?);
160+
}
161+
Ok(Self { enums })
162+
}
163+
}
164+
165+
/// A single `[vis] enum Name { V1 => Ty, ... }` declaration.
166+
struct VersionedEnum {
167+
attrs: Vec<Attribute>,
168+
vis: Visibility,
169+
name: Ident,
170+
variants: Vec<VersionedVariant>,
171+
}
172+
173+
impl Parse for VersionedEnum {
174+
fn parse(input: ParseStream<'_>) -> syn::Result<Self> {
175+
let attrs = input.call(Attribute::parse_outer)?;
176+
let vis: Visibility = input.parse()?;
177+
input.parse::<Token![enum]>()?;
178+
let name: Ident = input.parse()?;
179+
180+
let body;
181+
braced!(body in input);
182+
let mut variants = Vec::new();
183+
while !body.is_empty() {
184+
variants.push(body.parse()?);
185+
if body.peek(Token![,]) {
186+
body.parse::<Token![,]>()?;
187+
} else {
188+
break;
189+
}
190+
}
191+
192+
Ok(Self {
193+
attrs,
194+
vis,
195+
name,
196+
variants,
197+
})
198+
}
199+
}
200+
201+
/// A single `Vn` or `Vn => Ty` variant.
202+
struct VersionedVariant {
203+
attrs: Vec<Attribute>,
204+
ident: Ident,
205+
ty: Option<Type>,
206+
}
207+
208+
impl Parse for VersionedVariant {
209+
fn parse(input: ParseStream<'_>) -> syn::Result<Self> {
210+
let attrs = input.call(Attribute::parse_outer)?;
211+
let ident: Ident = input.parse()?;
212+
let ty = if input.peek(Token![=>]) {
213+
input.parse::<Token![=>]>()?;
214+
Some(input.parse()?)
215+
} else {
216+
None
217+
};
218+
Ok(Self { attrs, ident, ty })
219+
}
220+
}
221+
222+
/// Parse the `Vn` version number from a variant identifier.
223+
fn variant_version(ident: &Ident) -> syn::Result<u8> {
224+
let name = ident.to_string();
225+
let err = || syn::Error::new(ident.span(), "variant must be named `Vn` where n is a u8");
226+
name.strip_prefix('V')
227+
.ok_or_else(err)?
228+
.parse::<u8>()
229+
.map_err(|_| err())
230+
}
231+
232+
/// Generate versioned message envelopes.
233+
///
234+
/// ```ignore
235+
/// versioned_type! {
236+
/// pub enum HostFooRequest { V1 => v01::HostFooRequest }
237+
/// pub enum HostFooResponse { V1 }
238+
/// }
239+
/// ```
240+
///
241+
/// Each declaration becomes a SCALE enum with positional codec indices and an
242+
/// `impl Versioned` exposing `Latest`, `LATEST`, and `version()`. Single-version
243+
/// envelopes also get trivial `IntoLatest`/`FromLatest` impls; multi-version
244+
/// envelopes leave those to be written by hand, since the conversion is bespoke.
245+
///
246+
/// The declared visibility (`pub`, `pub(crate)`, or none) carries through to the
247+
/// generated enum.
248+
///
249+
/// The generated impls name `crate::versioned::*` traits, so invoke this from
250+
/// within the `truapi` crate.
251+
#[proc_macro]
252+
pub fn versioned_type(item: TokenStream) -> TokenStream {
253+
let input = parse_macro_input!(item as VersionedInput);
254+
match expand_versioned(&input) {
255+
Ok(tokens) => tokens.into(),
256+
Err(err) => err.to_compile_error().into(),
257+
}
258+
}
259+
260+
fn expand_versioned(input: &VersionedInput) -> syn::Result<proc_macro2::TokenStream> {
261+
let mut out = proc_macro2::TokenStream::new();
262+
for enum_def in &input.enums {
263+
out.extend(expand_versioned_enum(enum_def)?);
264+
}
265+
Ok(out)
266+
}
267+
268+
fn expand_versioned_enum(def: &VersionedEnum) -> syn::Result<proc_macro2::TokenStream> {
269+
let VersionedEnum {
270+
attrs,
271+
vis,
272+
name,
273+
variants,
274+
} = def;
275+
276+
if variants.is_empty() {
277+
return Err(syn::Error::new(
278+
name.span(),
279+
"versioned enum needs at least one variant",
280+
));
281+
}
282+
283+
let mut variant_defs = Vec::new();
284+
let mut version_arms = Vec::new();
285+
for (i, variant) in variants.iter().enumerate() {
286+
let expected = i + 1;
287+
let version = variant_version(&variant.ident)?;
288+
if usize::from(version) != expected {
289+
return Err(syn::Error::new(
290+
variant.ident.span(),
291+
format!("expected variant `V{expected}`; versions must be contiguous from 1"),
292+
));
293+
}
294+
295+
let index = Literal::u8_unsuffixed(i as u8);
296+
let version_lit = Literal::u8_unsuffixed(version);
297+
let vattrs = &variant.attrs;
298+
let vident = &variant.ident;
299+
match &variant.ty {
300+
Some(ty) => {
301+
variant_defs.push(quote! { #(#vattrs)* #[codec(index = #index)] #vident(#ty) });
302+
version_arms.push(quote! { Self::#vident(..) => #version_lit });
303+
}
304+
None => {
305+
variant_defs.push(quote! { #(#vattrs)* #[codec(index = #index)] #vident });
306+
version_arms.push(quote! { Self::#vident => #version_lit });
307+
}
308+
}
309+
}
310+
311+
let doc = format!("Versioned envelope for [`{name}`].");
312+
let latest_lit = Literal::u8_unsuffixed(variants.len() as u8);
313+
let latest_ty = match &variants.last().expect("checked non-empty").ty {
314+
Some(ty) => quote! { #ty },
315+
None => quote! { () },
316+
};
317+
318+
let mut tokens = quote! {
319+
#(#attrs)*
320+
#[doc = #doc]
321+
#[derive(Debug, Clone, PartialEq, Eq, parity_scale_codec::Encode, parity_scale_codec::Decode)]
322+
#vis enum #name {
323+
#(#variant_defs),*
324+
}
325+
326+
impl crate::versioned::Versioned for #name {
327+
type Latest = #latest_ty;
328+
const LATEST: u8 = #latest_lit;
329+
fn version(&self) -> u8 {
330+
match self {
331+
#(#version_arms),*
332+
}
333+
}
334+
}
335+
};
336+
337+
if let [only] = &variants[..] {
338+
let vident = &only.ident;
339+
let (into_body, from_param, from_body) = match &only.ty {
340+
Some(_) => (
341+
quote! { match self { Self::#vident(inner) => inner } },
342+
quote! { latest },
343+
quote! { Self::#vident(latest) },
344+
),
345+
None => (
346+
quote! { match self { Self::#vident => () } },
347+
quote! { _latest },
348+
quote! { Self::#vident },
349+
),
350+
};
351+
tokens.extend(quote! {
352+
impl crate::versioned::IntoLatest for #name {
353+
fn into_latest(self) -> Self::Latest {
354+
#into_body
355+
}
356+
}
357+
358+
impl crate::versioned::FromLatest for #name {
359+
fn from_latest(#from_param: Self::Latest, _target: u8) -> Self {
360+
#from_body
361+
}
362+
}
363+
});
364+
}
365+
366+
Ok(tokens)
367+
}

rust/crates/truapi/src/versioned/account.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
33
use crate::v01;
44

5-
versioned_type! {
5+
truapi_macros::versioned_type! {
66
pub enum HostAccountGetRequest { V1 => v01::HostAccountGetRequest }
77
pub enum HostAccountGetResponse { V1 => v01::HostAccountGetResponse }
88
pub enum HostAccountGetError { V1 => v01::HostAccountGetError }

rust/crates/truapi/src/versioned/chain.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
33
use crate::v01;
44

5-
versioned_type! {
5+
truapi_macros::versioned_type! {
66
pub enum RemoteChainHeadFollowRequest { V1 => v01::RemoteChainHeadFollowRequest }
77
pub enum RemoteChainHeadFollowItem { V1 => v01::RemoteChainHeadFollowItem }
88
pub enum RemoteChainHeadHeaderRequest { V1 => v01::RemoteChainHeadHeaderRequest }

rust/crates/truapi/src/versioned/chat.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
33
use crate::v01;
44

5-
versioned_type! {
5+
truapi_macros::versioned_type! {
66
pub enum HostChatCreateRoomRequest { V1 => v01::HostChatCreateRoomRequest }
77
pub enum HostChatCreateRoomResponse { V1 => v01::HostChatCreateRoomResponse }
88
pub enum HostChatCreateRoomError { V1 => v01::HostChatCreateRoomError }

rust/crates/truapi/src/versioned/coin_payment.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
33
use crate::v01;
44

5-
versioned_type! {
5+
truapi_macros::versioned_type! {
66
pub enum HostCoinPaymentCreatePurseRequest { V1 => v01::HostCoinPaymentCreatePurseRequest }
77
pub enum HostCoinPaymentCreatePurseResponse { V1 => v01::HostCoinPaymentCreatePurseResponse }
88
pub enum HostCoinPaymentCreatePurseError { V1 => v01::CoinPaymentError }

rust/crates/truapi/src/versioned/entropy.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
33
use crate::v01;
44

5-
versioned_type! {
5+
truapi_macros::versioned_type! {
66
pub enum HostDeriveEntropyRequest { V1 => v01::HostDeriveEntropyRequest }
77
pub enum HostDeriveEntropyResponse { V1 => v01::HostDeriveEntropyResponse }
88
pub enum HostDeriveEntropyError { V1 => v01::HostDeriveEntropyError }

rust/crates/truapi/src/versioned/local_storage.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
33
use crate::v01;
44

5-
versioned_type! {
5+
truapi_macros::versioned_type! {
66
pub enum HostLocalStorageReadRequest { V1 => v01::HostLocalStorageReadRequest }
77
pub enum HostLocalStorageReadResponse { V1 => v01::HostLocalStorageReadResponse }
88
pub enum HostLocalStorageReadError { V1 => v01::HostLocalStorageReadError }

0 commit comments

Comments
 (0)