|
1 | 1 | //! Proc-macros for TrUAPI trait annotations. |
2 | 2 | //! |
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 |
4 | 8 | //! its wire-protocol discriminant ids. The ids appear on the wire as the u8 discriminant in the |
5 | 9 | //! `Struct { request_id: str, payload: Enum(<methods>) }` envelope; method |
6 | 10 | //! ordering becomes part of the wire protocol. |
|
17 | 21 | //! rustdoc through the only attribute that is always preserved verbatim. |
18 | 22 |
|
19 | 23 | use proc_macro::TokenStream; |
| 24 | +use proc_macro2::Literal; |
20 | 25 | use quote::quote; |
21 | 26 | 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 | +}; |
23 | 31 |
|
24 | 32 | #[derive(Default)] |
25 | 33 | struct WireArgs { |
@@ -138,3 +146,222 @@ fn wire_tags(args: &WireArgs) -> Vec<String> { |
138 | 146 | .filter_map(|(name, value)| value.map(|id| format!("@wire_{name}={id}"))) |
139 | 147 | .collect() |
140 | 148 | } |
| 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 | +} |
0 commit comments