├── .prettierignore ├── .eslintignore ├── .gitattributes ├── commitlint.config.cjs ├── src ├── custom.d.ts ├── javascript │ ├── mock │ │ ├── schema.mts │ │ ├── util.mts │ │ ├── index.mts │ │ ├── response.mts │ │ ├── compose.mts │ │ ├── dataType.mts │ │ └── parse.mts │ ├── generateConstants.mts │ ├── files │ │ ├── hooksConfig.mts │ │ ├── httpRequest.mts │ │ └── config.mts │ ├── strings.mts │ ├── generateApis.mts │ ├── generateTypes.mts │ ├── signalR │ │ └── generator.mts │ ├── index.mts │ └── generator.mts ├── getJson.mts ├── utilities │ ├── swaggerToOpenApi.mts │ └── jsdoc.mts ├── kotlin │ ├── index.mts │ ├── generateTypes.mts │ ├── generateApis.mts │ ├── strings.mts │ ├── generator.mts │ └── utils.mts ├── updateJson.mts ├── utils.mts ├── index.mts └── types.mts ├── .husky ├── commit-msg └── pre-commit ├── .prettierrc ├── .npmignore ├── .eslintrc.cjs ├── __tests__ ├── e2e │ ├── local-openapi.config.json │ ├── petstore3.config.json │ ├── __snapshots__ │ │ └── kotlin-wms.test.mjs.snap │ ├── local-openapi-2.config.json │ ├── kotlin-wms.config.json │ ├── slack-api.config.json │ ├── kotlin-wms.test.mjs │ ├── slack-api.test.mjs │ ├── petstore3.test.mjs │ ├── local-openapi.test.mjs │ ├── local-openapi-2.test.mjs │ ├── utils.mjs │ └── README.md ├── updateJson │ └── index.test.mjs ├── main │ ├── index.test.mjs │ ├── excludes.test.mjs │ ├── includes.test.mjs │ ├── excludes-include.test.mjs │ └── utils.mjs ├── enums.test.mjs ├── discriminator.test.mjs ├── swagger.json └── __snapshots__ │ └── enums.test.mjs.snap ├── signalr.md ├── jest.config.mjs ├── bin └── index.mjs ├── LICENSE ├── .gitignore ├── package.json ├── schema └── v6.json ├── CODE_OF_CONDUCT.md ├── tsconfig.json ├── testFiles └── Wms.postman_collection.json └── readme.md /.prettierignore: -------------------------------------------------------------------------------- 1 | __tests__/swagger.json -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | coverage/ 3 | dist/ 4 | lib/ 5 | build/ 6 | 7 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=LF 3 | -------------------------------------------------------------------------------- /commitlint.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ["@commitlint/config-conventional"], 3 | }; 4 | -------------------------------------------------------------------------------- /src/custom.d.ts: -------------------------------------------------------------------------------- 1 | declare module "*.json" { 2 | const value: any; 3 | export default value; 4 | } 5 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | yarn commitlint --edit $1 5 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | yarn lint 5 | yarn test 6 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "all", 3 | "plugins": ["./node_modules/prettier-plugin-jsdoc/dist/index.js"] 4 | } 5 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .prettierrc 2 | .gitattributes 3 | .vscode 4 | .eslintignore 5 | .eslintrc.js 6 | .gitignore 7 | .prettierrc 8 | commitlint.config.js 9 | CODE_OF_CONDUCT.md 10 | test/ 11 | examples/ 12 | src/ 13 | -------------------------------------------------------------------------------- /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | parser: "@typescript-eslint/parser", 4 | extends: ["plugin:@typescript-eslint/eslint-recommended"], 5 | plugins: ["prettier", "@typescript-eslint"], 6 | }; 7 | -------------------------------------------------------------------------------- /__tests__/e2e/local-openapi.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../schema/v6.json", 3 | "url": "./__tests__/e2e/local-openapi.json", 4 | "dir": "./__tests__/e2e/outputs/local-openapi", 5 | "language": "typescript", 6 | "keepJson": true, 7 | "reactHooks": true, 8 | "generateEnumAsType": true 9 | } 10 | -------------------------------------------------------------------------------- /__tests__/e2e/petstore3.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../schema/v6.json", 3 | "url": "https://petstore3.swagger.io/api/v3/openapi.json", 4 | "dir": "./__tests__/e2e/outputs/petstore3", 5 | "language": "typescript", 6 | "keepJson": true, 7 | "reactHooks": true, 8 | "generateEnumAsType": false 9 | } 10 | -------------------------------------------------------------------------------- /signalr.md: -------------------------------------------------------------------------------- 1 | # SignalR swagger json 2 | 3 | Add hub to swagger.config.json 4 | 5 | ```json 6 | { 7 | ... 8 | "hub": "http://localhost:5000/api/signalRTypes/getSignalrType.json", 9 | } 10 | ``` 11 | 12 | ## CS implementation 13 | 14 | There is a repo we used to implement cs side 15 | https://github.com/majidbigdeli/SignalRTypes 16 | -------------------------------------------------------------------------------- /__tests__/e2e/__snapshots__/kotlin-wms.test.mjs.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`E2E: Kotlin WMS API Tests should generate Kotlin WMS API files correctly: kotlin-wms-generated-files 1`] = ` 4 | Object { 5 | "config.ts": "", 6 | "hooks.ts": "", 7 | "hooksConfig.ts": "", 8 | "httpRequest.ts": "", 9 | "services.ts": "", 10 | "types.ts": "", 11 | } 12 | `; 13 | -------------------------------------------------------------------------------- /__tests__/e2e/local-openapi-2.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../schema/v6.json", 3 | "url": "./__tests__/e2e/local-openapi-2.json", 4 | "dir": "./__tests__/e2e/outputs/local-openapi-2", 5 | "reactHooks": true, 6 | "generateEnumAsType": true, 7 | "useInfiniteQuery": [ 8 | "useGetSuppliersSuppliers", 9 | "useGetWarehouseWarehouses", 10 | "useGetFinanceAccounts" 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /jest.config.mjs: -------------------------------------------------------------------------------- 1 | import { defaults } from "jest-config"; 2 | const config = { 3 | moduleFileExtensions: [...defaults.moduleFileExtensions, "mts"], 4 | testMatch: ["**/__tests__/**/?(*.)+(spec|test).m[tj]s?(x)"], 5 | moduleNameMapper: { 6 | ...defaults.moduleNameMapper, 7 | "#(.*)": "/node_modules/$1", 8 | }, 9 | preset: "ts-jest", 10 | verbose: true, 11 | }; 12 | export default config; 13 | //# sourceMappingURL=jest.config.js.map 14 | -------------------------------------------------------------------------------- /bin/index.mjs: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import { generate } from "../lib/index.mjs"; 3 | 4 | import yargs from "yargs/yargs"; 5 | import { hideBin } from "yargs/helpers"; 6 | 7 | const argv = yargs(hideBin(process.argv)) 8 | .option("local") 9 | .option("branch") 10 | .option("config", { 11 | string: true, 12 | }).argv; 13 | 14 | generate(undefined, { 15 | tag: argv._, 16 | local: argv.local, 17 | branch: argv.branch, 18 | config: argv.config, 19 | }); 20 | -------------------------------------------------------------------------------- /__tests__/e2e/kotlin-wms.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../schema/v6.json", 3 | "url": "https://bitfinity.postman.co/workspace/Bitfinity~0878e325-dcc5-4adf-b37e-ac68386085f4/collection/4699480-5297d2ce-9573-4bca-a65b-5451c9809055?action=share&creator=4699480", 4 | "dir": "./__tests__/e2e/outputs/kotlin-wms", 5 | "language": "kotlin", 6 | "kotlinPackage": "com.wms.wms.data.api", 7 | "ignore": { 8 | "headerParams": ["Accept", "Content-Encoding", "Content-Type"] 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/javascript/mock/schema.mts: -------------------------------------------------------------------------------- 1 | import { Schema, SwaggerJson } from "../../types.mjs"; 2 | import { getSchemaData } from "./parse.mjs"; 3 | 4 | export type Schemas = { 5 | [schema: string]: Schema; 6 | }; 7 | 8 | export const extractSchemas = (obj: SwaggerJson): Schemas => { 9 | const { components } = obj; 10 | const schemas = components && components.schemas ? components.schemas : {}; 11 | return Object.keys(schemas).reduce((acc: any, name: string) => { 12 | acc[name] = getSchemaData(schemas, name); 13 | return acc; 14 | }, {}); 15 | }; 16 | -------------------------------------------------------------------------------- /src/javascript/generateConstants.mts: -------------------------------------------------------------------------------- 1 | import type { ConstantsAST } from "../types.mjs"; 2 | import { isAscending } from "../utils.mjs"; 3 | 4 | function generateConstants(types: ConstantsAST[]): string { 5 | try { 6 | return types 7 | .sort(({ name }, { name: _name }) => isAscending(name, _name)) 8 | .reduce((prev, { name, value }) => { 9 | prev += `export const ${name} = ${value};\n`; 10 | 11 | return prev; 12 | }, ""); 13 | } catch (error) { 14 | console.error({ error }); 15 | return ""; 16 | } 17 | } 18 | 19 | export { generateConstants }; 20 | -------------------------------------------------------------------------------- /__tests__/e2e/slack-api.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../schema/v6.json", 3 | "url": [ 4 | { 5 | "branch": "master", 6 | "url": "https://raw.githubusercontent.com/slackapi/slack-api-specs/master/web-api/slack_web_openapi_v2.json" 7 | }, 8 | { 9 | "branch": "develop", 10 | "url": "https://raw.githubusercontent.com/slackapi/slack-api-specs/master/web-api/slack_web_openapi_v2.json" 11 | } 12 | ], 13 | "dir": "./__tests__/e2e/outputs/slack-api", 14 | "language": "typescript", 15 | "keepJson": true, 16 | "reactHooks": true, 17 | "useInfiniteQuery": ["useGetRealmsRealm"], 18 | "ignore": { 19 | "headerParams": ["terminalId"] 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/getJson.mts: -------------------------------------------------------------------------------- 1 | import { default as Axios } from "axios"; 2 | import { readFileSync } from "fs"; 3 | import yaml from "js-yaml"; 4 | 5 | async function getJson(url: string) { 6 | let input; 7 | if (url.startsWith("http")) { 8 | process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0"; 9 | const { data } = await Axios.get(url); 10 | 11 | // if url is yaml file convert it to json 12 | input = data; 13 | } else { 14 | const data = readFileSync(url).toString(); 15 | input = data; 16 | } 17 | 18 | if (typeof input === "object") { 19 | return input; 20 | } else if (url.endsWith("json")) { 21 | return JSON.parse(input); 22 | } 23 | return yaml.load(input); 24 | } 25 | 26 | export { getJson }; 27 | -------------------------------------------------------------------------------- /src/utilities/swaggerToOpenApi.mts: -------------------------------------------------------------------------------- 1 | import { SwaggerJson } from "../types.mjs"; 2 | //@ts-ignore 3 | import converter from "swagger2openapi"; 4 | 5 | /** Support swagger v2 */ 6 | function swaggerToOpenApi(input: SwaggerJson) { 7 | const options: any = {}; 8 | options.patch = true; // fix up small errors in the source definition 9 | options.warnOnly = true; // Do not throw on non-patchable errors 10 | return new Promise((resolve, reject) => { 11 | converter.convertObj(input, options, function (err: Error, result: any) { 12 | if (err) { 13 | reject(err); 14 | return; 15 | } 16 | resolve(result.openapi); 17 | }); 18 | }); 19 | } 20 | 21 | export { swaggerToOpenApi }; 22 | -------------------------------------------------------------------------------- /__tests__/updateJson/index.test.mjs: -------------------------------------------------------------------------------- 1 | import { partialUpdateJson } from "../../lib/updateJson.mjs"; 2 | import newJson from "./new.json"; 3 | import oldJson from "./old.json"; 4 | 5 | test("update json", () => { 6 | //@ts-ignore 7 | const result = partialUpdateJson(oldJson, newJson, ["Account"]); 8 | 9 | expect(result).toHaveProperty([ 10 | "components", 11 | "schemas", 12 | "SeptaPay.Core.Models.Enums.CurrencyType", 13 | "isNew", 14 | ]); 15 | 16 | expect(result).toHaveProperty([ 17 | "components", 18 | "schemas", 19 | "SeptaPay.Client.Api.Models.AccountCreationApiModel", 20 | "properties", 21 | "isNew", 22 | ]); 23 | 24 | expect(result).not.toHaveProperty([ 25 | "paths", 26 | "/Account/{accountId}/balance", 27 | "get", 28 | ]); 29 | 30 | expect(result).toMatchSnapshot(); 31 | }); 32 | -------------------------------------------------------------------------------- /src/javascript/mock/util.mts: -------------------------------------------------------------------------------- 1 | import * as fs from "fs"; 2 | import { join } from "path"; 3 | 4 | // Replace `{}, /` charactors with `_` 5 | export const normalizePath = (path: string): string => { 6 | const replaced = path.replace(/^\/|{|}/g, ""); 7 | return replaced.replace(/\//g, "_"); 8 | }; 9 | 10 | export const normalizeName = (name: string): string => { 11 | return name.toLowerCase().replace(/ /g, "_").replace(/,/g, ""); 12 | }; 13 | 14 | // Write each response to JSON files. 15 | export const writeFiles = ( 16 | data: { [file: string]: any }, 17 | outputPath: string, 18 | ): void => { 19 | Object.keys(data).forEach((key) => { 20 | const val = data[key]; 21 | const fileName = `${key}.json`; 22 | const path = join(outputPath, fileName); 23 | const formatted = JSON.stringify(val, null, 2); 24 | fs.writeFileSync(path, formatted); 25 | }); 26 | }; 27 | -------------------------------------------------------------------------------- /src/kotlin/index.mts: -------------------------------------------------------------------------------- 1 | import { writeFileSync,} from "fs"; 2 | import { SwaggerJson, Config } from "../types.mjs"; 3 | import { generator } from "./generator.mjs"; 4 | import chalk from "chalk"; 5 | 6 | const generateKotlinService = async ( 7 | config: Config, 8 | input: SwaggerJson, 9 | ) => { 10 | const { url, hub, dir, prettierPath, language, mock, reactHooks, local } = 11 | config; 12 | 13 | try { 14 | 15 | const { code, type } = generator(input, config); 16 | 17 | writeFileSync(`${dir}/IApis.kt`, code); 18 | console.log(chalk.yellowBright("IApis Completed")); 19 | 20 | writeFileSync(`${dir}/Models.kt`, type); 21 | console.log(chalk.yellowBright("Models Completed")); 22 | 23 | console.log(chalk.greenBright("All Completed")); 24 | } catch (error) { 25 | console.log(chalk.redBright(error)); 26 | console.log(chalk.redBright("failed")); 27 | } 28 | }; 29 | 30 | 31 | export { generateKotlinService }; 32 | -------------------------------------------------------------------------------- /__tests__/main/index.test.mjs: -------------------------------------------------------------------------------- 1 | import { cleanOutputDir, generator } from "./utils.mjs"; 2 | import swaggerJson from "./swagger.json"; 3 | 4 | describe("generate", () => { 5 | beforeAll(async () => { 6 | await cleanOutputDir("./__tests__/main/outputs/index"); 7 | }); 8 | 9 | afterEach(async () => { 10 | await cleanOutputDir("./__tests__/main/outputs/index"); 11 | }); 12 | 13 | test("generate Code, hooks, and type", async () => { 14 | const { 15 | "services.ts": code, 16 | "hooks.ts": hooks, 17 | "types.ts": type, 18 | } = await generator( 19 | { 20 | url: "./__tests__/main/outputs/index/swagger.json", 21 | dir: "./__tests__/main/outputs/index", 22 | reactHooks: true, 23 | }, 24 | swaggerJson, 25 | ); 26 | 27 | expect(code).toMatchSnapshot("generate Code"); 28 | expect(hooks).toMatchSnapshot("generate hooks"); 29 | expect(type).toMatchSnapshot("generate type"); 30 | }, 10000); 31 | }); 32 | -------------------------------------------------------------------------------- /__tests__/main/excludes.test.mjs: -------------------------------------------------------------------------------- 1 | import { cleanOutputDir, generator } from "./utils.mjs"; 2 | import swaggerJson from "./swagger.json"; 3 | 4 | describe("excludes and include", () => { 5 | beforeAll(async () => { 6 | await cleanOutputDir("./__tests__/main/outputs/excludes/temp1"); 7 | }); 8 | 9 | afterEach(async () => { 10 | await cleanOutputDir("./__tests__/main/outputs/excludes/temp1"); 11 | }); 12 | 13 | test("excludes post methods", async () => { 14 | const { 15 | "services.ts": code, 16 | "hooks.ts": hooks, 17 | "types.ts": type, 18 | } = await generator( 19 | { 20 | url: "./__tests__/main/outputs/excludes/temp1/swagger.json", 21 | dir: "./__tests__/main/outputs/excludes/temp1", 22 | reactHooks: true, 23 | excludes: ["^post"], 24 | }, 25 | swaggerJson, 26 | ); 27 | expect(code).toMatchSnapshot("generate Code"); 28 | expect(hooks).toMatchSnapshot("generate hooks"); 29 | expect(type).toMatchSnapshot("generate type"); 30 | }, 10000); 31 | }); 32 | -------------------------------------------------------------------------------- /__tests__/main/includes.test.mjs: -------------------------------------------------------------------------------- 1 | import { cleanOutputDir, generator } from "./utils.mjs"; 2 | import swaggerJson from "./swagger.json"; 3 | 4 | describe("includes", () => { 5 | beforeAll(async () => { 6 | await cleanOutputDir("./__tests__/main/outputs/includes/temp2"); 7 | }); 8 | 9 | afterEach(async () => { 10 | await cleanOutputDir("./__tests__/main/outputs/includes/temp2"); 11 | }); 12 | 13 | test("only includes methods ends with Id", async () => { 14 | const { 15 | "services.ts": code, 16 | "hooks.ts": hooks, 17 | "types.ts": type, 18 | } = await generator( 19 | { 20 | url: "./__tests__/main/outputs/includes/temp2/swagger.json", 21 | dir: "./__tests__/main/outputs/includes/temp2", 22 | reactHooks: true, 23 | includes: ["Id$"], 24 | }, 25 | swaggerJson, 26 | ); 27 | 28 | expect(code).toMatchSnapshot("generate Code"); 29 | expect(hooks).toMatchSnapshot("generate hooks"); 30 | expect(type).toMatchSnapshot("generate type"); 31 | }); 32 | }); 33 | -------------------------------------------------------------------------------- /src/javascript/mock/index.mts: -------------------------------------------------------------------------------- 1 | /** Is A fork from https://github.com/yayoc/swagger-to-mock */ 2 | 3 | import chalk from "chalk"; 4 | import { extractResponses } from "./response.mjs"; 5 | import { writeFiles } from "./util.mjs"; 6 | import { extractSchemas } from "./schema.mjs"; 7 | import { composeMockData } from "./compose.mjs"; 8 | import { existsSync, mkdirSync } from "fs"; 9 | import { Config, SwaggerJson } from "../../types.mjs"; 10 | 11 | function generateMock(content: SwaggerJson, config: Config) { 12 | const { dir } = config; 13 | try { 14 | const output = `${dir}/mocks`; 15 | const responses = extractResponses(content, config); 16 | const schemas = extractSchemas(content); 17 | const composed = composeMockData(responses, schemas); 18 | 19 | if (!existsSync(output)) { 20 | mkdirSync(output); 21 | } 22 | writeFiles(composed, output); 23 | 24 | console.log(chalk.yellowBright("Mocks Completed")); 25 | } catch (e) { 26 | console.log(chalk.redBright(e)); 27 | console.log(chalk.redBright("Mocks Failed")); 28 | } 29 | } 30 | 31 | export { generateMock }; 32 | -------------------------------------------------------------------------------- /__tests__/e2e/kotlin-wms.test.mjs: -------------------------------------------------------------------------------- 1 | import { spawn } from "child_process"; 2 | import { promises as fs } from "fs"; 3 | import path from "path"; 4 | import { fileURLToPath } from "url"; 5 | import { cleanOutputDir, generate } from "./utils.mjs"; 6 | 7 | const __filename = fileURLToPath(import.meta.url); 8 | const __dirname = path.dirname(__filename); 9 | const configPath = path.join(__dirname, "kotlin-wms.config.json"); 10 | 11 | describe("E2E: Kotlin WMS API Tests", () => { 12 | beforeEach(async () => { 13 | await cleanOutputDir(configPath); 14 | }); 15 | 16 | afterEach(async () => { 17 | await cleanOutputDir(configPath); 18 | }); 19 | 20 | test("should generate Kotlin WMS API files correctly", async () => { 21 | const generatedFiles = await generate(configPath); 22 | 23 | // For Kotlin tests, verify the generation completed successfully 24 | // Note: Kotlin files have different structure than TypeScript 25 | expect(generatedFiles).toBeDefined(); 26 | 27 | // Create snapshot for Kotlin files 28 | expect(generatedFiles).toMatchSnapshot("kotlin-wms-generated-files"); 29 | }, 30000); 30 | }); 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Hossein Mohammadi 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /__tests__/main/excludes-include.test.mjs: -------------------------------------------------------------------------------- 1 | import { cleanOutputDir, generator } from "./utils.mjs"; 2 | import swaggerJson from "./swagger.json"; 3 | 4 | describe("excludes and include", () => { 5 | beforeAll(async () => { 6 | await cleanOutputDir("./__tests__/main/outputs/excludes/temp2"); 7 | }); 8 | 9 | afterEach(async () => { 10 | await cleanOutputDir("./__tests__/main/outputs/excludes/temp2"); 11 | }); 12 | 13 | test("only includes get methods does not ends with Id", async () => { 14 | const { 15 | "services.ts": code, 16 | "hooks.ts": hooks, 17 | "types.ts": type, 18 | } = await generator( 19 | { 20 | url: "./__tests__/main/outputs/excludes/temp2/swagger.json", 21 | dir: "./__tests__/main/outputs/excludes/temp2", 22 | reactHooks: true, 23 | includes: ["^get"], 24 | excludes: ["Id$"], 25 | }, 26 | swaggerJson, 27 | ); 28 | 29 | expect(code).toMatchSnapshot("generate Code"); 30 | expect(hooks).toMatchSnapshot("generate hooks"); 31 | expect(type).toMatchSnapshot("generate type"); 32 | }, 10000); // 10 seconds timeout for generation with includes/excludes filters 33 | }); 34 | -------------------------------------------------------------------------------- /src/javascript/mock/response.mts: -------------------------------------------------------------------------------- 1 | import { 2 | Method, 3 | Config, 4 | SwaggerJson, 5 | SwaggerRequest, 6 | SwaggerResponse, 7 | } from "../../types.mjs"; 8 | import { generateServiceName } from "../utils.mjs"; 9 | 10 | export const APPLICATION_JSON = "application/json"; 11 | 12 | export type ResponsesType = { 13 | [path: string]: { 14 | path: string; 15 | method: Method; 16 | response: { 17 | [status: string]: SwaggerResponse["content"]; 18 | }; 19 | }; 20 | }; 21 | 22 | export const extractResponses = ( 23 | input: SwaggerJson, 24 | config: Config, 25 | ): ResponsesType => { 26 | const ret: ResponsesType = {}; 27 | Object.entries(input.paths).forEach(([path, value]) => { 28 | Object.entries(value).forEach( 29 | ([method, options]: [string, SwaggerRequest]) => { 30 | const { operationId, responses } = options; 31 | const response: { [x: string]: any } = {}; 32 | Object.keys(responses).forEach((statusCode: string) => { 33 | const { content } = responses[statusCode]; 34 | response[statusCode] = content; 35 | }); 36 | const key = generateServiceName(path, method, operationId, config); 37 | 38 | ret[key] = { 39 | method: method as Method, 40 | path, 41 | response, 42 | }; 43 | }, 44 | ); 45 | }); 46 | return ret; 47 | }; 48 | -------------------------------------------------------------------------------- /__tests__/e2e/slack-api.test.mjs: -------------------------------------------------------------------------------- 1 | import { spawn } from "child_process"; 2 | import { promises as fs } from "fs"; 3 | import path from "path"; 4 | import { fileURLToPath } from "url"; 5 | import { cleanOutputDir, generate } from "./utils.mjs"; 6 | 7 | const __filename = fileURLToPath(import.meta.url); 8 | const __dirname = path.dirname(__filename); 9 | const configPath = path.join(__dirname, "slack-api.config.json"); 10 | 11 | describe("E2E: Slack API Tests", () => { 12 | beforeEach(async () => { 13 | await cleanOutputDir(configPath); 14 | }); 15 | 16 | afterEach(async () => { 17 | await cleanOutputDir(configPath); 18 | }); 19 | 20 | test("should generate Slack API files with infinite query support", async () => { 21 | const generatedFiles = await generate(configPath); 22 | 23 | // Verify critical content exists 24 | expect(generatedFiles["services.ts"]).toContain("export const"); 25 | expect(generatedFiles["types.ts"]).toContain("export interface"); 26 | expect(generatedFiles["hooks.ts"]).toContain("useQuery"); 27 | expect(generatedFiles["config.ts"]).toContain("SwaggerResponse"); 28 | expect(generatedFiles["httpRequest.ts"]).toContain("axios"); 29 | expect(generatedFiles["hooksConfig.ts"]).toContain("paginationFlattenData"); 30 | 31 | // Create snapshot of all generated files 32 | delete generatedFiles["swagger.json"]; 33 | expect(generatedFiles).toMatchSnapshot("slack-api-generated-files"); 34 | }, 30000); 35 | }); 36 | -------------------------------------------------------------------------------- /__tests__/e2e/petstore3.test.mjs: -------------------------------------------------------------------------------- 1 | import { spawn } from "child_process"; 2 | import { promises as fs } from "fs"; 3 | import path from "path"; 4 | import { fileURLToPath } from "url"; 5 | import { cleanOutputDir, generate } from "./utils.mjs"; 6 | 7 | const __filename = fileURLToPath(import.meta.url); 8 | const __dirname = path.dirname(__filename); 9 | const configPath = path.join(__dirname, "petstore3.config.json"); 10 | 11 | describe("E2E: Petstore3 API Tests", () => { 12 | beforeEach(async () => { 13 | await cleanOutputDir(configPath); 14 | }); 15 | 16 | afterEach(async () => { 17 | await cleanOutputDir(configPath); 18 | }); 19 | 20 | test("should generate Petstore3 API files with correct enum handling", async () => { 21 | const generatedFiles = await generate(configPath); 22 | 23 | // Verify critical content exists 24 | expect(generatedFiles["services.ts"]).toContain("export const"); 25 | expect(generatedFiles["types.ts"]).toContain("export interface"); 26 | expect(generatedFiles["hooks.ts"]).toContain("useQuery"); 27 | expect(generatedFiles["config.ts"]).toContain("SwaggerResponse"); 28 | expect(generatedFiles["httpRequest.ts"]).toContain("axios"); 29 | expect(generatedFiles["hooksConfig.ts"]).toContain("paginationFlattenData"); 30 | 31 | // Create snapshot of all generated files 32 | delete generatedFiles["swagger.json"]; 33 | expect(generatedFiles).toMatchSnapshot("petstore3-generated-files"); 34 | }, 30000); 35 | }); 36 | -------------------------------------------------------------------------------- /__tests__/enums.test.mjs: -------------------------------------------------------------------------------- 1 | import { cleanOutputDir, generator } from "./main/utils.mjs"; 2 | import swaggerJson from "./swagger.json"; 3 | 4 | describe("enums", () => { 5 | beforeAll(async () => { 6 | await cleanOutputDir("./__tests__/outputs/enums"); 7 | }); 8 | 9 | afterEach(async () => { 10 | await cleanOutputDir("./__tests__/outputs/enums"); 11 | }); 12 | 13 | test("generate enum as type", async () => { 14 | const { 15 | "services.ts": code, 16 | "hooks.ts": hooks, 17 | "types.ts": type, 18 | } = await generator( 19 | { 20 | url: "./__tests__/outputs/enums/swagger.json", 21 | dir: "./__tests__/outputs/enums", 22 | generateEnumAsType: true, 23 | }, 24 | swaggerJson, 25 | ); 26 | 27 | expect(code).toMatchSnapshot("generate Code"); 28 | expect(hooks).toMatchSnapshot("generate hooks"); 29 | expect(type).toMatchSnapshot("generate type"); 30 | }); 31 | 32 | test("generate enum", async () => { 33 | const { 34 | "services.ts": code, 35 | "hooks.ts": hooks, 36 | "types.ts": type, 37 | } = await generator( 38 | { 39 | url: "./__tests__/outputs/enums/swagger.json", 40 | dir: "./__tests__/outputs/enums", 41 | }, 42 | swaggerJson, 43 | ); 44 | 45 | expect(code).toMatchSnapshot("generate Code"); 46 | expect(hooks).toMatchSnapshot("generate hooks"); 47 | expect(type).toMatchSnapshot("generate type"); 48 | }); 49 | }); 50 | -------------------------------------------------------------------------------- /__tests__/e2e/local-openapi.test.mjs: -------------------------------------------------------------------------------- 1 | import { spawn } from "child_process"; 2 | import { promises as fs } from "fs"; 3 | import path from "path"; 4 | import { fileURLToPath } from "url"; 5 | import { cleanOutputDir, generate } from "./utils.mjs"; 6 | 7 | const __filename = fileURLToPath(import.meta.url); 8 | const __dirname = path.dirname(__filename); 9 | const configPath = path.join(__dirname, "local-openapi.config.json"); 10 | 11 | describe("E2E: Local OpenAPI Tests", () => { 12 | beforeEach(async () => { 13 | await cleanOutputDir(configPath); 14 | }); 15 | 16 | afterEach(async () => { 17 | await cleanOutputDir(configPath); 18 | }); 19 | 20 | test("should generate Django Allauth API files correctly", async () => { 21 | const generatedFiles = await generate(configPath); 22 | 23 | // Verify critical content exists 24 | expect(generatedFiles["services.ts"]).toContain("export const"); 25 | expect(generatedFiles["types.ts"]).toContain("export interface"); 26 | expect(generatedFiles["hooks.ts"]).toContain("useQuery"); 27 | expect(generatedFiles["config.ts"]).toContain("SwaggerResponse"); 28 | expect(generatedFiles["httpRequest.ts"]).toContain("axios"); 29 | expect(generatedFiles["hooksConfig.ts"]).toContain("paginationFlattenData"); 30 | 31 | // Create snapshot of all generated files for local OpenAPI 32 | delete generatedFiles["swagger.json"]; 33 | expect(generatedFiles).toMatchSnapshot("local-openapi-generated-files"); 34 | }, 30000); 35 | }); 36 | -------------------------------------------------------------------------------- /__tests__/e2e/local-openapi-2.test.mjs: -------------------------------------------------------------------------------- 1 | import { spawn } from "child_process"; 2 | import { promises as fs } from "fs"; 3 | import path from "path"; 4 | import { fileURLToPath } from "url"; 5 | import { cleanOutputDir, generate } from "./utils.mjs"; 6 | 7 | const __filename = fileURLToPath(import.meta.url); 8 | const __dirname = path.dirname(__filename); 9 | const configPath = path.join(__dirname, "local-openapi-2.config.json"); 10 | 11 | describe("E2E: Local OpenAPI Tests", () => { 12 | beforeEach(async () => { 13 | await cleanOutputDir(configPath); 14 | }); 15 | 16 | afterEach(async () => { 17 | await cleanOutputDir(configPath); 18 | }); 19 | 20 | test("should generate Django Allauth API files correctly", async () => { 21 | const generatedFiles = await generate(configPath); 22 | 23 | // Verify critical content exists 24 | expect(generatedFiles["services.ts"]).toContain("export const"); 25 | expect(generatedFiles["types.ts"]).toContain("export interface"); 26 | expect(generatedFiles["hooks.ts"]).toContain("useQuery"); 27 | expect(generatedFiles["config.ts"]).toContain("SwaggerResponse"); 28 | expect(generatedFiles["httpRequest.ts"]).toContain("axios"); 29 | expect(generatedFiles["hooksConfig.ts"]).toContain("paginationFlattenData"); 30 | 31 | // Create snapshot of all generated files for local OpenAPI 32 | delete generatedFiles["swagger.json"]; 33 | expect(generatedFiles).toMatchSnapshot("local-openapi-generated-files"); 34 | }, 60000); // Increased to 60 seconds for large OpenAPI file (2MB, 58K+ lines) 35 | }); 36 | -------------------------------------------------------------------------------- /src/javascript/files/hooksConfig.mts: -------------------------------------------------------------------------------- 1 | import { AUTOGENERATED_COMMENT } from "../strings.mjs"; 2 | 3 | const getHooksConfigFile = () => `${AUTOGENERATED_COMMENT} 4 | import type {SwaggerResponse} from "./config"; 5 | 6 | 7 | type GetDataType< 8 | T extends Array>, 9 | K extends string = "data" | "list" 10 | > = T extends Array> 11 | ? D extends { 12 | [P in K]?: infer R1; 13 | } 14 | ? R1 15 | : D extends Array 16 | ? D 17 | : never 18 | : never; 19 | 20 | const paginationFlattenData = >>( 21 | pages?: T, 22 | ): GetDataType | undefined => 23 | pages?.flatMap((page) => 24 | Array.isArray(page.data) 25 | ? page.data 26 | : Array.isArray(page.data?.data) 27 | ? page.data.data 28 | : Array.isArray(page.data?.list) 29 | ? page.data.list 30 | : [], 31 | ) as any; 32 | 33 | const getTotal = >>( 34 | pages?: T, 35 | ): number | undefined => { 36 | return pages && pages[pages.length - 1]?.data?.total; 37 | }; 38 | 39 | 40 | const getPageSize = (queryParams?: any): number | undefined => { 41 | const pageSize = Object.entries(queryParams || {}).find(([key, _value]) => { 42 | if ( 43 | key.toLowerCase() === "pagesize" || 44 | key.toLowerCase() === "pagenumber" 45 | ) { 46 | return true; 47 | } 48 | return false; 49 | }); 50 | 51 | return (pageSize?.[1] || 10) as number; 52 | }; 53 | 54 | export { 55 | paginationFlattenData, 56 | getTotal, 57 | getPageSize, 58 | };`; 59 | 60 | export default getHooksConfigFile; 61 | -------------------------------------------------------------------------------- /src/utilities/jsdoc.mts: -------------------------------------------------------------------------------- 1 | import { JsdocAST } from "../types.mjs"; 2 | 3 | function assignToDescription(params: JsdocAST) { 4 | if (Object.values(params).every((v) => !v)) { 5 | return undefined; 6 | } 7 | 8 | const { 9 | description, 10 | title, 11 | format, 12 | maxLength, 13 | minLength, 14 | max, 15 | min, 16 | minimum, 17 | maximum, 18 | pattern, 19 | } = params; 20 | 21 | return `${ 22 | title 23 | ? ` 24 | * ${title} 25 | * ` 26 | : "" 27 | }${ 28 | description 29 | ? ` 30 | * ${description}` 31 | : "" 32 | }${ 33 | format 34 | ? ` 35 | * - Format: ${format}` 36 | : "" 37 | }${ 38 | maxLength 39 | ? ` 40 | * - maxLength: ${maxLength}` 41 | : "" 42 | }${ 43 | minLength 44 | ? ` 45 | * - minLength: ${minLength}` 46 | : "" 47 | }${ 48 | min 49 | ? ` 50 | * - min: ${min}` 51 | : "" 52 | }${ 53 | max 54 | ? ` 55 | * - max: ${max}` 56 | : "" 57 | }${ 58 | minimum 59 | ? ` 60 | * - minimum: ${minimum}` 61 | : "" 62 | }${ 63 | maximum 64 | ? ` 65 | * - max: ${maximum}` 66 | : "" 67 | }${ 68 | pattern 69 | ? ` 70 | * - pattern: ${pattern}` 71 | : "" 72 | }`; 73 | } 74 | 75 | function getJsdoc(doc: JsdocAST) { 76 | const descriptionWithDetails = assignToDescription(doc); 77 | 78 | return doc.deprecated || descriptionWithDetails || doc.example 79 | ? ` 80 | /**${ 81 | descriptionWithDetails 82 | ? ` 83 | * ${normalizeDescription(descriptionWithDetails)}` 84 | : "" 85 | }${ 86 | doc.deprecated 87 | ? ` 88 | * @deprecated ${normalizeDescription(doc.deprecated) || ""}` 89 | : "" 90 | }${ 91 | doc.example 92 | ? ` 93 | * @example 94 | * ${doc.example}` 95 | : "" 96 | } 97 | */ 98 | ` 99 | : ""; 100 | } 101 | 102 | function normalizeDescription(str?: string) { 103 | return str?.replace(/\*\//g, "*\\/"); 104 | } 105 | 106 | export { getJsdoc }; 107 | -------------------------------------------------------------------------------- /__tests__/main/utils.mjs: -------------------------------------------------------------------------------- 1 | import { spawn } from "child_process"; 2 | import { promises as fs } from "fs"; 3 | import path from "path"; 4 | import { fileURLToPath } from "url"; 5 | import { generate } from "../../lib/index.mjs"; 6 | 7 | const __filename = fileURLToPath(import.meta.url); 8 | const __dirname = path.dirname(__filename); 9 | const rootDir = path.resolve(__dirname, "../../"); 10 | // Helper function to clean up output directory 11 | export const cleanOutputDir = async (dirPath) => { 12 | try { 13 | await fs.rm(path.join(rootDir, dirPath), { recursive: true, force: true }); 14 | } catch (error) { 15 | // Directory might not exist, that's okay 16 | } 17 | }; 18 | 19 | export const generator = async (config, swaggerJson) => { 20 | // write swagger.json in a temporary file 21 | const tempSwaggerJsonPath = path.join(rootDir, config.dir, "swagger.json"); 22 | await fs.mkdir(path.join(rootDir, config.dir), { recursive: true }); 23 | 24 | await fs.writeFile(tempSwaggerJsonPath, JSON.stringify(swaggerJson, null, 2)); 25 | 26 | await generate(config); 27 | 28 | // Helper function to read file content 29 | const readFileContent = async (fileName) => { 30 | try { 31 | return await fs.readFile(path.join(config.dir, fileName), "utf8"); 32 | } catch { 33 | return ""; 34 | } 35 | }; 36 | 37 | // Create snapshots for key generated files 38 | const generatedFiles = { 39 | "services.ts": await readFileContent("services.ts"), 40 | "types.ts": await readFileContent("types.ts"), 41 | "hooks.ts": await readFileContent("hooks.ts"), 42 | "config.ts": await readFileContent("config.ts"), 43 | "httpRequest.ts": await readFileContent("httpRequest.ts"), 44 | "hooksConfig.ts": await readFileContent("hooksConfig.ts"), 45 | }; 46 | 47 | // Parse and normalize swagger.json for consistent formatting 48 | const swaggerJsonContent = await readFileContent("swagger.json"); 49 | if (swaggerJsonContent) { 50 | try { 51 | const swaggerJson = JSON.parse(swaggerJsonContent); 52 | generatedFiles["swagger.json"] = JSON.stringify(swaggerJson, null, 2); 53 | } catch { 54 | generatedFiles["swagger.json"] = swaggerJsonContent; 55 | } 56 | } 57 | 58 | return generatedFiles; 59 | }; 60 | -------------------------------------------------------------------------------- /src/kotlin/generateTypes.mts: -------------------------------------------------------------------------------- 1 | import { getClassBody, getKotlinType, getRefName, getSchemaName } from "./utils.mjs"; 2 | import type { Schema, Config, TypeAST } from "../types.mjs"; 3 | import { AUTOGENERATED_COMMENT } from "./strings.mjs"; 4 | import { getJsdoc } from "../utilities/jsdoc.mjs"; 5 | import { isAscending } from "../utils.mjs"; 6 | 7 | function generateTypes(types: TypeAST[], config: Config): string { 8 | let code = AUTOGENERATED_COMMENT; 9 | try { 10 | code += types 11 | .sort(({ name }, { name: _name }) => isAscending(name, _name)) 12 | .reduce((prev, { name: _name, schema, description }) => { 13 | const name = getSchemaName(_name); 14 | prev += ` 15 | ${getJsdoc({ 16 | ...schema, 17 | description: description || schema?.description, 18 | deprecated: schema?.deprecated 19 | ? schema?.["x-deprecatedMessage"] || String(schema?.deprecated) 20 | : undefined, 21 | })} 22 | ${getTypeDefinition(name, schema, config)}`; 23 | 24 | return prev; 25 | }, ""); 26 | 27 | return code; 28 | } catch (error) { 29 | console.error({ error }); 30 | return ""; 31 | } 32 | } 33 | 34 | function getTypeDefinition(name: string, schema: Schema = {}, config: Config) { 35 | const { 36 | type, 37 | enum: Enum, 38 | "x-enumNames": enumNames, 39 | allOf, 40 | oneOf, 41 | items, 42 | $ref, 43 | additionalProperties, 44 | properties, 45 | } = schema; 46 | 47 | if (Enum) { 48 | return `enum ${name} {${Enum.map( 49 | (e, index) => 50 | `${enumNames ? enumNames[index] : e}=${ 51 | typeof e === "string" ? `"${e}"` : `${e}` 52 | }`, 53 | )}}`; 54 | } 55 | 56 | if (allOf || oneOf) { 57 | return `union ${name} {${getKotlinType(schema, config)}}`; 58 | } 59 | 60 | if (type === "array" && items) { 61 | return `type ${name} = ${getKotlinType(items, config)}[]`; 62 | } 63 | 64 | if ($ref) { 65 | return `type ${name} ${getRefName($ref)}`; 66 | } 67 | 68 | if (type === "object") { 69 | const typeObject = getClassBody(schema, config); 70 | 71 | return `data class ${name}(${typeObject}){}`; 72 | } 73 | 74 | if (type === "string") { 75 | return `union ${name} {${type}}`; 76 | } 77 | 78 | return `union ${name} {Any}`; 79 | } 80 | 81 | export { generateTypes }; 82 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | lib/ 9 | test/ 10 | # Diagnostic reports (https://nodejs.org/api/report.html) 11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | *.pid.lock 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | *.lcov 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 30 | .grunt 31 | 32 | # Bower dependency directory (https://bower.io/) 33 | bower_components 34 | 35 | # node-waf configuration 36 | .lock-wscript 37 | 38 | # Compiled binary addons (https://nodejs.org/api/addons.html) 39 | build/Release 40 | 41 | # Dependency directories 42 | node_modules/ 43 | jspm_packages/ 44 | 45 | # TypeScript v1 declaration files 46 | typings/ 47 | 48 | # TypeScript cache 49 | *.tsbuildinfo 50 | 51 | # Optional npm cache directory 52 | .npm 53 | 54 | # Optional eslint cache 55 | .eslintcache 56 | 57 | # Microbundle cache 58 | .rpt2_cache/ 59 | .rts2_cache_cjs/ 60 | .rts2_cache_es/ 61 | .rts2_cache_umd/ 62 | 63 | # Optional REPL history 64 | .node_repl_history 65 | 66 | # Output of 'npm pack' 67 | *.tgz 68 | 69 | # Yarn Integrity file 70 | .yarn-integrity 71 | 72 | # dotenv environment variables file 73 | .env 74 | .env.test 75 | 76 | # parcel-bundler cache (https://parceljs.org/) 77 | .cache 78 | 79 | # Next.js build output 80 | .next 81 | 82 | # Nuxt.js build / generate output 83 | .nuxt 84 | dist 85 | 86 | # Gatsby files 87 | .cache/ 88 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 89 | # https://nextjs.org/blog/next-9-1#public-directory-support 90 | # public 91 | 92 | # vuepress build output 93 | .vuepress/dist 94 | 95 | # Serverless directories 96 | .serverless/ 97 | 98 | # FuseBox cache 99 | .fusebox/ 100 | 101 | # DynamoDB Local files 102 | .dynamodb/ 103 | 104 | # TernJS port file 105 | .tern-port 106 | 107 | # vscode 108 | .vscode 109 | 110 | # MacOS 111 | .DS_Store 112 | 113 | # E2E test outputs 114 | __tests__/e2e/outputs/ 115 | __tests__/main/outputs/ 116 | __tests__/outputs/ 117 | __tests__/e2e/error-output/ -------------------------------------------------------------------------------- /src/javascript/mock/compose.mts: -------------------------------------------------------------------------------- 1 | import { ResponsesType } from "./response.mjs"; 2 | import { Schemas } from "./schema.mjs"; 3 | import { normalizePath } from "./util.mjs"; 4 | import { REF, parseObject, parseArray } from "./parse.mjs"; 5 | import { isObject, isArray, DataType } from "./dataType.mjs"; 6 | import { getRefName } from "../utils.mjs"; 7 | 8 | type MockData = { 9 | [path: string]: any; 10 | }; 11 | 12 | // Compose mock data 13 | export const composeMockData = ( 14 | responses: ResponsesType, 15 | schemas: Schemas, 16 | ): MockData => { 17 | const ret: any = {}; 18 | Object.keys(responses).forEach((path) => { 19 | const res = responses[path]; 20 | const pathKey = normalizePath(path); 21 | let response: any = ""; 22 | if (!res) { 23 | return; 24 | } 25 | 26 | Object.entries(res.response).forEach(([status, content]) => { 27 | const val = 28 | content?.["application/json"] || 29 | content?.["application/octet-stream"] || 30 | content?.["multipart/form-data"]; 31 | 32 | if (!val) { 33 | return; 34 | } 35 | 36 | if (val?.schema) { 37 | const { schema } = val; 38 | const ref = schema[REF]; 39 | if (ref) { 40 | const schemaName = getRefName(ref); 41 | if (schemaName) { 42 | response = schemas[schemaName]; 43 | } 44 | } else { 45 | if (isObject(schema)) { 46 | response = parseObject(schema, schemas); 47 | } else if (isArray(schema)) { 48 | response = parseArray(schema, schemas); 49 | } else if (schema.properties) { 50 | response = schema.properties; 51 | } else if (schema.type) { 52 | response = DataType.defaultValue(schema); 53 | } 54 | } 55 | } else if (val.example) { 56 | response = val.example; 57 | } else if (val.examples) { 58 | const examplesKey = Object.keys(val.examples); 59 | if (examplesKey.length <= 1) { 60 | response = val.examples; 61 | } else { 62 | // for (const [key, example] of Object.entries(val.examples)) { 63 | // const extendedPathKey = pathKey + "_" + normalizeName(key); 64 | // response = example["value"]; 65 | // } 66 | } 67 | } 68 | 69 | ret[pathKey] = { 70 | method: res.method, 71 | path: res.path, 72 | response: { [status]: response }, 73 | }; 74 | }); 75 | }); 76 | return ret; 77 | }; 78 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "swagger-typescript", 3 | "version": "6.10.0", 4 | "description": "An auto ts/js code generate from swagger/openApi", 5 | "main": "lib/index.mjs", 6 | "bin": { 7 | "swag-ts": "./bin/index.mjs" 8 | }, 9 | "type": "module", 10 | "scripts": { 11 | "prepare": "husky install && tsc", 12 | "test:api": "yarn prepare && node ./bin/index.mjs", 13 | "lint": "eslint src/**/*.ts", 14 | "eslint-fix": "eslint src/**/*.ts --fix", 15 | "prettier-fix": "prettier --write \"**/*.{ts,tsx}\"", 16 | "test": "yarn prepare && cross-env NODE_OPTIONS=--experimental-vm-modules npx jest", 17 | "release": "yarn prepare && yarn test && standard-version && git push --follow-tags origin master && npm publish" 18 | }, 19 | "repository": { 20 | "type": "git", 21 | "url": "git+https://github.com/hosseinmd/swagger-typescript.git" 22 | }, 23 | "keywords": [ 24 | "swagger", 25 | "typescript", 26 | "nswag", 27 | "openApi 3", 28 | "swagger 2", 29 | "code generator", 30 | "hossein mohammadi" 31 | ], 32 | "author": "Hossein mohammadi", 33 | "license": "MIT", 34 | "bugs": { 35 | "url": "https://github.com/hosseinmd/swagger-typescript/issues" 36 | }, 37 | "homepage": "https://github.com/hosseinmd/swagger-typescript#readme", 38 | "dependencies": { 39 | "chalk": "^5.0.1", 40 | "form-data": "^4.0.0", 41 | "js-yaml": "^4.1.0", 42 | "postman-ke-openapi": "^1.0.1", 43 | "qs": "^6.11.0", 44 | "recursive-readdir": "^2.2.3", 45 | "swagger2openapi": "^7.0.8", 46 | "tsc-prog": "^2.2.1", 47 | "yargs": "^17.5.1" 48 | }, 49 | "devDependencies": { 50 | "@commitlint/config-conventional": "^17.0.3", 51 | "@tanstack/react-query": "^5.25.0", 52 | "@types/jest": "^28.1.6", 53 | "@types/js-yaml": "^4.0.5", 54 | "@types/node": "^14.14.22", 55 | "@types/prettier": "^2.6.3", 56 | "@types/qs": "^6.9.7", 57 | "@typescript-eslint/eslint-plugin": "^5.30.7", 58 | "@typescript-eslint/parser": "^5.30.7", 59 | "axios": "^0.27.2", 60 | "commitlint": "^17.0.3", 61 | "cross-env": "^7.0.3", 62 | "eslint": "^8.20.0", 63 | "eslint-plugin-prettier": "^4.2.1", 64 | "husky": "^8.0.1", 65 | "jest": "^28.1.3", 66 | "prettier": "^3.0.0", 67 | "prettier-plugin-jsdoc": "^1.0.1", 68 | "standard-version": "^9.5.0", 69 | "ts-jest": "^28.0.7", 70 | "ts-node": "^10.9.1", 71 | "typescript": "^4.9.3" 72 | }, 73 | "peerDependencies": { 74 | "@tanstack/react-query": ">=4.8", 75 | "axios": ">=0.27", 76 | "prettier": ">=2.2" 77 | }, 78 | "files": [ 79 | "lib", 80 | "files", 81 | "bin" 82 | ], 83 | "packageManager": "yarn@1.22.22" 84 | } 85 | -------------------------------------------------------------------------------- /schema/v6.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-07/schema#", 3 | "properties": { 4 | "url": { 5 | "description": "Path of openApi, could be file or internet address", 6 | "oneOf": [ 7 | { 8 | "type": "string" 9 | }, 10 | { 11 | "type": "array", 12 | "items": { 13 | "type": "object", 14 | "properties": { 15 | "branch": { 16 | "type": "string" 17 | }, 18 | "url": { 19 | "type": "string" 20 | } 21 | }, 22 | "required": ["branch", "url"] 23 | } 24 | } 25 | ] 26 | }, 27 | "dir": { 28 | "type": "string", 29 | "filePathExists": true, 30 | "description": "Destination folder which generated file will be there. Start from root of project" 31 | }, 32 | "keepJson": { 33 | "type": "boolean", 34 | "default": false, 35 | "description": "Useful when you want to keep a json version in your local" 36 | }, 37 | "reactHooks": { 38 | "type": "boolean", 39 | "default": false, 40 | "description": "Generate react hooks for every api" 41 | }, 42 | "useQuery": { 43 | "type": "array", 44 | "items": { 45 | "type": "string" 46 | }, 47 | "description": "If an api is post but you want to generate it as get Api add name of it hook to this array" 48 | }, 49 | "useInfiniteQuery": { 50 | "type": "array", 51 | "items": { 52 | "type": "string" 53 | }, 54 | "description": "To infinity load functionality for your api hook add hook name to this array" 55 | }, 56 | "language": { 57 | "type": "string", 58 | "enum": ["javascript", "typescript", "kotlin"], 59 | "description": "Target language" 60 | }, 61 | "prettierPath": { 62 | "type": "string", 63 | "description": "Path of prettier config file" 64 | }, 65 | "methodName": { 66 | "type": "string", 67 | "description": "For customizing method name" 68 | }, 69 | "prefix": { 70 | "type": "string", 71 | "description": "Prefix of Apis which you want to remove for example: `/api`" 72 | }, 73 | "ignore": { 74 | "type": "object", 75 | "properties": { 76 | "headerParams": { 77 | "type": "array", 78 | "items": { 79 | "type": "string" 80 | } 81 | } 82 | }, 83 | "description": "For ignore property from all of api to not generating" 84 | }, 85 | "generateEnumAsType": { 86 | "type": "boolean" 87 | }, 88 | "kotlinPackage": { 89 | "description": "Package name of distance", 90 | "type": "string" 91 | } 92 | }, 93 | "required": ["url", "dir"] 94 | } 95 | -------------------------------------------------------------------------------- /src/javascript/mock/dataType.mts: -------------------------------------------------------------------------------- 1 | import { Schema } from "../../types.mjs"; 2 | 3 | let guid = 0; 4 | 5 | export enum DataType { 6 | string = "string", 7 | number = "number", 8 | integer = "integer", 9 | boolean = "boolean", 10 | array = "array", 11 | object = "object", 12 | } 13 | 14 | export namespace DataType { 15 | export function defaultValue(schema: Schema): any { 16 | if (schema.example) { 17 | return schema.example; 18 | } 19 | switch (schema.type) { 20 | case DataType.string: 21 | return getStringDefaultValue(schema); 22 | case DataType.number: 23 | case DataType.integer: 24 | return schema.minimum || schema.maximum || 0; 25 | case DataType.boolean: 26 | return true; 27 | case DataType.array: 28 | return []; 29 | case DataType.object: 30 | return {}; 31 | default: 32 | return {}; 33 | } 34 | } 35 | 36 | export function getStringDefaultValue(schema: Schema): string { 37 | if (schema.enum) { 38 | return schema.enum[0]; 39 | } 40 | 41 | if (schema.format) { 42 | switch (schema.format) { 43 | case "date": 44 | return "2017-07-21"; 45 | case "date-time": 46 | return "2017-07-21T17:32:28Z"; 47 | case "password": 48 | return "password"; 49 | case "byte": 50 | return "U3dhZ2dlciByb2Nrcw=="; 51 | case "binary": 52 | return "binary"; 53 | case "binary": 54 | return "binary"; 55 | case "guid": 56 | case "uuid": 57 | return `3ba89b92-8c02-4e5a-9843-${guid++}`; 58 | default: 59 | return ""; 60 | } 61 | } 62 | // TODO: pattern support 63 | return ""; 64 | } 65 | } 66 | 67 | export const isArray = ( 68 | property: Schema, 69 | ): property is Schema & { items: Schema } => { 70 | return property.type === DataType.array; 71 | }; 72 | 73 | export const isObject = ( 74 | schema: Schema, 75 | ): schema is Schema & { type: "object" } => { 76 | return schema.type === DataType.object || schema.properties !== undefined; 77 | }; 78 | 79 | export const isAllOf = ( 80 | schema: Schema, 81 | ): schema is Schema & { allOf: Schema[] } => { 82 | return schema.allOf !== undefined; 83 | }; 84 | 85 | export const isOneOf = ( 86 | schema: Schema, 87 | ): schema is Schema & { oneOf: Schema[] } => { 88 | return schema.oneOf !== undefined; 89 | }; 90 | 91 | export const isAnyOf = ( 92 | schema: Schema, 93 | ): schema is Schema & { anyOf: Schema[] } => { 94 | return schema.anyOf !== undefined; 95 | }; 96 | 97 | export const isReferenceObject = ( 98 | schema: Schema, 99 | ): schema is Schema & { $ref: string } => { 100 | return "$ref" in schema; 101 | }; 102 | -------------------------------------------------------------------------------- /src/kotlin/generateApis.mts: -------------------------------------------------------------------------------- 1 | import { 2 | getDefineParam, 3 | getParamString, 4 | getDefinitionBody, 5 | getHeaderString, 6 | getKotlinType, 7 | } from "./utils.mjs"; 8 | import { ApiAST, Config, Parameter, TypeAST } from "../types.mjs"; 9 | import { 10 | SERVICE_BEGINNING, 11 | DEPRECATED_WARM_MESSAGE, 12 | } from "./strings.mjs"; 13 | import { getJsdoc } from "../utilities/jsdoc.mjs"; 14 | import { isAscending } from "../utils.mjs"; 15 | 16 | function generateApis( 17 | apis: ApiAST[], 18 | types: TypeAST[], 19 | config: Config, 20 | ): string { 21 | let code = SERVICE_BEGINNING; 22 | try { 23 | const apisCode = apis 24 | .sort(({ serviceName }, { serviceName: _serviceName }) => 25 | isAscending(serviceName, _serviceName), 26 | ) 27 | .reduce( 28 | ( 29 | prev, 30 | { 31 | contentType, 32 | summary, 33 | deprecated, 34 | serviceName, 35 | queryParamsTypeName, 36 | pathParams, 37 | requestBody, 38 | headerParams, 39 | isQueryParamsNullable, 40 | isHeaderParamsNullable, 41 | responses, 42 | method, 43 | endPoint, 44 | pathParamsRefString, 45 | additionalAxiosConfig, 46 | security, 47 | }, 48 | ) => { 49 | return ( 50 | prev + 51 | `${getJsdoc({ 52 | description: summary, 53 | deprecated: deprecated ? DEPRECATED_WARM_MESSAGE : undefined, 54 | })}${(headerParams as Parameter[])?.map( 55 | ({ name, required, description, schema }) => { 56 | return getHeaderString( 57 | name, 58 | required, 59 | getKotlinType(schema, config), 60 | description, 61 | ); 62 | }, 63 | )} 64 | @${method.toUpperCase()}("${endPoint}") 65 | suspend fun ${serviceName}( 66 | ${pathParams 67 | ?.map(({ name, required, schema, description }) => 68 | getDefineParam(name, required, schema, config, description), 69 | ) 70 | .join(",\n")}${pathParams.length ? "," : ""}${ 71 | requestBody 72 | ? ` 73 | ${getDefinitionBody("requestBody", requestBody, config)},` 74 | : "" 75 | }${ 76 | queryParamsTypeName 77 | ? ` 78 | ${getParamString( 79 | "queryParams", 80 | !isQueryParamsNullable, 81 | queryParamsTypeName, 82 | )},` 83 | : "" 84 | } 85 | ): Response<${responses ? getKotlinType(responses, config) : "Any"}> 86 | 87 | ` 88 | ); 89 | }, 90 | "", 91 | ); 92 | 93 | code += ` 94 | package ${config.kotlinPackage} 95 | 96 | import retrofit2.Response 97 | import retrofit2.http.Body 98 | import retrofit2.http.DELETE 99 | import retrofit2.http.GET 100 | import retrofit2.http.PATCH 101 | import retrofit2.http.POST 102 | import retrofit2.http.PUT 103 | import retrofit2.http.Path 104 | 105 | interface IApis { 106 | ${apisCode} 107 | }`; 108 | return code; 109 | } catch (error) { 110 | console.error(error); 111 | return ""; 112 | } 113 | } 114 | 115 | export { generateApis }; 116 | -------------------------------------------------------------------------------- /src/javascript/files/httpRequest.mts: -------------------------------------------------------------------------------- 1 | import { AUTOGENERATED_COMMENT } from "../strings.mjs"; 2 | 3 | const getHttpRequestFile = () => `${AUTOGENERATED_COMMENT} 4 | import axios, { AxiosRequestConfig, CancelToken } from "axios"; 5 | import { getAxiosInstance } from "./config"; 6 | import type { Security, SwaggerResponse } from "./config"; 7 | 8 | /** 9 | * Cancellation handled here, you can cancel request by call promise.cancel() 10 | * 11 | * @example 12 | * const promise = getUsers(); 13 | * setTimeout(() => promise.cancel(), 30000); 14 | * const { data } = await promise; 15 | * 16 | * @param getPromise 17 | * @returns 18 | */ 19 | function cancellation( 20 | getPromise: (cancelToken: CancelToken) => Promise, 21 | ): Promise { 22 | const source = axios.CancelToken.source(); 23 | const promise = getPromise(source.token); 24 | //@ts-ignore 25 | promise.cancel = () => { 26 | source.cancel("request canceled"); 27 | }; 28 | 29 | return promise; 30 | } 31 | 32 | export const Http = { 33 | getRequest( 34 | url: string, 35 | queryParams: any | undefined, 36 | //@ts-ignore 37 | _requestBody: undefined, 38 | security: Security, 39 | configOverride?: AxiosRequestConfig, 40 | ): Promise> { 41 | return cancellation((cancelToken) => 42 | getAxiosInstance(security).get(url, { 43 | cancelToken, 44 | params: queryParams, 45 | ...configOverride, 46 | }), 47 | ); 48 | }, 49 | postRequest( 50 | url: string, 51 | queryParams: any | undefined, 52 | requestBody: any | undefined, 53 | security: Security, 54 | configOverride?: AxiosRequestConfig, 55 | ): Promise> { 56 | return cancellation((cancelToken) => 57 | getAxiosInstance(security).post(url, requestBody, { 58 | cancelToken, 59 | params: queryParams, 60 | ...configOverride, 61 | }), 62 | ); 63 | }, 64 | putRequest( 65 | url: string, 66 | queryParams: any | undefined, 67 | requestBody: any | undefined, 68 | security: Security, 69 | configOverride?: AxiosRequestConfig, 70 | ): Promise> { 71 | return cancellation((cancelToken) => 72 | getAxiosInstance(security).put(url, requestBody, { 73 | cancelToken, 74 | params: queryParams, 75 | ...configOverride, 76 | }), 77 | ); 78 | }, 79 | patchRequest( 80 | url: string, 81 | queryParams: any | undefined, 82 | requestBody: any | undefined, 83 | security: Security, 84 | configOverride?: AxiosRequestConfig, 85 | ): Promise> { 86 | return cancellation((cancelToken) => 87 | getAxiosInstance(security).patch(url, requestBody, { 88 | cancelToken, 89 | params: queryParams, 90 | ...configOverride, 91 | }), 92 | ); 93 | }, 94 | deleteRequest( 95 | url: string, 96 | queryParams: any | undefined, 97 | requestBody: any | undefined, 98 | security: Security, 99 | configOverride?: AxiosRequestConfig, 100 | ): Promise> { 101 | return cancellation((cancelToken) => 102 | getAxiosInstance(security).delete(url, { 103 | data: requestBody, 104 | cancelToken, 105 | params: queryParams, 106 | ...configOverride, 107 | }), 108 | ); 109 | }, 110 | };`; 111 | 112 | export default getHttpRequestFile; 113 | -------------------------------------------------------------------------------- /src/javascript/files/config.mts: -------------------------------------------------------------------------------- 1 | const getConfigFile = ({ baseUrl }: { baseUrl: string }) => `/** 2 | * You can modify this file 3 | * 4 | * @version ${6} 5 | * 6 | */ 7 | import Axios, { 8 | AxiosRequestConfig, 9 | AxiosError, 10 | AxiosResponse, 11 | AxiosInstance, 12 | } from "axios"; 13 | //@ts-ignore 14 | import qs from "qs"; 15 | 16 | const baseConfig: AxiosRequestConfig = { 17 | baseURL: "${baseUrl}", // <--- Add your base url 18 | headers: { 19 | "Content-Encoding": "UTF-8", 20 | Accept: "application/json", 21 | "Content-Type": "application/json-patch+json", 22 | }, 23 | paramsSerializer: (param) => qs.stringify(param, { indices: false }), 24 | }; 25 | 26 | let axiosInstance: AxiosInstance; 27 | 28 | function getAxiosInstance(security: Security): AxiosInstance { 29 | if (!axiosInstance) { 30 | axiosInstance = Axios.create(baseConfig); 31 | 32 | // Response interceptor 33 | axiosInstance.interceptors.response.use( 34 | (async (response: AxiosResponse): Promise> => { 35 | // Any status code that lie within the range of 2xx cause this function to trigger 36 | // Do something with response data 37 | /** 38 | * Example on response manipulation 39 | * 40 | * @example 41 | * const swaggerResponse: SwaggerResponse = { 42 | * ...response, 43 | * }; 44 | * return swaggerResponse; 45 | */ 46 | return response.data; 47 | }) as any, 48 | (error: AxiosError) => { 49 | // Any status codes that falls outside the range of 2xx cause this function to trigger 50 | // Do something with response error 51 | 52 | if (error.response) { 53 | return Promise.reject( 54 | new RequestError( 55 | error.response.data, 56 | error.response.status, 57 | error.response, 58 | ), 59 | ); 60 | } 61 | 62 | if (error.isAxiosError) { 63 | return Promise.reject( 64 | new RequestError( 65 | "noInternetConnection", 66 | ), 67 | ); 68 | } 69 | return Promise.reject(error); 70 | }, 71 | ); 72 | } 73 | 74 | // ًًRequest interceptor 75 | axiosInstance.interceptors.request.use( 76 | async (requestConfig) => { 77 | // Do something before request is sent 78 | /** Example on how to add authorization based on security */ 79 | if (security?.[0]) { 80 | // requestConfig.headers.authorization = ""; 81 | } 82 | 83 | return requestConfig; 84 | }, 85 | (error) => { 86 | // Do something with request error 87 | return Promise.reject(error); 88 | }, 89 | ); 90 | 91 | return axiosInstance; 92 | } 93 | 94 | class RequestError extends Error { 95 | constructor( 96 | public message: string, 97 | public status?: number, 98 | public response?: AxiosResponse, 99 | ) { 100 | super(message); 101 | } 102 | 103 | isApiException = true; 104 | 105 | static isRequestError(error: any): error is RequestError { 106 | return error.isApiException; 107 | } 108 | } 109 | 110 | export type Security = any[] | undefined; 111 | 112 | // export interface SwaggerResponse extends AxiosResponse {} 113 | export type SwaggerResponse = R 114 | 115 | export { 116 | getAxiosInstance, 117 | RequestError, 118 | };`; 119 | 120 | export default getConfigFile; 121 | -------------------------------------------------------------------------------- /__tests__/e2e/utils.mjs: -------------------------------------------------------------------------------- 1 | import { spawn } from "child_process"; 2 | import { promises as fs } from "fs"; 3 | import path from "path"; 4 | import { fileURLToPath } from "url"; 5 | 6 | const __filename = fileURLToPath(import.meta.url); 7 | const __dirname = path.dirname(__filename); 8 | const rootDir = path.resolve(__dirname, "../../"); 9 | const binPath = path.join(rootDir, "bin/index.mjs"); 10 | 11 | // Helper function to run CLI commands 12 | const runCommand = (args = [], options = {}) => { 13 | return new Promise((resolve, reject) => { 14 | const child = spawn("node", [binPath, ...args], { 15 | cwd: rootDir, 16 | stdio: ["pipe", "pipe", "pipe"], 17 | ...options, 18 | }); 19 | 20 | let stdout = ""; 21 | let stderr = ""; 22 | 23 | child.stdout.on("data", (data) => { 24 | stdout += data.toString(); 25 | }); 26 | 27 | child.stderr.on("data", (data) => { 28 | stderr += data.toString(); 29 | }); 30 | 31 | child.on("close", (code) => { 32 | resolve({ 33 | code, 34 | stdout, 35 | stderr, 36 | }); 37 | }); 38 | 39 | child.on("error", (error) => { 40 | reject(error); 41 | }); 42 | 43 | // Set a timeout to prevent hanging 44 | setTimeout(() => { 45 | child.kill(); 46 | reject(new Error("Command timeout")); 47 | }, 60000); // 60 seconds timeout for large OpenAPI files 48 | }); 49 | }; 50 | 51 | // Helper function to check if file exists 52 | const fileExists = async (filePath) => { 53 | try { 54 | await fs.access(filePath); 55 | return true; 56 | } catch { 57 | return false; 58 | } 59 | }; 60 | 61 | // Helper function to clean up output directory 62 | export const cleanOutputDir = async (configPath) => { 63 | const config = JSON.parse(await fs.readFile(configPath, "utf8")); 64 | 65 | try { 66 | await fs.rm(config.dir, { recursive: true, force: true }); 67 | } catch (error) { 68 | // Directory might not exist, that's okay 69 | } 70 | }; 71 | 72 | export const generate = async (configPath) => { 73 | const config = JSON.parse(await fs.readFile(configPath, "utf8")); 74 | const result = await runCommand(["--config", configPath]); 75 | 76 | if (result.code !== 0) { 77 | console.log("result.stderr ", result.stderr); 78 | } 79 | expect(result.code).toBe(0); 80 | 81 | // Helper function to read file content 82 | const readFileContent = async (fileName) => { 83 | try { 84 | return await fs.readFile(path.join(config.dir, fileName), "utf8"); 85 | } catch { 86 | return ""; 87 | } 88 | }; 89 | 90 | // Create snapshots for key generated files 91 | const generatedFiles = { 92 | "services.ts": await readFileContent("services.ts"), 93 | "types.ts": await readFileContent("types.ts"), 94 | "hooks.ts": await readFileContent("hooks.ts"), 95 | "config.ts": await readFileContent("config.ts"), 96 | "httpRequest.ts": await readFileContent("httpRequest.ts"), 97 | "hooksConfig.ts": await readFileContent("hooksConfig.ts"), 98 | }; 99 | 100 | // Parse and normalize swagger.json for consistent formatting 101 | const swaggerJsonContent = await readFileContent("swagger.json"); 102 | if (swaggerJsonContent) { 103 | try { 104 | const swaggerJson = JSON.parse(swaggerJsonContent); 105 | generatedFiles["swagger.json"] = JSON.stringify(swaggerJson, null, 2); 106 | } catch { 107 | generatedFiles["swagger.json"] = swaggerJsonContent; 108 | } 109 | } 110 | return generatedFiles; 111 | }; 112 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | - Using welcoming and inclusive language 18 | - Being respectful of differing viewpoints and experiences 19 | - Gracefully accepting constructive criticism 20 | - Focusing on what is best for the community 21 | - Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | - The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | - Trolling, insulting/derogatory comments, and personal or political attacks 28 | - Public or private harassment 29 | - Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | - Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at hosseinm.developer@gmail.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /src/kotlin/strings.mts: -------------------------------------------------------------------------------- 1 | //@ts-ignore 2 | // import { default as packageJson } from "../../package.json"; 3 | 4 | const AUTOGENERATED_COMMENT = ` 5 | //@ts-nocheck 6 | /** 7 | * AUTO_GENERATED Do not change this file directly, use config.ts file instead 8 | * 9 | * @version ${6} 10 | */ 11 | `; 12 | 13 | const SERVICE_BEGINNING = `${AUTOGENERATED_COMMENT} 14 | 15 | `; 16 | const SERVICE_NEEDED_FUNCTIONS = ` 17 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 18 | const __DEV__ = process.env.NODE_ENV !== "production"; 19 | 20 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 21 | function overrideConfig( 22 | config?: AxiosRequestConfig, 23 | configOverride?: AxiosRequestConfig, 24 | ): AxiosRequestConfig { 25 | return { 26 | ...config, 27 | ...configOverride, 28 | headers: { 29 | ...config?.headers, 30 | ...configOverride?.headers, 31 | }, 32 | }; 33 | } 34 | 35 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 36 | export function template(path: string, obj: { [x: string]: any } = {}) { 37 | Object.keys(obj).forEach((key) => { 38 | const re = new RegExp(\`{\${key}}\`, "i"); 39 | path = path.replace(re, obj[key]); 40 | }); 41 | 42 | return path; 43 | } 44 | 45 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 46 | function objToForm(requestBody: object) { 47 | const formData = new FormData(); 48 | 49 | Object.entries(requestBody).forEach(([key, value]) => { 50 | value && formData.append(key, value); 51 | }); 52 | 53 | return formData; 54 | } 55 | 56 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 57 | function objToUrlencoded(requestBody: object) { 58 | return qs.stringify(requestBody) 59 | } 60 | `; 61 | 62 | const getHooksImports = ({ 63 | hasInfinity, 64 | }: { 65 | hasInfinity?: boolean; 66 | }) => `${AUTOGENERATED_COMMENT} 67 | ${hasInfinity ? `import { useMemo } from "react";` : ""} 68 | import { AxiosRequestConfig } from "axios"; 69 | import { 70 | UseQueryOptions, 71 | useQuery, 72 | useMutation, 73 | UseMutationOptions, 74 | ${ 75 | hasInfinity 76 | ? ` useInfiniteQuery, 77 | UseInfiniteQueryOptions,` 78 | : "" 79 | } 80 | QueryClient, 81 | QueryKey, 82 | } from "@tanstack/react-query"; 83 | import { RequestError, SwaggerResponse } from "./config"; 84 | ${ 85 | hasInfinity 86 | ? `import { paginationFlattenData, getPageSize, getTotal } from "./hooksConfig";` 87 | : "" 88 | } 89 | `; 90 | const getHooksFunctions = ({ hasInfinity }: { hasInfinity?: boolean }) => 91 | hasInfinity 92 | ? ` 93 | const useHasMore = ( 94 | pages: Array> | undefined, 95 | list: any, 96 | queryParams: any, 97 | ) => 98 | useMemo(() => { 99 | if (!pages || (pages && pages.length < 1)) { 100 | return false; 101 | } 102 | 103 | const total = getTotal(pages); 104 | 105 | if (total !== undefined) { 106 | if (list && list.length < total) { 107 | return true; 108 | } 109 | return false; 110 | } 111 | if ( 112 | paginationFlattenData([pages[pages.length - 1]])?.length === getPageSize(queryParams as any) 113 | ) { 114 | return true; 115 | } 116 | 117 | return false; 118 | }, [pages, list, queryParams]); 119 | 120 | ` 121 | : ""; 122 | 123 | const DEPRECATED_WARM_MESSAGE = 124 | "This endpoint deprecated and will be remove. Please use an alternative"; 125 | 126 | export { 127 | AUTOGENERATED_COMMENT, 128 | SERVICE_NEEDED_FUNCTIONS, 129 | SERVICE_BEGINNING, 130 | getHooksFunctions, 131 | getHooksImports, 132 | DEPRECATED_WARM_MESSAGE, 133 | }; 134 | -------------------------------------------------------------------------------- /src/updateJson.mts: -------------------------------------------------------------------------------- 1 | import { default as chalk } from "chalk"; 2 | import type { 3 | SwaggerRequest, 4 | SwaggerJson, 5 | PathItem, 6 | Components, 7 | } from "./types.mjs"; 8 | 9 | function partialUpdateJson( 10 | input: SwaggerJson, 11 | newJson: SwaggerJson, 12 | tag: string[], 13 | ): SwaggerJson { 14 | let refs: string[] = []; 15 | 16 | const filteredPaths = Object.fromEntries( 17 | Object.entries(input.paths).map(([name, value]) => [ 18 | name, 19 | Object.fromEntries( 20 | (Object.entries(value) as [string, SwaggerRequest][]).filter( 21 | ([_, { tags }]) => !tags?.find((item) => tag.find((i) => i === item)), 22 | ), 23 | ), 24 | ]), 25 | ); 26 | 27 | const paths: SwaggerJson["paths"] = { ...filteredPaths }; 28 | Object.entries(newJson.paths).forEach(([endPoint, value]) => { 29 | (Object.entries(value) as [keyof PathItem, SwaggerRequest][]).forEach( 30 | ([method, options]) => { 31 | if (typeof options !== "object") { 32 | return; 33 | } 34 | 35 | if (tag.find((t) => options.tags?.includes(t))) { 36 | refs = refs.concat(findRefs(options)); 37 | 38 | if (!paths[endPoint]) { 39 | paths[endPoint] = { 40 | ...newJson.paths[endPoint], 41 | }; 42 | } 43 | paths[endPoint][method] = options as any; 44 | } 45 | }, 46 | ); 47 | }); 48 | 49 | refs = findRelatedRef(newJson, refs); 50 | 51 | const components = replaceComponents(input, newJson, refs); 52 | 53 | return { 54 | ...input, 55 | paths, 56 | components, 57 | }; 58 | } 59 | 60 | function findRelatedRef(newJson: SwaggerJson, refs: string[]): string[] { 61 | try { 62 | (["schemas", "requestBodies", "parameters"] as const).map((key) => { 63 | if (newJson?.components?.[key]) { 64 | Object.entries(newJson.components[key]!).forEach(([name, schema]) => { 65 | if (refs.includes(name)) { 66 | const schemaRefs = findRefs(schema); 67 | 68 | const newRefs = schemaRefs.filter((ref) => !refs.includes(ref)); 69 | 70 | if (newRefs.length > 0) { 71 | refs = findRelatedRef(newJson, [...refs, ...newRefs]); 72 | } 73 | } 74 | }); 75 | } 76 | }); 77 | } catch (error) { 78 | console.log(chalk.red(error)); 79 | } 80 | 81 | return refs; 82 | } 83 | 84 | function replaceComponents( 85 | input: SwaggerJson, 86 | newJson: SwaggerJson, 87 | refs: string[], 88 | ) { 89 | const components: Components = { 90 | ...input.components, 91 | }; 92 | 93 | (["schemas", "requestBodies", "parameters"] as const).map((key) => { 94 | if (newJson?.components?.[key]) { 95 | Object.entries(newJson.components[key]!).forEach(([name, schema]) => { 96 | if (refs.includes(name)) { 97 | if (!components[key]) { 98 | components[key] = { 99 | ...input.components![key], 100 | } as any; 101 | } 102 | components[key]![name] = schema; 103 | } 104 | }); 105 | } 106 | }); 107 | 108 | return components; 109 | } 110 | 111 | function findRefs( 112 | obj?: Record | string | number | any[], 113 | ): string[] { 114 | if (typeof obj !== "object") { 115 | return []; 116 | } 117 | 118 | if (Array.isArray(obj)) { 119 | return obj.flatMap((value) => { 120 | return findRefs(value); 121 | }); 122 | } 123 | 124 | return Object.entries(obj).flatMap(([key, value]) => { 125 | if (key === "$ref") { 126 | return [value.replace(/#\/components\/[\w]+\//g, "")]; 127 | } 128 | return findRefs(value); 129 | }); 130 | } 131 | 132 | export { partialUpdateJson }; 133 | -------------------------------------------------------------------------------- /src/javascript/mock/parse.mts: -------------------------------------------------------------------------------- 1 | import { Schema } from "../../types.mjs"; 2 | import { getRefName } from "../utils.mjs"; 3 | import { 4 | DataType, 5 | isObject, 6 | isArray, 7 | isAllOf, 8 | isOneOf, 9 | isAnyOf, 10 | isReferenceObject, 11 | } from "./dataType.mjs"; 12 | 13 | export const REF = "$ref"; 14 | type Schemas = { 15 | [schema: string]: Schema; 16 | }; 17 | 18 | export const mergeAllOf = (properties: Schema[], schemas: Schemas): any => { 19 | let ret: any = {}; 20 | properties.forEach((property) => { 21 | if (isReferenceObject(property)) { 22 | const schemaName = getRefName(property.$ref); 23 | if (schemaName) { 24 | const schemaData = getSchemaData(schemas, schemaName); 25 | ret = Object.assign({}, ret, schemaData); 26 | } 27 | } else { 28 | const parsed = parseObject(property, schemas); 29 | ret = Object.assign({}, ret, parsed); 30 | } 31 | }); 32 | return ret; 33 | }; 34 | 35 | export const pickOneOf = (properties: Schema[], schemas: Schemas): any => { 36 | const property = properties[0]; 37 | if (isReferenceObject(property)) { 38 | const schemaName = getRefName(property.$ref); 39 | if (schemaName) { 40 | const schemaData = getSchemaData(schemas, schemaName); 41 | return schemaData; 42 | } 43 | } 44 | const parsed = parseObject(property, schemas); 45 | return Object.assign({}, parsed); 46 | }; 47 | 48 | // Retrieve mock data of schema. 49 | export const getSchemaData = (schemas: Schemas, name: string): Object => { 50 | const schema = schemas[name]; 51 | 52 | if (isReferenceObject(schema)) { 53 | const schemaName = getRefName(schema.$ref); 54 | return schemaName ? getSchemaData(schemas, schemaName) : {}; 55 | } 56 | 57 | if (isAllOf(schema)) { 58 | return mergeAllOf(schema.allOf, schemas); 59 | } else if (isArray(schema)) { 60 | return parseArray(schema, schemas); 61 | } else if (isObject(schema)) { 62 | return parseObject(schema, schemas); 63 | } else if (schema.type) { 64 | return DataType.defaultValue(schema); 65 | } 66 | 67 | return schema; 68 | }; 69 | 70 | export const parseObject = (obj: Schema, schemas: Schemas): any => { 71 | if (obj.example) return obj.example; 72 | if (!obj.properties) { 73 | return {}; 74 | } 75 | return Object.keys(obj.properties).reduce((acc: any, key: string) => { 76 | const property = obj.properties![key]; 77 | if (isReferenceObject(property)) { 78 | const schemaName = getRefName(property[REF]); 79 | if (schemaName) { 80 | const schema = getSchemaData(schemas, schemaName); 81 | acc[key] = schema; 82 | } 83 | return acc; 84 | } 85 | if (isAllOf(property)) { 86 | acc[key] = mergeAllOf(property.allOf, schemas); 87 | } else if (isOneOf(property)) { 88 | acc[key] = pickOneOf(property.oneOf, schemas); 89 | } else if (isAnyOf(property)) { 90 | acc[key] = pickOneOf(property.anyOf, schemas); 91 | } else if (isObject(property)) { 92 | acc[key] = parseObject(property, schemas); 93 | } else if (isArray(property)) { 94 | acc[key] = parseArray(property, schemas); 95 | } else if (property.type) { 96 | acc[key] = DataType.defaultValue(property); 97 | } 98 | return acc; 99 | }, {}); 100 | }; 101 | 102 | export const parseArray = ( 103 | arr: Schema & { items: Schema }, 104 | schemas: Schemas, 105 | ): any => { 106 | if (isReferenceObject(arr.items)) { 107 | const schemaName = getRefName(arr.items[REF]); 108 | if (schemaName) { 109 | const schema = getSchemaData(schemas, schemaName); 110 | return [schema]; 111 | } 112 | return []; 113 | } else if (arr.example) { 114 | return arr.example; 115 | } else if (arr.items.type) { 116 | return [parseObject(arr.items, schemas)]; 117 | } 118 | return []; 119 | }; 120 | -------------------------------------------------------------------------------- /src/utils.mts: -------------------------------------------------------------------------------- 1 | import { existsSync, readFileSync } from "fs"; 2 | import { Config } from "./types.mjs"; 3 | import { exec } from "child_process"; 4 | 5 | function isAscending(a: string, b: string) { 6 | if (a > b) { 7 | return 1; 8 | } 9 | if (b > a) { 10 | return -1; 11 | } 12 | return 0; 13 | } 14 | 15 | function majorVersionsCheck(expectedV: string, inputV?: string) { 16 | if (!inputV) { 17 | throw new Error( 18 | `Swagger-Typescript working with openApi v3/ swagger v2, seem your json is not openApi openApi v3/ swagger v2`, 19 | ); 20 | } 21 | 22 | const expectedVMajor = expectedV.split(".")[0]; 23 | const inputVMajor = inputV.split(".")[0]; 24 | function isValidPart(x: string) { 25 | return /^\d+$/.test(x); 26 | } 27 | if (!isValidPart(expectedVMajor) || !isValidPart(inputVMajor)) { 28 | throw new Error( 29 | `Swagger-Typescript working with openApi v3/ swagger v2 your json openApi version is not valid "${inputV}"`, 30 | ); 31 | } 32 | 33 | const expectedMajorNumber = Number(expectedVMajor); 34 | const inputMajorNumber = Number(inputVMajor); 35 | 36 | if (expectedMajorNumber <= inputMajorNumber) { 37 | return; 38 | } 39 | 40 | throw new Error( 41 | `Swagger-Typescript working with openApi v3/ swagger v2 your json openApi version is ${inputV}`, 42 | ); 43 | } 44 | 45 | function isMatchWholeWord(stringToSearch: string, word: string) { 46 | return new RegExp("\\b" + word + "\\b").test(stringToSearch); 47 | } 48 | 49 | async function getCurrentUrl({ url, branch: branchName }: Config) { 50 | const urls = url as Exclude; 51 | if (!branchName) { 52 | branchName = await execAsync("git branch --show-current"); 53 | 54 | branchName = branchName?.split("/")[0]; 55 | 56 | branchName = urls.find((item) => branchName === item.branch)?.branch; 57 | } 58 | if (!branchName) { 59 | branchName = (await getSourceBranch()).find((treeItem) => 60 | urls.find((item) => treeItem === item.branch), 61 | ) as string; 62 | } 63 | 64 | const currentUrl = 65 | urls.find((item) => branchName === item.branch)?.url || urls[0].url; 66 | 67 | return currentUrl; 68 | } 69 | 70 | async function getSourceBranch() { 71 | const result = await execAsync('git log --format="%D"'); 72 | const branchesTree = result 73 | .split("\n") 74 | .flatMap((item) => item.split(", ")) 75 | .map((branch) => { 76 | branch = branch.trim(); 77 | 78 | branch = branch.replace("HEAD -> ", ""); 79 | branch = branch.trim(); 80 | 81 | return branch; 82 | }); 83 | 84 | return branchesTree; 85 | } 86 | 87 | async function execAsync(command: string) { 88 | return new Promise((resolve, reject) => { 89 | const child = exec(command, (error, stdout) => { 90 | child.kill(); 91 | if (error) { 92 | reject(error); 93 | return; 94 | } 95 | 96 | resolve(stdout); 97 | }); 98 | }); 99 | } 100 | 101 | const cache: { [key: string]: any } = {}; 102 | /** Load Prettier options from config file */ 103 | function getPrettierOptions(config: Config): any { 104 | const configPaths = [ 105 | config.prettierPath, 106 | ".prettierrc", 107 | "prettier.json", 108 | ].filter((path): path is string => Boolean(path)); 109 | 110 | for (const path of configPaths) { 111 | if (existsSync(path)) { 112 | if (cache[path]) { 113 | return cache[path]; 114 | } 115 | const options = JSON.parse(readFileSync(path).toString()); 116 | cache[path] = { parser: "typescript", ...options }; 117 | return cache[path]; 118 | } 119 | } 120 | 121 | return { parser: "typescript" }; 122 | } 123 | 124 | export { 125 | getCurrentUrl, 126 | majorVersionsCheck, 127 | isAscending, 128 | isMatchWholeWord, 129 | getPrettierOptions, 130 | }; 131 | -------------------------------------------------------------------------------- /src/javascript/strings.mts: -------------------------------------------------------------------------------- 1 | //@ts-ignore 2 | // import { default as packageJson } from "../../package.json"; 3 | 4 | const AUTOGENERATED_COMMENT = ` 5 | //@ts-nocheck 6 | /** 7 | * AUTO_GENERATED Do not change this file directly, use config.ts file instead 8 | * 9 | * @version ${6} 10 | */ 11 | `; 12 | 13 | const SERVICE_BEGINNING = `${AUTOGENERATED_COMMENT} 14 | import type { AxiosRequestConfig } from "axios"; 15 | import type { SwaggerResponse } from "./config"; 16 | import { Http } from "./httpRequest"; 17 | //@ts-ignore 18 | import qs from "qs"; 19 | `; 20 | const SERVICE_NEEDED_FUNCTIONS = ` 21 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 22 | const __DEV__ = process.env.NODE_ENV !== "production"; 23 | 24 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 25 | function overrideConfig( 26 | config?: AxiosRequestConfig, 27 | configOverride?: AxiosRequestConfig, 28 | ): AxiosRequestConfig { 29 | return { 30 | ...config, 31 | ...configOverride, 32 | headers: { 33 | ...config?.headers, 34 | ...configOverride?.headers, 35 | }, 36 | }; 37 | } 38 | 39 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 40 | export function template(path: string, obj: { [x: string]: any } = {}) { 41 | Object.keys(obj).forEach((key) => { 42 | const re = new RegExp(\`{\${key}}\`, "i"); 43 | path = path.replace(re, obj[key]); 44 | }); 45 | 46 | return path; 47 | } 48 | 49 | function isFormData(obj: any) { 50 | // This checks for the append method which should exist on FormData instances 51 | return ( 52 | (typeof obj === "object" && 53 | typeof obj.append === "function" && 54 | obj[Symbol.toStringTag] === undefined) || 55 | obj[Symbol.toStringTag] === "FormData" 56 | ); 57 | } 58 | 59 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 60 | function objToForm(requestBody: object) { 61 | if (isFormData(requestBody)) { 62 | return requestBody; 63 | } 64 | const formData = new FormData(); 65 | 66 | Object.entries(requestBody).forEach(([key, value]) => { 67 | value && formData.append(key, value); 68 | }); 69 | 70 | return formData; 71 | } 72 | 73 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 74 | function objToUrlencoded(requestBody: object) { 75 | return qs.stringify(requestBody) 76 | } 77 | `; 78 | 79 | const getHooksImports = ({ 80 | hasInfinity, 81 | }: { 82 | hasInfinity?: boolean; 83 | }) => `${AUTOGENERATED_COMMENT} 84 | ${hasInfinity ? `import { useMemo } from "react";` : ""} 85 | import { AxiosRequestConfig } from "axios"; 86 | import { 87 | UseQueryOptions, 88 | useQuery, 89 | useMutation, 90 | UseMutationOptions, 91 | ${ 92 | hasInfinity 93 | ? ` useInfiniteQuery, 94 | UseInfiniteQueryOptions,` 95 | : "" 96 | } 97 | QueryClient, 98 | QueryKey, 99 | } from "@tanstack/react-query"; 100 | import { RequestError, SwaggerResponse } from "./config"; 101 | ${ 102 | hasInfinity 103 | ? `import { paginationFlattenData, getPageSize, getTotal } from "./hooksConfig";` 104 | : "" 105 | } 106 | `; 107 | const getHooksFunctions = ({ hasInfinity }: { hasInfinity?: boolean }) => 108 | hasInfinity 109 | ? ` 110 | const useHasMore = ( 111 | pages: Array> | undefined, 112 | list: any, 113 | queryParams: any, 114 | ) => 115 | useMemo(() => { 116 | if (!pages || (pages && pages.length < 1)) { 117 | return false; 118 | } 119 | 120 | const total = getTotal(pages); 121 | 122 | if (total !== undefined) { 123 | if (list && list.length < total) { 124 | return true; 125 | } 126 | return false; 127 | } 128 | if ( 129 | paginationFlattenData([pages[pages.length - 1]])?.length === getPageSize(queryParams as any) 130 | ) { 131 | return true; 132 | } 133 | 134 | return false; 135 | }, [pages, list, queryParams]); 136 | 137 | ` 138 | : ""; 139 | 140 | const DEPRECATED_WARM_MESSAGE = 141 | "This endpoint deprecated and will be remove. Please use an alternative"; 142 | 143 | export { 144 | AUTOGENERATED_COMMENT, 145 | SERVICE_NEEDED_FUNCTIONS, 146 | SERVICE_BEGINNING, 147 | getHooksFunctions, 148 | getHooksImports, 149 | DEPRECATED_WARM_MESSAGE, 150 | }; 151 | -------------------------------------------------------------------------------- /src/javascript/generateApis.mts: -------------------------------------------------------------------------------- 1 | import { 2 | getTsType, 3 | getDefineParam, 4 | getParamString, 5 | getSchemaName, 6 | } from "./utils.mjs"; 7 | import { ApiAST, Config, TypeAST } from "../types.mjs"; 8 | import { 9 | SERVICE_BEGINNING, 10 | SERVICE_NEEDED_FUNCTIONS, 11 | DEPRECATED_WARM_MESSAGE, 12 | } from "./strings.mjs"; 13 | import { getJsdoc } from "../utilities/jsdoc.mjs"; 14 | import { isAscending, isMatchWholeWord } from "../utils.mjs"; 15 | 16 | function generateApis( 17 | apis: ApiAST[], 18 | types: TypeAST[], 19 | config: Config, 20 | ): string { 21 | let code = SERVICE_BEGINNING; 22 | try { 23 | const apisCode = apis 24 | .sort(({ serviceName }, { serviceName: _serviceName }) => 25 | isAscending(serviceName, _serviceName), 26 | ) 27 | .reduce( 28 | ( 29 | prev, 30 | { 31 | contentType, 32 | summary, 33 | deprecated, 34 | serviceName, 35 | queryParamsTypeName, 36 | pathParams, 37 | requestBody, 38 | headerParams, 39 | isQueryParamsNullable, 40 | isHeaderParamsNullable, 41 | responses, 42 | method, 43 | endPoint, 44 | pathParamsRefString, 45 | additionalAxiosConfig, 46 | security, 47 | }, 48 | ) => { 49 | return ( 50 | prev + 51 | ` 52 | ${getJsdoc({ 53 | description: summary, 54 | deprecated: deprecated ? DEPRECATED_WARM_MESSAGE : undefined, 55 | })}export const ${serviceName} = ( 56 | ${ 57 | /** Path parameters */ 58 | pathParams 59 | .map(({ name, required, schema, description }) => 60 | getDefineParam(name, required, schema, config, description), 61 | ) 62 | .join(",") 63 | }${pathParams.length > 0 ? "," : ""}${ 64 | /** Request Body */ 65 | requestBody 66 | ? `${getDefineParam("requestBody", true, requestBody, config)},` 67 | : "" 68 | }${ 69 | /** Query parameters */ 70 | queryParamsTypeName 71 | ? `${getParamString( 72 | "queryParams", 73 | !isQueryParamsNullable, 74 | queryParamsTypeName, 75 | )},` 76 | : "" 77 | }${ 78 | /** Header parameters */ 79 | headerParams 80 | ? `${getParamString( 81 | "headerParams", 82 | !isHeaderParamsNullable, 83 | headerParams as string, 84 | )},` 85 | : "" 86 | }configOverride?:AxiosRequestConfig 87 | ): Promise> => { 90 | ${ 91 | deprecated 92 | ? ` 93 | if (__DEV__) { 94 | console.warn( 95 | "${serviceName}", 96 | "${DEPRECATED_WARM_MESSAGE}", 97 | ); 98 | }` 99 | : "" 100 | } 101 | return Http.${method}Request( 102 | ${ 103 | pathParamsRefString 104 | ? `template(${serviceName}.key,${pathParamsRefString})` 105 | : `${serviceName}.key` 106 | }, 107 | ${queryParamsTypeName ? "queryParams" : "undefined"}, 108 | ${ 109 | requestBody 110 | ? contentType === "multipart/form-data" 111 | ? "objToForm(requestBody)" 112 | : contentType === "application/x-www-form-urlencoded" 113 | ? "objToUrlencoded(requestBody)" 114 | : "requestBody" 115 | : "undefined" 116 | }, 117 | ${security}, 118 | overrideConfig(${additionalAxiosConfig}, 119 | configOverride, 120 | ) 121 | ) 122 | } 123 | 124 | /** Key is end point string without base url */ 125 | ${serviceName}.key = "${endPoint}"; 126 | ` 127 | ); 128 | }, 129 | "", 130 | ); 131 | 132 | code += 133 | types.reduce((prev, { name: _name }) => { 134 | const name = getSchemaName(_name); 135 | 136 | if (!isMatchWholeWord(apisCode, name)) { 137 | return prev; 138 | } 139 | 140 | return prev + ` ${name},`; 141 | }, "import type {") + '} from "./types"\n'; 142 | 143 | code += SERVICE_NEEDED_FUNCTIONS; 144 | code += apisCode; 145 | return code; 146 | } catch (error) { 147 | console.error(error); 148 | return ""; 149 | } 150 | } 151 | 152 | export { generateApis }; 153 | -------------------------------------------------------------------------------- /__tests__/e2e/README.md: -------------------------------------------------------------------------------- 1 | # End-to-End Tests for swag-ts CLI 2 | 3 | This directory contains comprehensive end-to-end tests that validate the `swag-ts` CLI binary by actually running it and verifying the generated output. 4 | 5 | ## Structure 6 | 7 | All E2E tests follow a consistent naming pattern for better organization: 8 | 9 | - **Test files**: `{testname}.test.mjs` 10 | - **Config files**: `{testname}.config.json` 11 | - **Output directories**: `outputs/{testname}/` (git-ignored) 12 | - **Snapshots**: `__snapshots__/{testname}.test.mjs.snap` 13 | 14 | ## Test Suites 15 | 16 | | Test Name | Config File | Description | 17 | | --------------- | --------------------------- | --------------------------------------------- | 18 | | `local-openapi` | `local-openapi.config.json` | Tests generation from local OpenAPI JSON file | 19 | | `petstore3` | `petstore3.config.json` | Tests Petstore3 API with enum handling | 20 | | `slack-api` | `slack-api.config.json` | Tests Slack API with infinite query support | 21 | | `kotlin-wms` | `kotlin-wms.config.json` | Tests Kotlin generation for WMS API | 22 | 23 | ## Running the Tests 24 | 25 | ```bash 26 | # Run all E2E tests 27 | npm test __tests__/e2e/ 28 | 29 | # Run specific test 30 | npm test __tests__/e2e/local-openapi.test.mjs 31 | 32 | # Update snapshots 33 | npm test __tests__/e2e/ --updateSnapshot 34 | 35 | # Run with test name pattern 36 | npm test __tests__/e2e/ --testNamePattern="should generate" 37 | ``` 38 | 39 | ## Output Management 40 | 41 | - All test outputs are written to the `outputs/` directory 42 | - The `outputs/` directory is git-ignored to keep the repository clean 43 | - Each test has its own subdirectory within `outputs/` 44 | - Output directories are automatically cleaned before and after each test 45 | 46 | ## Test Coverage 47 | 48 | The E2E tests cover: 49 | 50 | 1. **TypeScript Generation** 51 | 52 | - React hooks generation 53 | - TypeScript type definitions 54 | - Service layer generation 55 | - Configuration file creation 56 | 57 | 2. **Kotlin Generation** 58 | 59 | - Kotlin class generation 60 | - Package structure handling 61 | - API client generation 62 | 63 | 3. **Configuration Validation** 64 | 65 | - Local file input 66 | - Remote URL fetching 67 | - Custom output directories 68 | - Language-specific options 69 | 70 | 4. **Feature Tests** 71 | 72 | - React hooks (`reactHooks: true`) 73 | - JSON preservation (`keepJson: true`) 74 | - Enum handling (`generateEnumAsType`) 75 | - Infinite query support (`useInfiniteQuery`) 76 | 77 | 5. **Error Handling** 78 | - Invalid configuration files 79 | - Network connectivity issues 80 | - Missing dependencies 81 | 82 | ## Snapshot Testing 83 | 84 | The snapshot tests capture the exact content of generated files and serve as regression tests: 85 | 86 | ### Generated Files Captured 87 | 88 | - **TypeScript files**: `services.ts`, `types.ts`, `hooks.ts`, `config.ts` 89 | - **Configuration files**: `httpRequest.ts`, `hooksConfig.ts` 90 | - **OpenAPI specification**: `swagger.json` (normalized) 91 | 92 | ### When to Update Snapshots 93 | 94 | Update snapshots when you intentionally change the code generation logic: 95 | 96 | ```bash 97 | # Update specific test snapshots 98 | npm test __tests__/e2e/local-openapi.test.mjs -u 99 | 100 | # Update all e2e snapshots 101 | npm test __tests__/e2e/ -u 102 | ``` 103 | 104 | ### Snapshot Benefits 105 | 106 | - **Regression Detection**: Automatically catch unintended changes 107 | - **Code Review**: Easily see what changed in generated output during PRs 108 | - **Version Consistency**: Ensure output remains consistent across versions 109 | - **Quality Assurance**: Validate generated code structure and content 110 | 111 | ## Test Configuration Examples 112 | 113 | ### TypeScript with React Hooks 114 | 115 | ```json 116 | { 117 | "$schema": "../../schema/v6.json", 118 | "url": "https://petstore3.swagger.io/api/v3/openapi.json", 119 | "dir": "./__tests__/e2e/outputs/petstore3", 120 | "language": "typescript", 121 | "keepJson": true, 122 | "reactHooks": true, 123 | "generateEnumAsType": false 124 | } 125 | ``` 126 | 127 | ### Kotlin Generation 128 | 129 | ```json 130 | { 131 | "$schema": "../../schema/v6.json", 132 | "url": "https://api.example.com/openapi.json", 133 | "dir": "./__tests__/e2e/outputs/kotlin-wms", 134 | "language": "kotlin", 135 | "kotlinPackage": "com.example.api", 136 | "ignore": { 137 | "headerParams": ["Accept", "Content-Type"] 138 | } 139 | } 140 | ``` 141 | 142 | ## Test Utilities 143 | 144 | The `utils.mjs` file provides shared utilities for all tests: 145 | 146 | - `cleanOutputDir(configPath)` - Clean up output directories 147 | - `generate(configPath)` - Run CLI and capture generated files 148 | - `runCommand(args)` - Execute CLI with custom arguments 149 | -------------------------------------------------------------------------------- /src/javascript/generateTypes.mts: -------------------------------------------------------------------------------- 1 | import { getRefName, getSchemaName, getTsType } from "./utils.mjs"; 2 | import type { Schema, Config, TypeAST } from "../types.mjs"; 3 | import { AUTOGENERATED_COMMENT } from "./strings.mjs"; 4 | import { getJsdoc } from "../utilities/jsdoc.mjs"; 5 | import { isAscending } from "../utils.mjs"; 6 | 7 | /** Generates TypeScript type definitions from an array of type ASTs */ 8 | function generateTypes(types: TypeAST[], config: Config): string { 9 | let code = AUTOGENERATED_COMMENT; 10 | 11 | const sortedTypes = types.sort(({ name }, { name: otherName }) => 12 | isAscending(name, otherName), 13 | ); 14 | 15 | const schemasMap = buildSchemasMap(sortedTypes); 16 | 17 | for (const typeAST of sortedTypes) { 18 | try { 19 | code += generateSingleType(typeAST, config, schemasMap); 20 | } catch (error) { 21 | console.error(`Failed to generate type for "${typeAST.name}":`, error); 22 | } 23 | } 24 | 25 | return code; 26 | } 27 | 28 | /** Build a map of all schemas for discriminator resolution */ 29 | function buildSchemasMap(types: TypeAST[]): Map { 30 | const schemasMap = new Map(); 31 | 32 | for (const { name, schema } of types) { 33 | if (schema) { 34 | schemasMap.set(getSchemaName(name), schema); 35 | } 36 | } 37 | 38 | return schemasMap; 39 | } 40 | 41 | /** Generate a single type definition with JSDoc */ 42 | function generateSingleType( 43 | typeAST: TypeAST, 44 | config: Config, 45 | schemasMap: Map, 46 | ): string { 47 | const { name: typeName, schema, description } = typeAST; 48 | const name = getSchemaName(typeName); 49 | 50 | const jsdoc = getJsdoc({ 51 | ...schema, 52 | description: description || schema?.description, 53 | deprecated: schema?.deprecated 54 | ? schema?.["x-deprecatedMessage"] || String(schema?.deprecated) 55 | : undefined, 56 | }); 57 | 58 | const typeDefinition = getTypeDefinition(name, schema, config, schemasMap); 59 | 60 | return ` 61 | ${jsdoc} 62 | ${typeDefinition} 63 | `; 64 | } 65 | 66 | /** Generate a TypeScript type definition for a given schema */ 67 | function getTypeDefinition( 68 | name: string, 69 | schema: Schema = {}, 70 | config: Config, 71 | schemasMap: Map, 72 | ): string { 73 | // Handle enums 74 | if (schema.enum) { 75 | return generateEnumDefinition( 76 | name, 77 | schema.enum, 78 | schema["x-enumNames"], 79 | config, 80 | ); 81 | } 82 | 83 | // Handle allOf/oneOf compositions 84 | if (schema.allOf || schema.oneOf) { 85 | return `export type ${name} = ${getTsType( 86 | schema, 87 | config, 88 | schemasMap, 89 | name, 90 | )};`; 91 | } 92 | 93 | // Handle arrays 94 | if (schema.type === "array" && schema.items) { 95 | return `export type ${name} = (${getTsType( 96 | schema.items, 97 | config, 98 | schemasMap, 99 | )})[];`; 100 | } 101 | 102 | // Handle references 103 | if (schema.$ref) { 104 | return `export type ${name} = ${getRefName(schema.$ref)};`; 105 | } 106 | 107 | // Handle objects 108 | if (schema.type === "object") { 109 | return generateObjectType(name, schema, config, schemasMap); 110 | } 111 | 112 | // Handle primitive types 113 | if (schema.type === "string") { 114 | return `export type ${name} = string;`; 115 | } 116 | 117 | // Fallback 118 | return `export type ${name} = any;`; 119 | } 120 | 121 | /** Generate object type or interface */ 122 | function generateObjectType( 123 | name: string, 124 | schema: Schema, 125 | config: Config, 126 | schemasMap: Map, 127 | ): string { 128 | const typeObject = getTsType(schema, config, schemasMap); 129 | const hasProperties = schema.additionalProperties || schema.properties; 130 | const hasOneOf = schema.oneOf; 131 | 132 | if (hasProperties && !hasOneOf) { 133 | return `export interface ${name} ${typeObject}`; 134 | } 135 | 136 | return `export type ${name} = ${typeObject};`; 137 | } 138 | 139 | /** Generate enum definition as type union or enum based on config */ 140 | function generateEnumDefinition( 141 | name: string, 142 | enumValues: string[], 143 | enumNames: string[] | undefined, 144 | config: Config, 145 | ): string { 146 | if (config.generateEnumAsType) { 147 | return generateEnumAsTypeUnion(name, enumValues); 148 | } 149 | 150 | return generateEnumAsEnum(name, enumValues, enumNames); 151 | } 152 | 153 | /** Generate enum as a type union */ 154 | function generateEnumAsTypeUnion(name: string, enumValues: string[]): string { 155 | const unionString = enumValues.length 156 | ? enumValues.map((value) => JSON.stringify(value)).join(" | ") 157 | : "unknown"; 158 | 159 | return `export type ${name} = ${unionString};`; 160 | } 161 | 162 | /** Generate enum as a TypeScript enum */ 163 | function generateEnumAsEnum( 164 | name: string, 165 | enumValues: string[], 166 | enumNames?: string[], 167 | ): string { 168 | const entries = enumValues.map((value, index) => { 169 | const key = enumNames?.[index] ?? JSON.stringify(value); 170 | return `${key}=${JSON.stringify(value)}`; 171 | }); 172 | 173 | return `export enum ${name} {${entries.join(",")}}`; 174 | } 175 | 176 | export { generateTypes }; 177 | -------------------------------------------------------------------------------- /src/javascript/signalR/generator.mts: -------------------------------------------------------------------------------- 1 | import type { TypeAST, Schema, Parameter, Config } from "../../types.mjs"; 2 | import { generateTypes } from "../generateTypes.mjs"; 3 | import { getDefineParam } from "../utils.mjs"; 4 | 5 | export interface HubJson { 6 | SignalrType: string; 7 | info: { 8 | title: string; 9 | termsOfService?: string; 10 | version: string; 11 | }; 12 | hubs: { 13 | [serviceName: string]: { 14 | name: string; 15 | operations: { [x: string]: Operation }; 16 | callbacks: { [x: string]: Operation }; 17 | }; 18 | }; 19 | definitions: { [x: string]: Schema }; 20 | } 21 | 22 | interface Operation { 23 | parameters: { [x: string]: Schema }; 24 | description?: string; 25 | returntype?: Schema; 26 | } 27 | 28 | interface ParsedOperation { 29 | name: string; 30 | parameters: { [x: string]: Parameter }; 31 | description?: string; 32 | } 33 | 34 | function signalRGenerator(json: HubJson, config: Config): string { 35 | const types: TypeAST[] = []; 36 | const hubs: { 37 | name: string; 38 | operations: ParsedOperation[]; 39 | callbacks: ParsedOperation[]; 40 | }[] = []; 41 | 42 | try { 43 | Object.values(json.hubs).map((hub) => { 44 | const operations: ParsedOperation[] = []; 45 | const callbacks: ParsedOperation[] = []; 46 | Object.entries(hub.operations).map( 47 | //@ts-ignore 48 | ([_name, operation]: [string, Operation]) => { 49 | operations.push({ 50 | name: _name, 51 | parameters: operation.parameters as any, 52 | description: operation.description, 53 | }); 54 | }, 55 | ); 56 | 57 | Object.entries(hub.callbacks).map( 58 | //@ts-ignore 59 | ([_name, operation]: [string, Operation]) => { 60 | callbacks.push({ 61 | name: _name, 62 | parameters: operation.parameters as any, 63 | description: operation.description, 64 | }); 65 | }, 66 | ); 67 | 68 | hubs.push({ 69 | name: hub.name, 70 | operations, 71 | callbacks, 72 | }); 73 | }); 74 | 75 | if (json.definitions) { 76 | types.push( 77 | ...Object.entries(json.definitions as { [x: string]: Schema }).map( 78 | ([name, schema]) => { 79 | return { 80 | name, 81 | schema, 82 | }; 83 | }, 84 | ), 85 | ); 86 | } 87 | let code = ""; 88 | hubs.map(({ name: hubsName, operations, callbacks }) => { 89 | const operationEnumsName = `${hubsName}OperationsNames`; 90 | const operationEnums = operations 91 | .map(({ name: operationKey }) => `${operationKey} = "${operationKey}"`) 92 | .join(",\n"); 93 | 94 | if (operationEnums) { 95 | code += ` 96 | export enum ${operationEnumsName} { 97 | ${operationEnums} 98 | }\n`; 99 | 100 | code += ` 101 | export type ${hubsName}Operations = { 102 | ${operations 103 | .map( 104 | ({ name, parameters }) => 105 | `[${operationEnumsName}.${name}]: (${Object.entries( 106 | parameters, 107 | ).map(([_name, schema]) => 108 | getDefineParam( 109 | _name, 110 | schema.required, 111 | schema as unknown as Schema, 112 | config, 113 | schema.description, 114 | ), 115 | )}) => Promise`, 116 | ) 117 | .join(";\n")} 118 | };\n`; 119 | } 120 | 121 | const callbackEnumsName = `${hubsName}CallbacksNames`; 122 | const callbackEnums = callbacks 123 | .map(({ name: callbackKey }) => `${callbackKey} = "${callbackKey}"`) 124 | .join(",\n"); 125 | 126 | if (callbackEnums) { 127 | code += ` 128 | export enum ${callbackEnumsName} { 129 | ${callbackEnums} 130 | }\n`; 131 | 132 | code += ` 133 | export type ${hubsName}Callbacks = { 134 | ${callbacks 135 | .map( 136 | ({ name, parameters }) => 137 | `[${callbackEnumsName}.${name}]: (${Object.entries( 138 | parameters, 139 | ).map(([_name, schema]) => 140 | getDefineParam( 141 | _name, 142 | schema.required, 143 | schema as unknown as Schema, 144 | config, 145 | schema.description, 146 | ), 147 | )}) => void`, 148 | ) 149 | .join(";\n")} 150 | };\n`; 151 | } 152 | 153 | code += ` 154 | export interface ${hubsName} { 155 | ${ 156 | callbackEnums 157 | ? ` 158 | callbacksName: ${callbackEnumsName}; 159 | callbacks: ${hubsName}Callbacks; 160 | ` 161 | : "" 162 | } 163 | ${ 164 | operationEnums 165 | ? ` 166 | methodsName: ${operationEnumsName}; 167 | methods: ${hubsName}Operations; 168 | ` 169 | : "" 170 | } 171 | } 172 | `; 173 | }); 174 | code += generateTypes(types, config); 175 | 176 | return code; 177 | } catch (error) { 178 | console.error({ error }); 179 | return ""; 180 | } 181 | } 182 | 183 | export { signalRGenerator }; 184 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "jsx": "react", 4 | "resolveJsonModule": true, 5 | "moduleResolution": "Node", 6 | /* Basic Options */ 7 | // "incremental": true, /* Enable incremental compilation */ 8 | "target": "ES2017" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */, 9 | "module": "ES6" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */, 10 | "lib": [ 11 | "ESNext", 12 | ] /* Specify library files to be included in the compilation. */, 13 | // "allowJs": true, /* Allow javascript files to be compiled. */ 14 | // "checkJs": true, /* Report errors in .js files. */ 15 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ 16 | "declaration": true /* Generates corresponding '.d.ts' file. */, 17 | "declarationMap": true /* Generates a sourcemap for each corresponding '.d.ts' file. */, 18 | "sourceMap": true /* Generates corresponding '.map' file. */, 19 | // "outFile": "./", /* Concatenate and emit output to single file. */ 20 | "outDir": "./lib" /* Redirect output structure to the directory. */, 21 | "rootDir": "./src" /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */, 22 | // "composite": true, /* Enable project compilation */ 23 | // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ 24 | // "removeComments": true, /* Do not emit comments to output. */ 25 | // "noEmit": true, /* Do not emit outputs. */ 26 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 27 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 28 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 29 | "skipLibCheck": true, 30 | /* Strict Type-Checking Options */ 31 | "strict": true /* Enable all strict type-checking options. */, 32 | // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ 33 | // "strictNullChecks": true, /* Enable strict null checks. */ 34 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */ 35 | // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ 36 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ 37 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 38 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 39 | /* Additional Checks */ 40 | // "noUnusedLocals": true, /* Report errors on unused locals. */ 41 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 42 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 43 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 44 | /* Module Resolution Options */ 45 | // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 46 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ 47 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 48 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 49 | // "typeRoots": [], /* List of folders to include type definitions from. */ 50 | "types": [ 51 | "node", 52 | "jest" 53 | ] /* Type declaration files to be included in compilation. */, 54 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 55 | "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ 56 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 57 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 58 | /* Source Map Options */ 59 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 60 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 61 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 62 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 63 | /* Experimental Options */ 64 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 65 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 66 | }, 67 | "exclude": [ 68 | "node_modules", 69 | "test/", 70 | "test2/", 71 | "lib/", 72 | "__tests__" 73 | ] 74 | } -------------------------------------------------------------------------------- /src/javascript/index.mts: -------------------------------------------------------------------------------- 1 | import { writeFileSync, existsSync, readFileSync, rmdirSync } from "fs"; 2 | import { format } from "prettier"; 3 | import { SwaggerJson, Config } from "../types.mjs"; 4 | import { generator } from "./generator.mjs"; 5 | import { build } from "tsc-prog"; 6 | import { HubJson, signalRGenerator } from "./signalR/generator.mjs"; 7 | import { generateMock } from "./mock/index.mjs"; 8 | import chalk from "chalk"; 9 | //@ts-ignore 10 | import recursive from "recursive-readdir"; 11 | import { getJson } from "../getJson.mjs"; 12 | import getConfigFile from "./files/config.mjs"; 13 | import getHttpRequestFile from "./files/httpRequest.mjs"; 14 | import getHooksConfigFile from "./files/hooksConfig.mjs"; 15 | import { getPrettierOptions } from "../utils.mjs"; 16 | 17 | const generateJavascriptService = async ( 18 | config: Config, 19 | input: SwaggerJson, 20 | ) => { 21 | try { 22 | const { dir, language, mock, reactHooks } = config; 23 | const isToJs = language === "javascript"; 24 | 25 | // Generate code 26 | const { code, hooks, type } = generator(input, config); 27 | 28 | if (mock) { 29 | generateMock(input, config); 30 | } 31 | 32 | // Write core files 33 | await writeFile(dir, "services.ts", code, config); 34 | await writeFile(dir, "types.ts", type, config); 35 | 36 | // Write React hooks if enabled 37 | if (reactHooks && hooks) { 38 | await writeFile(dir, "hooks.ts", hooks, config); 39 | await writeFileIfNotExists( 40 | dir, 41 | "hooksConfig", 42 | isToJs, 43 | getHooksConfigFile(), 44 | config, 45 | ); 46 | } 47 | 48 | // Write config files 49 | await writeFile(dir, "httpRequest.ts", getHttpRequestFile(), config); 50 | await writeFileIfNotExists( 51 | dir, 52 | "config", 53 | isToJs, 54 | getConfigFile({ baseUrl: input.servers?.[0]?.url || "" }), 55 | config, 56 | ); 57 | 58 | // Generate SignalR hub if configured 59 | const hubCode = await generateHub(config); 60 | 61 | // Convert TypeScript to JavaScript if needed 62 | if (isToJs) { 63 | const filesToConvert = buildFileList( 64 | config, 65 | Boolean(reactHooks), 66 | Boolean(hubCode), 67 | ); 68 | convertTsToJs(dir, filesToConvert); 69 | 70 | await formatAllFiles(dir, getPrettierOptions(config)); 71 | } 72 | 73 | // Format all generated files 74 | 75 | console.log(chalk.greenBright("All Completed")); 76 | } catch (error) { 77 | console.log(chalk.redBright(error), chalk.redBright("Generation failed")); 78 | } 79 | }; 80 | 81 | /** Write a file and log completion */ 82 | async function writeFile( 83 | dir: string, 84 | filename: string, 85 | content: string, 86 | config: Config, 87 | ): Promise { 88 | writeFileSync( 89 | `${dir}/${filename}`, 90 | await format(content, getPrettierOptions(config)), 91 | ); 92 | const name = filename.replace(/\.(ts|js)$/, ""); 93 | console.log(chalk.yellowBright(`${name} Completed`)); 94 | } 95 | 96 | /** Write a file only if it doesn't already exist */ 97 | async function writeFileIfNotExists( 98 | dir: string, 99 | basename: string, 100 | isJs: boolean, 101 | content: string, 102 | config: Config, 103 | ): Promise { 104 | const ext = isJs ? "js" : "ts"; 105 | const filepath = `${dir}/${basename}.${ext}`; 106 | 107 | if (!existsSync(filepath)) { 108 | writeFileSync( 109 | `${dir}/${basename}.ts`, 110 | await format(content, getPrettierOptions(config)), 111 | ); 112 | console.log(chalk.yellowBright(`${basename} Completed`)); 113 | } 114 | } 115 | 116 | /** Generate SignalR hub code if configured */ 117 | async function generateHub(config: Config): Promise { 118 | if (!config.hub) { 119 | return null; 120 | } 121 | 122 | const hubJson: HubJson = await getJson(config.hub); 123 | const hubCode = signalRGenerator(hubJson, config); 124 | 125 | if (hubCode) { 126 | writeFile(config.dir, "hub.ts", hubCode, config); 127 | } 128 | 129 | return hubCode; 130 | } 131 | 132 | /** Build list of files to convert from TypeScript to JavaScript */ 133 | function buildFileList( 134 | config: Config, 135 | includeHooks: boolean, 136 | includeHub: boolean, 137 | ): string[] { 138 | const files: string[] = []; 139 | 140 | if (includeHub) { 141 | files.push("hub"); 142 | } 143 | 144 | if (config.url || config.local) { 145 | files.push("httpRequest", "services", "types", "config"); 146 | 147 | if (includeHooks) { 148 | files.push("hooks", "hooksConfig"); 149 | } 150 | } 151 | 152 | return files; 153 | } 154 | 155 | /** Format a single file with Prettier */ 156 | async function formatFile(filePath: string, prettierOptions: any) { 157 | const code = readFileSync(filePath).toString(); 158 | writeFileSync(filePath, await format(code, prettierOptions)); 159 | } 160 | 161 | /** Format all files in directory */ 162 | async function formatAllFiles(dir: string, prettierOptions: any) { 163 | recursive(dir, async (err: Error | null, files: string[]) => { 164 | if (err) { 165 | console.log(chalk.redBright(err)); 166 | return; 167 | } 168 | 169 | for (const file of files) { 170 | const options = file.endsWith(".json") 171 | ? { ...prettierOptions, parser: "json" } 172 | : prettierOptions; 173 | 174 | if ( 175 | file.endsWith(".ts") || 176 | file.endsWith(".js") || 177 | file.endsWith(".json") 178 | ) { 179 | await formatFile(file, options); 180 | } 181 | } 182 | }); 183 | } 184 | 185 | /** Convert TypeScript files to JavaScript */ 186 | function convertTsToJs(dir: string, files: string[]) { 187 | build({ 188 | basePath: ".", 189 | compilerOptions: { 190 | listFiles: true, 191 | outDir: dir, 192 | declaration: true, 193 | skipLibCheck: true, 194 | module: "esnext", 195 | target: "esnext", 196 | lib: ["esnext"], 197 | }, 198 | files: files.map((file) => `${dir}/${file}.ts`), 199 | }); 200 | 201 | // Remove original .ts files after conversion 202 | files.forEach((file) => { 203 | const tsFile = `${dir}/${file}.ts`; 204 | if (existsSync(tsFile)) { 205 | rmdirSync(tsFile, { recursive: true }); 206 | } 207 | }); 208 | } 209 | 210 | export { generateJavascriptService }; 211 | -------------------------------------------------------------------------------- /__tests__/discriminator.test.mjs: -------------------------------------------------------------------------------- 1 | import { cleanOutputDir, generator } from "./main/utils.mjs"; 2 | 3 | describe("discriminator support", () => { 4 | beforeAll(async () => { 5 | await cleanOutputDir("./__tests__/outputs/discriminator"); 6 | }); 7 | 8 | afterEach(async () => { 9 | await cleanOutputDir("./__tests__/outputs/discriminator"); 10 | }); 11 | 12 | test("should handle discriminator with propertyName and mapping", async () => { 13 | const swagger = { 14 | openapi: "3.0.0", 15 | info: { 16 | title: "Test API", 17 | version: "1.0.0", 18 | }, 19 | paths: {}, 20 | components: { 21 | schemas: { 22 | HotShearBaseDto: { 23 | type: "object", 24 | properties: { 25 | id: { 26 | type: "string", 27 | format: "uuid", 28 | }, 29 | hotShearType: { 30 | enum: ["CRANK", "ROTARY", "COMBI"], 31 | type: "string", 32 | }, 33 | }, 34 | additionalProperties: false, 35 | }, 36 | HotShearDto: { 37 | required: ["hotShearType"], 38 | type: "object", 39 | properties: { 40 | hotShearType: { 41 | type: "string", 42 | }, 43 | id: { 44 | type: "string", 45 | format: "uuid", 46 | }, 47 | name: { 48 | type: "string", 49 | nullable: true, 50 | }, 51 | }, 52 | additionalProperties: false, 53 | discriminator: { 54 | propertyName: "hotShearType", 55 | mapping: { 56 | CRANK: "#/components/schemas/HotShearCRANKDto", 57 | ROTARY: "#/components/schemas/HotShearROTARYDto", 58 | COMBI: "#/components/schemas/HotShearCOMBIDto", 59 | }, 60 | }, 61 | }, 62 | HotShearCRANKDto: { 63 | type: "object", 64 | allOf: [ 65 | { 66 | $ref: "#/components/schemas/HotShearDto", 67 | }, 68 | ], 69 | properties: { 70 | crankRadius: { 71 | type: "number", 72 | format: "double", 73 | nullable: true, 74 | }, 75 | }, 76 | additionalProperties: false, 77 | }, 78 | HotShearROTARYDto: { 79 | type: "object", 80 | allOf: [ 81 | { 82 | $ref: "#/components/schemas/HotShearDto", 83 | }, 84 | ], 85 | properties: { 86 | bladesRadius: { 87 | type: "number", 88 | format: "double", 89 | nullable: true, 90 | }, 91 | }, 92 | additionalProperties: false, 93 | }, 94 | HotShearCOMBIDto: { 95 | type: "object", 96 | allOf: [ 97 | { 98 | $ref: "#/components/schemas/HotShearDto", 99 | }, 100 | ], 101 | properties: { 102 | crankRadius: { 103 | type: "number", 104 | format: "double", 105 | nullable: true, 106 | }, 107 | bladesRadius: { 108 | type: "number", 109 | format: "double", 110 | nullable: true, 111 | }, 112 | }, 113 | additionalProperties: false, 114 | }, 115 | }, 116 | }, 117 | }; 118 | 119 | const { "types.ts": type } = await generator( 120 | { 121 | url: "./__tests__/outputs/discriminator/swagger.json", 122 | dir: "./__tests__/outputs/discriminator", 123 | }, 124 | swagger, 125 | ); 126 | 127 | // Test that the base discriminator type has the union type and is required 128 | expect(type).toContain("export interface HotShearDto"); 129 | 130 | // Extract just the HotShearDto interface to check its properties 131 | const hotShearDtoMatch = type.match( 132 | /export interface HotShearDto \{[^}]+\}/s, 133 | ); 134 | expect(hotShearDtoMatch).toBeTruthy(); 135 | const hotShearDtoCode = hotShearDtoMatch[0]; 136 | 137 | // Check that discriminator property has union type 138 | expect(hotShearDtoCode).toMatch( 139 | /hotShearType:\s*"CRANK"\s*\|\s*"ROTARY"\s*\|\s*"COMBI"/, 140 | ); 141 | 142 | // Test that discriminator property is required (no question mark) in HotShearDto 143 | expect(hotShearDtoCode).not.toMatch(/"hotShearType"\?:/); 144 | 145 | // Test that child types have specific discriminator literals 146 | expect(type).toMatch(/HotShearCRANKDto.*hotShearType:\s*"CRANK"/s); 147 | expect(type).toMatch(/HotShearROTARYDto.*hotShearType:\s*"ROTARY"/s); 148 | expect(type).toMatch(/HotShearCOMBIDto.*hotShearType:\s*"COMBI"/s); 149 | }); 150 | 151 | test("should handle discriminator property when not explicitly in required array", async () => { 152 | const swagger = { 153 | openapi: "3.0.0", 154 | info: { 155 | title: "Test API", 156 | version: "1.0.0", 157 | }, 158 | paths: {}, 159 | components: { 160 | schemas: { 161 | Animal: { 162 | type: "object", 163 | properties: { 164 | animalType: { 165 | type: "string", 166 | }, 167 | name: { 168 | type: "string", 169 | }, 170 | }, 171 | discriminator: { 172 | propertyName: "animalType", 173 | mapping: { 174 | dog: "#/components/schemas/Dog", 175 | cat: "#/components/schemas/Cat", 176 | }, 177 | }, 178 | }, 179 | Dog: { 180 | allOf: [ 181 | { 182 | $ref: "#/components/schemas/Animal", 183 | }, 184 | ], 185 | properties: { 186 | breed: { 187 | type: "string", 188 | }, 189 | }, 190 | }, 191 | Cat: { 192 | allOf: [ 193 | { 194 | $ref: "#/components/schemas/Animal", 195 | }, 196 | ], 197 | properties: { 198 | color: { 199 | type: "string", 200 | }, 201 | }, 202 | }, 203 | }, 204 | }, 205 | }; 206 | 207 | const { "types.ts": type } = await generator( 208 | { 209 | url: "./__tests__/outputs/discriminator/swagger.json", 210 | dir: "./__tests__/outputs/discriminator", 211 | }, 212 | swagger, 213 | ); 214 | 215 | // Test that discriminator property uses union type from mapping 216 | expect(type).toContain("export interface Animal"); 217 | expect(type).toMatch(/animalType:\s*"dog"\s*\|\s*"cat"/); 218 | 219 | // Test that discriminator property is required (no question mark) 220 | expect(type).not.toMatch(/"animalType"\?:/); 221 | 222 | // Test that child types have specific discriminator literals 223 | expect(type).toMatch(/Dog.*animalType:\s*"dog"/s); 224 | expect(type).toMatch(/Cat.*animalType:\s*"cat"/s); 225 | }); 226 | }); 227 | -------------------------------------------------------------------------------- /testFiles/Wms.postman_collection.json: -------------------------------------------------------------------------------- 1 | { 2 | "info": { 3 | "_postman_id": "35b60b5f-ed03-413b-8880-608947635cb5", 4 | "name": "Wms", 5 | "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", 6 | "_exporter_id": "4699480" 7 | }, 8 | "item": [ 9 | { 10 | "name": "Local", 11 | "item": [ 12 | { 13 | "name": "login", 14 | "request": { 15 | "method": "POST", 16 | "header": [ 17 | { 18 | "key": "Content-Encoding", 19 | "value": "UTF-8", 20 | "type": "text" 21 | }, 22 | { 23 | "key": "Accept", 24 | "value": "application/json", 25 | "type": "text" 26 | }, 27 | { 28 | "key": "Content-Type", 29 | "name": "Content-Type", 30 | "value": "application/x-www-form-urlencoded", 31 | "type": "text" 32 | } 33 | ], 34 | "body": { 35 | "mode": "urlencoded", 36 | "urlencoded": [ 37 | { 38 | "key": "UserName", 39 | "value": "f.mehranfar", 40 | "type": "text" 41 | }, 42 | { 43 | "key": "Password", 44 | "value": "123456", 45 | "type": "text" 46 | } 47 | ] 48 | }, 49 | "url": { 50 | "raw": "http://localhost:42732/mobile/login", 51 | "protocol": "http", 52 | "host": [ 53 | "localhost" 54 | ], 55 | "port": "42732", 56 | "path": [ 57 | "mobile", 58 | "login" 59 | ] 60 | }, 61 | "description": "سرویس لاگین" 62 | }, 63 | "response": [] 64 | }, 65 | { 66 | "name": "ReceivingWorkerTaskGetItems", 67 | "request": { 68 | "method": "POST", 69 | "header": [ 70 | { 71 | "key": "Content-Type", 72 | "name": "Content-Type", 73 | "value": "application/x-www-form-urlencoded", 74 | "type": "text" 75 | }, 76 | { 77 | "key": "Accept", 78 | "value": "application/json", 79 | "type": "text" 80 | }, 81 | { 82 | "key": "Content-Encoding", 83 | "value": "UTF-8", 84 | "type": "text" 85 | } 86 | ], 87 | "body": { 88 | "mode": "urlencoded", 89 | "urlencoded": [] 90 | }, 91 | "url": { 92 | "raw": "http://localhost:42732/mobile/ReceivingWorkerTaskGetItems", 93 | "protocol": "http", 94 | "host": [ 95 | "localhost" 96 | ], 97 | "port": "42732", 98 | "path": [ 99 | "mobile", 100 | "ReceivingWorkerTaskGetItems" 101 | ] 102 | }, 103 | "description": "تسک دریافت" 104 | }, 105 | "response": [] 106 | }, 107 | { 108 | "name": "ReceivingWorkerTaskGetCount", 109 | "request": { 110 | "method": "POST", 111 | "header": [ 112 | { 113 | "key": "Content-Type", 114 | "name": "Content-Type", 115 | "type": "text", 116 | "value": "application/x-www-form-urlencoded" 117 | }, 118 | { 119 | "key": "Accept", 120 | "type": "text", 121 | "value": "application/json" 122 | }, 123 | { 124 | "key": "Content-Encoding", 125 | "type": "text", 126 | "value": "UTF-8" 127 | } 128 | ], 129 | "body": { 130 | "mode": "urlencoded", 131 | "urlencoded": [] 132 | }, 133 | "url": { 134 | "raw": "http://localhost:42732/mobile/ReceivingWorkerTaskGetCount", 135 | "protocol": "http", 136 | "host": [ 137 | "localhost" 138 | ], 139 | "port": "42732", 140 | "path": [ 141 | "mobile", 142 | "ReceivingWorkerTaskGetCount" 143 | ] 144 | }, 145 | "description": "تعداد تسک دریافت" 146 | }, 147 | "response": [] 148 | } 149 | ] 150 | }, 151 | { 152 | "name": "Server", 153 | "item": [ 154 | { 155 | "name": "login", 156 | "request": { 157 | "method": "POST", 158 | "header": [ 159 | { 160 | "key": "Content-Encoding", 161 | "value": "UTF-8", 162 | "type": "text" 163 | }, 164 | { 165 | "key": "Accept", 166 | "value": "application/json", 167 | "type": "text" 168 | }, 169 | { 170 | "key": "Content-Type", 171 | "name": "Content-Type", 172 | "value": "application/x-www-form-urlencoded", 173 | "type": "text" 174 | } 175 | ], 176 | "body": { 177 | "mode": "urlencoded", 178 | "urlencoded": [ 179 | { 180 | "key": "UserName", 181 | "value": "f.mehranfar", 182 | "type": "text" 183 | }, 184 | { 185 | "key": "Password", 186 | "value": "123456", 187 | "type": "text" 188 | } 189 | ] 190 | }, 191 | "url": { 192 | "raw": "http://192.168.1.6:1020/mobile/login", 193 | "protocol": "http", 194 | "host": [ 195 | "192", 196 | "168", 197 | "1", 198 | "6" 199 | ], 200 | "port": "1020", 201 | "path": [ 202 | "mobile", 203 | "login" 204 | ] 205 | }, 206 | "description": "سرویس لاگین" 207 | }, 208 | "response": [] 209 | }, 210 | { 211 | "name": "ReceivingWorkerTaskGetItems", 212 | "request": { 213 | "method": "POST", 214 | "header": [ 215 | { 216 | "key": "Content-Type", 217 | "name": "Content-Type", 218 | "value": "application/x-www-form-urlencoded", 219 | "type": "text" 220 | }, 221 | { 222 | "key": "Accept", 223 | "value": "application/json", 224 | "type": "text" 225 | }, 226 | { 227 | "key": "Content-Encoding", 228 | "value": "UTF-8", 229 | "type": "text" 230 | } 231 | ], 232 | "body": { 233 | "mode": "urlencoded", 234 | "urlencoded": [] 235 | }, 236 | "url": { 237 | "raw": "http://192.168.1.6:1020/mobile/ReceivingWorkerTaskGetItems", 238 | "protocol": "http", 239 | "host": [ 240 | "192", 241 | "168", 242 | "1", 243 | "6" 244 | ], 245 | "port": "1020", 246 | "path": [ 247 | "mobile", 248 | "ReceivingWorkerTaskGetItems" 249 | ] 250 | }, 251 | "description": "تسک دریافت" 252 | }, 253 | "response": [] 254 | }, 255 | { 256 | "name": "ReceivingWorkerTaskGetCount", 257 | "request": { 258 | "method": "POST", 259 | "header": [ 260 | { 261 | "key": "Content-Type", 262 | "name": "Content-Type", 263 | "type": "text", 264 | "value": "application/x-www-form-urlencoded" 265 | }, 266 | { 267 | "key": "Accept", 268 | "type": "text", 269 | "value": "application/json" 270 | }, 271 | { 272 | "key": "Content-Encoding", 273 | "type": "text", 274 | "value": "UTF-8" 275 | } 276 | ], 277 | "body": { 278 | "mode": "urlencoded", 279 | "urlencoded": [] 280 | }, 281 | "url": { 282 | "raw": "http://192.168.1.6:1020/mobile/ReceivingWorkerTaskGetCount", 283 | "protocol": "http", 284 | "host": [ 285 | "192", 286 | "168", 287 | "1", 288 | "6" 289 | ], 290 | "port": "1020", 291 | "path": [ 292 | "mobile", 293 | "ReceivingWorkerTaskGetCount" 294 | ] 295 | }, 296 | "description": "تعداد تسک دریافت" 297 | }, 298 | "response": [] 299 | } 300 | ] 301 | } 302 | ] 303 | } -------------------------------------------------------------------------------- /__tests__/swagger.json: -------------------------------------------------------------------------------- 1 | { 2 | "openapi": "3.0.0", 3 | "x-generator": "NSwag v13.14.8.0 (NJsonSchema v10.5.2.0 (Newtonsoft.Json v12.0.0.0))", 4 | "info": { 5 | "title": "API", 6 | "description": "API", 7 | "contact": { 8 | "name": "hossein", 9 | "email": "hosseinm.developer@gmail.com" 10 | }, 11 | "version": "v1" 12 | }, 13 | "paths": { 14 | "/settlement/v1/private/settlement/get": { 15 | "get": { 16 | "tags": [ 17 | "Settlement" 18 | ], 19 | "operationId": "Settlement_Get", 20 | "parameters": [ 21 | { 22 | "name": "UserWalletId", 23 | "in": "query", 24 | "x-nullable": true, 25 | "schema": { 26 | "type": "integer", 27 | "format": "int64" 28 | } 29 | }, 30 | { 31 | "name": "IsAuto", 32 | "in": "query", 33 | "x-nullable": true, 34 | "schema": { 35 | "type": "boolean" 36 | } 37 | }, 38 | { 39 | "name": "StartDate", 40 | "in": "query", 41 | "x-nullable": true, 42 | "schema": { 43 | "type": "string", 44 | "format": "date-time" 45 | } 46 | }, 47 | { 48 | "name": "EndDate", 49 | "in": "query", 50 | "x-nullable": true, 51 | "schema": { 52 | "type": "string" 53 | } 54 | }, 55 | { 56 | "name": "MinimumAmount", 57 | "in": "query", 58 | "x-nullable": true, 59 | "schema": { 60 | "type": "number", 61 | "format": "decimal" 62 | } 63 | }, 64 | { 65 | "name": "Skip", 66 | "in": "query", 67 | "x-nullable": true, 68 | "schema": { 69 | "type": "integer", 70 | "format": "int32" 71 | } 72 | }, 73 | { 74 | "name": "status", 75 | "in": "query", 76 | "x-nullable": true, 77 | "schema": { 78 | "$ref": "#/components/schemas/SettlementRequestStatus" 79 | } 80 | }, 81 | { 82 | "name": "States", 83 | "in": "query", 84 | "x-nullable": true, 85 | "explode": true, 86 | "schema": { 87 | "type": "array", 88 | "items": { 89 | "allOf": [ 90 | { 91 | "$ref": "#/components/schemas/SettlementRequestStatus" 92 | } 93 | ], 94 | "nullable": true 95 | } 96 | } 97 | } 98 | ], 99 | "responses": { 100 | "200": { 101 | "x-nullable": true, 102 | "description": "", 103 | "content": { 104 | "text/plain": { 105 | "schema": { 106 | "allOf": [ 107 | { 108 | "$ref": "#/components/schemas/SettlementRequestResultListResponseVM" 109 | } 110 | ], 111 | "nullable": true 112 | } 113 | }, 114 | "application/json": { 115 | "schema": { 116 | "allOf": [ 117 | { 118 | "$ref": "#/components/schemas/SettlementRequestResultListResponseVM" 119 | } 120 | ], 121 | "nullable": true 122 | } 123 | }, 124 | "text/json": { 125 | "schema": { 126 | "allOf": [ 127 | { 128 | "$ref": "#/components/schemas/SettlementRequestResultListResponseVM" 129 | } 130 | ], 131 | "nullable": true 132 | } 133 | } 134 | } 135 | } 136 | }, 137 | "security": [ 138 | { 139 | "bearer": [] 140 | } 141 | ] 142 | } 143 | } 144 | }, 145 | "components": { 146 | "securitySchemes": { 147 | "Bearer": { 148 | "type": "oauth2", 149 | "description": "Swagger Api", 150 | "in": "header", 151 | "flows": { 152 | "password": { 153 | "tokenUrl": "https://localhost:7600/connect/token", 154 | "scopes": {} 155 | } 156 | } 157 | } 158 | }, 159 | "schemas": { 160 | "SettlementRequestResultListResponseVM": { 161 | "type": "object", 162 | "required": [ 163 | "totalCount" 164 | ], 165 | "properties": { 166 | "settlementRequests": { 167 | "type": "array", 168 | "items": { 169 | "$ref": "#/components/schemas/SettlementRequestResultResponseVM" 170 | }, 171 | "nullable": true 172 | }, 173 | "totalCount": { 174 | "type": "integer", 175 | "format": "int64" 176 | } 177 | } 178 | }, 179 | "SettlementRequestResultResponseVM": { 180 | "type": "object", 181 | "required": [ 182 | "userWalletId", 183 | "automaticSettlement", 184 | "commissionAmount", 185 | "createDateUtc", 186 | "creatorUserId", 187 | "domainId", 188 | "id", 189 | "requestAmount", 190 | "status", 191 | "targetUserBankId", 192 | "userId", 193 | "voucherId" 194 | ], 195 | "properties": { 196 | "userWalletId": { 197 | "type": "integer", 198 | "format": "int64" 199 | }, 200 | "accountNumber": { 201 | "type": "string", 202 | "nullable": true 203 | }, 204 | "automaticSettlement": { 205 | "type": "boolean" 206 | }, 207 | "commissionAmount": { 208 | "type": "number", 209 | "format": "decimal" 210 | }, 211 | "createDateUtc": { 212 | "type": "string", 213 | "format": "date-time" 214 | }, 215 | "creatorUserId": { 216 | "type": "string", 217 | "format": "guid" 218 | }, 219 | "domainId": { 220 | "type": "integer", 221 | "format": "int32" 222 | }, 223 | "id": { 224 | "type": "integer", 225 | "format": "int64" 226 | }, 227 | "status": { 228 | "$ref": "#/components/schemas/SettlementRequestStatus" 229 | } 230 | } 231 | }, 232 | "SettlementRequestStatus": { 233 | "type": "string", 234 | "description": "", 235 | "x-enumNames": [ 236 | "Pending", 237 | "Paid" 238 | ], 239 | "enum": [ 240 | "Pending", 241 | "Paid" 242 | ] 243 | }, 244 | "NotificationLevel": { 245 | "type": "string", 246 | "description": "", 247 | "x-enumNames": [ 248 | "Unknown", 249 | "Default", 250 | "Success", 251 | "Info", 252 | "Warning", 253 | "Danger" 254 | ], 255 | "enum": [ 256 | "Unknown", 257 | "Default", 258 | "Success", 259 | "Info", 260 | "Warning", 261 | "Danger" 262 | ] 263 | }, 264 | "EnumWithoutName": { 265 | "description": "", 266 | "enum": [0, 1, 2, 10, 30] 267 | }, 268 | "Type": { 269 | "enum": [ 270 | "0", 271 | "-1" 272 | ], 273 | "type": "integer", 274 | "format": "int32" 275 | } 276 | } 277 | } 278 | } 279 | -------------------------------------------------------------------------------- /src/kotlin/generator.mts: -------------------------------------------------------------------------------- 1 | import { 2 | getPathParams, 3 | generateServiceName, 4 | getHeaderParams, 5 | getParametersInfo, 6 | getRefName, 7 | toPascalCase, 8 | } from "./utils.mjs"; 9 | import type { 10 | SwaggerRequest, 11 | SwaggerJson, 12 | SwaggerResponse, 13 | Config, 14 | ApiAST, 15 | TypeAST, 16 | Schema, 17 | Parameter, 18 | ConstantsAST, 19 | Method, 20 | } from "../types.mjs"; 21 | import { generateApis } from "./generateApis.mjs"; 22 | import { generateTypes } from "./generateTypes.mjs"; 23 | 24 | function generator( 25 | input: SwaggerJson, 26 | config: Config, 27 | ): { code: string; type: string } { 28 | const apis: ApiAST[] = []; 29 | const types: TypeAST[] = []; 30 | let constantsCounter = 0; 31 | const constants: ConstantsAST[] = []; 32 | 33 | function getConstantName(value: string) { 34 | const constant = constants.find((_constant) => _constant.value === value); 35 | if (constant) { 36 | return constant.name; 37 | } 38 | 39 | const name = `_CONSTANT${constantsCounter++}`; 40 | 41 | constants.push({ 42 | name, 43 | value, 44 | }); 45 | 46 | return name; 47 | } 48 | 49 | try { 50 | Object.entries(input.paths).forEach(([endPoint, value]) => { 51 | const parametersExtended = value.parameters as Parameter[] | undefined; 52 | Object.entries(value).forEach( 53 | ([method, options]: [string, SwaggerRequest]) => { 54 | if (method === "parameters") { 55 | return; 56 | } 57 | 58 | const { operationId, security } = options; 59 | 60 | const allParameters = 61 | parametersExtended || options.parameters 62 | ? [...(parametersExtended || []), ...(options.parameters || [])] 63 | : undefined; 64 | 65 | const parameters = allParameters?.map((parameter) => { 66 | const { $ref } = parameter; 67 | if ($ref) { 68 | const name = $ref.replace("#/components/parameters/", ""); 69 | return { 70 | ...input.components?.parameters?.[name]!, 71 | $ref, 72 | schema: { $ref } as Schema, 73 | }; 74 | } 75 | return parameter; 76 | }); 77 | 78 | const serviceName = generateServiceName( 79 | endPoint, 80 | method, 81 | operationId, 82 | config, 83 | ); 84 | 85 | const pathParams = getPathParams(parameters); 86 | 87 | const { 88 | exist: isQueryParamsExist, 89 | isNullable: isQueryParamsNullable, 90 | params: queryParameters, 91 | } = getParametersInfo(parameters, "query"); 92 | const queryParamsTypeName: string | false = isQueryParamsExist 93 | ? `${toPascalCase(serviceName)}QueryParams` 94 | : false; 95 | 96 | if (queryParamsTypeName) { 97 | types.push({ 98 | name: queryParamsTypeName, 99 | schema: { 100 | type: "object", 101 | nullable: isQueryParamsNullable, 102 | properties: queryParameters?.reduce( 103 | ( 104 | prev, 105 | { name, schema, $ref, required: _required, description }, 106 | ) => { 107 | return { 108 | ...prev, 109 | [name]: { 110 | ...($ref ? { $ref } : schema), 111 | nullable: !_required, 112 | description, 113 | } as Schema, 114 | }; 115 | }, 116 | {}, 117 | ), 118 | }, 119 | }); 120 | } 121 | 122 | const { params: headerParams, isNullable: hasNullableHeaderParams } = 123 | getHeaderParams(parameters, config); 124 | 125 | const requestBody = getBodyContent(options.requestBody); 126 | 127 | const contentType = Object.keys( 128 | options.requestBody?.content || 129 | (options.requestBody?.$ref && 130 | input.components?.requestBodies?.[ 131 | getRefName(options.requestBody.$ref as string) 132 | ]?.content) || { 133 | "application/json": null, 134 | }, 135 | )[0] as ApiAST["contentType"]; 136 | 137 | const accept = Object.keys( 138 | options.responses?.[200]?.content || { 139 | "application/json": null, 140 | }, 141 | )[0]; 142 | 143 | const responses = getBodyContent(options.responses?.[200]); 144 | 145 | let pathParamsRefString: string | undefined = pathParams.reduce( 146 | (prev, { name }) => `${prev}${name},`, 147 | "", 148 | ); 149 | pathParamsRefString = pathParamsRefString 150 | ? `{${pathParamsRefString}}` 151 | : undefined; 152 | 153 | const additionalAxiosConfig = headerParams 154 | ? `{ 155 | headers:{ 156 | ...${getConstantName(`{ 157 | "Content-Type": "${contentType}", 158 | Accept: "${accept}", 159 | 160 | }`)}, 161 | ...headerParams, 162 | }, 163 | }` 164 | : getConstantName(`{ 165 | headers: { 166 | "Content-Type": "${contentType}", 167 | Accept: "${accept}", 168 | }, 169 | }`); 170 | 171 | apis.push({ 172 | contentType, 173 | summary: options.summary, 174 | deprecated: options.deprecated, 175 | serviceName, 176 | queryParamsTypeName, 177 | pathParams, 178 | requestBody, 179 | headerParams, 180 | isQueryParamsNullable, 181 | isHeaderParamsNullable: hasNullableHeaderParams, 182 | responses, 183 | pathParamsRefString, 184 | endPoint, 185 | method: method as Method, 186 | security: security 187 | ? getConstantName(JSON.stringify(security)) 188 | : "undefined", 189 | additionalAxiosConfig, 190 | queryParameters, 191 | }); 192 | }, 193 | ); 194 | }); 195 | 196 | if (input?.components?.schemas) { 197 | types.push( 198 | ...Object.entries(input.components.schemas).map(([name, schema]) => { 199 | return { 200 | name, 201 | schema, 202 | }; 203 | }), 204 | ); 205 | } 206 | 207 | if (input?.components?.parameters) { 208 | types.push( 209 | ...Object.entries(input.components.parameters).map(([key, value]) => ({ 210 | ...value, 211 | name: key, 212 | })), 213 | ); 214 | } 215 | 216 | if (input?.components?.requestBodies) { 217 | types.push( 218 | ...(Object.entries(input.components.requestBodies) 219 | .map(([name, _requestBody]) => { 220 | return { 221 | name: `RequestBody${name}`, 222 | schema: Object.values(_requestBody.content || {})[0]?.schema, 223 | description: _requestBody.description, 224 | }; 225 | }) 226 | .filter((v) => v.schema) as any), 227 | ); 228 | } 229 | 230 | const code = generateApis(apis, types, config); 231 | const type = generateTypes(types, config); 232 | 233 | return { code, type }; 234 | } catch (error) { 235 | console.error({ error }); 236 | return { code: "", type: "" }; 237 | } 238 | } 239 | 240 | function getBodyContent(responses?: SwaggerResponse): Schema | undefined { 241 | if (!responses) { 242 | return responses; 243 | } 244 | 245 | return responses.content 246 | ? Object.values(responses.content)[0].schema 247 | : responses.$ref 248 | ? ({ 249 | $ref: responses.$ref, 250 | } as Schema) 251 | : undefined; 252 | } 253 | 254 | export { generator }; 255 | -------------------------------------------------------------------------------- /src/index.mts: -------------------------------------------------------------------------------- 1 | import { writeFileSync, existsSync, mkdirSync, readFileSync } from "fs"; 2 | import { 3 | SwaggerJson, 4 | SwaggerConfig, 5 | Config, 6 | CLIConfig, 7 | FileConfig, 8 | } from "./types.mjs"; 9 | import { getJson } from "./getJson.mjs"; 10 | import { generateJavascriptService } from "./javascript/index.mjs"; 11 | import { 12 | getCurrentUrl, 13 | getPrettierOptions, 14 | majorVersionsCheck, 15 | } from "./utils.mjs"; 16 | import { swaggerToOpenApi } from "./utilities/swaggerToOpenApi.mjs"; 17 | import chalk from "chalk"; 18 | import { partialUpdateJson } from "./updateJson.mjs"; 19 | import { default as postmanToOpenApi } from "postman-ke-openapi"; 20 | import yaml from "js-yaml"; 21 | import path from "path"; 22 | import { generateKotlinService } from "./kotlin/index.mjs"; 23 | import { format } from "prettier"; 24 | 25 | /** 26 | * Main generation function that processes one or multiple swagger 27 | * configurations 28 | * 29 | * @param config - Configuration object or array of configurations. If 30 | * undefined, will use swagger.config.json 31 | * @param cli - CLI options that override file-based configuration 32 | * @throws Error when configuration is invalid or generation fails 33 | */ 34 | async function generate( 35 | config?: SwaggerConfig, 36 | cli?: Partial, 37 | ): Promise { 38 | try { 39 | const resolvedConfig = config ?? getSwaggerConfig(cli); 40 | 41 | if (!resolvedConfig) { 42 | throw new Error( 43 | "Configuration is required for code generation, create a swagger.config.json file", 44 | ); 45 | } 46 | 47 | const configs = Array.isArray(resolvedConfig) 48 | ? resolvedConfig 49 | : [resolvedConfig]; 50 | 51 | // Validate each configuration before processing 52 | for (const singleConfig of configs) { 53 | validateConfiguration(singleConfig); 54 | } 55 | 56 | // Process each configuration 57 | for (const singleConfig of configs) { 58 | await generateService(singleConfig, cli); 59 | } 60 | } catch (error) { 61 | console.error("Generation failed:", error); 62 | throw error; 63 | } 64 | } 65 | 66 | /** 67 | * Validates a single configuration object 68 | * 69 | * @param config - Configuration to validate 70 | * @throws Error when configuration is invalid 71 | */ 72 | function validateConfiguration(config: FileConfig): void { 73 | if (!config.dir) { 74 | throw new Error("Configuration must specify an output directory (dir)"); 75 | } 76 | 77 | if (!config.url) { 78 | throw new Error( 79 | "Configuration must specify either 'url' for remote source or local file", 80 | ); 81 | } 82 | 83 | const validLanguages = ["javascript", "typescript", "kotlin"]; 84 | if (config.language && !validLanguages.includes(config.language)) { 85 | throw new Error( 86 | `Unsupported language: ${ 87 | config.language 88 | }. Supported languages: ${validLanguages.join(", ")}`, 89 | ); 90 | } 91 | 92 | if (config.language === "kotlin" && !config.kotlinPackage) { 93 | throw new Error("Kotlin language requires 'kotlinPackage' to be specified"); 94 | } 95 | } 96 | 97 | /** 98 | * Merges file configuration with CLI configuration 99 | * 100 | * @param fileConfig - Configuration from file 101 | * @param cli - CLI configuration overrides 102 | * @returns Merged configuration 103 | */ 104 | function mergeConfigurations( 105 | fileConfig: FileConfig, 106 | cli?: Partial, 107 | ): Config { 108 | return { 109 | ...fileConfig, 110 | tag: cli?.tag ?? fileConfig.tag, 111 | local: cli?.local ?? fileConfig.local, 112 | branch: cli?.branch ?? fileConfig.branch, 113 | config: cli?.config, 114 | }; 115 | } 116 | 117 | /** 118 | * Ensures the output directory exists 119 | * 120 | * @param dir - Directory path to create if it doesn't exist 121 | */ 122 | function ensureOutputDirectory(dir: string): void { 123 | if (!existsSync(dir)) { 124 | mkdirSync(dir, { recursive: true }); 125 | } 126 | } 127 | 128 | /** 129 | * Fetches and processes JSON data from URL or local file 130 | * 131 | * @param config - Configuration object 132 | * @returns Processed SwaggerJson object 133 | */ 134 | async function fetchAndProcessJson(config: Config): Promise { 135 | const { url, local, dir } = config; 136 | 137 | if (local) { 138 | return getLocalJson(dir); 139 | } 140 | 141 | if (!url) { 142 | throw new Error("Add url in swagger.config.json "); 143 | } 144 | 145 | const resolvedUrl = 146 | typeof url === "string" ? url : await getCurrentUrl(config); 147 | const input = await getJson(resolvedUrl); 148 | 149 | return normalizeJsonFormat(input); 150 | } 151 | 152 | /** 153 | * Normalizes different JSON formats to OpenAPI v3 154 | * 155 | * @param input - Input JSON in various formats 156 | * @returns Normalized SwaggerJson object 157 | */ 158 | async function normalizeJsonFormat(input: SwaggerJson): Promise { 159 | if (input.swagger) { 160 | majorVersionsCheck("2.0.0", input.swagger); 161 | return await swaggerToOpenApi(input); 162 | } 163 | 164 | if (input.openapi) { 165 | majorVersionsCheck("3.0.0", input.openapi); 166 | return input; 167 | } 168 | 169 | // Handle Postman collections 170 | return yaml.load( 171 | await postmanToOpenApi(JSON.stringify(input), undefined), 172 | ) as SwaggerJson; 173 | } 174 | 175 | /** 176 | * Handles JSON persistence based on configuration 177 | * 178 | * @param config - Configuration object 179 | * @param input - JSON data to persist 180 | * @param swaggerJsonPath - Path to save the JSON file 181 | * @returns Updated JSON data (if partial update was performed) 182 | */ 183 | async function handleJsonPersistence( 184 | config: Config, 185 | input: SwaggerJson, 186 | swaggerJsonPath: string, 187 | ): Promise { 188 | if (!config.keepJson) { 189 | return input; 190 | } 191 | 192 | try { 193 | if (!config.tag?.length) { 194 | writeFileSync( 195 | swaggerJsonPath, 196 | await format(JSON.stringify(input), getPrettierOptions(config)), 197 | ); 198 | return input; 199 | } 200 | 201 | const oldJson = getLocalJson(config.dir); 202 | const updatedInput = partialUpdateJson(oldJson, input, config.tag); 203 | writeFileSync(swaggerJsonPath, JSON.stringify(updatedInput)); 204 | return updatedInput; 205 | } catch (error) { 206 | console.log(chalk.red("keepJson failed"), chalk.red(error)); 207 | return input; 208 | } 209 | } 210 | 211 | /** 212 | * Dispatches to appropriate language-specific generator 213 | * 214 | * @param config - Configuration object 215 | * @param input - Processed JSON data 216 | */ 217 | async function dispatchToGenerator( 218 | config: Config, 219 | input: SwaggerJson, 220 | ): Promise { 221 | switch (config.language) { 222 | case "kotlin": 223 | await generateKotlinService(config, input); 224 | break; 225 | default: 226 | await generateJavascriptService(config, input); 227 | break; 228 | } 229 | } 230 | 231 | /** 232 | * Main service generation function with improved separation of concerns 233 | * 234 | * @param fileConfig - File-based configuration 235 | * @param cli - CLI configuration overrides 236 | */ 237 | const generateService = async ( 238 | fileConfig: FileConfig, 239 | cli?: Partial, 240 | ): Promise => { 241 | try { 242 | console.log(chalk.blueBright(`Start ${fileConfig.dir} generation...`)); 243 | 244 | const config = mergeConfigurations(fileConfig, cli); 245 | 246 | ensureOutputDirectory(config.dir); 247 | 248 | const swaggerJsonPath = `${config.dir}/swagger.json`; 249 | let input = await fetchAndProcessJson(config); 250 | 251 | input = await handleJsonPersistence(config, input, swaggerJsonPath); 252 | 253 | await dispatchToGenerator(config, input); 254 | } catch (error) { 255 | console.log(chalk.redBright(error), chalk.redBright("Generation failed")); 256 | } 257 | }; 258 | 259 | function getSwaggerConfig(cli?: CLIConfig): SwaggerConfig { 260 | try { 261 | const rawPath = cli?.config || "swagger.config.json"; 262 | const configPath = rawPath.endsWith(".json") 263 | ? rawPath 264 | : path.join(rawPath, "swagger.config.json"); 265 | 266 | const fullPath = path.isAbsolute(configPath) 267 | ? configPath 268 | : path.join(process.cwd(), configPath); 269 | 270 | console.log(chalk.grey(`Your config path: ${fullPath}`)); 271 | 272 | const config = readJson(fullPath); 273 | return config; 274 | } catch (error) { 275 | throw new Error("Please define swagger.config.json"); 276 | } 277 | } 278 | 279 | function getLocalJson(dir: string): SwaggerJson { 280 | const swaggerJsonPath = `${dir}/swagger.json`; 281 | 282 | try { 283 | return readJson(swaggerJsonPath); 284 | } catch (error) { 285 | console.log( 286 | chalk.red( 287 | "swagger.json file not found. You should set keepJson true to save json then run swag-ts without tag to save that", 288 | ), 289 | ); 290 | throw error; 291 | } 292 | } 293 | 294 | function readJson(path: string) { 295 | const old = readFileSync(path).toString(); 296 | return JSON.parse(old); 297 | } 298 | 299 | export { generate }; 300 | -------------------------------------------------------------------------------- /src/kotlin/utils.mts: -------------------------------------------------------------------------------- 1 | import { Schema, Parameter, Config } from "../types.mjs"; 2 | import { getJsdoc } from "../utilities/jsdoc.mjs"; 3 | import { isAscending } from "../utils.mjs"; 4 | 5 | function getPathParams(parameters?: Parameter[]): Parameter[] { 6 | return ( 7 | parameters?.filter(({ in: In }) => { 8 | return In === "path"; 9 | }) || [] 10 | ); 11 | } 12 | 13 | function getHeaderParams(parameters: Parameter[] | undefined, config: Config) { 14 | const params = 15 | parameters?.filter(({ in: In, name }) => { 16 | return In === "header" && !config.ignore?.headerParams?.includes(name); 17 | }) || []; 18 | 19 | return { 20 | params, 21 | isNullable: params.every(({ schema = {} }) => !schema.required), 22 | }; 23 | } 24 | 25 | function toPascalCase(str: string): string { 26 | return `${str.substring(0, 1).toUpperCase()}${str.substring(1)}`; 27 | } 28 | function replaceWithUpper(str: string, sp: string) { 29 | let pointArray = str.split(sp); 30 | pointArray = pointArray.map((point) => toPascalCase(point)); 31 | 32 | return pointArray.join(""); 33 | } 34 | 35 | function generateServiceName( 36 | endPoint: string, 37 | method: string, 38 | operationId: string | undefined, 39 | config: Config, 40 | ): string { 41 | const { methodName, prefix = "" } = config; 42 | 43 | const _endPoint = endPoint.replace(new RegExp(`^${prefix}`, "i"), ""); 44 | const path = getSchemaName(_endPoint); 45 | 46 | const methodNameTemplate = getTemplate(methodName, operationId); 47 | 48 | const serviceName = template(methodNameTemplate, { 49 | path, 50 | method, 51 | ...(operationId ? { operationId } : {}), 52 | }); 53 | return serviceName; 54 | } 55 | 56 | function getTemplate(methodName?: string, operationId?: string) { 57 | const defaultTemplate = "{method}{path}"; 58 | if (!methodName) { 59 | return defaultTemplate; 60 | } 61 | 62 | const hasMethodNameOperationId = /(\{operationId\})/i.test(methodName); 63 | 64 | if (hasMethodNameOperationId) { 65 | return operationId ? methodName : defaultTemplate; 66 | } 67 | 68 | return methodName; 69 | } 70 | 71 | const TYPES = { 72 | integer: "Int", 73 | number: "Long", 74 | boolean: "Boolean", 75 | object: "Any", 76 | string: "String", 77 | array: "List", 78 | }; 79 | 80 | function getDefineParam( 81 | name: string, 82 | required: boolean = false, 83 | schema: Schema | undefined, 84 | config: Config, 85 | description?: string, 86 | ): string { 87 | return getParamString( 88 | name, 89 | required, 90 | getKotlinType(schema, config), 91 | description, 92 | ); 93 | } 94 | 95 | function getDefinitionBody( 96 | name: string, 97 | schema: Schema | undefined, 98 | config: Config, 99 | description?: string, 100 | ): string { 101 | const type = getKotlinType(schema, config); 102 | return `${getJsdoc({ 103 | description, 104 | })}@Body ${name}: ${type}`; 105 | } 106 | 107 | function getHeaderString( 108 | name: string, 109 | required: boolean = false, 110 | type: string, 111 | description?: string, 112 | ): string { 113 | return `${ 114 | // getJsdoc({ 115 | // description, 116 | // }) 117 | "" 118 | } 119 | @Headers("Content-Type: ${type}")`; 120 | } 121 | 122 | function getParamString( 123 | name: string, 124 | required: boolean = false, 125 | type: string, 126 | description?: string, 127 | isPartial?: boolean, 128 | ): string { 129 | return `${ 130 | // getJsdoc({ 131 | // description, 132 | // }) 133 | "" 134 | }@Path("${name}") ${name}: ${type}${required ? "" : "?"}`; 135 | } 136 | //x-nullable 137 | function normalizeObjectPropertyNullable( 138 | propertyName: string, 139 | schema: Schema, 140 | required?: string[], 141 | ) { 142 | if (schema.nullable !== undefined) { 143 | return schema.nullable; 144 | } 145 | if (schema["x-nullable"] !== undefined) { 146 | return schema["x-nullable"]; 147 | } 148 | if (required) { 149 | return !required.includes(propertyName); 150 | } 151 | return true; 152 | } 153 | 154 | function getClassBody( 155 | schema: undefined | true | {} | Schema, 156 | config: Config, 157 | ): string { 158 | if (isTypeAny(schema)) { 159 | return "Any"; 160 | } 161 | 162 | const { properties, required } = schema as Schema; 163 | 164 | if (properties) { 165 | return getObjectType( 166 | Object.entries(properties).map(([pName, _schema]) => ({ 167 | schema: { 168 | ..._schema, 169 | nullable: normalizeObjectPropertyNullable(pName, _schema, required), 170 | }, 171 | name: pName, 172 | })), 173 | config, 174 | ); 175 | } 176 | 177 | return "Any"; 178 | } 179 | 180 | function getKotlinType( 181 | schema: undefined | true | {} | Schema, 182 | config: Config, 183 | ): string { 184 | if (isTypeAny(schema)) { 185 | return "Any"; 186 | } 187 | 188 | const { 189 | type, 190 | $ref, 191 | enum: Enum, 192 | items, 193 | properties, 194 | oneOf, 195 | additionalProperties, 196 | required, 197 | allOf, 198 | } = schema as Schema; 199 | 200 | if ($ref) { 201 | const refArray = $ref.split("/"); 202 | if (refArray[refArray.length - 2] === "requestBodies") { 203 | return `RequestBody${getRefName($ref)}`; 204 | } else { 205 | return getRefName($ref); 206 | } 207 | } 208 | if (Enum) { 209 | return "String"; 210 | // return `${Enum.map((t) => `"${t}"`).join(" | ")}`; 211 | } 212 | 213 | if (items) { 214 | return `List<${getKotlinType(items, config)}>`; 215 | } 216 | 217 | let result = ""; 218 | 219 | if (properties) { 220 | result = "Any"; 221 | } 222 | 223 | if (oneOf) { 224 | result = "Any"; 225 | // result = `${result} & (${oneOf 226 | // .map((t) => `(${getTsType(t, config)})`) 227 | // .join(" | ")})`; 228 | } 229 | 230 | if (allOf) { 231 | result = "Any"; 232 | 233 | // result = `${result} & (${allOf 234 | // .map((_schema) => getTsType(_schema, config)) 235 | // .join(" & ")})`; 236 | } 237 | 238 | if (type === "object" && !result) { 239 | // if (additionalProperties) { 240 | // return `{[x: string]: ${getTsType(additionalProperties, config)}}`; 241 | // } 242 | 243 | return "Any"; 244 | } 245 | 246 | return result || TYPES[type as keyof typeof TYPES]; 247 | } 248 | 249 | function getObjectType( 250 | parameter: { schema?: Schema; name: string }[], 251 | config: Config, 252 | ) { 253 | const object = parameter 254 | .sort( 255 | ( 256 | { name, schema: { nullable } = {} }, 257 | { name: _name, schema: { nullable: _nullable } = {} }, 258 | ) => { 259 | if (!nullable && _nullable) { 260 | return -1; 261 | } else if (nullable && !_nullable) { 262 | return 1; 263 | } 264 | 265 | return isAscending(name, _name); 266 | }, 267 | ) 268 | .reduce( 269 | ( 270 | prev, 271 | { 272 | schema: { 273 | deprecated, 274 | "x-deprecatedMessage": deprecatedMessage, 275 | example, 276 | nullable, 277 | } = {}, 278 | schema, 279 | name, 280 | }, 281 | ) => { 282 | const jsdoc = getJsdoc({ 283 | ...schema, 284 | deprecated: 285 | deprecated || deprecatedMessage ? deprecatedMessage : undefined, 286 | example, 287 | }); 288 | return `${ 289 | prev 290 | ? ` 291 | ${prev}` 292 | : "" 293 | }${jsdoc} 294 | val ${name}: ${getKotlinType(schema, config)}${nullable ? "?" : ""},`; 295 | }, 296 | "", 297 | ); 298 | 299 | return object; 300 | } 301 | function getSchemaName(name: string): string { 302 | ["/", ".", "`", "[", "]", "-", "*", "{", "}"].forEach((str) => { 303 | name = replaceWithUpper(name, str); 304 | }); 305 | 306 | return name; 307 | } 308 | 309 | function getRefName($ref: string): string { 310 | const parts = $ref.split("/").pop(); 311 | return getSchemaName(parts || ""); 312 | } 313 | 314 | function getParametersInfo( 315 | parameters: Parameter[] | undefined, 316 | type: "query" | "header", 317 | ) { 318 | const params = 319 | parameters?.filter(({ in: In }) => { 320 | return In === type; 321 | }) || []; 322 | 323 | return { 324 | params, 325 | exist: params.length > 0, 326 | isNullable: !params.some( 327 | ({ schema, required }) => 328 | //swagger 2 329 | required || 330 | // openapi 3 331 | schema?.required, 332 | ), 333 | }; 334 | } 335 | 336 | function isTypeAny(type: true | undefined | {} | Schema) { 337 | if (type === true) { 338 | return true; 339 | } 340 | 341 | if (typeof type === "object" && Object.keys(type).length <= 0) { 342 | return true; 343 | } 344 | 345 | if (!type || (type as Schema).AnyValue) { 346 | return true; 347 | } 348 | 349 | return false; 350 | } 351 | 352 | /** Used to replace {name} in string with obj.name */ 353 | function template(str: string, obj: { [x: string]: string } = {}) { 354 | Object.entries(obj).forEach(([key, value]) => { 355 | const re = new RegExp(`{${key}}`, "i"); 356 | str = str.replace(re, value); 357 | }); 358 | 359 | const re = new RegExp("{*}", "g"); 360 | if (re.test(str)) { 361 | throw new Error(`methodName: Some A key is missed "${str}"`); 362 | } 363 | return str; 364 | } 365 | 366 | export { 367 | getPathParams, 368 | getHeaderParams, 369 | generateServiceName, 370 | getKotlinType, 371 | getClassBody, 372 | getRefName, 373 | isAscending, 374 | getDefineParam, 375 | getParamString, 376 | getParametersInfo, 377 | isTypeAny, 378 | template, 379 | toPascalCase, 380 | getSchemaName, 381 | getDefinitionBody, 382 | getHeaderString, 383 | }; 384 | -------------------------------------------------------------------------------- /__tests__/__snapshots__/enums.test.mjs.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`enums generate enum as type: generate Code 1`] = ` 4 | "//@ts-nocheck 5 | /** 6 | * AUTO_GENERATED Do not change this file directly, use config.ts file instead 7 | * 8 | * @version 6 9 | */ 10 | 11 | import type { AxiosRequestConfig } from \\"axios\\"; 12 | import type { SwaggerResponse } from \\"./config\\"; 13 | import { Http } from \\"./httpRequest\\"; 14 | //@ts-ignore 15 | import qs from \\"qs\\"; 16 | import type { 17 | GetSettlementV1PrivateSettlementGetQueryParams, 18 | SettlementRequestResultListResponseVM, 19 | } from \\"./types\\"; 20 | 21 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 22 | const __DEV__ = process.env.NODE_ENV !== \\"production\\"; 23 | 24 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 25 | function overrideConfig( 26 | config?: AxiosRequestConfig, 27 | configOverride?: AxiosRequestConfig, 28 | ): AxiosRequestConfig { 29 | return { 30 | ...config, 31 | ...configOverride, 32 | headers: { 33 | ...config?.headers, 34 | ...configOverride?.headers, 35 | }, 36 | }; 37 | } 38 | 39 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 40 | export function template(path: string, obj: { [x: string]: any } = {}) { 41 | Object.keys(obj).forEach((key) => { 42 | const re = new RegExp(\`{\${key}}\`, \\"i\\"); 43 | path = path.replace(re, obj[key]); 44 | }); 45 | 46 | return path; 47 | } 48 | 49 | function isFormData(obj: any) { 50 | // This checks for the append method which should exist on FormData instances 51 | return ( 52 | (typeof obj === \\"object\\" && 53 | typeof obj.append === \\"function\\" && 54 | obj[Symbol.toStringTag] === undefined) || 55 | obj[Symbol.toStringTag] === \\"FormData\\" 56 | ); 57 | } 58 | 59 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 60 | function objToForm(requestBody: object) { 61 | if (isFormData(requestBody)) { 62 | return requestBody; 63 | } 64 | const formData = new FormData(); 65 | 66 | Object.entries(requestBody).forEach(([key, value]) => { 67 | value && formData.append(key, value); 68 | }); 69 | 70 | return formData; 71 | } 72 | 73 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 74 | function objToUrlencoded(requestBody: object) { 75 | return qs.stringify(requestBody); 76 | } 77 | 78 | export const getSettlementV1PrivateSettlementGet = ( 79 | queryParams?: GetSettlementV1PrivateSettlementGetQueryParams, 80 | configOverride?: AxiosRequestConfig, 81 | ): Promise> => { 82 | return Http.getRequest( 83 | getSettlementV1PrivateSettlementGet.key, 84 | queryParams, 85 | undefined, 86 | _CONSTANT0, 87 | overrideConfig(_CONSTANT1, configOverride), 88 | ); 89 | }; 90 | 91 | /** Key is end point string without base url */ 92 | getSettlementV1PrivateSettlementGet.key = 93 | \\"/settlement/v1/private/settlement/get\\"; 94 | export const _CONSTANT0 = [{ bearer: [] }]; 95 | export const _CONSTANT1 = { 96 | headers: { 97 | \\"Content-Type\\": \\"application/json\\", 98 | Accept: \\"text/plain\\", 99 | }, 100 | }; 101 | " 102 | `; 103 | 104 | exports[`enums generate enum as type: generate hooks 1`] = `""`; 105 | 106 | exports[`enums generate enum as type: generate type 1`] = ` 107 | "//@ts-nocheck 108 | /** 109 | * AUTO_GENERATED Do not change this file directly, use config.ts file instead 110 | * 111 | * @version 6 112 | */ 113 | 114 | export type EnumWithoutName = 0 | 1 | 2 | 10 | 30; 115 | 116 | export interface GetSettlementV1PrivateSettlementGetQueryParams { 117 | EndDate?: string; 118 | IsAuto?: boolean; 119 | /** - Format: decimal */ 120 | MinimumAmount?: number; 121 | /** - Format: int32 */ 122 | Skip?: number; 123 | /** - Format: date-time */ 124 | StartDate?: string; 125 | States?: SettlementRequestStatus[]; 126 | /** - Format: int64 */ 127 | UserWalletId?: number; 128 | status?: SettlementRequestStatus; 129 | } 130 | 131 | export type NotificationLevel = 132 | | \\"Unknown\\" 133 | | \\"Default\\" 134 | | \\"Success\\" 135 | | \\"Info\\" 136 | | \\"Warning\\" 137 | | \\"Danger\\"; 138 | 139 | export interface SettlementRequestResultListResponseVM { 140 | /** - Format: int64 */ 141 | totalCount: number; 142 | settlementRequests?: SettlementRequestResultResponseVM[]; 143 | } 144 | 145 | export interface SettlementRequestResultResponseVM { 146 | automaticSettlement: boolean; 147 | /** - Format: decimal */ 148 | commissionAmount: number; 149 | /** - Format: date-time */ 150 | createDateUtc: string; 151 | /** - Format: guid */ 152 | creatorUserId: string; 153 | /** - Format: int32 */ 154 | domainId: number; 155 | /** - Format: int64 */ 156 | id: number; 157 | status: SettlementRequestStatus; 158 | /** - Format: int64 */ 159 | userWalletId: number; 160 | accountNumber?: string; 161 | } 162 | 163 | export type SettlementRequestStatus = \\"Pending\\" | \\"Paid\\"; 164 | 165 | /** - Format: int32 */ 166 | 167 | export type Type = \\"0\\" | \\"-1\\"; 168 | " 169 | `; 170 | 171 | exports[`enums generate enum: generate Code 1`] = ` 172 | "//@ts-nocheck 173 | /** 174 | * AUTO_GENERATED Do not change this file directly, use config.ts file instead 175 | * 176 | * @version 6 177 | */ 178 | 179 | import type { AxiosRequestConfig } from \\"axios\\"; 180 | import type { SwaggerResponse } from \\"./config\\"; 181 | import { Http } from \\"./httpRequest\\"; 182 | //@ts-ignore 183 | import qs from \\"qs\\"; 184 | import type { 185 | GetSettlementV1PrivateSettlementGetQueryParams, 186 | SettlementRequestResultListResponseVM, 187 | } from \\"./types\\"; 188 | 189 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 190 | const __DEV__ = process.env.NODE_ENV !== \\"production\\"; 191 | 192 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 193 | function overrideConfig( 194 | config?: AxiosRequestConfig, 195 | configOverride?: AxiosRequestConfig, 196 | ): AxiosRequestConfig { 197 | return { 198 | ...config, 199 | ...configOverride, 200 | headers: { 201 | ...config?.headers, 202 | ...configOverride?.headers, 203 | }, 204 | }; 205 | } 206 | 207 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 208 | export function template(path: string, obj: { [x: string]: any } = {}) { 209 | Object.keys(obj).forEach((key) => { 210 | const re = new RegExp(\`{\${key}}\`, \\"i\\"); 211 | path = path.replace(re, obj[key]); 212 | }); 213 | 214 | return path; 215 | } 216 | 217 | function isFormData(obj: any) { 218 | // This checks for the append method which should exist on FormData instances 219 | return ( 220 | (typeof obj === \\"object\\" && 221 | typeof obj.append === \\"function\\" && 222 | obj[Symbol.toStringTag] === undefined) || 223 | obj[Symbol.toStringTag] === \\"FormData\\" 224 | ); 225 | } 226 | 227 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 228 | function objToForm(requestBody: object) { 229 | if (isFormData(requestBody)) { 230 | return requestBody; 231 | } 232 | const formData = new FormData(); 233 | 234 | Object.entries(requestBody).forEach(([key, value]) => { 235 | value && formData.append(key, value); 236 | }); 237 | 238 | return formData; 239 | } 240 | 241 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 242 | function objToUrlencoded(requestBody: object) { 243 | return qs.stringify(requestBody); 244 | } 245 | 246 | export const getSettlementV1PrivateSettlementGet = ( 247 | queryParams?: GetSettlementV1PrivateSettlementGetQueryParams, 248 | configOverride?: AxiosRequestConfig, 249 | ): Promise> => { 250 | return Http.getRequest( 251 | getSettlementV1PrivateSettlementGet.key, 252 | queryParams, 253 | undefined, 254 | _CONSTANT0, 255 | overrideConfig(_CONSTANT1, configOverride), 256 | ); 257 | }; 258 | 259 | /** Key is end point string without base url */ 260 | getSettlementV1PrivateSettlementGet.key = 261 | \\"/settlement/v1/private/settlement/get\\"; 262 | export const _CONSTANT0 = [{ bearer: [] }]; 263 | export const _CONSTANT1 = { 264 | headers: { 265 | \\"Content-Type\\": \\"application/json\\", 266 | Accept: \\"text/plain\\", 267 | }, 268 | }; 269 | " 270 | `; 271 | 272 | exports[`enums generate enum: generate hooks 1`] = `""`; 273 | 274 | exports[`enums generate enum: generate type 1`] = ` 275 | "//@ts-nocheck 276 | /** 277 | * AUTO_GENERATED Do not change this file directly, use config.ts file instead 278 | * 279 | * @version 6 280 | */ 281 | 282 | export enum EnumWithoutName { 283 | 0 = 0, 284 | 1 = 1, 285 | 2 = 2, 286 | 10 = 10, 287 | 30 = 30, 288 | } 289 | 290 | export interface GetSettlementV1PrivateSettlementGetQueryParams { 291 | EndDate?: string; 292 | IsAuto?: boolean; 293 | /** - Format: decimal */ 294 | MinimumAmount?: number; 295 | /** - Format: int32 */ 296 | Skip?: number; 297 | /** - Format: date-time */ 298 | StartDate?: string; 299 | States?: SettlementRequestStatus[]; 300 | /** - Format: int64 */ 301 | UserWalletId?: number; 302 | status?: SettlementRequestStatus; 303 | } 304 | 305 | export enum NotificationLevel { 306 | Unknown = \\"Unknown\\", 307 | Default = \\"Default\\", 308 | Success = \\"Success\\", 309 | Info = \\"Info\\", 310 | Warning = \\"Warning\\", 311 | Danger = \\"Danger\\", 312 | } 313 | 314 | export interface SettlementRequestResultListResponseVM { 315 | /** - Format: int64 */ 316 | totalCount: number; 317 | settlementRequests?: SettlementRequestResultResponseVM[]; 318 | } 319 | 320 | export interface SettlementRequestResultResponseVM { 321 | automaticSettlement: boolean; 322 | /** - Format: decimal */ 323 | commissionAmount: number; 324 | /** - Format: date-time */ 325 | createDateUtc: string; 326 | /** - Format: guid */ 327 | creatorUserId: string; 328 | /** - Format: int32 */ 329 | domainId: number; 330 | /** - Format: int64 */ 331 | id: number; 332 | status: SettlementRequestStatus; 333 | /** - Format: int64 */ 334 | userWalletId: number; 335 | accountNumber?: string; 336 | } 337 | 338 | export enum SettlementRequestStatus { 339 | Pending = \\"Pending\\", 340 | Paid = \\"Paid\\", 341 | } 342 | 343 | /** - Format: int32 */ 344 | 345 | export enum Type { 346 | \\"0\\" = \\"0\\", 347 | \\"-1\\" = \\"-1\\", 348 | } 349 | " 350 | `; 351 | -------------------------------------------------------------------------------- /src/types.mts: -------------------------------------------------------------------------------- 1 | type DataType = 2 | /** This includes dates and files */ 3 | "string" | "number" | "integer" | "boolean" | "array" | "object"; 4 | 5 | export interface Schema { 6 | title?: string; 7 | nullable?: boolean; 8 | ["x-nullable"]?: boolean; 9 | maxLength?: number; 10 | max?: number; 11 | min?: number; 12 | pattern?: string; 13 | type?: DataType; 14 | /** 15 | * An array of arbitrary types can be defined as: 16 | * 17 | * Type: array; 18 | * items: { 19 | * } 20 | */ 21 | items?: Schema | {}; 22 | 23 | /** 24 | * Files are defined as strings: "binary" | "byte" 25 | * 26 | * - integer int32 signed 32 bits 27 | * - integer int64 signed 64 bits (a.k.a long) 28 | * - number float 29 | * - number double 30 | * - string 31 | * - string byte base64 encoded characters 32 | * - string binary any sequence of octets 33 | * - boolean 34 | * - string date As defined by full-date - RFC3339 35 | * - string date-time As defined by date-time - RFC3339 36 | * - string password A hint to UIs to obscure input. 37 | */ 38 | format?: 39 | | "int32" 40 | | "int64" 41 | | "float" 42 | | "double" 43 | | "byte" 44 | | "binary" 45 | | "date" 46 | | "date-time" 47 | | "date" 48 | | "password" 49 | // C# 50 | | "guid" 51 | // Java 52 | | "uuid"; 53 | /** 54 | * A free-form object (arbitrary property/value pairs) is defined as: 55 | * 56 | * Type: object 57 | * additionalProperties: {} 58 | * Or additionalProperties: true 59 | */ 60 | additionalProperties?: Schema | true | {}; 61 | properties?: { [name: string]: Schema }; 62 | /** 63 | * By default, all object properties are optional. You can specify the 64 | * required properties in the required list: 65 | */ 66 | required?: string[]; 67 | description?: string; 68 | example?: string; 69 | deprecated?: boolean; 70 | "x-deprecatedMessage"?: string; 71 | "x-enumNames"?: string[]; 72 | enum?: string[]; 73 | $ref?: string; 74 | allOf?: Schema[]; 75 | oneOf?: Schema[]; 76 | /** Is something link oneOf */ 77 | anyOf?: Schema[]; 78 | /** 79 | * Use the minimum and maximum keywords to specify the range of possible 80 | * values: 81 | * 82 | * Type: integer; 83 | * minimum: 1; 84 | * maximum: 20; 85 | * 86 | * By default, the minimum and maximum values are included in the range, that 87 | * is: 88 | * 89 | * Minimum ≤ value ≤ maximum 90 | * 91 | * To exclude the boundary values, specify exclusiveMinimum: true and 92 | * exclusiveMaximum: true. For example, you can define a floating-point number 93 | * range as 0–50 and exclude the 0 value: 94 | */ 95 | minimum?: number; 96 | exclusiveMinimum?: boolean; 97 | exclusiveMaximum?: boolean; 98 | maximum?: number; 99 | 100 | /** 101 | * A schema without a type matches any data type – numbers, strings, objects, 102 | * and so on. {} is shorthand syntax for an arbitrary-type schema: 103 | * 104 | * @example 105 | * components: { 106 | * schemas: { 107 | * AnyValue: { 108 | * } 109 | * } 110 | * } 111 | */ 112 | AnyValue?: { 113 | nullable?: boolean; 114 | description?: string; 115 | }; 116 | 117 | discriminator?: { 118 | propertyName: string; 119 | mapping?: { 120 | [key: string]: string; 121 | }; 122 | }; 123 | readOnly?: boolean; 124 | writeOnly?: boolean; 125 | xml?: { 126 | name?: string; 127 | namespace?: string; 128 | prefix?: string; 129 | attribute?: boolean; 130 | wrapped?: boolean; 131 | }; 132 | externalDocs?: { 133 | description?: string; 134 | url: string; 135 | }; 136 | examples?: { [x: string]: string }; 137 | not?: Schema; 138 | default?: any; 139 | multipleOf?: number; 140 | minLength?: number; 141 | maxItems?: number; 142 | minItems?: number; 143 | uniqueItems?: boolean; 144 | maxProperties?: number; 145 | minProperties?: number; 146 | } 147 | 148 | export type Parameter = { 149 | /** 150 | * The name of the parameter. Parameter names are case sensitive. If in is 151 | * "path", the name field MUST correspond to a template expression occurring 152 | * within the path field in the Paths Object. See Path Templating for further 153 | * information. If in is "header" and the name field is "Accept", 154 | * "Content-Type" or "Authorization", the parameter definition SHALL be 155 | * ignored. For all other cases, the name corresponds to the parameter name 156 | * used by the in property. 157 | */ 158 | name: string; 159 | /** The location of the parameter. */ 160 | in: "query" | "header" | "cookie" | "path"; 161 | /** 162 | * Determines whether this parameter is mandatory. If the parameter location 163 | * is "path", this property is REQUIRED and its value MUST be true. Otherwise, 164 | * the property MAY be included and its default value is false. 165 | */ 166 | required?: boolean; // true; 167 | /** The schema defining the type used for the parameter. */ 168 | schema?: Schema; 169 | $ref?: string; 170 | /** 171 | * A brief description of the parameter. This could contain examples of use. 172 | * CommonMark syntax MAY be used for rich text representation. 173 | */ 174 | description?: string; 175 | /** 176 | * Specifies that a parameter is deprecated and SHOULD be transitioned out of 177 | * usage. Default value is false. 178 | */ 179 | deprecated?: boolean; 180 | }; 181 | 182 | export interface SwaggerResponse { 183 | $ref?: string; 184 | description?: string; 185 | content?: Partial< 186 | Record< 187 | ApiAST["contentType"], 188 | Pick & { 189 | schema?: Schema; 190 | } 191 | > 192 | >; 193 | } 194 | 195 | export interface SwaggerRequest { 196 | tags?: string[]; // ["Account"]; 197 | summary?: string; // "Get user account balance"; 198 | operationId?: string; // "Account_GetBalance"; 199 | parameters?: Parameter[]; 200 | requestBody?: SwaggerResponse; 201 | responses: { [x: string]: SwaggerResponse }; 202 | deprecated?: boolean; 203 | security?: any[]; 204 | description?: string; 205 | externalDocs?: any; 206 | callbacks?: any; 207 | servers?: any[]; 208 | } 209 | 210 | export interface Components { 211 | schemas?: Record; 212 | parameters?: Record; 213 | requestBodies?: Record; 214 | } 215 | 216 | export interface SwaggerJson { 217 | openapi?: string; 218 | swagger?: string; 219 | paths: { 220 | [url: string]: PathItem; 221 | }; 222 | components?: Components; 223 | info: InfoObject; 224 | servers?: any[]; 225 | security?: any[]; 226 | tags?: any[]; 227 | externalDocs?: any; 228 | } 229 | 230 | export interface PathItem { 231 | $ref?: string; 232 | summary?: string; 233 | description?: string; 234 | get?: SwaggerRequest; 235 | put?: SwaggerRequest; 236 | post?: SwaggerRequest; 237 | delete?: SwaggerRequest; 238 | options?: SwaggerRequest; 239 | head?: SwaggerRequest; 240 | patch?: SwaggerRequest; 241 | trace?: SwaggerRequest; 242 | servers?: any[]; 243 | parameters?: any[]; 244 | } 245 | export interface InfoObject { 246 | title: string; 247 | version: string; 248 | description?: string; 249 | termsOfService?: string; 250 | contact?: any; 251 | license?: any; 252 | } 253 | 254 | export interface FileConfig { 255 | url?: string | { branch: string; url: string }[]; 256 | dir: string; 257 | /** 258 | * SignalR json url generated by https://github.com/majidbigdeli/SigSpec 259 | * 260 | * @todo Repo need help 261 | */ 262 | hub?: string; 263 | /** Default is false */ 264 | keepJson?: boolean; 265 | reactHooks?: boolean; 266 | includes?: string[]; 267 | excludes?: string[]; 268 | useQuery?: string[]; 269 | useInfiniteQuery?: string[]; 270 | mock?: string; 271 | prettierPath?: string; 272 | language?: "javascript" | "typescript" | "kotlin"; 273 | methodName?: string; 274 | prefix?: string; 275 | ignore?: { 276 | headerParams?: string[]; 277 | }; 278 | generateEnumAsType?: boolean; 279 | 280 | //kotlin 281 | kotlinPackage?: string; 282 | 283 | // CLI 284 | tag?: string[]; 285 | /** Generate with local swagger.json */ 286 | local?: boolean; 287 | /** Generate specific branch swagger */ 288 | branch?: string; 289 | } 290 | 291 | export interface CLIConfig { 292 | tag?: string[]; 293 | /** Generate with local swagger.json */ 294 | local?: boolean; 295 | /** Generate specific branch swagger */ 296 | branch?: string; 297 | 298 | /** _ swagger.config.json file path */ 299 | config?: string; 300 | } 301 | 302 | export interface Config extends FileConfig, CLIConfig {} 303 | 304 | export type SwaggerConfig = FileConfig | FileConfig[]; 305 | 306 | export type Method = 307 | | "get" 308 | | "put" 309 | | "post" 310 | | "delete" 311 | | "options" 312 | | "head" 313 | | "patch" 314 | | "trace"; 315 | 316 | export type ApiAST = { 317 | contentType: 318 | | "*/*" 319 | | "text/json" 320 | | "application/json" 321 | | "application/octet-stream" 322 | | "application/json-patch+json" 323 | | "application/*+json" 324 | | "multipart/form-data" 325 | | "application/x-www-form-urlencoded"; 326 | 327 | summary: string | undefined; 328 | deprecated: boolean | undefined; 329 | serviceName: string; 330 | pathParams: Parameter[]; 331 | requestBody: Schema | undefined; 332 | queryParamsTypeName: string | false; 333 | headerParams: string | Parameter[]; 334 | isQueryParamsNullable: boolean; 335 | isHeaderParamsNullable: boolean; 336 | responses: Schema | undefined; 337 | pathParamsRefString: string | undefined; 338 | endPoint: string; 339 | method: Method; 340 | security: string; 341 | additionalAxiosConfig: string; 342 | queryParameters: Parameter[]; 343 | }; 344 | 345 | export type TypeAST = { 346 | name: string; 347 | schema?: Schema; 348 | description?: string; 349 | }; 350 | 351 | export type JsdocAST = Pick< 352 | Schema, 353 | | "min" 354 | | "max" 355 | | "title" 356 | | "description" 357 | | "format" 358 | | "minimum" 359 | | "maximum" 360 | | "pattern" 361 | | "maxLength" 362 | | "minLength" 363 | | "example" 364 | > & { 365 | deprecated?: string; 366 | }; 367 | 368 | export type ConstantsAST = { value: string; name: string }; 369 | -------------------------------------------------------------------------------- /src/javascript/generator.mts: -------------------------------------------------------------------------------- 1 | import { 2 | getPathParams, 3 | generateServiceName, 4 | getHeaderParams, 5 | getParametersInfo, 6 | getRefName, 7 | toPascalCase, 8 | } from "./utils.mjs"; 9 | import type { 10 | SwaggerRequest, 11 | SwaggerJson, 12 | SwaggerResponse, 13 | Config, 14 | ApiAST, 15 | TypeAST, 16 | Schema, 17 | Parameter, 18 | ConstantsAST, 19 | Method, 20 | PathItem, 21 | } from "../types.mjs"; 22 | import { generateApis } from "./generateApis.mjs"; 23 | import { generateTypes } from "./generateTypes.mjs"; 24 | import { generateConstants } from "./generateConstants.mjs"; 25 | import { generateHook } from "./generateHook.mjs"; 26 | 27 | type GeneratorContext = { 28 | apis: ApiAST[]; 29 | types: TypeAST[]; 30 | constants: ConstantsAST[]; 31 | constantsCounter: number; 32 | input: SwaggerJson; 33 | config: Config; 34 | includeFilters: RegExp[]; 35 | excludeFilters: RegExp[]; 36 | }; 37 | 38 | function generator( 39 | input: SwaggerJson, 40 | config: Config, 41 | ): { code: string; hooks: string; type: string } { 42 | const context: GeneratorContext = { 43 | apis: [], 44 | types: [], 45 | constants: [], 46 | constantsCounter: 0, 47 | input, 48 | config, 49 | includeFilters: (config.includes || []).map( 50 | (pattern) => new RegExp(pattern), 51 | ), 52 | excludeFilters: (config.excludes || []).map( 53 | (pattern) => new RegExp(pattern), 54 | ), 55 | }; 56 | 57 | try { 58 | // Process API paths 59 | processApiPaths(context); 60 | 61 | // Extract types from components 62 | extractComponentTypes(context); 63 | 64 | // Generate final code 65 | let code = generateApis(context.apis, context.types, config); 66 | code += generateConstants(context.constants); 67 | const type = generateTypes(context.types, config); 68 | const hooks = config.reactHooks 69 | ? generateHook(context.apis, context.types, config) 70 | : ""; 71 | 72 | return { code, hooks, type }; 73 | } catch (error) { 74 | console.error({ error }); 75 | return { code: "", hooks: "", type: "" }; 76 | } 77 | } 78 | 79 | /** Get or create a constant and return its name */ 80 | function getConstantName(context: GeneratorContext, value: string): string { 81 | const existing = context.constants.find((c) => c.value === value); 82 | if (existing) { 83 | return existing.name; 84 | } 85 | 86 | const name = `_CONSTANT${context.constantsCounter++}`; 87 | context.constants.push({ name, value }); 88 | return name; 89 | } 90 | 91 | /** Check if a method should be included based on filters */ 92 | function shouldIncludeMethod( 93 | context: GeneratorContext, 94 | serviceName: string, 95 | ): boolean { 96 | const matchesInclude = 97 | !context.includeFilters.length || 98 | context.includeFilters.some((regex) => regex.test(serviceName)); 99 | 100 | const matchesExclude = context.excludeFilters.some((regex) => 101 | regex.test(serviceName), 102 | ); 103 | 104 | return matchesInclude && !matchesExclude; 105 | } 106 | 107 | /** Resolve parameter references */ 108 | function resolveParameters( 109 | context: GeneratorContext, 110 | parameters?: Parameter[], 111 | ): Parameter[] | undefined { 112 | return parameters?.map((parameter) => { 113 | const { $ref } = parameter; 114 | if (!$ref) { 115 | return parameter; 116 | } 117 | 118 | const name = $ref.replace("#/components/parameters/", ""); 119 | return { 120 | ...context.input.components?.parameters?.[name]!, 121 | $ref, 122 | schema: { $ref } as Schema, 123 | }; 124 | }); 125 | } 126 | 127 | /** Create query params type if needed */ 128 | function createQueryParamsType( 129 | context: GeneratorContext, 130 | serviceName: string, 131 | parameters?: Parameter[], 132 | ): string | false { 133 | const { 134 | exist: isQueryParamsExist, 135 | isNullable: isQueryParamsNullable, 136 | params: queryParameters, 137 | } = getParametersInfo(parameters, "query"); 138 | 139 | if (!isQueryParamsExist) { 140 | return false; 141 | } 142 | 143 | const typeName = `${toPascalCase(serviceName)}QueryParams`; 144 | const properties = queryParameters?.reduce( 145 | (prev, { name, schema, $ref, required: _required, description }) => ({ 146 | ...prev, 147 | [name]: { 148 | ...($ref ? { $ref } : schema), 149 | nullable: !_required, 150 | description, 151 | } as Schema, 152 | }), 153 | {}, 154 | ); 155 | 156 | context.types.push({ 157 | name: typeName, 158 | schema: { 159 | type: "object", 160 | nullable: isQueryParamsNullable, 161 | properties, 162 | }, 163 | }); 164 | 165 | return typeName; 166 | } 167 | 168 | /** Get content type from request body */ 169 | function getContentType( 170 | context: GeneratorContext, 171 | requestBody?: SwaggerRequest["requestBody"], 172 | ): string { 173 | const content = requestBody?.content || 174 | (requestBody?.$ref && 175 | context.input.components?.requestBodies?.[ 176 | getRefName(requestBody.$ref as string) 177 | ]?.content) || { "application/json": null }; 178 | 179 | return Object.keys(content)[0]; 180 | } 181 | 182 | /** Get accept header from responses */ 183 | function getAcceptHeader(responses?: SwaggerRequest["responses"]): string { 184 | const content = responses?.[200]?.content || { "application/json": null }; 185 | return Object.keys(content)[0]; 186 | } 187 | 188 | /** Build path params reference string */ 189 | function buildPathParamsRefString(pathParams: Parameter[]): string | undefined { 190 | if (pathParams.length === 0) { 191 | return undefined; 192 | } 193 | 194 | const paramNames = pathParams.map(({ name }) => name).join(","); 195 | return `{${paramNames}}`; 196 | } 197 | 198 | /** Build Axios configuration object */ 199 | function buildAxiosConfig( 200 | context: GeneratorContext, 201 | contentType: string, 202 | accept: string, 203 | headerParams?: string, 204 | ): string { 205 | if (headerParams) { 206 | return `{ 207 | headers:{ 208 | ...${getConstantName( 209 | context, 210 | `{ 211 | "Content-Type": "${contentType}", 212 | Accept: "${accept}", 213 | }`, 214 | )}, 215 | ...headerParams, 216 | }, 217 | }`; 218 | } 219 | 220 | return getConstantName( 221 | context, 222 | `{ 223 | headers: { 224 | "Content-Type": "${contentType}", 225 | Accept: "${accept}", 226 | }, 227 | }`, 228 | ); 229 | } 230 | 231 | /** Process a single API endpoint method */ 232 | function processEndpointMethod( 233 | context: GeneratorContext, 234 | endPoint: string, 235 | method: string, 236 | options: SwaggerRequest, 237 | pathLevelParams?: Parameter[], 238 | ): void { 239 | const { operationId, security } = options; 240 | 241 | // Merge path-level and operation-level parameters 242 | const allParameters = [ 243 | ...(pathLevelParams || []), 244 | ...(options.parameters || []), 245 | ]; 246 | const parameters = resolveParameters( 247 | context, 248 | allParameters.length > 0 ? allParameters : undefined, 249 | ); 250 | 251 | const serviceName = generateServiceName( 252 | endPoint, 253 | method, 254 | operationId, 255 | context.config, 256 | ); 257 | 258 | if (!shouldIncludeMethod(context, serviceName)) { 259 | return; 260 | } 261 | 262 | // Extract parameters 263 | const pathParams = getPathParams(parameters); 264 | const { params: headerParams, isNullable: isHeaderParamsNullable } = 265 | getHeaderParams(parameters, context.config); 266 | const { isNullable: isQueryParamsNullable, params: queryParameters } = 267 | getParametersInfo(parameters, "query"); 268 | 269 | // Create query params type 270 | const queryParamsTypeName = createQueryParamsType( 271 | context, 272 | serviceName, 273 | parameters, 274 | ); 275 | 276 | // Extract body and response info 277 | const requestBody = getBodyContent(options.requestBody); 278 | const responses = getBodyContent(options.responses?.[200]); 279 | const contentType = getContentType(context, options.requestBody); 280 | const accept = getAcceptHeader(options.responses); 281 | 282 | // Build API object 283 | context.apis.push({ 284 | contentType: contentType as ApiAST["contentType"], 285 | summary: options.summary, 286 | deprecated: options.deprecated, 287 | serviceName, 288 | queryParamsTypeName, 289 | pathParams, 290 | requestBody, 291 | headerParams, 292 | isQueryParamsNullable, 293 | isHeaderParamsNullable, 294 | responses, 295 | pathParamsRefString: buildPathParamsRefString(pathParams), 296 | endPoint, 297 | method: method as Method, 298 | security: security 299 | ? getConstantName(context, JSON.stringify(security)) 300 | : "undefined", 301 | additionalAxiosConfig: buildAxiosConfig( 302 | context, 303 | contentType, 304 | accept, 305 | headerParams, 306 | ), 307 | queryParameters, 308 | }); 309 | } 310 | 311 | /** Process all API paths */ 312 | function processApiPaths(context: GeneratorContext): void { 313 | Object.entries(context.input.paths).forEach(([endPoint, pathItem]) => { 314 | const pathLevelParams = pathItem.parameters as Parameter[] | undefined; 315 | 316 | Object.entries(pathItem).forEach(([method, options]) => { 317 | if (method === "parameters") { 318 | return; 319 | } 320 | 321 | processEndpointMethod( 322 | context, 323 | endPoint, 324 | method, 325 | options as SwaggerRequest, 326 | pathLevelParams, 327 | ); 328 | }); 329 | }); 330 | } 331 | 332 | /** Extract types from OpenAPI components */ 333 | function extractComponentTypes(context: GeneratorContext): void { 334 | const { components } = context.input; 335 | 336 | // Extract schemas 337 | if (components?.schemas) { 338 | Object.entries(components.schemas).forEach(([name, schema]) => { 339 | context.types.push({ name, schema }); 340 | }); 341 | } 342 | 343 | // Extract parameters 344 | if (components?.parameters) { 345 | Object.entries(components.parameters).forEach(([key, value]) => { 346 | context.types.push({ ...value, name: key }); 347 | }); 348 | } 349 | 350 | // Extract request bodies 351 | if (components?.requestBodies) { 352 | Object.entries(components.requestBodies).forEach(([name, requestBody]) => { 353 | const schema = Object.values(requestBody.content || {})[0]?.schema; 354 | if (schema) { 355 | context.types.push({ 356 | name: `RequestBody${name}`, 357 | schema, 358 | description: requestBody.description, 359 | }); 360 | } 361 | }); 362 | } 363 | } 364 | 365 | /** Extract body content from response or request body */ 366 | function getBodyContent(responses?: SwaggerResponse): Schema | undefined { 367 | if (!responses) { 368 | return undefined; 369 | } 370 | 371 | if (responses.content) { 372 | return Object.values(responses.content)[0].schema; 373 | } 374 | 375 | if (responses.$ref) { 376 | return { $ref: responses.$ref } as Schema; 377 | } 378 | 379 | return undefined; 380 | } 381 | 382 | export { generator }; 383 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | [![NPM](https://nodei.co/npm/swagger-typescript.png)](https://nodei.co/npm/swagger-typescript/) 2 | 3 | [![install size](https://packagephobia.now.sh/badge?p=swagger-typescript)](https://packagephobia.now.sh/result?p=swagger-typescript) [![dependencies](https://david-dm.org/hosseinmd/swagger-typescript.svg)](https://david-dm.org/hosseinmd/swagger-typescript.svg) 4 | 5 | :star::star::star: If you would like to contribute, please refer to [To do list](https://github.com/hosseinmd/swagger-typescript/projects/1) and a list of [open tasks](https://github.com/hosseinmd/swagger-typescript/issues?q=is%3Aopen).:star::star::star: 6 | 7 | # Swagger-Typescript: Generate ts/js code from swagger/openApi JSON 8 | 9 | Support OpenApi v3, swagger v2 and postman collection 10 | 11 | An auto typescript/javascript/kotlin code generator from APIs doc. 12 | Each endpoint will be created as a function, full type base. 13 | Supported 14 | 15 | - Generating a function for every apis 16 | - Generating all types, interfaces and enums which used in apis 17 | - React hooks. 18 | - SignalR hub. 19 | - Generating mock data. 20 | 21 | For Example: 22 | Get method of '/Account' path will be this code in services.ts 23 | 24 | ```js 25 | import { getAccount } from "./services"; 26 | 27 | const response = await getAccount({ id: 1234 }); 28 | ``` 29 | 30 | ## install 31 | 32 | `$ yarn add swagger-typescript prettier -D && yarn add axios` 33 | 34 | ## get start 35 | 36 | Before running, add your config to swagger.config.json 37 | 38 | #### swagger.config.json 39 | 40 | ```json 41 | { 42 | "url": "http://example.com/api/swagger.json", 43 | "dir": "./services", 44 | "prefix": "/api" 45 | } 46 | ``` 47 | 48 | #### run 49 | 50 | ``` 51 | yarn swag-ts 52 | ``` 53 | 54 | #### config.ts 55 | 56 | [more](#config) 57 | 58 | baseConfig 59 | 60 | ```ts 61 | const baseConfig: AxiosRequestConfig = { 62 | baseURL: "", // <--- Add your base url 63 | //other static configs 64 | }; 65 | ``` 66 | 67 | Now you can use APIs, So for advanced config read below. 68 | 69 | ## swagger.config.json 70 | 71 | For Example: 72 | 73 | ```json 74 | { 75 | "$schema": "https://raw.githubusercontent.com/hosseinmd/swagger-typescript/master/schema/v6.json", 76 | "url": "http://example.com/api/swagger.json", 77 | "dir": "./services", 78 | "prettierPath": ".prettierrc", 79 | "language": "typescript" 80 | } 81 | ``` 82 | 83 | | [`Key`] | [`default`] | Comment | 84 | | -------------------- | ---------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | 85 | | `url` | Required | swagger or postman collection Address. can be online or local (json/yaml) ([specific branch](#specific-branch)) | 86 | | `dir` | Required | Address of output | 87 | | `language` | `typescript` | export to "javascript", "typescript" or "kotlin" | 88 | | `methodName` | `{method}{path}` | Supported mixed of "{method}{path}{operationId}". for Example: 'service{method}{path}' | 89 | | `prefix` | Optional | prefix value will be removed from method name For example your endpoints is like "/api/v2/users", If you don't want add "/api/v2" to method name, add it to prefix | 90 | | `ignore` | Optional | Ignore headers from type for Example: `"ignore": { "headerParams": ["terminalId"]}` | 91 | | `mock` | false | For generate response mocks | 92 | | `keepJson` | false | This code will keep previous JSON for updating partially. change it to true then generate service for creating your first json file then you can update a tag for example `$ yarn swag-ts User` will update your user APIs which have User tag | 93 | | `reactHooks` | false | For generate react hooks of all APIs (using react-query under the hood) | 94 | | `useQuery` | [] | List of apis which is get but developed with post methods (Is useful for rest apis) for Example: ["postTicketsGetall"] (Needed to enable `reactHooks`) | 95 | | `useInfiniteQuery` | [] | List of apis which is get and could be handle infinity (Needed to enable `reactHooks`) parameter should be one of `page`, `pageNo` or `pageNumber` | 96 | | `local` | false | update swagger with local swagger.json located in your dir folder. add it to your config file or run it with cli `$ yarn swag-ts --local` | 97 | | `kotlinPackage` | Required (Only kotlin) | package name of source dir | 98 | | `generateEnumAsType` | false | 99 | | `includes` | [] | A list of regex patterns that specify which APIs to include based on matching method names | 100 | | `excludes` | [] | A list of regex patterns that specify which APIs to exclude based on matching method names | 101 | 102 | - `enum ReferralStatus {Successed="Successed","Error"="Error"} ` 103 | - `type ReferralStatus="Successed" | "Error"; // generateEnumAsType = true ` 104 | 105 | # CLI Options 106 | 107 | | [`Key`] | [`default`] | Comment | 108 | | -------- | ----------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | 109 | | `local` | false | update swagger with local swagger.json located in your dir folder. add it to your config file or run it with cli `$ yarn swag-ts --local` | 110 | | `branch` | Current Branch | to generate swagger for develop run `yarn swag-ts --branch=develop` or your branch name should be `develop` or a branch which created from develop | 111 | | `config` | "./swagger.config.json" | A path for config file location
- `yarn swag-ts --config=./config` path is related for "swagger.config.json" file in config folder
- `yarn swag-ts --config=./config/myswagger.json` you could change config file name
- `yarn swag-ts --config=/user/hosseinmd/desktop/config/swagger.config.json` you could add absolute path | 112 | | | 113 | 114 | ## Config 115 | 116 | The config.ts file automatically will be created after first run. You could change this file for customization. Don't change other files, if you want another config create Issue or PR. 117 | 118 | - getAxiosInstance 119 | 120 | getAxiosInstance used for create an instance of axios request you can customize that for what you needed 121 | 122 | - baseConfig 123 | 124 | baseConfig used for get static configs and headers. if you need some dynamic configs like add authentication to headers use `requestConfig.headers.authorization` into of `axiosInstance.interceptors.request.use` function. 125 | 126 | ## Run by node 127 | 128 | ```js 129 | const { generate } = require("swagger-typescript"); 130 | 131 | generate(config); 132 | // or 133 | generate(); // will be use ./swagger.config.json 134 | ``` 135 | 136 | ## Conflict 137 | 138 | In some situation teams have parallel backend development which cause conflict when updating swagger for solving this we have partially update, you can update your service just for a few tags and keep other old services codes. 139 | 140 | For Doing this you need to add this to your swagger.config.json 141 | 142 | ``` 143 | "keepJson": true, 144 | ``` 145 | 146 | This code will keep previous JSON for updating partially. 147 | 148 | Run `$ yarn swag-ts` with your base backend, for example develop branch 149 | 150 | Others need to pull this changes 151 | 152 | Now you can update Tag1 and Tag2 `$ yarn swag-ts Tag1 Tag2`. 153 | 154 | ## Multiple Gateway 155 | 156 | if you have multiple gateway in your project you could handle it by add array of config in swagger.config.json 157 | 158 | ```json 159 | [ 160 | { 161 | "$schema": "https://raw.githubusercontent.com/hosseinmd/swagger-typescript/master/schema/v6.json", 162 | "url": "http://example1.com/api/swagger.json", 163 | "dir": "./service1", 164 | "prettierPath": ".prettierrc", 165 | "language": "typescript" 166 | }, 167 | { 168 | "$schema": "https://raw.githubusercontent.com/hosseinmd/swagger-typescript/master/schema/v6.json", 169 | "url": "http://example2.com/api/swagger.json", 170 | "dir": "./service2", 171 | "prettierPath": ".prettierrc", 172 | "language": "typescript" 173 | } 174 | ] 175 | ``` 176 | 177 | ## Specific branch 178 | 179 | if you are managing project by multiple branch like gitflow, you need to update swagger based on you working branch or parent branch (for example your parent is develop if you create a branch from develop). 180 | 181 | For Example: 182 | 183 | ```json 184 | { 185 | "url": [ 186 | { 187 | "branch": "master", 188 | "url": "http:/example.com/api/swagger.json" 189 | }, 190 | { 191 | "branch": "develop", 192 | "url": "http://stage.example.com/api/swagger.json" 193 | } 194 | ] 195 | } 196 | ``` 197 | 198 | to generate swagger for develop run `yarn swag-ts --branch=develop` or your branch name should be `develop` or a branch which created from develop 199 | 200 | ## Stories 201 | 202 | [why-you-should-use-swagger-typescript-for-generate-apis-code](https://medium.com/@hosseinm.developer/why-you-should-use-swagger-typescript-for-generate-apis-code-63eb8623fef8?source=friends_link&sk=2aa0e2d30b3be158d18c1feb4e12d4a6) 203 | --------------------------------------------------------------------------------