Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 7 additions & 8 deletions docs/local-e2e-testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 `<N>` than expected.
Symptom: codegen omits a payload arm for a request enum like
`HostGetUserIdRequest`. The wire-table loop test passes a smaller `<N>`
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

Expand Down
8 changes: 4 additions & 4 deletions rust/crates/truapi-codegen/src/rustdoc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -257,10 +257,10 @@ pub fn extract_api(krate: &Crate) -> Result<ApiDefinition> {

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;
Expand Down
231 changes: 229 additions & 2 deletions rust/crates/truapi-macros/src/lib.rs
Original file line number Diff line number Diff line change
@@ -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(<methods>) }` envelope; method
//! ordering becomes part of the wire protocol.
Expand All @@ -17,9 +21,13 @@
//! 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, Visibility, braced,
parse_macro_input,
};

#[derive(Default)]
struct WireArgs {
Expand Down Expand Up @@ -138,3 +146,222 @@ fn wire_tags(args: &WireArgs) -> Vec<String> {
.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<VersionedEnum>,
}

impl Parse for VersionedInput {
fn parse(input: ParseStream<'_>) -> syn::Result<Self> {
let mut enums = Vec::new();
while !input.is_empty() {
enums.push(input.parse()?);
}
Ok(Self { enums })
}
}

/// A single `[vis] enum Name { V1 => Ty, ... }` declaration.
struct VersionedEnum {
attrs: Vec<Attribute>,
vis: Visibility,
name: Ident,
variants: Vec<VersionedVariant>,
}

impl Parse for VersionedEnum {
fn parse(input: ParseStream<'_>) -> syn::Result<Self> {
let attrs = input.call(Attribute::parse_outer)?;
let vis: Visibility = input.parse()?;
input.parse::<Token![enum]>()?;
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::<Token![,]>()?;
} else {
break;
}
}

Ok(Self {
attrs,
vis,
name,
variants,
})
}
}

/// A single `Vn` or `Vn => Ty` variant.
struct VersionedVariant {
attrs: Vec<Attribute>,
ident: Ident,
ty: Option<Type>,
}

impl Parse for VersionedVariant {
fn parse(input: ParseStream<'_>) -> syn::Result<Self> {
let attrs = input.call(Attribute::parse_outer)?;
let ident: Ident = input.parse()?;
let ty = if input.peek(Token![=>]) {
input.parse::<Token![=>]>()?;
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<u8> {
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::<u8>()
.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.
///
/// 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);
match expand_versioned(&input) {
Ok(tokens) => tokens.into(),
Err(err) => err.to_compile_error().into(),
}
}

fn expand_versioned(input: &VersionedInput) -> syn::Result<proc_macro2::TokenStream> {
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<proc_macro2::TokenStream> {
let VersionedEnum {
attrs,
vis,
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 + 1;
let version = variant_version(&variant.ident)?;
if usize::from(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)]
#vis 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)
}
2 changes: 1 addition & 1 deletion rust/crates/truapi/src/versioned/account.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

use crate::v01;

versioned_type! {
truapi_macros::versioned_type! {
pub enum HostAccountGetRequest { V1 => v01::HostAccountGetRequest }
pub enum HostAccountGetResponse { V1 => v01::HostAccountGetResponse }
pub enum HostAccountGetError { V1 => v01::HostAccountGetError }
Expand Down
2 changes: 1 addition & 1 deletion rust/crates/truapi/src/versioned/chain.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

use crate::v01;

versioned_type! {
truapi_macros::versioned_type! {
pub enum RemoteChainHeadFollowRequest { V1 => v01::RemoteChainHeadFollowRequest }
pub enum RemoteChainHeadFollowItem { V1 => v01::RemoteChainHeadFollowItem }
pub enum RemoteChainHeadHeaderRequest { V1 => v01::RemoteChainHeadHeaderRequest }
Expand Down
2 changes: 1 addition & 1 deletion rust/crates/truapi/src/versioned/chat.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

use crate::v01;

versioned_type! {
truapi_macros::versioned_type! {
pub enum HostChatCreateRoomRequest { V1 => v01::HostChatCreateRoomRequest }
pub enum HostChatCreateRoomResponse { V1 => v01::HostChatCreateRoomResponse }
pub enum HostChatCreateRoomError { V1 => v01::HostChatCreateRoomError }
Expand Down
2 changes: 1 addition & 1 deletion rust/crates/truapi/src/versioned/coin_payment.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

use crate::v01;

versioned_type! {
truapi_macros::versioned_type! {
pub enum HostCoinPaymentCreatePurseRequest { V1 => v01::HostCoinPaymentCreatePurseRequest }
pub enum HostCoinPaymentCreatePurseResponse { V1 => v01::HostCoinPaymentCreatePurseResponse }
pub enum HostCoinPaymentCreatePurseError { V1 => v01::CoinPaymentError }
Expand Down
2 changes: 1 addition & 1 deletion rust/crates/truapi/src/versioned/entropy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

use crate::v01;

versioned_type! {
truapi_macros::versioned_type! {
pub enum HostDeriveEntropyRequest { V1 => v01::HostDeriveEntropyRequest }
pub enum HostDeriveEntropyResponse { V1 => v01::HostDeriveEntropyResponse }
pub enum HostDeriveEntropyError { V1 => v01::HostDeriveEntropyError }
Expand Down
2 changes: 1 addition & 1 deletion rust/crates/truapi/src/versioned/local_storage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

use crate::v01;

versioned_type! {
truapi_macros::versioned_type! {
pub enum HostLocalStorageReadRequest { V1 => v01::HostLocalStorageReadRequest }
pub enum HostLocalStorageReadResponse { V1 => v01::HostLocalStorageReadResponse }
pub enum HostLocalStorageReadError { V1 => v01::HostLocalStorageReadError }
Expand Down
Loading
Loading