feat(codegen): openapi-typescript-codegen fork (#3272)
## What Allow `medusa-oas` CLI to generate API client code from an OAS json file. ## Why Manually maintaining API clients is time consuming and error prone. We wish to automate the process by using code generation strategies. We also wish to eliminate direct import from `@medusajs/medusa` in clients which can lead to unwanted side effects like importing unnecessary dependencies. ## How Fork and customize an OAS code generator library that is TypeScript friendly. Attempt to match the interface and signature of our current client packages: `@medusajs/medusa-js`, `medusa-react` Add a new `client` command to the `medusa-oas` CLI as the main interface to interact with the code generation tooling. ## Test ### Prerequisites * From the root of the monorepo: * `yarn install` * `yarn build` ### Case - all in one build * From the root of the monorepo: * Run `yarn medusa-oas oas --out-dir ~/tmp/oas --type store` * Run `yarn medusa-oas client --src-file ~/tmp/oas/store.oas.json --out-dir ~/tmp/client-store --type store` * Expect `~/tmp/client-store` to contain the following files and directories: ``` core/ hooks/ models/ services/ index.ts MedusaStore.ts useMedusaStore.tsx ``` ### Case - types only * From the root of the monorepo: * Run `yarn medusa-oas oas --out-dir ~/tmp/oas --type store` * Run `yarn medusa-oas client --src-file ~/tmp/oas/store.oas.json --out-dir ~/tmp/client-types --type store --component types` * Expect `~/tmp/client-types` to contain the following files and directories: ``` models/ index.ts ``` ### Case - client only * From the root of the monorepo: * Run `yarn medusa-oas oas --out-dir ~/tmp/oas --type store` * Run `yarn medusa-oas client --src-file ~/tmp/oas/store.oas.json --out-dir ~/tmp/client-only --type store --component client --types-package @medusajs/client-types` * Expect `~/tmp/client-only` to contain the following files and directories: ``` core/ services/ index.ts MedusaStore.ts ```
This commit is contained in:
1 parent
6c04624727
commit
1c40346e9e
224 files changed
+7695
-127
No files matched your search
@@ -0,0 +1,6 @@
|
|||||||
|
---
|
||||||
|
"@medusajs/openapi-typescript-codegen": patch
|
||||||
|
"@medusajs/medusa-oas-cli": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
feat(codegen): openapi-typescript-codegen fork
|
||||||
@@ -12,7 +12,6 @@ const fallback = (filename: string) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Polyfill Node's `Module.createRequireFromPath` if not present (added in Node v10.12.0)
|
// Polyfill Node's `Module.createRequireFromPath` if not present (added in Node v10.12.0)
|
||||||
const createRequireFromPath =
|
const createRequireFromPath = Module.createRequire || fallback
|
||||||
Module.createRequire || Module.createRequireFromPath || fallback
|
|
||||||
|
|
||||||
export default createRequireFromPath
|
export default createRequireFromPath
|
||||||
@@ -19,6 +19,8 @@ N/A
|
|||||||
yarn medusa-oas <command>
|
yarn medusa-oas <command>
|
||||||
```
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
### Command - `oas`
|
### Command - `oas`
|
||||||
|
|
||||||
This command will scan the `@medusajs/medusa` package in order to extract JSDoc OAS into a json file.
|
This command will scan the `@medusajs/medusa` package in order to extract JSDoc OAS into a json file.
|
||||||
@@ -70,3 +72,51 @@ Ignore OAS errors and attempt to output generated OAS files.
|
|||||||
```bash
|
```bash
|
||||||
yarn medusa-oas oas --force
|
yarn medusa-oas oas --force
|
||||||
```
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Command - `client`
|
||||||
|
|
||||||
|
Will generate API client files from a given OAS file.
|
||||||
|
|
||||||
|
#### `--src-file <path>`
|
||||||
|
|
||||||
|
Specify the path to the OAS JSON file.
|
||||||
|
|
||||||
|
`yarm medusa-oas client --src-file ./store.oas.json`
|
||||||
|
|
||||||
|
#### `--name <name>`
|
||||||
|
|
||||||
|
Namespace for the generated client. Usually `admin` or `store`.
|
||||||
|
|
||||||
|
`yarm medusa-oas client --name admin`
|
||||||
|
|
||||||
|
#### `--out-dir <path>`
|
||||||
|
|
||||||
|
Specify in which directory should the files be outputted. Accepts relative and absolute path. It the directory doesn't
|
||||||
|
exist, it will be created. Defaults to `./`.
|
||||||
|
|
||||||
|
`yarm medusa-oas client --out-dir ./client`
|
||||||
|
|
||||||
|
#### `--type <type>`
|
||||||
|
|
||||||
|
Client component types to generate. Accepts `all`, `types`, `client`, `hooks`.
|
||||||
|
Defaults to `all`.
|
||||||
|
|
||||||
|
`yarn medusa-oas client --type types`
|
||||||
|
|
||||||
|
#### `--types-packages <name>`
|
||||||
|
|
||||||
|
Replace relative import statements by types package name. Mandatory when using `--type client` or `--type hooks`.
|
||||||
|
|
||||||
|
#### `--client-packages <name>`
|
||||||
|
|
||||||
|
Replace relative import statements by client package name. Mandatory when using `--type hooks`.
|
||||||
|
|
||||||
|
`yarn medusa-oas client --type types`
|
||||||
|
|
||||||
|
#### `--clean`
|
||||||
|
|
||||||
|
Delete destination directory content before generating client.
|
||||||
|
|
||||||
|
`yarn medusa-oas --clean`
|
||||||
@@ -32,8 +32,11 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@medusajs/medusa": "*",
|
"@medusajs/medusa": "*",
|
||||||
|
"@medusajs/openapi-typescript-codegen": "*",
|
||||||
"@readme/openapi-parser": "^2.4.0",
|
"@readme/openapi-parser": "^2.4.0",
|
||||||
|
"@types/lodash": "^4.14.191",
|
||||||
"commander": "^10.0.0",
|
"commander": "^10.0.0",
|
||||||
|
"lodash": "^4.17.21",
|
||||||
"openapi3-ts": "^3.1.2",
|
"openapi3-ts": "^3.1.2",
|
||||||
"swagger-inline": "^6.1.0"
|
"swagger-inline": "^6.1.0"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,175 @@
|
|||||||
|
import path from "path"
|
||||||
|
import {
|
||||||
|
generate,
|
||||||
|
HttpClient,
|
||||||
|
Indent,
|
||||||
|
PackageNames,
|
||||||
|
} from "@medusajs/openapi-typescript-codegen"
|
||||||
|
import { upperFirst } from "lodash"
|
||||||
|
import fs, { mkdir, readFile } from "fs/promises"
|
||||||
|
import { OpenAPIObject } from "openapi3-ts"
|
||||||
|
import { Command, Option, OptionValues } from "commander"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* CLI Command declaration
|
||||||
|
*/
|
||||||
|
export const commandName = "client"
|
||||||
|
export const commandDescription = "Generate API clients from OAS."
|
||||||
|
export const commandOptions: Option[] = [
|
||||||
|
new Option(
|
||||||
|
"-t, --type <type>",
|
||||||
|
"Namespace for the generated client. Usually `admin` or `store`."
|
||||||
|
).makeOptionMandatory(),
|
||||||
|
|
||||||
|
new Option(
|
||||||
|
"-s, --src-file <srcFile>",
|
||||||
|
"Path to source OAS JSON file."
|
||||||
|
).makeOptionMandatory(),
|
||||||
|
|
||||||
|
new Option(
|
||||||
|
"-o, --out-dir <outDir>",
|
||||||
|
"Output directory for generated client files."
|
||||||
|
).default(path.resolve(process.cwd(), "client")),
|
||||||
|
|
||||||
|
new Option(
|
||||||
|
"-c, --component <component>",
|
||||||
|
"Client component types to generate."
|
||||||
|
)
|
||||||
|
.choices(["all", "types", "client", "hooks"])
|
||||||
|
.default("all"),
|
||||||
|
|
||||||
|
new Option(
|
||||||
|
"--types-package <name>",
|
||||||
|
"Replace relative import statements by types package name."
|
||||||
|
),
|
||||||
|
|
||||||
|
new Option(
|
||||||
|
"--client-package <name>",
|
||||||
|
"Replace relative import statements by client package name."
|
||||||
|
),
|
||||||
|
|
||||||
|
new Option(
|
||||||
|
"--clean",
|
||||||
|
"Delete destination directory content before generating client."
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
export function getCommand() {
|
||||||
|
const command = new Command(commandName)
|
||||||
|
command.description(commandDescription)
|
||||||
|
for (const opt of commandOptions) {
|
||||||
|
command.addOption(opt)
|
||||||
|
}
|
||||||
|
command.action(async (options) => await execute(options))
|
||||||
|
return command
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Main
|
||||||
|
*/
|
||||||
|
export async function execute(cliParams: OptionValues) {
|
||||||
|
/**
|
||||||
|
* Process CLI options
|
||||||
|
*/
|
||||||
|
if (
|
||||||
|
["client", "hooks"].includes(cliParams.component) &&
|
||||||
|
!cliParams.typesPackage
|
||||||
|
) {
|
||||||
|
throw new Error(
|
||||||
|
`--types-package must be declared when using --component=${cliParams.component}`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (cliParams.component === "hooks" && !cliParams.clientPackage) {
|
||||||
|
throw new Error(
|
||||||
|
`--client-package must be declared when using --component=${cliParams.component}`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const shouldClean = !!cliParams.clean
|
||||||
|
const srcFile = path.resolve(cliParams.srcFile)
|
||||||
|
const outDir = path.resolve(cliParams.outDir)
|
||||||
|
const apiName = cliParams.type
|
||||||
|
const packageNames: PackageNames = {
|
||||||
|
models: cliParams.typesPackage,
|
||||||
|
client: cliParams.clientPackage,
|
||||||
|
}
|
||||||
|
const exportComponent = cliParams.component
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Command execution
|
||||||
|
*/
|
||||||
|
console.log(`🟣 Generating client - ${apiName} - ${exportComponent}`)
|
||||||
|
|
||||||
|
if (shouldClean) {
|
||||||
|
console.log(`🟠 Cleaning output directory`)
|
||||||
|
await fs.rm(outDir, { recursive: true, force: true })
|
||||||
|
}
|
||||||
|
await mkdir(outDir, { recursive: true })
|
||||||
|
|
||||||
|
const oas = await getOASFromFile(srcFile)
|
||||||
|
await generateClientSDK(oas, outDir, apiName, exportComponent, packageNames)
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
`🟢 Client generated - ${apiName} - ${exportComponent} - ${outDir}`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Methods
|
||||||
|
*/
|
||||||
|
const getOASFromFile = async (jsonFile: string): Promise<OpenAPIObject> => {
|
||||||
|
const jsonString = await readFile(jsonFile, "utf8")
|
||||||
|
return JSON.parse(jsonString)
|
||||||
|
}
|
||||||
|
|
||||||
|
const generateClientSDK = async (
|
||||||
|
oas: OpenAPIObject,
|
||||||
|
targetDir: string,
|
||||||
|
apiName: string,
|
||||||
|
exportComponent: "all" | "types" | "client" | "hooks",
|
||||||
|
packageNames: PackageNames = {}
|
||||||
|
) => {
|
||||||
|
const exports = {
|
||||||
|
exportCore: false,
|
||||||
|
exportServices: false,
|
||||||
|
exportModels: false,
|
||||||
|
exportHooks: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (exportComponent) {
|
||||||
|
case "types":
|
||||||
|
exports.exportModels = true
|
||||||
|
break
|
||||||
|
case "client":
|
||||||
|
exports.exportCore = true
|
||||||
|
exports.exportServices = true
|
||||||
|
break
|
||||||
|
case "hooks":
|
||||||
|
exports.exportHooks = true
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
exports.exportCore = true
|
||||||
|
exports.exportServices = true
|
||||||
|
exports.exportModels = true
|
||||||
|
exports.exportHooks = true
|
||||||
|
}
|
||||||
|
|
||||||
|
await generate({
|
||||||
|
input: oas,
|
||||||
|
output: targetDir,
|
||||||
|
httpClient: HttpClient.AXIOS,
|
||||||
|
useOptions: true,
|
||||||
|
useUnionTypes: true,
|
||||||
|
exportCore: exports.exportCore,
|
||||||
|
exportServices: exports.exportServices,
|
||||||
|
exportModels: exports.exportModels,
|
||||||
|
exportHooks: exports.exportHooks,
|
||||||
|
exportSchemas: false,
|
||||||
|
indent: Indent.SPACE_2,
|
||||||
|
postfixServices: "Service",
|
||||||
|
postfixModels: "",
|
||||||
|
clientName: `Medusa${upperFirst(apiName)}`,
|
||||||
|
request: undefined,
|
||||||
|
packageNames,
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
import { Command } from "commander"
|
import { Command } from "commander"
|
||||||
import { getCommand as oasGetCommand } from "./command-oas"
|
import { getCommand as oasGetCommand } from "./command-oas"
|
||||||
|
import { getCommand as clientGetCommand } from "./command-client"
|
||||||
|
|
||||||
const run = async () => {
|
const run = async () => {
|
||||||
const program = new Command()
|
const program = new Command()
|
||||||
@@ -11,6 +12,11 @@ const run = async () => {
|
|||||||
*/
|
*/
|
||||||
program.addCommand(oasGetCommand())
|
program.addCommand(oasGetCommand())
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Alias to command-client.ts
|
||||||
|
*/
|
||||||
|
program.addCommand(clientGetCommand())
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Run CLI
|
* Run CLI
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
/dist
|
||||||
|
node_modules
|
||||||
|
.DS_store
|
||||||
|
.env*
|
||||||
|
.env
|
||||||
|
*.sql
|
||||||
|
/bin
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
*.hbs
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
# @medusajs/openapi-typescript-codegen
|
||||||
|
|
||||||
|
## 0.1.0
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
# OpenAPI Typescript Codegen - 0.1.0 - experimental
|
||||||
|
|
||||||
|
Node.js library that generates Typescript clients based on the OpenAPI specification.
|
||||||
|
|
||||||
|
## About this fork
|
||||||
|
|
||||||
|
This package is a significantly customized fork of
|
||||||
|
the amazing npm [openapi-typescript-codegen](https://github.com/ferdikoomen/openapi-typescript-codegen) package 💜.
|
||||||
|
|
||||||
|
### Brief reasoning
|
||||||
|
|
||||||
|
We wanted a level of customization that was not achievable through the source package's interface.
|
||||||
|
We started with a conventional fork but the development workflow was hindering our ability to iterate quickly.
|
||||||
|
We decided to fold the package within our monorepo to hopefully accelerate development and innovation.
|
||||||
|
|
||||||
|
### Noteworthy differences
|
||||||
|
|
||||||
|
* Added ability to generate React hooks in the style of `medusa-react`.
|
||||||
|
* Added access to the raw OAS on parsed element from the parser.
|
||||||
|
* Added access `operationId` from path.
|
||||||
|
* Added access `x-codegen` from path.
|
||||||
|
* Added renaming of service method when `x-codegen.method` is declared.
|
||||||
|
* Added bundling of query params into a typed object when `x-codegen.queryParams` is declared.
|
||||||
|
* Added parameterization of import path for the client and models.
|
||||||
|
* Added generated `index.ts` files in directories to simplify imports.
|
||||||
|
* Updated npm dependencies' version to match existing versions found in the monorepo.
|
||||||
|
* Updated coding style to match the convention of the monorepo.
|
||||||
|
* Removed support for Angular client generation.
|
||||||
|
* Removed support for OAS v2 parsing.
|
||||||
|
* Removed documentation.
|
||||||
|
* Removed tests (temporarily).
|
||||||
|
|
||||||
|
## Install
|
||||||
|
|
||||||
|
`yarn add --dev @medusajs/openapi-typescript-codegen`
|
||||||
|
|
||||||
|
## How to use
|
||||||
|
|
||||||
|
See `@medusajs/medusa-oas-cli` for usage.
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"presets": [
|
||||||
|
[
|
||||||
|
"@babel/preset-env",
|
||||||
|
{
|
||||||
|
"targets": {
|
||||||
|
"node": "12"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"@babel/preset-typescript",
|
||||||
|
{
|
||||||
|
"onlyRemoveTypeImports": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
{
|
||||||
|
"name": "@medusajs/openapi-typescript-codegen",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"description": "Library that generates Typescript clients based on the OpenAPI specification.",
|
||||||
|
"main": "dist/index.js",
|
||||||
|
"types": "types/index.d.ts",
|
||||||
|
"files": [
|
||||||
|
"dist/index.js",
|
||||||
|
"types/index.d.ts"
|
||||||
|
],
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/medusajs/medusa",
|
||||||
|
"directory": "packages/oas/openapi-typescript-codegen"
|
||||||
|
},
|
||||||
|
"author": "Medusa",
|
||||||
|
"contributors": [
|
||||||
|
"Ferdi Koomen"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"scripts": {
|
||||||
|
"prepare": "cross-env NODE_ENV=production yarn run release",
|
||||||
|
"build": "rollup --config --environment NODE_ENV:development",
|
||||||
|
"release": "rollup --config --environment NODE_ENV:production",
|
||||||
|
"test": "jest --passWithNoTests",
|
||||||
|
"test:unit": "jest --passWithNoTests"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"camelcase": "^6.3.0",
|
||||||
|
"commander": "^9.4.1",
|
||||||
|
"fs-extra": "^10.1.0",
|
||||||
|
"handlebars": "^4.7.7",
|
||||||
|
"json-schema-ref-parser": "^9.0.9",
|
||||||
|
"pascalcase": "^2.0.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@babel/cli": "7.14.3",
|
||||||
|
"@babel/core": "7.14.3",
|
||||||
|
"@babel/preset-env": "7.11.5",
|
||||||
|
"@babel/preset-typescript": "7.18.6",
|
||||||
|
"@rollup/plugin-commonjs": "24.0.0",
|
||||||
|
"@rollup/plugin-node-resolve": "15.0.1",
|
||||||
|
"@rollup/plugin-typescript": "9.0.2",
|
||||||
|
"@types/fs-extra": "^9.0.12",
|
||||||
|
"@types/jest": "27.5.0",
|
||||||
|
"@types/node": "18.11.9",
|
||||||
|
"@types/node-fetch": "2.6.2",
|
||||||
|
"@types/pascalcase": "^1.0.1",
|
||||||
|
"@types/qs": "6.9.7",
|
||||||
|
"abort-controller": "3.0.0",
|
||||||
|
"axios": "1.2.0",
|
||||||
|
"form-data": "4.0.0",
|
||||||
|
"jest": "26.6.3",
|
||||||
|
"jest-cli": "26.6.3",
|
||||||
|
"node-fetch": "2.6.7",
|
||||||
|
"qs": "6.10.3",
|
||||||
|
"rollup": "3.9.1",
|
||||||
|
"rollup-plugin-terser": "7.0.2",
|
||||||
|
"tslib": "2.3.1",
|
||||||
|
"typescript": "4.9.5"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
import commonjs from "@rollup/plugin-commonjs"
|
||||||
|
import { nodeResolve } from "@rollup/plugin-node-resolve"
|
||||||
|
import typescript from "@rollup/plugin-typescript"
|
||||||
|
import { readFileSync } from "fs"
|
||||||
|
import handlebars from "handlebars"
|
||||||
|
import { dirname, extname, resolve } from "path"
|
||||||
|
import { terser } from "rollup-plugin-terser"
|
||||||
|
|
||||||
|
const { precompile } = handlebars
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Custom plugin to parse handlebar imports and precompile
|
||||||
|
* the template on the fly. This reduces runtime by about
|
||||||
|
* half on large projects.
|
||||||
|
*/
|
||||||
|
const handlebarsPlugin = () => ({
|
||||||
|
resolveId: (file, importer) => {
|
||||||
|
if (extname(file) === ".hbs") {
|
||||||
|
return resolve(dirname(importer), file)
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
},
|
||||||
|
load: (file) => {
|
||||||
|
if (extname(file) === ".hbs") {
|
||||||
|
const template = readFileSync(file, "utf8").toString().trim()
|
||||||
|
const templateSpec = precompile(template, {
|
||||||
|
strict: true,
|
||||||
|
noEscape: true,
|
||||||
|
preventIndent: true,
|
||||||
|
knownHelpersOnly: true,
|
||||||
|
knownHelpers: {
|
||||||
|
ifdef: true,
|
||||||
|
equals: true,
|
||||||
|
notEquals: true,
|
||||||
|
containsSpaces: true,
|
||||||
|
union: true,
|
||||||
|
intersection: true,
|
||||||
|
enumerator: true,
|
||||||
|
escapeComment: true,
|
||||||
|
escapeDescription: true,
|
||||||
|
camelCase: true,
|
||||||
|
pascalCase: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
return `export default ${templateSpec};`
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const getPlugins = () => {
|
||||||
|
const plugins = [
|
||||||
|
nodeResolve(),
|
||||||
|
commonjs({
|
||||||
|
sourceMap: false,
|
||||||
|
}),
|
||||||
|
handlebarsPlugin(),
|
||||||
|
typescript({
|
||||||
|
module: "esnext",
|
||||||
|
}),
|
||||||
|
]
|
||||||
|
if (process.env.NODE_ENV === "development") {
|
||||||
|
return plugins
|
||||||
|
}
|
||||||
|
return [...plugins, terser()]
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
input: "./src/index.ts",
|
||||||
|
output: {
|
||||||
|
exports: "named",
|
||||||
|
file: "./dist/index.js",
|
||||||
|
format: "cjs",
|
||||||
|
},
|
||||||
|
external: [
|
||||||
|
"camelcase",
|
||||||
|
"commander",
|
||||||
|
"fs-extra",
|
||||||
|
"handlebars",
|
||||||
|
"json-schema-ref-parser",
|
||||||
|
],
|
||||||
|
plugins: getPlugins(),
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
export enum HttpClient {
|
||||||
|
FETCH = "fetch",
|
||||||
|
XHR = "xhr",
|
||||||
|
NODE = "node",
|
||||||
|
AXIOS = "axios",
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
export enum Indent {
|
||||||
|
SPACE_4 = "4",
|
||||||
|
SPACE_2 = "2",
|
||||||
|
TAB = "tab",
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import type { Model } from "./Model"
|
||||||
|
import type { Service } from "./Service"
|
||||||
|
|
||||||
|
export interface Client {
|
||||||
|
version: string
|
||||||
|
server: string
|
||||||
|
models: Model[]
|
||||||
|
services: Service[]
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
export interface Enum {
|
||||||
|
name: string
|
||||||
|
value: string
|
||||||
|
type: string
|
||||||
|
description: string | null
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import type { Enum } from "./Enum"
|
||||||
|
import type { Schema } from "./Schema"
|
||||||
|
|
||||||
|
export interface Model extends Schema {
|
||||||
|
name: string
|
||||||
|
export:
|
||||||
|
| "reference"
|
||||||
|
| "generic"
|
||||||
|
| "enum"
|
||||||
|
| "array"
|
||||||
|
| "dictionary"
|
||||||
|
| "interface"
|
||||||
|
| "one-of"
|
||||||
|
| "any-of"
|
||||||
|
| "all-of"
|
||||||
|
type: string
|
||||||
|
base: string
|
||||||
|
template: string | null
|
||||||
|
link: Model | null
|
||||||
|
description: string | null
|
||||||
|
deprecated?: boolean
|
||||||
|
default?: string
|
||||||
|
imports: string[]
|
||||||
|
enum: Enum[]
|
||||||
|
enums: Model[]
|
||||||
|
properties: Model[]
|
||||||
|
}
|
||||||
+8
@@ -0,0 +1,8 @@
|
|||||||
|
import type { Model } from "./Model"
|
||||||
|
|
||||||
|
export interface ModelComposition {
|
||||||
|
type: "one-of" | "any-of" | "all-of"
|
||||||
|
imports: string[]
|
||||||
|
enums: Model[]
|
||||||
|
properties: Model[]
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import type { OperationError } from "./OperationError"
|
||||||
|
import type { OperationParameters } from "./OperationParameters"
|
||||||
|
import type { OperationResponse } from "./OperationResponse"
|
||||||
|
import type { OperationCodegen } from "./OperationCodegen"
|
||||||
|
|
||||||
|
export interface Operation extends OperationParameters {
|
||||||
|
service: string
|
||||||
|
name: string
|
||||||
|
operationId: string | null
|
||||||
|
summary: string | null
|
||||||
|
description: string | null
|
||||||
|
deprecated: boolean
|
||||||
|
method: string
|
||||||
|
path: string
|
||||||
|
errors: OperationError[]
|
||||||
|
results: OperationResponse[]
|
||||||
|
responseHeader: string | null
|
||||||
|
codegen: OperationCodegen
|
||||||
|
}
|
||||||
+4
@@ -0,0 +1,4 @@
|
|||||||
|
export interface OperationCodegen {
|
||||||
|
method?: string
|
||||||
|
queryParams?: string
|
||||||
|
}
|
||||||
+4
@@ -0,0 +1,4 @@
|
|||||||
|
export interface OperationError {
|
||||||
|
code: number
|
||||||
|
description: string
|
||||||
|
}
|
||||||
+10
@@ -0,0 +1,10 @@
|
|||||||
|
import type { Model } from "./Model"
|
||||||
|
import { OpenApiParameter } from "../../openApi/v3/interfaces/OpenApiParameter"
|
||||||
|
import { OpenApiRequestBody } from "../../openApi/v3/interfaces/OpenApiRequestBody"
|
||||||
|
|
||||||
|
export interface OperationParameter extends Model {
|
||||||
|
spec: OpenApiParameter | OpenApiRequestBody
|
||||||
|
in: "path" | "query" | "header" | "formData" | "body" | "cookie"
|
||||||
|
prop: string
|
||||||
|
mediaType: string | null
|
||||||
|
}
|
||||||
+12
@@ -0,0 +1,12 @@
|
|||||||
|
import type { OperationParameter } from "./OperationParameter"
|
||||||
|
|
||||||
|
export interface OperationParameters {
|
||||||
|
imports: string[]
|
||||||
|
parameters: OperationParameter[]
|
||||||
|
parametersPath: OperationParameter[]
|
||||||
|
parametersQuery: OperationParameter[]
|
||||||
|
parametersForm: OperationParameter[]
|
||||||
|
parametersCookie: OperationParameter[]
|
||||||
|
parametersHeader: OperationParameter[]
|
||||||
|
parametersBody: OperationParameter | null
|
||||||
|
}
|
||||||
+8
@@ -0,0 +1,8 @@
|
|||||||
|
import type { Model } from "./Model"
|
||||||
|
import { OpenApiResponse } from "../../openApi/v3/interfaces/OpenApiResponse"
|
||||||
|
|
||||||
|
export interface OperationResponse extends Model {
|
||||||
|
spec: OpenApiResponse
|
||||||
|
in: "response" | "header"
|
||||||
|
code: number
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import { OpenApiSchema } from "../../openApi/v3/interfaces/OpenApiSchema"
|
||||||
|
|
||||||
|
export interface Schema {
|
||||||
|
spec: OpenApiSchema
|
||||||
|
isDefinition: boolean
|
||||||
|
isReadOnly: boolean
|
||||||
|
isRequired: boolean
|
||||||
|
isNullable: boolean
|
||||||
|
format?:
|
||||||
|
| "int32"
|
||||||
|
| "int64"
|
||||||
|
| "float"
|
||||||
|
| "double"
|
||||||
|
| "string"
|
||||||
|
| "boolean"
|
||||||
|
| "byte"
|
||||||
|
| "binary"
|
||||||
|
| "date"
|
||||||
|
| "date-time"
|
||||||
|
| "password"
|
||||||
|
maximum?: number
|
||||||
|
exclusiveMaximum?: boolean
|
||||||
|
minimum?: number
|
||||||
|
exclusiveMinimum?: boolean
|
||||||
|
multipleOf?: number
|
||||||
|
maxLength?: number
|
||||||
|
minLength?: number
|
||||||
|
pattern?: string
|
||||||
|
maxItems?: number
|
||||||
|
minItems?: number
|
||||||
|
uniqueItems?: boolean
|
||||||
|
maxProperties?: number
|
||||||
|
minProperties?: number
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import type { Operation } from "./Operation"
|
||||||
|
|
||||||
|
export interface Service {
|
||||||
|
name: string
|
||||||
|
operations: Operation[]
|
||||||
|
imports: string[]
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
export interface Type {
|
||||||
|
type: string
|
||||||
|
base: string
|
||||||
|
template: string | null
|
||||||
|
imports: string[]
|
||||||
|
isNullable: boolean
|
||||||
|
}
|
||||||
@@ -0,0 +1,116 @@
|
|||||||
|
import { HttpClient } from "./HttpClient"
|
||||||
|
import { Indent } from "./Indent"
|
||||||
|
import { parse } from "./openApi/v3"
|
||||||
|
import { getOpenApiSpec } from "./utils/getOpenApiSpec"
|
||||||
|
import { getOpenApiVersion } from "./utils/getOpenApiVersion"
|
||||||
|
import { isString } from "./utils/isString"
|
||||||
|
import { postProcessClient } from "./utils/postProcessClient"
|
||||||
|
import { registerHandlebarTemplates } from "./utils/registerHandlebarTemplates"
|
||||||
|
import { writeClient } from "./utils/writeClient"
|
||||||
|
|
||||||
|
export { HttpClient } from "./HttpClient"
|
||||||
|
export { Indent } from "./Indent"
|
||||||
|
|
||||||
|
export type PackageNames = {
|
||||||
|
models?: string
|
||||||
|
client?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Options = {
|
||||||
|
input: string | Record<string, any>
|
||||||
|
output: string
|
||||||
|
httpClient?: HttpClient
|
||||||
|
clientName?: string
|
||||||
|
useOptions?: boolean
|
||||||
|
useUnionTypes?: boolean
|
||||||
|
exportCore?: boolean
|
||||||
|
exportServices?: boolean
|
||||||
|
exportModels?: boolean
|
||||||
|
exportHooks?: boolean
|
||||||
|
exportSchemas?: boolean
|
||||||
|
indent?: Indent
|
||||||
|
packageNames?: PackageNames
|
||||||
|
postfixServices?: string
|
||||||
|
postfixModels?: string
|
||||||
|
request?: string
|
||||||
|
write?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate the OpenAPI client. This method will read the OpenAPI specification and based on the
|
||||||
|
* given language it will generate the client, including the typed models, validation schemas,
|
||||||
|
* service layer, etc.
|
||||||
|
* @param input The relative location of the OpenAPI spec
|
||||||
|
* @param output The relative location of the output directory
|
||||||
|
* @param httpClient The selected httpClient (fetch, xhr, node or axios)
|
||||||
|
* @param clientName Custom client class name
|
||||||
|
* @param useOptions Use options or arguments functions
|
||||||
|
* @param useUnionTypes Use union types instead of enums
|
||||||
|
* @param exportCore Generate core client classes
|
||||||
|
* @param exportServices Generate services
|
||||||
|
* @param exportModels Generate models
|
||||||
|
* @param exportHooks Generate hooks
|
||||||
|
* @param exportSchemas Generate schemas
|
||||||
|
* @param indent Indentation options (4, 2 or tab)
|
||||||
|
* @param packageNames Package name to use in import statements.
|
||||||
|
* @param postfixServices Service name postfix
|
||||||
|
* @param postfixModels Model name postfix
|
||||||
|
* @param request Path to custom request file
|
||||||
|
* @param write Write the files to disk (true or false)
|
||||||
|
*/
|
||||||
|
export const generate = async ({
|
||||||
|
input,
|
||||||
|
output,
|
||||||
|
httpClient = HttpClient.FETCH,
|
||||||
|
clientName,
|
||||||
|
useOptions = false,
|
||||||
|
useUnionTypes = false,
|
||||||
|
exportCore = true,
|
||||||
|
exportServices = true,
|
||||||
|
exportModels = true,
|
||||||
|
exportHooks = true,
|
||||||
|
exportSchemas = false,
|
||||||
|
indent = Indent.SPACE_4,
|
||||||
|
packageNames = {},
|
||||||
|
postfixServices = "Service",
|
||||||
|
postfixModels = "",
|
||||||
|
request,
|
||||||
|
write = true,
|
||||||
|
}: Options): Promise<void> => {
|
||||||
|
const openApi = isString(input) ? await getOpenApiSpec(input) : input
|
||||||
|
const openApiVersion = getOpenApiVersion(openApi)
|
||||||
|
const templates = registerHandlebarTemplates({
|
||||||
|
httpClient,
|
||||||
|
useUnionTypes,
|
||||||
|
useOptions,
|
||||||
|
})
|
||||||
|
|
||||||
|
const client = parse(openApi)
|
||||||
|
const clientFinal = postProcessClient(client)
|
||||||
|
if (write) {
|
||||||
|
await writeClient(
|
||||||
|
clientFinal,
|
||||||
|
templates,
|
||||||
|
output,
|
||||||
|
httpClient,
|
||||||
|
useOptions,
|
||||||
|
useUnionTypes,
|
||||||
|
exportCore,
|
||||||
|
exportServices,
|
||||||
|
exportModels,
|
||||||
|
exportHooks,
|
||||||
|
exportSchemas,
|
||||||
|
indent,
|
||||||
|
packageNames,
|
||||||
|
postfixServices,
|
||||||
|
postfixModels,
|
||||||
|
clientName,
|
||||||
|
request
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
HttpClient,
|
||||||
|
generate,
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import type { Client } from "../../client/interfaces/Client"
|
||||||
|
import type { OpenApi } from "./interfaces/OpenApi"
|
||||||
|
import { getModels } from "./parser/getModels"
|
||||||
|
import { getServer } from "./parser/getServer"
|
||||||
|
import { getServices } from "./parser/getServices"
|
||||||
|
import { getServiceVersion } from "./parser/getServiceVersion"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse the OpenAPI specification to a Client model that contains
|
||||||
|
* all the models, services and schema's we should output.
|
||||||
|
* @param openApi The OpenAPI spec that we have loaded from disk.
|
||||||
|
*/
|
||||||
|
export const parse = (openApi: OpenApi): Client => {
|
||||||
|
const version = getServiceVersion(openApi.info.version)
|
||||||
|
const server = getServer(openApi)
|
||||||
|
const models = getModels(openApi)
|
||||||
|
const services = getServices(openApi)
|
||||||
|
|
||||||
|
return { version, server, models, services }
|
||||||
|
}
|
||||||
Vendored
+4
@@ -0,0 +1,4 @@
|
|||||||
|
export interface WithEnumExtension {
|
||||||
|
"x-enum-varnames"?: string[]
|
||||||
|
"x-enum-descriptions"?: string[]
|
||||||
|
}
|
||||||
+21
@@ -0,0 +1,21 @@
|
|||||||
|
import type { OpenApiComponents } from "./OpenApiComponents"
|
||||||
|
import type { OpenApiExternalDocs } from "./OpenApiExternalDocs"
|
||||||
|
import type { OpenApiInfo } from "./OpenApiInfo"
|
||||||
|
import type { OpenApiPaths } from "./OpenApiPaths"
|
||||||
|
import type { OpenApiSecurityRequirement } from "./OpenApiSecurityRequirement"
|
||||||
|
import type { OpenApiServer } from "./OpenApiServer"
|
||||||
|
import type { OpenApiTag } from "./OpenApiTag"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md
|
||||||
|
*/
|
||||||
|
export interface OpenApi {
|
||||||
|
openapi: string
|
||||||
|
info: OpenApiInfo
|
||||||
|
servers?: OpenApiServer[]
|
||||||
|
paths: OpenApiPaths
|
||||||
|
components?: OpenApiComponents
|
||||||
|
security?: OpenApiSecurityRequirement[]
|
||||||
|
tags?: OpenApiTag[]
|
||||||
|
externalDocs?: OpenApiExternalDocs
|
||||||
|
}
|
||||||
+9
@@ -0,0 +1,9 @@
|
|||||||
|
import type { OpenApiPath } from "./OpenApiPath"
|
||||||
|
import type { OpenApiReference } from "./OpenApiReference"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#callbackObject
|
||||||
|
*/
|
||||||
|
export interface OpenApiCallback extends OpenApiReference {
|
||||||
|
[key: string]: OpenApiPath
|
||||||
|
}
|
||||||
+25
@@ -0,0 +1,25 @@
|
|||||||
|
import type { Dictionary } from "../../../utils/types"
|
||||||
|
import type { OpenApiCallback } from "./OpenApiCallback"
|
||||||
|
import type { OpenApiExample } from "./OpenApiExample"
|
||||||
|
import type { OpenApiHeader } from "./OpenApiHeader"
|
||||||
|
import type { OpenApiLink } from "./OpenApiLink"
|
||||||
|
import type { OpenApiParameter } from "./OpenApiParameter"
|
||||||
|
import type { OpenApiRequestBody } from "./OpenApiRequestBody"
|
||||||
|
import type { OpenApiResponses } from "./OpenApiResponses"
|
||||||
|
import type { OpenApiSchema } from "./OpenApiSchema"
|
||||||
|
import type { OpenApiSecurityScheme } from "./OpenApiSecurityScheme"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#componentsObject
|
||||||
|
*/
|
||||||
|
export interface OpenApiComponents {
|
||||||
|
schemas?: Dictionary<OpenApiSchema>
|
||||||
|
responses?: Dictionary<OpenApiResponses>
|
||||||
|
parameters?: Dictionary<OpenApiParameter>
|
||||||
|
examples?: Dictionary<OpenApiExample>
|
||||||
|
requestBodies?: Dictionary<OpenApiRequestBody>
|
||||||
|
headers?: Dictionary<OpenApiHeader>
|
||||||
|
securitySchemes?: Dictionary<OpenApiSecurityScheme>
|
||||||
|
links?: Dictionary<OpenApiLink>
|
||||||
|
callbacks?: Dictionary<OpenApiCallback>
|
||||||
|
}
|
||||||
+8
@@ -0,0 +1,8 @@
|
|||||||
|
/**
|
||||||
|
* https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#contactObject
|
||||||
|
*/
|
||||||
|
export interface OpenApiContact {
|
||||||
|
name?: string
|
||||||
|
url?: string
|
||||||
|
email?: string
|
||||||
|
}
|
||||||
Vendored
+9
@@ -0,0 +1,9 @@
|
|||||||
|
import type { Dictionary } from "../../../utils/types"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#discriminatorObject
|
||||||
|
*/
|
||||||
|
export interface OpenApiDiscriminator {
|
||||||
|
propertyName: string
|
||||||
|
mapping?: Dictionary<string>
|
||||||
|
}
|
||||||
+13
@@ -0,0 +1,13 @@
|
|||||||
|
import type { Dictionary } from "../../../utils/types"
|
||||||
|
import type { OpenApiHeader } from "./OpenApiHeader"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#encodingObject
|
||||||
|
*/
|
||||||
|
export interface OpenApiEncoding {
|
||||||
|
contentType?: string
|
||||||
|
headers?: Dictionary<OpenApiHeader>
|
||||||
|
style?: string
|
||||||
|
explode?: boolean
|
||||||
|
allowReserved?: boolean
|
||||||
|
}
|
||||||
+11
@@ -0,0 +1,11 @@
|
|||||||
|
import type { OpenApiReference } from "./OpenApiReference"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#exampleObject
|
||||||
|
*/
|
||||||
|
export interface OpenApiExample extends OpenApiReference {
|
||||||
|
summary?: string
|
||||||
|
description?: string
|
||||||
|
value?: any
|
||||||
|
externalValue?: string
|
||||||
|
}
|
||||||
+7
@@ -0,0 +1,7 @@
|
|||||||
|
/**
|
||||||
|
* https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#externalDocumentationObject
|
||||||
|
*/
|
||||||
|
export interface OpenApiExternalDocs {
|
||||||
|
description?: string
|
||||||
|
url: string
|
||||||
|
}
|
||||||
+20
@@ -0,0 +1,20 @@
|
|||||||
|
import type { Dictionary } from "../../../utils/types"
|
||||||
|
import type { OpenApiExample } from "./OpenApiExample"
|
||||||
|
import type { OpenApiReference } from "./OpenApiReference"
|
||||||
|
import type { OpenApiSchema } from "./OpenApiSchema"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#headerObject
|
||||||
|
*/
|
||||||
|
export interface OpenApiHeader extends OpenApiReference {
|
||||||
|
description?: string
|
||||||
|
required?: boolean
|
||||||
|
deprecated?: boolean
|
||||||
|
allowEmptyValue?: boolean
|
||||||
|
style?: string
|
||||||
|
explode?: boolean
|
||||||
|
allowReserved?: boolean
|
||||||
|
schema?: OpenApiSchema
|
||||||
|
example?: any
|
||||||
|
examples?: Dictionary<OpenApiExample>
|
||||||
|
}
|
||||||
+14
@@ -0,0 +1,14 @@
|
|||||||
|
import type { OpenApiContact } from "./OpenApiContact"
|
||||||
|
import type { OpenApiLicense } from "./OpenApiLicense"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#infoObject
|
||||||
|
*/
|
||||||
|
export interface OpenApiInfo {
|
||||||
|
title: string
|
||||||
|
description?: string
|
||||||
|
termsOfService?: string
|
||||||
|
contact?: OpenApiContact
|
||||||
|
license?: OpenApiLicense
|
||||||
|
version: string
|
||||||
|
}
|
||||||
+7
@@ -0,0 +1,7 @@
|
|||||||
|
/**
|
||||||
|
* https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#licenseObject
|
||||||
|
*/
|
||||||
|
export interface OpenApiLicense {
|
||||||
|
name: string
|
||||||
|
url?: string
|
||||||
|
}
|
||||||
+15
@@ -0,0 +1,15 @@
|
|||||||
|
import type { Dictionary } from "../../../utils/types"
|
||||||
|
import type { OpenApiReference } from "./OpenApiReference"
|
||||||
|
import type { OpenApiServer } from "./OpenApiServer"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#linkObject
|
||||||
|
*/
|
||||||
|
export interface OpenApiLink extends OpenApiReference {
|
||||||
|
operationRef?: string
|
||||||
|
operationId?: string
|
||||||
|
parameters?: Dictionary<any>
|
||||||
|
requestBody?: any
|
||||||
|
description?: string
|
||||||
|
server?: OpenApiServer
|
||||||
|
}
|
||||||
+15
@@ -0,0 +1,15 @@
|
|||||||
|
import type { Dictionary } from "../../../utils/types"
|
||||||
|
import type { OpenApiEncoding } from "./OpenApiEncoding"
|
||||||
|
import type { OpenApiExample } from "./OpenApiExample"
|
||||||
|
import type { OpenApiReference } from "./OpenApiReference"
|
||||||
|
import type { OpenApiSchema } from "./OpenApiSchema"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#mediaTypeObject
|
||||||
|
*/
|
||||||
|
export interface OpenApiMediaType extends OpenApiReference {
|
||||||
|
schema?: OpenApiSchema
|
||||||
|
example?: any
|
||||||
|
examples?: Dictionary<OpenApiExample>
|
||||||
|
encoding?: Dictionary<OpenApiEncoding>
|
||||||
|
}
|
||||||
+11
@@ -0,0 +1,11 @@
|
|||||||
|
import type { Dictionary } from "../../../utils/types"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#oauthFlowObject
|
||||||
|
*/
|
||||||
|
export interface OpenApiOAuthFlow {
|
||||||
|
authorizationUrl: string
|
||||||
|
tokenUrl: string
|
||||||
|
refreshUrl?: string
|
||||||
|
scopes: Dictionary<string>
|
||||||
|
}
|
||||||
+11
@@ -0,0 +1,11 @@
|
|||||||
|
import type { OpenApiOAuthFlow } from "./OpenApiOAuthFlow"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#oauthFlowsObject
|
||||||
|
*/
|
||||||
|
export interface OpenApiOAuthFlows {
|
||||||
|
implicit?: OpenApiOAuthFlow
|
||||||
|
password?: OpenApiOAuthFlow
|
||||||
|
clientCredentials?: OpenApiOAuthFlow
|
||||||
|
authorizationCode?: OpenApiOAuthFlow
|
||||||
|
}
|
||||||
+27
@@ -0,0 +1,27 @@
|
|||||||
|
import type { Dictionary } from "../../../utils/types"
|
||||||
|
import type { OpenApiCallback } from "./OpenApiCallback"
|
||||||
|
import type { OpenApiExternalDocs } from "./OpenApiExternalDocs"
|
||||||
|
import type { OpenApiParameter } from "./OpenApiParameter"
|
||||||
|
import type { OpenApiRequestBody } from "./OpenApiRequestBody"
|
||||||
|
import type { OpenApiResponses } from "./OpenApiResponses"
|
||||||
|
import type { OpenApiSecurityRequirement } from "./OpenApiSecurityRequirement"
|
||||||
|
import type { OpenApiServer } from "./OpenApiServer"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#operationObject
|
||||||
|
*/
|
||||||
|
export interface OpenApiOperation {
|
||||||
|
tags?: string[]
|
||||||
|
summary?: string
|
||||||
|
description?: string
|
||||||
|
externalDocs?: OpenApiExternalDocs
|
||||||
|
operationId?: string
|
||||||
|
parameters?: OpenApiParameter[]
|
||||||
|
requestBody?: OpenApiRequestBody
|
||||||
|
responses: OpenApiResponses
|
||||||
|
callbacks?: Dictionary<OpenApiCallback>
|
||||||
|
deprecated?: boolean
|
||||||
|
security?: OpenApiSecurityRequirement[]
|
||||||
|
servers?: OpenApiServer[]
|
||||||
|
"x-codegen"?: Record<string, unknown>
|
||||||
|
}
|
||||||
+23
@@ -0,0 +1,23 @@
|
|||||||
|
import type { Dictionary } from "../../../utils/types"
|
||||||
|
import type { OpenApiExample } from "./OpenApiExample"
|
||||||
|
import type { OpenApiReference } from "./OpenApiReference"
|
||||||
|
import type { OpenApiSchema } from "./OpenApiSchema"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#parameterObject
|
||||||
|
*/
|
||||||
|
export interface OpenApiParameter extends OpenApiReference {
|
||||||
|
name: string
|
||||||
|
in: "path" | "query" | "header" | "formData" | "cookie"
|
||||||
|
description?: string
|
||||||
|
required?: boolean
|
||||||
|
nullable?: boolean
|
||||||
|
deprecated?: boolean
|
||||||
|
allowEmptyValue?: boolean
|
||||||
|
style?: string
|
||||||
|
explode?: boolean
|
||||||
|
allowReserved?: boolean
|
||||||
|
schema?: OpenApiSchema
|
||||||
|
example?: any
|
||||||
|
examples?: Dictionary<OpenApiExample>
|
||||||
|
}
|
||||||
+21
@@ -0,0 +1,21 @@
|
|||||||
|
import type { OpenApiOperation } from "./OpenApiOperation"
|
||||||
|
import type { OpenApiParameter } from "./OpenApiParameter"
|
||||||
|
import type { OpenApiServer } from "./OpenApiServer"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#pathItemObject
|
||||||
|
*/
|
||||||
|
export interface OpenApiPath {
|
||||||
|
summary?: string
|
||||||
|
description?: string
|
||||||
|
get?: OpenApiOperation
|
||||||
|
put?: OpenApiOperation
|
||||||
|
post?: OpenApiOperation
|
||||||
|
delete?: OpenApiOperation
|
||||||
|
options?: OpenApiOperation
|
||||||
|
head?: OpenApiOperation
|
||||||
|
patch?: OpenApiOperation
|
||||||
|
trace?: OpenApiOperation
|
||||||
|
servers?: OpenApiServer[]
|
||||||
|
parameters?: OpenApiParameter[]
|
||||||
|
}
|
||||||
+8
@@ -0,0 +1,8 @@
|
|||||||
|
import type { OpenApiPath } from "./OpenApiPath"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#pathsObject
|
||||||
|
*/
|
||||||
|
export interface OpenApiPaths {
|
||||||
|
[path: string]: OpenApiPath
|
||||||
|
}
|
||||||
+6
@@ -0,0 +1,6 @@
|
|||||||
|
/**
|
||||||
|
* https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#referenceObject
|
||||||
|
*/
|
||||||
|
export interface OpenApiReference {
|
||||||
|
$ref?: string
|
||||||
|
}
|
||||||
+13
@@ -0,0 +1,13 @@
|
|||||||
|
import type { Dictionary } from "../../../utils/types"
|
||||||
|
import type { OpenApiMediaType } from "./OpenApiMediaType"
|
||||||
|
import type { OpenApiReference } from "./OpenApiReference"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#requestBodyObject
|
||||||
|
*/
|
||||||
|
export interface OpenApiRequestBody extends OpenApiReference {
|
||||||
|
description?: string
|
||||||
|
content: Dictionary<OpenApiMediaType>
|
||||||
|
required?: boolean
|
||||||
|
nullable?: boolean
|
||||||
|
}
|
||||||
+15
@@ -0,0 +1,15 @@
|
|||||||
|
import type { Dictionary } from "../../../utils/types"
|
||||||
|
import type { OpenApiHeader } from "./OpenApiHeader"
|
||||||
|
import type { OpenApiLink } from "./OpenApiLink"
|
||||||
|
import type { OpenApiMediaType } from "./OpenApiMediaType"
|
||||||
|
import type { OpenApiReference } from "./OpenApiReference"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#responseObject
|
||||||
|
*/
|
||||||
|
export interface OpenApiResponse extends OpenApiReference {
|
||||||
|
description: string
|
||||||
|
headers?: Dictionary<OpenApiHeader>
|
||||||
|
content?: Dictionary<OpenApiMediaType>
|
||||||
|
links?: Dictionary<OpenApiLink>
|
||||||
|
}
|
||||||
+11
@@ -0,0 +1,11 @@
|
|||||||
|
import type { OpenApiReference } from "./OpenApiReference"
|
||||||
|
import type { OpenApiResponse } from "./OpenApiResponse"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#responsesObject
|
||||||
|
*/
|
||||||
|
export interface OpenApiResponses extends OpenApiReference {
|
||||||
|
default: OpenApiResponse
|
||||||
|
|
||||||
|
[httpcode: string]: OpenApiResponse
|
||||||
|
}
|
||||||
+58
@@ -0,0 +1,58 @@
|
|||||||
|
import type { Dictionary } from "../../../utils/types"
|
||||||
|
import type { WithEnumExtension } from "./Extensions/WithEnumExtension"
|
||||||
|
import type { OpenApiDiscriminator } from "./OpenApiDiscriminator"
|
||||||
|
import type { OpenApiExternalDocs } from "./OpenApiExternalDocs"
|
||||||
|
import type { OpenApiReference } from "./OpenApiReference"
|
||||||
|
import type { OpenApiXml } from "./OpenApiXml"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#schemaObject
|
||||||
|
*/
|
||||||
|
export interface OpenApiSchema extends OpenApiReference, WithEnumExtension {
|
||||||
|
title?: string
|
||||||
|
multipleOf?: number
|
||||||
|
maximum?: number
|
||||||
|
exclusiveMaximum?: boolean
|
||||||
|
minimum?: number
|
||||||
|
exclusiveMinimum?: boolean
|
||||||
|
maxLength?: number
|
||||||
|
minLength?: number
|
||||||
|
pattern?: string
|
||||||
|
maxItems?: number
|
||||||
|
minItems?: number
|
||||||
|
uniqueItems?: boolean
|
||||||
|
maxProperties?: number
|
||||||
|
minProperties?: number
|
||||||
|
required?: string[]
|
||||||
|
enum?: (string | number)[]
|
||||||
|
type?: string | string[]
|
||||||
|
allOf?: OpenApiSchema[]
|
||||||
|
oneOf?: OpenApiSchema[]
|
||||||
|
anyOf?: OpenApiSchema[]
|
||||||
|
not?: OpenApiSchema[]
|
||||||
|
items?: OpenApiSchema
|
||||||
|
properties?: Dictionary<OpenApiSchema>
|
||||||
|
additionalProperties?: boolean | OpenApiSchema
|
||||||
|
description?: string
|
||||||
|
format?:
|
||||||
|
| "int32"
|
||||||
|
| "int64"
|
||||||
|
| "float"
|
||||||
|
| "double"
|
||||||
|
| "string"
|
||||||
|
| "boolean"
|
||||||
|
| "byte"
|
||||||
|
| "binary"
|
||||||
|
| "date"
|
||||||
|
| "date-time"
|
||||||
|
| "password"
|
||||||
|
default?: any
|
||||||
|
nullable?: boolean
|
||||||
|
discriminator?: OpenApiDiscriminator
|
||||||
|
readOnly?: boolean
|
||||||
|
writeOnly?: boolean
|
||||||
|
xml?: OpenApiXml
|
||||||
|
externalDocs?: OpenApiExternalDocs
|
||||||
|
example?: any
|
||||||
|
deprecated?: boolean
|
||||||
|
}
|
||||||
Vendored
+6
@@ -0,0 +1,6 @@
|
|||||||
|
/**
|
||||||
|
* https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#securityRequirementObject
|
||||||
|
*/
|
||||||
|
export interface OpenApiSecurityRequirement {
|
||||||
|
[name: string]: string
|
||||||
|
}
|
||||||
Vendored
+16
@@ -0,0 +1,16 @@
|
|||||||
|
import type { OpenApiOAuthFlows } from "./OpenApiOAuthFlows"
|
||||||
|
import type { OpenApiReference } from "./OpenApiReference"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#securitySchemeObject
|
||||||
|
*/
|
||||||
|
export interface OpenApiSecurityScheme extends OpenApiReference {
|
||||||
|
type: "apiKey" | "http" | "oauth2" | "openIdConnect"
|
||||||
|
description?: string
|
||||||
|
name?: string
|
||||||
|
in?: "query" | "header" | "cookie"
|
||||||
|
scheme?: string
|
||||||
|
bearerFormat?: string
|
||||||
|
flows?: OpenApiOAuthFlows
|
||||||
|
openIdConnectUrl?: string
|
||||||
|
}
|
||||||
+11
@@ -0,0 +1,11 @@
|
|||||||
|
import type { Dictionary } from "../../../utils/types"
|
||||||
|
import type { OpenApiServerVariable } from "./OpenApiServerVariable"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#serverObject
|
||||||
|
*/
|
||||||
|
export interface OpenApiServer {
|
||||||
|
url: string
|
||||||
|
description?: string
|
||||||
|
variables?: Dictionary<OpenApiServerVariable>
|
||||||
|
}
|
||||||
Vendored
+10
@@ -0,0 +1,10 @@
|
|||||||
|
import type { WithEnumExtension } from "./Extensions/WithEnumExtension"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#serverVariableObject
|
||||||
|
*/
|
||||||
|
export interface OpenApiServerVariable extends WithEnumExtension {
|
||||||
|
enum?: (string | number)[]
|
||||||
|
default: string
|
||||||
|
description?: string
|
||||||
|
}
|
||||||
+10
@@ -0,0 +1,10 @@
|
|||||||
|
import type { OpenApiExternalDocs } from "./OpenApiExternalDocs"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#tagObject
|
||||||
|
*/
|
||||||
|
export interface OpenApiTag {
|
||||||
|
name: string
|
||||||
|
description?: string
|
||||||
|
externalDocs?: OpenApiExternalDocs
|
||||||
|
}
|
||||||
+10
@@ -0,0 +1,10 @@
|
|||||||
|
/**
|
||||||
|
* https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#xmlObject
|
||||||
|
*/
|
||||||
|
export interface OpenApiXml {
|
||||||
|
name?: string
|
||||||
|
namespace?: string
|
||||||
|
prefix?: string
|
||||||
|
attribute?: boolean
|
||||||
|
wrapped?: boolean
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
export const escapeName = (value: string): string => {
|
||||||
|
if (value || value === "") {
|
||||||
|
const validName = /^[a-zA-Z_$][\w$]+$/g.test(value)
|
||||||
|
if (!validName) {
|
||||||
|
return `'${value}'`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return value
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import type { Enum } from "../../../client/interfaces/Enum"
|
||||||
|
import { isString } from "../../../utils/isString"
|
||||||
|
import type { WithEnumExtension } from "../interfaces/Extensions/WithEnumExtension"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extend the enum with the x-enum properties. This adds the capability
|
||||||
|
* to use names and descriptions inside the generated enums.
|
||||||
|
* @param enumerators
|
||||||
|
* @param definition
|
||||||
|
*/
|
||||||
|
export const extendEnum = (
|
||||||
|
enumerators: Enum[],
|
||||||
|
definition: WithEnumExtension
|
||||||
|
): Enum[] => {
|
||||||
|
const names = definition["x-enum-varnames"]?.filter(isString)
|
||||||
|
const descriptions = definition["x-enum-descriptions"]?.filter(isString)
|
||||||
|
|
||||||
|
return enumerators.map((enumerator, index) => ({
|
||||||
|
name: names?.[index] || enumerator.name,
|
||||||
|
description: descriptions?.[index] || enumerator.description,
|
||||||
|
value: enumerator.value,
|
||||||
|
type: enumerator.type,
|
||||||
|
}))
|
||||||
|
}
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
import { isDefined } from "../../../utils/isDefined"
|
||||||
|
import type { Dictionary } from "../../../utils/types"
|
||||||
|
import type { OpenApi } from "../interfaces/OpenApi"
|
||||||
|
import type { OpenApiMediaType } from "../interfaces/OpenApiMediaType"
|
||||||
|
import type { OpenApiSchema } from "../interfaces/OpenApiSchema"
|
||||||
|
|
||||||
|
export interface Content {
|
||||||
|
mediaType: string
|
||||||
|
schema: OpenApiSchema
|
||||||
|
}
|
||||||
|
|
||||||
|
const BASIC_MEDIA_TYPES = [
|
||||||
|
"application/json-patch+json",
|
||||||
|
"application/json",
|
||||||
|
"application/x-www-form-urlencoded",
|
||||||
|
"text/json",
|
||||||
|
"text/plain",
|
||||||
|
"multipart/form-data",
|
||||||
|
"multipart/mixed",
|
||||||
|
"multipart/related",
|
||||||
|
"multipart/batch",
|
||||||
|
]
|
||||||
|
|
||||||
|
export const getContent = (
|
||||||
|
openApi: OpenApi,
|
||||||
|
content: Dictionary<OpenApiMediaType>
|
||||||
|
): Content | null => {
|
||||||
|
const basicMediaTypeWithSchema = Object.keys(content)
|
||||||
|
.filter((mediaType) => {
|
||||||
|
const cleanMediaType = mediaType.split(";")[0].trim()
|
||||||
|
return BASIC_MEDIA_TYPES.includes(cleanMediaType)
|
||||||
|
})
|
||||||
|
.find((mediaType) => isDefined(content[mediaType]?.schema))
|
||||||
|
if (basicMediaTypeWithSchema) {
|
||||||
|
return {
|
||||||
|
mediaType: basicMediaTypeWithSchema,
|
||||||
|
schema: content[basicMediaTypeWithSchema].schema as OpenApiSchema,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const firstMediaTypeWithSchema = Object.keys(content).find((mediaType) =>
|
||||||
|
isDefined(content[mediaType]?.schema)
|
||||||
|
)
|
||||||
|
if (firstMediaTypeWithSchema) {
|
||||||
|
return {
|
||||||
|
mediaType: firstMediaTypeWithSchema,
|
||||||
|
schema: content[firstMediaTypeWithSchema].schema as OpenApiSchema,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import type { Enum } from "../../../client/interfaces/Enum"
|
||||||
|
|
||||||
|
export const getEnum = (values?: (string | number)[]): Enum[] => {
|
||||||
|
if (Array.isArray(values)) {
|
||||||
|
return values
|
||||||
|
.filter((value, index, arr) => {
|
||||||
|
return arr.indexOf(value) === index
|
||||||
|
})
|
||||||
|
.filter((value: any) => {
|
||||||
|
return typeof value === "number" || typeof value === "string"
|
||||||
|
})
|
||||||
|
.map((value) => {
|
||||||
|
if (typeof value === "number") {
|
||||||
|
return {
|
||||||
|
name: `'_${value}'`,
|
||||||
|
value: String(value),
|
||||||
|
type: "number",
|
||||||
|
description: null,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
name: String(value)
|
||||||
|
.replace(/\W+/g, "_")
|
||||||
|
.replace(/^(\d+)/g, "_$1")
|
||||||
|
.replace(/([a-z])([A-Z]+)/g, "$1_$2")
|
||||||
|
.toUpperCase(),
|
||||||
|
value: `'${value.replace(/'/g, "\\'")}'`,
|
||||||
|
type: "string",
|
||||||
|
description: null,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return []
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
const TYPE_MAPPINGS = new Map<string, string>([
|
||||||
|
["file", "binary"],
|
||||||
|
["any", "any"],
|
||||||
|
["object", "any"],
|
||||||
|
["array", "any[]"],
|
||||||
|
["boolean", "boolean"],
|
||||||
|
["byte", "number"],
|
||||||
|
["int", "number"],
|
||||||
|
["integer", "number"],
|
||||||
|
["float", "number"],
|
||||||
|
["double", "number"],
|
||||||
|
["short", "number"],
|
||||||
|
["long", "number"],
|
||||||
|
["number", "number"],
|
||||||
|
["char", "string"],
|
||||||
|
["date", "string"],
|
||||||
|
["date-time", "string"],
|
||||||
|
["password", "string"],
|
||||||
|
["string", "string"],
|
||||||
|
["void", "void"],
|
||||||
|
["null", "null"],
|
||||||
|
])
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get mapped type for given type to any basic Typescript/Javascript type.
|
||||||
|
*/
|
||||||
|
export const getMappedType = (
|
||||||
|
type: string,
|
||||||
|
format?: string
|
||||||
|
): string | undefined => {
|
||||||
|
if (format === "binary") {
|
||||||
|
return "binary"
|
||||||
|
}
|
||||||
|
return TYPE_MAPPINGS.get(type)
|
||||||
|
}
|
||||||
@@ -0,0 +1,224 @@
|
|||||||
|
import type { Model } from "../../../client/interfaces/Model"
|
||||||
|
import { getPattern } from "../../../utils/getPattern"
|
||||||
|
import type { OpenApi } from "../interfaces/OpenApi"
|
||||||
|
import type { OpenApiSchema } from "../interfaces/OpenApiSchema"
|
||||||
|
import { extendEnum } from "./extendEnum"
|
||||||
|
import { getEnum } from "./getEnum"
|
||||||
|
import { getModelComposition } from "./getModelComposition"
|
||||||
|
import { getModelDefault } from "./getModelDefault"
|
||||||
|
import { getModelProperties } from "./getModelProperties"
|
||||||
|
import { getType } from "./getType"
|
||||||
|
|
||||||
|
export const getModel = (
|
||||||
|
openApi: OpenApi,
|
||||||
|
definition: OpenApiSchema,
|
||||||
|
isDefinition: boolean = false,
|
||||||
|
name: string = ""
|
||||||
|
): Model => {
|
||||||
|
const model: Model = {
|
||||||
|
spec: definition,
|
||||||
|
name,
|
||||||
|
export: "interface",
|
||||||
|
type: "any",
|
||||||
|
base: "any",
|
||||||
|
template: null,
|
||||||
|
link: null,
|
||||||
|
description: definition.description || null,
|
||||||
|
deprecated: definition.deprecated === true,
|
||||||
|
isDefinition,
|
||||||
|
isReadOnly: definition.readOnly === true,
|
||||||
|
isNullable: definition.nullable === true,
|
||||||
|
isRequired: false,
|
||||||
|
format: definition.format,
|
||||||
|
maximum: definition.maximum,
|
||||||
|
exclusiveMaximum: definition.exclusiveMaximum,
|
||||||
|
minimum: definition.minimum,
|
||||||
|
exclusiveMinimum: definition.exclusiveMinimum,
|
||||||
|
multipleOf: definition.multipleOf,
|
||||||
|
maxLength: definition.maxLength,
|
||||||
|
minLength: definition.minLength,
|
||||||
|
maxItems: definition.maxItems,
|
||||||
|
minItems: definition.minItems,
|
||||||
|
uniqueItems: definition.uniqueItems,
|
||||||
|
maxProperties: definition.maxProperties,
|
||||||
|
minProperties: definition.minProperties,
|
||||||
|
pattern: getPattern(definition.pattern),
|
||||||
|
imports: [],
|
||||||
|
enum: [],
|
||||||
|
enums: [],
|
||||||
|
properties: [],
|
||||||
|
}
|
||||||
|
|
||||||
|
if (definition.$ref) {
|
||||||
|
const definitionRef = getType(definition.$ref)
|
||||||
|
model.export = "reference"
|
||||||
|
model.type = definitionRef.type
|
||||||
|
model.base = definitionRef.base
|
||||||
|
model.template = definitionRef.template
|
||||||
|
model.imports.push(...definitionRef.imports)
|
||||||
|
model.default = getModelDefault(definition, model)
|
||||||
|
return model
|
||||||
|
}
|
||||||
|
|
||||||
|
if (definition.enum && definition.type !== "boolean") {
|
||||||
|
const enumerators = getEnum(definition.enum)
|
||||||
|
const extendedEnumerators = extendEnum(enumerators, definition)
|
||||||
|
if (extendedEnumerators.length) {
|
||||||
|
model.export = "enum"
|
||||||
|
model.type = "string"
|
||||||
|
model.base = "string"
|
||||||
|
model.enum.push(...extendedEnumerators)
|
||||||
|
model.default = getModelDefault(definition, model)
|
||||||
|
return model
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (definition.type === "array" && definition.items) {
|
||||||
|
if (definition.items.$ref) {
|
||||||
|
const arrayItems = getType(definition.items.$ref)
|
||||||
|
model.export = "array"
|
||||||
|
model.type = arrayItems.type
|
||||||
|
model.base = arrayItems.base
|
||||||
|
model.template = arrayItems.template
|
||||||
|
model.imports.push(...arrayItems.imports)
|
||||||
|
model.default = getModelDefault(definition, model)
|
||||||
|
return model
|
||||||
|
} else {
|
||||||
|
const arrayItems = getModel(openApi, definition.items)
|
||||||
|
model.export = "array"
|
||||||
|
model.type = arrayItems.type
|
||||||
|
model.base = arrayItems.base
|
||||||
|
model.template = arrayItems.template
|
||||||
|
model.link = arrayItems
|
||||||
|
model.imports.push(...arrayItems.imports)
|
||||||
|
model.default = getModelDefault(definition, model)
|
||||||
|
return model
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
definition.type === "object" &&
|
||||||
|
(typeof definition.additionalProperties === "object" ||
|
||||||
|
definition.additionalProperties === true)
|
||||||
|
) {
|
||||||
|
const ap =
|
||||||
|
typeof definition.additionalProperties === "object"
|
||||||
|
? definition.additionalProperties
|
||||||
|
: {}
|
||||||
|
if (ap.$ref) {
|
||||||
|
const additionalProperties = getType(ap.$ref)
|
||||||
|
model.export = "dictionary"
|
||||||
|
model.type = additionalProperties.type
|
||||||
|
model.base = additionalProperties.base
|
||||||
|
model.template = additionalProperties.template
|
||||||
|
model.imports.push(...additionalProperties.imports)
|
||||||
|
model.default = getModelDefault(definition, model)
|
||||||
|
return model
|
||||||
|
} else {
|
||||||
|
const additionalProperties = getModel(openApi, ap)
|
||||||
|
model.export = "dictionary"
|
||||||
|
model.type = additionalProperties.type
|
||||||
|
model.base = additionalProperties.base
|
||||||
|
model.template = additionalProperties.template
|
||||||
|
model.link = additionalProperties
|
||||||
|
model.imports.push(...additionalProperties.imports)
|
||||||
|
model.default = getModelDefault(definition, model)
|
||||||
|
return model
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (definition.oneOf?.length) {
|
||||||
|
const composition = getModelComposition(
|
||||||
|
openApi,
|
||||||
|
definition,
|
||||||
|
definition.oneOf,
|
||||||
|
"one-of",
|
||||||
|
getModel
|
||||||
|
)
|
||||||
|
model.export = composition.type
|
||||||
|
model.imports.push(...composition.imports)
|
||||||
|
model.properties.push(...composition.properties)
|
||||||
|
model.enums.push(...composition.enums)
|
||||||
|
return model
|
||||||
|
}
|
||||||
|
|
||||||
|
if (definition.anyOf?.length) {
|
||||||
|
const composition = getModelComposition(
|
||||||
|
openApi,
|
||||||
|
definition,
|
||||||
|
definition.anyOf,
|
||||||
|
"any-of",
|
||||||
|
getModel
|
||||||
|
)
|
||||||
|
model.export = composition.type
|
||||||
|
model.imports.push(...composition.imports)
|
||||||
|
model.properties.push(...composition.properties)
|
||||||
|
model.enums.push(...composition.enums)
|
||||||
|
return model
|
||||||
|
}
|
||||||
|
|
||||||
|
if (definition.allOf?.length) {
|
||||||
|
const composition = getModelComposition(
|
||||||
|
openApi,
|
||||||
|
definition,
|
||||||
|
definition.allOf,
|
||||||
|
"all-of",
|
||||||
|
getModel
|
||||||
|
)
|
||||||
|
model.export = composition.type
|
||||||
|
model.imports.push(...composition.imports)
|
||||||
|
model.properties.push(...composition.properties)
|
||||||
|
model.enums.push(...composition.enums)
|
||||||
|
return model
|
||||||
|
}
|
||||||
|
|
||||||
|
if (definition.type === "object") {
|
||||||
|
if (definition.properties) {
|
||||||
|
model.export = "interface"
|
||||||
|
model.type = "any"
|
||||||
|
model.base = "any"
|
||||||
|
model.default = getModelDefault(definition, model)
|
||||||
|
|
||||||
|
const modelProperties = getModelProperties(
|
||||||
|
openApi,
|
||||||
|
definition,
|
||||||
|
getModel,
|
||||||
|
model
|
||||||
|
)
|
||||||
|
modelProperties.forEach((modelProperty) => {
|
||||||
|
model.imports.push(...modelProperty.imports)
|
||||||
|
model.enums.push(...modelProperty.enums)
|
||||||
|
model.properties.push(modelProperty)
|
||||||
|
if (modelProperty.export === "enum") {
|
||||||
|
model.enums.push(modelProperty)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return model
|
||||||
|
} else {
|
||||||
|
const additionalProperties = getModel(openApi, {})
|
||||||
|
model.export = "dictionary"
|
||||||
|
model.type = additionalProperties.type
|
||||||
|
model.base = additionalProperties.base
|
||||||
|
model.template = additionalProperties.template
|
||||||
|
model.link = additionalProperties
|
||||||
|
model.imports.push(...additionalProperties.imports)
|
||||||
|
model.default = getModelDefault(definition, model)
|
||||||
|
return model
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the schema has a type than it can be a basic or generic type.
|
||||||
|
if (definition.type) {
|
||||||
|
const definitionType = getType(definition.type, definition.format)
|
||||||
|
model.export = "generic"
|
||||||
|
model.type = definitionType.type
|
||||||
|
model.base = definitionType.base
|
||||||
|
model.template = definitionType.template
|
||||||
|
model.isNullable = definitionType.isNullable || model.isNullable
|
||||||
|
model.imports.push(...definitionType.imports)
|
||||||
|
model.default = getModelDefault(definition, model)
|
||||||
|
return model
|
||||||
|
}
|
||||||
|
|
||||||
|
return model
|
||||||
|
}
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
import type { Model } from "../../../client/interfaces/Model"
|
||||||
|
import type { ModelComposition } from "../../../client/interfaces/ModelComposition"
|
||||||
|
import type { OpenApi } from "../interfaces/OpenApi"
|
||||||
|
import type { OpenApiSchema } from "../interfaces/OpenApiSchema"
|
||||||
|
import type { getModel } from "./getModel"
|
||||||
|
import { getModelProperties } from "./getModelProperties"
|
||||||
|
import { getRequiredPropertiesFromComposition } from "./getRequiredPropertiesFromComposition"
|
||||||
|
|
||||||
|
// Fix for circular dependency
|
||||||
|
export type GetModelFn = typeof getModel
|
||||||
|
|
||||||
|
export const getModelComposition = (
|
||||||
|
openApi: OpenApi,
|
||||||
|
definition: OpenApiSchema,
|
||||||
|
definitions: OpenApiSchema[],
|
||||||
|
type: "one-of" | "any-of" | "all-of",
|
||||||
|
getModel: GetModelFn
|
||||||
|
): ModelComposition => {
|
||||||
|
const composition: ModelComposition = {
|
||||||
|
type,
|
||||||
|
imports: [],
|
||||||
|
enums: [],
|
||||||
|
properties: [],
|
||||||
|
}
|
||||||
|
|
||||||
|
const properties: Model[] = []
|
||||||
|
|
||||||
|
definitions
|
||||||
|
.map((definition) => getModel(openApi, definition))
|
||||||
|
.filter((model) => {
|
||||||
|
const hasProperties = model.properties.length
|
||||||
|
const hasEnums = model.enums.length
|
||||||
|
const isObject = model.type === "any"
|
||||||
|
const isDictionary = model.export === "dictionary"
|
||||||
|
const isEmpty = isObject && !hasProperties && !hasEnums
|
||||||
|
return !isEmpty || isDictionary
|
||||||
|
})
|
||||||
|
.forEach((model) => {
|
||||||
|
composition.imports.push(...model.imports)
|
||||||
|
composition.enums.push(...model.enums)
|
||||||
|
composition.properties.push(model)
|
||||||
|
})
|
||||||
|
|
||||||
|
if (definition.required) {
|
||||||
|
const requiredProperties = getRequiredPropertiesFromComposition(
|
||||||
|
openApi,
|
||||||
|
definition.required,
|
||||||
|
definitions,
|
||||||
|
getModel
|
||||||
|
)
|
||||||
|
requiredProperties.forEach((requiredProperty) => {
|
||||||
|
composition.imports.push(...requiredProperty.imports)
|
||||||
|
composition.enums.push(...requiredProperty.enums)
|
||||||
|
})
|
||||||
|
properties.push(...requiredProperties)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (definition.properties) {
|
||||||
|
const modelProperties = getModelProperties(openApi, definition, getModel)
|
||||||
|
modelProperties.forEach((modelProperty) => {
|
||||||
|
composition.imports.push(...modelProperty.imports)
|
||||||
|
composition.enums.push(...modelProperty.enums)
|
||||||
|
if (modelProperty.export === "enum") {
|
||||||
|
composition.enums.push(modelProperty)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
properties.push(...modelProperties)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (properties.length) {
|
||||||
|
composition.properties.push({
|
||||||
|
spec: definition,
|
||||||
|
name: "properties",
|
||||||
|
export: "interface",
|
||||||
|
type: "any",
|
||||||
|
base: "any",
|
||||||
|
template: null,
|
||||||
|
link: null,
|
||||||
|
description: "",
|
||||||
|
isDefinition: false,
|
||||||
|
isReadOnly: false,
|
||||||
|
isNullable: false,
|
||||||
|
isRequired: false,
|
||||||
|
imports: [],
|
||||||
|
enum: [],
|
||||||
|
enums: [],
|
||||||
|
properties,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return composition
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
import type { Model } from "../../../client/interfaces/Model"
|
||||||
|
import type { OpenApiSchema } from "../interfaces/OpenApiSchema"
|
||||||
|
|
||||||
|
export const getModelDefault = (
|
||||||
|
definition: OpenApiSchema,
|
||||||
|
model?: Model
|
||||||
|
): string | undefined => {
|
||||||
|
if (definition.default === undefined) {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
if (definition.default === null) {
|
||||||
|
return "null"
|
||||||
|
}
|
||||||
|
|
||||||
|
const type = definition.type || typeof definition.default
|
||||||
|
|
||||||
|
switch (type) {
|
||||||
|
case "int":
|
||||||
|
case "integer":
|
||||||
|
case "number":
|
||||||
|
if (model?.export === "enum" && model.enum?.[definition.default]) {
|
||||||
|
return model.enum[definition.default].value
|
||||||
|
}
|
||||||
|
return definition.default
|
||||||
|
|
||||||
|
case "boolean":
|
||||||
|
return JSON.stringify(definition.default)
|
||||||
|
|
||||||
|
case "string":
|
||||||
|
return `'${definition.default}'`
|
||||||
|
|
||||||
|
case "object":
|
||||||
|
try {
|
||||||
|
return JSON.stringify(definition.default, null, 4)
|
||||||
|
} catch (e) {
|
||||||
|
// Ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
@@ -0,0 +1,112 @@
|
|||||||
|
import type { Model } from "../../../client/interfaces/Model"
|
||||||
|
import {
|
||||||
|
findOneOfParentDiscriminator,
|
||||||
|
mapPropertyValue,
|
||||||
|
} from "../../../utils/discriminator"
|
||||||
|
import { getPattern } from "../../../utils/getPattern"
|
||||||
|
import type { OpenApi } from "../interfaces/OpenApi"
|
||||||
|
import type { OpenApiSchema } from "../interfaces/OpenApiSchema"
|
||||||
|
import { escapeName } from "./escapeName"
|
||||||
|
import type { getModel } from "./getModel"
|
||||||
|
import { getType } from "./getType"
|
||||||
|
|
||||||
|
// Fix for circular dependency
|
||||||
|
export type GetModelFn = typeof getModel
|
||||||
|
|
||||||
|
export const getModelProperties = (
|
||||||
|
openApi: OpenApi,
|
||||||
|
definition: OpenApiSchema,
|
||||||
|
getModel: GetModelFn,
|
||||||
|
parent?: Model
|
||||||
|
): Model[] => {
|
||||||
|
const models: Model[] = []
|
||||||
|
const discriminator = findOneOfParentDiscriminator(openApi, parent)
|
||||||
|
for (const propertyName in definition.properties) {
|
||||||
|
if (definition.properties.hasOwnProperty(propertyName)) {
|
||||||
|
const property = definition.properties[propertyName]
|
||||||
|
const propertyRequired = !!definition.required?.includes(propertyName)
|
||||||
|
const propertyValues: Omit<
|
||||||
|
Model,
|
||||||
|
| "export"
|
||||||
|
| "type"
|
||||||
|
| "base"
|
||||||
|
| "template"
|
||||||
|
| "link"
|
||||||
|
| "isNullable"
|
||||||
|
| "imports"
|
||||||
|
| "enum"
|
||||||
|
| "enums"
|
||||||
|
| "properties"
|
||||||
|
> = {
|
||||||
|
spec: definition,
|
||||||
|
name: escapeName(propertyName),
|
||||||
|
description: property.description || null,
|
||||||
|
deprecated: property.deprecated === true,
|
||||||
|
isDefinition: false,
|
||||||
|
isReadOnly: property.readOnly === true,
|
||||||
|
isRequired: propertyRequired,
|
||||||
|
format: property.format,
|
||||||
|
maximum: property.maximum,
|
||||||
|
exclusiveMaximum: property.exclusiveMaximum,
|
||||||
|
minimum: property.minimum,
|
||||||
|
exclusiveMinimum: property.exclusiveMinimum,
|
||||||
|
multipleOf: property.multipleOf,
|
||||||
|
maxLength: property.maxLength,
|
||||||
|
minLength: property.minLength,
|
||||||
|
maxItems: property.maxItems,
|
||||||
|
minItems: property.minItems,
|
||||||
|
uniqueItems: property.uniqueItems,
|
||||||
|
maxProperties: property.maxProperties,
|
||||||
|
minProperties: property.minProperties,
|
||||||
|
pattern: getPattern(property.pattern),
|
||||||
|
}
|
||||||
|
if (parent && discriminator?.propertyName == propertyName) {
|
||||||
|
models.push({
|
||||||
|
export: "reference",
|
||||||
|
type: "string",
|
||||||
|
base: `'${mapPropertyValue(discriminator, parent)}'`,
|
||||||
|
template: null,
|
||||||
|
isNullable: property.nullable === true,
|
||||||
|
link: null,
|
||||||
|
imports: [],
|
||||||
|
enum: [],
|
||||||
|
enums: [],
|
||||||
|
properties: [],
|
||||||
|
...propertyValues,
|
||||||
|
})
|
||||||
|
} else if (property.$ref) {
|
||||||
|
const model = getType(property.$ref)
|
||||||
|
models.push({
|
||||||
|
export: "reference",
|
||||||
|
type: model.type,
|
||||||
|
base: model.base,
|
||||||
|
template: model.template,
|
||||||
|
link: null,
|
||||||
|
isNullable: model.isNullable || property.nullable === true,
|
||||||
|
imports: model.imports,
|
||||||
|
enum: [],
|
||||||
|
enums: [],
|
||||||
|
properties: [],
|
||||||
|
...propertyValues,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
const model = getModel(openApi, property)
|
||||||
|
models.push({
|
||||||
|
export: model.export,
|
||||||
|
type: model.type,
|
||||||
|
base: model.base,
|
||||||
|
template: model.template,
|
||||||
|
link: model.link,
|
||||||
|
isNullable: model.isNullable || property.nullable === true,
|
||||||
|
imports: model.imports,
|
||||||
|
enum: model.enum,
|
||||||
|
enums: model.enums,
|
||||||
|
properties: model.properties,
|
||||||
|
...propertyValues,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return models
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import type { Type } from "../../../client/interfaces/Type"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If our model has a template type, then we want to generalize that!
|
||||||
|
* In that case we should return "<T>" as our template type.
|
||||||
|
* @param modelClass The parsed model class type.
|
||||||
|
* @returns The model template type (<T> or empty).
|
||||||
|
*/
|
||||||
|
export const getModelTemplate = (modelClass: Type): string => {
|
||||||
|
return modelClass.template ? "<T>" : ""
|
||||||
|
}
|
||||||
@@ -0,0 +1,112 @@
|
|||||||
|
import type { Model } from "../../../client/interfaces/Model"
|
||||||
|
import type { OpenApi } from "../interfaces/OpenApi"
|
||||||
|
import { getModel } from "./getModel"
|
||||||
|
import { reservedWords } from "./getOperationParameterName"
|
||||||
|
import { getType } from "./getType"
|
||||||
|
import { OperationParameter } from "../../../client/interfaces/OperationParameter"
|
||||||
|
import { OpenApiSchema } from "../interfaces/OpenApiSchema"
|
||||||
|
import { Dictionary } from "../../../utils/types"
|
||||||
|
import { OpenApiParameter } from "../interfaces/OpenApiParameter"
|
||||||
|
import { listOperations } from "./listOperations"
|
||||||
|
|
||||||
|
export const getModels = (openApi: OpenApi): Model[] => {
|
||||||
|
const models: Model[] = []
|
||||||
|
if (openApi.components) {
|
||||||
|
for (const definitionName in openApi.components.schemas) {
|
||||||
|
if (openApi.components.schemas.hasOwnProperty(definitionName)) {
|
||||||
|
const definition = openApi.components.schemas[definitionName]
|
||||||
|
const definitionType = getType(definitionName)
|
||||||
|
const model = getModel(
|
||||||
|
openApi,
|
||||||
|
definition,
|
||||||
|
true,
|
||||||
|
definitionType.base.replace(reservedWords, "_$1")
|
||||||
|
)
|
||||||
|
models.push(model)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bundle all query parameters in a single typed object
|
||||||
|
* when x-codegen.queryParams is declared on the operation.
|
||||||
|
*/
|
||||||
|
const operations = listOperations(openApi)
|
||||||
|
for (const operation of operations) {
|
||||||
|
if (operation.codegen.queryParams) {
|
||||||
|
const definition = getDefinitionFromParametersQuery(
|
||||||
|
operation.parametersQuery
|
||||||
|
)
|
||||||
|
const model = getModel(
|
||||||
|
openApi,
|
||||||
|
definition,
|
||||||
|
true,
|
||||||
|
operation.codegen.queryParams
|
||||||
|
)
|
||||||
|
models.push(model)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return models
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Combines and converts query parameters into schema properties.
|
||||||
|
* Given a typical parameter OAS:
|
||||||
|
* ```
|
||||||
|
* parameters:
|
||||||
|
* - in: query
|
||||||
|
* name: limit
|
||||||
|
* schema:
|
||||||
|
* type: integer
|
||||||
|
* default: 10
|
||||||
|
* required: true
|
||||||
|
* description: Limit the number of results returned.
|
||||||
|
* ```
|
||||||
|
* Convert into a schema property:
|
||||||
|
* ```
|
||||||
|
* UnnamedSchema:
|
||||||
|
* type: object
|
||||||
|
* required:
|
||||||
|
* - limit
|
||||||
|
* properties:
|
||||||
|
* limit:
|
||||||
|
* description: Limit the number of results returned.
|
||||||
|
* type: integer
|
||||||
|
* default: 10
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
const getDefinitionFromParametersQuery = (
|
||||||
|
parametersQuery: OperationParameter[]
|
||||||
|
): OpenApiSchema => {
|
||||||
|
/**
|
||||||
|
* Identify query parameters that are required (non-optional).
|
||||||
|
*/
|
||||||
|
const required = parametersQuery
|
||||||
|
.filter((parameter) => (parameter.spec as OpenApiParameter).required)
|
||||||
|
.map((parameter) => (parameter.spec as OpenApiParameter).name)
|
||||||
|
|
||||||
|
const properties: Dictionary<OpenApiSchema> = {}
|
||||||
|
for (const parameter of parametersQuery) {
|
||||||
|
const spec = parameter.spec as OpenApiParameter
|
||||||
|
/**
|
||||||
|
* Augment a copy of schema with description and deprecated
|
||||||
|
* and assign it as a named property on the schema.
|
||||||
|
*/
|
||||||
|
properties[spec.name] = Object.assign(
|
||||||
|
{ ...spec.schema },
|
||||||
|
{
|
||||||
|
description: spec.description,
|
||||||
|
deprecated: spec.deprecated,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Return an unnamed schema definition.
|
||||||
|
*/
|
||||||
|
return {
|
||||||
|
type: "object",
|
||||||
|
required,
|
||||||
|
properties,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
import type { Operation } from "../../../client/interfaces/Operation"
|
||||||
|
import type { OperationParameters } from "../../../client/interfaces/OperationParameters"
|
||||||
|
import type { OperationCodegen } from "../../../client/interfaces/OperationCodegen"
|
||||||
|
import type { OpenApi } from "../interfaces/OpenApi"
|
||||||
|
import type { OpenApiOperation } from "../interfaces/OpenApiOperation"
|
||||||
|
import type { OpenApiRequestBody } from "../interfaces/OpenApiRequestBody"
|
||||||
|
import { getOperationErrors } from "./getOperationErrors"
|
||||||
|
import { getOperationName } from "./getOperationName"
|
||||||
|
import { getOperationParameters } from "./getOperationParameters"
|
||||||
|
import { getOperationRequestBody } from "./getOperationRequestBody"
|
||||||
|
import { getOperationResponseHeader } from "./getOperationResponseHeader"
|
||||||
|
import { getOperationResponses } from "./getOperationResponses"
|
||||||
|
import { getOperationResults } from "./getOperationResults"
|
||||||
|
import { getRef } from "./getRef"
|
||||||
|
import { getServiceName } from "./getServiceName"
|
||||||
|
import { sortByRequired } from "./sortByRequired"
|
||||||
|
|
||||||
|
export const getOperation = (
|
||||||
|
openApi: OpenApi,
|
||||||
|
url: string,
|
||||||
|
method: string,
|
||||||
|
tag: string,
|
||||||
|
op: OpenApiOperation,
|
||||||
|
pathParams: OperationParameters
|
||||||
|
): Operation => {
|
||||||
|
const serviceName = getServiceName(tag)
|
||||||
|
const operationName = getOperationName(url, method, op.operationId)
|
||||||
|
const codegen: OperationCodegen = op["x-codegen"] || {}
|
||||||
|
|
||||||
|
// Create a new operation object for this method.
|
||||||
|
const operation: Operation = {
|
||||||
|
service: serviceName,
|
||||||
|
name: codegen.method || operationName,
|
||||||
|
operationId: op.operationId || null,
|
||||||
|
summary: op.summary || null,
|
||||||
|
description: op.description || null,
|
||||||
|
deprecated: op.deprecated === true,
|
||||||
|
method: method.toUpperCase(),
|
||||||
|
path: url,
|
||||||
|
parameters: [...pathParams.parameters],
|
||||||
|
parametersPath: [...pathParams.parametersPath],
|
||||||
|
parametersQuery: [...pathParams.parametersQuery],
|
||||||
|
parametersForm: [...pathParams.parametersForm],
|
||||||
|
parametersHeader: [...pathParams.parametersHeader],
|
||||||
|
parametersCookie: [...pathParams.parametersCookie],
|
||||||
|
parametersBody: pathParams.parametersBody,
|
||||||
|
imports: [],
|
||||||
|
errors: [],
|
||||||
|
results: [],
|
||||||
|
responseHeader: null,
|
||||||
|
codegen,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse the operation parameters (path, query, body, etc).
|
||||||
|
if (op.parameters) {
|
||||||
|
const parameters = getOperationParameters(openApi, op.parameters)
|
||||||
|
operation.imports.push(...parameters.imports)
|
||||||
|
operation.parameters.push(...parameters.parameters)
|
||||||
|
operation.parametersPath.push(...parameters.parametersPath)
|
||||||
|
operation.parametersQuery.push(...parameters.parametersQuery)
|
||||||
|
operation.parametersForm.push(...parameters.parametersForm)
|
||||||
|
operation.parametersHeader.push(...parameters.parametersHeader)
|
||||||
|
operation.parametersCookie.push(...parameters.parametersCookie)
|
||||||
|
operation.parametersBody = parameters.parametersBody
|
||||||
|
}
|
||||||
|
|
||||||
|
if (op.requestBody) {
|
||||||
|
const requestBodyDef = getRef<OpenApiRequestBody>(openApi, op.requestBody)
|
||||||
|
const requestBody = getOperationRequestBody(openApi, requestBodyDef)
|
||||||
|
operation.imports.push(...requestBody.imports)
|
||||||
|
operation.parameters.push(requestBody)
|
||||||
|
operation.parametersBody = requestBody
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse the operation responses.
|
||||||
|
if (op.responses) {
|
||||||
|
const operationResponses = getOperationResponses(openApi, op.responses)
|
||||||
|
const operationResults = getOperationResults(operationResponses)
|
||||||
|
operation.errors = getOperationErrors(operationResponses)
|
||||||
|
operation.responseHeader = getOperationResponseHeader(operationResults)
|
||||||
|
|
||||||
|
operationResults.forEach((operationResult) => {
|
||||||
|
operation.results.push(operationResult)
|
||||||
|
operation.imports.push(...operationResult.imports)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (codegen.queryParams) {
|
||||||
|
operation.imports.push(codegen.queryParams)
|
||||||
|
}
|
||||||
|
|
||||||
|
operation.parameters = operation.parameters.sort(sortByRequired)
|
||||||
|
|
||||||
|
return operation
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import type { OperationError } from "../../../client/interfaces/OperationError"
|
||||||
|
import type { OperationResponse } from "../../../client/interfaces/OperationResponse"
|
||||||
|
|
||||||
|
export const getOperationErrors = (
|
||||||
|
operationResponses: OperationResponse[]
|
||||||
|
): OperationError[] => {
|
||||||
|
return operationResponses
|
||||||
|
.filter((operationResponse) => {
|
||||||
|
return operationResponse.code >= 300 && operationResponse.description
|
||||||
|
})
|
||||||
|
.map((response) => ({
|
||||||
|
code: response.code,
|
||||||
|
description: response.description!,
|
||||||
|
}))
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import camelCase from "camelcase"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert the input value to a correct operation (method) classname.
|
||||||
|
* This will use the operation ID - if available - and otherwise fallback
|
||||||
|
* on a generated name from the URL
|
||||||
|
*/
|
||||||
|
export const getOperationName = (
|
||||||
|
url: string,
|
||||||
|
method: string,
|
||||||
|
operationId?: string
|
||||||
|
): string => {
|
||||||
|
if (operationId) {
|
||||||
|
return camelCase(
|
||||||
|
operationId
|
||||||
|
.replace(/^[^a-zA-Z]+/g, "")
|
||||||
|
.replace(/[^\w\-]+/g, "-")
|
||||||
|
.trim()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const urlWithoutPlaceholders = url
|
||||||
|
.replace(/[^/]*?{api-version}.*?\//g, "")
|
||||||
|
.replace(/{(.*?)}/g, "")
|
||||||
|
.replace(/\//g, "-")
|
||||||
|
|
||||||
|
return camelCase(`${method}-${urlWithoutPlaceholders}`)
|
||||||
|
}
|
||||||
+99
@@ -0,0 +1,99 @@
|
|||||||
|
import type { OperationParameter } from "../../../client/interfaces/OperationParameter"
|
||||||
|
import { getPattern } from "../../../utils/getPattern"
|
||||||
|
import type { OpenApi } from "../interfaces/OpenApi"
|
||||||
|
import type { OpenApiParameter } from "../interfaces/OpenApiParameter"
|
||||||
|
import type { OpenApiSchema } from "../interfaces/OpenApiSchema"
|
||||||
|
import { getModel } from "./getModel"
|
||||||
|
import { getModelDefault } from "./getModelDefault"
|
||||||
|
import { getOperationParameterName } from "./getOperationParameterName"
|
||||||
|
import { getRef } from "./getRef"
|
||||||
|
import { getType } from "./getType"
|
||||||
|
|
||||||
|
export const getOperationParameter = (
|
||||||
|
openApi: OpenApi,
|
||||||
|
parameter: OpenApiParameter
|
||||||
|
): OperationParameter => {
|
||||||
|
const operationParameter: OperationParameter = {
|
||||||
|
spec: parameter,
|
||||||
|
in: parameter.in,
|
||||||
|
prop: parameter.name,
|
||||||
|
export: "interface",
|
||||||
|
name: getOperationParameterName(parameter.name),
|
||||||
|
type: "any",
|
||||||
|
base: "any",
|
||||||
|
template: null,
|
||||||
|
link: null,
|
||||||
|
description: parameter.description || null,
|
||||||
|
deprecated: parameter.deprecated === true,
|
||||||
|
isDefinition: false,
|
||||||
|
isReadOnly: false,
|
||||||
|
isRequired: parameter.required === true,
|
||||||
|
isNullable: parameter.nullable === true,
|
||||||
|
imports: [],
|
||||||
|
enum: [],
|
||||||
|
enums: [],
|
||||||
|
properties: [],
|
||||||
|
mediaType: null,
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parameter.$ref) {
|
||||||
|
const definitionRef = getType(parameter.$ref)
|
||||||
|
operationParameter.export = "reference"
|
||||||
|
operationParameter.type = definitionRef.type
|
||||||
|
operationParameter.base = definitionRef.base
|
||||||
|
operationParameter.template = definitionRef.template
|
||||||
|
operationParameter.imports.push(...definitionRef.imports)
|
||||||
|
return operationParameter
|
||||||
|
}
|
||||||
|
|
||||||
|
let schema = parameter.schema
|
||||||
|
if (schema) {
|
||||||
|
if (schema.$ref?.startsWith("#/components/parameters/")) {
|
||||||
|
schema = getRef<OpenApiSchema>(openApi, schema)
|
||||||
|
}
|
||||||
|
if (schema.$ref) {
|
||||||
|
const model = getType(schema.$ref)
|
||||||
|
operationParameter.export = "reference"
|
||||||
|
operationParameter.type = model.type
|
||||||
|
operationParameter.base = model.base
|
||||||
|
operationParameter.template = model.template
|
||||||
|
operationParameter.imports.push(...model.imports)
|
||||||
|
operationParameter.default = getModelDefault(schema)
|
||||||
|
return operationParameter
|
||||||
|
} else {
|
||||||
|
const model = getModel(openApi, schema)
|
||||||
|
operationParameter.export = model.export
|
||||||
|
operationParameter.type = model.type
|
||||||
|
operationParameter.base = model.base
|
||||||
|
operationParameter.template = model.template
|
||||||
|
operationParameter.link = model.link
|
||||||
|
operationParameter.isReadOnly = model.isReadOnly
|
||||||
|
operationParameter.isRequired =
|
||||||
|
operationParameter.isRequired || model.isRequired
|
||||||
|
operationParameter.isNullable =
|
||||||
|
operationParameter.isNullable || model.isNullable
|
||||||
|
operationParameter.format = model.format
|
||||||
|
operationParameter.maximum = model.maximum
|
||||||
|
operationParameter.exclusiveMaximum = model.exclusiveMaximum
|
||||||
|
operationParameter.minimum = model.minimum
|
||||||
|
operationParameter.exclusiveMinimum = model.exclusiveMinimum
|
||||||
|
operationParameter.multipleOf = model.multipleOf
|
||||||
|
operationParameter.maxLength = model.maxLength
|
||||||
|
operationParameter.minLength = model.minLength
|
||||||
|
operationParameter.maxItems = model.maxItems
|
||||||
|
operationParameter.minItems = model.minItems
|
||||||
|
operationParameter.uniqueItems = model.uniqueItems
|
||||||
|
operationParameter.maxProperties = model.maxProperties
|
||||||
|
operationParameter.minProperties = model.minProperties
|
||||||
|
operationParameter.pattern = getPattern(model.pattern)
|
||||||
|
operationParameter.default = model.default
|
||||||
|
operationParameter.imports.push(...model.imports)
|
||||||
|
operationParameter.enum.push(...model.enum)
|
||||||
|
operationParameter.enums.push(...model.enums)
|
||||||
|
operationParameter.properties.push(...model.properties)
|
||||||
|
return operationParameter
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return operationParameter
|
||||||
|
}
|
||||||
+16
@@ -0,0 +1,16 @@
|
|||||||
|
import camelCase from "camelcase"
|
||||||
|
|
||||||
|
export const reservedWords =
|
||||||
|
/^(arguments|break|case|catch|class|const|continue|debugger|default|delete|do|else|enum|eval|export|extends|false|finally|for|function|if|implements|import|in|instanceof|interface|let|new|null|package|private|protected|public|return|static|super|switch|this|throw|true|try|typeof|var|void|while|with|yield)$/g
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Replaces any invalid characters from a parameter name.
|
||||||
|
* For example: 'filter.someProperty' becomes 'filterSomeProperty'.
|
||||||
|
*/
|
||||||
|
export const getOperationParameterName = (value: string): string => {
|
||||||
|
const clean = value
|
||||||
|
.replace(/^[^a-zA-Z]+/g, "")
|
||||||
|
.replace(/[^\w\-]+/g, "-")
|
||||||
|
.trim()
|
||||||
|
return camelCase(clean).replace(reservedWords, "_$1")
|
||||||
|
}
|
||||||
+64
@@ -0,0 +1,64 @@
|
|||||||
|
import type { OperationParameters } from "../../../client/interfaces/OperationParameters"
|
||||||
|
import type { OpenApi } from "../interfaces/OpenApi"
|
||||||
|
import type { OpenApiParameter } from "../interfaces/OpenApiParameter"
|
||||||
|
import { getOperationParameter } from "./getOperationParameter"
|
||||||
|
import { getRef } from "./getRef"
|
||||||
|
|
||||||
|
export const getOperationParameters = (
|
||||||
|
openApi: OpenApi,
|
||||||
|
parameters: OpenApiParameter[]
|
||||||
|
): OperationParameters => {
|
||||||
|
const operationParameters: OperationParameters = {
|
||||||
|
imports: [],
|
||||||
|
parameters: [],
|
||||||
|
parametersPath: [],
|
||||||
|
parametersQuery: [],
|
||||||
|
parametersForm: [],
|
||||||
|
parametersCookie: [],
|
||||||
|
parametersHeader: [],
|
||||||
|
parametersBody: null, // Not used in V3 -> @see requestBody
|
||||||
|
}
|
||||||
|
|
||||||
|
// Iterate over the parameters
|
||||||
|
parameters.forEach((parameterOrReference) => {
|
||||||
|
const parameterDef = getRef<OpenApiParameter>(openApi, parameterOrReference)
|
||||||
|
const parameter = getOperationParameter(openApi, parameterDef)
|
||||||
|
|
||||||
|
// We ignore the "api-version" param, since we do not want to add this
|
||||||
|
// as the first / default parameter for each of the service calls.
|
||||||
|
if (parameter.prop !== "api-version") {
|
||||||
|
switch (parameterDef.in) {
|
||||||
|
case "path":
|
||||||
|
operationParameters.parametersPath.push(parameter)
|
||||||
|
operationParameters.parameters.push(parameter)
|
||||||
|
operationParameters.imports.push(...parameter.imports)
|
||||||
|
break
|
||||||
|
|
||||||
|
case "query":
|
||||||
|
operationParameters.parametersQuery.push(parameter)
|
||||||
|
operationParameters.parameters.push(parameter)
|
||||||
|
operationParameters.imports.push(...parameter.imports)
|
||||||
|
break
|
||||||
|
|
||||||
|
case "formData":
|
||||||
|
operationParameters.parametersForm.push(parameter)
|
||||||
|
operationParameters.parameters.push(parameter)
|
||||||
|
operationParameters.imports.push(...parameter.imports)
|
||||||
|
break
|
||||||
|
|
||||||
|
case "cookie":
|
||||||
|
operationParameters.parametersCookie.push(parameter)
|
||||||
|
operationParameters.parameters.push(parameter)
|
||||||
|
operationParameters.imports.push(...parameter.imports)
|
||||||
|
break
|
||||||
|
|
||||||
|
case "header":
|
||||||
|
operationParameters.parametersHeader.push(parameter)
|
||||||
|
operationParameters.parameters.push(parameter)
|
||||||
|
operationParameters.imports.push(...parameter.imports)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return operationParameters
|
||||||
|
}
|
||||||
+90
@@ -0,0 +1,90 @@
|
|||||||
|
import type { OperationParameter } from "../../../client/interfaces/OperationParameter"
|
||||||
|
import { getPattern } from "../../../utils/getPattern"
|
||||||
|
import type { OpenApi } from "../interfaces/OpenApi"
|
||||||
|
import type { OpenApiRequestBody } from "../interfaces/OpenApiRequestBody"
|
||||||
|
import { getContent } from "./getContent"
|
||||||
|
import { getModel } from "./getModel"
|
||||||
|
import { getType } from "./getType"
|
||||||
|
|
||||||
|
export const getOperationRequestBody = (
|
||||||
|
openApi: OpenApi,
|
||||||
|
body: OpenApiRequestBody
|
||||||
|
): OperationParameter => {
|
||||||
|
const requestBody: OperationParameter = {
|
||||||
|
spec: body,
|
||||||
|
in: "body",
|
||||||
|
export: "interface",
|
||||||
|
prop: "requestBody",
|
||||||
|
name: "requestBody",
|
||||||
|
type: "any",
|
||||||
|
base: "any",
|
||||||
|
template: null,
|
||||||
|
link: null,
|
||||||
|
description: body.description || null,
|
||||||
|
default: undefined,
|
||||||
|
isDefinition: false,
|
||||||
|
isReadOnly: false,
|
||||||
|
isRequired: body.required === true,
|
||||||
|
isNullable: body.nullable === true,
|
||||||
|
imports: [],
|
||||||
|
enum: [],
|
||||||
|
enums: [],
|
||||||
|
properties: [],
|
||||||
|
mediaType: null,
|
||||||
|
}
|
||||||
|
|
||||||
|
if (body.content) {
|
||||||
|
const content = getContent(openApi, body.content)
|
||||||
|
if (content) {
|
||||||
|
requestBody.mediaType = content.mediaType
|
||||||
|
switch (requestBody.mediaType) {
|
||||||
|
case "application/x-www-form-urlencoded":
|
||||||
|
case "multipart/form-data":
|
||||||
|
requestBody.in = "formData"
|
||||||
|
requestBody.name = "formData"
|
||||||
|
requestBody.prop = "formData"
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if (content.schema.$ref) {
|
||||||
|
const model = getType(content.schema.$ref)
|
||||||
|
requestBody.export = "reference"
|
||||||
|
requestBody.type = model.type
|
||||||
|
requestBody.base = model.base
|
||||||
|
requestBody.template = model.template
|
||||||
|
requestBody.imports.push(...model.imports)
|
||||||
|
return requestBody
|
||||||
|
} else {
|
||||||
|
const model = getModel(openApi, content.schema)
|
||||||
|
requestBody.export = model.export
|
||||||
|
requestBody.type = model.type
|
||||||
|
requestBody.base = model.base
|
||||||
|
requestBody.template = model.template
|
||||||
|
requestBody.link = model.link
|
||||||
|
requestBody.isReadOnly = model.isReadOnly
|
||||||
|
requestBody.isRequired = requestBody.isRequired || model.isRequired
|
||||||
|
requestBody.isNullable = requestBody.isNullable || model.isNullable
|
||||||
|
requestBody.format = model.format
|
||||||
|
requestBody.maximum = model.maximum
|
||||||
|
requestBody.exclusiveMaximum = model.exclusiveMaximum
|
||||||
|
requestBody.minimum = model.minimum
|
||||||
|
requestBody.exclusiveMinimum = model.exclusiveMinimum
|
||||||
|
requestBody.multipleOf = model.multipleOf
|
||||||
|
requestBody.maxLength = model.maxLength
|
||||||
|
requestBody.minLength = model.minLength
|
||||||
|
requestBody.maxItems = model.maxItems
|
||||||
|
requestBody.minItems = model.minItems
|
||||||
|
requestBody.uniqueItems = model.uniqueItems
|
||||||
|
requestBody.maxProperties = model.maxProperties
|
||||||
|
requestBody.minProperties = model.minProperties
|
||||||
|
requestBody.pattern = getPattern(model.pattern)
|
||||||
|
requestBody.imports.push(...model.imports)
|
||||||
|
requestBody.enum.push(...model.enum)
|
||||||
|
requestBody.enums.push(...model.enums)
|
||||||
|
requestBody.properties.push(...model.properties)
|
||||||
|
return requestBody
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return requestBody
|
||||||
|
}
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
import type { OperationResponse } from "../../../client/interfaces/OperationResponse"
|
||||||
|
import { getPattern } from "../../../utils/getPattern"
|
||||||
|
import type { OpenApi } from "../interfaces/OpenApi"
|
||||||
|
import type { OpenApiResponse } from "../interfaces/OpenApiResponse"
|
||||||
|
import type { OpenApiSchema } from "../interfaces/OpenApiSchema"
|
||||||
|
import { getContent } from "./getContent"
|
||||||
|
import { getModel } from "./getModel"
|
||||||
|
import { getRef } from "./getRef"
|
||||||
|
import { getType } from "./getType"
|
||||||
|
|
||||||
|
export const getOperationResponse = (
|
||||||
|
openApi: OpenApi,
|
||||||
|
response: OpenApiResponse,
|
||||||
|
responseCode: number
|
||||||
|
): OperationResponse => {
|
||||||
|
const operationResponse: OperationResponse = {
|
||||||
|
spec: response,
|
||||||
|
in: "response",
|
||||||
|
name: "",
|
||||||
|
code: responseCode,
|
||||||
|
description: response.description || null,
|
||||||
|
export: "generic",
|
||||||
|
type: "any",
|
||||||
|
base: "any",
|
||||||
|
template: null,
|
||||||
|
link: null,
|
||||||
|
isDefinition: false,
|
||||||
|
isReadOnly: false,
|
||||||
|
isRequired: false,
|
||||||
|
isNullable: false,
|
||||||
|
imports: [],
|
||||||
|
enum: [],
|
||||||
|
enums: [],
|
||||||
|
properties: [],
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.content) {
|
||||||
|
const content = getContent(openApi, response.content)
|
||||||
|
if (content) {
|
||||||
|
if (content.schema.$ref?.startsWith("#/components/responses/")) {
|
||||||
|
content.schema = getRef<OpenApiSchema>(openApi, content.schema)
|
||||||
|
}
|
||||||
|
if (content.schema.$ref) {
|
||||||
|
const model = getType(content.schema.$ref)
|
||||||
|
operationResponse.export = "reference"
|
||||||
|
operationResponse.type = model.type
|
||||||
|
operationResponse.base = model.base
|
||||||
|
operationResponse.template = model.template
|
||||||
|
operationResponse.imports.push(...model.imports)
|
||||||
|
return operationResponse
|
||||||
|
} else {
|
||||||
|
const model = getModel(openApi, content.schema)
|
||||||
|
operationResponse.export = model.export
|
||||||
|
operationResponse.type = model.type
|
||||||
|
operationResponse.base = model.base
|
||||||
|
operationResponse.template = model.template
|
||||||
|
operationResponse.link = model.link
|
||||||
|
operationResponse.isReadOnly = model.isReadOnly
|
||||||
|
operationResponse.isRequired = model.isRequired
|
||||||
|
operationResponse.isNullable = model.isNullable
|
||||||
|
operationResponse.format = model.format
|
||||||
|
operationResponse.maximum = model.maximum
|
||||||
|
operationResponse.exclusiveMaximum = model.exclusiveMaximum
|
||||||
|
operationResponse.minimum = model.minimum
|
||||||
|
operationResponse.exclusiveMinimum = model.exclusiveMinimum
|
||||||
|
operationResponse.multipleOf = model.multipleOf
|
||||||
|
operationResponse.maxLength = model.maxLength
|
||||||
|
operationResponse.minLength = model.minLength
|
||||||
|
operationResponse.maxItems = model.maxItems
|
||||||
|
operationResponse.minItems = model.minItems
|
||||||
|
operationResponse.uniqueItems = model.uniqueItems
|
||||||
|
operationResponse.maxProperties = model.maxProperties
|
||||||
|
operationResponse.minProperties = model.minProperties
|
||||||
|
operationResponse.pattern = getPattern(model.pattern)
|
||||||
|
operationResponse.imports.push(...model.imports)
|
||||||
|
operationResponse.enum.push(...model.enum)
|
||||||
|
operationResponse.enums.push(...model.enums)
|
||||||
|
operationResponse.properties.push(...model.properties)
|
||||||
|
return operationResponse
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// We support basic properties from response headers, since both
|
||||||
|
// fetch and XHR client just support string types.
|
||||||
|
if (response.headers) {
|
||||||
|
for (const name in response.headers) {
|
||||||
|
if (response.headers.hasOwnProperty(name)) {
|
||||||
|
operationResponse.in = "header"
|
||||||
|
operationResponse.name = name
|
||||||
|
operationResponse.type = "string"
|
||||||
|
operationResponse.base = "string"
|
||||||
|
return operationResponse
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return operationResponse
|
||||||
|
}
|
||||||
+18
@@ -0,0 +1,18 @@
|
|||||||
|
export const getOperationResponseCode = (
|
||||||
|
value: string | "default"
|
||||||
|
): number | null => {
|
||||||
|
// You can specify a "default" response, this is treated as HTTP code 200
|
||||||
|
if (value === "default") {
|
||||||
|
return 200
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if we can parse the code and return of successful.
|
||||||
|
if (/[0-9]+/g.test(value)) {
|
||||||
|
const code = parseInt(value)
|
||||||
|
if (Number.isInteger(code)) {
|
||||||
|
return Math.abs(code)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
+13
@@ -0,0 +1,13 @@
|
|||||||
|
import type { OperationResponse } from "../../../client/interfaces/OperationResponse"
|
||||||
|
|
||||||
|
export const getOperationResponseHeader = (
|
||||||
|
operationResponses: OperationResponse[]
|
||||||
|
): string | null => {
|
||||||
|
const header = operationResponses.find((operationResponses) => {
|
||||||
|
return operationResponses.in === "header"
|
||||||
|
})
|
||||||
|
if (header) {
|
||||||
|
return header.name
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
+38
@@ -0,0 +1,38 @@
|
|||||||
|
import type { OperationResponse } from "../../../client/interfaces/OperationResponse"
|
||||||
|
import type { OpenApi } from "../interfaces/OpenApi"
|
||||||
|
import type { OpenApiResponse } from "../interfaces/OpenApiResponse"
|
||||||
|
import type { OpenApiResponses } from "../interfaces/OpenApiResponses"
|
||||||
|
import { getOperationResponse } from "./getOperationResponse"
|
||||||
|
import { getOperationResponseCode } from "./getOperationResponseCode"
|
||||||
|
import { getRef } from "./getRef"
|
||||||
|
|
||||||
|
export const getOperationResponses = (
|
||||||
|
openApi: OpenApi,
|
||||||
|
responses: OpenApiResponses
|
||||||
|
): OperationResponse[] => {
|
||||||
|
const operationResponses: OperationResponse[] = []
|
||||||
|
|
||||||
|
// Iterate over each response code and get the
|
||||||
|
// status code and response message (if any).
|
||||||
|
for (const code in responses) {
|
||||||
|
if (responses.hasOwnProperty(code)) {
|
||||||
|
const responseOrReference = responses[code]
|
||||||
|
const response = getRef<OpenApiResponse>(openApi, responseOrReference)
|
||||||
|
const responseCode = getOperationResponseCode(code)
|
||||||
|
|
||||||
|
if (responseCode) {
|
||||||
|
const operationResponse = getOperationResponse(
|
||||||
|
openApi,
|
||||||
|
response,
|
||||||
|
responseCode
|
||||||
|
)
|
||||||
|
operationResponses.push(operationResponse)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort the responses to 2XX success codes come before 4XX and 5XX error codes.
|
||||||
|
return operationResponses.sort((a, b): number => {
|
||||||
|
return a.code < b.code ? -1 : a.code > b.code ? 1 : 0
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
import type { Model } from "../../../client/interfaces/Model"
|
||||||
|
import type { OperationResponse } from "../../../client/interfaces/OperationResponse"
|
||||||
|
|
||||||
|
const areEqual = (a: Model, b: Model): boolean => {
|
||||||
|
const equal =
|
||||||
|
a.type === b.type && a.base === b.base && a.template === b.template
|
||||||
|
if (equal && a.link && b.link) {
|
||||||
|
return areEqual(a.link, b.link)
|
||||||
|
}
|
||||||
|
return equal
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getOperationResults = (
|
||||||
|
operationResponses: OperationResponse[]
|
||||||
|
): OperationResponse[] => {
|
||||||
|
const operationResults: OperationResponse[] = []
|
||||||
|
|
||||||
|
// Filter out success response codes, but skip "204 No Content"
|
||||||
|
operationResponses.forEach((operationResponse) => {
|
||||||
|
const { code } = operationResponse
|
||||||
|
if (code && code !== 204 && code >= 200 && code < 300) {
|
||||||
|
operationResults.push(operationResponse)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!operationResults.length) {
|
||||||
|
operationResults.push({
|
||||||
|
spec: { description: "" },
|
||||||
|
in: "response",
|
||||||
|
name: "",
|
||||||
|
code: 200,
|
||||||
|
description: "",
|
||||||
|
export: "generic",
|
||||||
|
type: "void",
|
||||||
|
base: "void",
|
||||||
|
template: null,
|
||||||
|
link: null,
|
||||||
|
isDefinition: false,
|
||||||
|
isReadOnly: false,
|
||||||
|
isRequired: false,
|
||||||
|
isNullable: false,
|
||||||
|
imports: [],
|
||||||
|
enum: [],
|
||||||
|
enums: [],
|
||||||
|
properties: [],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return operationResults.filter((operationResult, index, arr) => {
|
||||||
|
return (
|
||||||
|
arr.findIndex((item) => {
|
||||||
|
return areEqual(item, operationResult)
|
||||||
|
}) === index
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
import type { OpenApi } from "../interfaces/OpenApi"
|
||||||
|
import type { OpenApiReference } from "../interfaces/OpenApiReference"
|
||||||
|
|
||||||
|
const ESCAPED_REF_SLASH = /~1/g
|
||||||
|
const ESCAPED_REF_TILDE = /~0/g
|
||||||
|
|
||||||
|
export const getRef = <T>(openApi: OpenApi, item: T & OpenApiReference): T => {
|
||||||
|
if (item.$ref) {
|
||||||
|
// Fetch the paths to the definitions, this converts:
|
||||||
|
// "#/components/schemas/Form" to ["components", "schemas", "Form"]
|
||||||
|
const paths = item.$ref
|
||||||
|
.replace(/^#/g, "")
|
||||||
|
.split("/")
|
||||||
|
.filter((item) => item)
|
||||||
|
|
||||||
|
// Try to find the reference by walking down the path,
|
||||||
|
// if we cannot find it, then we throw an error.
|
||||||
|
let result: any = openApi
|
||||||
|
paths.forEach((path) => {
|
||||||
|
const decodedPath = decodeURIComponent(
|
||||||
|
path.replace(ESCAPED_REF_SLASH, "/").replace(ESCAPED_REF_TILDE, "~")
|
||||||
|
)
|
||||||
|
if (result.hasOwnProperty(decodedPath)) {
|
||||||
|
result = result[decodedPath]
|
||||||
|
} else {
|
||||||
|
throw new Error(`Could not find reference: "${item.$ref}"`)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return result as T
|
||||||
|
}
|
||||||
|
return item as T
|
||||||
|
}
|
||||||
+33
@@ -0,0 +1,33 @@
|
|||||||
|
import type { Model } from "../../../client/interfaces/Model"
|
||||||
|
import type { OpenApi } from "../interfaces/OpenApi"
|
||||||
|
import type { OpenApiSchema } from "../interfaces/OpenApiSchema"
|
||||||
|
import type { getModel } from "./getModel"
|
||||||
|
import { getRef } from "./getRef"
|
||||||
|
|
||||||
|
// Fix for circular dependency
|
||||||
|
export type GetModelFn = typeof getModel
|
||||||
|
|
||||||
|
export const getRequiredPropertiesFromComposition = (
|
||||||
|
openApi: OpenApi,
|
||||||
|
required: string[],
|
||||||
|
definitions: OpenApiSchema[],
|
||||||
|
getModel: GetModelFn
|
||||||
|
): Model[] => {
|
||||||
|
return definitions
|
||||||
|
.reduce((properties, definition) => {
|
||||||
|
if (definition.$ref) {
|
||||||
|
const schema = getRef<OpenApiSchema>(openApi, definition)
|
||||||
|
return [...properties, ...getModel(openApi, schema).properties]
|
||||||
|
}
|
||||||
|
return [...properties, ...getModel(openApi, definition).properties]
|
||||||
|
}, [] as Model[])
|
||||||
|
.filter((property) => {
|
||||||
|
return !property.isRequired && required.includes(property.name)
|
||||||
|
})
|
||||||
|
.map((property) => {
|
||||||
|
return {
|
||||||
|
...property,
|
||||||
|
isRequired: true,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import type { OpenApi } from "../interfaces/OpenApi"
|
||||||
|
|
||||||
|
export const getServer = (openApi: OpenApi): string => {
|
||||||
|
const server = openApi.servers?.[0]
|
||||||
|
const variables = server?.variables || {}
|
||||||
|
let url = server?.url || ""
|
||||||
|
for (const variable in variables) {
|
||||||
|
if (variables.hasOwnProperty(variable)) {
|
||||||
|
url = url.replace(`{${variable}}`, variables[variable].default)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return url.replace(/\/$/g, "")
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import camelCase from "camelcase"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert the input value to a correct service name. This converts
|
||||||
|
* the input string to PascalCase.
|
||||||
|
*/
|
||||||
|
export const getServiceName = (value: string): string => {
|
||||||
|
const clean = value
|
||||||
|
.replace(/^[^a-zA-Z]+/g, "")
|
||||||
|
.replace(/[^\w\-]+/g, "-")
|
||||||
|
.trim()
|
||||||
|
return camelCase(clean, { pascalCase: true })
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
/**
|
||||||
|
* Convert the service version to 'normal' version.
|
||||||
|
* This basically removes any "v" prefix from the version string.
|
||||||
|
* @param version
|
||||||
|
*/
|
||||||
|
export const getServiceVersion = (version = "1.0"): string => {
|
||||||
|
return String(version).replace(/^v/gi, "")
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import type { Service } from "../../../client/interfaces/Service"
|
||||||
|
import type { OpenApi } from "../interfaces/OpenApi"
|
||||||
|
import { listOperations } from "./listOperations"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the OpenAPI services
|
||||||
|
*/
|
||||||
|
export const getServices = (openApi: OpenApi): Service[] => {
|
||||||
|
const services = new Map<string, Service>()
|
||||||
|
const operations = listOperations(openApi)
|
||||||
|
for (const operation of operations) {
|
||||||
|
// If we have already declared a service, then we should fetch that and
|
||||||
|
// append the new method to it. Otherwise we should create a new service object.
|
||||||
|
const service: Service = services.get(operation.service) || {
|
||||||
|
name: operation.service,
|
||||||
|
operations: [],
|
||||||
|
imports: [],
|
||||||
|
}
|
||||||
|
|
||||||
|
// Push the operation in the service
|
||||||
|
service.operations.push(operation)
|
||||||
|
service.imports.push(...operation.imports)
|
||||||
|
services.set(operation.service, service)
|
||||||
|
}
|
||||||
|
return Array.from(services.values())
|
||||||
|
}
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
import type { Type } from "../../../client/interfaces/Type"
|
||||||
|
import { isDefined } from "../../../utils/isDefined"
|
||||||
|
import { getMappedType } from "./getMappedType"
|
||||||
|
import { stripNamespace } from "./stripNamespace"
|
||||||
|
|
||||||
|
const encode = (value: string): string => {
|
||||||
|
return value.replace(/^[^a-zA-Z_$]+/g, "").replace(/[^\w$]+/g, "_")
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse any string value into a type object.
|
||||||
|
* @param type String or String[] value like "integer", "Link[Model]" or ["string", "null"].
|
||||||
|
* @param format String value like "binary" or "date".
|
||||||
|
*/
|
||||||
|
export const getType = (
|
||||||
|
type: string | string[] = "any",
|
||||||
|
format?: string
|
||||||
|
): Type => {
|
||||||
|
const result: Type = {
|
||||||
|
type: "any",
|
||||||
|
base: "any",
|
||||||
|
template: null,
|
||||||
|
imports: [],
|
||||||
|
isNullable: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Special case for JSON Schema spec (december 2020, page 17),
|
||||||
|
// that allows type to be an array of primitive types...
|
||||||
|
if (Array.isArray(type)) {
|
||||||
|
const joinedType = type
|
||||||
|
.filter((value) => value !== "null")
|
||||||
|
.map((value) => getMappedType(value, format))
|
||||||
|
.filter(isDefined)
|
||||||
|
.join(" | ")
|
||||||
|
result.type = joinedType
|
||||||
|
result.base = joinedType
|
||||||
|
result.isNullable = type.includes("null")
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapped = getMappedType(type, format)
|
||||||
|
if (mapped) {
|
||||||
|
result.type = mapped
|
||||||
|
result.base = mapped
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
const typeWithoutNamespace = decodeURIComponent(stripNamespace(type))
|
||||||
|
|
||||||
|
if (/\[.*\]$/g.test(typeWithoutNamespace)) {
|
||||||
|
const matches = typeWithoutNamespace.match(/(.*?)\[(.*)\]$/)
|
||||||
|
if (matches?.length) {
|
||||||
|
const match1 = getType(encode(matches[1]))
|
||||||
|
const match2 = getType(encode(matches[2]))
|
||||||
|
|
||||||
|
if (match1.type === "any[]") {
|
||||||
|
result.type = `${match2.type}[]`
|
||||||
|
result.base = `${match2.type}`
|
||||||
|
match1.imports = []
|
||||||
|
} else if (match2.type) {
|
||||||
|
result.type = `${match1.type}<${match2.type}>`
|
||||||
|
result.base = match1.type
|
||||||
|
result.template = match2.type
|
||||||
|
} else {
|
||||||
|
result.type = match1.type
|
||||||
|
result.base = match1.type
|
||||||
|
result.template = match1.type
|
||||||
|
}
|
||||||
|
|
||||||
|
result.imports.push(...match1.imports)
|
||||||
|
result.imports.push(...match2.imports)
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeWithoutNamespace) {
|
||||||
|
const type = encode(typeWithoutNamespace)
|
||||||
|
result.type = type
|
||||||
|
result.base = type
|
||||||
|
result.imports.push(type)
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
import { OpenApi } from "../interfaces/OpenApi"
|
||||||
|
import { Operation } from "../../../client/interfaces/Operation"
|
||||||
|
import { getOperationParameters } from "./getOperationParameters"
|
||||||
|
import { unique } from "../../../utils/unique"
|
||||||
|
import { getOperation } from "./getOperation"
|
||||||
|
|
||||||
|
export const listOperations = (openApi: OpenApi): Operation[] => {
|
||||||
|
const operations: Operation[] = []
|
||||||
|
for (const url in openApi.paths) {
|
||||||
|
if (openApi.paths.hasOwnProperty(url)) {
|
||||||
|
// Grab path and parse any global path parameters
|
||||||
|
const path = openApi.paths[url]
|
||||||
|
const pathParams = getOperationParameters(openApi, path.parameters || [])
|
||||||
|
|
||||||
|
// Parse all the methods for this path
|
||||||
|
for (const method in path) {
|
||||||
|
if (path.hasOwnProperty(method)) {
|
||||||
|
switch (method) {
|
||||||
|
case "get":
|
||||||
|
case "put":
|
||||||
|
case "post":
|
||||||
|
case "delete":
|
||||||
|
case "options":
|
||||||
|
case "head":
|
||||||
|
case "patch":
|
||||||
|
// Each method contains an OpenAPI operation, we parse the operation
|
||||||
|
const op = path[method]!
|
||||||
|
const tags = op.tags?.length
|
||||||
|
? op.tags.filter(unique)
|
||||||
|
: ["Default"]
|
||||||
|
tags.forEach((tag) => {
|
||||||
|
const operation = getOperation(
|
||||||
|
openApi,
|
||||||
|
url,
|
||||||
|
method,
|
||||||
|
tag,
|
||||||
|
op,
|
||||||
|
pathParams
|
||||||
|
)
|
||||||
|
operations.push(operation)
|
||||||
|
})
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return operations
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import type { OperationParameter } from "../../../client/interfaces/OperationParameter"
|
||||||
|
|
||||||
|
export const sortByRequired = (
|
||||||
|
a: OperationParameter,
|
||||||
|
b: OperationParameter
|
||||||
|
): number => {
|
||||||
|
const aNeedsValue = a.isRequired && a.default === undefined
|
||||||
|
const bNeedsValue = b.isRequired && b.default === undefined
|
||||||
|
if (aNeedsValue && !bNeedsValue) return -1
|
||||||
|
if (bNeedsValue && !aNeedsValue) return 1
|
||||||
|
return 0
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
/**
|
||||||
|
* Strip (OpenAPI) namespaces fom values.
|
||||||
|
* @param value
|
||||||
|
*/
|
||||||
|
export const stripNamespace = (value: string): string => {
|
||||||
|
return value
|
||||||
|
.trim()
|
||||||
|
.replace(/^#\/components\/schemas\//, "")
|
||||||
|
.replace(/^#\/components\/responses\//, "")
|
||||||
|
.replace(/^#\/components\/parameters\//, "")
|
||||||
|
.replace(/^#\/components\/examples\//, "")
|
||||||
|
.replace(/^#\/components\/requestBodies\//, "")
|
||||||
|
.replace(/^#\/components\/headers\//, "")
|
||||||
|
.replace(/^#\/components\/securitySchemes\//, "")
|
||||||
|
.replace(/^#\/components\/links\//, "")
|
||||||
|
.replace(/^#\/components\/callbacks\//, "")
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
{{>header}}
|
||||||
|
|
||||||
|
import type { BaseHttpRequest } from './core/BaseHttpRequest';
|
||||||
|
import type { OpenAPIConfig } from './core/OpenAPI';
|
||||||
|
import { {{{httpRequest}}} } from './core/{{{httpRequest}}}';
|
||||||
|
|
||||||
|
{{#if services}}
|
||||||
|
{{#each services}}
|
||||||
|
import { {{{name}}}{{{@root.postfix}}} } from './services/{{{name}}}{{{@root.postfix}}}';
|
||||||
|
{{/each}}
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
|
type HttpRequestConstructor = new (config: OpenAPIConfig) => BaseHttpRequest;
|
||||||
|
|
||||||
|
export class {{{clientName}}} {
|
||||||
|
|
||||||
|
{{#each services}}
|
||||||
|
public readonly {{{camelCase name}}}: {{{name}}}{{{@root.postfix}}};
|
||||||
|
{{/each}}
|
||||||
|
|
||||||
|
public readonly request: BaseHttpRequest;
|
||||||
|
|
||||||
|
constructor(config?: Partial<OpenAPIConfig>, HttpRequest: HttpRequestConstructor = {{{httpRequest}}}) {
|
||||||
|
this.request = new HttpRequest({
|
||||||
|
BASE: config?.BASE ?? '{{{server}}}',
|
||||||
|
VERSION: config?.VERSION ?? '{{{version}}}',
|
||||||
|
WITH_CREDENTIALS: config?.WITH_CREDENTIALS ?? false,
|
||||||
|
CREDENTIALS: config?.CREDENTIALS ?? 'include',
|
||||||
|
TOKEN: config?.TOKEN,
|
||||||
|
USERNAME: config?.USERNAME,
|
||||||
|
PASSWORD: config?.PASSWORD,
|
||||||
|
HEADERS: config?.HEADERS,
|
||||||
|
ENCODE_PATH: config?.ENCODE_PATH,
|
||||||
|
});
|
||||||
|
|
||||||
|
{{#each services}}
|
||||||
|
this.{{{camelCase name}}} = new {{{name}}}{{{@root.postfix}}}(this.request);
|
||||||
|
{{/each}}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
{{>header}}
|
||||||
|
|
||||||
|
import type { ApiRequestOptions } from './ApiRequestOptions';
|
||||||
|
import type { ApiResult } from './ApiResult';
|
||||||
|
|
||||||
|
export class ApiError extends Error {
|
||||||
|
public readonly url: string;
|
||||||
|
public readonly status: number;
|
||||||
|
public readonly statusText: string;
|
||||||
|
public readonly body: any;
|
||||||
|
public readonly request: ApiRequestOptions;
|
||||||
|
|
||||||
|
constructor(request: ApiRequestOptions, response: ApiResult, message: string) {
|
||||||
|
super(message);
|
||||||
|
|
||||||
|
this.name = 'ApiError';
|
||||||
|
this.url = response.url;
|
||||||
|
this.status = response.status;
|
||||||
|
this.statusText = response.statusText;
|
||||||
|
this.body = response.body;
|
||||||
|
this.request = request;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
{{>header}}
|
||||||
|
|
||||||
|
export type ApiRequestOptions = {
|
||||||
|
readonly method: 'GET' | 'PUT' | 'POST' | 'DELETE' | 'OPTIONS' | 'HEAD' | 'PATCH';
|
||||||
|
readonly url: string;
|
||||||
|
readonly path?: Record<string, any>;
|
||||||
|
readonly cookies?: Record<string, any>;
|
||||||
|
readonly headers?: Record<string, any>;
|
||||||
|
readonly query?: Record<string, any>;
|
||||||
|
readonly formData?: Record<string, any>;
|
||||||
|
readonly body?: any;
|
||||||
|
readonly mediaType?: string;
|
||||||
|
readonly responseHeader?: string;
|
||||||
|
readonly errors?: Record<number, string>;
|
||||||
|
};
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
{{>header}}
|
||||||
|
|
||||||
|
export type ApiResult = {
|
||||||
|
readonly url: string;
|
||||||
|
readonly ok: boolean;
|
||||||
|
readonly status: number;
|
||||||
|
readonly statusText: string;
|
||||||
|
readonly body: any;
|
||||||
|
};
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
{{>header}}
|
||||||
|
|
||||||
|
import type { ApiRequestOptions } from './ApiRequestOptions';
|
||||||
|
import type { CancelablePromise } from './CancelablePromise';
|
||||||
|
import type { OpenAPIConfig } from './OpenAPI';
|
||||||
|
|
||||||
|
export abstract class BaseHttpRequest {
|
||||||
|
|
||||||
|
constructor(public readonly config: OpenAPIConfig) {}
|
||||||
|
|
||||||
|
public abstract request<T>(options: ApiRequestOptions): CancelablePromise<T>;
|
||||||
|
}
|
||||||
@@ -0,0 +1,127 @@
|
|||||||
|
{{>header}}
|
||||||
|
|
||||||
|
export class CancelError extends Error {
|
||||||
|
|
||||||
|
constructor(message: string) {
|
||||||
|
super(message);
|
||||||
|
this.name = 'CancelError';
|
||||||
|
}
|
||||||
|
|
||||||
|
public get isCancelled(): boolean {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface OnCancel {
|
||||||
|
readonly isResolved: boolean;
|
||||||
|
readonly isRejected: boolean;
|
||||||
|
readonly isCancelled: boolean;
|
||||||
|
|
||||||
|
(cancelHandler: () => void): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class CancelablePromise<T> implements Promise<T> {
|
||||||
|
readonly [Symbol.toStringTag]!: string;
|
||||||
|
|
||||||
|
private _isResolved: boolean;
|
||||||
|
private _isRejected: boolean;
|
||||||
|
private _isCancelled: boolean;
|
||||||
|
private readonly _cancelHandlers: (() => void)[];
|
||||||
|
private readonly _promise: Promise<T>;
|
||||||
|
private _resolve?: (value: T | PromiseLike<T>) => void;
|
||||||
|
private _reject?: (reason?: any) => void;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
executor: (
|
||||||
|
resolve: (value: T | PromiseLike<T>) => void,
|
||||||
|
reject: (reason?: any) => void,
|
||||||
|
onCancel: OnCancel
|
||||||
|
) => void
|
||||||
|
) {
|
||||||
|
this._isResolved = false;
|
||||||
|
this._isRejected = false;
|
||||||
|
this._isCancelled = false;
|
||||||
|
this._cancelHandlers = [];
|
||||||
|
this._promise = new Promise<T>((resolve, reject) => {
|
||||||
|
this._resolve = resolve;
|
||||||
|
this._reject = reject;
|
||||||
|
|
||||||
|
const onResolve = (value: T | PromiseLike<T>): void => {
|
||||||
|
if (this._isResolved || this._isRejected || this._isCancelled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this._isResolved = true;
|
||||||
|
this._resolve?.(value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onReject = (reason?: any): void => {
|
||||||
|
if (this._isResolved || this._isRejected || this._isCancelled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this._isRejected = true;
|
||||||
|
this._reject?.(reason);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onCancel = (cancelHandler: () => void): void => {
|
||||||
|
if (this._isResolved || this._isRejected || this._isCancelled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this._cancelHandlers.push(cancelHandler);
|
||||||
|
};
|
||||||
|
|
||||||
|
Object.defineProperty(onCancel, 'isResolved', {
|
||||||
|
get: (): boolean => this._isResolved,
|
||||||
|
});
|
||||||
|
|
||||||
|
Object.defineProperty(onCancel, 'isRejected', {
|
||||||
|
get: (): boolean => this._isRejected,
|
||||||
|
});
|
||||||
|
|
||||||
|
Object.defineProperty(onCancel, 'isCancelled', {
|
||||||
|
get: (): boolean => this._isCancelled,
|
||||||
|
});
|
||||||
|
|
||||||
|
return executor(onResolve, onReject, onCancel as OnCancel);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public then<TResult1 = T, TResult2 = never>(
|
||||||
|
onFulfilled?: ((value: T) => TResult1 | PromiseLike<TResult1>) | null,
|
||||||
|
onRejected?: ((reason: any) => TResult2 | PromiseLike<TResult2>) | null
|
||||||
|
): Promise<TResult1 | TResult2> {
|
||||||
|
return this._promise.then(onFulfilled, onRejected);
|
||||||
|
}
|
||||||
|
|
||||||
|
public catch<TResult = never>(
|
||||||
|
onRejected?: ((reason: any) => TResult | PromiseLike<TResult>) | null
|
||||||
|
): Promise<T | TResult> {
|
||||||
|
return this._promise.catch(onRejected);
|
||||||
|
}
|
||||||
|
|
||||||
|
public finally(onFinally?: (() => void) | null): Promise<T> {
|
||||||
|
return this._promise.finally(onFinally);
|
||||||
|
}
|
||||||
|
|
||||||
|
public cancel(): void {
|
||||||
|
if (this._isResolved || this._isRejected || this._isCancelled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this._isCancelled = true;
|
||||||
|
if (this._cancelHandlers.length) {
|
||||||
|
try {
|
||||||
|
for (const cancelHandler of this._cancelHandlers) {
|
||||||
|
cancelHandler();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Cancellation threw an error', error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this._cancelHandlers.length = 0;
|
||||||
|
this._reject?.(new CancelError('Request aborted'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public get isCancelled(): boolean {
|
||||||
|
return this._isCancelled;
|
||||||
|
}
|
||||||
|
}
|
||||||
Loaded 100 of 224 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user