From d5f769339298b43831901c467c69b401b67385f1 Mon Sep 17 00:00:00 2001 From: mtyszczak Date: Thu, 4 Sep 2025 14:42:51 +0200 Subject: [PATCH] Add support for JSON-RPC OpenAPI in TS generator --- .gitignore | 1 + README.md | 6 +++--- package.json | 3 ++- src/generator.ts | 7 ++++--- src/hooks/on-create-route.ts | 18 ++++++++++++++---- src/index.ts | 6 +++++- src/npm.ts | 5 +++-- src/parse-options.ts | 17 +++++++++++++---- templates/README.md.eta | 6 +++--- 9 files changed, 48 insertions(+), 21 deletions(-) diff --git a/.gitignore b/.gitignore index a5e70b1..82cf404 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ dist .vscode generated tsconfig.tsbuildinfo +test-api diff --git a/README.md b/README.md index d09063b..d85e34d 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Wax spec generator -Wax library REST API specification generator +Wax library REST/JSON-RPC API specification generator No-download example usage: @@ -45,13 +45,13 @@ npx generate-wax-spec --help #### generate JS and D.TS files only using the hafbe namsepace ```bash -npx generate-wax-spec -i data/hafbe.json -N hafbe +npx generate-wax-spec -i data/hafbe.json -N hafbe -T rest ``` #### Emit entire npm project along the JS and D.TS files ```bash -npx generate-wax-spec -i data/hafbe.json -N hafbe -e --npm-name "@hiveio/wax-api-hafbe" --npm-version 1.27.0-rc1 +npx generate-wax-spec -i data/hafbe.json -N hafbe -T rest -e --npm-name "@hiveio/wax-api-hafbe" --npm-version 1.27.0-rc1 ``` ## Building diff --git a/package.json b/package.json index 1dce11e..ebd8185 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@hiveio/wax-spec-generator", "version": "0.0.1-LastGitTagPlaceholder.GitHashPlaceholder", - "description": "Wax library REST API specification generator", + "description": "Wax library REST/JSON-RPC API specification generator", "main": "dist/index.js", "private": false, "packageManager": "pnpm@10.0.0+sha512.b8fef5494bd3fe4cbd4edabd0745df2ee5be3e4b0b8b08fa643aa3e4c6702ccc0f00d68fa8a8c9858a735a0032485a44990ed2810526c875e416f001b17df12b", @@ -47,6 +47,7 @@ "blockchain", "hive", "swagger", + "openapi", "wax" ], "files": [ diff --git a/src/generator.ts b/src/generator.ts index cf143b2..63639c8 100644 --- a/src/generator.ts +++ b/src/generator.ts @@ -8,6 +8,7 @@ import { addNamespace } from "./utils/object.js"; import { stringifyObjectWithUnstringifiedKeys } from "./utils/text.js"; export interface IGeneratorConfig { + apiType: "jsonrpc" | "rest"; inputFile: string; outputDirectory: string; /** @@ -56,7 +57,7 @@ export const generate = async(config: IGeneratorConfig): Promise => { } }), hooks: { - onCreateRoute: onCreateRoute.bind(undefined, result, runtimeDataResult) + onCreateRoute: onCreateRoute.bind(undefined, config.apiType, result, runtimeDataResult) } }); @@ -68,9 +69,9 @@ export const generate = async(config: IGeneratorConfig): Promise => { }); fs.appendFileSync( outDeclarationsPath, - `${EOL }type TWaxRestAPiExtended = ${ + `${EOL }type TWaxExtended = ${ stringifyObjectWithUnstringifiedKeys([ "result", "params" ], addNamespace(result, config.namespace, false), indentCount) - }${EOL }declare var WaxExtendedData: TWaxRestAPiExtended${ EOL }export default WaxExtendedData${ EOL}`, + }${EOL }declare var WaxExtendedData: TWaxExtended${ EOL }export default WaxExtendedData${ EOL}`, { encoding: fileEncoding } ); diff --git a/src/hooks/on-create-route.ts b/src/hooks/on-create-route.ts index 8da4272..675f71b 100644 --- a/src/hooks/on-create-route.ts +++ b/src/hooks/on-create-route.ts @@ -3,10 +3,17 @@ import { type ParsedRoute } from "swagger-typescript-api"; import { indentCharacter, indentCount } from "../constants.js"; import { camelize } from "../utils/text.js"; -export const onCreateRoute = (result: Record, runtimeDataResult: Record, routeData: ParsedRoute): undefined => { +export const onCreateRoute = ( + apiType: "jsonrpc" | "rest", + result: Record, + runtimeDataResult: Record, + routeData: ParsedRoute +): undefined => { const { type } = routeData.response; - const routeParts = routeData.raw.route.split("/").filter(node => node.length); + const isRestApi = apiType === "rest"; + + const routeParts = routeData.raw.route.split(isRestApi ? "/" : ".").filter(node => node.length); let currObj = result; let currObjRuntime = runtimeDataResult; @@ -23,7 +30,7 @@ export const onCreateRoute = (result: Record, runtimeDataResult: Re if (currObj[normalizedName] === undefined) { currObjRuntime[normalizedName] = { - urlPath: camelCaseName + urlPath: urlPathName }; currObj[normalizedName] = {}; } @@ -35,10 +42,13 @@ export const onCreateRoute = (result: Record, runtimeDataResult: Re currObjRuntime.method = routeData.raw.method.toUpperCase(); currObj.result = type; // No query and path params (set params to undefined to allow generation of function with no arguments) - if ((routeData.request as any).pathParams === undefined && (routeData.request as any).query === undefined) + if ( (routeData.request as any).pathParams === undefined + && (routeData.request as any).query === undefined + && (routeData.request as any).payload?.type === undefined) currObj.params = undefined; else // Either query params or no query params, but path params exist, so use TEmptyReq - {} currObj.params = `${((routeData.request as any).requestParams?.typeName) ?? "TEmptyReq" + } & ${(routeData.request as any).payload?.type ?? "TEmptyReq" } & {${EOL}${ (routeData.request as any).parameters.map(node => `${indentCharacter.repeat(indentCount)}/** ${node.description} */${EOL}${ indentCharacter.repeat(indentCount) + node.name + (node.optional ? "?" : "") diff --git a/src/index.ts b/src/index.ts index 799d1fe..9b91ea5 100644 --- a/src/index.ts +++ b/src/index.ts @@ -14,13 +14,16 @@ const outputDirectory = makePathAbsolute(argv.outputDirectory); if (!fs.existsSync(outputDirectory)) fs.mkdirSync(outputDirectory); -const namespace = argv.namespace ?? parseNamespace(inputFile); +const apiType = argv.apiType as "jsonrpc" | "rest"; + +const namespace = typeof argv.addNamespace === "undefined" ? undefined : (argv.addNamespace || parseNamespace(inputFile)); if (argv.emitNpmProject) { if (argv.npmName === undefined || argv.npmVersion === undefined) throw new Error("Trying to create npm project without npm package name and/or version"); prepareNpmPackage({ + apiType, outputDirectory, version: argv.npmVersion, name: argv.npmName, @@ -30,6 +33,7 @@ if (argv.emitNpmProject) { } await generate({ + apiType, inputFile, outputDirectory, namespace diff --git a/src/npm.ts b/src/npm.ts index 2fc0bbc..a158c45 100644 --- a/src/npm.ts +++ b/src/npm.ts @@ -5,6 +5,7 @@ import { fileEncoding } from "./constants.js"; import { makePathAbsolute } from "./utils/paths.js"; export interface INpmPackageConfig { + apiType: "jsonrpc" | "rest"; outputDirectory: string; name: string; version: string; @@ -14,7 +15,7 @@ export interface INpmPackageConfig { export const prepareNpmPackage = (config: INpmPackageConfig): void => { const displayName = config.namespace?.length > 0 ? `${config.namespace} API definitions` : config.name; - const description = `Wax REST API definitions${(config.namespace.length > 0 ? " for " : "")}${(config.namespace || "")}`; + const description = `Wax REST API definitions${(config.namespace?.length > 0 ? " for " : "")}${(config.namespace || "")}`; const { name, version } = config; const packageJson = { @@ -47,7 +48,7 @@ export const prepareNpmPackage = (config: INpmPackageConfig): void => { const eta = new Eta({ views: templatesDirectory }); const year = new Date().getFullYear(); - const etaData = { displayName, description, name, version, year }; + const etaData = { apiType: config.apiType, displayName, description, name, version, year }; fs.writeFileSync(path.join(config.outputDirectory, "package.json"), JSON.stringify(packageJson, undefined, 2), { encoding: fileEncoding }); diff --git a/src/parse-options.ts b/src/parse-options.ts index 5738931..1c2f732 100644 --- a/src/parse-options.ts +++ b/src/parse-options.ts @@ -6,12 +6,21 @@ import { hideBin } from "yargs/helpers"; const __dirname = dirname(fileURLToPath(import.meta.url)); export const parseOptions = () => yargs(hideBin(process.argv)) - .option("namespace", { + .option("add-namespace", { alias: "N", type: "string", - description: "Optional namespace name for the application - adds a nested level of object with given namespace name for all of the parsed API definitions." - + " Sets the namespace to the server URL if not provided or leaves empty if server URL is not present in the Swagger file", - default: undefined + description: "Namespace name for the application - adds a nested level of object with given namespace name for all of the parsed API definitions." + + " Sets the namespace to the server URL if implicitly set (--add-namespace). Usually, when dealing with JSON-RPC APIs, the namespace would be " + + "left empty, as the methods are globally available. For REST APIs, it's recommended to set the namespace to avoid potential name clashes. " + + "In most cases, the namespace can be automatically deduced from the input file name, so you can just use --add-namespace without a value.", + implies: "" + }) + .option("api-type", { + alias: "T", + type: "string", + description: "API type for the application - specifies the type of API being defined. Affects the way the definitions are generated.", + choices: [ "jsonrpc", "rest" ], + default: "rest" }) .option("input-file", { alias: "i", diff --git a/templates/README.md.eta b/templates/README.md.eta index 10baa2d..68876a5 100644 --- a/templates/README.md.eta +++ b/templates/README.md.eta @@ -4,7 +4,7 @@ <%= it.description %> -> This package has been auto-generated by the @hiveio/wax-spec-generator script +> This package has been auto-generated by the @hiveio/wax-spec-generator script for usage with the [Wax library](https://www.npmjs.com/package/@hiveio/wax). ## Usage @@ -15,13 +15,13 @@ pnpm add <%= it.name %> ``` -And import into your Wax project: +And import into your [Wax](https://www.npmjs.com/package/@hiveio/wax) project: ```ts import WaxExtendedData from '<%= it.name %>'; import { createHiveChain } from '@hiveio/wax'; -const chainExtended = (await createHiveChain()).extendRest(WaxExtendedData); +const chainExtended = (await createHiveChain()).<%= it.apiType === "rest" ? "extendRest" : "extend" %>(WaxExtendedData); ``` ## License -- GitLab