From d7f749e6dfced6e762e2405f8b17dc37370c35b6 Mon Sep 17 00:00:00 2001 From: Javad Date: Sat, 3 May 2025 15:03:00 +0330 Subject: [PATCH 1/7] fix: add normalization dot for method name --- src/components/client.ts | 9 +++++++-- src/components/helper.ts | 11 +++++++++++ 2 files changed, 18 insertions(+), 2 deletions(-) create mode 100644 src/components/helper.ts diff --git a/src/components/client.ts b/src/components/client.ts index debe5380..722f4466 100644 --- a/src/components/client.ts +++ b/src/components/client.ts @@ -7,6 +7,7 @@ import { promisify } from 'util'; const writeFile = promisify(fs.writeFile); import TOML from '@iarna/toml'; import { template } from 'lodash'; +import { normalizeMethodName } from './helper'; const tsTemplate = template(` // Code generated by @open-rpc/generator DO NOT EDIT. @@ -214,13 +215,17 @@ export class <%= className %> { * <%= method.summary %> */ // tslint:disable-next-line:max-line-length - public <%= method.name %>: <%= methodTypings.getTypingNames("typescript", method).method %> = (...params) => { + public <%= normalizeMethodName(method.name) %>: <%= methodTypings.getTypingNames("typescript", method).method %> = (...params) => { return this.request("<%= method.name %>", params); } <% }); %> } export default <%= className %>; -`); +`, { + imports: { + normalizeMethodName + } +}); const rsTemplate = template(` #[macro_use] diff --git a/src/components/helper.ts b/src/components/helper.ts new file mode 100644 index 00000000..6ab4409b --- /dev/null +++ b/src/components/helper.ts @@ -0,0 +1,11 @@ +export function normalizeMethodName(name: string): string { + // backward compatibility only change on dot inside in name. + if (!name.includes('.')) return name; + + const parts = name.split(/[\._]/); + return parts + .map((part, index) => + index === 0 ? part : part.charAt(0).toUpperCase() + part.slice(1) + ) + .join(''); +} From 9f8abef6208726469b4907d6fce143df8a30b5c0 Mon Sep 17 00:00:00 2001 From: Javad Date: Sat, 3 May 2025 15:21:50 +0330 Subject: [PATCH 2/7] fix: linter issues --- src/components/client.ts | 13 ++++++++----- src/components/helper.ts | 6 ++---- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/src/components/client.ts b/src/components/client.ts index 722f4466..46907385 100644 --- a/src/components/client.ts +++ b/src/components/client.ts @@ -9,7 +9,8 @@ import TOML from '@iarna/toml'; import { template } from 'lodash'; import { normalizeMethodName } from './helper'; -const tsTemplate = template(` +const tsTemplate = template( + ` // Code generated by @open-rpc/generator DO NOT EDIT. import { RequestManager, @@ -221,11 +222,13 @@ export class <%= className %> { <% }); %> } export default <%= className %>; -`, { - imports: { - normalizeMethodName +`, + { + imports: { + normalizeMethodName, + }, } -}); +); const rsTemplate = template(` #[macro_use] diff --git a/src/components/helper.ts b/src/components/helper.ts index 6ab4409b..6f241833 100644 --- a/src/components/helper.ts +++ b/src/components/helper.ts @@ -2,10 +2,8 @@ export function normalizeMethodName(name: string): string { // backward compatibility only change on dot inside in name. if (!name.includes('.')) return name; - const parts = name.split(/[\._]/); + const parts = name.split(/[._]/); return parts - .map((part, index) => - index === 0 ? part : part.charAt(0).toUpperCase() + part.slice(1) - ) + .map((part, index) => (index === 0 ? part : part.charAt(0).toUpperCase() + part.slice(1))) .join(''); } From 86d5c1888e28fdd1065d573dd157a36a743172be Mon Sep 17 00:00:00 2001 From: Javad Date: Sat, 3 May 2025 15:29:35 +0330 Subject: [PATCH 3/7] chore: add unit test for nomalized method name helper --- src/components/helper.test.ts | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 src/components/helper.test.ts diff --git a/src/components/helper.test.ts b/src/components/helper.test.ts new file mode 100644 index 00000000..0bfd1e91 --- /dev/null +++ b/src/components/helper.test.ts @@ -0,0 +1,32 @@ +import { normalizeMethodName } from './helper'; + +describe('normalizeMethodName', () => { + it('returns the original name if it does not contain a dot', () => { + expect(normalizeMethodName('hello')).toBe('hello'); + expect(normalizeMethodName('foo_bar')).toBe('foo_bar'); + }); + + it('splits and capitalizes correctly with dot', () => { + expect(normalizeMethodName('foo.bar')).toBe('fooBar'); + expect(normalizeMethodName('foo.bar.baz')).toBe('fooBarBaz'); + }); + + it('handles both dots and underscores', () => { + expect(normalizeMethodName('foo_bar.baz_qux')).toBe('fooBarBazQux'); + }); + + it('capitalizes only subsequent parts', () => { + expect(normalizeMethodName('hello.world')).toBe('helloWorld'); + expect(normalizeMethodName('one.two.three')).toBe('oneTwoThree'); + }); + + it('handles leading/trailing separators gracefully', () => { + expect(normalizeMethodName('.foo.bar')).toBe('FooBar'); + expect(normalizeMethodName('foo.bar.')).toBe('fooBar'); + expect(normalizeMethodName('..foo..bar..')).toBe('FooBar'); + }); + + it('handles empty string', () => { + expect(normalizeMethodName('')).toBe(''); + }); +}); From 4979f4196380c47be537d3274ab996ef77accdd5 Mon Sep 17 00:00:00 2001 From: Javad Date: Sat, 3 May 2025 15:34:07 +0330 Subject: [PATCH 4/7] test: Refactor normalizeMethodName tests Removed redundant tests. --- src/components/helper.test.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/components/helper.test.ts b/src/components/helper.test.ts index 0bfd1e91..aea0f8e8 100644 --- a/src/components/helper.test.ts +++ b/src/components/helper.test.ts @@ -1,3 +1,4 @@ +import { describe, it, expect } from '@jest/globals'; import { normalizeMethodName } from './helper'; describe('normalizeMethodName', () => { @@ -11,12 +12,7 @@ describe('normalizeMethodName', () => { expect(normalizeMethodName('foo.bar.baz')).toBe('fooBarBaz'); }); - it('handles both dots and underscores', () => { - expect(normalizeMethodName('foo_bar.baz_qux')).toBe('fooBarBazQux'); - }); - it('capitalizes only subsequent parts', () => { - expect(normalizeMethodName('hello.world')).toBe('helloWorld'); expect(normalizeMethodName('one.two.three')).toBe('oneTwoThree'); }); From a75c28b163014bc83b4aa709fab018235d1c0b04 Mon Sep 17 00:00:00 2001 From: Mostafa Date: Wed, 1 Oct 2025 17:05:05 +0800 Subject: [PATCH 5/7] support Rust --- src/components/client.ts | 55 ++++++++++++++++---- src/components/docs.ts | 4 +- src/components/helper.test.ts | 98 +++++++++++++++++++++++++++++++---- src/components/helper.ts | 36 ++++++++++++- src/index.test.ts | 1 - src/index.ts | 4 +- 6 files changed, 169 insertions(+), 29 deletions(-) diff --git a/src/components/client.ts b/src/components/client.ts index 46907385..0e799c3e 100644 --- a/src/components/client.ts +++ b/src/components/client.ts @@ -7,7 +7,7 @@ import { promisify } from 'util'; const writeFile = promisify(fs.writeFile); import TOML from '@iarna/toml'; import { template } from 'lodash'; -import { normalizeMethodName } from './helper'; +import { normalizeMethodNameJavascript, normalizeMethodNameRust, extractParameterNames } from './helper'; const tsTemplate = template( ` @@ -213,10 +213,10 @@ export class <%= className %> { <% openrpcDocument.methods.forEach((method) => { %> /** - * <%= method.summary %> + * <%= method.description %> */ // tslint:disable-next-line:max-line-length - public <%= normalizeMethodName(method.name) %>: <%= methodTypings.getTypingNames("typescript", method).method %> = (...params) => { + public <%= normalizeMethodNameJavascript(method.name) %>: <%= methodTypings.getTypingNames("typescript", method).method %> = (...params) => { return this.request("<%= method.name %>", params); } <% }); %> @@ -225,21 +225,54 @@ export default <%= className %>; `, { imports: { - normalizeMethodName, + normalizeMethodNameJavascript, }, } ); -const rsTemplate = template(` -#[macro_use] -extern crate jsonrpc_client_core; +const rsTemplate = template( + `// @generated +// Code generated by @open-rpc/generator DO NOT EDIT. + +use jsonrpc_client_core::{ + Transport, RpcRequest, call_method +}; <%= methodTypings.toString("rust", { includeSchemaTypings: true, includeMethodAliasTypings: false }) %> -jsonrpc_client!(pub struct <%= className %> { -<%= methodTypings.toString("rust", { includeSchemaTypings: false, includeMethodAliasTypings: true }) %> -}); -`); +pub struct <%= className %> { + transport: T, +} + +impl <%= className %> { + + pub fn new(transport: T) -> Self { + Self { transport } + } + +<% openrpcDocument.methods.forEach((method) => { %> + /** + * <%= method.description %> + */ + pub fn <%= normalizeMethodNameRust(method.name) %>(&mut self, <%= methodTypings.getParamsTyping("rust", method) %>) -> RpcRequest<<%= methodTypings.getTypingNames("rust", method).result %>, T::Future> { + let method = String::from(stringify!(<%= method.name %>)); + let params: HashMap<&str, serde_json::Value> = HashMap::from([ + <% extractParameterNames(methodTypings.getParamsTyping("rust", method)).forEach((paramName, index) => { %> + ("<%= paramName %>", serde_json::to_value(<%= paramName %>).unwrap()), + <% }); %> + ]); + call_method(&mut self.transport, method, params) + } + <% }); %> +} +`, + { + imports: { + normalizeMethodNameRust, + extractParameterNames, + }, + } +); const hooks: IHooks = { afterCopyStatic: [ diff --git a/src/components/docs.ts b/src/components/docs.ts index 3edc2b1e..48b4d2c3 100644 --- a/src/components/docs.ts +++ b/src/components/docs.ts @@ -1,7 +1,7 @@ import * as path from 'path'; import { copy, ensureDir, remove } from 'fs-extra'; import { IHooks, IComponent } from './types'; -import { IDocsConfig, IDocsExtraConfig } from 'src/config'; +import { IDocsExtraConfig } from 'src/config'; import * as fs from 'fs'; import { promisify } from 'util'; import { template, startCase } from 'lodash'; @@ -198,7 +198,7 @@ const hooks: IHooks = { ], }, afterCompileTemplate: [ - async (dest, frm, component, openrpcDocument): Promise => { + async (dest, frm, component, _): Promise => { const docsComponent = component as IComponent; if (!docsComponent.extraConfig) { return; diff --git a/src/components/helper.test.ts b/src/components/helper.test.ts index aea0f8e8..5152a4f2 100644 --- a/src/components/helper.test.ts +++ b/src/components/helper.test.ts @@ -1,28 +1,104 @@ import { describe, it, expect } from '@jest/globals'; -import { normalizeMethodName } from './helper'; +import { + normalizeMethodNameJavascript, + normalizeMethodNameRust, + extractParameterNames, +} from './helper'; -describe('normalizeMethodName', () => { +describe('Test normalizeMethodNameJavascript', () => { it('returns the original name if it does not contain a dot', () => { - expect(normalizeMethodName('hello')).toBe('hello'); - expect(normalizeMethodName('foo_bar')).toBe('foo_bar'); + expect(normalizeMethodNameJavascript('hello')).toBe('hello'); + expect(normalizeMethodNameJavascript('foo_bar')).toBe('foo_bar'); }); it('splits and capitalizes correctly with dot', () => { - expect(normalizeMethodName('foo.bar')).toBe('fooBar'); - expect(normalizeMethodName('foo.bar.baz')).toBe('fooBarBaz'); + expect(normalizeMethodNameJavascript('foo.bar')).toBe('fooBar'); + expect(normalizeMethodNameJavascript('foo.bar.baz')).toBe('fooBarBaz'); }); it('capitalizes only subsequent parts', () => { - expect(normalizeMethodName('one.two.three')).toBe('oneTwoThree'); + expect(normalizeMethodNameJavascript('one.two.three')).toBe('oneTwoThree'); }); it('handles leading/trailing separators gracefully', () => { - expect(normalizeMethodName('.foo.bar')).toBe('FooBar'); - expect(normalizeMethodName('foo.bar.')).toBe('fooBar'); - expect(normalizeMethodName('..foo..bar..')).toBe('FooBar'); + expect(normalizeMethodNameJavascript('.foo.bar')).toBe('FooBar'); + expect(normalizeMethodNameJavascript('foo.bar.')).toBe('fooBar'); + expect(normalizeMethodNameJavascript('..foo..bar..')).toBe('FooBar'); }); it('handles empty string', () => { - expect(normalizeMethodName('')).toBe(''); + expect(normalizeMethodNameJavascript('')).toBe(''); + }); +}); + +describe('Test normalizeMethodNameRust', () => { + it('returns the original name if it does not contain a dot', () => { + expect(normalizeMethodNameRust('hello')).toBe('hello'); + expect(normalizeMethodNameRust('foo_bar')).toBe('foo_bar'); + }); + + it('splits and capitalizes correctly with dot', () => { + expect(normalizeMethodNameRust('foo.bar')).toBe('foo_bar'); + expect(normalizeMethodNameRust('foo.bar.baz')).toBe('foo_bar_baz'); + }); + + it('capitalizes only subsequent parts', () => { + expect(normalizeMethodNameRust('one.two.three')).toBe('one_two_three'); + }); + + it('handles leading/trailing separators gracefully', () => { + expect(normalizeMethodNameRust('.foo.bar')).toBe('foo_bar'); + expect(normalizeMethodNameRust('foo.bar.')).toBe('foo_bar'); + expect(normalizeMethodNameRust('..foo..bar..')).toBe('foo_bar'); + }); + + it('handles empty string', () => { + expect(normalizeMethodNameRust('')).toBe(''); + }); +}); + +describe('Test extractParameterNames', () => { + it('extracts parameter names from a string with types', () => { + expect(extractParameterNames('number:int, address: string')).toEqual(['number', 'address']); + expect(extractParameterNames('id:number, name:string, active:boolean')).toEqual([ + 'id', + 'name', + 'active', + ]); + }); + + it('handles single parameter', () => { + expect(extractParameterNames('value:string')).toEqual(['value']); + }); + + it('handles parameters without types', () => { + expect(extractParameterNames('param1, param2, param3')).toEqual(['param1', 'param2', 'param3']); + }); + + it('handles mixed parameters with and without types', () => { + expect(extractParameterNames('id:number, name, active:boolean')).toEqual([ + 'id', + 'name', + 'active', + ]); + }); + + it('handles empty string', () => { + expect(extractParameterNames('')).toEqual([]); + }); + + it('handles string with only whitespace', () => { + expect(extractParameterNames(' ')).toEqual([]); + }); + + it('handles parameters with extra whitespace', () => { + expect(extractParameterNames(' number : int , address : string ')).toEqual([ + 'number', + 'address', + ]); + }); + + it('filters out empty parameter names', () => { + expect(extractParameterNames('valid:type, , another:type')).toEqual(['valid', 'another']); }); }); diff --git a/src/components/helper.ts b/src/components/helper.ts index 6f241833..2a718893 100644 --- a/src/components/helper.ts +++ b/src/components/helper.ts @@ -1,4 +1,6 @@ -export function normalizeMethodName(name: string): string { +// This function is used in the template files to normalize method names for JavaScript/TypeScript. +// It removes dots and applies camelCase formatting. +export function normalizeMethodNameJavascript(name: string): string { // backward compatibility only change on dot inside in name. if (!name.includes('.')) return name; @@ -7,3 +9,35 @@ export function normalizeMethodName(name: string): string { .map((part, index) => (index === 0 ? part : part.charAt(0).toUpperCase() + part.slice(1))) .join(''); } + +// This function is used in the template files to normalize method names for Rust. +// It replace dots with underscores and applies snake_case formatting. +export function normalizeMethodNameRust(name: string): string { + // backward compatibility only change on dot inside in name. + if (!name.includes('.')) return name; + + name = name.replace(/\./g, '_'); + name = name.toLowerCase(); + name = name.replace(/__+/g, '_'); // replace multiple underscores with a single underscore + while (name.startsWith('_')) { + name = name.slice(1); // remove leading underscore + } + while (name.endsWith('_')) { + name = name.slice(0, -1); // remove trailing underscore + } + + return name; +} + +// Extract parameter names from a parameter string into an array +export function extractParameterNames(paramStr: string): string[] { + if (!paramStr || paramStr.trim() === '') { + return []; + } + + return paramStr + .split(',') + .map((param) => param.trim()) + .map((param) => param.split(':')[0].trim()) + .filter((name) => name.length > 0); +} diff --git a/src/index.test.ts b/src/index.test.ts index b8eba8ee..28e379fd 100644 --- a/src/index.test.ts +++ b/src/index.test.ts @@ -7,7 +7,6 @@ import { forEach } from 'lodash'; import { OpenRPCDocumentDereferencingError } from '@open-rpc/schema-utils-js'; import { OpenrpcDocument as OpenRPC } from '@open-rpc/meta-schema'; import { describe, it, expect, beforeAll, afterAll } from '@jest/globals'; -import { isMapIterator } from 'util/types'; const stat = promisify(fs.stat); const rmdir = promisify(fs.rmdir); diff --git a/src/index.ts b/src/index.ts index 4209b541..61d689e1 100644 --- a/src/index.ts +++ b/src/index.ts @@ -5,7 +5,7 @@ import { promisify } from 'util'; import { startCase } from 'lodash'; import { OpenrpcDocument as OpenRPC } from '@open-rpc/meta-schema'; import { parseOpenRPCDocument } from '@open-rpc/schema-utils-js'; -import { IDocsConfig, TComponentConfig } from './config'; +import { TComponentConfig } from './config'; import Typings from '@open-rpc/typings'; import { @@ -13,9 +13,7 @@ import { defaultDocComponent, defaultServerComponent, IComponentModule, - IHooks, FHook, - getDefaultComponentTemplatePath, IComponent, } from './components'; export * as components from './components'; From 00d50412310637328b30737d4ad578c979bb9165 Mon Sep 17 00:00:00 2001 From: Mostafa Date: Thu, 2 Oct 2025 21:06:23 +0800 Subject: [PATCH 6/7] use jsonrpsee crate --- src/components/client.ts | 38 +++++++++++++++++++++----------------- 1 file changed, 21 insertions(+), 17 deletions(-) diff --git a/src/components/client.ts b/src/components/client.ts index 0e799c3e..116bc036 100644 --- a/src/components/client.ts +++ b/src/components/client.ts @@ -230,38 +230,42 @@ export default <%= className %>; } ); +import Rust from "@json-schema-tools/transpiler/build/codegens/rust"; +Rust.prototype.getCodePrefix = function () { + return ""; +}; + const rsTemplate = template( `// @generated // Code generated by @open-rpc/generator DO NOT EDIT. -use jsonrpc_client_core::{ - Transport, RpcRequest, call_method -}; +use jsonrpsee::core::{client::ClientT, ClientError as Error}; <%= methodTypings.toString("rust", { includeSchemaTypings: true, includeMethodAliasTypings: false }) %> -pub struct <%= className %> { - transport: T, +pub struct <%= className %> { + client: T, } -impl <%= className %> { +impl <%= className %> { - pub fn new(transport: T) -> Self { - Self { transport } - } + pub fn new(client: T) -> Self { + Self { client } + } <% openrpcDocument.methods.forEach((method) => { %> /** * <%= method.description %> */ - pub fn <%= normalizeMethodNameRust(method.name) %>(&mut self, <%= methodTypings.getParamsTyping("rust", method) %>) -> RpcRequest<<%= methodTypings.getTypingNames("rust", method).result %>, T::Future> { - let method = String::from(stringify!(<%= method.name %>)); - let params: HashMap<&str, serde_json::Value> = HashMap::from([ - <% extractParameterNames(methodTypings.getParamsTyping("rust", method)).forEach((paramName, index) => { %> - ("<%= paramName %>", serde_json::to_value(<%= paramName %>).unwrap()), - <% }); %> - ]); - call_method(&mut self.transport, method, params) + pub async fn <%= normalizeMethodNameRust(method.name) %>(&self, <%= methodTypings.getParamsTyping("rust", method) %>) -> Result<<%= methodTypings.getTypingNames("rust", method).result %>, Error> { + let method = "<%= method.name %>"; + <% const paramNames = extractParameterNames(methodTypings.getParamsTyping("rust", method)); %> + <% if (paramNames.length > 0) { %> + let mut params: serde_json::Map = serde_json::Map::new(); + <% paramNames.forEach((paramName, index) => { %>params.insert("<%= paramName %>".to_string(), serde_json::to_value(<%= paramName %>).unwrap());<% }); %> + <% } else { %>let params: serde_json::Map = serde_json::Map::new();<% } %> + + self.client.request(&method, params).await } <% }); %> } From ad9e99fe010f1ffaf67c075e2549475ff537aff8 Mon Sep 17 00:00:00 2001 From: Mostafa Date: Thu, 2 Oct 2025 23:50:55 +0800 Subject: [PATCH 7/7] fix clippy issue --- src/components/client.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/client.ts b/src/components/client.ts index 116bc036..1c686c1c 100644 --- a/src/components/client.ts +++ b/src/components/client.ts @@ -238,6 +238,7 @@ Rust.prototype.getCodePrefix = function () { const rsTemplate = template( `// @generated // Code generated by @open-rpc/generator DO NOT EDIT. +#![allow(clippy::too_many_arguments)] use jsonrpsee::core::{client::ClientT, ClientError as Error}; @@ -258,14 +259,13 @@ impl <%= className %> { * <%= method.description %> */ pub async fn <%= normalizeMethodNameRust(method.name) %>(&self, <%= methodTypings.getParamsTyping("rust", method) %>) -> Result<<%= methodTypings.getTypingNames("rust", method).result %>, Error> { - let method = "<%= method.name %>"; <% const paramNames = extractParameterNames(methodTypings.getParamsTyping("rust", method)); %> <% if (paramNames.length > 0) { %> let mut params: serde_json::Map = serde_json::Map::new(); <% paramNames.forEach((paramName, index) => { %>params.insert("<%= paramName %>".to_string(), serde_json::to_value(<%= paramName %>).unwrap());<% }); %> <% } else { %>let params: serde_json::Map = serde_json::Map::new();<% } %> - self.client.request(&method, params).await + self.client.request("<%= method.name %>", params).await } <% }); %> }