lib.rs 14.7 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.

Pierre Krieger's avatar
Pierre Krieger committed
extern crate proc_macro;

use proc_macro::TokenStream;
Igor Aleksanov's avatar
Igor Aleksanov committed
use quote::quote;
David's avatar
David committed
use rpc_macro::RpcDescription;
David's avatar
David committed
mod attributes;
Igor Aleksanov's avatar
Igor Aleksanov committed
mod helpers;
David's avatar
David committed
mod render_client;
mod render_server;
mod rpc_macro;
pub(crate) mod visitor;
Igor Aleksanov's avatar
Igor Aleksanov committed
/// Main RPC macro.
Igor Aleksanov's avatar
Igor Aleksanov committed
/// ## Description
Igor Aleksanov's avatar
Igor Aleksanov committed
/// This macro is capable of generating both server and client implementations on demand.
/// Based on the attributes provided to the `rpc` macro, either one or both of implementations
/// will be generated.
///
David's avatar
David committed
/// For clients, it will be an extension trait that adds all the required methods to a
Igor Aleksanov's avatar
Igor Aleksanov committed
/// type that implements `Client` or `SubscriptionClient` (depending on whether trait has
/// subscriptions methods or not), namely `HttpClient` and `WsClient`.
///
Alexandru Vasile's avatar
Alexandru Vasile committed
/// For servers, it will generate a trait mostly equivalent to the input, with the following differences:
Igor Aleksanov's avatar
Igor Aleksanov committed
///
/// - The trait will have one additional (already implemented) method, `into_rpc`, which turns any object that
///   implements the server trait into an `RpcModule`.
Alexandru Vasile's avatar
Alexandru Vasile committed
/// - For subscription methods:
///   - There will be one additional argument inserted right after `&self`: `subscription_sink: SubscriptionSink`.
///   It should be used to accept or reject a subscription and send data back to subscribers.
///   - The return type of the subscription method is `SubscriptionResult` for improved ergonomics.
Igor Aleksanov's avatar
Igor Aleksanov committed
///
/// Since this macro can generate up to two traits, both server and client traits will have
/// a new name. For the `Foo` trait, server trait will be named `FooServer`, and client,
/// correspondingly, `FooClient`.
///
/// To use the `FooClient`, just import it in the context. To use the server, the `FooServer` trait must be implemented
/// on your type first.
Igor Aleksanov's avatar
Igor Aleksanov committed
///
/// Note: you need to import the `jsonrpsee` façade crate in your code for the macro to work properly.
///
Igor Aleksanov's avatar
Igor Aleksanov committed
/// ## Prerequisites
///
/// - Implementors of the server trait must be `Sync`, `Send`, `Sized` and `'static`. If you want to implement this
///   trait on some type that is not thread-safe, consider using `Arc<RwLock<..>>`.
Igor Aleksanov's avatar
Igor Aleksanov committed
///
/// ## Examples
///
David's avatar
David committed
/// Below you can find examples of the macro usage along with the code
/// that generated for it by the macro.
Igor Aleksanov's avatar
Igor Aleksanov committed
///
/// ```ignore
/// #[rpc(client, server, namespace = "foo")]
/// pub trait Rpc {
///     #[method(name = "foo")]
///     async fn async_method(&self, param_a: u8, param_b: String) -> u16;
///     #[method(name = "bar")]
///     fn sync_method(&self) -> String;
///     #[subscription(name = "subscribe", item = "String")]
Igor Aleksanov's avatar
Igor Aleksanov committed
///     fn sub(&self);
/// }
/// ```
///
/// Server code that will be generated:
///
/// ```ignore
/// #[async_trait]
/// pub trait RpcServer {
David's avatar
David committed
///     // RPC methods are normal methods and can be either sync or async.
Igor Aleksanov's avatar
Igor Aleksanov committed
///     async fn async_method(&self, param_a: u8, param_b: String) -> u16;
///     fn sync_method(&self) -> String;
///
///     // Note that `subscription_sink` and `SubscriptionResult` were added automatically.
///     fn sub(&self, subscription_sink: SubscriptionResult) -> SubscriptionResult;
Igor Aleksanov's avatar
Igor Aleksanov committed
///
Maciej Hirsz's avatar
Maciej Hirsz committed
///     fn into_rpc(self) -> Result<Self, jsonrpsee::core::Error> {
Igor Aleksanov's avatar
Igor Aleksanov committed
///         // Actual implementation stripped, but inside we will create
///         // a module with one method and one subscription
///     }
/// }
/// ```
///
/// Client code that will be generated:
///
/// ```ignore
/// #[async_trait]
/// pub trait RpcClient: SubscriptionClient {
///     // In client implementation all the methods are (obviously) async.
///     async fn async_method(&self, param_a: u8, param_b: String) -> Result<u16, Error> {
///         // Actual implementations are stripped, but inside a corresponding `Client` or
///         // `SubscriptionClient` method is called.
///     }
///     async fn sync_method(&self) -> Result<String, Error> {
///         // ...
///     }
Igor Aleksanov's avatar
Igor Aleksanov committed
///     // Subscription method returns `Subscription` object in case of success.
///     async fn sub(&self) -> Result<Subscription<String>, Error> {
///         // ...
///     }
/// }
///
/// impl<T> RpcClient for T where T: SubscriptionClient {}
/// ```
///
/// ## Attributes
///
/// ### `rpc` attribute
///
/// `rpc` attribute is applied to a trait in order to turn it into an RPC implementation.
///
/// **Arguments:**
///
/// - `server`: generate `<Trait>Server` trait for the server implementation.
David's avatar
David committed
/// - `client`: generate `<Trait>Client` extension trait that builds RPC clients to invoke a concrete RPC
///   implementation's methods conveniently.
/// - `namespace`: add a prefix to all the methods and subscriptions in this RPC. For example, with namespace `foo` and
///   method `spam`, the resulting method name will be `foo_spam`.
Igor Aleksanov's avatar
Igor Aleksanov committed
///
/// **Trait requirements:**
///
David's avatar
David committed
/// A trait wrapped with the `rpc` attribute **must not**:
Igor Aleksanov's avatar
Igor Aleksanov committed
///
/// - have associated types or constants;
David's avatar
David committed
/// - have Rust methods not marked with either the `method` or `subscription` attribute;
Igor Aleksanov's avatar
Igor Aleksanov committed
/// - be empty.
///
/// At least one of the `server` or `client` flags must be provided, otherwise the compilation will err.
///
/// ### `method` attribute
///
/// `method` attribute is used to define an RPC method.
///
/// **Arguments:**
///
/// - `name` (mandatory): name of the RPC method. Does not have to be the same as the Rust method name.
/// - `aliases`: list of name aliases for the RPC method as a comma separated string.
///              Aliases are processed ignoring the namespace, so add the complete name, including the
///              namespace.
/// - `blocking`: when set method execution will always spawn on a dedicated thread. Only usable with non-`async` methods.
/// - `param_kind`: kind of structure to use for parameter passing. Can be "array" or "map", defaults to "array".
Igor Aleksanov's avatar
Igor Aleksanov committed
///
/// **Method requirements:**
///
David's avatar
David committed
/// A Rust method marked with the `method` attribute, **may**:
Igor Aleksanov's avatar
Igor Aleksanov committed
///
/// - be either `async` or not;
/// - have input parameters or not;
David's avatar
David committed
/// - have a return value or not (in the latter case, it will be considered a notification method).
Igor Aleksanov's avatar
Igor Aleksanov committed
///
/// ### `subscription` attribute
///
/// `subscription` attribute is used to define a publish/subscribe interface according to the [ethereum pubsub specification](https://geth.ethereum.org/docs/rpc/pubsub)
///
Igor Aleksanov's avatar
Igor Aleksanov committed
/// **Arguments:**
///
/// - `name` (mandatory): name of the RPC method. Does not have to be the same as the Rust method name.
/// - `unsubscribe` (optional): name of the RPC method to unsubscribe from the subscription. Must not be the same as `name`.
///                             This is generated for you if the subscription name starts with `subscribe`.
/// - `aliases` (optional): aliases for `name`. Aliases are processed ignoring the namespace,
///                         so add the complete name, including the namespace.
/// - `unsubscribe_aliases` (optional): Similar to `aliases` but for `unsubscribe`.
Igor Aleksanov's avatar
Igor Aleksanov committed
/// - `item` (mandatory): type of items yielded by the subscription. Note that it must be the type, not string.
/// - `param_kind`: kind of structure to use for parameter passing. Can be "array" or "map", defaults to "array".
Igor Aleksanov's avatar
Igor Aleksanov committed
///
/// **Method requirements:**
///
David's avatar
David committed
/// Rust method marked with the `subscription` attribute **must**:
Igor Aleksanov's avatar
Igor Aleksanov committed
///
/// - be synchronous;
Igor Aleksanov's avatar
Igor Aleksanov committed
///
/// Rust method marked with `subscription` attribute **may**:
///
/// - have input parameters or not.
///
/// ## Full workflow example
///
/// ```rust
/// //! Example of using proc macro to generate working client and server.
///
/// use std::net::SocketAddr;
///
/// use futures_channel::oneshot;
/// use jsonrpsee::{ws_client::*, ws_server::WsServerBuilder};
///
David's avatar
David committed
/// // RPC is put into a separate module to clearly show names of generated entities.
Igor Aleksanov's avatar
Igor Aleksanov committed
/// mod rpc_impl {
Alexandru Vasile's avatar
Alexandru Vasile committed
///     use jsonrpsee::{proc_macros::rpc, core::async_trait, core::RpcResult, ws_server::SubscriptionSink};
///     use jsonrpsee::types::SubscriptionResult;
Igor Aleksanov's avatar
Igor Aleksanov committed
///
///     // Generate both server and client implementations, prepend all the methods with `foo_` prefix.
///     #[rpc(client, server, namespace = "foo")]
David's avatar
David committed
///     pub trait MyRpc {
Igor Aleksanov's avatar
Igor Aleksanov committed
///         #[method(name = "foo")]
David's avatar
David committed
///         async fn async_method(&self, param_a: u8, param_b: String) -> RpcResult<u16>;
Igor Aleksanov's avatar
Igor Aleksanov committed
///
///         #[method(name = "bar")]
David's avatar
David committed
///         fn sync_method(&self) -> RpcResult<u16>;
Igor Aleksanov's avatar
Igor Aleksanov committed
///
///         #[method(name = "baz", blocking)]
///         fn blocking_method(&self) -> RpcResult<u16>;
///
///         /// Override the `foo_sub` and use `foo_subNotif` for the notifications.
///         ///
///         /// The item field indicates which type goes into result field below.
///         ///
///         /// The notification format:
///         ///
///         /// ```
///         /// {
///         ///     "jsonrpc":"2.0",
///         ///     "method":"foo_subNotif",
///         ///     "params":["subscription":"someID", "result":"some string"]
///         /// }
///         /// ```
///         #[subscription(name = "sub" => "subNotif", unsubscribe = "unsub", item = String)]
///         fn sub_override_notif_method(&self);
///
///         /// Use the same method name for both the `subscribe call` and `notifications`
///         ///
///         /// The unsubscribe method name is generated here `foo_unsubscribe`
///         /// Thus the `unsubscribe attribute` is not needed unless a custom unsubscribe method name is wanted.
///         ///
///         /// The notification format:
///         ///
///         /// ```
///         /// {
///         ///     "jsonrpc":"2.0",
///         ///     "method":"foo_subscribe",
///         ///     "params":["subscription":"someID", "result":"some string"]
///         /// }
///         /// ```
///         #[subscription(name = "subscribe", item = String)]
Igor Aleksanov's avatar
Igor Aleksanov committed
///     }
///
David's avatar
David committed
///     // Structure that will implement the `MyRpcServer` trait.
///     // It can have fields, if required, as long as it's still `Send + Sync + 'static`.
Igor Aleksanov's avatar
Igor Aleksanov committed
///     pub struct RpcServerImpl;
///
David's avatar
David committed
///     // Note that the trait name we use is `MyRpcServer`, not `MyRpc`!
Igor Aleksanov's avatar
Igor Aleksanov committed
///     #[async_trait]
David's avatar
David committed
///     impl MyRpcServer for RpcServerImpl {
David's avatar
David committed
///         async fn async_method(&self, _param_a: u8, _param_b: String) -> RpcResult<u16> {
///             Ok(42)
Igor Aleksanov's avatar
Igor Aleksanov committed
///         }
///
David's avatar
David committed
///         fn sync_method(&self) -> RpcResult<u16> {
///             Ok(10)
///         }
///
///         fn blocking_method(&self) -> RpcResult<u16> {
///             // This will block current thread for 1 second, which is fine since we marked
///             // this method as `blocking` above.
///             std::thread::sleep(std::time::Duration::from_millis(1000));
///             Ok(11)
Igor Aleksanov's avatar
Igor Aleksanov committed
///         }
///
///         // The stream API can be used to pipe items from the underlying stream
///         // as subscription responses.
Alexandru Vasile's avatar
Alexandru Vasile committed
///         fn sub_override_notif_method(&self, mut sink: SubscriptionSink) -> SubscriptionResult {
///             tokio::spawn(async move {
///                 let stream = futures_util::stream::iter(["one", "two", "three"]);
Alexandru Vasile's avatar
Alexandru Vasile committed
///                 sink.pipe_from_stream(stream).await;
Alexandru Vasile's avatar
Alexandru Vasile committed
///             Ok(())
Igor Aleksanov's avatar
Igor Aleksanov committed
///         // We could've spawned a `tokio` future that yields values while our program works,
///         // but for simplicity of the example we will only send two values and then close
///         // the subscription.
Alexandru Vasile's avatar
Alexandru Vasile committed
///         fn sub(&self, mut sink: SubscriptionSink) -> SubscriptionResult {
///             let _ = sink.send(&"Response_A");
///             let _ = sink.send(&"Response_B");
Alexandru Vasile's avatar
Alexandru Vasile committed
///             Ok(())
Igor Aleksanov's avatar
Igor Aleksanov committed
///         }
///     }
/// }
///
David's avatar
David committed
/// // Use the generated implementations of server and client.
/// use rpc_impl::{MyRpcClient, MyRpcServer, RpcServerImpl};
Igor Aleksanov's avatar
Igor Aleksanov committed
///
/// pub async fn websocket_server() -> SocketAddr {
Maciej Hirsz's avatar
Maciej Hirsz committed
///     let server = WsServerBuilder::default().build("127.0.0.1:0").await.unwrap();
///     let addr = server.local_addr().unwrap();
Igor Aleksanov's avatar
Igor Aleksanov committed
///
Maciej Hirsz's avatar
Maciej Hirsz committed
///     // `into_rpc()` method was generated inside of the `RpcServer` trait under the hood.
///     server.start(RpcServerImpl.into_rpc()).unwrap();
Igor Aleksanov's avatar
Igor Aleksanov committed
///
Maciej Hirsz's avatar
Maciej Hirsz committed
///     addr
Igor Aleksanov's avatar
Igor Aleksanov committed
/// }
///
David's avatar
David committed
/// // In the main function, we start the server, create a client connected to this server,
/// // and call the available methods.
Igor Aleksanov's avatar
Igor Aleksanov committed
/// #[tokio::main]
/// async fn main() {
///     let server_addr = websocket_server().await;
///     let server_url = format!("ws://{}", server_addr);
David's avatar
David committed
///     // Note that we create the client as usual, but thanks to the `use rpc_impl::MyRpcClient`,
Igor Aleksanov's avatar
Igor Aleksanov committed
///     // the client object will have all the methods to interact with the server.
///     let client = WsClientBuilder::default().build(&server_url).await.unwrap();
///
///     // Invoke RPC methods.
///     assert_eq!(client.async_method(10, "a".into()).await.unwrap(), 42);
///     assert_eq!(client.sync_method().await.unwrap(), 10);
///
///     // Subscribe and receive messages from the subscription.
///     let mut sub = client.sub().await.unwrap();
///     let first_recv = sub.next().await.unwrap().unwrap();
///     assert_eq!(first_recv, "Response_A".to_string());
///     let second_recv = sub.next().await.unwrap().unwrap();
///     assert_eq!(second_recv, "Response_B".to_string());
Igor Aleksanov's avatar
Igor Aleksanov committed
/// }
/// ```
#[proc_macro_attribute]
pub fn rpc(attr: TokenStream, item: TokenStream) -> TokenStream {
	let attr = proc_macro2::TokenStream::from(attr);

	let rebuilt_rpc_attribute = syn::Attribute {
		pound_token: syn::token::Pound::default(),
		style: syn::AttrStyle::Outer,
		bracket_token: syn::token::Bracket::default(),
		path: syn::Ident::new("rpc", proc_macro2::Span::call_site()).into(),
		tokens: quote! { (#attr) },
	};
Igor Aleksanov's avatar
Igor Aleksanov committed
	match rpc_impl(rebuilt_rpc_attribute, item) {
		Ok(tokens) => tokens,
		Err(err) => err.to_compile_error(),
Igor Aleksanov's avatar
Igor Aleksanov committed
	.into()
Igor Aleksanov's avatar
Igor Aleksanov committed
/// Convenience form of `rpc` that may use `?` for error handling to avoid boilerplate.
fn rpc_impl(attr: syn::Attribute, item: TokenStream) -> Result<proc_macro2::TokenStream, syn::Error> {
	let trait_data: syn::ItemTrait = syn::parse(item)?;
	let rpc = RpcDescription::from_item(attr, trait_data)?;
	rpc.render()