rpc_macro.rs 8.03 KiB
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.

Igor Aleksanov's avatar
Igor Aleksanov committed
//! Declaration of the JSON RPC generator procedural macros.

David's avatar
David committed
use crate::{attributes, respan::Respan};

Igor Aleksanov's avatar
Igor Aleksanov committed
use proc_macro2::TokenStream as TokenStream2;
use quote::quote;
use syn::Attribute;

#[derive(Debug, Clone)]
pub struct RpcMethod {
	pub name: syn::LitStr,
	pub params: Vec<(syn::PatIdent, syn::Type)>,
	pub returns: Option<syn::Type>,
	pub signature: syn::TraitItemMethod,
}

impl RpcMethod {
	pub fn from_item(mut method: syn::TraitItemMethod) -> Result<Self, syn::Error> {
		let attributes = attributes::Method::from_attributes(&method.attrs).respan(&method.attrs.first())?;
		let sig = method.sig.clone();
		let name = attributes.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();

		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 { name, params, returns, signature: method })
	}
}

#[derive(Debug, Clone)]
pub struct RpcSubscription {
	pub name: syn::LitStr,
	pub unsub_method: syn::LitStr,
	pub params: Vec<(syn::PatIdent, syn::Type)>,
	pub item: syn::Type,
	pub signature: syn::TraitItemMethod,
}

impl RpcSubscription {
	pub fn from_item(mut sub: syn::TraitItemMethod) -> Result<Self, syn::Error> {
		let attributes = attributes::Subscription::from_attributes(&sub.attrs).respan(&sub.attrs.first())?;
		let sig = sub.sig.clone();
		let name = attributes.name;
		let unsub_method = attributes.unsub;
		let item = attributes.item;
Igor Aleksanov's avatar
Igor Aleksanov committed
		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, unsub_method, params, item, signature: sub })
	}
}

#[derive(Debug)]
pub struct RpcDescription {
	/// Path to the `jsonrpsee` client types part.
David's avatar
David committed
	pub(crate) jsonrpsee_client_path: Option<TokenStream2>,
Igor Aleksanov's avatar
Igor Aleksanov committed
	/// Path to the `jsonrpsee` server types part.
David's avatar
David committed
	pub(crate) jsonrpsee_server_path: Option<TokenStream2>,
Igor Aleksanov's avatar
Igor Aleksanov committed
	/// Data about RPC declaration
David's avatar
David committed
	pub(crate) attrs: attributes::Rpc,
Igor Aleksanov's avatar
Igor Aleksanov committed
	/// Trait definition in which all the attributes were stripped.
David's avatar
David committed
	pub(crate) trait_def: syn::ItemTrait,
Igor Aleksanov's avatar
Igor Aleksanov committed
	/// List of RPC methods defined in the trait.
David's avatar
David committed
	pub(crate) methods: Vec<RpcMethod>,
	/// List of RPC subscriptions defined in the trait.
David's avatar
David committed
	pub(crate) subscriptions: Vec<RpcSubscription>,
Igor Aleksanov's avatar
Igor Aleksanov committed
}

impl RpcDescription {
	pub fn from_item(attr: syn::Attribute, mut item: syn::ItemTrait) -> Result<Self, syn::Error> {
		let attrs = attributes::Rpc::from_attributes(&[attr.clone()]).respan(&attr)?;
		if !attrs.is_correct() {
			return Err(syn::Error::new_spanned(&item.ident, "Either 'server' or 'client' attribute must be applied"));
		}

		let jsonrpsee_client_path = crate::helpers::find_jsonrpsee_client_crate().ok();
		let jsonrpsee_server_path = crate::helpers::find_jsonrpsee_server_crate().ok();

		if attrs.needs_client() && jsonrpsee_client_path.is_none() {
			return Err(syn::Error::new_spanned(&item.ident, "Unable to locate 'jsonrpsee' client dependency"));
		}
		if attrs.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 has_attr(&method.attrs, "method") {
					is_method = true;

					let method_data = RpcMethod::from_item(method.clone())?;
					methods.push(method_data);
				}
				if has_attr(&method.attrs, "subscription") {
					is_sub = true;
					if is_method {
						return Err(syn::Error::new_spanned(
							&method,
							"Element cannot be both subscription and method at the same time",
						));
					}
					if method.sig.asyncness.is_some() {
						return Err(syn::Error::new_spanned(&method, "Subscription methods must not be `async`"));
					}
					if !matches!(method.sig.output, syn::ReturnType::Default) {
						return Err(syn::Error::new_spanned(&method, "Subscription methods must not return anything"));
					}

					let sub_data = RpcSubscription::from_item(method.clone())?;
					subscriptions.push(sub_data);
				}

				if !is_method && !is_sub {
					return Err(syn::Error::new_spanned(
						&method,
						"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, attrs, trait_def: item, methods, subscriptions })
	}

	pub fn render(self) -> Result<TokenStream2, syn::Error> {
		let server_impl = if self.attrs.needs_server() { self.render_server()? } else { TokenStream2::new() };
		let client_impl = if self.attrs.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.
David's avatar
David committed
	pub(crate) fn jrps_client_item(&self, item: impl quote::ToTokens) -> TokenStream2 {
Igor Aleksanov's avatar
Igor Aleksanov committed
		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.
David's avatar
David committed
	pub(crate) fn jrps_server_item(&self, item: impl quote::ToTokens) -> TokenStream2 {
Igor Aleksanov's avatar
Igor Aleksanov committed
		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.
David's avatar
David committed
	pub(crate) fn rpc_identifier(&self, method: &syn::LitStr) -> String {
Igor Aleksanov's avatar
Igor Aleksanov committed
		if let Some(ns) = &self.attrs.namespace {
			format!("{}_{}", ns.value(), method.value())
		} else {
			method.value()
		}
	}
}

fn has_attr(attrs: &[Attribute], ident: &str) -> bool {
	for attr in attrs.iter().filter_map(|a| a.path.get_ident()) {
		if attr == ident {
			return true;
		}
	}
	false
}