Skip to content

feat: add namespace_separator option for RPC methods #1544

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Apr 8, 2025
Merged
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
4 changes: 3 additions & 1 deletion proc-macros/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@
/// that generated for it by the macro.
///
/// ```ignore
/// #[rpc(client, server, namespace = "foo")]
/// #[rpc(client, server, namespace = "foo", namespace_separator = ".")]
/// pub trait Rpc {
/// #[method(name = "foo")]
/// async fn async_method(&self, param_a: u8, param_b: String) -> u16;
Expand Down Expand Up @@ -147,6 +147,8 @@
/// 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`.
/// - `namespace_separator`: customize the separator used between namespace and method name. Defaults to `_`.
/// For example, `namespace = "foo", namespace_separator = "."` results in method names like `foo.bar` instead of `foo_bar`.

Check warning on line 151 in proc-macros/src/lib.rs

View workflow job for this annotation

GitHub Actions / Check style

using tabs in doc comments is not recommended

Check warning on line 151 in proc-macros/src/lib.rs

View workflow job for this annotation

GitHub Actions / Check style

using tabs in doc comments is not recommended
/// - `server_bounds`: replace *all* auto-generated trait bounds with the user-defined ones for the server
/// implementation.
/// - `client_bounds`: replace *all* auto-generated trait bounds with the user-defined ones for the client
Expand All @@ -170,8 +172,8 @@
///
/// - `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

Check warning on line 175 in proc-macros/src/lib.rs

View workflow job for this annotation

GitHub Actions / Check style

doc list item overindented
/// namespace.

Check warning on line 176 in proc-macros/src/lib.rs

View workflow job for this annotation

GitHub Actions / Check style

doc list item overindented
/// - `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".
///
Expand All @@ -191,9 +193,9 @@
///
/// - `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`.

Check warning on line 196 in proc-macros/src/lib.rs

View workflow job for this annotation

GitHub Actions / Check style

doc list item overindented
/// - `aliases` (optional): aliases for `name`. Aliases are processed ignoring the namespace,
/// so add the complete name, including the namespace.

Check warning on line 198 in proc-macros/src/lib.rs

View workflow job for this annotation

GitHub Actions / Check style

doc list item overindented
/// - `unsubscribe_aliases` (optional): Similar to `aliases` but for `unsubscribe`.
/// - `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".
Expand Down
29 changes: 23 additions & 6 deletions proc-macros/src/rpc_macro.rs
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,8 @@ pub struct RpcDescription {
pub(crate) needs_client: bool,
/// Optional prefix for RPC namespace.
pub(crate) namespace: Option<String>,
/// Optional separator between namespace and method name. Defaults to `_`.
pub(crate) namespace_separator: Option<String>,
/// Trait definition in which all the attributes were stripped.
pub(crate) trait_def: syn::ItemTrait,
/// List of RPC methods defined in the trait.
Expand All @@ -276,12 +278,20 @@ pub struct RpcDescription {

impl RpcDescription {
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 [client, server, namespace, namespace_separator, client_bounds, server_bounds] =
AttributeMeta::parse(attr)?.retain([
"client",
"server",
"namespace",
"namespace_separator",
"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 namespace_separator = optional(namespace_separator, Argument::string)?;
let client_bounds = optional(client_bounds, Argument::group)?;
let server_bounds = optional(server_bounds, Argument::group)?;
if !needs_server && !needs_client {
Expand Down Expand Up @@ -368,6 +378,7 @@ impl RpcDescription {
needs_server,
needs_client,
namespace,
namespace_separator,
trait_def: item,
methods,
subscriptions,
Expand Down Expand Up @@ -400,12 +411,18 @@ impl RpcDescription {
quote! { #jsonrpsee::#item }
}

/// Based on the namespace, renders the full name of the RPC method/subscription.
/// Based on the namespace and separator, 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`.
/// For namespace `foo`, method `makeSpam`, and separator `_`, result will be `foo_makeSpam`.
/// For separator `.`, result will be `foo.makeSpam`.
/// For no namespace, returns just `makeSpam`.
pub(crate) fn rpc_identifier<'a>(&self, method: &'a str) -> Cow<'a, str> {
if let Some(ns) = &self.namespace { format!("{ns}_{method}").into() } else { Cow::Borrowed(method) }
if let Some(ns) = &self.namespace {
let sep = self.namespace_separator.as_deref().unwrap_or("_");
format!("{ns}{sep}{method}").into()
} else {
Cow::Borrowed(method)
}
}
}

Expand Down
54 changes: 54 additions & 0 deletions tests/tests/proc_macros.rs
Original file line number Diff line number Diff line change
Expand Up @@ -296,6 +296,60 @@ async fn macro_zero_copy_cow() {
assert_eq!(resp, r#"{"jsonrpc":"2.0","id":0,"result":"Zero copy params: false"}"#);
}

#[tokio::test]
async fn namespace_separator_dot_formatting_works() {
use jsonrpsee::core::async_trait;
use jsonrpsee::proc_macros::rpc;
use serde_json::json;

#[rpc(server, namespace = "foo", namespace_separator = ".")]
pub trait DotSeparatorRpc {
#[method(name = "dot")]
fn dot(&self, a: u32, b: &str) -> Result<String, jsonrpsee::types::ErrorObjectOwned>;
}

struct DotImpl;

#[async_trait]
impl DotSeparatorRpcServer for DotImpl {
fn dot(&self, a: u32, b: &str) -> Result<String, jsonrpsee::types::ErrorObjectOwned> {
Ok(format!("Called with: {}, {}", a, b))
}
}

let module = DotImpl.into_rpc();
let res: String = module.call("foo.dot", [json!(1_u64), json!("test")]).await.unwrap();

assert_eq!(&res, "Called with: 1, test");
}

#[tokio::test]
async fn namespace_separator_slash_formatting_works() {
use jsonrpsee::core::async_trait;
use jsonrpsee::proc_macros::rpc;
use serde_json::json;

#[rpc(server, namespace = "math", namespace_separator = "/")]
pub trait SlashSeparatorRpc {
#[method(name = "add")]
fn add(&self, x: i32, y: i32) -> Result<i32, jsonrpsee::types::ErrorObjectOwned>;
}

struct SlashImpl;

#[async_trait]
impl SlashSeparatorRpcServer for SlashImpl {
fn add(&self, x: i32, y: i32) -> Result<i32, jsonrpsee::types::ErrorObjectOwned> {
Ok(x + y)
}
}

let module = SlashImpl.into_rpc();
let result: i32 = module.call("math/add", [json!(20), json!(22)]).await.unwrap();

assert_eq!(result, 42);
}

// Disabled on MacOS as GH CI timings on Mac vary wildly (~100ms) making this test fail.
#[cfg(not(target_os = "macos"))]
#[ignore]
Expand Down
Loading