├── .changeset ├── README.md ├── config.json ├── tame-poets-act.md └── tidy-trams-tap.md ├── .editorconfig ├── .envrc ├── .github ├── FUNDING.yml ├── dependabot.yml └── workflows │ ├── main.yml │ ├── typedoc.yml │ └── version-or-publish.yml ├── .gitignore ├── .nvmrc ├── .yarnrc.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── biome.json ├── index.ts ├── package.json ├── src ├── code-formatter.ts ├── code-gen-process.ts ├── commands │ └── generate-templates │ │ ├── configuration.ts │ │ ├── index.ts │ │ └── templates-gen-process.ts ├── component-type-name-resolver.ts ├── configuration.ts ├── constants.ts ├── index.ts ├── schema-components-map.ts ├── schema-parser │ ├── base-schema-parsers │ │ ├── array.ts │ │ ├── complex.ts │ │ ├── discriminator.ts │ │ ├── enum.ts │ │ ├── object.ts │ │ └── primitive.ts │ ├── complex-schema-parsers │ │ ├── all-of.ts │ │ ├── any-of.ts │ │ ├── not.ts │ │ └── one-of.ts │ ├── mono-schema-parser.ts │ ├── schema-formatters.ts │ ├── schema-parser-fabric.ts │ ├── schema-parser.ts │ ├── schema-utils.ts │ └── util │ │ └── enum-key-resolver.ts ├── schema-routes │ ├── schema-routes.ts │ └── util │ │ └── specific-arg-name-resolver.ts ├── schema-walker.ts ├── swagger-schema-resolver.ts ├── templates-worker.ts ├── translators │ ├── javascript.ts │ └── translator.ts ├── type-name-formatter.ts └── util │ ├── file-system.ts │ ├── id.ts │ ├── internal-case.ts │ ├── name-resolver.ts │ ├── object-assign.ts │ ├── pascal-case.ts │ ├── random.ts │ ├── request.ts │ └── sort-by-property.ts ├── templates ├── README.md ├── base │ ├── README.md │ ├── data-contract-jsdoc.ejs │ ├── data-contracts.ejs │ ├── enum-data-contract.ejs │ ├── http-client.ejs │ ├── http-clients │ │ ├── axios-http-client.ejs │ │ └── fetch-http-client.ejs │ ├── interface-data-contract.ejs │ ├── object-field-jsdoc.ejs │ ├── route-docs.ejs │ ├── route-name.ejs │ ├── route-type.ejs │ └── type-data-contract.ejs ├── default │ ├── README.md │ ├── api.ejs │ ├── procedure-call.ejs │ └── route-types.ejs └── modular │ ├── README.md │ ├── api.ejs │ ├── procedure-call.ejs │ └── route-types.ejs ├── tests ├── __snapshots__ │ ├── extended.test.ts.snap │ └── simple.test.ts.snap ├── extended.test.ts ├── fixtures │ └── schemas │ │ ├── v2.0 │ │ ├── adafruit.yaml │ │ ├── another-example.json │ │ ├── another-schema.json │ │ ├── api-with-examples.yaml │ │ ├── authentiq.json │ │ ├── enums.json │ │ ├── example1.json │ │ ├── file-formdata-example.json │ │ ├── furkot-example.yaml │ │ ├── giphy.yaml │ │ ├── path-args.yaml │ │ ├── petstore-expanded.json │ │ ├── petstore-minimal.json │ │ ├── petstore-simple.yaml │ │ ├── petstore-swagger-io.json │ │ ├── petstore-with-external-docs.yaml │ │ ├── petstore.yaml │ │ ├── query-path-param.yaml │ │ └── uber.yaml │ │ └── v3.0 │ │ ├── additional-properties.yaml │ │ ├── additional-properties2.json │ │ ├── allof-example.yaml │ │ ├── anyof-example.yaml │ │ ├── api-with-examples.yaml │ │ ├── callback-example.yaml │ │ ├── components-responses.yaml │ │ ├── explode-param-3.yaml │ │ ├── full-swagger-scheme.json │ │ ├── link-example.yaml │ │ ├── no-definitions-schema.yaml │ │ ├── oneof-example.yaml │ │ ├── personal-api-example.json │ │ ├── petstore-expanded.yaml │ │ ├── petstore.yaml │ │ ├── recursive-schema.json │ │ ├── responses.yaml │ │ ├── swaggerhub-template.yaml │ │ ├── tsoa-odd-types-3.json │ │ ├── up-banking.json │ │ ├── uspto.yaml │ │ ├── wrong-enum-subtypes.yaml │ │ └── wrong-schema-names.yaml ├── simple.test.ts ├── spec │ ├── additional-properties-2.0 │ │ ├── __snapshots__ │ │ │ └── basic.test.ts.snap │ │ ├── basic.test.ts │ │ └── schema.json │ ├── another-array-type │ │ ├── __snapshots__ │ │ │ └── basic.test.ts.snap │ │ ├── basic.test.ts │ │ └── schema.json │ ├── another-query-params │ │ ├── __snapshots__ │ │ │ └── basic.test.ts.snap │ │ ├── basic.test.ts │ │ └── schema.json │ ├── axios │ │ ├── __snapshots__ │ │ │ └── basic.test.ts.snap │ │ ├── basic.test.ts │ │ └── schema.json │ ├── axiosSingleHttpClient │ │ ├── __snapshots__ │ │ │ └── basic.test.ts.snap │ │ ├── basic.test.ts │ │ └── schema.json │ ├── const-keyword │ │ ├── __snapshots__ │ │ │ └── basic.test.ts.snap │ │ ├── basic.test.ts │ │ └── schema.json │ ├── custom-extensions │ │ ├── __snapshots__ │ │ │ └── basic.test.ts.snap │ │ ├── basic.test.ts │ │ └── schema.json │ ├── defaultAsSuccess │ │ ├── __snapshots__ │ │ │ └── basic.test.ts.snap │ │ ├── basic.test.ts │ │ └── schema.json │ ├── defaultResponse │ │ ├── __snapshots__ │ │ │ └── basic.test.ts.snap │ │ ├── basic.test.ts │ │ └── schema.json │ ├── deprecated │ │ ├── __snapshots__ │ │ │ └── basic.test.ts.snap │ │ ├── basic.test.ts │ │ └── schema.json │ ├── discriminator │ │ ├── __snapshots__ │ │ │ └── basic.test.ts.snap │ │ ├── basic.test.ts │ │ └── schema.json │ ├── dot-path-params │ │ ├── __snapshots__ │ │ │ └── basic.test.ts.snap │ │ ├── basic.test.ts │ │ └── schema.json │ ├── enumNamesAsValues │ │ ├── __snapshots__ │ │ │ └── basic.test.ts.snap │ │ ├── basic.test.ts │ │ └── schema.json │ ├── enumNotFirstInComponents │ │ ├── __snapshots__ │ │ │ └── basic.test.ts.snap │ │ ├── basic.test.ts │ │ └── schema.json │ ├── enums-2.0 │ │ ├── __snapshots__ │ │ │ └── basic.test.ts.snap │ │ ├── basic.test.ts │ │ └── schema.json │ ├── extractRequestBody │ │ ├── __snapshots__ │ │ │ └── basic.test.ts.snap │ │ ├── basic.test.ts │ │ └── schema.json │ ├── extractRequestParams │ │ ├── __snapshots__ │ │ │ └── basic.test.ts.snap │ │ ├── basic.test.ts │ │ └── schema.json │ ├── extractResponseBody │ │ ├── __snapshots__ │ │ │ └── basic.test.ts.snap │ │ ├── basic.test.ts │ │ └── schema.json │ ├── extractResponseError │ │ ├── __snapshots__ │ │ │ └── basic.test.ts.snap │ │ ├── basic.test.ts │ │ └── schema.json │ ├── js │ │ ├── __snapshots__ │ │ │ └── basic.test.ts.snap │ │ ├── basic.test.ts │ │ └── schema.json │ ├── jsAxios │ │ ├── __snapshots__ │ │ │ └── basic.test.ts.snap │ │ ├── basic.test.ts │ │ └── schema.json │ ├── jsonapi-media-type │ │ ├── __snapshots__ │ │ │ └── basic.test.ts.snap │ │ ├── basic.test.ts │ │ └── schema.json │ ├── moduleNameFirstTag │ │ ├── __snapshots__ │ │ │ └── basic.test.ts.snap │ │ ├── basic.test.ts │ │ └── schema.json │ ├── moduleNameIndex │ │ ├── __snapshots__ │ │ │ └── basic.test.ts.snap │ │ ├── basic.test.ts │ │ └── schema.json │ ├── noClient │ │ ├── __snapshots__ │ │ │ └── basic.test.ts.snap │ │ ├── basic.test.ts │ │ └── schema.json │ ├── nullable-2.0 │ │ ├── __snapshots__ │ │ │ └── basic.test.ts.snap │ │ ├── basic.test.ts │ │ └── schema.json │ ├── nullable-3.0 │ │ ├── __snapshots__ │ │ │ └── basic.test.ts.snap │ │ ├── basic.test.ts │ │ └── schema.json │ ├── object-types │ │ ├── __snapshots__ │ │ │ └── basic.test.ts.snap │ │ ├── basic.test.ts │ │ └── schema.json │ ├── on-insert-path-param │ │ ├── __snapshots__ │ │ │ └── basic.test.ts.snap │ │ ├── basic.test.ts │ │ └── schema.json │ ├── patch │ │ ├── __snapshots__ │ │ │ └── basic.test.ts.snap │ │ ├── basic.test.ts │ │ └── schema.json │ ├── readonly │ │ ├── __snapshots__ │ │ │ └── basic.test.ts.snap │ │ ├── basic.test.ts │ │ └── schema.json │ ├── responses │ │ ├── __snapshots__ │ │ │ └── basic.test.ts.snap │ │ ├── basic.test.ts │ │ └── schema.json │ ├── routeTypes │ │ ├── __snapshots__ │ │ │ └── basic.test.ts.snap │ │ ├── basic.test.ts │ │ └── schema.json │ ├── singleHttpClient │ │ ├── __snapshots__ │ │ │ └── basic.test.ts.snap │ │ ├── basic.test.ts │ │ └── schema.json │ ├── sortTypes-false │ │ ├── __snapshots__ │ │ │ └── basic.test.ts.snap │ │ ├── basic.test.ts │ │ └── schema.json │ ├── sortTypes │ │ ├── __snapshots__ │ │ │ └── basic.test.ts.snap │ │ ├── basic.test.ts │ │ └── schema.json │ ├── specProperty │ │ ├── __snapshots__ │ │ │ └── basic.test.ts.snap │ │ ├── basic.test.ts │ │ └── schema.json │ ├── typeSuffixPrefix │ │ ├── __snapshots__ │ │ │ └── basic.test.ts.snap │ │ ├── basic.test.ts │ │ └── schema.json │ └── unionEnums │ │ ├── __snapshots__ │ │ └── basic.test.ts.snap │ │ ├── basic.test.ts │ │ └── schema.json └── utils.ts ├── tsconfig.json ├── tsdown.config.ts ├── types └── index.ts ├── vitest.config.ts └── yarn.lock /.changeset/README.md: -------------------------------------------------------------------------------- 1 | # Changesets 2 | 3 | Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works 4 | with multi-package repos, or single-package repos to help you version and publish your code. You can 5 | find the full documentation for it [in our repository](https://github.com/changesets/changesets) 6 | 7 | We have a quick list of common questions to get you started engaging with this project in 8 | [our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md) 9 | -------------------------------------------------------------------------------- /.changeset/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://unpkg.com/@changesets/config/schema.json", 3 | "changelog": [ 4 | "@changesets/changelog-github", 5 | { "repo": "acacode/swagger-typescript-api" } 6 | ], 7 | "commit": false, 8 | "fixed": [], 9 | "linked": [], 10 | "access": "public", 11 | "baseBranch": "main", 12 | "updateInternalDependencies": "patch", 13 | "ignore": [] 14 | } 15 | -------------------------------------------------------------------------------- /.changeset/tame-poets-act.md: -------------------------------------------------------------------------------- 1 | --- 2 | "swagger-typescript-api": patch 3 | --- 4 | 5 | Revert "Integrate `generateCommand` arguments and run method into main command (#1198)". 6 | -------------------------------------------------------------------------------- /.changeset/tidy-trams-tap.md: -------------------------------------------------------------------------------- 1 | --- 2 | "swagger-typescript-api": patch 3 | --- 4 | 5 | Update Node.js version requirements. 6 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | -------------------------------------------------------------------------------- /.envrc: -------------------------------------------------------------------------------- 1 | # shellcheck shell=bash 2 | export NODE_VERSIONS="${HOME}/.nvm/versions/node" 3 | export NODE_VERSION_PREFIX="v" 4 | use node 5 | layout node 6 | corepack enable 7 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | custom: ["https://paypal.me/acacode"] 2 | github: smorimoto 3 | ko_fi: js2me 4 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: github-actions 4 | directory: / 5 | schedule: 6 | interval: weekly 7 | - package-ecosystem: npm 8 | directory: / 9 | schedule: 10 | interval: daily 11 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Builds, tests & co 2 | 3 | on: 4 | - push 5 | - pull_request 6 | 7 | permissions: read-all 8 | 9 | jobs: 10 | build-and-test: 11 | strategy: 12 | fail-fast: false 13 | matrix: 14 | node-version: [24, 22, 20] 15 | runs-on: ubuntu-latest 16 | steps: 17 | - name: Checkout tree 18 | uses: actions/checkout@v4 19 | 20 | - name: Set-up Node.js 21 | uses: actions/setup-node@v4 22 | with: 23 | check-latest: true 24 | node-version: ${{ matrix.node-version }} 25 | 26 | - run: corepack enable 27 | 28 | - run: yarn install --immutable 29 | 30 | - run: yarn format:check 31 | 32 | - run: yarn build 33 | 34 | - run: yarn test 35 | -------------------------------------------------------------------------------- /.github/workflows/typedoc.yml: -------------------------------------------------------------------------------- 1 | name: Deploy typedoc to Pages 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | workflow_dispatch: 8 | 9 | permissions: read-all 10 | 11 | concurrency: 12 | group: pages 13 | cancel-in-progress: true 14 | 15 | jobs: 16 | deploy: 17 | environment: 18 | name: github-pages 19 | url: ${{ steps.deployment.outputs.page_url }} 20 | permissions: 21 | contents: read 22 | pages: write 23 | id-token: write 24 | runs-on: ubuntu-latest 25 | steps: 26 | - name: Checkout tree 27 | uses: actions/checkout@v4 28 | 29 | - name: Set-up Node.js 30 | uses: actions/setup-node@v4 31 | with: 32 | check-latest: true 33 | node-version-file: .nvmrc 34 | 35 | - run: corepack enable 36 | 37 | - run: yarn install --immutable 38 | 39 | - run: yarn typedoc 40 | 41 | - name: Set-up Pages 42 | uses: actions/configure-pages@v5 43 | 44 | - name: Upload artifact 45 | uses: actions/upload-pages-artifact@v3 46 | with: 47 | path: docs 48 | 49 | - name: Deploy odoc to GitHub Pages 50 | uses: actions/deploy-pages@v4 51 | id: deployment 52 | -------------------------------------------------------------------------------- /.github/workflows/version-or-publish.yml: -------------------------------------------------------------------------------- 1 | name: Version or Publish 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | concurrency: ${{ github.workflow }} 9 | 10 | permissions: read-all 11 | 12 | jobs: 13 | release: 14 | if: ${{ github.repository_owner == 'acacode' }} 15 | permissions: 16 | contents: write # to create release 17 | id-token: write # to generate provenance 18 | issues: write # to post issue comments 19 | pull-requests: write # to create pull request 20 | runs-on: ubuntu-latest 21 | steps: 22 | - name: Checkout tree 23 | uses: actions/checkout@v4 24 | 25 | - name: Set-up Node.js 26 | uses: actions/setup-node@v4 27 | with: 28 | check-latest: true 29 | node-version-file: .nvmrc 30 | 31 | - run: corepack enable 32 | - run: yarn install --immutable 33 | 34 | - name: Create Release Pull Request 35 | uses: changesets/action@v1 36 | with: 37 | version: yarn changeset version 38 | publish: yarn npm publish --tolerate-republish 39 | env: 40 | GITHUB_TOKEN: ${{ github.token }} 41 | YARN_NPM_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 42 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.yarn/ 2 | /dist/ 3 | /docs/ 4 | /node_modules/ 5 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 24 2 | -------------------------------------------------------------------------------- /.yarnrc.yml: -------------------------------------------------------------------------------- 1 | defaultSemverRangePrefix: "" 2 | 3 | nodeLinker: node-modules 4 | 5 | preferInteractive: true 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019-present acacode 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | 'Software'), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 21 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 22 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Swagger TypeScript API 2 | 3 | - Support for OpenAPI 3.0, 2.0, JSON and YAML 4 | - Generate the API Client for Fetch or Axios from an OpenAPI Specification 5 | 6 | Any questions you can ask [**here**](https://github.com/acacode/swagger-typescript-api/discussions) 7 | 8 | ## Examples 9 | 10 | All examples you can find [**here**](https://github.com/acacode/swagger-typescript-api/tree/main/tests) 11 | 12 | ## Usage 13 | 14 | You can use this package in two ways: 15 | 16 | ### CLI 17 | 18 | ```bash 19 | npx swagger-typescript-api generate --path ./swagger.json 20 | ``` 21 | 22 | Or install locally in your project: 23 | 24 | ```bash 25 | npm install --save-dev swagger-typescript-api 26 | npx swagger-typescript-api generate --path ./swagger.json 27 | ``` 28 | 29 | ### Library 30 | 31 | ```bash 32 | npm install --save-dev swagger-typescript-api 33 | ``` 34 | 35 | ```typescript 36 | import * as path from "node:path"; 37 | import * as process from "node:process"; 38 | import { generateApi } from "swagger-typescript-api"; 39 | 40 | await generateApi({ input: path.resolve(process.cwd(), "./swagger.json") }); 41 | ``` 42 | 43 | For more detailed configuration options, please consult the documentation. 44 | 45 | ## Mass media 46 | 47 | - [5 Lessons learned about swagger-typescript-api](https://christo8989.medium.com/5-lessons-learned-about-swagger-typescript-api-511240b34c1) 48 | - [Why Swagger schemes are needed in frontend development ?](https://dev.to/js2me/why-swagger-schemes-are-needed-in-frontend-development-2cb4) 49 | - [Migration en douceur vers TypeScript (French)](https://www.premieroctet.com/blog/migration-typescript/) 50 | - [swagger-typescript-api usage (Japanese)](https://zenn.dev/watahaya/articles/2f4a716c47903b) 51 | 52 | ## License 53 | 54 | Licensed under the [MIT License](https://github.com/acacode/swagger-typescript-api/blob/main/LICENSE). 55 | -------------------------------------------------------------------------------- /biome.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "node_modules/@biomejs/biome/configuration_schema.json", 3 | "files": { 4 | "maxSize": 10000000 5 | }, 6 | "vcs": { 7 | "enabled": true, 8 | "clientKind": "git", 9 | "useIgnoreFile": true, 10 | "defaultBranch": "main" 11 | }, 12 | "linter": { 13 | "enabled": true, 14 | "domains": { 15 | "project": "recommended" 16 | }, 17 | "rules": { 18 | "recommended": true 19 | } 20 | }, 21 | "formatter": { 22 | "enabled": true, 23 | "useEditorconfig": true 24 | }, 25 | "assist": { 26 | "enabled": true, 27 | "actions": { 28 | "recommended": true 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "swagger-typescript-api", 3 | "version": "13.2.0", 4 | "description": "Generate the API client for Fetch or Axios from an OpenAPI Specification", 5 | "homepage": "https://github.com/acacode/swagger-typescript-api", 6 | "bugs": "https://github.com/acacode/swagger-typescript-api/issues", 7 | "repository": "github:acacode/swagger-typescript-api", 8 | "license": "MIT", 9 | "author": "Sergey Volkov ", 10 | "maintainers": [ 11 | "Sora Morimoto " 12 | ], 13 | "type": "module", 14 | "exports": { 15 | ".": { 16 | "import": { 17 | "types": "./dist/lib.d.ts", 18 | "default": "./dist/lib.js" 19 | }, 20 | "require": { 21 | "types": "./dist/lib.d.cts", 22 | "default": "./dist/lib.cjs" 23 | } 24 | } 25 | }, 26 | "main": "./dist/lib.cjs", 27 | "module": "./dist/lib.js", 28 | "types": "./dist/lib.d.cts", 29 | "bin": { 30 | "sta": "./dist/cli.js", 31 | "swagger-typescript-api": "./dist/cli.js" 32 | }, 33 | "files": [ 34 | "dist", 35 | "templates" 36 | ], 37 | "scripts": { 38 | "build": "tsdown", 39 | "cli:help": "node index.js -h", 40 | "cli:json": "node index.js -r -d -p ./swagger-test-cli.json -n swagger-test-cli.ts", 41 | "cli:yaml": "node index.js -r -d -p ./swagger-test-cli.yaml -n swagger-test-cli.ts", 42 | "format": "biome format --write .", 43 | "format:check": "biome format .", 44 | "lint": "biome check", 45 | "prepack": "tsdown", 46 | "test": "vitest run", 47 | "typedoc": "typedoc" 48 | }, 49 | "dependencies": { 50 | "@biomejs/js-api": "^0.8.0-beta.3", 51 | "@biomejs/wasm-nodejs": "^2.0.0-beta.6", 52 | "@types/swagger-schema-official": "^2.0.25", 53 | "c12": "^3.0.4", 54 | "citty": "^0.1.6", 55 | "consola": "^3.4.2", 56 | "eta": "^2.2.0", 57 | "js-yaml": "^4.1.0", 58 | "lodash": "^4.17.21", 59 | "nanoid": "^5.1.5", 60 | "swagger-schema-official": "2.0.0-bab6bed", 61 | "swagger2openapi": "^7.0.8", 62 | "typescript": "~5.8.3" 63 | }, 64 | "devDependencies": { 65 | "@biomejs/biome": "2.0.0-beta.6", 66 | "@changesets/changelog-github": "0.5.1", 67 | "@changesets/cli": "2.29.4", 68 | "@tsconfig/node18": "18.2.4", 69 | "@tsconfig/strictest": "2.0.5", 70 | "@types/js-yaml": "4.0.9", 71 | "@types/lodash": "4.17.17", 72 | "@types/node": "22.15.30", 73 | "@types/swagger2openapi": "7.0.4", 74 | "axios": "1.9.0", 75 | "openapi-types": "12.1.3", 76 | "tsdown": "0.12.7", 77 | "typedoc": "0.28.5", 78 | "vitest": "3.2.2" 79 | }, 80 | "packageManager": "yarn@4.9.2", 81 | "engines": { 82 | "node": ">=20" 83 | }, 84 | "publishConfig": { 85 | "access": "public", 86 | "provenance": true, 87 | "registry": "https://registry.npmjs.org" 88 | }, 89 | "typedocOptions": { 90 | "entryPoints": [ 91 | "src/index.ts" 92 | ], 93 | "skipErrorChecking": true 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/code-formatter.ts: -------------------------------------------------------------------------------- 1 | import * as path from "node:path"; 2 | import { Biome, Distribution } from "@biomejs/js-api"; 3 | import * as nanoid from "nanoid"; 4 | import * as typescript from "typescript"; 5 | import type { CodeGenConfig } from "./configuration.js"; 6 | 7 | export class CodeFormatter { 8 | config: CodeGenConfig; 9 | 10 | constructor(config: CodeGenConfig) { 11 | this.config = config; 12 | } 13 | 14 | removeUnusedImports = (content: string) => { 15 | const tempFileName = "file.ts"; 16 | 17 | const host = new TsLanguageServiceHost(tempFileName, content); 18 | const languageService = typescript.createLanguageService(host); 19 | 20 | const fileTextChanges = languageService.organizeImports( 21 | { type: "file", fileName: tempFileName }, 22 | { newLineCharacter: typescript.sys.newLine }, 23 | undefined, 24 | )[0]; 25 | 26 | if (fileTextChanges?.textChanges.length) { 27 | return fileTextChanges.textChanges.reduceRight( 28 | (content, { span, newText }) => 29 | `${content.slice(0, span.start)}${newText}${content.slice( 30 | span.start + span.length, 31 | )}`, 32 | content, 33 | ); 34 | } 35 | 36 | return content; 37 | }; 38 | 39 | format = async (content: string) => { 40 | const biome = await Biome.create({ distribution: Distribution.NODE }); 41 | const biomeProject = biome.openProject(); 42 | biome.applyConfiguration(biomeProject.projectKey, { 43 | files: { maxSize: Number.MAX_SAFE_INTEGER }, 44 | formatter: { indentStyle: "space" }, 45 | }); 46 | const formatted = biome.formatContent(biomeProject.projectKey, content, { 47 | filePath: path.format({ name: nanoid.nanoid(), ext: "ts" }), 48 | }); 49 | return formatted.content; 50 | }; 51 | 52 | formatCode = async ( 53 | code: string, 54 | { removeUnusedImports = true, format = true } = {}, 55 | ) => { 56 | if (removeUnusedImports) { 57 | code = this.removeUnusedImports(code); 58 | } 59 | if (format) { 60 | code = await this.format(code); 61 | } 62 | return code; 63 | }; 64 | } 65 | 66 | class TsLanguageServiceHost { 67 | fileName: string; 68 | content: string; 69 | compilerOptions: typescript.CompilerOptions; 70 | 71 | constructor(fileName: string, content: string) { 72 | this.fileName = fileName; 73 | this.content = content; 74 | const tsconfig = typescript.findConfigFile( 75 | fileName, 76 | typescript.sys.fileExists, 77 | ); 78 | this.compilerOptions = tsconfig 79 | ? typescript.convertCompilerOptionsFromJson( 80 | typescript.readConfigFile(tsconfig, typescript.sys.readFile).config 81 | .compilerOptions, 82 | "", 83 | ).options 84 | : typescript.getDefaultCompilerOptions(); 85 | } 86 | 87 | getNewLine() { 88 | return "newLine" in typescript.sys ? typescript.sys.newLine : "\n"; 89 | } 90 | getScriptFileNames() { 91 | return [this.fileName]; 92 | } 93 | getCompilationSettings() { 94 | return this.compilerOptions; 95 | } 96 | getDefaultLibFileName() { 97 | return typescript.getDefaultLibFileName(this.getCompilationSettings()); 98 | } 99 | getCurrentDirectory() { 100 | return process.cwd(); 101 | } 102 | getScriptVersion() { 103 | return typescript.version; 104 | } 105 | getScriptSnapshot() { 106 | return typescript.ScriptSnapshot.fromString(this.content); 107 | } 108 | readFile(fileName: string, encoding: string) { 109 | if (fileName === this.fileName) { 110 | return this.content; 111 | } 112 | 113 | return typescript.sys.readFile(fileName, encoding); 114 | } 115 | fileExists(path: string) { 116 | return typescript.sys.fileExists(path); 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /src/commands/generate-templates/configuration.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | GenerateTemplatesParams, 3 | HttpClientType, 4 | } from "../../../types/index.js"; 5 | import { HTTP_CLIENT, PROJECT_VERSION } from "../../constants.js"; 6 | import { objectAssign } from "../../util/object-assign.js"; 7 | 8 | export class TemplatesGenConfig { 9 | cleanOutput = false; 10 | debug = false; 11 | httpClientType: HttpClientType = HTTP_CLIENT.FETCH; 12 | modular = false; 13 | output = undefined; 14 | rewrite = false; 15 | silent = false; 16 | version = PROJECT_VERSION; 17 | 18 | constructor(config: GenerateTemplatesParams) { 19 | this.update(config); 20 | } 21 | 22 | update = (update: Partial) => { 23 | objectAssign(this, update); 24 | }; 25 | } 26 | -------------------------------------------------------------------------------- /src/commands/generate-templates/index.ts: -------------------------------------------------------------------------------- 1 | import { consola } from "consola"; 2 | import type { GenerateTemplatesParams } from "../../../types/index.js"; 3 | import { TemplatesGenProcess } from "./templates-gen-process.js"; 4 | 5 | export async function generateTemplates(config: GenerateTemplatesParams) { 6 | if (config.debug) consola.level = Number.MAX_SAFE_INTEGER; 7 | if (config.silent) consola.level = 0; 8 | const codeGenProcess = new TemplatesGenProcess(config); 9 | return await codeGenProcess.start(); 10 | } 11 | -------------------------------------------------------------------------------- /src/component-type-name-resolver.ts: -------------------------------------------------------------------------------- 1 | import { consola } from "consola"; 2 | import type { CodeGenConfig } from "./configuration.js"; 3 | import { NameResolver } from "./util/name-resolver.js"; 4 | import { getRandomInt } from "./util/random.js"; 5 | 6 | export class ComponentTypeNameResolver extends NameResolver { 7 | counter = 1; 8 | fallbackNameCounter = 1; 9 | countersByVariant = new Map(); 10 | 11 | constructor(config: CodeGenConfig, reservedNames: string[]) { 12 | super(config, reservedNames, (variants) => { 13 | const randomVariant = variants[getRandomInt(0, variants.length - 1)]; 14 | if (randomVariant) { 15 | if (!this.countersByVariant.has(randomVariant)) { 16 | this.countersByVariant.set(randomVariant, 0); 17 | } 18 | const variantCounter = 19 | (this.countersByVariant.get(randomVariant) as number) + 1; 20 | this.countersByVariant.set(randomVariant, variantCounter); 21 | const dirtyResolvedName = `${randomVariant}${variantCounter}`; 22 | consola.debug( 23 | "generated dirty resolved type name for component - ", 24 | dirtyResolvedName, 25 | ); 26 | return dirtyResolvedName; 27 | } 28 | 29 | const fallbackName = `${this.config.componentTypeNameResolver}${this 30 | .fallbackNameCounter++}`; 31 | consola.debug( 32 | "generated fallback type name for component - ", 33 | fallbackName, 34 | ); 35 | return fallbackName; 36 | }); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/constants.ts: -------------------------------------------------------------------------------- 1 | import packageJson from "../package.json" with { type: "json" }; 2 | 3 | export const DEFAULT_BODY_ARG_NAME = "data"; 4 | 5 | export const FILE_PREFIX = `/* eslint-disable */ 6 | /* tslint:disable */ 7 | // @ts-nocheck 8 | /* 9 | * --------------------------------------------------------------- 10 | * ## THIS FILE WAS GENERATED VIA SWAGGER-TYPESCRIPT-API ## 11 | * ## ## 12 | * ## AUTHOR: acacode ## 13 | * ## SOURCE: https://github.com/acacode/swagger-typescript-api ## 14 | * --------------------------------------------------------------- 15 | */ 16 | 17 | `; 18 | 19 | export const HTTP_CLIENT = { 20 | FETCH: "fetch", 21 | AXIOS: "axios", 22 | } as const; 23 | 24 | export const PROJECT_VERSION = packageJson.version; 25 | 26 | export const RESERVED_BODY_ARG_NAMES = ["data", "body", "reqBody"]; 27 | 28 | export const RESERVED_HEADER_ARG_NAMES = ["headers", "headersParams"]; 29 | 30 | export const RESERVED_PATH_ARG_NAMES = ["path", "pathParams"]; 31 | 32 | export const RESERVED_QUERY_ARG_NAMES = ["query", "queryParams", "queryArg"]; 33 | 34 | export const RESERVED_REQ_PARAMS_ARG_NAMES = [ 35 | "params", 36 | "requestParams", 37 | "reqParams", 38 | "httpParams", 39 | ]; 40 | 41 | export const SCHEMA_TYPES = { 42 | ARRAY: "array", 43 | OBJECT: "object", 44 | ENUM: "enum", 45 | REF: "$ref", 46 | PRIMITIVE: "primitive", 47 | COMPLEX: "complex", 48 | DISCRIMINATOR: "discriminator", 49 | COMPLEX_ONE_OF: "oneOf", 50 | COMPLEX_ANY_OF: "anyOf", 51 | COMPLEX_ALL_OF: "allOf", 52 | COMPLEX_NOT: "not", 53 | COMPLEX_UNKNOWN: "__unknown", 54 | } as const; 55 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { consola } from "consola"; 2 | import type { GenerateApiConfiguration } from "../types/index.js"; 3 | import { CodeGenProcess } from "./code-gen-process.js"; 4 | 5 | export async function generateApi( 6 | config: Partial, 7 | ) { 8 | if (config.debug) consola.level = Number.MAX_SAFE_INTEGER; 9 | if (config.silent) consola.level = 0; 10 | const codeGenProcess = new CodeGenProcess(config); 11 | return await codeGenProcess.start(); 12 | } 13 | 14 | export { generateTemplates } from "./commands/generate-templates/index.js"; 15 | export * as constants from "./constants.js"; 16 | -------------------------------------------------------------------------------- /src/schema-components-map.ts: -------------------------------------------------------------------------------- 1 | import type { SchemaComponent } from "../types/index.js"; 2 | import type { CodeGenConfig } from "./configuration.js"; 3 | 4 | export class SchemaComponentsMap { 5 | _data: SchemaComponent[] = []; 6 | config: CodeGenConfig; 7 | 8 | constructor(config: CodeGenConfig) { 9 | this.config = config; 10 | } 11 | 12 | clear() { 13 | this._data = []; 14 | } 15 | 16 | createRef = (paths: string[]) => { 17 | return ["#", ...paths].join("/"); 18 | }; 19 | 20 | parseRef = (ref: string) => { 21 | return ref.split("/"); 22 | }; 23 | 24 | createComponent( 25 | $ref: string, 26 | rawTypeData: SchemaComponent["rawTypeData"], 27 | ): SchemaComponent { 28 | const parsed = this.parseRef($ref); 29 | const typeName = parsed[parsed.length - 1]!; 30 | const componentName = parsed[ 31 | parsed.length - 2 32 | ] as SchemaComponent["componentName"]; 33 | const componentSchema: SchemaComponent = { 34 | $ref, 35 | typeName, 36 | rawTypeData, 37 | componentName, 38 | /** result from schema parser */ 39 | typeData: null, 40 | }; 41 | 42 | const usageComponent = 43 | this.config.hooks.onCreateComponent(componentSchema) || componentSchema; 44 | 45 | const refIndex = this._data.findIndex((c) => c.$ref === $ref); 46 | 47 | if (refIndex === -1) { 48 | this._data.push(usageComponent); 49 | } else { 50 | this._data[refIndex] = usageComponent; 51 | } 52 | 53 | return usageComponent; 54 | } 55 | 56 | getComponents() { 57 | return this._data; 58 | } 59 | 60 | filter(...componentNames: (string[] | string)[]) { 61 | return this._data.filter((it) => 62 | componentNames.some((componentName) => 63 | it.$ref.startsWith(`#/components/${componentName}`), 64 | ), 65 | ); 66 | } 67 | 68 | get($ref: string) { 69 | return this._data.find((c) => c.$ref === $ref) || null; 70 | } 71 | 72 | // Ensure enums are at the top of components list 73 | enumsFirst() { 74 | this._data.sort((a, b) => { 75 | if (Object.keys(a.rawTypeData || {}).includes("enum")) return -1; 76 | if (Object.keys(b.rawTypeData || {}).includes("enum")) return 1; 77 | return 0; 78 | }); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/schema-parser/base-schema-parsers/array.ts: -------------------------------------------------------------------------------- 1 | import { SCHEMA_TYPES } from "../../constants.js"; 2 | import { MonoSchemaParser } from "../mono-schema-parser.js"; 3 | 4 | export class ArraySchemaParser extends MonoSchemaParser { 5 | override parse() { 6 | let contentType; 7 | const { type, description, items } = this.schema || {}; 8 | 9 | if (Array.isArray(items) && type === SCHEMA_TYPES.ARRAY) { 10 | const tupleContent = []; 11 | for (const item of items) { 12 | tupleContent.push( 13 | this.schemaParserFabric 14 | .createSchemaParser({ schema: item, schemaPath: this.schemaPath }) 15 | .getInlineParseContent(), 16 | ); 17 | } 18 | contentType = this.config.Ts.Tuple(tupleContent); 19 | } else { 20 | const content = this.schemaParserFabric 21 | .createSchemaParser({ schema: items, schemaPath: this.schemaPath }) 22 | .getInlineParseContent(); 23 | contentType = this.config.Ts.ArrayType(content); 24 | } 25 | 26 | return { 27 | ...(typeof this.schema === "object" ? this.schema : {}), 28 | $schemaPath: this.schemaPath.slice(), 29 | $parsedSchema: true, 30 | schemaType: SCHEMA_TYPES.PRIMITIVE, 31 | type: SCHEMA_TYPES.PRIMITIVE, 32 | typeIdentifier: this.config.Ts.Keyword.Type, 33 | name: this.typeName, 34 | description: this.schemaFormatters.formatDescription(description), 35 | content: this.schemaUtils.safeAddNullToType(this.schema, contentType), 36 | }; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/schema-parser/base-schema-parsers/complex.ts: -------------------------------------------------------------------------------- 1 | import lodash from "lodash"; 2 | import { SCHEMA_TYPES } from "../../constants.js"; 3 | import { MonoSchemaParser } from "../mono-schema-parser.js"; 4 | 5 | export class ComplexSchemaParser extends MonoSchemaParser { 6 | override parse() { 7 | const complexType = this.schemaUtils.getComplexType(this.schema); 8 | const simpleSchema = lodash.omit( 9 | lodash.clone(this.schema), 10 | lodash.keys(this.schemaParser._complexSchemaParsers), 11 | ); 12 | const complexSchemaContent = this.schemaParser._complexSchemaParsers[ 13 | complexType 14 | ](this.schema); 15 | 16 | return { 17 | ...(typeof this.schema === "object" ? this.schema : {}), 18 | $schemaPath: this.schemaPath.slice(), 19 | $parsedSchema: true, 20 | schemaType: SCHEMA_TYPES.COMPLEX, 21 | type: SCHEMA_TYPES.PRIMITIVE, 22 | typeIdentifier: this.config.Ts.Keyword.Type, 23 | name: this.typeName, 24 | description: this.schemaFormatters.formatDescription( 25 | this.schema.description || 26 | lodash.compact( 27 | lodash.map(this.schema[complexType], "description"), 28 | )[0] || 29 | "", 30 | ), 31 | content: 32 | this.config.Ts.IntersectionType( 33 | lodash.compact([ 34 | this.config.Ts.ExpressionGroup(complexSchemaContent), 35 | this.schemaUtils.getInternalSchemaType(simpleSchema) === 36 | SCHEMA_TYPES.OBJECT && 37 | this.config.Ts.ExpressionGroup( 38 | this.schemaParserFabric 39 | .createSchemaParser({ 40 | schema: simpleSchema, 41 | schemaPath: this.schemaPath, 42 | }) 43 | .getInlineParseContent(), 44 | ), 45 | ]), 46 | ) || this.config.Ts.Keyword.Any, 47 | }; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/schema-parser/base-schema-parsers/object.ts: -------------------------------------------------------------------------------- 1 | import lodash from "lodash"; 2 | import { SCHEMA_TYPES } from "../../constants.js"; 3 | import { MonoSchemaParser } from "../mono-schema-parser.js"; 4 | 5 | export class ObjectSchemaParser extends MonoSchemaParser { 6 | override parse() { 7 | const contentProperties = this.getObjectSchemaContent(this.schema); 8 | 9 | return { 10 | ...(typeof this.schema === "object" ? this.schema : {}), 11 | $schemaPath: this.schemaPath.slice(), 12 | $parsedSchema: true, 13 | schemaType: SCHEMA_TYPES.OBJECT, 14 | type: SCHEMA_TYPES.OBJECT, 15 | typeIdentifier: this.config.Ts.Keyword.Interface, 16 | name: this.typeName, 17 | description: this.schemaFormatters.formatDescription( 18 | this.schema.description, 19 | ), 20 | allFieldsAreOptional: !contentProperties.some((part) => part.isRequired), 21 | content: contentProperties, 22 | }; 23 | } 24 | 25 | getObjectSchemaContent = (schema) => { 26 | const { properties, additionalProperties } = schema || {}; 27 | 28 | const propertiesContent = lodash.map(properties, (property, name) => { 29 | const required = this.schemaUtils.isPropertyRequired( 30 | name, 31 | property, 32 | schema, 33 | ); 34 | const rawTypeData = lodash.get( 35 | this.schemaUtils.getSchemaRefType(property), 36 | "rawTypeData", 37 | {}, 38 | ); 39 | const nullable = !!(rawTypeData.nullable || property.nullable); 40 | const fieldName = this.typeNameFormatter.isValidName(name) 41 | ? name 42 | : this.config.Ts.StringValue(name); 43 | const fieldValue = this.schemaParserFabric 44 | .createSchemaParser({ 45 | schema: property, 46 | schemaPath: [...this.schemaPath, name], 47 | }) 48 | .getInlineParseContent(); 49 | const readOnly = property.readOnly; 50 | 51 | return { 52 | ...property, 53 | $$raw: property, 54 | title: property.title, 55 | description: 56 | property.description || 57 | lodash.compact( 58 | lodash.map( 59 | property[this.schemaUtils.getComplexType(property)], 60 | "description", 61 | ), 62 | )[0] || 63 | rawTypeData.description || 64 | lodash.compact( 65 | lodash.map( 66 | rawTypeData[this.schemaUtils.getComplexType(rawTypeData)], 67 | "description", 68 | ), 69 | )[0] || 70 | "", 71 | isRequired: required, 72 | isNullable: nullable, 73 | name: fieldName, 74 | value: fieldValue, 75 | field: this.config.Ts.TypeField({ 76 | readonly: readOnly && this.config.addReadonly, 77 | optional: !required, 78 | key: fieldName, 79 | value: fieldValue, 80 | }), 81 | }; 82 | }); 83 | 84 | if (additionalProperties) { 85 | propertiesContent.push({ 86 | $$raw: { additionalProperties }, 87 | description: "", 88 | isRequired: false, 89 | field: this.config.Ts.InterfaceDynamicField( 90 | this.config.Ts.Keyword.String, 91 | this.config.Ts.Keyword.Any, 92 | ), 93 | }); 94 | } 95 | 96 | return propertiesContent; 97 | }; 98 | } 99 | -------------------------------------------------------------------------------- /src/schema-parser/base-schema-parsers/primitive.ts: -------------------------------------------------------------------------------- 1 | import { SCHEMA_TYPES } from "../../constants.js"; 2 | import { MonoSchemaParser } from "../mono-schema-parser.js"; 3 | 4 | export class PrimitiveSchemaParser extends MonoSchemaParser { 5 | override parse() { 6 | let contentType = null; 7 | const { additionalProperties, type, description, items } = 8 | this.schema || {}; 9 | 10 | if (type === this.config.Ts.Keyword.Object && additionalProperties) { 11 | const fieldType = 12 | typeof additionalProperties === "object" 13 | ? this.schemaParserFabric 14 | .createSchemaParser({ 15 | schema: additionalProperties, 16 | schemaPath: this.schemaPath, 17 | }) 18 | .getInlineParseContent() 19 | : this.config.Ts.Keyword.Any; 20 | contentType = this.config.Ts.RecordType( 21 | this.config.Ts.Keyword.String, 22 | fieldType, 23 | ); 24 | } 25 | 26 | if (Array.isArray(type) && type.length) { 27 | contentType = this.schemaParser._complexSchemaParsers.oneOf({ 28 | ...(typeof this.schema === "object" ? this.schema : {}), 29 | oneOf: type.map((type) => ({ type })), 30 | }); 31 | } 32 | 33 | if (Array.isArray(items) && type === SCHEMA_TYPES.ARRAY) { 34 | contentType = this.config.Ts.Tuple( 35 | items.map((item) => 36 | this.schemaParserFabric 37 | .createSchemaParser({ schema: item, schemaPath: this.schemaPath }) 38 | .getInlineParseContent(), 39 | ), 40 | ); 41 | } 42 | 43 | return { 44 | ...(typeof this.schema === "object" ? this.schema : {}), 45 | $schemaPath: this.schemaPath.slice(), 46 | $parsedSchema: true, 47 | schemaType: SCHEMA_TYPES.PRIMITIVE, 48 | type: SCHEMA_TYPES.PRIMITIVE, 49 | typeIdentifier: this.config.Ts.Keyword.Type, 50 | name: this.typeName, 51 | description: this.schemaFormatters.formatDescription(description), 52 | // TODO: probably it should be refactored. `type === 'null'` is not flexible 53 | content: 54 | type === this.config.Ts.Keyword.Null 55 | ? type 56 | : contentType || this.schemaUtils.getSchemaType(this.schema), 57 | }; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/schema-parser/complex-schema-parsers/all-of.ts: -------------------------------------------------------------------------------- 1 | import { MonoSchemaParser } from "../mono-schema-parser.js"; 2 | 3 | // T1 & T2 4 | export class AllOfSchemaParser extends MonoSchemaParser { 5 | override parse() { 6 | const ignoreTypes = [this.config.Ts.Keyword.Any]; 7 | const combined = this.schema.allOf.map((childSchema) => 8 | this.schemaParserFabric.getInlineParseContent( 9 | this.schemaUtils.makeAddRequiredToChildSchema(this.schema, childSchema), 10 | null, 11 | this.schemaPath, 12 | ), 13 | ); 14 | const filtered = this.schemaUtils.filterSchemaContents( 15 | combined, 16 | (content) => !ignoreTypes.includes(content), 17 | ); 18 | 19 | const type = this.config.Ts.IntersectionType(filtered); 20 | 21 | return this.schemaUtils.safeAddNullToType(this.schema, type); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/schema-parser/complex-schema-parsers/any-of.ts: -------------------------------------------------------------------------------- 1 | import { MonoSchemaParser } from "../mono-schema-parser.js"; 2 | 3 | // T1 | T2 4 | export class AnyOfSchemaParser extends MonoSchemaParser { 5 | override parse() { 6 | const ignoreTypes = [this.config.Ts.Keyword.Any]; 7 | const combined = this.schema.anyOf.map((childSchema) => 8 | this.schemaParserFabric.getInlineParseContent( 9 | this.schemaUtils.makeAddRequiredToChildSchema(this.schema, childSchema), 10 | null, 11 | this.schemaPath, 12 | ), 13 | ); 14 | 15 | const filtered = this.schemaUtils.filterSchemaContents( 16 | combined, 17 | (content) => !ignoreTypes.includes(content), 18 | ); 19 | 20 | const type = this.config.Ts.UnionType(filtered); 21 | 22 | return this.schemaUtils.safeAddNullToType(this.schema, type); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/schema-parser/complex-schema-parsers/not.ts: -------------------------------------------------------------------------------- 1 | import { MonoSchemaParser } from "../mono-schema-parser.js"; 2 | 3 | export class NotSchemaParser extends MonoSchemaParser { 4 | override parse() { 5 | return this.config.Ts.Keyword.Any; 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/schema-parser/complex-schema-parsers/one-of.ts: -------------------------------------------------------------------------------- 1 | import { MonoSchemaParser } from "../mono-schema-parser.js"; 2 | 3 | // T1 | T2 4 | export class OneOfSchemaParser extends MonoSchemaParser { 5 | override parse() { 6 | const ignoreTypes = [this.config.Ts.Keyword.Any]; 7 | const combined = this.schema.oneOf.map((childSchema) => 8 | this.schemaParserFabric.getInlineParseContent( 9 | this.schemaUtils.makeAddRequiredToChildSchema(this.schema, childSchema), 10 | null, 11 | this.schemaPath, 12 | ), 13 | ); 14 | 15 | const filtered = this.schemaUtils.filterSchemaContents( 16 | combined, 17 | (content) => !ignoreTypes.includes(content), 18 | ); 19 | 20 | const type = this.config.Ts.UnionType(filtered); 21 | 22 | return this.schemaUtils.safeAddNullToType(this.schema, type); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/schema-parser/mono-schema-parser.ts: -------------------------------------------------------------------------------- 1 | import type { CodeGenConfig } from "../configuration.js"; 2 | import type { SchemaComponentsMap } from "../schema-components-map.js"; 3 | import type { TypeNameFormatter } from "../type-name-formatter.js"; 4 | import type { SchemaFormatters } from "./schema-formatters.js"; 5 | import type { SchemaParser } from "./schema-parser.js"; 6 | import type { SchemaParserFabric } from "./schema-parser-fabric.js"; 7 | import type { SchemaUtils } from "./schema-utils.js"; 8 | 9 | export class MonoSchemaParser { 10 | schema; 11 | typeName; 12 | schemaPath; 13 | 14 | schemaParser: SchemaParser; 15 | schemaParserFabric: SchemaParserFabric; 16 | typeNameFormatter: TypeNameFormatter; 17 | schemaComponentsMap: SchemaComponentsMap; 18 | schemaUtils: SchemaUtils; 19 | config: CodeGenConfig; 20 | schemaFormatters: SchemaFormatters; 21 | 22 | constructor( 23 | schemaParser: SchemaParser, 24 | schema, 25 | typeName = null, 26 | schemaPath = [], 27 | ) { 28 | this.schemaParser = schemaParser; 29 | this.schemaParserFabric = schemaParser.schemaParserFabric; 30 | this.schema = schema; 31 | this.typeName = typeName; 32 | this.typeNameFormatter = schemaParser.typeNameFormatter; 33 | this.schemaPath = schemaPath; 34 | this.schemaComponentsMap = this.schemaParser.schemaComponentsMap; 35 | this.schemaUtils = this.schemaParser.schemaUtils; 36 | this.config = this.schemaParser.config; 37 | this.schemaFormatters = this.schemaParser.schemaFormatters; 38 | } 39 | 40 | parse() { 41 | throw new Error("not implemented"); 42 | } 43 | 44 | buildTypeNameFromPath = () => { 45 | return this.schemaUtils.buildTypeNameFromPath(this.schemaPath); 46 | }; 47 | } 48 | -------------------------------------------------------------------------------- /src/schema-parser/schema-parser-fabric.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | ParsedSchema, 3 | SchemaComponent, 4 | SchemaTypeEnumContent, 5 | SchemaTypeObjectContent, 6 | SchemaTypePrimitiveContent, 7 | } from "../../types/index.js"; 8 | import type { CodeGenConfig } from "../configuration.js"; 9 | import type { SchemaComponentsMap } from "../schema-components-map.js"; 10 | import type { SchemaWalker } from "../schema-walker.js"; 11 | import type { TemplatesWorker } from "../templates-worker.js"; 12 | import type { TypeNameFormatter } from "../type-name-formatter.js"; 13 | import { SchemaFormatters } from "./schema-formatters.js"; 14 | import { SchemaParser } from "./schema-parser.js"; 15 | import { SchemaUtils } from "./schema-utils.js"; 16 | 17 | export class SchemaParserFabric { 18 | config: CodeGenConfig; 19 | schemaComponentsMap: SchemaComponentsMap; 20 | typeNameFormatter: TypeNameFormatter; 21 | schemaFormatters: SchemaFormatters; 22 | templatesWorker: TemplatesWorker; 23 | schemaUtils: SchemaUtils; 24 | schemaWalker: SchemaWalker; 25 | 26 | constructor( 27 | config: CodeGenConfig, 28 | templatesWorker: TemplatesWorker, 29 | schemaComponentsMap: SchemaComponentsMap, 30 | typeNameFormatter: TypeNameFormatter, 31 | schemaWalker: SchemaWalker, 32 | ) { 33 | this.config = config; 34 | this.schemaComponentsMap = schemaComponentsMap; 35 | this.typeNameFormatter = typeNameFormatter; 36 | this.templatesWorker = templatesWorker; 37 | this.schemaWalker = schemaWalker; 38 | this.schemaUtils = new SchemaUtils(this); 39 | this.schemaFormatters = new SchemaFormatters(this); 40 | } 41 | 42 | createSchemaParser = ({ schema, typeName, schemaPath }) => { 43 | return new SchemaParser(this, { schema, typeName, schemaPath }); 44 | }; 45 | 46 | createSchema = ({ 47 | content, 48 | linkedSchema = {}, 49 | linkedComponent, 50 | schemaPath, 51 | ...otherSchemaProps 52 | }) => { 53 | // @ts-expect-error TS(2345) FIXME: Argument of type '{ schema: any; schemaPath: any; ... Remove this comment to see the full error message 54 | const parser = this.createSchemaParser({ 55 | schema: linkedComponent || linkedSchema, 56 | schemaPath, 57 | }); 58 | const parsed = parser.parseSchema(); 59 | parsed.content = content; 60 | Object.assign(parsed, otherSchemaProps); 61 | if (linkedComponent) { 62 | linkedComponent.typeData = parsed; 63 | } 64 | return parser.schema; 65 | }; 66 | 67 | createParsedComponent = ({ 68 | typeName, 69 | schema, 70 | schemaPath, 71 | }): SchemaComponent => { 72 | const schemaCopy = structuredClone(schema); 73 | const customComponent = this.schemaComponentsMap.createComponent( 74 | this.schemaComponentsMap.createRef(["components", "schemas", typeName]), 75 | schemaCopy, 76 | ); 77 | const parsed = this.parseSchema(schemaCopy, null, schemaPath); 78 | 79 | parsed.name = typeName; 80 | customComponent.typeData = parsed; 81 | 82 | return customComponent; 83 | }; 84 | 85 | parseSchema = ( 86 | schema: string, 87 | typeName: string | null = null, 88 | schemaPath: string[] = [], 89 | ): ParsedSchema< 90 | SchemaTypeObjectContent | SchemaTypeEnumContent | SchemaTypePrimitiveContent 91 | > => { 92 | const schemaParser = this.createSchemaParser({ 93 | schema, 94 | typeName, 95 | schemaPath, 96 | }); 97 | return schemaParser.parseSchema(); 98 | }; 99 | 100 | getInlineParseContent = ( 101 | schema: string, 102 | typeName: string | null, 103 | schemaPath: string[], 104 | ): Record => { 105 | const parser = this.createSchemaParser({ schema, typeName, schemaPath }); 106 | return parser.getInlineParseContent(); 107 | }; 108 | 109 | getParseContent = ( 110 | schema: string, 111 | typeName: string | null, 112 | schemaPath: string[], 113 | ): Record => { 114 | const parser = this.createSchemaParser({ schema, typeName, schemaPath }); 115 | return parser.getParseContent(); 116 | }; 117 | } 118 | -------------------------------------------------------------------------------- /src/schema-parser/util/enum-key-resolver.ts: -------------------------------------------------------------------------------- 1 | import { consola } from "consola"; 2 | import type { CodeGenConfig } from "../../configuration.js"; 3 | import { NameResolver } from "../../util/name-resolver.js"; 4 | 5 | export class EnumKeyResolver extends NameResolver { 6 | counter = 1; 7 | constructor(config: CodeGenConfig, reservedNames: string[]) { 8 | super(config, reservedNames, (variants) => { 9 | const generatedVariant = 10 | (variants[0] && `${variants[0]}${this.counter++}`) || 11 | `${this.config.enumKeyResolverName}${this.counter++}`; 12 | consola.debug( 13 | "generated fallback type name for enum key - ", 14 | generatedVariant, 15 | ); 16 | return generatedVariant; 17 | }); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/schema-routes/util/specific-arg-name-resolver.ts: -------------------------------------------------------------------------------- 1 | import { consola } from "consola"; 2 | import type { CodeGenConfig } from "../../configuration.js"; 3 | import { NameResolver } from "../../util/name-resolver.js"; 4 | 5 | export class SpecificArgNameResolver extends NameResolver { 6 | counter = 1; 7 | constructor(config: CodeGenConfig, reservedNames: string[]) { 8 | super(config, reservedNames, (variants) => { 9 | const generatedVariant = 10 | (variants[0] && `${variants[0]}${this.counter++}`) || 11 | `${this.config.specificArgNameResolverName}${this.counter++}`; 12 | consola.debug( 13 | "generated fallback type name for specific arg - ", 14 | generatedVariant, 15 | ); 16 | return generatedVariant; 17 | }); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/schema-walker.ts: -------------------------------------------------------------------------------- 1 | import lodash from "lodash"; 2 | import type { OpenAPI } from "openapi-types"; 3 | import type { CodeGenConfig } from "./configuration.js"; 4 | import type { SwaggerSchemaResolver } from "./swagger-schema-resolver.js"; 5 | 6 | // TODO: WIP 7 | // this class will be needed to walk by schema everywhere 8 | export class SchemaWalker { 9 | config: CodeGenConfig; 10 | swaggerSchemaResolver: SwaggerSchemaResolver; 11 | schemas = new Map(); 12 | caches = new Map(); 13 | 14 | constructor( 15 | config: CodeGenConfig, 16 | swaggerSchemaResolver: SwaggerSchemaResolver, 17 | ) { 18 | this.config = config; 19 | this.swaggerSchemaResolver = swaggerSchemaResolver; 20 | } 21 | 22 | addSchema = (name: string, schema: OpenAPI.Document) => { 23 | this.schemas.set(name, structuredClone(schema)); 24 | }; 25 | 26 | _isLocalRef = (ref: string) => { 27 | return ref.startsWith("#"); 28 | }; 29 | 30 | _isRemoteRef = (ref: string) => { 31 | return ref.startsWith("http://") || ref.startsWith("https://"); 32 | }; 33 | 34 | _getRefDataFromSchema = (schema: Record, ref: string) => { 35 | const path = ref.replace("#", "").split("/"); 36 | const refData = lodash.get(schema, path); 37 | if (refData) { 38 | this.caches.set(ref, refData); 39 | } 40 | return refData; 41 | }; 42 | } 43 | -------------------------------------------------------------------------------- /src/translators/javascript.ts: -------------------------------------------------------------------------------- 1 | import * as typescript from "typescript"; 2 | import { Translator, type TranslatorIO } from "./translator.js"; 3 | 4 | export class JavascriptTranslator extends Translator { 5 | compileTSCode = (input: TranslatorIO): Record => { 6 | const fileNameFull = `${input.fileName}${input.fileExtension}`; 7 | const output = {}; 8 | const host = typescript.createCompilerHost( 9 | this.config.compilerTsConfig, 10 | true, 11 | ); 12 | const fileNames = [fileNameFull]; 13 | const originalSourceFileGet = host.getSourceFile.bind(host); 14 | host.getSourceFile = ( 15 | sourceFileName, 16 | languageVersion, 17 | onError, 18 | shouldCreateNewSourceFile, 19 | ) => { 20 | if (sourceFileName !== fileNameFull) 21 | return originalSourceFileGet( 22 | sourceFileName, 23 | languageVersion, 24 | onError, 25 | shouldCreateNewSourceFile, 26 | ); 27 | 28 | return typescript.createSourceFile( 29 | sourceFileName, 30 | input.fileContent, 31 | languageVersion, 32 | true, 33 | typescript.ScriptKind.TS, 34 | ); 35 | }; 36 | 37 | host.writeFile = (fileName, contents) => { 38 | output[fileName] = contents; 39 | }; 40 | 41 | typescript 42 | .createProgram(fileNames, this.config.compilerTsConfig, host) 43 | .emit(); 44 | 45 | return output; 46 | }; 47 | 48 | translate = async (input) => { 49 | const compiled = this.compileTSCode(input); 50 | 51 | const jsFileName = `${input.fileName}${typescript.Extension.Js}`; 52 | const dtsFileName = `${input.fileName}${typescript.Extension.Dts}`; 53 | const sourceContent = compiled[jsFileName]; 54 | const tsImportRows = input.fileContent 55 | .split("\n") 56 | .filter((line) => line.startsWith("import ")); 57 | const declarationContent = compiled[dtsFileName] 58 | .split("\n") 59 | .map((line) => { 60 | if (line.startsWith("import ")) { 61 | return tsImportRows.shift(); 62 | } 63 | return line; 64 | }) 65 | .join("\n"); 66 | 67 | return [ 68 | { 69 | fileName: input.fileName, 70 | fileExtension: typescript.Extension.Js, 71 | fileContent: await this.codeFormatter.formatCode(sourceContent), 72 | }, 73 | { 74 | fileName: input.fileName, 75 | fileExtension: typescript.Extension.Dts, 76 | fileContent: await this.codeFormatter.formatCode(declarationContent), 77 | }, 78 | ]; 79 | }; 80 | } 81 | -------------------------------------------------------------------------------- /src/translators/translator.ts: -------------------------------------------------------------------------------- 1 | import type { CodeFormatter } from "../code-formatter.js"; 2 | import type { CodeGenConfig } from "../configuration.js"; 3 | 4 | export interface TranslatorIO { 5 | fileName: string; 6 | fileExtension: string; 7 | fileContent: string; 8 | } 9 | 10 | export class Translator { 11 | config: CodeGenConfig; 12 | codeFormatter: CodeFormatter; 13 | 14 | constructor(config: CodeGenConfig, codeFormatter: CodeFormatter) { 15 | this.config = config; 16 | this.codeFormatter = codeFormatter; 17 | } 18 | 19 | translate(_input: TranslatorIO): Promise { 20 | throw new Error("not implemented"); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/type-name-formatter.ts: -------------------------------------------------------------------------------- 1 | import { consola } from "consola"; 2 | import lodash from "lodash"; 3 | import type { CodeGenConfig } from "./configuration.js"; 4 | 5 | type FormattingSchemaType = "enum-key" | "type-name"; 6 | 7 | export class TypeNameFormatter { 8 | formattedModelNamesMap = new Map(); 9 | config: CodeGenConfig; 10 | 11 | constructor(config: CodeGenConfig) { 12 | this.config = config; 13 | } 14 | 15 | format = (name: string, options: { type?: FormattingSchemaType } = {}) => { 16 | const schemaType = options.type ?? "type-name"; 17 | 18 | const typePrefix = 19 | schemaType === "enum-key" 20 | ? this.config.enumKeyPrefix 21 | : this.config.typePrefix; 22 | const typeSuffix = 23 | schemaType === "enum-key" 24 | ? this.config.enumKeySuffix 25 | : this.config.typeSuffix; 26 | 27 | const hashKey = `${typePrefix}_${name}_${typeSuffix}`; 28 | 29 | if (typeof name !== "string") { 30 | consola.warn("wrong model name", name); 31 | return name; 32 | } 33 | 34 | // constant names like LEFT_ARROW, RIGHT_FORWARD, ETC_KEY, _KEY_NUM_ 35 | if (/^([A-Z_]{1,})$/g.test(name)) { 36 | return lodash.compact([typePrefix, name, typeSuffix]).join("_"); 37 | } 38 | 39 | if (this.formattedModelNamesMap.has(hashKey)) { 40 | return this.formattedModelNamesMap.get(hashKey); 41 | } 42 | 43 | const fixedModelName = this.fixModelName(name, { type: schemaType }); 44 | 45 | const formattedName = lodash 46 | .startCase(`${typePrefix}_${fixedModelName}_${typeSuffix}`) 47 | .replace(/\s/g, ""); 48 | const formattedResultName = 49 | this.config.hooks.onFormatTypeName(formattedName, name, schemaType) || 50 | formattedName; 51 | 52 | this.formattedModelNamesMap.set(hashKey, formattedResultName); 53 | 54 | return formattedResultName; 55 | }; 56 | 57 | isValidName = (name: string) => /^([A-Za-z$_]{1,})$/g.test(name); 58 | 59 | fixModelName = ( 60 | name: string, 61 | options: { type?: FormattingSchemaType }, 62 | ): string => { 63 | if (!this.isValidName(name)) { 64 | if (!/^[a-zA-Z_$]/g.test(name)) { 65 | const fixPrefix = 66 | options.type === "enum-key" 67 | ? this.config.fixInvalidEnumKeyPrefix 68 | : this.config.fixInvalidTypeNamePrefix; 69 | return `${fixPrefix} ${name}`; 70 | } 71 | 72 | // specific replaces for TSOA 3.x 73 | if (name.includes(".")) { 74 | return name 75 | .replace(/Exclude_keyof[A-Za-z]+/g, () => "ExcludeKeys") 76 | .replace(/%22~AND~%22/g, "And") 77 | .replace(/%22~OR~%22/g, "Or") 78 | .replace(/(\.?%22)|\./g, "_") 79 | .replace(/__+$/, ""); 80 | } 81 | 82 | if (name.includes("-")) { 83 | return lodash.startCase(name).replace(/ /g, ""); 84 | } 85 | } 86 | 87 | return name; 88 | }; 89 | } 90 | -------------------------------------------------------------------------------- /src/util/file-system.ts: -------------------------------------------------------------------------------- 1 | import * as fs from "node:fs"; 2 | import * as path from "node:path"; 3 | import * as url from "node:url"; 4 | import { consola } from "consola"; 5 | import { FILE_PREFIX } from "../constants.js"; 6 | 7 | export class FileSystem { 8 | getFileContent = (path: string) => { 9 | return fs.readFileSync(path, { encoding: "utf8" }); 10 | }; 11 | 12 | readDir = (path: string) => { 13 | return fs.readdirSync(path); 14 | }; 15 | 16 | pathIsDir = (path: string) => { 17 | if (!path) return false; 18 | 19 | try { 20 | const stat = fs.statSync(path); 21 | return stat.isDirectory(); 22 | } catch (e) { 23 | return false; 24 | } 25 | }; 26 | 27 | cropExtension = (fileName: string) => { 28 | const fileNameParts = fileName.split("."); 29 | 30 | if (fileNameParts.length > 1) { 31 | fileNameParts.pop(); 32 | } 33 | 34 | return fileNameParts.join("."); 35 | }; 36 | 37 | removeDir = (path: string) => { 38 | try { 39 | if (typeof fs.rmSync === "function") { 40 | fs.rmSync(path, { recursive: true }); 41 | } else { 42 | fs.rmdirSync(path, { recursive: true }); 43 | } 44 | } catch (e) { 45 | consola.debug("failed to remove dir", e); 46 | } 47 | }; 48 | 49 | createDir = (path: string) => { 50 | try { 51 | fs.mkdirSync(path, { recursive: true }); 52 | } catch (e) { 53 | consola.debug("failed to create dir", e); 54 | } 55 | }; 56 | 57 | cleanDir = (path: string) => { 58 | this.removeDir(path); 59 | this.createDir(path); 60 | }; 61 | 62 | pathIsExist = (path: string) => { 63 | return !!path && fs.existsSync(path); 64 | }; 65 | 66 | createFile = ({ path: path_, fileName, content, withPrefix }) => { 67 | const __dirname = path.dirname(url.fileURLToPath(import.meta.url)); 68 | const absolutePath = path.resolve(__dirname, path_, `./${fileName}`); 69 | const fileContent = `${withPrefix ? FILE_PREFIX : ""}${content}`; 70 | 71 | return fs.writeFileSync(absolutePath, fileContent); 72 | }; 73 | } 74 | -------------------------------------------------------------------------------- /src/util/id.ts: -------------------------------------------------------------------------------- 1 | import * as nanoid from "nanoid"; 2 | 3 | const ALPHABET = "abcdefghijklmnopqrstuvwxyz0123456789"; 4 | 5 | export const generateId = nanoid.customAlphabet(ALPHABET, 12); 6 | -------------------------------------------------------------------------------- /src/util/internal-case.ts: -------------------------------------------------------------------------------- 1 | import lodash from "lodash"; 2 | 3 | export function internalCase(value: string) { 4 | return lodash.camelCase(lodash.lowerCase(value)); 5 | } 6 | -------------------------------------------------------------------------------- /src/util/name-resolver.ts: -------------------------------------------------------------------------------- 1 | import { consola } from "consola"; 2 | import lodash from "lodash"; 3 | import type { CodeGenConfig } from "../configuration.js"; 4 | 5 | type Resolver = (reserved: string[], extras?: string[]) => string; 6 | 7 | export class NameResolver { 8 | reservedNames: string[] = []; 9 | getFallbackName: Resolver; 10 | 11 | config: CodeGenConfig; 12 | 13 | constructor( 14 | config: CodeGenConfig, 15 | reservedNames: string[], 16 | getFallbackName: Resolver, 17 | ) { 18 | this.config = config; 19 | this.getFallbackName = getFallbackName; 20 | this.reserve(reservedNames); 21 | } 22 | 23 | reserve(names: string[]) { 24 | const fixedNames = lodash.uniq(lodash.compact(names)); 25 | for (const name of fixedNames) { 26 | if (this.reservedNames.indexOf(name) === -1) { 27 | this.reservedNames.push(name); 28 | } 29 | } 30 | } 31 | 32 | unreserve(names: string[]) { 33 | this.reservedNames = this.reservedNames.filter( 34 | (reservedName) => !names.some((name) => name === reservedName), 35 | ); 36 | } 37 | 38 | isReserved(name: string) { 39 | return this.reservedNames.some((reservedName) => reservedName === name); 40 | } 41 | 42 | resolve( 43 | variants: string[], 44 | resolver?: Resolver, 45 | extras?: string[], 46 | shouldReserve = true, 47 | ): string | null { 48 | if (typeof resolver === "function") { 49 | let usageName: string | null = null; 50 | while (usageName === null) { 51 | const variant = resolver(variants, extras); 52 | 53 | if (variant === undefined) { 54 | consola.warn( 55 | "unable to resolve name. current reserved names: ", 56 | ...this.reservedNames, 57 | ); 58 | return null; 59 | } 60 | if (!shouldReserve || !this.isReserved(variant)) { 61 | usageName = variant; 62 | } 63 | } 64 | 65 | shouldReserve && this.reserve([usageName]); 66 | return usageName; 67 | } 68 | 69 | if (Array.isArray(variants)) { 70 | let usageName: string | null = null; 71 | const uniqVariants = lodash.uniq(lodash.compact(variants)); 72 | 73 | for (const variant of uniqVariants) { 74 | if (!usageName && (!shouldReserve || !this.isReserved(variant))) { 75 | usageName = variant; 76 | } 77 | } 78 | 79 | if (usageName) { 80 | shouldReserve && this.reserve([usageName]); 81 | return usageName; 82 | } 83 | 84 | consola.debug( 85 | "trying to resolve name with using fallback name generator using variants", 86 | ...variants, 87 | ); 88 | return this.resolve(variants, this.getFallbackName, extras); 89 | } 90 | 91 | consola.debug( 92 | "problem with reserving names. current reserved names: ", 93 | ...this.reservedNames, 94 | ); 95 | return null; 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/util/object-assign.ts: -------------------------------------------------------------------------------- 1 | import lodash from "lodash"; 2 | 3 | type Updater = (target: unknown) => unknown; 4 | 5 | export const objectAssign = (target: object, updater: Updater | unknown) => { 6 | if (!updater) return; 7 | const update = typeof updater === "function" ? updater(target) : updater; 8 | const undefinedKeys = lodash 9 | .map(update, (value, key) => value === undefined && key) 10 | .filter((key) => typeof key === "string"); 11 | Object.assign(target, lodash.merge(target, update)); 12 | for (const key of undefinedKeys) { 13 | target[key] = undefined; 14 | } 15 | }; 16 | -------------------------------------------------------------------------------- /src/util/pascal-case.ts: -------------------------------------------------------------------------------- 1 | import lodash from "lodash"; 2 | 3 | export function pascalCase(value: string) { 4 | return lodash.upperFirst(lodash.camelCase(value)); 5 | } 6 | -------------------------------------------------------------------------------- /src/util/random.ts: -------------------------------------------------------------------------------- 1 | export const getRandomFloat = (min = 0, max = 1) => { 2 | return Math.random() * (max - min) + min; 3 | }; 4 | 5 | export const getRandomInt = (min = 0, max = 1) => { 6 | if (min === max) return min; 7 | 8 | return Math.round(getRandomFloat(min, max)); 9 | }; 10 | -------------------------------------------------------------------------------- /src/util/request.ts: -------------------------------------------------------------------------------- 1 | import { consola } from "consola"; 2 | import lodash from "lodash"; 3 | import type { CodeGenConfig } from "../configuration.js"; 4 | 5 | export class Request { 6 | config: CodeGenConfig; 7 | 8 | constructor(config: CodeGenConfig) { 9 | this.config = config; 10 | } 11 | 12 | async download({ 13 | url, 14 | authToken, 15 | ...options 16 | }: { 17 | url: string; 18 | authToken?: string; 19 | options?: Partial; 20 | }) { 21 | const requestOptions: Partial = {}; 22 | 23 | if (authToken) { 24 | requestOptions.headers = { 25 | Authorization: authToken, 26 | }; 27 | } 28 | 29 | lodash.merge(requestOptions, options, this.config.requestOptions); 30 | 31 | try { 32 | const response = await fetch(url, requestOptions); 33 | return await response.text(); 34 | } catch (error) { 35 | const message = `error while fetching data from URL "${url}"`; 36 | consola.error(message, error); 37 | return message; 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/util/sort-by-property.ts: -------------------------------------------------------------------------------- 1 | export const sortByProperty = 2 | (propertyName: string) => 3 | (o1: Record, o2: Record): 1 | -1 | 0 => { 4 | if (o1[propertyName] > o2[propertyName]) { 5 | return 1; 6 | } 7 | if (o1[propertyName] < o2[propertyName]) { 8 | return -1; 9 | } 10 | return 0; 11 | }; 12 | -------------------------------------------------------------------------------- /templates/README.md: -------------------------------------------------------------------------------- 1 | # swagger-typescript-api 2 | 3 | # templates 4 | 5 | Templates: 6 | 7 | - `api.ejs` - _(generates file)_ Api class module (locations: [default](https://github.com/acacode/swagger-typescript-api/tree/main/templates/default/api.ejs), [modular](https://github.com/acacode/swagger-typescript-api/tree/main/templates/modular/api.ejs)) 8 | - `data-contracts.ejs` - _(generates file)_ all types (data contracts) from swagger schema (locations: [base](https://github.com/acacode/swagger-typescript-api/tree/main/templates/base/data-contracts.ejs)) 9 | - `http-client.ejs` - _(generates file)_ HttpClient class module (locations: [base](https://github.com/acacode/swagger-typescript-api/tree/main/templates/base/http-client.ejs)) 10 | - `procedure-call.ejs` - _(subtemplate)_ route in Api class (locations: [default](https://github.com/acacode/swagger-typescript-api/tree/main/templates/default/procedure-call.ejs), [modular](https://github.com/acacode/swagger-typescript-api/tree/main/templates/modular/procedure-call.ejs)) 11 | - `route-docs.ejs` - _(generates file)_ documentation for route in Api class (locations: [base](https://github.com/acacode/swagger-typescript-api/tree/main/templates/base/route-docs.ejs)) 12 | - `route-name.ejs` - _(subtemplate)_ route name for route in Api class (locations: [base](https://github.com/acacode/swagger-typescript-api/tree/main/templates/base/route-name.ejs)) 13 | - `route-type.ejs` - _(`--route-types` option)_ _(subtemplate)_ (locations: [base](https://github.com/acacode/swagger-typescript-api/tree/main/templates/base/route-type.ejs)) 14 | - `route-types.ejs` - _(`--route-types` option)_ _(subtemplate)_ (locations: [base](https://github.com/acacode/swagger-typescript-api/tree/main/templates/base/route-types.ejs)) - `data-contract-jsdoc.ejs` - _(subtemplate)_ generates JSDOC for data contract (locations: [base](https://github.com/acacode/swagger-typescript-api/tree/main/templates/base/data-contract-jsdoc.ejs)) 15 | 16 | [//]: # "- `enum-data-contract.ejs` - *(subtemplate)* generates `enum` data contract (locations: [base](https://github.com/acacode/swagger-typescript-api/tree/main/templates/base/enum-data-contract.ejs))" 17 | [//]: # "- `interface-data-contract.ejs` - *(subtemplate)* generates `interface` data contract (locations: [base](https://github.com/acacode/swagger-typescript-api/tree/main/templates/base/interface-data-contract.ejs))" 18 | [//]: # "- `type-data-contract.ejs` - *(subtemplate)* generates `type` data contract (locations: [base](https://github.com/acacode/swagger-typescript-api/tree/main/templates/base/type-data-contract.ejs))" 19 | -------------------------------------------------------------------------------- /templates/base/README.md: -------------------------------------------------------------------------------- 1 | # swagger-typescript-api 2 | 3 | # templates/base 4 | 5 | This templates use both for multiple api files and single api file 6 | 7 | path prefix `@base` 8 | -------------------------------------------------------------------------------- /templates/base/data-contract-jsdoc.ejs: -------------------------------------------------------------------------------- 1 | <% 2 | const { data, utils } = it; 3 | const { formatDescription, require, _ } = utils; 4 | 5 | const stringify = (value) => _.isObject(value) ? JSON.stringify(value) : _.isString(value) ? `"${value}"` : value 6 | 7 | const jsDocLines = _.compact([ 8 | data.title, 9 | data.description && formatDescription(data.description), 10 | !_.isUndefined(data.deprecated) && data.deprecated && '@deprecated', 11 | !_.isUndefined(data.format) && `@format ${data.format}`, 12 | !_.isUndefined(data.minimum) && `@min ${data.minimum}`, 13 | !_.isUndefined(data.multipleOf) && `@multipleOf ${data.multipleOf}`, 14 | !_.isUndefined(data.exclusiveMinimum) && `@exclusiveMin ${data.exclusiveMinimum}`, 15 | !_.isUndefined(data.maximum) && `@max ${data.maximum}`, 16 | !_.isUndefined(data.minLength) && `@minLength ${data.minLength}`, 17 | !_.isUndefined(data.maxLength) && `@maxLength ${data.maxLength}`, 18 | !_.isUndefined(data.exclusiveMaximum) && `@exclusiveMax ${data.exclusiveMaximum}`, 19 | !_.isUndefined(data.maxItems) && `@maxItems ${data.maxItems}`, 20 | !_.isUndefined(data.minItems) && `@minItems ${data.minItems}`, 21 | !_.isUndefined(data.uniqueItems) && `@uniqueItems ${data.uniqueItems}`, 22 | !_.isUndefined(data.default) && `@default ${stringify(data.default)}`, 23 | !_.isUndefined(data.pattern) && `@pattern ${data.pattern}`, 24 | !_.isUndefined(data.example) && `@example ${stringify(data.example)}` 25 | ]).join('\n').split('\n'); 26 | %> 27 | <% if (jsDocLines.every(_.isEmpty)) { %> 28 | <% } else if (jsDocLines.length === 1) { %> 29 | /** <%~ jsDocLines[0] %> */ 30 | <% } else if (jsDocLines.length) { %> 31 | /** 32 | <% for (jsDocLine of jsDocLines) { %> 33 | * <%~ jsDocLine %> 34 | 35 | <% } %> 36 | */ 37 | <% } %> 38 | -------------------------------------------------------------------------------- /templates/base/data-contracts.ejs: -------------------------------------------------------------------------------- 1 | <% 2 | const { modelTypes, utils, config } = it; 3 | const { formatDescription, require, _, Ts } = utils; 4 | 5 | 6 | const buildGenerics = (contract) => { 7 | if (!contract.genericArgs || !contract.genericArgs.length) return ''; 8 | 9 | return '<' + contract.genericArgs.map(({ name, default: defaultType, extends: extendsType }) => { 10 | return [ 11 | name, 12 | extendsType && `extends ${extendsType}`, 13 | defaultType && `= ${defaultType}`, 14 | ].join('') 15 | }).join(',') + '>' 16 | } 17 | 18 | const dataContractTemplates = { 19 | enum: (contract) => { 20 | return `enum ${contract.name} {\r\n${contract.content} \r\n }`; 21 | }, 22 | interface: (contract) => { 23 | return `interface ${contract.name}${buildGenerics(contract)} {\r\n${contract.content}}`; 24 | }, 25 | type: (contract) => { 26 | return `type ${contract.name}${buildGenerics(contract)} = ${contract.content}`; 27 | }, 28 | } 29 | %> 30 | 31 | <% if (config.internalTemplateOptions.addUtilRequiredKeysType) { %> 32 | type <%~ config.Ts.CodeGenKeyword.UtilRequiredKeys %> = Omit & Required> 33 | <% } %> 34 | 35 | <% for (const contract of modelTypes) { %> 36 | <%~ includeFile('@base/data-contract-jsdoc.ejs', { ...it, data: { ...contract, ...contract.typeData } }) %> 37 | <%~ contract.internal ? '' : 'export'%> <%~ (dataContractTemplates[contract.typeIdentifier] || dataContractTemplates.type)(contract) %> 38 | 39 | 40 | <% } %> 41 | -------------------------------------------------------------------------------- /templates/base/enum-data-contract.ejs: -------------------------------------------------------------------------------- 1 | <% 2 | const { contract, utils, config } = it; 3 | const { formatDescription, require, _ } = utils; 4 | const { name, $content } = contract; 5 | %> 6 | <% if (config.generateUnionEnums) { %> 7 | export type <%~ name %> = <%~ _.map($content, ({ value }) => value).join(" | ") %> 8 | <% } else { %> 9 | export enum <%~ name %> { 10 | <%~ _.map($content, ({ key, value }) => `${key} = ${value}`).join(",\n") %> 11 | } 12 | <% } %> 13 | -------------------------------------------------------------------------------- /templates/base/http-client.ejs: -------------------------------------------------------------------------------- 1 | <% const { config } = it; %> 2 | <% /* https://github.com/acacode/swagger-typescript-api/tree/main/templates/base/http-clients/ */ %> 3 | <%~ includeFile(`@base/http-clients/${config.httpClientType}-http-client`, it) %> 4 | -------------------------------------------------------------------------------- /templates/base/interface-data-contract.ejs: -------------------------------------------------------------------------------- 1 | <% 2 | const { contract, utils } = it; 3 | const { formatDescription, require, _ } = utils; 4 | %> 5 | export interface <%~ contract.name %> { 6 | <% for (const field of contract.$content) { %> 7 | <%~ includeFile('@base/object-field-jsdoc.ejs', { ...it, field }) %> 8 | <%~ field.name %><%~ field.isRequired ? '' : '?' %>: <%~ field.value %><%~ field.isNullable ? ' | null' : ''%>; 9 | <% } %> 10 | } 11 | -------------------------------------------------------------------------------- /templates/base/object-field-jsdoc.ejs: -------------------------------------------------------------------------------- 1 | <% 2 | const { field, utils } = it; 3 | const { formatDescription, require, _ } = utils; 4 | 5 | const comments = _.uniq( 6 | _.compact([ 7 | field.title, 8 | field.description, 9 | field.deprecated && ` * @deprecated`, 10 | !_.isUndefined(field.format) && `@format ${field.format}`, 11 | !_.isUndefined(field.minimum) && `@min ${field.minimum}`, 12 | !_.isUndefined(field.maximum) && `@max ${field.maximum}`, 13 | !_.isUndefined(field.pattern) && `@pattern ${field.pattern}`, 14 | !_.isUndefined(field.example) && 15 | `@example ${_.isObject(field.example) ? JSON.stringify(field.example) : field.example}`, 16 | ]).reduce((acc, comment) => [...acc, ...comment.split(/\n/g)], []), 17 | ); 18 | %> 19 | <% if (comments.length === 1) { %> 20 | /** <%~ comments[0] %> */ 21 | <% } else if (comments.length) { %> 22 | /** 23 | <% comments.forEach(comment => { %> 24 | * <%~ comment %> 25 | 26 | <% }) %> 27 | */ 28 | <% } %> 29 | -------------------------------------------------------------------------------- /templates/base/route-docs.ejs: -------------------------------------------------------------------------------- 1 | <% 2 | const { config, route, utils } = it; 3 | const { _, formatDescription, fmtToJSDocLine, pascalCase, require } = utils; 4 | const { raw, request, routeName } = route; 5 | 6 | const jsDocDescription = raw.description ? 7 | ` * @description ${formatDescription(raw.description, true)}` : 8 | fmtToJSDocLine('No description', { eol: false }); 9 | const jsDocLines = _.compact([ 10 | _.size(raw.tags) && ` * @tags ${raw.tags.join(", ")}`, 11 | ` * @name ${pascalCase(routeName.usage)}`, 12 | raw.summary && ` * @summary ${raw.summary}`, 13 | ` * @request ${_.upperCase(request.method)}:${raw.route}`, 14 | raw.deprecated && ` * @deprecated`, 15 | routeName.duplicate && ` * @originalName ${routeName.original}`, 16 | routeName.duplicate && ` * @duplicate`, 17 | request.security && ` * @secure`, 18 | ...(config.generateResponses && raw.responsesTypes.length 19 | ? raw.responsesTypes.map( 20 | ({ type, status, description, isSuccess }) => 21 | ` * @response \`${status}\` \`${_.replace(_.replace(type, /\/\*/g, "\\*"), /\*\//g, "*\\")}\` ${description}`, 22 | ) 23 | : []), 24 | ]).map(str => str.trimEnd()).join("\n"); 25 | 26 | return { 27 | description: jsDocDescription, 28 | lines: jsDocLines, 29 | } 30 | %> 31 | -------------------------------------------------------------------------------- /templates/base/route-name.ejs: -------------------------------------------------------------------------------- 1 | <% 2 | const { routeInfo, utils } = it; 3 | const { 4 | operationId, 5 | method, 6 | route, 7 | moduleName, 8 | responsesTypes, 9 | description, 10 | tags, 11 | summary, 12 | pathArgs, 13 | } = routeInfo; 14 | const { _, fmtToJSDocLine, require } = utils; 15 | 16 | const methodAliases = { 17 | get: (pathName, hasPathInserts) => 18 | _.camelCase(`${pathName}_${hasPathInserts ? "detail" : "list"}`), 19 | post: (pathName, hasPathInserts) => _.camelCase(`${pathName}_create`), 20 | put: (pathName, hasPathInserts) => _.camelCase(`${pathName}_update`), 21 | patch: (pathName, hasPathInserts) => _.camelCase(`${pathName}_partial_update`), 22 | delete: (pathName, hasPathInserts) => _.camelCase(`${pathName}_delete`), 23 | }; 24 | 25 | const createCustomOperationId = (method, route, moduleName) => { 26 | const hasPathInserts = /\{(\w){1,}\}$/g.test(route); 27 | const splittedRouteBySlash = _.compact(_.replace(route, /\{(\w){1,}\}/g, "").split("/")); 28 | const routeParts = (splittedRouteBySlash.length > 1 29 | ? splittedRouteBySlash.splice(1) 30 | : splittedRouteBySlash 31 | ).join("_"); 32 | return routeParts.length > 3 && methodAliases[method] 33 | ? methodAliases[method](routeParts, hasPathInserts) 34 | : _.camelCase(_.lowerCase(method) + "_" + [moduleName].join("_")) || "index"; 35 | }; 36 | 37 | if (operationId) 38 | return _.camelCase(operationId); 39 | if (route === "/") 40 | return _.camelCase(`${_.lowerCase(method)}Root`); 41 | 42 | return createCustomOperationId(method, route, moduleName); 43 | %> 44 | -------------------------------------------------------------------------------- /templates/base/route-type.ejs: -------------------------------------------------------------------------------- 1 | <% 2 | const { route, utils, config } = it; 3 | const { _, pascalCase, require } = utils; 4 | const { query, payload, pathParams, headers } = route.request; 5 | 6 | const routeDocs = includeFile("@base/route-docs", { config, route, utils }); 7 | const routeNamespace = pascalCase(route.routeName.usage); 8 | 9 | %> 10 | 11 | /** 12 | <%~ routeDocs.description %> 13 | 14 | <%~ routeDocs.lines %> 15 | 16 | */ 17 | export namespace <%~ routeNamespace %> { 18 | export type RequestParams = <%~ (pathParams && pathParams.type) || '{}' %>; 19 | export type RequestQuery = <%~ (query && query.type) || '{}' %>; 20 | export type RequestBody = <%~ (payload && payload.type) || 'never' %>; 21 | export type RequestHeaders = <%~ (headers && headers.type) || '{}' %>; 22 | export type ResponseBody = <%~ route.response.type %>; 23 | } 24 | -------------------------------------------------------------------------------- /templates/base/type-data-contract.ejs: -------------------------------------------------------------------------------- 1 | <% 2 | const { contract, utils } = it; 3 | const { formatDescription, require, _ } = utils; 4 | 5 | %> 6 | <% if (contract.$content.length) { %> 7 | export type <%~ contract.name %> = { 8 | <% for (const field of contract.$content) { %> 9 | <%~ includeFile('@base/object-field-jsdoc.ejs', { ...it, field }) %> 10 | <%~ field.field %>; 11 | <% } %> 12 | }<%~ utils.isNeedToAddNull(contract) ? ' | null' : ''%> 13 | <% } else { %> 14 | export type <%~ contract.name %> = Record; 15 | <% } %> 16 | -------------------------------------------------------------------------------- /templates/default/README.md: -------------------------------------------------------------------------------- 1 | # swagger-typescript-api 2 | 3 | # templates/default 4 | 5 | This templates use for single api file (without `--modular` option) 6 | 7 | path prefix `@default` 8 | -------------------------------------------------------------------------------- /templates/default/api.ejs: -------------------------------------------------------------------------------- 1 | <% 2 | const { apiConfig, routes, utils, config } = it; 3 | const { info, servers, externalDocs } = apiConfig; 4 | const { _, require, formatDescription } = utils; 5 | 6 | const server = (servers && servers[0]) || { url: "" }; 7 | 8 | const descriptionLines = _.compact([ 9 | `@title ${info.title || "No title"}`, 10 | info.version && `@version ${info.version}`, 11 | info.license && `@license ${_.compact([ 12 | info.license.name, 13 | info.license.url && `(${info.license.url})`, 14 | ]).join(" ")}`, 15 | info.termsOfService && `@termsOfService ${info.termsOfService}`, 16 | server.url && `@baseUrl ${server.url}`, 17 | externalDocs.url && `@externalDocs ${externalDocs.url}`, 18 | info.contact && `@contact ${_.compact([ 19 | info.contact.name, 20 | info.contact.email && `<${info.contact.email}>`, 21 | info.contact.url && `(${info.contact.url})`, 22 | ]).join(" ")}`, 23 | info.description && " ", 24 | info.description && _.replace(formatDescription(info.description), /\n/g, "\n * "), 25 | ]); 26 | 27 | %> 28 | 29 | <% if (config.httpClientType === config.constants.HTTP_CLIENT.AXIOS) { %> import type { AxiosRequestConfig, AxiosResponse } from "axios"; <% } %> 30 | 31 | <% if (descriptionLines.length) { %> 32 | /** 33 | <% descriptionLines.forEach((descriptionLine) => { %> 34 | * <%~ descriptionLine %> 35 | 36 | <% }) %> 37 | */ 38 | <% } %> 39 | export class <%~ config.apiClassName %><% if (!config.singleHttpClient) { %> extends HttpClient <% } %> { 40 | 41 | <% if(config.singleHttpClient) { %> 42 | http: HttpClient; 43 | 44 | constructor (http: HttpClient) { 45 | this.http = http; 46 | } 47 | <% } %> 48 | 49 | 50 | <% if (routes.outOfModule) { %> 51 | <% for (const route of routes.outOfModule) { %> 52 | 53 | <%~ includeFile('./procedure-call.ejs', { ...it, route }) %> 54 | 55 | <% } %> 56 | <% } %> 57 | 58 | <% if (routes.combined) { %> 59 | <% for (const { routes: combinedRoutes = [], moduleName } of routes.combined) { %> 60 | <%~ moduleName %> = { 61 | <% for (const route of combinedRoutes) { %> 62 | 63 | <%~ includeFile('./procedure-call.ejs', { ...it, route }) %> 64 | 65 | <% } %> 66 | } 67 | <% } %> 68 | <% } %> 69 | } 70 | -------------------------------------------------------------------------------- /templates/default/procedure-call.ejs: -------------------------------------------------------------------------------- 1 | <% 2 | const { utils, route, config } = it; 3 | const { requestBodyInfo, responseBodyInfo, specificArgNameResolver } = route; 4 | const { _, getInlineParseContent, getParseContent, parseSchema, getComponentByRef, require } = utils; 5 | const { parameters, path, method, payload, query, formData, security, requestParams } = route.request; 6 | const { type, errorType, contentTypes } = route.response; 7 | const { HTTP_CLIENT, RESERVED_REQ_PARAMS_ARG_NAMES } = config.constants; 8 | const routeDocs = includeFile("@base/route-docs", { config, route, utils }); 9 | const queryName = (query && query.name) || "query"; 10 | const pathParams = _.values(parameters); 11 | const pathParamsNames = _.map(pathParams, "name"); 12 | 13 | const isFetchTemplate = config.httpClientType === HTTP_CLIENT.FETCH; 14 | 15 | const requestConfigParam = { 16 | name: specificArgNameResolver.resolve(RESERVED_REQ_PARAMS_ARG_NAMES), 17 | optional: true, 18 | type: "RequestParams", 19 | defaultValue: "{}", 20 | } 21 | 22 | const argToTmpl = ({ name, optional, type, defaultValue }) => `${name}${!defaultValue && optional ? '?' : ''}: ${type}${defaultValue ? ` = ${defaultValue}` : ''}`; 23 | 24 | const rawWrapperArgs = config.extractRequestParams ? 25 | _.compact([ 26 | requestParams && { 27 | name: pathParams.length ? `{ ${_.join(pathParamsNames, ", ")}, ...${queryName} }` : queryName, 28 | optional: false, 29 | type: getInlineParseContent(requestParams), 30 | }, 31 | ...(!requestParams ? pathParams : []), 32 | payload, 33 | requestConfigParam, 34 | ]) : 35 | _.compact([ 36 | ...pathParams, 37 | query, 38 | payload, 39 | requestConfigParam, 40 | ]) 41 | 42 | const wrapperArgs = _ 43 | // Sort by optionality 44 | .sortBy(rawWrapperArgs, [o => o.optional]) 45 | .map(argToTmpl) 46 | .join(', ') 47 | 48 | // RequestParams["type"] 49 | const requestContentKind = { 50 | "JSON": "ContentType.Json", 51 | "JSON_API": "ContentType.JsonApi", 52 | "URL_ENCODED": "ContentType.UrlEncoded", 53 | "FORM_DATA": "ContentType.FormData", 54 | "TEXT": "ContentType.Text", 55 | } 56 | // RequestParams["format"] 57 | const responseContentKind = { 58 | "JSON": '"json"', 59 | "IMAGE": '"blob"', 60 | "FORM_DATA": isFetchTemplate ? '"formData"' : '"document"' 61 | } 62 | 63 | const bodyTmpl = _.get(payload, "name") || null; 64 | const queryTmpl = (query != null && queryName) || null; 65 | const bodyContentKindTmpl = requestContentKind[requestBodyInfo.contentKind] || null; 66 | const responseFormatTmpl = responseContentKind[responseBodyInfo.success && responseBodyInfo.success.schema && responseBodyInfo.success.schema.contentKind] || null; 67 | const securityTmpl = security ? 'true' : null; 68 | 69 | const describeReturnType = () => { 70 | if (!config.toJS) return ""; 71 | 72 | switch(config.httpClientType) { 73 | case HTTP_CLIENT.AXIOS: { 74 | return `Promise>` 75 | } 76 | default: { 77 | return `Promise` 78 | } 79 | } 80 | } 81 | 82 | %> 83 | /** 84 | <%~ routeDocs.description %> 85 | 86 | *<% /* Here you can add some other JSDoc tags */ %> 87 | 88 | <%~ routeDocs.lines %> 89 | 90 | */ 91 | <%~ route.routeName.usage %><%~ route.namespace ? ': ' : ' = ' %>(<%~ wrapperArgs %>)<%~ config.toJS ? `: ${describeReturnType()}` : "" %> => 92 | <%~ config.singleHttpClient ? 'this.http.request' : 'this.request' %><<%~ type %>, <%~ errorType %>>({ 93 | path: `<%~ path %>`, 94 | method: '<%~ _.upperCase(method) %>', 95 | <%~ queryTmpl ? `query: ${queryTmpl},` : '' %> 96 | <%~ bodyTmpl ? `body: ${bodyTmpl},` : '' %> 97 | <%~ securityTmpl ? `secure: ${securityTmpl},` : '' %> 98 | <%~ bodyContentKindTmpl ? `type: ${bodyContentKindTmpl},` : '' %> 99 | <%~ responseFormatTmpl ? `format: ${responseFormatTmpl},` : '' %> 100 | ...<%~ _.get(requestConfigParam, "name") %>, 101 | })<%~ route.namespace ? ',' : '' %> 102 | -------------------------------------------------------------------------------- /templates/default/route-types.ejs: -------------------------------------------------------------------------------- 1 | <% 2 | const { utils, config, routes, modelTypes } = it; 3 | const { _, pascalCase } = utils; 4 | const dataContracts = config.modular ? _.map(modelTypes, "name") : []; 5 | %> 6 | 7 | <% if (dataContracts.length) { %> 8 | import { <%~ dataContracts.join(", ") %> } from "./<%~ config.fileNames.dataContracts %>" 9 | <% } %> 10 | 11 | <% 12 | /* TODO: outOfModule, combined should be attributes of route, which will allow to avoid duplication of code */ 13 | %> 14 | 15 | <% if (routes.outOfModule) { %> 16 | <% for (const { routes: outOfModuleRoutes = [] } of routes.outOfModule) { %> 17 | <% for (const route of outOfModuleRoutes) { %> 18 | <%~ includeFile('@base/route-type.ejs', { ...it, route }) %> 19 | <% } %> 20 | <% } %> 21 | <% } %> 22 | 23 | <% if (routes.combined) { %> 24 | <% for (const { routes: combinedRoutes = [], moduleName } of routes.combined) { %> 25 | export namespace <%~ pascalCase(moduleName) %> { 26 | <% for (const route of combinedRoutes) { %> 27 | <%~ includeFile('@base/route-type.ejs', { ...it, route }) %> 28 | <% } %> 29 | } 30 | 31 | <% } %> 32 | <% } %> 33 | -------------------------------------------------------------------------------- /templates/modular/README.md: -------------------------------------------------------------------------------- 1 | # swagger-typescript-api 2 | 3 | # templates/modular 4 | 5 | This templates use for multiple api files (`--modular` option) 6 | 7 | path prefix `@modular` 8 | -------------------------------------------------------------------------------- /templates/modular/api.ejs: -------------------------------------------------------------------------------- 1 | <% 2 | const { utils, route, config, modelTypes } = it; 3 | const { _, pascalCase, require } = utils; 4 | const apiClassName = pascalCase(route.moduleName); 5 | const routes = route.routes; 6 | const dataContracts = _.map(modelTypes, "name"); 7 | %> 8 | 9 | <% if (config.httpClientType === config.constants.HTTP_CLIENT.AXIOS) { %> import type { AxiosRequestConfig, AxiosResponse } from "axios"; <% } %> 10 | 11 | import { HttpClient, RequestParams, ContentType, HttpResponse } from "./<%~ config.fileNames.httpClient %>"; 12 | <% if (dataContracts.length) { %> 13 | import { <%~ dataContracts.join(", ") %> } from "./<%~ config.fileNames.dataContracts %>" 14 | <% } %> 15 | 16 | export class <%= apiClassName %><% if (!config.singleHttpClient) { %> extends HttpClient <% } %> { 17 | <% if(config.singleHttpClient) { %> 18 | http: HttpClient; 19 | 20 | constructor (http: HttpClient) { 21 | this.http = http; 22 | } 23 | <% } %> 24 | 25 | <% for (const route of routes) { %> 26 | <%~ includeFile('./procedure-call.ejs', { ...it, route }) %> 27 | <% } %> 28 | } 29 | -------------------------------------------------------------------------------- /templates/modular/procedure-call.ejs: -------------------------------------------------------------------------------- 1 | <% 2 | const { utils, route, config } = it; 3 | const { requestBodyInfo, responseBodyInfo, specificArgNameResolver } = route; 4 | const { _, getInlineParseContent, getParseContent, parseSchema, getComponentByRef, require } = utils; 5 | const { parameters, path, method, payload, query, formData, security, requestParams } = route.request; 6 | const { type, errorType, contentTypes } = route.response; 7 | const { HTTP_CLIENT, RESERVED_REQ_PARAMS_ARG_NAMES } = config.constants; 8 | const routeDocs = includeFile("@base/route-docs", { config, route, utils }); 9 | const queryName = (query && query.name) || "query"; 10 | const pathParams = _.values(parameters); 11 | const pathParamsNames = _.map(pathParams, "name"); 12 | 13 | const isFetchTemplate = config.httpClientType === HTTP_CLIENT.FETCH; 14 | 15 | const requestConfigParam = { 16 | name: specificArgNameResolver.resolve(RESERVED_REQ_PARAMS_ARG_NAMES), 17 | optional: true, 18 | type: "RequestParams", 19 | defaultValue: "{}", 20 | } 21 | 22 | const argToTmpl = ({ name, optional, type, defaultValue }) => `${name}${!defaultValue && optional ? '?' : ''}: ${type}${defaultValue ? ` = ${defaultValue}` : ''}`; 23 | 24 | const rawWrapperArgs = config.extractRequestParams ? 25 | _.compact([ 26 | requestParams && { 27 | name: pathParams.length ? `{ ${_.join(pathParamsNames, ", ")}, ...${queryName} }` : queryName, 28 | optional: false, 29 | type: getInlineParseContent(requestParams), 30 | }, 31 | ...(!requestParams ? pathParams : []), 32 | payload, 33 | requestConfigParam, 34 | ]) : 35 | _.compact([ 36 | ...pathParams, 37 | query, 38 | payload, 39 | requestConfigParam, 40 | ]) 41 | 42 | const wrapperArgs = _ 43 | // Sort by optionality 44 | .sortBy(rawWrapperArgs, [o => o.optional]) 45 | .map(argToTmpl) 46 | .join(', ') 47 | 48 | // RequestParams["type"] 49 | const requestContentKind = { 50 | "JSON": "ContentType.Json", 51 | "JSON_API": "ContentType.JsonApi", 52 | "URL_ENCODED": "ContentType.UrlEncoded", 53 | "FORM_DATA": "ContentType.FormData", 54 | "TEXT": "ContentType.Text", 55 | } 56 | // RequestParams["format"] 57 | const responseContentKind = { 58 | "JSON": '"json"', 59 | "IMAGE": '"blob"', 60 | "FORM_DATA": isFetchTemplate ? '"formData"' : '"document"' 61 | } 62 | 63 | const bodyTmpl = _.get(payload, "name") || null; 64 | const queryTmpl = (query != null && queryName) || null; 65 | const bodyContentKindTmpl = requestContentKind[requestBodyInfo.contentKind] || null; 66 | const responseFormatTmpl = responseContentKind[responseBodyInfo.success && responseBodyInfo.success.schema && responseBodyInfo.success.schema.contentKind] || null; 67 | const securityTmpl = security ? 'true' : null; 68 | 69 | const describeReturnType = () => { 70 | if (!config.toJS) return ""; 71 | 72 | switch(config.httpClientType) { 73 | case HTTP_CLIENT.AXIOS: { 74 | return `Promise>` 75 | } 76 | default: { 77 | return `Promise` 78 | } 79 | } 80 | } 81 | 82 | %> 83 | /** 84 | <%~ routeDocs.description %> 85 | 86 | *<% /* Here you can add some other JSDoc tags */ %> 87 | 88 | <%~ routeDocs.lines %> 89 | 90 | */ 91 | <%~ route.routeName.usage %> = (<%~ wrapperArgs %>)<%~ config.toJS ? `: ${describeReturnType()}` : "" %> => 92 | <%~ config.singleHttpClient ? 'this.http.request' : 'this.request' %><<%~ type %>, <%~ errorType %>>({ 93 | path: `<%~ path %>`, 94 | method: '<%~ _.upperCase(method) %>', 95 | <%~ queryTmpl ? `query: ${queryTmpl},` : '' %> 96 | <%~ bodyTmpl ? `body: ${bodyTmpl},` : '' %> 97 | <%~ securityTmpl ? `secure: ${securityTmpl},` : '' %> 98 | <%~ bodyContentKindTmpl ? `type: ${bodyContentKindTmpl},` : '' %> 99 | <%~ responseFormatTmpl ? `format: ${responseFormatTmpl},` : '' %> 100 | ...<%~ _.get(requestConfigParam, "name") %>, 101 | }) 102 | -------------------------------------------------------------------------------- /templates/modular/route-types.ejs: -------------------------------------------------------------------------------- 1 | <% 2 | const { utils, config, route, modelTypes } = it; 3 | const { _, pascalCase } = utils; 4 | const { routes, moduleName } = route; 5 | const dataContracts = config.modular ? _.map(modelTypes, "name") : []; 6 | 7 | %> 8 | <% if (dataContracts.length) { %> 9 | import { <%~ dataContracts.join(", ") %> } from "./<%~ config.fileNames.dataContracts %>" 10 | <% } %> 11 | 12 | export namespace <%~ pascalCase(moduleName) %> { 13 | <% for (const route of routes) { %> 14 | 15 | <%~ includeFile('@base/route-type.ejs', { ...it, route }) %> 16 | 17 | <% } %> 18 | } 19 | -------------------------------------------------------------------------------- /tests/extended.test.ts: -------------------------------------------------------------------------------- 1 | import * as fs from "node:fs/promises"; 2 | import * as os from "node:os"; 3 | import * as path from "node:path"; 4 | 5 | import { afterAll, beforeAll, describe, expect, test } from "vitest"; 6 | 7 | import { generateApi } from "../src/index.js"; 8 | import { collectAllSchemas } from "./utils.js"; 9 | 10 | describe("extended", async () => { 11 | let tmpdir: string; 12 | 13 | beforeAll(async () => { 14 | tmpdir = await fs.mkdtemp(path.join(os.tmpdir(), "swagger-typescript-api")); 15 | }); 16 | 17 | afterAll(async () => { 18 | await fs.rm(tmpdir, { recursive: true }); 19 | }); 20 | 21 | const schemas = await collectAllSchemas(); 22 | 23 | test.each(schemas)("$name", async (schema) => { 24 | await generateApi({ 25 | fileName: schema.name, 26 | input: schema.filePath, 27 | output: tmpdir, 28 | silent: true, 29 | extractEnums: true, 30 | extractRequestBody: true, 31 | extractRequestParams: true, 32 | extractResponseBody: true, 33 | extractResponseError: true, 34 | extractResponses: true, 35 | generateClient: true, 36 | generateRouteTypes: true, 37 | sortRoutes: true, 38 | sortTypes: true, 39 | }); 40 | 41 | const content = await fs.readFile(path.join(tmpdir, `${schema.name}.ts`), { 42 | encoding: "utf8", 43 | }); 44 | 45 | expect(content).toMatchSnapshot(); 46 | }); 47 | }); 48 | -------------------------------------------------------------------------------- /tests/fixtures/schemas/v2.0/another-schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "x-generator": "NSwag v11.14.0.0 (NJsonSchema v9.10.24.0 (Newtonsoft.Json v9.0.0.0))", 3 | "swagger": "2.0", 4 | "info": { 5 | "title": "", 6 | "version": "" 7 | }, 8 | "schemes": [], 9 | "consumes": ["application/json"], 10 | "produces": ["application/json"], 11 | "paths": { 12 | "/api/Foo/GetBarDescriptions": { 13 | "get": { 14 | "tags": ["Foo"], 15 | "operationId": "Foo_GetBarDescriptions", 16 | "parameters": [], 17 | "responses": { 18 | "200": { 19 | "description": "", 20 | "schema": { 21 | "type": "array", 22 | "items": { 23 | "type": "string" 24 | } 25 | }, 26 | "x-nullable": true 27 | } 28 | } 29 | } 30 | }, 31 | "/api/Foo/GetBar": { 32 | "get": { 33 | "tags": ["Foo"], 34 | "operationId": "Foo_GetBar", 35 | "parameters": [ 36 | { 37 | "type": "integer", 38 | "name": "id", 39 | "in": "query", 40 | "required": true, 41 | "x-nullable": false, 42 | "format": "int32" 43 | } 44 | ], 45 | "responses": { 46 | "200": { 47 | "description": "", 48 | "schema": { 49 | "$ref": "#/definitions/Bar" 50 | }, 51 | "x-nullable": true 52 | } 53 | } 54 | } 55 | }, 56 | "/api/Foo/SetBar": { 57 | "post": { 58 | "tags": ["Foo"], 59 | "operationId": "Foo_SetBar", 60 | "parameters": [ 61 | { 62 | "name": "value", 63 | "in": "body", 64 | "required": true, 65 | "schema": { 66 | "$ref": "#/definitions/Bar" 67 | }, 68 | "x-nullable": true 69 | } 70 | ], 71 | "responses": { 72 | "204": { 73 | "description": "" 74 | } 75 | } 76 | } 77 | } 78 | }, 79 | "definitions": { 80 | "Bar": { 81 | "type": "object", 82 | "additionalProperties": false, 83 | "required": ["B", "C"], 84 | "properties": { 85 | "A": { 86 | "type": "string" 87 | }, 88 | "B": { 89 | "type": "integer", 90 | "format": "int32" 91 | }, 92 | "C": { 93 | "type": "string", 94 | "format": "date-time" 95 | }, 96 | "Baz": { 97 | "$ref": "#/definitions/Baz" 98 | } 99 | } 100 | }, 101 | "Baz": { 102 | "type": "object", 103 | "additionalProperties": false, 104 | "required": ["D", "Color"], 105 | "properties": { 106 | "D": { 107 | "type": "number", 108 | "format": "decimal" 109 | }, 110 | "Color": { 111 | "$ref": "#/definitions/Color" 112 | } 113 | } 114 | }, 115 | "Color": { 116 | "type": "integer", 117 | "description": "", 118 | "x-enumNames": ["RED", "GREEN", "BLUE"], 119 | "enum": [0, 1, 2] 120 | } 121 | }, 122 | "parameters": {}, 123 | "responses": {}, 124 | "securityDefinitions": {} 125 | } 126 | -------------------------------------------------------------------------------- /tests/fixtures/schemas/v2.0/enums.json: -------------------------------------------------------------------------------- 1 | { 2 | "swagger": "2.0", 3 | "schemes": ["https"], 4 | "host": "ffff.com", 5 | "basePath": "/", 6 | "info": {}, 7 | "definitions": { 8 | "OnlyEnumNames": { 9 | "x-enumNames": ["Bla", "Blabla", "Boiler"] 10 | }, 11 | "StringOnlyEnumNames": { 12 | "type": "int32", 13 | "x-enumNames": ["Bla", "Blabla", "Boiler"] 14 | }, 15 | "StringEnums": { 16 | "type": "int32", 17 | "enum": ["foo", "bar"], 18 | "x-enumNames": ["Bla", "Blabla", "Boiler"] 19 | }, 20 | "StringCompleteEnums": { 21 | "type": "int32", 22 | "enum": ["foo", "bar", "baz"], 23 | "x-enumNames": ["Bla", "Blabla", "Boiler"] 24 | }, 25 | "EmptyEnum": { 26 | "format": "int32", 27 | "type": "integer", 28 | "x-enumNames": ["Bla", "Blabla", "Boiler"] 29 | }, 30 | "EnumWithMoreNames": { 31 | "format": "int32", 32 | "type": "integer", 33 | "enum": [1], 34 | "x-enumNames": ["Bla", "Blabla", "Boiler"] 35 | }, 36 | "SomeInterestEnum": { 37 | "format": "int32", 38 | "enum": [ 39 | 6, 2, 1, 67, 88, 122, 88, 0, 213, 12378, 123125, 32452, 1111, 66666 40 | ], 41 | "type": "integer", 42 | "x-enumNames": [ 43 | "Bla", 44 | "Blabla", 45 | "Boiler", 46 | "Bbabab", 47 | "Nowadays", 48 | "FAIL", 49 | "Vvvvv", 50 | "ASdasAS", 51 | "ASDsacZX", 52 | "Zook", 53 | "EnumMm", 54 | "VCsa", 55 | "Yuuu", 56 | "ASddd", 57 | "ASdsdsa", 58 | "ASDds", 59 | "HSDFDS" 60 | ] 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /tests/fixtures/schemas/v2.0/file-formdata-example.json: -------------------------------------------------------------------------------- 1 | { 2 | "swagger": "2.0", 3 | "info": { 4 | "version": "v0.1", 5 | "title": "Title" 6 | }, 7 | "paths": { 8 | "/upload-file": { 9 | "post": { 10 | "tags": ["tag"], 11 | "summary": "Upload file", 12 | "operationId": "UploadFile", 13 | "consumes": ["multipart/form-data"], 14 | "produces": ["application/json", "application/vnd.api+json"], 15 | "parameters": [ 16 | { 17 | "name": "file", 18 | "in": "formData", 19 | "description": "File description", 20 | "required": false, 21 | "type": "file" 22 | }, 23 | { 24 | "name": "someFlag", 25 | "in": "formData", 26 | "description": "Boolean flag", 27 | "required": false, 28 | "type": "boolean" 29 | } 30 | ], 31 | "responses": { 32 | "200": { 33 | "description": "Success" 34 | } 35 | } 36 | } 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /tests/fixtures/schemas/v2.0/path-args.yaml: -------------------------------------------------------------------------------- 1 | swagger: "2.0" 2 | info: 3 | version: 1.0.0 4 | title: Path Args 5 | license: 6 | name: MIT 7 | host: unknown.io 8 | basePath: /v666 9 | schemes: 10 | - http 11 | consumes: 12 | - application/json 13 | produces: 14 | - application/json 15 | paths: 16 | /pets/{param1}/{param2}/{param3}: 17 | get: 18 | summary: List all pets 19 | operationId: listPets 20 | tags: 21 | - pets 22 | parameters: 23 | - name: queryParam 24 | in: query 25 | description: How many items to return at one time (max 100) 26 | required: false 27 | type: integer 28 | format: int32 29 | - name: param1 30 | in: path 31 | description: How many items to return at one time (max 100) 32 | required: false 33 | type: integer 34 | format: int32 35 | - name: param2 36 | in: path 37 | description: How many items to return at one time (max 100) 38 | required: false 39 | type: integer 40 | format: int32 41 | - name: param3 42 | in: path 43 | description: How many items to return at one time (max 100) 44 | required: true 45 | type: integer 46 | format: int32 47 | responses: 48 | "200": 49 | description: A paged array of pets 50 | headers: 51 | x-next: 52 | type: string 53 | description: A link to the next page of responses 54 | schema: 55 | type: object 56 | -------------------------------------------------------------------------------- /tests/fixtures/schemas/v2.0/petstore-minimal.json: -------------------------------------------------------------------------------- 1 | { 2 | "swagger": "2.0", 3 | "info": { 4 | "version": "1.0.0", 5 | "title": "Swagger Petstore", 6 | "description": "A sample API that uses a petstore as an example to demonstrate features in the swagger-2.0 specification", 7 | "termsOfService": "http://swagger.io/terms/", 8 | "contact": { 9 | "name": "Swagger API Team" 10 | }, 11 | "license": { 12 | "name": "MIT" 13 | } 14 | }, 15 | "host": "petstore.swagger.io", 16 | "basePath": "/api", 17 | "schemes": ["http"], 18 | "consumes": ["application/json"], 19 | "produces": ["application/json"], 20 | "paths": { 21 | "/pets": { 22 | "get": { 23 | "description": "Returns all pets from the system that the user has access to", 24 | "produces": ["application/json"], 25 | "responses": { 26 | "200": { 27 | "description": "A list of pets.", 28 | "schema": { 29 | "type": "array", 30 | "items": { 31 | "$ref": "#/definitions/Pet" 32 | } 33 | } 34 | } 35 | } 36 | } 37 | } 38 | }, 39 | "definitions": { 40 | "Pet": { 41 | "type": "object", 42 | "required": ["id", "name"], 43 | "properties": { 44 | "id": { 45 | "type": "integer", 46 | "format": "int64" 47 | }, 48 | "name": { 49 | "type": "string" 50 | }, 51 | "tag": { 52 | "type": "string" 53 | }, 54 | "multiple": { 55 | "type": ["string", "number"] 56 | } 57 | } 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /tests/fixtures/schemas/v2.0/petstore-simple.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | swagger: "2.0" 3 | info: 4 | version: "1.0.0" 5 | title: "Swagger Petstore" 6 | description: "A sample API that uses a petstore as an example to demonstrate features in the swagger-2.0 specification" 7 | termsOfService: "http://swagger.io/terms/" 8 | contact: 9 | name: "Swagger API Team" 10 | license: 11 | name: "MIT" 12 | host: "petstore.swagger.io" 13 | basePath: "/api" 14 | schemes: 15 | - "http" 16 | consumes: 17 | - "application/json" 18 | produces: 19 | - "application/json" 20 | paths: 21 | /pets: 22 | get: 23 | description: "Returns all pets from the system that the user has access to" 24 | operationId: "findPets" 25 | produces: 26 | - "application/json" 27 | - "application/xml" 28 | - "text/xml" 29 | - "text/html" 30 | parameters: 31 | - name: "tags" 32 | in: "query" 33 | description: "tags to filter by" 34 | required: false 35 | type: "array" 36 | items: 37 | type: "string" 38 | collectionFormat: "csv" 39 | - name: "limit" 40 | in: "query" 41 | description: "maximum number of results to return" 42 | required: false 43 | type: "integer" 44 | format: "int32" 45 | responses: 46 | "200": 47 | description: "pet response" 48 | schema: 49 | type: "array" 50 | items: 51 | $ref: "#/definitions/Pet" 52 | default: 53 | description: "unexpected error" 54 | schema: 55 | $ref: "#/definitions/ErrorModel" 56 | post: 57 | description: "Creates a new pet in the store. Duplicates are allowed" 58 | operationId: "addPet" 59 | produces: 60 | - "application/json" 61 | parameters: 62 | - name: "pet" 63 | in: "body" 64 | description: "Pet to add to the store" 65 | required: true 66 | schema: 67 | $ref: "#/definitions/NewPet" 68 | responses: 69 | "200": 70 | description: "pet response" 71 | schema: 72 | $ref: "#/definitions/Pet" 73 | default: 74 | description: "unexpected error" 75 | schema: 76 | $ref: "#/definitions/ErrorModel" 77 | /pets/{id}: 78 | get: 79 | description: "Returns a user based on a single ID, if the user does not have access to the pet" 80 | operationId: "findPetById" 81 | produces: 82 | - "application/json" 83 | - "application/xml" 84 | - "text/xml" 85 | - "text/html" 86 | parameters: 87 | - name: "id" 88 | in: "path" 89 | description: "ID of pet to fetch" 90 | required: true 91 | type: "integer" 92 | format: "int64" 93 | responses: 94 | "200": 95 | description: "pet response" 96 | schema: 97 | $ref: "#/definitions/Pet" 98 | default: 99 | description: "unexpected error" 100 | schema: 101 | $ref: "#/definitions/ErrorModel" 102 | delete: 103 | description: "deletes a single pet based on the ID supplied" 104 | operationId: "deletePet" 105 | parameters: 106 | - name: "id" 107 | in: "path" 108 | description: "ID of pet to delete" 109 | required: true 110 | type: "integer" 111 | format: "int64" 112 | responses: 113 | "204": 114 | description: "pet deleted" 115 | default: 116 | description: "unexpected error" 117 | schema: 118 | $ref: "#/definitions/ErrorModel" 119 | definitions: 120 | Pet: 121 | type: "object" 122 | allOf: 123 | - $ref: "#/definitions/NewPet" 124 | - required: 125 | - "id" 126 | properties: 127 | id: 128 | type: "integer" 129 | format: "int64" 130 | Test: 131 | $ref: "#/definitions/NewPet" 132 | description: Description of Test type 133 | Test2: 134 | type: "object" 135 | properties: 136 | data: 137 | $ref: "#/definitions/NewPet" 138 | description: Field description 139 | NewPet: 140 | type: "object" 141 | required: 142 | - "name" 143 | properties: 144 | name: 145 | type: "string" 146 | tag: 147 | type: "string" 148 | ErrorModel: 149 | type: "object" 150 | required: 151 | - "code" 152 | - "message" 153 | properties: 154 | code: 155 | type: "integer" 156 | format: "int32" 157 | message: 158 | type: "string" 159 | -------------------------------------------------------------------------------- /tests/fixtures/schemas/v2.0/petstore.yaml: -------------------------------------------------------------------------------- 1 | swagger: "2.0" 2 | info: 3 | version: 1.0.0 4 | title: Swagger Petstore 5 | license: 6 | name: MIT 7 | host: petstore.swagger.io 8 | basePath: /v1 9 | schemes: 10 | - http 11 | consumes: 12 | - application/json 13 | produces: 14 | - application/json 15 | paths: 16 | /pets: 17 | get: 18 | summary: List all pets 19 | operationId: listPets 20 | tags: 21 | - pets 22 | parameters: 23 | - name: limit 24 | in: query 25 | description: How many items to return at one time (max 100) 26 | required: false 27 | type: integer 28 | format: int32 29 | responses: 30 | "200": 31 | description: A paged array of pets 32 | headers: 33 | x-next: 34 | type: string 35 | description: A link to the next page of responses 36 | schema: 37 | $ref: "#/definitions/Pets" 38 | default: 39 | description: unexpected error 40 | schema: 41 | $ref: "#/definitions/Error" 42 | post: 43 | summary: Create a pet 44 | operationId: createPets 45 | tags: 46 | - pets 47 | responses: 48 | "201": 49 | description: Null response 50 | default: 51 | description: unexpected error 52 | schema: 53 | $ref: "#/definitions/Error" 54 | /pets/{petId}: 55 | get: 56 | summary: Info for a specific pet 57 | operationId: showPetById 58 | tags: 59 | - pets 60 | parameters: 61 | - name: petId 62 | in: path 63 | required: true 64 | description: The id of the pet to retrieve 65 | type: string 66 | responses: 67 | "200": 68 | description: Expected response to a valid request 69 | schema: 70 | $ref: "#/definitions/Pets" 71 | default: 72 | description: unexpected error 73 | schema: 74 | $ref: "#/definitions/Error" 75 | definitions: 76 | Pet: 77 | type: "object" 78 | required: 79 | - id 80 | - name 81 | properties: 82 | id: 83 | type: integer 84 | format: int64 85 | name: 86 | type: string 87 | tag: 88 | type: string 89 | Pets: 90 | type: array 91 | items: 92 | $ref: "#/definitions/Pet" 93 | Error: 94 | type: "object" 95 | required: 96 | - code 97 | - message 98 | properties: 99 | code: 100 | type: integer 101 | format: int32 102 | message: 103 | type: string 104 | -------------------------------------------------------------------------------- /tests/fixtures/schemas/v2.0/query-path-param.yaml: -------------------------------------------------------------------------------- 1 | swagger: "2.0" 2 | info: 3 | version: 1.0.0 4 | title: Query Path Param 5 | license: 6 | name: MIT 7 | host: unknown.io 8 | basePath: /v666 9 | schemes: 10 | - http 11 | consumes: 12 | - application/json 13 | produces: 14 | - application/json 15 | paths: 16 | /foobarbaz/{query}: 17 | get: 18 | summary: List all pets 19 | operationId: listPets 20 | tags: 21 | - pets 22 | parameters: 23 | - name: queryParam 24 | in: query 25 | description: How many items to return at one time (max 100) 26 | required: false 27 | type: integer 28 | format: int32 29 | - name: query 30 | in: path 31 | description: How many items to return at one time (max 100) 32 | required: false 33 | type: integer 34 | format: int32 35 | responses: 36 | "200": 37 | description: A paged array of pets 38 | headers: 39 | x-next: 40 | type: string 41 | description: A link to the next page of responses 42 | schema: 43 | type: object 44 | -------------------------------------------------------------------------------- /tests/fixtures/schemas/v3.0/additional-properties.yaml: -------------------------------------------------------------------------------- 1 | openapi: "3.0.0" 2 | info: 3 | title: Additional properties Example 4 | version: 1.0.0 5 | components: 6 | schemas: 7 | Messages: # <---- dictionary 8 | type: object 9 | additionalProperties: 10 | $ref: "#/components/schemas/Message" 11 | Message: 12 | type: object 13 | properties: 14 | code: 15 | type: integer 16 | text: 17 | type: string 18 | -------------------------------------------------------------------------------- /tests/fixtures/schemas/v3.0/additional-properties2.json: -------------------------------------------------------------------------------- 1 | { 2 | "info": { "title": "" }, 3 | "openapi": "3.0.0", 4 | "components": { 5 | "schemas": { 6 | "Primitive": { 7 | "anyOf": [ 8 | { 9 | "type": "string" 10 | }, 11 | { 12 | "type": "number", 13 | "format": "double" 14 | }, 15 | { 16 | "type": "boolean" 17 | }, 18 | { 19 | "type": "number", 20 | "enum": [null], 21 | "nullable": true 22 | } 23 | ] 24 | }, 25 | "PrimitiveMap": { 26 | "properties": {}, 27 | "type": "object", 28 | "additionalProperties": { 29 | "$ref": "#/components/schemas/Primitive" 30 | } 31 | } 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /tests/fixtures/schemas/v3.0/allof-example.yaml: -------------------------------------------------------------------------------- 1 | openapi: "3.0.0" 2 | info: 3 | title: Allof Example 4 | version: 1.0.0 5 | paths: 6 | /pets: 7 | patch: 8 | requestBody: 9 | content: 10 | application/json: 11 | schema: 12 | oneOf: 13 | - $ref: "#/components/schemas/Cat" 14 | - $ref: "#/components/schemas/Dog" 15 | discriminator: 16 | propertyName: pet_type 17 | responses: 18 | "200": 19 | description: Updated 20 | components: 21 | schemas: 22 | Pet: 23 | type: object 24 | required: 25 | - pet_type 26 | properties: 27 | pet_type: 28 | type: string 29 | discriminator: 30 | propertyName: pet_type 31 | Dog: # "Dog" is a value for the pet_type property (the discriminator value) 32 | required: 33 | - breed 34 | allOf: # Combines the main `Pet` schema with `Dog`-specific properties 35 | - $ref: "#/components/schemas/Pet" 36 | - type: object 37 | # all other properties specific to a `Dog` 38 | properties: 39 | bark: 40 | type: boolean 41 | breed: 42 | type: string 43 | enum: [Dingo, Husky, Retriever, Shepherd] 44 | Cat: # "Cat" is a value for the pet_type property (the discriminator value) 45 | allOf: # Combines the main `Pet` schema with `Cat`-specific properties 46 | - $ref: "#/components/schemas/Pet" 47 | - type: object 48 | # all other properties specific to a `Cat` 49 | properties: 50 | hunts: 51 | type: boolean 52 | age: 53 | type: integer 54 | -------------------------------------------------------------------------------- /tests/fixtures/schemas/v3.0/anyof-example.yaml: -------------------------------------------------------------------------------- 1 | openapi: "3.0.0" 2 | info: 3 | title: Anyof Example 4 | version: 1.0.0 5 | paths: 6 | /pets: 7 | patch: 8 | requestBody: 9 | content: 10 | application/json: 11 | schema: 12 | anyOf: 13 | - $ref: "#/components/schemas/PetByAge" 14 | - $ref: "#/components/schemas/PetByType" 15 | responses: 16 | "200": 17 | description: Updated 18 | components: 19 | schemas: 20 | PetByAge: 21 | type: object 22 | properties: 23 | age: 24 | type: integer 25 | nickname: 26 | type: string 27 | required: 28 | - age 29 | 30 | PetByType: 31 | type: object 32 | properties: 33 | pet_type: 34 | type: string 35 | enum: [Cat, Dog] 36 | hunts: 37 | type: boolean 38 | required: 39 | - pet_type 40 | -------------------------------------------------------------------------------- /tests/fixtures/schemas/v3.0/callback-example.yaml: -------------------------------------------------------------------------------- 1 | openapi: 3.0.0 2 | info: 3 | title: Callback Example 4 | version: 1.0.0 5 | paths: 6 | /streams: 7 | post: 8 | description: subscribes a client to receive out-of-band data 9 | parameters: 10 | - name: callbackUrl 11 | in: query 12 | required: true 13 | description: | 14 | the location where data will be sent. Must be network accessible 15 | by the source server 16 | schema: 17 | type: string 18 | format: uri 19 | example: https://tonys-server.com 20 | responses: 21 | "201": 22 | description: subscription successfully created 23 | content: 24 | application/json: 25 | schema: 26 | description: subscription information 27 | required: 28 | - subscriptionId 29 | properties: 30 | subscriptionId: 31 | description: this unique identifier allows management of the subscription 32 | type: string 33 | example: 2531329f-fb09-4ef7-887e-84e648214436 34 | callbacks: 35 | # the name `onData` is a convenience locator 36 | onData: 37 | # when data is sent, it will be sent to the `callbackUrl` provided 38 | # when making the subscription PLUS the suffix `/data` 39 | "{$request.query.callbackUrl}/data": 40 | post: 41 | requestBody: 42 | description: subscription payload 43 | content: 44 | application/json: 45 | schema: 46 | type: object 47 | properties: 48 | timestamp: 49 | type: string 50 | format: date-time 51 | userData: 52 | type: string 53 | responses: 54 | "202": 55 | description: | 56 | Your server implementation should return this HTTP status code 57 | if the data was received successfully 58 | "204": 59 | description: | 60 | Your server should return this HTTP status code if no longer interested 61 | in further updates 62 | -------------------------------------------------------------------------------- /tests/fixtures/schemas/v3.0/components-responses.yaml: -------------------------------------------------------------------------------- 1 | openapi: "3.0.0" 2 | 3 | info: 4 | description: "Description" 5 | version: "latest" 6 | title: "Title" 7 | 8 | paths: 9 | "/api": 10 | get: 11 | operationId: getData 12 | responses: 13 | 200: 14 | $ref: "#/components/responses/default" 15 | 16 | components: 17 | responses: 18 | default: 19 | description: OK 20 | content: 21 | "application/json": 22 | schema: 23 | type: object 24 | properties: 25 | data: { type: string } 26 | examples: | 27 | Lorem ipsum de foo bar 28 | -------------------------------------------------------------------------------- /tests/fixtures/schemas/v3.0/explode-param-3.yaml: -------------------------------------------------------------------------------- 1 | openapi: 3.0.1 2 | info: 3 | title: API 4 | description: Documentation 5 | version: "0.1" 6 | paths: 7 | "/{user}/foos": 8 | parameters: 9 | - schema: 10 | type: string 11 | user: blbalbla 12 | in: path 13 | required: true 14 | post: 15 | summary: Some summary 16 | operationId: createFile 17 | responses: 18 | "200": 19 | description: OK 20 | content: 21 | application/json: 22 | schema: 23 | $ref: "#/components/schemas/Floop" 24 | requestBody: 25 | content: 26 | application/json: 27 | schema: 28 | type: object 29 | properties: 30 | meme: 31 | type: string 32 | default: "" 33 | memeType: 34 | type: string 35 | required: 36 | - meme 37 | required: true 38 | description: "" 39 | /something/: 40 | get: 41 | operationId: gets 42 | parameters: 43 | - name: params 44 | in: query 45 | required: false 46 | explode: true 47 | schema: 48 | $ref: "#/components/schemas/QueryParams" 49 | 50 | components: 51 | schemas: 52 | Floop: 53 | type: object 54 | properties: 55 | info: 56 | type: string 57 | QueryParams: 58 | type: object 59 | properties: 60 | page: 61 | minimum: 0 62 | type: integer 63 | description: Page number 64 | format: int32 65 | nullable: true 66 | page-size: 67 | minimum: 0 68 | type: integer 69 | description: Page size 70 | format: int32 71 | nullable: true 72 | -------------------------------------------------------------------------------- /tests/fixtures/schemas/v3.0/no-definitions-schema.yaml: -------------------------------------------------------------------------------- 1 | openapi: "3.0.0" 2 | info: 3 | title: Empty schema example 4 | version: 1.0.0 5 | components: 6 | schemas: 7 | BasicErrorModel: 8 | type: object 9 | required: 10 | - message 11 | - code 12 | properties: 13 | message: 14 | type: string 15 | field: 16 | type: string 17 | x-nullable: true 18 | code: 19 | type: integer 20 | minimum: 100 21 | maximum: 600 22 | ExtendedErrorModel: 23 | allOf: # Combines the BasicErrorModel and the inline model 24 | - $ref: "#/components/schemas/BasicErrorModel" 25 | - type: object 26 | required: 27 | - rootCause 28 | properties: 29 | rootCause: 30 | type: string 31 | -------------------------------------------------------------------------------- /tests/fixtures/schemas/v3.0/oneof-example.yaml: -------------------------------------------------------------------------------- 1 | openapi: "3.0.0" 2 | info: 3 | title: Oneof Example 4 | version: 1.0.0 5 | paths: 6 | /pets: 7 | patch: 8 | requestBody: 9 | content: 10 | application/json: 11 | schema: 12 | oneOf: 13 | - $ref: "#/components/schemas/Cat" 14 | - $ref: "#/components/schemas/Dog" 15 | responses: 16 | "200": 17 | description: Updated 18 | components: 19 | schemas: 20 | Dog: 21 | type: object 22 | properties: 23 | bark: 24 | type: boolean 25 | breed: 26 | type: string 27 | enum: [Dingo, Husky, Retriever, Shepherd] 28 | Cat: 29 | type: object 30 | properties: 31 | hunts: 32 | type: boolean 33 | age: 34 | type: integer 35 | -------------------------------------------------------------------------------- /tests/fixtures/schemas/v3.0/petstore.yaml: -------------------------------------------------------------------------------- 1 | openapi: "3.0.0" 2 | info: 3 | version: 1.0.0 4 | title: Swagger Petstore 5 | license: 6 | name: MIT 7 | servers: 8 | - url: http://petstore.swagger.io/v1 9 | paths: 10 | /pets: 11 | get: 12 | summary: List all pets 13 | operationId: listPets 14 | tags: 15 | - pets 16 | parameters: 17 | - name: limit 18 | in: query 19 | description: How many items to return at one time (max 100) 20 | required: false 21 | schema: 22 | type: integer 23 | format: int32 24 | responses: 25 | "200": 26 | description: A paged array of pets 27 | headers: 28 | x-next: 29 | description: A link to the next page of responses 30 | schema: 31 | type: string 32 | content: 33 | application/json: 34 | schema: 35 | $ref: "#/components/schemas/Pets" 36 | default: 37 | description: unexpected error 38 | content: 39 | application/json: 40 | schema: 41 | $ref: "#/components/schemas/Error" 42 | post: 43 | summary: Create a pet 44 | operationId: createPets 45 | tags: 46 | - pets 47 | responses: 48 | "201": 49 | description: Null response 50 | default: 51 | description: unexpected error 52 | content: 53 | application/json: 54 | schema: 55 | $ref: "#/components/schemas/Error" 56 | /pets/{petId}: 57 | get: 58 | summary: Info for a specific pet 59 | operationId: showPetById 60 | tags: 61 | - pets 62 | parameters: 63 | - name: petId 64 | in: path 65 | required: true 66 | description: The id of the pet to retrieve 67 | schema: 68 | type: string 69 | responses: 70 | "200": 71 | description: Expected response to a valid request 72 | content: 73 | application/json: 74 | schema: 75 | $ref: "#/components/schemas/Pet" 76 | default: 77 | description: unexpected error 78 | content: 79 | application/json: 80 | schema: 81 | $ref: "#/components/schemas/Error" 82 | components: 83 | schemas: 84 | StringNullable: 85 | type: string 86 | nullable: true 87 | Pet: 88 | type: object 89 | required: 90 | - id 91 | - name 92 | properties: 93 | id: 94 | type: integer 95 | format: int64 96 | name: 97 | type: string 98 | tag: 99 | type: string 100 | Pets: 101 | type: array 102 | items: 103 | $ref: "#/components/schemas/Pet" 104 | Error: 105 | type: object 106 | required: 107 | - code 108 | - message 109 | properties: 110 | code: 111 | type: integer 112 | format: int32 113 | message: 114 | type: string 115 | -------------------------------------------------------------------------------- /tests/fixtures/schemas/v3.0/recursive-schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "components": { 3 | "examples": {}, 4 | "headers": {}, 5 | "parameters": {}, 6 | "requestBodies": {}, 7 | "responses": {}, 8 | "schemas": { 9 | "recursive-object": { 10 | "type": "object", 11 | "description": "RECURSIVE", 12 | "properties": { 13 | "id": { 14 | "description": "Unique identifier of the GitHub app", 15 | "example": 37, 16 | "type": "integer" 17 | }, 18 | "bar": { 19 | "$ref": "#/components/schemas/recursive-object" 20 | } 21 | } 22 | } 23 | }, 24 | "securitySchemes": {} 25 | }, 26 | "info": { 27 | "title": "" 28 | }, 29 | "openapi": "3.0.0", 30 | "paths": {}, 31 | "servers": [] 32 | } 33 | -------------------------------------------------------------------------------- /tests/fixtures/schemas/v3.0/responses.yaml: -------------------------------------------------------------------------------- 1 | openapi: "3.0.0" 2 | 3 | info: 4 | description: "Description" 5 | version: "latest" 6 | title: "Title" 7 | 8 | paths: 9 | "/api": 10 | get: 11 | operationId: getData 12 | responses: 13 | 200: 14 | description: OK 15 | content: 16 | "application/json": 17 | schema: 18 | type: object 19 | properties: 20 | data: { type: string } 21 | -------------------------------------------------------------------------------- /tests/fixtures/schemas/v3.0/swaggerhub-template.yaml: -------------------------------------------------------------------------------- 1 | openapi: 3.0.0 2 | # Added by API Auto Mocking Plugin 3 | servers: 4 | - description: SwaggerHub API Auto Mocking 5 | url: https://virtserver.swaggerhub.com/sdfsdfsffs/sdfff/1.0.0 6 | info: 7 | version: "1.0.0" 8 | title: Sample Application Flow OAuth2 Project 9 | description: >- 10 | This is an example of using OAuth2 Application Flow in a specification to 11 | describe security to your API. 12 | security: 13 | - application: 14 | - read 15 | - write 16 | paths: 17 | /example: 18 | get: 19 | summary: Server example operation 20 | description: >- 21 | This is an example operation to show how security is applied to the 22 | call. 23 | responses: 24 | "200": 25 | description: OK 26 | /ping: 27 | get: 28 | summary: Server heartbeat operation 29 | description: >- 30 | This operation shows how to override the global security defined above, 31 | as we want to open it up for all users. 32 | security: [] 33 | responses: 34 | "200": 35 | description: OK 36 | components: 37 | schemas: {} 38 | securitySchemes: 39 | application: 40 | type: oauth2 41 | flows: 42 | clientCredentials: 43 | tokenUrl: "http://example.com/oauth/token" 44 | scopes: 45 | write: allows modifying resources 46 | read: allows reading resources 47 | -------------------------------------------------------------------------------- /tests/fixtures/schemas/v3.0/wrong-enum-subtypes.yaml: -------------------------------------------------------------------------------- 1 | openapi: 3.0.1 2 | info: 3 | title: Test 4 | version: test 5 | paths: {} 6 | components: 7 | schemas: 8 | Test: 9 | type: object 10 | allOf: 11 | - type: object 12 | properties: 13 | x: 14 | type: array 15 | items: 16 | type: string 17 | enum: 18 | - A-B 19 | - type: object 20 | properties: 21 | y: 22 | type: string 23 | -------------------------------------------------------------------------------- /tests/simple.test.ts: -------------------------------------------------------------------------------- 1 | import * as fs from "node:fs/promises"; 2 | import * as os from "node:os"; 3 | import * as path from "node:path"; 4 | 5 | import { afterAll, beforeAll, describe, expect, test } from "vitest"; 6 | 7 | import { generateApi } from "../src/index.js"; 8 | import { collectAllSchemas } from "./utils.js"; 9 | 10 | describe("simple", async () => { 11 | let tmpdir: string; 12 | 13 | beforeAll(async () => { 14 | tmpdir = await fs.mkdtemp(path.join(os.tmpdir(), "swagger-typescript-api")); 15 | }); 16 | 17 | afterAll(async () => { 18 | await fs.rm(tmpdir, { recursive: true }); 19 | }); 20 | 21 | const schemas = await collectAllSchemas(); 22 | 23 | test.each(schemas)("$name", async (schema) => { 24 | await generateApi({ 25 | fileName: schema.name, 26 | input: schema.filePath, 27 | output: tmpdir, 28 | silent: true, 29 | generateClient: true, 30 | generateRouteTypes: false, 31 | sortTypes: true, 32 | }); 33 | 34 | const content = await fs.readFile(path.join(tmpdir, `${schema.name}.ts`), { 35 | encoding: "utf8", 36 | }); 37 | 38 | expect(content).toMatchSnapshot(); 39 | }); 40 | }); 41 | -------------------------------------------------------------------------------- /tests/spec/additional-properties-2.0/__snapshots__/basic.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[`basic > additional properties 2.0 1`] = ` 4 | "/* eslint-disable */ 5 | /* tslint:disable */ 6 | // @ts-nocheck 7 | /* 8 | * --------------------------------------------------------------- 9 | * ## THIS FILE WAS GENERATED VIA SWAGGER-TYPESCRIPT-API ## 10 | * ## ## 11 | * ## AUTHOR: acacode ## 12 | * ## SOURCE: https://github.com/acacode/swagger-typescript-api ## 13 | * --------------------------------------------------------------- 14 | */ 15 | 16 | export interface MyObject1 { 17 | id?: string; 18 | [key: string]: any; 19 | } 20 | 21 | export interface MyObject2 { 22 | id?: string; 23 | [key: string]: any; 24 | } 25 | 26 | export interface MyObject3 { 27 | id?: string; 28 | [key: string]: any; 29 | } 30 | 31 | export type MyObject4 = Record< 32 | string, 33 | { 34 | content?: string; 35 | filename?: string | null; 36 | } 37 | >; 38 | " 39 | `; 40 | -------------------------------------------------------------------------------- /tests/spec/additional-properties-2.0/basic.test.ts: -------------------------------------------------------------------------------- 1 | import * as fs from "node:fs/promises"; 2 | import * as os from "node:os"; 3 | import * as path from "node:path"; 4 | 5 | import { afterAll, beforeAll, describe, expect, test } from "vitest"; 6 | 7 | import { generateApi } from "../../../src/index.js"; 8 | 9 | describe("basic", async () => { 10 | let tmpdir = ""; 11 | 12 | beforeAll(async () => { 13 | tmpdir = await fs.mkdtemp(path.join(os.tmpdir(), "swagger-typescript-api")); 14 | }); 15 | 16 | afterAll(async () => { 17 | await fs.rm(tmpdir, { recursive: true }); 18 | }); 19 | 20 | test("additional properties 2.0", async () => { 21 | await generateApi({ 22 | fileName: "schema", 23 | input: path.resolve(import.meta.dirname, "schema.json"), 24 | output: tmpdir, 25 | silent: true, 26 | addReadonly: true, 27 | generateClient: false, 28 | }); 29 | 30 | const content = await fs.readFile(path.join(tmpdir, "schema.ts"), { 31 | encoding: "utf8", 32 | }); 33 | 34 | expect(content).toMatchSnapshot(); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /tests/spec/additional-properties-2.0/schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "swagger": "2.0", 3 | "schemes": ["https"], 4 | "host": "ffff.com", 5 | "basePath": "/", 6 | "info": {}, 7 | "definitions": { 8 | "MyObject1": { 9 | "type": "object", 10 | "properties": { 11 | "id": { 12 | "type": "string" 13 | } 14 | }, 15 | "additionalProperties": { 16 | "type": "object" 17 | } 18 | }, 19 | "MyObject2": { 20 | "type": "object", 21 | "properties": { 22 | "id": { 23 | "type": "string" 24 | } 25 | }, 26 | "additionalProperties": {} 27 | }, 28 | "MyObject3": { 29 | "type": "object", 30 | "properties": { 31 | "id": { 32 | "type": "string" 33 | } 34 | }, 35 | "additionalProperties": true 36 | }, 37 | "MyObject4": { 38 | "type": "object", 39 | "additionalProperties": { 40 | "type": "object", 41 | "nullable": true, 42 | "properties": { 43 | "content": { 44 | "type": "string" 45 | }, 46 | "filename": { 47 | "type": "string", 48 | "nullable": true 49 | } 50 | } 51 | } 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /tests/spec/another-array-type/__snapshots__/basic.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[`basic > --another-array-type 1`] = ` 4 | "/* eslint-disable */ 5 | /* tslint:disable */ 6 | // @ts-nocheck 7 | /* 8 | * --------------------------------------------------------------- 9 | * ## THIS FILE WAS GENERATED VIA SWAGGER-TYPESCRIPT-API ## 10 | * ## ## 11 | * ## AUTHOR: acacode ## 12 | * ## SOURCE: https://github.com/acacode/swagger-typescript-api ## 13 | * --------------------------------------------------------------- 14 | */ 15 | 16 | export type AnotherArrayType1 = Array; 17 | 18 | export type AnotherArrayType2 = Array; 19 | " 20 | `; 21 | -------------------------------------------------------------------------------- /tests/spec/another-array-type/basic.test.ts: -------------------------------------------------------------------------------- 1 | import * as fs from "node:fs/promises"; 2 | import * as os from "node:os"; 3 | import * as path from "node:path"; 4 | 5 | import { afterAll, beforeAll, describe, expect, test } from "vitest"; 6 | 7 | import { generateApi } from "../../../src/index.js"; 8 | 9 | describe("basic", async () => { 10 | let tmpdir = ""; 11 | 12 | beforeAll(async () => { 13 | tmpdir = await fs.mkdtemp(path.join(os.tmpdir(), "swagger-typescript-api")); 14 | }); 15 | 16 | afterAll(async () => { 17 | await fs.rm(tmpdir, { recursive: true }); 18 | }); 19 | 20 | test("--another-array-type", async () => { 21 | await generateApi({ 22 | fileName: "schema", 23 | input: path.resolve(import.meta.dirname, "schema.json"), 24 | output: tmpdir, 25 | silent: true, 26 | anotherArrayType: true, 27 | generateClient: false, 28 | }); 29 | 30 | const content = await fs.readFile(path.join(tmpdir, "schema.ts"), { 31 | encoding: "utf8", 32 | }); 33 | 34 | expect(content).toMatchSnapshot(); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /tests/spec/another-array-type/schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "swagger": "2.0", 3 | "info": {}, 4 | "parameters": {}, 5 | "responses": {}, 6 | "paths": {}, 7 | "definitions": { 8 | "AnotherArrayType1": { 9 | "type": "array", 10 | "items": [ 11 | { 12 | "type": "string" 13 | } 14 | ] 15 | }, 16 | "AnotherArrayType2": { 17 | "items": [ 18 | { 19 | "type": "string" 20 | } 21 | ] 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /tests/spec/another-query-params/basic.test.ts: -------------------------------------------------------------------------------- 1 | import * as fs from "node:fs/promises"; 2 | import * as os from "node:os"; 3 | import * as path from "node:path"; 4 | 5 | import { afterAll, beforeAll, describe, expect, test } from "vitest"; 6 | 7 | import { generateApi } from "../../../src/index.js"; 8 | 9 | describe("basic", async () => { 10 | let tmpdir = ""; 11 | 12 | beforeAll(async () => { 13 | tmpdir = await fs.mkdtemp(path.join(os.tmpdir(), "swagger-typescript-api")); 14 | }); 15 | 16 | afterAll(async () => { 17 | await fs.rm(tmpdir, { recursive: true }); 18 | }); 19 | 20 | test("another-query-params", async () => { 21 | await generateApi({ 22 | fileName: "schema", 23 | input: path.resolve(import.meta.dirname, "schema.json"), 24 | output: tmpdir, 25 | silent: true, 26 | generateClient: true, 27 | }); 28 | 29 | const content = await fs.readFile(path.join(tmpdir, "schema.ts"), { 30 | encoding: "utf8", 31 | }); 32 | 33 | expect(content).toMatchSnapshot(); 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /tests/spec/axios/basic.test.ts: -------------------------------------------------------------------------------- 1 | import * as fs from "node:fs/promises"; 2 | import * as os from "node:os"; 3 | import * as path from "node:path"; 4 | 5 | import { afterAll, beforeAll, describe, expect, test } from "vitest"; 6 | 7 | import { generateApi } from "../../../src/index.js"; 8 | 9 | describe("basic", async () => { 10 | let tmpdir = ""; 11 | 12 | beforeAll(async () => { 13 | tmpdir = await fs.mkdtemp(path.join(os.tmpdir(), "swagger-typescript-api")); 14 | }); 15 | 16 | afterAll(async () => { 17 | await fs.rm(tmpdir, { recursive: true }); 18 | }); 19 | 20 | test("--axios option", async () => { 21 | await generateApi({ 22 | fileName: "schema", 23 | input: path.resolve(import.meta.dirname, "schema.json"), 24 | output: tmpdir, 25 | silent: true, 26 | generateClient: true, 27 | httpClientType: "axios", 28 | }); 29 | 30 | const content = await fs.readFile(path.join(tmpdir, "schema.ts"), { 31 | encoding: "utf8", 32 | }); 33 | 34 | expect(content).toMatchSnapshot(); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /tests/spec/axiosSingleHttpClient/basic.test.ts: -------------------------------------------------------------------------------- 1 | import * as fs from "node:fs/promises"; 2 | import * as os from "node:os"; 3 | import * as path from "node:path"; 4 | 5 | import { afterAll, beforeAll, describe, expect, test } from "vitest"; 6 | 7 | import { generateApi } from "../../../src/index.js"; 8 | 9 | describe("basic", async () => { 10 | let tmpdir = ""; 11 | 12 | beforeAll(async () => { 13 | tmpdir = await fs.mkdtemp(path.join(os.tmpdir(), "swagger-typescript-api")); 14 | }); 15 | 16 | afterAll(async () => { 17 | await fs.rm(tmpdir, { recursive: true }); 18 | }); 19 | 20 | test("--axios --single-http-client", async () => { 21 | await generateApi({ 22 | fileName: "schema", 23 | input: path.resolve(import.meta.dirname, "schema.json"), 24 | output: tmpdir, 25 | silent: true, 26 | generateClient: true, 27 | httpClientType: "axios", 28 | singleHttpClient: true, 29 | }); 30 | 31 | const content = await fs.readFile(path.join(tmpdir, "schema.ts"), { 32 | encoding: "utf8", 33 | }); 34 | 35 | expect(content).toMatchSnapshot(); 36 | }); 37 | }); 38 | -------------------------------------------------------------------------------- /tests/spec/const-keyword/__snapshots__/basic.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[`basic > const-keyword 1`] = ` 4 | "/* eslint-disable */ 5 | /* tslint:disable */ 6 | // @ts-nocheck 7 | /* 8 | * --------------------------------------------------------------- 9 | * ## THIS FILE WAS GENERATED VIA SWAGGER-TYPESCRIPT-API ## 10 | * ## ## 11 | * ## AUTHOR: acacode ## 12 | * ## SOURCE: https://github.com/acacode/swagger-typescript-api ## 13 | * --------------------------------------------------------------- 14 | */ 15 | 16 | export interface ObjTest { 17 | /** 18 | * title 19 | * description 20 | * @default "main" 21 | */ 22 | page_type?: "main"; 23 | /** 24 | * title 25 | * description 26 | */ 27 | page_type_nullable?: "main" | null; 28 | } 29 | 30 | /** 31 | * title 32 | * description 33 | * @default "string" 34 | */ 35 | export type TestString = "string"; 36 | 37 | /** 38 | * title 39 | * description 40 | */ 41 | export type TestStringNullable = "string" | null; 42 | 43 | /** 44 | * title 45 | * description 46 | */ 47 | export type TestBooleanNullable = false | null; 48 | 49 | /** 50 | * title 51 | * description 52 | * @default false 53 | */ 54 | export type TestBooleanFalse = false; 55 | 56 | /** 57 | * title 58 | * description 59 | * @default true 60 | */ 61 | export type TestBooleanTrue = true; 62 | 63 | /** 64 | * title 65 | * description 66 | * @default 5 67 | */ 68 | export type TestNumber5 = 5; 69 | 70 | /** 71 | * title 72 | * description 73 | */ 74 | export type TestNumberNullable = 666 | null; 75 | 76 | /** 77 | * title 78 | * description 79 | * @default 0 80 | */ 81 | export type TestNumber0 = 0; 82 | 83 | /** 84 | * title 85 | * description 86 | * @default 10 87 | */ 88 | export type TestNumber10 = 10; 89 | 90 | /** 91 | * title 92 | * description 93 | * @default null 94 | */ 95 | export type TestNull = null; 96 | " 97 | `; 98 | -------------------------------------------------------------------------------- /tests/spec/const-keyword/basic.test.ts: -------------------------------------------------------------------------------- 1 | import * as fs from "node:fs/promises"; 2 | import * as os from "node:os"; 3 | import * as path from "node:path"; 4 | 5 | import { afterAll, beforeAll, describe, expect, test } from "vitest"; 6 | 7 | import { generateApi } from "../../../src/index.js"; 8 | 9 | describe("basic", async () => { 10 | let tmpdir = ""; 11 | 12 | beforeAll(async () => { 13 | tmpdir = await fs.mkdtemp(path.join(os.tmpdir(), "swagger-typescript-api")); 14 | }); 15 | 16 | afterAll(async () => { 17 | await fs.rm(tmpdir, { recursive: true }); 18 | }); 19 | 20 | test("const-keyword", async () => { 21 | await generateApi({ 22 | fileName: "schema", 23 | input: path.resolve(import.meta.dirname, "schema.json"), 24 | output: tmpdir, 25 | silent: true, 26 | addReadonly: true, 27 | generateClient: false, 28 | }); 29 | 30 | const content = await fs.readFile(path.join(tmpdir, "schema.ts"), { 31 | encoding: "utf8", 32 | }); 33 | 34 | expect(content).toMatchSnapshot(); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /tests/spec/const-keyword/schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "openapi": "3.0.1", 3 | "info": { 4 | "title": "Nullable Refs Example", 5 | "version": "1.0.0" 6 | }, 7 | "components": { 8 | "schemas": { 9 | "obj-test": { 10 | "type": "object", 11 | "properties": { 12 | "page_type": { 13 | "const": "main", 14 | "default": "main", 15 | "description": "description", 16 | "title": "title" 17 | }, 18 | "page_type_nullable": { 19 | "const": "main", 20 | "description": "description", 21 | "title": "title", 22 | "nullable": true 23 | } 24 | } 25 | }, 26 | "test-string": { 27 | "const": "string", 28 | "default": "string", 29 | "description": "description", 30 | "title": "title" 31 | }, 32 | "test-string-nullable": { 33 | "const": "string", 34 | "description": "description", 35 | "title": "title", 36 | "nullable": true 37 | }, 38 | "test-boolean-nullable": { 39 | "const": false, 40 | "description": "description", 41 | "title": "title", 42 | "nullable": true 43 | }, 44 | "test-boolean-false": { 45 | "const": false, 46 | "default": false, 47 | "description": "description", 48 | "title": "title" 49 | }, 50 | "test-boolean-true": { 51 | "const": true, 52 | "default": true, 53 | "description": "description", 54 | "title": "title" 55 | }, 56 | "test-number-5": { 57 | "const": 5, 58 | "default": 5, 59 | "description": "description", 60 | "title": "title" 61 | }, 62 | "test-number-nullable": { 63 | "const": 666, 64 | "description": "description", 65 | "title": "title", 66 | "nullable": true 67 | }, 68 | "test-number-0": { 69 | "const": 0, 70 | "default": 0, 71 | "description": "description", 72 | "title": "title" 73 | }, 74 | "test-number-10": { 75 | "const": 10, 76 | "default": 10, 77 | "description": "description", 78 | "title": "title" 79 | }, 80 | "test-null": { 81 | "const": null, 82 | "default": null, 83 | "description": "description", 84 | "title": "title" 85 | } 86 | } 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /tests/spec/custom-extensions/basic.test.ts: -------------------------------------------------------------------------------- 1 | import * as fs from "node:fs/promises"; 2 | import * as os from "node:os"; 3 | import * as path from "node:path"; 4 | 5 | import { afterAll, beforeAll, describe, expect, test } from "vitest"; 6 | 7 | import { generateApi } from "../../../src/index.js"; 8 | 9 | describe("basic", async () => { 10 | let tmpdir = ""; 11 | 12 | beforeAll(async () => { 13 | tmpdir = await fs.mkdtemp(path.join(os.tmpdir(), "swagger-typescript-api")); 14 | }); 15 | 16 | afterAll(async () => { 17 | await fs.rm(tmpdir, { recursive: true }); 18 | }); 19 | 20 | test("custom extensions", async () => { 21 | await generateApi({ 22 | fileName: "schema", 23 | input: path.resolve(import.meta.dirname, "schema.json"), 24 | output: tmpdir, 25 | silent: true, 26 | }); 27 | 28 | const content = await fs.readFile(path.join(tmpdir, "schema.ts"), { 29 | encoding: "utf8", 30 | }); 31 | 32 | expect(content).toMatchSnapshot(); 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /tests/spec/custom-extensions/schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "swagger": "2.0", 3 | "schemes": ["https"], 4 | "host": "example.com", 5 | "basePath": "/", 6 | "paths": { 7 | "/test": { 8 | "x-my-extension": true, 9 | "x-my-extension-2": { 10 | "dynamic": {} 11 | }, 12 | "get": { 13 | "description": "Test", 14 | "operationId": "test", 15 | "responses": { 16 | "200": { 17 | "description": "Success.", 18 | "schema": { 19 | "type": "array", 20 | "items": { 21 | "type": "string" 22 | } 23 | } 24 | } 25 | } 26 | } 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /tests/spec/defaultAsSuccess/basic.test.ts: -------------------------------------------------------------------------------- 1 | import * as fs from "node:fs/promises"; 2 | import * as os from "node:os"; 3 | import * as path from "node:path"; 4 | 5 | import { afterAll, beforeAll, describe, expect, test } from "vitest"; 6 | 7 | import { generateApi } from "../../../src/index.js"; 8 | 9 | describe("basic", async () => { 10 | let tmpdir = ""; 11 | 12 | beforeAll(async () => { 13 | tmpdir = await fs.mkdtemp(path.join(os.tmpdir(), "swagger-typescript-api")); 14 | }); 15 | 16 | afterAll(async () => { 17 | await fs.rm(tmpdir, { recursive: true }); 18 | }); 19 | 20 | test("--default-as-success", async () => { 21 | await generateApi({ 22 | fileName: "schema", 23 | input: path.resolve(import.meta.dirname, "schema.json"), 24 | output: tmpdir, 25 | silent: true, 26 | defaultResponseAsSuccess: true, 27 | }); 28 | 29 | const content = await fs.readFile(path.join(tmpdir, "schema.ts"), { 30 | encoding: "utf8", 31 | }); 32 | 33 | expect(content).toMatchSnapshot(); 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /tests/spec/defaultResponse/basic.test.ts: -------------------------------------------------------------------------------- 1 | import * as fs from "node:fs/promises"; 2 | import * as os from "node:os"; 3 | import * as path from "node:path"; 4 | 5 | import { afterAll, beforeAll, describe, expect, test } from "vitest"; 6 | 7 | import { generateApi } from "../../../src/index.js"; 8 | 9 | describe("basic", async () => { 10 | let tmpdir = ""; 11 | 12 | beforeAll(async () => { 13 | tmpdir = await fs.mkdtemp(path.join(os.tmpdir(), "swagger-typescript-api")); 14 | }); 15 | 16 | afterAll(async () => { 17 | await fs.rm(tmpdir, { recursive: true }); 18 | }); 19 | 20 | test("--default-response", async () => { 21 | await generateApi({ 22 | fileName: "schema", 23 | input: path.resolve(import.meta.dirname, "schema.json"), 24 | output: tmpdir, 25 | silent: true, 26 | defaultResponseType: "unknown", 27 | }); 28 | 29 | const content = await fs.readFile(path.join(tmpdir, "schema.ts"), { 30 | encoding: "utf8", 31 | }); 32 | 33 | expect(content).toMatchSnapshot(); 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /tests/spec/defaultResponse/schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "swagger": "2.0", 3 | "info": { 4 | "version": "1.0.0", 5 | "title": "Swagger Petstore", 6 | "description": "A sample API that uses a petstore as an example to demonstrate features in the swagger-2.0 specification", 7 | "termsOfService": "http://swagger.io/terms/", 8 | "contact": { 9 | "name": "Swagger API Team", 10 | "email": "apiteam@swagger.io", 11 | "url": "http://swagger.io" 12 | }, 13 | "license": { 14 | "name": "Apache 2.0", 15 | "url": "https://www.apache.org/licenses/LICENSE-2.0.html" 16 | } 17 | }, 18 | "host": "petstore.swagger.io", 19 | "basePath": "/api", 20 | "schemes": ["http"], 21 | "consumes": ["application/json"], 22 | "produces": ["application/json"], 23 | "paths": { 24 | "/pets": { 25 | "get": { 26 | "description": "Returns all pets from the system that the user has access to\nNam sed condimentum est. Maecenas tempor sagittis sapien, nec rhoncus sem sagittis sit amet. Aenean at gravida augue, ac iaculis sem. Curabitur odio lorem, ornare eget elementum nec, cursus id lectus. Duis mi turpis, pulvinar ac eros ac, tincidunt varius justo. In hac habitasse platea dictumst. Integer at adipiscing ante, a sagittis ligula. Aenean pharetra tempor ante molestie imperdiet. Vivamus id aliquam diam. Cras quis velit non tortor eleifend sagittis. Praesent at enim pharetra urna volutpat venenatis eget eget mauris. In eleifend fermentum facilisis. Praesent enim enim, gravida ac sodales sed, placerat id erat. Suspendisse lacus dolor, consectetur non augue vel, vehicula interdum libero. Morbi euismod sagittis libero sed lacinia.\n\nSed tempus felis lobortis leo pulvinar rutrum. Nam mattis velit nisl, eu condimentum ligula luctus nec. Phasellus semper velit eget aliquet faucibus. In a mattis elit. Phasellus vel urna viverra, condimentum lorem id, rhoncus nibh. Ut pellentesque posuere elementum. Sed a varius odio. Morbi rhoncus ligula libero, vel eleifend nunc tristique vitae. Fusce et sem dui. Aenean nec scelerisque tortor. Fusce malesuada accumsan magna vel tempus. Quisque mollis felis eu dolor tristique, sit amet auctor felis gravida. Sed libero lorem, molestie sed nisl in, accumsan tempor nisi. Fusce sollicitudin massa ut lacinia mattis. Sed vel eleifend lorem. Pellentesque vitae felis pretium, pulvinar elit eu, euismod sapien.\n", 27 | "operationId": "findPets", 28 | "parameters": [ 29 | { 30 | "name": "tags", 31 | "in": "query", 32 | "description": "tags to filter by", 33 | "required": false, 34 | "type": "array", 35 | "collectionFormat": "csv", 36 | "items": { 37 | "type": "string" 38 | } 39 | }, 40 | { 41 | "name": "limit", 42 | "in": "query", 43 | "description": "maximum number of results to return", 44 | "required": false, 45 | "type": "integer", 46 | "format": "int32" 47 | } 48 | ], 49 | "responses": { 50 | "200": {}, 51 | "default": {} 52 | } 53 | } 54 | } 55 | }, 56 | "definitions": {} 57 | } 58 | -------------------------------------------------------------------------------- /tests/spec/deprecated/basic.test.ts: -------------------------------------------------------------------------------- 1 | import * as fs from "node:fs/promises"; 2 | import * as os from "node:os"; 3 | import * as path from "node:path"; 4 | 5 | import { afterAll, beforeAll, describe, expect, test } from "vitest"; 6 | 7 | import { generateApi } from "../../../src/index.js"; 8 | 9 | describe("basic", async () => { 10 | let tmpdir = ""; 11 | 12 | beforeAll(async () => { 13 | tmpdir = await fs.mkdtemp(path.join(os.tmpdir(), "swagger-typescript-api")); 14 | }); 15 | 16 | afterAll(async () => { 17 | await fs.rm(tmpdir, { recursive: true }); 18 | }); 19 | 20 | test("@deprecated", async () => { 21 | await generateApi({ 22 | fileName: "schema", 23 | input: path.resolve(import.meta.dirname, "schema.json"), 24 | output: tmpdir, 25 | silent: true, 26 | }); 27 | 28 | const content = await fs.readFile(path.join(tmpdir, "schema.ts"), { 29 | encoding: "utf8", 30 | }); 31 | 32 | expect(content).toMatchSnapshot(); 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /tests/spec/discriminator/basic.test.ts: -------------------------------------------------------------------------------- 1 | import * as fs from "node:fs/promises"; 2 | import * as os from "node:os"; 3 | import * as path from "node:path"; 4 | 5 | import { afterAll, beforeAll, describe, expect, test } from "vitest"; 6 | 7 | import { generateApi } from "../../../src/index.js"; 8 | 9 | describe("basic", async () => { 10 | let tmpdir = ""; 11 | 12 | beforeAll(async () => { 13 | tmpdir = await fs.mkdtemp(path.join(os.tmpdir(), "swagger-typescript-api")); 14 | }); 15 | 16 | afterAll(async () => { 17 | await fs.rm(tmpdir, { recursive: true }); 18 | }); 19 | 20 | test("discriminator", async () => { 21 | await generateApi({ 22 | fileName: "schema", 23 | input: path.resolve(import.meta.dirname, "schema.json"), 24 | output: tmpdir, 25 | silent: true, 26 | addReadonly: true, 27 | generateClient: false, 28 | }); 29 | 30 | const content = await fs.readFile(path.join(tmpdir, "schema.ts"), { 31 | encoding: "utf8", 32 | }); 33 | 34 | expect(content).toMatchSnapshot(); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /tests/spec/dot-path-params/basic.test.ts: -------------------------------------------------------------------------------- 1 | import * as fs from "node:fs/promises"; 2 | import * as os from "node:os"; 3 | import * as path from "node:path"; 4 | 5 | import { afterAll, beforeAll, describe, expect, test } from "vitest"; 6 | 7 | import { generateApi } from "../../../src/index.js"; 8 | 9 | describe("basic", async () => { 10 | let tmpdir = ""; 11 | 12 | beforeAll(async () => { 13 | tmpdir = await fs.mkdtemp(path.join(os.tmpdir(), "swagger-typescript-api")); 14 | }); 15 | 16 | afterAll(async () => { 17 | await fs.rm(tmpdir, { recursive: true }); 18 | }); 19 | 20 | test("--dot-path-params", async () => { 21 | await generateApi({ 22 | fileName: "schema", 23 | input: path.resolve(import.meta.dirname, "schema.json"), 24 | output: tmpdir, 25 | silent: true, 26 | generateClient: true, 27 | }); 28 | 29 | const content = await fs.readFile(path.join(tmpdir, "schema.ts"), { 30 | encoding: "utf8", 31 | }); 32 | 33 | expect(content).toMatchSnapshot(); 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /tests/spec/dot-path-params/schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "swagger": "2.0", 3 | "info": { 4 | "title": "unset", 5 | "version": "unset" 6 | }, 7 | "consumes": ["application/json"], 8 | "produces": ["application/json"], 9 | "paths": { 10 | "/foobar/{truck.id}/item": { 11 | "post": { 12 | "summary": "Repros an issue", 13 | "operationId": "ReproFunc", 14 | "responses": { 15 | "default": { 16 | "description": "A response.", 17 | "schema": { 18 | "$ref": "#/definitions/myResponse" 19 | } 20 | } 21 | }, 22 | "parameters": [ 23 | { 24 | "name": "truck.id", 25 | "in": "path", 26 | "required": true, 27 | "type": "string" 28 | }, 29 | { 30 | "name": "query.id", 31 | "in": "query", 32 | "required": true, 33 | "type": "string" 34 | } 35 | ] 36 | } 37 | } 38 | }, 39 | "definitions": { 40 | "myResponse": { 41 | "type": "object" 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /tests/spec/enumNamesAsValues/basic.test.ts: -------------------------------------------------------------------------------- 1 | import * as fs from "node:fs/promises"; 2 | import * as os from "node:os"; 3 | import * as path from "node:path"; 4 | 5 | import { afterAll, beforeAll, describe, expect, test } from "vitest"; 6 | 7 | import { generateApi } from "../../../src/index.js"; 8 | 9 | describe("basic", async () => { 10 | let tmpdir = ""; 11 | 12 | beforeAll(async () => { 13 | tmpdir = await fs.mkdtemp(path.join(os.tmpdir(), "swagger-typescript-api")); 14 | }); 15 | 16 | afterAll(async () => { 17 | await fs.rm(tmpdir, { recursive: true }); 18 | }); 19 | 20 | test("--enum-names-as-values", async () => { 21 | await generateApi({ 22 | fileName: "schema", 23 | input: path.resolve(import.meta.dirname, "schema.json"), 24 | output: tmpdir, 25 | silent: true, 26 | enumNamesAsValues: true, 27 | }); 28 | 29 | const content = await fs.readFile(path.join(tmpdir, "schema.ts"), { 30 | encoding: "utf8", 31 | }); 32 | 33 | expect(content).toMatchSnapshot(); 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /tests/spec/enumNotFirstInComponents/basic.test.ts: -------------------------------------------------------------------------------- 1 | import * as fs from "node:fs/promises"; 2 | import * as os from "node:os"; 3 | import * as path from "node:path"; 4 | 5 | import { afterAll, beforeAll, describe, expect, test } from "vitest"; 6 | 7 | import { generateApi } from "../../../src/index.js"; 8 | 9 | describe("basic", async () => { 10 | let tmpdir = ""; 11 | 12 | beforeAll(async () => { 13 | tmpdir = await fs.mkdtemp(path.join(os.tmpdir(), "swagger-typescript-api")); 14 | }); 15 | 16 | afterAll(async () => { 17 | await fs.rm(tmpdir, { recursive: true }); 18 | }); 19 | 20 | test("enum not first in components", async () => { 21 | await generateApi({ 22 | fileName: "schema", 23 | input: path.resolve(import.meta.dirname, "schema.json"), 24 | output: tmpdir, 25 | silent: true, 26 | enumNamesAsValues: true, 27 | }); 28 | 29 | const content = await fs.readFile(path.join(tmpdir, "schema.ts"), { 30 | encoding: "utf8", 31 | }); 32 | 33 | expect(content).toMatchSnapshot(); 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /tests/spec/enumNotFirstInComponents/schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "openapi": "3.0.1", 3 | "info": { 4 | "description": "API definition for example service", 5 | "version": "0.0.1", 6 | "title": "example-service" 7 | }, 8 | "paths": { 9 | "/api/example": { 10 | "get": { 11 | "operationId": "getExample", 12 | "tags": ["test-api"], 13 | "responses": { 14 | "200": { 15 | "description": "Return example", 16 | "content": { 17 | "application/json": { 18 | "schema": { 19 | "$ref": "#/components/schemas/ExampleObject" 20 | } 21 | } 22 | } 23 | } 24 | } 25 | } 26 | } 27 | }, 28 | "components": { 29 | "examples": {}, 30 | "headers": {}, 31 | "parameters": {}, 32 | "requestBodies": {}, 33 | "responses": {}, 34 | "schemas": { 35 | "DtoExample1": { 36 | "type": "object", 37 | "properties": { 38 | "name": { 39 | "type": "string" 40 | }, 41 | "age": { 42 | "type": "integer" 43 | }, 44 | "toto": { 45 | "$ref": "#/components/schemas/DtoExample2" 46 | } 47 | }, 48 | "required": ["name", "age"] 49 | }, 50 | "DtoExample2": { 51 | "type": "object", 52 | "properties": { 53 | "title": { 54 | "type": "string" 55 | }, 56 | "description": { 57 | "type": "string" 58 | } 59 | }, 60 | "required": ["title", "description"] 61 | }, 62 | "ExampleObject": { 63 | "properties": { 64 | "type": { 65 | "$ref": "#/components/schemas/ExampleEnum" 66 | } 67 | }, 68 | "discriminator": { 69 | "propertyName": "type", 70 | "mapping": { 71 | "Example1": "#/components/schemas/DtoExample1", 72 | "Example2": "#/components/schemas/DtoExample2" 73 | } 74 | } 75 | }, 76 | "ExampleEnum": { 77 | "type": "string", 78 | "enum": ["Example1", "Example2"] 79 | } 80 | } 81 | }, 82 | "servers": [{ "url": "http://localhost:8080/api/v1" }] 83 | } 84 | -------------------------------------------------------------------------------- /tests/spec/enums-2.0/__snapshots__/basic.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[`basic > enums-2.0 1`] = ` 4 | "/* eslint-disable */ 5 | /* tslint:disable */ 6 | // @ts-nocheck 7 | /* 8 | * --------------------------------------------------------------- 9 | * ## THIS FILE WAS GENERATED VIA SWAGGER-TYPESCRIPT-API ## 10 | * ## ## 11 | * ## AUTHOR: acacode ## 12 | * ## SOURCE: https://github.com/acacode/swagger-typescript-api ## 13 | * --------------------------------------------------------------- 14 | */ 15 | 16 | /** @format int32 */ 17 | export enum SomeInterestEnum { 18 | Bla = 6, 19 | Blabla = 2, 20 | Boiler = 1, 21 | Bbabab = 67, 22 | Nowadays = 88, 23 | FAIL = 122, 24 | Vvvvv = 88, 25 | ASdasAS = 0, 26 | ASDsacZX = 213, 27 | Zook = 12378, 28 | EnumMm = 123125, 29 | VCsa = 32452, 30 | Yuuu = 1111, 31 | ASddd = 66666, 32 | ASdsdsa = "ASdsdsa", 33 | ASDds = "ASDds", 34 | HSDFDS = "HSDFDS", 35 | } 36 | 37 | /** @format int32 */ 38 | export enum EnumWithMoreNames { 39 | Bla = 1, 40 | Blabla = "Blabla", 41 | Boiler = "Boiler", 42 | } 43 | 44 | export enum StringCompleteEnums { 45 | Bla = "foo", 46 | Blabla = "bar", 47 | Boiler = "baz", 48 | } 49 | 50 | export enum StringEnums { 51 | Bla = "foo", 52 | Blabla = "bar", 53 | Boiler = "Boiler", 54 | } 55 | 56 | export enum SimpleEnumNonNullable { 57 | Value0 = 0, 58 | Value1 = 1, 59 | Value2 = 2, 60 | Value3 = 3, 61 | Value4 = 4, 62 | Value5 = 5, 63 | } 64 | 65 | export enum XNullableEnum { 66 | Value0 = 0, 67 | Value1 = 1, 68 | Value2 = 2, 69 | Value3 = 3, 70 | Value4 = 4, 71 | Value5 = 5, 72 | } 73 | 74 | export interface ObjWithEnum { 75 | "prop-enum-nullable"?: 0 | 1 | 2 | 3 | 4 | 5 | null; 76 | "prop-enum"?: 0 | 1 | 2 | 3 | 4 | 5; 77 | } 78 | 79 | export enum OnlyEnumNames { 80 | Bla = "Bla", 81 | Blabla = "Blabla", 82 | Boiler = "Boiler", 83 | } 84 | 85 | export enum StringOnlyEnumNames { 86 | Bla = "Bla", 87 | Blabla = "Blabla", 88 | Boiler = "Boiler", 89 | } 90 | 91 | /** @format int32 */ 92 | export enum EmptyEnum { 93 | Bla = "Bla", 94 | Blabla = "Blabla", 95 | Boiler = "Boiler", 96 | } 97 | 98 | export interface PostFooPayload { 99 | someTypeId?: 1 | 2 | 3 | 4 | 5; 100 | } 101 | 102 | export interface PostFooParams { 103 | testKek: 1 | 2 | 3 | 4 | 5; 104 | } 105 | " 106 | `; 107 | -------------------------------------------------------------------------------- /tests/spec/enums-2.0/basic.test.ts: -------------------------------------------------------------------------------- 1 | import * as fs from "node:fs/promises"; 2 | import * as os from "node:os"; 3 | import * as path from "node:path"; 4 | 5 | import { afterAll, beforeAll, describe, expect, test } from "vitest"; 6 | 7 | import { generateApi } from "../../../src/index.js"; 8 | 9 | describe("basic", async () => { 10 | let tmpdir = ""; 11 | 12 | beforeAll(async () => { 13 | tmpdir = await fs.mkdtemp(path.join(os.tmpdir(), "swagger-typescript-api")); 14 | }); 15 | 16 | afterAll(async () => { 17 | await fs.rm(tmpdir, { recursive: true }); 18 | }); 19 | 20 | test("enums-2.0", async () => { 21 | await generateApi({ 22 | fileName: "schema", 23 | input: path.resolve(import.meta.dirname, "schema.json"), 24 | output: tmpdir, 25 | silent: true, 26 | extractRequestParams: true, 27 | extractRequestBody: true, 28 | extractResponseBody: true, 29 | extractResponseError: true, 30 | generateClient: false, 31 | }); 32 | 33 | const content = await fs.readFile(path.join(tmpdir, "schema.ts"), { 34 | encoding: "utf8", 35 | }); 36 | 37 | expect(content).toMatchSnapshot(); 38 | }); 39 | }); 40 | -------------------------------------------------------------------------------- /tests/spec/enums-2.0/schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "swagger": "2.0", 3 | "schemes": ["https"], 4 | "host": "ffff.com", 5 | "basePath": "/", 6 | "info": {}, 7 | "definitions": { 8 | "objWithEnum": { 9 | "type": "object", 10 | "properties": { 11 | "prop-enum-nullable": { 12 | "type": "integer", 13 | "x-nullable": true, 14 | "enum": [0, 1, 2, 3, 4, 5] 15 | }, 16 | "prop-enum": { 17 | "type": "integer", 18 | "enum": [0, 1, 2, 3, 4, 5] 19 | } 20 | } 21 | }, 22 | "x-nullable-enum": { 23 | "type": "integer", 24 | "x-nullable": true, 25 | "enum": [0, 1, 2, 3, 4, 5] 26 | }, 27 | "simple-enum-non-nullable": { 28 | "type": "integer", 29 | "enum": [0, 1, 2, 3, 4, 5] 30 | }, 31 | "OnlyEnumNames": { 32 | "x-enumNames": ["Bla", "Blabla", "Boiler"] 33 | }, 34 | "StringOnlyEnumNames": { 35 | "type": "int32", 36 | "x-enumNames": ["Bla", "Blabla", "Boiler"] 37 | }, 38 | "StringEnums": { 39 | "type": "int32", 40 | "enum": ["foo", "bar"], 41 | "x-enumNames": ["Bla", "Blabla", "Boiler"] 42 | }, 43 | "StringCompleteEnums": { 44 | "type": "int32", 45 | "enum": ["foo", "bar", "baz"], 46 | "x-enumNames": ["Bla", "Blabla", "Boiler"] 47 | }, 48 | "EmptyEnum": { 49 | "format": "int32", 50 | "type": "integer", 51 | "x-enumNames": ["Bla", "Blabla", "Boiler"] 52 | }, 53 | "EnumWithMoreNames": { 54 | "format": "int32", 55 | "type": "integer", 56 | "enum": [1], 57 | "x-enumNames": ["Bla", "Blabla", "Boiler"] 58 | }, 59 | "SomeInterestEnum": { 60 | "format": "int32", 61 | "enum": [ 62 | 6, 2, 1, 67, 88, 122, 88, 0, 213, 12378, 123125, 32452, 1111, 66666 63 | ], 64 | "type": "integer", 65 | "x-enumNames": [ 66 | "Bla", 67 | "Blabla", 68 | "Boiler", 69 | "Bbabab", 70 | "Nowadays", 71 | "FAIL", 72 | "Vvvvv", 73 | "ASdasAS", 74 | "ASDsacZX", 75 | "Zook", 76 | "EnumMm", 77 | "VCsa", 78 | "Yuuu", 79 | "ASddd", 80 | "ASdsdsa", 81 | "ASDds", 82 | "HSDFDS" 83 | ] 84 | } 85 | }, 86 | "paths": { 87 | "/foo": { 88 | "post": { 89 | "operationId": "postFoo", 90 | "parameters": [ 91 | { 92 | "in": "query", 93 | "name": "testKek", 94 | "required": true, 95 | "type": "integer", 96 | "enum": [1, 2, 3, 4, 5] 97 | } 98 | ], 99 | "requestBody": { 100 | "content": { 101 | "application/json": { 102 | "schema": { 103 | "type": "object", 104 | "properties": { 105 | "someTypeId": { 106 | "type": "integer", 107 | "enum": [1, 2, 3, 4, 5] 108 | } 109 | } 110 | } 111 | } 112 | } 113 | }, 114 | "responses": {} 115 | } 116 | } 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /tests/spec/extractRequestBody/basic.test.ts: -------------------------------------------------------------------------------- 1 | import * as fs from "node:fs/promises"; 2 | import * as os from "node:os"; 3 | import * as path from "node:path"; 4 | 5 | import { afterAll, beforeAll, describe, expect, test } from "vitest"; 6 | 7 | import { generateApi } from "../../../src/index.js"; 8 | 9 | describe("basic", async () => { 10 | let tmpdir = ""; 11 | 12 | beforeAll(async () => { 13 | tmpdir = await fs.mkdtemp(path.join(os.tmpdir(), "swagger-typescript-api")); 14 | }); 15 | 16 | afterAll(async () => { 17 | await fs.rm(tmpdir, { recursive: true }); 18 | }); 19 | 20 | test("--extract-request-body", async () => { 21 | await generateApi({ 22 | fileName: "schema", 23 | input: path.resolve(import.meta.dirname, "schema.json"), 24 | output: tmpdir, 25 | silent: true, 26 | extractRequestBody: true, 27 | typeSuffix: "TTT", 28 | }); 29 | 30 | const content = await fs.readFile(path.join(tmpdir, "schema.ts"), { 31 | encoding: "utf8", 32 | }); 33 | 34 | expect(content).toMatchSnapshot(); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /tests/spec/extractRequestParams/basic.test.ts: -------------------------------------------------------------------------------- 1 | import * as fs from "node:fs/promises"; 2 | import * as os from "node:os"; 3 | import * as path from "node:path"; 4 | 5 | import { afterAll, beforeAll, describe, expect, test } from "vitest"; 6 | 7 | import { generateApi } from "../../../src/index.js"; 8 | 9 | describe("basic", async () => { 10 | let tmpdir = ""; 11 | 12 | beforeAll(async () => { 13 | tmpdir = await fs.mkdtemp(path.join(os.tmpdir(), "swagger-typescript-api")); 14 | }); 15 | 16 | afterAll(async () => { 17 | await fs.rm(tmpdir, { recursive: true }); 18 | }); 19 | 20 | test("--extract-request-params", async () => { 21 | await generateApi({ 22 | fileName: "schema", 23 | input: path.resolve(import.meta.dirname, "schema.json"), 24 | output: tmpdir, 25 | silent: true, 26 | extractRequestParams: true, 27 | }); 28 | 29 | const content = await fs.readFile(path.join(tmpdir, "schema.ts"), { 30 | encoding: "utf8", 31 | }); 32 | 33 | expect(content).toMatchSnapshot(); 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /tests/spec/extractResponseBody/basic.test.ts: -------------------------------------------------------------------------------- 1 | import * as fs from "node:fs/promises"; 2 | import * as os from "node:os"; 3 | import * as path from "node:path"; 4 | 5 | import { afterAll, beforeAll, describe, expect, test } from "vitest"; 6 | 7 | import { generateApi } from "../../../src/index.js"; 8 | 9 | describe("basic", async () => { 10 | let tmpdir = ""; 11 | 12 | beforeAll(async () => { 13 | tmpdir = await fs.mkdtemp(path.join(os.tmpdir(), "swagger-typescript-api")); 14 | }); 15 | 16 | afterAll(async () => { 17 | await fs.rm(tmpdir, { recursive: true }); 18 | }); 19 | 20 | test("--extract-response-body", async () => { 21 | await generateApi({ 22 | fileName: "schema", 23 | input: path.resolve(import.meta.dirname, "schema.json"), 24 | output: tmpdir, 25 | silent: true, 26 | extractResponseBody: true, 27 | typeSuffix: "TTT", 28 | }); 29 | 30 | const content = await fs.readFile(path.join(tmpdir, "schema.ts"), { 31 | encoding: "utf8", 32 | }); 33 | 34 | expect(content).toMatchSnapshot(); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /tests/spec/extractResponseError/basic.test.ts: -------------------------------------------------------------------------------- 1 | import * as fs from "node:fs/promises"; 2 | import * as os from "node:os"; 3 | import * as path from "node:path"; 4 | 5 | import { afterAll, beforeAll, describe, expect, test } from "vitest"; 6 | 7 | import { generateApi } from "../../../src/index.js"; 8 | 9 | describe("basic", async () => { 10 | let tmpdir = ""; 11 | 12 | beforeAll(async () => { 13 | tmpdir = await fs.mkdtemp(path.join(os.tmpdir(), "swagger-typescript-api")); 14 | }); 15 | 16 | afterAll(async () => { 17 | await fs.rm(tmpdir, { recursive: true }); 18 | }); 19 | 20 | test("--extract-response-body", async () => { 21 | await generateApi({ 22 | fileName: "schema", 23 | input: path.resolve(import.meta.dirname, "schema.json"), 24 | output: tmpdir, 25 | silent: true, 26 | extractResponseError: true, 27 | typeSuffix: "TTT", 28 | }); 29 | 30 | const content = await fs.readFile(path.join(tmpdir, "schema.ts"), { 31 | encoding: "utf8", 32 | }); 33 | 34 | expect(content).toMatchSnapshot(); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /tests/spec/js/basic.test.ts: -------------------------------------------------------------------------------- 1 | import * as fs from "node:fs/promises"; 2 | import * as os from "node:os"; 3 | import * as path from "node:path"; 4 | 5 | import { afterAll, beforeAll, describe, expect, test } from "vitest"; 6 | 7 | import { generateApi } from "../../../src/index.js"; 8 | 9 | describe("basic", async () => { 10 | let tmpdir = ""; 11 | 12 | beforeAll(async () => { 13 | tmpdir = await fs.mkdtemp(path.join(os.tmpdir(), "swagger-typescript-api")); 14 | }); 15 | 16 | afterAll(async () => { 17 | await fs.rm(tmpdir, { recursive: true }); 18 | }); 19 | 20 | test("--js --axios", async () => { 21 | await generateApi({ 22 | fileName: "schema", 23 | input: path.resolve(import.meta.dirname, "schema.json"), 24 | output: tmpdir, 25 | silent: true, 26 | httpClientType: "axios", 27 | toJS: true, 28 | }); 29 | 30 | const content = await fs.readFile(path.join(tmpdir, "schema.js"), { 31 | encoding: "utf8", 32 | }); 33 | 34 | expect(content).toMatchSnapshot(); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /tests/spec/jsAxios/basic.test.ts: -------------------------------------------------------------------------------- 1 | import * as fs from "node:fs/promises"; 2 | import * as os from "node:os"; 3 | import * as path from "node:path"; 4 | 5 | import { afterAll, beforeAll, describe, expect, test } from "vitest"; 6 | 7 | import { generateApi } from "../../../src/index.js"; 8 | 9 | describe("basic", async () => { 10 | let tmpdir = ""; 11 | 12 | beforeAll(async () => { 13 | tmpdir = await fs.mkdtemp(path.join(os.tmpdir(), "swagger-typescript-api")); 14 | }); 15 | 16 | afterAll(async () => { 17 | await fs.rm(tmpdir, { recursive: true }); 18 | }); 19 | 20 | test("--js", async () => { 21 | await generateApi({ 22 | fileName: "schema", 23 | input: path.resolve(import.meta.dirname, "schema.json"), 24 | output: tmpdir, 25 | silent: true, 26 | toJS: true, 27 | }); 28 | 29 | const content = await fs.readFile(path.join(tmpdir, "schema.js"), { 30 | encoding: "utf8", 31 | }); 32 | 33 | expect(content).toMatchSnapshot(); 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /tests/spec/jsonapi-media-type/basic.test.ts: -------------------------------------------------------------------------------- 1 | import * as fs from "node:fs/promises"; 2 | import * as os from "node:os"; 3 | import * as path from "node:path"; 4 | 5 | import { afterAll, beforeAll, describe, expect, test } from "vitest"; 6 | 7 | import { generateApi } from "../../../src/index.js"; 8 | 9 | describe("basic", async () => { 10 | let tmpdir = ""; 11 | 12 | beforeAll(async () => { 13 | tmpdir = await fs.mkdtemp(path.join(os.tmpdir(), "swagger-typescript-api")); 14 | }); 15 | 16 | afterAll(async () => { 17 | await fs.rm(tmpdir, { recursive: true }); 18 | }); 19 | 20 | test("jsonapi-media-type", async () => { 21 | await generateApi({ 22 | fileName: "schema", 23 | input: path.resolve(import.meta.dirname, "schema.json"), 24 | output: tmpdir, 25 | silent: true, 26 | enumNamesAsValues: true, 27 | }); 28 | 29 | const content = await fs.readFile(path.join(tmpdir, "schema.ts"), { 30 | encoding: "utf8", 31 | }); 32 | 33 | expect(content).toMatchSnapshot(); 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /tests/spec/moduleNameFirstTag/basic.test.ts: -------------------------------------------------------------------------------- 1 | import * as fs from "node:fs/promises"; 2 | import * as os from "node:os"; 3 | import * as path from "node:path"; 4 | 5 | import { afterAll, beforeAll, describe, expect, test } from "vitest"; 6 | 7 | import { generateApi } from "../../../src/index.js"; 8 | 9 | describe("basic", async () => { 10 | let tmpdir = ""; 11 | 12 | beforeAll(async () => { 13 | tmpdir = await fs.mkdtemp(path.join(os.tmpdir(), "swagger-typescript-api")); 14 | }); 15 | 16 | afterAll(async () => { 17 | await fs.rm(tmpdir, { recursive: true }); 18 | }); 19 | 20 | test("--module-name-first-tag", async () => { 21 | await generateApi({ 22 | fileName: "schema", 23 | input: path.resolve(import.meta.dirname, "schema.json"), 24 | output: tmpdir, 25 | silent: true, 26 | moduleNameFirstTag: true, 27 | }); 28 | 29 | const content = await fs.readFile(path.join(tmpdir, "schema.ts"), { 30 | encoding: "utf8", 31 | }); 32 | 33 | expect(content).toMatchSnapshot(); 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /tests/spec/moduleNameIndex/basic.test.ts: -------------------------------------------------------------------------------- 1 | import * as fs from "node:fs/promises"; 2 | import * as os from "node:os"; 3 | import * as path from "node:path"; 4 | 5 | import { afterAll, beforeAll, describe, expect, test } from "vitest"; 6 | 7 | import { generateApi } from "../../../src/index.js"; 8 | 9 | describe("basic", async () => { 10 | let tmpdir = ""; 11 | 12 | beforeAll(async () => { 13 | tmpdir = await fs.mkdtemp(path.join(os.tmpdir(), "swagger-typescript-api")); 14 | }); 15 | 16 | afterAll(async () => { 17 | await fs.rm(tmpdir, { recursive: true }); 18 | }); 19 | 20 | test("--module-name-index", async () => { 21 | await generateApi({ 22 | fileName: "schema", 23 | input: path.resolve(import.meta.dirname, "schema.json"), 24 | output: tmpdir, 25 | silent: true, 26 | moduleNameIndex: 2, 27 | }); 28 | 29 | const content = await fs.readFile(path.join(tmpdir, "schema.ts"), { 30 | encoding: "utf8", 31 | }); 32 | 33 | expect(content).toMatchSnapshot(); 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /tests/spec/noClient/basic.test.ts: -------------------------------------------------------------------------------- 1 | import * as fs from "node:fs/promises"; 2 | import * as os from "node:os"; 3 | import * as path from "node:path"; 4 | 5 | import { afterAll, beforeAll, describe, expect, test } from "vitest"; 6 | 7 | import { generateApi } from "../../../src/index.js"; 8 | 9 | describe("basic", async () => { 10 | let tmpdir = ""; 11 | 12 | beforeAll(async () => { 13 | tmpdir = await fs.mkdtemp(path.join(os.tmpdir(), "swagger-typescript-api")); 14 | }); 15 | 16 | afterAll(async () => { 17 | await fs.rm(tmpdir, { recursive: true }); 18 | }); 19 | 20 | test("--no-client", async () => { 21 | await generateApi({ 22 | fileName: "schema", 23 | input: path.resolve(import.meta.dirname, "schema.json"), 24 | output: tmpdir, 25 | silent: true, 26 | generateClient: false, 27 | }); 28 | 29 | const content = await fs.readFile(path.join(tmpdir, "schema.ts"), { 30 | encoding: "utf8", 31 | }); 32 | 33 | expect(content).toMatchSnapshot(); 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /tests/spec/nullable-2.0/__snapshots__/basic.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[`basic > nullable-2.0 refs 1`] = ` 4 | "/* eslint-disable */ 5 | /* tslint:disable */ 6 | // @ts-nocheck 7 | /* 8 | * --------------------------------------------------------------- 9 | * ## THIS FILE WAS GENERATED VIA SWAGGER-TYPESCRIPT-API ## 10 | * ## ## 11 | * ## AUTHOR: acacode ## 12 | * ## SOURCE: https://github.com/acacode/swagger-typescript-api ## 13 | * --------------------------------------------------------------- 14 | */ 15 | 16 | export type MyObject = { 17 | id: string; 18 | name: string; 19 | } | null; 20 | 21 | export interface TestObject { 22 | stringMaybeUndefined?: string; 23 | stringMaybeNullA?: string | null; 24 | stringMaybeNullB?: string | null; 25 | stringMaybeNullAndUndefined?: string | null; 26 | otherObjectMaybeUndefined?: OtherObject; 27 | otherObjectMaybeNullA?: OtherObject | null; 28 | otherObjectMaybeNullB?: OtherObject | null; 29 | otherObjectMaybeNullC?: OtherObject | null; 30 | otherObjectMaybeNullD: OtherObject; 31 | } 32 | 33 | export type OtherObject = object; 34 | " 35 | `; 36 | -------------------------------------------------------------------------------- /tests/spec/nullable-2.0/basic.test.ts: -------------------------------------------------------------------------------- 1 | import * as fs from "node:fs/promises"; 2 | import * as os from "node:os"; 3 | import * as path from "node:path"; 4 | 5 | import { afterAll, beforeAll, describe, expect, test } from "vitest"; 6 | 7 | import { generateApi } from "../../../src/index.js"; 8 | 9 | describe("basic", async () => { 10 | let tmpdir = ""; 11 | 12 | beforeAll(async () => { 13 | tmpdir = await fs.mkdtemp(path.join(os.tmpdir(), "swagger-typescript-api")); 14 | }); 15 | 16 | afterAll(async () => { 17 | await fs.rm(tmpdir, { recursive: true }); 18 | }); 19 | 20 | test("nullable-2.0 refs", async () => { 21 | await generateApi({ 22 | fileName: "schema", 23 | input: path.resolve(import.meta.dirname, "schema.json"), 24 | output: tmpdir, 25 | silent: true, 26 | addReadonly: true, 27 | generateClient: false, 28 | }); 29 | 30 | const content = await fs.readFile(path.join(tmpdir, "schema.ts"), { 31 | encoding: "utf8", 32 | }); 33 | 34 | expect(content).toMatchSnapshot(); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /tests/spec/nullable-2.0/schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "swagger": "2.0", 3 | "schemes": ["https"], 4 | "host": "ffff.com", 5 | "basePath": "/", 6 | "info": {}, 7 | "definitions": { 8 | "myObject": { 9 | "type": "object", 10 | "nullable": true, 11 | "properties": { 12 | "id": { 13 | "type": "string" 14 | }, 15 | "name": { 16 | "type": "string" 17 | } 18 | }, 19 | "required": ["id", "name"] 20 | }, 21 | "TestObject": { 22 | "type": "object", 23 | "properties": { 24 | "stringMaybeUndefined": { 25 | "type": "string" 26 | }, 27 | "stringMaybeNullA": { 28 | "type": "string", 29 | "nullable": true 30 | }, 31 | "stringMaybeNullB": { 32 | "anyOf": [ 33 | { 34 | "type": "string" 35 | } 36 | ], 37 | "nullable": true 38 | }, 39 | "stringMaybeNullAndUndefined": { 40 | "anyOf": [ 41 | { 42 | "type": "string" 43 | } 44 | ], 45 | "nullable": true 46 | }, 47 | "otherObjectMaybeUndefined": { 48 | "$ref": "#/components/schemas/OtherObject" 49 | }, 50 | "otherObjectMaybeNullA": { 51 | "$ref": "#/components/schemas/OtherObject", 52 | "nullable": true 53 | }, 54 | "otherObjectMaybeNullB": { 55 | "anyOf": [ 56 | { 57 | "$ref": "#/components/schemas/OtherObject" 58 | } 59 | ], 60 | "nullable": true 61 | }, 62 | "otherObjectMaybeNullC": { 63 | "anyOf": [ 64 | { 65 | "$ref": "#/components/schemas/OtherObject" 66 | } 67 | ], 68 | "type": "null" 69 | }, 70 | "otherObjectMaybeNullD": { 71 | "anyOf": [ 72 | { 73 | "$ref": "#/components/schemas/OtherObject" 74 | }, 75 | { 76 | "type": "null" 77 | } 78 | ] 79 | } 80 | }, 81 | "required": [ 82 | "stringMaybeNullA", 83 | "stringMaybeNullB", 84 | "otherObjectMaybeNullA", 85 | "otherObjectMaybeNullB", 86 | "otherObjectMaybeNullC", 87 | "otherObjectMaybeNullD" 88 | ] 89 | }, 90 | "OtherObject": { 91 | "type": "object" 92 | } 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /tests/spec/nullable-3.0/__snapshots__/basic.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[`basic > nullable-3.0 refs 1`] = ` 4 | "/* eslint-disable */ 5 | /* tslint:disable */ 6 | // @ts-nocheck 7 | /* 8 | * --------------------------------------------------------------- 9 | * ## THIS FILE WAS GENERATED VIA SWAGGER-TYPESCRIPT-API ## 10 | * ## ## 11 | * ## AUTHOR: acacode ## 12 | * ## SOURCE: https://github.com/acacode/swagger-typescript-api ## 13 | * --------------------------------------------------------------- 14 | */ 15 | 16 | export type MyObject = { 17 | id: string; 18 | name: string; 19 | } | null; 20 | 21 | export interface TestObject { 22 | stringMaybeUndefined?: string; 23 | stringMaybeNullA: string | null; 24 | stringMaybeNullB: string | null; 25 | stringMaybeNullAndUndefined?: string | null; 26 | otherObjectMaybeUndefined?: OtherObject; 27 | otherObjectMaybeNullA: OtherObject | null; 28 | otherObjectMaybeNullB: OtherObject | null; 29 | otherObjectMaybeNullC: OtherObject | null; 30 | otherObjectMaybeNullD: OtherObject | null; 31 | } 32 | 33 | export type OtherObject = object; 34 | " 35 | `; 36 | -------------------------------------------------------------------------------- /tests/spec/nullable-3.0/basic.test.ts: -------------------------------------------------------------------------------- 1 | import * as fs from "node:fs/promises"; 2 | import * as os from "node:os"; 3 | import * as path from "node:path"; 4 | 5 | import { afterAll, beforeAll, describe, expect, test } from "vitest"; 6 | 7 | import { generateApi } from "../../../src/index.js"; 8 | 9 | describe("basic", async () => { 10 | let tmpdir = ""; 11 | 12 | beforeAll(async () => { 13 | tmpdir = await fs.mkdtemp(path.join(os.tmpdir(), "swagger-typescript-api")); 14 | }); 15 | 16 | afterAll(async () => { 17 | await fs.rm(tmpdir, { recursive: true }); 18 | }); 19 | 20 | test("nullable-3.0 refs", async () => { 21 | await generateApi({ 22 | fileName: "schema", 23 | input: path.resolve(import.meta.dirname, "schema.json"), 24 | output: tmpdir, 25 | silent: true, 26 | addReadonly: true, 27 | generateClient: false, 28 | }); 29 | 30 | const content = await fs.readFile(path.join(tmpdir, "schema.ts"), { 31 | encoding: "utf8", 32 | }); 33 | 34 | expect(content).toMatchSnapshot(); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /tests/spec/nullable-3.0/schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "openapi": "3.0.1", 3 | "info": { 4 | "title": "Nullable Refs Example", 5 | "version": "1.0.0" 6 | }, 7 | "components": { 8 | "schemas": { 9 | "myObject": { 10 | "type": "object", 11 | "nullable": true, 12 | "properties": { 13 | "id": { 14 | "type": "string" 15 | }, 16 | "name": { 17 | "type": "string" 18 | } 19 | }, 20 | "required": ["id", "name"] 21 | }, 22 | "TestObject": { 23 | "type": "object", 24 | "properties": { 25 | "stringMaybeUndefined": { 26 | "type": "string" 27 | }, 28 | "stringMaybeNullA": { 29 | "type": "string", 30 | "nullable": true 31 | }, 32 | "stringMaybeNullB": { 33 | "anyOf": [ 34 | { 35 | "type": "string" 36 | } 37 | ], 38 | "nullable": true 39 | }, 40 | "stringMaybeNullAndUndefined": { 41 | "anyOf": [ 42 | { 43 | "type": "string" 44 | } 45 | ], 46 | "nullable": true 47 | }, 48 | "otherObjectMaybeUndefined": { 49 | "$ref": "#/components/schemas/OtherObject" 50 | }, 51 | "otherObjectMaybeNullA": { 52 | "$ref": "#/components/schemas/OtherObject", 53 | "nullable": true 54 | }, 55 | "otherObjectMaybeNullB": { 56 | "anyOf": [ 57 | { 58 | "$ref": "#/components/schemas/OtherObject" 59 | } 60 | ], 61 | "nullable": true 62 | }, 63 | "otherObjectMaybeNullC": { 64 | "anyOf": [ 65 | { 66 | "$ref": "#/components/schemas/OtherObject" 67 | } 68 | ], 69 | "type": "null" 70 | }, 71 | "otherObjectMaybeNullD": { 72 | "anyOf": [ 73 | { 74 | "$ref": "#/components/schemas/OtherObject" 75 | }, 76 | { 77 | "type": "null" 78 | } 79 | ] 80 | } 81 | }, 82 | "required": [ 83 | "stringMaybeNullA", 84 | "stringMaybeNullB", 85 | "otherObjectMaybeNullA", 86 | "otherObjectMaybeNullB", 87 | "otherObjectMaybeNullC", 88 | "otherObjectMaybeNullD" 89 | ] 90 | }, 91 | "OtherObject": { 92 | "type": "object" 93 | } 94 | } 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /tests/spec/object-types/__snapshots__/basic.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[`basic > object-types 1`] = ` 4 | "/* eslint-disable */ 5 | /* tslint:disable */ 6 | // @ts-nocheck 7 | /* 8 | * --------------------------------------------------------------- 9 | * ## THIS FILE WAS GENERATED VIA SWAGGER-TYPESCRIPT-API ## 10 | * ## ## 11 | * ## AUTHOR: acacode ## 12 | * ## SOURCE: https://github.com/acacode/swagger-typescript-api ## 13 | * --------------------------------------------------------------- 14 | */ 15 | 16 | type UtilRequiredKeys = Omit & Required>; 17 | 18 | /** 19 | * title 20 | * description 21 | * @example "https://ya.ru/a.png" 22 | */ 23 | export type AnyOfWithNullable = string | null; 24 | 25 | /** https://github.com/acacode/swagger-typescript-api/issues/445 */ 26 | export interface SpecificEnum1 { 27 | myEnum?: ["foo", "bar", "baz"]; 28 | } 29 | 30 | /** https://github.com/acacode/swagger-typescript-api/issues/445 */ 31 | export interface SpecificEnum2 { 32 | myEnum?: ["foo", "bar", "baz"] | ["foo", "bar", "bad"]; 33 | } 34 | 35 | /** 36 | * It is a Pet title 37 | * It is a Pet description 38 | */ 39 | export type Test1 = { 40 | /** 41 | * It is an id title 42 | * It is an id description 43 | */ 44 | id: number; 45 | /** 46 | * It is a name title 47 | * It is a name description 48 | */ 49 | name: string; 50 | /** 51 | * It is a tag title 52 | * It is a tag description 53 | * @deprecated 54 | */ 55 | tag: string; 56 | /** 57 | * It is a multiple title 58 | * It is a multiple description 59 | */ 60 | multiple: string | number; 61 | } | null; 62 | 63 | export interface AdditionalObjectProperties { 64 | id?: string; 65 | [key: string]: any; 66 | } 67 | 68 | export interface AdditionalIntProperties { 69 | id?: string; 70 | [key: string]: any; 71 | } 72 | 73 | export interface ABCOptional { 74 | /** a */ 75 | a?: string; 76 | /** b */ 77 | b?: string; 78 | /** c */ 79 | c?: number; 80 | } 81 | 82 | export type ABCOptionalWithRequiredId = UtilRequiredKeys & { 83 | /** id */ 84 | id: number; 85 | }; 86 | 87 | export type NestedObjectWithRequiredId = { 88 | /** id */ 89 | id: number; 90 | } & { 91 | /** id */ 92 | id1?: number; 93 | }; 94 | " 95 | `; 96 | -------------------------------------------------------------------------------- /tests/spec/object-types/basic.test.ts: -------------------------------------------------------------------------------- 1 | import * as fs from "node:fs/promises"; 2 | import * as os from "node:os"; 3 | import * as path from "node:path"; 4 | 5 | import { afterAll, beforeAll, describe, expect, test } from "vitest"; 6 | 7 | import { generateApi } from "../../../src/index.js"; 8 | 9 | describe("basic", async () => { 10 | let tmpdir = ""; 11 | 12 | beforeAll(async () => { 13 | tmpdir = await fs.mkdtemp(path.join(os.tmpdir(), "swagger-typescript-api")); 14 | }); 15 | 16 | afterAll(async () => { 17 | await fs.rm(tmpdir, { recursive: true }); 18 | }); 19 | 20 | test("object-types", async () => { 21 | await generateApi({ 22 | fileName: "schema", 23 | input: path.resolve(import.meta.dirname, "schema.json"), 24 | output: tmpdir, 25 | silent: true, 26 | addReadonly: true, 27 | generateClient: false, 28 | }); 29 | 30 | const content = await fs.readFile(path.join(tmpdir, "schema.ts"), { 31 | encoding: "utf8", 32 | }); 33 | 34 | expect(content).toMatchSnapshot(); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /tests/spec/object-types/schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "swagger": "2.0", 3 | "info": { 4 | "version": "1.0.0", 5 | "title": "Swagger Petstore", 6 | "description": "A sample API that uses a petstore as an example to demonstrate features in the swagger-2.0 specification", 7 | "termsOfService": "http://swagger.io/terms/", 8 | "contact": { 9 | "name": "Swagger API Team" 10 | }, 11 | "license": { 12 | "name": "MIT" 13 | } 14 | }, 15 | "host": "petstore.swagger.io", 16 | "basePath": "/api", 17 | "schemes": ["http"], 18 | "consumes": ["application/json"], 19 | "produces": ["application/json"], 20 | "paths": {}, 21 | "definitions": { 22 | "AnyOfWithNullable": { 23 | "anyOf": [ 24 | { 25 | "type": "string" 26 | }, 27 | { 28 | "type": "null" 29 | } 30 | ], 31 | "description": "description", 32 | "example": "https://ya.ru/a.png", 33 | "nullable": true, 34 | "title": "title" 35 | }, 36 | "SpecificEnum1": { 37 | "type": "object", 38 | "description": "https://github.com/acacode/swagger-typescript-api/issues/445", 39 | "properties": { 40 | "myEnum": { 41 | "enum": [["foo", "bar", "baz"], ["foo", "bar", "baz"]] 42 | } 43 | }, 44 | "additionalProperties": false 45 | }, 46 | "SpecificEnum2": { 47 | "type": "object", 48 | "description": "https://github.com/acacode/swagger-typescript-api/issues/445", 49 | "properties": { 50 | "myEnum": { 51 | "enum": [["foo", "bar", "baz"], ["foo", "bar", "bad"]] 52 | } 53 | }, 54 | "additionalProperties": false 55 | }, 56 | "Test1": { 57 | "type": "object", 58 | "required": ["id", "name", "tag", "multiple"], 59 | "nullable": true, 60 | "title": "It is a Pet title", 61 | "description": "It is a Pet description", 62 | "properties": { 63 | "id": { 64 | "type": "integer", 65 | "title": "It is an id title", 66 | "description": "It is an id description" 67 | }, 68 | "name": { 69 | "type": "string", 70 | "title": "It is a name title", 71 | "description": "It is a name description" 72 | }, 73 | "tag": { 74 | "type": "string", 75 | "deprecated": true, 76 | "title": "It is a tag title", 77 | "description": "It is a tag description" 78 | }, 79 | "multiple": { 80 | "type": ["string", "number"], 81 | "title": "It is a multiple title", 82 | "description": "It is a multiple description" 83 | } 84 | } 85 | }, 86 | "AdditionalObjectProperties": { 87 | "type": "object", 88 | "properties": { 89 | "id": { 90 | "type": "string" 91 | } 92 | }, 93 | "additionalProperties": { 94 | "type": "object" 95 | } 96 | }, 97 | "AdditionalIntProperties": { 98 | "type": "object", 99 | "properties": { 100 | "id": { 101 | "type": "string" 102 | } 103 | }, 104 | "additionalProperties": { 105 | "type": "integer", 106 | "format": "int32" 107 | } 108 | }, 109 | "ABCOptional": { 110 | "required": [], 111 | "properties": { 112 | "a": { 113 | "type": "string", 114 | "description": "a" 115 | }, 116 | "b": { 117 | "type": "string", 118 | "description": "b" 119 | }, 120 | "c": { 121 | "type": "integer", 122 | "description": "c" 123 | } 124 | } 125 | }, 126 | "ABCOptionalWithRequiredId": { 127 | "required": ["id", "b"], 128 | "properties": { 129 | "id": { 130 | "type": "integer", 131 | "description": "id" 132 | } 133 | }, 134 | "allOf": [ 135 | { 136 | "$ref": "#/definitions/ABCOptional" 137 | } 138 | ] 139 | }, 140 | "NestedObjectWithRequiredId": { 141 | "required": ["id"], 142 | "properties": { 143 | "id1": { 144 | "type": "integer", 145 | "description": "id" 146 | } 147 | }, 148 | "allOf": [ 149 | { 150 | "properties": { 151 | "id": { 152 | "type": "integer", 153 | "description": "id" 154 | } 155 | } 156 | } 157 | ] 158 | } 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /tests/spec/on-insert-path-param/basic.test.ts: -------------------------------------------------------------------------------- 1 | import * as fs from "node:fs/promises"; 2 | import * as os from "node:os"; 3 | import * as path from "node:path"; 4 | 5 | import { afterAll, beforeAll, describe, expect, test } from "vitest"; 6 | 7 | import { generateApi } from "../../../src/index.js"; 8 | 9 | describe("basic", async () => { 10 | let tmpdir = ""; 11 | 12 | beforeAll(async () => { 13 | tmpdir = await fs.mkdtemp(path.join(os.tmpdir(), "swagger-typescript-api")); 14 | }); 15 | 16 | afterAll(async () => { 17 | await fs.rm(tmpdir, { recursive: true }); 18 | }); 19 | 20 | test("on-insert-path-param", async () => { 21 | await generateApi({ 22 | fileName: "schema", 23 | input: path.resolve(import.meta.dirname, "schema.json"), 24 | output: tmpdir, 25 | silent: true, 26 | generateClient: true, 27 | hooks: { 28 | onInsertPathParam: (paramName) => `encodeURIComponent(${paramName})`, 29 | }, 30 | }); 31 | 32 | const content = await fs.readFile(path.join(tmpdir, "schema.ts"), { 33 | encoding: "utf8", 34 | }); 35 | 36 | expect(content).toMatchSnapshot(); 37 | }); 38 | }); 39 | -------------------------------------------------------------------------------- /tests/spec/on-insert-path-param/schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "swagger": "2.0", 3 | "info": { 4 | "version": "1.0.0", 5 | "title": "Swagger Petstore", 6 | "description": "A sample API that uses a petstore as an example to demonstrate features in the swagger-2.0 specification", 7 | "termsOfService": "http://swagger.io/terms/", 8 | "contact": { 9 | "name": "Swagger API Team" 10 | }, 11 | "license": { 12 | "name": "MIT" 13 | } 14 | }, 15 | "host": "petstore.swagger.io", 16 | "basePath": "/api", 17 | "schemes": ["http"], 18 | "consumes": ["application/json"], 19 | "produces": ["application/json"], 20 | "paths": { 21 | "/wrong-path-params1/{pathParam1}/{path_param2}/{path_param3}/:pathParam4": { 22 | "delete": { 23 | "description": "DDD", 24 | "operationId": "wrong-path-params1", 25 | "parameters": [], 26 | "produces": ["application/json"], 27 | "responses": { 28 | "200": { 29 | "description": "Successfully deleted" 30 | } 31 | }, 32 | "tags": ["key", "delete"] 33 | } 34 | }, 35 | "/checks": { 36 | "get": { 37 | "tags": ["check"], 38 | "summary": "Get Checks", 39 | "description": "

description

", 40 | "operationId": "get_checks_check_get", 41 | "parameters": [ 42 | { 43 | "required": false, 44 | "schema": { 45 | "title": "Page", 46 | "minimum": 1.0, 47 | "type": "integer", 48 | "default": 1 49 | }, 50 | "name": "page", 51 | "in": "query" 52 | }, 53 | { 54 | "required": false, 55 | "schema": { 56 | "title": "Size", 57 | "type": "integer", 58 | "default": 1 59 | }, 60 | "name": "size", 61 | "in": "query" 62 | } 63 | ], 64 | "responses": { 65 | "200": { 66 | "description": "Successful Response", 67 | "content": { 68 | "application/json": { 69 | "schema": { 70 | "$ref": "#/components/schemas/BarBaz" 71 | } 72 | } 73 | } 74 | }, 75 | "422": { 76 | "description": "Validation Error", 77 | "content": { 78 | "application/json": { 79 | "schema": { 80 | "$ref": "#/components/schemas/Foobar" 81 | } 82 | } 83 | } 84 | } 85 | }, 86 | "security": [ 87 | { 88 | "OAuth2PasswordBearer": [] 89 | } 90 | ] 91 | } 92 | } 93 | }, 94 | "definitions": {} 95 | } 96 | -------------------------------------------------------------------------------- /tests/spec/patch/basic.test.ts: -------------------------------------------------------------------------------- 1 | import * as fs from "node:fs/promises"; 2 | import * as os from "node:os"; 3 | import * as path from "node:path"; 4 | 5 | import { afterAll, beforeAll, describe, expect, test } from "vitest"; 6 | 7 | import { generateApi } from "../../../src/index.js"; 8 | 9 | describe("basic", async () => { 10 | let tmpdir = ""; 11 | 12 | beforeAll(async () => { 13 | tmpdir = await fs.mkdtemp(path.join(os.tmpdir(), "swagger-typescript-api")); 14 | }); 15 | 16 | afterAll(async () => { 17 | await fs.rm(tmpdir, { recursive: true }); 18 | }); 19 | 20 | test("--patch", async () => { 21 | await generateApi({ 22 | fileName: "schema", 23 | input: path.resolve(import.meta.dirname, "schema.json"), 24 | output: tmpdir, 25 | silent: true, 26 | patch: true, 27 | }); 28 | 29 | const content = await fs.readFile(path.join(tmpdir, "schema.ts"), { 30 | encoding: "utf8", 31 | }); 32 | 33 | expect(content).toMatchSnapshot(); 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /tests/spec/readonly/__snapshots__/basic.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[`basic > readonly 1`] = ` 4 | "/* eslint-disable */ 5 | /* tslint:disable */ 6 | // @ts-nocheck 7 | /* 8 | * --------------------------------------------------------------- 9 | * ## THIS FILE WAS GENERATED VIA SWAGGER-TYPESCRIPT-API ## 10 | * ## ## 11 | * ## AUTHOR: acacode ## 12 | * ## SOURCE: https://github.com/acacode/swagger-typescript-api ## 13 | * --------------------------------------------------------------- 14 | */ 15 | 16 | export interface Pet { 17 | /** @format int64 */ 18 | readonly id: number; 19 | readonly name: string; 20 | readonly tag?: string; 21 | readonly multiple?: string | number; 22 | } 23 | " 24 | `; 25 | -------------------------------------------------------------------------------- /tests/spec/readonly/basic.test.ts: -------------------------------------------------------------------------------- 1 | import * as fs from "node:fs/promises"; 2 | import * as os from "node:os"; 3 | import * as path from "node:path"; 4 | 5 | import { afterAll, beforeAll, describe, expect, test } from "vitest"; 6 | 7 | import { generateApi } from "../../../src/index.js"; 8 | 9 | describe("basic", async () => { 10 | let tmpdir = ""; 11 | 12 | beforeAll(async () => { 13 | tmpdir = await fs.mkdtemp(path.join(os.tmpdir(), "swagger-typescript-api")); 14 | }); 15 | 16 | afterAll(async () => { 17 | await fs.rm(tmpdir, { recursive: true }); 18 | }); 19 | 20 | test("readonly", async () => { 21 | await generateApi({ 22 | fileName: "schema", 23 | input: path.resolve(import.meta.dirname, "schema.json"), 24 | output: tmpdir, 25 | silent: true, 26 | addReadonly: true, 27 | generateClient: false, 28 | }); 29 | 30 | const content = await fs.readFile(path.join(tmpdir, "schema.ts"), { 31 | encoding: "utf8", 32 | }); 33 | 34 | expect(content).toMatchSnapshot(); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /tests/spec/readonly/schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "swagger": "2.0", 3 | "info": { 4 | "version": "1.0.0", 5 | "title": "Swagger Petstore", 6 | "description": "A sample API that uses a petstore as an example to demonstrate features in the swagger-2.0 specification", 7 | "termsOfService": "http://swagger.io/terms/", 8 | "contact": { 9 | "name": "Swagger API Team" 10 | }, 11 | "license": { 12 | "name": "MIT" 13 | } 14 | }, 15 | "host": "petstore.swagger.io", 16 | "basePath": "/api", 17 | "schemes": ["http"], 18 | "consumes": ["application/json"], 19 | "produces": ["application/json"], 20 | "paths": { 21 | "/pets": { 22 | "get": { 23 | "description": "Returns all pets from the system that the user has access to", 24 | "produces": ["application/json"], 25 | "responses": { 26 | "200": { 27 | "description": "A list of pets.", 28 | "schema": { 29 | "type": "array", 30 | "items": { 31 | "$ref": "#/definitions/Pet" 32 | } 33 | } 34 | } 35 | } 36 | } 37 | } 38 | }, 39 | "definitions": { 40 | "Pet": { 41 | "type": "object", 42 | "required": ["id", "name"], 43 | "properties": { 44 | "id": { 45 | "type": "integer", 46 | "readOnly": true, 47 | "format": "int64" 48 | }, 49 | "name": { 50 | "type": "string", 51 | "readOnly": true 52 | }, 53 | "tag": { 54 | "type": "string", 55 | "readOnly": true 56 | }, 57 | "multiple": { 58 | "type": ["string", "number"], 59 | "readOnly": true 60 | } 61 | } 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /tests/spec/responses/basic.test.ts: -------------------------------------------------------------------------------- 1 | import * as fs from "node:fs/promises"; 2 | import * as os from "node:os"; 3 | import * as path from "node:path"; 4 | 5 | import { afterAll, beforeAll, describe, expect, test } from "vitest"; 6 | 7 | import { generateApi } from "../../../src/index.js"; 8 | 9 | describe("basic", async () => { 10 | let tmpdir = ""; 11 | 12 | beforeAll(async () => { 13 | tmpdir = await fs.mkdtemp(path.join(os.tmpdir(), "swagger-typescript-api")); 14 | }); 15 | 16 | afterAll(async () => { 17 | await fs.rm(tmpdir, { recursive: true }); 18 | }); 19 | 20 | test("responses", async () => { 21 | await generateApi({ 22 | fileName: "schema", 23 | input: path.resolve(import.meta.dirname, "schema.json"), 24 | output: tmpdir, 25 | silent: true, 26 | generateResponses: true, 27 | }); 28 | 29 | const content = await fs.readFile(path.join(tmpdir, "schema.ts"), { 30 | encoding: "utf8", 31 | }); 32 | 33 | expect(content).toMatchSnapshot(); 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /tests/spec/routeTypes/basic.test.ts: -------------------------------------------------------------------------------- 1 | import * as fs from "node:fs/promises"; 2 | import * as os from "node:os"; 3 | import * as path from "node:path"; 4 | 5 | import { afterAll, beforeAll, describe, expect, test } from "vitest"; 6 | 7 | import { generateApi } from "../../../src/index.js"; 8 | 9 | describe("basic", async () => { 10 | let tmpdir = ""; 11 | 12 | beforeAll(async () => { 13 | tmpdir = await fs.mkdtemp(path.join(os.tmpdir(), "swagger-typescript-api")); 14 | }); 15 | 16 | afterAll(async () => { 17 | await fs.rm(tmpdir, { recursive: true }); 18 | }); 19 | 20 | test("--route-types", async () => { 21 | await generateApi({ 22 | fileName: "schema", 23 | input: path.resolve(import.meta.dirname, "schema.json"), 24 | output: tmpdir, 25 | silent: true, 26 | generateClient: false, 27 | generateRouteTypes: true, 28 | }); 29 | 30 | const content = await fs.readFile(path.join(tmpdir, "schema.ts"), { 31 | encoding: "utf8", 32 | }); 33 | 34 | expect(content).toMatchSnapshot(); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /tests/spec/singleHttpClient/basic.test.ts: -------------------------------------------------------------------------------- 1 | import * as fs from "node:fs/promises"; 2 | import * as os from "node:os"; 3 | import * as path from "node:path"; 4 | 5 | import { afterAll, beforeAll, describe, expect, test } from "vitest"; 6 | 7 | import { generateApi } from "../../../src/index.js"; 8 | 9 | describe("basic", async () => { 10 | let tmpdir = ""; 11 | 12 | beforeAll(async () => { 13 | tmpdir = await fs.mkdtemp(path.join(os.tmpdir(), "swagger-typescript-api")); 14 | }); 15 | 16 | afterAll(async () => { 17 | await fs.rm(tmpdir, { recursive: true }); 18 | }); 19 | 20 | test("--single-http-client", async () => { 21 | await generateApi({ 22 | fileName: "schema", 23 | input: path.resolve(import.meta.dirname, "schema.json"), 24 | output: tmpdir, 25 | silent: true, 26 | singleHttpClient: true, 27 | }); 28 | 29 | const content = await fs.readFile(path.join(tmpdir, "schema.ts"), { 30 | encoding: "utf8", 31 | }); 32 | 33 | expect(content).toMatchSnapshot(); 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /tests/spec/singleHttpClient/schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "swagger": "2.0", 3 | "schemes": ["https"], 4 | "host": "6-dot-authentiqio.appspot.com", 5 | "basePath": "/", 6 | "info": { 7 | "contact": { 8 | "email": "hello@authentiq.com", 9 | "name": "Authentiq team", 10 | "url": "http://authentiq.io/support" 11 | }, 12 | "description": "Strong authentication, without the passwords.", 13 | "license": { 14 | "name": "Apache 2.0", 15 | "url": "http://www.apache.org/licenses/LICENSE-2.0.html" 16 | }, 17 | "termsOfService": "http://authentiq.com/terms/", 18 | "title": "Authentiq", 19 | "version": "6", 20 | "x-apisguru-categories": ["security"], 21 | "x-logo": { 22 | "backgroundColor": "#F26641", 23 | "url": "https://api.apis.guru/v2/cache/logo/https_www.authentiq.com_theme_images_authentiq-logo-a-inverse.svg" 24 | }, 25 | "x-origin": [ 26 | { 27 | "format": "swagger", 28 | "url": "https://raw.githubusercontent.com/AuthentiqID/authentiq-docs/master/docs/swagger/issuer.yaml", 29 | "version": "2.0" 30 | } 31 | ], 32 | "x-preferred": true, 33 | "x-providerName": "6-dot-authentiqio.appspot.com" 34 | }, 35 | "parameters": { 36 | "AuthentiqID": { 37 | "description": "Authentiq ID to register", 38 | "in": "body", 39 | "name": "body", 40 | "required": true, 41 | "schema": { 42 | "$ref": "#/definitions/AuthentiqID" 43 | } 44 | }, 45 | "JobID": { 46 | "description": "Job ID (20 chars)", 47 | "in": "path", 48 | "name": "job", 49 | "required": true, 50 | "type": "string" 51 | }, 52 | "PK": { 53 | "description": "Public Signing Key - Authentiq ID (43 chars)", 54 | "in": "path", 55 | "name": "PK", 56 | "required": true, 57 | "type": "string" 58 | }, 59 | "PushToken": { 60 | "description": "Push Token.", 61 | "in": "body", 62 | "name": "body", 63 | "required": true, 64 | "schema": { 65 | "$ref": "#/definitions/PushToken" 66 | } 67 | }, 68 | "Scope": { 69 | "description": "Claims of scope", 70 | "in": "body", 71 | "name": "body", 72 | "required": true, 73 | "schema": { 74 | "$ref": "#/definitions/Claims" 75 | } 76 | } 77 | }, 78 | "responses": { 79 | "ErrorResponse": { 80 | "description": "Error response", 81 | "schema": { 82 | "$ref": "#/definitions/Error" 83 | } 84 | } 85 | }, 86 | "paths": { 87 | "/key": { 88 | "post": { 89 | "consumes": ["application/jwt"], 90 | "description": "Register a new ID `JWT(sub, devtoken)`\n\nv5: `JWT(sub, pk, devtoken, ...)`\n\nSee: https://github.com/skion/authentiq/wiki/JWT-Examples\n", 91 | "operationId": "key_register", 92 | "parameters": [ 93 | { 94 | "$ref": "#/parameters/AuthentiqID" 95 | } 96 | ], 97 | "produces": ["application/json"], 98 | "responses": { 99 | "201": { 100 | "description": "Successfully registered", 101 | "schema": { 102 | "properties": { 103 | "secret": { 104 | "description": "revoke key", 105 | "type": "string" 106 | }, 107 | "status": { 108 | "description": "registered", 109 | "type": "string" 110 | } 111 | }, 112 | "type": "object" 113 | } 114 | }, 115 | "409": { 116 | "description": "Key already registered `duplicate-key`", 117 | "schema": { 118 | "$ref": "#/definitions/Error" 119 | } 120 | }, 121 | "default": { 122 | "$ref": "#/responses/ErrorResponse" 123 | } 124 | }, 125 | "tags": ["key", "post"] 126 | } 127 | } 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /tests/spec/sortTypes-false/basic.test.ts: -------------------------------------------------------------------------------- 1 | import * as fs from "node:fs/promises"; 2 | import * as os from "node:os"; 3 | import * as path from "node:path"; 4 | 5 | import { afterAll, beforeAll, describe, expect, test } from "vitest"; 6 | 7 | import { generateApi } from "../../../src/index.js"; 8 | 9 | describe("basic", async () => { 10 | let tmpdir = ""; 11 | 12 | beforeAll(async () => { 13 | tmpdir = await fs.mkdtemp(path.join(os.tmpdir(), "swagger-typescript-api")); 14 | }); 15 | 16 | afterAll(async () => { 17 | await fs.rm(tmpdir, { recursive: true }); 18 | }); 19 | 20 | test("--sort-types=false", async () => { 21 | await generateApi({ 22 | fileName: "schema", 23 | input: path.resolve(import.meta.dirname, "schema.json"), 24 | output: tmpdir, 25 | silent: true, 26 | generateClient: true, 27 | sortTypes: false, 28 | }); 29 | 30 | const content = await fs.readFile(path.join(tmpdir, "schema.ts"), { 31 | encoding: "utf8", 32 | }); 33 | 34 | expect(content).toMatchSnapshot(); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /tests/spec/sortTypes/basic.test.ts: -------------------------------------------------------------------------------- 1 | import * as fs from "node:fs/promises"; 2 | import * as os from "node:os"; 3 | import * as path from "node:path"; 4 | 5 | import { afterAll, beforeAll, describe, expect, test } from "vitest"; 6 | 7 | import { generateApi } from "../../../src/index.js"; 8 | 9 | describe("basic", async () => { 10 | let tmpdir = ""; 11 | 12 | beforeAll(async () => { 13 | tmpdir = await fs.mkdtemp(path.join(os.tmpdir(), "swagger-typescript-api")); 14 | }); 15 | 16 | afterAll(async () => { 17 | await fs.rm(tmpdir, { recursive: true }); 18 | }); 19 | 20 | test("--sort-types", async () => { 21 | await generateApi({ 22 | fileName: "schema", 23 | input: path.resolve(import.meta.dirname, "schema.json"), 24 | output: tmpdir, 25 | silent: true, 26 | generateClient: true, 27 | sortTypes: true, 28 | }); 29 | 30 | const content = await fs.readFile(path.join(tmpdir, "schema.ts"), { 31 | encoding: "utf8", 32 | }); 33 | 34 | expect(content).toMatchSnapshot(); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /tests/spec/specProperty/basic.test.ts: -------------------------------------------------------------------------------- 1 | import * as fs from "node:fs/promises"; 2 | import * as os from "node:os"; 3 | import * as path from "node:path"; 4 | 5 | import { afterAll, beforeAll, describe, expect, test } from "vitest"; 6 | 7 | import { generateApi } from "../../../src/index.js"; 8 | 9 | describe("basic", async () => { 10 | let tmpdir = ""; 11 | 12 | beforeAll(async () => { 13 | tmpdir = await fs.mkdtemp(path.join(os.tmpdir(), "swagger-typescript-api")); 14 | }); 15 | 16 | afterAll(async () => { 17 | await fs.rm(tmpdir, { recursive: true }); 18 | }); 19 | 20 | test("specProperty", async () => { 21 | await generateApi({ 22 | fileName: "schema", 23 | input: path.resolve(import.meta.dirname, "schema.json"), 24 | output: tmpdir, 25 | silent: true, 26 | generateClient: false, 27 | generateRouteTypes: true, 28 | }); 29 | 30 | const content = await fs.readFile(path.join(tmpdir, "schema.ts"), { 31 | encoding: "utf8", 32 | }); 33 | 34 | expect(content).toMatchSnapshot(); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /tests/spec/typeSuffixPrefix/basic.test.ts: -------------------------------------------------------------------------------- 1 | import * as fs from "node:fs/promises"; 2 | import * as os from "node:os"; 3 | import * as path from "node:path"; 4 | 5 | import { afterAll, beforeAll, describe, expect, test } from "vitest"; 6 | 7 | import { generateApi } from "../../../src/index.js"; 8 | 9 | describe("basic", async () => { 10 | let tmpdir = ""; 11 | 12 | beforeAll(async () => { 13 | tmpdir = await fs.mkdtemp(path.join(os.tmpdir(), "swagger-typescript-api")); 14 | }); 15 | 16 | afterAll(async () => { 17 | await fs.rm(tmpdir, { recursive: true }); 18 | }); 19 | 20 | test("--type-prefix and --type-suffix", async () => { 21 | await generateApi({ 22 | fileName: "schema", 23 | input: path.resolve(import.meta.dirname, "schema.json"), 24 | output: tmpdir, 25 | silent: true, 26 | generateClient: true, 27 | generateResponses: true, 28 | typePrefix: "SwaggerType", 29 | typeSuffix: "GeneratedDataContract", 30 | }); 31 | 32 | const content = await fs.readFile(path.join(tmpdir, "schema.ts"), { 33 | encoding: "utf8", 34 | }); 35 | 36 | expect(content).toMatchSnapshot(); 37 | }); 38 | }); 39 | -------------------------------------------------------------------------------- /tests/spec/unionEnums/basic.test.ts: -------------------------------------------------------------------------------- 1 | import * as fs from "node:fs/promises"; 2 | import * as os from "node:os"; 3 | import * as path from "node:path"; 4 | 5 | import { afterAll, beforeAll, describe, expect, test } from "vitest"; 6 | 7 | import { generateApi } from "../../../src/index.js"; 8 | 9 | describe("basic", async () => { 10 | let tmpdir = ""; 11 | 12 | beforeAll(async () => { 13 | tmpdir = await fs.mkdtemp(path.join(os.tmpdir(), "swagger-typescript-api")); 14 | }); 15 | 16 | afterAll(async () => { 17 | await fs.rm(tmpdir, { recursive: true }); 18 | }); 19 | 20 | test("--generate-union-enums", async () => { 21 | await generateApi({ 22 | fileName: "schema", 23 | input: path.resolve(import.meta.dirname, "schema.json"), 24 | output: tmpdir, 25 | silent: true, 26 | generateUnionEnums: true, 27 | }); 28 | 29 | const content = await fs.readFile(path.join(tmpdir, "schema.ts"), { 30 | encoding: "utf8", 31 | }); 32 | 33 | expect(content).toMatchSnapshot(); 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /tests/spec/unionEnums/schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "components": { 3 | "examples": {}, 4 | "headers": {}, 5 | "parameters": {}, 6 | "requestBodies": {}, 7 | "responses": {}, 8 | "schemas": { 9 | "StringEnum": { 10 | "enum": ["String1", "String2", "String3", "String4"], 11 | "type": "string" 12 | }, 13 | "NumberEnum": { 14 | "enum": [1, 2, 3, 4], 15 | "type": "number" 16 | }, 17 | "BooleanEnum": { 18 | "enum": ["true", "false"], 19 | "type": "boolean" 20 | }, 21 | "IntEnumWithNames": { 22 | "enum": [0, 1, 2, 3, 4, 5, 6, 7, 8, 9], 23 | "type": "integer", 24 | "description": "FooBar", 25 | "format": "int32", 26 | "x-enumNames": [ 27 | "Unknown", 28 | "String", 29 | "Int32", 30 | "Int64", 31 | "Double", 32 | "DateTime", 33 | "Test2", 34 | "Test23", 35 | "Tess44", 36 | "BooFar" 37 | ] 38 | } 39 | }, 40 | "securitySchemes": {} 41 | }, 42 | "info": { 43 | "title": "" 44 | }, 45 | "openapi": "3.0.0", 46 | "paths": {}, 47 | "servers": [ 48 | { 49 | "url": "http://localhost:8080/api/v1" 50 | } 51 | ] 52 | } 53 | -------------------------------------------------------------------------------- /tests/utils.ts: -------------------------------------------------------------------------------- 1 | import * as fs from "node:fs/promises"; 2 | import * as path from "node:path"; 3 | 4 | interface Schema { 5 | name: string; 6 | filePath: string; 7 | } 8 | 9 | export async function collectAllSchemas() { 10 | const schemas: Schema[] = []; 11 | const schemaPath = path.join(import.meta.dirname, "fixtures", "schemas"); 12 | const schemaFiles = await fs.readdir(schemaPath, { recursive: true }); 13 | for (const schemaFile of schemaFiles) { 14 | if ( 15 | schemaFile.endsWith(".json") || 16 | schemaFile.endsWith(".yaml") || 17 | schemaFile.endsWith(".yml") 18 | ) { 19 | const name = path.basename(schemaFile).replace(/\.(json|yaml|yml)$/, ""); 20 | const filePath = path.join(schemaPath, schemaFile); 21 | schemas.push({ name, filePath }); 22 | } 23 | } 24 | return schemas; 25 | } 26 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "@tsconfig/strictest/tsconfig.json", 4 | "@tsconfig/node18/tsconfig.json" 5 | ], 6 | "compilerOptions": { 7 | "exactOptionalPropertyTypes": false, 8 | "module": "NodeNext", 9 | "moduleResolution": "NodeNext", 10 | "noEmit": true, 11 | "noPropertyAccessFromIndexSignature": false, 12 | "resolveJsonModule": true 13 | }, 14 | "exclude": ["dist"] 15 | } 16 | -------------------------------------------------------------------------------- /tsdown.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "tsdown"; 2 | 3 | export default defineConfig({ 4 | entry: { 5 | lib: "src/index.ts", 6 | cli: "index.ts", 7 | }, 8 | format: ["esm", "cjs"], 9 | sourcemap: true, 10 | }); 11 | -------------------------------------------------------------------------------- /vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vitest/config"; 2 | 3 | export default defineConfig({ 4 | test: { 5 | testTimeout: 10000, 6 | typecheck: { 7 | enabled: true, 8 | }, 9 | }, 10 | }); 11 | --------------------------------------------------------------------------------