Skip to content
Draft
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
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ tower-http = "0.6"
tracing = "0.1.34"
url = "2.4"
wasm-bindgen-futures = "0.4.19"
open-rpc = { git = "https://github.com/Velnbur/open-rpc", branch = "feature/utoipa-integration" }

# Dev dependencies
anyhow = "1"
Expand Down
5 changes: 3 additions & 2 deletions examples/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,12 @@ futures = { workspace = true }
jsonrpsee = { path = "../jsonrpsee", features = ["server", "http-client", "ws-client", "macros", "client-ws-transport-tls"] }
tracing = { workspace = true }
tracing-subscriber = { workspace = true, features = ["env-filter"] }
tokio = { workspace = true, features = ["rt-multi-thread", "time"] }
tokio = { workspace = true, features = ["rt-multi-thread", "time", "signal"] }
tokio-stream = { workspace = true, features = ["sync"] }
serde_json = { workspace = true }
tower-http = { workspace = true, features = ["cors", "compression-full", "sensitive-headers", "trace", "timeout"] }
tower = { workspace = true, features = ["timeout"] }
hyper = { workspace = true }
hyper-util = { workspace = true, features = ["client", "client-legacy"]}
console-subscriber = { workspace = true }
console-subscriber = { workspace = true }
serde = { workspace = true }
74 changes: 74 additions & 0 deletions examples/examples/rpc_discover.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
use std::net::SocketAddr;

use jsonrpsee::core::{RpcResult, async_trait};
use jsonrpsee::open_rpc::utoipa;
use jsonrpsee::proc_macros::rpc;
use jsonrpsee::server::ServerBuilder;

#[derive(utoipa::ToSchema, serde::Serialize, serde::Deserialize)]
pub struct AddRequest {
pub a: u8,
pub b: u8,
}

#[derive(utoipa::ToSchema, serde::Serialize, serde::Deserialize, Clone)]
pub struct AddResponse {
pub sum: u16,
}

#[derive(utoipa::ToSchema, serde::Serialize, serde::Deserialize, Clone)]
#[serde(rename_all = "snake_case")]
pub enum Operation {
Add,
Mul,
Sub,
}

#[rpc(server, discover)]
pub trait Rpc {
#[method(name = "foo")]
async fn async_method(&self, param_a: u8, param_b: String) -> RpcResult<u16>;

#[method(name = "add")]
async fn add(&self, request: AddRequest) -> RpcResult<AddResponse>;

#[method(name = "calculate")]
async fn calculate(&self, args: Vec<i64>, operation: Operation) -> RpcResult<i64>;
}

pub struct RpcServerImpl;

#[async_trait]
impl RpcServer for RpcServerImpl {
async fn async_method(&self, _param_a: u8, _param_b: String) -> RpcResult<u16> {
Ok(42u16)
}

async fn add(&self, request: AddRequest) -> RpcResult<AddResponse> {
Ok(AddResponse { sum: request.a as u16 + request.b as u16 })
}

async fn calculate(&self, args: Vec<i64>, operation: Operation) -> RpcResult<i64> {
match operation {
Operation::Add => Ok(args.iter().sum()),
Operation::Mul => Ok(args.iter().product()),
Operation::Sub => Ok(args.iter().skip(1).fold(args[0], |acc, x| acc - x)),
}
}
}

pub async fn server() -> SocketAddr {
let server = ServerBuilder::default().build("127.0.0.1:8080").await.unwrap();
let addr = server.local_addr().unwrap();
let server_handle = server.start(RpcServerImpl.into_rpc());

tokio::spawn(server_handle.stopped());

tokio::signal::ctrl_c().await.unwrap();
addr
}

#[tokio::main]
async fn main() {
let _server_addr = server().await;
}
1 change: 1 addition & 0 deletions jsonrpsee/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ jsonrpsee-server = { workspace = true, optional = true }
jsonrpsee-proc-macros = { workspace = true, optional = true }
jsonrpsee-core = { workspace = true, optional = true }
jsonrpsee-types = { workspace = true, optional = true }
open-rpc = { workspace = true, features = ["utoipa"] }
tracing = { workspace = true, optional = true }
tokio = { workspace = true, optional = true }

Expand Down
2 changes: 2 additions & 0 deletions jsonrpsee/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -101,3 +101,5 @@ cfg_client_or_server! {
cfg_client! {
pub use jsonrpsee_core::rpc_params;
}

pub use open_rpc;
83 changes: 80 additions & 3 deletions proc-macros/src/render_server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,11 @@ use std::str::FromStr;
use super::RpcDescription;
use crate::{
helpers::{generate_where_clause, is_option},
rpc_macro::RpcFnArg,
rpc_macro::{RPC_DISCOVER_METHOD, RpcFnArg},
};
use proc_macro2::{Span, TokenStream as TokenStream2};
use quote::{quote, quote_spanned};
use syn::Attribute;
use syn::{Attribute, Type};

impl RpcDescription {
pub(super) fn render_server(&self) -> Result<TokenStream2, syn::Error> {
Expand Down Expand Up @@ -62,7 +62,8 @@ impl RpcDescription {
}

fn render_methods(&self) -> Result<TokenStream2, syn::Error> {
let methods = self.methods.iter().map(|method| {
// skip discover method, as we render it seperatly
let methods = self.methods.iter().filter(|m| m.name != RPC_DISCOVER_METHOD).map(|method| {
let docs = &method.docs;
let mut method_sig = method.signature.clone();

Expand Down Expand Up @@ -101,12 +102,66 @@ impl RpcDescription {
}
});

let discover_method = if self.discover {
self.rpc_render_discover_method()
} else {
quote! {}
};

Ok(quote! {
#(#methods)*
#(#subscriptions)*
#discover_method
})
}

fn rpc_render_discover_method(&self) -> TokenStream2 {
let title = self.trait_def.ident.to_string();
// skip discover method itself, we don't want to appear it in the doc, for now.
let methods = self.methods.iter().filter(|m| m.name != RPC_DISCOVER_METHOD);

let names = methods.clone().map(|m| m.name.to_string());
let docs = methods.clone().map(|m| m.docs.to_string());
let param_expansion = methods.clone().map(|m| {
let param_names = m.params.iter().map(|p| p.arg_pat.ident.to_string()).collect::<Vec<String>>();
let param_types = m.params.iter().map(|p| &p.ty).collect::<Vec<&Type>>();

// generate code for generating schema from types and content descriptor for each parameter
quote! {
#({
let __schema: Schema = schema!(#[inline] #param_types).into();
jsonrpsee::open_rpc::ContentDescriptor::new(#param_names, __schema.try_into().expect("invalid schema"))
}),*
}
});
let return_types = methods.map(|m| {
m.returns
.as_ref()
.map(|r| {
let r = extract_ok_value_from_return_type(r);
quote! {{
let __schema: Schema = schema!(#[inline] #r).into();
Some(jsonrpsee::open_rpc::ContentDescriptor::new("return".to_string(), __schema.try_into().expect("invalid schema")))
}}
})
.unwrap_or_else(|| quote! { None })
});

quote! {
async fn discover(&self) -> Result<jsonrpsee::open_rpc::OpenRpc, std::convert::Infallible> {
use jsonrpsee::open_rpc::utoipa::{schema, openapi::{RefOr, Schema}};
pub use jsonrpsee::open_rpc::utoipa;

let __response = jsonrpsee::open_rpc::OpenRpc::new(#title)
#(
.with_method(#names, #docs, std::vec![#param_expansion], #return_types)
)*;

Ok(__response)
}
}
}

/// Helper that will ignore results of `register_*` method calls, and panic if there have been
/// any errors in debug builds.
///
Expand Down Expand Up @@ -458,3 +513,25 @@ impl RpcDescription {
(parsing, params_fields)
}
}

/// This method extracts the `T` from `Result<T, E>` or `RpcResult<T, E>` types,
/// otherwise returns the original type.
fn extract_ok_value_from_return_type(r: &syn::Type) -> &syn::Type {
match r {
syn::Type::Path(type_path)
if type_path
.path
.segments
.last()
.map(|s| s.ident == "Result" || s.ident == "RpcResult")
.unwrap_or(false) =>
{
if let syn::PathArguments::AngleBracketed(ref args) = type_path.path.segments.last().unwrap().arguments {
if let Some(syn::GenericArgument::Type(ty)) = args.args.first() { ty } else { r }
} else {
r
}
}
_ => r,
}
}
33 changes: 31 additions & 2 deletions proc-macros/src/rpc_macro.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,13 @@ use crate::attributes::{
};
use crate::helpers::extract_doc_comments;
use proc_macro2::TokenStream as TokenStream2;
use quote::quote;
use quote::{ToTokens, quote};
use syn::parse2;
use syn::spanned::Spanned;
use syn::{Attribute, Token, punctuated::Punctuated};

pub(crate) const RPC_DISCOVER_METHOD: &str = "rpc.discover";

/// Represents a single argument in a RPC call.
///
/// stores modifications based on attributes
Expand Down Expand Up @@ -274,18 +277,21 @@ pub struct RpcDescription {
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![,]>>,
/// Optional, specifies whenther rpc.discover method shold be generated.
pub(crate) discover: bool,
}

impl RpcDescription {
pub fn from_item(attr: Attribute, mut item: syn::ItemTrait) -> syn::Result<Self> {
let [client, server, namespace, namespace_separator, client_bounds, server_bounds] =
let [client, server, namespace, namespace_separator, client_bounds, server_bounds, discover] =
AttributeMeta::parse(attr)?.retain([
"client",
"server",
"namespace",
"namespace_separator",
"client_bounds",
"server_bounds",
"discover",
])?;

let needs_server = optional(server, Argument::flag)?.is_some();
Expand All @@ -294,6 +300,7 @@ impl RpcDescription {
let namespace_separator = optional(namespace_separator, Argument::string)?;
let client_bounds = optional(client_bounds, Argument::group)?;
let server_bounds = optional(server_bounds, Argument::group)?;
let discover = optional(discover, Argument::flag)?.is_some();
if !needs_server && !needs_client {
return Err(syn::Error::new_spanned(&item.ident, "Either 'server' or 'client' attribute must be applied"));
}
Expand Down Expand Up @@ -372,6 +379,27 @@ impl RpcDescription {
return Err(syn::Error::new_spanned(&item, "RPC cannot be empty"));
}

// If discover is enabled, add the discover method for `into_rpc`.
if discover {
// TODO(Velnbur): may be there is a more elegant way to add it for `into_rpc`,
// but for now this hack is used:
methods.push(RpcMethod {
name: RPC_DISCOVER_METHOD.to_string(),
blocking: false,
docs: "Discover the available methods".into_token_stream(),
deprecated: "false".to_token_stream(),
params: Vec::new(),
param_kind: ParamKind::Array,
returns: Some(parse2(quote! {jsonrpsee::open_rpc::OpenRpc}).expect("to be valid type")),
signature: parse2(
quote! {async fn discover(&self) -> Result<jsonrpsee::open_rpc::OpenRpc, std::convert::Infallible>;},
)
.expect("to be valid signature"),
aliases: Vec::new(),
with_extensions: false,
});
}

Ok(Self {
jsonrpsee_client_path,
jsonrpsee_server_path,
Expand All @@ -384,6 +412,7 @@ impl RpcDescription {
subscriptions,
client_bounds,
server_bounds,
discover,
})
}

Expand Down