Skip to main content

jsonrpsee_ts/
lib.rs

1//! Generate an [`rpckit`](https://rpckit.dev) schema from a `jsonrpsee` RPC trait.
2//!
3//! Add [`export_schema`] next to `#[rpc(...)]` and the macro generates a
4//! `<Trait>Schema` type that:
5//!
6//! - builds an in-memory schema with `schema(&Config)`
7//! - implements [`ts_rs::TS`]
8//! - can be exported with `export`, `export_all`, or `export_to_string`
9//!
10//! The generated schema reuses the original RPC trait metadata:
11//! `namespace`, `method(name)`, `subscription(name, item)`, `param_kind`,
12//! and `#[argument(rename = "...")]`.
13//!
14//! # Example
15//!
16//! ```ignore
17//! use jsonrpsee::core::SubscriptionResult;
18//! use jsonrpsee::proc_macros::rpc;
19//! use jsonrpsee::types::ErrorObjectOwned;
20//! use jsonrpsee_ts::export_schema;
21//! use ts_rs::TS;
22//!
23//! #[derive(TS)]
24//! #[ts(export)]
25//! struct Hash {
26//!     value: String,
27//! }
28//!
29//! #[derive(TS)]
30//! #[ts(export)]
31//! struct StorageKey {
32//!     bytes: String,
33//! }
34//!
35//! #[export_schema]
36//! #[rpc(server, client, namespace = "state")]
37//! trait StateRpc<HashTy, StorageKeyTy> {
38//!     #[method(name = "getKeys")]
39//!     async fn storage_keys(
40//!         &self,
41//!         storage_key: StorageKeyTy,
42//!         hash: Option<HashTy>,
43//!     ) -> Result<Vec<StorageKeyTy>, ErrorObjectOwned>;
44//!
45//!     #[subscription(name = "subscribeStorage", item = Vec<HashTy>)]
46//!     async fn subscribe_storage(
47//!         &self,
48//!         keys: Option<Vec<StorageKeyTy>>,
49//!     ) -> SubscriptionResult;
50//! }
51//!
52//! let cfg = ts_rs::Config::default();
53//! let schema = StateRpcSchema::<Hash, StorageKey>::schema(&cfg);
54//! println!("{}", schema.render_type_alias("StateRpcSchema"));
55//! ```
56//!
57//! `Option<T>` parameters become optional TypeScript parameters. `Result<T, E>`
58//! and `RpcResult<T>` use the success type as the generated `return` type.
59//! Referenced `ts-rs` types are imported and exported automatically when using
60//! `export_all`.
61extern crate self as jsonrpsee_ts;
62
63use std::fmt::{Display, Formatter};
64
65/// Generate a `<Trait>Schema` type for a `jsonrpsee` RPC trait.
66///
67/// See the crate-level documentation for a complete example.
68pub use jsonrpsee_ts_macros::export_schema;
69
70/// Parameter encoding mode mirroring `jsonrpsee`'s `param_kind`.
71#[derive(Clone, Copy, Debug, Eq, PartialEq)]
72pub enum ParamKind {
73    Array,
74    Map,
75}
76
77#[macro_export]
78macro_rules! ts_ident {
79    ($rs_ident:ty) => {
80        <$rs_ident as ::ts_rs::TS>::ident(&Default::default())
81    };
82}
83
84/// Return the TypeScript name for a `ts-rs` type.
85pub fn type_name<T: ::ts_rs::TS>(cfg: &::ts_rs::Config) -> String {
86    T::name(cfg)
87}
88
89/// Return the TypeScript `void` type used for methods without a return value.
90pub fn void_type() -> String {
91    "void".to_string()
92}
93
94/// A single rpckit parameter entry.
95#[derive(Clone, Debug, Eq, PartialEq)]
96pub struct Param {
97    ident: String,
98    optional: bool,
99    ts_ident: String,
100}
101
102impl Param {
103    /// Create a required parameter.
104    pub fn new(ident: &str, ts_ident: &str) -> Self {
105        Self {
106            ident: ident.to_string(),
107            optional: false,
108            ts_ident: ts_ident.to_string(),
109        }
110    }
111
112    /// Mark this parameter as optional.
113    pub fn optional(mut self) -> Self {
114        self.optional = true;
115        self
116    }
117
118    fn render_array(&self) -> String {
119        let mut rendered = self.ident.clone();
120        if self.optional {
121            rendered.push('?');
122        }
123        rendered.push_str(": ");
124        rendered.push_str(&self.ts_ident);
125        rendered
126    }
127
128    fn render_map(&self) -> String {
129        let mut rendered = ts_property_name(&self.ident);
130        if self.optional {
131            rendered.push('?');
132        }
133        rendered.push_str(": ");
134        rendered.push_str(&self.ts_ident);
135        rendered
136    }
137}
138
139fn ts_property_name(name: &str) -> String {
140    if is_ts_identifier(name) {
141        name.to_string()
142    } else {
143        format!("'{name}'")
144    }
145}
146
147fn is_ts_identifier(name: &str) -> bool {
148    let mut chars = name.chars();
149    let Some(first) = chars.next() else {
150        return false;
151    };
152
153    if !(first == '_' || first == '$' || first.is_ascii_alphabetic()) {
154        return false;
155    }
156
157    chars.all(|ch| ch == '_' || ch == '$' || ch.is_ascii_alphanumeric())
158}
159
160/// A single rpckit schema entry.
161#[derive(Clone, Debug, Eq, PartialEq)]
162pub struct Method {
163    name: String,
164    params: Vec<Param>,
165    param_kind: ParamKind,
166    return_ts_ident: String,
167}
168
169impl Method {
170    /// Create a new schema entry with array-style params by default.
171    pub fn new(name: &str, return_ts_ident: &str) -> Self {
172        Self {
173            name: name.to_string(),
174            params: vec![],
175            param_kind: ParamKind::Array,
176            return_ts_ident: return_ts_ident.to_string(),
177        }
178    }
179
180    /// Set the parameter encoding mode.
181    pub fn with_param_kind(mut self, param_kind: ParamKind) -> Self {
182        self.param_kind = param_kind;
183        self
184    }
185
186    /// Append one parameter.
187    pub fn param(mut self, param: Param) -> Self {
188        self.params.push(param);
189        self
190    }
191
192    fn render_params(&self) -> String {
193        match self.param_kind {
194            ParamKind::Array => {
195                let params = self
196                    .params
197                    .iter()
198                    .map(Param::render_array)
199                    .collect::<Vec<_>>()
200                    .join(", ");
201                format!("[{params}]")
202            }
203            ParamKind::Map => {
204                let params = self
205                    .params
206                    .iter()
207                    .map(Param::render_map)
208                    .collect::<Vec<_>>()
209                    .join("; ");
210                format!("{{ {params} }}")
211            }
212        }
213    }
214}
215
216impl Display for Method {
217    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
218        write!(
219            f,
220            "{{ method: '{}'; params: {}; return: {} }}",
221            self.name,
222            self.render_params(),
223            self.return_ts_ident
224        )
225    }
226}
227
228/// In-memory rpckit schema builder used by the generated macro output.
229#[derive(Clone, Debug, Eq, PartialEq)]
230pub struct Schema {
231    requests: Vec<Method>,
232    subscriptions: Vec<Method>,
233}
234
235impl Display for Schema {
236    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
237        write!(f, "{}", self.render_inline())
238    }
239}
240
241impl Schema {
242    /// Create an empty schema.
243    pub fn new() -> Self {
244        Self {
245            requests: vec![],
246            subscriptions: vec![],
247        }
248    }
249
250    /// Append a request entry.
251    pub fn request(mut self, method: Method) -> Self {
252        self.requests.push(method);
253        self
254    }
255
256    /// Append a subscription entry.
257    pub fn subscription(mut self, subscription: Method) -> Self {
258        self.subscriptions.push(subscription);
259        self
260    }
261
262    /// Merge two schemas together.
263    pub fn merge(mut self, mut other: Self) -> Self {
264        self.requests.append(&mut other.requests);
265        self.subscriptions.append(&mut other.subscriptions);
266        self
267    }
268
269    /// Render the schema body as an inline TypeScript object.
270    pub fn render_inline(&self) -> String {
271        let requests = self.render_entries(&self.requests, 2);
272        let subscriptions = self.render_entries(&self.subscriptions, 2);
273
274        format!("{{\n  requests: {requests};\n  subscriptions: {subscriptions};\n}}")
275    }
276
277    /// Render `type <ident> = ...`.
278    pub fn render_type_alias(&self, ident: &str) -> String {
279        format!("type {ident} = {};", self.render_inline())
280    }
281
282    fn render_entries(&self, entries: &[Method], indent: usize) -> String {
283        if entries.is_empty() {
284            return "[]".to_string();
285        }
286
287        let padding = " ".repeat(indent);
288        let inner_padding = " ".repeat(indent + 2);
289        let rendered_entries = entries
290            .iter()
291            .map(|entry| format!("{inner_padding}{entry},"))
292            .collect::<Vec<_>>()
293            .join("\n");
294
295        format!("[\n{rendered_entries}\n{padding}]")
296    }
297}
298
299impl Default for Schema {
300    fn default() -> Self {
301        Self::new()
302    }
303}
304
305#[doc(hidden)]
306#[macro_export]
307macro_rules! __jsonrpsee_ts_return_type {
308    ($cfg:ident, void) => {
309        ::jsonrpsee_ts::void_type()
310    };
311    ($cfg:ident, ty($ty:ty)) => {
312        ::jsonrpsee_ts::type_name::<$ty>($cfg)
313    };
314}
315
316#[doc(hidden)]
317#[macro_export]
318macro_rules! __jsonrpsee_ts_param {
319    ($cfg:ident, $name:literal, $ty:ty, required) => {
320        ::jsonrpsee_ts::Param::new($name, &::jsonrpsee_ts::type_name::<$ty>($cfg))
321    };
322    ($cfg:ident, $name:literal, $ty:ty, optional) => {
323        ::jsonrpsee_ts::Param::new($name, &::jsonrpsee_ts::type_name::<$ty>($cfg)).optional()
324    };
325}
326
327#[doc(hidden)]
328#[macro_export]
329macro_rules! __jsonrpsee_ts_method {
330    (
331        cfg = $cfg:ident,
332        name = $name:literal,
333        param_kind = $param_kind:ident,
334        params = [$(($param_name:literal, $param_ty:ty, $optional:ident)),* $(,)?],
335        return = $($return:tt)+
336    ) => {{
337        ::jsonrpsee_ts::Method::new($name, &::jsonrpsee_ts::__jsonrpsee_ts_return_type!($cfg, $($return)+))
338            .with_param_kind(::jsonrpsee_ts::ParamKind::$param_kind)
339            $(.param(::jsonrpsee_ts::__jsonrpsee_ts_param!($cfg, $param_name, $param_ty, $optional)))*
340    }};
341}
342
343#[doc(hidden)]
344#[macro_export]
345macro_rules! __jsonrpsee_ts_schema_impl {
346    (
347        schema = $schema_ident:ident,
348        builder = $builder_fn:ident,
349        builder_generics = [$($builder_generics:tt)*],
350        struct_generics = [$($struct_generics:tt)*],
351        marker = [$($marker:tt)*],
352        impl_generics = [$($impl_generics:tt)*],
353        type_generics = [$($type_generics:tt)*],
354        where_clause = [$($where_clause:tt)*],
355        used_types = [$($used_ty:ty),* $(,)?]
356    ) => {
357        #[doc(hidden)]
358        pub struct $schema_ident $($struct_generics)* $($marker)* $($where_clause)*;
359
360        impl $($impl_generics)* $schema_ident $($type_generics)* $($where_clause)* {
361            pub fn schema(cfg: &::ts_rs::Config) -> ::jsonrpsee_ts::Schema {
362                $builder_fn $($builder_generics)*(cfg)
363            }
364
365            pub fn export(cfg: &::ts_rs::Config) -> ::std::result::Result<(), ::ts_rs::ExportError>
366            where
367                Self: 'static,
368            {
369                <Self as ::ts_rs::TS>::export(cfg)
370            }
371
372            pub fn export_all(cfg: &::ts_rs::Config) -> ::std::result::Result<(), ::ts_rs::ExportError>
373            where
374                Self: 'static,
375            {
376                <Self as ::ts_rs::TS>::export_all(cfg)
377            }
378
379            pub fn export_to_string(
380                cfg: &::ts_rs::Config,
381            ) -> ::std::result::Result<::std::string::String, ::ts_rs::ExportError>
382            where
383                Self: 'static,
384            {
385                <Self as ::ts_rs::TS>::export_to_string(cfg)
386            }
387        }
388
389        impl $($impl_generics)* ::ts_rs::TS for $schema_ident $($type_generics)* $($where_clause)* {
390            type WithoutGenerics = Self;
391            type OptionInnerType = Self;
392
393            fn ident(_: &::ts_rs::Config) -> ::std::string::String {
394                stringify!($schema_ident).to_owned()
395            }
396
397            fn name(cfg: &::ts_rs::Config) -> ::std::string::String {
398                <Self as ::ts_rs::TS>::ident(cfg)
399            }
400
401            fn decl(cfg: &::ts_rs::Config) -> ::std::string::String {
402                $builder_fn $($builder_generics)*(cfg).render_type_alias(&<Self as ::ts_rs::TS>::ident(cfg))
403            }
404
405            fn decl_concrete(cfg: &::ts_rs::Config) -> ::std::string::String {
406                <Self as ::ts_rs::TS>::decl(cfg)
407            }
408
409            fn inline(cfg: &::ts_rs::Config) -> ::std::string::String {
410                $builder_fn $($builder_generics)*(cfg).render_inline()
411            }
412
413            fn visit_dependencies(v: &mut impl ::ts_rs::TypeVisitor)
414            where
415                Self: 'static,
416            {
417                $(
418                    v.visit::<$used_ty>();
419                    <$used_ty as ::ts_rs::TS>::visit_generics(v);
420                )*
421            }
422
423            fn output_path() -> ::std::option::Option<::std::path::PathBuf> {
424                ::std::option::Option::Some(::std::path::PathBuf::from(format!(
425                    "{}.ts",
426                    stringify!($schema_ident),
427                )))
428            }
429        }
430    };
431}
432
433#[cfg(test)]
434mod tests {
435    use super::{Method, Param, ParamKind, Schema};
436
437    #[test]
438    fn renders_array_and_map_params() {
439        let schema = Schema::new()
440            .request(
441                Method::new("state_getKeys", "Array<string>")
442                    .param(Param::new("storage_key", "string"))
443                    .param(Param::new("hash", "string").optional()),
444            )
445            .request(
446                Method::new("state_query", "number")
447                    .with_param_kind(ParamKind::Map)
448                    .param(Param::new("type", "number"))
449                    .param(Param::new("include-proofs", "boolean").optional()),
450            );
451
452        assert_eq!(
453            schema.render_inline(),
454            "{\n  requests: [\n    { method: 'state_getKeys'; params: [storage_key: string, hash?: string]; return: Array<string> },\n    { method: 'state_query'; params: { type: number; 'include-proofs'?: boolean }; return: number },\n  ];\n  subscriptions: [];\n}"
455        );
456    }
457}