├── .changeset ├── README.md └── config.json ├── .eslintignore ├── .eslintrc.js ├── .github ├── actions │ └── setup │ │ └── action.yaml └── workflows │ ├── main.yaml │ ├── release.yaml │ └── snapshot-release.yaml ├── .gitignore ├── .vscode ├── launch.json └── settings.json ├── LICENSE ├── README.md ├── babel.config.js ├── examples └── react-query-petstore │ ├── .gitignore │ ├── CHANGELOG.md │ ├── package.json │ ├── petstore-backend.sh │ ├── petstore.yaml │ ├── src │ ├── App.tsx │ ├── api │ │ ├── components │ │ │ ├── requestBodies │ │ │ │ ├── Pet.ts │ │ │ │ ├── UserArray.ts │ │ │ │ └── index.ts │ │ │ └── schemas │ │ │ │ ├── Address.ts │ │ │ │ ├── ApiResponse.ts │ │ │ │ ├── Category.ts │ │ │ │ ├── Customer.ts │ │ │ │ ├── Order.ts │ │ │ │ ├── Pet.ts │ │ │ │ ├── Tag.ts │ │ │ │ ├── User.ts │ │ │ │ └── index.ts │ │ ├── operations │ │ │ ├── addPet.ts │ │ │ ├── createUser.ts │ │ │ ├── createUsersWithListInput.ts │ │ │ ├── deleteOrder.ts │ │ │ ├── deletePet.ts │ │ │ ├── deleteUser.ts │ │ │ ├── findPetsByStatus.ts │ │ │ ├── findPetsByTags.ts │ │ │ ├── getInventory.ts │ │ │ ├── getOrderById.ts │ │ │ ├── getPetById.ts │ │ │ ├── getUserByName.ts │ │ │ ├── index.ts │ │ │ ├── loginUser.ts │ │ │ ├── logoutUser.ts │ │ │ ├── placeOrder.ts │ │ │ ├── updatePet.ts │ │ │ ├── updatePetWithForm.ts │ │ │ ├── updateUser.ts │ │ │ └── uploadFile.ts │ │ └── servers.ts │ ├── common │ │ ├── fetchRequestAdapter.ts │ │ └── react-query.ts │ ├── features │ │ └── pet │ │ │ ├── AddPet.tsx │ │ │ ├── EditPet.tsx │ │ │ ├── PetForm.tsx │ │ │ ├── PetsHome.tsx │ │ │ ├── PetsIndex.tsx │ │ │ ├── PetsTable.tsx │ │ │ └── petService.ts │ ├── index.html │ └── index.tsx │ └── tsconfig.json ├── package.json ├── packages ├── cli │ ├── CHANGELOG.md │ ├── README.md │ ├── package.json │ ├── src │ │ ├── bin.ts │ │ ├── codegen │ │ │ ├── body.ts │ │ │ ├── common.ts │ │ │ ├── components.ts │ │ │ ├── context.ts │ │ │ ├── format.ts │ │ │ ├── fs.ts │ │ │ ├── index.ts │ │ │ ├── main.ts │ │ │ ├── operations.ts │ │ │ ├── operationsIndex.ts │ │ │ ├── parameter.ts │ │ │ ├── response.ts │ │ │ ├── schema.ts │ │ │ └── servers.ts │ │ ├── environment.ts │ │ ├── index.ts │ │ ├── parser │ │ │ ├── JSONReference.ts │ │ │ ├── __tests__ │ │ │ │ ├── __fixtures__ │ │ │ │ │ └── pet-store.json │ │ │ │ └── schema.ts │ │ │ ├── body.ts │ │ │ ├── common.ts │ │ │ ├── components.ts │ │ │ ├── context.ts │ │ │ ├── index.ts │ │ │ ├── main.ts │ │ │ ├── operation.ts │ │ │ ├── parameter.ts │ │ │ ├── parseDocument.ts │ │ │ ├── parserOutput.ts │ │ │ ├── response.ts │ │ │ ├── schema.ts │ │ │ └── server.ts │ │ └── utils.ts │ └── tsconfig.json ├── core │ ├── README.md │ ├── package.json │ ├── src │ │ ├── index.ts │ │ ├── mediaTypes.ts │ │ └── operation.ts │ └── tsconfig.json └── runtime │ ├── CHANGELOG.md │ ├── README.md │ ├── package.json │ ├── src │ ├── client │ │ ├── __fixtures__ │ │ │ └── baseOperations.ts │ │ ├── apiError.ts │ │ ├── apiResponse.ts │ │ ├── httpRequestAdapter.ts │ │ ├── index.ts │ │ ├── parseResponse.ts │ │ ├── prepareRequest.test.ts │ │ ├── prepareRequest.ts │ │ └── request.ts │ ├── index.ts │ └── model │ │ ├── body.ts │ │ ├── index.ts │ │ ├── operation.ts │ │ ├── parameters.ts │ │ └── responses.ts │ └── tsconfig.json ├── pnpm-lock.yaml ├── pnpm-workspace.yaml └── tsconfig.json /.changeset/README.md: -------------------------------------------------------------------------------- 1 | # Changesets 2 | 3 | Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works 4 | with multi-package repos, or single-package repos to help you version and publish your code. You can 5 | find the full documentation for it [in our repository](https://github.com/changesets/changesets) 6 | 7 | We have a quick list of common questions to get you started engaging with this project in 8 | [our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md) 9 | -------------------------------------------------------------------------------- /.changeset/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://unpkg.com/@changesets/config@1.6.3/schema.json", 3 | "changelog": [ 4 | "@changesets/changelog-github", 5 | { "repo": "Fredx87/openapi-io-ts" } 6 | ], 7 | "commit": false, 8 | "linked": [], 9 | "access": "public", 10 | "baseBranch": "main", 11 | "updateInternalDependencies": "patch", 12 | "ignore": [] 13 | } 14 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | out 4 | .eslintrc.js 5 | babel.config.js 6 | jest.config.js -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | parser: "@typescript-eslint/parser", 4 | plugins: ["@typescript-eslint", "fp-ts", "prettier"], 5 | parserOptions: { 6 | tsconfigRootDir: __dirname, 7 | project: ["./packages/*/tsconfig.json", "./examples/*/tsconfig.json"], 8 | }, 9 | extends: [ 10 | "eslint:recommended", 11 | "plugin:@typescript-eslint/recommended", 12 | "plugin:@typescript-eslint/recommended-requiring-type-checking", 13 | "plugin:fp-ts/all", 14 | "plugin:prettier/recommended", 15 | ], 16 | rules: { 17 | "fp-ts/no-module-imports": "off", 18 | }, 19 | }; 20 | -------------------------------------------------------------------------------- /.github/actions/setup/action.yaml: -------------------------------------------------------------------------------- 1 | name: "Setup" 2 | description: "Setup Node, PNPM and install dependencies" 3 | 4 | runs: 5 | using: "composite" 6 | steps: 7 | - name: Setup PNPM 8 | uses: pnpm/action-setup@v2.0.1 9 | with: 10 | version: 6 11 | 12 | - name: Setup Node 13 | uses: actions/setup-node@v2 14 | with: 15 | node-version: "16" 16 | cache: "pnpm" 17 | registry-url: "https://registry.npmjs.org" 18 | 19 | - name: Install dependencies 20 | run: pnpm install 21 | shell: bash 22 | -------------------------------------------------------------------------------- /.github/workflows/main.yaml: -------------------------------------------------------------------------------- 1 | name: Build and test 2 | 3 | on: 4 | pull_request: 5 | branches: ["**"] 6 | 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | 11 | steps: 12 | - uses: actions/checkout@v2 13 | 14 | - uses: "./.github/actions/setup" 15 | 16 | - name: Build 17 | run: pnpm build 18 | 19 | - name: Lint 20 | run: pnpm lint 21 | 22 | - name: Test 23 | run: pnpm test 24 | 25 | - name: Type check examples 26 | run: pnpm examples:type-check 27 | -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | release: 10 | name: Release 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v2 14 | 15 | - uses: "./.github/actions/setup" 16 | 17 | - name: Create Release Pull Request or Publish to npm 18 | id: changesets 19 | uses: changesets/action@v1 20 | with: 21 | publish: pnpm release 22 | env: 23 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 24 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 25 | -------------------------------------------------------------------------------- /.github/workflows/snapshot-release.yaml: -------------------------------------------------------------------------------- 1 | name: Snapshot Release 2 | 3 | on: 4 | workflow_dispatch: 5 | 6 | jobs: 7 | publish: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v2 11 | 12 | - uses: "./.github/actions/setup" 13 | 14 | - name: Build 15 | run: pnpm build 16 | 17 | - name: Publish 18 | run: | 19 | pnpm changeset version --snapshot 20 | pnpm changeset publish --no-git-tag --snapshot --tag next 21 | env: 22 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 23 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | out 3 | dist 4 | .parcel-cache 5 | coverage -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "node", 9 | "request": "launch", 10 | "name": "Jest All", 11 | "program": "${workspaceFolder}/node_modules/jest/bin/jest.js", 12 | "args": ["--runInBand"], 13 | "console": "integratedTerminal", 14 | "internalConsoleOptions": "neverOpen", 15 | "disableOptimisticBPs": true, 16 | "windows": { 17 | "program": "${workspaceFolder}/node_modules/jest/bin/jest" 18 | } 19 | }, 20 | { 21 | "type": "node", 22 | "request": "launch", 23 | "name": "Jest Current File", 24 | "program": "${workspaceFolder}/node_modules/jest/bin/jest.js", 25 | "args": ["${fileBasenameNoExtension}", "--config", "jest.config.js"], 26 | "console": "integratedTerminal", 27 | "internalConsoleOptions": "neverOpen", 28 | "disableOptimisticBPs": true, 29 | "windows": { 30 | "program": "${workspaceFolder}/node_modules/jest/bin/jest" 31 | } 32 | } 33 | ] 34 | } 35 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules/typescript/lib", 3 | "jestrunner.jestPath": "${workspaceFolder}/node_modules/jest/bin/jest.js" 4 | } 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Gianluca Frediani 4 | Copyright (c) 2021 openapi-io-ts contributors 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # openapi-io-ts 2 | 3 | `openapi-io-ts` is a code generation tool capable of generating [io-ts](https://github.com/gcanti/io-ts) decoders from an [OpenAPI](https://www.openapis.org/) document. It can also generate the code needed to perform the request and decode/parse the response returned by the server. 4 | 5 | It is composed of a CLI for code generation and a runtime used to perform the request and decode the response. 6 | 7 | ## Project status 8 | 9 | **WARNING** The project is still an alpha version. The generation of the decoders is working (apart from some edge cases like recursive decoders), but the API of the runtime can be changed in future versions with breaking changes. 10 | 11 | ## CLI 12 | 13 | ### Installation 14 | 15 | You can install `@openapi-io-ts/cli` as a `devDependency`: 16 | 17 | ```bash 18 | # npm 19 | npm i -D @openapi-io-ts/cli 20 | 21 | # yarn 22 | yarn add -D @openapi-io-ts/cli 23 | ``` 24 | 25 | ### Usage 26 | 27 | You can launch the `openapi-io-ts` binary by adding a script to the `package.json`. 28 | 29 | ```json 30 | { 31 | "scripts": { 32 | "generate-api": "openapi-io-ts -i petstore.yaml -o src/api" 33 | } 34 | } 35 | ``` 36 | 37 | ```bash 38 | # npm 39 | npm run generate-api 40 | 41 | # yarn 42 | yarn generate-api 43 | ``` 44 | 45 | The `-i` parameter is used to pass an OpenAPI document, and the `-o` parameter is the output directory. 46 | 47 | **Note**: At the moment the only OpenAPI version supported is `3.0`. If you have a `2.0` file you have to convert it to `3.0` before. Version `3.1` is not supported at the moment. 48 | 49 | **Note**: Each operation defined in the `paths` section of the OpenAPI document must have an `operationId`. 50 | 51 | ### Generated code 52 | 53 | The code generated by the CLI contains the `io-ts` decoders for the schemas that are defined in the OpenAPI document and other objects that are used by the runtime. 54 | 55 | The folder structure of the generated code is similar to the structure of the OpenAPI document, with a `components` folder that contains common objects, and an `operations` folder containing the various operations. 56 | 57 | You can see an example in the [examples/react-query-petstore/src/api](examples/react-query-petstore/src/api) folder. 58 | 59 | **Note**: The generated code is formatted using the default TypeScript formatter. If you want to format it with another tool like Prettier you can run it after the code generation: 60 | 61 | ```json 62 | { 63 | "scripts": { 64 | "generate": "openapi-io-ts -i petstore.yaml -o src/api && prettier --write ./src/api" 65 | } 66 | } 67 | ``` 68 | 69 | ## Generated client 70 | 71 | The CLI will generate code for performing requests defined in the OpenAPI document and decode the returned response. For using the generated code you will need to install the runtime, that is a package that contains functions for preparing the request and decoding the response 72 | 73 | ### Installation 74 | 75 | You can install `@openapi-io-ts/runtime` as a dependency of your project: 76 | 77 | ```bash 78 | # npm 79 | npm i @openapi-io-ts/runtime 80 | 81 | # yarn 82 | yarn add @openapi-io-ts/runtime 83 | ``` 84 | 85 | The runtime has `fp-ts`, `io-ts`, and `io-ts-types` as peer dependencies, so you have to install them as well if they are not already installed in your project: 86 | 87 | ```bash 88 | # npm 89 | npm i fp-ts io-ts io-ts-types 90 | 91 | # yarn 92 | yarn add fp-ts io-ts io-ts-types 93 | ``` 94 | 95 | ### Generated request functions 96 | 97 | For each operation described in the OpenAPI document, the code generator will generate a function for calling the API and decoding the result. 98 | The return type of the function is `TaskEither>`, where `T` is the type returned by the API on a successful response. 99 | 100 | When the `TaskEither` is executed, the API will be called and the result returned in the promise will be either a `Right` containing the response or a `Left` containing an error. 101 | 102 | The result is `Right` when the API returns a successful HTTP code and the decoding of the returned value succeeds. The `ApiResponse` object will contain the decoded data and a `Response` object that can be useful for example for reading response headers. 103 | 104 | Otherwise, the result will be a `Left`. The `ApiError` is a tagged union with one of these types: 105 | 106 | - `RequestError` when the function using for calling the API threw an exception 107 | - `HttpError` when the response HTTP code is not a `2XX` code 108 | - `DecodeError` when the decoding of the response failed 109 | - `ContentParseError` when there was an error during the parse of the returned JSON. 110 | 111 | ### HttpRequestAdapter 112 | 113 | The sequence of operation for calling an API are: 114 | 115 | 1. Preparing the request: replacing path parameters, encoding query parameters in the query string, creating headers, encoding the body 116 | 2. Performing the request using an HTTP client 117 | 3. Parsing the response and returning either an `ApiError` or an `ApiResponse`. 118 | 119 | The code generated uses the runtime for steps 1 and 3, and requires a function supplied by the user for step 2. This function takes in input an URL and a `RequestInit` object created by the first step and returns a promise containing a `Response` object used by the following step: 120 | 121 | ```ts 122 | type HttpRequestAdapter = (url: string, req: RequestInit) => Promise; 123 | ``` 124 | 125 | The URL passed to the function is a relative URL, and you need to add the base URL. In your function, you can also modify the `RequestInit` object, for example adding authorization headers. 126 | 127 | As you can see, the API is identical to the `fetch` API. In fact, the easiest way to define the `HttpRequestAdapter` is using the `fetch` API: 128 | 129 | ```ts 130 | const fetchRequestAdapter: HttpRequestAdapter = (url, init) => 131 | fetch(`http://example.com/api${url}`, init); 132 | ``` 133 | 134 | You can also use a different HTTP client, but you need to convert the `RequestInit` to the request object used by the client and create a `Response` object from the response returned by the client. Like `fetch`, the promise should be always resolved with any HTTP code. Keep also in mind that since `Request` and `Response` are part of the `fetch` API, you should polyfill them if fetch is not available in your execution environment. 135 | 136 | ### Using the request functions 137 | 138 | The CLI will generate a request function for each operation defined in the OpenAPI document. There is a function builder (`requestFunctionsBuilder`) that accepts an `HttpRequestAdapter` and returns request functions for all operations. 139 | 140 | For convenience, the client will also generate a service builder for each tag defined in the OpenAPI document. This service builder takes the result of `requestFunctionsBuilder` as a parameter and returns an object containing all of the request functions for that tag. 141 | 142 | For example, if your OpenAPI document contains a `updateUser` operation that takes a `username` as a path parameter, a `User` object in the body, and returns an `User` on success, the request functions builder will have an `updateUser` key that return a function with this signature: 143 | 144 | ```ts 145 | export type UpdateUserRequestParameters = { 146 | username: string; 147 | }; 148 | 149 | const requestFunctions = requestFunctionsBuilder(fetchRequestAdapter); 150 | 151 | // requestFunctions.updateUser will have this signature: 152 | declare function updateUser({ 153 | params, 154 | body, 155 | }: { 156 | params: UpdateUserRequestParameters; 157 | body: schemas.User; 158 | }): TaskEither>; 159 | ``` 160 | 161 | You can use it passing your defined `HttpRequestAdapter`: 162 | 163 | ```ts 164 | const requestFunctions = requestFunctionsBuilder(fetchRequestAdapter); 165 | 166 | requestFunctions 167 | .updateUser({ 168 | params: { username: "johndoe" }, 169 | body: { email: "john.doe@example.com" }, 170 | })() 171 | .then(/*...*/); 172 | ``` 173 | 174 | If this operation has a tag called user you can also use the generated `userServiceBuilder` 175 | 176 | ```ts 177 | const userService = userServiceBuilder(requestFunctions); 178 | 179 | userService 180 | .updateUser({ 181 | params: { username: "johndoe" }, 182 | body: { email: "john.doe@example.com" }, 183 | })() 184 | .then(/*...*/); 185 | ``` 186 | 187 | You can see more examples in the [examples/react-query-petstore/src/api](examples/react-query-petstore/src/api) folder. 188 | 189 | As described above, the type returned by the generated request functions is a `TaskEither`, which represents an asynchronous computation that can fail with an `ApiError` or succeeds with an `ApiResponse`. 190 | 191 | If you want to run the asynchronous computation you can call the `()` method of the `TaskEither` and then distinguish between successful and unsuccessful responses using the `fold` method of the `Either` data type. 192 | 193 | ```ts 194 | import * as E from "fp-ts/Either"; 195 | 196 | requestFunctions 197 | .updateUser({ 198 | params: { username: "johndoe" }, 199 | body: { email: "john.doe@example.com" }, 200 | })() 201 | .then( 202 | E.fold( 203 | (e) => { 204 | /* e is an ApiError */ 205 | }, 206 | (res) => { 207 | /* res.data is schemas.User, res.response is the Response object */ 208 | } 209 | ) 210 | ); 211 | ``` 212 | 213 | ## Contributors 214 | 215 | 216 | 217 | 218 | 219 | Made with [contributors-img](https://contrib.rocks). 220 | 221 | ## License 222 | 223 | [MIT](https://choosealicense.com/licenses/mit/) 224 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | ["@babel/preset-env", { targets: { node: "current" } }], 4 | "@babel/preset-typescript", 5 | ], 6 | }; 7 | -------------------------------------------------------------------------------- /examples/react-query-petstore/.gitignore: -------------------------------------------------------------------------------- 1 | build -------------------------------------------------------------------------------- /examples/react-query-petstore/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @openapi-io-ts/example-react-query-petstore 2 | 3 | ## 0.1.3 4 | 5 | ### Patch Changes 6 | 7 | - Updated dependencies [[`2d5a207`](https://github.com/Fredx87/openapi-io-ts/commit/2d5a207d50a35553b68c788a5895f7454620a560)]: 8 | - @openapi-io-ts/runtime@0.3.1 9 | 10 | ## 0.1.2 11 | 12 | ### Patch Changes 13 | 14 | - Updated dependencies [[`1df5450`](https://github.com/Fredx87/openapi-io-ts/commit/1df545029aef4853eb958cffb92cf9f7517acd02)]: 15 | - @openapi-io-ts/runtime@0.3.0 16 | 17 | ## 0.1.1 18 | 19 | ### Patch Changes 20 | 21 | - Updated dependencies [[`84d6bf1`](https://github.com/Fredx87/openapi-io-ts/commit/84d6bf1cc2cedc0f818fa3e88da71135ee94e58f)]: 22 | - @openapi-io-ts/runtime@0.2.0 23 | -------------------------------------------------------------------------------- /examples/react-query-petstore/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@openapi-io-ts/example-react-query-petstore", 3 | "version": "0.1.3", 4 | "license": "MIT", 5 | "private": true, 6 | "dependencies": { 7 | "@chakra-ui/icons": "^1.0.10", 8 | "@chakra-ui/react": "^1.5.1", 9 | "@emotion/react": "^11", 10 | "@emotion/styled": "^11", 11 | "@openapi-io-ts/runtime": "workspace:*", 12 | "@types/react": "^17.0.0", 13 | "@types/react-dom": "^17.0.0", 14 | "@types/react-router-dom": "^5.1.7", 15 | "fp-ts": "^2.11.0", 16 | "framer-motion": "^4", 17 | "io-ts": "^2.2.0", 18 | "io-ts-types": "^0.5.16", 19 | "react": "^17.0.2", 20 | "react-dom": "^17.0.2", 21 | "react-hook-form": "^7.1.1", 22 | "react-query": "^3.13.8", 23 | "react-router-dom": "^5.2.0" 24 | }, 25 | "devDependencies": { 26 | "parcel": "^2.0.0-beta.2", 27 | "@openapi-io-ts/cli": "workspace:*" 28 | }, 29 | "scripts": { 30 | "generate": "openapi-io-ts -i petstore.yaml -o src/api && prettier --write ./src/api", 31 | "start": "parcel src/index.html", 32 | "type-check": "tsc -p ." 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /examples/react-query-petstore/petstore-backend.sh: -------------------------------------------------------------------------------- 1 | docker run --rm --name swaggerapi-petstore3 -d -p 8080:8080 swaggerapi/petstore3:1.0.5 -------------------------------------------------------------------------------- /examples/react-query-petstore/petstore.yaml: -------------------------------------------------------------------------------- 1 | openapi: 3.0.2 2 | servers: 3 | - url: /v3 4 | info: 5 | description: |- 6 | This is a sample Pet Store Server based on the OpenAPI 3.0 specification. You can find out more about 7 | Swagger at [http://swagger.io](http://swagger.io). In the third iteration of the pet store, we've switched to the design first approach! 8 | You can now help us improve the API whether it's by making changes to the definition itself or to the code. 9 | That way, with time, we can improve the API in general, and expose some of the new features in OAS3. 10 | 11 | Some useful links: 12 | - [The Pet Store repository](https://github.com/swagger-api/swagger-petstore) 13 | - [The source API definition for the Pet Store](https://github.com/swagger-api/swagger-petstore/blob/master/src/main/resources/openapi.yaml) 14 | version: 1.0.5 15 | title: Swagger Petstore - OpenAPI 3.0 16 | termsOfService: "http://swagger.io/terms/" 17 | contact: 18 | email: apiteam@swagger.io 19 | license: 20 | name: Apache 2.0 21 | url: "http://www.apache.org/licenses/LICENSE-2.0.html" 22 | tags: 23 | - name: pet 24 | description: Everything about your Pets 25 | externalDocs: 26 | description: Find out more 27 | url: "http://swagger.io" 28 | - name: store 29 | description: Operations about user 30 | - name: user 31 | description: Access to Petstore orders 32 | externalDocs: 33 | description: Find out more about our store 34 | url: "http://swagger.io" 35 | paths: 36 | /pet: 37 | post: 38 | tags: 39 | - pet 40 | summary: Add a new pet to the store 41 | description: Add a new pet to the store 42 | operationId: addPet 43 | responses: 44 | "200": 45 | description: Successful operation 46 | content: 47 | application/xml: 48 | schema: 49 | $ref: "#/components/schemas/Pet" 50 | application/json: 51 | schema: 52 | $ref: "#/components/schemas/Pet" 53 | "405": 54 | description: Invalid input 55 | security: 56 | - petstore_auth: 57 | - "write:pets" 58 | - "read:pets" 59 | requestBody: 60 | description: Create a new pet in the store 61 | required: true 62 | content: 63 | application/json: 64 | schema: 65 | $ref: "#/components/schemas/Pet" 66 | application/xml: 67 | schema: 68 | $ref: "#/components/schemas/Pet" 69 | application/x-www-form-urlencoded: 70 | schema: 71 | $ref: "#/components/schemas/Pet" 72 | put: 73 | tags: 74 | - pet 75 | summary: Update an existing pet 76 | description: Update an existing pet by Id 77 | operationId: updatePet 78 | responses: 79 | "200": 80 | description: Successful operation 81 | content: 82 | application/xml: 83 | schema: 84 | $ref: "#/components/schemas/Pet" 85 | application/json: 86 | schema: 87 | $ref: "#/components/schemas/Pet" 88 | "400": 89 | description: Invalid ID supplied 90 | "404": 91 | description: Pet not found 92 | "405": 93 | description: Validation exception 94 | security: 95 | - petstore_auth: 96 | - "write:pets" 97 | - "read:pets" 98 | requestBody: 99 | description: Update an existent pet in the store 100 | required: true 101 | content: 102 | application/json: 103 | schema: 104 | $ref: "#/components/schemas/Pet" 105 | application/xml: 106 | schema: 107 | $ref: "#/components/schemas/Pet" 108 | application/x-www-form-urlencoded: 109 | schema: 110 | $ref: "#/components/schemas/Pet" 111 | /pet/findByStatus: 112 | get: 113 | tags: 114 | - pet 115 | summary: Finds Pets by status 116 | description: Multiple status values can be provided with comma separated strings 117 | operationId: findPetsByStatus 118 | parameters: 119 | - name: status 120 | in: query 121 | description: Status values that need to be considered for filter 122 | required: false 123 | explode: true 124 | schema: 125 | type: string 126 | enum: 127 | - available 128 | - pending 129 | - sold 130 | default: available 131 | responses: 132 | "200": 133 | description: successful operation 134 | content: 135 | application/xml: 136 | schema: 137 | type: array 138 | items: 139 | $ref: "#/components/schemas/Pet" 140 | application/json: 141 | schema: 142 | type: array 143 | items: 144 | $ref: "#/components/schemas/Pet" 145 | "400": 146 | description: Invalid status value 147 | security: 148 | - petstore_auth: 149 | - "write:pets" 150 | - "read:pets" 151 | /pet/findByTags: 152 | get: 153 | tags: 154 | - pet 155 | summary: Finds Pets by tags 156 | description: >- 157 | Multiple tags can be provided with comma separated strings. Use tag1, 158 | tag2, tag3 for testing. 159 | operationId: findPetsByTags 160 | parameters: 161 | - name: tags 162 | in: query 163 | description: Tags to filter by 164 | required: false 165 | explode: true 166 | schema: 167 | type: array 168 | items: 169 | type: string 170 | responses: 171 | "200": 172 | description: successful operation 173 | content: 174 | application/xml: 175 | schema: 176 | type: array 177 | items: 178 | $ref: "#/components/schemas/Pet" 179 | application/json: 180 | schema: 181 | type: array 182 | items: 183 | $ref: "#/components/schemas/Pet" 184 | "400": 185 | description: Invalid tag value 186 | security: 187 | - petstore_auth: 188 | - "write:pets" 189 | - "read:pets" 190 | "/pet/{petId}": 191 | get: 192 | tags: 193 | - pet 194 | summary: Find pet by ID 195 | description: Returns a single pet 196 | operationId: getPetById 197 | parameters: 198 | - name: petId 199 | in: path 200 | description: ID of pet to return 201 | required: true 202 | schema: 203 | type: integer 204 | format: int64 205 | responses: 206 | "200": 207 | description: successful operation 208 | content: 209 | application/xml: 210 | schema: 211 | $ref: "#/components/schemas/Pet" 212 | application/json: 213 | schema: 214 | $ref: "#/components/schemas/Pet" 215 | "400": 216 | description: Invalid ID supplied 217 | "404": 218 | description: Pet not found 219 | security: 220 | - api_key: [] 221 | - petstore_auth: 222 | - "write:pets" 223 | - "read:pets" 224 | post: 225 | tags: 226 | - pet 227 | summary: Updates a pet in the store with form data 228 | description: "" 229 | operationId: updatePetWithForm 230 | parameters: 231 | - name: petId 232 | in: path 233 | description: ID of pet that needs to be updated 234 | required: true 235 | schema: 236 | type: integer 237 | format: int64 238 | - name: name 239 | in: query 240 | description: Name of pet that needs to be updated 241 | schema: 242 | type: string 243 | - name: status 244 | in: query 245 | description: Status of pet that needs to be updated 246 | schema: 247 | type: string 248 | responses: 249 | "405": 250 | description: Invalid input 251 | security: 252 | - petstore_auth: 253 | - "write:pets" 254 | - "read:pets" 255 | delete: 256 | tags: 257 | - pet 258 | summary: Deletes a pet 259 | description: "" 260 | operationId: deletePet 261 | parameters: 262 | - name: api_key 263 | in: header 264 | description: "" 265 | required: false 266 | schema: 267 | type: string 268 | - name: petId 269 | in: path 270 | description: Pet id to delete 271 | required: true 272 | schema: 273 | type: integer 274 | format: int64 275 | responses: 276 | "400": 277 | description: Invalid pet value 278 | security: 279 | - petstore_auth: 280 | - "write:pets" 281 | - "read:pets" 282 | "/pet/{petId}/uploadImage": 283 | post: 284 | tags: 285 | - pet 286 | summary: uploads an image 287 | description: "" 288 | operationId: uploadFile 289 | parameters: 290 | - name: petId 291 | in: path 292 | description: ID of pet to update 293 | required: true 294 | schema: 295 | type: integer 296 | format: int64 297 | - name: additionalMetadata 298 | in: query 299 | description: Additional Metadata 300 | required: false 301 | schema: 302 | type: string 303 | responses: 304 | "200": 305 | description: successful operation 306 | content: 307 | application/json: 308 | schema: 309 | $ref: "#/components/schemas/ApiResponse" 310 | security: 311 | - petstore_auth: 312 | - "write:pets" 313 | - "read:pets" 314 | requestBody: 315 | content: 316 | application/octet-stream: 317 | schema: 318 | type: string 319 | format: binary 320 | /store/inventory: 321 | get: 322 | tags: 323 | - store 324 | summary: Returns pet inventories by status 325 | description: Returns a map of status codes to quantities 326 | operationId: getInventory 327 | x-swagger-router-controller: OrderController 328 | responses: 329 | "200": 330 | description: successful operation 331 | content: 332 | application/json: 333 | schema: 334 | type: object 335 | additionalProperties: 336 | type: integer 337 | format: int32 338 | security: 339 | - api_key: [] 340 | /store/order: 341 | post: 342 | tags: 343 | - store 344 | summary: Place an order for a pet 345 | description: Place a new order in the store 346 | operationId: placeOrder 347 | x-swagger-router-controller: OrderController 348 | responses: 349 | "200": 350 | description: successful operation 351 | content: 352 | application/json: 353 | schema: 354 | $ref: "#/components/schemas/Order" 355 | "405": 356 | description: Invalid input 357 | requestBody: 358 | content: 359 | application/json: 360 | schema: 361 | $ref: "#/components/schemas/Order" 362 | application/xml: 363 | schema: 364 | $ref: "#/components/schemas/Order" 365 | application/x-www-form-urlencoded: 366 | schema: 367 | $ref: "#/components/schemas/Order" 368 | "/store/order/{orderId}": 369 | get: 370 | tags: 371 | - store 372 | summary: Find purchase order by ID 373 | x-swagger-router-controller: OrderController 374 | description: >- 375 | For valid response try integer IDs with value <= 5 or > 10. Other values 376 | will generated exceptions 377 | operationId: getOrderById 378 | parameters: 379 | - name: orderId 380 | in: path 381 | description: ID of order that needs to be fetched 382 | required: true 383 | schema: 384 | type: integer 385 | format: int64 386 | responses: 387 | "200": 388 | description: successful operation 389 | content: 390 | application/xml: 391 | schema: 392 | $ref: "#/components/schemas/Order" 393 | application/json: 394 | schema: 395 | $ref: "#/components/schemas/Order" 396 | "400": 397 | description: Invalid ID supplied 398 | "404": 399 | description: Order not found 400 | delete: 401 | tags: 402 | - store 403 | summary: Delete purchase order by ID 404 | x-swagger-router-controller: OrderController 405 | description: >- 406 | For valid response try integer IDs with value < 1000. Anything above 407 | 1000 or nonintegers will generate API errors 408 | operationId: deleteOrder 409 | parameters: 410 | - name: orderId 411 | in: path 412 | description: ID of the order that needs to be deleted 413 | required: true 414 | schema: 415 | type: integer 416 | format: int64 417 | responses: 418 | "400": 419 | description: Invalid ID supplied 420 | "404": 421 | description: Order not found 422 | /user: 423 | post: 424 | tags: 425 | - user 426 | summary: Create user 427 | description: This can only be done by the logged in user. 428 | operationId: createUser 429 | responses: 430 | default: 431 | description: successful operation 432 | content: 433 | application/json: 434 | schema: 435 | $ref: "#/components/schemas/User" 436 | application/xml: 437 | schema: 438 | $ref: "#/components/schemas/User" 439 | requestBody: 440 | content: 441 | application/json: 442 | schema: 443 | $ref: "#/components/schemas/User" 444 | application/xml: 445 | schema: 446 | $ref: "#/components/schemas/User" 447 | application/x-www-form-urlencoded: 448 | schema: 449 | $ref: "#/components/schemas/User" 450 | description: Created user object 451 | /user/createWithList: 452 | post: 453 | tags: 454 | - user 455 | summary: Creates list of users with given input array 456 | description: "Creates list of users with given input array" 457 | x-swagger-router-controller: UserController 458 | operationId: createUsersWithListInput 459 | responses: 460 | "200": 461 | description: Successful operation 462 | content: 463 | application/xml: 464 | schema: 465 | $ref: "#/components/schemas/User" 466 | application/json: 467 | schema: 468 | $ref: "#/components/schemas/User" 469 | default: 470 | description: successful operation 471 | requestBody: 472 | content: 473 | application/json: 474 | schema: 475 | type: array 476 | items: 477 | $ref: "#/components/schemas/User" 478 | /user/login: 479 | get: 480 | tags: 481 | - user 482 | summary: Logs user into the system 483 | description: "" 484 | operationId: loginUser 485 | parameters: 486 | - name: username 487 | in: query 488 | description: The user name for login 489 | required: false 490 | schema: 491 | type: string 492 | - name: password 493 | in: query 494 | description: The password for login in clear text 495 | required: false 496 | schema: 497 | type: string 498 | responses: 499 | "200": 500 | description: successful operation 501 | headers: 502 | X-Rate-Limit: 503 | description: calls per hour allowed by the user 504 | schema: 505 | type: integer 506 | format: int32 507 | X-Expires-After: 508 | description: date in UTC when toekn expires 509 | schema: 510 | type: string 511 | format: date-time 512 | content: 513 | application/xml: 514 | schema: 515 | type: string 516 | application/json: 517 | schema: 518 | type: string 519 | "400": 520 | description: Invalid username/password supplied 521 | /user/logout: 522 | get: 523 | tags: 524 | - user 525 | summary: Logs out current logged in user session 526 | description: "" 527 | operationId: logoutUser 528 | parameters: [] 529 | responses: 530 | default: 531 | description: successful operation 532 | "/user/{username}": 533 | get: 534 | tags: 535 | - user 536 | summary: Get user by user name 537 | description: "" 538 | operationId: getUserByName 539 | parameters: 540 | - name: username 541 | in: path 542 | description: "The name that needs to be fetched. Use user1 for testing. " 543 | required: true 544 | schema: 545 | type: string 546 | responses: 547 | "200": 548 | description: successful operation 549 | content: 550 | application/xml: 551 | schema: 552 | $ref: "#/components/schemas/User" 553 | application/json: 554 | schema: 555 | $ref: "#/components/schemas/User" 556 | "400": 557 | description: Invalid username supplied 558 | "404": 559 | description: User not found 560 | put: 561 | tags: 562 | - user 563 | summary: Update user 564 | x-swagger-router-controller: UserController 565 | description: This can only be done by the logged in user. 566 | operationId: updateUser 567 | parameters: 568 | - name: username 569 | in: path 570 | description: name that need to be deleted 571 | required: true 572 | schema: 573 | type: string 574 | responses: 575 | default: 576 | description: successful operation 577 | requestBody: 578 | description: Update an existent user in the store 579 | content: 580 | application/json: 581 | schema: 582 | $ref: "#/components/schemas/User" 583 | application/xml: 584 | schema: 585 | $ref: "#/components/schemas/User" 586 | application/x-www-form-urlencoded: 587 | schema: 588 | $ref: "#/components/schemas/User" 589 | delete: 590 | tags: 591 | - user 592 | summary: Delete user 593 | description: This can only be done by the logged in user. 594 | operationId: deleteUser 595 | parameters: 596 | - name: username 597 | in: path 598 | description: The name that needs to be deleted 599 | required: true 600 | schema: 601 | type: string 602 | responses: 603 | "400": 604 | description: Invalid username supplied 605 | "404": 606 | description: User not found 607 | externalDocs: 608 | description: Find out more about Swagger 609 | url: "http://swagger.io" 610 | components: 611 | schemas: 612 | Order: 613 | x-swagger-router-model: io.swagger.petstore.model.Order 614 | properties: 615 | id: 616 | type: integer 617 | format: int64 618 | example: 10 619 | petId: 620 | type: integer 621 | format: int64 622 | example: 198772 623 | quantity: 624 | type: integer 625 | format: int32 626 | example: 7 627 | shipDate: 628 | type: string 629 | format: date-time 630 | status: 631 | type: string 632 | description: Order Status 633 | enum: 634 | - placed 635 | - approved 636 | - delivered 637 | example: approved 638 | complete: 639 | type: boolean 640 | xml: 641 | name: order 642 | type: object 643 | Customer: 644 | properties: 645 | id: 646 | type: integer 647 | format: int64 648 | example: 100000 649 | username: 650 | type: string 651 | example: fehguy 652 | address: 653 | type: array 654 | items: 655 | $ref: "#/components/schemas/Address" 656 | xml: 657 | wrapped: true 658 | name: addresses 659 | xml: 660 | name: customer 661 | type: object 662 | Address: 663 | properties: 664 | street: 665 | type: string 666 | example: 437 Lytton 667 | city: 668 | type: string 669 | example: Palo Alto 670 | state: 671 | type: string 672 | example: CA 673 | zip: 674 | type: string 675 | example: 94301 676 | xml: 677 | name: address 678 | type: object 679 | Category: 680 | x-swagger-router-model: io.swagger.petstore.model.Category 681 | properties: 682 | id: 683 | type: integer 684 | format: int64 685 | example: 1 686 | name: 687 | type: string 688 | example: Dogs 689 | xml: 690 | name: category 691 | type: object 692 | User: 693 | x-swagger-router-model: io.swagger.petstore.model.User 694 | properties: 695 | id: 696 | type: integer 697 | format: int64 698 | example: 10 699 | username: 700 | type: string 701 | example: theUser 702 | firstName: 703 | type: string 704 | example: John 705 | lastName: 706 | type: string 707 | example: James 708 | email: 709 | type: string 710 | example: john@email.com 711 | password: 712 | type: string 713 | example: 12345 714 | phone: 715 | type: string 716 | example: 12345 717 | userStatus: 718 | type: integer 719 | format: int32 720 | example: 1 721 | description: User Status 722 | xml: 723 | name: user 724 | type: object 725 | Tag: 726 | x-swagger-router-model: io.swagger.petstore.model.Tag 727 | properties: 728 | id: 729 | type: integer 730 | format: int64 731 | name: 732 | type: string 733 | xml: 734 | name: tag 735 | type: object 736 | Pet: 737 | x-swagger-router-model: io.swagger.petstore.model.Pet 738 | required: 739 | - name 740 | - photoUrls 741 | properties: 742 | id: 743 | type: integer 744 | format: int64 745 | example: 10 746 | name: 747 | type: string 748 | example: doggie 749 | category: 750 | $ref: "#/components/schemas/Category" 751 | photoUrls: 752 | type: array 753 | xml: 754 | wrapped: true 755 | items: 756 | type: string 757 | xml: 758 | name: photoUrl 759 | tags: 760 | type: array 761 | xml: 762 | wrapped: true 763 | items: 764 | $ref: "#/components/schemas/Tag" 765 | xml: 766 | name: tag 767 | status: 768 | type: string 769 | description: pet status in the store 770 | enum: 771 | - available 772 | - pending 773 | - sold 774 | xml: 775 | name: pet 776 | type: object 777 | ApiResponse: 778 | properties: 779 | code: 780 | type: integer 781 | format: int32 782 | type: 783 | type: string 784 | message: 785 | type: string 786 | xml: 787 | name: "##default" 788 | type: object 789 | requestBodies: 790 | Pet: 791 | content: 792 | application/json: 793 | schema: 794 | $ref: "#/components/schemas/Pet" 795 | application/xml: 796 | schema: 797 | $ref: "#/components/schemas/Pet" 798 | description: Pet object that needs to be added to the store 799 | UserArray: 800 | content: 801 | application/json: 802 | schema: 803 | type: array 804 | items: 805 | $ref: "#/components/schemas/User" 806 | description: List of user object 807 | securitySchemes: 808 | petstore_auth: 809 | type: oauth2 810 | flows: 811 | implicit: 812 | authorizationUrl: "https://petstore.swagger.io/oauth/authorize" 813 | scopes: 814 | "write:pets": modify pets in your account 815 | "read:pets": read your pets 816 | api_key: 817 | type: apiKey 818 | name: api_key 819 | in: header 820 | -------------------------------------------------------------------------------- /examples/react-query-petstore/src/App.tsx: -------------------------------------------------------------------------------- 1 | import { QueryClient, QueryClientProvider } from "react-query"; 2 | import { ChakraProvider, Container } from "@chakra-ui/react"; 3 | import { 4 | BrowserRouter as Router, 5 | Switch, 6 | Route, 7 | Redirect, 8 | } from "react-router-dom"; 9 | import { PetsIndex } from "./features/pet/PetsIndex"; 10 | import React from "react"; 11 | 12 | const queryClient = new QueryClient(); 13 | 14 | function App() { 15 | return ( 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | ); 31 | } 32 | 33 | export default App; 34 | -------------------------------------------------------------------------------- /examples/react-query-petstore/src/api/components/requestBodies/Pet.ts: -------------------------------------------------------------------------------- 1 | import { OperationBody } from "@openapi-io-ts/runtime"; 2 | 3 | export const Pet: OperationBody = { 4 | _tag: "JsonBody", 5 | }; 6 | -------------------------------------------------------------------------------- /examples/react-query-petstore/src/api/components/requestBodies/UserArray.ts: -------------------------------------------------------------------------------- 1 | import { OperationBody } from "@openapi-io-ts/runtime"; 2 | 3 | export const UserArray: OperationBody = { 4 | _tag: "JsonBody", 5 | }; 6 | -------------------------------------------------------------------------------- /examples/react-query-petstore/src/api/components/requestBodies/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./Pet"; 2 | export * from "./UserArray"; 3 | -------------------------------------------------------------------------------- /examples/react-query-petstore/src/api/components/schemas/Address.ts: -------------------------------------------------------------------------------- 1 | import * as t from "io-ts"; 2 | 3 | export const Address = t.partial({ 4 | street: t.string, 5 | city: t.string, 6 | state: t.string, 7 | zip: t.string, 8 | }); 9 | 10 | export interface Address { 11 | street?: string; 12 | city?: string; 13 | state?: string; 14 | zip?: string; 15 | } 16 | -------------------------------------------------------------------------------- /examples/react-query-petstore/src/api/components/schemas/ApiResponse.ts: -------------------------------------------------------------------------------- 1 | import * as t from "io-ts"; 2 | 3 | export const ApiResponse = t.partial({ 4 | code: t.number, 5 | type: t.string, 6 | message: t.string, 7 | }); 8 | 9 | export interface ApiResponse { 10 | code?: number; 11 | type?: string; 12 | message?: string; 13 | } 14 | -------------------------------------------------------------------------------- /examples/react-query-petstore/src/api/components/schemas/Category.ts: -------------------------------------------------------------------------------- 1 | import * as t from "io-ts"; 2 | 3 | export const Category = t.partial({ 4 | id: t.number, 5 | name: t.string, 6 | }); 7 | 8 | export interface Category { 9 | id?: number; 10 | name?: string; 11 | } 12 | -------------------------------------------------------------------------------- /examples/react-query-petstore/src/api/components/schemas/Customer.ts: -------------------------------------------------------------------------------- 1 | import * as t from "io-ts"; 2 | import { Address } from "./Address"; 3 | 4 | export const Customer = t.partial({ 5 | id: t.number, 6 | username: t.string, 7 | address: t.array(Address), 8 | }); 9 | 10 | export interface Customer { 11 | id?: number; 12 | username?: string; 13 | address?: Array
; 14 | } 15 | -------------------------------------------------------------------------------- /examples/react-query-petstore/src/api/components/schemas/Order.ts: -------------------------------------------------------------------------------- 1 | import * as t from "io-ts"; 2 | import { DateFromISOString } from "io-ts-types/DateFromISOString"; 3 | 4 | export const Order = t.partial({ 5 | id: t.number, 6 | petId: t.number, 7 | quantity: t.number, 8 | shipDate: DateFromISOString, 9 | status: t.union([ 10 | t.literal("placed"), 11 | t.literal("approved"), 12 | t.literal("delivered"), 13 | ]), 14 | complete: t.boolean, 15 | }); 16 | 17 | export interface Order { 18 | id?: number; 19 | petId?: number; 20 | quantity?: number; 21 | shipDate?: Date; 22 | status?: "placed" | "approved" | "delivered"; 23 | complete?: boolean; 24 | } 25 | -------------------------------------------------------------------------------- /examples/react-query-petstore/src/api/components/schemas/Pet.ts: -------------------------------------------------------------------------------- 1 | import * as t from "io-ts"; 2 | import { Category } from "./Category"; 3 | import { Tag } from "./Tag"; 4 | 5 | export const Pet = t.intersection([ 6 | t.type({ 7 | name: t.string, 8 | photoUrls: t.array(t.string), 9 | }), 10 | t.partial({ 11 | id: t.number, 12 | category: Category, 13 | tags: t.array(Tag), 14 | status: t.union([ 15 | t.literal("available"), 16 | t.literal("pending"), 17 | t.literal("sold"), 18 | ]), 19 | }), 20 | ]); 21 | 22 | export interface Pet { 23 | id?: number; 24 | name: string; 25 | category?: Category; 26 | photoUrls: Array; 27 | tags?: Array; 28 | status?: "available" | "pending" | "sold"; 29 | } 30 | -------------------------------------------------------------------------------- /examples/react-query-petstore/src/api/components/schemas/Tag.ts: -------------------------------------------------------------------------------- 1 | import * as t from "io-ts"; 2 | 3 | export const Tag = t.partial({ 4 | id: t.number, 5 | name: t.string, 6 | }); 7 | 8 | export interface Tag { 9 | id?: number; 10 | name?: string; 11 | } 12 | -------------------------------------------------------------------------------- /examples/react-query-petstore/src/api/components/schemas/User.ts: -------------------------------------------------------------------------------- 1 | import * as t from "io-ts"; 2 | 3 | export const User = t.partial({ 4 | id: t.number, 5 | username: t.string, 6 | firstName: t.string, 7 | lastName: t.string, 8 | email: t.string, 9 | password: t.string, 10 | phone: t.string, 11 | userStatus: t.number, 12 | }); 13 | 14 | export interface User { 15 | id?: number; 16 | username?: string; 17 | firstName?: string; 18 | lastName?: string; 19 | email?: string; 20 | password?: string; 21 | phone?: string; 22 | userStatus?: number; 23 | } 24 | -------------------------------------------------------------------------------- /examples/react-query-petstore/src/api/components/schemas/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./Address"; 2 | export * from "./ApiResponse"; 3 | export * from "./Category"; 4 | export * from "./Customer"; 5 | export * from "./Order"; 6 | export * from "./Pet"; 7 | export * from "./Tag"; 8 | export * from "./User"; 9 | -------------------------------------------------------------------------------- /examples/react-query-petstore/src/api/operations/addPet.ts: -------------------------------------------------------------------------------- 1 | import type { RequestFunction } from "@openapi-io-ts/runtime"; 2 | import * as schemas from "../components/schemas"; 3 | 4 | export const addPetOperation = { 5 | path: "/pet", 6 | method: "post", 7 | responses: { 8 | "200": { _tag: "JsonResponse", decoder: schemas.Pet }, 9 | "405": { _tag: "EmptyResponse" }, 10 | }, 11 | parameters: [], 12 | requestDefaultHeaders: { 13 | "Content-Type": "application/json", 14 | Accept: "application/json", 15 | }, 16 | body: { 17 | _tag: "JsonBody", 18 | }, 19 | } as const; 20 | 21 | export type AddPetRequestFunction = RequestFunction< 22 | { body: schemas.Pet }, 23 | schemas.Pet 24 | >; 25 | -------------------------------------------------------------------------------- /examples/react-query-petstore/src/api/operations/createUser.ts: -------------------------------------------------------------------------------- 1 | import type { RequestFunction } from "@openapi-io-ts/runtime"; 2 | import * as schemas from "../components/schemas"; 3 | 4 | export const createUserOperation = { 5 | path: "/user", 6 | method: "post", 7 | responses: { default: { _tag: "JsonResponse", decoder: schemas.User } }, 8 | parameters: [], 9 | requestDefaultHeaders: { "Content-Type": "application/json" }, 10 | body: { 11 | _tag: "JsonBody", 12 | }, 13 | } as const; 14 | 15 | export type CreateUserRequestFunction = RequestFunction< 16 | { body: schemas.User }, 17 | void 18 | >; 19 | -------------------------------------------------------------------------------- /examples/react-query-petstore/src/api/operations/createUsersWithListInput.ts: -------------------------------------------------------------------------------- 1 | import type { RequestFunction } from "@openapi-io-ts/runtime"; 2 | import * as schemas from "../components/schemas"; 3 | 4 | export const createUsersWithListInputOperation = { 5 | path: "/user/createWithList", 6 | method: "post", 7 | responses: { 8 | "200": { _tag: "JsonResponse", decoder: schemas.User }, 9 | default: { _tag: "EmptyResponse" }, 10 | }, 11 | parameters: [], 12 | requestDefaultHeaders: { 13 | "Content-Type": "application/json", 14 | Accept: "application/json", 15 | }, 16 | body: { 17 | _tag: "JsonBody", 18 | }, 19 | } as const; 20 | 21 | export type CreateUsersWithListInputRequestFunction = RequestFunction< 22 | { body: Array }, 23 | schemas.User 24 | >; 25 | -------------------------------------------------------------------------------- /examples/react-query-petstore/src/api/operations/deleteOrder.ts: -------------------------------------------------------------------------------- 1 | import type { RequestFunction } from "@openapi-io-ts/runtime"; 2 | 3 | export type DeleteOrderRequestParameters = { 4 | orderId: number; 5 | }; 6 | 7 | export const deleteOrderOperation = { 8 | path: "/store/order/{orderId}", 9 | method: "delete", 10 | responses: { 11 | "400": { _tag: "EmptyResponse" }, 12 | "404": { _tag: "EmptyResponse" }, 13 | }, 14 | parameters: [ 15 | { 16 | _tag: "FormParameter", 17 | explode: false, 18 | in: "path", 19 | name: "orderId", 20 | }, 21 | ], 22 | requestDefaultHeaders: {}, 23 | } as const; 24 | 25 | export type DeleteOrderRequestFunction = RequestFunction< 26 | { params: DeleteOrderRequestParameters }, 27 | void 28 | >; 29 | -------------------------------------------------------------------------------- /examples/react-query-petstore/src/api/operations/deletePet.ts: -------------------------------------------------------------------------------- 1 | import type { RequestFunction } from "@openapi-io-ts/runtime"; 2 | 3 | export type DeletePetRequestParameters = { 4 | api_key?: string; 5 | petId: number; 6 | }; 7 | 8 | export const deletePetOperation = { 9 | path: "/pet/{petId}", 10 | method: "delete", 11 | responses: { "400": { _tag: "EmptyResponse" } }, 12 | parameters: [ 13 | { 14 | _tag: "FormParameter", 15 | explode: false, 16 | in: "header", 17 | name: "api_key", 18 | }, 19 | { 20 | _tag: "FormParameter", 21 | explode: false, 22 | in: "path", 23 | name: "petId", 24 | }, 25 | ], 26 | requestDefaultHeaders: {}, 27 | } as const; 28 | 29 | export type DeletePetRequestFunction = RequestFunction< 30 | { params: DeletePetRequestParameters }, 31 | void 32 | >; 33 | -------------------------------------------------------------------------------- /examples/react-query-petstore/src/api/operations/deleteUser.ts: -------------------------------------------------------------------------------- 1 | import type { RequestFunction } from "@openapi-io-ts/runtime"; 2 | 3 | export type DeleteUserRequestParameters = { 4 | username: string; 5 | }; 6 | 7 | export const deleteUserOperation = { 8 | path: "/user/{username}", 9 | method: "delete", 10 | responses: { 11 | "400": { _tag: "EmptyResponse" }, 12 | "404": { _tag: "EmptyResponse" }, 13 | }, 14 | parameters: [ 15 | { 16 | _tag: "FormParameter", 17 | explode: false, 18 | in: "path", 19 | name: "username", 20 | }, 21 | ], 22 | requestDefaultHeaders: {}, 23 | } as const; 24 | 25 | export type DeleteUserRequestFunction = RequestFunction< 26 | { params: DeleteUserRequestParameters }, 27 | void 28 | >; 29 | -------------------------------------------------------------------------------- /examples/react-query-petstore/src/api/operations/findPetsByStatus.ts: -------------------------------------------------------------------------------- 1 | import type { RequestFunction } from "@openapi-io-ts/runtime"; 2 | import * as t from "io-ts"; 3 | import * as schemas from "../components/schemas"; 4 | 5 | export type FindPetsByStatusRequestParameters = { 6 | status?: "available" | "pending" | "sold"; 7 | }; 8 | 9 | export const findPetsByStatusOperation = { 10 | path: "/pet/findByStatus", 11 | method: "get", 12 | responses: { 13 | "200": { _tag: "JsonResponse", decoder: t.array(schemas.Pet) }, 14 | "400": { _tag: "EmptyResponse" }, 15 | }, 16 | parameters: [ 17 | { 18 | _tag: "FormParameter", 19 | explode: true, 20 | in: "query", 21 | name: "status", 22 | }, 23 | ], 24 | requestDefaultHeaders: { Accept: "application/json" }, 25 | } as const; 26 | 27 | export type FindPetsByStatusRequestFunction = RequestFunction< 28 | { params: FindPetsByStatusRequestParameters }, 29 | Array 30 | >; 31 | -------------------------------------------------------------------------------- /examples/react-query-petstore/src/api/operations/findPetsByTags.ts: -------------------------------------------------------------------------------- 1 | import type { RequestFunction } from "@openapi-io-ts/runtime"; 2 | import * as t from "io-ts"; 3 | import * as schemas from "../components/schemas"; 4 | 5 | export type FindPetsByTagsRequestParameters = { 6 | tags?: Array; 7 | }; 8 | 9 | export const findPetsByTagsOperation = { 10 | path: "/pet/findByTags", 11 | method: "get", 12 | responses: { 13 | "200": { _tag: "JsonResponse", decoder: t.array(schemas.Pet) }, 14 | "400": { _tag: "EmptyResponse" }, 15 | }, 16 | parameters: [ 17 | { 18 | _tag: "FormParameter", 19 | explode: true, 20 | in: "query", 21 | name: "tags", 22 | }, 23 | ], 24 | requestDefaultHeaders: { Accept: "application/json" }, 25 | } as const; 26 | 27 | export type FindPetsByTagsRequestFunction = RequestFunction< 28 | { params: FindPetsByTagsRequestParameters }, 29 | Array 30 | >; 31 | -------------------------------------------------------------------------------- /examples/react-query-petstore/src/api/operations/getInventory.ts: -------------------------------------------------------------------------------- 1 | import type { RequestFunction } from "@openapi-io-ts/runtime"; 2 | import * as t from "io-ts"; 3 | 4 | export const getInventoryOperation = { 5 | path: "/store/inventory", 6 | method: "get", 7 | responses: { "200": { _tag: "JsonResponse", decoder: t.UnknownRecord } }, 8 | parameters: [], 9 | requestDefaultHeaders: { Accept: "application/json" }, 10 | } as const; 11 | 12 | export type GetInventoryRequestFunction = RequestFunction< 13 | undefined, 14 | Record 15 | >; 16 | -------------------------------------------------------------------------------- /examples/react-query-petstore/src/api/operations/getOrderById.ts: -------------------------------------------------------------------------------- 1 | import type { RequestFunction } from "@openapi-io-ts/runtime"; 2 | import * as schemas from "../components/schemas"; 3 | 4 | export type GetOrderByIdRequestParameters = { 5 | orderId: number; 6 | }; 7 | 8 | export const getOrderByIdOperation = { 9 | path: "/store/order/{orderId}", 10 | method: "get", 11 | responses: { 12 | "200": { _tag: "JsonResponse", decoder: schemas.Order }, 13 | "400": { _tag: "EmptyResponse" }, 14 | "404": { _tag: "EmptyResponse" }, 15 | }, 16 | parameters: [ 17 | { 18 | _tag: "FormParameter", 19 | explode: false, 20 | in: "path", 21 | name: "orderId", 22 | }, 23 | ], 24 | requestDefaultHeaders: { Accept: "application/json" }, 25 | } as const; 26 | 27 | export type GetOrderByIdRequestFunction = RequestFunction< 28 | { params: GetOrderByIdRequestParameters }, 29 | schemas.Order 30 | >; 31 | -------------------------------------------------------------------------------- /examples/react-query-petstore/src/api/operations/getPetById.ts: -------------------------------------------------------------------------------- 1 | import type { RequestFunction } from "@openapi-io-ts/runtime"; 2 | import * as schemas from "../components/schemas"; 3 | 4 | export type GetPetByIdRequestParameters = { 5 | petId: number; 6 | }; 7 | 8 | export const getPetByIdOperation = { 9 | path: "/pet/{petId}", 10 | method: "get", 11 | responses: { 12 | "200": { _tag: "JsonResponse", decoder: schemas.Pet }, 13 | "400": { _tag: "EmptyResponse" }, 14 | "404": { _tag: "EmptyResponse" }, 15 | }, 16 | parameters: [ 17 | { 18 | _tag: "FormParameter", 19 | explode: false, 20 | in: "path", 21 | name: "petId", 22 | }, 23 | ], 24 | requestDefaultHeaders: { Accept: "application/json" }, 25 | } as const; 26 | 27 | export type GetPetByIdRequestFunction = RequestFunction< 28 | { params: GetPetByIdRequestParameters }, 29 | schemas.Pet 30 | >; 31 | -------------------------------------------------------------------------------- /examples/react-query-petstore/src/api/operations/getUserByName.ts: -------------------------------------------------------------------------------- 1 | import type { RequestFunction } from "@openapi-io-ts/runtime"; 2 | import * as schemas from "../components/schemas"; 3 | 4 | export type GetUserByNameRequestParameters = { 5 | username: string; 6 | }; 7 | 8 | export const getUserByNameOperation = { 9 | path: "/user/{username}", 10 | method: "get", 11 | responses: { 12 | "200": { _tag: "JsonResponse", decoder: schemas.User }, 13 | "400": { _tag: "EmptyResponse" }, 14 | "404": { _tag: "EmptyResponse" }, 15 | }, 16 | parameters: [ 17 | { 18 | _tag: "FormParameter", 19 | explode: false, 20 | in: "path", 21 | name: "username", 22 | }, 23 | ], 24 | requestDefaultHeaders: { Accept: "application/json" }, 25 | } as const; 26 | 27 | export type GetUserByNameRequestFunction = RequestFunction< 28 | { params: GetUserByNameRequestParameters }, 29 | schemas.User 30 | >; 31 | -------------------------------------------------------------------------------- /examples/react-query-petstore/src/api/operations/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | HttpRequestAdapter, 3 | requestFunctionBuilder, 4 | } from "@openapi-io-ts/runtime"; 5 | import { addPetOperation, AddPetRequestFunction } from "./addPet"; 6 | import { createUserOperation, CreateUserRequestFunction } from "./createUser"; 7 | import { 8 | createUsersWithListInputOperation, 9 | CreateUsersWithListInputRequestFunction, 10 | } from "./createUsersWithListInput"; 11 | import { 12 | deleteOrderOperation, 13 | DeleteOrderRequestFunction, 14 | } from "./deleteOrder"; 15 | import { deletePetOperation, DeletePetRequestFunction } from "./deletePet"; 16 | import { deleteUserOperation, DeleteUserRequestFunction } from "./deleteUser"; 17 | import { 18 | findPetsByStatusOperation, 19 | FindPetsByStatusRequestFunction, 20 | } from "./findPetsByStatus"; 21 | import { 22 | findPetsByTagsOperation, 23 | FindPetsByTagsRequestFunction, 24 | } from "./findPetsByTags"; 25 | import { 26 | getInventoryOperation, 27 | GetInventoryRequestFunction, 28 | } from "./getInventory"; 29 | import { 30 | getOrderByIdOperation, 31 | GetOrderByIdRequestFunction, 32 | } from "./getOrderById"; 33 | import { getPetByIdOperation, GetPetByIdRequestFunction } from "./getPetById"; 34 | import { 35 | getUserByNameOperation, 36 | GetUserByNameRequestFunction, 37 | } from "./getUserByName"; 38 | import { loginUserOperation, LoginUserRequestFunction } from "./loginUser"; 39 | import { logoutUserOperation, LogoutUserRequestFunction } from "./logoutUser"; 40 | import { placeOrderOperation, PlaceOrderRequestFunction } from "./placeOrder"; 41 | import { updatePetOperation, UpdatePetRequestFunction } from "./updatePet"; 42 | import { 43 | updatePetWithFormOperation, 44 | UpdatePetWithFormRequestFunction, 45 | } from "./updatePetWithForm"; 46 | import { updateUserOperation, UpdateUserRequestFunction } from "./updateUser"; 47 | import { uploadFileOperation, UploadFileRequestFunction } from "./uploadFile"; 48 | 49 | export const operations = { 50 | addPet: addPetOperation, 51 | updatePet: updatePetOperation, 52 | findPetsByStatus: findPetsByStatusOperation, 53 | findPetsByTags: findPetsByTagsOperation, 54 | getPetById: getPetByIdOperation, 55 | updatePetWithForm: updatePetWithFormOperation, 56 | deletePet: deletePetOperation, 57 | uploadFile: uploadFileOperation, 58 | getInventory: getInventoryOperation, 59 | placeOrder: placeOrderOperation, 60 | getOrderById: getOrderByIdOperation, 61 | deleteOrder: deleteOrderOperation, 62 | createUser: createUserOperation, 63 | createUsersWithListInput: createUsersWithListInputOperation, 64 | loginUser: loginUserOperation, 65 | logoutUser: logoutUserOperation, 66 | getUserByName: getUserByNameOperation, 67 | updateUser: updateUserOperation, 68 | deleteUser: deleteUserOperation, 69 | } as const; 70 | 71 | export interface OperationRequestFunctionMap { 72 | addPet: AddPetRequestFunction; 73 | updatePet: UpdatePetRequestFunction; 74 | findPetsByStatus: FindPetsByStatusRequestFunction; 75 | findPetsByTags: FindPetsByTagsRequestFunction; 76 | getPetById: GetPetByIdRequestFunction; 77 | updatePetWithForm: UpdatePetWithFormRequestFunction; 78 | deletePet: DeletePetRequestFunction; 79 | uploadFile: UploadFileRequestFunction; 80 | getInventory: GetInventoryRequestFunction; 81 | placeOrder: PlaceOrderRequestFunction; 82 | getOrderById: GetOrderByIdRequestFunction; 83 | deleteOrder: DeleteOrderRequestFunction; 84 | createUser: CreateUserRequestFunction; 85 | createUsersWithListInput: CreateUsersWithListInputRequestFunction; 86 | loginUser: LoginUserRequestFunction; 87 | logoutUser: LogoutUserRequestFunction; 88 | getUserByName: GetUserByNameRequestFunction; 89 | updateUser: UpdateUserRequestFunction; 90 | deleteUser: DeleteUserRequestFunction; 91 | } 92 | 93 | export const requestFunctionsBuilder = ( 94 | requestAdapter: HttpRequestAdapter 95 | ): OperationRequestFunctionMap => ({ 96 | addPet: requestFunctionBuilder(operations.addPet, requestAdapter), 97 | updatePet: requestFunctionBuilder(operations.updatePet, requestAdapter), 98 | findPetsByStatus: requestFunctionBuilder( 99 | operations.findPetsByStatus, 100 | requestAdapter 101 | ), 102 | findPetsByTags: requestFunctionBuilder( 103 | operations.findPetsByTags, 104 | requestAdapter 105 | ), 106 | getPetById: requestFunctionBuilder(operations.getPetById, requestAdapter), 107 | updatePetWithForm: requestFunctionBuilder( 108 | operations.updatePetWithForm, 109 | requestAdapter 110 | ), 111 | deletePet: requestFunctionBuilder(operations.deletePet, requestAdapter), 112 | uploadFile: requestFunctionBuilder(operations.uploadFile, requestAdapter), 113 | getInventory: requestFunctionBuilder(operations.getInventory, requestAdapter), 114 | placeOrder: requestFunctionBuilder(operations.placeOrder, requestAdapter), 115 | getOrderById: requestFunctionBuilder(operations.getOrderById, requestAdapter), 116 | deleteOrder: requestFunctionBuilder(operations.deleteOrder, requestAdapter), 117 | createUser: requestFunctionBuilder(operations.createUser, requestAdapter), 118 | createUsersWithListInput: requestFunctionBuilder( 119 | operations.createUsersWithListInput, 120 | requestAdapter 121 | ), 122 | loginUser: requestFunctionBuilder(operations.loginUser, requestAdapter), 123 | logoutUser: requestFunctionBuilder(operations.logoutUser, requestAdapter), 124 | getUserByName: requestFunctionBuilder( 125 | operations.getUserByName, 126 | requestAdapter 127 | ), 128 | updateUser: requestFunctionBuilder(operations.updateUser, requestAdapter), 129 | deleteUser: requestFunctionBuilder(operations.deleteUser, requestAdapter), 130 | }); 131 | 132 | export const petServiceBuilder = ( 133 | requestFunctions: OperationRequestFunctionMap 134 | ) => ({ 135 | addPet: requestFunctions.addPet, 136 | updatePet: requestFunctions.updatePet, 137 | findPetsByStatus: requestFunctions.findPetsByStatus, 138 | findPetsByTags: requestFunctions.findPetsByTags, 139 | getPetById: requestFunctions.getPetById, 140 | updatePetWithForm: requestFunctions.updatePetWithForm, 141 | deletePet: requestFunctions.deletePet, 142 | uploadFile: requestFunctions.uploadFile, 143 | }); 144 | 145 | export const storeServiceBuilder = ( 146 | requestFunctions: OperationRequestFunctionMap 147 | ) => ({ 148 | getInventory: requestFunctions.getInventory, 149 | placeOrder: requestFunctions.placeOrder, 150 | getOrderById: requestFunctions.getOrderById, 151 | deleteOrder: requestFunctions.deleteOrder, 152 | }); 153 | 154 | export const userServiceBuilder = ( 155 | requestFunctions: OperationRequestFunctionMap 156 | ) => ({ 157 | createUser: requestFunctions.createUser, 158 | createUsersWithListInput: requestFunctions.createUsersWithListInput, 159 | loginUser: requestFunctions.loginUser, 160 | logoutUser: requestFunctions.logoutUser, 161 | getUserByName: requestFunctions.getUserByName, 162 | updateUser: requestFunctions.updateUser, 163 | deleteUser: requestFunctions.deleteUser, 164 | }); 165 | -------------------------------------------------------------------------------- /examples/react-query-petstore/src/api/operations/loginUser.ts: -------------------------------------------------------------------------------- 1 | import type { RequestFunction } from "@openapi-io-ts/runtime"; 2 | import * as t from "io-ts"; 3 | 4 | export type LoginUserRequestParameters = { 5 | username?: string; 6 | password?: string; 7 | }; 8 | 9 | export const loginUserOperation = { 10 | path: "/user/login", 11 | method: "get", 12 | responses: { 13 | "200": { _tag: "JsonResponse", decoder: t.string }, 14 | "400": { _tag: "EmptyResponse" }, 15 | }, 16 | parameters: [ 17 | { 18 | _tag: "FormParameter", 19 | explode: true, 20 | in: "query", 21 | name: "username", 22 | }, 23 | { 24 | _tag: "FormParameter", 25 | explode: true, 26 | in: "query", 27 | name: "password", 28 | }, 29 | ], 30 | requestDefaultHeaders: { Accept: "application/json" }, 31 | } as const; 32 | 33 | export type LoginUserRequestFunction = RequestFunction< 34 | { params: LoginUserRequestParameters }, 35 | string 36 | >; 37 | -------------------------------------------------------------------------------- /examples/react-query-petstore/src/api/operations/logoutUser.ts: -------------------------------------------------------------------------------- 1 | import type { RequestFunction } from "@openapi-io-ts/runtime"; 2 | 3 | export const logoutUserOperation = { 4 | path: "/user/logout", 5 | method: "get", 6 | responses: { default: { _tag: "EmptyResponse" } }, 7 | parameters: [], 8 | requestDefaultHeaders: {}, 9 | } as const; 10 | 11 | export type LogoutUserRequestFunction = RequestFunction; 12 | -------------------------------------------------------------------------------- /examples/react-query-petstore/src/api/operations/placeOrder.ts: -------------------------------------------------------------------------------- 1 | import type { RequestFunction } from "@openapi-io-ts/runtime"; 2 | import * as schemas from "../components/schemas"; 3 | 4 | export const placeOrderOperation = { 5 | path: "/store/order", 6 | method: "post", 7 | responses: { 8 | "200": { _tag: "JsonResponse", decoder: schemas.Order }, 9 | "405": { _tag: "EmptyResponse" }, 10 | }, 11 | parameters: [], 12 | requestDefaultHeaders: { 13 | "Content-Type": "application/json", 14 | Accept: "application/json", 15 | }, 16 | body: { 17 | _tag: "JsonBody", 18 | }, 19 | } as const; 20 | 21 | export type PlaceOrderRequestFunction = RequestFunction< 22 | { body: schemas.Order }, 23 | schemas.Order 24 | >; 25 | -------------------------------------------------------------------------------- /examples/react-query-petstore/src/api/operations/updatePet.ts: -------------------------------------------------------------------------------- 1 | import type { RequestFunction } from "@openapi-io-ts/runtime"; 2 | import * as schemas from "../components/schemas"; 3 | 4 | export const updatePetOperation = { 5 | path: "/pet", 6 | method: "put", 7 | responses: { 8 | "200": { _tag: "JsonResponse", decoder: schemas.Pet }, 9 | "400": { _tag: "EmptyResponse" }, 10 | "404": { _tag: "EmptyResponse" }, 11 | "405": { _tag: "EmptyResponse" }, 12 | }, 13 | parameters: [], 14 | requestDefaultHeaders: { 15 | "Content-Type": "application/json", 16 | Accept: "application/json", 17 | }, 18 | body: { 19 | _tag: "JsonBody", 20 | }, 21 | } as const; 22 | 23 | export type UpdatePetRequestFunction = RequestFunction< 24 | { body: schemas.Pet }, 25 | schemas.Pet 26 | >; 27 | -------------------------------------------------------------------------------- /examples/react-query-petstore/src/api/operations/updatePetWithForm.ts: -------------------------------------------------------------------------------- 1 | import type { RequestFunction } from "@openapi-io-ts/runtime"; 2 | 3 | export type UpdatePetWithFormRequestParameters = { 4 | petId: number; 5 | name?: string; 6 | status?: string; 7 | }; 8 | 9 | export const updatePetWithFormOperation = { 10 | path: "/pet/{petId}", 11 | method: "post", 12 | responses: { "405": { _tag: "EmptyResponse" } }, 13 | parameters: [ 14 | { 15 | _tag: "FormParameter", 16 | explode: false, 17 | in: "path", 18 | name: "petId", 19 | }, 20 | { 21 | _tag: "FormParameter", 22 | explode: true, 23 | in: "query", 24 | name: "name", 25 | }, 26 | { 27 | _tag: "FormParameter", 28 | explode: true, 29 | in: "query", 30 | name: "status", 31 | }, 32 | ], 33 | requestDefaultHeaders: {}, 34 | } as const; 35 | 36 | export type UpdatePetWithFormRequestFunction = RequestFunction< 37 | { params: UpdatePetWithFormRequestParameters }, 38 | void 39 | >; 40 | -------------------------------------------------------------------------------- /examples/react-query-petstore/src/api/operations/updateUser.ts: -------------------------------------------------------------------------------- 1 | import type { RequestFunction } from "@openapi-io-ts/runtime"; 2 | import * as schemas from "../components/schemas"; 3 | 4 | export type UpdateUserRequestParameters = { 5 | username: string; 6 | }; 7 | 8 | export const updateUserOperation = { 9 | path: "/user/{username}", 10 | method: "put", 11 | responses: { default: { _tag: "EmptyResponse" } }, 12 | parameters: [ 13 | { 14 | _tag: "FormParameter", 15 | explode: false, 16 | in: "path", 17 | name: "username", 18 | }, 19 | ], 20 | requestDefaultHeaders: { "Content-Type": "application/json" }, 21 | body: { 22 | _tag: "JsonBody", 23 | }, 24 | } as const; 25 | 26 | export type UpdateUserRequestFunction = RequestFunction< 27 | { params: UpdateUserRequestParameters; body: schemas.User }, 28 | void 29 | >; 30 | -------------------------------------------------------------------------------- /examples/react-query-petstore/src/api/operations/uploadFile.ts: -------------------------------------------------------------------------------- 1 | import type { RequestFunction } from "@openapi-io-ts/runtime"; 2 | import * as schemas from "../components/schemas"; 3 | 4 | export type UploadFileRequestParameters = { 5 | petId: number; 6 | additionalMetadata?: string; 7 | }; 8 | 9 | export const uploadFileOperation = { 10 | path: "/pet/{petId}/uploadImage", 11 | method: "post", 12 | responses: { "200": { _tag: "JsonResponse", decoder: schemas.ApiResponse } }, 13 | parameters: [ 14 | { 15 | _tag: "FormParameter", 16 | explode: false, 17 | in: "path", 18 | name: "petId", 19 | }, 20 | { 21 | _tag: "FormParameter", 22 | explode: true, 23 | in: "query", 24 | name: "additionalMetadata", 25 | }, 26 | ], 27 | requestDefaultHeaders: { Accept: "application/json" }, 28 | body: { 29 | _tag: "BinaryBody", 30 | mediaType: "application/octet-stream", 31 | }, 32 | } as const; 33 | 34 | export type UploadFileRequestFunction = RequestFunction< 35 | { params: UploadFileRequestParameters; body: Blob }, 36 | schemas.ApiResponse 37 | >; 38 | -------------------------------------------------------------------------------- /examples/react-query-petstore/src/api/servers.ts: -------------------------------------------------------------------------------- 1 | export const servers = ["/v3"]; 2 | -------------------------------------------------------------------------------- /examples/react-query-petstore/src/common/fetchRequestAdapter.ts: -------------------------------------------------------------------------------- 1 | import { HttpRequestAdapter } from "@openapi-io-ts/runtime"; 2 | import { servers } from "../api/servers"; 3 | 4 | export const fetchRequestAdapter: HttpRequestAdapter = (url, init) => 5 | fetch(`http://localhost:8080/api${servers[0]}${url}`, init); 6 | -------------------------------------------------------------------------------- /examples/react-query-petstore/src/common/react-query.ts: -------------------------------------------------------------------------------- 1 | import { TaskEither } from "fp-ts/TaskEither"; 2 | import { fold } from "fp-ts/Either"; 3 | import { ApiError, ApiResponse } from "@openapi-io-ts/runtime"; 4 | import { 5 | QueryKey, 6 | useMutation, 7 | UseMutationOptions, 8 | useQuery, 9 | } from "react-query"; 10 | import { failure } from "io-ts/PathReporter"; 11 | 12 | export function useOpenApiQuery( 13 | queryKey: TQueryKey, 14 | request: TaskEither> 15 | ) { 16 | return useQuery, ApiError>(queryKey, () => 17 | toPromise(request) 18 | ); 19 | } 20 | 21 | export function useOpenApiMutation< 22 | TData, 23 | TVariables = void, 24 | TContext = unknown 25 | >( 26 | mutationFn: ( 27 | variables: TVariables 28 | ) => TaskEither>, 29 | options?: UseMutationOptions, Error, TVariables, TContext> 30 | ) { 31 | return useMutation( 32 | (variables: TVariables) => toPromise(mutationFn(variables)), 33 | options 34 | ); 35 | } 36 | 37 | function toPromise( 38 | te: TaskEither> 39 | ): Promise> { 40 | return te().then( 41 | fold( 42 | (error) => { 43 | if (error._tag === "DecodeError") { 44 | console.error("Decode error", failure(error.errors)); 45 | } 46 | return Promise.reject(error); 47 | }, 48 | (resp) => Promise.resolve(resp) 49 | ) 50 | ); 51 | } 52 | -------------------------------------------------------------------------------- /examples/react-query-petstore/src/features/pet/AddPet.tsx: -------------------------------------------------------------------------------- 1 | import { Heading, VStack } from "@chakra-ui/react"; 2 | import React from "react"; 3 | import { Pet } from "../../api/components/schemas"; 4 | import { PetForm } from "./PetForm"; 5 | 6 | export function AddPet(): JSX.Element { 7 | const defaultPet: Pet = { 8 | photoUrls: [], 9 | status: "available", 10 | name: "", 11 | }; 12 | 13 | return ( 14 | 15 | Add a new pet 16 | 17 | 18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /examples/react-query-petstore/src/features/pet/EditPet.tsx: -------------------------------------------------------------------------------- 1 | import { Heading, Spinner, VStack } from "@chakra-ui/react"; 2 | import React from "react"; 3 | import { useParams } from "react-router"; 4 | import { useOpenApiQuery } from "../../common/react-query"; 5 | import { PetForm } from "./PetForm"; 6 | import { petService } from "./petService"; 7 | 8 | export function EditPet(): JSX.Element { 9 | const { petId } = useParams<{ petId: string }>(); 10 | 11 | const { isLoading, error, data } = useOpenApiQuery( 12 | ["pets", { id: petId }], 13 | petService.getPetById({ params: { petId: +petId } }) 14 | ); 15 | 16 | if (isLoading) { 17 | return ; 18 | } 19 | 20 | if (error) { 21 | return
{error._tag}
; 22 | } 23 | 24 | return ( 25 | 26 | Edit pet {data!.data.name} 27 | 28 | 29 | ); 30 | } 31 | -------------------------------------------------------------------------------- /examples/react-query-petstore/src/features/pet/PetForm.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Box, 3 | Button, 4 | FormControl, 5 | FormLabel, 6 | Input, 7 | VStack, 8 | } from "@chakra-ui/react"; 9 | import React from "react"; 10 | import { useForm } from "react-hook-form"; 11 | import { useHistory } from "react-router-dom"; 12 | import { Pet } from "../../api/components/schemas"; 13 | import { useOpenApiMutation } from "../../common/react-query"; 14 | import { petService } from "./petService"; 15 | 16 | export interface PetFormProps { 17 | pet: Pet; 18 | operation: "add" | "update"; 19 | } 20 | 21 | export function PetForm({ pet, operation }: PetFormProps): JSX.Element { 22 | const { register, handleSubmit } = useForm({ 23 | defaultValues: pet, 24 | }); 25 | 26 | const history = useHistory(); 27 | 28 | const mutation = useOpenApiMutation( 29 | (pet: Pet) => 30 | operation === "update" 31 | ? petService.updatePet({ body: pet }) 32 | : petService.addPet({ body: pet }), 33 | { 34 | onSuccess: () => { 35 | history.push("/pets"); 36 | }, 37 | } 38 | ); 39 | 40 | function onSubmit(pet: Pet) { 41 | mutation.mutate(pet); 42 | } 43 | 44 | return ( 45 |
46 | 47 | {operation === "add" && ( 48 | 49 | Id 50 | 51 | 52 | )} 53 | 54 | Name 55 | 56 | 57 | 58 | 68 | 71 | 72 | 73 |
74 | ); 75 | } 76 | -------------------------------------------------------------------------------- /examples/react-query-petstore/src/features/pet/PetsHome.tsx: -------------------------------------------------------------------------------- 1 | import { Flex, Heading, IconButton, Spacer, VStack } from "@chakra-ui/react"; 2 | import { AddIcon } from "@chakra-ui/icons"; 3 | import { useHistory, useRouteMatch } from "react-router-dom"; 4 | import { PetsTable } from "./PetsTable"; 5 | import React from "react"; 6 | 7 | export function PetsHome(): JSX.Element { 8 | const { url } = useRouteMatch(); 9 | const history = useHistory(); 10 | 11 | function onAddPet() { 12 | history.push(`${url}/addPet`); 13 | } 14 | 15 | return ( 16 | 17 | 18 | Pets 19 | 20 | } 22 | colorScheme="teal" 23 | aria-label="Add Pet" 24 | onClick={onAddPet} 25 | /> 26 | 27 | 28 | 29 | ); 30 | } 31 | -------------------------------------------------------------------------------- /examples/react-query-petstore/src/features/pet/PetsIndex.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Switch, Route, useRouteMatch } from "react-router-dom"; 3 | import { AddPet } from "./AddPet"; 4 | import { EditPet } from "./EditPet"; 5 | import { PetsHome } from "./PetsHome"; 6 | 7 | export function PetsIndex(): JSX.Element { 8 | const { path } = useRouteMatch(); 9 | 10 | return ( 11 | 12 | 13 | 14 | 15 | 16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /examples/react-query-petstore/src/features/pet/PetsTable.tsx: -------------------------------------------------------------------------------- 1 | import { EditIcon } from "@chakra-ui/icons"; 2 | import { 3 | Table, 4 | Tr, 5 | Td, 6 | Tbody, 7 | Thead, 8 | Th, 9 | Spinner, 10 | IconButton, 11 | } from "@chakra-ui/react"; 12 | import React from "react"; 13 | import { useHistory } from "react-router"; 14 | import { Pet } from "../../api/components/schemas/Pet"; 15 | import { useOpenApiQuery } from "../../common/react-query"; 16 | import { petService } from "./petService"; 17 | 18 | export interface PetsTableProps { 19 | status: Pet["status"]; 20 | } 21 | 22 | export function PetsTable({ status }: PetsTableProps): JSX.Element { 23 | const { isLoading, error, data } = useOpenApiQuery( 24 | ["pets", { status }], 25 | petService.findPetsByStatus({ params: { status } }) 26 | ); 27 | const history = useHistory(); 28 | 29 | function editPet(petId: number) { 30 | history.push(`/pets/${petId}`); 31 | } 32 | 33 | if (isLoading) { 34 | return ; 35 | } 36 | 37 | if (error) { 38 | return
{error._tag}
; 39 | } 40 | 41 | return ( 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | {data?.data.map((p) => ( 52 | 53 | 54 | 55 | 64 | 65 | ))} 66 | 67 |
IdNameActions
{p.id}{p.name} 56 | {p.id != null && ( 57 | } 59 | aria-label="Edit" 60 | onClick={() => editPet(p.id!)} 61 | /> 62 | )} 63 |
68 | ); 69 | } 70 | -------------------------------------------------------------------------------- /examples/react-query-petstore/src/features/pet/petService.ts: -------------------------------------------------------------------------------- 1 | import { 2 | petServiceBuilder, 3 | requestFunctionsBuilder, 4 | } from "../../api/operations"; 5 | import { fetchRequestAdapter } from "../../common/fetchRequestAdapter"; 6 | 7 | const requestFunctions = requestFunctionsBuilder(fetchRequestAdapter); 8 | export const petService = petServiceBuilder(requestFunctions); 9 | -------------------------------------------------------------------------------- /examples/react-query-petstore/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | openapi-io-ts petstore React example 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /examples/react-query-petstore/src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | import App from "./App"; 4 | 5 | ReactDOM.render( 6 | 7 | 8 | , 9 | document.getElementById("root") 10 | ); 11 | -------------------------------------------------------------------------------- /examples/react-query-petstore/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "target": "es5", 5 | "lib": ["dom", "dom.iterable", "esnext"], 6 | "allowJs": true, 7 | "skipLibCheck": true, 8 | "esModuleInterop": true, 9 | "allowSyntheticDefaultImports": true, 10 | "module": "esnext", 11 | "moduleResolution": "node", 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "noEmit": true, 15 | "jsx": "react", 16 | "noFallthroughCasesInSwitch": true 17 | }, 18 | "include": ["src"] 19 | } 20 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@openapi-io-ts/root", 3 | "private": true, 4 | "scripts": { 5 | "preinstall": "npx -y only-allow pnpm", 6 | "build": "pnpm run build -r", 7 | "test": "jest", 8 | "lint": "eslint . --ext .ts,.tsx", 9 | "examples:generate": "pnpm run generate --filter ./examples", 10 | "examples:type-check": "pnpm run type-check --filter ./examples", 11 | "sanity-check": "pnpm build && pnpm lint && pnpm test && pnpm examples:generate && pnpm examples:type-check", 12 | "release": "pnpm build && changeset publish" 13 | }, 14 | "devDependencies": { 15 | "@babel/core": "^7.16.10", 16 | "@babel/preset-env": "^7.16.11", 17 | "@babel/preset-typescript": "^7.16.7", 18 | "@changesets/changelog-github": "^0.4.2", 19 | "@changesets/cli": "^2.19.0", 20 | "@types/jest": "^27.4.0", 21 | "@types/node": "^17.0.10", 22 | "@typescript-eslint/eslint-plugin": "^5.10.0", 23 | "@typescript-eslint/parser": "^5.10.0", 24 | "babel-jest": "^27.4.6", 25 | "eslint": "^8.7.0", 26 | "eslint-config-prettier": "^8.1.0", 27 | "eslint-plugin-fp-ts": "^0.3.0", 28 | "eslint-plugin-prettier": "^4.0.0", 29 | "fp-ts": "^2.11.8", 30 | "husky": "^3.1.0", 31 | "io-ts": "^2.2.16", 32 | "jest": "^27.4.7", 33 | "lint-staged": "^10.5.4", 34 | "prettier": "^2.5.1", 35 | "ts-node": "^10.4.0", 36 | "tsup": "^5.11.11", 37 | "typescript": "^4.5.5" 38 | }, 39 | "husky": { 40 | "hooks": { 41 | "pre-commit": "lint-staged" 42 | } 43 | }, 44 | "lint-staged": { 45 | "*.ts": [ 46 | "eslint --fix" 47 | ], 48 | "*.{js,css,json,md,yml,yaml}": [ 49 | "prettier --write" 50 | ] 51 | }, 52 | "version": "0.1.0" 53 | } 54 | -------------------------------------------------------------------------------- /packages/cli/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @openapi-io-ts/cli 2 | 3 | ## 0.5.0 4 | 5 | ### Minor Changes 6 | 7 | - [#20](https://github.com/Fredx87/openapi-io-ts/pull/20) [`bde97b8`](https://github.com/Fredx87/openapi-io-ts/commit/bde97b88456eb5dd6b1d5024c613e61919529242) Thanks [@dcaf-mocha](https://github.com/dcaf-mocha)! - fix: add imports for requestBodies and responses 8 | 9 | ## 0.4.1 10 | 11 | ### Patch Changes 12 | 13 | - [#17](https://github.com/Fredx87/openapi-io-ts/pull/17) [`39494f5`](https://github.com/Fredx87/openapi-io-ts/commit/39494f5bc1949cc92b55f6ae294af6f27596d81b) Thanks [@Fredx87](https://github.com/Fredx87)! - Fixed generation for schema with nullable and allOf 14 | 15 | ## 0.4.0 16 | 17 | ### Minor Changes 18 | 19 | - [#16](https://github.com/Fredx87/openapi-io-ts/pull/16) [`aab6f0b`](https://github.com/Fredx87/openapi-io-ts/commit/aab6f0b0dc352f9ac501a9b114974fa098b3565b) Thanks [@Fredx87](https://github.com/Fredx87)! - Added support for nullable schemas 20 | 21 | ## 0.3.0 22 | 23 | ### Minor Changes 24 | 25 | - [#12](https://github.com/Fredx87/openapi-io-ts/pull/12) [`1df5450`](https://github.com/Fredx87/openapi-io-ts/commit/1df545029aef4853eb958cffb92cf9f7517acd02) Thanks [@Fredx87](https://github.com/Fredx87)! - Changed types of generated request functions. Simplified types in runtime package, removed operation types and 26 | added generation of request function types. 27 | 28 | ## 0.2.0 29 | 30 | ### Minor Changes 31 | 32 | - [#9](https://github.com/Fredx87/openapi-io-ts/pull/9) [`84d6bf1`](https://github.com/Fredx87/openapi-io-ts/commit/84d6bf1cc2cedc0f818fa3e88da71135ee94e58f) Thanks [@Fredx87](https://github.com/Fredx87)! - Changes in codegen for generating new request functions for runtime. 33 | 34 | **BREAKING CHANGE** 35 | The generated code will not have single request function anymore, but a single `requestFunctionsBuilder` function that creates all the request functions. 36 | The generated services builder will now take the object returned by the `requestFunctionsBuilder` function instead of an `HttpRequestAdapter`. 37 | 38 | ### Patch Changes 39 | 40 | - [#11](https://github.com/Fredx87/openapi-io-ts/pull/11) [`355b111`](https://github.com/Fredx87/openapi-io-ts/commit/355b111d83cb308428f09f8bb6231bf3126bcc2c) Thanks [@Fredx87](https://github.com/Fredx87)! - Fixed a typo in response component generation 41 | -------------------------------------------------------------------------------- /packages/cli/README.md: -------------------------------------------------------------------------------- 1 | # @openapi-io-ts/cli 2 | 3 | `openapi-io-ts` is a code generation tool capable of generating [io-ts](https://github.com/gcanti/io-ts) decoders from an [OpenAPI](https://www.openapis.org/) document. It can also generate the code needed to perform the request and decode/parse the response returned by the server. 4 | 5 | This package is the CLI used for code generation. See documentation at the project homepage. 6 | -------------------------------------------------------------------------------- /packages/cli/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@openapi-io-ts/cli", 3 | "version": "0.5.0", 4 | "description": "OpenAPI code generation tool with validation powered by io-ts", 5 | "keywords": [ 6 | "openapi", 7 | "io-ts", 8 | "codegen", 9 | "code generation", 10 | "swagger", 11 | "validation" 12 | ], 13 | "license": "MIT", 14 | "homepage": "https://github.com/Fredx87/openapi-io-ts", 15 | "repository": { 16 | "type": "git", 17 | "url": "https://github.com/Fredx87/openapi-io-ts.git", 18 | "directory": "packages/cli" 19 | }, 20 | "main": "dist/index.js", 21 | "module": "dist/esm/index.js", 22 | "types": "dist/index.d.ts", 23 | "files": [ 24 | "dist" 25 | ], 26 | "bin": { 27 | "openapi-io-ts": "dist/bin.js" 28 | }, 29 | "publishConfig": { 30 | "access": "public" 31 | }, 32 | "scripts": { 33 | "build": "tsup src/index.ts src/bin.ts --format esm,cjs --dts --clean --sourcemap --legacy-output", 34 | "start": "ts-node ./src/bin.ts -i ../../examples/react-query-petstore/petstore.yaml" 35 | }, 36 | "dependencies": { 37 | "@apidevtools/swagger-parser": "^10.0.2", 38 | "@openapi-io-ts/core": "workspace:*", 39 | "@types/node": "^17.0.10", 40 | "@types/yargs": "^16.0.0", 41 | "fp-ts": "^2.11.0", 42 | "immer": "^9.0.12", 43 | "io-ts": "^2.2.0", 44 | "io-ts-codegen": "^0.4.5", 45 | "openapi-types": "^7.2.3", 46 | "yargs": "^16.2.0" 47 | }, 48 | "peerDependencies": { 49 | "typescript": ">4.0.0" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /packages/cli/src/bin.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import * as yargs from "yargs"; 4 | import { main } from "."; 5 | 6 | const argv = yargs 7 | .usage("Usage: $0 [options]") 8 | .example( 9 | "$0 -i ./petstore.yaml", 10 | "Generates io-ts files from the schema given in input" 11 | ) 12 | .options({ 13 | input: { 14 | alias: "i", 15 | demandOption: true, 16 | description: "OpenAPI file to parse", 17 | }, 18 | output: { 19 | alias: "o", 20 | demandOption: true, 21 | description: "Output directory", 22 | default: "./out", 23 | }, 24 | }).argv; 25 | 26 | main(argv.input as string, argv.output); 27 | -------------------------------------------------------------------------------- /packages/cli/src/codegen/body.ts: -------------------------------------------------------------------------------- 1 | import { BodyItemOrRef, ParsedBody } from "../parser/body"; 2 | import { ParsedItem } from "../parser/common"; 3 | import * as gen from "io-ts-codegen"; 4 | import { 5 | generateSchemaIfDeclaration, 6 | getItemOrRefPrefix, 7 | getParsedItem, 8 | isParsedItem, 9 | } from "./common"; 10 | import { CodegenRTE } from "./context"; 11 | import * as RTE from "fp-ts/ReaderTaskEither"; 12 | import { pipe } from "fp-ts/function"; 13 | 14 | export function generateOperationBody(body: ParsedItem): string { 15 | switch (body.item._tag) { 16 | case "ParsedBinaryBody": { 17 | return `{ 18 | _tag: "BinaryBody", 19 | mediaType: "${body.item.mediaType}" 20 | }`; 21 | } 22 | case "ParsedFormBody": { 23 | return `{ 24 | _tag: "FormBody" 25 | }`; 26 | } 27 | case "ParsedMultipartBody": { 28 | return `{ 29 | _tag: "MultipartBody" 30 | }`; 31 | } 32 | case "ParsedJsonBody": { 33 | return `{ 34 | _tag: "JsonBody" 35 | }`; 36 | } 37 | case "ParsedTextBody": { 38 | return `{ 39 | _tag: "TextBody" 40 | }`; 41 | } 42 | } 43 | } 44 | 45 | export function generateOperationBodySchema(body: ParsedBody): string { 46 | if (body._tag === "ParsedBinaryBody" || body._tag === "ParsedTextBody") { 47 | return ""; 48 | } 49 | 50 | return generateSchemaIfDeclaration(body.type); 51 | } 52 | 53 | export function getBodyOrRefStaticType( 54 | itemOrRef: BodyItemOrRef 55 | ): CodegenRTE { 56 | if (isParsedItem(itemOrRef)) { 57 | return RTE.right(getBodyStaticType(itemOrRef.item)); 58 | } 59 | 60 | return pipe( 61 | getParsedItem(itemOrRef), 62 | RTE.map((body) => 63 | getBodyStaticType(body.item, getItemOrRefPrefix(itemOrRef)) 64 | ) 65 | ); 66 | } 67 | 68 | function getBodyStaticType(body: ParsedBody, prefix = ""): string { 69 | switch (body._tag) { 70 | case "ParsedBinaryBody": { 71 | return "Blob"; 72 | } 73 | case "ParsedTextBody": { 74 | return "string"; 75 | } 76 | case "ParsedFormBody": 77 | case "ParsedMultipartBody": 78 | case "ParsedJsonBody": { 79 | const { type } = body; 80 | 81 | if (type.kind === "TypeDeclaration") { 82 | return `${prefix}${type.name}`; 83 | } 84 | 85 | return gen.printStatic(type); 86 | } 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /packages/cli/src/codegen/common.ts: -------------------------------------------------------------------------------- 1 | import { pipe } from "fp-ts/function"; 2 | import * as RTE from "fp-ts/ReaderTaskEither"; 3 | import { TypeDeclaration, TypeReference } from "io-ts-codegen"; 4 | import { 5 | ComponentRef, 6 | ComponentRefItemType, 7 | ComponentType, 8 | ItemOrRef, 9 | } from "../parser/common"; 10 | import { CodegenContext, CodegenRTE } from "./context"; 11 | import { generateSchema } from "./schema"; 12 | 13 | export const SCHEMAS_PATH = "components/schemas"; 14 | export const PARAMETERS_PATH = "components/parameters"; 15 | export const RESPONSES_PATH = "components/responses"; 16 | export const REQUEST_BODIES_PATH = "components/requestBodies"; 17 | export const OPERATIONS_PATH = "operations"; 18 | export const SERVICES_PATH = "services"; 19 | export const RUNTIME_PACKAGE = "@openapi-io-ts/runtime"; 20 | 21 | export function getImports(): string { 22 | return `import * as t from "io-ts"; 23 | import * as schemas from "./"; 24 | import { DateFromISOString } from "io-ts-types/DateFromISOString"; 25 | `; 26 | } 27 | 28 | export function generateSchemaIfDeclaration( 29 | type: TypeDeclaration | TypeReference 30 | ): string { 31 | return type.kind === "TypeDeclaration" ? generateSchema(type) : ""; 32 | } 33 | 34 | export function writeGeneratedFile( 35 | path: string, 36 | fileName: string, 37 | content: string 38 | ): CodegenRTE { 39 | return pipe( 40 | RTE.asks((context: CodegenContext) => context.outputDir), 41 | RTE.bindTo("outputDir"), 42 | RTE.bind("writeFile", () => 43 | RTE.asks((context: CodegenContext) => context.writeFile) 44 | ), 45 | RTE.chainFirst(({ outputDir, writeFile }) => 46 | RTE.fromTaskEither(writeFile(`${outputDir}/${path}`, fileName, content)) 47 | ), 48 | RTE.map(() => void 0) 49 | ); 50 | } 51 | 52 | export function getParsedItem( 53 | itemOrRef: ItemOrRef 54 | ): CodegenRTE> { 55 | if (itemOrRef._tag === "ParsedItem") { 56 | return RTE.right(itemOrRef); 57 | } 58 | 59 | return RTE.asks( 60 | (context) => 61 | context.parserOutput.components[itemOrRef.componentType][ 62 | itemOrRef.pointer 63 | ] as ComponentRefItemType 64 | ); 65 | } 66 | 67 | export function isParsedItem( 68 | itemOrRef: ItemOrRef 69 | ): itemOrRef is ComponentRefItemType { 70 | return itemOrRef._tag === "ParsedItem"; 71 | } 72 | 73 | export function isComponentRef( 74 | itemOrRef: ItemOrRef 75 | ): itemOrRef is ComponentRef { 76 | return itemOrRef._tag === "ComponentRef"; 77 | } 78 | 79 | export function getItemOrRefPrefix( 80 | itemOrRef: ItemOrRef 81 | ): string { 82 | return isComponentRef(itemOrRef) ? `${itemOrRef.componentType}.` : ""; 83 | } 84 | -------------------------------------------------------------------------------- /packages/cli/src/codegen/components.ts: -------------------------------------------------------------------------------- 1 | import { pipe } from "fp-ts/function"; 2 | import * as RTE from "fp-ts/ReaderTaskEither"; 3 | import { TypeDeclaration } from "io-ts-codegen"; 4 | import { ParsedBody } from "../parser/body"; 5 | import { ParsedItem } from "../parser/common"; 6 | import { ParsedParameter } from "../parser/parameter"; 7 | import { ParsedResponse } from "../parser/response"; 8 | import { generateOperationBody, generateOperationBodySchema } from "./body"; 9 | import { 10 | generateSchemaIfDeclaration, 11 | getImports, 12 | PARAMETERS_PATH, 13 | REQUEST_BODIES_PATH, 14 | RESPONSES_PATH, 15 | RUNTIME_PACKAGE, 16 | SCHEMAS_PATH, 17 | writeGeneratedFile, 18 | } from "./common"; 19 | import { CodegenContext, CodegenRTE } from "./context"; 20 | import { generateOperationParameter } from "./parameter"; 21 | import { generateOperationResponse } from "./response"; 22 | import { generateSchema } from "./schema"; 23 | import * as gen from "io-ts-codegen"; 24 | 25 | export function generateComponents(): CodegenRTE { 26 | return pipe( 27 | RTE.asks((context: CodegenContext) => context.parserOutput.components), 28 | RTE.chain((components) => 29 | pipe( 30 | generateSchemas(Object.values(components.schemas)), 31 | RTE.chain(() => 32 | generateParameters(Object.values(components.parameters)) 33 | ), 34 | RTE.chain(() => generateResponses(Object.values(components.responses))), 35 | RTE.chain(() => 36 | generateRequestBodies(Object.values(components.requestBodies)) 37 | ) 38 | ) 39 | ) 40 | ); 41 | } 42 | 43 | function generateSchemas( 44 | schemas: ParsedItem[] 45 | ): CodegenRTE { 46 | return writeComponentsFiles(SCHEMAS_PATH, schemas, writeSchemaFile); 47 | } 48 | 49 | function writeSchemaFile( 50 | declaration: ParsedItem 51 | ): CodegenRTE { 52 | const content = `${getSchemaFileImports(declaration)} 53 | 54 | ${generateSchema(declaration.item).replace(/schemas./g, "")}`; 55 | 56 | return writeGeneratedFile(SCHEMAS_PATH, `${declaration.name}.ts`, content); 57 | } 58 | 59 | function getSchemaFileImports( 60 | declaration: ParsedItem 61 | ): string { 62 | const dependencies = gen.getNodeDependencies(declaration.item); 63 | const imports = dependencies.map((d) => { 64 | if (d.startsWith("schemas.")) { 65 | const schemaName = d.replace("schemas.", ""); 66 | return `import { ${schemaName} } from "./${schemaName}"`; 67 | } 68 | 69 | if (d === "DateFromISOString") { 70 | return `import { DateFromISOString } from "io-ts-types/DateFromISOString"`; 71 | } 72 | 73 | return ""; 74 | }); 75 | 76 | return `import * as t from "io-ts"; 77 | ${imports.join("\n")}`; 78 | } 79 | 80 | function generateParameters( 81 | parameters: ParsedItem[] 82 | ): CodegenRTE { 83 | return writeComponentsFiles(PARAMETERS_PATH, parameters, writeParameterFile); 84 | } 85 | 86 | function writeParameterFile( 87 | parameter: ParsedItem 88 | ): CodegenRTE { 89 | const content = `${getImports()} 90 | import { OperationParameter } from "${RUNTIME_PACKAGE}"; 91 | 92 | ${generateSchemaIfDeclaration(parameter.item.type)} 93 | 94 | export const ${ 95 | parameter.name 96 | }: OperationParameter = ${generateOperationParameter(parameter.item)}`; 97 | 98 | return writeGeneratedFile(PARAMETERS_PATH, `${parameter.name}.ts`, content); 99 | } 100 | 101 | function generateResponses( 102 | responses: ParsedItem[] 103 | ): CodegenRTE { 104 | return writeComponentsFiles(RESPONSES_PATH, responses, writeResponseFile); 105 | } 106 | 107 | function writeResponseFile( 108 | response: ParsedItem 109 | ): CodegenRTE { 110 | return pipe( 111 | generateOperationResponse(response), 112 | RTE.map((def) => `export const ${response.name} = ${def};`), 113 | RTE.map( 114 | (code) => `import * as t from "io-ts"; 115 | import * as schemas from "../schemas"; 116 | 117 | ${code}` 118 | ), 119 | RTE.chain((content) => 120 | writeGeneratedFile(RESPONSES_PATH, `${response.name}.ts`, content) 121 | ) 122 | ); 123 | } 124 | 125 | function generateRequestBodies( 126 | bodies: ParsedItem[] 127 | ): CodegenRTE { 128 | return writeComponentsFiles( 129 | REQUEST_BODIES_PATH, 130 | bodies, 131 | writeRequestBodyFile 132 | ); 133 | } 134 | 135 | function writeRequestBodyFile( 136 | parsedBody: ParsedItem 137 | ): CodegenRTE { 138 | return pipe( 139 | RTE.Do, 140 | RTE.bind("schema", () => 141 | RTE.right(generateOperationBodySchema(parsedBody.item)) 142 | ), 143 | RTE.bind("body", () => RTE.right(generateOperationBody(parsedBody))), 144 | RTE.map( 145 | ({ schema, body }) => `import { OperationBody } from "${RUNTIME_PACKAGE}"; 146 | import * as t from "io-ts"; 147 | import * as schemas from "../schemas"; 148 | ${schema} 149 | 150 | export const ${parsedBody.name}: OperationBody = ${body}` 151 | ), 152 | RTE.chain((content) => 153 | writeGeneratedFile(REQUEST_BODIES_PATH, `${parsedBody.name}.ts`, content) 154 | ) 155 | ); 156 | } 157 | 158 | function writeComponentsFiles( 159 | indexPath: string, 160 | components: ParsedItem[], 161 | writeFile: (item: ParsedItem) => CodegenRTE 162 | ): CodegenRTE { 163 | return pipe( 164 | components, 165 | RTE.traverseSeqArray((component) => 166 | pipe( 167 | writeFile(component), 168 | RTE.map(() => component.name) 169 | ) 170 | ), 171 | RTE.chain((names) => writeIndex(indexPath, names)) 172 | ); 173 | } 174 | 175 | function writeIndex(path: string, names: readonly string[]): CodegenRTE { 176 | if (names.length === 0) { 177 | return RTE.right(void 0); 178 | } 179 | 180 | const content = names.map((n) => `export * from "./${n}";`).join("\n"); 181 | return writeGeneratedFile(path, "index.ts", content); 182 | } 183 | -------------------------------------------------------------------------------- /packages/cli/src/codegen/context.ts: -------------------------------------------------------------------------------- 1 | import * as RTE from "fp-ts/ReaderTaskEither"; 2 | import * as TE from "fp-ts/TaskEither"; 3 | import { Environment } from "../environment"; 4 | import { ParserOutput } from "../parser/parserOutput"; 5 | 6 | export interface CodegenContext extends Environment { 7 | writeFile: ( 8 | path: string, 9 | fileName: string, 10 | content: string 11 | ) => TE.TaskEither; 12 | createDir: (dirName: string) => TE.TaskEither; 13 | parserOutput: ParserOutput; 14 | } 15 | 16 | export type CodegenRTE = RTE.ReaderTaskEither; 17 | -------------------------------------------------------------------------------- /packages/cli/src/codegen/format.ts: -------------------------------------------------------------------------------- 1 | import * as ts from "typescript"; 2 | import * as E from "fp-ts/Either"; 3 | 4 | export function formatFile( 5 | fileName: string, 6 | content: string 7 | ): E.Either { 8 | return E.tryCatch(() => { 9 | const languageServiceHost = new LanguageServiceHost(); 10 | languageServiceHost.setFileContent(fileName, content); 11 | 12 | const languageService = ts.createLanguageService(languageServiceHost); 13 | organizeImports(fileName, languageService, languageServiceHost); 14 | formatContent(fileName, languageService, languageServiceHost); 15 | return languageServiceHost.getFileContent(fileName) ?? content; 16 | }, E.toError); 17 | } 18 | 19 | function organizeImports( 20 | fileName: string, 21 | languageService: ts.LanguageService, 22 | languageServiceHost: LanguageServiceHost 23 | ): void { 24 | const content = languageServiceHost.getFileContent(fileName); 25 | 26 | if (content) { 27 | const fileTextChanges = languageService.organizeImports( 28 | { 29 | type: "file", 30 | fileName, 31 | }, 32 | {}, 33 | undefined 34 | ); 35 | languageServiceHost.setFileContent( 36 | fileName, 37 | applyTextChanges(content, fileTextChanges[0].textChanges) 38 | ); 39 | } 40 | } 41 | 42 | function formatContent( 43 | fileName: string, 44 | languageService: ts.LanguageService, 45 | languageServiceHost: LanguageServiceHost 46 | ): void { 47 | const content = languageServiceHost.getFileContent(fileName); 48 | 49 | if (content) { 50 | const textChanges = languageService.getFormattingEditsForDocument( 51 | fileName, 52 | ts.getDefaultFormatCodeSettings() 53 | ); 54 | 55 | languageServiceHost.setFileContent( 56 | fileName, 57 | applyTextChanges(content, textChanges) 58 | ); 59 | } 60 | } 61 | 62 | function applyTextChanges( 63 | input: string, 64 | changes: readonly ts.TextChange[] 65 | ): string { 66 | let res = input; 67 | 68 | [...changes].reverse().forEach((c) => { 69 | const head = res.slice(0, c.span.start); 70 | const tail = res.slice(c.span.start + c.span.length); 71 | res = `${head}${c.newText}${tail}`; 72 | }); 73 | 74 | return res; 75 | } 76 | 77 | class LanguageServiceHost implements ts.LanguageServiceHost { 78 | private files = new Map(); 79 | 80 | setFileContent(fileName: string, content: string): void { 81 | this.files.set(fileName, content); 82 | } 83 | 84 | getFileContent(fileName: string): string | undefined { 85 | return this.files.get(fileName); 86 | } 87 | 88 | getCompilationSettings(): ts.CompilerOptions { 89 | return ts.getDefaultCompilerOptions(); 90 | } 91 | 92 | getScriptFileNames(): string[] { 93 | return Array.from(this.files.keys()); 94 | } 95 | 96 | getScriptVersion(): string { 97 | return "0"; 98 | } 99 | 100 | getScriptSnapshot(fileName: string): ts.IScriptSnapshot | undefined { 101 | const content = this.getFileContent(fileName); 102 | return content ? ts.ScriptSnapshot.fromString(content) : undefined; 103 | } 104 | 105 | getCurrentDirectory(): string { 106 | return ""; 107 | } 108 | 109 | getDefaultLibFileName(options: ts.CompilerOptions): string { 110 | return ts.getDefaultLibFilePath(options); 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /packages/cli/src/codegen/fs.ts: -------------------------------------------------------------------------------- 1 | import * as TE from "fp-ts/TaskEither"; 2 | import * as E from "fp-ts/Either"; 3 | import * as fs from "fs"; 4 | import { pipe } from "fp-ts/function"; 5 | import * as util from "util"; 6 | import { formatFile } from "./format"; 7 | 8 | export function createDir(path: string): TE.TaskEither { 9 | return pipe( 10 | TE.tryCatch( 11 | () => util.promisify(fs.mkdir)(path, { recursive: true }), 12 | (e) => new Error(`Cannot create directory "${path}". Error: ${String(e)}`) 13 | ), 14 | TE.map(() => void 0) 15 | ); 16 | } 17 | 18 | export function writeFile( 19 | path: string, 20 | name: string, 21 | content: string 22 | ): TE.TaskEither { 23 | const fileName = `${path}/${name}`; 24 | 25 | return pipe( 26 | formatFile(fileName, content), 27 | E.getOrElse(() => content), 28 | TE.right, 29 | TE.chain((formattedContent) => 30 | TE.tryCatch( 31 | () => util.promisify(fs.writeFile)(fileName, formattedContent), 32 | (e) => new Error(`Cannot save file "${fileName}". Error: ${String(e)}`) 33 | ) 34 | ) 35 | ); 36 | } 37 | -------------------------------------------------------------------------------- /packages/cli/src/codegen/index.ts: -------------------------------------------------------------------------------- 1 | import { pipe } from "fp-ts/function"; 2 | import * as R from "fp-ts/Reader"; 3 | import { Environment, ProgramRTE } from "../environment"; 4 | import { ParserOutput } from "../parser/parserOutput"; 5 | import { CodegenContext } from "./context"; 6 | import { createDir, writeFile } from "./fs"; 7 | import { main } from "./main"; 8 | 9 | export function codegen(parserOutput: ParserOutput): ProgramRTE { 10 | return pipe( 11 | main(), 12 | R.local( 13 | (env: Environment): CodegenContext => ({ 14 | ...env, 15 | writeFile, 16 | createDir, 17 | parserOutput, 18 | }) 19 | ) 20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /packages/cli/src/codegen/main.ts: -------------------------------------------------------------------------------- 1 | import { pipe } from "fp-ts/function"; 2 | import * as RTE from "fp-ts/ReaderTaskEither"; 3 | import { 4 | SCHEMAS_PATH, 5 | PARAMETERS_PATH, 6 | OPERATIONS_PATH, 7 | SERVICES_PATH, 8 | RESPONSES_PATH, 9 | REQUEST_BODIES_PATH, 10 | } from "./common"; 11 | import { generateComponents } from "./components"; 12 | import { CodegenRTE, CodegenContext } from "./context"; 13 | import { generateOperations } from "./operations"; 14 | import { generateServers } from "./servers"; 15 | import { generateOperationsIndex } from "./operationsIndex"; 16 | 17 | export function main(): CodegenRTE { 18 | return pipe( 19 | createDirs(), 20 | RTE.chain(() => generateComponents()), 21 | RTE.chain(() => generateOperations()), 22 | RTE.chain(() => generateOperationsIndex()), 23 | RTE.chain(() => generateServers()) 24 | ); 25 | } 26 | 27 | function createDirs(): CodegenRTE { 28 | const paths = [ 29 | SCHEMAS_PATH, 30 | PARAMETERS_PATH, 31 | RESPONSES_PATH, 32 | REQUEST_BODIES_PATH, 33 | OPERATIONS_PATH, 34 | SERVICES_PATH, 35 | ]; 36 | 37 | return pipe( 38 | RTE.ask(), 39 | RTE.chain(({ createDir, outputDir }) => 40 | pipe( 41 | paths, 42 | RTE.traverseSeqArray((path) => 43 | RTE.fromTaskEither(createDir(`${outputDir}/${path}`)) 44 | ) 45 | ) 46 | ), 47 | RTE.map(() => void 0) 48 | ); 49 | } 50 | -------------------------------------------------------------------------------- /packages/cli/src/codegen/operations.ts: -------------------------------------------------------------------------------- 1 | import { pipe } from "fp-ts/function"; 2 | import * as A from "fp-ts/Array"; 3 | import * as O from "fp-ts/Option"; 4 | import * as RTE from "fp-ts/ReaderTaskEither"; 5 | import * as R from "fp-ts/Record"; 6 | import * as gen from "io-ts-codegen"; 7 | import { capitalize } from "../utils"; 8 | import { BodyItemOrRef } from "../parser/body"; 9 | import { ParsedOperation } from "../parser/operation"; 10 | import { 11 | generateSchemaIfDeclaration, 12 | getParsedItem, 13 | isParsedItem, 14 | OPERATIONS_PATH, 15 | PARAMETERS_PATH, 16 | RUNTIME_PACKAGE, 17 | SCHEMAS_PATH, 18 | writeGeneratedFile, 19 | getItemOrRefPrefix, 20 | REQUEST_BODIES_PATH, 21 | RESPONSES_PATH, 22 | } from "./common"; 23 | import { CodegenContext, CodegenRTE } from "./context"; 24 | import { generateOperationParameter } from "./parameter"; 25 | import { ParameterItemOrRef } from "../parser/parameter"; 26 | import { generateOperationResponses } from "./response"; 27 | import { ParsedJsonResponse, ResponseItemOrRef } from "../parser/response"; 28 | import { 29 | generateOperationBody, 30 | generateOperationBodySchema, 31 | getBodyOrRefStaticType, 32 | } from "./body"; 33 | import { 34 | FORM_ENCODED_MEDIA_TYPE, 35 | JSON_MEDIA_TYPE, 36 | TEXT_PLAIN_MEDIA_TYPE, 37 | } from "@openapi-io-ts/core"; 38 | import { ParsedItem } from "../parser/common"; 39 | 40 | export function generateOperations(): CodegenRTE { 41 | return pipe( 42 | RTE.asks((context: CodegenContext) => context.parserOutput.operations), 43 | RTE.bindTo("operations"), 44 | RTE.chainFirst(({ operations }) => 45 | R.traverseWithIndex(RTE.ApplicativeSeq)(generateOperation)(operations) 46 | ), 47 | RTE.map(() => void 0) 48 | ); 49 | } 50 | 51 | export function requestBuilderName(operationId: string): string { 52 | return `${capitalize(operationId, "camel")}Builder`; 53 | } 54 | 55 | export function requestFunctionName(operationId: string): string { 56 | return `${capitalize(operationId, "pascal")}RequestFunction`; 57 | } 58 | 59 | export function operationName(operationId: string): string { 60 | return `${capitalize(operationId, "camel")}Operation`; 61 | } 62 | 63 | interface GeneratedOperationParameters { 64 | schemas: string; 65 | definition: string; 66 | requestMap: string; 67 | } 68 | 69 | interface GeneratedBody { 70 | operationBody: string; 71 | requestBody: string; 72 | schema?: string; 73 | } 74 | 75 | interface GeneratedResponses { 76 | schemas: string; 77 | operationResponses: string; 78 | } 79 | 80 | interface GeneratedItems { 81 | parameters?: GeneratedOperationParameters; 82 | body?: GeneratedBody; 83 | responses: GeneratedResponses; 84 | defaultHeaders: string; 85 | returnType: string; 86 | } 87 | 88 | function generateOperation( 89 | operationId: string, 90 | operation: ParsedOperation 91 | ): CodegenRTE { 92 | return pipe( 93 | generateItems(operationId, operation), 94 | RTE.chain((items) => generateFileContent(operationId, operation, items)), 95 | RTE.chain((content) => 96 | writeGeneratedFile(OPERATIONS_PATH, `${operationId}.ts`, content) 97 | ) 98 | ); 99 | } 100 | 101 | function generateItems( 102 | operationId: string, 103 | operation: ParsedOperation 104 | ): CodegenRTE { 105 | return pipe( 106 | RTE.Do, 107 | RTE.bind("parameters", () => 108 | generateOperationParameters(operationId, operation.parameters) 109 | ), 110 | RTE.bind("body", () => generateBody(operation.body)), 111 | RTE.bind("responses", () => generateResponses(operation.responses)), 112 | RTE.bind("returnType", () => getReturnType(operation.responses)), 113 | RTE.bind("defaultHeaders", () => getDefaultHeaders(operation)) 114 | ); 115 | } 116 | 117 | function generateFileContent( 118 | operationId: string, 119 | operation: ParsedOperation, 120 | items: GeneratedItems 121 | ): CodegenRTE { 122 | const content = `import * as t from "io-ts"; 123 | import type { RequestFunction } from "${RUNTIME_PACKAGE}"; 124 | import * as schemas from "../${SCHEMAS_PATH}"; 125 | import * as parameters from "../${PARAMETERS_PATH}"; 126 | import * as responses from "../${RESPONSES_PATH}"; 127 | import * as requestBodies from "../${REQUEST_BODIES_PATH}"; 128 | 129 | ${ 130 | items.parameters 131 | ? `${items.parameters.schemas} 132 | ${items.parameters.requestMap}` 133 | : "" 134 | } 135 | 136 | ${items.body?.schema ?? ""} 137 | 138 | ${items.responses.schemas} 139 | 140 | ${generateOperationObject(operationId, operation, items)} 141 | 142 | ${generateRequestFunctionType(operationId, items)} 143 | `; 144 | 145 | return RTE.right(content); 146 | } 147 | 148 | function generateOperationParameters( 149 | operationId: string, 150 | parameters: ParameterItemOrRef[] 151 | ): CodegenRTE { 152 | if (parameters.length === 0) { 153 | return RTE.right(undefined); 154 | } 155 | 156 | return pipe( 157 | RTE.Do, 158 | RTE.bind("schemas", () => generateOperationParametersSchemas(parameters)), 159 | RTE.bind("definition", () => 160 | generateOperationParametersDefinition(parameters) 161 | ), 162 | RTE.bind("requestMap", () => 163 | generateRequestParametersMap(operationId, parameters) 164 | ) 165 | ); 166 | } 167 | 168 | function generateOperationParametersSchemas( 169 | parameters: ParameterItemOrRef[] 170 | ): CodegenRTE { 171 | const schemas = pipe( 172 | parameters, 173 | A.filter(isParsedItem), 174 | A.map((parameter) => generateSchemaIfDeclaration(parameter.item.type)) 175 | ); 176 | 177 | return RTE.right(schemas.join("\n")); 178 | } 179 | 180 | function generateOperationParametersDefinition( 181 | parameters: ParameterItemOrRef[] 182 | ): CodegenRTE { 183 | return pipe( 184 | parameters, 185 | RTE.traverseSeqArray(generateOperationParameterDefinition), 186 | RTE.map((defs) => `[ ${defs.join(",")} ]`) 187 | ); 188 | } 189 | 190 | function generateOperationParameterDefinition( 191 | parameter: ParameterItemOrRef 192 | ): CodegenRTE { 193 | return pipe( 194 | getParsedItem(parameter), 195 | RTE.map((item) => { 196 | if (parameter._tag === "ComponentRef") { 197 | return `${parameter.componentType}.${item.name}`; 198 | } else { 199 | return generateOperationParameter(item.item); 200 | } 201 | }) 202 | ); 203 | } 204 | 205 | function generateRequestParametersMap( 206 | operationId: string, 207 | parameters: ParameterItemOrRef[] 208 | ): CodegenRTE { 209 | const mapName = requestParametersMapName(operationId); 210 | 211 | return pipe( 212 | parameters, 213 | RTE.traverseSeqArray(generateRequestParameter), 214 | RTE.map( 215 | (ps) => `export type ${mapName} = { 216 | ${ps.join("\n")} 217 | }` 218 | ) 219 | ); 220 | } 221 | 222 | function generateRequestParameter( 223 | itemOrRef: ParameterItemOrRef 224 | ): CodegenRTE { 225 | return pipe( 226 | getParsedItem(itemOrRef), 227 | RTE.map((parameter) => { 228 | const modifier = parameter.item.required ? "" : "?"; 229 | 230 | const staticType = 231 | parameter.item.type.kind === "TypeDeclaration" 232 | ? `${getItemOrRefPrefix(parameter)}${parameter.name}` 233 | : gen.printStatic(parameter.item.type); 234 | 235 | return `${parameter.name}${modifier}: ${staticType};`; 236 | }) 237 | ); 238 | } 239 | 240 | function generateOperationObject( 241 | operationId: string, 242 | operation: ParsedOperation, 243 | generatedItems: GeneratedItems 244 | ): string { 245 | const { path, method } = operation; 246 | 247 | return `export const ${operationName(operationId)} = { 248 | path: "${path}", 249 | method: "${method}", 250 | responses: ${generatedItems.responses.operationResponses}, 251 | parameters: ${generatedItems.parameters?.definition ?? "[]"}, 252 | requestDefaultHeaders: ${generatedItems.defaultHeaders}, 253 | ${generatedItems.body ? `body: ${generatedItems.body.operationBody}` : ""} 254 | } as const`; 255 | } 256 | 257 | function generateBody( 258 | itemOrRef: O.Option 259 | ): CodegenRTE { 260 | if (O.isNone(itemOrRef)) { 261 | return RTE.right(undefined); 262 | } 263 | 264 | return pipe( 265 | RTE.Do, 266 | RTE.bind("body", () => getParsedItem(itemOrRef.value)), 267 | RTE.bind("requestBody", () => getBodyOrRefStaticType(itemOrRef.value)), 268 | RTE.map(({ body, requestBody }) => { 269 | const res: GeneratedBody = { 270 | operationBody: isParsedItem(itemOrRef.value) 271 | ? generateOperationBody(body) 272 | : `${getItemOrRefPrefix(itemOrRef.value)}${body.name}`, 273 | requestBody, 274 | schema: isParsedItem(itemOrRef.value) 275 | ? generateOperationBodySchema(itemOrRef.value.item) 276 | : undefined, 277 | }; 278 | 279 | return res; 280 | }) 281 | ); 282 | } 283 | 284 | function generateResponses( 285 | responses: Record 286 | ): CodegenRTE { 287 | return pipe( 288 | RTE.Do, 289 | RTE.bind("schemas", () => generateResponsesSchemas(responses)), 290 | RTE.bind("operationResponses", () => generateOperationResponses(responses)) 291 | ); 292 | } 293 | 294 | function generateResponsesSchemas( 295 | responses: Record 296 | ): CodegenRTE { 297 | return RTE.right( 298 | Object.values(responses) 299 | .filter(isParsedJsonResponse) 300 | .map((r) => generateSchemaIfDeclaration(r.item.type)) 301 | .join("\n") 302 | ); 303 | } 304 | 305 | function isParsedJsonResponse( 306 | response: ResponseItemOrRef 307 | ): response is ParsedItem { 308 | return isParsedItem(response) && response.item._tag === "ParsedJsonResponse"; 309 | } 310 | 311 | function generateRequestFunctionType( 312 | operationId: string, 313 | generatedItems: GeneratedItems 314 | ): string { 315 | const { parameters, body } = generatedItems; 316 | 317 | const argsArray: string[] = []; 318 | 319 | if (parameters != null) { 320 | argsArray.push(`params: ${requestParametersMapName(operationId)}`); 321 | } 322 | 323 | if (body != null) { 324 | argsArray.push(`body: ${body.requestBody}`); 325 | } 326 | 327 | const args = 328 | argsArray.length > 0 ? `{ ${argsArray.join("; ")} }` : "undefined"; 329 | 330 | return `export type ${requestFunctionName( 331 | operationId 332 | )} = RequestFunction<${args}, ${generatedItems.returnType}>`; 333 | } 334 | 335 | function getReturnType( 336 | responses: Record 337 | ): CodegenRTE { 338 | const successfulResponse = getSuccessfulResponse(responses); 339 | 340 | if (successfulResponse == null) { 341 | return RTE.right("void"); 342 | } 343 | 344 | return pipe( 345 | getParsedItem(successfulResponse), 346 | RTE.map((response) => { 347 | if (response.item._tag === "ParsedEmptyResponse") { 348 | return "void"; 349 | } 350 | 351 | if (response.item._tag === "ParsedFileResponse") { 352 | return "Blob"; 353 | } 354 | 355 | const { type } = response.item; 356 | 357 | const staticType = 358 | type.kind === "TypeDeclaration" 359 | ? `${getItemOrRefPrefix(response)}${type.name}` 360 | : gen.printStatic(type); 361 | 362 | return staticType; 363 | }) 364 | ); 365 | } 366 | 367 | function getDefaultHeaders(operation: ParsedOperation): CodegenRTE { 368 | return pipe( 369 | RTE.Do, 370 | RTE.bind("contentType", () => getContentTypeHeader(operation)), 371 | RTE.bind("accept", () => getAcceptHeader(operation)), 372 | RTE.map(({ contentType, accept }) => { 373 | const headers: Record = {}; 374 | 375 | if (contentType) { 376 | headers["Content-Type"] = contentType; 377 | } 378 | 379 | if (accept) { 380 | headers["Accept"] = accept; 381 | } 382 | 383 | return JSON.stringify(headers); 384 | }) 385 | ); 386 | } 387 | 388 | function getContentTypeHeader( 389 | operation: ParsedOperation 390 | ): CodegenRTE { 391 | return pipe( 392 | operation.body, 393 | O.fold( 394 | () => RTE.right(undefined), 395 | (itemOrRef) => 396 | pipe( 397 | getParsedItem(itemOrRef), 398 | RTE.map((body) => { 399 | switch (body.item._tag) { 400 | case "ParsedBinaryBody": { 401 | return undefined; 402 | } 403 | case "ParsedFormBody": { 404 | return FORM_ENCODED_MEDIA_TYPE; 405 | } 406 | case "ParsedMultipartBody": { 407 | return undefined; 408 | } 409 | case "ParsedJsonBody": { 410 | return JSON_MEDIA_TYPE; 411 | } 412 | case "ParsedTextBody": { 413 | return TEXT_PLAIN_MEDIA_TYPE; 414 | } 415 | } 416 | }) 417 | ) 418 | ) 419 | ); 420 | } 421 | 422 | function getAcceptHeader( 423 | operation: ParsedOperation 424 | ): CodegenRTE { 425 | const successfulResponse = getSuccessfulResponse(operation.responses); 426 | 427 | if (successfulResponse == null) { 428 | return RTE.right(undefined); 429 | } 430 | 431 | return pipe( 432 | getParsedItem(successfulResponse), 433 | RTE.map((response) => 434 | response.item._tag === "ParsedJsonResponse" ? JSON_MEDIA_TYPE : undefined 435 | ) 436 | ); 437 | } 438 | 439 | function getSuccessfulResponse( 440 | responses: Record 441 | ): ResponseItemOrRef | undefined { 442 | const successfulCode = Object.keys(responses).find((c) => c.startsWith("2")); 443 | 444 | return successfulCode ? responses[successfulCode] : undefined; 445 | } 446 | 447 | function requestParametersMapName(operationId: string): string { 448 | return `${capitalize(operationId, "pascal")}RequestParameters`; 449 | } 450 | -------------------------------------------------------------------------------- /packages/cli/src/codegen/operationsIndex.ts: -------------------------------------------------------------------------------- 1 | import { pipe } from "fp-ts/function"; 2 | import * as RTE from "fp-ts/ReaderTaskEither"; 3 | import * as R from "fp-ts/Record"; 4 | import { capitalize, CapitalizeCasing } from "../utils"; 5 | import { OPERATIONS_PATH, RUNTIME_PACKAGE, writeGeneratedFile } from "./common"; 6 | import { CodegenContext, CodegenRTE } from "./context"; 7 | import { operationName, requestFunctionName } from "./operations"; 8 | 9 | const OPERATIONS_OBJECT_NAME = "operations"; 10 | const REQUEST_FUNCTIONS_MAP = "OperationRequestFunctionMap"; 11 | 12 | export function generateOperationsIndex(): CodegenRTE { 13 | return pipe( 14 | RTE.asks((context: CodegenContext) => context.parserOutput), 15 | RTE.map(({ operations, tags }) => 16 | generateIndexContent(Object.keys(operations), tags) 17 | ), 18 | RTE.chain((content) => 19 | writeGeneratedFile(OPERATIONS_PATH, `index.ts`, content) 20 | ) 21 | ); 22 | } 23 | 24 | function generateIndexContent( 25 | operationIds: string[], 26 | tags: Record 27 | ): string { 28 | const imports = generateImports(operationIds); 29 | const requestFunctionsBuilder = generateRequestFunctionsBuilder(operationIds); 30 | const tagsOperation = generateTagsOperations(tags); 31 | 32 | return `${imports} 33 | 34 | ${requestFunctionsBuilder} 35 | 36 | ${tagsOperation} 37 | `; 38 | } 39 | 40 | function generateImports(operationIds: string[]): string { 41 | const runtimeImports = ` 42 | import { 43 | HttpRequestAdapter, 44 | requestFunctionBuilder, 45 | RequestFunctionsMap, 46 | } from "${RUNTIME_PACKAGE}";`; 47 | 48 | const operationsImport = operationIds 49 | .map( 50 | (id) => 51 | `import { ${operationName(id)}, ${requestFunctionName( 52 | id 53 | )} } from "./${id}"` 54 | ) 55 | .join("\n"); 56 | 57 | return `${runtimeImports} 58 | ${operationsImport}`; 59 | } 60 | 61 | function generateRequestFunctionsBuilder(operationIds: string[]): string { 62 | return ` 63 | export const ${OPERATIONS_OBJECT_NAME} = { 64 | ${operationIds.map((id) => `${id}: ${operationName(id)}, `).join("\n")} 65 | } as const; 66 | 67 | export interface ${REQUEST_FUNCTIONS_MAP} { 68 | ${operationIds 69 | .map((id) => `${id}: ${requestFunctionName(id)}; `) 70 | .join("\n")} 71 | } 72 | 73 | export const requestFunctionsBuilder = ( 74 | requestAdapter: HttpRequestAdapter 75 | ): ${REQUEST_FUNCTIONS_MAP} => ({ 76 | ${operationIds 77 | .map( 78 | (id) => 79 | `${id}: requestFunctionBuilder(${OPERATIONS_OBJECT_NAME}.${id}, requestAdapter),` 80 | ) 81 | .join("\n")} 82 | })`; 83 | } 84 | 85 | function generateTagsOperations(tags: Record): string { 86 | const generatedOperations = pipe(tags, R.mapWithIndex(generateTagOperations)); 87 | return Object.values(generatedOperations).join("\n"); 88 | } 89 | 90 | function generateTagOperations(tag: string, operationIds: string[]): string { 91 | return ` 92 | export const ${tagServiceBuilderName(tag)} = ( 93 | requestFunctions: ${REQUEST_FUNCTIONS_MAP} 94 | ) => ({ 95 | ${operationIds.map((id) => `${id}: requestFunctions.${id},`).join("\n")} 96 | }); 97 | `; 98 | } 99 | 100 | function tagServiceBuilderName(tag: string): string { 101 | return `${tagName(tag)}ServiceBuilder`; 102 | } 103 | 104 | function tagName(tag: string, casing: CapitalizeCasing = "camel"): string { 105 | return `${capitalize(tag, casing)}`; 106 | } 107 | -------------------------------------------------------------------------------- /packages/cli/src/codegen/parameter.ts: -------------------------------------------------------------------------------- 1 | import { ParsedParameter } from "../parser/parameter"; 2 | 3 | export function generateOperationParameter(parameter: ParsedParameter): string { 4 | const baseParameter = ` in: "${parameter.in}", 5 | name: "${parameter.name}" 6 | `; 7 | 8 | switch (parameter._tag) { 9 | case "ParsedJsonParameter": { 10 | return `{ 11 | _tag: "JsonParameter", 12 | ${baseParameter} 13 | }`; 14 | } 15 | case "ParsedFormParameter": { 16 | return ` { 17 | _tag: "FormParameter", 18 | explode: ${parameter.explode ? "true" : "false"}, 19 | ${baseParameter} 20 | }`; 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /packages/cli/src/codegen/response.ts: -------------------------------------------------------------------------------- 1 | import { pipe } from "fp-ts/function"; 2 | import { ResponseItemOrRef } from "../parser/response"; 3 | import { getItemOrRefPrefix, getParsedItem } from "./common"; 4 | import { CodegenRTE } from "./context"; 5 | import * as RTE from "fp-ts/ReaderTaskEither"; 6 | import * as R from "fp-ts/Record"; 7 | import * as gen from "io-ts-codegen"; 8 | 9 | export function generateOperationResponses( 10 | responses: Record 11 | ): CodegenRTE { 12 | return pipe( 13 | responses, 14 | R.traverseWithIndex(RTE.ApplicativeSeq)((_, itemOrRef) => 15 | generateOperationResponse(itemOrRef) 16 | ), 17 | RTE.map((responses) => { 18 | const items = Object.entries(responses) 19 | .map(([code, response]) => `"${code}": ${response}`) 20 | .join(",\n"); 21 | return `{ ${items} }`; 22 | }) 23 | ); 24 | } 25 | 26 | export function generateOperationResponse( 27 | itemOrRef: ResponseItemOrRef 28 | ): CodegenRTE { 29 | return pipe( 30 | getParsedItem(itemOrRef), 31 | RTE.map((response) => { 32 | if (response.item._tag === "ParsedEmptyResponse") { 33 | return `{ _tag: "EmptyResponse" }`; 34 | } 35 | 36 | if (response.item._tag === "ParsedFileResponse") { 37 | return `{ _tag: "FileResponse" }`; 38 | } 39 | 40 | const { type } = response.item; 41 | 42 | const runtimeType = 43 | type.kind === "TypeDeclaration" 44 | ? `${getItemOrRefPrefix(response)}${type.name}` 45 | : gen.printRuntime(type); 46 | 47 | return `{ _tag: "JsonResponse", decoder: ${runtimeType}}`; 48 | }) 49 | ); 50 | } 51 | -------------------------------------------------------------------------------- /packages/cli/src/codegen/schema.ts: -------------------------------------------------------------------------------- 1 | import { printRuntime, printStatic, TypeDeclaration } from "io-ts-codegen"; 2 | 3 | export function generateSchema(declaration: TypeDeclaration): string { 4 | return `${printRuntime(declaration)} 5 | 6 | ${printStatic(declaration)}`; 7 | } 8 | -------------------------------------------------------------------------------- /packages/cli/src/codegen/servers.ts: -------------------------------------------------------------------------------- 1 | import { pipe } from "fp-ts/function"; 2 | import * as RTE from "fp-ts/ReaderTaskEither"; 3 | import { ParsedServer } from "../parser/server"; 4 | import { writeGeneratedFile } from "./common"; 5 | import { CodegenContext, CodegenRTE } from "./context"; 6 | 7 | export function generateServers(): CodegenRTE { 8 | return pipe( 9 | RTE.asks((context: CodegenContext) => context.parserOutput.servers), 10 | RTE.chain(generateServerFile) 11 | ); 12 | } 13 | 14 | function generateServerFile(servers: ParsedServer[]): CodegenRTE { 15 | const content = `export const servers = [ ${servers 16 | .map((s) => `"${s.url}"`) 17 | .join(", ")} ]`; 18 | 19 | return writeGeneratedFile("", "servers.ts", content); 20 | } 21 | -------------------------------------------------------------------------------- /packages/cli/src/environment.ts: -------------------------------------------------------------------------------- 1 | import * as RTE from "fp-ts/ReaderTaskEither"; 2 | 3 | export interface Environment { 4 | inputFile: string; 5 | outputDir: string; 6 | } 7 | 8 | export type ProgramRTE = RTE.ReaderTaskEither; 9 | -------------------------------------------------------------------------------- /packages/cli/src/index.ts: -------------------------------------------------------------------------------- 1 | import { error, log } from "fp-ts/Console"; 2 | import { pipe } from "fp-ts/function"; 3 | import * as RTE from "fp-ts/ReaderTaskEither"; 4 | import * as T from "fp-ts/Task"; 5 | import * as TE from "fp-ts/TaskEither"; 6 | import { codegen } from "./codegen"; 7 | import { Environment } from "./environment"; 8 | import { parse } from "./parser"; 9 | 10 | export function main(inputFile: string, outputDir: string): void { 11 | const env: Environment = { 12 | inputFile, 13 | outputDir, 14 | }; 15 | 16 | const program = pipe(parse(), RTE.chain(codegen)); 17 | 18 | // eslint-disable-next-line @typescript-eslint/no-floating-promises 19 | pipe(program(env), TE.fold(onLeft, onRight))(); 20 | } 21 | 22 | function onLeft(e: Error): T.Task { 23 | return T.fromIO(error(e)); 24 | } 25 | 26 | function onRight(): T.Task { 27 | return T.fromIO(log("Files generated successfully!")); 28 | } 29 | -------------------------------------------------------------------------------- /packages/cli/src/parser/JSONReference.ts: -------------------------------------------------------------------------------- 1 | import * as E from "fp-ts/Either"; 2 | import { pipe } from "fp-ts/function"; 3 | import * as RNEA from "fp-ts/ReadonlyNonEmptyArray"; 4 | import * as t from "io-ts"; 5 | 6 | export class JsonPointer { 7 | constructor(public tokens: RNEA.ReadonlyNonEmptyArray) {} 8 | 9 | concat(tokens: RNEA.ReadonlyNonEmptyArray): JsonPointer { 10 | return new JsonPointer(RNEA.concat(this.tokens, tokens)); 11 | } 12 | 13 | toString(): string { 14 | return this.tokens.map(jsonPointerTokenEncode).join("/"); 15 | } 16 | } 17 | 18 | export const JsonReference = t.type({ 19 | $ref: t.string, 20 | }); 21 | 22 | export function createJsonPointer( 23 | pointer: string 24 | ): E.Either { 25 | return pipe( 26 | RNEA.fromArray(pointer.split("/")), 27 | E.fromOption( 28 | () => new Error(`Cannot create JsonPointer for pointer "${pointer}"`) 29 | ), 30 | E.map((tokens) => { 31 | const decoded = RNEA.map(jsonPointerTokenDecode)(tokens); 32 | return new JsonPointer(decoded); 33 | }) 34 | ); 35 | } 36 | 37 | function jsonPointerTokenEncode(token: string): string { 38 | return token.replace(/~/g, "~0").replace(/\//g, "~1"); 39 | } 40 | 41 | function jsonPointerTokenDecode(token: string): string { 42 | return token.replace(/~1/g, "/").replace(/~0/g, "~"); 43 | } 44 | -------------------------------------------------------------------------------- /packages/cli/src/parser/__tests__/schema.ts: -------------------------------------------------------------------------------- 1 | import { OpenAPIV3 } from "openapi-types"; 2 | import { parseSchema } from "../schema"; 3 | import * as gen from "io-ts-codegen"; 4 | import * as E from "fp-ts/Either"; 5 | 6 | describe("OpenAPI schema", () => { 7 | it("parses empty schema", () => { 8 | const schema: OpenAPIV3.SchemaObject = {}; 9 | 10 | const result = parseSchema(schema); 11 | const expected = gen.unknownType; 12 | 13 | expect(result).toEqual(E.right(expected)); 14 | }); 15 | 16 | it("parses basic string schema", () => { 17 | const schema: OpenAPIV3.SchemaObject = { 18 | type: "string", 19 | }; 20 | 21 | const result = parseSchema(schema); 22 | const expected = gen.stringType; 23 | 24 | expect(result).toEqual(E.right(expected)); 25 | }); 26 | 27 | it("parses number schema", () => { 28 | const schema: OpenAPIV3.SchemaObject = { 29 | type: "number", 30 | }; 31 | 32 | const result = parseSchema(schema); 33 | const expected = gen.numberType; 34 | 35 | expect(result).toEqual(E.right(expected)); 36 | }); 37 | 38 | it("parses integer schema", () => { 39 | const schema: OpenAPIV3.SchemaObject = { 40 | type: "integer", 41 | }; 42 | 43 | const result = parseSchema(schema); 44 | const expected = gen.numberType; 45 | 46 | expect(result).toEqual(E.right(expected)); 47 | }); 48 | 49 | it("parses boolean schema", () => { 50 | const schema: OpenAPIV3.SchemaObject = { 51 | type: "boolean", 52 | }; 53 | 54 | const result = parseSchema(schema); 55 | const expected = gen.booleanType; 56 | 57 | expect(result).toEqual(E.right(expected)); 58 | }); 59 | 60 | it("parses basic array schema", () => { 61 | const schema: OpenAPIV3.SchemaObject = { 62 | type: "array", 63 | items: { 64 | type: "string", 65 | }, 66 | }; 67 | 68 | const result = parseSchema(schema); 69 | const expected = gen.arrayCombinator(gen.stringType); 70 | 71 | expect(result).toEqual(E.right(expected)); 72 | }); 73 | 74 | it("parses empty array schema", () => { 75 | const schema: OpenAPIV3.SchemaObject = { 76 | type: "array", 77 | items: {}, 78 | }; 79 | 80 | const result = parseSchema(schema); 81 | const expected = gen.arrayCombinator(gen.unknownType); 82 | 83 | expect(result).toEqual(E.right(expected)); 84 | }); 85 | 86 | it("parses basic object schema", () => { 87 | const schema: OpenAPIV3.SchemaObject = { 88 | type: "object", 89 | properties: { 90 | name: { 91 | type: "string", 92 | }, 93 | age: { 94 | type: "number", 95 | }, 96 | }, 97 | required: ["name"], 98 | }; 99 | 100 | const result = parseSchema(schema); 101 | const expected = gen.typeCombinator([ 102 | gen.property("name", gen.stringType, false), 103 | gen.property("age", gen.numberType, true), 104 | ]); 105 | 106 | expect(result).toEqual(E.right(expected)); 107 | }); 108 | 109 | it("parses empty object schema", () => { 110 | const schema: OpenAPIV3.SchemaObject = { 111 | type: "object", 112 | }; 113 | 114 | const result = parseSchema(schema); 115 | const expected = gen.unknownRecordType; 116 | 117 | expect(result).toEqual(E.right(expected)); 118 | }); 119 | 120 | it("parses string schema with date format", () => { 121 | const schema: OpenAPIV3.SchemaObject = { 122 | type: "string", 123 | format: "date", 124 | }; 125 | 126 | const result = parseSchema(schema); 127 | const expected = gen.customCombinator("Date", "DateFromISOString", [ 128 | "DateFromISOString", 129 | ]); 130 | 131 | expect(result).toEqual(E.right(expected)); 132 | }); 133 | 134 | it("parses string schema with date-time format", () => { 135 | const schema: OpenAPIV3.SchemaObject = { 136 | type: "string", 137 | format: "date-time", 138 | }; 139 | 140 | const result = parseSchema(schema); 141 | const expected = gen.customCombinator("Date", "DateFromISOString", [ 142 | "DateFromISOString", 143 | ]); 144 | 145 | expect(result).toEqual(E.right(expected)); 146 | }); 147 | 148 | it("parses string schema with multiple enums", () => { 149 | const schema: OpenAPIV3.SchemaObject = { 150 | type: "string", 151 | enum: ["foo", "bar", "baz"], 152 | }; 153 | 154 | const result = parseSchema(schema); 155 | const expected = gen.unionCombinator([ 156 | gen.literalCombinator("foo"), 157 | gen.literalCombinator("bar"), 158 | gen.literalCombinator("baz"), 159 | ]); 160 | 161 | expect(result).toEqual(E.right(expected)); 162 | }); 163 | 164 | it("parses string schema with single enum", () => { 165 | const schema: OpenAPIV3.SchemaObject = { 166 | type: "string", 167 | enum: ["foo"], 168 | }; 169 | 170 | const result = parseSchema(schema); 171 | const expected = gen.literalCombinator("foo"); 172 | 173 | expect(result).toEqual(E.right(expected)); 174 | }); 175 | 176 | it("parses allOf schema", () => { 177 | const schema: OpenAPIV3.SchemaObject = { 178 | allOf: [{ type: "string" }, { type: "number" }], 179 | }; 180 | 181 | const result = parseSchema(schema); 182 | const expected = gen.intersectionCombinator([ 183 | gen.stringType, 184 | gen.numberType, 185 | ]); 186 | 187 | expect(result).toEqual(E.right(expected)); 188 | }); 189 | 190 | it("parses oneOf schema", () => { 191 | const schema: OpenAPIV3.SchemaObject = { 192 | oneOf: [{ type: "string" }, { type: "number" }], 193 | readOnly: false, 194 | }; 195 | 196 | const result = parseSchema(schema); 197 | const expected = gen.unionCombinator([gen.stringType, gen.numberType]); 198 | 199 | expect(result).toEqual(E.right(expected)); 200 | }); 201 | 202 | it("parses anyOf schema", () => { 203 | const schema: OpenAPIV3.SchemaObject = { 204 | anyOf: [{ type: "string" }, { type: "number" }], 205 | readOnly: false, 206 | }; 207 | 208 | const result = parseSchema(schema); 209 | const expected = gen.unionCombinator([gen.stringType, gen.numberType]); 210 | 211 | expect(result).toEqual(E.right(expected)); 212 | }); 213 | 214 | it("parses a nullable schema", () => { 215 | const schema: OpenAPIV3.SchemaObject = { 216 | type: "string", 217 | nullable: true, 218 | }; 219 | 220 | const result = parseSchema(schema); 221 | const expected = gen.unionCombinator([gen.stringType, gen.nullType]); 222 | 223 | expect(result).toEqual(E.right(expected)); 224 | }); 225 | 226 | it("parses a nullable schema with allOf", () => { 227 | const schema: OpenAPIV3.SchemaObject = { 228 | nullable: true, 229 | allOf: [ 230 | { 231 | type: "array", 232 | items: { type: "string" }, 233 | }, 234 | ], 235 | }; 236 | 237 | const result = parseSchema(schema); 238 | const expected = gen.unionCombinator([ 239 | gen.arrayCombinator(gen.stringType), 240 | gen.nullType, 241 | ]); 242 | 243 | expect(result).toEqual(E.right(expected)); 244 | }); 245 | }); 246 | -------------------------------------------------------------------------------- /packages/cli/src/parser/body.ts: -------------------------------------------------------------------------------- 1 | import { pipe } from "fp-ts/function"; 2 | import * as RTE from "fp-ts/ReaderTaskEither"; 3 | import * as gen from "io-ts-codegen"; 4 | import { OpenAPIV3 } from "openapi-types"; 5 | import { JsonReference } from "./JSONReference"; 6 | import { 7 | createComponentRef, 8 | getOrCreateType, 9 | parsedItem, 10 | ParsedItem, 11 | ComponentRef, 12 | } from "./common"; 13 | import { 14 | JSON_MEDIA_TYPE, 15 | TEXT_PLAIN_MEDIA_TYPE, 16 | FORM_ENCODED_MEDIA_TYPE, 17 | MULTIPART_FORM_MEDIA_TYPE, 18 | } from "@openapi-io-ts/core"; 19 | import { ParserRTE } from "./context"; 20 | 21 | interface BaseParsedBody { 22 | required: boolean; 23 | } 24 | 25 | export interface ParsedBinaryBody extends BaseParsedBody { 26 | _tag: "ParsedBinaryBody"; 27 | mediaType: string; 28 | } 29 | 30 | export interface ParsedFormBody extends BaseParsedBody { 31 | _tag: "ParsedFormBody"; 32 | type: gen.TypeDeclaration | gen.TypeReference; 33 | } 34 | 35 | export interface ParsedMultipartBody extends BaseParsedBody { 36 | _tag: "ParsedMultipartBody"; 37 | type: gen.TypeDeclaration | gen.TypeReference; 38 | } 39 | 40 | export interface ParsedJsonBody extends BaseParsedBody { 41 | _tag: "ParsedJsonBody"; 42 | type: gen.TypeDeclaration | gen.TypeReference; 43 | } 44 | 45 | export interface ParsedTextBody extends BaseParsedBody { 46 | _tag: "ParsedTextBody"; 47 | } 48 | 49 | export type ParsedBody = 50 | | ParsedBinaryBody 51 | | ParsedFormBody 52 | | ParsedMultipartBody 53 | | ParsedJsonBody 54 | | ParsedTextBody; 55 | 56 | export type BodyItemOrRef = 57 | | ParsedItem 58 | | ComponentRef<"requestBodies">; 59 | 60 | export function parseBody( 61 | name: string, 62 | body: OpenAPIV3.ReferenceObject | OpenAPIV3.RequestBodyObject 63 | ): ParserRTE { 64 | if (JsonReference.is(body)) { 65 | return RTE.fromEither(createComponentRef("requestBodies", body.$ref)); 66 | } 67 | 68 | return parseBodyObject(name, body); 69 | } 70 | 71 | export function parseBodyObject( 72 | name: string, 73 | body: OpenAPIV3.RequestBodyObject 74 | ): ParserRTE> { 75 | const { content } = body; 76 | const required = body.required ?? false; 77 | 78 | const jsonContent = content?.[JSON_MEDIA_TYPE]; 79 | 80 | if (jsonContent) { 81 | return pipe( 82 | getOrCreateTypeFromOptional(name, jsonContent.schema), 83 | RTE.map((type) => { 84 | const parsedBody: ParsedJsonBody = { 85 | _tag: "ParsedJsonBody", 86 | type, 87 | required, 88 | }; 89 | return parsedItem(parsedBody, name); 90 | }) 91 | ); 92 | } 93 | 94 | const textPlainContent = content?.[TEXT_PLAIN_MEDIA_TYPE]; 95 | 96 | if (textPlainContent) { 97 | const parsedBody: ParsedTextBody = { 98 | _tag: "ParsedTextBody", 99 | required, 100 | }; 101 | return RTE.right(parsedItem(parsedBody, name)); 102 | } 103 | 104 | const formEncodedContent = content?.[FORM_ENCODED_MEDIA_TYPE]; 105 | 106 | if (formEncodedContent) { 107 | return pipe( 108 | getOrCreateTypeFromOptional(name, formEncodedContent.schema), 109 | RTE.map((type) => { 110 | const parsedBody: ParsedFormBody = { 111 | _tag: "ParsedFormBody", 112 | type, 113 | required, 114 | }; 115 | return parsedItem(parsedBody, name); 116 | }) 117 | ); 118 | } 119 | 120 | const multipartFormContent = content?.[MULTIPART_FORM_MEDIA_TYPE]; 121 | 122 | if (multipartFormContent) { 123 | return pipe( 124 | getOrCreateTypeFromOptional(name, multipartFormContent.schema), 125 | RTE.map((type) => { 126 | const parsedBody: ParsedMultipartBody = { 127 | _tag: "ParsedMultipartBody", 128 | type, 129 | required, 130 | }; 131 | return parsedItem(parsedBody, name); 132 | }) 133 | ); 134 | } 135 | 136 | const mediaTypes = Object.keys(content); 137 | const mediaType = mediaTypes.length > 0 ? mediaTypes[0] : "*/*"; 138 | 139 | const parsedBody: ParsedBinaryBody = { 140 | _tag: "ParsedBinaryBody", 141 | mediaType, 142 | required, 143 | }; 144 | 145 | return RTE.right(parsedItem(parsedBody, name)); 146 | } 147 | 148 | function getOrCreateTypeFromOptional( 149 | name: string, 150 | schema: OpenAPIV3.ReferenceObject | OpenAPIV3.SchemaObject | undefined 151 | ): ParserRTE { 152 | if (schema == null) { 153 | return RTE.right(gen.unknownType); 154 | } 155 | return getOrCreateType(name, schema); 156 | } 157 | -------------------------------------------------------------------------------- /packages/cli/src/parser/common.ts: -------------------------------------------------------------------------------- 1 | import { pipe } from "fp-ts/function"; 2 | import * as E from "fp-ts/Either"; 3 | import * as RTE from "fp-ts/ReaderTaskEither"; 4 | import * as gen from "io-ts-codegen"; 5 | import { OpenAPIV3 } from "openapi-types"; 6 | import { createJsonPointer, JsonPointer, JsonReference } from "./JSONReference"; 7 | import { ParsedBody } from "./body"; 8 | import { ParserRTE, readParserOutput } from "./context"; 9 | import { ParsedParameter } from "./parameter"; 10 | import { ParsedResponse } from "./response"; 11 | import { parseSchema } from "./schema"; 12 | 13 | export interface ParsedItem { 14 | _tag: "ParsedItem"; 15 | name: string; 16 | item: T; 17 | } 18 | 19 | export function parsedItem(item: T, name: string): ParsedItem { 20 | return { 21 | _tag: "ParsedItem", 22 | name, 23 | item, 24 | }; 25 | } 26 | 27 | export interface ParsedComponents { 28 | schemas: Record>; 29 | parameters: Record>; 30 | responses: Record>; 31 | requestBodies: Record>; 32 | } 33 | 34 | export type ComponentType = keyof ParsedComponents; 35 | 36 | export interface ComponentRef { 37 | _tag: "ComponentRef"; 38 | componentType: T; 39 | pointer: string; 40 | } 41 | 42 | function componentRef( 43 | componentType: T, 44 | pointer: string 45 | ): ComponentRef { 46 | return { 47 | _tag: "ComponentRef", 48 | componentType, 49 | pointer, 50 | }; 51 | } 52 | 53 | export type ComponentRefItemType = 54 | ParsedComponents[C][string]; 55 | 56 | export type ItemOrRef = 57 | | ComponentRefItemType 58 | | ComponentRef; 59 | 60 | export function checkValidReference( 61 | componentType: ComponentType, 62 | pointer: JsonPointer 63 | ): E.Either { 64 | const { tokens } = pointer; 65 | 66 | if ( 67 | tokens.length === 4 && 68 | tokens[1] === "components" && 69 | tokens[2] === componentType 70 | ) { 71 | return E.right(pointer); 72 | } 73 | 74 | return E.left( 75 | new Error( 76 | `Cannot parse a reference to a ${componentType} not in '#/components/${componentType}'. Reference: ${pointer.toString()}` 77 | ) 78 | ); 79 | } 80 | 81 | export function createComponentRef( 82 | componentType: T, 83 | pointer: string 84 | ): E.Either> { 85 | return pipe( 86 | createJsonPointer(pointer), 87 | E.chain((jsonPointer) => checkValidReference(componentType, jsonPointer)), 88 | E.map((jsonPointer) => componentRef(componentType, jsonPointer.toString())) 89 | ); 90 | } 91 | 92 | function getComponent( 93 | componentType: T, 94 | pointer: string 95 | ): ParserRTE { 96 | return pipe( 97 | readParserOutput(), 98 | RTE.map((output) => output.components[componentType][pointer]), 99 | RTE.chain((component) => 100 | component 101 | ? RTE.right(component as ParsedComponents[T][string]) 102 | : RTE.left( 103 | new Error( 104 | `Cannot get component name for componentType ${componentType}, pointer ${pointer}` 105 | ) 106 | ) 107 | ) 108 | ); 109 | } 110 | 111 | export function getOrCreateType( 112 | name: string, 113 | schema: OpenAPIV3.ReferenceObject | OpenAPIV3.SchemaObject 114 | ): ParserRTE { 115 | if (JsonReference.is(schema)) { 116 | return pipe( 117 | getComponent("schemas", schema.$ref), 118 | RTE.map((component) => 119 | gen.customCombinator( 120 | `schemas.${component.name}`, 121 | `schemas.${component.name}` 122 | ) 123 | ) 124 | ); 125 | } 126 | 127 | return pipe( 128 | parseSchema(schema), 129 | RTE.fromEither, 130 | RTE.map((type) => createDeclarationOrReturnType(`${name}Schema`, type)) 131 | ); 132 | } 133 | 134 | function createDeclarationOrReturnType( 135 | name: string, 136 | type: gen.TypeReference 137 | ): gen.TypeDeclaration | gen.TypeReference { 138 | return shouldCreateDeclaration(type) 139 | ? gen.typeDeclaration(name, type, true) 140 | : type; 141 | } 142 | 143 | function shouldCreateDeclaration(type: gen.TypeReference): boolean { 144 | switch (type.kind) { 145 | case "ArrayCombinator": 146 | return shouldCreateDeclaration(type.type); 147 | case "IntersectionCombinator": 148 | case "UnionCombinator": 149 | case "TupleCombinator": 150 | return type.types.some(shouldCreateDeclaration); 151 | case "InterfaceCombinator": 152 | case "TaggedUnionCombinator": 153 | return true; 154 | default: 155 | return false; 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /packages/cli/src/parser/components.ts: -------------------------------------------------------------------------------- 1 | import * as E from "fp-ts/Either"; 2 | import { pipe } from "fp-ts/function"; 3 | import * as RTE from "fp-ts/ReaderTaskEither"; 4 | import * as gen from "io-ts-codegen"; 5 | import { OpenAPIV3 } from "openapi-types"; 6 | import { JsonPointer, JsonReference } from "./JSONReference"; 7 | import { toValidVariableName } from "../utils"; 8 | import { parseBodyObject } from "./body"; 9 | import { parsedItem, ParsedItem } from "./common"; 10 | import { modifyParserOutput, ParserContext, ParserRTE } from "./context"; 11 | import { parseParameterObject } from "./parameter"; 12 | import { parseResponseObject } from "./response"; 13 | import { parseSchema } from "./schema"; 14 | 15 | export function parseAllComponents(): ParserRTE { 16 | return pipe( 17 | RTE.asks((context: ParserContext) => context.document.components), 18 | RTE.chain((components) => { 19 | const tasks: ParserRTE[] = components 20 | ? createTasks(components) 21 | : []; 22 | return pipe( 23 | RTE.sequenceSeqArray(tasks), 24 | RTE.map(() => void 0) 25 | ); 26 | }) 27 | ); 28 | } 29 | 30 | function createTasks( 31 | components: OpenAPIV3.ComponentsObject 32 | ): ParserRTE[] { 33 | const pointer = new JsonPointer(["#", "components"]); 34 | 35 | const tasks: ParserRTE[] = []; 36 | 37 | const { schemas, parameters, requestBodies, responses } = components; 38 | 39 | if (schemas) { 40 | tasks.push(parseAllSchemas(pointer, schemas)); 41 | } 42 | 43 | if (parameters) { 44 | tasks.push(parseAllParameters(pointer, parameters)); 45 | } 46 | 47 | if (requestBodies) { 48 | tasks.push(parseAllBodies(pointer, requestBodies)); 49 | } 50 | 51 | if (responses) { 52 | tasks.push(parseAllResponses(pointer, responses)); 53 | } 54 | 55 | return tasks; 56 | } 57 | 58 | function parseAllSchemas( 59 | componentsPointer: JsonPointer, 60 | schemas: Record 61 | ): ParserRTE { 62 | return pipe( 63 | Object.entries(schemas), 64 | RTE.traverseSeqArray(([name, schema]) => { 65 | const pointer = componentsPointer.concat(["schemas", name]); 66 | const generatedName = toValidVariableName(name, "pascal"); 67 | 68 | return pipe( 69 | createSchemaComponent(generatedName, schema), 70 | RTE.fromEither, 71 | RTE.chain((component) => 72 | modifyParserOutput((draft) => { 73 | draft.components.schemas[pointer.toString()] = component; 74 | }) 75 | ) 76 | ); 77 | }), 78 | RTE.map(() => void 0) 79 | ); 80 | } 81 | 82 | function createSchemaComponent( 83 | name: string, 84 | schema: OpenAPIV3.ReferenceObject | OpenAPIV3.SchemaObject 85 | ): E.Either> { 86 | return pipe( 87 | parseSchema(schema), 88 | E.map((type) => parsedItem(gen.typeDeclaration(name, type, true), name)) 89 | ); 90 | } 91 | 92 | function parseAllParameters( 93 | componentsPointer: JsonPointer, 94 | parameters: Record< 95 | string, 96 | OpenAPIV3.ReferenceObject | OpenAPIV3.ParameterObject 97 | > 98 | ): ParserRTE { 99 | const pointer = componentsPointer.concat(["parameters"]); 100 | 101 | return pipe( 102 | Object.entries(parameters), 103 | RTE.traverseSeqArray(([name, param]) => 104 | parseParameterComponent(pointer, name, param) 105 | ), 106 | RTE.map(() => void 0) 107 | ); 108 | } 109 | 110 | function parseParameterComponent( 111 | parametersPointer: JsonPointer, 112 | name: string, 113 | parameter: OpenAPIV3.ReferenceObject | OpenAPIV3.ParameterObject 114 | ): ParserRTE { 115 | if (JsonReference.is(parameter)) { 116 | return RTE.left(new Error("Found $ref in components/parameter")); 117 | } 118 | 119 | const pointer = parametersPointer.concat([name]); 120 | const generatedName = toValidVariableName(name, "camel"); 121 | 122 | return pipe( 123 | parseParameterObject(generatedName, parameter), 124 | RTE.map((res) => parsedItem(res.item, generatedName)), 125 | RTE.chain((item) => 126 | modifyParserOutput((draft) => { 127 | draft.components.parameters[pointer.toString()] = item; 128 | }) 129 | ) 130 | ); 131 | } 132 | 133 | function parseAllBodies( 134 | componentsPointer: JsonPointer, 135 | bodies: Record< 136 | string, 137 | OpenAPIV3.ReferenceObject | OpenAPIV3.RequestBodyObject 138 | > 139 | ): ParserRTE { 140 | const pointer = componentsPointer.concat(["requestBodies"]); 141 | 142 | return pipe( 143 | Object.entries(bodies), 144 | RTE.traverseSeqArray(([name, body]) => 145 | parseBodyComponent(pointer, name, body) 146 | ), 147 | RTE.map(() => void 0) 148 | ); 149 | } 150 | 151 | function parseBodyComponent( 152 | bodiesPointer: JsonPointer, 153 | name: string, 154 | body: OpenAPIV3.ReferenceObject | OpenAPIV3.RequestBodyObject 155 | ): ParserRTE { 156 | if (JsonReference.is(body)) { 157 | return RTE.left(new Error("Found $ref in components/requestBodies")); 158 | } 159 | 160 | const pointer = bodiesPointer.concat([name]); 161 | const generatedName = toValidVariableName(name, "pascal"); 162 | 163 | return pipe( 164 | parseBodyObject(generatedName, body), 165 | RTE.map((res) => parsedItem(res.item, generatedName)), 166 | RTE.chain((item) => 167 | modifyParserOutput((draft) => { 168 | draft.components.requestBodies[pointer.toString()] = item; 169 | }) 170 | ) 171 | ); 172 | } 173 | 174 | function parseAllResponses( 175 | componentsPointer: JsonPointer, 176 | responses: Record< 177 | string, 178 | OpenAPIV3.ReferenceObject | OpenAPIV3.ResponseObject 179 | > 180 | ): ParserRTE { 181 | const pointer = componentsPointer.concat(["responses"]); 182 | 183 | return pipe( 184 | Object.entries(responses), 185 | RTE.traverseSeqArray(([name, response]) => 186 | parseResponseComponent(pointer, name, response) 187 | ), 188 | RTE.map(() => void 0) 189 | ); 190 | } 191 | 192 | function parseResponseComponent( 193 | responsesPointer: JsonPointer, 194 | name: string, 195 | response: OpenAPIV3.ReferenceObject | OpenAPIV3.ResponseObject 196 | ): ParserRTE { 197 | if (JsonReference.is(response)) { 198 | return RTE.left(new Error("Found $ref in components/responses")); 199 | } 200 | 201 | const pointer = responsesPointer.concat([name]); 202 | const generatedName = toValidVariableName(name, "camel"); 203 | 204 | return pipe( 205 | parseResponseObject(generatedName, response), 206 | RTE.map((res) => parsedItem(res.item, generatedName)), 207 | RTE.chain((item) => 208 | modifyParserOutput((draft) => { 209 | draft.components.responses[pointer.toString()] = item; 210 | }) 211 | ) 212 | ); 213 | } 214 | -------------------------------------------------------------------------------- /packages/cli/src/parser/context.ts: -------------------------------------------------------------------------------- 1 | import { IORef } from "fp-ts/IORef"; 2 | import { pipe } from "fp-ts/function"; 3 | import * as RTE from "fp-ts/ReaderTaskEither"; 4 | import produce, { Draft } from "immer"; 5 | import { OpenAPIV3 } from "openapi-types"; 6 | import { ParserOutput } from "./parserOutput"; 7 | 8 | export interface ParserContext { 9 | document: OpenAPIV3.Document; 10 | outputRef: IORef; 11 | } 12 | 13 | export type ParserRTE = RTE.ReaderTaskEither; 14 | 15 | export function readParserOutput(): ParserRTE { 16 | return pipe( 17 | RTE.asks((e: ParserContext) => e.outputRef), 18 | RTE.chain((ref) => RTE.rightIO(ref.read)) 19 | ); 20 | } 21 | 22 | export function modifyParserOutput( 23 | recipe: (draft: Draft) => void 24 | ): ParserRTE { 25 | return pipe( 26 | RTE.asks((e: ParserContext) => e.outputRef), 27 | RTE.chain((ref) => 28 | RTE.fromIO(ref.modify((output) => produce(output, recipe))) 29 | ) 30 | ); 31 | } 32 | -------------------------------------------------------------------------------- /packages/cli/src/parser/index.ts: -------------------------------------------------------------------------------- 1 | import { pipe } from "fp-ts/function"; 2 | import { newIORef } from "fp-ts/IORef"; 3 | import * as RTE from "fp-ts/ReaderTaskEither"; 4 | import { Environment, ProgramRTE } from "../environment"; 5 | import { ParserContext } from "./context"; 6 | import { main } from "./main"; 7 | import { parseDocument } from "./parseDocument"; 8 | import { ParserOutput, parserOutput } from "./parserOutput"; 9 | 10 | export function parse(): ProgramRTE { 11 | return pipe( 12 | createParserContext(), 13 | RTE.chain((context) => RTE.fromTaskEither(main()(context))) 14 | ); 15 | } 16 | 17 | function createParserContext(): ProgramRTE { 18 | return pipe( 19 | RTE.ask(), 20 | RTE.bindTo("env"), 21 | RTE.bind("document", ({ env }) => 22 | RTE.fromTaskEither(parseDocument(env.inputFile)) 23 | ), 24 | RTE.bind("outputRef", () => RTE.rightIO(newIORef(parserOutput()))), 25 | RTE.map(({ document, outputRef }) => ({ document, outputRef })) 26 | ); 27 | } 28 | -------------------------------------------------------------------------------- /packages/cli/src/parser/main.ts: -------------------------------------------------------------------------------- 1 | import { pipe } from "fp-ts/function"; 2 | import * as RTE from "fp-ts/ReaderTaskEither"; 3 | import { parseAllComponents } from "./components"; 4 | import { ParserRTE, readParserOutput } from "./context"; 5 | import { parseAllPaths } from "./operation"; 6 | import { ParserOutput } from "./parserOutput"; 7 | import { parseAllServers } from "./server"; 8 | 9 | export function main(): ParserRTE { 10 | return pipe( 11 | parseAllComponents(), 12 | RTE.chain(() => parseAllPaths()), 13 | RTE.chain(() => parseAllServers()), 14 | RTE.chain(() => readParserOutput()) 15 | ); 16 | } 17 | -------------------------------------------------------------------------------- /packages/cli/src/parser/operation.ts: -------------------------------------------------------------------------------- 1 | import { pipe } from "fp-ts/function"; 2 | import * as O from "fp-ts/Option"; 3 | import * as RTE from "fp-ts/ReaderTaskEither"; 4 | import * as R from "fp-ts/Record"; 5 | import { OpenAPIV3 } from "openapi-types"; 6 | import { OperationMethod } from "@openapi-io-ts/core"; 7 | import { toValidVariableName } from "../utils"; 8 | import { BodyItemOrRef, parseBody } from "./body"; 9 | import { parsedItem } from "./common"; 10 | import { modifyParserOutput, ParserContext, ParserRTE } from "./context"; 11 | import { ParameterItemOrRef, parseParameter } from "./parameter"; 12 | import { parseResponse, ResponseItemOrRef } from "./response"; 13 | import * as gen from "io-ts-codegen"; 14 | 15 | export type ParsedOperation = { 16 | path: string; 17 | method: OperationMethod; 18 | parameters: ParameterItemOrRef[]; 19 | body: O.Option; 20 | responses: Record; 21 | }; 22 | 23 | export function parseAllPaths(): ParserRTE { 24 | return pipe( 25 | RTE.asks((context: ParserContext) => context.document.paths), 26 | RTE.chain((paths) => { 27 | const tasks = Object.entries(paths).map(([path, pathObject]) => 28 | pathObject ? parsePath(path, pathObject) : RTE.right(undefined) 29 | ); 30 | return RTE.sequenceSeqArray(tasks); 31 | }), 32 | RTE.map(() => void 0) 33 | ); 34 | } 35 | 36 | function parsePath( 37 | path: string, 38 | pathObject: OpenAPIV3.PathItemObject 39 | ): ParserRTE { 40 | const operations = { 41 | get: pathObject?.get, 42 | post: pathObject?.post, 43 | put: pathObject?.put, 44 | delete: pathObject?.delete, 45 | }; 46 | 47 | return pipe( 48 | Object.entries(operations), 49 | RTE.traverseSeqArray(([method, operation]) => 50 | operation 51 | ? parseAndAddOperation(path, method as OperationMethod, operation) 52 | : RTE.right(undefined) 53 | ), 54 | RTE.map(() => void 0) 55 | ); 56 | } 57 | 58 | function parseAndAddOperation( 59 | path: string, 60 | method: OperationMethod, 61 | operation: OpenAPIV3.OperationObject 62 | ): ParserRTE { 63 | const { operationId, tags } = operation; 64 | 65 | if (operationId == null) { 66 | return RTE.left(new Error(`Missing operationId in path ${path}`)); 67 | } 68 | 69 | const generatedName = toValidVariableName(operationId, "camel"); 70 | 71 | return pipe( 72 | parseOperation(path, method, operation), 73 | RTE.chain((parsed) => 74 | modifyParserOutput((draft) => { 75 | draft.operations[generatedName] = parsed; 76 | }) 77 | ), 78 | RTE.chain(() => parseOperationTags(generatedName, tags)) 79 | ); 80 | } 81 | 82 | function parseOperation( 83 | path: string, 84 | method: OperationMethod, 85 | operation: OpenAPIV3.OperationObject 86 | ): ParserRTE { 87 | const { operationId } = operation; 88 | 89 | if (operationId == null) { 90 | return RTE.left( 91 | new Error(`Missing operationId on path ${path}, method ${method}`) 92 | ); 93 | } 94 | 95 | return pipe( 96 | RTE.Do, 97 | RTE.bind("parameters", () => 98 | parseOperationParameters(operation.parameters) 99 | ), 100 | RTE.bind("body", () => 101 | parseOperationBody(operation.requestBody, operationId) 102 | ), 103 | RTE.bind("responses", () => 104 | parseOperationResponses(operation.responses, operationId) 105 | ), 106 | RTE.map(({ parameters, body, responses }) => { 107 | const operation: ParsedOperation = { 108 | path, 109 | method, 110 | parameters, 111 | body, 112 | responses, 113 | }; 114 | return operation; 115 | }) 116 | ); 117 | } 118 | 119 | function parseOperationTags( 120 | operationId: string, 121 | tags?: string[] 122 | ): ParserRTE { 123 | if (tags == null) { 124 | return RTE.right(undefined); 125 | } 126 | 127 | return pipe( 128 | tags, 129 | RTE.traverseSeqArray((tag) => 130 | modifyParserOutput((draft) => { 131 | const currentTags = draft.tags[tag]; 132 | draft.tags[tag] = currentTags 133 | ? currentTags.concat(operationId) 134 | : [operationId]; 135 | }) 136 | ), 137 | RTE.map(() => void 0) 138 | ); 139 | } 140 | 141 | function parseOperationParameters( 142 | params?: Array 143 | ): ParserRTE { 144 | if (params == null) { 145 | return RTE.right([]); 146 | } 147 | 148 | return pipe( 149 | params, 150 | RTE.traverseSeqArray((p) => parseParameter("", p)), 151 | RTE.map((res) => res as ParameterItemOrRef[]) 152 | ); 153 | } 154 | 155 | function parseOperationBody( 156 | requestBody: 157 | | OpenAPIV3.ReferenceObject 158 | | OpenAPIV3.RequestBodyObject 159 | | undefined, 160 | operationId: string 161 | ): ParserRTE> { 162 | if (requestBody == null) { 163 | return RTE.right(O.none); 164 | } 165 | 166 | const name = `${toValidVariableName(operationId, "pascal")}RequestBody`; 167 | 168 | return pipe(parseBody(name, requestBody), RTE.map(O.some)); 169 | } 170 | 171 | function parseOperationResponses( 172 | responses: OpenAPIV3.ResponsesObject | undefined, 173 | operationId: string 174 | ): ParserRTE> { 175 | if (responses == null) { 176 | return RTE.right({ 177 | "2XX": parsedItem( 178 | { _tag: "ParsedJsonResponse", type: gen.unknownType }, 179 | "SuccessfulResponse" 180 | ), 181 | }); 182 | } 183 | 184 | return pipe( 185 | responses, 186 | R.traverseWithIndex(RTE.ApplicativeSeq)((code, response) => 187 | parseResponse( 188 | `${toValidVariableName(operationId, "pascal")}Response${code}`, 189 | response 190 | ) 191 | ) 192 | ); 193 | } 194 | -------------------------------------------------------------------------------- /packages/cli/src/parser/parameter.ts: -------------------------------------------------------------------------------- 1 | import { pipe } from "fp-ts/function"; 2 | import * as RTE from "fp-ts/ReaderTaskEither"; 3 | import * as gen from "io-ts-codegen"; 4 | import { OpenAPIV3 } from "openapi-types"; 5 | import { JsonReference } from "./JSONReference"; 6 | import { OperationParameterIn, JSON_MEDIA_TYPE } from "@openapi-io-ts/core"; 7 | import { 8 | ComponentRef, 9 | createComponentRef, 10 | getOrCreateType, 11 | parsedItem, 12 | ParsedItem, 13 | } from "./common"; 14 | import { ParserContext, ParserRTE } from "./context"; 15 | 16 | export interface ParsedBaseParameter { 17 | in: OperationParameterIn; 18 | name: string; 19 | type: gen.TypeDeclaration | gen.TypeReference; 20 | required: boolean; 21 | defaultValue?: unknown; 22 | } 23 | 24 | export interface ParsedJsonParameter extends ParsedBaseParameter { 25 | _tag: "ParsedJsonParameter"; 26 | } 27 | 28 | export interface ParsedFormParameter extends ParsedBaseParameter { 29 | _tag: "ParsedFormParameter"; 30 | explode: boolean; 31 | } 32 | 33 | export type ParsedParameter = ParsedJsonParameter | ParsedFormParameter; 34 | 35 | export type ParameterItemOrRef = 36 | | ParsedItem 37 | | ComponentRef<"parameters">; 38 | 39 | export function parseParameter( 40 | name: string, 41 | param: OpenAPIV3.ReferenceObject | OpenAPIV3.ParameterObject 42 | ): ParserRTE { 43 | if (JsonReference.is(param)) { 44 | return RTE.fromEither(createComponentRef("parameters", param.$ref)); 45 | } 46 | 47 | return parseParameterObject(name, param); 48 | } 49 | 50 | export function parseParameterObject( 51 | name: string, 52 | param: OpenAPIV3.ParameterObject 53 | ): ParserRTE> { 54 | if (param.schema != null) { 55 | return parseParameterWithSchema( 56 | name, 57 | param, 58 | param.schema, 59 | "ParsedFormParameter" 60 | ); 61 | } 62 | 63 | const jsonContentSchema = param.content?.[JSON_MEDIA_TYPE].schema; 64 | 65 | if (jsonContentSchema != null) { 66 | return parseParameterWithSchema( 67 | name, 68 | param, 69 | jsonContentSchema, 70 | "ParsedJsonParameter" 71 | ); 72 | } 73 | 74 | return RTE.left( 75 | new Error( 76 | `Error parsing parameter ${param.name}: no schema or application/json schema defined` 77 | ) 78 | ); 79 | } 80 | 81 | function parseParameterWithSchema( 82 | name: string, 83 | param: OpenAPIV3.ParameterObject, 84 | schema: OpenAPIV3.SchemaObject | OpenAPIV3.ReferenceObject, 85 | tag: ParsedParameter["_tag"] 86 | ): ParserRTE> { 87 | return pipe( 88 | RTE.Do, 89 | RTE.bind("type", () => getOrCreateType(name, schema)), 90 | RTE.bind("defaultValue", () => getDefaultValue(schema)), 91 | RTE.map(({ type, defaultValue }) => 92 | buildParsedParameter(param, type, defaultValue, tag) 93 | ) 94 | ); 95 | } 96 | 97 | function getDefaultValue( 98 | s: OpenAPIV3.ReferenceObject | OpenAPIV3.SchemaObject 99 | ): ParserRTE { 100 | return pipe( 101 | RTE.Do, 102 | RTE.bind("schema", () => 103 | JsonReference.is(s) 104 | ? getOpenapiSchemaFromRef(s) 105 | : RTE.right(s) 106 | ), 107 | // eslint-disable-next-line @typescript-eslint/no-unsafe-return 108 | RTE.map((obj) => obj.schema.default) 109 | ); 110 | } 111 | 112 | function getOpenapiSchemaFromRef( 113 | ref: OpenAPIV3.ReferenceObject 114 | ): ParserRTE { 115 | return pipe( 116 | RTE.asks( 117 | (context) => 118 | context.document.components?.schemas?.[ 119 | ref.$ref 120 | ] as OpenAPIV3.SchemaObject 121 | ) 122 | ); 123 | } 124 | 125 | function buildParsedParameter( 126 | param: OpenAPIV3.ParameterObject, 127 | type: gen.TypeDeclaration | gen.TypeReference, 128 | defaultValue: unknown, 129 | tag: ParsedParameter["_tag"] 130 | ): ParsedItem { 131 | const { name } = param; 132 | const paramIn = param.in as OperationParameterIn; 133 | const required = param.required ?? false; 134 | 135 | const baseParameter: ParsedBaseParameter = { 136 | name, 137 | in: paramIn, 138 | required, 139 | type, 140 | defaultValue, 141 | }; 142 | 143 | if (tag === "ParsedFormParameter") { 144 | const defaultExplode = paramIn === "query" || paramIn === "cookie"; 145 | const item: ParsedFormParameter = { 146 | ...baseParameter, 147 | _tag: "ParsedFormParameter", 148 | explode: param.explode ?? defaultExplode, 149 | }; 150 | return parsedItem(item, name); 151 | } else { 152 | const item: ParsedJsonParameter = { 153 | ...baseParameter, 154 | _tag: "ParsedJsonParameter", 155 | }; 156 | return parsedItem(item, name); 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /packages/cli/src/parser/parseDocument.ts: -------------------------------------------------------------------------------- 1 | import SwaggerParser from "@apidevtools/swagger-parser"; 2 | import { pipe } from "fp-ts/function"; 3 | import * as TE from "fp-ts/TaskEither"; 4 | import { OpenAPI, OpenAPIV3 } from "openapi-types"; 5 | 6 | export function parseDocument( 7 | inputFile: string 8 | ): TE.TaskEither { 9 | return pipe( 10 | TE.tryCatch( 11 | () => SwaggerParser.bundle(inputFile), 12 | (e) => new Error(`Error in OpenApi file: ${String(e)}`) 13 | ), 14 | TE.chain((document) => 15 | isOpenApiV3Document(document) 16 | ? TE.right(document) 17 | : TE.left(new Error("Cannot parse OpenAPI v2 document")) 18 | ) 19 | ); 20 | } 21 | 22 | function isOpenApiV3Document(doc: OpenAPI.Document): doc is OpenAPIV3.Document { 23 | return "openapi" in doc; 24 | } 25 | -------------------------------------------------------------------------------- /packages/cli/src/parser/parserOutput.ts: -------------------------------------------------------------------------------- 1 | import { ParsedComponents } from "./common"; 2 | import { ParsedOperation } from "./operation"; 3 | import { ParsedServer } from "./server"; 4 | 5 | export interface ParserOutput { 6 | components: ParsedComponents; 7 | operations: Record; 8 | tags: Record; 9 | servers: ParsedServer[]; 10 | } 11 | 12 | export function parserOutput(): ParserOutput { 13 | return { 14 | components: { 15 | schemas: {}, 16 | parameters: {}, 17 | responses: {}, 18 | requestBodies: {}, 19 | }, 20 | operations: {}, 21 | tags: {}, 22 | servers: [], 23 | }; 24 | } 25 | -------------------------------------------------------------------------------- /packages/cli/src/parser/response.ts: -------------------------------------------------------------------------------- 1 | import { pipe } from "fp-ts/function"; 2 | import * as RTE from "fp-ts/ReaderTaskEither"; 3 | import * as gen from "io-ts-codegen"; 4 | import { OpenAPIV3 } from "openapi-types"; 5 | import { JsonReference } from "./JSONReference"; 6 | import { 7 | createComponentRef, 8 | getOrCreateType, 9 | parsedItem, 10 | ParsedItem, 11 | ComponentRef, 12 | } from "./common"; 13 | import { JSON_MEDIA_TYPE } from "@openapi-io-ts/core"; 14 | import { ParserRTE } from "./context"; 15 | 16 | export interface ParsedEmptyResponse { 17 | _tag: "ParsedEmptyResponse"; 18 | } 19 | 20 | export interface ParsedFileResponse { 21 | _tag: "ParsedFileResponse"; 22 | } 23 | 24 | export interface ParsedJsonResponse { 25 | _tag: "ParsedJsonResponse"; 26 | type: gen.TypeDeclaration | gen.TypeReference; 27 | } 28 | 29 | export type ParsedResponse = 30 | | ParsedEmptyResponse 31 | | ParsedFileResponse 32 | | ParsedJsonResponse; 33 | 34 | export type ResponseItemOrRef = 35 | | ParsedItem 36 | | ComponentRef<"responses">; 37 | 38 | export function parseResponse( 39 | name: string, 40 | response: OpenAPIV3.ReferenceObject | OpenAPIV3.ResponseObject 41 | ): ParserRTE { 42 | if (JsonReference.is(response)) { 43 | return RTE.fromEither(createComponentRef("responses", response.$ref)); 44 | } 45 | 46 | return parseResponseObject(name, response); 47 | } 48 | 49 | export function parseResponseObject( 50 | name: string, 51 | response: OpenAPIV3.ResponseObject 52 | ): ParserRTE> { 53 | const { content } = response; 54 | 55 | const jsonSchema = content?.[JSON_MEDIA_TYPE]?.schema; 56 | 57 | if (jsonSchema != null) { 58 | return pipe( 59 | getOrCreateType(name, jsonSchema), 60 | RTE.map((type) => parsedItem({ _tag: "ParsedJsonResponse", type }, name)) 61 | ); 62 | } 63 | 64 | const contents = content && Object.values(content); 65 | 66 | if (contents == null || contents.length === 0 || contents[0].schema == null) { 67 | return RTE.right(parsedItem({ _tag: "ParsedEmptyResponse" }, name)); 68 | } 69 | 70 | const firstContentSchema = contents[0].schema; 71 | 72 | if ( 73 | !JsonReference.is(firstContentSchema) && 74 | firstContentSchema.type === "string" && 75 | firstContentSchema.format === "binary" 76 | ) { 77 | return RTE.right(parsedItem({ _tag: "ParsedFileResponse" }, name)); 78 | } 79 | 80 | return pipe( 81 | getOrCreateType(name, firstContentSchema), 82 | RTE.map((type) => parsedItem({ _tag: "ParsedJsonResponse", type }, name)) 83 | ); 84 | } 85 | -------------------------------------------------------------------------------- /packages/cli/src/parser/schema.ts: -------------------------------------------------------------------------------- 1 | import * as E from "fp-ts/Either"; 2 | import { pipe } from "fp-ts/function"; 3 | import * as gen from "io-ts-codegen"; 4 | import { OpenAPIV3 } from "openapi-types"; 5 | import { createJsonPointer, JsonReference } from "./JSONReference"; 6 | import { toValidVariableName } from "../utils"; 7 | import { checkValidReference } from "./common"; 8 | 9 | export function parseSchema( 10 | schema: OpenAPIV3.ReferenceObject | OpenAPIV3.SchemaObject 11 | ): E.Either { 12 | if (JsonReference.is(schema)) { 13 | return parseJsonReference(schema.$ref); 14 | } 15 | 16 | if (schema.nullable && schema.allOf) { 17 | return pipe( 18 | parseAllOf(schema.allOf), 19 | E.map((allOf) => gen.unionCombinator([allOf, gen.nullType])) 20 | ); 21 | } 22 | 23 | return pipe( 24 | parseBaseSchema(schema), 25 | E.map((baseSchema) => 26 | schema.nullable 27 | ? gen.unionCombinator([baseSchema, gen.nullType]) 28 | : baseSchema 29 | ) 30 | ); 31 | } 32 | 33 | function parseBaseSchema( 34 | schema: OpenAPIV3.SchemaObject 35 | ): E.Either { 36 | if (schema.allOf) { 37 | return parseAllOf(schema.allOf); 38 | } 39 | 40 | if (schema.oneOf) { 41 | return parseOneOf(schema.oneOf); 42 | } 43 | 44 | if (schema.anyOf) { 45 | return parseOneOf(schema.anyOf); 46 | } 47 | 48 | switch (schema.type) { 49 | case "boolean": 50 | return E.right(gen.booleanType); 51 | case "integer": 52 | case "number": 53 | return E.right(gen.numberType); 54 | case "string": 55 | return parseString(schema); 56 | case "array": 57 | return parseArray(schema); 58 | case "object": 59 | return parseObject(schema); 60 | } 61 | 62 | return E.right(gen.unknownType); 63 | } 64 | 65 | function parseJsonReference( 66 | pointer: string 67 | ): E.Either { 68 | return pipe( 69 | createJsonPointer(pointer), 70 | E.chain((jsonPointer) => checkValidReference("schemas", jsonPointer)), 71 | E.map((jsonPointer) => { 72 | const name = `${jsonPointer.tokens[2]}.${toValidVariableName( 73 | jsonPointer.tokens[3], 74 | "pascal" 75 | )}`; 76 | return gen.customCombinator(name, name, [name]); 77 | }) 78 | ); 79 | } 80 | 81 | function parseAllOf( 82 | schemas: Array 83 | ): E.Either { 84 | return pipe( 85 | parseSchemas(schemas), 86 | E.map((schemas) => 87 | schemas.length === 1 ? schemas[0] : gen.intersectionCombinator(schemas) 88 | ) 89 | ); 90 | } 91 | 92 | function parseOneOf( 93 | schemas: Array 94 | ): E.Either { 95 | return pipe( 96 | parseSchemas(schemas), 97 | E.map((schemas) => 98 | schemas.length === 1 ? schemas[0] : gen.unionCombinator(schemas) 99 | ) 100 | ); 101 | } 102 | 103 | function parseString( 104 | schema: OpenAPIV3.SchemaObject 105 | ): E.Either { 106 | if (schema.enum) { 107 | return parseEnum(schema.enum as string[]); 108 | } 109 | 110 | if (schema.format === "date" || schema.format === "date-time") { 111 | return E.right( 112 | gen.customCombinator("Date", "DateFromISOString", ["DateFromISOString"]) 113 | ); 114 | } 115 | 116 | return E.right(gen.stringType); 117 | } 118 | 119 | function parseEnum(enums: string[]): E.Either { 120 | if (enums.length === 1) { 121 | return E.right(gen.literalCombinator(enums[0])); 122 | } 123 | 124 | const literals = enums.map((e) => gen.literalCombinator(e)); 125 | return E.right(gen.unionCombinator(literals)); 126 | } 127 | 128 | function parseArray( 129 | schema: OpenAPIV3.ArraySchemaObject 130 | ): E.Either { 131 | return pipe( 132 | parseSchema(schema.items), 133 | E.map((t) => gen.arrayCombinator(t)) 134 | ); 135 | } 136 | 137 | function parseSchemas( 138 | schemas: Array 139 | ): E.Either { 140 | return pipe(schemas, E.traverseArray(parseSchema)) as E.Either< 141 | Error, 142 | gen.TypeReference[] 143 | >; 144 | } 145 | 146 | function parseObject( 147 | schema: OpenAPIV3.NonArraySchemaObject 148 | ): E.Either { 149 | if (schema.properties) { 150 | return pipe( 151 | Object.entries(schema.properties), 152 | E.traverseArray(([name, propSchema]) => 153 | parseProperty(name, propSchema, schema) 154 | ), 155 | E.map((props) => gen.typeCombinator(props as gen.Property[])) 156 | ); 157 | } 158 | 159 | return E.right(gen.unknownRecordType); 160 | } 161 | 162 | function parseProperty( 163 | name: string, 164 | schema: OpenAPIV3.SchemaObject | OpenAPIV3.ReferenceObject, 165 | parentSchema: OpenAPIV3.NonArraySchemaObject 166 | ): E.Either { 167 | return pipe( 168 | parseSchema(schema), 169 | E.map((t) => 170 | gen.property( 171 | name, 172 | t, 173 | parentSchema.required ? !parentSchema.required.includes(name) : true 174 | ) 175 | ) 176 | ); 177 | } 178 | -------------------------------------------------------------------------------- /packages/cli/src/parser/server.ts: -------------------------------------------------------------------------------- 1 | import { OpenAPIV3 } from "openapi-types"; 2 | import { modifyParserOutput, ParserContext, ParserRTE } from "./context"; 3 | import * as RTE from "fp-ts/ReaderTaskEither"; 4 | import { pipe } from "fp-ts/function"; 5 | 6 | export interface ParsedServer { 7 | url: string; 8 | } 9 | 10 | export function parseAllServers(): ParserRTE { 11 | return pipe( 12 | RTE.asks((context: ParserContext) => context.document.servers ?? []), 13 | RTE.map((servers) => servers.map(parseServer)), 14 | RTE.chain((parsedServers) => 15 | modifyParserOutput((draft) => { 16 | draft.servers = parsedServers; 17 | }) 18 | ) 19 | ); 20 | } 21 | 22 | function parseServer(server: OpenAPIV3.ServerObject): ParsedServer { 23 | return { url: server.url }; 24 | } 25 | -------------------------------------------------------------------------------- /packages/cli/src/utils.ts: -------------------------------------------------------------------------------- 1 | export function toValidVariableName( 2 | input: string, 3 | casing: "camel" | "pascal" 4 | ): string { 5 | const joined = input 6 | .split(" ") 7 | .map(removeInvalidChars) 8 | .map((i) => capitalize(i, "pascal")) 9 | .join(""); 10 | 11 | return capitalize(joined, casing); 12 | } 13 | 14 | export type CapitalizeCasing = "camel" | "pascal"; 15 | 16 | export function capitalize(input: string, casing: CapitalizeCasing): string { 17 | const firstChar = 18 | casing === "camel" 19 | ? input.charAt(0).toLocaleLowerCase() 20 | : input.charAt(0).toLocaleUpperCase(); 21 | return firstChar + input.slice(1); 22 | } 23 | 24 | function removeInvalidChars(input: string): string { 25 | return input.replace(/(\W+)/gi, ""); 26 | } 27 | -------------------------------------------------------------------------------- /packages/cli/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "include": ["src"] 4 | } 5 | -------------------------------------------------------------------------------- /packages/core/README.md: -------------------------------------------------------------------------------- 1 | # @openapi-io-ts/core 2 | 3 | `openapi-io-ts` is a code generation tool capable of generating [io-ts](https://github.com/gcanti/io-ts) decoders from an [OpenAPI](https://www.openapis.org/) document. It can also generate the code needed to perform the request and decode/parse the response returned by the server. 4 | 5 | This package contains common definitions. 6 | -------------------------------------------------------------------------------- /packages/core/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@openapi-io-ts/core", 3 | "version": "0.1.0", 4 | "description": "openapi-io-ts core package", 5 | "keywords": [ 6 | "openapi", 7 | "io-ts", 8 | "codegen", 9 | "code generation", 10 | "swagger", 11 | "validation" 12 | ], 13 | "license": "MIT", 14 | "homepage": "https://github.com/Fredx87/openapi-io-ts", 15 | "repository": { 16 | "type": "git", 17 | "url": "https://github.com/Fredx87/openapi-io-ts.git", 18 | "directory": "packages/core" 19 | }, 20 | "main": "dist/index.js", 21 | "module": "dist/esm/index.js", 22 | "types": "dist/index.d.ts", 23 | "files": [ 24 | "dist" 25 | ], 26 | "publishConfig": { 27 | "access": "public" 28 | }, 29 | "scripts": { 30 | "build": "tsup src/index.ts --format esm,cjs --dts --clean --sourcemap --legacy-output" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /packages/core/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./mediaTypes"; 2 | export * from "./operation"; 3 | -------------------------------------------------------------------------------- /packages/core/src/mediaTypes.ts: -------------------------------------------------------------------------------- 1 | export const JSON_MEDIA_TYPE = "application/json"; 2 | export const TEXT_PLAIN_MEDIA_TYPE = "text/plain"; 3 | export const FORM_ENCODED_MEDIA_TYPE = "application/x-www-form-urlencoded"; 4 | export const MULTIPART_FORM_MEDIA_TYPE = "multipart/form-data"; 5 | -------------------------------------------------------------------------------- /packages/core/src/operation.ts: -------------------------------------------------------------------------------- 1 | export type OperationMethod = "get" | "post" | "put" | "delete"; 2 | export type OperationParameterIn = "path" | "query" | "header" | "cookie"; 3 | -------------------------------------------------------------------------------- /packages/core/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "include": ["src"] 4 | } 5 | -------------------------------------------------------------------------------- /packages/runtime/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @openapi-io-ts/runtime 2 | 3 | ## 0.3.1 4 | 5 | ### Patch Changes 6 | 7 | - [#14](https://github.com/Fredx87/openapi-io-ts/pull/14) [`2d5a207`](https://github.com/Fredx87/openapi-io-ts/commit/2d5a207d50a35553b68c788a5895f7454620a560) Thanks [@Fredx87](https://github.com/Fredx87)! - Fixed a bug for serializing of Date parameters and added some tests 8 | 9 | ## 0.3.0 10 | 11 | ### Minor Changes 12 | 13 | - [#12](https://github.com/Fredx87/openapi-io-ts/pull/12) [`1df5450`](https://github.com/Fredx87/openapi-io-ts/commit/1df545029aef4853eb958cffb92cf9f7517acd02) Thanks [@Fredx87](https://github.com/Fredx87)! - Changed types of generated request functions. Simplified types in runtime package, removed operation types and 14 | added generation of request function types. 15 | 16 | ## 0.2.0 17 | 18 | ### Minor Changes 19 | 20 | - [#9](https://github.com/Fredx87/openapi-io-ts/pull/9) [`84d6bf1`](https://github.com/Fredx87/openapi-io-ts/commit/84d6bf1cc2cedc0f818fa3e88da71135ee94e58f) Thanks [@Fredx87](https://github.com/Fredx87)! - Changed types of request functions and operations. 21 | 22 | **BREAKING CHANGE** 23 | Request functions now have only one parameter: an object containing `params` and `body` (they can be optional if the operation 24 | does not have parameters or body). 25 | 26 | Example of operation with `{ id: string }` params and `{name: string; age: number }` body: 27 | 28 | ```ts 29 | // Before: 30 | operation({ id: "abc123" }, { name: "Jonh Doe", age: 35 }); 31 | // After: 32 | operation({ params: { id: "abc123" }, body: { name: "Jonh Doe", age: 35 } }); 33 | ``` 34 | -------------------------------------------------------------------------------- /packages/runtime/README.md: -------------------------------------------------------------------------------- 1 | # @openapi-io-ts/runtime 2 | 3 | `openapi-io-ts` is a code generation tool capable of generating [io-ts](https://github.com/gcanti/io-ts) decoders from an [OpenAPI](https://www.openapis.org/) document. It can also generate the code needed to perform the request and decode/parse the response returned by the server. 4 | 5 | This package contains code used at runtime. See documentation at the project homepage. 6 | -------------------------------------------------------------------------------- /packages/runtime/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@openapi-io-ts/runtime", 3 | "version": "0.3.1", 4 | "description": "Runtime for openapi-io-ts", 5 | "keywords": [ 6 | "openapi", 7 | "io-ts", 8 | "codegen", 9 | "code generation", 10 | "swagger", 11 | "validation" 12 | ], 13 | "license": "MIT", 14 | "homepage": "https://github.com/Fredx87/openapi-io-ts", 15 | "repository": { 16 | "type": "git", 17 | "url": "https://github.com/Fredx87/openapi-io-ts.git", 18 | "directory": "packages/runtime" 19 | }, 20 | "main": "dist/index.js", 21 | "module": "dist/esm/index.js", 22 | "types": "dist/index.d.ts", 23 | "sideEffects": false, 24 | "files": [ 25 | "dist" 26 | ], 27 | "publishConfig": { 28 | "access": "public" 29 | }, 30 | "scripts": { 31 | "build": "tsup src/index.ts --format esm,cjs --dts --clean --sourcemap --legacy-output" 32 | }, 33 | "dependencies": { 34 | "@openapi-io-ts/core": "workspace:*" 35 | }, 36 | "peerDependencies": { 37 | "fp-ts": "^2.11.0", 38 | "io-ts": "^2.2.0" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /packages/runtime/src/client/__fixtures__/baseOperations.ts: -------------------------------------------------------------------------------- 1 | import { Operation } from "../../model"; 2 | 3 | export const getArticlesOperationBase: Operation = { 4 | path: "/getArticles", 5 | method: "get", 6 | responses: {}, 7 | parameters: [], 8 | requestDefaultHeaders: {}, 9 | } as const; 10 | 11 | export const getUserOperationBase: Operation = { 12 | path: "/users/{username}", 13 | method: "get", 14 | responses: {}, 15 | parameters: [ 16 | { 17 | _tag: "FormParameter", 18 | explode: false, 19 | in: "path", 20 | name: "username", 21 | }, 22 | ], 23 | requestDefaultHeaders: { Accept: "application/json" }, 24 | } as const; 25 | 26 | export const postOperationBase: Operation = { 27 | path: "/post", 28 | method: "post", 29 | responses: {}, 30 | parameters: [], 31 | requestDefaultHeaders: {}, 32 | }; 33 | -------------------------------------------------------------------------------- /packages/runtime/src/client/apiError.ts: -------------------------------------------------------------------------------- 1 | import * as t from "io-ts"; 2 | 3 | export interface RequestError { 4 | _tag: "RequestError"; 5 | error: Error; 6 | } 7 | 8 | export function requestError(error: Error): RequestError { 9 | return { 10 | _tag: "RequestError", 11 | error, 12 | }; 13 | } 14 | 15 | export interface HttpError { 16 | _tag: "HttpError"; 17 | response: Response; 18 | } 19 | 20 | export function httpError(response: Response): HttpError { 21 | return { 22 | _tag: "HttpError", 23 | response, 24 | }; 25 | } 26 | 27 | export interface DecodeError { 28 | _tag: "DecodeError"; 29 | errors: t.Errors; 30 | } 31 | 32 | export function decodeError(errors: t.Errors): DecodeError { 33 | return { 34 | _tag: "DecodeError", 35 | errors, 36 | }; 37 | } 38 | 39 | export interface ContentParseError { 40 | _tag: "ContentParseError"; 41 | error: Error; 42 | } 43 | 44 | export function contentParseError(error: Error): ContentParseError { 45 | return { 46 | _tag: "ContentParseError", 47 | error, 48 | }; 49 | } 50 | 51 | export type ApiError = 52 | | RequestError 53 | | HttpError 54 | | DecodeError 55 | | ContentParseError; 56 | -------------------------------------------------------------------------------- /packages/runtime/src/client/apiResponse.ts: -------------------------------------------------------------------------------- 1 | export interface ApiResponse { 2 | data: T; 3 | response: Response; 4 | } 5 | -------------------------------------------------------------------------------- /packages/runtime/src/client/httpRequestAdapter.ts: -------------------------------------------------------------------------------- 1 | export type HttpRequestAdapter = ( 2 | url: string, 3 | req: RequestInit 4 | ) => Promise; 5 | -------------------------------------------------------------------------------- /packages/runtime/src/client/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./apiError"; 2 | export * from "./apiResponse"; 3 | export * from "./httpRequestAdapter"; 4 | export * from "./request"; 5 | -------------------------------------------------------------------------------- /packages/runtime/src/client/parseResponse.ts: -------------------------------------------------------------------------------- 1 | import { OperationResponse, OperationResponses } from "../model"; 2 | import { 3 | ApiError, 4 | decodeError, 5 | DecodeError, 6 | httpError, 7 | contentParseError, 8 | ContentParseError, 9 | } from "./apiError"; 10 | import * as TE from "fp-ts/TaskEither"; 11 | import * as E from "fp-ts/Either"; 12 | import { pipe } from "fp-ts/function"; 13 | import * as t from "io-ts"; 14 | import { ApiResponse } from "./apiResponse"; 15 | 16 | export function parseResponse( 17 | response: Response, 18 | responses: OperationResponses 19 | ): TE.TaskEither> { 20 | if (response.ok) { 21 | return parseSuccessfulResponse(response, responses); 22 | } 23 | 24 | return parseFailedResponse(response); 25 | } 26 | 27 | function parseSuccessfulResponse( 28 | response: Response, 29 | responses: OperationResponses 30 | ): TE.TaskEither> { 31 | const operationResponse = getOperationResponseByCode( 32 | response.status, 33 | responses 34 | ); 35 | 36 | if (operationResponse == null) { 37 | return TE.right({ data: undefined as unknown as ReturnType, response }); 38 | } 39 | 40 | switch (operationResponse._tag) { 41 | case "EmptyResponse": { 42 | return TE.right({ data: undefined as unknown as ReturnType, response }); 43 | } 44 | case "FileResponse": { 45 | return parseBlobResponse(response); 46 | } 47 | case "JsonResponse": { 48 | const decoder = operationResponse.decoder as t.Decoder< 49 | unknown, 50 | ReturnType 51 | >; 52 | return parseJsonResponse(response, decoder); 53 | } 54 | } 55 | } 56 | 57 | function parseFailedResponse( 58 | response: Response 59 | ): TE.TaskEither { 60 | return TE.left(httpError(response)); 61 | } 62 | 63 | function getOperationResponseByCode( 64 | code: number, 65 | responses: OperationResponses 66 | ): OperationResponse | undefined { 67 | const exactResponse = responses[code.toString()]; 68 | if (exactResponse != null) { 69 | return exactResponse; 70 | } 71 | 72 | const rangeResponse = responses[`${code.toString()[0]}XX`]; 73 | if (rangeResponse != null) { 74 | return rangeResponse; 75 | } 76 | 77 | const defaultResponse = responses["default"]; 78 | if (defaultResponse != null) { 79 | return defaultResponse; 80 | } 81 | 82 | return undefined; 83 | } 84 | 85 | function parseJsonResponse( 86 | response: Response, 87 | decoder: t.Decoder 88 | ): TE.TaskEither> { 89 | return pipe( 90 | parseJson(response), 91 | TE.chainW((json) => 92 | pipe(decoder.decode(json), TE.fromEither, TE.mapLeft(decodeError)) 93 | ), 94 | TE.map((data) => ({ 95 | data, 96 | response, 97 | })) 98 | ); 99 | } 100 | 101 | function parseJson( 102 | response: Response 103 | ): TE.TaskEither { 104 | return TE.tryCatch( 105 | () => response.json(), 106 | (e) => contentParseError(E.toError(e)) 107 | ); 108 | } 109 | 110 | function parseBlobResponse( 111 | response: Response 112 | ): TE.TaskEither> { 113 | return pipe( 114 | TE.tryCatch( 115 | () => response.blob(), 116 | (e) => contentParseError(E.toError(e)) 117 | ), 118 | TE.map((blob) => ({ 119 | data: blob as unknown as ReturnType, 120 | response, 121 | })) 122 | ); 123 | } 124 | -------------------------------------------------------------------------------- /packages/runtime/src/client/prepareRequest.test.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @jest-environment jsdom 3 | */ 4 | 5 | import * as E from "fp-ts/Either"; 6 | import { Operation } from "../model"; 7 | import { prepareRequest, PrepareRequestResult } from "./prepareRequest"; 8 | import { 9 | getArticlesOperationBase, 10 | getUserOperationBase, 11 | postOperationBase, 12 | } from "./__fixtures__/baseOperations"; 13 | 14 | describe("prepareRequest", () => { 15 | it("should support simple request", () => { 16 | const expected: PrepareRequestResult = { 17 | url: getArticlesOperationBase.path, 18 | init: { 19 | method: "get", 20 | body: null, 21 | headers: {}, 22 | }, 23 | }; 24 | 25 | const result = prepareRequest(getArticlesOperationBase, {}, undefined); 26 | expect(result).toEqual(E.right(expected)); 27 | }); 28 | 29 | it("should support basic path, query and header parameters", () => { 30 | const getUserOperation: Operation = { 31 | ...getUserOperationBase, 32 | parameters: [ 33 | ...getUserOperationBase.parameters, 34 | { 35 | _tag: "FormParameter", 36 | explode: false, 37 | in: "query", 38 | name: "showDetails", 39 | }, 40 | { 41 | _tag: "FormParameter", 42 | explode: false, 43 | in: "header", 44 | name: "api-key", 45 | }, 46 | ], 47 | } as const; 48 | 49 | const requestParameters = { 50 | username: "john.doe", 51 | showDetails: true, 52 | "api-key": "ABC-123", 53 | }; 54 | 55 | const expected: PrepareRequestResult = { 56 | url: "/users/john.doe?showDetails=true", 57 | init: { 58 | method: "get", 59 | headers: { 60 | Accept: "application/json", 61 | "api-key": "ABC-123", 62 | }, 63 | body: null, 64 | }, 65 | }; 66 | 67 | const result = prepareRequest( 68 | getUserOperation, 69 | requestParameters, 70 | undefined 71 | ); 72 | 73 | expect(result).toEqual(E.right(expected)); 74 | }); 75 | 76 | it("should support optional parameter", () => { 77 | const getArticlesOperation: Operation = { 78 | ...getArticlesOperationBase, 79 | parameters: [ 80 | ...getArticlesOperationBase.parameters, 81 | { 82 | _tag: "FormParameter", 83 | in: "query", 84 | explode: false, 85 | name: "search", 86 | }, 87 | ], 88 | } as const; 89 | 90 | const expected: PrepareRequestResult = { 91 | url: getArticlesOperation.path, 92 | init: { 93 | method: "get", 94 | headers: {}, 95 | body: null, 96 | }, 97 | }; 98 | 99 | const result = prepareRequest(getArticlesOperation, {}, undefined); 100 | 101 | expect(result).toEqual(E.right(expected)); 102 | }); 103 | 104 | it("should serialize a Date parameter", () => { 105 | const getArticlesOperation: Operation = { 106 | ...getArticlesOperationBase, 107 | parameters: [ 108 | ...getArticlesOperationBase.parameters, 109 | { 110 | _tag: "FormParameter", 111 | in: "query", 112 | explode: false, 113 | name: "fromDate", 114 | }, 115 | ], 116 | } as const; 117 | 118 | const fromDate = new Date(2022, 1, 2, 10, 0, 0, 0); 119 | 120 | const requestParameters = { 121 | fromDate, 122 | }; 123 | 124 | const expected: PrepareRequestResult = { 125 | url: `${getArticlesOperation.path}?fromDate=${encodeURIComponent( 126 | fromDate.toISOString() 127 | )}`, 128 | init: { 129 | method: "get", 130 | headers: {}, 131 | body: null, 132 | }, 133 | }; 134 | 135 | const result = prepareRequest( 136 | getArticlesOperation, 137 | requestParameters, 138 | undefined 139 | ); 140 | 141 | expect(result).toEqual(E.right(expected)); 142 | }); 143 | 144 | it("should support JSON parameter", () => { 145 | const getUserOperation: Operation = { 146 | ...getUserOperationBase, 147 | parameters: [ 148 | ...getUserOperationBase.parameters, 149 | { 150 | _tag: "JsonParameter", 151 | in: "query", 152 | name: "viewInfo", 153 | }, 154 | ], 155 | } as const; 156 | 157 | const requestParameters = { 158 | username: "foo", 159 | viewInfo: { showDetails: true, avatar: "large" }, 160 | }; 161 | 162 | const expected: PrepareRequestResult = { 163 | url: `/users/foo?viewInfo=${encodeURIComponent( 164 | `{"showDetails":true,"avatar":"large"}` 165 | )}`, 166 | init: { 167 | method: "get", 168 | headers: { Accept: "application/json" }, 169 | body: null, 170 | }, 171 | }; 172 | 173 | const result = prepareRequest( 174 | getUserOperation, 175 | requestParameters, 176 | undefined 177 | ); 178 | 179 | expect(result).toEqual(E.right(expected)); 180 | }); 181 | 182 | it("should support unexploded array parameter", () => { 183 | const getArticlesOperation: Operation = { 184 | ...getArticlesOperationBase, 185 | parameters: [ 186 | ...getArticlesOperationBase.parameters, 187 | { 188 | _tag: "FormParameter", 189 | in: "query", 190 | explode: false, 191 | name: "articleIds", 192 | }, 193 | ], 194 | } as const; 195 | 196 | const requestParameters = { 197 | articleIds: ["1", "2", "3"], 198 | }; 199 | 200 | const expected: PrepareRequestResult = { 201 | url: `${getArticlesOperation.path}?articleIds=${encodeURIComponent( 202 | "1,2,3" 203 | )}`, 204 | init: { 205 | method: "get", 206 | headers: {}, 207 | body: null, 208 | }, 209 | }; 210 | 211 | const result = prepareRequest( 212 | getArticlesOperation, 213 | requestParameters, 214 | undefined 215 | ); 216 | 217 | expect(result).toEqual(E.right(expected)); 218 | }); 219 | 220 | it("should support exploded array parameter", () => { 221 | const getArticlesOperation: Operation = { 222 | ...getArticlesOperationBase, 223 | parameters: [ 224 | ...getArticlesOperationBase.parameters, 225 | { 226 | _tag: "FormParameter", 227 | in: "query", 228 | explode: true, 229 | name: "articleIds", 230 | }, 231 | ], 232 | } as const; 233 | 234 | const requestParameters = { 235 | articleIds: ["1", "2", "3"], 236 | }; 237 | 238 | const expected: PrepareRequestResult = { 239 | url: `${getArticlesOperation.path}?articleIds=1&articleIds=2&articleIds=3`, 240 | init: { 241 | method: "get", 242 | headers: {}, 243 | body: null, 244 | }, 245 | }; 246 | 247 | const result = prepareRequest( 248 | getArticlesOperation, 249 | requestParameters, 250 | undefined 251 | ); 252 | 253 | expect(result).toEqual(E.right(expected)); 254 | }); 255 | 256 | it("should support unexploded object parameter", () => { 257 | const getArticlesOperation: Operation = { 258 | ...getArticlesOperationBase, 259 | parameters: [ 260 | ...getArticlesOperationBase.parameters, 261 | { 262 | _tag: "FormParameter", 263 | in: "query", 264 | explode: false, 265 | name: "filter", 266 | }, 267 | ], 268 | } as const; 269 | 270 | const requestParameters = { 271 | filter: { field: "date", dir: "asc" }, 272 | }; 273 | 274 | const expected: PrepareRequestResult = { 275 | url: `${getArticlesOperation.path}?filter=${encodeURIComponent( 276 | "field,date,dir,asc" 277 | )}`, 278 | init: { 279 | method: "get", 280 | headers: {}, 281 | body: null, 282 | }, 283 | }; 284 | 285 | const result = prepareRequest( 286 | getArticlesOperation, 287 | requestParameters, 288 | undefined 289 | ); 290 | 291 | expect(result).toEqual(E.right(expected)); 292 | }); 293 | 294 | it("should support exploded object parameter", () => { 295 | const getArticlesOperation: Operation = { 296 | ...getArticlesOperationBase, 297 | parameters: [ 298 | ...getArticlesOperationBase.parameters, 299 | { 300 | _tag: "FormParameter", 301 | in: "query", 302 | explode: true, 303 | name: "filter", 304 | }, 305 | ], 306 | } as const; 307 | 308 | const requestParameters = { 309 | filter: { field: "date", dir: "asc" }, 310 | }; 311 | 312 | const expected: PrepareRequestResult = { 313 | url: `${getArticlesOperation.path}?field=date&dir=asc`, 314 | init: { 315 | method: "get", 316 | headers: {}, 317 | body: null, 318 | }, 319 | }; 320 | 321 | const result = prepareRequest( 322 | getArticlesOperation, 323 | requestParameters, 324 | undefined 325 | ); 326 | 327 | expect(result).toEqual(E.right(expected)); 328 | }); 329 | 330 | it("should support text body", () => { 331 | const postOperation: Operation = { 332 | ...postOperationBase, 333 | body: { _tag: "TextBody" }, 334 | }; 335 | 336 | const body = "Request Body"; 337 | 338 | const expected: PrepareRequestResult = { 339 | url: postOperation.path, 340 | init: { 341 | method: "post", 342 | headers: {}, 343 | body, 344 | }, 345 | }; 346 | 347 | const result = prepareRequest(postOperation, {}, body); 348 | 349 | expect(result).toEqual(E.right(expected)); 350 | }); 351 | 352 | it("should support JSON body", () => { 353 | const postOperation: Operation = { 354 | ...postOperationBase, 355 | body: { _tag: "JsonBody" }, 356 | }; 357 | 358 | const body = { user: "john.doe", age: 35 }; 359 | 360 | const expected: PrepareRequestResult = { 361 | url: postOperation.path, 362 | init: { 363 | method: "post", 364 | headers: {}, 365 | body: JSON.stringify(body), 366 | }, 367 | }; 368 | 369 | const result = prepareRequest(postOperation, {}, body); 370 | 371 | expect(result).toEqual(E.right(expected)); 372 | }); 373 | 374 | it("should support form encoded body", () => { 375 | const postOperation: Operation = { 376 | ...postOperationBase, 377 | body: { _tag: "FormBody" }, 378 | }; 379 | 380 | const body = { user: "john.doe", age: 35 }; 381 | 382 | const expected: PrepareRequestResult = { 383 | url: postOperation.path, 384 | init: { 385 | method: "post", 386 | headers: {}, 387 | body: new URLSearchParams("user=john.doe&age=35"), 388 | }, 389 | }; 390 | 391 | const result = prepareRequest(postOperation, {}, body); 392 | 393 | expect(result).toEqual(E.right(expected)); 394 | }); 395 | 396 | it("should support multipart form body", () => { 397 | const postOperation: Operation = { 398 | ...postOperationBase, 399 | body: { _tag: "MultipartBody" }, 400 | }; 401 | 402 | const body = { user: "john.doe", age: 35 }; 403 | 404 | const expectedBody = new FormData(); 405 | expectedBody.append("user", "john.doe"); 406 | expectedBody.append("age", "35"); 407 | 408 | const expected: PrepareRequestResult = { 409 | url: postOperation.path, 410 | init: { 411 | method: "post", 412 | headers: {}, 413 | body: expectedBody, 414 | }, 415 | }; 416 | 417 | const result = prepareRequest(postOperation, {}, body); 418 | 419 | expect(result).toEqual(E.right(expected)); 420 | }); 421 | 422 | it("should support binary body", () => { 423 | const postOperation: Operation = { 424 | ...postOperationBase, 425 | body: { _tag: "BinaryBody", mediaType: "text/string" }, 426 | }; 427 | 428 | const body = new Blob(["foo"]); 429 | 430 | const expected: PrepareRequestResult = { 431 | url: postOperation.path, 432 | init: { 433 | method: "post", 434 | headers: {}, 435 | body, 436 | }, 437 | }; 438 | 439 | const result = prepareRequest(postOperation, {}, body); 440 | 441 | expect(result).toEqual(E.right(expected)); 442 | }); 443 | }); 444 | -------------------------------------------------------------------------------- /packages/runtime/src/client/prepareRequest.ts: -------------------------------------------------------------------------------- 1 | import { OperationParameterIn } from "@openapi-io-ts/core"; 2 | import { pipe } from "fp-ts/function"; 3 | import * as E from "fp-ts/Either"; 4 | import { requestError, RequestError } from "./apiError"; 5 | import { Operation, OperationParameter, OperationBody } from "../model"; 6 | 7 | export interface PrepareRequestResult { 8 | url: string; 9 | init: RequestInit; 10 | } 11 | 12 | export function prepareRequest( 13 | operation: Operation, 14 | requestParameters: Record, 15 | requestBody: unknown 16 | ): E.Either { 17 | return pipe( 18 | E.Do, 19 | E.bind("url", () => prepareUrl(operation, requestParameters)), 20 | E.bind("headers", () => prepareHeaders(operation, requestParameters)), 21 | E.bind("body", () => prepareBody(operation.body, requestBody)), 22 | E.map(({ url, headers, body }) => { 23 | const init: RequestInit = { 24 | method: operation.method, 25 | body, 26 | headers, 27 | }; 28 | 29 | return { url, init }; 30 | }) 31 | ); 32 | } 33 | 34 | function prepareUrl( 35 | operation: Operation, 36 | requestParameters: Record 37 | ): E.Either { 38 | return pipe( 39 | E.Do, 40 | E.bind("path", () => 41 | preparePath( 42 | operation.path, 43 | filterParametersByType("path", operation.parameters), 44 | requestParameters 45 | ) 46 | ), 47 | E.bind("queryString", () => 48 | prepareQueryString( 49 | filterParametersByType("query", operation.parameters), 50 | requestParameters 51 | ) 52 | ), 53 | E.map( 54 | ({ path, queryString }) => 55 | `${path}${queryString ? `?${queryString}` : ""}` 56 | ) 57 | ); 58 | } 59 | 60 | function preparePath( 61 | path: string, 62 | pathParameters: OperationParameter[], 63 | requestParameters: Record 64 | ): E.Either { 65 | let res = path; 66 | 67 | for (const parameter of pathParameters) { 68 | const value = stringifyParameterValue(requestParameters[parameter.name]); 69 | res = res.replace(`{${parameter.name}}`, value); 70 | } 71 | 72 | return E.right(res); 73 | } 74 | 75 | function prepareQueryString( 76 | parameters: OperationParameter[], 77 | requestParameters: Record 78 | ): E.Either { 79 | const qs = new URLSearchParams(); 80 | 81 | for (const parameter of parameters) { 82 | const encodeResult = encodeRequestParameter( 83 | parameter.name, 84 | parameter, 85 | requestParameters[parameter.name] 86 | ); 87 | 88 | for (const [n, v] of encodeResult) { 89 | qs.append(n, v); 90 | } 91 | } 92 | 93 | return E.right(qs.toString()); 94 | } 95 | 96 | function encodeRequestParameter( 97 | name: string, 98 | parameter: OperationParameter, 99 | value: unknown 100 | ): Array<[name: string, value: string]> { 101 | if (value == null) { 102 | return []; 103 | } 104 | 105 | switch (parameter._tag) { 106 | case "JsonParameter": 107 | return [[name, JSON.stringify(value)]]; 108 | case "FormParameter": 109 | return encodeFormParameter(name, parameter.explode, value); 110 | } 111 | } 112 | 113 | function encodeFormParameter( 114 | name: string, 115 | explode: boolean, 116 | value: unknown 117 | ): Array<[name: string, value: string]> { 118 | if (Array.isArray(value)) { 119 | if (explode) { 120 | return value.map((v) => [name, stringifyParameterValue(v)]); 121 | } else { 122 | return [[name, value.map(stringifyParameterValue).join(",")]]; 123 | } 124 | } 125 | 126 | if (typeof value === "object" && value != null && !(value instanceof Date)) { 127 | return encodeFormObjectParameter(name, explode, value); 128 | } 129 | 130 | return [[name, stringifyParameterValue(value)]]; 131 | } 132 | 133 | function encodeFormObjectParameter( 134 | name: string, 135 | explode: boolean, 136 | // eslint-disable-next-line @typescript-eslint/ban-types 137 | value: object 138 | ): Array<[name: string, value: string]> { 139 | if (explode) { 140 | return Object.entries(value).map(([k, v]) => [ 141 | k, 142 | stringifyParameterValue(v), 143 | ]); 144 | } else { 145 | return [ 146 | [ 147 | name, 148 | Object.entries(value) 149 | .flatMap(([k, v]) => [k, stringifyParameterValue(v)]) 150 | .join(","), 151 | ], 152 | ]; 153 | } 154 | } 155 | 156 | function prepareHeaders( 157 | operation: Operation, 158 | requestParameters: Record 159 | ): E.Either> { 160 | const headers: Record = {}; 161 | 162 | for (const parameter of filterParametersByType( 163 | "header", 164 | operation.parameters 165 | )) { 166 | headers[parameter.name] = stringifyParameterValue( 167 | requestParameters[parameter.name] 168 | ); 169 | } 170 | 171 | return E.right({ ...operation.requestDefaultHeaders, ...headers }); 172 | } 173 | 174 | function prepareBody( 175 | body: OperationBody | undefined, 176 | requestBody: unknown 177 | ): E.Either { 178 | if (body == null) { 179 | return E.right(null); 180 | } 181 | 182 | switch (body._tag) { 183 | case "TextBody": { 184 | return E.right(requestBody as string); 185 | } 186 | case "BinaryBody": { 187 | return E.right(requestBody as Blob); 188 | } 189 | case "JsonBody": { 190 | return E.right(JSON.stringify(requestBody)); 191 | } 192 | case "FormBody": { 193 | return prepareFormBody(requestBody); 194 | } 195 | case "MultipartBody": { 196 | return prepareMultipartBody(requestBody); 197 | } 198 | } 199 | } 200 | 201 | function prepareFormBody( 202 | requestBody: unknown 203 | ): E.Either { 204 | if (typeof requestBody === "object" && requestBody != null) { 205 | const res = new URLSearchParams(); 206 | 207 | for (const [k, v] of encodeFormObjectParameter("", true, requestBody)) { 208 | res.append(k, v); 209 | } 210 | 211 | return E.right(res); 212 | } 213 | 214 | return pipe( 215 | new Error( 216 | `requestBody for a form encoded body should be a not null object, received ${typeof requestBody}` 217 | ), 218 | requestError, 219 | E.left 220 | ); 221 | } 222 | 223 | function prepareMultipartBody( 224 | requestBody: unknown 225 | ): E.Either { 226 | if (typeof requestBody === "object" && requestBody != null) { 227 | const formData = new FormData(); 228 | 229 | for (const [name, value] of Object.entries(requestBody)) { 230 | // eslint-disable-next-line @typescript-eslint/no-unsafe-argument 231 | formData.append(name, value); 232 | } 233 | 234 | return E.right(formData); 235 | } 236 | 237 | return pipe( 238 | new Error( 239 | `requestBody for a multipart form body should be a not null object, received ${typeof requestBody}` 240 | ), 241 | requestError, 242 | E.left 243 | ); 244 | } 245 | 246 | function filterParametersByType( 247 | type: OperationParameterIn, 248 | parameters: readonly OperationParameter[] 249 | ): OperationParameter[] { 250 | return parameters.filter((p) => p.in === type); 251 | } 252 | 253 | function stringifyParameterValue(value: unknown): string { 254 | if (value instanceof Date) { 255 | return value.toISOString(); 256 | } 257 | 258 | return String(value); 259 | } 260 | -------------------------------------------------------------------------------- /packages/runtime/src/client/request.ts: -------------------------------------------------------------------------------- 1 | import { HttpRequestAdapter } from "./httpRequestAdapter"; 2 | import { Operation } from "../model"; 3 | import { ApiError, requestError } from "./apiError"; 4 | import * as TE from "fp-ts/TaskEither"; 5 | import * as E from "fp-ts/Either"; 6 | import { pipe } from "fp-ts/function"; 7 | import { prepareRequest } from "./prepareRequest"; 8 | import { parseResponse } from "./parseResponse"; 9 | import { ApiResponse } from "./apiResponse"; 10 | 11 | export type RequestFunctionArgs = 12 | | { 13 | params?: Record; 14 | body?: unknown; 15 | } 16 | | undefined; 17 | 18 | export type RequestFunction = ( 19 | ...params: undefined extends Args ? [args?: Args] : [args: Args] 20 | ) => TE.TaskEither>; 21 | 22 | export const requestFunctionBuilder = 23 | ( 24 | operation: Operation, 25 | requestAdapter: HttpRequestAdapter 26 | ): RequestFunction => 27 | (...params) => { 28 | const [args] = params; 29 | 30 | return pipe( 31 | prepareRequest(operation, args?.params ?? {}, args?.body), 32 | TE.fromEither, 33 | TE.chainW(({ url, init }) => performRequest(url, init, requestAdapter)), 34 | TE.chain((response) => parseResponse(response, operation.responses)) 35 | ); 36 | }; 37 | 38 | function performRequest( 39 | url: string, 40 | init: RequestInit, 41 | requestAdapter: HttpRequestAdapter 42 | ): TE.TaskEither { 43 | return TE.tryCatch( 44 | () => requestAdapter(url, init), 45 | (e) => requestError(E.toError(e)) 46 | ); 47 | } 48 | -------------------------------------------------------------------------------- /packages/runtime/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./client"; 2 | export * from "./model"; 3 | -------------------------------------------------------------------------------- /packages/runtime/src/model/body.ts: -------------------------------------------------------------------------------- 1 | export interface BinaryBody { 2 | _tag: "BinaryBody"; 3 | mediaType: string; 4 | } 5 | 6 | export interface FormBody { 7 | _tag: "FormBody"; 8 | } 9 | 10 | export interface MultipartBody { 11 | _tag: "MultipartBody"; 12 | } 13 | 14 | export interface JsonBody { 15 | _tag: "JsonBody"; 16 | } 17 | 18 | export interface TextBody { 19 | _tag: "TextBody"; 20 | } 21 | 22 | export type OperationBody = 23 | | BinaryBody 24 | | FormBody 25 | | MultipartBody 26 | | JsonBody 27 | | TextBody; 28 | -------------------------------------------------------------------------------- /packages/runtime/src/model/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./body"; 2 | export * from "./operation"; 3 | export * from "./parameters"; 4 | export * from "./responses"; 5 | -------------------------------------------------------------------------------- /packages/runtime/src/model/operation.ts: -------------------------------------------------------------------------------- 1 | import { OperationMethod } from "@openapi-io-ts/core"; 2 | import { OperationBody } from "./body"; 3 | import { OperationParameter } from "./parameters"; 4 | import { OperationResponses } from "./responses"; 5 | 6 | export type OperationResponseType = "empty" | "string" | "blob"; 7 | 8 | export interface Operation { 9 | readonly path: string; 10 | readonly method: OperationMethod; 11 | readonly requestDefaultHeaders: Record; 12 | readonly parameters: readonly OperationParameter[]; 13 | readonly responses: OperationResponses; 14 | readonly body?: OperationBody; 15 | } 16 | -------------------------------------------------------------------------------- /packages/runtime/src/model/parameters.ts: -------------------------------------------------------------------------------- 1 | import { OperationParameterIn } from "@openapi-io-ts/core"; 2 | 3 | export interface BaseParameter { 4 | in: OperationParameterIn; 5 | name: string; 6 | } 7 | 8 | export interface JsonParameter extends BaseParameter { 9 | _tag: "JsonParameter"; 10 | } 11 | 12 | export interface FormParameter extends BaseParameter { 13 | _tag: "FormParameter"; 14 | explode: boolean; 15 | } 16 | 17 | export type OperationParameter = JsonParameter | FormParameter; 18 | -------------------------------------------------------------------------------- /packages/runtime/src/model/responses.ts: -------------------------------------------------------------------------------- 1 | import { Decoder } from "io-ts"; 2 | 3 | export interface EmptyResponse { 4 | _tag: "EmptyResponse"; 5 | } 6 | 7 | export interface FileResponse { 8 | _tag: "FileResponse"; 9 | } 10 | 11 | export interface JsonResponse { 12 | _tag: "JsonResponse"; 13 | decoder: Decoder; 14 | } 15 | 16 | export type OperationResponse = EmptyResponse | FileResponse | JsonResponse; 17 | 18 | export type OperationResponses = Record; 19 | -------------------------------------------------------------------------------- /packages/runtime/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "lib": ["DOM", "ES2020"] 5 | }, 6 | "include": ["src"] 7 | } 8 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - "packages/**" 3 | - "examples/**" 4 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "ESNext", 4 | "moduleResolution": "node", 5 | "target": "ESNext", 6 | "noEmit": true, 7 | "strict": true, 8 | "noImplicitAny": true, 9 | "strictNullChecks": true, 10 | "useUnknownInCatchVariables": true, 11 | "noImplicitReturns": true, 12 | "noFallthroughCasesInSwitch": true, 13 | "esModuleInterop": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "resolveJsonModule": true, 16 | "declaration": true 17 | } 18 | } 19 | --------------------------------------------------------------------------------