From 5c3380dcd7d3a6cca89134196b599b5fd492e357 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=23=E4=BA=91=E6=B7=A1=E7=84=B6?= Date: Thu, 16 Mar 2023 16:42:21 +0800 Subject: [PATCH 1/8] =?UTF-8?q?chore:=20=E5=A2=9E=E5=8A=A0=E5=8D=95?= =?UTF-8?q?=E5=85=83=E6=B5=8B=E8=AF=95=E8=A6=86=E7=9B=96=E7=8E=87=E4=B8=8A?= =?UTF-8?q?=E6=8A=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/review.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/review.yml b/.github/workflows/review.yml index 18a5c15..b284c0d 100644 --- a/.github/workflows/review.yml +++ b/.github/workflows/review.yml @@ -23,3 +23,8 @@ jobs: - run: npm ci - run: npm run lint - run: npm run test + - uses: codacy/codacy-coverage-reporter-action@v1 + with: + api-token: ${{ secrets.CODACY_API_TOKEN }} + coverage-reports: coverage/coverage-final.json + language: json From 84742384e90e8fba78cdb146a5932fb0e571e78e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=23=E4=BA=91=E6=B7=A1=E7=84=B6?= Date: Thu, 16 Mar 2023 16:50:08 +0800 Subject: [PATCH 2/8] =?UTF-8?q?chore:=20=E5=A2=9E=E5=8A=A0=E5=8D=95?= =?UTF-8?q?=E5=85=83=E6=B5=8B=E8=AF=95=E8=A6=86=E7=9B=96=E7=8E=87=E5=BE=BD?= =?UTF-8?q?=E7=AB=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 5087285..164e8c5 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ --- -[![review][review-badge]][review-link] ![version][version-badge] ![license][license-badge] +[![review][review-badge]][review-link] [![Codacy Badge][coverage-badge]][coverage-link] ![version][version-badge] ![license][license-badge] OpenAPI Specification ➡️ TypeScript @@ -143,5 +143,7 @@ generate({ [review-badge]: https://github.com/cloudcome/oas-gen-ts/actions/workflows/review.yml/badge.svg [review-link]: https://github.com/cloudcome/oas-gen-ts/actions/workflows/review.yml +[coverage-badge]: https://app.codacy.com/project/badge/Grade/e788387e5e27472ba3b5003bf19aeea7 +[coverage-link]: https://www.codacy.com/gh/cloudcome/oas-gen-ts/dashboard?utm_source=github.com&utm_medium=referral&utm_content=cloudcome/oas-gen-ts&utm_campaign=Badge_Grade [version-badge]: https://img.shields.io/npm/v/oas-gen-ts [license-badge]: https://img.shields.io/github/license/cloudcome/oas-gen-ts From c757e825448b99c9b76afd7779c0504f75a2bce3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=23=E4=BA=91=E6=B7=A1=E7=84=B6?= Date: Thu, 16 Mar 2023 18:14:38 +0800 Subject: [PATCH 3/8] =?UTF-8?q?docs:=20=E4=BF=AE=E6=AD=A3=20review=20quali?= =?UTF-8?q?ty=20=E5=BE=BD=E7=AB=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 164e8c5..4223935 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ --- -[![review][review-badge]][review-link] [![Codacy Badge][coverage-badge]][coverage-link] ![version][version-badge] ![license][license-badge] +[![review][review-badge]][review-link] [![quality][quality-badge]][quality-link] ![version][version-badge] ![license][license-badge] OpenAPI Specification ➡️ TypeScript @@ -143,7 +143,7 @@ generate({ [review-badge]: https://github.com/cloudcome/oas-gen-ts/actions/workflows/review.yml/badge.svg [review-link]: https://github.com/cloudcome/oas-gen-ts/actions/workflows/review.yml -[coverage-badge]: https://app.codacy.com/project/badge/Grade/e788387e5e27472ba3b5003bf19aeea7 -[coverage-link]: https://www.codacy.com/gh/cloudcome/oas-gen-ts/dashboard?utm_source=github.com&utm_medium=referral&utm_content=cloudcome/oas-gen-ts&utm_campaign=Badge_Grade +[quality-badge]: https://app.codacy.com/project/badge/Grade/e788387e5e27472ba3b5003bf19aeea7 +[quality-link]: https://www.codacy.com/gh/cloudcome/oas-gen-ts/dashboard?utm_source=github.com&utm_medium=referral&utm_content=cloudcome/oas-gen-ts&utm_campaign=Badge_Grade [version-badge]: https://img.shields.io/npm/v/oas-gen-ts [license-badge]: https://img.shields.io/github/license/cloudcome/oas-gen-ts From 94e924331890305cf31427f5e9107244521f3b22 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=23=E4=BA=91=E6=B7=A1=E7=84=B6?= Date: Thu, 16 Mar 2023 18:33:06 +0800 Subject: [PATCH 4/8] =?UTF-8?q?chore:=20=E4=BF=AE=E5=A4=8D=E5=8D=95?= =?UTF-8?q?=E5=85=83=E6=B5=8B=E8=AF=95=E8=A6=86=E7=9B=96=E7=8E=87=E4=B8=8A?= =?UTF-8?q?=E6=8A=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/review.yml | 3 +-- vitest.config.mjs | 9 +++++++++ 2 files changed, 10 insertions(+), 2 deletions(-) create mode 100644 vitest.config.mjs diff --git a/.github/workflows/review.yml b/.github/workflows/review.yml index b284c0d..6c0554a 100644 --- a/.github/workflows/review.yml +++ b/.github/workflows/review.yml @@ -26,5 +26,4 @@ jobs: - uses: codacy/codacy-coverage-reporter-action@v1 with: api-token: ${{ secrets.CODACY_API_TOKEN }} - coverage-reports: coverage/coverage-final.json - language: json + coverage-reports: coverage/lcov.info diff --git a/vitest.config.mjs b/vitest.config.mjs new file mode 100644 index 0000000..28a0615 --- /dev/null +++ b/vitest.config.mjs @@ -0,0 +1,9 @@ +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + coverage: { + reporter: ['lcov', 'text'], + }, + }, +}); From 1a3dd7db35d5907b7d7d8a88b0d4e4884a0c5946 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=23=E4=BA=91=E6=B7=A1=E7=84=B6?= Date: Thu, 16 Mar 2023 19:10:37 +0800 Subject: [PATCH 5/8] =?UTF-8?q?chore:=20workflow=20=E9=87=8D=E5=91=BD?= =?UTF-8?q?=E5=90=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/{review.yml => code-review.yml} | 9 ++++----- .github/workflows/{release.yml => release-please.yml} | 8 ++------ README.md | 10 +++++----- 3 files changed, 11 insertions(+), 16 deletions(-) rename .github/workflows/{review.yml => code-review.yml} (85%) rename .github/workflows/{release.yml => release-please.yml} (82%) diff --git a/.github/workflows/review.yml b/.github/workflows/code-review.yml similarity index 85% rename from .github/workflows/review.yml rename to .github/workflows/code-review.yml index 6c0554a..cc1e2ca 100644 --- a/.github/workflows/review.yml +++ b/.github/workflows/code-review.yml @@ -1,18 +1,17 @@ -name: review +name: code-review on: push: pull_request: schedule: - # 每天凌晨 3 点 + # 每天凌晨 3 点(UTC) - cron: '0 3 * * *' jobs: - review: + code-review: runs-on: ubuntu-latest steps: - - name: 修改为中国时区 - run: | + - run: | sudo rm /etc/localtime sudo ln -s /usr/share/zoneinfo/Asia/Shanghai /etc/localtime - uses: actions/checkout@v3 diff --git a/.github/workflows/release.yml b/.github/workflows/release-please.yml similarity index 82% rename from .github/workflows/release.yml rename to .github/workflows/release-please.yml index c7821c4..aab20e1 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release-please.yml @@ -1,4 +1,4 @@ -name: release +name: release-please on: push: @@ -10,13 +10,9 @@ permissions: pull-requests: write jobs: - release: + release-please: runs-on: ubuntu-latest steps: - - name: 修改为中国时区 - run: | - sudo rm /etc/localtime - sudo ln -s /usr/share/zoneinfo/Asia/Shanghai /etc/localtime - uses: google-github-actions/release-please-action@v3 id: release with: diff --git a/README.md b/README.md index 4223935..6be8631 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ --- -[![review][review-badge]][review-link] [![quality][quality-badge]][quality-link] ![version][version-badge] ![license][license-badge] +[![code-review][code-review-badge]][code-review-link] [![code-quality][code-quality-badge]][code-quality-link] ![version][version-badge] ![license][license-badge] OpenAPI Specification ➡️ TypeScript @@ -141,9 +141,9 @@ generate({ **At least one of `url` and `spec` exists** -[review-badge]: https://github.com/cloudcome/oas-gen-ts/actions/workflows/review.yml/badge.svg -[review-link]: https://github.com/cloudcome/oas-gen-ts/actions/workflows/review.yml -[quality-badge]: https://app.codacy.com/project/badge/Grade/e788387e5e27472ba3b5003bf19aeea7 -[quality-link]: https://www.codacy.com/gh/cloudcome/oas-gen-ts/dashboard?utm_source=github.com&utm_medium=referral&utm_content=cloudcome/oas-gen-ts&utm_campaign=Badge_Grade +[code-review-badge]: https://github.com/cloudcome/oas-gen-ts/actions/workflows/code-review.yml/badge.svg +[code-review-link]: https://github.com/cloudcome/oas-gen-ts/actions/workflows/code-review.yml +[code-quality-badge]: https://app.codacy.com/project/badge/Grade/e788387e5e27472ba3b5003bf19aeea7 +[code-quality-link]: https://www.codacy.com/gh/cloudcome/oas-gen-ts/dashboard?utm_source=github.com&utm_medium=referral&utm_content=cloudcome/oas-gen-ts&utm_campaign=Badge_Grade [version-badge]: https://img.shields.io/npm/v/oas-gen-ts [license-badge]: https://img.shields.io/github/license/cloudcome/oas-gen-ts From 5e14f489fd5470b1ee2677309397c31899ccc4cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=23=E4=BA=91=E6=B7=A1=E7=84=B6?= Date: Thu, 16 Mar 2023 19:15:09 +0800 Subject: [PATCH 6/8] =?UTF-8?q?chore:=20=E4=BF=AE=E6=AD=A3=E5=8D=95?= =?UTF-8?q?=E6=B5=8B=E8=A6=86=E7=9B=96=E8=8C=83=E5=9B=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- vitest.config.mjs | 1 + 1 file changed, 1 insertion(+) diff --git a/vitest.config.mjs b/vitest.config.mjs index 28a0615..4e41b8a 100644 --- a/vitest.config.mjs +++ b/vitest.config.mjs @@ -2,6 +2,7 @@ import { defineConfig } from 'vitest/config'; export default defineConfig({ test: { + include: ['src/**/*.ts'], coverage: { reporter: ['lcov', 'text'], }, From 7bac37ccde5901dcdbdb322d05d9628b363998bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=23=E4=BA=91=E6=B7=A1=E7=84=B6?= Date: Thu, 16 Mar 2023 19:37:13 +0800 Subject: [PATCH 7/8] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=20Oas=20=E7=B1=BB?= =?UTF-8?q?=E5=9E=8B=E8=81=94=E5=90=88=E9=94=99=E8=AF=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/generator.ts | 8 ++++---- src/index.ts | 1 + src/types.ts | 6 +++--- 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/src/generator.ts b/src/generator.ts index aec2101..2accab2 100644 --- a/src/generator.ts +++ b/src/generator.ts @@ -5,17 +5,17 @@ import path from 'path'; import { generateApi } from 'swagger-typescript-api'; import { defineConfig } from './configure'; import { axiosImportDefault, helpersImport, templatesDir } from './const'; -import { StrictConfig, Oas, UserConfig } from './types'; +import { StrictConfig, Oas, UserConfig, OasAsUrl, OasAsSpec } from './types'; import { exitError, normalizeError, tryCatch } from './utils'; export async function generateItem(oas: Oas, config: StrictConfig) { - const { name, url, spec, axiosImport: axiosImportScope } = oas; + const { name, axiosImport: axiosImportScope } = oas; const { cwd, dest, axiosImport: axiosImportGlobal, unwrapResponseData } = config; const axiosImport = axiosImportScope || axiosImportGlobal || axiosImportDefault; const { files } = await generateApi({ name, - url, - spec, + url: (oas as OasAsUrl).url, + spec: (oas as OasAsSpec).spec, output: false, httpClientType: 'axios', templates: templatesDir, diff --git a/src/index.ts b/src/index.ts index 1bef346..8a14eb6 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,2 +1,3 @@ export * from './generator'; export * from './configure'; +export * from './types'; diff --git a/src/types.ts b/src/types.ts index 802e9ca..a145b4b 100644 --- a/src/types.ts +++ b/src/types.ts @@ -10,15 +10,15 @@ interface OasBase { axiosImport?: string; } -interface OasAsUrl extends OasBase { +export interface OasAsUrl extends OasBase { url: string; } -interface OasAsSpec extends OasBase { +export interface OasAsSpec extends OasBase { spec: import('swagger-schema-official').Spec; } -export interface Oas extends OasAsUrl, OasAsSpec {} +export type Oas = OasAsUrl | OasAsSpec; export interface UserConfig { /** From 5dc61060f1cfb312ba526d349b24f930dd688414 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=23=E4=BA=91=E6=B7=A1=E7=84=B6?= Date: Thu, 16 Mar 2023 21:30:06 +0800 Subject: [PATCH 8/8] =?UTF-8?q?feat:=20=E5=AE=8C=E5=96=84=E5=A4=A7?= =?UTF-8?q?=E9=83=A8=E5=88=86=E6=BA=90=E4=BB=A3=E7=A0=81=E7=9A=84=E5=8D=95?= =?UTF-8?q?=E5=85=83=E6=B5=8B=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package-lock.json | 2 +- package.json | 1 - src/commands.ts | 79 +++++ src/configure.ts | 1 + src/generator.ts | 79 +++-- src/index.ts | 1 + src/types.ts | 35 ++- src/utils.ts | 21 +- test/commands.test.ts | 11 + test/configure.test.ts | 21 ++ test/generator.test.ts | 90 ++++++ test/oas.config.cjs | 15 + test/petstore3.json | 638 +++++++++++++++++++++++++++++++++++++++++ test/utils.test.ts | 39 +++ vitest.config.mjs | 4 +- 15 files changed, 983 insertions(+), 54 deletions(-) create mode 100644 src/commands.ts create mode 100644 test/commands.test.ts create mode 100644 test/configure.test.ts create mode 100644 test/generator.test.ts create mode 100644 test/oas.config.cjs create mode 100644 test/petstore3.json create mode 100644 test/utils.test.ts diff --git a/package-lock.json b/package-lock.json index c65fb0d..9ad91a0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,6 @@ "license": "MIT", "dependencies": { "chalk": "^4.1.2", - "cosmiconfig": "^8.1.0", "lodash-es": "^4.17.21", "swagger-typescript-api": "^12.0.3" }, @@ -2108,6 +2107,7 @@ "version": "8.1.0", "resolved": "https://registry.npmmirror.com/cosmiconfig/-/cosmiconfig-8.1.0.tgz", "integrity": "sha512-0tLZ9URlPGU7JsKq0DQOQ3FoRsYX8xDZ7xMiATQfaiGMz7EHowNkbU9u1coAOmnh9p/1ySpm0RB3JNWRXM5GCg==", + "dev": true, "dependencies": { "import-fresh": "^3.2.1", "js-yaml": "^4.1.0", diff --git a/package.json b/package.json index 47de099..115be04 100644 --- a/package.json +++ b/package.json @@ -54,7 +54,6 @@ "license": "MIT", "dependencies": { "chalk": "^4.1.2", - "cosmiconfig": "^8.1.0", "lodash-es": "^4.17.21", "swagger-typescript-api": "^12.0.3" }, diff --git a/src/commands.ts b/src/commands.ts new file mode 100644 index 0000000..63450f9 --- /dev/null +++ b/src/commands.ts @@ -0,0 +1,79 @@ +import chalk from 'chalk'; +import path from 'path'; +import * as process from 'process'; +import { defineConfig } from './configure'; +import { generate } from './generator'; +import { UserConfig } from './types'; +import { exitError, isFile, normalizeError, tryCatch } from './utils'; + +interface StartConfig { + // 指定配置文件绝对路径 + configFile?: string; +} + +export const startConfigFiles = ['oas.config.cjs', 'oas.config.js', 'oas.json']; + +export async function resolveConfigFile(cwd: string, configFile?: string) { + if (configFile) { + if (!(await isFile(path.join(configFile)))) { + throw new Error(`指定配置文件 "${configFile}" 不存在`); + } + + return configFile; + } + + for (const file of startConfigFiles.values()) { + if (await isFile(path.join(cwd, file))) { + return file; + } + } +} + +export async function start(startConfig?: StartConfig) { + const configFile = await resolveConfigFile(process.cwd(), startConfig?.configFile); + + if (!configFile) { + return exitError(`配置文件未找到,配置文件可以为 ${startConfigFiles.join('、')} 之一`); + } + + // eslint-disable-next-line @typescript-eslint/no-var-requires + const [err, config] = await tryCatch(require(configFile)); + + if (err) { + return exitError(err.message); + } + + if (!config) { + return exitError('配置文件内容可能为空'); + } + + const strictConfig = defineConfig(config); + + try { + await generate(strictConfig, (generated, info) => { + const { oasItem } = generated; + const { index, length, done, start, end } = info; + const width = Math.min(String(length).length, 2); + const stepText = String(index + 1).padStart(width, '0'); + + if (done) { + const past = end - start; + console.log( + chalk.cyanBright(`[${stepText}/${length}]`), + 'generated ', + chalk.yellowBright(oasItem.name), + chalk.gray(`${past}ms`) + ); + } else { + console.log( + chalk.cyanBright(`[${stepText}/${length}]`), + 'generating', + chalk.yellowBright(oasItem.name), + chalk.gray('...') + ); + } + }); + } catch (err) { + exitError(normalizeError(err).message); + } +} diff --git a/src/configure.ts b/src/configure.ts index c6f6118..eae09dd 100644 --- a/src/configure.ts +++ b/src/configure.ts @@ -7,6 +7,7 @@ export const defaults: StrictConfig = { axiosImport: axiosImportDefault, unwrapResponseData: false, list: [], + onGenerated: () => 0, }; export function defineConfig(config: UserConfig) { diff --git a/src/generator.ts b/src/generator.ts index 2accab2..a0cad2d 100644 --- a/src/generator.ts +++ b/src/generator.ts @@ -1,21 +1,17 @@ -import chalk from 'chalk'; -import { cosmiconfig } from 'cosmiconfig'; import fs from 'fs/promises'; import path from 'path'; import { generateApi } from 'swagger-typescript-api'; -import { defineConfig } from './configure'; import { axiosImportDefault, helpersImport, templatesDir } from './const'; -import { StrictConfig, Oas, UserConfig, OasAsUrl, OasAsSpec } from './types'; -import { exitError, normalizeError, tryCatch } from './utils'; +import { Generated, GeneratedCallback, OasItem, OasItemAsSpec, OasItemAsUrl, StrictConfig } from './types'; -export async function generateItem(oas: Oas, config: StrictConfig) { - const { name, axiosImport: axiosImportScope } = oas; +export async function generateItem(oasItem: OasItem, config: StrictConfig): Promise { + const { name, axiosImport: axiosImportScope } = oasItem; const { cwd, dest, axiosImport: axiosImportGlobal, unwrapResponseData } = config; const axiosImport = axiosImportScope || axiosImportGlobal || axiosImportDefault; const { files } = await generateApi({ name, - url: (oas as OasAsUrl).url, - spec: (oas as OasAsSpec).spec, + url: (oasItem as OasItemAsUrl).url, + spec: (oasItem as OasItemAsSpec).spec, output: false, httpClientType: 'axios', templates: templatesDir, @@ -23,6 +19,12 @@ export async function generateItem(oas: Oas, config: StrictConfig) { unwrapResponseData, }); + const generated: Generated = { + files: [], + oasItem, + config, + }; + for (const { content, name: filename } of files) { const contentFinal = [axiosImport, helpersImport, content].join('\n'); const file = path.join(cwd, dest, filename); @@ -30,46 +32,35 @@ export async function generateItem(oas: Oas, config: StrictConfig) { await fs.mkdir(dir, { recursive: true }); await fs.writeFile(file, contentFinal); - } -} -export async function generate(config: StrictConfig) { - const { list } = config; - let step = 0; - const length = list.length; - const width = String(length).length; - - for (const oas of list) { - step++; - const stepText = String(step).padStart(width, '0'); - console.log(chalk.cyanBright(`[${stepText}/${length}]`), 'generating', chalk.yellow(oas.name)); - await generateItem(oas, config); + generated.files.push(file); } -} -export async function start() { - const explorer = cosmiconfig('oas', { - searchPlaces: ['oas.config.cjs', 'oas.config.js', 'oas.json'], - }); - const [err1, result] = await tryCatch(explorer.search()); + return generated; +} - if (err1) { - return exitError('配置文件查找失败'); - } +export async function generate(config: StrictConfig, callback?: GeneratedCallback): Promise { + const { list, onGenerated } = config; + let index = 0; + const length = list.length; + const generatedList: Generated[] = []; - if (!result) { - return exitError('配置文件未找到'); + for (const oasItem of list) { + const start = Date.now(); + callback?.( + { + files: [], + oasItem, + config, + }, + { index, length, done: false, start, end: start } + ); + const generated = await generateItem(oasItem, config); + generatedList.push(generated); + onGenerated(generated); + callback?.(generated, { index, length, done: true, start, end: Date.now() }); + index++; } - const config = result.filepath.endsWith('js') - ? // js 文件使用 defineConfig,返回的是 StrictConfig - (result.config as StrictConfig) - : // json 文件是纯文本,返回的 UserConfig - defineConfig(result.config as UserConfig); - - try { - await generate(config); - } catch (err) { - exitError(normalizeError(err).message); - } + return generatedList; } diff --git a/src/index.ts b/src/index.ts index 8a14eb6..10f5377 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,3 +1,4 @@ export * from './generator'; export * from './configure'; export * from './types'; +export * from './commands'; diff --git a/src/types.ts b/src/types.ts index a145b4b..d8ce0c5 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,4 +1,4 @@ -interface OasBase { +interface OasItemBase { name: string; /** @@ -10,15 +10,17 @@ interface OasBase { axiosImport?: string; } -export interface OasAsUrl extends OasBase { +export interface OasItemAsUrl extends OasItemBase { url: string; } -export interface OasAsSpec extends OasBase { - spec: import('swagger-schema-official').Spec; +export type Oas = import('swagger-schema-official').Spec; + +export interface OasItemAsSpec extends OasItemBase { + spec: Oas; } -export type Oas = OasAsUrl | OasAsSpec; +export type OasItem = OasItemAsUrl | OasItemAsSpec; export interface UserConfig { /** @@ -47,10 +49,16 @@ export interface UserConfig { */ unwrapResponseData?: boolean; + /** + * 单个 oas 生成后回调 + * @param {Generated} generated + */ + onGenerated?: (generated: Generated) => any; + /** * oas 列表 */ - list: Oas[]; + list: OasItem[]; } export type StrictConfig = Required; @@ -62,3 +70,18 @@ export enum ContentKind { TEXT = 'TEXT', OTHER = 'OTHER', } + +export interface Generated { + files: string[]; + oasItem: OasItem; + config: StrictConfig; +} + +export type GenerateInfo = { + index: number; + length: number; + done: boolean; + start: number; + end: number; +}; +export type GeneratedCallback = (generated: Generated, info: GenerateInfo) => any; diff --git a/src/utils.ts b/src/utils.ts index 27ca3db..d687231 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,9 +1,28 @@ import chalk from 'chalk'; +import fs from 'fs/promises'; +import path from 'path'; import * as process from 'process'; +export async function isFile(p: string) { + try { + const state = await fs.stat(p); + return state.isFile(); + } catch (err) { + return false; + } +} + +export async function cleanDir(p: string) { + await fs.rm(p, { recursive: true, force: true }); +} + export function exitError(message: string) { console.log(chalk.redBright(message)); - process.exit(1); + + /* istanbul ignore if */ + if (!process.env.VITEST) { + process.exit(1); + } } export function normalizeError(err: unknown) { diff --git a/test/commands.test.ts b/test/commands.test.ts new file mode 100644 index 0000000..9d8a6cc --- /dev/null +++ b/test/commands.test.ts @@ -0,0 +1,11 @@ +import path from 'path'; +import { start } from '../src'; +import { test } from 'vitest'; +import { cleanDir } from '../src/utils'; + +test('start', async () => { + await start({ + configFile: path.join(__dirname, './oas.config.cjs'), + }); + await cleanDir(path.resolve('dist-test')); +}); diff --git a/test/configure.test.ts b/test/configure.test.ts new file mode 100644 index 0000000..d68313f --- /dev/null +++ b/test/configure.test.ts @@ -0,0 +1,21 @@ +import { expect, test } from 'vitest'; +import { defaults, defineConfig, StrictConfig, UserConfig } from '../src'; + +test('defaults', () => { + expect(defaults.axiosImport).toBe(`import { Axios } from 'axios'; +const axios = new Axios();`); + expect(defaults.dest).toBe('src/apis'); + expect(defaults.cwd).toBe(process.cwd()); + expect(defaults.list).toHaveLength(0); + expect(defaults.unwrapResponseData).toBe(false); + expect(defaults.onGenerated).toBeTypeOf('function'); +}); + +test('defineConfig', () => { + const userConfig: UserConfig = { + list: [], + }; + const strictConfig = defineConfig(userConfig); + + expect(strictConfig).not.toBe(userConfig); +}); diff --git a/test/generator.test.ts b/test/generator.test.ts new file mode 100644 index 0000000..28bd76a --- /dev/null +++ b/test/generator.test.ts @@ -0,0 +1,90 @@ +import { random } from 'lodash-es'; +import path from 'path'; +import { cleanDir, isFile } from 'src/utils'; +import { afterEach, beforeAll, beforeEach, describe, expect, test, vi } from 'vitest'; +import { generate, Generated, GenerateInfo, generateItem, Oas } from '../src'; +import petstore3 from './petstore3.json'; + +describe('generate-item', () => { + const cwd = process.cwd(); + const dest = 'dist-test'; + const config = { axiosImport: '', cwd, dest: dest, list: [], unwrapResponseData: false, onGenerated: () => 0 }; + + afterEach(async () => { + await cleanDir(path.join(cwd, dest)); + }); + + test( + 'from url', + async () => { + const name = '/' + random(1, 1000) + '/' + random(1, 1000); + const expectedFile = path.join(cwd, dest, name + '.ts'); + + const generated = await generateItem( + { + name, + url: 'https://petstore3.swagger.io/api/v3/openapi.json', + }, + config + ); + expect(generated.files).toHaveLength(1); + const [generatedFile] = generated.files; + expect(generatedFile).toBe(expectedFile); + await expect(isFile(generatedFile)).toBeTruthy(); + }, + { timeout: -1 } + ); + + test('from sepc', async () => { + const name = '/' + random(1, 1000) + '/' + random(1, 1000); + const expectedFile = path.join(cwd, dest, name + '.ts'); + + const generated = await generateItem( + { + name, + spec: petstore3 as unknown as Oas, + }, + config + ); + expect(generated.files).toHaveLength(1); + const [generatedFile] = generated.files; + expect(generatedFile).toBe(expectedFile); + await expect(isFile(generatedFile)).toBeTruthy(); + }); +}); + +test('generate', async () => { + const cwd = process.cwd(); + const dest = 'dist-test'; + const config = { + axiosImport: '', + cwd, + dest: dest, + list: [ + { + name: random(1, 1000).toString(), + spec: petstore3 as unknown as Oas, + }, + { + name: random(1, 1000).toString(), + spec: petstore3 as unknown as Oas, + }, + ], + unwrapResponseData: false, + onGenerated: () => 0, + }; + const fn1 = vi.fn<[Generated, GenerateInfo]>(); + + const generatedList = await generate(config, fn1); + await cleanDir(path.join(cwd, dest)); + expect(generatedList).toHaveLength(2); + expect(fn1).toHaveBeenCalledTimes(4); + expect(fn1.mock.calls[0][1].index).toBe(0); + expect(fn1.mock.calls[0][1].length).toBe(2); + expect(fn1.mock.calls[1][1].index).toBe(0); + expect(fn1.mock.calls[1][1].length).toBe(2); + expect(fn1.mock.calls[2][1].index).toBe(1); + expect(fn1.mock.calls[2][1].length).toBe(2); + expect(fn1.mock.calls[3][1].index).toBe(1); + expect(fn1.mock.calls[3][1].length).toBe(2); +}); diff --git a/test/oas.config.cjs b/test/oas.config.cjs new file mode 100644 index 0000000..deb1f63 --- /dev/null +++ b/test/oas.config.cjs @@ -0,0 +1,15 @@ +module.exports = { + axiosImport: `import axios from '@/utils/axios';`, + unwrapResponseData: true, + dest: 'dist-test', + list: [ + { + name: 'swagger/pet1', + spec: require('./petstore3.json'), + }, + { + name: 'swagger/pet2', + spec: require('./petstore3.json'), + }, + ], +}; diff --git a/test/petstore3.json b/test/petstore3.json new file mode 100644 index 0000000..3c1d937 --- /dev/null +++ b/test/petstore3.json @@ -0,0 +1,638 @@ +{ + "openapi": "3.0.2", + "info": { + "title": "Swagger Petstore - OpenAPI 3.0", + "description": "This is a sample Pet Store Server based on the OpenAPI 3.0 specification. You can find out more about\nSwagger at [http://swagger.io](http://swagger.io). In the third iteration of the pet store, we've switched to the design first approach!\nYou can now help us improve the API whether it's by making changes to the definition itself or to the code.\nThat way, with time, we can improve the API in general, and expose some of the new features in OAS3.\n\nSome useful links:\n- [The Pet Store repository](https://github.com/swagger-api/swagger-petstore)\n- [The source API definition for the Pet Store](https://github.com/swagger-api/swagger-petstore/blob/master/src/main/resources/openapi.yaml)", + "termsOfService": "http://swagger.io/terms/", + "contact": { "email": "apiteam@swagger.io" }, + "license": { "name": "Apache 2.0", "url": "http://www.apache.org/licenses/LICENSE-2.0.html" }, + "version": "1.0.17" + }, + "externalDocs": { "description": "Find out more about Swagger", "url": "http://swagger.io" }, + "servers": [{ "url": "/api/v3" }], + "tags": [ + { + "name": "pet", + "description": "Everything about your Pets", + "externalDocs": { "description": "Find out more", "url": "http://swagger.io" } + }, + { + "name": "store", + "description": "Access to Petstore orders", + "externalDocs": { "description": "Find out more about our store", "url": "http://swagger.io" } + }, + { "name": "user", "description": "Operations about user" } + ], + "paths": { + "/pet": { + "put": { + "tags": ["pet"], + "summary": "Update an existing pet", + "description": "Update an existing pet by Id", + "operationId": "updatePet", + "requestBody": { + "description": "Update an existent pet in the store", + "content": { + "application/json": { "schema": { "$ref": "#/components/schemas/Pet" } }, + "application/xml": { "schema": { "$ref": "#/components/schemas/Pet" } }, + "application/x-www-form-urlencoded": { "schema": { "$ref": "#/components/schemas/Pet" } } + }, + "required": true + }, + "responses": { + "200": { + "description": "Successful operation", + "content": { + "application/xml": { "schema": { "$ref": "#/components/schemas/Pet" } }, + "application/json": { "schema": { "$ref": "#/components/schemas/Pet" } } + } + }, + "400": { "description": "Invalid ID supplied" }, + "404": { "description": "Pet not found" }, + "405": { "description": "Validation exception" } + }, + "security": [{ "petstore_auth": ["write:pets", "read:pets"] }] + }, + "post": { + "tags": ["pet"], + "summary": "Add a new pet to the store", + "description": "Add a new pet to the store", + "operationId": "addPet", + "requestBody": { + "description": "Create a new pet in the store", + "content": { + "application/json": { "schema": { "$ref": "#/components/schemas/Pet" } }, + "application/xml": { "schema": { "$ref": "#/components/schemas/Pet" } }, + "application/x-www-form-urlencoded": { "schema": { "$ref": "#/components/schemas/Pet" } } + }, + "required": true + }, + "responses": { + "200": { + "description": "Successful operation", + "content": { + "application/xml": { "schema": { "$ref": "#/components/schemas/Pet" } }, + "application/json": { "schema": { "$ref": "#/components/schemas/Pet" } } + } + }, + "405": { "description": "Invalid input" } + }, + "security": [{ "petstore_auth": ["write:pets", "read:pets"] }] + } + }, + "/pet/findByStatus": { + "get": { + "tags": ["pet"], + "summary": "Finds Pets by status", + "description": "Multiple status values can be provided with comma separated strings", + "operationId": "findPetsByStatus", + "parameters": [ + { + "name": "status", + "in": "query", + "description": "Status values that need to be considered for filter", + "required": false, + "explode": true, + "schema": { "type": "string", "default": "available", "enum": ["available", "pending", "sold"] } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/xml": { "schema": { "type": "array", "items": { "$ref": "#/components/schemas/Pet" } } }, + "application/json": { "schema": { "type": "array", "items": { "$ref": "#/components/schemas/Pet" } } } + } + }, + "400": { "description": "Invalid status value" } + }, + "security": [{ "petstore_auth": ["write:pets", "read:pets"] }] + } + }, + "/pet/findByTags": { + "get": { + "tags": ["pet"], + "summary": "Finds Pets by tags", + "description": "Multiple tags can be provided with comma separated strings. Use tag1, tag2, tag3 for testing.", + "operationId": "findPetsByTags", + "parameters": [ + { + "name": "tags", + "in": "query", + "description": "Tags to filter by", + "required": false, + "explode": true, + "schema": { "type": "array", "items": { "type": "string" } } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/xml": { "schema": { "type": "array", "items": { "$ref": "#/components/schemas/Pet" } } }, + "application/json": { "schema": { "type": "array", "items": { "$ref": "#/components/schemas/Pet" } } } + } + }, + "400": { "description": "Invalid tag value" } + }, + "security": [{ "petstore_auth": ["write:pets", "read:pets"] }] + } + }, + "/pet/{petId}": { + "get": { + "tags": ["pet"], + "summary": "Find pet by ID", + "description": "Returns a single pet", + "operationId": "getPetById", + "parameters": [ + { + "name": "petId", + "in": "path", + "description": "ID of pet to return", + "required": true, + "schema": { "type": "integer", "format": "int64" } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/xml": { "schema": { "$ref": "#/components/schemas/Pet" } }, + "application/json": { "schema": { "$ref": "#/components/schemas/Pet" } } + } + }, + "400": { "description": "Invalid ID supplied" }, + "404": { "description": "Pet not found" } + }, + "security": [{ "api_key": [] }, { "petstore_auth": ["write:pets", "read:pets"] }] + }, + "post": { + "tags": ["pet"], + "summary": "Updates a pet in the store with form data", + "description": "", + "operationId": "updatePetWithForm", + "parameters": [ + { + "name": "petId", + "in": "path", + "description": "ID of pet that needs to be updated", + "required": true, + "schema": { "type": "integer", "format": "int64" } + }, + { + "name": "name", + "in": "query", + "description": "Name of pet that needs to be updated", + "schema": { "type": "string" } + }, + { + "name": "status", + "in": "query", + "description": "Status of pet that needs to be updated", + "schema": { "type": "string" } + } + ], + "responses": { "405": { "description": "Invalid input" } }, + "security": [{ "petstore_auth": ["write:pets", "read:pets"] }] + }, + "delete": { + "tags": ["pet"], + "summary": "Deletes a pet", + "description": "", + "operationId": "deletePet", + "parameters": [ + { "name": "api_key", "in": "header", "description": "", "required": false, "schema": { "type": "string" } }, + { + "name": "petId", + "in": "path", + "description": "Pet id to delete", + "required": true, + "schema": { "type": "integer", "format": "int64" } + } + ], + "responses": { "400": { "description": "Invalid pet value" } }, + "security": [{ "petstore_auth": ["write:pets", "read:pets"] }] + } + }, + "/pet/{petId}/uploadImage": { + "post": { + "tags": ["pet"], + "summary": "uploads an image", + "description": "", + "operationId": "uploadFile", + "parameters": [ + { + "name": "petId", + "in": "path", + "description": "ID of pet to update", + "required": true, + "schema": { "type": "integer", "format": "int64" } + }, + { + "name": "additionalMetadata", + "in": "query", + "description": "Additional Metadata", + "required": false, + "schema": { "type": "string" } + } + ], + "requestBody": { + "content": { "application/octet-stream": { "schema": { "type": "string", "format": "binary" } } } + }, + "responses": { + "200": { + "description": "successful operation", + "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiResponse" } } } + } + }, + "security": [{ "petstore_auth": ["write:pets", "read:pets"] }] + } + }, + "/store/inventory": { + "get": { + "tags": ["store"], + "summary": "Returns pet inventories by status", + "description": "Returns a map of status codes to quantities", + "operationId": "getInventory", + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { "type": "object", "additionalProperties": { "type": "integer", "format": "int32" } } + } + } + } + }, + "security": [{ "api_key": [] }] + } + }, + "/store/order": { + "post": { + "tags": ["store"], + "summary": "Place an order for a pet", + "description": "Place a new order in the store", + "operationId": "placeOrder", + "requestBody": { + "content": { + "application/json": { "schema": { "$ref": "#/components/schemas/Order" } }, + "application/xml": { "schema": { "$ref": "#/components/schemas/Order" } }, + "application/x-www-form-urlencoded": { "schema": { "$ref": "#/components/schemas/Order" } } + } + }, + "responses": { + "200": { + "description": "successful operation", + "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Order" } } } + }, + "405": { "description": "Invalid input" } + } + } + }, + "/store/order/{orderId}": { + "get": { + "tags": ["store"], + "summary": "Find purchase order by ID", + "description": "For valid response try integer IDs with value <= 5 or > 10. Other values will generate exceptions.", + "operationId": "getOrderById", + "parameters": [ + { + "name": "orderId", + "in": "path", + "description": "ID of order that needs to be fetched", + "required": true, + "schema": { "type": "integer", "format": "int64" } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/xml": { "schema": { "$ref": "#/components/schemas/Order" } }, + "application/json": { "schema": { "$ref": "#/components/schemas/Order" } } + } + }, + "400": { "description": "Invalid ID supplied" }, + "404": { "description": "Order not found" } + } + }, + "delete": { + "tags": ["store"], + "summary": "Delete purchase order by ID", + "description": "For valid response try integer IDs with value < 1000. Anything above 1000 or nonintegers will generate API errors", + "operationId": "deleteOrder", + "parameters": [ + { + "name": "orderId", + "in": "path", + "description": "ID of the order that needs to be deleted", + "required": true, + "schema": { "type": "integer", "format": "int64" } + } + ], + "responses": { "400": { "description": "Invalid ID supplied" }, "404": { "description": "Order not found" } } + } + }, + "/user": { + "post": { + "tags": ["user"], + "summary": "Create user", + "description": "This can only be done by the logged in user.", + "operationId": "createUser", + "requestBody": { + "description": "Created user object", + "content": { + "application/json": { "schema": { "$ref": "#/components/schemas/User" } }, + "application/xml": { "schema": { "$ref": "#/components/schemas/User" } }, + "application/x-www-form-urlencoded": { "schema": { "$ref": "#/components/schemas/User" } } + } + }, + "responses": { + "default": { + "description": "successful operation", + "content": { + "application/json": { "schema": { "$ref": "#/components/schemas/User" } }, + "application/xml": { "schema": { "$ref": "#/components/schemas/User" } } + } + } + } + } + }, + "/user/createWithList": { + "post": { + "tags": ["user"], + "summary": "Creates list of users with given input array", + "description": "Creates list of users with given input array", + "operationId": "createUsersWithListInput", + "requestBody": { + "content": { + "application/json": { "schema": { "type": "array", "items": { "$ref": "#/components/schemas/User" } } } + } + }, + "responses": { + "200": { + "description": "Successful operation", + "content": { + "application/xml": { "schema": { "$ref": "#/components/schemas/User" } }, + "application/json": { "schema": { "$ref": "#/components/schemas/User" } } + } + }, + "default": { "description": "successful operation" } + } + } + }, + "/user/login": { + "get": { + "tags": ["user"], + "summary": "Logs user into the system", + "description": "", + "operationId": "loginUser", + "parameters": [ + { + "name": "username", + "in": "query", + "description": "The user name for login", + "required": false, + "schema": { "type": "string" } + }, + { + "name": "password", + "in": "query", + "description": "The password for login in clear text", + "required": false, + "schema": { "type": "string" } + } + ], + "responses": { + "200": { + "description": "successful operation", + "headers": { + "X-Rate-Limit": { + "description": "calls per hour allowed by the user", + "schema": { "type": "integer", "format": "int32" } + }, + "X-Expires-After": { + "description": "date in UTC when token expires", + "schema": { "type": "string", "format": "date-time" } + } + }, + "content": { + "application/xml": { "schema": { "type": "string" } }, + "application/json": { "schema": { "type": "string" } } + } + }, + "400": { "description": "Invalid username/password supplied" } + } + } + }, + "/user/logout": { + "get": { + "tags": ["user"], + "summary": "Logs out current logged in user session", + "description": "", + "operationId": "logoutUser", + "parameters": [], + "responses": { "default": { "description": "successful operation" } } + } + }, + "/user/{username}": { + "get": { + "tags": ["user"], + "summary": "Get user by user name", + "description": "", + "operationId": "getUserByName", + "parameters": [ + { + "name": "username", + "in": "path", + "description": "The name that needs to be fetched. Use user1 for testing. ", + "required": true, + "schema": { "type": "string" } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/xml": { "schema": { "$ref": "#/components/schemas/User" } }, + "application/json": { "schema": { "$ref": "#/components/schemas/User" } } + } + }, + "400": { "description": "Invalid username supplied" }, + "404": { "description": "User not found" } + } + }, + "put": { + "tags": ["user"], + "summary": "Update user", + "description": "This can only be done by the logged in user.", + "operationId": "updateUser", + "parameters": [ + { + "name": "username", + "in": "path", + "description": "name that need to be deleted", + "required": true, + "schema": { "type": "string" } + } + ], + "requestBody": { + "description": "Update an existent user in the store", + "content": { + "application/json": { "schema": { "$ref": "#/components/schemas/User" } }, + "application/xml": { "schema": { "$ref": "#/components/schemas/User" } }, + "application/x-www-form-urlencoded": { "schema": { "$ref": "#/components/schemas/User" } } + } + }, + "responses": { "default": { "description": "successful operation" } } + }, + "delete": { + "tags": ["user"], + "summary": "Delete user", + "description": "This can only be done by the logged in user.", + "operationId": "deleteUser", + "parameters": [ + { + "name": "username", + "in": "path", + "description": "The name that needs to be deleted", + "required": true, + "schema": { "type": "string" } + } + ], + "responses": { + "400": { "description": "Invalid username supplied" }, + "404": { "description": "User not found" } + } + } + } + }, + "components": { + "schemas": { + "Order": { + "type": "object", + "properties": { + "id": { "type": "integer", "format": "int64", "example": 10 }, + "petId": { "type": "integer", "format": "int64", "example": 198772 }, + "quantity": { "type": "integer", "format": "int32", "example": 7 }, + "shipDate": { "type": "string", "format": "date-time" }, + "status": { + "type": "string", + "description": "Order Status", + "example": "approved", + "enum": ["placed", "approved", "delivered"] + }, + "complete": { "type": "boolean" } + }, + "xml": { "name": "order" } + }, + "Customer": { + "type": "object", + "properties": { + "id": { "type": "integer", "format": "int64", "example": 100000 }, + "username": { "type": "string", "example": "fehguy" }, + "address": { + "type": "array", + "xml": { "name": "addresses", "wrapped": true }, + "items": { "$ref": "#/components/schemas/Address" } + } + }, + "xml": { "name": "customer" } + }, + "Address": { + "type": "object", + "properties": { + "street": { "type": "string", "example": "437 Lytton" }, + "city": { "type": "string", "example": "Palo Alto" }, + "state": { "type": "string", "example": "CA" }, + "zip": { "type": "string", "example": "94301" } + }, + "xml": { "name": "address" } + }, + "Category": { + "type": "object", + "properties": { + "id": { "type": "integer", "format": "int64", "example": 1 }, + "name": { "type": "string", "example": "Dogs" } + }, + "xml": { "name": "category" } + }, + "User": { + "type": "object", + "properties": { + "id": { "type": "integer", "format": "int64", "example": 10 }, + "username": { "type": "string", "example": "theUser" }, + "firstName": { "type": "string", "example": "John" }, + "lastName": { "type": "string", "example": "James" }, + "email": { "type": "string", "example": "john@email.com" }, + "password": { "type": "string", "example": "12345" }, + "phone": { "type": "string", "example": "12345" }, + "userStatus": { "type": "integer", "description": "User Status", "format": "int32", "example": 1 } + }, + "xml": { "name": "user" } + }, + "Tag": { + "type": "object", + "properties": { "id": { "type": "integer", "format": "int64" }, "name": { "type": "string" } }, + "xml": { "name": "tag" } + }, + "Pet": { + "required": ["name", "photoUrls"], + "type": "object", + "properties": { + "id": { "type": "integer", "format": "int64", "example": 10 }, + "name": { "type": "string", "example": "doggie" }, + "category": { "$ref": "#/components/schemas/Category" }, + "photoUrls": { + "type": "array", + "xml": { "wrapped": true }, + "items": { "type": "string", "xml": { "name": "photoUrl" } } + }, + "tags": { "type": "array", "xml": { "wrapped": true }, "items": { "$ref": "#/components/schemas/Tag" } }, + "status": { + "type": "string", + "description": "pet status in the store", + "enum": ["available", "pending", "sold"] + } + }, + "xml": { "name": "pet" } + }, + "ApiResponse": { + "type": "object", + "properties": { + "code": { "type": "integer", "format": "int32" }, + "type": { "type": "string" }, + "message": { "type": "string" } + }, + "xml": { "name": "##default" } + } + }, + "requestBodies": { + "Pet": { + "description": "Pet object that needs to be added to the store", + "content": { + "application/json": { "schema": { "$ref": "#/components/schemas/Pet" } }, + "application/xml": { "schema": { "$ref": "#/components/schemas/Pet" } } + } + }, + "UserArray": { + "description": "List of user object", + "content": { + "application/json": { "schema": { "type": "array", "items": { "$ref": "#/components/schemas/User" } } } + } + } + }, + "securitySchemes": { + "petstore_auth": { + "type": "oauth2", + "flows": { + "implicit": { + "authorizationUrl": "https://petstore3.swagger.io/oauth/authorize", + "scopes": { "write:pets": "modify pets in your account", "read:pets": "read your pets" } + } + } + }, + "api_key": { "type": "apiKey", "name": "api_key", "in": "header" } + } + } +} diff --git a/test/utils.test.ts b/test/utils.test.ts new file mode 100644 index 0000000..ff896e6 --- /dev/null +++ b/test/utils.test.ts @@ -0,0 +1,39 @@ +import { describe, expect, test } from 'vitest'; +import { exitError, normalizeError, tryCatch } from '../src/utils'; + +test('Hello', () => { + exitError('Hello'); +}); + +test('normalizeError', () => { + expect(normalizeError(1).message).toBe('1'); + const err1 = new Error('1'); + const err2 = normalizeError(err1); + expect(err2).toBe(err1); +}); + +describe('tryCatch', async () => { + const createPromise = () => { + return (res: number, rej: number) => + new Promise((resolve, reject) => { + if (rej) return reject(normalizeError(rej)); + resolve(res); + }); + }; + + const promise = createPromise(); + + test('try', async () => { + const res = Math.random() + 1; + const [e, p] = await tryCatch(promise(res, 0)); + expect(e).toBeNull(); + expect(p).toBe(res); + }); + + test('catch', async () => { + const rej = Math.random() + 1; + const [e, p] = await tryCatch(promise(0, rej)); + expect((e as Error).message).toBe(rej.toString()); + expect(p).toBeNull(); + }); +}); diff --git a/vitest.config.mjs b/vitest.config.mjs index 4e41b8a..f64633c 100644 --- a/vitest.config.mjs +++ b/vitest.config.mjs @@ -2,9 +2,11 @@ import { defineConfig } from 'vitest/config'; export default defineConfig({ test: { - include: ['src/**/*.ts'], + include: ['test/**/*.test.ts'], coverage: { + include: ['src/**/*.ts'], reporter: ['lcov', 'text'], + all: true, }, }, });