diff --git a/Cargo.toml b/Cargo.toml index 5860b19d45..9448b84b63 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" diff --git a/examples/Cargo.toml b/examples/Cargo.toml index 1ae0e22265..fec61bf5f4 100644 --- a/examples/Cargo.toml +++ b/examples/Cargo.toml @@ -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 } \ No newline at end of file +console-subscriber = { workspace = true } +serde = { workspace = true } diff --git a/examples/examples/rpc_discover.rs b/examples/examples/rpc_discover.rs new file mode 100644 index 0000000000..a470ec9a38 --- /dev/null +++ b/examples/examples/rpc_discover.rs @@ -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; + + #[method(name = "add")] + async fn add(&self, request: AddRequest) -> RpcResult; + + #[method(name = "calculate")] + async fn calculate(&self, args: Vec, operation: Operation) -> RpcResult; +} + +pub struct RpcServerImpl; + +#[async_trait] +impl RpcServer for RpcServerImpl { + async fn async_method(&self, _param_a: u8, _param_b: String) -> RpcResult { + Ok(42u16) + } + + async fn add(&self, request: AddRequest) -> RpcResult { + Ok(AddResponse { sum: request.a as u16 + request.b as u16 }) + } + + async fn calculate(&self, args: Vec, operation: Operation) -> RpcResult { + 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; +} diff --git a/jsonrpsee/Cargo.toml b/jsonrpsee/Cargo.toml index 379382f066..10d71c1bc5 100644 --- a/jsonrpsee/Cargo.toml +++ b/jsonrpsee/Cargo.toml @@ -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 } diff --git a/jsonrpsee/src/lib.rs b/jsonrpsee/src/lib.rs index b026fd0b28..49cd426cb0 100644 --- a/jsonrpsee/src/lib.rs +++ b/jsonrpsee/src/lib.rs @@ -101,3 +101,5 @@ cfg_client_or_server! { cfg_client! { pub use jsonrpsee_core::rpc_params; } + +pub use open_rpc; diff --git a/proc-macros/src/render_server.rs b/proc-macros/src/render_server.rs index f8e8057abf..c18a8eb03d 100644 --- a/proc-macros/src/render_server.rs +++ b/proc-macros/src/render_server.rs @@ -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 { @@ -62,7 +62,8 @@ impl RpcDescription { } fn render_methods(&self) -> Result { - 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(); @@ -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::>(); + let param_types = m.params.iter().map(|p| &p.ty).collect::>(); + + // 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 { + 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. /// @@ -458,3 +513,25 @@ impl RpcDescription { (parsing, params_fields) } } + +/// This method extracts the `T` from `Result` or `RpcResult` 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, + } +} diff --git a/proc-macros/src/rpc_macro.rs b/proc-macros/src/rpc_macro.rs index d6c374d595..c6d4b5d925 100644 --- a/proc-macros/src/rpc_macro.rs +++ b/proc-macros/src/rpc_macro.rs @@ -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 @@ -274,11 +277,13 @@ pub struct RpcDescription { pub(crate) client_bounds: Option>, /// Optional user defined trait bounds for the server implementation. pub(crate) server_bounds: Option>, + /// 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 { - 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", @@ -286,6 +291,7 @@ impl RpcDescription { "namespace_separator", "client_bounds", "server_bounds", + "discover", ])?; let needs_server = optional(server, Argument::flag)?.is_some(); @@ -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")); } @@ -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;}, + ) + .expect("to be valid signature"), + aliases: Vec::new(), + with_extensions: false, + }); + } + Ok(Self { jsonrpsee_client_path, jsonrpsee_server_path, @@ -384,6 +412,7 @@ impl RpcDescription { subscriptions, client_bounds, server_bounds, + discover, }) }