diff --git a/src/components/client.ts b/src/components/client.ts index debe5380..1c686c1c 100644 --- a/src/components/client.ts +++ b/src/components/client.ts @@ -7,8 +7,10 @@ import { promisify } from 'util'; const writeFile = promisify(fs.writeFile); import TOML from '@iarna/toml'; import { template } from 'lodash'; +import { normalizeMethodNameJavascript, normalizeMethodNameRust, extractParameterNames } from './helper'; -const tsTemplate = template(` +const tsTemplate = template( + ` // Code generated by @open-rpc/generator DO NOT EDIT. import { RequestManager, @@ -211,27 +213,70 @@ export class <%= className %> { <% openrpcDocument.methods.forEach((method) => { %> /** - * <%= method.summary %> + * <%= method.description %> */ // tslint:disable-next-line:max-line-length - public <%= method.name %>: <%= methodTypings.getTypingNames("typescript", method).method %> = (...params) => { + public <%= normalizeMethodNameJavascript(method.name) %>: <%= methodTypings.getTypingNames("typescript", method).method %> = (...params) => { return this.request("<%= method.name %>", params); } <% }); %> } export default <%= className %>; -`); +`, + { + imports: { + normalizeMethodNameJavascript, + }, + } +); + +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. +#![allow(clippy::too_many_arguments)] -const rsTemplate = template(` -#[macro_use] -extern crate jsonrpc_client_core; +use jsonrpsee::core::{client::ClientT, ClientError as Error}; <%= methodTypings.toString("rust", { includeSchemaTypings: true, includeMethodAliasTypings: false }) %> -jsonrpc_client!(pub struct <%= className %> { -<%= methodTypings.toString("rust", { includeSchemaTypings: false, includeMethodAliasTypings: true }) %> -}); -`); +pub struct <%= className %> { + client: T, +} + +impl <%= className %> { + + pub fn new(client: T) -> Self { + Self { client } + } + +<% openrpcDocument.methods.forEach((method) => { %> + /** + * <%= method.description %> + */ + pub async fn <%= normalizeMethodNameRust(method.name) %>(&self, <%= methodTypings.getParamsTyping("rust", method) %>) -> Result<<%= methodTypings.getTypingNames("rust", method).result %>, Error> { + <% 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.name %>", params).await + } + <% }); %> +} +`, + { + 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 new file mode 100644 index 00000000..5152a4f2 --- /dev/null +++ b/src/components/helper.test.ts @@ -0,0 +1,104 @@ +import { describe, it, expect } from '@jest/globals'; +import { + normalizeMethodNameJavascript, + normalizeMethodNameRust, + extractParameterNames, +} from './helper'; + +describe('Test normalizeMethodNameJavascript', () => { + it('returns the original name if it does not contain a dot', () => { + expect(normalizeMethodNameJavascript('hello')).toBe('hello'); + expect(normalizeMethodNameJavascript('foo_bar')).toBe('foo_bar'); + }); + + it('splits and capitalizes correctly with dot', () => { + expect(normalizeMethodNameJavascript('foo.bar')).toBe('fooBar'); + expect(normalizeMethodNameJavascript('foo.bar.baz')).toBe('fooBarBaz'); + }); + + it('capitalizes only subsequent parts', () => { + expect(normalizeMethodNameJavascript('one.two.three')).toBe('oneTwoThree'); + }); + + it('handles leading/trailing separators gracefully', () => { + expect(normalizeMethodNameJavascript('.foo.bar')).toBe('FooBar'); + expect(normalizeMethodNameJavascript('foo.bar.')).toBe('fooBar'); + expect(normalizeMethodNameJavascript('..foo..bar..')).toBe('FooBar'); + }); + + it('handles empty string', () => { + 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 new file mode 100644 index 00000000..2a718893 --- /dev/null +++ b/src/components/helper.ts @@ -0,0 +1,43 @@ +// 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; + + const parts = name.split(/[._]/); + return parts + .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';