1extern crate self as jsonrpsee_ts;
62
63use std::fmt::{Display, Formatter};
64
65pub use jsonrpsee_ts_macros::export_schema;
69
70#[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
84pub fn type_name<T: ::ts_rs::TS>(cfg: &::ts_rs::Config) -> String {
86 T::name(cfg)
87}
88
89pub fn void_type() -> String {
91 "void".to_string()
92}
93
94#[derive(Clone, Debug, Eq, PartialEq)]
96pub struct Param {
97 ident: String,
98 optional: bool,
99 ts_ident: String,
100}
101
102impl Param {
103 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 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#[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 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 pub fn with_param_kind(mut self, param_kind: ParamKind) -> Self {
182 self.param_kind = param_kind;
183 self
184 }
185
186 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#[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 pub fn new() -> Self {
244 Self {
245 requests: vec![],
246 subscriptions: vec![],
247 }
248 }
249
250 pub fn request(mut self, method: Method) -> Self {
252 self.requests.push(method);
253 self
254 }
255
256 pub fn subscription(mut self, subscription: Method) -> Self {
258 self.subscriptions.push(subscription);
259 self
260 }
261
262 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 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 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}