Newer
Older
// Copyright 2019-2021 Parity Technologies (UK) Ltd.
//
// Permission is hereby granted, free of charge, to any
// person obtaining a copy of this software and associated
// documentation files (the "Software"), to deal in the
// Software without restriction, including without
// limitation the rights to use, copy, modify, merge,
// publish, distribute, sublicense, and/or sell copies of
// the Software, and to permit persons to whom the Software
// is furnished to do so, subject to the following
// conditions:
//
// The above copyright notice and this permission notice
// shall be included in all copies or substantial portions
// of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
// ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
// TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
// PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
// SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
// CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
// IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.
//! Declaration of the JSON RPC generator procedural macros.
use crate::attributes::{
optional, parse_param_kind, Aliases, Argument, AttributeMeta, MissingArgument, NameMapping, ParamKind, Resource,
};
use crate::helpers::extract_doc_comments;
use proc_macro2::TokenStream as TokenStream2;
use quote::quote;
use syn::{punctuated::Punctuated, Attribute, Token};
#[derive(Debug, Clone)]
pub struct RpcMethod {
pub docs: TokenStream2,
pub deprecated: TokenStream2,
pub returns: Option<syn::Type>,
pub signature: syn::TraitItemMethod,
pub aliases: Vec<String>,
pub fn from_item(attr: Attribute, mut method: syn::TraitItemMethod) -> syn::Result<Self> {
let [aliases, blocking, name, param_kind, resources] =
AttributeMeta::parse(attr)?.retain(["aliases", "blocking", "name", "param_kind", "resources"])?;
let blocking = optional(blocking, Argument::flag)?.is_some();
let param_kind = parse_param_kind(param_kind)?;
let resources = optional(resources, Argument::group)?.unwrap_or_default();
let docs = extract_doc_comments(&method.attrs);
let deprecated = match find_attr(&method.attrs, "deprecated") {
Some(attr) => quote!(#attr),
None => quote!(),
};
if blocking && sig.asyncness.is_some() {
return Err(syn::Error::new(sig.span(), "Blocking method must be synchronous"));
}
let params: Vec<_> = sig
.inputs
.into_iter()
.filter_map(|arg| match arg {
syn::FnArg::Receiver(_) => None,
syn::FnArg::Typed(arg) => match *arg.pat {
syn::Pat::Ident(name) => Some(Ok((name, *arg.ty))),
Maciej Hirsz
committed
syn::Pat::Wild(wild) => Some(Err(syn::Error::new(
wild.underscore_token.span(),
"Method argument names must be valid Rust identifiers; got `_` instead",
))),
_ => Some(Err(syn::Error::new(
arg.span(),
format!("Unexpected method signature input; got {:?} ", *arg.pat),
))),
.collect::<Result<_, _>>()?;
let returns = match sig.output {
syn::ReturnType::Default => None,
syn::ReturnType::Type(_, output) => Some(*output),
};
// We've analyzed attributes and don't need them anymore.
method.attrs.clear();
Ok(Self {
aliases,
blocking,
name,
params,
param_kind,
returns,
signature: method,
docs,
resources,
deprecated,
})
}
}
#[derive(Debug, Clone)]
pub struct RpcSubscription {
/// When subscribing to an RPC, users can override the content of the `method` field
/// in the JSON data sent to subscribers.
/// Each subscription thus has one method name to set up the subscription,
/// one to unsubscribe and, optionally, a third method name used to describe the
/// payload (aka "notification") sent back from the server to subscribers.
/// If no override is provided, the subscription method name is used.
pub notif_name_override: Option<String>,
pub docs: TokenStream2,
pub unsubscribe: String,
pub item: syn::Type,
pub signature: syn::TraitItemMethod,
pub aliases: Vec<String>,
pub unsubscribe_aliases: Vec<String>,
pub resources: Punctuated<Resource, Token![,]>,
pub fn from_item(attr: syn::Attribute, mut sub: syn::TraitItemMethod) -> syn::Result<Self> {
let [aliases, item, name, param_kind, unsubscribe, unsubscribe_aliases, resources] =
AttributeMeta::parse(attr)?.retain([
"aliases",
"item",
"name",
"param_kind",
"unsubscribe",
"unsubscribe_aliases",
"resources",
])?;
let map = name?.value::<NameMapping>()?;
let name = map.name;
let notif_name_override = map.mapped;
let param_kind = parse_param_kind(param_kind)?;
let unsubscribe_aliases = parse_aliases(unsubscribe_aliases)?;
let resources = optional(resources, Argument::group)?.unwrap_or_default();
let docs = extract_doc_comments(&sub.attrs);
let unsubscribe = match parse_subscribe(unsubscribe)? {
Some(unsub) => unsub,
None => build_unsubscribe_method(&name).unwrap_or_else(||
panic!("Could not generate the unsubscribe method with name '{}'. You need to provide the name manually using the `unsubscribe` attribute in your RPC API definition", name),
),
};
let params: Vec<_> = sig
.inputs
.into_iter()
.filter_map(|arg| match arg {
syn::FnArg::Receiver(_) => None,
syn::FnArg::Typed(arg) => match *arg.pat {
syn::Pat::Ident(name) => Some((name, *arg.ty)),
_ => panic!("Identifier in signature must be an ident"),
},
})
.collect();
// We've analyzed attributes and don't need them anymore.
sub.attrs.clear();
Ok(Self {
name,
notif_name_override,
unsubscribe,
unsubscribe_aliases,
params,
param_kind,
item,
signature: sub,
aliases,
docs,
})
}
}
#[derive(Debug)]
pub struct RpcDescription {
/// Path to the `jsonrpsee` client types part.
pub(crate) jsonrpsee_client_path: Option<TokenStream2>,
pub(crate) jsonrpsee_server_path: Option<TokenStream2>,
/// Switch denoting that server trait must be generated.
/// Assuming that trait to which attribute is applied is named `Foo`, the generated
/// server trait will have `FooServer` name.
pub(crate) needs_server: bool,
/// Switch denoting that client extension trait must be generated.
/// Assuming that trait to which attribute is applied is named `Foo`, the generated
/// client trait will have `FooClient` name.
pub(crate) needs_client: bool,
/// Optional prefix for RPC namespace.
pub(crate) namespace: Option<String>,
/// Trait definition in which all the attributes were stripped.
/// List of RPC subscriptions defined in the trait.
/// Optional user defined trait bounds for the client implementation.
pub(crate) client_bounds: Option<Punctuated<syn::WherePredicate, Token![,]>>,
/// Optional user defined trait bounds for the server implementation.
pub(crate) server_bounds: Option<Punctuated<syn::WherePredicate, Token![,]>>,
pub fn from_item(attr: Attribute, mut item: syn::ItemTrait) -> syn::Result<Self> {
let [client, server, namespace, client_bounds, server_bounds] =
AttributeMeta::parse(attr)?.retain(["client", "server", "namespace", "client_bounds", "server_bounds"])?;
let needs_server = optional(server, Argument::flag)?.is_some();
let needs_client = optional(client, Argument::flag)?.is_some();
let namespace = optional(namespace, Argument::string)?;
let client_bounds = optional(client_bounds, Argument::group)?;
let server_bounds = optional(server_bounds, Argument::group)?;
return Err(syn::Error::new_spanned(&item.ident, "Either 'server' or 'client' attribute must be applied"));
}
if client_bounds.is_some() && !needs_client {
return Err(syn::Error::new_spanned(
&item.ident,
"Attribute 'client' must be specified with 'client_bounds'",
));
}
if server_bounds.is_some() && !needs_server {
return Err(syn::Error::new_spanned(
&item.ident,
"Attribute 'server' must be specified with 'server_bounds'",
));
}
let jsonrpsee_client_path = crate::helpers::find_jsonrpsee_client_crate().ok();
let jsonrpsee_server_path = crate::helpers::find_jsonrpsee_server_crate().ok();
if needs_client && jsonrpsee_client_path.is_none() {
return Err(syn::Error::new_spanned(&item.ident, "Unable to locate 'jsonrpsee' client dependency"));
}
if needs_server && jsonrpsee_server_path.is_none() {
return Err(syn::Error::new_spanned(&item.ident, "Unable to locate 'jsonrpsee' server dependency"));
}
item.attrs.clear(); // Remove RPC attributes.
let mut methods = Vec::new();
let mut subscriptions = Vec::new();
// Go through all the methods in the trait and collect methods and
// subscriptions.
for entry in item.items.iter() {
if let syn::TraitItem::Method(method) = entry {
if method.sig.receiver().is_none() {
return Err(syn::Error::new_spanned(&method.sig, "First argument of the trait must be '&self'"));
}
let mut is_method = false;
let mut is_sub = false;
if let Some(attr) = find_attr(&method.attrs, "method") {
let method_data = RpcMethod::from_item(attr.clone(), method.clone())?;
if let Some(attr) = find_attr(&method.attrs, "subscription") {
is_sub = true;
if is_method {
return Err(syn::Error::new_spanned(
"Element cannot be both subscription and method at the same time",
));
}
if !matches!(method.sig.output, syn::ReturnType::Default) {
return Err(syn::Error::new_spanned(
"Subscription methods must not return anything; the error must send via subscription via either `SubscriptionSink::reject` or `SubscriptionSink::close`",
));
}
return Err(syn::Error::new_spanned(method, "Subscription methods must not be `async`"));
let sub_data = RpcSubscription::from_item(attr.clone(), method.clone())?;
subscriptions.push(sub_data);
}
if !is_method && !is_sub {
return Err(syn::Error::new_spanned(
"Methods must have either 'method' or 'subscription' attribute",
));
}
} else {
return Err(syn::Error::new_spanned(entry, "Only methods allowed in RPC traits"));
}
}
if methods.is_empty() && subscriptions.is_empty() {
return Err(syn::Error::new_spanned(&item, "RPC cannot be empty"));
}
Ok(Self {
jsonrpsee_client_path,
jsonrpsee_server_path,
needs_server,
needs_client,
namespace,
trait_def: item,
methods,
subscriptions,
client_bounds,
server_bounds,
}
pub fn render(self) -> Result<TokenStream2, syn::Error> {
let server_impl = if self.needs_server { self.render_server()? } else { TokenStream2::new() };
let client_impl = if self.needs_client { self.render_client()? } else { TokenStream2::new() };
Ok(quote! {
#server_impl
#client_impl
})
}
/// Formats the identifier as a path relative to the resolved
/// `jsonrpsee` client path.
pub(crate) fn jrps_client_item(&self, item: impl quote::ToTokens) -> TokenStream2 {
let jsonrpsee = self.jsonrpsee_client_path.as_ref().unwrap();
quote! { #jsonrpsee::#item }
}
/// Formats the identifier as a path relative to the resolved
/// `jsonrpsee` server path.
pub(crate) fn jrps_server_item(&self, item: impl quote::ToTokens) -> TokenStream2 {
let jsonrpsee = self.jsonrpsee_server_path.as_ref().unwrap();
quote! { #jsonrpsee::#item }
}
/// Based on the namespace, renders the full name of the RPC method/subscription.
/// Examples:
/// For namespace `foo` and method `makeSpam`, result will be `foo_makeSpam`.
/// For no namespace and method `makeSpam` it will be just `makeSpam`.
pub(crate) fn rpc_identifier<'a>(&self, method: &'a str) -> Cow<'a, str> {
fn parse_aliases(arg: Result<Argument, MissingArgument>) -> syn::Result<Vec<String>> {
let aliases = optional(arg, Argument::value::<Aliases>)?;
Ok(aliases.map(|a| a.list.into_iter().map(|lit| lit.value()).collect()).unwrap_or_default())
fn parse_subscribe(arg: Result<Argument, MissingArgument>) -> syn::Result<Option<String>> {
let unsub = optional(arg, Argument::string)?;
Ok(unsub)
}
fn find_attr<'a>(attrs: &'a [Attribute], ident: &str) -> Option<&'a Attribute> {
attrs.iter().find(|a| a.path.is_ident(ident))
fn build_unsubscribe_method(method: &str) -> Option<String> {
method.strip_prefix("subscribe").map(|s| format!("unsubscribe{}", s))