use crate::app::extract_app_meta;
use crate::server_attrs::{has_server_hidden, has_server_skip, validate_server_attrs};
use proc_macro2::TokenStream as TokenStream2;
use quote::{format_ident, quote};
use server_less_parse::{MethodInfo, extract_methods, get_impl_name, partition_methods};
use server_less_rpc::{self, AsyncHandling};
use syn::{ItemImpl, Token, parse::Parse};
use crate::context::partition_context_params;
#[derive(Default)]
pub(crate) struct JsonRpcArgs {
pub path: Option<String>,
}
impl Parse for JsonRpcArgs {
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
let mut args = JsonRpcArgs::default();
while !input.is_empty() {
let ident: syn::Ident = input.parse()?;
input.parse::<Token![=]>()?;
match ident.to_string().as_str() {
"path" => {
let lit: syn::LitStr = input.parse()?;
args.path = Some(lit.value());
}
other => {
const VALID: &[&str] = &["path"];
let suggestion = crate::did_you_mean(other, VALID)
.map(|s| format!(" — did you mean `{s}`?"))
.unwrap_or_default();
return Err(syn::Error::new(
ident.span(),
format!("unknown argument `{other}`{suggestion}. Valid arguments: path"),
));
}
}
if input.peek(Token![,]) {
input.parse::<Token![,]>()?;
}
}
Ok(args)
}
}
pub(crate) fn expand_jsonrpc(args: JsonRpcArgs, mut impl_block: ItemImpl) -> syn::Result<TokenStream2> {
crate::reject_generic_impl(&impl_block)?;
let _app_meta = extract_app_meta(&mut impl_block.attrs);
let struct_name = get_impl_name(&impl_block)?;
let (impl_generics, _ty_generics, where_clause) = impl_block.generics.split_for_impl();
let self_ty = &impl_block.self_ty;
let methods = extract_methods(&impl_block)?;
let path = args.path.unwrap_or_else(|| "/rpc".to_string());
for m in &methods {
validate_server_attrs(m)?;
}
let partitioned = partition_methods(&methods, has_server_skip);
let visible_leaf: Vec<_> = partitioned
.leaf
.iter()
.copied()
.filter(|m| !has_server_hidden(m))
.collect();
let dispatch_arms_async: Vec<_> = partitioned
.leaf
.iter()
.map(|m| {
let arm = generate_dispatch_arm(m)?;
let cfg_attrs = &m.cfg_attrs;
Ok(quote! {
#(#cfg_attrs)*
#arm
})
})
.collect::<syn::Result<Vec<_>>>()?;
let dispatch_arms_sync: Vec<_> = partitioned
.leaf
.iter()
.map(|m| {
let arm = generate_sync_dispatch_arm(m)?;
let cfg_attrs = &m.cfg_attrs;
Ok(quote! {
#(#cfg_attrs)*
#arm
})
})
.collect::<syn::Result<Vec<_>>>()?;
let method_name_strings: Vec<String> = visible_leaf
.iter()
.map(|m| m.wire_name_or(|n| n))
.collect();
let method_name_stmts: Vec<_> = visible_leaf
.iter()
.map(|m| {
let name = m.wire_name_or(|n| n);
let cfg_attrs = &m.cfg_attrs;
quote! {
#(#cfg_attrs)*
names.push(#name.to_string());
}
})
.collect();
let method_names = &method_name_strings;
let jsonrpc_method_doc_entries: Vec<String> = visible_leaf
.iter()
.map(|m| {
let name = m.wire_name_or(|n| n);
match &m.docs {
Some(doc) => format!("- `{name}` — {doc}"),
None => format!("- `{name}`"),
}
})
.collect();
let has_jsonrpc_mounts =
!partitioned.static_mounts.is_empty() || !partitioned.slug_mounts.is_empty();
let jsonrpc_methods_doc = if jsonrpc_method_doc_entries.is_empty() && !has_jsonrpc_mounts {
"Get available JSON-RPC method names.".to_string()
} else {
let mount_note = if has_jsonrpc_mounts {
"\n\nAlso includes methods from mounted sub-services."
} else {
""
};
format!(
"Get available JSON-RPC method names.\n\n# Methods\n\n{}{}",
jsonrpc_method_doc_entries.join("\n"),
mount_note
)
};
let jsonrpc_router_doc = format!(
"Create an axum Router with JSON-RPC endpoint at `{}`.\n\n\
Exposes {} method{}.",
path,
method_names.len(),
if method_names.len() == 1 { "" } else { "s" }
);
let mount_dispatch_arms: Vec<_> = partitioned
.static_mounts
.iter()
.map(|m| generate_static_mount_dispatch(m))
.chain(
partitioned
.slug_mounts
.iter()
.map(|m| generate_slug_mount_dispatch(m)),
)
.collect::<syn::Result<Vec<_>>>()?;
let mount_dispatch_arms_sync: Vec<_> = partitioned
.static_mounts
.iter()
.map(|m| generate_static_mount_dispatch_sync(m))
.chain(
partitioned
.slug_mounts
.iter()
.map(|m| generate_slug_mount_dispatch_sync(m)),
)
.collect::<syn::Result<Vec<_>>>()?;
let mount_method_names: Vec<_> = partitioned
.static_mounts
.iter()
.chain(partitioned.slug_mounts.iter())
.map(|m| generate_mount_method_names(m))
.collect::<syn::Result<Vec<_>>>()?;
let uses_context = partitioned.leaf.iter().any(|m| {
partition_context_params(&m.params)
.map(|(ctx, _)| ctx.is_some())
.unwrap_or(false)
});
let mount_dispatch_inner = if uses_context {
quote! {
async fn jsonrpc_mount_dispatch_inner(
&self,
method: &str,
args: ::server_less::serde_json::Value,
) -> ::std::result::Result<::server_less::serde_json::Value, String> {
let __ctx = ::server_less::Context::new();
self.jsonrpc_dispatch(__ctx, method, args).await.map_err(|(_, msg)| msg)
}
}
} else {
quote! {
async fn jsonrpc_mount_dispatch_inner(
&self,
method: &str,
args: ::server_less::serde_json::Value,
) -> ::std::result::Result<::server_less::serde_json::Value, String> {
self.jsonrpc_dispatch(method, args).await.map_err(|(_, msg)| msg)
}
}
};
let mount_dispatch_sync_inner = quote! {
fn jsonrpc_mount_dispatch_sync_inner(
&self,
method: &str,
args: ::server_less::serde_json::Value,
) -> ::std::result::Result<::server_less::serde_json::Value, String> {
match method {
#(#dispatch_arms_sync)*
#(#mount_dispatch_arms_sync)*
_ => Err(format!("Method not found: {}", method)),
}
}
};
let struct_name_snake = struct_name.to_string().to_lowercase();
let handler_name = format_ident!("__server_less_jsonrpc_handler_{}", struct_name_snake);
let (
dispatch_sig,
dispatch_call,
handle_sig,
handle_single_sig,
handle_single_call_batch,
handle_single_call,
handler_call,
ctx_creation,
) = if uses_context {
(
quote! {
async fn jsonrpc_dispatch(
&self,
__ctx: ::server_less::Context,
method: &str,
args: ::server_less::serde_json::Value,
) -> ::std::result::Result<::server_less::serde_json::Value, (i32, String)>
},
quote! { self.jsonrpc_dispatch(__ctx, method, params).await },
quote! {
pub async fn jsonrpc_handle_async(
&self,
__ctx: ::server_less::Context,
request: ::server_less::serde_json::Value,
) -> ::server_less::serde_json::Value
},
quote! {
async fn jsonrpc_handle_single(
&self,
__ctx: ::server_less::Context,
request: ::server_less::serde_json::Value,
) -> Option<::server_less::serde_json::Value>
},
quote! { self.jsonrpc_handle_single(__ctx.clone(), req.clone()).await },
quote! { self.jsonrpc_handle_single(__ctx, request).await },
quote! { state.jsonrpc_handle_async(__ctx, request).await },
quote! {},
)
} else {
(
quote! {
async fn jsonrpc_dispatch(
&self,
method: &str,
args: ::server_less::serde_json::Value,
) -> ::std::result::Result<::server_less::serde_json::Value, (i32, String)>
},
quote! { self.jsonrpc_dispatch(method, params).await },
quote! {
pub async fn jsonrpc_handle_async(
&self,
request: ::server_less::serde_json::Value,
) -> ::server_less::serde_json::Value
},
quote! {
async fn jsonrpc_handle_single(
&self,
request: ::server_less::serde_json::Value,
) -> Option<::server_less::serde_json::Value>
},
quote! { self.jsonrpc_handle_single(req.clone()).await },
quote! { self.jsonrpc_handle_single(request).await },
quote! { state.jsonrpc_handle_async(request).await },
quote! { let __ctx = ::server_less::Context::new(); },
)
};
let maybe_impl = if crate::is_protocol_impl_emitter(&impl_block, "jsonrpc") {
quote! { #impl_block }
} else {
quote! {}
};
Ok(quote! {
#maybe_impl
impl #impl_generics ::server_less::JsonRpcMount for #self_ty #where_clause {
fn jsonrpc_mount_methods() -> Vec<String> {
Self::jsonrpc_methods()
}
fn jsonrpc_mount_dispatch(
&self,
method: &str,
params: ::server_less::serde_json::Value,
) -> ::std::result::Result<::server_less::serde_json::Value, String> {
self.jsonrpc_mount_dispatch_sync_inner(method, params)
}
async fn jsonrpc_mount_dispatch_async(
&self,
method: &str,
params: ::server_less::serde_json::Value,
) -> ::std::result::Result<::server_less::serde_json::Value, String> {
self.jsonrpc_mount_dispatch_inner(method, params).await
}
}
impl #impl_generics #self_ty #where_clause {
#[doc = #jsonrpc_methods_doc]
pub fn jsonrpc_methods() -> Vec<String> {
let mut names: Vec<String> = Vec::new();
#(#method_name_stmts)*
#(#mount_method_names)*
names
}
#handle_sig {
#ctx_creation
if let Some(arr) = request.as_array() {
let mut responses = Vec::new();
for req in arr {
if let Some(resp) = #handle_single_call_batch {
responses.push(resp);
}
}
if responses.is_empty() {
::server_less::serde_json::Value::Null
} else {
::server_less::serde_json::Value::Array(responses)
}
} else {
#handle_single_call
.unwrap_or(::server_less::serde_json::Value::Null)
}
}
#handle_single_sig {
let id = request.get("id").cloned();
let is_notification = id.is_none();
let version = request.get("jsonrpc").and_then(|v| v.as_str());
if version != Some("2.0") {
if is_notification {
return None;
}
return Some(Self::jsonrpc_error(-32600, "Invalid Request: missing jsonrpc 2.0", id));
}
let method = match request.get("method").and_then(|v| v.as_str()) {
Some(m) => m,
None => {
if is_notification {
return None;
}
return Some(Self::jsonrpc_error(-32600, "Invalid Request: missing method", id));
}
};
let params = request.get("params")
.cloned()
.unwrap_or(::server_less::serde_json::json!({}));
let result = #dispatch_call;
if is_notification {
return None;
}
Some(match result {
Ok(value) => {
::server_less::serde_json::json!({
"jsonrpc": "2.0",
"result": value,
"id": id
})
}
Err((code, err)) => Self::jsonrpc_error(code, &err, id),
})
}
fn jsonrpc_error(
code: i32,
message: &str,
id: Option<::server_less::serde_json::Value>,
) -> ::server_less::serde_json::Value {
::server_less::serde_json::json!({
"jsonrpc": "2.0",
"error": {
"code": code,
"message": message
},
"id": id
})
}
#dispatch_sig {
match method {
#(#dispatch_arms_async)*
#(#mount_dispatch_arms)*
_ => Err((-32601i32, format!("Method not found: {}", method))),
}
}
#mount_dispatch_sync_inner
#mount_dispatch_inner
#[doc = #jsonrpc_router_doc]
pub fn jsonrpc_router(self) -> ::server_less::axum::Router
where
Self: Clone + Send + Sync + 'static,
{
let state = ::std::sync::Arc::new(self);
::server_less::axum::Router::new()
.route(#path, ::server_less::axum::routing::post(#handler_name))
.with_state(state)
}
pub fn jsonrpc_openapi_paths() -> ::std::vec::Vec<::server_less::OpenApiPath> {
let methods: Vec<&str> = vec![#(#method_names),*];
let methods_desc = methods.join(", ");
vec![
::server_less::OpenApiPath {
path: #path.to_string(),
method: "post".to_string(),
operation: ::server_less::OpenApiOperation {
summary: Some(format!("JSON-RPC 2.0 endpoint (methods: {})", methods_desc)),
description: None,
operation_id: Some("jsonrpc".to_string()),
tags: vec!["jsonrpc".to_string()],
deprecated: false,
parameters: vec![],
request_body: Some(::server_less::serde_json::json!({
"required": true,
"content": {
"application/json": {
"schema": {
"type": "object",
"required": ["jsonrpc", "method"],
"properties": {
"jsonrpc": {
"type": "string",
"enum": ["2.0"]
},
"method": {
"type": "string",
"enum": methods
},
"params": {
"type": "object"
},
"id": {
"oneOf": [
{"type": "string"},
{"type": "integer"},
{"type": "null"}
]
}
}
}
}
}
})),
responses: {
let mut r = ::server_less::serde_json::Map::new();
r.insert("200".to_string(), ::server_less::serde_json::json!({
"description": "JSON-RPC response",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"jsonrpc": {"type": "string"},
"result": {},
"error": {
"type": "object",
"properties": {
"code": {"type": "integer"},
"message": {"type": "string"}
}
},
"id": {}
}
}
}
}
}));
r.insert("204".to_string(), ::server_less::serde_json::json!({
"description": "Notification (no response)"
}));
r
},
extra: ::server_less::serde_json::Map::new(),
},
}
]
}
}
async fn #handler_name(
::server_less::axum::extract::State(state): ::server_less::axum::extract::State<::std::sync::Arc<#self_ty>>,
__context_headers: ::server_less::axum::http::HeaderMap,
::server_less::axum::Json(request): ::server_less::axum::Json<::server_less::serde_json::Value>,
) -> impl ::server_less::axum::response::IntoResponse {
use ::server_less::axum::response::IntoResponse;
let mut __ctx = ::server_less::Context::new();
for (name, value) in __context_headers.iter() {
if let Ok(value_str) = value.to_str() {
__ctx.set(name.as_str(), value_str);
}
}
if let Some(request_id) = __context_headers.get("x-request-id")
.and_then(|v| v.to_str().ok())
{
__ctx.set_request_id(request_id);
}
let response = #handler_call;
if response.is_null() {
::server_less::axum::http::StatusCode::NO_CONTENT.into_response()
} else {
::server_less::axum::Json(response).into_response()
}
}
})
}
fn generate_jsonrpc_json_response(method: &MethodInfo) -> TokenStream2 {
let ret = &method.return_info;
if ret.is_unit {
quote! {
Ok(::server_less::serde_json::json!({"success": true}))
}
} else if ret.is_stream {
quote! {
{
use ::server_less::futures::StreamExt;
let collected: Vec<_> = result.collect().await;
Ok(::server_less::serde_json::to_value(collected)
.map_err(|e| (-32603i32, format!("Serialization error: {}", e)))
.map_err(|e| e)?)
}
}
} else if ret.is_iterator {
quote! {
{
let __collected: Vec<_> = result.collect();
::server_less::serde_json::to_value(&__collected)
.map(Ok)
.map_err(|e| Err((-32603i32, format!("Serialization error: {}", e))))
.unwrap_or_else(|e| e)
}
}
} else if ret.is_result {
quote! {
match result {
Ok(value) => ::server_less::serde_json::to_value(value)
.map(Ok)
.map_err(|e| Err((-32603i32, format!("Serialization error: {}", e))))
.unwrap_or_else(|e| e),
Err(err) => {
let __code = ::server_less::IntoErrorCode::jsonrpc_code(&err);
let __msg = ::server_less::IntoErrorCode::message(&err);
Err((__code, __msg))
}
}
}
} else if ret.is_option {
quote! {
match result {
Some(value) => ::server_less::serde_json::to_value(value)
.map(Ok)
.map_err(|e| Err((-32603i32, format!("Serialization error: {}", e))))
.unwrap_or_else(|e| e),
None => Ok(::server_less::serde_json::Value::Null),
}
}
} else {
quote! {
::server_less::serde_json::to_value(result)
.map(Ok)
.map_err(|e| Err((-32603i32, format!("Serialization error: {}", e))))
.unwrap_or_else(|e| e)
}
}
}
fn generate_jsonrpc_param_extraction(param: &server_less_parse::ParamInfo) -> TokenStream2 {
let name = ¶m.name;
let name_str = param.name_str();
let ty = ¶m.ty;
if param.is_optional {
let inner_ty: syn::Type = if let syn::Type::Path(ref type_path) = *ty {
if let Some(seg) = type_path.path.segments.last() {
if seg.ident == "Option" {
if let syn::PathArguments::AngleBracketed(ref args) = seg.arguments {
if let Some(syn::GenericArgument::Type(inner)) = args.args.first() {
inner.clone()
} else {
ty.clone()
}
} else {
ty.clone()
}
} else {
ty.clone()
}
} else {
ty.clone()
}
} else {
ty.clone()
};
let inner_ty_str = quote::quote!(#inner_ty).to_string().replace(" ", "");
quote! {
let #name: #ty = match args.get(#name_str) {
None => None,
Some(__v) if __v.is_null() => None,
Some(__v) => match ::server_less::serde_json::from_value(__v.clone()) {
Ok(__val) => Some(__val),
Err(__e) => return Err((-32602i32, format!(
"Optional parameter '{}' has invalid type (expected {}): {}", #name_str, #inner_ty_str, __e
))),
},
};
}
} else {
let ty_str = quote::quote!(#ty).to_string().replace(" ", "");
quote! {
let __val = args.get(#name_str)
.ok_or_else(|| (-32602i32, format!("Missing required parameter: {} (expected {})", #name_str, #ty_str)))?
.clone();
let #name: #ty = ::server_less::serde_json::from_value::<#ty>(__val)
.map_err(|e| (-32602i32, format!("Invalid parameter {} (expected {}): {}", #name_str, #ty_str, e)))?;
}
}
}
fn generate_sync_dispatch_arm(
method: &MethodInfo,
) -> syn::Result<TokenStream2> {
let method_name_str = method.wire_name_or(|n| n);
let (context_param, regular_params) = partition_context_params(&method.params)?;
if context_param.is_none() {
return Ok(server_less_rpc::generate_dispatch_arm(
method,
None,
AsyncHandling::Error,
));
}
let param_extractions = server_less_rpc::generate_param_extractions_for(®ular_params);
let unknown_warn =
server_less_rpc::generate_unknown_param_warning(&method_name_str, ®ular_params);
let mut arg_exprs = Vec::new();
for param in &method.params {
if crate::context::should_inject_context(¶m.ty, &method.params) {
arg_exprs.push(quote! { ::server_less::Context::new() });
} else {
let name = ¶m.name;
arg_exprs.push(quote! { #name });
}
}
let call =
server_less_rpc::generate_method_call_with_args(method, arg_exprs, AsyncHandling::Error);
let response = server_less_rpc::generate_json_response(method);
Ok(quote! {
#method_name_str => {
#unknown_warn
#(#param_extractions)*
#call
#response
}
})
}
fn generate_dispatch_arm(method: &MethodInfo) -> syn::Result<TokenStream2> {
let method_name_str = method.wire_name_or(|n| n);
let (context_param, regular_params) = partition_context_params(&method.params)?;
let response = generate_jsonrpc_json_response(method);
if context_param.is_none() {
let param_extractions: Vec<_> = method
.params
.iter()
.map(generate_jsonrpc_param_extraction)
.collect();
let all_param_refs: Vec<&server_less_parse::ParamInfo> = method.params.iter().collect();
let unknown_warn =
server_less_rpc::generate_unknown_param_warning(&method_name_str, &all_param_refs);
let call = server_less_rpc::generate_method_call(method, AsyncHandling::Await);
return Ok(quote! {
#method_name_str => {
#unknown_warn
#(#param_extractions)*
#call
#response
}
});
}
let param_extractions: Vec<_> = regular_params
.iter()
.map(|p| generate_jsonrpc_param_extraction(p))
.collect();
let unknown_warn =
server_less_rpc::generate_unknown_param_warning(&method_name_str, ®ular_params);
let mut arg_exprs = Vec::new();
for param in &method.params {
if crate::context::should_inject_context(¶m.ty, &method.params) {
arg_exprs.push(quote! { __ctx.clone() });
} else {
let name = ¶m.name;
arg_exprs.push(quote! { #name });
}
}
let call =
server_less_rpc::generate_method_call_with_args(method, arg_exprs, AsyncHandling::Await);
Ok(quote! {
#method_name_str => {
#unknown_warn
#(#param_extractions)*
#call
#response
}
})
}
fn generate_mount_method_names(method: &MethodInfo) -> syn::Result<TokenStream2> {
let mount_name = method.wire_name_or(|n| n);
let mount_prefix = format!("{}.", mount_name);
let inner_ty = method.return_info.reference_inner.as_ref().ok_or_else(|| {
syn::Error::new_spanned(
&method.method.sig,
"BUG: mount method must have a reference return type (&T)",
)
})?;
Ok(quote! {
{
let child_methods = <#inner_ty as ::server_less::JsonRpcMount>::jsonrpc_mount_methods();
for child_name in child_methods {
let prefixed = format!("{}{}", #mount_prefix, child_name);
names.push(prefixed);
}
}
})
}
fn generate_static_mount_dispatch(method: &MethodInfo) -> syn::Result<TokenStream2> {
let mount_name = method.wire_name_or(|n| n);
let mount_prefix = format!("{}.", mount_name);
let method_name = &method.name;
let inner_ty = method.return_info.reference_inner.as_ref().ok_or_else(|| {
syn::Error::new_spanned(
&method.method.sig,
"BUG: mount method must have a reference return type (&T)",
)
})?;
Ok(quote! {
__method if __method.starts_with(#mount_prefix) => {
let __stripped = &__method[#mount_prefix.len()..];
let __delegate = self.#method_name();
<#inner_ty as ::server_less::JsonRpcMount>::jsonrpc_mount_dispatch_async(__delegate, __stripped, args).await
.map_err(|msg| (-32603i32, msg))
}
})
}
fn generate_static_mount_dispatch_sync(method: &MethodInfo) -> syn::Result<TokenStream2> {
let mount_name = method.wire_name_or(|n| n);
let mount_prefix = format!("{}.", mount_name);
let method_name = &method.name;
let inner_ty = method.return_info.reference_inner.as_ref().ok_or_else(|| {
syn::Error::new_spanned(
&method.method.sig,
"BUG: mount method must have a reference return type (&T)",
)
})?;
Ok(quote! {
__method if __method.starts_with(#mount_prefix) => {
let __stripped = &__method[#mount_prefix.len()..];
let __delegate = self.#method_name();
<#inner_ty as ::server_less::JsonRpcMount>::jsonrpc_mount_dispatch(__delegate, __stripped, args)
}
})
}
fn generate_slug_mount_dispatch(method: &MethodInfo) -> syn::Result<TokenStream2> {
let mount_name = method.wire_name_or(|n| n);
let mount_prefix = format!("{}.", mount_name);
let method_name = &method.name;
let inner_ty = method.return_info.reference_inner.as_ref().ok_or_else(|| {
syn::Error::new_spanned(
&method.method.sig,
"BUG: mount method must have a reference return type (&T)",
)
})?;
let slug_extractions: Vec<_> = method
.params
.iter()
.map(generate_jsonrpc_param_extraction)
.collect();
let slug_names: Vec<_> = method.params.iter().map(|p| &p.name).collect();
Ok(quote! {
__method if __method.starts_with(#mount_prefix) => {
let __stripped = &__method[#mount_prefix.len()..];
#(#slug_extractions)*
let __delegate = self.#method_name(#(#slug_names),*);
<#inner_ty as ::server_less::JsonRpcMount>::jsonrpc_mount_dispatch_async(__delegate, __stripped, args).await
.map_err(|msg| (-32603i32, msg))
}
})
}
fn generate_slug_mount_dispatch_sync(method: &MethodInfo) -> syn::Result<TokenStream2> {
let mount_name = method.wire_name_or(|n| n);
let mount_prefix = format!("{}.", mount_name);
let method_name = &method.name;
let inner_ty = method.return_info.reference_inner.as_ref().ok_or_else(|| {
syn::Error::new_spanned(
&method.method.sig,
"BUG: mount method must have a reference return type (&T)",
)
})?;
let slug_extractions: Vec<_> = method
.params
.iter()
.map(server_less_rpc::generate_param_extraction)
.collect();
let slug_names: Vec<_> = method.params.iter().map(|p| &p.name).collect();
Ok(quote! {
__method if __method.starts_with(#mount_prefix) => {
let __stripped = &__method[#mount_prefix.len()..];
#(#slug_extractions)*
let __delegate = self.#method_name(#(#slug_names),*);
<#inner_ty as ::server_less::JsonRpcMount>::jsonrpc_mount_dispatch(__delegate, __stripped, args)
}
})
}